diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 03fd40d..a01697c 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -1,15 +1,22 @@ -ARG PYTHON_VERSION=3.10-bullseye -FROM python:$PYTHON_VERSION - -# Allow the vscode user to pip install globally w/o sudo -# ENV PIP_TARGET=/usr/local/pip-global -# ENV PYTHONPATH=${PIP_TARGET}:${PYTHONPATH} -# ENV PATH=${PIP_TARGET}/bin:${PATH} -# RUN if ! cat /etc/group | grep -e "^pip-global:" > /dev/null 2>&1; then groupadd -r pip-global; fi \ -# && usermod -a -G pip-global vscode \ -# && umask 0002 && mkdir -p ${PIP_TARGET} \ -# && chown :pip-global ${PIP_TARGET} \ -# && ( [ ! -f "/etc/profile.d/00-restore-env.sh" ] || sed -i -e "s/export PATH=/export PATH=\/usr\/local\/pip-global:/" /etc/profile.d/00-restore-env.sh ) - -RUN pip install poetry -RUN poetry config virtualenvs.create false \ No newline at end of file +ARG PYTHON_VERSION=3.13-bookworm +FROM python:${PYTHON_VERSION} + +# Install system dependencies +RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ + && apt-get -y install --no-install-recommends \ + git \ + curl \ + docker.io \ + postgresql-client \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* + +# Install UV +RUN curl -LsSf https://astral.sh/uv/install.sh | sh +ENV PATH="/root/.cargo/bin:$PATH" + +# Set working directory +WORKDIR /workspace + +# Configure git to trust the workspace +RUN git config --global --add safe.directory /workspace diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 0373611..ad31e29 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,32 +1,41 @@ { - "name": "Python 3", + "name": "PGMob Development", "build": { "dockerfile": "Dockerfile", "context": "..", "args": { - "PYTHON_VERSION": "3.10-bullseye" + "PYTHON_VERSION": "3.13-bookworm" } }, - // Configure tool-specific properties. "customizations": { - // Configure properties specific to VS Code. "vscode": { - // Set *default* container specific settings.json values on container create. "settings": { "terminal.integrated.profiles.linux": { "bash": { "path": "/bin/bash" } }, - "python.defaultInterpreterPath": "/usr/local/bin/python", - "python.languageServer": "Default", - "python.linting.enabled": true, - "python.linting.pylintEnabled": true + "python.defaultInterpreterPath": "${workspaceFolder}/.venv/bin/python", + "python.languageServer": "Pylance", + "[python]": { + "editor.defaultFormatter": "charliermarsh.ruff", + "editor.formatOnSave": true, + "editor.codeActionsOnSave": { + "source.fixAll.ruff": "explicit", + "source.organizeImports.ruff": "explicit" + } + }, + "files.eol": "\n", + "python.testing.pytestArgs": [ + "src/tests" + ], + "python.testing.unittestEnabled": false, + "python.testing.pytestEnabled": true }, - // Add the IDs of extensions you want installed when the container is created. "extensions": [ "ms-python.python", - "ms-python.vscode-pylance" + "ms-python.vscode-pylance", + "charliermarsh.ruff" ] } }, @@ -38,10 +47,9 @@ "mounts": [ "source=//var/run/docker.sock,target=/var/run/docker.sock,type=bind" ], - // Use 'postCreateCommand' to run commands after the container is created. - "postCreateCommand": "poetry install -E psycopg2", + "postCreateCommand": "uv sync --all-extras", "initializeCommand": "docker network create pgmob-network || docker network inspect pgmob-network", "runArgs": [ "--network=pgmob-network" ] -} \ No newline at end of file +} diff --git a/.github/actions/bump-version/action.yaml b/.github/actions/bump-version/action.yaml index ec35f9c..0f21c68 100644 --- a/.github/actions/bump-version/action.yaml +++ b/.github/actions/bump-version/action.yaml @@ -1,4 +1,4 @@ -name: Poetry Publish +name: UV Bump Version description: Publish package to PyPI branding: icon: package diff --git a/.github/actions/publish/action.yaml b/.github/actions/publish/action.yaml index 4ab7100..a7c67a6 100644 --- a/.github/actions/publish/action.yaml +++ b/.github/actions/publish/action.yaml @@ -1,4 +1,4 @@ -name: Poetry Publish +name: UV Publish description: Publish package to PyPI branding: icon: package @@ -8,22 +8,10 @@ inputs: PYTHON_VERSION: description: Python version required: false - default: 3.9 - POETRY_VERSION: - description: Poetry version - required: false - default: 1.3.2 - PYPI_USERNAME: - description: PyPI username. '__token__' by default - required: false - default: "__token__" + default: "3.13" PYPI_TOKEN: description: PyPI API token. required: true - PYPI_REGISTRY: - description: PYPI registry address - required: false - default: https://upload.pypi.org/legacy/ runs: using: "composite" @@ -33,23 +21,23 @@ runs: with: ref: main - - name: Install poetry - run: pip install poetry==${{ inputs.POETRY_VERSION }} + - name: Install UV + run: curl -LsSf https://astral.sh/uv/install.sh | sh + shell: bash + + - name: Add UV to PATH + run: echo "$HOME/.local/bin" >> $GITHUB_PATH shell: bash - name: Setup python uses: actions/setup-python@v4 with: python-version: ${{ inputs.PYTHON_VERSION }} - cache: 'poetry' - - name: Install poetry dependencies - run: | - poetry install -n + - name: Build package + run: uv build shell: bash - - name: Build and Publish - run: | - poetry config repositories.publish ${{ inputs.PYPI_REGISTRY }} - poetry publish -n -u "${{ inputs.PYPI_USERNAME }}" -p "${{ inputs.PYPI_TOKEN }}" -r publish --build + - name: Publish to PyPI + run: uv publish --token "${{ inputs.PYPI_TOKEN }}" shell: bash diff --git a/.github/actions/test-docs/action.yaml b/.github/actions/test-docs/action.yaml index e45fddc..57e5ac7 100644 --- a/.github/actions/test-docs/action.yaml +++ b/.github/actions/test-docs/action.yaml @@ -8,11 +8,7 @@ inputs: PYTHON_VERSION: description: Python version required: false - default: 3.9 - POETRY_VERSION: - description: Poetry version - required: false - default: 1.3.2 + default: "3.13" POSTGRES_VERSION: description: Postgres major version to use required: false @@ -28,24 +24,25 @@ runs: - name: Checkout uses: actions/checkout@v3 - - name: Install poetry - run: pip install poetry==${{ inputs.POETRY_VERSION }} + - name: Install UV + run: curl -LsSf https://astral.sh/uv/install.sh | sh + shell: bash + + - name: Add UV to PATH + run: echo "$HOME/.local/bin" >> $GITHUB_PATH shell: bash - name: Setup python uses: actions/setup-python@v4 with: python-version: ${{ inputs.PYTHON_VERSION }} - cache: 'poetry' - - name: Install poetry dependencies - run: | - poetry install -n + - name: Install dependencies + run: uv sync shell: bash working-directory: docs - name: Testing docs - run: | - poetry run -n make -b html + run: uv run make html shell: bash working-directory: docs diff --git a/.github/actions/test/action.yaml b/.github/actions/test/action.yaml index f0d491a..e480cfb 100644 --- a/.github/actions/test/action.yaml +++ b/.github/actions/test/action.yaml @@ -1,4 +1,4 @@ -name: Poetry Test +name: UV Test description: Run tests branding: icon: play-circle @@ -8,12 +8,8 @@ inputs: PYTHON_VERSION: description: Python version required: false - default: 3.9 - POETRY_VERSION: - description: Poetry version - required: false - default: 1.3.2 - POETRY_EXTRAS: + default: "3.13" + UV_EXTRAS: description: PyPI extras to install required: false default: psycopg2-binary @@ -41,19 +37,19 @@ runs: run: > docker build . -t pgmobtest --build-arg PYTHON_VERSION=${{ inputs.PYTHON_VERSION }} - --build-arg POETRY_VERSION=${{ inputs.POETRY_VERSION }} - --build-arg POETRY_EXTRAS=${{ inputs.POETRY_EXTRAS }} + --build-arg UV_EXTRAS=${{ inputs.UV_EXTRAS }} shell: bash - - name: Run mypy tests - run: docker run --rm -i pgmobtest mypy src/pgmob + - name: Run ty type checks + run: docker run --rm -i pgmobtest uv run ty check src/pgmob shell: bash - - name: Run poetry tests + - name: Run pytest tests run: > docker run --rm -i --network ${{ inputs.CONTAINER_NETWORK}} -e PGMOB_IMAGE=postgres:${{ inputs.POSTGRES_VERSION }} -e PGMOB_CONTAINER_NETWORK=${{ inputs.CONTAINER_NETWORK}} + -e PGMOB_HOST=pgmob-postgres -v /var/run/docker.sock:/var/run/docker.sock - pgmobtest pytest -vv + pgmobtest uv run pytest -vv shell: bash diff --git a/.github/instructions/api-standards.md b/.github/instructions/api-standards.md new file mode 120000 index 0000000..29752a3 --- /dev/null +++ b/.github/instructions/api-standards.md @@ -0,0 +1 @@ +../../.windsurf/rules/api-standards.md \ No newline at end of file diff --git a/.github/instructions/cicd-workflow.md b/.github/instructions/cicd-workflow.md new file mode 120000 index 0000000..b1f8dcd --- /dev/null +++ b/.github/instructions/cicd-workflow.md @@ -0,0 +1 @@ +../../.windsurf/rules/cicd-workflow.md \ No newline at end of file diff --git a/.github/instructions/code-conventions.md b/.github/instructions/code-conventions.md new file mode 120000 index 0000000..21fae87 --- /dev/null +++ b/.github/instructions/code-conventions.md @@ -0,0 +1 @@ +../../.windsurf/rules/code-conventions.md \ No newline at end of file diff --git a/.github/instructions/documentation-standards.md b/.github/instructions/documentation-standards.md new file mode 120000 index 0000000..f37d694 --- /dev/null +++ b/.github/instructions/documentation-standards.md @@ -0,0 +1 @@ +../../.windsurf/rules/documentation-standards.md \ No newline at end of file diff --git a/.github/instructions/project-patterns.md b/.github/instructions/project-patterns.md new file mode 120000 index 0000000..14a7d0f --- /dev/null +++ b/.github/instructions/project-patterns.md @@ -0,0 +1 @@ +../../.windsurf/rules/project-patterns.md \ No newline at end of file diff --git a/.github/instructions/security-guidelines.md b/.github/instructions/security-guidelines.md new file mode 120000 index 0000000..64cb681 --- /dev/null +++ b/.github/instructions/security-guidelines.md @@ -0,0 +1 @@ +../../.windsurf/rules/security-guidelines.md \ No newline at end of file diff --git a/.github/instructions/testing-standards.md b/.github/instructions/testing-standards.md new file mode 120000 index 0000000..10fca20 --- /dev/null +++ b/.github/instructions/testing-standards.md @@ -0,0 +1 @@ +../../.windsurf/rules/testing-standards.md \ No newline at end of file diff --git a/.kiro/steering/api-standards.md b/.kiro/steering/api-standards.md new file mode 120000 index 0000000..29752a3 --- /dev/null +++ b/.kiro/steering/api-standards.md @@ -0,0 +1 @@ +../../.windsurf/rules/api-standards.md \ No newline at end of file diff --git a/.kiro/steering/cicd-workflow.md b/.kiro/steering/cicd-workflow.md new file mode 120000 index 0000000..b1f8dcd --- /dev/null +++ b/.kiro/steering/cicd-workflow.md @@ -0,0 +1 @@ +../../.windsurf/rules/cicd-workflow.md \ No newline at end of file diff --git a/.kiro/steering/code-conventions.md b/.kiro/steering/code-conventions.md new file mode 120000 index 0000000..21fae87 --- /dev/null +++ b/.kiro/steering/code-conventions.md @@ -0,0 +1 @@ +../../.windsurf/rules/code-conventions.md \ No newline at end of file diff --git a/.kiro/steering/documentation-standards.md b/.kiro/steering/documentation-standards.md new file mode 120000 index 0000000..f37d694 --- /dev/null +++ b/.kiro/steering/documentation-standards.md @@ -0,0 +1 @@ +../../.windsurf/rules/documentation-standards.md \ No newline at end of file diff --git a/.kiro/steering/project-patterns.md b/.kiro/steering/project-patterns.md new file mode 120000 index 0000000..14a7d0f --- /dev/null +++ b/.kiro/steering/project-patterns.md @@ -0,0 +1 @@ +../../.windsurf/rules/project-patterns.md \ No newline at end of file diff --git a/.kiro/steering/security-guidelines.md b/.kiro/steering/security-guidelines.md new file mode 120000 index 0000000..64cb681 --- /dev/null +++ b/.kiro/steering/security-guidelines.md @@ -0,0 +1 @@ +../../.windsurf/rules/security-guidelines.md \ No newline at end of file diff --git a/.kiro/steering/testing-standards.md b/.kiro/steering/testing-standards.md new file mode 120000 index 0000000..10fca20 --- /dev/null +++ b/.kiro/steering/testing-standards.md @@ -0,0 +1 @@ +../../.windsurf/rules/testing-standards.md \ No newline at end of file diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 95b9bf1..409e1f3 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -9,7 +9,11 @@ version: 2 build: os: ubuntu-22.04 tools: - python: "3.9" + python: "3.13" + jobs: + post_install: + - pip install uv + - cd docs && uv sync # Build documentation in the docs/ directory with Sphinx sphinx: @@ -18,8 +22,3 @@ sphinx: # If using Sphinx, optionally build your docs in additional formats such as PDF # formats: # - pdf - -# Optionally declare the Python requirements required to build your docs -python: - install: - - requirements: docs/requirements.txt diff --git a/.vscode/settings.json b/.vscode/settings.json index f373258..531f361 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,13 +1,32 @@ { - "python.linting.pylintEnabled": false, - "python.linting.mypyEnabled": true, - "python.linting.enabled": true, + "python.defaultInterpreterPath": "${workspaceFolder}/.venv/bin/python", + "python.languageServer": "Pylance", + "[python]": { + "editor.defaultFormatter": "charliermarsh.ruff", + "editor.formatOnSave": true, + "editor.codeActionsOnSave": { + "source.fixAll.ruff": "explicit", + "source.organizeImports.ruff": "explicit" + } + }, "files.eol": "\n", "python.testing.pytestArgs": [ "src/tests" ], "python.testing.unittestEnabled": false, "python.testing.pytestEnabled": true, - "editor.formatOnSave": true, - "python.formatting.provider": "black", -} \ No newline at end of file + "editor.rulers": [ + 110 + ], + "files.trimTrailingWhitespace": true, + "files.insertFinalNewline": true, + "files.exclude": { + "**/__pycache__": true, + "**/*.pyc": true, + "**/.pytest_cache": true, + "**/.ruff_cache": true, + "**/.mypy_cache": true, + "**/htmlcov": true, + "**/.coverage": true + } +} diff --git a/.windsurf/rules/README.md b/.windsurf/rules/README.md new file mode 100644 index 0000000..e107a01 --- /dev/null +++ b/.windsurf/rules/README.md @@ -0,0 +1,73 @@ +# Agent Steering Rules + +This directory contains the source of truth for all AI agent steering rules used across different IDEs. + +## File Structure + +- `api-standards.md` - API design patterns, method naming, return types, error handling +- `code-conventions.md` - Code formatting, naming conventions, type hints, logging +- `testing-standards.md` - Test organization, AAA pattern, fixtures, coverage requirements +- `security-guidelines.md` - SQL injection prevention, credential management, security best practices +- `project-patterns.md` - Project-specific patterns (adapters, SQL composition, lazy loading, mixins) +- `cicd-workflow.md` - CI/CD workflows, UV commands, Docker builds, release process +- `copilot-instructions.md` - Comprehensive project overview and instructions + +## IDE Integration + +### Windsurf/Cascade +- Reads directly from `.windsurf/rules/` directory +- Uses `applies_to` frontmatter to scope rules to specific file patterns +- Automatically discovers all `.md` files in the rules directory + +### GitHub Copilot +- Symlinks in `.github/instructions/` directory point to `.windsurf/rules/` +- Uses `applyTo` frontmatter to scope rules to specific file patterns +- All rule files are automatically discovered in the instructions directory + +### Kiro +- Include files in `.kiro/steering/` use `#[[file:...]]` syntax +- Example: `#[[file:../../.windsurf/rules/api-standards.md]]` +- Frontmatter `inclusion: always` ensures files are always loaded + +## Frontmatter Format + +Each rule file uses YAML frontmatter with directives for all three IDEs: + +```yaml +--- +# Kiro: Always include this file +inclusion: always + +# Windsurf: Apply to Python files +applies_to: + - "**/*.py" + +# GitHub Copilot: Apply to Python files +applyTo: + - "**/*.py" +--- +``` + +## Editing Rules + +1. Edit files in `.windsurf/rules/` directory (source of truth) +2. Changes automatically propagate to: + - Windsurf (reads directly) + - GitHub Copilot (via symlinks in `.github/instructions/`) + - Kiro (via include syntax) +3. No need to update files in `.kiro/steering/` or `.github/instructions/` manually + +## Adding New Rules + +1. Create new file in `.windsurf/rules/` +2. Add appropriate frontmatter with `inclusion`, `applies_to`, and `applyTo` +3. Create symlink in `.github/instructions/`: `ln -s ../../.windsurf/rules/newfile.md .github/instructions/newfile.md` +4. Create include file in `.kiro/steering/` with `#[[file:../../.windsurf/rules/newfile.md]]` +5. All three IDEs will automatically discover and use the new rule + +## Testing + +After making changes: +- Windsurf: Rules apply immediately +- GitHub Copilot: Restart Copilot or reload window +- Kiro: Rules apply on next agent invocation diff --git a/.windsurf/rules/api-standards.md b/.windsurf/rules/api-standards.md new file mode 100644 index 0000000..a1733b8 --- /dev/null +++ b/.windsurf/rules/api-standards.md @@ -0,0 +1,346 @@ +--- +# Kiro: Always include this file +inclusion: always + +# Windsurf: Apply to Python files +applies_to: + - "**/*.py" + +# GitHub Copilot: Apply to Python files +applyTo: + - "**/*.py" +--- + +# API Standards for PGMob + +## Method Naming Conventions + +### CRUD Operations +- `create()`: Create object on PostgreSQL server +- `drop(cascade: bool = False)`: Remove object from server +- `alter()`: Apply pending changes to server +- `refresh()`: Reload object state from server +- `script(as_composable: bool = False)`: Generate DDL + +### Collection Operations +- `new(**kwargs)`: Create ephemeral object (not yet on server) +- `add(obj)`: Add object to collection and create on server +- `refresh()`: Reload all objects from server +- `keys()`: Get all object keys without loading objects + +### Query Operations +- `execute(query, params)`: Execute SQL query +- `execute_with_cursor(task)`: Execute task with cursor +- `execute_many(queries)`: Execute multiple queries (async) + +## Return Types + +### Methods that Modify State +```python +def create(self) -> None: + """Create object on server. Returns None.""" + +def alter(self) -> None: + """Apply changes. Returns None.""" + +def drop(self, cascade: bool = False) -> None: + """Drop object. Returns None.""" +``` + +### Query Methods +```python +def execute(self, query: str, params: tuple = None) -> List[Tuple[Any]]: + """Execute query. Returns list of tuples or empty list.""" + +def fetchall(self) -> List[Tuple[Any]]: + """Fetch all rows. Returns list of tuples.""" +``` + +### Object Getters +```python +def __getitem__(self, key: str) -> T: + """Get object by key. Raises KeyError if not found.""" + +def get(self, key: str, default: T = None) -> Optional[T]: + """Get object by key. Returns default if not found.""" +``` + +## Parameter Conventions + +### Required vs Optional +- Required parameters: positional or keyword +- Optional parameters: keyword-only with defaults +- Use `Optional[T]` for nullable types + +```python +def __init__( + self, + name: str, # Required positional + cluster: "Cluster" = None, # Optional with default + *, # Keyword-only marker + owner: Optional[str] = None, # Optional keyword-only + schema: str = "public" # Optional with default +): +``` + +### Boolean Flags +- Use descriptive names: `cascade`, `force`, `if_exists` +- Default to safe behavior (False for destructive operations) +- Document behavior for both True and False + +```python +def drop(self, cascade: bool = False, if_exists: bool = False) -> None: + """Drop object. + + Args: + cascade: Drop dependent objects + if_exists: Don't raise error if object doesn't exist + """ +``` + +## Error Handling + +### Exception Types +```python +from .errors import PostgresError, AdapterError + +# Use PostgresError for database-related errors +if not result: + raise PostgresError(f"Table '{name}' not found in schema '{schema}'") + +# Use AdapterError for adapter-specific errors +try: + cursor.execute(query) +except Exception as e: + raise AdapterError(f"Query execution failed: {e}") +``` + +### Error Messages +- Include context: object name, operation, parameters +- Provide actionable information +- Don't expose sensitive data (passwords, connection strings) +- Use f-strings for formatting + +```python +# Good +raise PostgresError( + f"Failed to create table '{self.name}' in schema '{self.schema}': {error}" +) + +# Bad +raise PostgresError("Error") # Not descriptive +raise PostgresError(f"Failed with password {password}") # Exposes sensitive data +``` + +## Type Hints + +### Always Use Type Hints +```python +from typing import TYPE_CHECKING, Optional, List, Dict, Any, Union + +if TYPE_CHECKING: + from ..cluster import Cluster + +def execute( + self, + query: Union[Composable, str], + params: Optional[Tuple[Any, ...]] = None +) -> List[Tuple[Any]]: + """Execute query with type hints.""" +``` + +### Generic Types +```python +from typing import TypeVar, Generic + +T = TypeVar("T") + +class Collection(Generic[T]): + def __getitem__(self, key: str) -> T: + ... +``` + +## Docstring Format + +### Google Style +```python +def create_table( + self, + name: str, + columns: List[Dict[str, Any]], + *, + schema: str = "public", + if_not_exists: bool = False +) -> None: + """Create a new table on the PostgreSQL server. + + This method generates and executes a CREATE TABLE statement based on + the provided column definitions. + + Args: + name: Table name + columns: List of column definitions with 'name', 'type', and optional + 'nullable', 'default' keys + schema: Schema name (default: 'public') + if_not_exists: Don't raise error if table exists + + Raises: + PostgresError: If table creation fails + ValueError: If column definitions are invalid + + Example: + >>> cluster.create_table( + ... "users", + ... [ + ... {"name": "id", "type": "serial", "nullable": False}, + ... {"name": "username", "type": "varchar(50)"}, + ... ] + ... ) + """ +``` + +## Property Patterns + +### Read-Only Properties +```python +@property +def oid(self) -> Optional[int]: + """Object ID (read-only).""" + return self._oid +``` + +### Read-Write Properties with Validation +```python +@property +def owner(self) -> Optional[str]: + """Object owner.""" + return self._owner + +@owner.setter +def owner(self, value: str) -> None: + """Set object owner. Queues change for alter().""" + if not value: + raise ValueError("Owner cannot be empty") + generic._set_ephemeral_attr(self, "owner", value) +``` + +### Lazy Properties +```python +from ._decorators import get_lazy_property + +@property +def tables(self) -> TableCollection: + """Table collection (lazy-loaded).""" + return get_lazy_property( + self, + "tables", + lambda: TableCollection(cluster=self, load_strategy=self.load_strategy) + ) +``` + +## Context Managers + +### Use Context Managers for Resources +```python +# Cursor management +with cluster.adapter.cursor() as cursor: + cursor.execute(query) + return cursor.fetchall() + +# Transaction management +with cluster._no_autocommit(): + # Multiple operations in transaction + cluster.execute(query1) + cluster.execute(query2) +``` + +## Async API Conventions + +### Async Method Naming +- Prefix with `async_` or use separate async module +- Provide sync wrappers when appropriate + +```python +# Async method +async def load_collections_parallel(self, *names: str) -> Dict[str, Any]: + """Load multiple collections in parallel.""" + +# Sync wrapper +def load_collections(self, *names: str, parallel: bool = False) -> Dict[str, Any]: + """Load collections. Use parallel=True for async loading.""" + if parallel and self.enable_async: + return asyncio.run(self.load_collections_parallel(*names)) + else: + return self._load_collections_sequential(*names) +``` + +## Deprecation + +### Deprecation Warnings +```python +import warnings + +def old_method(self): + """Deprecated method.""" + warnings.warn( + "old_method() is deprecated. Use new_method() instead.", + DeprecationWarning, + stacklevel=2 + ) + return self.new_method() +``` + +### Version Information +- Document when feature was added: `.. versionadded:: 0.2.0` +- Document when deprecated: `.. deprecated:: 0.2.0` +- Remove after 2 major versions + +## Backward Compatibility + +### Feature Flags +```python +def __init__( + self, + *args, + load_strategy: LoadStrategy = LoadStrategy.LAZY, + legacy_mode: bool = False, + **kwargs +): + """Initialize cluster. + + Args: + load_strategy: Collection loading strategy + legacy_mode: Use v0.1.x behavior (deprecated) + """ + if legacy_mode: + warnings.warn( + "legacy_mode is deprecated. Use load_strategy=LoadStrategy.EAGER", + DeprecationWarning + ) + load_strategy = LoadStrategy.EAGER +``` + +## Testing API + +### Public API Must Have Tests +```python +def test_table_create(): + """Test table creation via API.""" + cluster = Cluster(...) + table = cluster.tables.new("test_table") + table.create() + + assert "test_table" in cluster.tables + assert cluster.tables["test_table"].owner == "postgres" +``` + +### Test Error Cases +```python +def test_table_create_duplicate_raises_error(): + """Test creating duplicate table raises error.""" + cluster = Cluster(...) + cluster.tables.new("test_table").create() + + with pytest.raises(PostgresError, match="already exists"): + cluster.tables.new("test_table").create() +``` diff --git a/.windsurf/rules/cicd-workflow.md b/.windsurf/rules/cicd-workflow.md new file mode 100644 index 0000000..3f0018c --- /dev/null +++ b/.windsurf/rules/cicd-workflow.md @@ -0,0 +1,379 @@ +--- +# Kiro: Always include this file +inclusion: always + +# Windsurf: Apply to CI/CD and build files +applies_to: + - ".github/**/*" + - "Dockerfile" + - "pyproject.toml" + - ".devcontainer/**/*" + +# GitHub Copilot: Apply to CI/CD and build files +applyTo: + - ".github/**/*" + - "Dockerfile" + - "pyproject.toml" + - ".devcontainer/**/*" +--- + +# CI/CD Workflow Guidelines for PGMob + +## Build System + +### UV Package Manager +- Use UV for all dependency management +- UV is faster and more reliable than Poetry +- Lock file: `uv.lock` +- Configuration: `pyproject.toml` + +### Installation Commands +```bash +# Install all dependencies with extras +uv sync --all-extras + +# Install specific extras +uv sync --extra psycopg2-binary --extra dev + +# Update dependencies +uv sync --upgrade + +# Add new dependency +uv add package-name + +# Add dev dependency +uv add --dev package-name +``` + +## Testing + +### Running Tests +```bash +# Run all tests +uv run pytest + +# Run with coverage +uv run pytest --cov=pgmob --cov-report=html --cov-report=term + +# Run specific markers +uv run pytest -m unit +uv run pytest -m integration +uv run pytest -m "not slow" + +# Run with verbose output +uv run pytest -vv + +# Run specific test file +uv run pytest src/tests/test_cluster.py + +# Run specific test function +uv run pytest src/tests/test_cluster.py::test_cluster_init +``` + +### Test Markers +- `unit`: Unit tests (fast, no external dependencies) +- `integration`: Integration tests (require PostgreSQL) +- `slow`: Slow tests (may take several seconds) +- `asyncio`: Async tests + +### Coverage Requirements +- Overall coverage: >90% +- New code: 100% +- Critical paths: 100% + +## Code Quality + +### Formatting with Ruff +```bash +# Format all code +uv run ruff format . + +# Check formatting without changes +uv run ruff format --check . + +# Format specific file +uv run ruff format src/pgmob/cluster.py +``` + +### Linting with Ruff +```bash +# Lint and auto-fix +uv run ruff check --fix . + +# Lint without fixes +uv run ruff check . + +# Lint specific file +uv run ruff check src/pgmob/cluster.py +``` + +### Type Checking with ty +```bash +# Type check entire package +uv run ty check src/pgmob + +# Type check specific file +uv run ty check src/pgmob/cluster.py + +# Watch mode (for development) +uv run ty watch src/pgmob +``` + +## Docker Builds + +### Main Dockerfile +- Uses UV for dependency installation +- Base image: `python:3.10-bullseye` +- Build args: + - `PYTHON_VERSION`: Python version (default: 3.10-bullseye) + - `UV_EXTRAS`: Extras to install (default: psycopg2-binary) + +### Building Docker Image +```bash +# Build with defaults +docker build -t pgmob . + +# Build with specific Python version +docker build -t pgmob --build-arg PYTHON_VERSION=3.11-bullseye . + +# Build with specific extras +docker build -t pgmob --build-arg UV_EXTRAS=psycopg2 . +``` + +### Running Tests in Docker +```bash +# Run tests +docker run --rm pgmob uv run pytest + +# Run with coverage +docker run --rm pgmob uv run pytest --cov=pgmob + +# Run ty +docker run --rm pgmob uv run ty check src/pgmob +``` + +## GitHub Actions + +### CI Workflow +- Location: `.github/workflows/CI.yaml` +- Triggers: Push (except docs), Pull requests to main +- Uses custom action: `.github/actions/test` + +### Test Action +- Location: `.github/actions/test/action.yaml` +- Inputs: + - `PYTHON_VERSION`: Python version (default: 3.9) + - `UV_EXTRAS`: Extras to install (default: psycopg2-binary) + - `POSTGRES_VERSION`: PostgreSQL version (default: 12) + - `CONTAINER_NETWORK`: Docker network (default: pgmob-network) + +### Running CI Locally +```bash +# Build test container +docker build . -t pgmobtest + +# Run ty +docker run --rm pgmobtest uv run ty check src/pgmob + +# Run pytest +docker network create pgmob-network || true +docker run --rm --network pgmob-network \ + -e PGMOB_IMAGE=postgres:12 \ + -e PGMOB_CONTAINER_NETWORK=pgmob-network \ + -v /var/run/docker.sock:/var/run/docker.sock \ + pgmobtest uv run pytest -vv +``` + +## Pre-commit Checks + +### Before Committing +```bash +# Format code +uv run ruff format . + +# Fix linting issues +uv run ruff check --fix . + +# Run type checks +uv run ty check src/pgmob + +# Run unit tests +uv run pytest -m unit + +# Run all tests (if time permits) +uv run pytest +``` + +### Recommended Git Hooks +Create `.git/hooks/pre-commit`: +```bash +#!/bin/bash +set -e + +echo "Running pre-commit checks..." + +# Format check +echo "Checking formatting..." +uv run ruff format --check . + +# Lint check +echo "Checking linting..." +uv run ruff check . + +# Type check +echo "Checking types..." +uv run ty check src/pgmob + +# Unit tests +echo "Running unit tests..." +uv run pytest -m unit + +echo "All checks passed!" +``` + +Make executable: +```bash +chmod +x .git/hooks/pre-commit +``` + +## Release Process + +### Version Bumping +- Version is in `pyproject.toml` under `[project]` section +- Follow semantic versioning: MAJOR.MINOR.PATCH +- Development versions: `0.2.0-dev` +- Release versions: `0.2.0` + +### Release Checklist +1. Update version in `pyproject.toml` +2. Update `CHANGELOG.md` (if exists) +3. Run full test suite: `uv run pytest` +4. Run type checks: `uv run ty check src/pgmob` +5. Format and lint: `uv run ruff format . && uv run ruff check --fix .` +6. Build package: `uv build` +7. Test package installation: `uv pip install dist/pgmob-*.whl` +8. Create git tag: `git tag v0.2.0` +9. Push tag: `git push origin v0.2.0` +10. Publish to PyPI: `uv publish` + +## Continuous Integration Best Practices + +### Fast Feedback +- Run unit tests first (fast) +- Run integration tests after (slower) +- Run slow tests last or in parallel + +### Caching +- Cache UV dependencies in CI +- Cache Docker layers when possible +- Cache test databases between runs + +### Matrix Testing +Test against multiple versions: +- Python: 3.9, 3.10, 3.11, 3.12 +- PostgreSQL: 10, 11, 12, 13, 14, 15, 16 + +### Fail Fast +- Stop on first failure in CI +- Use `pytest -x` to stop on first test failure +- Use `pytest --maxfail=3` to stop after 3 failures + +## Troubleshooting CI + +### Common Issues + +#### UV Not Found +```bash +# Install UV +curl -LsSf https://astral.sh/uv/install.sh | sh +export PATH="$HOME/.local/bin:$PATH" +``` + +#### Lock File Out of Sync +```bash +# Regenerate lock file +uv lock --upgrade +``` + +#### Test Failures in Docker +```bash +# Check Docker network +docker network ls +docker network inspect pgmob-network + +# Check PostgreSQL container +docker ps -a +docker logs +``` + +#### Permission Issues +```bash +# Fix file permissions +chmod -R u+w . +chown -R $USER:$USER . +``` + +## Environment Variables + +### Testing +- `PGMOB_IMAGE`: PostgreSQL Docker image (default: postgres:12) +- `PGMOB_CONTAINER_NETWORK`: Docker network name +- `PGHOST`: PostgreSQL host +- `PGPORT`: PostgreSQL port +- `PGUSER`: PostgreSQL user +- `PGPASSWORD`: PostgreSQL password +- `PGDATABASE`: PostgreSQL database + +### CI/CD +- `UV_CACHE_DIR`: UV cache directory +- `PYTHONPATH`: Python path for imports +- `CI`: Set to `true` in CI environment + +## Performance Optimization + +### Parallel Testing +```bash +# Install pytest-xdist +uv add --dev pytest-xdist + +# Run tests in parallel +uv run pytest -n auto +``` + +### Test Selection +```bash +# Run only changed tests +uv run pytest --lf # Last failed +uv run pytest --ff # Failed first + +# Run tests matching pattern +uv run pytest -k "test_cluster" +``` + +### Docker Build Optimization +```dockerfile +# Use build cache +RUN --mount=type=cache,target=/root/.cache/uv \ + uv sync --extra psycopg2-binary --extra dev +``` + +## Documentation + +### Building Docs +```bash +# Install docs dependencies +cd docs +uv sync + +# Build HTML docs +uv run make html + +# View docs +open _build/html/index.html +``` + +### Docs Testing +```bash +# Test code examples in docs +uv run pytest --doctest-modules src/pgmob +``` diff --git a/.windsurf/rules/code-conventions.md b/.windsurf/rules/code-conventions.md new file mode 100644 index 0000000..ff87359 --- /dev/null +++ b/.windsurf/rules/code-conventions.md @@ -0,0 +1,643 @@ +--- +# Kiro: Always include this file +inclusion: always + +# Windsurf: Apply to Python files +applies_to: + - "**/*.py" + +# GitHub Copilot: Apply to Python files +applyTo: + - "**/*.py" +--- + +# Code Conventions for PGMob + +## Formatting + +### Ruff Configuration +- Line length: 110 characters +- Use Ruff for both linting and formatting +- Run before committing: `uv run ruff format . && uv run ruff check --fix .` + +### Import Formatting +```python +# Standard library (alphabetical) +import asyncio +import logging +from pathlib import Path +from typing import Any, Dict, List, Optional, Tuple, Union + +# Third-party (alphabetical) +from packaging import Version +import pytest + +# Local imports (relative, alphabetical) +from . import util +from ._decorators import get_lazy_property, LAZY_PREFIX +from .errors import PostgresError, AdapterError +from .sql import SQL, Composable, Identifier, Literal + +# TYPE_CHECKING imports (avoid circular imports) +from typing import TYPE_CHECKING +if TYPE_CHECKING: + from ..cluster import Cluster +``` + +### Line Breaks +```python +# Good: Break after opening parenthesis +result = cluster.execute( + "SELECT * FROM pg_tables WHERE tablename = %s", + ("mytable",) +) + +# Good: Break long method chains +( + cluster.tables["mytable"] + .set_owner("new_owner") + .alter() + .refresh() +) + +# Good: Break long conditionals +if ( + table.owner == "postgres" + and table.schema == "public" + and not table.is_temporary +): + process_table(table) +``` + +## Naming Conventions + +### Classes +```python +# PascalCase for classes +class TableCollection: + pass + +class MetadataProvider: + pass + +# Prefix with underscore for internal/private +class _BaseCollection: + pass + +class _LazyBaseCollection: + pass +``` + +### Functions and Methods +```python +# snake_case for functions and methods +def create_table(name: str) -> None: + pass + +def get_table_metadata(oid: int) -> Dict[str, Any]: + pass + +# Prefix with underscore for internal/private +def _fetch_metadata() -> List[Dict]: + pass + +def _build_query() -> SQL: + pass +``` + +### Variables +```python +# snake_case for variables +table_name = "users" +connection_string = "host=localhost" +max_retries = 3 + +# UPPER_SNAKE_CASE for constants +DEFAULT_SCHEMA = "public" +MAX_CONNECTIONS = 100 +LAZY_PREFIX = "_pgmlazy_" +``` + +### Properties +```python +# snake_case for properties +@property +def table_name(self) -> str: + return self._table_name + +@property +def is_connected(self) -> bool: + return self._is_connected +``` + +## Type Hints + +### Always Use Type Hints +```python +# Function signatures +def execute( + self, + query: Union[Composable, str], + params: Optional[Tuple[Any, ...]] = None +) -> List[Tuple[Any]]: + pass + +# Variable annotations +tables: Dict[str, Table] = {} +count: int = 0 +name: Optional[str] = None +``` + +### Generic Types +```python +from typing import TypeVar, Generic, List, Dict, Optional + +T = TypeVar("T") + +class Collection(Generic[T]): + def __init__(self) -> None: + self._items: Dict[str, T] = {} + + def get(self, key: str) -> Optional[T]: + return self._items.get(key) + + def add(self, key: str, item: T) -> None: + self._items[key] = item +``` + +### TYPE_CHECKING Pattern +```python +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + # Only imported for type checking, not at runtime + from ..cluster import Cluster + from .adapters.base import BaseAdapter + +class Table: + def __init__(self, cluster: "Cluster") -> None: + self.cluster = cluster +``` + +## Docstrings + +### Module Docstrings +```python +"""Table objects for PostgreSQL database management. + +This module provides the Table class and TableCollection for managing +PostgreSQL tables through an object-oriented interface. + +Example: + >>> cluster = Cluster(host="localhost") + >>> table = cluster.tables["users"] + >>> table.owner = "new_owner" + >>> table.alter() +""" +``` + +### Class Docstrings +```python +class Table: + """Represents a PostgreSQL table. + + Provides methods to create, alter, drop, and query table properties. + Changes are tracked and applied via the alter() method. + + Args: + name: Table name + cluster: Postgres cluster object + schema: Schema name (default: 'public') + owner: Table owner + oid: Table OID + + Attributes: + name: Table name + schema: Schema name + owner: Table owner + oid: Table OID + tablespace: Tablespace name + row_security: Whether row security is enabled + + Example: + >>> table = cluster.tables["users"] + >>> table.owner = "app_user" + >>> table.alter() + """ +``` + +### Method Docstrings +```python +def create_table( + self, + name: str, + columns: List[Dict[str, Any]], + *, + schema: str = "public" +) -> None: + """Create a new table on the PostgreSQL server. + + Generates and executes a CREATE TABLE statement based on the + provided column definitions. + + Args: + name: Table name + columns: List of column definitions + schema: Schema name (default: 'public') + + Raises: + PostgresError: If table creation fails + ValueError: If column definitions are invalid + + Example: + >>> cluster.create_table( + ... "users", + ... [{"name": "id", "type": "serial"}] + ... ) + """ +``` + +## Error Handling + +### Exception Hierarchy +```python +# Base exception +class PostgresError(Exception): + """Base exception for PGMob errors.""" + pass + +# Specific exceptions +class AdapterError(PostgresError): + """Adapter-specific errors.""" + pass + +class ObjectNotFoundError(PostgresError): + """Object not found in database.""" + pass +``` + +### Raising Exceptions +```python +# Good: Descriptive error with context +if not result: + raise PostgresError( + f"Table '{name}' not found in schema '{schema}'" + ) + +# Good: Chain exceptions +try: + cursor.execute(query) +except Exception as e: + raise AdapterError(f"Query execution failed: {e}") from e + +# Bad: Generic error +raise Exception("Error") # Don't use generic Exception + +# Bad: No context +raise PostgresError("Not found") # Not descriptive +``` + +### Exception Handling +```python +# Good: Specific exception handling +try: + table = cluster.tables["nonexistent"] +except KeyError: + logger.warning("Table not found, creating new one") + table = cluster.tables.new("nonexistent") + table.create() + +# Good: Cleanup in finally +try: + cursor = adapter.cursor() + cursor.execute(query) + return cursor.fetchall() +finally: + cursor.close() + +# Better: Use context manager +with adapter.cursor() as cursor: + cursor.execute(query) + return cursor.fetchall() +``` + +## Logging + +### Logger Setup +```python +import logging + +# Module-level logger +LOGGER = logging.getLogger(__name__) + +# Usage +LOGGER.debug("Executing query: %s", query) +LOGGER.info("Connected to cluster: %s", host) +LOGGER.warning("Table not found: %s", name) +LOGGER.error("Failed to execute query: %s", error) +``` + +### Log Levels +- `DEBUG`: Detailed information for debugging +- `INFO`: General informational messages +- `WARNING`: Warning messages for recoverable issues +- `ERROR`: Error messages for failures +- `CRITICAL`: Critical errors that may cause system failure + +### Logging Best Practices +```python +# Good: Use lazy formatting +LOGGER.debug("Query: %s, Params: %s", query, params) + +# Bad: String formatting in log call +LOGGER.debug(f"Query: {query}, Params: {params}") # Evaluated even if not logged + +# Good: Don't log sensitive data +LOGGER.info("Connected to host: %s", host) + +# Bad: Logging sensitive data +LOGGER.info("Connected with password: %s", password) # NEVER +``` + +## Comments + +### When to Comment +```python +# Good: Explain WHY, not WHAT +# Use lazy loading to avoid loading thousands of objects on connection +if load_strategy == LoadStrategy.LAZY: + return self._load_metadata_only() + +# Good: Explain complex logic +# PostgreSQL 11+ uses different procedure query format +if self.version >= Version("11.0"): + sql = util.get_sql("get_procedure_11") +else: + sql = util.get_sql("get_procedure") + +# Bad: Obvious comment +# Set owner to postgres +table.owner = "postgres" # Comment adds no value + +# Bad: Commented-out code +# table.drop() # Remove commented code, use version control +``` + +### TODO Comments +```python +# TODO(username): Add support for partitioned tables +# FIXME: This breaks with special characters in table names +# HACK: Workaround for psycopg2 bug #123 +# NOTE: This must be called before alter() +``` + +## Code Organization + +### File Structure +```python +"""Module docstring.""" + +# Imports +import standard_library +import third_party +from . import local + +# Constants +DEFAULT_VALUE = 100 +MAX_RETRIES = 3 + +# Private helper functions +def _helper_function(): + pass + +# Public classes +class PublicClass: + pass + +# Public functions +def public_function(): + pass +``` + +### Class Structure +```python +class MyClass: + """Class docstring.""" + + # Class variables + class_var: int = 0 + + def __init__(self): + """Initialize instance.""" + # Instance variables + self._private_var = None + self.public_var = None + + # Properties + @property + def my_property(self): + return self._private_var + + # Public methods + def public_method(self): + pass + + # Private methods + def _private_method(self): + pass + + # Special methods + def __repr__(self): + return f"{self.__class__.__name__}()" + + def __str__(self): + return self.name +``` + +## Best Practices + +### Use Context Managers +```python +# Good: Automatic resource cleanup +with adapter.cursor() as cursor: + cursor.execute(query) + return cursor.fetchall() + +# Bad: Manual cleanup +cursor = adapter.cursor() +try: + cursor.execute(query) + return cursor.fetchall() +finally: + cursor.close() +``` + +### Use Comprehensions +```python +# Good: List comprehension +tables = [t for t in cluster.tables if t.schema == "public"] + +# Good: Dict comprehension +table_map = {t.name: t for t in cluster.tables} + +# Good: Generator expression for large datasets +total = sum(t.size for t in cluster.tables) + +# Bad: Unnecessary loop +tables = [] +for t in cluster.tables: + if t.schema == "public": + tables.append(t) +``` + +### Use f-strings +```python +# Good: f-string +message = f"Table '{name}' in schema '{schema}'" + +# Good: Multi-line f-string +query = ( + f"SELECT * FROM {schema}.{table} " + f"WHERE id = {id}" +) + +# Bad: % formatting +message = "Table '%s' in schema '%s'" % (name, schema) + +# Bad: .format() +message = "Table '{}' in schema '{}'".format(name, schema) +``` + +### Avoid Mutable Default Arguments +```python +# Good: Use None as default +def create_table(name: str, columns: Optional[List] = None) -> None: + if columns is None: + columns = [] + # Use columns + +# Bad: Mutable default +def create_table(name: str, columns: List = []) -> None: # NEVER + # columns is shared across all calls! + pass +``` + +### Use Enums for Constants +```python +from enum import Enum + +class LoadStrategy(Enum): + """Collection loading strategies.""" + LAZY = "lazy" + EAGER = "eager" + HYBRID = "hybrid" + +# Usage +if strategy == LoadStrategy.LAZY: + pass +``` + +### Property vs Method +```python +# Use property for simple attribute access +@property +def name(self) -> str: + """Table name (property).""" + return self._name + +# Use method for operations that do work +def refresh(self) -> None: + """Reload from server (method).""" + self._load_from_server() +``` + +## Performance Considerations + +### Lazy Evaluation +```python +# Good: Lazy property +@property +def tables(self): + return get_lazy_property( + self, + "tables", + lambda: TableCollection(cluster=self) + ) + +# Good: Generator for large datasets +def iter_tables(self): + for row in self.execute("SELECT * FROM pg_tables"): + yield Table(*row) +``` + +### Avoid Repeated Queries +```python +# Good: Cache metadata +def _load_metadata(self): + if not self._metadata_loaded: + self._metadata_cache = self._fetch_metadata() + self._metadata_loaded = True + return self._metadata_cache + +# Bad: Query every time +def get_table_count(self): + return len(self.execute("SELECT * FROM pg_tables")) # Slow! +``` + +### Use Appropriate Data Structures +```python +# Good: Dict for lookups +tables_by_name = {t.name: t for t in tables} +table = tables_by_name["users"] # O(1) + +# Bad: List for lookups +tables_list = list(tables) +table = next(t for t in tables_list if t.name == "users") # O(n) +``` + +## Security + +### SQL Injection Prevention +```python +# Good: Parameterized query +cluster.execute( + "SELECT * FROM pg_tables WHERE tablename = %s", + ("users",) +) + +# Good: Use Identifier for names +SQL("SELECT * FROM {table}").format(table=Identifier("users")) + +# Bad: String concatenation +query = f"SELECT * FROM {table_name}" # SQL INJECTION RISK! + +# Bad: String formatting +query = "SELECT * FROM %s" % table_name # SQL INJECTION RISK! +``` + +### Shell Command Execution +```python +# Good: Use shell.quote() +command = f"pg_dump {self.shell.quote(database)}" + +# Good: Use cluster.run_os_command() +result = cluster.run_os_command(f"whoami") + +# Bad: Direct string interpolation +command = f"pg_dump {database}" # SHELL INJECTION RISK! +``` + +### Credential Handling +```python +# Good: Don't log credentials +LOGGER.info("Connected to %s", host) + +# Bad: Logging credentials +LOGGER.info("Connected with password %s", password) # NEVER! + +# Good: Mask in error messages +raise PostgresError(f"Connection failed to {host}") + +# Bad: Expose credentials in errors +raise PostgresError(f"Connection failed: {connection_string}") # May contain password +``` diff --git a/.windsurf/rules/documentation-standards.md b/.windsurf/rules/documentation-standards.md new file mode 100644 index 0000000..437a0a9 --- /dev/null +++ b/.windsurf/rules/documentation-standards.md @@ -0,0 +1,108 @@ +--- +# Kiro: Always include this file +inclusion: always + +# Windsurf: Apply to documentation files +applies_to: + - "**/*.md" + - "**/*.rst" + +# GitHub Copilot: Apply to documentation files +applyTo: + - "**/*.md" + - "**/*.rst" +--- + +# Documentation Standards for PGMob + +## Task completion documents + +NEVER create LLM activity report files in the repository, other than plans. Show the results in the chat instead. + +This includes any files ending with `_COMPLETE.md`, `_SUMMARY.md`, `_PLAN.md`, `_REPORT.md`. These are temporary working documents created during development and will not be committed to the repository. + +**CRITICAL**: Never reference or link temporary LLM activity report files in official documentation. + +## Official Documentation Files + +Only reference these files in official documentation: + +- `README.md` - Main project README +- `CONTRIBUTING.md` - Contribution guidelines +- `LICENSE` - License file +- `docs/**/*.rst` - Sphinx documentation +- `.windsurf/rules/README.md` - Steering rules documentation (internal) + +## Documentation Best Practices + +### README.md +- Keep it concise and user-focused +- Include: Installation, Quick Start, Features, Links to full docs +- No internal planning or migration details +- No references to temporary files + +### CONTRIBUTING.md +- Development setup instructions +- Code style guidelines +- Testing requirements +- PR process + +### Sphinx Documentation (docs/) +- Comprehensive API documentation +- Tutorials and guides +- Architecture overview +- No internal planning documents + +## When Creating Documentation + +1. **User-facing docs** (README, CONTRIBUTING, docs/): + - Focus on what users need to know + - Clear, concise, actionable + - No internal planning or temporary files + +2. **Internal docs** (steering rules, planning): + - Can reference temporary files + - Not linked from user-facing docs + - Clearly marked as internal + +3. **Temporary files** (reports, summaries, plans): + - Use for AI agent context only + - Never commit to repository + - Never reference in official docs + - Add to `.gitignore` if needed + +## Examples + +### ❌ Bad - Don't Do This +```markdown +# README.md + +For migration details, see [MIGRATION_TO_UV.md](MIGRATION_TO_UV.md) +For the optimization plan, see [OPTIMIZATION_PLAN.md](OPTIMIZATION_PLAN.md) +``` + +### ✅ Good - Do This +```markdown +# README.md + +## Development + +```bash +# Install dependencies +uv sync --all-extras + +# Run tests +uv run pytest +``` + +For detailed contribution guidelines, see [CONTRIBUTING.md](CONTRIBUTING.md) +``` + +## Checking Documentation + +Before committing documentation changes: + +1. Search for references to temporary files +2. Ensure only official docs are linked +3. Verify all links point to committed files +4. Remove any internal planning references diff --git a/.windsurf/rules/project-patterns.md b/.windsurf/rules/project-patterns.md new file mode 100644 index 0000000..42eda11 --- /dev/null +++ b/.windsurf/rules/project-patterns.md @@ -0,0 +1,575 @@ +--- +# Kiro: Always include this file +inclusion: always + +# Windsurf: Apply to Python files +applies_to: + - "**/*.py" + +# GitHub Copilot: Apply to Python files +applyTo: + - "**/*.py" +--- + +# Project-Specific Patterns for PGMob + +## Adapter Pattern Usage + +### Implementing Adapters +```python +from .adapters.base import BaseAdapter, BaseCursor + +class MyAdapter(BaseAdapter): + """Custom adapter implementation.""" + + def connect(self, *args, **kwargs) -> Any: + """Establish connection.""" + # Implementation + pass + + def cursor(self) -> BaseCursor: + """Get cursor object.""" + return MyCursor(self) + + @property + def is_connected(self) -> bool: + """Check connection status.""" + return self.connection is not None +``` + +### Using Adapters +```python +# Auto-detect adapter +from .adapters import detect_adapter + +adapter = detect_adapter() +cluster = Cluster(adapter=adapter, host="localhost") + +# Specify adapter explicitly +from .adapters.psycopg2 import Psycopg2Adapter + +adapter = Psycopg2Adapter() +cluster = Cluster(adapter=adapter, host="localhost") +``` + +## SQL String Composition + +### Loading SQL from Files +```python +from . import util + +# Load SQL query +sql = util.get_sql("get_table") + +# Load version-specific SQL +sql = util.get_sql("get_procedure", cluster.version) + +# Add WHERE clause +sql = sql + SQL(" WHERE c.oid = %s") +result = cluster.execute(sql, (oid,)) +``` + +### Building Dynamic Queries +```python +from .sql import SQL, Identifier, Literal + +# Build query with identifiers +query = SQL("SELECT * FROM {schema}.{table}").format( + schema=Identifier("public"), + table=Identifier("users") +) + +# Build query with conditions +conditions = [] +params = [] + +if schema: + conditions.append(SQL("schemaname = %s")) + params.append(schema) + +if owner: + conditions.append(SQL("tableowner = %s")) + params.append(owner) + +if conditions: + query = query + SQL(" WHERE ") + SQL(" AND ").join(conditions) + +result = cluster.execute(query, tuple(params)) +``` + +### Composing Complex Queries +```python +# Build INSERT statement +columns = ["id", "name", "email"] +values_placeholder = SQL(", ").join([SQL("%s")] * len(columns)) + +query = SQL("INSERT INTO {table} ({columns}) VALUES ({values})").format( + table=Identifier("users"), + columns=SQL(", ").join([Identifier(c) for c in columns]), + values=values_placeholder +) + +cluster.execute(query, (1, "John", "john@example.com")) +``` + +## Lazy Loading Implementation + +### Implementing Lazy Collections +```python +from .objects.generic import _LazyBaseCollection + +class MyObjectCollection(_LazyBaseCollection[MyObject]): + """Lazy-loading collection.""" + + def _fetch_metadata(self) -> List[Dict[str, Any]]: + """Fetch lightweight metadata.""" + sql = SQL(""" + SELECT name, schema, oid + FROM my_objects + WHERE schema NOT IN ('pg_catalog', 'information_schema') + """) + + result = self.cluster.execute(sql) + return [ + {"name": row[0], "schema": row[1], "oid": row[2]} + for row in result + ] + + def _fetch_object(self, key: str) -> MyObject: + """Fetch full object details.""" + oid = self._metadata_cache[key]["oid"] + + sql = util.get_sql("get_my_object") + SQL(" WHERE oid = %s") + result = self.cluster.execute(sql, (oid,)) + + if not result: + raise PostgresError(f"Object {key} not found") + + return self._map_result(result[0]) + + def _get_key(self, item: Dict[str, Any]) -> str: + """Generate key from metadata.""" + return self._index(name=item["name"], schema=item["schema"]) +``` + +### Using Lazy Collections +```python +# Lazy loading (default) +cluster = Cluster(host="localhost", load_strategy=LoadStrategy.LAZY) + +# Check existence without loading +if "mytable" in cluster.tables: # Fast - uses metadata + table = cluster.tables["mytable"] # Loads full object + +# Iterate loads on-demand +for table in cluster.tables: # Loads each table as iterated + print(table.name) + +# Get keys without loading objects +table_names = cluster.tables.keys() # Fast - uses metadata +``` + +## Inheritance and Mixins + +### Using Mixins +```python +from .objects.generic import ( + _DynamicObject, + _CollectionChild, + OwnedObjectMixin, + SchemaObjectMixin, + NamedObjectMixin +) + +class MyObject( + _DynamicObject, + _CollectionChild, + OwnedObjectMixin, + SchemaObjectMixin, + NamedObjectMixin +): + """Object with common properties via mixins.""" + + def __init__( + self, + name: str, + schema: str = "public", + owner: Optional[str] = None, + cluster: "Cluster" = None, + **kwargs + ): + super().__init__(kind="MY_OBJECT", cluster=cluster, name=name, schema=schema) + _CollectionChild.__init__(self, parent=kwargs.get('parent')) + + # Initialize mixins + self.__init_name__(name) + self.__init_schema__(schema) + self.__init_owner__(owner) + + # Object-specific attributes + self._custom_attr = kwargs.get('custom_attr') +``` + +### Change Tracking Pattern +```python +class MyObject(_DynamicObject): + """Object with change tracking.""" + + @property + def custom_property(self) -> str: + return self._custom_property + + @custom_property.setter + def custom_property(self, value: str) -> None: + """Set property. Queues change for alter().""" + if self._custom_property != value: + self._changes["custom_property"] = generic._SQLChange( + obj=self, + sql=SQL("ALTER MY_OBJECT {fqn} SET CUSTOM {value}").format( + fqn=self._sql_fqn(), + value=Literal(value) + ) + ) + self._custom_property = value + + def alter(self) -> None: + """Apply pending changes.""" + super().alter() # Applies all queued changes +``` + +## Provider Pattern + +### Implementing Metadata Providers +```python +from .providers.base import MetadataProvider + +class CustomMetadataProvider(MetadataProvider): + """Custom metadata provider.""" + + def __init__(self, cluster: "Cluster"): + super().__init__(cluster) + self._cache: Dict[str, Any] = {} + + def get_tables(self, schema: Optional[str] = None) -> List[Dict[str, Any]]: + """Get table metadata.""" + cache_key = f"tables:{schema}" + + if cache_key not in self._cache: + sql = util.get_sql("get_table") + if schema: + sql += SQL(" WHERE n.nspname = %s") + result = self.cluster.execute(sql, (schema,)) + else: + result = self.cluster.execute(sql) + + self._cache[cache_key] = [ + self._map_table_row(row) for row in result + ] + + return self._cache[cache_key] + + def invalidate_cache(self, object_type: Optional[str] = None) -> None: + """Invalidate cache.""" + if object_type: + keys_to_remove = [k for k in self._cache if k.startswith(f"{object_type}:")] + for key in keys_to_remove: + del self._cache[key] + else: + self._cache.clear() +``` + +### Using Providers +```python +# Use default provider +cluster = Cluster(host="localhost") + +# Use custom provider +from .providers.custom import CustomMetadataProvider + +provider = CustomMetadataProvider(cluster) +cluster.metadata_provider = provider + +# Access through provider +tables = cluster.metadata_provider.get_tables(schema="public") +``` + +## Version-Specific Handling + +### Version Detection +```python +from packaging import Version + +# Check version +if cluster.version >= Version("11.0"): + # Use PostgreSQL 11+ features + sql = util.get_sql("get_procedure_11") +else: + # Use older query + sql = util.get_sql("get_procedure") +``` + +### Version-Specific SQL Files +``` +src/pgmob/scripts/sql/ +├── get_procedure.sql # Default (PostgreSQL 10+) +└── get_procedure_11.sql # PostgreSQL 11+ +``` + +```python +# Load version-specific SQL +sql = util.get_sql("get_procedure", cluster.version) +``` + +## Error Handling Patterns + +### Custom Exceptions +```python +from .errors import PostgresError + +class ObjectNotFoundError(PostgresError): + """Raised when object not found.""" + pass + +class PermissionDeniedError(PostgresError): + """Raised when operation not permitted.""" + pass + +# Usage +if not result: + raise ObjectNotFoundError( + f"Table '{name}' not found in schema '{schema}'" + ) +``` + +### Error Recovery +```python +def execute_with_retry( + self, + query: str, + params: tuple = None, + max_retries: int = 3 +) -> List[Tuple[Any]]: + """Execute query with retry logic.""" + for attempt in range(max_retries): + try: + return self.execute(query, params) + except AdapterError as e: + if attempt == max_retries - 1: + raise + + LOGGER.warning( + "Query failed (attempt %d/%d): %s", + attempt + 1, + max_retries, + e + ) + + # Reconnect if connection lost + if not self.adapter.is_connected: + self._acquire_connection() +``` + +## Context Manager Patterns + +### Transaction Management +```python +# Use context manager for transactions +with cluster._no_autocommit(): + # Multiple operations in single transaction + cluster.execute("INSERT INTO users VALUES (%s, %s)", (1, "John")) + cluster.execute("INSERT INTO logs VALUES (%s, %s)", (1, "Created")) + # Automatically commits on exit +``` + +### Resource Management +```python +# Cursor management +with cluster.adapter.cursor() as cursor: + cursor.execute(query) + result = cursor.fetchall() +# Cursor automatically closed + +# Connection management +with Cluster(host="localhost") as cluster: + # Use cluster + tables = cluster.tables +# Connection automatically closed +``` + +## Async Patterns + +### Async Operations +```python +import asyncio + +# Async collection loading +async def load_all_collections(): + cluster = AsyncCluster(...) + + collections = await cluster.load_collections_parallel( + "tables", "roles", "databases", "schemas" + ) + + return collections + +# Run async operation +collections = asyncio.run(load_all_collections()) +``` + +### Sync Wrapper +```python +def load_collections(self, *names: str, parallel: bool = False): + """Load collections with optional parallel execution.""" + if parallel and self.enable_async: + return asyncio.run(self._load_collections_async(*names)) + else: + return self._load_collections_sync(*names) +``` + +## Testing Patterns + +### Fixture Patterns +```python +@pytest.fixture +def cluster(): + """Provide test cluster.""" + cluster = Cluster(host="localhost", user="postgres") + yield cluster + cluster.adapter.close_connection() + +@pytest.fixture +def test_table(cluster): + """Provide test table.""" + table_name = "test_table" + cluster.execute(f"CREATE TABLE {table_name} (id serial, name text)") + + yield table_name + + cluster.execute(f"DROP TABLE IF EXISTS {table_name} CASCADE") +``` + +### Mock Patterns +```python +from unittest.mock import Mock, patch + +@patch('pgmob.adapters.detect_adapter') +def test_with_mock_adapter(mock_detect): + """Test with mocked adapter.""" + mock_adapter = Mock() + mock_adapter.is_connected = True + mock_detect.return_value = mock_adapter + + cluster = Cluster(host="localhost") + + assert cluster.adapter == mock_adapter +``` + +## Logging Patterns + +### Module Logger +```python +import logging + +LOGGER = logging.getLogger(__name__) + +class MyClass: + def my_method(self): + LOGGER.debug("Executing method: %s", self.name) + LOGGER.info("Operation completed successfully") +``` + +### Structured Logging +```python +LOGGER.info( + "Table created", + extra={ + "table_name": table.name, + "schema": table.schema, + "owner": table.owner, + "operation": "create" + } +) +``` + +## Performance Patterns + +### Caching +```python +from functools import lru_cache + +@lru_cache(maxsize=128) +def get_table_metadata(oid: int) -> Dict[str, Any]: + """Get table metadata with caching.""" + sql = util.get_sql("get_table") + SQL(" WHERE c.oid = %s") + result = cluster.execute(sql, (oid,)) + return dict(zip(COLUMNS, result[0])) +``` + +### Batch Operations +```python +def bulk_alter_owner(self, tables: List[Table], new_owner: str) -> None: + """Bulk alter table owners.""" + statements = [] + + for table in tables: + if table.owner != new_owner: + statements.append( + SQL("ALTER TABLE {table} OWNER TO {owner}").format( + table=table._sql_fqn(), + owner=Identifier(new_owner) + ) + ) + + if statements: + # Execute all in single transaction + with self._no_autocommit(): + for stmt in statements: + self.execute(stmt) +``` + +## Documentation Patterns + +### Example in Docstring +```python +def create_table(self, name: str, columns: List[Dict]) -> None: + """Create a new table. + + Args: + name: Table name + columns: Column definitions + + Example: + >>> cluster.create_table( + ... "users", + ... [ + ... {"name": "id", "type": "serial", "primary_key": True}, + ... {"name": "username", "type": "varchar(50)", "nullable": False}, + ... {"name": "email", "type": "varchar(100)"}, + ... ] + ... ) + """ +``` + +### Type Hints in Documentation +```python +from typing import Union, Optional, List + +def execute( + self, + query: Union[Composable, str], + params: Optional[Tuple[Any, ...]] = None +) -> List[Tuple[Any]]: + """Execute SQL query. + + Args: + query: SQL query string or Composable object + params: Query parameters as tuple + + Returns: + List of tuples containing query results + + Raises: + PostgresError: If query execution fails + AdapterError: If adapter error occurs + """ +``` diff --git a/.windsurf/rules/security-guidelines.md b/.windsurf/rules/security-guidelines.md new file mode 100644 index 0000000..f182684 --- /dev/null +++ b/.windsurf/rules/security-guidelines.md @@ -0,0 +1,535 @@ +--- +# Kiro: Always include this file +inclusion: always + +# Windsurf: Apply to Python files +applies_to: + - "**/*.py" + +# GitHub Copilot: Apply to Python files +applyTo: + - "**/*.py" +--- + +# Security Guidelines for PGMob + +## SQL Injection Prevention + +### Always Use Parameterized Queries +```python +# SECURE: Parameterized query +cluster.execute( + "SELECT * FROM pg_tables WHERE tablename = %s", + ("users",) +) + +# SECURE: Multiple parameters +cluster.execute( + "SELECT * FROM pg_tables WHERE tablename = %s AND schemaname = %s", + ("users", "public") +) + +# INSECURE: String concatenation +query = f"SELECT * FROM pg_tables WHERE tablename = '{table_name}'" # NEVER! +cluster.execute(query) + +# INSECURE: String formatting +query = "SELECT * FROM pg_tables WHERE tablename = '%s'" % table_name # NEVER! +cluster.execute(query) +``` + +### Use SQL Composition for Identifiers +```python +from .sql import SQL, Identifier, Literal + +# SECURE: Use Identifier for table/column names +query = SQL("SELECT * FROM {table} WHERE {column} = %s").format( + table=Identifier("users"), + column=Identifier("username") +) +cluster.execute(query, ("admin",)) + +# SECURE: Use Literal for SQL literals +query = SQL("SET TIME ZONE {tz}").format(tz=Literal("UTC")) +cluster.execute(query) + +# INSECURE: String interpolation for identifiers +query = f"SELECT * FROM {table_name}" # SQL INJECTION RISK! +``` + +### Validate User Input +```python +# SECURE: Validate before use +def get_table(name: str) -> Table: + # Validate table name format + if not re.match(r'^[a-zA-Z_][a-zA-Z0-9_]*$', name): + raise ValueError(f"Invalid table name: {name}") + + return cluster.tables[name] + +# SECURE: Whitelist allowed values +ALLOWED_SCHEMAS = {"public", "app", "data"} + +def get_schema(name: str) -> Schema: + if name not in ALLOWED_SCHEMAS: + raise ValueError(f"Schema not allowed: {name}") + + return cluster.schemas[name] +``` + +## Shell Command Injection Prevention + +### Use Shell Quoting +```python +from .os import ShellEnv + +shell = ShellEnv() + +# SECURE: Use shell.quote() +database = "my_database" +command = f"pg_dump {shell.quote(database)}" + +# SECURE: Quote all user-provided arguments +command = f"pg_dump -h {shell.quote(host)} -U {shell.quote(user)} {shell.quote(database)}" + +# INSECURE: Direct interpolation +command = f"pg_dump {database}" # SHELL INJECTION RISK! +``` + +### Use cluster.run_os_command() +```python +# SECURE: Use cluster method +result = cluster.run_os_command("whoami") + +# SECURE: With proper quoting +database = user_input +result = cluster.run_os_command(f"pg_dump {shell.quote(database)}") + +# INSECURE: Direct subprocess +import subprocess +subprocess.run(f"pg_dump {database}", shell=True) # NEVER! +``` + +### Avoid Shell=True +```python +import subprocess + +# SECURE: Use list of arguments +subprocess.run(["pg_dump", "-d", database]) + +# INSECURE: shell=True with string +subprocess.run(f"pg_dump -d {database}", shell=True) # NEVER! +``` + +## Credential Management + +### Never Log Credentials +```python +import logging + +LOGGER = logging.getLogger(__name__) + +# SECURE: Log without credentials +LOGGER.info("Connecting to host: %s", host) +LOGGER.info("Connected as user: %s", user) + +# INSECURE: Logging credentials +LOGGER.info("Password: %s", password) # NEVER! +LOGGER.debug("Connection string: %s", connstring) # May contain password! +``` + +### Mask Credentials in Error Messages +```python +# SECURE: Generic error message +try: + cluster.connect(host=host, user=user, password=password) +except Exception as e: + raise PostgresError(f"Connection failed to {host}") from e + +# INSECURE: Expose credentials +try: + cluster.connect(connstring) +except Exception as e: + raise PostgresError(f"Connection failed: {connstring}") from e # May contain password! +``` + +### Use Environment Variables +```python +import os + +# SECURE: Read from environment +password = os.environ.get("PGPASSWORD") +cluster = Cluster( + host="localhost", + user="postgres", + password=password +) + +# INSECURE: Hardcoded credentials +cluster = Cluster( + host="localhost", + user="postgres", + password="secret123" # NEVER! +) +``` + +### Secure Password Storage +```python +# SECURE: Use keyring or secrets manager +import keyring + +password = keyring.get_password("pgmob", "postgres") +cluster = Cluster(host="localhost", user="postgres", password=password) + +# SECURE: Use .pgpass file +# PostgreSQL will automatically read from ~/.pgpass +cluster = Cluster(host="localhost", user="postgres") +``` + +## Connection Security + +### Use SSL/TLS +```python +# SECURE: Require SSL +cluster = Cluster( + host="production.example.com", + user="app_user", + password=password, + sslmode="require" +) + +# SECURE: Verify certificate +cluster = Cluster( + host="production.example.com", + user="app_user", + password=password, + sslmode="verify-full", + sslrootcert="/path/to/ca.crt" +) +``` + +### Limit Connection Permissions +```python +# SECURE: Use least privilege principle +# Connect as read-only user for queries +readonly_cluster = Cluster( + host="localhost", + user="readonly_user", + password=readonly_password +) + +# Use admin user only when needed +admin_cluster = Cluster( + host="localhost", + user="postgres", + password=admin_password +) +``` + +## Input Validation + +### Validate All User Input +```python +# SECURE: Validate table name +def validate_table_name(name: str) -> None: + if not name: + raise ValueError("Table name cannot be empty") + + if len(name) > 63: # PostgreSQL limit + raise ValueError("Table name too long") + + if not re.match(r'^[a-zA-Z_][a-zA-Z0-9_]*$', name): + raise ValueError("Invalid table name format") + +# SECURE: Validate schema name +def validate_schema_name(name: str) -> None: + if name not in ALLOWED_SCHEMAS: + raise ValueError(f"Schema not allowed: {name}") +``` + +### Sanitize File Paths +```python +from pathlib import Path + +# SECURE: Validate file path +def validate_backup_path(path: str) -> Path: + backup_dir = Path("/var/backups/pgmob") + full_path = (backup_dir / path).resolve() + + # Prevent directory traversal + if not str(full_path).startswith(str(backup_dir)): + raise ValueError("Invalid backup path") + + return full_path + +# Usage +backup_path = validate_backup_path(user_provided_path) +``` + +## Access Control + +### Check Permissions Before Operations +```python +# SECURE: Check if user has permission +def drop_table(cluster: Cluster, table_name: str) -> None: + table = cluster.tables[table_name] + + # Check if current user is owner or superuser + current_user = cluster.execute("SELECT current_user")[0][0] + if table.owner != current_user and not is_superuser(cluster): + raise PermissionError(f"User {current_user} cannot drop table {table_name}") + + table.drop() +``` + +### Implement Role-Based Access +```python +from enum import Enum + +class Permission(Enum): + READ = "read" + WRITE = "write" + ADMIN = "admin" + +class SecureCluster: + def __init__(self, cluster: Cluster, user_permissions: set[Permission]): + self.cluster = cluster + self.permissions = user_permissions + + def execute(self, query: str, params: tuple = None): + # Check if query is read-only + if query.strip().upper().startswith(("SELECT", "SHOW")): + if Permission.READ not in self.permissions: + raise PermissionError("User does not have READ permission") + else: + if Permission.WRITE not in self.permissions: + raise PermissionError("User does not have WRITE permission") + + return self.cluster.execute(query, params) +``` + +## Secure Defaults + +### Use Secure Defaults +```python +# SECURE: Default to safe behavior +def drop_table(self, cascade: bool = False, force: bool = False) -> None: + """Drop table. + + Args: + cascade: Drop dependent objects (default: False) + force: Force drop without confirmation (default: False) + """ + if not force: + # Require explicit confirmation for destructive operations + raise ValueError("Set force=True to confirm table drop") + + # ... drop logic +``` + +### Disable Dangerous Features by Default +```python +class Cluster: + def __init__( + self, + *args, + allow_shell_commands: bool = False, # Disabled by default + allow_file_operations: bool = False, # Disabled by default + **kwargs + ): + self.allow_shell_commands = allow_shell_commands + self.allow_file_operations = allow_file_operations + + def run_os_command(self, command: str) -> str: + if not self.allow_shell_commands: + raise PermissionError("Shell commands are disabled") + + # ... execute command +``` + +## Audit Logging + +### Log Security-Relevant Events +```python +import logging + +SECURITY_LOGGER = logging.getLogger("pgmob.security") + +# Log authentication attempts +def connect(self, *args, **kwargs): + try: + result = self._connect(*args, **kwargs) + SECURITY_LOGGER.info( + "Successful connection: user=%s, host=%s", + kwargs.get("user"), + kwargs.get("host") + ) + return result + except Exception as e: + SECURITY_LOGGER.warning( + "Failed connection attempt: user=%s, host=%s, error=%s", + kwargs.get("user"), + kwargs.get("host"), + str(e) + ) + raise + +# Log privilege escalation +def become_role(self, role: str): + SECURITY_LOGGER.info( + "Role change: from=%s, to=%s", + self.current_user, + role + ) + self.execute(f"SET ROLE {Identifier(role)}") +``` + +### Log Destructive Operations +```python +def drop_table(self, cascade: bool = False): + SECURITY_LOGGER.warning( + "Dropping table: name=%s, schema=%s, cascade=%s, user=%s", + self.name, + self.schema, + cascade, + self.cluster.current_user + ) + + # ... drop logic +``` + +## Rate Limiting + +### Implement Rate Limiting +```python +from time import time +from collections import defaultdict + +class RateLimiter: + def __init__(self, max_requests: int = 100, window: int = 60): + self.max_requests = max_requests + self.window = window + self.requests = defaultdict(list) + + def check(self, key: str) -> bool: + now = time() + # Remove old requests + self.requests[key] = [ + req_time for req_time in self.requests[key] + if now - req_time < self.window + ] + + # Check limit + if len(self.requests[key]) >= self.max_requests: + return False + + self.requests[key].append(now) + return True + +# Usage +rate_limiter = RateLimiter(max_requests=100, window=60) + +def execute(self, query: str, params: tuple = None): + if not rate_limiter.check(self.current_user): + raise PermissionError("Rate limit exceeded") + + return self._execute(query, params) +``` + +## Secure Configuration + +### Configuration Validation +```python +from typing import Dict, Any + +def validate_config(config: Dict[str, Any]) -> None: + """Validate cluster configuration for security issues.""" + + # Check for insecure SSL mode + if config.get("sslmode") in (None, "disable", "allow"): + raise ValueError("Insecure SSL mode. Use 'require' or 'verify-full'") + + # Check for hardcoded credentials + if "password" in config: + raise ValueError("Do not hardcode passwords in configuration") + + # Check for overly permissive settings + if config.get("allow_shell_commands") and config.get("allow_file_operations"): + raise ValueError("Dangerous: both shell commands and file operations enabled") +``` + +## Dependency Security + +### Pin Dependencies +```toml +# pyproject.toml +[project] +dependencies = [ + "psycopg2-binary>=2.9.5,<3.0", # Pin major version + "packaging>=21.3,<24.0", +] +``` + +### Regular Security Audits +```bash +# Check for known vulnerabilities +uv pip check + +# Update dependencies +uv sync --upgrade + +# Audit with safety +uv run safety check +``` + +## Testing Security + +### Security Test Cases +```python +def test_sql_injection_prevention(): + """Test SQL injection is prevented.""" + cluster = Cluster(...) + + # Attempt SQL injection + malicious_input = "users'; DROP TABLE users; --" + + # Should not execute DROP TABLE + with pytest.raises(KeyError): + cluster.tables[malicious_input] + +def test_shell_injection_prevention(): + """Test shell injection is prevented.""" + cluster = Cluster(...) + + # Attempt shell injection + malicious_input = "db; rm -rf /" + + # Should be properly quoted + result = cluster.run_os_command(f"echo {shell.quote(malicious_input)}") + assert "rm -rf" not in result.text + +def test_credential_not_logged(): + """Test credentials are not logged.""" + with LogCapture() as logs: + cluster = Cluster(host="localhost", password="secret") + + # Password should not appear in logs + assert "secret" not in str(logs) +``` + +## Security Checklist + +Before deploying or releasing: + +- [ ] All SQL queries use parameterization or SQL composition +- [ ] All shell commands use proper quoting +- [ ] No credentials in code or logs +- [ ] SSL/TLS enabled for production connections +- [ ] Input validation on all user-provided data +- [ ] Least privilege principle applied +- [ ] Audit logging for security events +- [ ] Rate limiting implemented +- [ ] Dependencies pinned and audited +- [ ] Security tests passing +- [ ] Code review completed +- [ ] Penetration testing performed (for production) diff --git a/.windsurf/rules/testing-standards.md b/.windsurf/rules/testing-standards.md new file mode 100644 index 0000000..9e92559 --- /dev/null +++ b/.windsurf/rules/testing-standards.md @@ -0,0 +1,586 @@ +--- +# Kiro: Always include this file +inclusion: always + +# Windsurf: Apply to Python test files +applies_to: + - "src/tests/**/*.py" + +# GitHub Copilot: Apply to Python test files +applyTo: + - "src/tests/**/*.py" +--- + +# Testing Standards for PGMob + +## Test Organization + +### Directory Structure +``` +src/tests/ +├── conftest.py # Shared fixtures +├── test_*.py # Unit tests +└── functional/ + ├── conftest.py # Integration test fixtures + └── test_*.py # Integration tests +``` + +### Test File Naming +- Unit tests: `test_.py` +- Integration tests: `functional/test_.py` +- One test file per source module + +## Installing Test Dependencies + +When running tests for this project, you need to install the psycopg2-binary extra: + +```bash +uv sync --extra psycopg2-binary --extra dev +``` + +## Why psycopg2-binary? + +The project supports both `psycopg2` and `psycopg2-binary` as optional dependencies. For testing and development: + +- **psycopg2-binary**: Pre-compiled binary package, easier to install, recommended for development +- **psycopg2**: Source package requiring PostgreSQL development libraries, recommended for production + +## Running Tests + +After installing dependencies: + +```bash +# Run all tests +uv run pytest -vv + +# Run unit tests only +uv run pytest -m unit -vv + +# Run functional tests only (requires Docker, will start container automatically) +uv run pytest -m integration -vv +``` + +## Type Checking + +Only matters for the module itself. + +```bash +uv run ty check src/pgmob +``` + +## Linting + +Should apply to both tests and source code. + +```bash +uv run ruff check src +uv run ruff format --check src +``` + +## Test Naming Convention + +### Pattern +```python +def test___(): + """Test description.""" +``` + +### Examples +```python +def test_table_create_success(): + """Test successful table creation.""" + +def test_table_create_duplicate_raises_error(): + """Test creating duplicate table raises PostgresError.""" + +def test_lazy_loading_loads_object_on_first_access(): + """Test lazy loading defers object loading until first access.""" + +def test_collection_refresh_clears_cache(): + """Test refresh() clears cached objects.""" +``` + +## Test Structure (AAA Pattern) + +### Arrange, Act, Assert +```python +def test_table_owner_change(): + """Test changing table owner queues change for alter().""" + # Arrange + cluster = Cluster(host="localhost", user="postgres") + table = cluster.tables["test_table"] + original_owner = table.owner + + # Act + table.owner = "new_owner" + table.alter() + table.refresh() + + # Assert + assert table.owner == "new_owner" + assert table.owner != original_owner +``` + +## Fixtures + +### Shared Fixtures in conftest.py +```python +import pytest +from pgmob import Cluster + +@pytest.fixture(scope="session") +def docker_postgres(): + """Start PostgreSQL in Docker for testing.""" + # Setup + container = start_postgres_container() + yield container + # Teardown + container.stop() + +@pytest.fixture +def cluster(docker_postgres): + """Provide connected cluster for tests.""" + cluster = Cluster( + host=docker_postgres.host, + port=docker_postgres.port, + user="postgres", + password="test" + ) + yield cluster + cluster.adapter.close_connection() + +@pytest.fixture +def test_table(cluster): + """Create test table for tests.""" + cluster.execute("CREATE TABLE test_table (id serial, name text)") + yield "test_table" + cluster.execute("DROP TABLE IF EXISTS test_table CASCADE") +``` + +### Fixture Scopes +- `scope="session"`: Once per test session (Docker containers) +- `scope="module"`: Once per test module +- `scope="function"`: Once per test (default) + +## Mocking + +### Mock External Dependencies +```python +from unittest.mock import Mock, patch, MagicMock + +def test_execute_with_mocked_adapter(): + """Test execute() with mocked adapter.""" + # Arrange + mock_adapter = Mock() + mock_cursor = Mock() + mock_cursor.fetchall.return_value = [(1, "test")] + mock_adapter.cursor.return_value.__enter__.return_value = mock_cursor + + cluster = Cluster(adapter=mock_adapter) + + # Act + result = cluster.execute("SELECT * FROM test") + + # Assert + assert result == [(1, "test")] + mock_cursor.execute.assert_called_once() +``` + +### Patch for Testing +```python +@patch('pgmob.cluster.detect_adapter') +def test_cluster_init_detects_adapter(mock_detect): + """Test Cluster initialization detects adapter.""" + mock_detect.return_value = Mock() + + cluster = Cluster(host="localhost") + + mock_detect.assert_called_once() +``` + +## Assertions + +### Use Descriptive Assertions +```python +# Good: Descriptive message +assert table.owner == "postgres", f"Expected owner 'postgres', got '{table.owner}'" + +# Good: pytest assertions with context +assert "test_table" in cluster.tables, "Table should exist in collection" + +# Bad: No context +assert table.owner == "postgres" +``` + +### Multiple Assertions +```python +def test_table_properties(): + """Test table has correct properties.""" + table = cluster.tables["test_table"] + + assert table.name == "test_table" + assert table.schema == "public" + assert table.owner == "postgres" + assert table.oid is not None +``` + +### Exception Testing +```python +def test_invalid_table_raises_key_error(): + """Test accessing non-existent table raises KeyError.""" + cluster = Cluster(...) + + with pytest.raises(KeyError, match="nonexistent"): + _ = cluster.tables["nonexistent"] + +def test_invalid_sql_raises_postgres_error(): + """Test invalid SQL raises PostgresError.""" + cluster = Cluster(...) + + with pytest.raises(PostgresError): + cluster.execute("INVALID SQL") +``` + +## Parametrized Tests + +### Test Multiple Scenarios +```python +@pytest.mark.parametrize("load_strategy,expected_loaded", [ + (LoadStrategy.LAZY, False), + (LoadStrategy.EAGER, True), + (LoadStrategy.HYBRID, False), +]) +def test_collection_loading_strategy(load_strategy, expected_loaded): + """Test collection respects loading strategy.""" + cluster = Cluster(load_strategy=load_strategy) + collection = cluster.tables + + is_loaded = len(collection._loaded_keys) > 0 + assert is_loaded == expected_loaded +``` + +### Test Edge Cases +```python +@pytest.mark.parametrize("value,should_raise", [ + ("", True), # Empty string + (None, True), # None + ("valid", False), # Valid value + ("a" * 1000, False), # Long string +]) +def test_owner_validation(value, should_raise): + """Test owner property validates input.""" + table = Table(name="test") + + if should_raise: + with pytest.raises(ValueError): + table.owner = value + else: + table.owner = value + assert table.owner == value +``` + +## Integration Tests + +### Use Real PostgreSQL +```python +@pytest.mark.integration +def test_table_create_and_drop_integration(cluster): + """Integration test for table lifecycle.""" + # Create + table = cluster.tables.new("integration_test") + table.create() + cluster.tables.refresh() + + assert "integration_test" in cluster.tables + + # Drop + cluster.tables["integration_test"].drop() + cluster.tables.refresh() + + assert "integration_test" not in cluster.tables +``` + +### Test Against Multiple PostgreSQL Versions +```python +@pytest.mark.parametrize("pg_version", ["10", "11", "12", "13", "14", "15", "16"]) +def test_compatibility_with_postgres_version(pg_version): + """Test compatibility with PostgreSQL version.""" + cluster = start_postgres_cluster(version=pg_version) + + # Test basic operations + assert cluster.version.major == int(pg_version) + assert len(cluster.tables) >= 0 +``` + +## Performance Tests + +### Benchmark Critical Operations +```python +import time + +def test_lazy_loading_performance(): + """Test lazy loading is faster than eager loading.""" + # Lazy loading + start = time.time() + cluster_lazy = Cluster(load_strategy=LoadStrategy.LAZY) + lazy_time = time.time() - start + + # Eager loading + start = time.time() + cluster_eager = Cluster(load_strategy=LoadStrategy.EAGER) + eager_time = time.time() - start + + # Lazy should be significantly faster + assert lazy_time < eager_time * 0.5 +``` + +### Memory Usage Tests +```python +import tracemalloc + +def test_lazy_loading_memory_usage(): + """Test lazy loading uses less memory.""" + # Measure lazy loading + tracemalloc.start() + cluster_lazy = Cluster(load_strategy=LoadStrategy.LAZY) + _ = cluster_lazy.tables.keys() # Load metadata only + lazy_memory = tracemalloc.get_traced_memory()[0] + tracemalloc.stop() + + # Measure eager loading + tracemalloc.start() + cluster_eager = Cluster(load_strategy=LoadStrategy.EAGER) + eager_memory = tracemalloc.get_traced_memory()[0] + tracemalloc.stop() + + # Lazy should use significantly less memory + assert lazy_memory < eager_memory * 0.3 +``` + +## Coverage Requirements + +### Minimum Coverage +- Overall: >90% +- New code: 100% +- Critical paths: 100% + +### Run Coverage +```bash +# Generate coverage report +uv run pytest --cov=pgmob --cov-report=html --cov-report=term + +# View HTML report +open htmlcov/index.html +``` + +### Coverage Configuration +```ini +# pyproject.toml +[tool.coverage.run] +source = ["src/pgmob"] +omit = [ + "*/tests/*", + "*/conftest.py", +] + +[tool.coverage.report] +exclude_lines = [ + "pragma: no cover", + "def __repr__", + "raise AssertionError", + "raise NotImplementedError", + "if TYPE_CHECKING:", + "if __name__ == .__main__.:", +] +``` + +## Test Markers + +### Mark Test Categories +```python +@pytest.mark.unit +def test_unit_test(): + """Unit test.""" + +@pytest.mark.integration +def test_integration_test(): + """Integration test.""" + +@pytest.mark.slow +def test_slow_operation(): + """Slow test.""" + +@pytest.mark.skip(reason="Not implemented yet") +def test_future_feature(): + """Future feature test.""" +``` + +### Run Specific Tests +```bash +# Run only unit tests +uv run pytest -m unit + +# Run only integration tests +uv run pytest -m integration + +# Skip slow tests +uv run pytest -m "not slow" +``` + +## Async Tests + +### Test Async Operations +```python +import pytest +import asyncio + +@pytest.mark.asyncio +async def test_async_collection_loading(): + """Test async parallel collection loading.""" + cluster = AsyncCluster(...) + + collections = await cluster.load_collections_parallel( + "tables", "roles", "databases" + ) + + assert "tables" in collections + assert "roles" in collections + assert "databases" in collections +``` + +## Test Data Management + +### Use Factories for Test Data +```python +def create_test_table(cluster, name="test_table", **kwargs): + """Factory for creating test tables.""" + defaults = { + "schema": "public", + "owner": "postgres", + } + defaults.update(kwargs) + + cluster.execute(f""" + CREATE TABLE {defaults['schema']}.{name} ( + id serial PRIMARY KEY, + name text + ) + """) + return name + +def test_with_factory(cluster): + """Test using factory.""" + table_name = create_test_table(cluster, name="my_test") + assert table_name in cluster.tables +``` + +### Cleanup After Tests +```python +@pytest.fixture +def test_database(cluster): + """Create and cleanup test database.""" + db_name = "test_db" + cluster.databases.new(db_name).create() + + yield db_name + + # Cleanup + if db_name in cluster.databases: + cluster.databases[db_name].drop(force=True) +``` + +## Continuous Integration + +### GitHub Actions Configuration +```yaml +# .github/workflows/test.yml +name: Tests + +on: [push, pull_request] + +jobs: + test: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.9", "3.10", "3.11", "3.12"] + postgres-version: ["10", "12", "14", "16"] + + steps: + - uses: actions/checkout@v3 + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + - name: Install UV + run: pip install uv + - name: Install dependencies + run: uv sync --all-extras + - name: Run tests + run: uv run pytest --cov=pgmob +``` + +## Test Documentation + +### Document Test Purpose +```python +def test_lazy_loading_defers_object_creation(): + """Test lazy loading defers object creation until access. + + This test verifies that when using LAZY loading strategy, + objects are not created immediately when accessing the collection, + but only when a specific object is accessed by key. + + This is important for performance when dealing with clusters + that have thousands of objects. + """ + cluster = Cluster(load_strategy=LoadStrategy.LAZY) + + # Accessing collection should not load objects + tables = cluster.tables + assert len(tables._loaded_keys) == 0 + + # Accessing specific table should load only that table + table = tables["test_table"] + assert len(tables._loaded_keys) == 1 +``` + +## Common Testing Patterns + +### Test Object Lifecycle +```python +def test_object_lifecycle(cluster): + """Test complete object lifecycle.""" + # Create + obj = cluster.tables.new("lifecycle_test") + obj.create() + assert "lifecycle_test" in cluster.tables + + # Read + obj = cluster.tables["lifecycle_test"] + assert obj.name == "lifecycle_test" + + # Update + obj.owner = "new_owner" + obj.alter() + obj.refresh() + assert obj.owner == "new_owner" + + # Delete + obj.drop() + cluster.tables.refresh() + assert "lifecycle_test" not in cluster.tables +``` + +### Test Error Recovery +```python +def test_error_recovery(cluster): + """Test system recovers from errors.""" + # Cause error + with pytest.raises(PostgresError): + cluster.execute("INVALID SQL") + + # Verify system still works + result = cluster.execute("SELECT 1") + assert result == [(1,)] +``` diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..e59860c --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,85 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [0.3.0] - 2026-02-06 + +### Breaking Changes + +- **Python 3.13 Required**: Minimum Python version upgraded from 3.9 to 3.13 + - Python 3.9, 3.10, 3.11, and 3.12 are no longer supported + - This change enables the use of modern Python features and performance improvements + - See [MIGRATION_GUIDE.md](MIGRATION_GUIDE.md) for upgrade instructions + +### Added + +- Support for Python 3.13 features and performance improvements +- Modern type annotation syntax using PEP 604 (union operator `|`) and PEP 585 (built-in generics) +- Enhanced type safety with refined type hints throughout the codebase +- Improved developer experience with better error messages and REPL features from Python 3.13 + +### Changed + +- **Type Annotations Modernized**: All type annotations now use modern Python 3.10+ syntax + - `Union[X, Y]` replaced with `X | Y` syntax + - `Optional[X]` replaced with `X | None` syntax + - `List`, `Dict`, `Set`, `Tuple` from typing module replaced with built-in `list`, `dict`, `set`, `tuple` + - Removed legacy typing imports (List, Dict, Optional, Union) +- **Dependency Versions Updated**: All dependencies locked to latest stable releases + - `packaging` updated to >=26.0 (from >=21.3) + - `psycopg2` updated to >=2.9.11,<3 (from >=2.9.5,<3) + - `pytest` updated to >=9.0.0 (from >=7.1.1) + - `pytest-cov` updated to >=7.0.0 (from >=3.0.0) + - `pytest-mock` updated to >=3.15.0 (from >=3.7.0) + - `pytest-asyncio` updated to >=1.3.0 (from >=0.21.0) + - `docker` updated to >=7.1.0 (from >=6.0.0) + - `ruff` updated to >=0.15.0 (from >=0.1.0) +- **Code Quality Improvements**: + - Removed UP006 and UP035 from ruff ignore list (legacy type annotation rules) + - Refined type: ignore comments with specific error codes + - Minimized type suppressions where possible + +### Removed + +- Support for Python 3.9, 3.10, 3.11, and 3.12 +- Legacy typing imports (List, Dict, Set, Tuple, Optional, Union) from codebase + +### Performance + +- **5-15% performance improvement** from Python 3.13 optimizations +- **Additional benefits** from cumulative Python 3.11 and 3.12 performance enhancements: + - Python 3.11 introduced 10-60% performance improvements through optimizations + - Python 3.12 continued performance enhancements + - Python 3.13 adds experimental JIT compiler (PEP 744) for up to 30% speedups in computation-heavy tasks +- **7% smaller memory footprint** compared to Python 3.12 +- Faster test execution times due to interpreter improvements + +### Documentation + +- Updated README.md to specify Python 3.13 requirement +- Updated CONTRIBUTING.md with Python 3.13 development setup instructions +- Added migration guide for users upgrading from 0.2.x +- Enhanced code comments for modern Python features + +### Internal + +- Updated all configuration files to Python 3.13: + - pyproject.toml requires-python and classifiers + - docs/pyproject.toml requires-python + - .readthedocs.yaml Python version + - .github/actions/test/action.yaml default Python version + - Dockerfile base image to Python 3.13-bookworm +- Updated ruff target-version to py313 +- All 267 tests (164 unit + 103 functional) pass with Python 3.13 +- Type checking passes with zero errors +- Linting passes with zero errors + +--- + +## [0.2.0-dev] - Previous Development Version + +Previous development version supporting Python 3.9+. + diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2d54cba..022b20b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,6 +2,10 @@ This module is very much work-in-progress, and if you see a potential improvement, feel free to submit your code! +## Requirements + +**Python 3.13 or higher** is required for development. + ## Support questions Use one of the following options: @@ -24,14 +28,17 @@ Baseline for all contributions: * Attributes: for public classes * Returns: return type and description * Example: how to use and expected output -* Code formatter: `black`. All submitted code should be formatted with `black` using the settings in `pyproject.toml` -* Typing validations: `mypy`. The code should have necessary typing hints for any code in `src/pgmob`. +* Code formatter: `ruff format`. All submitted code should be formatted with Ruff using the settings in `pyproject.toml` +* Linter: `ruff check`. Code should pass all Ruff linting checks. +* Type checker: `ty check`. The code should have necessary typing hints for any code in `src/pgmob`. * Lists, Dicts or any Generic types should specify the member type in all cases. * Use `Union` or a parent class when multiple types are involved. * Try to avoid using `Any` at all costs. * VSCode recommended plugins: * Python * Pylance + * Ruff + * ty * Dev Containers ## Tests @@ -54,9 +61,10 @@ Use the following guidelines when writing tests: ### Executing the tests ```shell -$ poetry install -$ cd src -$ pytest + +# Install dependencies and run tests +$ uv sync --all-extras +$ uv run pytest ``` ## Building the docs @@ -65,8 +73,8 @@ Build the docs in the docs directory using Sphinx. ```shell $ cd docs -$ poetry install -$ poetry run make html +$ uv sync +$ uv run make html ``` Open _build/html/index.html in your browser to view the docs. diff --git a/Dockerfile b/Dockerfile index 0302b33..8c1f6e3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,16 +1,18 @@ -ARG PYTHON_VERSION=3.10-bullseye +ARG PYTHON_VERSION=3.13-bookworm FROM python:$PYTHON_VERSION -ARG POETRY_VERSION=1.3.2 -ARG POETRY_EXTRAS=psycopg2 +ARG UV_EXTRAS=psycopg2-binary WORKDIR /opt/pgmob -RUN pip install poetry==$POETRY_VERSION -RUN poetry config virtualenvs.create false +# Install UV +RUN curl -LsSf https://astral.sh/uv/install.sh | sh +ENV PATH="/root/.local/bin:$PATH" +# Copy project files COPY ./src ./src/ -COPY poetry.lock . +COPY uv.lock . COPY pyproject.toml . COPY *.md ./ -RUN poetry install -E $POETRY_EXTRAS +# Install dependencies with UV +RUN uv sync --extra $UV_EXTRAS --extra dev --no-dev diff --git a/README.md b/README.md index bfa0d84..978c861 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,10 @@ With PGMob, you can: * Script and export your database objects on the fly +## Requirements + +PGMob requires **Python 3.13 or higher**. + ## Installing PGMob requires an adapter to talk to PostgreSQL, which it can detect automatically. Currently supported adapters: @@ -32,7 +36,53 @@ $ pip install -U pgmob To include the adapter, use pip extras feature: ```shell -$ pip install -U pgmob[psycopg2] +$ pip install -U pgmob[psycopg2-binary] +``` + +## Development + +PGMob uses [UV](https://docs.astral.sh/uv/) for dependency management, [Ruff](https://docs.astral.sh/ruff/) for code formatting and linting, and [Ty](https://docs.astral.sh/ty/) for type checking. + +### Setup Development Environment + +```shell +# Install UV (if not already installed) +$ curl -LsSf https://astral.sh/uv/install.sh | sh + +# Clone the repository +$ git clone https://github.com/dataplat/pgmob.git +$ cd pgmob + +# Install dependencies with all extras +$ uv sync --all-extras +``` + +### Running Tests + +```shell +# Run all tests +$ uv run pytest + +# Run with coverage +$ uv run pytest --cov=pgmob --cov-report=html + +# Run specific test markers +$ uv run pytest -m unit +$ uv run pytest -m integration +``` + +### Code Quality + +```shell +# Format code with Ruff +$ uv run ruff format . + +# Lint code with Ruff +$ uv run ruff check --fix . + +# Type checking with ty +$ uvx ty +$ uv run ty check src/pgmob ``` ## Documentation diff --git a/docs/conf.py b/docs/conf.py index f600f6e..785f960 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -15,7 +15,6 @@ sys.path.insert(0, os.path.abspath("../src")) -import pgmob # -- Project information ----------------------------------------------------- diff --git a/docs/poetry.lock b/docs/poetry.lock deleted file mode 100644 index 4147790..0000000 --- a/docs/poetry.lock +++ /dev/null @@ -1,629 +0,0 @@ -# This file is automatically @generated by Poetry and should not be changed by hand. - -[[package]] -name = "alabaster" -version = "0.7.13" -description = "A configurable sidebar-enabled Sphinx theme" -category = "main" -optional = false -python-versions = ">=3.6" -files = [ - {file = "alabaster-0.7.13-py3-none-any.whl", hash = "sha256:1ee19aca801bbabb5ba3f5f258e4422dfa86f82f3e9cefb0859b283cdd7f62a3"}, - {file = "alabaster-0.7.13.tar.gz", hash = "sha256:a27a4a084d5e690e16e01e03ad2b2e552c61a65469419b907243193de1a84ae2"}, -] - -[[package]] -name = "babel" -version = "2.11.0" -description = "Internationalization utilities" -category = "main" -optional = false -python-versions = ">=3.6" -files = [ - {file = "Babel-2.11.0-py3-none-any.whl", hash = "sha256:1ad3eca1c885218f6dce2ab67291178944f810a10a9b5f3cb8382a5a232b64fe"}, - {file = "Babel-2.11.0.tar.gz", hash = "sha256:5ef4b3226b0180dedded4229651c8b0e1a3a6a2837d45a073272f313e4cf97f6"}, -] - -[package.dependencies] -pytz = ">=2015.7" - -[[package]] -name = "certifi" -version = "2022.12.7" -description = "Python package for providing Mozilla's CA Bundle." -category = "main" -optional = false -python-versions = ">=3.6" -files = [ - {file = "certifi-2022.12.7-py3-none-any.whl", hash = "sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18"}, - {file = "certifi-2022.12.7.tar.gz", hash = "sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3"}, -] - -[[package]] -name = "charset-normalizer" -version = "3.0.1" -description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." -category = "main" -optional = false -python-versions = "*" -files = [ - {file = "charset-normalizer-3.0.1.tar.gz", hash = "sha256:ebea339af930f8ca5d7a699b921106c6e29c617fe9606fa7baa043c1cdae326f"}, - {file = "charset_normalizer-3.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:88600c72ef7587fe1708fd242b385b6ed4b8904976d5da0893e31df8b3480cb6"}, - {file = "charset_normalizer-3.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c75ffc45f25324e68ab238cb4b5c0a38cd1c3d7f1fb1f72b5541de469e2247db"}, - {file = "charset_normalizer-3.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:db72b07027db150f468fbada4d85b3b2729a3db39178abf5c543b784c1254539"}, - {file = "charset_normalizer-3.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62595ab75873d50d57323a91dd03e6966eb79c41fa834b7a1661ed043b2d404d"}, - {file = "charset_normalizer-3.0.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ff6f3db31555657f3163b15a6b7c6938d08df7adbfc9dd13d9d19edad678f1e8"}, - {file = "charset_normalizer-3.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:772b87914ff1152b92a197ef4ea40efe27a378606c39446ded52c8f80f79702e"}, - {file = "charset_normalizer-3.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70990b9c51340e4044cfc394a81f614f3f90d41397104d226f21e66de668730d"}, - {file = "charset_normalizer-3.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:292d5e8ba896bbfd6334b096e34bffb56161c81408d6d036a7dfa6929cff8783"}, - {file = "charset_normalizer-3.0.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:2edb64ee7bf1ed524a1da60cdcd2e1f6e2b4f66ef7c077680739f1641f62f555"}, - {file = "charset_normalizer-3.0.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:31a9ddf4718d10ae04d9b18801bd776693487cbb57d74cc3458a7673f6f34639"}, - {file = "charset_normalizer-3.0.1-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:44ba614de5361b3e5278e1241fda3dc1838deed864b50a10d7ce92983797fa76"}, - {file = "charset_normalizer-3.0.1-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:12db3b2c533c23ab812c2b25934f60383361f8a376ae272665f8e48b88e8e1c6"}, - {file = "charset_normalizer-3.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c512accbd6ff0270939b9ac214b84fb5ada5f0409c44298361b2f5e13f9aed9e"}, - {file = "charset_normalizer-3.0.1-cp310-cp310-win32.whl", hash = "sha256:502218f52498a36d6bf5ea77081844017bf7982cdbe521ad85e64cabee1b608b"}, - {file = "charset_normalizer-3.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:601f36512f9e28f029d9481bdaf8e89e5148ac5d89cffd3b05cd533eeb423b59"}, - {file = "charset_normalizer-3.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0298eafff88c99982a4cf66ba2efa1128e4ddaca0b05eec4c456bbc7db691d8d"}, - {file = "charset_normalizer-3.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a8d0fc946c784ff7f7c3742310cc8a57c5c6dc31631269876a88b809dbeff3d3"}, - {file = "charset_normalizer-3.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:87701167f2a5c930b403e9756fab1d31d4d4da52856143b609e30a1ce7160f3c"}, - {file = "charset_normalizer-3.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:14e76c0f23218b8f46c4d87018ca2e441535aed3632ca134b10239dfb6dadd6b"}, - {file = "charset_normalizer-3.0.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0c0a590235ccd933d9892c627dec5bc7511ce6ad6c1011fdf5b11363022746c1"}, - {file = "charset_normalizer-3.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c7fe7afa480e3e82eed58e0ca89f751cd14d767638e2550c77a92a9e749c317"}, - {file = "charset_normalizer-3.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:79909e27e8e4fcc9db4addea88aa63f6423ebb171db091fb4373e3312cb6d603"}, - {file = "charset_normalizer-3.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8ac7b6a045b814cf0c47f3623d21ebd88b3e8cf216a14790b455ea7ff0135d18"}, - {file = "charset_normalizer-3.0.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:72966d1b297c741541ca8cf1223ff262a6febe52481af742036a0b296e35fa5a"}, - {file = "charset_normalizer-3.0.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:f9d0c5c045a3ca9bedfc35dca8526798eb91a07aa7a2c0fee134c6c6f321cbd7"}, - {file = "charset_normalizer-3.0.1-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:5995f0164fa7df59db4746112fec3f49c461dd6b31b841873443bdb077c13cfc"}, - {file = "charset_normalizer-3.0.1-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4a8fcf28c05c1f6d7e177a9a46a1c52798bfe2ad80681d275b10dcf317deaf0b"}, - {file = "charset_normalizer-3.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:761e8904c07ad053d285670f36dd94e1b6ab7f16ce62b9805c475b7aa1cffde6"}, - {file = "charset_normalizer-3.0.1-cp311-cp311-win32.whl", hash = "sha256:71140351489970dfe5e60fc621ada3e0f41104a5eddaca47a7acb3c1b851d6d3"}, - {file = "charset_normalizer-3.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:9ab77acb98eba3fd2a85cd160851816bfce6871d944d885febf012713f06659c"}, - {file = "charset_normalizer-3.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:84c3990934bae40ea69a82034912ffe5a62c60bbf6ec5bc9691419641d7d5c9a"}, - {file = "charset_normalizer-3.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:74292fc76c905c0ef095fe11e188a32ebd03bc38f3f3e9bcb85e4e6db177b7ea"}, - {file = "charset_normalizer-3.0.1-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c95a03c79bbe30eec3ec2b7f076074f4281526724c8685a42872974ef4d36b72"}, - {file = "charset_normalizer-3.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f4c39b0e3eac288fedc2b43055cfc2ca7a60362d0e5e87a637beac5d801ef478"}, - {file = "charset_normalizer-3.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:df2c707231459e8a4028eabcd3cfc827befd635b3ef72eada84ab13b52e1574d"}, - {file = "charset_normalizer-3.0.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:93ad6d87ac18e2a90b0fe89df7c65263b9a99a0eb98f0a3d2e079f12a0735837"}, - {file = "charset_normalizer-3.0.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:59e5686dd847347e55dffcc191a96622f016bc0ad89105e24c14e0d6305acbc6"}, - {file = "charset_normalizer-3.0.1-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:cd6056167405314a4dc3c173943f11249fa0f1b204f8b51ed4bde1a9cd1834dc"}, - {file = "charset_normalizer-3.0.1-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:083c8d17153ecb403e5e1eb76a7ef4babfc2c48d58899c98fcaa04833e7a2f9a"}, - {file = "charset_normalizer-3.0.1-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:f5057856d21e7586765171eac8b9fc3f7d44ef39425f85dbcccb13b3ebea806c"}, - {file = "charset_normalizer-3.0.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:7eb33a30d75562222b64f569c642ff3dc6689e09adda43a082208397f016c39a"}, - {file = "charset_normalizer-3.0.1-cp36-cp36m-win32.whl", hash = "sha256:95dea361dd73757c6f1c0a1480ac499952c16ac83f7f5f4f84f0658a01b8ef41"}, - {file = "charset_normalizer-3.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:eaa379fcd227ca235d04152ca6704c7cb55564116f8bc52545ff357628e10602"}, - {file = "charset_normalizer-3.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:3e45867f1f2ab0711d60c6c71746ac53537f1684baa699f4f668d4c6f6ce8e14"}, - {file = "charset_normalizer-3.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cadaeaba78750d58d3cc6ac4d1fd867da6fc73c88156b7a3212a3cd4819d679d"}, - {file = "charset_normalizer-3.0.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:911d8a40b2bef5b8bbae2e36a0b103f142ac53557ab421dc16ac4aafee6f53dc"}, - {file = "charset_normalizer-3.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:503e65837c71b875ecdd733877d852adbc465bd82c768a067badd953bf1bc5a3"}, - {file = "charset_normalizer-3.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a60332922359f920193b1d4826953c507a877b523b2395ad7bc716ddd386d866"}, - {file = "charset_normalizer-3.0.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:16a8663d6e281208d78806dbe14ee9903715361cf81f6d4309944e4d1e59ac5b"}, - {file = "charset_normalizer-3.0.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:a16418ecf1329f71df119e8a65f3aa68004a3f9383821edcb20f0702934d8087"}, - {file = "charset_normalizer-3.0.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:9d9153257a3f70d5f69edf2325357251ed20f772b12e593f3b3377b5f78e7ef8"}, - {file = "charset_normalizer-3.0.1-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:02a51034802cbf38db3f89c66fb5d2ec57e6fe7ef2f4a44d070a593c3688667b"}, - {file = "charset_normalizer-3.0.1-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:2e396d70bc4ef5325b72b593a72c8979999aa52fb8bcf03f701c1b03e1166918"}, - {file = "charset_normalizer-3.0.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:11b53acf2411c3b09e6af37e4b9005cba376c872503c8f28218c7243582df45d"}, - {file = "charset_normalizer-3.0.1-cp37-cp37m-win32.whl", hash = "sha256:0bf2dae5291758b6f84cf923bfaa285632816007db0330002fa1de38bfcb7154"}, - {file = "charset_normalizer-3.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:2c03cc56021a4bd59be889c2b9257dae13bf55041a3372d3295416f86b295fb5"}, - {file = "charset_normalizer-3.0.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:024e606be3ed92216e2b6952ed859d86b4cfa52cd5bc5f050e7dc28f9b43ec42"}, - {file = "charset_normalizer-3.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:4b0d02d7102dd0f997580b51edc4cebcf2ab6397a7edf89f1c73b586c614272c"}, - {file = "charset_normalizer-3.0.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:358a7c4cb8ba9b46c453b1dd8d9e431452d5249072e4f56cfda3149f6ab1405e"}, - {file = "charset_normalizer-3.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:81d6741ab457d14fdedc215516665050f3822d3e56508921cc7239f8c8e66a58"}, - {file = "charset_normalizer-3.0.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8b8af03d2e37866d023ad0ddea594edefc31e827fee64f8de5611a1dbc373174"}, - {file = "charset_normalizer-3.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9cf4e8ad252f7c38dd1f676b46514f92dc0ebeb0db5552f5f403509705e24753"}, - {file = "charset_normalizer-3.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e696f0dd336161fca9adbb846875d40752e6eba585843c768935ba5c9960722b"}, - {file = "charset_normalizer-3.0.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c22d3fe05ce11d3671297dc8973267daa0f938b93ec716e12e0f6dee81591dc1"}, - {file = "charset_normalizer-3.0.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:109487860ef6a328f3eec66f2bf78b0b72400280d8f8ea05f69c51644ba6521a"}, - {file = "charset_normalizer-3.0.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:37f8febc8ec50c14f3ec9637505f28e58d4f66752207ea177c1d67df25da5aed"}, - {file = "charset_normalizer-3.0.1-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:f97e83fa6c25693c7a35de154681fcc257c1c41b38beb0304b9c4d2d9e164479"}, - {file = "charset_normalizer-3.0.1-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:a152f5f33d64a6be73f1d30c9cc82dfc73cec6477ec268e7c6e4c7d23c2d2291"}, - {file = "charset_normalizer-3.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:39049da0ffb96c8cbb65cbf5c5f3ca3168990adf3551bd1dee10c48fce8ae820"}, - {file = "charset_normalizer-3.0.1-cp38-cp38-win32.whl", hash = "sha256:4457ea6774b5611f4bed5eaa5df55f70abde42364d498c5134b7ef4c6958e20e"}, - {file = "charset_normalizer-3.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:e62164b50f84e20601c1ff8eb55620d2ad25fb81b59e3cd776a1902527a788af"}, - {file = "charset_normalizer-3.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8eade758719add78ec36dc13201483f8e9b5d940329285edcd5f70c0a9edbd7f"}, - {file = "charset_normalizer-3.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8499ca8f4502af841f68135133d8258f7b32a53a1d594aa98cc52013fff55678"}, - {file = "charset_normalizer-3.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3fc1c4a2ffd64890aebdb3f97e1278b0cc72579a08ca4de8cd2c04799a3a22be"}, - {file = "charset_normalizer-3.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:00d3ffdaafe92a5dc603cb9bd5111aaa36dfa187c8285c543be562e61b755f6b"}, - {file = "charset_normalizer-3.0.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c2ac1b08635a8cd4e0cbeaf6f5e922085908d48eb05d44c5ae9eabab148512ca"}, - {file = "charset_normalizer-3.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f6f45710b4459401609ebebdbcfb34515da4fc2aa886f95107f556ac69a9147e"}, - {file = "charset_normalizer-3.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ae1de54a77dc0d6d5fcf623290af4266412a7c4be0b1ff7444394f03f5c54e3"}, - {file = "charset_normalizer-3.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3b590df687e3c5ee0deef9fc8c547d81986d9a1b56073d82de008744452d6541"}, - {file = "charset_normalizer-3.0.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ab5de034a886f616a5668aa5d098af2b5385ed70142090e2a31bcbd0af0fdb3d"}, - {file = "charset_normalizer-3.0.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9cb3032517f1627cc012dbc80a8ec976ae76d93ea2b5feaa9d2a5b8882597579"}, - {file = "charset_normalizer-3.0.1-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:608862a7bf6957f2333fc54ab4399e405baad0163dc9f8d99cb236816db169d4"}, - {file = "charset_normalizer-3.0.1-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:0f438ae3532723fb6ead77e7c604be7c8374094ef4ee2c5e03a3a17f1fca256c"}, - {file = "charset_normalizer-3.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:356541bf4381fa35856dafa6a965916e54bed415ad8a24ee6de6e37deccf2786"}, - {file = "charset_normalizer-3.0.1-cp39-cp39-win32.whl", hash = "sha256:39cf9ed17fe3b1bc81f33c9ceb6ce67683ee7526e65fde1447c772afc54a1bb8"}, - {file = "charset_normalizer-3.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:0a11e971ed097d24c534c037d298ad32c6ce81a45736d31e0ff0ad37ab437d59"}, - {file = "charset_normalizer-3.0.1-py3-none-any.whl", hash = "sha256:7e189e2e1d3ed2f4aebabd2d5b0f931e883676e51c7624826e0a4e5fe8a0bf24"}, -] - -[[package]] -name = "colorama" -version = "0.4.6" -description = "Cross-platform colored terminal text." -category = "main" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" -files = [ - {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, - {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, -] - -[[package]] -name = "docutils" -version = "0.19" -description = "Docutils -- Python Documentation Utilities" -category = "main" -optional = false -python-versions = ">=3.7" -files = [ - {file = "docutils-0.19-py3-none-any.whl", hash = "sha256:5e1de4d849fee02c63b040a4a3fd567f4ab104defd8a5511fbbc24a8a017efbc"}, - {file = "docutils-0.19.tar.gz", hash = "sha256:33995a6753c30b7f577febfc2c50411fec6aac7f7ffeb7c4cfe5991072dcf9e6"}, -] - -[[package]] -name = "idna" -version = "3.4" -description = "Internationalized Domain Names in Applications (IDNA)" -category = "main" -optional = false -python-versions = ">=3.5" -files = [ - {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, - {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, -] - -[[package]] -name = "imagesize" -version = "1.4.1" -description = "Getting image size from png/jpeg/jpeg2000/gif file" -category = "main" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -files = [ - {file = "imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b"}, - {file = "imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a"}, -] - -[[package]] -name = "importlib-metadata" -version = "6.0.0" -description = "Read metadata from Python packages" -category = "main" -optional = false -python-versions = ">=3.7" -files = [ - {file = "importlib_metadata-6.0.0-py3-none-any.whl", hash = "sha256:7efb448ec9a5e313a57655d35aa54cd3e01b7e1fbcf72dce1bf06119420f5bad"}, - {file = "importlib_metadata-6.0.0.tar.gz", hash = "sha256:e354bedeb60efa6affdcc8ae121b73544a7aa74156d047311948f6d711cd378d"}, -] - -[package.dependencies] -zipp = ">=0.5" - -[package.extras] -docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -perf = ["ipython"] -testing = ["flake8 (<5)", "flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)"] - -[[package]] -name = "jinja2" -version = "3.1.2" -description = "A very fast and expressive template engine." -category = "main" -optional = false -python-versions = ">=3.7" -files = [ - {file = "Jinja2-3.1.2-py3-none-any.whl", hash = "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"}, - {file = "Jinja2-3.1.2.tar.gz", hash = "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852"}, -] - -[package.dependencies] -MarkupSafe = ">=2.0" - -[package.extras] -i18n = ["Babel (>=2.7)"] - -[[package]] -name = "markupsafe" -version = "2.1.2" -description = "Safely add untrusted strings to HTML/XML markup." -category = "main" -optional = false -python-versions = ">=3.7" -files = [ - {file = "MarkupSafe-2.1.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:665a36ae6f8f20a4676b53224e33d456a6f5a72657d9c83c2aa00765072f31f7"}, - {file = "MarkupSafe-2.1.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:340bea174e9761308703ae988e982005aedf427de816d1afe98147668cc03036"}, - {file = "MarkupSafe-2.1.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22152d00bf4a9c7c83960521fc558f55a1adbc0631fbb00a9471e097b19d72e1"}, - {file = "MarkupSafe-2.1.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28057e985dace2f478e042eaa15606c7efccb700797660629da387eb289b9323"}, - {file = "MarkupSafe-2.1.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca244fa73f50a800cf8c3ebf7fd93149ec37f5cb9596aa8873ae2c1d23498601"}, - {file = "MarkupSafe-2.1.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d9d971ec1e79906046aa3ca266de79eac42f1dbf3612a05dc9368125952bd1a1"}, - {file = "MarkupSafe-2.1.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:7e007132af78ea9df29495dbf7b5824cb71648d7133cf7848a2a5dd00d36f9ff"}, - {file = "MarkupSafe-2.1.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7313ce6a199651c4ed9d7e4cfb4aa56fe923b1adf9af3b420ee14e6d9a73df65"}, - {file = "MarkupSafe-2.1.2-cp310-cp310-win32.whl", hash = "sha256:c4a549890a45f57f1ebf99c067a4ad0cb423a05544accaf2b065246827ed9603"}, - {file = "MarkupSafe-2.1.2-cp310-cp310-win_amd64.whl", hash = "sha256:835fb5e38fd89328e9c81067fd642b3593c33e1e17e2fdbf77f5676abb14a156"}, - {file = "MarkupSafe-2.1.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2ec4f2d48ae59bbb9d1f9d7efb9236ab81429a764dedca114f5fdabbc3788013"}, - {file = "MarkupSafe-2.1.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:608e7073dfa9e38a85d38474c082d4281f4ce276ac0010224eaba11e929dd53a"}, - {file = "MarkupSafe-2.1.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:65608c35bfb8a76763f37036547f7adfd09270fbdbf96608be2bead319728fcd"}, - {file = "MarkupSafe-2.1.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2bfb563d0211ce16b63c7cb9395d2c682a23187f54c3d79bfec33e6705473c6"}, - {file = "MarkupSafe-2.1.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:da25303d91526aac3672ee6d49a2f3db2d9502a4a60b55519feb1a4c7714e07d"}, - {file = "MarkupSafe-2.1.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:9cad97ab29dfc3f0249b483412c85c8ef4766d96cdf9dcf5a1e3caa3f3661cf1"}, - {file = "MarkupSafe-2.1.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:085fd3201e7b12809f9e6e9bc1e5c96a368c8523fad5afb02afe3c051ae4afcc"}, - {file = "MarkupSafe-2.1.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1bea30e9bf331f3fef67e0a3877b2288593c98a21ccb2cf29b74c581a4eb3af0"}, - {file = "MarkupSafe-2.1.2-cp311-cp311-win32.whl", hash = "sha256:7df70907e00c970c60b9ef2938d894a9381f38e6b9db73c5be35e59d92e06625"}, - {file = "MarkupSafe-2.1.2-cp311-cp311-win_amd64.whl", hash = "sha256:e55e40ff0cc8cc5c07996915ad367fa47da6b3fc091fdadca7f5403239c5fec3"}, - {file = "MarkupSafe-2.1.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a6e40afa7f45939ca356f348c8e23048e02cb109ced1eb8420961b2f40fb373a"}, - {file = "MarkupSafe-2.1.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf877ab4ed6e302ec1d04952ca358b381a882fbd9d1b07cccbfd61783561f98a"}, - {file = "MarkupSafe-2.1.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63ba06c9941e46fa389d389644e2d8225e0e3e5ebcc4ff1ea8506dce646f8c8a"}, - {file = "MarkupSafe-2.1.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f1cd098434e83e656abf198f103a8207a8187c0fc110306691a2e94a78d0abb2"}, - {file = "MarkupSafe-2.1.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:55f44b440d491028addb3b88f72207d71eeebfb7b5dbf0643f7c023ae1fba619"}, - {file = "MarkupSafe-2.1.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:a6f2fcca746e8d5910e18782f976489939d54a91f9411c32051b4aab2bd7c513"}, - {file = "MarkupSafe-2.1.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:0b462104ba25f1ac006fdab8b6a01ebbfbce9ed37fd37fd4acd70c67c973e460"}, - {file = "MarkupSafe-2.1.2-cp37-cp37m-win32.whl", hash = "sha256:7668b52e102d0ed87cb082380a7e2e1e78737ddecdde129acadb0eccc5423859"}, - {file = "MarkupSafe-2.1.2-cp37-cp37m-win_amd64.whl", hash = "sha256:6d6607f98fcf17e534162f0709aaad3ab7a96032723d8ac8750ffe17ae5a0666"}, - {file = "MarkupSafe-2.1.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:a806db027852538d2ad7555b203300173dd1b77ba116de92da9afbc3a3be3eed"}, - {file = "MarkupSafe-2.1.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a4abaec6ca3ad8660690236d11bfe28dfd707778e2442b45addd2f086d6ef094"}, - {file = "MarkupSafe-2.1.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f03a532d7dee1bed20bc4884194a16160a2de9ffc6354b3878ec9682bb623c54"}, - {file = "MarkupSafe-2.1.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4cf06cdc1dda95223e9d2d3c58d3b178aa5dacb35ee7e3bbac10e4e1faacb419"}, - {file = "MarkupSafe-2.1.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:22731d79ed2eb25059ae3df1dfc9cb1546691cc41f4e3130fe6bfbc3ecbbecfa"}, - {file = "MarkupSafe-2.1.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:f8ffb705ffcf5ddd0e80b65ddf7bed7ee4f5a441ea7d3419e861a12eaf41af58"}, - {file = "MarkupSafe-2.1.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8db032bf0ce9022a8e41a22598eefc802314e81b879ae093f36ce9ddf39ab1ba"}, - {file = "MarkupSafe-2.1.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2298c859cfc5463f1b64bd55cb3e602528db6fa0f3cfd568d3605c50678f8f03"}, - {file = "MarkupSafe-2.1.2-cp38-cp38-win32.whl", hash = "sha256:50c42830a633fa0cf9e7d27664637532791bfc31c731a87b202d2d8ac40c3ea2"}, - {file = "MarkupSafe-2.1.2-cp38-cp38-win_amd64.whl", hash = "sha256:bb06feb762bade6bf3c8b844462274db0c76acc95c52abe8dbed28ae3d44a147"}, - {file = "MarkupSafe-2.1.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:99625a92da8229df6d44335e6fcc558a5037dd0a760e11d84be2260e6f37002f"}, - {file = "MarkupSafe-2.1.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8bca7e26c1dd751236cfb0c6c72d4ad61d986e9a41bbf76cb445f69488b2a2bd"}, - {file = "MarkupSafe-2.1.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40627dcf047dadb22cd25ea7ecfe9cbf3bbbad0482ee5920b582f3809c97654f"}, - {file = "MarkupSafe-2.1.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40dfd3fefbef579ee058f139733ac336312663c6706d1163b82b3003fb1925c4"}, - {file = "MarkupSafe-2.1.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:090376d812fb6ac5f171e5938e82e7f2d7adc2b629101cec0db8b267815c85e2"}, - {file = "MarkupSafe-2.1.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:2e7821bffe00aa6bd07a23913b7f4e01328c3d5cc0b40b36c0bd81d362faeb65"}, - {file = "MarkupSafe-2.1.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:c0a33bc9f02c2b17c3ea382f91b4db0e6cde90b63b296422a939886a7a80de1c"}, - {file = "MarkupSafe-2.1.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:b8526c6d437855442cdd3d87eede9c425c4445ea011ca38d937db299382e6fa3"}, - {file = "MarkupSafe-2.1.2-cp39-cp39-win32.whl", hash = "sha256:137678c63c977754abe9086a3ec011e8fd985ab90631145dfb9294ad09c102a7"}, - {file = "MarkupSafe-2.1.2-cp39-cp39-win_amd64.whl", hash = "sha256:0576fe974b40a400449768941d5d0858cc624e3249dfd1e0c33674e5c7ca7aed"}, - {file = "MarkupSafe-2.1.2.tar.gz", hash = "sha256:abcabc8c2b26036d62d4c746381a6f7cf60aafcc653198ad678306986b09450d"}, -] - -[[package]] -name = "packaging" -version = "23.0" -description = "Core utilities for Python packages" -category = "main" -optional = false -python-versions = ">=3.7" -files = [ - {file = "packaging-23.0-py3-none-any.whl", hash = "sha256:714ac14496c3e68c99c29b00845f7a2b85f3bb6f1078fd9f72fd20f0570002b2"}, - {file = "packaging-23.0.tar.gz", hash = "sha256:b6ad297f8907de0fa2fe1ccbd26fdaf387f5f47c7275fedf8cce89f99446cf97"}, -] - -[[package]] -name = "pgmob" -version = "0.1.3a0" -description = "Postgres Managed Objects - a Postgres database management interface" -category = "main" -optional = false -python-versions = ">=3.9,<4" -files = [] -develop = false - -[package.dependencies] -packaging = ">=21.3" -psycopg2-binary = {version = ">=2.8.5,<3", optional = true} - -[package.extras] -psycopg2 = ["psycopg2 (>=2.8.5,<3)"] -psycopg2-binary = ["psycopg2-binary (>=2.8.5,<3)"] - -[package.source] -type = "directory" -url = ".." - -[[package]] -name = "psycopg2-binary" -version = "2.9.5" -description = "psycopg2 - Python-PostgreSQL Database Adapter" -category = "main" -optional = false -python-versions = ">=3.6" -files = [ - {file = "psycopg2-binary-2.9.5.tar.gz", hash = "sha256:33e632d0885b95a8b97165899006c40e9ecdc634a529dca7b991eb7de4ece41c"}, - {file = "psycopg2_binary-2.9.5-cp310-cp310-macosx_10_15_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:0775d6252ccb22b15da3b5d7adbbf8cfe284916b14b6dc0ff503a23edb01ee85"}, - {file = "psycopg2_binary-2.9.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2ec46ed947801652c9643e0b1dc334cfb2781232e375ba97312c2fc256597632"}, - {file = "psycopg2_binary-2.9.5-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3520d7af1ebc838cc6084a3281145d5cd5bdd43fdef139e6db5af01b92596cb7"}, - {file = "psycopg2_binary-2.9.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5cbc554ba47ecca8cd3396ddaca85e1ecfe3e48dd57dc5e415e59551affe568e"}, - {file = "psycopg2_binary-2.9.5-cp310-cp310-manylinux_2_24_aarch64.whl", hash = "sha256:5d28ecdf191db558d0c07d0f16524ee9d67896edf2b7990eea800abeb23ebd61"}, - {file = "psycopg2_binary-2.9.5-cp310-cp310-manylinux_2_24_ppc64le.whl", hash = "sha256:b9c33d4aef08dfecbd1736ceab8b7b3c4358bf10a0121483e5cd60d3d308cc64"}, - {file = "psycopg2_binary-2.9.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:05b3d479425e047c848b9782cd7aac9c6727ce23181eb9647baf64ffdfc3da41"}, - {file = "psycopg2_binary-2.9.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:1e491e6489a6cb1d079df8eaa15957c277fdedb102b6a68cfbf40c4994412fd0"}, - {file = "psycopg2_binary-2.9.5-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:9e32cedc389bcb76d9f24ea8a012b3cb8385ee362ea437e1d012ffaed106c17d"}, - {file = "psycopg2_binary-2.9.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:46850a640df62ae940e34a163f72e26aca1f88e2da79148e1862faaac985c302"}, - {file = "psycopg2_binary-2.9.5-cp310-cp310-win32.whl", hash = "sha256:3d790f84201c3698d1bfb404c917f36e40531577a6dda02e45ba29b64d539867"}, - {file = "psycopg2_binary-2.9.5-cp310-cp310-win_amd64.whl", hash = "sha256:1764546ffeaed4f9428707be61d68972eb5ede81239b46a45843e0071104d0dd"}, - {file = "psycopg2_binary-2.9.5-cp311-cp311-macosx_10_9_universal2.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:426c2ae999135d64e6a18849a7d1ad0e1bd007277e4a8f4752eaa40a96b550ff"}, - {file = "psycopg2_binary-2.9.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7cf1d44e710ca3a9ce952bda2855830fe9f9017ed6259e01fcd71ea6287565f5"}, - {file = "psycopg2_binary-2.9.5-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:024030b13bdcbd53d8a93891a2cf07719715724fc9fee40243f3bd78b4264b8f"}, - {file = "psycopg2_binary-2.9.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bcda1c84a1c533c528356da5490d464a139b6e84eb77cc0b432e38c5c6dd7882"}, - {file = "psycopg2_binary-2.9.5-cp311-cp311-manylinux_2_24_aarch64.whl", hash = "sha256:2ef892cabdccefe577088a79580301f09f2a713eb239f4f9f62b2b29cafb0577"}, - {file = "psycopg2_binary-2.9.5-cp311-cp311-manylinux_2_24_ppc64le.whl", hash = "sha256:af0516e1711995cb08dc19bbd05bec7dbdebf4185f68870595156718d237df3e"}, - {file = "psycopg2_binary-2.9.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e72c91bda9880f097c8aa3601a2c0de6c708763ba8128006151f496ca9065935"}, - {file = "psycopg2_binary-2.9.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:e67b3c26e9b6d37b370c83aa790bbc121775c57bfb096c2e77eacca25fd0233b"}, - {file = "psycopg2_binary-2.9.5-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:5fc447058d083b8c6ac076fc26b446d44f0145308465d745fba93a28c14c9e32"}, - {file = "psycopg2_binary-2.9.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d892bfa1d023c3781a3cab8dd5af76b626c483484d782e8bd047c180db590e4c"}, - {file = "psycopg2_binary-2.9.5-cp311-cp311-win32.whl", hash = "sha256:2abccab84d057723d2ca8f99ff7b619285d40da6814d50366f61f0fc385c3903"}, - {file = "psycopg2_binary-2.9.5-cp311-cp311-win_amd64.whl", hash = "sha256:bef7e3f9dc6f0c13afdd671008534be5744e0e682fb851584c8c3a025ec09720"}, - {file = "psycopg2_binary-2.9.5-cp36-cp36m-macosx_10_14_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:6e63814ec71db9bdb42905c925639f319c80e7909fb76c3b84edc79dadef8d60"}, - {file = "psycopg2_binary-2.9.5-cp36-cp36m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:212757ffcecb3e1a5338d4e6761bf9c04f750e7d027117e74aa3cd8a75bb6fbd"}, - {file = "psycopg2_binary-2.9.5-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f8a9bcab7b6db2e3dbf65b214dfc795b4c6b3bb3af922901b6a67f7cb47d5f8"}, - {file = "psycopg2_binary-2.9.5-cp36-cp36m-manylinux_2_24_aarch64.whl", hash = "sha256:56b2957a145f816726b109ee3d4e6822c23f919a7d91af5a94593723ed667835"}, - {file = "psycopg2_binary-2.9.5-cp36-cp36m-manylinux_2_24_ppc64le.whl", hash = "sha256:f95b8aca2703d6a30249f83f4fe6a9abf2e627aa892a5caaab2267d56be7ab69"}, - {file = "psycopg2_binary-2.9.5-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:70831e03bd53702c941da1a1ad36c17d825a24fbb26857b40913d58df82ec18b"}, - {file = "psycopg2_binary-2.9.5-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:dbc332beaf8492b5731229a881807cd7b91b50dbbbaf7fe2faf46942eda64a24"}, - {file = "psycopg2_binary-2.9.5-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:2d964eb24c8b021623df1c93c626671420c6efadbdb8655cb2bd5e0c6fa422ba"}, - {file = "psycopg2_binary-2.9.5-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:95076399ec3b27a8f7fa1cc9a83417b1c920d55cf7a97f718a94efbb96c7f503"}, - {file = "psycopg2_binary-2.9.5-cp36-cp36m-win32.whl", hash = "sha256:3fc33295cfccad697a97a76dec3f1e94ad848b7b163c3228c1636977966b51e2"}, - {file = "psycopg2_binary-2.9.5-cp36-cp36m-win_amd64.whl", hash = "sha256:02551647542f2bf89073d129c73c05a25c372fc0a49aa50e0de65c3c143d8bd0"}, - {file = "psycopg2_binary-2.9.5-cp37-cp37m-macosx_10_15_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:63e318dbe52709ed10d516a356f22a635e07a2e34c68145484ed96a19b0c4c68"}, - {file = "psycopg2_binary-2.9.5-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a7e518a0911c50f60313cb9e74a169a65b5d293770db4770ebf004245f24b5c5"}, - {file = "psycopg2_binary-2.9.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b9d38a4656e4e715d637abdf7296e98d6267df0cc0a8e9a016f8ba07e4aa3eeb"}, - {file = "psycopg2_binary-2.9.5-cp37-cp37m-manylinux_2_24_aarch64.whl", hash = "sha256:68d81a2fe184030aa0c5c11e518292e15d342a667184d91e30644c9d533e53e1"}, - {file = "psycopg2_binary-2.9.5-cp37-cp37m-manylinux_2_24_ppc64le.whl", hash = "sha256:7ee3095d02d6f38bd7d9a5358fcc9ea78fcdb7176921528dd709cc63f40184f5"}, - {file = "psycopg2_binary-2.9.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:46512486be6fbceef51d7660dec017394ba3e170299d1dc30928cbedebbf103a"}, - {file = "psycopg2_binary-2.9.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:b911dfb727e247340d36ae20c4b9259e4a64013ab9888ccb3cbba69b77fd9636"}, - {file = "psycopg2_binary-2.9.5-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:422e3d43b47ac20141bc84b3d342eead8d8099a62881a501e97d15f6addabfe9"}, - {file = "psycopg2_binary-2.9.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:c5682a45df7d9642eff590abc73157c887a68f016df0a8ad722dcc0f888f56d7"}, - {file = "psycopg2_binary-2.9.5-cp37-cp37m-win32.whl", hash = "sha256:b8104f709590fff72af801e916817560dbe1698028cd0afe5a52d75ceb1fce5f"}, - {file = "psycopg2_binary-2.9.5-cp37-cp37m-win_amd64.whl", hash = "sha256:7b3751857da3e224f5629400736a7b11e940b5da5f95fa631d86219a1beaafec"}, - {file = "psycopg2_binary-2.9.5-cp38-cp38-macosx_10_15_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:043a9fd45a03858ff72364b4b75090679bd875ee44df9c0613dc862ca6b98460"}, - {file = "psycopg2_binary-2.9.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:9ffdc51001136b699f9563b1c74cc1f8c07f66ef7219beb6417a4c8aaa896c28"}, - {file = "psycopg2_binary-2.9.5-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c15ba5982c177bc4b23a7940c7e4394197e2d6a424a2d282e7c236b66da6d896"}, - {file = "psycopg2_binary-2.9.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc85b3777068ed30aff8242be2813038a929f2084f69e43ef869daddae50f6ee"}, - {file = "psycopg2_binary-2.9.5-cp38-cp38-manylinux_2_24_aarch64.whl", hash = "sha256:215d6bf7e66732a514f47614f828d8c0aaac9a648c46a831955cb103473c7147"}, - {file = "psycopg2_binary-2.9.5-cp38-cp38-manylinux_2_24_ppc64le.whl", hash = "sha256:7d07f552d1e412f4b4e64ce386d4c777a41da3b33f7098b6219012ba534fb2c2"}, - {file = "psycopg2_binary-2.9.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:a0adef094c49f242122bb145c3c8af442070dc0e4312db17e49058c1702606d4"}, - {file = "psycopg2_binary-2.9.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:00475004e5ed3e3bf5e056d66e5dcdf41a0dc62efcd57997acd9135c40a08a50"}, - {file = "psycopg2_binary-2.9.5-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:7d88db096fa19d94f433420eaaf9f3c45382da2dd014b93e4bf3215639047c16"}, - {file = "psycopg2_binary-2.9.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:902844f9c4fb19b17dfa84d9e2ca053d4a4ba265723d62ea5c9c26b38e0aa1e6"}, - {file = "psycopg2_binary-2.9.5-cp38-cp38-win32.whl", hash = "sha256:4e7904d1920c0c89105c0517dc7e3f5c20fb4e56ba9cdef13048db76947f1d79"}, - {file = "psycopg2_binary-2.9.5-cp38-cp38-win_amd64.whl", hash = "sha256:a36a0e791805aa136e9cbd0ffa040d09adec8610453ee8a753f23481a0057af5"}, - {file = "psycopg2_binary-2.9.5-cp39-cp39-macosx_10_15_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:25382c7d174c679ce6927c16b6fbb68b10e56ee44b1acb40671e02d29f2fce7c"}, - {file = "psycopg2_binary-2.9.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9c38d3869238e9d3409239bc05bc27d6b7c99c2a460ea337d2814b35fb4fea1b"}, - {file = "psycopg2_binary-2.9.5-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5c6527c8efa5226a9e787507652dd5ba97b62d29b53c371a85cd13f957fe4d42"}, - {file = "psycopg2_binary-2.9.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e59137cdb970249ae60be2a49774c6dfb015bd0403f05af1fe61862e9626642d"}, - {file = "psycopg2_binary-2.9.5-cp39-cp39-manylinux_2_24_aarch64.whl", hash = "sha256:d4c7b3a31502184e856df1f7bbb2c3735a05a8ce0ade34c5277e1577738a5c91"}, - {file = "psycopg2_binary-2.9.5-cp39-cp39-manylinux_2_24_ppc64le.whl", hash = "sha256:b9a794cef1d9c1772b94a72eec6da144c18e18041d294a9ab47669bc77a80c1d"}, - {file = "psycopg2_binary-2.9.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:c5254cbd4f4855e11cebf678c1a848a3042d455a22a4ce61349c36aafd4c2267"}, - {file = "psycopg2_binary-2.9.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:c5e65c6ac0ae4bf5bef1667029f81010b6017795dcb817ba5c7b8a8d61fab76f"}, - {file = "psycopg2_binary-2.9.5-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:74eddec4537ab1f701a1647214734bc52cee2794df748f6ae5908e00771f180a"}, - {file = "psycopg2_binary-2.9.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:01ad49d68dd8c5362e4bfb4158f2896dc6e0c02e87b8a3770fc003459f1a4425"}, - {file = "psycopg2_binary-2.9.5-cp39-cp39-win32.whl", hash = "sha256:937880290775033a743f4836aa253087b85e62784b63fd099ee725d567a48aa1"}, - {file = "psycopg2_binary-2.9.5-cp39-cp39-win_amd64.whl", hash = "sha256:484405b883630f3e74ed32041a87456c5e0e63a8e3429aa93e8714c366d62bd1"}, -] - -[[package]] -name = "pygments" -version = "2.14.0" -description = "Pygments is a syntax highlighting package written in Python." -category = "main" -optional = false -python-versions = ">=3.6" -files = [ - {file = "Pygments-2.14.0-py3-none-any.whl", hash = "sha256:fa7bd7bd2771287c0de303af8bfdfc731f51bd2c6a47ab69d117138893b82717"}, - {file = "Pygments-2.14.0.tar.gz", hash = "sha256:b3ed06a9e8ac9a9aae5a6f5dbe78a8a58655d17b43b93c078f094ddc476ae297"}, -] - -[package.extras] -plugins = ["importlib-metadata"] - -[[package]] -name = "pytz" -version = "2022.7.1" -description = "World timezone definitions, modern and historical" -category = "main" -optional = false -python-versions = "*" -files = [ - {file = "pytz-2022.7.1-py2.py3-none-any.whl", hash = "sha256:78f4f37d8198e0627c5f1143240bb0206b8691d8d7ac6d78fee88b78733f8c4a"}, - {file = "pytz-2022.7.1.tar.gz", hash = "sha256:01a0681c4b9684a28304615eba55d1ab31ae00bf68ec157ec3708a8182dbbcd0"}, -] - -[[package]] -name = "requests" -version = "2.28.2" -description = "Python HTTP for Humans." -category = "main" -optional = false -python-versions = ">=3.7, <4" -files = [ - {file = "requests-2.28.2-py3-none-any.whl", hash = "sha256:64299f4909223da747622c030b781c0d7811e359c37124b4bd368fb8c6518baa"}, - {file = "requests-2.28.2.tar.gz", hash = "sha256:98b1b2782e3c6c4904938b84c0eb932721069dfdb9134313beff7c83c2df24bf"}, -] - -[package.dependencies] -certifi = ">=2017.4.17" -charset-normalizer = ">=2,<4" -idna = ">=2.5,<4" -urllib3 = ">=1.21.1,<1.27" - -[package.extras] -socks = ["PySocks (>=1.5.6,!=1.5.7)"] -use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] - -[[package]] -name = "snowballstemmer" -version = "2.2.0" -description = "This package provides 29 stemmers for 28 languages generated from Snowball algorithms." -category = "main" -optional = false -python-versions = "*" -files = [ - {file = "snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a"}, - {file = "snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1"}, -] - -[[package]] -name = "sphinx" -version = "6.1.3" -description = "Python documentation generator" -category = "main" -optional = false -python-versions = ">=3.8" -files = [ - {file = "Sphinx-6.1.3.tar.gz", hash = "sha256:0dac3b698538ffef41716cf97ba26c1c7788dba73ce6f150c1ff5b4720786dd2"}, - {file = "sphinx-6.1.3-py3-none-any.whl", hash = "sha256:807d1cb3d6be87eb78a381c3e70ebd8d346b9a25f3753e9947e866b2786865fc"}, -] - -[package.dependencies] -alabaster = ">=0.7,<0.8" -babel = ">=2.9" -colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""} -docutils = ">=0.18,<0.20" -imagesize = ">=1.3" -importlib-metadata = {version = ">=4.8", markers = "python_version < \"3.10\""} -Jinja2 = ">=3.0" -packaging = ">=21.0" -Pygments = ">=2.13" -requests = ">=2.25.0" -snowballstemmer = ">=2.0" -sphinxcontrib-applehelp = "*" -sphinxcontrib-devhelp = "*" -sphinxcontrib-htmlhelp = ">=2.0.0" -sphinxcontrib-jsmath = "*" -sphinxcontrib-qthelp = "*" -sphinxcontrib-serializinghtml = ">=1.1.5" - -[package.extras] -docs = ["sphinxcontrib-websupport"] -lint = ["docutils-stubs", "flake8 (>=3.5.0)", "flake8-simplify", "isort", "mypy (>=0.990)", "ruff", "sphinx-lint", "types-requests"] -test = ["cython", "html5lib", "pytest (>=4.6)"] - -[[package]] -name = "sphinxcontrib-applehelp" -version = "1.0.4" -description = "sphinxcontrib-applehelp is a Sphinx extension which outputs Apple help books" -category = "main" -optional = false -python-versions = ">=3.8" -files = [ - {file = "sphinxcontrib-applehelp-1.0.4.tar.gz", hash = "sha256:828f867945bbe39817c210a1abfd1bc4895c8b73fcaade56d45357a348a07d7e"}, - {file = "sphinxcontrib_applehelp-1.0.4-py3-none-any.whl", hash = "sha256:29d341f67fb0f6f586b23ad80e072c8e6ad0b48417db2bde114a4c9746feb228"}, -] - -[package.extras] -lint = ["docutils-stubs", "flake8", "mypy"] -test = ["pytest"] - -[[package]] -name = "sphinxcontrib-devhelp" -version = "1.0.2" -description = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp document." -category = "main" -optional = false -python-versions = ">=3.5" -files = [ - {file = "sphinxcontrib-devhelp-1.0.2.tar.gz", hash = "sha256:ff7f1afa7b9642e7060379360a67e9c41e8f3121f2ce9164266f61b9f4b338e4"}, - {file = "sphinxcontrib_devhelp-1.0.2-py2.py3-none-any.whl", hash = "sha256:8165223f9a335cc1af7ffe1ed31d2871f325254c0423bc0c4c7cd1c1e4734a2e"}, -] - -[package.extras] -lint = ["docutils-stubs", "flake8", "mypy"] -test = ["pytest"] - -[[package]] -name = "sphinxcontrib-htmlhelp" -version = "2.0.1" -description = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML help files" -category = "main" -optional = false -python-versions = ">=3.8" -files = [ - {file = "sphinxcontrib-htmlhelp-2.0.1.tar.gz", hash = "sha256:0cbdd302815330058422b98a113195c9249825d681e18f11e8b1f78a2f11efff"}, - {file = "sphinxcontrib_htmlhelp-2.0.1-py3-none-any.whl", hash = "sha256:c38cb46dccf316c79de6e5515e1770414b797162b23cd3d06e67020e1d2a6903"}, -] - -[package.extras] -lint = ["docutils-stubs", "flake8", "mypy"] -test = ["html5lib", "pytest"] - -[[package]] -name = "sphinxcontrib-jsmath" -version = "1.0.1" -description = "A sphinx extension which renders display math in HTML via JavaScript" -category = "main" -optional = false -python-versions = ">=3.5" -files = [ - {file = "sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8"}, - {file = "sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178"}, -] - -[package.extras] -test = ["flake8", "mypy", "pytest"] - -[[package]] -name = "sphinxcontrib-qthelp" -version = "1.0.3" -description = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp document." -category = "main" -optional = false -python-versions = ">=3.5" -files = [ - {file = "sphinxcontrib-qthelp-1.0.3.tar.gz", hash = "sha256:4c33767ee058b70dba89a6fc5c1892c0d57a54be67ddd3e7875a18d14cba5a72"}, - {file = "sphinxcontrib_qthelp-1.0.3-py2.py3-none-any.whl", hash = "sha256:bd9fc24bcb748a8d51fd4ecaade681350aa63009a347a8c14e637895444dfab6"}, -] - -[package.extras] -lint = ["docutils-stubs", "flake8", "mypy"] -test = ["pytest"] - -[[package]] -name = "sphinxcontrib-serializinghtml" -version = "1.1.5" -description = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)." -category = "main" -optional = false -python-versions = ">=3.5" -files = [ - {file = "sphinxcontrib-serializinghtml-1.1.5.tar.gz", hash = "sha256:aa5f6de5dfdf809ef505c4895e51ef5c9eac17d0f287933eb49ec495280b6952"}, - {file = "sphinxcontrib_serializinghtml-1.1.5-py2.py3-none-any.whl", hash = "sha256:352a9a00ae864471d3a7ead8d7d79f5fc0b57e8b3f95e9867eb9eb28999b92fd"}, -] - -[package.extras] -lint = ["docutils-stubs", "flake8", "mypy"] -test = ["pytest"] - -[[package]] -name = "urllib3" -version = "1.26.14" -description = "HTTP library with thread-safe connection pooling, file post, and more." -category = "main" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" -files = [ - {file = "urllib3-1.26.14-py2.py3-none-any.whl", hash = "sha256:75edcdc2f7d85b137124a6c3c9fc3933cdeaa12ecb9a6a959f22797a0feca7e1"}, - {file = "urllib3-1.26.14.tar.gz", hash = "sha256:076907bf8fd355cde77728471316625a4d2f7e713c125f51953bb5b3eecf4f72"}, -] - -[package.extras] -brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotlipy (>=0.6.0)"] -secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress", "pyOpenSSL (>=0.14)", "urllib3-secure-extra"] -socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] - -[[package]] -name = "zipp" -version = "3.14.0" -description = "Backport of pathlib-compatible object wrapper for zip files" -category = "main" -optional = false -python-versions = ">=3.7" -files = [ - {file = "zipp-3.14.0-py3-none-any.whl", hash = "sha256:188834565033387710d046e3fe96acfc9b5e86cbca7f39ff69cf21a4128198b7"}, - {file = "zipp-3.14.0.tar.gz", hash = "sha256:9e5421e176ef5ab4c0ad896624e87a7b2f07aca746c9b2aa305952800cb8eecb"}, -] - -[package.extras] -docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -testing = ["flake8 (<5)", "func-timeout", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"] - -[metadata] -lock-version = "2.0" -python-versions = ">=3.9,<4" -content-hash = "0063b9b2c89e250e0b184a94af30d82ecfb94bbbc5bcd7af8d92446ae66d0e32" diff --git a/docs/pyproject.toml b/docs/pyproject.toml index 07ed3a4..29d4896 100644 --- a/docs/pyproject.toml +++ b/docs/pyproject.toml @@ -1,15 +1,12 @@ -[tool.poetry] -name = "pgmob.docs" +[project] +name = "pgmob-docs" version = "0.1.0" -description = "Generates docs for pgmob" -authors = ["kirill.kravtsov "] +description = "Documentation dependencies for pgmob" +requires-python = ">=3.13" +dependencies = [ + "pgmob[psycopg2-binary]", + "sphinx>=6.1.3", +] - -[tool.poetry.dependencies] -python = ">=3.9,<4" -pgmob = {path = "..", extras = ["psycopg2-binary"]} -sphinx = "^6.1.3" - -[build-system] -requires = ["poetry-core>=1.0.0"] -build-backend = "poetry.core.masonry.api" \ No newline at end of file +[tool.uv.sources] +pgmob = { path = ".." } diff --git a/docs/requirements.txt b/docs/requirements.txt deleted file mode 100644 index de24f44..0000000 --- a/docs/requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -packaging==23.0 ; python_version >= "3.9" and python_version < "4" -psycopg2-binary==2.9.5 ; python_version >= "3.9" and python_version < "4" diff --git a/docs/uv.lock b/docs/uv.lock new file mode 100644 index 0000000..aab3e5a --- /dev/null +++ b/docs/uv.lock @@ -0,0 +1,387 @@ +version = 1 +revision = 3 +requires-python = ">=3.13" + +[[package]] +name = "alabaster" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a6/f8/d9c74d0daf3f742840fd818d69cfae176fa332022fd44e3469487d5a9420/alabaster-1.0.0.tar.gz", hash = "sha256:c00dca57bca26fa62a6d7d0a9fcce65f3e026e9bfe33e9c538fd3fbb2144fd9e", size = 24210, upload-time = "2024-07-26T18:15:03.762Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/b3/6b4067be973ae96ba0d615946e314c5ae35f9f993eca561b356540bb0c2b/alabaster-1.0.0-py3-none-any.whl", hash = "sha256:fc6786402dc3fcb2de3cabd5fe455a2db534b371124f1f21de8731783dec828b", size = 13929, upload-time = "2024-07-26T18:15:02.05Z" }, +] + +[[package]] +name = "babel" +version = "2.18.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/b2/51899539b6ceeeb420d40ed3cd4b7a40519404f9baf3d4ac99dc413a834b/babel-2.18.0.tar.gz", hash = "sha256:b80b99a14bd085fcacfa15c9165f651fbb3406e66cc603abf11c5750937c992d", size = 9959554, upload-time = "2026-02-01T12:30:56.078Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl", hash = "sha256:e2b422b277c2b9a9630c1d7903c2a00d0830c409c59ac8cae9081c92f1aeba35", size = 10196845, upload-time = "2026-02-01T12:30:53.445Z" }, +] + +[[package]] +name = "certifi" +version = "2026.1.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/2d/a891ca51311197f6ad14a7ef42e2399f36cf2f9bd44752b3dc4eab60fdc5/certifi-2026.1.4.tar.gz", hash = "sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120", size = 154268, upload-time = "2026-01-04T02:42:41.825Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl", hash = "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c", size = 152900, upload-time = "2026-01-04T02:42:40.15Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", size = 208091, upload-time = "2025-10-14T04:41:13.346Z" }, + { url = "https://files.pythonhosted.org/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", size = 147936, upload-time = "2025-10-14T04:41:14.461Z" }, + { url = "https://files.pythonhosted.org/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", size = 144180, upload-time = "2025-10-14T04:41:15.588Z" }, + { url = "https://files.pythonhosted.org/packages/91/ed/9706e4070682d1cc219050b6048bfd293ccf67b3d4f5a4f39207453d4b99/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", size = 161346, upload-time = "2025-10-14T04:41:16.738Z" }, + { url = "https://files.pythonhosted.org/packages/d5/0d/031f0d95e4972901a2f6f09ef055751805ff541511dc1252ba3ca1f80cf5/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", size = 158874, upload-time = "2025-10-14T04:41:17.923Z" }, + { url = "https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", size = 153076, upload-time = "2025-10-14T04:41:19.106Z" }, + { url = "https://files.pythonhosted.org/packages/75/1e/5ff781ddf5260e387d6419959ee89ef13878229732732ee73cdae01800f2/charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", size = 150601, upload-time = "2025-10-14T04:41:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/d7/57/71be810965493d3510a6ca79b90c19e48696fb1ff964da319334b12677f0/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", size = 150376, upload-time = "2025-10-14T04:41:21.398Z" }, + { url = "https://files.pythonhosted.org/packages/e5/d5/c3d057a78c181d007014feb7e9f2e65905a6c4ef182c0ddf0de2924edd65/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", size = 144825, upload-time = "2025-10-14T04:41:22.583Z" }, + { url = "https://files.pythonhosted.org/packages/e6/8c/d0406294828d4976f275ffbe66f00266c4b3136b7506941d87c00cab5272/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", size = 162583, upload-time = "2025-10-14T04:41:23.754Z" }, + { url = "https://files.pythonhosted.org/packages/d7/24/e2aa1f18c8f15c4c0e932d9287b8609dd30ad56dbe41d926bd846e22fb8d/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", size = 150366, upload-time = "2025-10-14T04:41:25.27Z" }, + { url = "https://files.pythonhosted.org/packages/e4/5b/1e6160c7739aad1e2df054300cc618b06bf784a7a164b0f238360721ab86/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", size = 160300, upload-time = "2025-10-14T04:41:26.725Z" }, + { url = "https://files.pythonhosted.org/packages/7a/10/f882167cd207fbdd743e55534d5d9620e095089d176d55cb22d5322f2afd/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", size = 154465, upload-time = "2025-10-14T04:41:28.322Z" }, + { url = "https://files.pythonhosted.org/packages/89/66/c7a9e1b7429be72123441bfdbaf2bc13faab3f90b933f664db506dea5915/charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", size = 99404, upload-time = "2025-10-14T04:41:29.95Z" }, + { url = "https://files.pythonhosted.org/packages/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", size = 107092, upload-time = "2025-10-14T04:41:31.188Z" }, + { url = "https://files.pythonhosted.org/packages/af/8f/3ed4bfa0c0c72a7ca17f0380cd9e4dd842b09f664e780c13cff1dcf2ef1b/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", size = 100408, upload-time = "2025-10-14T04:41:32.624Z" }, + { url = "https://files.pythonhosted.org/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746, upload-time = "2025-10-14T04:41:33.773Z" }, + { url = "https://files.pythonhosted.org/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889, upload-time = "2025-10-14T04:41:34.897Z" }, + { url = "https://files.pythonhosted.org/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641, upload-time = "2025-10-14T04:41:36.116Z" }, + { url = "https://files.pythonhosted.org/packages/ba/33/0ad65587441fc730dc7bd90e9716b30b4702dc7b617e6ba4997dc8651495/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", size = 160779, upload-time = "2025-10-14T04:41:37.229Z" }, + { url = "https://files.pythonhosted.org/packages/67/ed/331d6b249259ee71ddea93f6f2f0a56cfebd46938bde6fcc6f7b9a3d0e09/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", size = 159035, upload-time = "2025-10-14T04:41:38.368Z" }, + { url = "https://files.pythonhosted.org/packages/67/ff/f6b948ca32e4f2a4576aa129d8bed61f2e0543bf9f5f2b7fc3758ed005c9/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", size = 152542, upload-time = "2025-10-14T04:41:39.862Z" }, + { url = "https://files.pythonhosted.org/packages/16/85/276033dcbcc369eb176594de22728541a925b2632f9716428c851b149e83/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", size = 149524, upload-time = "2025-10-14T04:41:41.319Z" }, + { url = "https://files.pythonhosted.org/packages/9e/f2/6a2a1f722b6aba37050e626530a46a68f74e63683947a8acff92569f979a/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", size = 150395, upload-time = "2025-10-14T04:41:42.539Z" }, + { url = "https://files.pythonhosted.org/packages/60/bb/2186cb2f2bbaea6338cad15ce23a67f9b0672929744381e28b0592676824/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", size = 143680, upload-time = "2025-10-14T04:41:43.661Z" }, + { url = "https://files.pythonhosted.org/packages/7d/a5/bf6f13b772fbb2a90360eb620d52ed8f796f3c5caee8398c3b2eb7b1c60d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", size = 162045, upload-time = "2025-10-14T04:41:44.821Z" }, + { url = "https://files.pythonhosted.org/packages/df/c5/d1be898bf0dc3ef9030c3825e5d3b83f2c528d207d246cbabe245966808d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", size = 149687, upload-time = "2025-10-14T04:41:46.442Z" }, + { url = "https://files.pythonhosted.org/packages/a5/42/90c1f7b9341eef50c8a1cb3f098ac43b0508413f33affd762855f67a410e/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", size = 160014, upload-time = "2025-10-14T04:41:47.631Z" }, + { url = "https://files.pythonhosted.org/packages/76/be/4d3ee471e8145d12795ab655ece37baed0929462a86e72372fd25859047c/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", size = 154044, upload-time = "2025-10-14T04:41:48.81Z" }, + { url = "https://files.pythonhosted.org/packages/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", size = 99940, upload-time = "2025-10-14T04:41:49.946Z" }, + { url = "https://files.pythonhosted.org/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", size = 107104, upload-time = "2025-10-14T04:41:51.051Z" }, + { url = "https://files.pythonhosted.org/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", size = 100743, upload-time = "2025-10-14T04:41:52.122Z" }, + { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "docutils" +version = "0.22.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ae/b6/03bb70946330e88ffec97aefd3ea75ba575cb2e762061e0e62a213befee8/docutils-0.22.4.tar.gz", hash = "sha256:4db53b1fde9abecbb74d91230d32ab626d94f6badfc575d6db9194a49df29968", size = 2291750, upload-time = "2025-12-18T19:00:26.443Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/10/5da547df7a391dcde17f59520a231527b8571e6f46fc8efb02ccb370ab12/docutils-0.22.4-py3-none-any.whl", hash = "sha256:d0013f540772d1420576855455d050a2180186c91c15779301ac2ccb3eeb68de", size = 633196, upload-time = "2025-12-18T19:00:18.077Z" }, +] + +[[package]] +name = "idna" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, +] + +[[package]] +name = "imagesize" +version = "1.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a7/84/62473fb57d61e31fef6e36d64a179c8781605429fd927b5dd608c997be31/imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a", size = 1280026, upload-time = "2022-07-01T12:21:05.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ff/62/85c4c919272577931d407be5ba5d71c20f0b616d31a0befe0ae45bb79abd/imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b", size = 8769, upload-time = "2022-07-01T12:21:02.467Z" }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, + { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, + { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, + { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, + { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, + { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, + { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, + { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, + { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, + { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, + { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, + { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, + { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, + { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, + { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, + { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, + { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, + { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, + { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, +] + +[[package]] +name = "packaging" +version = "26.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" }, +] + +[[package]] +name = "pgmob" +version = "0.3.0" +source = { directory = "../" } +dependencies = [ + { name = "packaging" }, +] + +[package.optional-dependencies] +psycopg2-binary = [ + { name = "psycopg2-binary" }, +] + +[package.metadata] +requires-dist = [ + { name = "docker", marker = "extra == 'dev'", specifier = ">=7.1.0" }, + { name = "packaging", specifier = ">=26.0" }, + { name = "psycopg2", marker = "extra == 'psycopg2'", specifier = ">=2.9.11,<3" }, + { name = "psycopg2-binary", marker = "extra == 'psycopg2-binary'", specifier = ">=2.9.11,<3" }, + { name = "pytest", marker = "extra == 'dev'", specifier = ">=9.0.0" }, + { name = "pytest-asyncio", marker = "extra == 'dev'", specifier = ">=1.3.0" }, + { name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=7.0.0" }, + { name = "pytest-mock", marker = "extra == 'dev'", specifier = ">=3.15.0" }, + { name = "pyyaml", marker = "extra == 'dev'", specifier = ">=6.0.3" }, + { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.15.0" }, + { name = "ty", marker = "extra == 'dev'", specifier = ">=0.0.15" }, +] +provides-extras = ["psycopg2", "psycopg2-binary", "dev"] + +[[package]] +name = "pgmob-docs" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "pgmob", extra = ["psycopg2-binary"] }, + { name = "sphinx" }, +] + +[package.metadata] +requires-dist = [ + { name = "pgmob", extras = ["psycopg2-binary"], directory = "../" }, + { name = "sphinx", specifier = ">=6.1.3" }, +] + +[[package]] +name = "psycopg2-binary" +version = "2.9.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ac/6c/8767aaa597ba424643dc87348c6f1754dd9f48e80fdc1b9f7ca5c3a7c213/psycopg2-binary-2.9.11.tar.gz", hash = "sha256:b6aed9e096bf63f9e75edf2581aa9a7e7186d97ab5c177aa6c87797cd591236c", size = 379620, upload-time = "2025-10-10T11:14:48.041Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ff/a8/a2709681b3ac11b0b1786def10006b8995125ba268c9a54bea6f5ae8bd3e/psycopg2_binary-2.9.11-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b8fb3db325435d34235b044b199e56cdf9ff41223a4b9752e8576465170bb38c", size = 3756572, upload-time = "2025-10-10T11:12:32.873Z" }, + { url = "https://files.pythonhosted.org/packages/62/e1/c2b38d256d0dafd32713e9f31982a5b028f4a3651f446be70785f484f472/psycopg2_binary-2.9.11-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:366df99e710a2acd90efed3764bb1e28df6c675d33a7fb40df9b7281694432ee", size = 3864529, upload-time = "2025-10-10T11:12:36.791Z" }, + { url = "https://files.pythonhosted.org/packages/11/32/b2ffe8f3853c181e88f0a157c5fb4e383102238d73c52ac6d93a5c8bffe6/psycopg2_binary-2.9.11-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8c55b385daa2f92cb64b12ec4536c66954ac53654c7f15a203578da4e78105c0", size = 4411242, upload-time = "2025-10-10T11:12:42.388Z" }, + { url = "https://files.pythonhosted.org/packages/10/04/6ca7477e6160ae258dc96f67c371157776564679aefd247b66f4661501a2/psycopg2_binary-2.9.11-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:c0377174bf1dd416993d16edc15357f6eb17ac998244cca19bc67cdc0e2e5766", size = 4468258, upload-time = "2025-10-10T11:12:48.654Z" }, + { url = "https://files.pythonhosted.org/packages/3c/7e/6a1a38f86412df101435809f225d57c1a021307dd0689f7a5e7fe83588b1/psycopg2_binary-2.9.11-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5c6ff3335ce08c75afaed19e08699e8aacf95d4a260b495a4a8545244fe2ceb3", size = 4166295, upload-time = "2025-10-10T11:12:52.525Z" }, + { url = "https://files.pythonhosted.org/packages/f2/7d/c07374c501b45f3579a9eb761cbf2604ddef3d96ad48679112c2c5aa9c25/psycopg2_binary-2.9.11-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:84011ba3109e06ac412f95399b704d3d6950e386b7994475b231cf61eec2fc1f", size = 3983133, upload-time = "2025-10-30T02:55:24.329Z" }, + { url = "https://files.pythonhosted.org/packages/82/56/993b7104cb8345ad7d4516538ccf8f0d0ac640b1ebd8c754a7b024e76878/psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ba34475ceb08cccbdd98f6b46916917ae6eeb92b5ae111df10b544c3a4621dc4", size = 3652383, upload-time = "2025-10-10T11:12:56.387Z" }, + { url = "https://files.pythonhosted.org/packages/2d/ac/eaeb6029362fd8d454a27374d84c6866c82c33bfc24587b4face5a8e43ef/psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b31e90fdd0f968c2de3b26ab014314fe814225b6c324f770952f7d38abf17e3c", size = 3298168, upload-time = "2025-10-10T11:13:00.403Z" }, + { url = "https://files.pythonhosted.org/packages/2b/39/50c3facc66bded9ada5cbc0de867499a703dc6bca6be03070b4e3b65da6c/psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:d526864e0f67f74937a8fce859bd56c979f5e2ec57ca7c627f5f1071ef7fee60", size = 3044712, upload-time = "2025-10-30T02:55:27.975Z" }, + { url = "https://files.pythonhosted.org/packages/9c/8e/b7de019a1f562f72ada81081a12823d3c1590bedc48d7d2559410a2763fe/psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:04195548662fa544626c8ea0f06561eb6203f1984ba5b4562764fbeb4c3d14b1", size = 3347549, upload-time = "2025-10-10T11:13:03.971Z" }, + { url = "https://files.pythonhosted.org/packages/80/2d/1bb683f64737bbb1f86c82b7359db1eb2be4e2c0c13b947f80efefa7d3e5/psycopg2_binary-2.9.11-cp313-cp313-win_amd64.whl", hash = "sha256:efff12b432179443f54e230fdf60de1f6cc726b6c832db8701227d089310e8aa", size = 2714215, upload-time = "2025-10-10T11:13:07.14Z" }, + { url = "https://files.pythonhosted.org/packages/64/12/93ef0098590cf51d9732b4f139533732565704f45bdc1ffa741b7c95fb54/psycopg2_binary-2.9.11-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:92e3b669236327083a2e33ccfa0d320dd01b9803b3e14dd986a4fc54aa00f4e1", size = 3756567, upload-time = "2025-10-10T11:13:11.885Z" }, + { url = "https://files.pythonhosted.org/packages/7c/a9/9d55c614a891288f15ca4b5209b09f0f01e3124056924e17b81b9fa054cc/psycopg2_binary-2.9.11-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e0deeb03da539fa3577fcb0b3f2554a97f7e5477c246098dbb18091a4a01c16f", size = 3864755, upload-time = "2025-10-10T11:13:17.727Z" }, + { url = "https://files.pythonhosted.org/packages/13/1e/98874ce72fd29cbde93209977b196a2edae03f8490d1bd8158e7f1daf3a0/psycopg2_binary-2.9.11-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:9b52a3f9bb540a3e4ec0f6ba6d31339727b2950c9772850d6545b7eae0b9d7c5", size = 4411646, upload-time = "2025-10-10T11:13:24.432Z" }, + { url = "https://files.pythonhosted.org/packages/5a/bd/a335ce6645334fb8d758cc358810defca14a1d19ffbc8a10bd38a2328565/psycopg2_binary-2.9.11-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:db4fd476874ccfdbb630a54426964959e58da4c61c9feba73e6094d51303d7d8", size = 4468701, upload-time = "2025-10-10T11:13:29.266Z" }, + { url = "https://files.pythonhosted.org/packages/44/d6/c8b4f53f34e295e45709b7568bf9b9407a612ea30387d35eb9fa84f269b4/psycopg2_binary-2.9.11-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:47f212c1d3be608a12937cc131bd85502954398aaa1320cb4c14421a0ffccf4c", size = 4166293, upload-time = "2025-10-10T11:13:33.336Z" }, + { url = "https://files.pythonhosted.org/packages/4b/e0/f8cc36eadd1b716ab36bb290618a3292e009867e5c97ce4aba908cb99644/psycopg2_binary-2.9.11-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e35b7abae2b0adab776add56111df1735ccc71406e56203515e228a8dc07089f", size = 3983184, upload-time = "2025-10-30T02:55:32.483Z" }, + { url = "https://files.pythonhosted.org/packages/53/3e/2a8fe18a4e61cfb3417da67b6318e12691772c0696d79434184a511906dc/psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fcf21be3ce5f5659daefd2b3b3b6e4727b028221ddc94e6c1523425579664747", size = 3652650, upload-time = "2025-10-10T11:13:38.181Z" }, + { url = "https://files.pythonhosted.org/packages/76/36/03801461b31b29fe58d228c24388f999fe814dfc302856e0d17f97d7c54d/psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:9bd81e64e8de111237737b29d68039b9c813bdf520156af36d26819c9a979e5f", size = 3298663, upload-time = "2025-10-10T11:13:44.878Z" }, + { url = "https://files.pythonhosted.org/packages/97/77/21b0ea2e1a73aa5fa9222b2a6b8ba325c43c3a8d54272839c991f2345656/psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:32770a4d666fbdafab017086655bcddab791d7cb260a16679cc5a7338b64343b", size = 3044737, upload-time = "2025-10-30T02:55:35.69Z" }, + { url = "https://files.pythonhosted.org/packages/67/69/f36abe5f118c1dca6d3726ceae164b9356985805480731ac6712a63f24f0/psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c3cb3a676873d7506825221045bd70e0427c905b9c8ee8d6acd70cfcbd6e576d", size = 3347643, upload-time = "2025-10-10T11:13:53.499Z" }, + { url = "https://files.pythonhosted.org/packages/e1/36/9c0c326fe3a4227953dfb29f5d0c8ae3b8eb8c1cd2967aa569f50cb3c61f/psycopg2_binary-2.9.11-cp314-cp314-win_amd64.whl", hash = "sha256:4012c9c954dfaccd28f94e84ab9f94e12df76b4afb22331b1f0d3154893a6316", size = 2803913, upload-time = "2025-10-10T11:13:57.058Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "requests" +version = "2.32.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, +] + +[[package]] +name = "roman-numerals" +version = "4.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ae/f9/41dc953bbeb056c17d5f7a519f50fdf010bd0553be2d630bc69d1e022703/roman_numerals-4.1.0.tar.gz", hash = "sha256:1af8b147eb1405d5839e78aeb93131690495fe9da5c91856cb33ad55a7f1e5b2", size = 9077, upload-time = "2025-12-17T18:25:34.381Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/54/6f679c435d28e0a568d8e8a7c0a93a09010818634c3c3907fc98d8983770/roman_numerals-4.1.0-py3-none-any.whl", hash = "sha256:647ba99caddc2cc1e55a51e4360689115551bf4476d90e8162cf8c345fe233c7", size = 7676, upload-time = "2025-12-17T18:25:33.098Z" }, +] + +[[package]] +name = "snowballstemmer" +version = "3.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/75/a7/9810d872919697c9d01295633f5d574fb416d47e535f258272ca1f01f447/snowballstemmer-3.0.1.tar.gz", hash = "sha256:6d5eeeec8e9f84d4d56b847692bacf79bc2c8e90c7f80ca4444ff8b6f2e52895", size = 105575, upload-time = "2025-05-09T16:34:51.843Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/78/3565d011c61f5a43488987ee32b6f3f656e7f107ac2782dd57bdd7d91d9a/snowballstemmer-3.0.1-py3-none-any.whl", hash = "sha256:6cd7b3897da8d6c9ffb968a6781fa6532dce9c3618a4b127d920dab764a19064", size = 103274, upload-time = "2025-05-09T16:34:50.371Z" }, +] + +[[package]] +name = "sphinx" +version = "9.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "alabaster" }, + { name = "babel" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "docutils" }, + { name = "imagesize" }, + { name = "jinja2" }, + { name = "packaging" }, + { name = "pygments" }, + { name = "requests" }, + { name = "roman-numerals" }, + { name = "snowballstemmer" }, + { name = "sphinxcontrib-applehelp" }, + { name = "sphinxcontrib-devhelp" }, + { name = "sphinxcontrib-htmlhelp" }, + { name = "sphinxcontrib-jsmath" }, + { name = "sphinxcontrib-qthelp" }, + { name = "sphinxcontrib-serializinghtml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cd/bd/f08eb0f4eed5c83f1ba2a3bd18f7745a2b1525fad70660a1c00224ec468a/sphinx-9.1.0.tar.gz", hash = "sha256:7741722357dd75f8190766926071fed3bdc211c74dd2d7d4df5404da95930ddb", size = 8718324, upload-time = "2025-12-31T15:09:27.646Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/73/f7/b1884cb3188ab181fc81fa00c266699dab600f927a964df02ec3d5d1916a/sphinx-9.1.0-py3-none-any.whl", hash = "sha256:c84fdd4e782504495fe4f2c0b3413d6c2bf388589bb352d439b2a3bb99991978", size = 3921742, upload-time = "2025-12-31T15:09:25.561Z" }, +] + +[[package]] +name = "sphinxcontrib-applehelp" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/6e/b837e84a1a704953c62ef8776d45c3e8d759876b4a84fe14eba2859106fe/sphinxcontrib_applehelp-2.0.0.tar.gz", hash = "sha256:2f29ef331735ce958efa4734873f084941970894c6090408b079c61b2e1c06d1", size = 20053, upload-time = "2024-07-29T01:09:00.465Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/85/9ebeae2f76e9e77b952f4b274c27238156eae7979c5421fba91a28f4970d/sphinxcontrib_applehelp-2.0.0-py3-none-any.whl", hash = "sha256:4cd3f0ec4ac5dd9c17ec65e9ab272c9b867ea77425228e68ecf08d6b28ddbdb5", size = 119300, upload-time = "2024-07-29T01:08:58.99Z" }, +] + +[[package]] +name = "sphinxcontrib-devhelp" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/d2/5beee64d3e4e747f316bae86b55943f51e82bb86ecd325883ef65741e7da/sphinxcontrib_devhelp-2.0.0.tar.gz", hash = "sha256:411f5d96d445d1d73bb5d52133377b4248ec79db5c793ce7dbe59e074b4dd1ad", size = 12967, upload-time = "2024-07-29T01:09:23.417Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/35/7a/987e583882f985fe4d7323774889ec58049171828b58c2217e7f79cdf44e/sphinxcontrib_devhelp-2.0.0-py3-none-any.whl", hash = "sha256:aefb8b83854e4b0998877524d1029fd3e6879210422ee3780459e28a1f03a8a2", size = 82530, upload-time = "2024-07-29T01:09:21.945Z" }, +] + +[[package]] +name = "sphinxcontrib-htmlhelp" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/93/983afd9aa001e5201eab16b5a444ed5b9b0a7a010541e0ddfbbfd0b2470c/sphinxcontrib_htmlhelp-2.1.0.tar.gz", hash = "sha256:c9e2916ace8aad64cc13a0d233ee22317f2b9025b9cf3295249fa985cc7082e9", size = 22617, upload-time = "2024-07-29T01:09:37.889Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0a/7b/18a8c0bcec9182c05a0b3ec2a776bba4ead82750a55ff798e8d406dae604/sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl", hash = "sha256:166759820b47002d22914d64a075ce08f4c46818e17cfc9470a9786b759b19f8", size = 98705, upload-time = "2024-07-29T01:09:36.407Z" }, +] + +[[package]] +name = "sphinxcontrib-jsmath" +version = "1.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/e8/9ed3830aeed71f17c026a07a5097edcf44b692850ef215b161b8ad875729/sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8", size = 5787, upload-time = "2019-01-21T16:10:16.347Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/42/4c8646762ee83602e3fb3fbe774c2fac12f317deb0b5dbeeedd2d3ba4b77/sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178", size = 5071, upload-time = "2019-01-21T16:10:14.333Z" }, +] + +[[package]] +name = "sphinxcontrib-qthelp" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/68/bc/9104308fc285eb3e0b31b67688235db556cd5b0ef31d96f30e45f2e51cae/sphinxcontrib_qthelp-2.0.0.tar.gz", hash = "sha256:4fe7d0ac8fc171045be623aba3e2a8f613f8682731f9153bb2e40ece16b9bbab", size = 17165, upload-time = "2024-07-29T01:09:56.435Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/83/859ecdd180cacc13b1f7e857abf8582a64552ea7a061057a6c716e790fce/sphinxcontrib_qthelp-2.0.0-py3-none-any.whl", hash = "sha256:b18a828cdba941ccd6ee8445dbe72ffa3ef8cbe7505d8cd1fa0d42d3f2d5f3eb", size = 88743, upload-time = "2024-07-29T01:09:54.885Z" }, +] + +[[package]] +name = "sphinxcontrib-serializinghtml" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3b/44/6716b257b0aa6bfd51a1b31665d1c205fb12cb5ad56de752dfa15657de2f/sphinxcontrib_serializinghtml-2.0.0.tar.gz", hash = "sha256:e9d912827f872c029017a53f0ef2180b327c3f7fd23c87229f7a8e8b70031d4d", size = 16080, upload-time = "2024-07-29T01:10:09.332Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/a7/d2782e4e3f77c8450f727ba74a8f12756d5ba823d81b941f1b04da9d033a/sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl", hash = "sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331", size = 92072, upload-time = "2024-07-29T01:10:08.203Z" }, +] + +[[package]] +name = "urllib3" +version = "2.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, +] diff --git a/poetry.lock b/poetry.lock deleted file mode 100644 index df4278f..0000000 --- a/poetry.lock +++ /dev/null @@ -1,711 +0,0 @@ -[[package]] -name = "attrs" -version = "22.2.0" -description = "Classes Without Boilerplate" -category = "dev" -optional = false -python-versions = ">=3.6" - -[package.extras] -cov = ["attrs", "coverage-enable-subprocess", "coverage[toml] (>=5.3)"] -dev = ["attrs"] -docs = ["furo", "sphinx", "myst-parser", "zope.interface", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier"] -tests = ["attrs", "zope.interface"] -tests-no-zope = ["hypothesis", "pympler", "pytest (>=4.3.0)", "pytest-xdist", "cloudpickle", "mypy (>=0.971,<0.990)", "pytest-mypy-plugins"] -tests_no_zope = ["hypothesis", "pympler", "pytest (>=4.3.0)", "pytest-xdist", "cloudpickle", "mypy (>=0.971,<0.990)", "pytest-mypy-plugins"] - -[[package]] -name = "black" -version = "22.12.0" -description = "The uncompromising code formatter." -category = "dev" -optional = false -python-versions = ">=3.7" - -[package.dependencies] -click = ">=8.0.0" -mypy-extensions = ">=0.4.3" -pathspec = ">=0.9.0" -platformdirs = ">=2" -tomli = {version = ">=1.1.0", markers = "python_full_version < \"3.11.0a7\""} -typing-extensions = {version = ">=3.10.0.0", markers = "python_version < \"3.10\""} - -[package.extras] -colorama = ["colorama (>=0.4.3)"] -d = ["aiohttp (>=3.7.4)"] -jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] -uvloop = ["uvloop (>=0.15.2)"] - -[[package]] -name = "certifi" -version = "2022.12.7" -description = "Python package for providing Mozilla's CA Bundle." -category = "dev" -optional = false -python-versions = ">=3.6" - -[[package]] -name = "charset-normalizer" -version = "3.0.1" -description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." -category = "dev" -optional = false -python-versions = "*" - -[[package]] -name = "click" -version = "8.1.3" -description = "Composable command line interface toolkit" -category = "dev" -optional = false -python-versions = ">=3.7" - -[package.dependencies] -colorama = {version = "*", markers = "platform_system == \"Windows\""} - -[[package]] -name = "colorama" -version = "0.4.6" -description = "Cross-platform colored terminal text." -category = "dev" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" - -[[package]] -name = "coverage" -version = "7.1.0" -description = "Code coverage measurement for Python" -category = "dev" -optional = false -python-versions = ">=3.7" - -[package.dependencies] -tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} - -[package.extras] -toml = ["tomli"] - -[[package]] -name = "docker" -version = "6.0.1" -description = "A Python library for the Docker Engine API." -category = "dev" -optional = false -python-versions = ">=3.7" - -[package.dependencies] -packaging = ">=14.0" -pywin32 = {version = ">=304", markers = "sys_platform == \"win32\""} -requests = ">=2.26.0" -urllib3 = ">=1.26.0" -websocket-client = ">=0.32.0" - -[package.extras] -ssh = ["paramiko (>=2.4.3)"] - -[[package]] -name = "exceptiongroup" -version = "1.1.0" -description = "Backport of PEP 654 (exception groups)" -category = "dev" -optional = false -python-versions = ">=3.7" - -[package.extras] -test = ["pytest (>=6)"] - -[[package]] -name = "idna" -version = "3.4" -description = "Internationalized Domain Names in Applications (IDNA)" -category = "dev" -optional = false -python-versions = ">=3.5" - -[[package]] -name = "iniconfig" -version = "2.0.0" -description = "brain-dead simple config-ini parsing" -category = "dev" -optional = false -python-versions = ">=3.7" - -[[package]] -name = "mypy" -version = "0.971" -description = "Optional static typing for Python" -category = "dev" -optional = false -python-versions = ">=3.6" - -[package.dependencies] -mypy-extensions = ">=0.4.3" -tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} -typing-extensions = ">=3.10" - -[package.extras] -dmypy = ["psutil (>=4.0)"] -python2 = ["typed-ast (>=1.4.0,<2)"] -reports = ["lxml"] - -[[package]] -name = "mypy-extensions" -version = "1.0.0" -description = "Type system extensions for programs checked with the mypy type checker." -category = "dev" -optional = false -python-versions = ">=3.5" - -[[package]] -name = "packaging" -version = "23.0" -description = "Core utilities for Python packages" -category = "main" -optional = false -python-versions = ">=3.7" - -[[package]] -name = "pathspec" -version = "0.11.0" -description = "Utility library for gitignore style pattern matching of file paths." -category = "dev" -optional = false -python-versions = ">=3.7" - -[[package]] -name = "platformdirs" -version = "3.0.0" -description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." -category = "dev" -optional = false -python-versions = ">=3.7" - -[package.extras] -docs = ["furo (>=2022.12.7)", "proselint (>=0.13)", "sphinx-autodoc-typehints (>=1.22,!=1.23.4)", "sphinx (>=6.1.3)"] -test = ["appdirs (==1.4.4)", "covdefaults (>=2.2.2)", "pytest-cov (>=4)", "pytest-mock (>=3.10)", "pytest (>=7.2.1)"] - -[[package]] -name = "pluggy" -version = "1.0.0" -description = "plugin and hook calling mechanisms for python" -category = "dev" -optional = false -python-versions = ">=3.6" - -[package.extras] -testing = ["pytest-benchmark", "pytest"] -dev = ["tox", "pre-commit"] - -[[package]] -name = "psycopg2" -version = "2.9.5" -description = "psycopg2 - Python-PostgreSQL Database Adapter" -category = "main" -optional = true -python-versions = ">=3.6" - -[[package]] -name = "psycopg2-binary" -version = "2.9.5" -description = "psycopg2 - Python-PostgreSQL Database Adapter" -category = "main" -optional = true -python-versions = ">=3.6" - -[[package]] -name = "pytest" -version = "7.2.1" -description = "pytest: simple powerful testing with Python" -category = "dev" -optional = false -python-versions = ">=3.7" - -[package.dependencies] -attrs = ">=19.2.0" -colorama = {version = "*", markers = "sys_platform == \"win32\""} -exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} -iniconfig = "*" -packaging = "*" -pluggy = ">=0.12,<2.0" -tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} - -[package.extras] -testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"] - -[[package]] -name = "pytest-cov" -version = "3.0.0" -description = "Pytest plugin for measuring coverage." -category = "dev" -optional = false -python-versions = ">=3.6" - -[package.dependencies] -coverage = {version = ">=5.2.1", extras = ["toml"]} -pytest = ">=4.6" - -[package.extras] -testing = ["virtualenv", "pytest-xdist", "six", "process-tests", "hunter", "fields"] - -[[package]] -name = "pytest-mock" -version = "3.10.0" -description = "Thin-wrapper around the mock package for easier use with pytest" -category = "dev" -optional = false -python-versions = ">=3.7" - -[package.dependencies] -pytest = ">=5.0" - -[package.extras] -dev = ["pre-commit", "tox", "pytest-asyncio"] - -[[package]] -name = "pywin32" -version = "305" -description = "Python for Window Extensions" -category = "dev" -optional = false -python-versions = "*" - -[[package]] -name = "requests" -version = "2.28.2" -description = "Python HTTP for Humans." -category = "dev" -optional = false -python-versions = ">=3.7, <4" - -[package.dependencies] -certifi = ">=2017.4.17" -charset-normalizer = ">=2,<4" -idna = ">=2.5,<4" -urllib3 = ">=1.21.1,<1.27" - -[package.extras] -socks = ["PySocks (>=1.5.6,!=1.5.7)"] -use_chardet_on_py3 = ["chardet (>=3.0.2,<6)"] - -[[package]] -name = "tomli" -version = "2.0.1" -description = "A lil' TOML parser" -category = "dev" -optional = false -python-versions = ">=3.7" - -[[package]] -name = "typing-extensions" -version = "4.5.0" -description = "Backported and Experimental Type Hints for Python 3.7+" -category = "dev" -optional = false -python-versions = ">=3.7" - -[[package]] -name = "urllib3" -version = "1.26.14" -description = "HTTP library with thread-safe connection pooling, file post, and more." -category = "dev" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" - -[package.extras] -brotli = ["brotlicffi (>=0.8.0)", "brotli (>=1.0.9)", "brotlipy (>=0.6.0)"] -secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "urllib3-secure-extra", "ipaddress"] -socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] - -[[package]] -name = "websocket-client" -version = "1.5.1" -description = "WebSocket client for Python with low level API options" -category = "dev" -optional = false -python-versions = ">=3.7" - -[package.extras] -docs = ["Sphinx (>=3.4)", "sphinx-rtd-theme (>=0.5)"] -optional = ["python-socks", "wsaccel"] -test = ["websockets"] - -[extras] -psycopg2 = ["psycopg2"] -psycopg2-binary = ["psycopg2-binary"] - -[metadata] -lock-version = "1.1" -python-versions = ">=3.9,<4" -content-hash = "3806821f3a95b4e9f5f5508bddb1367a2f5333e48fd9a7985474bc6e4d235423" - -[metadata.files] -attrs = [ - {file = "attrs-22.2.0-py3-none-any.whl", hash = "sha256:29e95c7f6778868dbd49170f98f8818f78f3dc5e0e37c0b1f474e3561b240836"}, - {file = "attrs-22.2.0.tar.gz", hash = "sha256:c9227bfc2f01993c03f68db37d1d15c9690188323c067c641f1a35ca58185f99"}, -] -black = [ - {file = "black-22.12.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9eedd20838bd5d75b80c9f5487dbcb06836a43833a37846cf1d8c1cc01cef59d"}, - {file = "black-22.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:159a46a4947f73387b4d83e87ea006dbb2337eab6c879620a3ba52699b1f4351"}, - {file = "black-22.12.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d30b212bffeb1e252b31dd269dfae69dd17e06d92b87ad26e23890f3efea366f"}, - {file = "black-22.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:7412e75863aa5c5411886804678b7d083c7c28421210180d67dfd8cf1221e1f4"}, - {file = "black-22.12.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c116eed0efb9ff870ded8b62fe9f28dd61ef6e9ddd28d83d7d264a38417dcee2"}, - {file = "black-22.12.0-cp37-cp37m-win_amd64.whl", hash = "sha256:1f58cbe16dfe8c12b7434e50ff889fa479072096d79f0a7f25e4ab8e94cd8350"}, - {file = "black-22.12.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77d86c9f3db9b1bf6761244bc0b3572a546f5fe37917a044e02f3166d5aafa7d"}, - {file = "black-22.12.0-cp38-cp38-win_amd64.whl", hash = "sha256:82d9fe8fee3401e02e79767016b4907820a7dc28d70d137eb397b92ef3cc5bfc"}, - {file = "black-22.12.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:101c69b23df9b44247bd88e1d7e90154336ac4992502d4197bdac35dd7ee3320"}, - {file = "black-22.12.0-cp39-cp39-win_amd64.whl", hash = "sha256:559c7a1ba9a006226f09e4916060982fd27334ae1998e7a38b3f33a37f7a2148"}, - {file = "black-22.12.0-py3-none-any.whl", hash = "sha256:436cc9167dd28040ad90d3b404aec22cedf24a6e4d7de221bec2730ec0c97bcf"}, - {file = "black-22.12.0.tar.gz", hash = "sha256:229351e5a18ca30f447bf724d007f890f97e13af070bb6ad4c0a441cd7596a2f"}, -] -certifi = [ - {file = "certifi-2022.12.7-py3-none-any.whl", hash = "sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18"}, - {file = "certifi-2022.12.7.tar.gz", hash = "sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3"}, -] -charset-normalizer = [ - {file = "charset-normalizer-3.0.1.tar.gz", hash = "sha256:ebea339af930f8ca5d7a699b921106c6e29c617fe9606fa7baa043c1cdae326f"}, - {file = "charset_normalizer-3.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:88600c72ef7587fe1708fd242b385b6ed4b8904976d5da0893e31df8b3480cb6"}, - {file = "charset_normalizer-3.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c75ffc45f25324e68ab238cb4b5c0a38cd1c3d7f1fb1f72b5541de469e2247db"}, - {file = "charset_normalizer-3.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:db72b07027db150f468fbada4d85b3b2729a3db39178abf5c543b784c1254539"}, - {file = "charset_normalizer-3.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62595ab75873d50d57323a91dd03e6966eb79c41fa834b7a1661ed043b2d404d"}, - {file = "charset_normalizer-3.0.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ff6f3db31555657f3163b15a6b7c6938d08df7adbfc9dd13d9d19edad678f1e8"}, - {file = "charset_normalizer-3.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:772b87914ff1152b92a197ef4ea40efe27a378606c39446ded52c8f80f79702e"}, - {file = "charset_normalizer-3.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70990b9c51340e4044cfc394a81f614f3f90d41397104d226f21e66de668730d"}, - {file = "charset_normalizer-3.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:292d5e8ba896bbfd6334b096e34bffb56161c81408d6d036a7dfa6929cff8783"}, - {file = "charset_normalizer-3.0.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:2edb64ee7bf1ed524a1da60cdcd2e1f6e2b4f66ef7c077680739f1641f62f555"}, - {file = "charset_normalizer-3.0.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:31a9ddf4718d10ae04d9b18801bd776693487cbb57d74cc3458a7673f6f34639"}, - {file = "charset_normalizer-3.0.1-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:44ba614de5361b3e5278e1241fda3dc1838deed864b50a10d7ce92983797fa76"}, - {file = "charset_normalizer-3.0.1-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:12db3b2c533c23ab812c2b25934f60383361f8a376ae272665f8e48b88e8e1c6"}, - {file = "charset_normalizer-3.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c512accbd6ff0270939b9ac214b84fb5ada5f0409c44298361b2f5e13f9aed9e"}, - {file = "charset_normalizer-3.0.1-cp310-cp310-win32.whl", hash = "sha256:502218f52498a36d6bf5ea77081844017bf7982cdbe521ad85e64cabee1b608b"}, - {file = "charset_normalizer-3.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:601f36512f9e28f029d9481bdaf8e89e5148ac5d89cffd3b05cd533eeb423b59"}, - {file = "charset_normalizer-3.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0298eafff88c99982a4cf66ba2efa1128e4ddaca0b05eec4c456bbc7db691d8d"}, - {file = "charset_normalizer-3.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a8d0fc946c784ff7f7c3742310cc8a57c5c6dc31631269876a88b809dbeff3d3"}, - {file = "charset_normalizer-3.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:87701167f2a5c930b403e9756fab1d31d4d4da52856143b609e30a1ce7160f3c"}, - {file = "charset_normalizer-3.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:14e76c0f23218b8f46c4d87018ca2e441535aed3632ca134b10239dfb6dadd6b"}, - {file = "charset_normalizer-3.0.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0c0a590235ccd933d9892c627dec5bc7511ce6ad6c1011fdf5b11363022746c1"}, - {file = "charset_normalizer-3.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c7fe7afa480e3e82eed58e0ca89f751cd14d767638e2550c77a92a9e749c317"}, - {file = "charset_normalizer-3.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:79909e27e8e4fcc9db4addea88aa63f6423ebb171db091fb4373e3312cb6d603"}, - {file = "charset_normalizer-3.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8ac7b6a045b814cf0c47f3623d21ebd88b3e8cf216a14790b455ea7ff0135d18"}, - {file = "charset_normalizer-3.0.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:72966d1b297c741541ca8cf1223ff262a6febe52481af742036a0b296e35fa5a"}, - {file = "charset_normalizer-3.0.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:f9d0c5c045a3ca9bedfc35dca8526798eb91a07aa7a2c0fee134c6c6f321cbd7"}, - {file = "charset_normalizer-3.0.1-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:5995f0164fa7df59db4746112fec3f49c461dd6b31b841873443bdb077c13cfc"}, - {file = "charset_normalizer-3.0.1-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4a8fcf28c05c1f6d7e177a9a46a1c52798bfe2ad80681d275b10dcf317deaf0b"}, - {file = "charset_normalizer-3.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:761e8904c07ad053d285670f36dd94e1b6ab7f16ce62b9805c475b7aa1cffde6"}, - {file = "charset_normalizer-3.0.1-cp311-cp311-win32.whl", hash = "sha256:71140351489970dfe5e60fc621ada3e0f41104a5eddaca47a7acb3c1b851d6d3"}, - {file = "charset_normalizer-3.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:9ab77acb98eba3fd2a85cd160851816bfce6871d944d885febf012713f06659c"}, - {file = "charset_normalizer-3.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:84c3990934bae40ea69a82034912ffe5a62c60bbf6ec5bc9691419641d7d5c9a"}, - {file = "charset_normalizer-3.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:74292fc76c905c0ef095fe11e188a32ebd03bc38f3f3e9bcb85e4e6db177b7ea"}, - {file = "charset_normalizer-3.0.1-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c95a03c79bbe30eec3ec2b7f076074f4281526724c8685a42872974ef4d36b72"}, - {file = "charset_normalizer-3.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f4c39b0e3eac288fedc2b43055cfc2ca7a60362d0e5e87a637beac5d801ef478"}, - {file = "charset_normalizer-3.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:df2c707231459e8a4028eabcd3cfc827befd635b3ef72eada84ab13b52e1574d"}, - {file = "charset_normalizer-3.0.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:93ad6d87ac18e2a90b0fe89df7c65263b9a99a0eb98f0a3d2e079f12a0735837"}, - {file = "charset_normalizer-3.0.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:59e5686dd847347e55dffcc191a96622f016bc0ad89105e24c14e0d6305acbc6"}, - {file = "charset_normalizer-3.0.1-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:cd6056167405314a4dc3c173943f11249fa0f1b204f8b51ed4bde1a9cd1834dc"}, - {file = "charset_normalizer-3.0.1-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:083c8d17153ecb403e5e1eb76a7ef4babfc2c48d58899c98fcaa04833e7a2f9a"}, - {file = "charset_normalizer-3.0.1-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:f5057856d21e7586765171eac8b9fc3f7d44ef39425f85dbcccb13b3ebea806c"}, - {file = "charset_normalizer-3.0.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:7eb33a30d75562222b64f569c642ff3dc6689e09adda43a082208397f016c39a"}, - {file = "charset_normalizer-3.0.1-cp36-cp36m-win32.whl", hash = "sha256:95dea361dd73757c6f1c0a1480ac499952c16ac83f7f5f4f84f0658a01b8ef41"}, - {file = "charset_normalizer-3.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:eaa379fcd227ca235d04152ca6704c7cb55564116f8bc52545ff357628e10602"}, - {file = "charset_normalizer-3.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:3e45867f1f2ab0711d60c6c71746ac53537f1684baa699f4f668d4c6f6ce8e14"}, - {file = "charset_normalizer-3.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cadaeaba78750d58d3cc6ac4d1fd867da6fc73c88156b7a3212a3cd4819d679d"}, - {file = "charset_normalizer-3.0.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:911d8a40b2bef5b8bbae2e36a0b103f142ac53557ab421dc16ac4aafee6f53dc"}, - {file = "charset_normalizer-3.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:503e65837c71b875ecdd733877d852adbc465bd82c768a067badd953bf1bc5a3"}, - {file = "charset_normalizer-3.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a60332922359f920193b1d4826953c507a877b523b2395ad7bc716ddd386d866"}, - {file = "charset_normalizer-3.0.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:16a8663d6e281208d78806dbe14ee9903715361cf81f6d4309944e4d1e59ac5b"}, - {file = "charset_normalizer-3.0.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:a16418ecf1329f71df119e8a65f3aa68004a3f9383821edcb20f0702934d8087"}, - {file = "charset_normalizer-3.0.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:9d9153257a3f70d5f69edf2325357251ed20f772b12e593f3b3377b5f78e7ef8"}, - {file = "charset_normalizer-3.0.1-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:02a51034802cbf38db3f89c66fb5d2ec57e6fe7ef2f4a44d070a593c3688667b"}, - {file = "charset_normalizer-3.0.1-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:2e396d70bc4ef5325b72b593a72c8979999aa52fb8bcf03f701c1b03e1166918"}, - {file = "charset_normalizer-3.0.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:11b53acf2411c3b09e6af37e4b9005cba376c872503c8f28218c7243582df45d"}, - {file = "charset_normalizer-3.0.1-cp37-cp37m-win32.whl", hash = "sha256:0bf2dae5291758b6f84cf923bfaa285632816007db0330002fa1de38bfcb7154"}, - {file = "charset_normalizer-3.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:2c03cc56021a4bd59be889c2b9257dae13bf55041a3372d3295416f86b295fb5"}, - {file = "charset_normalizer-3.0.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:024e606be3ed92216e2b6952ed859d86b4cfa52cd5bc5f050e7dc28f9b43ec42"}, - {file = "charset_normalizer-3.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:4b0d02d7102dd0f997580b51edc4cebcf2ab6397a7edf89f1c73b586c614272c"}, - {file = "charset_normalizer-3.0.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:358a7c4cb8ba9b46c453b1dd8d9e431452d5249072e4f56cfda3149f6ab1405e"}, - {file = "charset_normalizer-3.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:81d6741ab457d14fdedc215516665050f3822d3e56508921cc7239f8c8e66a58"}, - {file = "charset_normalizer-3.0.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8b8af03d2e37866d023ad0ddea594edefc31e827fee64f8de5611a1dbc373174"}, - {file = "charset_normalizer-3.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9cf4e8ad252f7c38dd1f676b46514f92dc0ebeb0db5552f5f403509705e24753"}, - {file = "charset_normalizer-3.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e696f0dd336161fca9adbb846875d40752e6eba585843c768935ba5c9960722b"}, - {file = "charset_normalizer-3.0.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c22d3fe05ce11d3671297dc8973267daa0f938b93ec716e12e0f6dee81591dc1"}, - {file = "charset_normalizer-3.0.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:109487860ef6a328f3eec66f2bf78b0b72400280d8f8ea05f69c51644ba6521a"}, - {file = "charset_normalizer-3.0.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:37f8febc8ec50c14f3ec9637505f28e58d4f66752207ea177c1d67df25da5aed"}, - {file = "charset_normalizer-3.0.1-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:f97e83fa6c25693c7a35de154681fcc257c1c41b38beb0304b9c4d2d9e164479"}, - {file = "charset_normalizer-3.0.1-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:a152f5f33d64a6be73f1d30c9cc82dfc73cec6477ec268e7c6e4c7d23c2d2291"}, - {file = "charset_normalizer-3.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:39049da0ffb96c8cbb65cbf5c5f3ca3168990adf3551bd1dee10c48fce8ae820"}, - {file = "charset_normalizer-3.0.1-cp38-cp38-win32.whl", hash = "sha256:4457ea6774b5611f4bed5eaa5df55f70abde42364d498c5134b7ef4c6958e20e"}, - {file = "charset_normalizer-3.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:e62164b50f84e20601c1ff8eb55620d2ad25fb81b59e3cd776a1902527a788af"}, - {file = "charset_normalizer-3.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8eade758719add78ec36dc13201483f8e9b5d940329285edcd5f70c0a9edbd7f"}, - {file = "charset_normalizer-3.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8499ca8f4502af841f68135133d8258f7b32a53a1d594aa98cc52013fff55678"}, - {file = "charset_normalizer-3.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3fc1c4a2ffd64890aebdb3f97e1278b0cc72579a08ca4de8cd2c04799a3a22be"}, - {file = "charset_normalizer-3.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:00d3ffdaafe92a5dc603cb9bd5111aaa36dfa187c8285c543be562e61b755f6b"}, - {file = "charset_normalizer-3.0.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c2ac1b08635a8cd4e0cbeaf6f5e922085908d48eb05d44c5ae9eabab148512ca"}, - {file = "charset_normalizer-3.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f6f45710b4459401609ebebdbcfb34515da4fc2aa886f95107f556ac69a9147e"}, - {file = "charset_normalizer-3.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ae1de54a77dc0d6d5fcf623290af4266412a7c4be0b1ff7444394f03f5c54e3"}, - {file = "charset_normalizer-3.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3b590df687e3c5ee0deef9fc8c547d81986d9a1b56073d82de008744452d6541"}, - {file = "charset_normalizer-3.0.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ab5de034a886f616a5668aa5d098af2b5385ed70142090e2a31bcbd0af0fdb3d"}, - {file = "charset_normalizer-3.0.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9cb3032517f1627cc012dbc80a8ec976ae76d93ea2b5feaa9d2a5b8882597579"}, - {file = "charset_normalizer-3.0.1-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:608862a7bf6957f2333fc54ab4399e405baad0163dc9f8d99cb236816db169d4"}, - {file = "charset_normalizer-3.0.1-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:0f438ae3532723fb6ead77e7c604be7c8374094ef4ee2c5e03a3a17f1fca256c"}, - {file = "charset_normalizer-3.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:356541bf4381fa35856dafa6a965916e54bed415ad8a24ee6de6e37deccf2786"}, - {file = "charset_normalizer-3.0.1-cp39-cp39-win32.whl", hash = "sha256:39cf9ed17fe3b1bc81f33c9ceb6ce67683ee7526e65fde1447c772afc54a1bb8"}, - {file = "charset_normalizer-3.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:0a11e971ed097d24c534c037d298ad32c6ce81a45736d31e0ff0ad37ab437d59"}, - {file = "charset_normalizer-3.0.1-py3-none-any.whl", hash = "sha256:7e189e2e1d3ed2f4aebabd2d5b0f931e883676e51c7624826e0a4e5fe8a0bf24"}, -] -click = [ - {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"}, - {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, -] -colorama = [ - {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, - {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, -] -coverage = [ - {file = "coverage-7.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3b946bbcd5a8231383450b195cfb58cb01cbe7f8949f5758566b881df4b33baf"}, - {file = "coverage-7.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ec8e767f13be637d056f7e07e61d089e555f719b387a7070154ad80a0ff31801"}, - {file = "coverage-7.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4a5a5879a939cb84959d86869132b00176197ca561c664fc21478c1eee60d75"}, - {file = "coverage-7.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b643cb30821e7570c0aaf54feaf0bfb630b79059f85741843e9dc23f33aaca2c"}, - {file = "coverage-7.1.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:32df215215f3af2c1617a55dbdfb403b772d463d54d219985ac7cd3bf124cada"}, - {file = "coverage-7.1.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:33d1ae9d4079e05ac4cc1ef9e20c648f5afabf1a92adfaf2ccf509c50b85717f"}, - {file = "coverage-7.1.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:29571503c37f2ef2138a306d23e7270687c0efb9cab4bd8038d609b5c2393a3a"}, - {file = "coverage-7.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:63ffd21aa133ff48c4dff7adcc46b7ec8b565491bfc371212122dd999812ea1c"}, - {file = "coverage-7.1.0-cp310-cp310-win32.whl", hash = "sha256:4b14d5e09c656de5038a3f9bfe5228f53439282abcab87317c9f7f1acb280352"}, - {file = "coverage-7.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:8361be1c2c073919500b6601220a6f2f98ea0b6d2fec5014c1d9cfa23dd07038"}, - {file = "coverage-7.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:da9b41d4539eefd408c46725fb76ecba3a50a3367cafb7dea5f250d0653c1040"}, - {file = "coverage-7.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c5b15ed7644ae4bee0ecf74fee95808dcc34ba6ace87e8dfbf5cb0dc20eab45a"}, - {file = "coverage-7.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d12d076582507ea460ea2a89a8c85cb558f83406c8a41dd641d7be9a32e1274f"}, - {file = "coverage-7.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e2617759031dae1bf183c16cef8fcfb3de7617f394c813fa5e8e46e9b82d4222"}, - {file = "coverage-7.1.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c4e4881fa9e9667afcc742f0c244d9364d197490fbc91d12ac3b5de0bf2df146"}, - {file = "coverage-7.1.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:9d58885215094ab4a86a6aef044e42994a2bd76a446dc59b352622655ba6621b"}, - {file = "coverage-7.1.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:ffeeb38ee4a80a30a6877c5c4c359e5498eec095878f1581453202bfacc8fbc2"}, - {file = "coverage-7.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3baf5f126f30781b5e93dbefcc8271cb2491647f8283f20ac54d12161dff080e"}, - {file = "coverage-7.1.0-cp311-cp311-win32.whl", hash = "sha256:ded59300d6330be27bc6cf0b74b89ada58069ced87c48eaf9344e5e84b0072f7"}, - {file = "coverage-7.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:6a43c7823cd7427b4ed763aa7fb63901ca8288591323b58c9cd6ec31ad910f3c"}, - {file = "coverage-7.1.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:7a726d742816cb3a8973c8c9a97539c734b3a309345236cd533c4883dda05b8d"}, - {file = "coverage-7.1.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bc7c85a150501286f8b56bd8ed3aa4093f4b88fb68c0843d21ff9656f0009d6a"}, - {file = "coverage-7.1.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f5b4198d85a3755d27e64c52f8c95d6333119e49fd001ae5798dac872c95e0f8"}, - {file = "coverage-7.1.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ddb726cb861c3117a553f940372a495fe1078249ff5f8a5478c0576c7be12050"}, - {file = "coverage-7.1.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:51b236e764840a6df0661b67e50697aaa0e7d4124ca95e5058fa3d7cbc240b7c"}, - {file = "coverage-7.1.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:7ee5c9bb51695f80878faaa5598040dd6c9e172ddcf490382e8aedb8ec3fec8d"}, - {file = "coverage-7.1.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:c31b75ae466c053a98bf26843563b3b3517b8f37da4d47b1c582fdc703112bc3"}, - {file = "coverage-7.1.0-cp37-cp37m-win32.whl", hash = "sha256:3b155caf3760408d1cb903b21e6a97ad4e2bdad43cbc265e3ce0afb8e0057e73"}, - {file = "coverage-7.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:2a60d6513781e87047c3e630b33b4d1e89f39836dac6e069ffee28c4786715f5"}, - {file = "coverage-7.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f2cba5c6db29ce991029b5e4ac51eb36774458f0a3b8d3137241b32d1bb91f06"}, - {file = "coverage-7.1.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:beeb129cacea34490ffd4d6153af70509aa3cda20fdda2ea1a2be870dfec8d52"}, - {file = "coverage-7.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0c45948f613d5d18c9ec5eaa203ce06a653334cf1bd47c783a12d0dd4fd9c851"}, - {file = "coverage-7.1.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ef382417db92ba23dfb5864a3fc9be27ea4894e86620d342a116b243ade5d35d"}, - {file = "coverage-7.1.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7c7c0d0827e853315c9bbd43c1162c006dd808dbbe297db7ae66cd17b07830f0"}, - {file = "coverage-7.1.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:e5cdbb5cafcedea04924568d990e20ce7f1945a1dd54b560f879ee2d57226912"}, - {file = "coverage-7.1.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:9817733f0d3ea91bea80de0f79ef971ae94f81ca52f9b66500c6a2fea8e4b4f8"}, - {file = "coverage-7.1.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:218fe982371ac7387304153ecd51205f14e9d731b34fb0568181abaf7b443ba0"}, - {file = "coverage-7.1.0-cp38-cp38-win32.whl", hash = "sha256:04481245ef966fbd24ae9b9e537ce899ae584d521dfbe78f89cad003c38ca2ab"}, - {file = "coverage-7.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:8ae125d1134bf236acba8b83e74c603d1b30e207266121e76484562bc816344c"}, - {file = "coverage-7.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2bf1d5f2084c3932b56b962a683074a3692bce7cabd3aa023c987a2a8e7612f6"}, - {file = "coverage-7.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:98b85dd86514d889a2e3dd22ab3c18c9d0019e696478391d86708b805f4ea0fa"}, - {file = "coverage-7.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38da2db80cc505a611938d8624801158e409928b136c8916cd2e203970dde4dc"}, - {file = "coverage-7.1.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3164d31078fa9efe406e198aecd2a02d32a62fecbdef74f76dad6a46c7e48311"}, - {file = "coverage-7.1.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db61a79c07331e88b9a9974815c075fbd812bc9dbc4dc44b366b5368a2936063"}, - {file = "coverage-7.1.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9ccb092c9ede70b2517a57382a601619d20981f56f440eae7e4d7eaafd1d1d09"}, - {file = "coverage-7.1.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:33ff26d0f6cc3ca8de13d14fde1ff8efe1456b53e3f0273e63cc8b3c84a063d8"}, - {file = "coverage-7.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d47dd659a4ee952e90dc56c97d78132573dc5c7b09d61b416a9deef4ebe01a0c"}, - {file = "coverage-7.1.0-cp39-cp39-win32.whl", hash = "sha256:d248cd4a92065a4d4543b8331660121b31c4148dd00a691bfb7a5cdc7483cfa4"}, - {file = "coverage-7.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:7ed681b0f8e8bcbbffa58ba26fcf5dbc8f79e7997595bf071ed5430d8c08d6f3"}, - {file = "coverage-7.1.0-pp37.pp38.pp39-none-any.whl", hash = "sha256:755e89e32376c850f826c425ece2c35a4fc266c081490eb0a841e7c1cb0d3bda"}, - {file = "coverage-7.1.0.tar.gz", hash = "sha256:10188fe543560ec4874f974b5305cd1a8bdcfa885ee00ea3a03733464c4ca265"}, -] -docker = [ - {file = "docker-6.0.1-py3-none-any.whl", hash = "sha256:dbcb3bd2fa80dca0788ed908218bf43972772009b881ed1e20dfc29a65e49782"}, - {file = "docker-6.0.1.tar.gz", hash = "sha256:896c4282e5c7af5c45e8b683b0b0c33932974fe6e50fc6906a0a83616ab3da97"}, -] -exceptiongroup = [ - {file = "exceptiongroup-1.1.0-py3-none-any.whl", hash = "sha256:327cbda3da756e2de031a3107b81ab7b3770a602c4d16ca618298c526f4bec1e"}, - {file = "exceptiongroup-1.1.0.tar.gz", hash = "sha256:bcb67d800a4497e1b404c2dd44fca47d3b7a5e5433dbab67f96c1a685cdfdf23"}, -] -idna = [ - {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, - {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, -] -iniconfig = [ - {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, - {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, -] -mypy = [ - {file = "mypy-0.971-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f2899a3cbd394da157194f913a931edfd4be5f274a88041c9dc2d9cdcb1c315c"}, - {file = "mypy-0.971-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:98e02d56ebe93981c41211c05adb630d1d26c14195d04d95e49cd97dbc046dc5"}, - {file = "mypy-0.971-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:19830b7dba7d5356d3e26e2427a2ec91c994cd92d983142cbd025ebe81d69cf3"}, - {file = "mypy-0.971-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:02ef476f6dcb86e6f502ae39a16b93285fef97e7f1ff22932b657d1ef1f28655"}, - {file = "mypy-0.971-cp310-cp310-win_amd64.whl", hash = "sha256:25c5750ba5609a0c7550b73a33deb314ecfb559c350bb050b655505e8aed4103"}, - {file = "mypy-0.971-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:d3348e7eb2eea2472db611486846742d5d52d1290576de99d59edeb7cd4a42ca"}, - {file = "mypy-0.971-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:3fa7a477b9900be9b7dd4bab30a12759e5abe9586574ceb944bc29cddf8f0417"}, - {file = "mypy-0.971-cp36-cp36m-win_amd64.whl", hash = "sha256:2ad53cf9c3adc43cf3bea0a7d01a2f2e86db9fe7596dfecb4496a5dda63cbb09"}, - {file = "mypy-0.971-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:855048b6feb6dfe09d3353466004490b1872887150c5bb5caad7838b57328cc8"}, - {file = "mypy-0.971-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:23488a14a83bca6e54402c2e6435467a4138785df93ec85aeff64c6170077fb0"}, - {file = "mypy-0.971-cp37-cp37m-win_amd64.whl", hash = "sha256:4b21e5b1a70dfb972490035128f305c39bc4bc253f34e96a4adf9127cf943eb2"}, - {file = "mypy-0.971-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:9796a2ba7b4b538649caa5cecd398d873f4022ed2333ffde58eaf604c4d2cb27"}, - {file = "mypy-0.971-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:5a361d92635ad4ada1b1b2d3630fc2f53f2127d51cf2def9db83cba32e47c856"}, - {file = "mypy-0.971-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b793b899f7cf563b1e7044a5c97361196b938e92f0a4343a5d27966a53d2ec71"}, - {file = "mypy-0.971-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:d1ea5d12c8e2d266b5fb8c7a5d2e9c0219fedfeb493b7ed60cd350322384ac27"}, - {file = "mypy-0.971-cp38-cp38-win_amd64.whl", hash = "sha256:23c7ff43fff4b0df93a186581885c8512bc50fc4d4910e0f838e35d6bb6b5e58"}, - {file = "mypy-0.971-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:1f7656b69974a6933e987ee8ffb951d836272d6c0f81d727f1d0e2696074d9e6"}, - {file = "mypy-0.971-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d2022bfadb7a5c2ef410d6a7c9763188afdb7f3533f22a0a32be10d571ee4bbe"}, - {file = "mypy-0.971-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ef943c72a786b0f8d90fd76e9b39ce81fb7171172daf84bf43eaf937e9f220a9"}, - {file = "mypy-0.971-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:d744f72eb39f69312bc6c2abf8ff6656973120e2eb3f3ec4f758ed47e414a4bf"}, - {file = "mypy-0.971-cp39-cp39-win_amd64.whl", hash = "sha256:77a514ea15d3007d33a9e2157b0ba9c267496acf12a7f2b9b9f8446337aac5b0"}, - {file = "mypy-0.971-py3-none-any.whl", hash = "sha256:0d054ef16b071149917085f51f89555a576e2618d5d9dd70bd6eea6410af3ac9"}, - {file = "mypy-0.971.tar.gz", hash = "sha256:40b0f21484238269ae6a57200c807d80debc6459d444c0489a102d7c6a75fa56"}, -] -mypy-extensions = [ - {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, - {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, -] -packaging = [ - {file = "packaging-23.0-py3-none-any.whl", hash = "sha256:714ac14496c3e68c99c29b00845f7a2b85f3bb6f1078fd9f72fd20f0570002b2"}, - {file = "packaging-23.0.tar.gz", hash = "sha256:b6ad297f8907de0fa2fe1ccbd26fdaf387f5f47c7275fedf8cce89f99446cf97"}, -] -pathspec = [ - {file = "pathspec-0.11.0-py3-none-any.whl", hash = "sha256:3a66eb970cbac598f9e5ccb5b2cf58930cd8e3ed86d393d541eaf2d8b1705229"}, - {file = "pathspec-0.11.0.tar.gz", hash = "sha256:64d338d4e0914e91c1792321e6907b5a593f1ab1851de7fc269557a21b30ebbc"}, -] -platformdirs = [ - {file = "platformdirs-3.0.0-py3-none-any.whl", hash = "sha256:b1d5eb14f221506f50d6604a561f4c5786d9e80355219694a1b244bcd96f4567"}, - {file = "platformdirs-3.0.0.tar.gz", hash = "sha256:8a1228abb1ef82d788f74139988b137e78692984ec7b08eaa6c65f1723af28f9"}, -] -pluggy = [ - {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, - {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, -] -psycopg2 = [ - {file = "psycopg2-2.9.5-cp310-cp310-win32.whl", hash = "sha256:d3ef67e630b0de0779c42912fe2cbae3805ebaba30cda27fea2a3de650a9414f"}, - {file = "psycopg2-2.9.5-cp310-cp310-win_amd64.whl", hash = "sha256:4cb9936316d88bfab614666eb9e32995e794ed0f8f6b3b718666c22819c1d7ee"}, - {file = "psycopg2-2.9.5-cp311-cp311-win32.whl", hash = "sha256:093e3894d2d3c592ab0945d9eba9d139c139664dcf83a1c440b8a7aa9bb21955"}, - {file = "psycopg2-2.9.5-cp311-cp311-win_amd64.whl", hash = "sha256:920bf418000dd17669d2904472efeab2b20546efd0548139618f8fa305d1d7ad"}, - {file = "psycopg2-2.9.5-cp36-cp36m-win32.whl", hash = "sha256:b9ac1b0d8ecc49e05e4e182694f418d27f3aedcfca854ebd6c05bb1cffa10d6d"}, - {file = "psycopg2-2.9.5-cp36-cp36m-win_amd64.whl", hash = "sha256:fc04dd5189b90d825509caa510f20d1d504761e78b8dfb95a0ede180f71d50e5"}, - {file = "psycopg2-2.9.5-cp37-cp37m-win32.whl", hash = "sha256:922cc5f0b98a5f2b1ff481f5551b95cd04580fd6f0c72d9b22e6c0145a4840e0"}, - {file = "psycopg2-2.9.5-cp37-cp37m-win_amd64.whl", hash = "sha256:1e5a38aa85bd660c53947bd28aeaafb6a97d70423606f1ccb044a03a1203fe4a"}, - {file = "psycopg2-2.9.5-cp38-cp38-win32.whl", hash = "sha256:f5b6320dbc3cf6cfb9f25308286f9f7ab464e65cfb105b64cc9c52831748ced2"}, - {file = "psycopg2-2.9.5-cp38-cp38-win_amd64.whl", hash = "sha256:1a5c7d7d577e0eabfcf15eb87d1e19314c8c4f0e722a301f98e0e3a65e238b4e"}, - {file = "psycopg2-2.9.5-cp39-cp39-win32.whl", hash = "sha256:322fd5fca0b1113677089d4ebd5222c964b1760e361f151cbb2706c4912112c5"}, - {file = "psycopg2-2.9.5-cp39-cp39-win_amd64.whl", hash = "sha256:190d51e8c1b25a47484e52a79638a8182451d6f6dff99f26ad9bd81e5359a0fa"}, - {file = "psycopg2-2.9.5.tar.gz", hash = "sha256:a5246d2e683a972e2187a8714b5c2cf8156c064629f9a9b1a873c1730d9e245a"}, -] -psycopg2-binary = [ - {file = "psycopg2-binary-2.9.5.tar.gz", hash = "sha256:33e632d0885b95a8b97165899006c40e9ecdc634a529dca7b991eb7de4ece41c"}, - {file = "psycopg2_binary-2.9.5-cp310-cp310-macosx_10_15_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:0775d6252ccb22b15da3b5d7adbbf8cfe284916b14b6dc0ff503a23edb01ee85"}, - {file = "psycopg2_binary-2.9.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2ec46ed947801652c9643e0b1dc334cfb2781232e375ba97312c2fc256597632"}, - {file = "psycopg2_binary-2.9.5-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3520d7af1ebc838cc6084a3281145d5cd5bdd43fdef139e6db5af01b92596cb7"}, - {file = "psycopg2_binary-2.9.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5cbc554ba47ecca8cd3396ddaca85e1ecfe3e48dd57dc5e415e59551affe568e"}, - {file = "psycopg2_binary-2.9.5-cp310-cp310-manylinux_2_24_aarch64.whl", hash = "sha256:5d28ecdf191db558d0c07d0f16524ee9d67896edf2b7990eea800abeb23ebd61"}, - {file = "psycopg2_binary-2.9.5-cp310-cp310-manylinux_2_24_ppc64le.whl", hash = "sha256:b9c33d4aef08dfecbd1736ceab8b7b3c4358bf10a0121483e5cd60d3d308cc64"}, - {file = "psycopg2_binary-2.9.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:05b3d479425e047c848b9782cd7aac9c6727ce23181eb9647baf64ffdfc3da41"}, - {file = "psycopg2_binary-2.9.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:1e491e6489a6cb1d079df8eaa15957c277fdedb102b6a68cfbf40c4994412fd0"}, - {file = "psycopg2_binary-2.9.5-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:9e32cedc389bcb76d9f24ea8a012b3cb8385ee362ea437e1d012ffaed106c17d"}, - {file = "psycopg2_binary-2.9.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:46850a640df62ae940e34a163f72e26aca1f88e2da79148e1862faaac985c302"}, - {file = "psycopg2_binary-2.9.5-cp310-cp310-win32.whl", hash = "sha256:3d790f84201c3698d1bfb404c917f36e40531577a6dda02e45ba29b64d539867"}, - {file = "psycopg2_binary-2.9.5-cp310-cp310-win_amd64.whl", hash = "sha256:1764546ffeaed4f9428707be61d68972eb5ede81239b46a45843e0071104d0dd"}, - {file = "psycopg2_binary-2.9.5-cp311-cp311-macosx_10_9_universal2.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:426c2ae999135d64e6a18849a7d1ad0e1bd007277e4a8f4752eaa40a96b550ff"}, - {file = "psycopg2_binary-2.9.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7cf1d44e710ca3a9ce952bda2855830fe9f9017ed6259e01fcd71ea6287565f5"}, - {file = "psycopg2_binary-2.9.5-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:024030b13bdcbd53d8a93891a2cf07719715724fc9fee40243f3bd78b4264b8f"}, - {file = "psycopg2_binary-2.9.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bcda1c84a1c533c528356da5490d464a139b6e84eb77cc0b432e38c5c6dd7882"}, - {file = "psycopg2_binary-2.9.5-cp311-cp311-manylinux_2_24_aarch64.whl", hash = "sha256:2ef892cabdccefe577088a79580301f09f2a713eb239f4f9f62b2b29cafb0577"}, - {file = "psycopg2_binary-2.9.5-cp311-cp311-manylinux_2_24_ppc64le.whl", hash = "sha256:af0516e1711995cb08dc19bbd05bec7dbdebf4185f68870595156718d237df3e"}, - {file = "psycopg2_binary-2.9.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e72c91bda9880f097c8aa3601a2c0de6c708763ba8128006151f496ca9065935"}, - {file = "psycopg2_binary-2.9.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:e67b3c26e9b6d37b370c83aa790bbc121775c57bfb096c2e77eacca25fd0233b"}, - {file = "psycopg2_binary-2.9.5-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:5fc447058d083b8c6ac076fc26b446d44f0145308465d745fba93a28c14c9e32"}, - {file = "psycopg2_binary-2.9.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d892bfa1d023c3781a3cab8dd5af76b626c483484d782e8bd047c180db590e4c"}, - {file = "psycopg2_binary-2.9.5-cp311-cp311-win32.whl", hash = "sha256:2abccab84d057723d2ca8f99ff7b619285d40da6814d50366f61f0fc385c3903"}, - {file = "psycopg2_binary-2.9.5-cp311-cp311-win_amd64.whl", hash = "sha256:bef7e3f9dc6f0c13afdd671008534be5744e0e682fb851584c8c3a025ec09720"}, - {file = "psycopg2_binary-2.9.5-cp36-cp36m-macosx_10_14_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:6e63814ec71db9bdb42905c925639f319c80e7909fb76c3b84edc79dadef8d60"}, - {file = "psycopg2_binary-2.9.5-cp36-cp36m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:212757ffcecb3e1a5338d4e6761bf9c04f750e7d027117e74aa3cd8a75bb6fbd"}, - {file = "psycopg2_binary-2.9.5-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f8a9bcab7b6db2e3dbf65b214dfc795b4c6b3bb3af922901b6a67f7cb47d5f8"}, - {file = "psycopg2_binary-2.9.5-cp36-cp36m-manylinux_2_24_aarch64.whl", hash = "sha256:56b2957a145f816726b109ee3d4e6822c23f919a7d91af5a94593723ed667835"}, - {file = "psycopg2_binary-2.9.5-cp36-cp36m-manylinux_2_24_ppc64le.whl", hash = "sha256:f95b8aca2703d6a30249f83f4fe6a9abf2e627aa892a5caaab2267d56be7ab69"}, - {file = "psycopg2_binary-2.9.5-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:70831e03bd53702c941da1a1ad36c17d825a24fbb26857b40913d58df82ec18b"}, - {file = "psycopg2_binary-2.9.5-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:dbc332beaf8492b5731229a881807cd7b91b50dbbbaf7fe2faf46942eda64a24"}, - {file = "psycopg2_binary-2.9.5-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:2d964eb24c8b021623df1c93c626671420c6efadbdb8655cb2bd5e0c6fa422ba"}, - {file = "psycopg2_binary-2.9.5-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:95076399ec3b27a8f7fa1cc9a83417b1c920d55cf7a97f718a94efbb96c7f503"}, - {file = "psycopg2_binary-2.9.5-cp36-cp36m-win32.whl", hash = "sha256:3fc33295cfccad697a97a76dec3f1e94ad848b7b163c3228c1636977966b51e2"}, - {file = "psycopg2_binary-2.9.5-cp36-cp36m-win_amd64.whl", hash = "sha256:02551647542f2bf89073d129c73c05a25c372fc0a49aa50e0de65c3c143d8bd0"}, - {file = "psycopg2_binary-2.9.5-cp37-cp37m-macosx_10_15_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:63e318dbe52709ed10d516a356f22a635e07a2e34c68145484ed96a19b0c4c68"}, - {file = "psycopg2_binary-2.9.5-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a7e518a0911c50f60313cb9e74a169a65b5d293770db4770ebf004245f24b5c5"}, - {file = "psycopg2_binary-2.9.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b9d38a4656e4e715d637abdf7296e98d6267df0cc0a8e9a016f8ba07e4aa3eeb"}, - {file = "psycopg2_binary-2.9.5-cp37-cp37m-manylinux_2_24_aarch64.whl", hash = "sha256:68d81a2fe184030aa0c5c11e518292e15d342a667184d91e30644c9d533e53e1"}, - {file = "psycopg2_binary-2.9.5-cp37-cp37m-manylinux_2_24_ppc64le.whl", hash = "sha256:7ee3095d02d6f38bd7d9a5358fcc9ea78fcdb7176921528dd709cc63f40184f5"}, - {file = "psycopg2_binary-2.9.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:46512486be6fbceef51d7660dec017394ba3e170299d1dc30928cbedebbf103a"}, - {file = "psycopg2_binary-2.9.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:b911dfb727e247340d36ae20c4b9259e4a64013ab9888ccb3cbba69b77fd9636"}, - {file = "psycopg2_binary-2.9.5-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:422e3d43b47ac20141bc84b3d342eead8d8099a62881a501e97d15f6addabfe9"}, - {file = "psycopg2_binary-2.9.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:c5682a45df7d9642eff590abc73157c887a68f016df0a8ad722dcc0f888f56d7"}, - {file = "psycopg2_binary-2.9.5-cp37-cp37m-win32.whl", hash = "sha256:b8104f709590fff72af801e916817560dbe1698028cd0afe5a52d75ceb1fce5f"}, - {file = "psycopg2_binary-2.9.5-cp37-cp37m-win_amd64.whl", hash = "sha256:7b3751857da3e224f5629400736a7b11e940b5da5f95fa631d86219a1beaafec"}, - {file = "psycopg2_binary-2.9.5-cp38-cp38-macosx_10_15_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:043a9fd45a03858ff72364b4b75090679bd875ee44df9c0613dc862ca6b98460"}, - {file = "psycopg2_binary-2.9.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:9ffdc51001136b699f9563b1c74cc1f8c07f66ef7219beb6417a4c8aaa896c28"}, - {file = "psycopg2_binary-2.9.5-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c15ba5982c177bc4b23a7940c7e4394197e2d6a424a2d282e7c236b66da6d896"}, - {file = "psycopg2_binary-2.9.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc85b3777068ed30aff8242be2813038a929f2084f69e43ef869daddae50f6ee"}, - {file = "psycopg2_binary-2.9.5-cp38-cp38-manylinux_2_24_aarch64.whl", hash = "sha256:215d6bf7e66732a514f47614f828d8c0aaac9a648c46a831955cb103473c7147"}, - {file = "psycopg2_binary-2.9.5-cp38-cp38-manylinux_2_24_ppc64le.whl", hash = "sha256:7d07f552d1e412f4b4e64ce386d4c777a41da3b33f7098b6219012ba534fb2c2"}, - {file = "psycopg2_binary-2.9.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:a0adef094c49f242122bb145c3c8af442070dc0e4312db17e49058c1702606d4"}, - {file = "psycopg2_binary-2.9.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:00475004e5ed3e3bf5e056d66e5dcdf41a0dc62efcd57997acd9135c40a08a50"}, - {file = "psycopg2_binary-2.9.5-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:7d88db096fa19d94f433420eaaf9f3c45382da2dd014b93e4bf3215639047c16"}, - {file = "psycopg2_binary-2.9.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:902844f9c4fb19b17dfa84d9e2ca053d4a4ba265723d62ea5c9c26b38e0aa1e6"}, - {file = "psycopg2_binary-2.9.5-cp38-cp38-win32.whl", hash = "sha256:4e7904d1920c0c89105c0517dc7e3f5c20fb4e56ba9cdef13048db76947f1d79"}, - {file = "psycopg2_binary-2.9.5-cp38-cp38-win_amd64.whl", hash = "sha256:a36a0e791805aa136e9cbd0ffa040d09adec8610453ee8a753f23481a0057af5"}, - {file = "psycopg2_binary-2.9.5-cp39-cp39-macosx_10_15_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:25382c7d174c679ce6927c16b6fbb68b10e56ee44b1acb40671e02d29f2fce7c"}, - {file = "psycopg2_binary-2.9.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9c38d3869238e9d3409239bc05bc27d6b7c99c2a460ea337d2814b35fb4fea1b"}, - {file = "psycopg2_binary-2.9.5-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5c6527c8efa5226a9e787507652dd5ba97b62d29b53c371a85cd13f957fe4d42"}, - {file = "psycopg2_binary-2.9.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e59137cdb970249ae60be2a49774c6dfb015bd0403f05af1fe61862e9626642d"}, - {file = "psycopg2_binary-2.9.5-cp39-cp39-manylinux_2_24_aarch64.whl", hash = "sha256:d4c7b3a31502184e856df1f7bbb2c3735a05a8ce0ade34c5277e1577738a5c91"}, - {file = "psycopg2_binary-2.9.5-cp39-cp39-manylinux_2_24_ppc64le.whl", hash = "sha256:b9a794cef1d9c1772b94a72eec6da144c18e18041d294a9ab47669bc77a80c1d"}, - {file = "psycopg2_binary-2.9.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:c5254cbd4f4855e11cebf678c1a848a3042d455a22a4ce61349c36aafd4c2267"}, - {file = "psycopg2_binary-2.9.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:c5e65c6ac0ae4bf5bef1667029f81010b6017795dcb817ba5c7b8a8d61fab76f"}, - {file = "psycopg2_binary-2.9.5-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:74eddec4537ab1f701a1647214734bc52cee2794df748f6ae5908e00771f180a"}, - {file = "psycopg2_binary-2.9.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:01ad49d68dd8c5362e4bfb4158f2896dc6e0c02e87b8a3770fc003459f1a4425"}, - {file = "psycopg2_binary-2.9.5-cp39-cp39-win32.whl", hash = "sha256:937880290775033a743f4836aa253087b85e62784b63fd099ee725d567a48aa1"}, - {file = "psycopg2_binary-2.9.5-cp39-cp39-win_amd64.whl", hash = "sha256:484405b883630f3e74ed32041a87456c5e0e63a8e3429aa93e8714c366d62bd1"}, -] -pytest = [ - {file = "pytest-7.2.1-py3-none-any.whl", hash = "sha256:c7c6ca206e93355074ae32f7403e8ea12163b1163c976fee7d4d84027c162be5"}, - {file = "pytest-7.2.1.tar.gz", hash = "sha256:d45e0952f3727241918b8fd0f376f5ff6b301cc0777c6f9a556935c92d8a7d42"}, -] -pytest-cov = [ - {file = "pytest-cov-3.0.0.tar.gz", hash = "sha256:e7f0f5b1617d2210a2cabc266dfe2f4c75a8d32fb89eafb7ad9d06f6d076d470"}, - {file = "pytest_cov-3.0.0-py3-none-any.whl", hash = "sha256:578d5d15ac4a25e5f961c938b85a05b09fdaae9deef3bb6de9a6e766622ca7a6"}, -] -pytest-mock = [ - {file = "pytest-mock-3.10.0.tar.gz", hash = "sha256:fbbdb085ef7c252a326fd8cdcac0aa3b1333d8811f131bdcc701002e1be7ed4f"}, - {file = "pytest_mock-3.10.0-py3-none-any.whl", hash = "sha256:f4c973eeae0282963eb293eb173ce91b091a79c1334455acfac9ddee8a1c784b"}, -] -pywin32 = [ - {file = "pywin32-305-cp310-cp310-win32.whl", hash = "sha256:421f6cd86e84bbb696d54563c48014b12a23ef95a14e0bdba526be756d89f116"}, - {file = "pywin32-305-cp310-cp310-win_amd64.whl", hash = "sha256:73e819c6bed89f44ff1d690498c0a811948f73777e5f97c494c152b850fad478"}, - {file = "pywin32-305-cp310-cp310-win_arm64.whl", hash = "sha256:742eb905ce2187133a29365b428e6c3b9001d79accdc30aa8969afba1d8470f4"}, - {file = "pywin32-305-cp311-cp311-win32.whl", hash = "sha256:19ca459cd2e66c0e2cc9a09d589f71d827f26d47fe4a9d09175f6aa0256b51c2"}, - {file = "pywin32-305-cp311-cp311-win_amd64.whl", hash = "sha256:326f42ab4cfff56e77e3e595aeaf6c216712bbdd91e464d167c6434b28d65990"}, - {file = "pywin32-305-cp311-cp311-win_arm64.whl", hash = "sha256:4ecd404b2c6eceaca52f8b2e3e91b2187850a1ad3f8b746d0796a98b4cea04db"}, - {file = "pywin32-305-cp36-cp36m-win32.whl", hash = "sha256:48d8b1659284f3c17b68587af047d110d8c44837736b8932c034091683e05863"}, - {file = "pywin32-305-cp36-cp36m-win_amd64.whl", hash = "sha256:13362cc5aa93c2beaf489c9c9017c793722aeb56d3e5166dadd5ef82da021fe1"}, - {file = "pywin32-305-cp37-cp37m-win32.whl", hash = "sha256:a55db448124d1c1484df22fa8bbcbc45c64da5e6eae74ab095b9ea62e6d00496"}, - {file = "pywin32-305-cp37-cp37m-win_amd64.whl", hash = "sha256:109f98980bfb27e78f4df8a51a8198e10b0f347257d1e265bb1a32993d0c973d"}, - {file = "pywin32-305-cp38-cp38-win32.whl", hash = "sha256:9dd98384da775afa009bc04863426cb30596fd78c6f8e4e2e5bbf4edf8029504"}, - {file = "pywin32-305-cp38-cp38-win_amd64.whl", hash = "sha256:56d7a9c6e1a6835f521788f53b5af7912090674bb84ef5611663ee1595860fc7"}, - {file = "pywin32-305-cp39-cp39-win32.whl", hash = "sha256:9d968c677ac4d5cbdaa62fd3014ab241718e619d8e36ef8e11fb930515a1e918"}, - {file = "pywin32-305-cp39-cp39-win_amd64.whl", hash = "sha256:50768c6b7c3f0b38b7fb14dd4104da93ebced5f1a50dc0e834594bff6fbe1271"}, -] -requests = [ - {file = "requests-2.28.2-py3-none-any.whl", hash = "sha256:64299f4909223da747622c030b781c0d7811e359c37124b4bd368fb8c6518baa"}, - {file = "requests-2.28.2.tar.gz", hash = "sha256:98b1b2782e3c6c4904938b84c0eb932721069dfdb9134313beff7c83c2df24bf"}, -] -tomli = [ - {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, - {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, -] -typing-extensions = [ - {file = "typing_extensions-4.5.0-py3-none-any.whl", hash = "sha256:fb33085c39dd998ac16d1431ebc293a8b3eedd00fd4a32de0ff79002c19511b4"}, - {file = "typing_extensions-4.5.0.tar.gz", hash = "sha256:5cb5f4a79139d699607b3ef622a1dedafa84e115ab0024e0d9c044a9479ca7cb"}, -] -urllib3 = [ - {file = "urllib3-1.26.14-py2.py3-none-any.whl", hash = "sha256:75edcdc2f7d85b137124a6c3c9fc3933cdeaa12ecb9a6a959f22797a0feca7e1"}, - {file = "urllib3-1.26.14.tar.gz", hash = "sha256:076907bf8fd355cde77728471316625a4d2f7e713c125f51953bb5b3eecf4f72"}, -] -websocket-client = [ - {file = "websocket-client-1.5.1.tar.gz", hash = "sha256:3f09e6d8230892547132177f575a4e3e73cfdf06526e20cc02aa1c3b47184d40"}, - {file = "websocket_client-1.5.1-py3-none-any.whl", hash = "sha256:cdf5877568b7e83aa7cf2244ab56a3213de587bbe0ce9d8b9600fc77b455d89e"}, -] diff --git a/pyproject.toml b/pyproject.toml index 3d23303..5479044 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,45 +1,132 @@ -[tool.poetry] +[project] name = "pgmob" -version = "0.1.3a0" +version = "0.3.0" description = "Postgres Managed Objects - a Postgres database management interface" -authors = ["Kirill Kravtsov "] -packages = [ - { include = "pgmob", from = "src" }, +authors = [ + {name = "Kirill Kravtsov", email = "nvarscar@gmail.com"} ] -include = ["src/pgmob/scripts/*"] +readme = "README.md" +license = {text = "MIT"} +requires-python = ">=3.13" classifiers = [ "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.13", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", ] -license = "MIT" -readme = "README.md" -homepage = "https://github.com/dataplat/pgmob/" -repository = "https://github.com/dataplat/pgmob" -documentation = "https://pgmob.readthedocs.io/en/latest/" -keywords = ["postgres", "sql", "backup", "restore"] - -[tool.poetry.dependencies] -python = ">=3.9,<4" -psycopg2-binary = {version = ">=2.8.5,<3", optional = true} -psycopg2 = {version = ">=2.8.5,<3", optional = true} -packaging = ">=21.3" - -[tool.poetry.dev-dependencies] -pytest = "^7.1.1" -pytest-cov = "^3.0.0" -pytest-mock = "^3.7.0" -black = "^22.3.0" -docker = "^6.0.0" -mypy = "^0.971" +keywords = ["postgres", "postgresql", "sql", "backup", "restore", "database", "management"] +dependencies = [ + "packaging>=26.0", +] + +[project.optional-dependencies] +psycopg2 = ["psycopg2>=2.9.11,<3"] +psycopg2-binary = ["psycopg2-binary>=2.9.11,<3"] +dev = [ + "pytest>=9.0.0", + "pytest-cov>=7.0.0", + "pytest-mock>=3.15.0", + "pytest-asyncio>=1.3.0", + "docker>=7.1.0", + "ruff>=0.15.0", + "ty>=0.0.15", + "pyyaml>=6.0.3", +] + +[project.urls] +Homepage = "https://github.com/dataplat/pgmob/" +Repository = "https://github.com/dataplat/pgmob" +Documentation = "https://pgmob.readthedocs.io/en/latest/" +"Bug Tracker" = "https://github.com/dataplat/pgmob/issues" [build-system] -requires = ["poetry-core>=1.0.0"] -build-backend = "poetry.core.masonry.api" +requires = ["hatchling"] +build-backend = "hatchling.build" -[tool.poetry.extras] -psycopg2-binary = ["psycopg2-binary"] -psycopg2 = ["psycopg2"] +[tool.hatch.build.targets.wheel] +packages = ["src/pgmob"] + +[tool.hatch.build.targets.sdist] +include = [ + "/src", + "/README.md", + "/LICENSE", +] + +[tool.ruff] +line-length = 110 +target-version = "py313" + +[tool.ruff.lint] +select = [ + "E", # pycodestyle errors + "W", # pycodestyle warnings + "F", # pyflakes + "I", # isort + "B", # flake8-bugbear + "C4", # flake8-comprehensions + "UP", # pyupgrade + "ARG", # flake8-unused-arguments + "SIM", # flake8-simplify +] +ignore = [ + "E501", # line too long (handled by formatter) + "B008", # do not perform function calls in argument defaults + "B904", # raise from + "ARG002", # unused method argument +] + +[tool.ruff.lint.per-file-ignores] +"__init__.py" = ["F401"] # unused imports +"src/tests/**" = ["ARG001", "ARG002", "F403", "F405", "E711", "E712"] # test patterns + +[tool.ruff.format] +quote-style = "double" +indent-style = "space" +skip-magic-trailing-comma = false +line-ending = "lf" + +[tool.pytest.ini_options] +testpaths = ["src/tests"] +python_files = ["test_*.py"] +python_classes = ["Test*"] +python_functions = ["test_*"] +addopts = [ + "-ra", + "--strict-markers", + "--strict-config", + "--showlocals", +] +markers = [ + "unit: Unit tests", + "integration: Integration tests", + "slow: Slow tests", + "asyncio: Async tests", +] + +[tool.coverage.run] +source = ["src/pgmob"] +omit = [ + "*/tests/*", + "*/conftest.py", + "*/__pycache__/*", +] +branch = true + +[tool.coverage.report] +exclude_lines = [ + "pragma: no cover", + "def __repr__", + "def __str__", + "raise AssertionError", + "raise NotImplementedError", + "if __name__ == .__main__.:", + "if TYPE_CHECKING:", + "class .*\\bProtocol\\):", + "@(abc\\.)?abstractmethod", +] +precision = 2 +show_missing = true -[tool.black] -line-length = 110 \ No newline at end of file +[tool.coverage.html] +directory = "htmlcov" diff --git a/src/pgmob/_decorators.py b/src/pgmob/_decorators.py index 3ca41c2..5051d35 100644 --- a/src/pgmob/_decorators.py +++ b/src/pgmob/_decorators.py @@ -1,20 +1,21 @@ import functools import inspect -from typing import Any, Callable, TypeVar import warnings +from collections.abc import Callable +from typing import Any, TypeVar T = TypeVar("T") LAZY_PREFIX = "_pgmlazy_" -class RefreshProperty(object): +class RefreshProperty: """An instance of this class marks a lazy-evaluated property as requiring a refresh""" def __eq__(self, __o: object) -> bool: return self.__class__ == __o.__class__ -def get_lazy_property(obj: object, name: str, func: Callable[..., T], *args: Any, **kwargs: Any) -> T: +def get_lazy_property[T](obj: object, name: str, func: Callable[..., T], *args: Any, **kwargs: Any) -> T: """Retrieves a lazy property value""" attribute = LAZY_PREFIX + name has_attribute = hasattr(obj, attribute) @@ -53,16 +54,21 @@ def decorator(func): @functools.wraps(func) def wrapper(*args, **kwargs): - message = "Call to deprecated function {}. {}".format(func.__name__, instructions) - - frame = inspect.currentframe().f_back - - warnings.warn_explicit( - message, - category=DeprecatedWarning, - filename=inspect.getfile(frame.f_code), - lineno=frame.f_lineno, - ) + message = f"Call to deprecated function {func.__name__}. {instructions}" + + frame = inspect.currentframe() + if frame is not None: + frame = frame.f_back + + if frame is not None: + warnings.warn_explicit( + message, + category=DeprecatedWarning, + filename=inspect.getfile(frame.f_code), + lineno=frame.f_lineno, + ) + else: + warnings.warn(message, category=DeprecatedWarning, stacklevel=2) return func(*args, **kwargs) diff --git a/src/pgmob/adapters/__init__.py b/src/pgmob/adapters/__init__.py index 78e3cce..bd63549 100644 --- a/src/pgmob/adapters/__init__.py +++ b/src/pgmob/adapters/__init__.py @@ -1,9 +1,8 @@ -"""Adapters are wrappers around SQL Providers, which translate internal module syntax into the proper Provider syntax -""" +"""Adapters are wrappers around SQL Providers, which translate internal module syntax into the proper Provider syntax""" from typing import TYPE_CHECKING -from .errors import * +from .errors import AdapterError, NoResultsToFetch, ProgrammingError ADAPTERS = [(".psycopg2", "Psycopg2Adapter")] """Use the following variable to register adapters for autodiscovery. @@ -31,7 +30,7 @@ def detect_adapter() -> "BaseAdapter": for module, name in ADAPTERS: try: adapter_module = import_module(module, package="pgmob.adapters") - except: + except (ImportError, ModuleNotFoundError): pass else: return getattr(adapter_module, name)() diff --git a/src/pgmob/adapters/base.py b/src/pgmob/adapters/base.py index 78c686c..9a1a6c8 100644 --- a/src/pgmob/adapters/base.py +++ b/src/pgmob/adapters/base.py @@ -1,5 +1,7 @@ -from typing import Any, Sequence, Union -from abc import abstractmethod, ABC +from abc import ABC, abstractmethod +from collections.abc import Sequence +from typing import Any + from ..sql import Composable @@ -33,7 +35,7 @@ def close(self) -> None: raise NotImplementedError() @abstractmethod - def execute(self, query: Union[Composable, str], params: tuple = None) -> None: + def execute(self, query: Composable | str, params: tuple | None = None) -> None: """Execute a query with parameters. Should be overridden by the adapter, which should provide support for one of the two potential inputs: @@ -54,7 +56,7 @@ def execute(self, query: Union[Composable, str], params: tuple = None) -> None: raise NotImplementedError() @abstractmethod - def executemany(self, query: Composable, params: Sequence[tuple] = None) -> None: + def executemany(self, query: Composable, params: Sequence[tuple] | None = None) -> None: """Execute a query with multiple parameter sets. Should be overridden by the adapter, which should provide support for all possible Composable objects that represent query parts: SQL, Literal, Identifier. Adapter should call the .compose() method of the query object @@ -63,7 +65,7 @@ def executemany(self, query: Composable, params: Sequence[tuple] = None) -> None raise NotImplementedError() @abstractmethod - def mogrify(self, query: Composable, params: tuple = None) -> bytes: + def mogrify(self, query: Composable, params: tuple | None = None) -> bytes: """Returns a parsed SQL query based on the parameters provided. Should be overridden by the adapter, which should provide support for all possible Composable objects that represent query parts: SQL, Literal, Identifier. Adapter should call the .compose() method of the query object @@ -104,7 +106,7 @@ def fetchone(self) -> Any: # default implementations - def scalar(self, query: Composable, params: tuple = None) -> Any: + def scalar(self, query: Composable, params: tuple | None = None) -> Any: """Same as execute, but returns only the first item of the first row. Args: @@ -169,7 +171,7 @@ def write(self, data: bytes) -> int: raise NotImplementedError() @abstractmethod - def read(self) -> Union[str, bytes]: + def read(self) -> str | bytes: """Read large object Returns: diff --git a/src/pgmob/adapters/psycopg2.py b/src/pgmob/adapters/psycopg2.py index f6855ab..b492023 100644 --- a/src/pgmob/adapters/psycopg2.py +++ b/src/pgmob/adapters/psycopg2.py @@ -1,10 +1,15 @@ -from typing import Any, Callable, Optional, Sequence, Union -import psycopg2 # type: ignore -import psycopg2.sql # type: ignore -import psycopg2.extras # type: ignore -import psycopg2.extensions # type: ignore -from . import ProgrammingError, AdapterError, NoResultsToFetch -from ..sql import SQL, Identifier, Literal, Composable +from collections.abc import Callable, Sequence +from typing import Any + +# psycopg2 is a third-party library without type stubs +# Type checking is limited for psycopg2 imports +import psycopg2 +import psycopg2.extensions +import psycopg2.extras +import psycopg2.sql + +from ..sql import SQL, Composable, Identifier, Literal +from . import AdapterError, NoResultsToFetch, ProgrammingError from .base import BaseAdapter, BaseCursor, BaseLargeObject @@ -35,7 +40,7 @@ def rowcount(self) -> int: """Row count of the most recent execute""" return self.cursor.rowcount - def _convert_query(self, query: Union[Composable, str]) -> Union[psycopg2.sql.Composable, str]: + def _convert_query(self, query: Composable | str) -> psycopg2.sql.Composable | str: conv_map = {SQL: psycopg2.sql.SQL, Literal: psycopg2.sql.Literal, Identifier: psycopg2.sql.Identifier} if isinstance(query, Composable): return psycopg2.sql.Composed([conv_map[part.__class__](part.value()) for part in query.compose()]) @@ -57,7 +62,7 @@ def close(self) -> None: """Close the currently open cursor""" return self.cursor.close() - def execute(self, query: Union[Composable, str], params: tuple = None) -> None: + def execute(self, query: Composable | str, params: tuple | None = None) -> None: """Execute a query with parameters Args: @@ -67,11 +72,11 @@ def execute(self, query: Union[Composable, str], params: tuple = None) -> None: """ self._try_exec(lambda: self.cursor.execute(self._convert_query(query), params)) - def executemany(self, query: Union[Composable, str], params: Sequence[tuple] = None) -> None: + def executemany(self, query: Composable | str, params: Sequence[tuple] | None = None) -> None: """Execute a query with multiple parameter sets""" self._try_exec(lambda: self.cursor.executemany(self._convert_query(query), params)) - def mogrify(self, query: Union[Composable, str], params: tuple = None) -> bytes: + def mogrify(self, query: Composable | str, params: tuple | None = None) -> bytes: """Returns a parsed SQL query based on the parameters provided Returns: @@ -112,7 +117,8 @@ class Psycopg2LargeObject(BaseLargeObject): """ def __init__(self, connection: Any, oid: int, mode: str, *args, **kwargs) -> None: - self.lobject = connection.lobject(oid, mode=mode, *args, **kwargs) + kwargs["mode"] = mode + self.lobject = connection.lobject(oid, *args, **kwargs) @property def closed(self) -> bool: @@ -138,7 +144,7 @@ def write(self, data: bytes) -> int: int: number of bytes written""" return self.lobject.write(data) - def read(self) -> Union[str, bytes]: + def read(self) -> str | bytes: """Read large object Returns: @@ -151,7 +157,7 @@ def unlink(self): self.lobject.unlink() def __del__(self): - if not self.lobject.closed: + if hasattr(self, "lobject") and not self.lobject.closed: self.lobject.close() @@ -162,7 +168,7 @@ class Psycopg2Adapter(BaseAdapter): def __init__( self, - cursor_factory: Optional[psycopg2.extras.DictCursorBase] = None, + cursor_factory: psycopg2.extras.DictCursorBase | None = None, ) -> None: self._cursor_factory = cursor_factory self.connection: Any = None diff --git a/src/pgmob/backup.py b/src/pgmob/backup.py index 0deb6ef..bca27dc 100644 --- a/src/pgmob/backup.py +++ b/src/pgmob/backup.py @@ -14,38 +14,39 @@ >>> file_restore = FileRestore(cluster=cluster) >>> file_restore.restore(database=new_db, path="/tmp/db.bak") """ -from typing import List, Optional + import logging from pathlib import Path -from .os import ShellEnv, _BaseShellEnv + from . import cluster +from .os import ShellEnv, _BaseShellEnv LOGGER = logging.getLogger(__name__) -class _CommonOptions(object): +class _CommonOptions: """Common backup/restore options""" - def __init__(self, shell: Optional[_BaseShellEnv] = None, **kwargs) -> None: + def __init__(self, shell: _BaseShellEnv | None = None, **kwargs) -> None: self.shell = shell if shell else ShellEnv() self._add_if_exists = False self._clean = False self._create = False self._data_only = False - self._exclude_schemas: List[str] = [] - self._format: Optional[str] = None + self._exclude_schemas: list[str] = [] + self._format: str | None = None self._no_privileges = False self._no_publications = False self._no_subscriptions = False self._no_tablespaces = False self._no_owner = False self._schema_only = False - self._schemas: List[str] = [] - self._section: Optional[str] = None - self._set_role: Optional[str] = None + self._schemas: list[str] = [] + self._section: str | None = None + self._set_role: str | None = None self._strict_names = False - self._superuser: Optional[str] = None - self._tables: List[str] = [] + self._superuser: str | None = None + self._tables: list[str] = [] self._verbose = False for k, v in kwargs.items(): @@ -89,19 +90,19 @@ def data_only(self, value: bool) -> None: self._data_only = bool(value) @property - def exclude_schemas(self) -> List[str]: + def exclude_schemas(self) -> list[str]: return self._exclude_schemas @exclude_schemas.setter - def exclude_schemas(self, value: List[str]) -> None: + def exclude_schemas(self, value: list[str]) -> None: self._exclude_schemas = [str(x) for x in value] @property - def format(self) -> Optional[str]: + def format(self) -> str | None: return self._format @format.setter - def format(self, value: Optional[str]) -> None: + def format(self, value: str | None) -> None: self._format = str(value) if value else None @property @@ -153,27 +154,27 @@ def schema_only(self, value: bool) -> None: self._schema_only = bool(value) @property - def schemas(self) -> List[str]: + def schemas(self) -> list[str]: return self._schemas @schemas.setter - def schemas(self, value: List[str]) -> None: + def schemas(self, value: list[str]) -> None: self._schemas = [str(x) for x in value] @property - def section(self) -> Optional[str]: + def section(self) -> str | None: return self._section @section.setter - def section(self, value: Optional[str]) -> None: + def section(self, value: str | None) -> None: self._section = str(value) if value else None @property - def set_role(self) -> Optional[str]: + def set_role(self) -> str | None: return self._set_role @set_role.setter - def set_role(self, value: Optional[str]) -> None: + def set_role(self, value: str | None) -> None: self._set_role = str(value) if value else None @property @@ -185,19 +186,19 @@ def strict_names(self, value: bool) -> None: self._strict_names = bool(value) @property - def superuser(self) -> Optional[str]: + def superuser(self) -> str | None: return self._superuser @superuser.setter - def superuser(self, value: Optional[str]) -> None: + def superuser(self, value: str | None) -> None: self._superuser = str(value) if value else None @property - def tables(self) -> List[str]: + def tables(self) -> list[str]: return self._tables @tables.setter - def tables(self, value: List[str]) -> None: + def tables(self, value: list[str]) -> None: self._tables = [str(x) for x in value] @property @@ -208,13 +209,13 @@ def verbose(self) -> bool: def verbose(self, value: bool) -> None: self._verbose = bool(value) - def render_args(self) -> List[str]: + def render_args(self) -> list[str]: """Renders options as command line arguments Returns: - List[str]: list of command line arguments + list[str]: list of command line arguments """ - options: List[str] = [] + options: list[str] = [] if self.clean: options.append("--clean") if self.create: @@ -275,15 +276,15 @@ class BackupOptions(_CommonOptions): data_only (bool): dump only the data, not the schema clean (bool): clean (drop) database objects before recreating create (bool): include commands to create database in dump - schemas (List[str]): dump the named schemas only - exclude_schemas (List[str]): do NOT dump the named schemas + schemas (list[str]): dump the named schemas only + exclude_schemas (list[str]): do NOT dump the named schemas no_owner (bool): skip restoration of object ownership in plain-text format schema_only (bool): dump only the schema, no data superuser (str): superuser user name to use in plain-text format - tables (List[str]): dump the named tables only - exclude_tables (List[str]): do NOT dump the named tables + tables (list[str]): dump the named tables only + exclude_tables (list[str]): do NOT dump the named tables no_privileges (bool): do not dump privileges (grant/revoke) - exclude_table_data (List[str]): do NOT dump data for the named table(s) + exclude_table_data (list[str]): do NOT dump data for the named table(s) add_if_exists (bool): use IF EXISTS when dropping objects as_inserts (bool): dump data as INSERT commands, rather than COPY no_subscriptions (bool): do not dump subscriptions @@ -295,15 +296,15 @@ class BackupOptions(_CommonOptions): blobs (bool): Include large objects in the dump """ - def __init__(self, shell: Optional[_BaseShellEnv] = None, **kwargs) -> None: + def __init__(self, shell: _BaseShellEnv | None = None, **kwargs) -> None: self._compress = False self._compression_level = 5 - self._exclude_tables: List[str] = [] - self._exclude_table_data: List[str] = [] + self._exclude_tables: list[str] = [] + self._exclude_table_data: list[str] = [] self._as_inserts = False self._create_database = False - self._lock_wait_timeout: Optional[int] = None - self._blobs: Optional[bool] = None + self._lock_wait_timeout: int | None = None + self._blobs: bool | None = None super().__init__(shell=shell, **kwargs) self.format = "c" @@ -340,42 +341,42 @@ def compression_level(self, value: int) -> None: self._compression_level = int(value) @property - def exclude_tables(self) -> List[str]: + def exclude_tables(self) -> list[str]: return self._exclude_tables @exclude_tables.setter - def exclude_tables(self, value: List[str]) -> None: + def exclude_tables(self, value: list[str]) -> None: self._exclude_tables = [str(x) for x in value] @property - def exclude_table_data(self) -> List[str]: + def exclude_table_data(self) -> list[str]: return self._exclude_table_data @exclude_table_data.setter - def exclude_table_data(self, value: List[str]) -> None: + def exclude_table_data(self, value: list[str]) -> None: self._exclude_table_data = [str(x) for x in value] @property - def lock_wait_timeout(self) -> Optional[int]: + def lock_wait_timeout(self) -> int | None: return self._lock_wait_timeout @lock_wait_timeout.setter - def lock_wait_timeout(self, value: Optional[int]) -> None: + def lock_wait_timeout(self, value: int | None) -> None: self._lock_wait_timeout = None if value is None else int(value) @property - def blobs(self) -> Optional[bool]: + def blobs(self) -> bool | None: return self._blobs @blobs.setter - def blobs(self, value: Optional[bool]) -> None: + def blobs(self, value: bool | None) -> None: self._blobs = None if value is None else bool(value) def render_args(self): """Renders options as command line arguments Returns: - List[str]: A list of command line arguments + list[str]: A list of command line arguments """ options = super().render_args() @@ -414,16 +415,16 @@ class RestoreOptions(_CommonOptions): clean (bool): clean (drop) database objects before recreating create (bool): create the target database exit_on_error (bool): exit on error, default is to continue - indexes (List[str]): restore named indexes - schemas (List[str]): restore only objects in these schemas - exclude_schemas (List[str]): do not restore objects in these schemas + indexes (list[str]): restore named indexes + schemas (list[str]): restore only objects in these schemas + exclude_schemas (list[str]): do not restore objects in these schemas use_list (str): use table of contents from this file for selecting/ordering output no_owner (bool): skip restoration of object ownership - functions (List[str]): (NAME(args)) restore named functions + functions (list[str]): (NAME(args)) restore named functions schema_only (bool): restore only the schema, no data superuser (str): superuser user name to use for disabling triggers - tables (List[str]): restore named relations (table, view, etc.) - triggers (List[str]): restore named triggers + tables (list[str]): restore named relations (table, view, etc.) + triggers (list[str]): restore named triggers no_privileges (bool): skip restoration of access privileges (grant/revoke) single_transaction (bool): restore as a single transaction disable_triggers (bool): disable triggers during data-only restore @@ -437,13 +438,13 @@ class RestoreOptions(_CommonOptions): set_role (str): invoke SET ROLE before dump """ - def __init__(self, shell: Optional[_BaseShellEnv] = None, **kwargs) -> None: + def __init__(self, shell: _BaseShellEnv | None = None, **kwargs) -> None: self._exit_on_error = False - self._indexes: List[str] = [] - self._functions: List[str] = [] - self._triggers: List[str] = [] - self._jobs: Optional[int] = None - self._use_list: Optional[str] = None + self._indexes: list[str] = [] + self._functions: list[str] = [] + self._triggers: list[str] = [] + self._jobs: int | None = None + self._use_list: str | None = None self._single_transaction = False self._disable_triggers = False self._no_data_for_failed_tables = False @@ -458,43 +459,43 @@ def exit_on_error(self, value: bool) -> None: self._exit_on_error = bool(value) @property - def indexes(self) -> List[str]: + def indexes(self) -> list[str]: return self._indexes @indexes.setter - def indexes(self, value: List[str]) -> None: + def indexes(self, value: list[str]) -> None: self._indexes = [str(x) for x in value] @property - def functions(self) -> List[str]: + def functions(self) -> list[str]: return self._functions @functions.setter - def functions(self, value: List[str]) -> None: + def functions(self, value: list[str]) -> None: self._functions = [str(x) for x in value] @property - def triggers(self) -> List[str]: + def triggers(self) -> list[str]: return self._triggers @triggers.setter - def triggers(self, value: List[str]) -> None: + def triggers(self, value: list[str]) -> None: self._triggers = [str(x) for x in value] @property - def jobs(self) -> Optional[int]: + def jobs(self) -> int | None: return self._jobs @jobs.setter - def jobs(self, value: Optional[int]) -> None: + def jobs(self, value: int | None) -> None: self._jobs = int(value) if value else None @property - def use_list(self) -> Optional[str]: + def use_list(self) -> str | None: return self._use_list @use_list.setter - def use_list(self, value: Optional[str]) -> None: + def use_list(self, value: str | None) -> None: self._use_list = str(value) if value else None @property @@ -525,7 +526,7 @@ def render_args(self): """Renders options as command line arguments Returns: - List[str]: A list of command line arguments + list[str]: A list of command line arguments """ options = super().render_args() if self.exit_on_error: @@ -549,7 +550,7 @@ def render_args(self): return options -class _BackupRestoreOperation(object): +class _BackupRestoreOperation: """Base backup/restore operation class that implements binary execution. Args: @@ -568,18 +569,18 @@ def __init__( command: str, base_path: str, options: _CommonOptions, - shell: Optional[_BaseShellEnv] = None, + shell: _BaseShellEnv | None = None, ) -> None: self.shell = shell if shell else ShellEnv() self.command = command self.binary = binary_path self.cluster = cluster self.base_path = base_path.rstrip("/") - self.on_start_commands: List[str] = [] - self.on_finish_commands: List[str] = [] + self.on_start_commands: list[str] = [] + self.on_finish_commands: list[str] = [] self.options = options - def _exec_commands(self, commands: List[str], **kwargs): + def _exec_commands(self, commands: list[str], **kwargs): for command in commands: formatted_command = command.format(**kwargs) LOGGER.debug(f"Running {self.__class__.__name__} command: {formatted_command}") @@ -596,13 +597,13 @@ def execute_command(self, database: str, path: str): str: stdin and stdout of executed command """ full_path = self.shell.join_path(self.base_path, path) - params = dict( - database=database, - path=full_path, - options=" ".join(self.options.render_args()), - binary=self.binary, - filename=Path(full_path).name, - ) + params = { + "database": database, + "path": full_path, + "options": " ".join(self.options.render_args()), + "binary": self.binary, + "filename": Path(full_path).name, + } self._exec_commands(self.on_start_commands, **params) try: result = self._exec_commands([self.command], **params) @@ -627,9 +628,9 @@ class FileBackup(_BackupRestoreOperation): cluster (cluster.Cluster): Postgres cluster object binary (str): Path to the pg_dump binary shell (_BaseShellEnv): shell processor that defines pathing and escaping for the current environment - on_start_commands (List[str]): commands to execute prior to launching the backup + on_start_commands (list[str]): commands to execute prior to launching the backup command (str): main backup command - on_finish_commands (List[str]): commands to execute after backup is completed or failed + on_finish_commands (list[str]): commands to execute after backup is completed or failed Example: Backup a database schema for tables "a" and "b" @@ -646,8 +647,8 @@ def __init__( cluster: cluster.Cluster, base_path: str = "", binary_path: str = "pg_dump", - options: Optional[BackupOptions] = None, - shell: Optional[_BaseShellEnv] = None, + options: BackupOptions | None = None, + shell: _BaseShellEnv | None = None, ): super().__init__( cluster=cluster, @@ -689,9 +690,9 @@ class GCPBackup(FileBackup): options (BackupOptions): backup options represented by the BackupOptions class cluster (cluster.Cluster): Postgres cluster object binary: Path to the pg_dump binary. - on_start_commands (List[str]): commands to execute prior to launching the backup + on_start_commands (list[str]): commands to execute prior to launching the backup command (str): main backup command - on_finish_commands (List[str]): commands to execute after backup is completed or failed + on_finish_commands (list[str]): commands to execute after backup is completed or failed Example: Backup schema "public" of database "foo" with no privileges into the bucket gs://my-bucket/ @@ -707,8 +708,8 @@ def __init__( cluster: cluster.Cluster, bucket: str = "", binary_path: str = "pg_dump", - options: Optional[BackupOptions] = None, - shell: Optional[_BaseShellEnv] = None, + options: BackupOptions | None = None, + shell: _BaseShellEnv | None = None, ): super().__init__( cluster=cluster, binary_path=binary_path, base_path=bucket, options=options, shell=shell @@ -731,9 +732,9 @@ class FileRestore(_BackupRestoreOperation): options (RestoreOptions): restore options represented by the RestoreOptions class cluster: Postgres cluster object binary: Path to the pg_restore binary - on_start_commands (List[str]): commands to execute prior to launching the restore + on_start_commands (list[str]): commands to execute prior to launching the restore command (str): main restore command - on_finish_commands (List[str]): commands to execute after restore is completed or failed + on_finish_commands (list[str]): commands to execute after restore is completed or failed Example: @@ -757,8 +758,8 @@ def __init__( cluster: cluster.Cluster, base_path: str = "", binary_path: str = "pg_restore", - options: RestoreOptions = None, - shell: Optional[_BaseShellEnv] = None, + options: RestoreOptions | None = None, + shell: _BaseShellEnv | None = None, ): super().__init__( cluster=cluster, @@ -800,9 +801,9 @@ class GCPRestore(FileRestore): options (RestoreOptions): restore options represented by RestoreOptions class cluster: Postgres cluster to execute the restore binary: Path to the pg_restore binary. - on_start_commands (List[str]): commands to execute prior to launching the restore + on_start_commands (list[str]): commands to execute prior to launching the restore command (str): main restore command - on_finish_commands (List[str]): commands to execute after restore is completed or failed + on_finish_commands (list[str]): commands to execute after restore is completed or failed temp_path (str): Path to a temporary folder Example: @@ -822,8 +823,8 @@ def __init__( bucket: str = "", binary_path: str = "pg_restore", temp_path: str = "/tmp", - options: Optional[RestoreOptions] = None, - shell: Optional[_BaseShellEnv] = None, + options: RestoreOptions | None = None, + shell: _BaseShellEnv | None = None, ): super().__init__( cluster=cluster, binary_path=binary_path, base_path=bucket, options=options, shell=shell diff --git a/src/pgmob/cluster.py b/src/pgmob/cluster.py index 411aef7..e822ec6 100644 --- a/src/pgmob/cluster.py +++ b/src/pgmob/cluster.py @@ -8,22 +8,23 @@ - Execute ad-hoc SQL queries and shell commands - Run backup/restore operations """ + import logging -from typing import Any, Callable, List, Optional, Tuple, Union +from collections.abc import Callable +from typing import Any -from .os import _BaseShellEnv, ShellEnv, OSCommandResult -from ._decorators import RefreshProperty, LAZY_PREFIX, get_lazy_property -from .errors import * -from .adapters import detect_adapter, NoResultsToFetch, AdapterError -from .sql import SQL, Composable, Identifier, Literal +from . import objects, util +from ._decorators import LAZY_PREFIX, RefreshProperty, get_lazy_property +from .adapters import AdapterError, NoResultsToFetch, detect_adapter from .adapters.base import BaseAdapter, BaseCursor -from . import objects -from . import util +from .errors import PostgresError +from .os import OSCommandResult, ShellEnv, _BaseShellEnv +from .sql import SQL, Composable, Identifier, Literal LOGGER = logging.getLogger(__name__) -class _NoAutocommitContextManager(object): +class _NoAutocommitContextManager: def __init__(self, cluster: "Cluster"): self.cluster = cluster self.autocommit = self.cluster.adapter.get_autocommit() @@ -39,7 +40,7 @@ def __exit__(self, exc_type, exc_val, exc_tb): self.cluster.adapter.set_autocommit(True) -class Cluster(object): +class Cluster: """Provides a management interface for postgres cluster configuration. Args: @@ -77,10 +78,10 @@ def __init__( self, connection=None, become=None, - adapter: BaseAdapter = None, - shell: Optional[_BaseShellEnv] = None, + adapter: BaseAdapter | None = None, + shell: _BaseShellEnv | None = None, *args, - **kwargs + **kwargs, ): self.shell = shell if shell else ShellEnv() self.adapter: BaseAdapter = adapter if adapter else detect_adapter() @@ -97,9 +98,13 @@ def __del__(self): def _initialize(self): init_data = self.execute(SQL("SELECT current_database(), version()")) - if len(init_data) > 0: + if len(init_data) > 0 and len(init_data[0]) >= 2: self.current_database = init_data[0][0] - dbms, version_string = init_data[0][1].split()[0:2] + version_parts = init_data[0][1].split() + if len(version_parts) >= 2: + dbms, version_string = version_parts[0:2] + else: + raise PostgresError("Unable to parse database version information") if dbms != "PostgreSQL": raise PostgresError("DBMS is not postgres, version not supported") version = util.Version(version_string) @@ -134,7 +139,7 @@ def execute_with_cursor(self, task: Callable[[BaseCursor], Any], *args, **kwargs task (Callable[[BaseCursor], Any]): callable with cursor object as an only argument Returns: - Optional[List[Tuple[Any]]]]: List of tuples returned from the server or None if no rows selected. + list[tuple[Any]] | None: List of tuples returned from the server or None if no rows selected. Example: Run a task that fetches a row from a cursor @@ -175,17 +180,15 @@ def refresh(self): for attr in [attr for attr in lazy_attributes if attr.startswith(LAZY_PREFIX)]: setattr(self, attr, RefreshProperty()) - def execute( - self, query: Union[Composable, str], params: Union[Tuple[Any], Any] = None - ) -> List[Tuple[Any]]: - """Execute a query against Postgres server. Transaction would be automatically committed upon completion. + def execute(self, query: Composable | str, params: tuple[Any] | Any = None) -> list[tuple[Any]]: + r"""Execute a query against Postgres server. Transaction would be automatically committed upon completion. Args: - query (Union[Composable, str]): Query text or a Composable object - params (Union[Tuple[Any], Any]): Tuple of parameter values (or a single value) to replace parameters in the query + query (Composable | str): Query text or a Composable object + params (tuple[Any] | Any): Tuple of parameter values (or a single value) to replace parameters in the query Returns: - List[Tuple[Any]]]: List of tuples returned from the server, or empty list if no rows were selected. + list[tuple[Any]]: List of tuples returned from the server, or empty list if no rows were selected. Raises: AdapterError: Whenever the adapter returns an error. @@ -212,11 +215,8 @@ def execute( LOGGER.debug("Executing query: %s", query) - def execute_task(cursor: BaseCursor) -> Optional[List[Tuple[Any]]]: - if params: - param_set = params if isinstance(params, tuple) else tuple([params]) - else: - param_set = None + def execute_task(cursor: BaseCursor) -> list[tuple[Any]] | None: + param_set = (params if isinstance(params, tuple) else (params,)) if params else None cursor.execute(query, param_set) if cursor.statusmessage: try: @@ -230,27 +230,27 @@ def execute_task(cursor: BaseCursor) -> Optional[List[Tuple[Any]]]: def terminate( self, - all_connections: bool = None, - databases: List[str] = None, - pids: List[int] = None, - roles: List[str] = None, - exclude_roles: List[str] = None, - exclude_databases: List[str] = None, - exclude_pids: List[int] = None, - ) -> List[int]: + all_connections: bool | None = None, + databases: list[str] | None = None, + pids: list[int] | None = None, + roles: list[str] | None = None, + exclude_roles: list[str] | None = None, + exclude_databases: list[str] | None = None, + exclude_pids: list[int] | None = None, + ) -> list[int]: """Terminates connections based on provided parameters. Will avoid terminating system PIDs and self. Args: - databases (List[str]): names of target databases - roles (List[str]): roles which connections should be terminated - pids (List[int]): pids to terminate - exclude_roles (List[str]): roles to exclude - exclude_databases (List[str]): databases to exclude - exclude_pids (List[str]): pids to exclude + databases (list[str]): names of target databases + roles (list[str]): roles which connections should be terminated + pids (list[int]): pids to terminate + exclude_roles (list[str]): roles to exclude + exclude_databases (list[str]): databases to exclude + exclude_pids (list[str]): pids to exclude all_connections (bool): terminate all connections (except own and system PIDs) Returns: - List[int]: PIDs of the terminated connections + list[int]: PIDs of the terminated connections Example: Terminate connections from a specific role to a specific database @@ -268,7 +268,7 @@ def join_sql_in(sql, in_members): return SQL(sql).format(SQL(", ").join([Literal(x) for x in in_members])) sql = SQL("SELECT pid, pg_terminate_backend(pid) FROM pg_stat_activity WHERE {where}") - params: List[Tuple[Union[str, int], ...]] = [] + params: list[tuple[str | int, ...]] = [] # start collecting where clauses depending on parameters # never allow killing system processes or self where = [ @@ -296,7 +296,7 @@ def join_sql_in(sql, in_members): params.append(tuple(exclude_pids)) # join where clauses using AND formatted_sql = sql.format(where=SQL(" AND ").join(where)) - terminated_pids: List[int] = [] + terminated_pids: list[int] = [] result = self.execute(formatted_sql, tuple(params)) if result: terminated_pids.extend([x[0] for x in result]) @@ -534,7 +534,7 @@ def large_objects(self) -> objects.LargeObjectCollection: """Postgres large objects""" return get_lazy_property(self, "large_objects", lambda: objects.LargeObjectCollection(cluster=self)) - def reassign_owner(self, new_owner: str, owner: str = None, objects: list = None): + def reassign_owner(self, new_owner: str, owner: str | None = None, objects: list | None = None): """Reassigns ownership of Postgres objects. When "objects" parameter is provided, only the ownership for those objects will be changed. When both "owner" and "new_owner" are specified, reassigns ownership of all objects owned by "owner". @@ -567,7 +567,7 @@ def get_change(obj): # merge sql code from all pending changes into a single statement, # limited by 30k params in one go, and execute it statements = [] - parameters: List[tuple] = [] + parameters: list[tuple] = [] while changes: change = changes.pop(0) statements.append(change.sql) diff --git a/src/pgmob/errors.py b/src/pgmob/errors.py index 73677e4..31952e2 100644 --- a/src/pgmob/errors.py +++ b/src/pgmob/errors.py @@ -1,4 +1,5 @@ """A list of PGMob errors raised by the module""" + import logging diff --git a/src/pgmob/objects/__init__.py b/src/pgmob/objects/__init__.py index 3ddd361..7649dbf 100644 --- a/src/pgmob/objects/__init__.py +++ b/src/pgmob/objects/__init__.py @@ -7,21 +7,21 @@ """ from .databases import Database, DatabaseCollection -from .replication_slots import ReplicationSlot, ReplicationSlotCollection -from .roles import Role, RoleCollection from .hba_rules import HBARule, HBARuleCollection -from .sequences import Sequence, SequenceCollection -from .tables import Table, TableCollection +from .large_objects import LargeObject, LargeObjectCollection from .procedures import ( - Procedure, - Function, Aggregate, - WindowFunction, - Volatility, + Function, ParallelSafety, - ProcedureVariations, + Procedure, ProcedureCollection, + ProcedureVariations, + Volatility, + WindowFunction, ) -from .views import View, ViewCollection +from .replication_slots import ReplicationSlot, ReplicationSlotCollection +from .roles import Role, RoleCollection from .schemas import Schema, SchemaCollection -from .large_objects import LargeObject, LargeObjectCollection +from .sequences import Sequence, SequenceCollection +from .tables import Table, TableCollection +from .views import View, ViewCollection diff --git a/src/pgmob/objects/databases.py b/src/pgmob/objects/databases.py index 8c3b89a..c7f8f7d 100644 --- a/src/pgmob/objects/databases.py +++ b/src/pgmob/objects/databases.py @@ -1,11 +1,14 @@ """Database objects. Represent databases of the Postgres cluster.""" -from typing import TYPE_CHECKING, Any, Dict, Optional, Union -from ..sql import SQL, Composable, Identifier, Literal -from ..errors import * + +from __future__ import annotations + +from typing import TYPE_CHECKING, Any + from .. import util +from ..errors import PostgresError +from ..sql import SQL, Composable, Identifier, Literal from . import generic - if TYPE_CHECKING: from ..cluster import Cluster @@ -44,14 +47,14 @@ class Database(generic._DynamicObject, generic._CollectionChild): def __init__( self, name: str, - cluster: "Cluster" = None, - parent: "DatabaseCollection" = None, - owner: str = None, - encoding: str = None, - collation: str = None, + cluster: Cluster | None = None, + parent: DatabaseCollection | None = None, + owner: str | None = None, + encoding: str | None = None, + collation: str | None = None, is_template: bool = False, - oid: Optional[int] = None, - from_template: str = None, + oid: int | None = None, + from_template: str | None = None, ): super().__init__(cluster=cluster, name=name, kind="DATABASE", oid=oid) generic._CollectionChild.__init__(self, parent=parent) @@ -60,14 +63,14 @@ def __init__( self._collation = collation self._is_template = is_template self._from_template = from_template - self._character_type: Optional[str] = None + self._character_type: str | None = None self._allow_connections = True - self._connection_limit: Optional[int] = None - self._last_sys_oid: Optional[int] = None - self._frozen_xid: Optional[int] = None - self._min_multixact_id: Optional[int] = None - self._tablespace: Optional[str] = None - self._acl: Optional[str] = None + self._connection_limit: int | None = None + self._last_sys_oid: int | None = None + self._frozen_xid: int | None = None + self._min_multixact_id: int | None = None + self._tablespace: str | None = None + self._acl: str | None = None def _modify(self, column: str, value: Any): """Internal pg_database modification function""" @@ -87,7 +90,7 @@ def name(self, name: str): generic._set_ephemeral_attr(self, "name", name) @property - def owner(self) -> Optional[str]: + def owner(self) -> str | None: return self._owner @owner.setter @@ -95,15 +98,15 @@ def owner(self, owner: str): generic._set_ephemeral_attr(self, "owner", owner) @property - def encoding(self) -> Optional[str]: + def encoding(self) -> str | None: return self._encoding @property - def collation(self) -> Optional[str]: + def collation(self) -> str | None: return self._collation @property - def character_type(self) -> Optional[str]: + def character_type(self) -> str | None: return self._character_type @property @@ -126,7 +129,7 @@ def allow_connections(self) -> bool: return self._allow_connections @property - def connection_limit(self) -> Optional[int]: + def connection_limit(self) -> int | None: return self._connection_limit @connection_limit.setter @@ -141,19 +144,19 @@ def connection_limit(self, value: int): self._connection_limit = value @property - def last_sys_oid(self) -> Optional[int]: + def last_sys_oid(self) -> int | None: return self._last_sys_oid @property - def frozen_xid(self) -> Optional[int]: + def frozen_xid(self) -> int | None: return self._frozen_xid @property - def min_multixact_id(self) -> Optional[int]: + def min_multixact_id(self) -> int | None: return self._min_multixact_id @property - def tablespace(self) -> Optional[str]: + def tablespace(self) -> str | None: return self._tablespace @tablespace.setter @@ -161,7 +164,7 @@ def tablespace(self, value: str): generic._set_ephemeral_attr(self, "tablespace", value) @property - def acl(self) -> Optional[str]: + def acl(self) -> str | None: return self._acl # methods @@ -213,17 +216,17 @@ def create(self): sql = util.get_sql("get_database") + SQL(" WHERE datname = %s") _DatabaseMapper(self.cluster.execute(sql, self.name)[0]).map(self) - def script(self, as_composable: bool = False) -> Union[str, Composable]: + def script(self, as_composable: bool = False) -> str | Composable: """Generate a database creation script. Args: as_composable (bool): return Composable object instead of plain text Returns: - Union[str, Composable]: database creation script + str | Composable: database creation script """ sql = "CREATE DATABASE {db}" - params: Dict[str, Composable] = {"db": self._sql_fqn()} + params: dict[str, Composable] = {"db": self._sql_fqn()} if self._from_template: sql += " TEMPLATE {template}" params["template"] = Identifier(self._from_template) @@ -271,7 +274,7 @@ class _DatabaseMapper(generic._BaseObjectMapper[Database]): class DatabaseCollection(generic._BaseCollection[Database]): """An iterable collection of databases indexed by database name.""" - def __init__(self, cluster: "Cluster"): + def __init__(self, cluster: Cluster): super().__init__(cluster=cluster) if cluster: self.refresh() @@ -289,11 +292,11 @@ def refresh(self): def new( self, name: str, - template: str = None, - owner: str = None, + template: str | None = None, + owner: str | None = None, is_template: bool = False, - encoding: str = None, - collation: str = None, + encoding: str | None = None, + collation: str | None = None, ) -> Database: """Create a database object on the current Postgres cluster. The object is created ephemeral and either needs to be added to the database collection, diff --git a/src/pgmob/objects/generic.py b/src/pgmob/objects/generic.py index 55eea6b..4fd066b 100644 --- a/src/pgmob/objects/generic.py +++ b/src/pgmob/objects/generic.py @@ -1,36 +1,34 @@ """Internal generic objects shared between other objects.""" -from abc import abstractmethod +from __future__ import annotations + +from collections.abc import Iterator from enum import Enum -import re -from typing import Any, Dict, Generic, Iterator, List, Optional, TYPE_CHECKING, Tuple, TypeVar +from typing import TYPE_CHECKING, Any, override + from pgmob import errors -from pgmob._decorators import deprecated -from pgmob.sql import SQL, Identifier, Composable +from pgmob.sql import SQL, Composable, Identifier if TYPE_CHECKING: from ..cluster import Cluster -T = TypeVar("T") - - -class _BasePostgresObject(object): +class _BasePostgresObject: """Base class for any Postgres object with oid. Args: - oid (Optional[int]): object id + oid (int | None): object id Attributes: - oid (Optional[int]): object id + oid (int | None): object id """ - def __init__(self, oid: Optional[int] = None): + def __init__(self, oid: int | None = None): self._oid = oid # properties @property - def oid(self) -> Optional[int]: + def oid(self) -> int | None: return self._oid @oid.setter @@ -44,16 +42,16 @@ def _ephemeral(self) -> bool: return self._oid is None or self._oid <= 0 -class _BaseObjectMapper(Generic[T]): +class _BaseObjectMapper[T]: """Maps the resultset to a Dynamic Object""" - attributes: List[str] = [] - exclude: List[str] = [] + attributes: list[str] = [] + exclude: list[str] = [] def __init__(self, definition: tuple): self.definition = definition - def __getitem__(self, key: str) -> object: + def __getitem__(self, key: str) -> Any: return self.definition[self.attributes.index(key)] def map(self, obj: T) -> T: @@ -70,14 +68,14 @@ def map(self, obj: T) -> T: return obj -class _ClusterBound(object): +class _ClusterBound: """Object that is attached to a Cluster. Implements cluster retrieval internal function""" - def __init__(self, cluster: "Cluster" = None): + def __init__(self, cluster: Cluster | None = None): self._cluster = cluster @property - def cluster(self) -> "Cluster": + def cluster(self) -> Cluster: """Retrieves the Cluster instance bound to the object Returns: @@ -88,7 +86,7 @@ def cluster(self) -> "Cluster": return self._cluster @cluster.setter - def cluster(self, value: "Cluster"): + def cluster(self, value: Cluster): """Bounds object to a cluster Args: @@ -101,15 +99,15 @@ def cluster(self, value: "Cluster"): self._cluster = value -class Fqn(object): +class Fqn: """Fully qualified object name. Defined by schema name (optional) and object name. Args: name (str): object name - schema (Optional[str]): schema name + schema (str | None): schema name """ - def __init__(self, name: str, schema: Optional[str] = None): + def __init__(self, name: str, schema: str | None = None): self.name = name self.schema = schema @@ -132,9 +130,9 @@ def __init__( self, kind: str, name: str, - oid: Optional[int] = None, - schema: Optional[str] = None, - cluster: "Cluster" = None, + oid: int | None = None, + schema: str | None = None, + cluster: Cluster | None = None, ): _BasePostgresObject.__init__(self, oid=oid) _ClusterBound.__init__(self, cluster=cluster) @@ -172,9 +170,9 @@ def __init__( self, kind: str, name: str, - oid: Optional[int] = None, - schema: Optional[str] = None, - cluster: "Cluster" = None, + oid: int | None = None, + schema: str | None = None, + cluster: Cluster | None = None, ): super().__init__(kind=kind, name=name, schema=schema, cluster=cluster, oid=oid) self._changes = _ChangeCollection() @@ -194,27 +192,28 @@ def refresh(self): def _set_ephemeral_attr(obj: _DynamicObject, attr: str, value: Any): - params = dict( - fqn=obj._sql_fqn(), - value=Identifier(value), - ) - stmt_map = dict( - owner=SQL(f"ALTER {obj._kind} {{fqn}} OWNER TO {{value}}").format(**params), - name=SQL(f"ALTER {obj._kind} {{fqn}} RENAME TO {{value}}").format(**params), - schema=SQL(f"ALTER {obj.kind} {{fqn}} SET SCHEMA {{value}}").format(**params), - tablespace=SQL(f"ALTER {obj.kind} {{fqn}} SET TABLESPACE {{value}}").format(**params), - ) + params = { + "fqn": obj._sql_fqn(), + "value": Identifier(value), + } + stmt_map = { + "owner": SQL(f"ALTER {obj._kind} {{fqn}} OWNER TO {{value}}").format(**params), + "name": SQL(f"ALTER {obj._kind} {{fqn}} RENAME TO {{value}}").format(**params), + "schema": SQL(f"ALTER {obj.kind} {{fqn}} SET SCHEMA {{value}}").format(**params), + "tablespace": SQL(f"ALTER {obj.kind} {{fqn}} SET TABLESPACE {{value}}").format(**params), + } if getattr(obj, f"_{attr}") != value: obj._changes[attr] = _SQLChange(obj=obj, sql=stmt_map[attr]) setattr(obj, f"_{attr}", value) -class MappedCollection(Dict[str, T]): +class MappedCollection[T](dict[str, T]): """Class implements an iterable dictionary. Items are accessed via a key, but when iterated over, acts as a list.""" - def __iter__(self) -> Iterator[T]: # type: ignore[override] + @override + def __iter__(self) -> Iterator[T]: # type: ignore[override] # Intentional: yields values, not keys for key in self.keys(): yield self[key] @@ -222,11 +221,12 @@ def __repr__(self) -> str: return f"{type(self).__name__}({self.keys()})" -class SortedMappedCollection(Dict[str, T]): +class SortedMappedCollection[T](dict[str, T]): """Class implements an iterable sorted dictionary. Items are accessed via a key, but when iterated over, acts as a sorted list.""" - def __iter__(self) -> Iterator[T]: # type: ignore[override] + @override + def __iter__(self) -> Iterator[T]: # type: ignore[override] # Intentional: yields values, not keys for key in sorted(self.keys()): yield self[key] @@ -234,18 +234,19 @@ def __repr__(self) -> str: return f"{type(self).__name__}({sorted(self.keys())})" -class _BaseCollection(_ClusterBound, SortedMappedCollection[T]): +class _BaseCollection[T](_ClusterBound, SortedMappedCollection[T]): """Generic Postgres collection object bound to a cluster.""" - def __init__(self, cluster: "Cluster"): - SortedMappedCollection.__init__(self) # type: ignore + def __init__(self, cluster: Cluster): + SortedMappedCollection.__init__(self) # Multiple inheritance init pattern _ClusterBound.__init__(self, cluster=cluster) - def __setitem__(self, key: str, value: T): + @override + def __setitem__(self, key: str, value: T): # Adds cluster binding validation if not isinstance(value, _ClusterBound): raise AttributeError("Unsupported type %s", type(value)) value.cluster = self.cluster - SortedMappedCollection.__setitem__(self, key, value) # type: ignore + SortedMappedCollection.__setitem__(self, key, value) # type: ignore[arg-type] @staticmethod def _index(name: str, schema: str): @@ -269,18 +270,18 @@ def refresh(self): # raise NotImplementedError() -class _CollectionChild(object): +class _CollectionChild: """Defines an object belonging to a Postgres collection""" - def __init__(self, parent: _BaseCollection = None): + def __init__(self, parent: _BaseCollection | None = None): self._parent = parent @property - def parent(self) -> Optional[_BaseCollection]: + def parent(self) -> _BaseCollection | None: return self._parent -class _ObjectChange(object): +class _ObjectChange: def __init__(self, obj: _DynamicObject, task, *args, **kwargs): if not isinstance(obj, _DynamicObject): raise AttributeError("This object class is not supported") @@ -295,7 +296,7 @@ def apply(self): class _SQLChange(_ObjectChange): - def __init__(self, obj: _DynamicObject, sql: Composable, params: tuple = None): + def __init__(self, obj: _DynamicObject, sql: Composable, params: tuple | None = None): def task(): obj.cluster.execute(sql, params) @@ -307,10 +308,10 @@ def task(): class _ChangeCollection(MappedCollection[_ObjectChange]): """An iterable collection of changes indexed by attribute name.""" - def __setitem__(self, key, value): + def __setitem__(self, key: str, value: _ObjectChange): if not isinstance(value, _ObjectChange): raise AttributeError(f"{value.__class__.__name__} is not of an _ObjectChange type") - MappedCollection.__setitem__(self, key, value) + MappedCollection.__setitem__(self, key, value) # type: ignore[arg-type] class AliasEnum(Enum): diff --git a/src/pgmob/objects/hba_rules.py b/src/pgmob/objects/hba_rules.py index 020df0e..df1bb11 100644 --- a/src/pgmob/objects/hba_rules.py +++ b/src/pgmob/objects/hba_rules.py @@ -1,15 +1,18 @@ """HBA rules as collection of strings that ignore whitespace on comparison""" + +from __future__ import annotations + import collections import re -from typing import Any, Iterable, List, Optional, Tuple, Union, TYPE_CHECKING +from collections.abc import Iterable +from typing import TYPE_CHECKING, override from ..adapters import ProgrammingError -from ..sql import SQL, Literal from ..adapters.base import BaseCursor -from ..errors import * +from ..errors import PostgresError +from ..sql import SQL, Literal from . import generic - if TYPE_CHECKING: from ..cluster import Cluster @@ -30,10 +33,10 @@ def __eq__(self, other): def __ne__(self, other): return not self.__eq__(other) - def _get_field(self, name: str) -> Optional[str]: + def _get_field(self, name: str) -> str | None: fields = self.fields field_map = {} - auth_options: List[str] = [] + auth_options: list[str] = [] for i in range(len(fields)): if fields[i].startswith("#"): # anything after is a comment @@ -56,7 +59,7 @@ def _get_field(self, name: str) -> Optional[str]: if i == 4: if field_map["type"] == "local": auth_options.append(fields[i]) - elif re.match("\\d{1,3}\.\\d{1,3}\.\\d{1,3}\\.\\d{1,3}", fields[i]): + elif re.match("\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}", fields[i]): field_map["mask"] = fields[i] else: field_map["auth_method"] = fields[i] @@ -74,43 +77,43 @@ def _get_field(self, name: str) -> Optional[str]: if name == "auth_options": return " ".join(auth_options) else: - return field_map.get(name, None) + return field_map.get(name) @property - def fields(self) -> List[str]: + def fields(self) -> list[str]: """A tuple of pg_hba.conf fields extracted from the record""" return self.split() @property - def type(self) -> Optional[str]: + def type(self) -> str | None: """The type field""" return self._get_field("type") @property - def database(self) -> Optional[str]: + def database(self) -> str | None: """The database field""" return self._get_field("database") @property - def user(self) -> Optional[str]: + def user(self) -> str | None: """The user field""" return self._get_field("user") @property - def address(self) -> Optional[str]: + def address(self) -> str | None: """The address field""" return self._get_field("address") @property - def mask(self) -> Optional[str]: + def mask(self) -> str | None: return self._get_field("mask") @property - def auth_method(self) -> Optional[str]: + def auth_method(self) -> str | None: return self._get_field("auth_method") @property - def auth_options(self) -> Optional[str]: + def auth_options(self) -> str | None: return self._get_field("auth_options") @@ -125,7 +128,7 @@ class HBARuleCollection(collections.UserList[HBARule], generic._ClusterBound): cluster (str): Postgres cluster object """ - def __init__(self, cluster: "Cluster"): + def __init__(self, cluster: Cluster): collections.UserList.__init__(self, []) generic._ClusterBound.__init__(self, cluster=cluster) if cluster: @@ -163,7 +166,8 @@ def task(cursor: BaseCursor): lines = self.cluster.execute_with_cursor(task) self.data.extend([HBARule(x) for x in lines]) - def __contains__(self, o: object) -> bool: + @override + def __contains__(self, o: object) -> bool: # type: ignore[override] # Intentional: accepts any object, converts to HBARule return HBARule(o) in self.data def __iadd__(self, other: Iterable[HBARule]): @@ -174,7 +178,8 @@ def __add__(self, other: Iterable[HBARule]): self.data.extend([HBARule(r) for r in other]) return self - def extend(self, item: Iterable[HBARule]): + @override + def extend(self, item: Iterable[HBARule]): # type: ignore[override] # Intentional: more specific than parent's Iterable[HBARule] """Add multiple HBA rules to the collection Args: @@ -182,35 +187,36 @@ def extend(self, item: Iterable[HBARule]): """ self.data.extend([HBARule(x) for x in item]) - def append(self, item: Union[str, HBARule]): + def append(self, item: str | HBARule): """Append an HBA rule to the collection Args: - item(Union[str, HBARule]): A line from pg_hba or HBARule object + item(str | HBARule): A line from pg_hba or HBARule object """ self.data.append(HBARule(item)) - def remove(self, item: Union[str, HBARule]): + def remove(self, item: str | HBARule): """Remove an HBA rule from the collection Args: - item(Union[str, HBARule]): A line from pg_hba or HBARule object + item(str | HBARule): A line from pg_hba or HBARule object """ self.data.remove(HBARule(item)) - def index(self, item: Union[str, HBARule], *args) -> int: + def index(self, item: str | HBARule, *args) -> int: """Return a first index of a matching HBA rule from the collection Args: - item(Union[str, HBARule]): A line from pg_hba or HBARule object + item(str | HBARule): A line from pg_hba or HBARule object """ return self.data.index(HBARule(item), *args) - def insert(self, index: int, item: Union[str, HBARule]): + @override + def insert(self, index: int, item: str | HBARule): # type: ignore[override] # Intentional: accepts str | HBARule, not just HBARule """Insert an HBA rule into the rule collection with a certain index Args: - item(Union[str, HBARule]): A line from pg_hba or HBARule object + item(str | HBARule): A line from pg_hba or HBARule object index(int): Position to use """ self.data.insert(index, HBARule(item)) diff --git a/src/pgmob/objects/large_objects.py b/src/pgmob/objects/large_objects.py index beba078..bfb055c 100644 --- a/src/pgmob/objects/large_objects.py +++ b/src/pgmob/objects/large_objects.py @@ -1,9 +1,14 @@ """Postgresql largeobject objects""" -from typing import TYPE_CHECKING, Optional -from pgmob.sql import SQL, Literal -from pgmob.adapters.base import BaseLargeObject -from pgmob.errors import * + +from __future__ import annotations + +from typing import TYPE_CHECKING + from pgmob import util +from pgmob.adapters.base import BaseLargeObject +from pgmob.errors import PostgresError +from pgmob.sql import SQL, Literal + from . import generic if TYPE_CHECKING: @@ -27,10 +32,10 @@ class LargeObject(generic._DynamicObject, generic._CollectionChild): def __init__( self, - oid: int = None, - cluster: "Cluster" = None, - parent: "LargeObjectCollection" = None, - owner: str = None, + oid: int | None = None, + cluster: Cluster | None = None, + parent: LargeObjectCollection | None = None, + owner: str | None = None, ): """Initialize a new LargeObject object""" super().__init__(kind="LARGE OBJECT", cluster=cluster, oid=oid, name=str(oid)) @@ -42,6 +47,8 @@ def _sql_fqn(self) -> Literal: def _with_lobject(self, task, mode="rw"): cluster = self.cluster + if self._oid is None: + raise PostgresError("Large object OID is not set") with cluster._no_autocommit(): lo = cluster.adapter.lobject(self._oid, mode=mode) result = task(lo) @@ -49,7 +56,7 @@ def _with_lobject(self, task, mode="rw"): return result @property - def owner(self) -> Optional[str]: + def owner(self) -> str | None: return self._owner @owner.setter diff --git a/src/pgmob/objects/procedures.py b/src/pgmob/objects/procedures.py index d7ada06..4d7cbd1 100644 --- a/src/pgmob/objects/procedures.py +++ b/src/pgmob/objects/procedures.py @@ -1,11 +1,14 @@ """Postgresql procedure objects""" -from typing import TYPE_CHECKING, Dict, List, Optional, Type -from ..sql import SQL, Identifier, Composable -from ..errors import * + +from __future__ import annotations + +from typing import TYPE_CHECKING + from .. import util +from ..errors import PostgresError +from ..sql import SQL, Composable, Identifier from . import generic - if TYPE_CHECKING: from ..cluster import Cluster @@ -54,7 +57,7 @@ class _BaseProcedure(generic._DynamicObject, generic._CollectionChild): strict (bool): Whether the function is strict volatility (Volatility): Volatility mode (IMMUTABLE/STABLE/VOLATILE) parallel_mode (ParallelSafety): Parallel mode (SAFE/RESTRICTED/UNSAFE) - argument_types (List[str]): A list of argument types, if any + argument_types (list[str]): A list of argument types, if any oid (int): Procedure OID parent (ProcedureCollection): parent collection @@ -69,7 +72,7 @@ class _BaseProcedure(generic._DynamicObject, generic._CollectionChild): strict (bool): Whether the function is strict volatility (Volatility): Volatility mode (IMMUTABLE/STABLE/VOLATILE) parallel_mode (ParallelSafety): Parallel mode (SAFE/RESTRICTED/UNSAFE) - argument_types (List[str]): A list of argument types, if any + argument_types (list[str]): A list of argument types, if any oid (int): Procedure OID """ @@ -78,11 +81,11 @@ def __init__( name: str, kind: str, schema: str = "public", - argument_types: List[str] = None, - oid: Optional[int] = None, - parent: "ProcedureCollection" = None, - owner: str = None, - cluster: "Cluster" = None, + argument_types: list[str] | None = None, + oid: int | None = None, + parent: ProcedureCollection | None = None, + owner: str | None = None, + cluster: Cluster | None = None, language: str = "sql", security_definer: bool = False, strict: bool = False, @@ -129,7 +132,7 @@ def parallel_mode(self) -> ParallelSafety: return self._parallel_mode @property - def argument_types(self) -> Optional[List[str]]: + def argument_types(self) -> list[str] | None: return self._argument_types @property @@ -160,7 +163,7 @@ def refresh(self): mapper.map(self) @property - def owner(self) -> Optional[str]: + def owner(self) -> str | None: return self._owner @owner.setter @@ -168,7 +171,7 @@ def owner(self, owner: str): generic._set_ephemeral_attr(self, "owner", owner) @property - def schema(self) -> Optional[str]: + def schema(self) -> str | None: return self._schema @schema.setter @@ -197,7 +200,7 @@ class Procedure(_BaseProcedure): strict (bool): Whether the procedure is strict volatility (Volatility): Volatility mode (IMMUTABLE/STABLE/VOLATILE) parallel_mode (ParallelSafety): Parallel mode (SAFE/RESTRICTED/UNSAFE) - argument_types (List[str]): A list of argument types, if any + argument_types (list[str]): A list of argument types, if any oid (int): Procedure OID Attributes: @@ -211,12 +214,13 @@ class Procedure(_BaseProcedure): strict (bool): Whether the function is strict volatility (Volatility): Volatility mode (IMMUTABLE/STABLE/VOLATILE) parallel_mode (ParallelSafety): Parallel mode (SAFE/RESTRICTED/UNSAFE) - argument_types (List[str]): A list of argument types, if any + argument_types (list[str]): A list of argument types, if any oid (int): Procedure OID """ def __init__(self, *args, **kwargs): - super().__init__(kind=ProcedureKind.PROCEDURE.name, *args, **kwargs) + kwargs["kind"] = ProcedureKind.PROCEDURE.name + super().__init__(*args, **kwargs) class Function(_BaseProcedure): @@ -232,7 +236,7 @@ class Function(_BaseProcedure): strict (bool): Whether the function is strict volatility (Volatility): Volatility mode (IMMUTABLE/STABLE/VOLATILE) parallel_mode (ParallelSafety): Parallel mode (SAFE/RESTRICTED/UNSAFE) - argument_types (List[str]): A list of argument types, if any + argument_types (list[str]): A list of argument types, if any oid (int): Procedure OID Attributes: @@ -245,12 +249,13 @@ class Function(_BaseProcedure): strict (bool): Whether the function is strict volatility (Volatility): Volatility mode (IMMUTABLE/STABLE/VOLATILE) parallel_mode (ParallelSafety): Parallel mode (SAFE/RESTRICTED/UNSAFE) - argument_types (List[str]): A list of argument types, if any + argument_types (list[str]): A list of argument types, if any oid (int): Procedure OID """ def __init__(self, *args, **kwargs): - super().__init__(kind=ProcedureKind.FUNCTION.name, *args, **kwargs) + kwargs["kind"] = ProcedureKind.FUNCTION.name + super().__init__(*args, **kwargs) class WindowFunction(_BaseProcedure): @@ -266,7 +271,7 @@ class WindowFunction(_BaseProcedure): strict (bool): Whether the function is strict volatility (Volatility): Volatility mode (IMMUTABLE/STABLE/VOLATILE) parallel_mode (ParallelSafety): Parallel mode (SAFE/RESTRICTED/UNSAFE) - argument_types (List[str]): A list of argument types, if any + argument_types (list[str]): A list of argument types, if any oid (int): Procedure OID Attributes: @@ -279,12 +284,13 @@ class WindowFunction(_BaseProcedure): strict (bool): Whether the function is strict volatility (Volatility): Volatility mode (IMMUTABLE/STABLE/VOLATILE) parallel_mode (ParallelSafety): Parallel mode (SAFE/RESTRICTED/UNSAFE) - argument_types (List[str]): A list of argument types, if any + argument_types (list[str]): A list of argument types, if any oid (int): Procedure OID """ def __init__(self, *args, **kwargs): - super().__init__(kind=ProcedureKind.WINDOW_FUNCTION.name, *args, **kwargs) + kwargs["kind"] = ProcedureKind.WINDOW_FUNCTION.name + super().__init__(*args, **kwargs) class Aggregate(_BaseProcedure): @@ -300,7 +306,7 @@ class Aggregate(_BaseProcedure): strict (bool): Whether the function is strict volatility (Volatility): Volatility mode (IMMUTABLE/STABLE/VOLATILE) parallel_mode (ParallelSafety): Parallel mode (SAFE/RESTRICTED/UNSAFE) - argument_types (List[str]): A list of argument types, if any + argument_types (list[str]): A list of argument types, if any oid (int): Procedure OID @@ -314,12 +320,13 @@ class Aggregate(_BaseProcedure): strict (bool): Whether the function is strict volatility (Volatility): Volatility mode (IMMUTABLE/STABLE/VOLATILE) parallel_mode (ParallelSafety): Parallel mode (SAFE/RESTRICTED/UNSAFE) - argument_types (List[str]): A list of argument types, if any + argument_types (list[str]): A list of argument types, if any oid (int): Procedure OID """ def __init__(self, *args, **kwargs): - super().__init__(kind=ProcedureKind.AGGREGATE.name, *args, **kwargs) + kwargs["kind"] = ProcedureKind.AGGREGATE.name + super().__init__(*args, **kwargs) class _ProcedureMapper(generic._BaseObjectMapper[_BaseProcedure]): @@ -355,15 +362,15 @@ def map(self, obj: _BaseProcedure) -> _BaseProcedure: return obj -class ProcedureVariations(List[_BaseProcedure], generic._ClusterBound): +class ProcedureVariations(list[_BaseProcedure], generic._ClusterBound): """A list of procedures with the same name, but variable argument sets.""" - def __init__(self, cluster: "Cluster"): + def __init__(self, cluster: Cluster): super().__init__() generic._ClusterBound.__init__(self, cluster=cluster) -_procedure_kinds: Dict[str, Type[_BaseProcedure]] = { +_procedure_kinds: dict[str, type[_BaseProcedure]] = { "f": Function, "p": Procedure, "w": WindowFunction, @@ -378,7 +385,7 @@ class ProcedureCollection(generic._BaseCollection[ProcedureVariations]): set of arguments. """ - def __init__(self, cluster: "Cluster"): + def __init__(self, cluster: Cluster): super().__init__(cluster=cluster) if cluster: self.refresh() @@ -404,7 +411,9 @@ def refresh(self): proc_class = _procedure_kinds[kind] except KeyError: raise PostgresError(f"Unknown procedure kind: {kind}") - proc = proc_class( + # Subclasses provide 'kind' via kwargs in their __init__ + # Type checker can't verify dynamic class instantiation signature compatibility + proc = proc_class( # type: ignore[call-arg] # Dynamic instantiation: subclasses add 'kind' in __init__ name=mapper["name"], schema=mapper["schema"], oid=mapper["oid"], diff --git a/src/pgmob/objects/replication_slots.py b/src/pgmob/objects/replication_slots.py index 9e62e2d..a3c3a11 100644 --- a/src/pgmob/objects/replication_slots.py +++ b/src/pgmob/objects/replication_slots.py @@ -1,9 +1,13 @@ """Replication slots. Represent replication slots of the Postgres cluster.""" -from typing import TYPE_CHECKING, Optional, Union + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from .. import util from ..adapters import AdapterError -from ..sql import SQL, Literal, Composable from ..errors import PostgresError -from .. import util +from ..sql import SQL, Composable, Literal from . import generic if TYPE_CHECKING: @@ -39,8 +43,8 @@ def __init__( self, name: str, plugin: str, - cluster: "Cluster" = None, - parent: "ReplicationSlotCollection" = None, + cluster: Cluster | None = None, + parent: ReplicationSlotCollection | None = None, ): """Initialize a new ReplicationSlot object""" super().__init__(cluster=cluster, name=name, kind="REPLICATION SLOT") @@ -58,7 +62,7 @@ def __init__( # properties @property - def plugin(self) -> Optional[str]: + def plugin(self) -> str | None: return self._plugin @property @@ -66,7 +70,7 @@ def slot_type(self) -> str: return self._slot_type @property - def database(self) -> Optional[str]: + def database(self) -> str | None: return self._database @property @@ -74,27 +78,27 @@ def temporary(self) -> bool: return self._temporary @property - def is_active(self) -> Optional[bool]: + def is_active(self) -> bool | None: return self._is_active @property - def active_pid(self) -> Optional[int]: + def active_pid(self) -> int | None: return self._active_pid @property - def xmin(self) -> Optional[int]: + def xmin(self) -> int | None: return self._xmin @property - def catalog_xmin(self) -> Optional[int]: + def catalog_xmin(self) -> int | None: return self._catalog_xmin @property - def restart_lsn(self) -> Optional[int]: + def restart_lsn(self) -> int | None: return self._restart_lsn @property - def confirmed_flush_lsn(self) -> Optional[int]: + def confirmed_flush_lsn(self) -> int | None: return self._confirmed_flush_lsn # methods @@ -122,14 +126,14 @@ def create(self) -> None: self.cluster.execute(self.script(as_composable=True)) self.refresh() - def script(self, as_composable: bool = False) -> Union[str, Composable]: + def script(self, as_composable: bool = False) -> str | Composable: """Generate a database creation script. Args: as_composable (bool): return Composable object instead of plain text Returns: - Union[str, Composable]: replication slot creation script + str | Composable: replication slot creation script """ sql = SQL("SELECT pg_create_logical_replication_slot({}, {})") @@ -181,7 +185,7 @@ class _ReplicationSlotMapper(generic._BaseObjectMapper[ReplicationSlot]): class ReplicationSlotCollection(generic._BaseCollection[ReplicationSlot]): """An iterable collection of replication slots indexed by slot name.""" - def __init__(self, cluster: "Cluster"): + def __init__(self, cluster: Cluster): super().__init__(cluster=cluster) if cluster: self.refresh() diff --git a/src/pgmob/objects/roles.py b/src/pgmob/objects/roles.py index dfa165c..4db5162 100644 --- a/src/pgmob/objects/roles.py +++ b/src/pgmob/objects/roles.py @@ -1,13 +1,14 @@ """Postgresql roles""" + from datetime import datetime -from typing import TYPE_CHECKING, Any, Dict, Optional, Union -from ..adapters import AdapterError -from ..sql import SQL, Composable, Literal, Identifier -from ..errors import * +from typing import TYPE_CHECKING, Any + from .. import util +from ..adapters import AdapterError +from ..errors import PostgresError +from ..sql import SQL, Composable, Identifier, Literal from . import generic - if TYPE_CHECKING: from ..cluster import Cluster @@ -50,8 +51,8 @@ class Role(generic._DynamicObject, generic._CollectionChild): def __init__( self, name: str, - password: str = None, - cluster: "Cluster" = None, + password: str | None = None, + cluster: "Cluster | None" = None, superuser: bool = False, inherit: bool = True, createrole: bool = False, @@ -60,9 +61,9 @@ def __init__( replication: bool = False, bypassrls: bool = False, connection_limit: int = -1, - valid_until: datetime = None, - oid: int = None, - parent: "RoleCollection" = None, + valid_until: datetime | None = None, + oid: int | None = None, + parent: "RoleCollection | None" = None, ): super().__init__(kind="ROLE", cluster=cluster, oid=oid, name=name) generic._CollectionChild.__init__(self, parent=parent) @@ -144,11 +145,11 @@ def bypassrls(self, value: bool): self._set_permission("bypassrls", value) @property - def valid_until(self) -> Optional[datetime]: + def valid_until(self) -> datetime | None: return self._valid_until @valid_until.setter - def valid_until(self, value: Optional[datetime]): + def valid_until(self, value: datetime | None): self._set_attribute("valid_until", str(value)) @property @@ -156,7 +157,7 @@ def connection_limit(self) -> int: return self._connection_limit @connection_limit.setter - def connection_limit(self, value: Optional[int]): + def connection_limit(self, value: int | None): self._set_attribute("connection_limit", value) def _set_attribute(self, attr: str, value: Any): @@ -174,14 +175,14 @@ def _set_permission(self, attr: str, value: bool): self._changes[attr] = generic._SQLChange(obj=self, sql=stmt) setattr(self, f"_{attr}", permission) - def script(self, as_composable: bool = False) -> Union[str, Composable]: + def script(self, as_composable: bool = False) -> str | Composable: """Scripts out a role. Args: as_composable (bool): return Composable object instead of plain text Returns: - Union[str, Composable]: role creation script + str | Composable: role creation script """ permission_list = { "SUPERUSER": self.superuser, @@ -193,12 +194,12 @@ def script(self, as_composable: bool = False) -> Union[str, Composable]: "BYPASSRLS": self.bypassrls, } sql = "CREATE ROLE {role}" - params: Dict[str, Composable] = {"role": self._sql_fqn()} + params: dict[str, Composable] = {"role": self._sql_fqn()} password = self._password if self._password else self.get_password_md5() if password: sql += " PASSWORD {password}" params["password"] = Literal(password) - for permission in permission_list.keys(): + for permission in permission_list: sql += " " + (permission if permission_list[permission] else f"NO{permission}") if self.connection_limit is not None: sql += " CONNECTION LIMIT {limit}" @@ -236,7 +237,7 @@ def drop(self, force: bool = False) -> None: raise self.cluster.execute(SQL("DROP ROLE {fqn}").format(fqn=self._sql_fqn())) - def get_password_md5(self) -> Optional[str]: + def get_password_md5(self) -> str | None: """Returns md5 password hash for the role""" sql = SQL("SELECT rolpassword FROM pg_catalog.pg_authid WHERE rolname = %s") result = self.cluster.execute(sql, self.name) @@ -311,7 +312,7 @@ def refresh(self): def new( self, name: str, - password: str = None, + password: str | None = None, superuser: bool = False, inherit: bool = True, createrole: bool = False, @@ -320,7 +321,7 @@ def new( replication: bool = False, bypassrls: bool = False, connection_limit: int = -1, - valid_until: datetime = None, + valid_until: datetime | None = None, ) -> Role: """Create a role object on the current Postgres cluster. The object is created ephemeral and either needs to be added to the role collection, diff --git a/src/pgmob/objects/schemas.py b/src/pgmob/objects/schemas.py index 7232e49..8a4b34d 100644 --- a/src/pgmob/objects/schemas.py +++ b/src/pgmob/objects/schemas.py @@ -1,11 +1,12 @@ """Schema objects. Represents schemas on the Postgres cluster""" -from typing import TYPE_CHECKING, Any, Dict, Optional, Union -from ..sql import SQL, Composable, Identifier -from ..errors import * + +from typing import TYPE_CHECKING + from .. import util +from ..errors import PostgresError +from ..sql import SQL, Composable, Identifier from . import generic - if TYPE_CHECKING: from ..cluster import Cluster @@ -30,10 +31,10 @@ class Schema(generic._DynamicObject, generic._CollectionChild): def __init__( self, name: str, - owner: str = None, - cluster: "Cluster" = None, - parent: "SchemaCollection" = None, - oid: Optional[int] = None, + owner: str | None = None, + cluster: "Cluster | None" = None, + parent: "SchemaCollection | None" = None, + oid: int | None = None, ): super().__init__(cluster=cluster, name=name, kind="SCHEMA", oid=oid) generic._CollectionChild.__init__(self, parent=parent) @@ -48,7 +49,7 @@ def name(self, name: str): generic._set_ephemeral_attr(self, "name", name) @property - def owner(self) -> Optional[str]: + def owner(self) -> str | None: return self._owner @owner.setter @@ -84,17 +85,17 @@ def create(self): sql = util.get_sql("get_schema") + SQL(" WHERE n.nspname = %s") _SchemaMapper(self.cluster.execute(sql, self.name)[0]).map(self) - def script(self, as_composable: bool = False) -> Union[str, Composable]: + def script(self, as_composable: bool = False) -> str | Composable: """Generate a schema creation script. Args: as_composable (bool): return Composable object instead of plain text Returns: - Union[str, Composable]: schema creation script + str | Composable: schema creation script """ sql = "CREATE SCHEMA {schema}" - params: Dict[str, Composable] = {"schema": self._sql_fqn()} + params: dict[str, Composable] = {"schema": self._sql_fqn()} if self.owner: sql += " AUTHORIZATION {owner}" params["owner"] = Identifier(self.owner) @@ -136,7 +137,7 @@ def refresh(self): def new( self, name: str, - owner: str = None, + owner: str | None = None, ) -> Schema: """Create a schema object on the current Postgres cluster. The object is created ephemeral and either needs to be added to the schema collection, diff --git a/src/pgmob/objects/sequences.py b/src/pgmob/objects/sequences.py index 338e0bf..be269ff 100644 --- a/src/pgmob/objects/sequences.py +++ b/src/pgmob/objects/sequences.py @@ -1,11 +1,14 @@ """Sequence objects""" -from typing import TYPE_CHECKING, Optional -from ..sql import SQL, Literal -from ..errors import * + +from __future__ import annotations + +from typing import TYPE_CHECKING + from .. import util +from ..errors import PostgresError +from ..sql import SQL, Literal from . import generic - if TYPE_CHECKING: from ..cluster import Cluster @@ -41,24 +44,24 @@ def __init__( self, name: str, schema: str = "public", - owner: str = None, - cluster: "Cluster" = None, - parent: "SequenceCollection" = None, - oid: Optional[int] = None, + owner: str | None = None, + cluster: Cluster | None = None, + parent: SequenceCollection | None = None, + oid: int | None = None, ): """Initialize a new Sequence object""" super().__init__(kind="SEQUENCE", cluster=cluster, oid=oid, name=name, schema=schema) generic._CollectionChild.__init__(self, parent=parent) self._owner = owner self._schema: str = schema - self._data_type: Optional[str] = None - self._start_value: Optional[int] = None - self._min_value: Optional[int] = None - self._max_value: Optional[int] = None - self._increment_by: Optional[int] = None - self._cycle: Optional[bool] = None - self._cache_size: Optional[int] = None - self._last_value: Optional[int] = None + self._data_type: str | None = None + self._start_value: int | None = None + self._min_value: int | None = None + self._max_value: int | None = None + self._increment_by: int | None = None + self._cycle: bool | None = None + self._cache_size: int | None = None + self._last_value: int | None = None @property def data_type(self): @@ -73,7 +76,7 @@ def data_type(self, value: str): self._data_type = value @property - def start_value(self) -> Optional[int]: + def start_value(self) -> int | None: return self._start_value @start_value.setter @@ -88,13 +91,13 @@ def start_value(self, value: int): self._start_value = value @property - def min_value(self) -> Optional[int]: + def min_value(self) -> int | None: return self._min_value @min_value.setter def min_value(self, value: int): if self._min_value != value: - if value == None: + if value is None: sql = SQL("ALTER SEQUENCE {fqn} NO MINVALUE").format(fqn=self._sql_fqn()) else: sql = SQL("ALTER SEQUENCE {fqn} MINVALUE {value}").format( @@ -104,13 +107,13 @@ def min_value(self, value: int): self._min_value = value @property - def max_value(self) -> Optional[int]: + def max_value(self) -> int | None: return self._max_value @max_value.setter def max_value(self, value: int): if self._max_value != value: - if value == None: + if value is None: sql = SQL("ALTER SEQUENCE {fqn} NO MAXVALUE").format(fqn=self._sql_fqn()) else: sql = SQL("ALTER SEQUENCE {fqn} MAXVALUE {value}").format( @@ -120,7 +123,7 @@ def max_value(self, value: int): self._max_value = value @property - def increment_by(self) -> Optional[int]: + def increment_by(self) -> int | None: return self._increment_by @increment_by.setter @@ -135,19 +138,19 @@ def increment_by(self, value: int): self._increment_by = value @property - def cycle(self) -> Optional[bool]: + def cycle(self) -> bool | None: return self._cycle @property - def cache_size(self) -> Optional[int]: + def cache_size(self) -> int | None: return self._cache_size @property - def last_value(self) -> Optional[int]: + def last_value(self) -> int | None: return self._last_value @property - def owner(self) -> Optional[str]: + def owner(self) -> str | None: return self._owner @owner.setter @@ -201,10 +204,7 @@ def setval(self, value: int, is_called: bool = False): Args: is_called (bool): set is_called flag """ - if is_called: - sql = SQL("SELECT setval(%s, %s, true)") - else: - sql = SQL("SELECT setval(%s, %s)") + sql = SQL("SELECT setval(%s, %s, true)") if is_called else SQL("SELECT setval(%s, %s)") self.cluster.execute(sql, (self.oid, value))[0][0] def refresh(self): @@ -243,7 +243,7 @@ class SequenceCollection(generic._BaseCollection[Sequence]): For sequences outside of the 'public' schema, index becomes "schemaname.sequencename". """ - def __init__(self, cluster: "Cluster"): + def __init__(self, cluster: Cluster): super().__init__(cluster=cluster) if cluster: self.refresh() diff --git a/src/pgmob/objects/tables.py b/src/pgmob/objects/tables.py index 0fcd72f..e148ddd 100644 --- a/src/pgmob/objects/tables.py +++ b/src/pgmob/objects/tables.py @@ -1,11 +1,14 @@ """Postgresql table objects""" -from typing import TYPE_CHECKING, Optional -from ..sql import SQL, Identifier -from ..errors import * + +from __future__ import annotations + +from typing import TYPE_CHECKING + from .. import util +from ..errors import PostgresError +from ..sql import SQL, Identifier from . import generic - if TYPE_CHECKING: from ..cluster import Cluster @@ -35,17 +38,17 @@ def __init__( self, name: str, schema: str = "public", - owner: str = None, - cluster: "Cluster" = None, - parent: "TableCollection" = None, - oid: Optional[int] = None, + owner: str | None = None, + cluster: Cluster | None = None, + parent: TableCollection | None = None, + oid: int | None = None, ): """Initialize a new Table object""" super().__init__(kind="TABLE", cluster=cluster, oid=oid, name=name, schema=schema) generic._CollectionChild.__init__(self, parent=parent) self._schema: str = schema self._owner = owner - self._tablespace: Optional[str] = None + self._tablespace: str | None = None self._row_security: bool = False def drop(self, cascade: bool = False): @@ -100,7 +103,7 @@ def row_security(self, value: bool): self._row_security = value @property - def owner(self) -> Optional[str]: + def owner(self) -> str | None: return self._owner @owner.setter @@ -142,7 +145,7 @@ class TableCollection(generic._BaseCollection[Table]): For tables outside of the 'public' schema, index becomes "schemaname.tablename". """ - def __init__(self, cluster: "Cluster"): + def __init__(self, cluster: Cluster): super().__init__(cluster=cluster) if cluster: self.refresh() diff --git a/src/pgmob/objects/views.py b/src/pgmob/objects/views.py index dde148f..a721c75 100644 --- a/src/pgmob/objects/views.py +++ b/src/pgmob/objects/views.py @@ -1,11 +1,14 @@ """Postgresql view objects""" -from typing import TYPE_CHECKING, Optional -from ..sql import SQL -from ..errors import * + +from __future__ import annotations + +from typing import TYPE_CHECKING + from .. import util +from ..errors import PostgresError +from ..sql import SQL from . import generic - if TYPE_CHECKING: from ..cluster import Cluster @@ -33,10 +36,10 @@ def __init__( self, name: str, schema: str = "public", - owner: str = None, - cluster: "Cluster" = None, - parent: "ViewCollection" = None, - oid: Optional[int] = None, + owner: str | None = None, + cluster: Cluster | None = None, + parent: ViewCollection | None = None, + oid: int | None = None, ): """Initialize a new View object""" super().__init__(kind="VIEW", cluster=cluster, oid=oid, name=name, schema=schema) @@ -67,7 +70,7 @@ def refresh(self): mapper.map(self) @property - def owner(self) -> Optional[str]: + def owner(self) -> str | None: return self._owner @owner.setter @@ -107,7 +110,7 @@ class ViewCollection(generic._BaseCollection[View]): For views outside of the 'public' schema, index becomes "schemaname.tablename". """ - def __init__(self, cluster: "Cluster"): + def __init__(self, cluster: Cluster): super().__init__(cluster=cluster) if cluster: self.refresh() diff --git a/src/pgmob/os.py b/src/pgmob/os.py index 268670b..cd9ce0a 100644 --- a/src/pgmob/os.py +++ b/src/pgmob/os.py @@ -1,11 +1,13 @@ """PGMob will try to abstract OS commands using classes that provide support for a specific OS.""" -from abc import abstractmethod + import shlex +from abc import abstractmethod + from . import util from .errors import PostgresShellCommandError -class _BaseShellEnv(object): +class _BaseShellEnv: """Base OS class interface that outlines necessary shell-dependent operations""" @staticmethod @@ -65,7 +67,7 @@ def get_os_command_wrapper() -> str: return util.get_shell("run_postgres_command") -class OSCommandResult(object): +class OSCommandResult: """Results of the OS command execution Attributes: diff --git a/src/pgmob/sql.py b/src/pgmob/sql.py index 2d40df6..abcced0 100644 --- a/src/pgmob/sql.py +++ b/src/pgmob/sql.py @@ -2,18 +2,19 @@ classes are eventually decoded by adapters into SQL statements with appropriate syntax, parameters and quotation. """ -import string +from __future__ import annotations -from typing import Generator, List, Sequence, Union +import string +from collections.abc import Generator, Sequence class Composable: """Common interface for SQL-like objects""" - def __add__(self, other: "Composable") -> "Composed": + def __add__(self, other: Composable) -> Composed: return Composed(self, other) - def __mul__(self, other: int) -> "Composed": + def __mul__(self, other: int) -> Composed: if not isinstance(other, int): raise TypeError("int type is required") return Composed(*[self for _ in range(other)]) @@ -24,7 +25,7 @@ def __eq__(self, other: object) -> bool: else: return False - def compose(self) -> "Composed": + def compose(self) -> Composed: """Method utilized by an adapter to retrieve a Composed object that contains a list of objects and is ready to be iterated upon. All iterated objects are guaranteed to be one of: SQL, Literal, Identifier. @@ -71,19 +72,17 @@ class Composed(Composable): def __init__(self, *args: Composable) -> None: self._parts = list(self._process_parts(list(args))) - def _process_parts(self, parts: Union[List[Composable], "Composed"]) -> Generator[_Singleton, None, None]: + def _process_parts(self, parts: list[Composable] | Composed) -> Generator[_Singleton]: for part in iter(parts): if isinstance(part, Composed): - for part in self._process_parts(part): - yield part + yield from self._process_parts(part) elif isinstance(part, _Singleton): yield part else: - raise TypeError("Unexpected type " "%s" "", part.__class__.__name__) + raise TypeError("Unexpected type %s", part.__class__.__name__) - def __iter__(self) -> Generator[_Singleton, None, None]: - for part in iter(self._parts): - yield part + def __iter__(self) -> Generator[_Singleton]: + yield from iter(self._parts) def __len__(self) -> int: return len(self._parts) @@ -150,7 +149,7 @@ def format(self, *args: Composable, **kwargs: Composable) -> Composed: formatter = string.Formatter() parts = [] field_counter = -1 - for text, field_name, format_spec, conversion in formatter.parse(str(self._value)): + for text, field_name, _format_spec, _conversion in formatter.parse(str(self._value)): parts.append(SQL(text)) if field_name is not None: if field_name: diff --git a/src/pgmob/util.py b/src/pgmob/util.py index 541fe95..5ab351e 100644 --- a/src/pgmob/util.py +++ b/src/pgmob/util.py @@ -1,17 +1,16 @@ """Internal module utilities.""" -from typing import Callable, Dict, List, Sequence, Type, TypeVar -from pgmob.sql import SQL -from pathlib import Path + import re -from functools import reduce from collections import defaultdict -from packaging.version import Version as _Version +from collections.abc import Callable, Sequence +from pathlib import Path +from packaging.version import Version as _Version -_T = TypeVar("_T") +from pgmob.sql import SQL -def group_by(key: Callable[..., str], seq: Sequence[_T]) -> Dict[str, List[_T]]: +def group_by[T](key: Callable[..., str], seq: Sequence[T]) -> dict[str, list[T]]: """Groups a list by key Args: @@ -21,7 +20,11 @@ def group_by(key: Callable[..., str], seq: Sequence[_T]) -> Dict[str, List[_T]]: Returns: dict: a dictionary grouped by keys """ - return reduce(lambda grp, val: grp[key(val)].append(val) or grp, seq, defaultdict(list)) # type: ignore + # Type checker needs explicit annotation for defaultdict with lambda in reduce + result: dict[str, list[T]] = defaultdict(list) + for val in seq: + result[key(val)].append(val) + return result class Version(_Version): @@ -38,14 +41,14 @@ def revision(self): def __new__(cls, version): try: parts = [int(x) for x in version.split(".")] - except: + except (ValueError, AttributeError): raise ValueError("Unsupported version string. Only dot-separated numbers are supported.") if len(parts) == 0: raise ValueError("Empty version string") return _Version.__new__(cls) -def get_sql(name: str, version: Version = None) -> SQL: +def get_sql(name: str, version: Version | None = None) -> SQL: """Retrieves SQL code from a file in a 'sql' folder Args: diff --git a/src/tests/conftest.py b/src/tests/conftest.py index 4d08c21..b46f66c 100644 --- a/src/tests/conftest.py +++ b/src/tests/conftest.py @@ -1,18 +1,20 @@ from collections import namedtuple -from typing import List -import pytest from unittest.mock import Mock + +import pytest from pytest_mock import MockerFixture -from pgmob.sql import Composed + from pgmob.adapters.base import BaseAdapter from pgmob.cluster import Cluster +from pgmob.sql import Composed class PGMobTester: @staticmethod - def _parse_calls(*args, statement: int = None) -> List[str]: - singletons: List[str] = [] - statements = [args[statement]] if statement else args + def _parse_calls(*args, statement: int | None = None) -> list[str]: + singletons: list[str] = [] + # If statement is specified, only process that specific call + statements = [args[statement]] if statement is not None else args for singleton in [x.args[0] for x in statements]: if isinstance(singleton, Composed): singletons.extend([str(x._value) for x in singleton._parts]) @@ -21,13 +23,29 @@ def _parse_calls(*args, statement: int = None) -> List[str]: return singletons @staticmethod - def assertSql(sql: str, cursor: Mock, statement: int = None, mogrify: bool = False): - singletons = PGMobTester._parse_calls( - *(cursor.mogrify.call_args_list if mogrify else cursor.execute.call_args_list) + def assertSql(sql: str, cursor: Mock, mogrify: bool = False, statement: int | None = None): + # If statement is None, parse all calls and check all singletons + # If statement is provided, it refers to the index in the final singletons list + if statement is None: + singletons = PGMobTester._parse_calls( + *(cursor.mogrify.call_args_list if mogrify else cursor.execute.call_args_list) + ) + else: + # Parse all calls to get all singletons, then check specific index + all_singletons = PGMobTester._parse_calls( + *(cursor.mogrify.call_args_list if mogrify else cursor.execute.call_args_list) + ) + # Check if the statement index is valid + if statement >= len(all_singletons): + raise IndexError( + f"Statement index {statement} out of range (only {len(all_singletons)} singletons)" + ) + # Check only the specific singleton at the given index + singletons = [all_singletons[statement]] + + assert any(sql in x for x in singletons), ( + "{sql} was supposed to be among statements:\n{stmts}".format(sql=sql, stmts="\n".join(singletons)) ) - assert any( - [sql in x for x in singletons] - ), "{sql} was supposed to be among statements:\n{stmts}".format(sql=sql, stmts="\n".join(singletons)) @pytest.fixture diff --git a/src/tests/functional/conftest.py b/src/tests/functional/conftest.py index 17e43bb..6e96a77 100644 --- a/src/tests/functional/conftest.py +++ b/src/tests/functional/conftest.py @@ -1,11 +1,13 @@ -from typing import Callable -from types import ModuleType -from dataclasses import dataclass -import pytest -import docker import os import time +from collections.abc import Callable +from dataclasses import dataclass +from types import ModuleType + +import docker +import pytest from docker.types import ContainerSpec + from pgmob.cluster import Cluster @@ -71,9 +73,11 @@ def container(docker_client: docker.DockerClient, container_name, pg_password): # wait until pg is ready attempts = 0 while attempts < 30: - if container.exec_run("pg_isready").exit_code == 0: - if container.exec_run('psql -U postgres -c "select 1"').exit_code == 0: - break + if ( + container.exec_run("pg_isready").exit_code == 0 + and container.exec_run('psql -U postgres -c "select 1"').exit_code == 0 + ): + break attempts += 1 time.sleep(1) @@ -214,7 +218,7 @@ def tablespace(psql, container): @pytest.fixture -def connect(container, container_name, pg_password): +def connect(container, hostname, pg_password): """Cluster object factory. Args: @@ -227,7 +231,7 @@ def wrapper(db=None, adapter=None): if not adapter: adapter = _psycopg2.Psycopg2Adapter(cursor_factory=None) return Cluster( - host=container_name, + host=hostname, port=5432, user="postgres", password=pg_password, diff --git a/src/tests/functional/test_adapters.py b/src/tests/functional/test_adapters.py index d06d28d..f5b4ffc 100644 --- a/src/tests/functional/test_adapters.py +++ b/src/tests/functional/test_adapters.py @@ -1,14 +1,14 @@ -from typing import Sequence import pytest -from pgmob.sql import SQL, Identifier, Literal + from pgmob.adapters import ProgrammingError, detect_adapter from pgmob.adapters.base import BaseAdapter, BaseCursor, BaseLargeObject +from pgmob.sql import SQL, Identifier, Literal ADAPTERS = ["psycopg2"] @pytest.fixture() -def adapter_factory(container, container_name, pg_password, db): +def adapter_factory(container, hostname, pg_password, db): """Adapter factory. Args: @@ -21,7 +21,7 @@ def wrapper(adapter_type: str): adapter = _psycopg2.Psycopg2Adapter() adapter.connect( - host=container_name, + host=hostname, port=5432, user="postgres", password=pg_password, @@ -137,10 +137,9 @@ def test_unlink(self, adapter, lo_ids_factory, psql, db): adapter.commit() assert self.get_current(psql, db, lo_id) == "" - with pytest.raises(Exception): - with adapter.lobject(lo_id, "r") as lobject_r: - with pytest.raises(Exception): - lobject_r.unlink() + # After unlinking, trying to open the large object should raise an error + with pytest.raises((OSError, IOError, Exception)), adapter.lobject(lo_id, "r") as lobject_r: + lobject_r.unlink() def test_read(self, adapter, lo_ids_factory, db): lo_id = lo_ids_factory(db=db)[0] @@ -157,10 +156,9 @@ def test_write(self, adapter, lo_ids_factory, psql, db): adapter.commit() assert self.get_current(psql, db, lo_id) == "new data" - with pytest.raises(Exception): - with adapter.lobject(lo_id, "r") as lobject_r: - with pytest.raises(Exception): - lobject_r.write(b"new data2") + # Writing to a read-only large object should raise an error + with pytest.raises((OSError, IOError, Exception)), adapter.lobject(lo_id, "r") as lobject_r: + lobject_r.write(b"new data2") def test_truncate(self, adapter, lo_ids_factory, psql, db): lo_id = lo_ids_factory(db=db)[0] @@ -174,10 +172,9 @@ def test_truncate(self, adapter, lo_ids_factory, psql, db): adapter.commit() assert self.get_current(psql, db, lo_id) == "" - with pytest.raises(Exception): - with adapter.lobject(lo_id, "r") as lobject_r: - with pytest.raises(Exception): - lobject_r.truncate() + # Truncating a read-only large object should raise an error + with pytest.raises((OSError, IOError, Exception)), adapter.lobject(lo_id, "r") as lobject_r: + lobject_r.truncate() class TestAdapter: diff --git a/src/tests/functional/test_backup.py b/src/tests/functional/test_backup.py index 6cd4d8c..46901d2 100644 --- a/src/tests/functional/test_backup.py +++ b/src/tests/functional/test_backup.py @@ -1,5 +1,7 @@ -import pytest import doctest + +import pytest + from pgmob.backup import FileBackup, FileRestore from pgmob.errors import PostgresShellCommandError @@ -36,7 +38,6 @@ def test_file_base_path_backup(self, connect, db_with_table, container): cleanup_file(container, path) def test_nonexistent_base_path_backup(self, connect, db_with_table): - path = "/nonexistingpath/" + db_with_table cluster = connect() backup = FileBackup(cluster=cluster, base_path="/nonexistingpath/") pytest.raises( diff --git a/src/tests/functional/test_cluster.py b/src/tests/functional/test_cluster.py index 7468b8a..6afdd1c 100644 --- a/src/tests/functional/test_cluster.py +++ b/src/tests/functional/test_cluster.py @@ -1,8 +1,10 @@ -from pgmob import objects, util, Cluster -from pgmob.errors import PostgresShellCommandError -import pytest import doctest +import pytest + +from pgmob import Cluster, objects, util +from pgmob.errors import PostgresShellCommandError + @pytest.fixture def db_table_owner(psql, db, role): diff --git a/src/tests/functional/test_hba_rules.py b/src/tests/functional/test_hba_rules.py index d576ff8..ec9a288 100644 --- a/src/tests/functional/test_hba_rules.py +++ b/src/tests/functional/test_hba_rules.py @@ -20,7 +20,7 @@ def test_alter(self, cluster, container): rules.refresh() assert "local postgres postgres any" in rules assert "local postgres postgres any" in container.exec_run( - f"cat /var/lib/postgresql/data/pg_hba.conf" + "cat /var/lib/postgresql/data/pg_hba.conf" ).output.decode("utf8").split("\n") # remove rule rules.remove(objects.HBARule(rule)) @@ -28,5 +28,5 @@ def test_alter(self, cluster, container): rules.refresh() assert "local postgres postgres any" not in rules assert "local postgres postgres any" not in container.exec_run( - f"cat /var/lib/postgresql/data/pg_hba.conf" + "cat /var/lib/postgresql/data/pg_hba.conf" ).output.decode("utf8").split("\n") diff --git a/src/tests/functional/test_large_objects.py b/src/tests/functional/test_large_objects.py index 7f90dc3..a1632fa 100644 --- a/src/tests/functional/test_large_objects.py +++ b/src/tests/functional/test_large_objects.py @@ -1,4 +1,5 @@ import pytest + from pgmob import objects diff --git a/src/tests/functional/test_procedures.py b/src/tests/functional/test_procedures.py index 6616408..923c50f 100644 --- a/src/tests/functional/test_procedures.py +++ b/src/tests/functional/test_procedures.py @@ -1,4 +1,5 @@ import pytest + from pgmob import objects @@ -8,7 +9,7 @@ def procedures(psql, db, cluster_db, schema): func_list = ["public.tmpzzz", f"{schema}.tmpzzz"] for f in func_list: assert ( - psql(f"CREATE FUNCTION {f} (a int) RETURNS int" " AS 'SELECT $1 a' LANGUAGE SQL", db=db).exit_code + psql(f"CREATE FUNCTION {f} (a int) RETURNS int AS 'SELECT $1 a' LANGUAGE SQL", db=db).exit_code == 0 ) assert psql(f"CREATE FUNCTION {f} () RETURNS int AS 'SELECT 1' LANGUAGE SQL", db=db).exit_code == 0 diff --git a/src/tests/functional/test_replication_slots.py b/src/tests/functional/test_replication_slots.py index a0b6337..de6039d 100644 --- a/src/tests/functional/test_replication_slots.py +++ b/src/tests/functional/test_replication_slots.py @@ -2,7 +2,7 @@ class TestFunctionalReplicationSlot: - slot_query = "SELECT {field} FROM pg_catalog.pg_replication_slots" " WHERE slot_name = '{slot}'" + slot_query = "SELECT {field} FROM pg_catalog.pg_replication_slots WHERE slot_name = '{slot}'" def test_init(self, connect, replication_slot, plugin, db): cluster = connect() @@ -34,9 +34,7 @@ def test_script(self, connect, replication_slot, plugin): slots = objects.ReplicationSlotCollection(cluster=cluster) assert slots[ replication_slot - ].script() == f"SELECT pg_create_logical_replication_slot('{replication_slot}', '{plugin}')".encode( - "utf8" - ) + ].script() == f"SELECT pg_create_logical_replication_slot('{replication_slot}', '{plugin}')".encode() def test_create(self, connect, db, psql, plugin): cluster = connect(db=db) diff --git a/src/tests/functional/test_roles.py b/src/tests/functional/test_roles.py index e810166..c27a588 100644 --- a/src/tests/functional/test_roles.py +++ b/src/tests/functional/test_roles.py @@ -1,5 +1,6 @@ -from datetime import date, datetime import re +from datetime import date + import pytest from pgmob import objects @@ -25,7 +26,7 @@ def roles(cluster, tmp_role): class TestFunctionalRoles: - role_query = "SELECT {field} FROM pg_catalog.pg_roles" " WHERE rolname = '{role}'" + role_query = "SELECT {field} FROM pg_catalog.pg_roles WHERE rolname = '{role}'" def test_roles(self, roles: objects.RoleCollection, tmp_role: str): day = date.today() diff --git a/src/tests/functional/test_schemas.py b/src/tests/functional/test_schemas.py index 6763132..ce583bc 100644 --- a/src/tests/functional/test_schemas.py +++ b/src/tests/functional/test_schemas.py @@ -1,5 +1,7 @@ import re + import pytest + from pgmob import objects @@ -12,7 +14,7 @@ def schemas(psql, db, cluster_db): ] for s in schema_list: psql(f'CREATE SCHEMA "{s}"', db=db) - psql(f'CREATE TABLE "tmp-2".a (a int)', db=db) + psql('CREATE TABLE "tmp-2".a (a int)', db=db) schemas = objects.SchemaCollection(cluster=cluster_db) yield schemas diff --git a/src/tests/functional/test_sequences.py b/src/tests/functional/test_sequences.py index b2e73bd..f082821 100644 --- a/src/tests/functional/test_sequences.py +++ b/src/tests/functional/test_sequences.py @@ -1,4 +1,5 @@ import pytest + from pgmob import objects diff --git a/src/tests/functional/test_tables.py b/src/tests/functional/test_tables.py index 6753643..4798bd4 100644 --- a/src/tests/functional/test_tables.py +++ b/src/tests/functional/test_tables.py @@ -1,4 +1,5 @@ import pytest + from pgmob import objects diff --git a/src/tests/functional/test_views.py b/src/tests/functional/test_views.py index 340cdd9..61fa081 100644 --- a/src/tests/functional/test_views.py +++ b/src/tests/functional/test_views.py @@ -1,4 +1,5 @@ import pytest + from pgmob import objects diff --git a/src/tests/test_backup.py b/src/tests/test_backup.py index 1f5718d..75c7315 100644 --- a/src/tests/test_backup.py +++ b/src/tests/test_backup.py @@ -1,6 +1,6 @@ from unittest.mock import call -from pgmob.backup import FileBackup, FileRestore, GCPBackup, GCPRestore, BackupOptions, RestoreOptions +from pgmob.backup import BackupOptions, FileBackup, FileRestore, GCPBackup, GCPRestore, RestoreOptions class TestBackup: @@ -230,9 +230,7 @@ def test_file_restore_shared_params(self, cluster): restore.options.set_role = "mahrole" restore.restore(database="foo", path="/tmp/foo") cluster.run_os_command.assert_called_with( - command=( - 'pg_restore --schema-only --table="a" --table="b"' ' --role="mahrole" -d "foo" "/tmp/foo"' - ) + command=('pg_restore --schema-only --table="a" --table="b" --role="mahrole" -d "foo" "/tmp/foo"') ) def test_file_restore_params(self, cluster): diff --git a/src/tests/test_cluster.py b/src/tests/test_cluster.py index bb30d64..255458a 100644 --- a/src/tests/test_cluster.py +++ b/src/tests/test_cluster.py @@ -1,11 +1,13 @@ from unittest.mock import MagicMock, call + +import pytest from pytest_mock import MockerFixture -from pgmob.sql import SQL, Identifier + +from pgmob import objects, util from pgmob.cluster import Cluster -from pgmob.objects import generic from pgmob.errors import PostgresShellCommandError -from pgmob import objects, util -import pytest +from pgmob.objects import generic +from pgmob.sql import SQL, Identifier class TestCluster: diff --git a/src/tests/test_databases.py b/src/tests/test_databases.py index d8bbac5..a7af8dc 100644 --- a/src/tests/test_databases.py +++ b/src/tests/test_databases.py @@ -1,7 +1,9 @@ -import pytest from unittest.mock import call -from pgmob.sql import SQL, Identifier + +import pytest + from pgmob import objects +from pgmob.sql import SQL, Identifier @pytest.fixture @@ -108,8 +110,8 @@ def test_alter(self, database: objects.Database, db_cursor, db_tuples): def test_disable(self, database, cursor, pgmob_tester): database.disable() - pgmob_tester.assertSql("UPDATE", cursor, statement=0) - pgmob_tester.assertSql("False", cursor, statement=3) + pgmob_tester.assertSql("UPDATE", cursor, statement=1) + pgmob_tester.assertSql("False", cursor, statement=4) def test_create(self, database: objects.Database, db_cursor, pgmob_tester): database.create() diff --git a/src/tests/test_decorators.py b/src/tests/test_decorators.py index 2820061..a9d0dc1 100644 --- a/src/tests/test_decorators.py +++ b/src/tests/test_decorators.py @@ -1,5 +1,4 @@ -from pgmob._decorators import lazy_property, get_lazy_property, LAZY_PREFIX -from dataclasses import dataclass, field +from pgmob._decorators import get_lazy_property, lazy_property def test_lazy_property(): diff --git a/src/tests/test_large_objects.py b/src/tests/test_large_objects.py index eef0610..a4e9ce4 100644 --- a/src/tests/test_large_objects.py +++ b/src/tests/test_large_objects.py @@ -1,9 +1,9 @@ -from importlib import import_module from unittest.mock import call + import pytest -from pgmob.sql import SQL, Identifier, Literal -from pgmob.cluster import _NoAutocommitContextManager + from pgmob import objects +from pgmob.sql import SQL, Identifier, Literal @pytest.fixture diff --git a/src/tests/test_os.py b/src/tests/test_os.py index dad0ef4..f156a63 100644 --- a/src/tests/test_os.py +++ b/src/tests/test_os.py @@ -1,4 +1,5 @@ import pytest + from pgmob import os diff --git a/src/tests/test_procedures.py b/src/tests/test_procedures.py index c3a7429..c4bfab6 100644 --- a/src/tests/test_procedures.py +++ b/src/tests/test_procedures.py @@ -1,8 +1,10 @@ from unittest.mock import call + import pytest -from pgmob.sql import SQL, Identifier + from pgmob import objects from pgmob.objects.procedures import _BaseProcedure +from pgmob.sql import SQL, Identifier from pgmob.util import Version diff --git a/src/tests/test_replication_slots.py b/src/tests/test_replication_slots.py index a296f97..74982e1 100644 --- a/src/tests/test_replication_slots.py +++ b/src/tests/test_replication_slots.py @@ -1,4 +1,5 @@ import pytest + from pgmob import objects diff --git a/src/tests/test_roles.py b/src/tests/test_roles.py index 01a56b3..a8e5ba1 100644 --- a/src/tests/test_roles.py +++ b/src/tests/test_roles.py @@ -1,4 +1,5 @@ import pytest + from pgmob import objects diff --git a/src/tests/test_schemas.py b/src/tests/test_schemas.py index 6735ff5..a2f11c5 100644 --- a/src/tests/test_schemas.py +++ b/src/tests/test_schemas.py @@ -1,4 +1,5 @@ import pytest + from pgmob import objects diff --git a/src/tests/test_sequences.py b/src/tests/test_sequences.py index ce33374..83eea86 100644 --- a/src/tests/test_sequences.py +++ b/src/tests/test_sequences.py @@ -1,7 +1,9 @@ from unittest.mock import call + import pytest -from pgmob.sql import SQL, Identifier + from pgmob import objects +from pgmob.sql import SQL, Identifier @pytest.fixture @@ -55,14 +57,14 @@ def test_init(self, sequence, sequence_tuples): def test_drop(self, cursor, sequence, pgmob_tester): sequence.drop() - pgmob_tester.assertSql(f"DROP SEQUENCE ", cursor) + pgmob_tester.assertSql("DROP SEQUENCE ", cursor) pgmob_tester.assertSql(sequence.name, cursor) pgmob_tester.assertSql(sequence.schema, cursor) def test_drop_cascade(self, cursor, sequence, pgmob_tester): sequence.drop(True) - pgmob_tester.assertSql(f"DROP SEQUENCE ", cursor) - pgmob_tester.assertSql(f" CASCADE", cursor) + pgmob_tester.assertSql("DROP SEQUENCE ", cursor) + pgmob_tester.assertSql(" CASCADE", cursor) pgmob_tester.assertSql(sequence.name, cursor) pgmob_tester.assertSql(sequence.schema, cursor) diff --git a/src/tests/test_sql.py b/src/tests/test_sql.py index bdbcd5c..5a3fd83 100644 --- a/src/tests/test_sql.py +++ b/src/tests/test_sql.py @@ -1,5 +1,7 @@ from datetime import datetime + import pytest + from pgmob.sql import * diff --git a/src/tests/test_tables.py b/src/tests/test_tables.py index 7d2750f..f2c1312 100644 --- a/src/tests/test_tables.py +++ b/src/tests/test_tables.py @@ -1,7 +1,9 @@ from unittest.mock import call + import pytest -from pgmob.sql import SQL, Identifier + from pgmob import objects +from pgmob.sql import SQL, Identifier @pytest.fixture @@ -42,22 +44,22 @@ def test_init(self, table_tuples, table): assert table.owner == tbl[1] assert table.schema == tbl[2] assert table.tablespace is None - assert table.row_security == False + assert not table.row_security assert table.oid == tbl[5] assert str(table) == f"Table('{_get_key(tbl)}')" def test_drop(self, cursor, table, pgmob_tester): table.drop() - pgmob_tester.assertSql(f"DROP TABLE ", cursor) + pgmob_tester.assertSql("DROP TABLE ", cursor) pgmob_tester.assertSql(table.name, cursor) pgmob_tester.assertSql(table.schema, cursor) def test_drop_cascade(self, cursor, table, pgmob_tester): table.drop(True) - pgmob_tester.assertSql(f"DROP TABLE ", cursor) + pgmob_tester.assertSql("DROP TABLE ", cursor) pgmob_tester.assertSql(table.name, cursor) pgmob_tester.assertSql(table.schema, cursor) - pgmob_tester.assertSql(f" CASCADE", cursor) + pgmob_tester.assertSql(" CASCADE", cursor) def test_refresh(self, table, table_cursor, table_tuples, pgmob_tester): tbl = table_tuples[0] diff --git a/src/tests/test_util.py b/src/tests/test_util.py index 4f79798..f5ba0f6 100644 --- a/src/tests/test_util.py +++ b/src/tests/test_util.py @@ -1,8 +1,10 @@ +import re from collections import namedtuple + import pytest + from pgmob.sql import SQL from pgmob.util import * -import re class TestVersion: diff --git a/src/tests/test_views.py b/src/tests/test_views.py index aa5beff..ba927da 100644 --- a/src/tests/test_views.py +++ b/src/tests/test_views.py @@ -1,7 +1,9 @@ from unittest.mock import call + import pytest -from pgmob.sql import SQL, Identifier + from pgmob import objects +from pgmob.sql import SQL, Identifier @pytest.fixture @@ -46,16 +48,16 @@ def test_init(self, view_tuples, view): def test_drop(self, cursor, view, pgmob_tester): view.drop() - pgmob_tester.assertSql(f"DROP VIEW ", cursor) + pgmob_tester.assertSql("DROP VIEW ", cursor) pgmob_tester.assertSql(view.name, cursor) pgmob_tester.assertSql(view.schema, cursor) def test_drop_cascade(self, cursor, view, pgmob_tester): view.drop(True) - pgmob_tester.assertSql(f"DROP VIEW ", cursor) + pgmob_tester.assertSql("DROP VIEW ", cursor) pgmob_tester.assertSql(view.name, cursor) pgmob_tester.assertSql(view.schema, cursor) - pgmob_tester.assertSql(f" CASCADE", cursor) + pgmob_tester.assertSql(" CASCADE", cursor) def test_refresh(self, view, view_cursor, view_tuples, pgmob_tester): v = view_tuples[0] diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..12cc858 --- /dev/null +++ b/uv.lock @@ -0,0 +1,440 @@ +version = 1 +revision = 3 +requires-python = ">=3.13" + +[[package]] +name = "certifi" +version = "2026.1.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/2d/a891ca51311197f6ad14a7ef42e2399f36cf2f9bd44752b3dc4eab60fdc5/certifi-2026.1.4.tar.gz", hash = "sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120", size = 154268, upload-time = "2026-01-04T02:42:41.825Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl", hash = "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c", size = 152900, upload-time = "2026-01-04T02:42:40.15Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", size = 208091, upload-time = "2025-10-14T04:41:13.346Z" }, + { url = "https://files.pythonhosted.org/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", size = 147936, upload-time = "2025-10-14T04:41:14.461Z" }, + { url = "https://files.pythonhosted.org/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", size = 144180, upload-time = "2025-10-14T04:41:15.588Z" }, + { url = "https://files.pythonhosted.org/packages/91/ed/9706e4070682d1cc219050b6048bfd293ccf67b3d4f5a4f39207453d4b99/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", size = 161346, upload-time = "2025-10-14T04:41:16.738Z" }, + { url = "https://files.pythonhosted.org/packages/d5/0d/031f0d95e4972901a2f6f09ef055751805ff541511dc1252ba3ca1f80cf5/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", size = 158874, upload-time = "2025-10-14T04:41:17.923Z" }, + { url = "https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", size = 153076, upload-time = "2025-10-14T04:41:19.106Z" }, + { url = "https://files.pythonhosted.org/packages/75/1e/5ff781ddf5260e387d6419959ee89ef13878229732732ee73cdae01800f2/charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", size = 150601, upload-time = "2025-10-14T04:41:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/d7/57/71be810965493d3510a6ca79b90c19e48696fb1ff964da319334b12677f0/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", size = 150376, upload-time = "2025-10-14T04:41:21.398Z" }, + { url = "https://files.pythonhosted.org/packages/e5/d5/c3d057a78c181d007014feb7e9f2e65905a6c4ef182c0ddf0de2924edd65/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", size = 144825, upload-time = "2025-10-14T04:41:22.583Z" }, + { url = "https://files.pythonhosted.org/packages/e6/8c/d0406294828d4976f275ffbe66f00266c4b3136b7506941d87c00cab5272/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", size = 162583, upload-time = "2025-10-14T04:41:23.754Z" }, + { url = "https://files.pythonhosted.org/packages/d7/24/e2aa1f18c8f15c4c0e932d9287b8609dd30ad56dbe41d926bd846e22fb8d/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", size = 150366, upload-time = "2025-10-14T04:41:25.27Z" }, + { url = "https://files.pythonhosted.org/packages/e4/5b/1e6160c7739aad1e2df054300cc618b06bf784a7a164b0f238360721ab86/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", size = 160300, upload-time = "2025-10-14T04:41:26.725Z" }, + { url = "https://files.pythonhosted.org/packages/7a/10/f882167cd207fbdd743e55534d5d9620e095089d176d55cb22d5322f2afd/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", size = 154465, upload-time = "2025-10-14T04:41:28.322Z" }, + { url = "https://files.pythonhosted.org/packages/89/66/c7a9e1b7429be72123441bfdbaf2bc13faab3f90b933f664db506dea5915/charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", size = 99404, upload-time = "2025-10-14T04:41:29.95Z" }, + { url = "https://files.pythonhosted.org/packages/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", size = 107092, upload-time = "2025-10-14T04:41:31.188Z" }, + { url = "https://files.pythonhosted.org/packages/af/8f/3ed4bfa0c0c72a7ca17f0380cd9e4dd842b09f664e780c13cff1dcf2ef1b/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", size = 100408, upload-time = "2025-10-14T04:41:32.624Z" }, + { url = "https://files.pythonhosted.org/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746, upload-time = "2025-10-14T04:41:33.773Z" }, + { url = "https://files.pythonhosted.org/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889, upload-time = "2025-10-14T04:41:34.897Z" }, + { url = "https://files.pythonhosted.org/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641, upload-time = "2025-10-14T04:41:36.116Z" }, + { url = "https://files.pythonhosted.org/packages/ba/33/0ad65587441fc730dc7bd90e9716b30b4702dc7b617e6ba4997dc8651495/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", size = 160779, upload-time = "2025-10-14T04:41:37.229Z" }, + { url = "https://files.pythonhosted.org/packages/67/ed/331d6b249259ee71ddea93f6f2f0a56cfebd46938bde6fcc6f7b9a3d0e09/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", size = 159035, upload-time = "2025-10-14T04:41:38.368Z" }, + { url = "https://files.pythonhosted.org/packages/67/ff/f6b948ca32e4f2a4576aa129d8bed61f2e0543bf9f5f2b7fc3758ed005c9/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", size = 152542, upload-time = "2025-10-14T04:41:39.862Z" }, + { url = "https://files.pythonhosted.org/packages/16/85/276033dcbcc369eb176594de22728541a925b2632f9716428c851b149e83/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", size = 149524, upload-time = "2025-10-14T04:41:41.319Z" }, + { url = "https://files.pythonhosted.org/packages/9e/f2/6a2a1f722b6aba37050e626530a46a68f74e63683947a8acff92569f979a/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", size = 150395, upload-time = "2025-10-14T04:41:42.539Z" }, + { url = "https://files.pythonhosted.org/packages/60/bb/2186cb2f2bbaea6338cad15ce23a67f9b0672929744381e28b0592676824/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", size = 143680, upload-time = "2025-10-14T04:41:43.661Z" }, + { url = "https://files.pythonhosted.org/packages/7d/a5/bf6f13b772fbb2a90360eb620d52ed8f796f3c5caee8398c3b2eb7b1c60d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", size = 162045, upload-time = "2025-10-14T04:41:44.821Z" }, + { url = "https://files.pythonhosted.org/packages/df/c5/d1be898bf0dc3ef9030c3825e5d3b83f2c528d207d246cbabe245966808d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", size = 149687, upload-time = "2025-10-14T04:41:46.442Z" }, + { url = "https://files.pythonhosted.org/packages/a5/42/90c1f7b9341eef50c8a1cb3f098ac43b0508413f33affd762855f67a410e/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", size = 160014, upload-time = "2025-10-14T04:41:47.631Z" }, + { url = "https://files.pythonhosted.org/packages/76/be/4d3ee471e8145d12795ab655ece37baed0929462a86e72372fd25859047c/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", size = 154044, upload-time = "2025-10-14T04:41:48.81Z" }, + { url = "https://files.pythonhosted.org/packages/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", size = 99940, upload-time = "2025-10-14T04:41:49.946Z" }, + { url = "https://files.pythonhosted.org/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", size = 107104, upload-time = "2025-10-14T04:41:51.051Z" }, + { url = "https://files.pythonhosted.org/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", size = 100743, upload-time = "2025-10-14T04:41:52.122Z" }, + { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "coverage" +version = "7.13.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/11/43/3e4ac666cc35f231fa70c94e9f38459299de1a152813f9d2f60fc5f3ecaf/coverage-7.13.3.tar.gz", hash = "sha256:f7f6182d3dfb8802c1747eacbfe611b669455b69b7c037484bb1efbbb56711ac", size = 826832, upload-time = "2026-02-03T14:02:30.944Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/f3/4c333da7b373e8c8bfb62517e8174a01dcc373d7a9083698e3b39d50d59c/coverage-7.13.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:853c3d3c79ff0db65797aad79dee6be020efd218ac4510f15a205f1e8d13ce25", size = 219468, upload-time = "2026-02-03T14:00:45.829Z" }, + { url = "https://files.pythonhosted.org/packages/d6/31/0714337b7d23630c8de2f4d56acf43c65f8728a45ed529b34410683f7217/coverage-7.13.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f75695e157c83d374f88dcc646a60cb94173304a9258b2e74ba5a66b7614a51a", size = 219839, upload-time = "2026-02-03T14:00:47.407Z" }, + { url = "https://files.pythonhosted.org/packages/12/99/bd6f2a2738144c98945666f90cae446ed870cecf0421c767475fcf42cdbe/coverage-7.13.3-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2d098709621d0819039f3f1e471ee554f55a0b2ac0d816883c765b14129b5627", size = 250828, upload-time = "2026-02-03T14:00:49.029Z" }, + { url = "https://files.pythonhosted.org/packages/6f/99/97b600225fbf631e6f5bfd3ad5bcaf87fbb9e34ff87492e5a572ff01bbe2/coverage-7.13.3-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:16d23d6579cf80a474ad160ca14d8b319abaa6db62759d6eef53b2fc979b58c8", size = 253432, upload-time = "2026-02-03T14:00:50.655Z" }, + { url = "https://files.pythonhosted.org/packages/5f/5c/abe2b3490bda26bd4f5e3e799be0bdf00bd81edebedc2c9da8d3ef288fa8/coverage-7.13.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:00d34b29a59d2076e6f318b30a00a69bf63687e30cd882984ed444e753990cc1", size = 254672, upload-time = "2026-02-03T14:00:52.757Z" }, + { url = "https://files.pythonhosted.org/packages/31/ba/5d1957c76b40daff53971fe0adb84d9c2162b614280031d1d0653dd010c1/coverage-7.13.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ab6d72bffac9deb6e6cb0f61042e748de3f9f8e98afb0375a8e64b0b6e11746b", size = 251050, upload-time = "2026-02-03T14:00:54.332Z" }, + { url = "https://files.pythonhosted.org/packages/69/dc/dffdf3bfe9d32090f047d3c3085378558cb4eb6778cda7de414ad74581ed/coverage-7.13.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e129328ad1258e49cae0123a3b5fcb93d6c2fa90d540f0b4c7cdcdc019aaa3dc", size = 252801, upload-time = "2026-02-03T14:00:56.121Z" }, + { url = "https://files.pythonhosted.org/packages/87/51/cdf6198b0f2746e04511a30dc9185d7b8cdd895276c07bdb538e37f1cd50/coverage-7.13.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2213a8d88ed35459bda71597599d4eec7c2ebad201c88f0bfc2c26fd9b0dd2ea", size = 250763, upload-time = "2026-02-03T14:00:58.719Z" }, + { url = "https://files.pythonhosted.org/packages/d7/1a/596b7d62218c1d69f2475b69cc6b211e33c83c902f38ee6ae9766dd422da/coverage-7.13.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:00dd3f02de6d5f5c9c3d95e3e036c3c2e2a669f8bf2d3ceb92505c4ce7838f67", size = 250587, upload-time = "2026-02-03T14:01:01.197Z" }, + { url = "https://files.pythonhosted.org/packages/f7/46/52330d5841ff660f22c130b75f5e1dd3e352c8e7baef5e5fef6b14e3e991/coverage-7.13.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f9bada7bc660d20b23d7d312ebe29e927b655cf414dadcdb6335a2075695bd86", size = 252358, upload-time = "2026-02-03T14:01:02.824Z" }, + { url = "https://files.pythonhosted.org/packages/36/8a/e69a5be51923097ba7d5cff9724466e74fe486e9232020ba97c809a8b42b/coverage-7.13.3-cp313-cp313-win32.whl", hash = "sha256:75b3c0300f3fa15809bd62d9ca8b170eb21fcf0100eb4b4154d6dc8b3a5bbd43", size = 222007, upload-time = "2026-02-03T14:01:04.876Z" }, + { url = "https://files.pythonhosted.org/packages/0a/09/a5a069bcee0d613bdd48ee7637fa73bc09e7ed4342b26890f2df97cc9682/coverage-7.13.3-cp313-cp313-win_amd64.whl", hash = "sha256:a2f7589c6132c44c53f6e705e1a6677e2b7821378c22f7703b2cf5388d0d4587", size = 222812, upload-time = "2026-02-03T14:01:07.296Z" }, + { url = "https://files.pythonhosted.org/packages/3d/4f/d62ad7dfe32f9e3d4a10c178bb6f98b10b083d6e0530ca202b399371f6c1/coverage-7.13.3-cp313-cp313-win_arm64.whl", hash = "sha256:123ceaf2b9d8c614f01110f908a341e05b1b305d6b2ada98763b9a5a59756051", size = 221433, upload-time = "2026-02-03T14:01:09.156Z" }, + { url = "https://files.pythonhosted.org/packages/04/b2/4876c46d723d80b9c5b695f1a11bf5f7c3dabf540ec00d6edc076ff025e6/coverage-7.13.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:cc7fd0f726795420f3678ac82ff882c7fc33770bd0074463b5aef7293285ace9", size = 220162, upload-time = "2026-02-03T14:01:11.409Z" }, + { url = "https://files.pythonhosted.org/packages/fc/04/9942b64a0e0bdda2c109f56bda42b2a59d9d3df4c94b85a323c1cae9fc77/coverage-7.13.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d358dc408edc28730aed5477a69338e444e62fba0b7e9e4a131c505fadad691e", size = 220510, upload-time = "2026-02-03T14:01:13.038Z" }, + { url = "https://files.pythonhosted.org/packages/5a/82/5cfe1e81eae525b74669f9795f37eb3edd4679b873d79d1e6c1c14ee6c1c/coverage-7.13.3-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5d67b9ed6f7b5527b209b24b3df9f2e5bf0198c1bbf99c6971b0e2dcb7e2a107", size = 261801, upload-time = "2026-02-03T14:01:14.674Z" }, + { url = "https://files.pythonhosted.org/packages/0b/ec/a553d7f742fd2cd12e36a16a7b4b3582d5934b496ef2b5ea8abeb10903d4/coverage-7.13.3-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:59224bfb2e9b37c1335ae35d00daa3a5b4e0b1a20f530be208fff1ecfa436f43", size = 263882, upload-time = "2026-02-03T14:01:16.343Z" }, + { url = "https://files.pythonhosted.org/packages/e1/58/8f54a2a93e3d675635bc406de1c9ac8d551312142ff52c9d71b5e533ad45/coverage-7.13.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ae9306b5299e31e31e0d3b908c66bcb6e7e3ddca143dea0266e9ce6c667346d3", size = 266306, upload-time = "2026-02-03T14:01:18.02Z" }, + { url = "https://files.pythonhosted.org/packages/1a/be/e593399fd6ea1f00aee79ebd7cc401021f218d34e96682a92e1bae092ff6/coverage-7.13.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:343aaeb5f8bb7bcd38620fd7bc56e6ee8207847d8c6103a1e7b72322d381ba4a", size = 261051, upload-time = "2026-02-03T14:01:19.757Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e5/e9e0f6138b21bcdebccac36fbfde9cf15eb1bbcea9f5b1f35cd1f465fb91/coverage-7.13.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b2182129f4c101272ff5f2f18038d7b698db1bf8e7aa9e615cb48440899ad32e", size = 263868, upload-time = "2026-02-03T14:01:21.487Z" }, + { url = "https://files.pythonhosted.org/packages/9a/bf/de72cfebb69756f2d4a2dde35efcc33c47d85cd3ebdf844b3914aac2ef28/coverage-7.13.3-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:94d2ac94bd0cc57c5626f52f8c2fffed1444b5ae8c9fc68320306cc2b255e155", size = 261498, upload-time = "2026-02-03T14:01:23.097Z" }, + { url = "https://files.pythonhosted.org/packages/f2/91/4a2d313a70fc2e98ca53afd1c8ce67a89b1944cd996589a5b1fe7fbb3e5c/coverage-7.13.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:65436cde5ecabe26fb2f0bf598962f0a054d3f23ad529361326ac002c61a2a1e", size = 260394, upload-time = "2026-02-03T14:01:24.949Z" }, + { url = "https://files.pythonhosted.org/packages/40/83/25113af7cf6941e779eb7ed8de2a677865b859a07ccee9146d4cc06a03e3/coverage-7.13.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:db83b77f97129813dbd463a67e5335adc6a6a91db652cc085d60c2d512746f96", size = 262579, upload-time = "2026-02-03T14:01:26.703Z" }, + { url = "https://files.pythonhosted.org/packages/1e/19/a5f2b96262977e82fb9aabbe19b4d83561f5d063f18dde3e72f34ffc3b2f/coverage-7.13.3-cp313-cp313t-win32.whl", hash = "sha256:dfb428e41377e6b9ba1b0a32df6db5409cb089a0ed1d0a672dc4953ec110d84f", size = 222679, upload-time = "2026-02-03T14:01:28.553Z" }, + { url = "https://files.pythonhosted.org/packages/81/82/ef1747b88c87a5c7d7edc3704799ebd650189a9158e680a063308b6125ef/coverage-7.13.3-cp313-cp313t-win_amd64.whl", hash = "sha256:5badd7e596e6b0c89aa8ec6d37f4473e4357f982ce57f9a2942b0221cd9cf60c", size = 223740, upload-time = "2026-02-03T14:01:30.776Z" }, + { url = "https://files.pythonhosted.org/packages/1c/4c/a67c7bb5b560241c22736a9cb2f14c5034149ffae18630323fde787339e4/coverage-7.13.3-cp313-cp313t-win_arm64.whl", hash = "sha256:989aa158c0eb19d83c76c26f4ba00dbb272485c56e452010a3450bdbc9daafd9", size = 221996, upload-time = "2026-02-03T14:01:32.495Z" }, + { url = "https://files.pythonhosted.org/packages/5e/b3/677bb43427fed9298905106f39c6520ac75f746f81b8f01104526a8026e4/coverage-7.13.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:c6f6169bbdbdb85aab8ac0392d776948907267fcc91deeacf6f9d55f7a83ae3b", size = 219513, upload-time = "2026-02-03T14:01:34.29Z" }, + { url = "https://files.pythonhosted.org/packages/42/53/290046e3bbf8986cdb7366a42dab3440b9983711eaff044a51b11006c67b/coverage-7.13.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2f5e731627a3d5ef11a2a35aa0c6f7c435867c7ccbc391268eb4f2ca5dbdcc10", size = 219850, upload-time = "2026-02-03T14:01:35.984Z" }, + { url = "https://files.pythonhosted.org/packages/ea/2b/ab41f10345ba2e49d5e299be8663be2b7db33e77ac1b85cd0af985ea6406/coverage-7.13.3-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9db3a3285d91c0b70fab9f39f0a4aa37d375873677efe4e71e58d8321e8c5d39", size = 250886, upload-time = "2026-02-03T14:01:38.287Z" }, + { url = "https://files.pythonhosted.org/packages/72/2d/b3f6913ee5a1d5cdd04106f257e5fac5d048992ffc2d9995d07b0f17739f/coverage-7.13.3-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:06e49c5897cb12e3f7ecdc111d44e97c4f6d0557b81a7a0204ed70a8b038f86f", size = 253393, upload-time = "2026-02-03T14:01:40.118Z" }, + { url = "https://files.pythonhosted.org/packages/f0/f6/b1f48810ffc6accf49a35b9943636560768f0812330f7456aa87dc39aff5/coverage-7.13.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fb25061a66802df9fc13a9ba1967d25faa4dae0418db469264fd9860a921dde4", size = 254740, upload-time = "2026-02-03T14:01:42.413Z" }, + { url = "https://files.pythonhosted.org/packages/57/d0/e59c54f9be0b61808f6bc4c8c4346bd79f02dd6bbc3f476ef26124661f20/coverage-7.13.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:99fee45adbb1caeb914da16f70e557fb7ff6ddc9e4b14de665bd41af631367ef", size = 250905, upload-time = "2026-02-03T14:01:44.163Z" }, + { url = "https://files.pythonhosted.org/packages/d5/f7/5291bcdf498bafbee3796bb32ef6966e9915aebd4d0954123c8eae921c32/coverage-7.13.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:318002f1fd819bdc1651c619268aa5bc853c35fa5cc6d1e8c96bd9cd6c828b75", size = 252753, upload-time = "2026-02-03T14:01:45.974Z" }, + { url = "https://files.pythonhosted.org/packages/a0/a9/1dcafa918c281554dae6e10ece88c1add82db685be123e1b05c2056ff3fb/coverage-7.13.3-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:71295f2d1d170b9977dc386d46a7a1b7cbb30e5405492529b4c930113a33f895", size = 250716, upload-time = "2026-02-03T14:01:48.844Z" }, + { url = "https://files.pythonhosted.org/packages/44/bb/4ea4eabcce8c4f6235df6e059fbc5db49107b24c4bdffc44aee81aeca5a8/coverage-7.13.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:5b1ad2e0dc672625c44bc4fe34514602a9fd8b10d52ddc414dc585f74453516c", size = 250530, upload-time = "2026-02-03T14:01:50.793Z" }, + { url = "https://files.pythonhosted.org/packages/6d/31/4a6c9e6a71367e6f923b27b528448c37f4e959b7e4029330523014691007/coverage-7.13.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b2beb64c145593a50d90db5c7178f55daeae129123b0d265bdb3cbec83e5194a", size = 252186, upload-time = "2026-02-03T14:01:52.607Z" }, + { url = "https://files.pythonhosted.org/packages/27/92/e1451ef6390a4f655dc42da35d9971212f7abbbcad0bdb7af4407897eb76/coverage-7.13.3-cp314-cp314-win32.whl", hash = "sha256:3d1aed4f4e837a832df2f3b4f68a690eede0de4560a2dbc214ea0bc55aabcdb4", size = 222253, upload-time = "2026-02-03T14:01:55.071Z" }, + { url = "https://files.pythonhosted.org/packages/8a/98/78885a861a88de020c32a2693487c37d15a9873372953f0c3c159d575a43/coverage-7.13.3-cp314-cp314-win_amd64.whl", hash = "sha256:9f9efbbaf79f935d5fbe3ad814825cbce4f6cdb3054384cb49f0c0f496125fa0", size = 223069, upload-time = "2026-02-03T14:01:56.95Z" }, + { url = "https://files.pythonhosted.org/packages/eb/fb/3784753a48da58a5337972abf7ca58b1fb0f1bda21bc7b4fae992fd28e47/coverage-7.13.3-cp314-cp314-win_arm64.whl", hash = "sha256:31b6e889c53d4e6687ca63706148049494aace140cffece1c4dc6acadb70a7b3", size = 221633, upload-time = "2026-02-03T14:01:58.758Z" }, + { url = "https://files.pythonhosted.org/packages/40/f9/75b732d9674d32cdbffe801ed5f770786dd1c97eecedef2125b0d25102dc/coverage-7.13.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:c5e9787cec750793a19a28df7edd85ac4e49d3fb91721afcdc3b86f6c08d9aa8", size = 220243, upload-time = "2026-02-03T14:02:01.109Z" }, + { url = "https://files.pythonhosted.org/packages/cf/7e/2868ec95de5a65703e6f0c87407ea822d1feb3619600fbc3c1c4fa986090/coverage-7.13.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e5b86db331c682fd0e4be7098e6acee5e8a293f824d41487c667a93705d415ca", size = 220515, upload-time = "2026-02-03T14:02:02.862Z" }, + { url = "https://files.pythonhosted.org/packages/7d/eb/9f0d349652fced20bcaea0f67fc5777bd097c92369f267975732f3dc5f45/coverage-7.13.3-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:edc7754932682d52cf6e7a71806e529ecd5ce660e630e8bd1d37109a2e5f63ba", size = 261874, upload-time = "2026-02-03T14:02:04.727Z" }, + { url = "https://files.pythonhosted.org/packages/ee/a5/6619bc4a6c7b139b16818149a3e74ab2e21599ff9a7b6811b6afde99f8ec/coverage-7.13.3-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d3a16d6398666510a6886f67f43d9537bfd0e13aca299688a19daa84f543122f", size = 264004, upload-time = "2026-02-03T14:02:06.634Z" }, + { url = "https://files.pythonhosted.org/packages/29/b7/90aa3fc645a50c6f07881fca4fd0ba21e3bfb6ce3a7078424ea3a35c74c9/coverage-7.13.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:303d38b19626c1981e1bb067a9928236d88eb0e4479b18a74812f05a82071508", size = 266408, upload-time = "2026-02-03T14:02:09.037Z" }, + { url = "https://files.pythonhosted.org/packages/62/55/08bb2a1e4dcbae384e638f0effef486ba5987b06700e481691891427d879/coverage-7.13.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:284e06eadfe15ddfee2f4ee56631f164ef897a7d7d5a15bca5f0bb88889fc5ba", size = 260977, upload-time = "2026-02-03T14:02:11.755Z" }, + { url = "https://files.pythonhosted.org/packages/9b/76/8bd4ae055a42d8fb5dd2230e5cf36ff2e05f85f2427e91b11a27fea52ed7/coverage-7.13.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d401f0864a1d3198422816878e4e84ca89ec1c1bf166ecc0ae01380a39b888cd", size = 263868, upload-time = "2026-02-03T14:02:13.565Z" }, + { url = "https://files.pythonhosted.org/packages/e3/f9/ba000560f11e9e32ec03df5aa8477242c2d95b379c99ac9a7b2e7fbacb1a/coverage-7.13.3-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3f379b02c18a64de78c4ccdddf1c81c2c5ae1956c72dacb9133d7dd7809794ab", size = 261474, upload-time = "2026-02-03T14:02:16.069Z" }, + { url = "https://files.pythonhosted.org/packages/90/4b/4de4de8f9ca7af4733bfcf4baa440121b7dbb3856daf8428ce91481ff63b/coverage-7.13.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:7a482f2da9086971efb12daca1d6547007ede3674ea06e16d7663414445c683e", size = 260317, upload-time = "2026-02-03T14:02:17.996Z" }, + { url = "https://files.pythonhosted.org/packages/05/71/5cd8436e2c21410ff70be81f738c0dddea91bcc3189b1517d26e0102ccb3/coverage-7.13.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:562136b0d401992118d9b49fbee5454e16f95f85b120a4226a04d816e33fe024", size = 262635, upload-time = "2026-02-03T14:02:20.405Z" }, + { url = "https://files.pythonhosted.org/packages/e7/f8/2834bb45bdd70b55a33ec354b8b5f6062fc90e5bb787e14385903a979503/coverage-7.13.3-cp314-cp314t-win32.whl", hash = "sha256:ca46e5c3be3b195098dd88711890b8011a9fa4feca942292bb84714ce5eab5d3", size = 223035, upload-time = "2026-02-03T14:02:22.323Z" }, + { url = "https://files.pythonhosted.org/packages/26/75/f8290f0073c00d9ae14056d2b84ab92dff21d5370e464cb6cb06f52bf580/coverage-7.13.3-cp314-cp314t-win_amd64.whl", hash = "sha256:06d316dbb3d9fd44cca05b2dbcfbef22948493d63a1f28e828d43e6cc505fed8", size = 224142, upload-time = "2026-02-03T14:02:24.143Z" }, + { url = "https://files.pythonhosted.org/packages/03/01/43ac78dfea8946c4a9161bbc034b5549115cb2b56781a4b574927f0d141a/coverage-7.13.3-cp314-cp314t-win_arm64.whl", hash = "sha256:299d66e9218193f9dc6e4880629ed7c4cd23486005166247c283fb98531656c3", size = 222166, upload-time = "2026-02-03T14:02:26.005Z" }, + { url = "https://files.pythonhosted.org/packages/7d/fb/70af542d2d938c778c9373ce253aa4116dbe7c0a5672f78b2b2ae0e1b94b/coverage-7.13.3-py3-none-any.whl", hash = "sha256:90a8af9dba6429b2573199622d72e0ebf024d6276f16abce394ad4d181bb0910", size = 211237, upload-time = "2026-02-03T14:02:27.986Z" }, +] + +[[package]] +name = "docker" +version = "7.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pywin32", marker = "sys_platform == 'win32'" }, + { name = "requests" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/91/9b/4a2ea29aeba62471211598dac5d96825bb49348fa07e906ea930394a83ce/docker-7.1.0.tar.gz", hash = "sha256:ad8c70e6e3f8926cb8a92619b832b4ea5299e2831c14284663184e200546fa6c", size = 117834, upload-time = "2024-05-23T11:13:57.216Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e3/26/57c6fb270950d476074c087527a558ccb6f4436657314bfb6cdf484114c4/docker-7.1.0-py3-none-any.whl", hash = "sha256:c96b93b7f0a746f9e77d325bcfb87422a3d8bd4f03136ae8a85b37f1898d5fc0", size = 147774, upload-time = "2024-05-23T11:13:55.01Z" }, +] + +[[package]] +name = "idna" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "packaging" +version = "26.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" }, +] + +[[package]] +name = "pgmob" +version = "0.3.0" +source = { editable = "." } +dependencies = [ + { name = "packaging" }, +] + +[package.optional-dependencies] +dev = [ + { name = "docker" }, + { name = "pytest" }, + { name = "pytest-asyncio" }, + { name = "pytest-cov" }, + { name = "pytest-mock" }, + { name = "pyyaml" }, + { name = "ruff" }, + { name = "ty" }, +] +psycopg2 = [ + { name = "psycopg2" }, +] +psycopg2-binary = [ + { name = "psycopg2-binary" }, +] + +[package.metadata] +requires-dist = [ + { name = "docker", marker = "extra == 'dev'", specifier = ">=7.1.0" }, + { name = "packaging", specifier = ">=26.0" }, + { name = "psycopg2", marker = "extra == 'psycopg2'", specifier = ">=2.9.11,<3" }, + { name = "psycopg2-binary", marker = "extra == 'psycopg2-binary'", specifier = ">=2.9.11,<3" }, + { name = "pytest", marker = "extra == 'dev'", specifier = ">=9.0.0" }, + { name = "pytest-asyncio", marker = "extra == 'dev'", specifier = ">=1.3.0" }, + { name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=7.0.0" }, + { name = "pytest-mock", marker = "extra == 'dev'", specifier = ">=3.15.0" }, + { name = "pyyaml", marker = "extra == 'dev'", specifier = ">=6.0.3" }, + { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.15.0" }, + { name = "ty", marker = "extra == 'dev'", specifier = ">=0.0.15" }, +] +provides-extras = ["psycopg2", "psycopg2-binary", "dev"] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "psycopg2" +version = "2.9.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/89/8d/9d12bc8677c24dad342ec777529bce705b3e785fa05d85122b5502b9ab55/psycopg2-2.9.11.tar.gz", hash = "sha256:964d31caf728e217c697ff77ea69c2ba0865fa41ec20bb00f0977e62fdcc52e3", size = 379598, upload-time = "2025-10-10T11:14:46.075Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/5a/18c8cb13fc6908dc41a483d2c14d927a7a3f29883748747e8cb625da6587/psycopg2-2.9.11-cp313-cp313-win_amd64.whl", hash = "sha256:8dc379166b5b7d5ea66dcebf433011dfc51a7bb8a5fc12367fa05668e5fc53c8", size = 2714048, upload-time = "2025-10-10T11:10:19.816Z" }, + { url = "https://files.pythonhosted.org/packages/47/08/737aa39c78d705a7ce58248d00eeba0e9fc36be488f9b672b88736fbb1f7/psycopg2-2.9.11-cp314-cp314-win_amd64.whl", hash = "sha256:f10a48acba5fe6e312b891f290b4d2ca595fc9a06850fe53320beac353575578", size = 2803738, upload-time = "2025-10-10T11:10:23.196Z" }, +] + +[[package]] +name = "psycopg2-binary" +version = "2.9.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ac/6c/8767aaa597ba424643dc87348c6f1754dd9f48e80fdc1b9f7ca5c3a7c213/psycopg2-binary-2.9.11.tar.gz", hash = "sha256:b6aed9e096bf63f9e75edf2581aa9a7e7186d97ab5c177aa6c87797cd591236c", size = 379620, upload-time = "2025-10-10T11:14:48.041Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ff/a8/a2709681b3ac11b0b1786def10006b8995125ba268c9a54bea6f5ae8bd3e/psycopg2_binary-2.9.11-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b8fb3db325435d34235b044b199e56cdf9ff41223a4b9752e8576465170bb38c", size = 3756572, upload-time = "2025-10-10T11:12:32.873Z" }, + { url = "https://files.pythonhosted.org/packages/62/e1/c2b38d256d0dafd32713e9f31982a5b028f4a3651f446be70785f484f472/psycopg2_binary-2.9.11-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:366df99e710a2acd90efed3764bb1e28df6c675d33a7fb40df9b7281694432ee", size = 3864529, upload-time = "2025-10-10T11:12:36.791Z" }, + { url = "https://files.pythonhosted.org/packages/11/32/b2ffe8f3853c181e88f0a157c5fb4e383102238d73c52ac6d93a5c8bffe6/psycopg2_binary-2.9.11-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8c55b385daa2f92cb64b12ec4536c66954ac53654c7f15a203578da4e78105c0", size = 4411242, upload-time = "2025-10-10T11:12:42.388Z" }, + { url = "https://files.pythonhosted.org/packages/10/04/6ca7477e6160ae258dc96f67c371157776564679aefd247b66f4661501a2/psycopg2_binary-2.9.11-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:c0377174bf1dd416993d16edc15357f6eb17ac998244cca19bc67cdc0e2e5766", size = 4468258, upload-time = "2025-10-10T11:12:48.654Z" }, + { url = "https://files.pythonhosted.org/packages/3c/7e/6a1a38f86412df101435809f225d57c1a021307dd0689f7a5e7fe83588b1/psycopg2_binary-2.9.11-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5c6ff3335ce08c75afaed19e08699e8aacf95d4a260b495a4a8545244fe2ceb3", size = 4166295, upload-time = "2025-10-10T11:12:52.525Z" }, + { url = "https://files.pythonhosted.org/packages/f2/7d/c07374c501b45f3579a9eb761cbf2604ddef3d96ad48679112c2c5aa9c25/psycopg2_binary-2.9.11-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:84011ba3109e06ac412f95399b704d3d6950e386b7994475b231cf61eec2fc1f", size = 3983133, upload-time = "2025-10-30T02:55:24.329Z" }, + { url = "https://files.pythonhosted.org/packages/82/56/993b7104cb8345ad7d4516538ccf8f0d0ac640b1ebd8c754a7b024e76878/psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ba34475ceb08cccbdd98f6b46916917ae6eeb92b5ae111df10b544c3a4621dc4", size = 3652383, upload-time = "2025-10-10T11:12:56.387Z" }, + { url = "https://files.pythonhosted.org/packages/2d/ac/eaeb6029362fd8d454a27374d84c6866c82c33bfc24587b4face5a8e43ef/psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b31e90fdd0f968c2de3b26ab014314fe814225b6c324f770952f7d38abf17e3c", size = 3298168, upload-time = "2025-10-10T11:13:00.403Z" }, + { url = "https://files.pythonhosted.org/packages/2b/39/50c3facc66bded9ada5cbc0de867499a703dc6bca6be03070b4e3b65da6c/psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:d526864e0f67f74937a8fce859bd56c979f5e2ec57ca7c627f5f1071ef7fee60", size = 3044712, upload-time = "2025-10-30T02:55:27.975Z" }, + { url = "https://files.pythonhosted.org/packages/9c/8e/b7de019a1f562f72ada81081a12823d3c1590bedc48d7d2559410a2763fe/psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:04195548662fa544626c8ea0f06561eb6203f1984ba5b4562764fbeb4c3d14b1", size = 3347549, upload-time = "2025-10-10T11:13:03.971Z" }, + { url = "https://files.pythonhosted.org/packages/80/2d/1bb683f64737bbb1f86c82b7359db1eb2be4e2c0c13b947f80efefa7d3e5/psycopg2_binary-2.9.11-cp313-cp313-win_amd64.whl", hash = "sha256:efff12b432179443f54e230fdf60de1f6cc726b6c832db8701227d089310e8aa", size = 2714215, upload-time = "2025-10-10T11:13:07.14Z" }, + { url = "https://files.pythonhosted.org/packages/64/12/93ef0098590cf51d9732b4f139533732565704f45bdc1ffa741b7c95fb54/psycopg2_binary-2.9.11-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:92e3b669236327083a2e33ccfa0d320dd01b9803b3e14dd986a4fc54aa00f4e1", size = 3756567, upload-time = "2025-10-10T11:13:11.885Z" }, + { url = "https://files.pythonhosted.org/packages/7c/a9/9d55c614a891288f15ca4b5209b09f0f01e3124056924e17b81b9fa054cc/psycopg2_binary-2.9.11-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e0deeb03da539fa3577fcb0b3f2554a97f7e5477c246098dbb18091a4a01c16f", size = 3864755, upload-time = "2025-10-10T11:13:17.727Z" }, + { url = "https://files.pythonhosted.org/packages/13/1e/98874ce72fd29cbde93209977b196a2edae03f8490d1bd8158e7f1daf3a0/psycopg2_binary-2.9.11-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:9b52a3f9bb540a3e4ec0f6ba6d31339727b2950c9772850d6545b7eae0b9d7c5", size = 4411646, upload-time = "2025-10-10T11:13:24.432Z" }, + { url = "https://files.pythonhosted.org/packages/5a/bd/a335ce6645334fb8d758cc358810defca14a1d19ffbc8a10bd38a2328565/psycopg2_binary-2.9.11-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:db4fd476874ccfdbb630a54426964959e58da4c61c9feba73e6094d51303d7d8", size = 4468701, upload-time = "2025-10-10T11:13:29.266Z" }, + { url = "https://files.pythonhosted.org/packages/44/d6/c8b4f53f34e295e45709b7568bf9b9407a612ea30387d35eb9fa84f269b4/psycopg2_binary-2.9.11-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:47f212c1d3be608a12937cc131bd85502954398aaa1320cb4c14421a0ffccf4c", size = 4166293, upload-time = "2025-10-10T11:13:33.336Z" }, + { url = "https://files.pythonhosted.org/packages/4b/e0/f8cc36eadd1b716ab36bb290618a3292e009867e5c97ce4aba908cb99644/psycopg2_binary-2.9.11-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e35b7abae2b0adab776add56111df1735ccc71406e56203515e228a8dc07089f", size = 3983184, upload-time = "2025-10-30T02:55:32.483Z" }, + { url = "https://files.pythonhosted.org/packages/53/3e/2a8fe18a4e61cfb3417da67b6318e12691772c0696d79434184a511906dc/psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fcf21be3ce5f5659daefd2b3b3b6e4727b028221ddc94e6c1523425579664747", size = 3652650, upload-time = "2025-10-10T11:13:38.181Z" }, + { url = "https://files.pythonhosted.org/packages/76/36/03801461b31b29fe58d228c24388f999fe814dfc302856e0d17f97d7c54d/psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:9bd81e64e8de111237737b29d68039b9c813bdf520156af36d26819c9a979e5f", size = 3298663, upload-time = "2025-10-10T11:13:44.878Z" }, + { url = "https://files.pythonhosted.org/packages/97/77/21b0ea2e1a73aa5fa9222b2a6b8ba325c43c3a8d54272839c991f2345656/psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:32770a4d666fbdafab017086655bcddab791d7cb260a16679cc5a7338b64343b", size = 3044737, upload-time = "2025-10-30T02:55:35.69Z" }, + { url = "https://files.pythonhosted.org/packages/67/69/f36abe5f118c1dca6d3726ceae164b9356985805480731ac6712a63f24f0/psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c3cb3a676873d7506825221045bd70e0427c905b9c8ee8d6acd70cfcbd6e576d", size = 3347643, upload-time = "2025-10-10T11:13:53.499Z" }, + { url = "https://files.pythonhosted.org/packages/e1/36/9c0c326fe3a4227953dfb29f5d0c8ae3b8eb8c1cd2967aa569f50cb3c61f/psycopg2_binary-2.9.11-cp314-cp314-win_amd64.whl", hash = "sha256:4012c9c954dfaccd28f94e84ab9f94e12df76b4afb22331b1f0d3154893a6316", size = 2803913, upload-time = "2025-10-10T11:13:57.058Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "pytest" +version = "9.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" }, +] + +[[package]] +name = "pytest-asyncio" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/90/2c/8af215c0f776415f3590cac4f9086ccefd6fd463befeae41cd4d3f193e5a/pytest_asyncio-1.3.0.tar.gz", hash = "sha256:d7f52f36d231b80ee124cd216ffb19369aa168fc10095013c6b014a34d3ee9e5", size = 50087, upload-time = "2025-11-10T16:07:47.256Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/35/f8b19922b6a25bc0880171a2f1a003eaeb93657475193ab516fd87cac9da/pytest_asyncio-1.3.0-py3-none-any.whl", hash = "sha256:611e26147c7f77640e6d0a92a38ed17c3e9848063698d5c93d5aa7aa11cebff5", size = 15075, upload-time = "2025-11-10T16:07:45.537Z" }, +] + +[[package]] +name = "pytest-cov" +version = "7.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage" }, + { name = "pluggy" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5e/f7/c933acc76f5208b3b00089573cf6a2bc26dc80a8aece8f52bb7d6b1855ca/pytest_cov-7.0.0.tar.gz", hash = "sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1", size = 54328, upload-time = "2025-09-09T10:57:02.113Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861", size = 22424, upload-time = "2025-09-09T10:57:00.695Z" }, +] + +[[package]] +name = "pytest-mock" +version = "3.15.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/68/14/eb014d26be205d38ad5ad20d9a80f7d201472e08167f0bb4361e251084a9/pytest_mock-3.15.1.tar.gz", hash = "sha256:1849a238f6f396da19762269de72cb1814ab44416fa73a8686deac10b0d87a0f", size = 34036, upload-time = "2025-09-16T16:37:27.081Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/cc/06253936f4a7fa2e0f48dfe6d851d9c56df896a9ab09ac019d70b760619c/pytest_mock-3.15.1-py3-none-any.whl", hash = "sha256:0a25e2eb88fe5168d535041d09a4529a188176ae608a6d249ee65abc0949630d", size = 10095, upload-time = "2025-09-16T16:37:25.734Z" }, +] + +[[package]] +name = "pywin32" +version = "311" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a5/be/3fd5de0979fcb3994bfee0d65ed8ca9506a8a1260651b86174f6a86f52b3/pywin32-311-cp313-cp313-win32.whl", hash = "sha256:f95ba5a847cba10dd8c4d8fefa9f2a6cf283b8b88ed6178fa8a6c1ab16054d0d", size = 8705700, upload-time = "2025-07-14T20:13:26.471Z" }, + { url = "https://files.pythonhosted.org/packages/e3/28/e0a1909523c6890208295a29e05c2adb2126364e289826c0a8bc7297bd5c/pywin32-311-cp313-cp313-win_amd64.whl", hash = "sha256:718a38f7e5b058e76aee1c56ddd06908116d35147e133427e59a3983f703a20d", size = 9494700, upload-time = "2025-07-14T20:13:28.243Z" }, + { url = "https://files.pythonhosted.org/packages/04/bf/90339ac0f55726dce7d794e6d79a18a91265bdf3aa70b6b9ca52f35e022a/pywin32-311-cp313-cp313-win_arm64.whl", hash = "sha256:7b4075d959648406202d92a2310cb990fea19b535c7f4a78d3f5e10b926eeb8a", size = 8709318, upload-time = "2025-07-14T20:13:30.348Z" }, + { url = "https://files.pythonhosted.org/packages/c9/31/097f2e132c4f16d99a22bfb777e0fd88bd8e1c634304e102f313af69ace5/pywin32-311-cp314-cp314-win32.whl", hash = "sha256:b7a2c10b93f8986666d0c803ee19b5990885872a7de910fc460f9b0c2fbf92ee", size = 8840714, upload-time = "2025-07-14T20:13:32.449Z" }, + { url = "https://files.pythonhosted.org/packages/90/4b/07c77d8ba0e01349358082713400435347df8426208171ce297da32c313d/pywin32-311-cp314-cp314-win_amd64.whl", hash = "sha256:3aca44c046bd2ed8c90de9cb8427f581c479e594e99b5c0bb19b29c10fd6cb87", size = 9656800, upload-time = "2025-07-14T20:13:34.312Z" }, + { url = "https://files.pythonhosted.org/packages/c0/d2/21af5c535501a7233e734b8af901574572da66fcc254cb35d0609c9080dd/pywin32-311-cp314-cp314-win_arm64.whl", hash = "sha256:a508e2d9025764a8270f93111a970e1d0fbfc33f4153b388bb649b7eec4f9b42", size = 8932540, upload-time = "2025-07-14T20:13:36.379Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, +] + +[[package]] +name = "requests" +version = "2.32.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, +] + +[[package]] +name = "ruff" +version = "0.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c8/39/5cee96809fbca590abea6b46c6d1c586b49663d1d2830a751cc8fc42c666/ruff-0.15.0.tar.gz", hash = "sha256:6bdea47cdbea30d40f8f8d7d69c0854ba7c15420ec75a26f463290949d7f7e9a", size = 4524893, upload-time = "2026-02-03T17:53:35.357Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bc/88/3fd1b0aa4b6330d6aaa63a285bc96c9f71970351579152d231ed90914586/ruff-0.15.0-py3-none-linux_armv6l.whl", hash = "sha256:aac4ebaa612a82b23d45964586f24ae9bc23ca101919f5590bdb368d74ad5455", size = 10354332, upload-time = "2026-02-03T17:52:54.892Z" }, + { url = "https://files.pythonhosted.org/packages/72/f6/62e173fbb7eb75cc29fe2576a1e20f0a46f671a2587b5f604bfb0eaf5f6f/ruff-0.15.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:dcd4be7cc75cfbbca24a98d04d0b9b36a270d0833241f776b788d59f4142b14d", size = 10767189, upload-time = "2026-02-03T17:53:19.778Z" }, + { url = "https://files.pythonhosted.org/packages/99/e4/968ae17b676d1d2ff101d56dc69cf333e3a4c985e1ec23803df84fc7bf9e/ruff-0.15.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d747e3319b2bce179c7c1eaad3d884dc0a199b5f4d5187620530adf9105268ce", size = 10075384, upload-time = "2026-02-03T17:53:29.241Z" }, + { url = "https://files.pythonhosted.org/packages/a2/bf/9843c6044ab9e20af879c751487e61333ca79a2c8c3058b15722386b8cae/ruff-0.15.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:650bd9c56ae03102c51a5e4b554d74d825ff3abe4db22b90fd32d816c2e90621", size = 10481363, upload-time = "2026-02-03T17:52:43.332Z" }, + { url = "https://files.pythonhosted.org/packages/55/d9/4ada5ccf4cd1f532db1c8d44b6f664f2208d3d93acbeec18f82315e15193/ruff-0.15.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a6664b7eac559e3048223a2da77769c2f92b43a6dfd4720cef42654299a599c9", size = 10187736, upload-time = "2026-02-03T17:53:00.522Z" }, + { url = "https://files.pythonhosted.org/packages/86/e2/f25eaecd446af7bb132af0a1d5b135a62971a41f5366ff41d06d25e77a91/ruff-0.15.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6f811f97b0f092b35320d1556f3353bf238763420ade5d9e62ebd2b73f2ff179", size = 10968415, upload-time = "2026-02-03T17:53:15.705Z" }, + { url = "https://files.pythonhosted.org/packages/e7/dc/f06a8558d06333bf79b497d29a50c3a673d9251214e0d7ec78f90b30aa79/ruff-0.15.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:761ec0a66680fab6454236635a39abaf14198818c8cdf691e036f4bc0f406b2d", size = 11809643, upload-time = "2026-02-03T17:53:23.031Z" }, + { url = "https://files.pythonhosted.org/packages/dd/45/0ece8db2c474ad7df13af3a6d50f76e22a09d078af63078f005057ca59eb/ruff-0.15.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:940f11c2604d317e797b289f4f9f3fa5555ffe4fb574b55ed006c3d9b6f0eb78", size = 11234787, upload-time = "2026-02-03T17:52:46.432Z" }, + { url = "https://files.pythonhosted.org/packages/8a/d9/0e3a81467a120fd265658d127db648e4d3acfe3e4f6f5d4ea79fac47e587/ruff-0.15.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bcbca3d40558789126da91d7ef9a7c87772ee107033db7191edefa34e2c7f1b4", size = 11112797, upload-time = "2026-02-03T17:52:49.274Z" }, + { url = "https://files.pythonhosted.org/packages/b2/cb/8c0b3b0c692683f8ff31351dfb6241047fa873a4481a76df4335a8bff716/ruff-0.15.0-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:9a121a96db1d75fa3eb39c4539e607f628920dd72ff1f7c5ee4f1b768ac62d6e", size = 11033133, upload-time = "2026-02-03T17:53:33.105Z" }, + { url = "https://files.pythonhosted.org/packages/f8/5e/23b87370cf0f9081a8c89a753e69a4e8778805b8802ccfe175cc410e50b9/ruff-0.15.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:5298d518e493061f2eabd4abd067c7e4fb89e2f63291c94332e35631c07c3662", size = 10442646, upload-time = "2026-02-03T17:53:06.278Z" }, + { url = "https://files.pythonhosted.org/packages/e1/9a/3c94de5ce642830167e6d00b5c75aacd73e6347b4c7fc6828699b150a5ee/ruff-0.15.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:afb6e603d6375ff0d6b0cee563fa21ab570fd15e65c852cb24922cef25050cf1", size = 10195750, upload-time = "2026-02-03T17:53:26.084Z" }, + { url = "https://files.pythonhosted.org/packages/30/15/e396325080d600b436acc970848d69df9c13977942fb62bb8722d729bee8/ruff-0.15.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:77e515f6b15f828b94dc17d2b4ace334c9ddb7d9468c54b2f9ed2b9c1593ef16", size = 10676120, upload-time = "2026-02-03T17:53:09.363Z" }, + { url = "https://files.pythonhosted.org/packages/8d/c9/229a23d52a2983de1ad0fb0ee37d36e0257e6f28bfd6b498ee2c76361874/ruff-0.15.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:6f6e80850a01eb13b3e42ee0ebdf6e4497151b48c35051aab51c101266d187a3", size = 11201636, upload-time = "2026-02-03T17:52:57.281Z" }, + { url = "https://files.pythonhosted.org/packages/6f/b0/69adf22f4e24f3677208adb715c578266842e6e6a3cc77483f48dd999ede/ruff-0.15.0-py3-none-win32.whl", hash = "sha256:238a717ef803e501b6d51e0bdd0d2c6e8513fe9eec14002445134d3907cd46c3", size = 10465945, upload-time = "2026-02-03T17:53:12.591Z" }, + { url = "https://files.pythonhosted.org/packages/51/ad/f813b6e2c97e9b4598be25e94a9147b9af7e60523b0cb5d94d307c15229d/ruff-0.15.0-py3-none-win_amd64.whl", hash = "sha256:dd5e4d3301dc01de614da3cdffc33d4b1b96fb89e45721f1598e5532ccf78b18", size = 11564657, upload-time = "2026-02-03T17:52:51.893Z" }, + { url = "https://files.pythonhosted.org/packages/f6/b0/2d823f6e77ebe560f4e397d078487e8d52c1516b331e3521bc75db4272ca/ruff-0.15.0-py3-none-win_arm64.whl", hash = "sha256:c480d632cc0ca3f0727acac8b7d053542d9e114a462a145d0b00e7cd658c515a", size = 10865753, upload-time = "2026-02-03T17:53:03.014Z" }, +] + +[[package]] +name = "ty" +version = "0.0.15" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4e/25/257602d316b9333089b688a7a11b33ebc660b74e8dacf400dc3dfdea1594/ty-0.0.15.tar.gz", hash = "sha256:4f9a5b8df208c62dba56e91b93bed8b5bb714839691b8cff16d12c983bfa1174", size = 5101936, upload-time = "2026-02-05T01:06:34.922Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/c5/35626e732b79bf0e6213de9f79aff59b5f247c0a1e3ce0d93e675ab9b728/ty-0.0.15-py3-none-linux_armv6l.whl", hash = "sha256:68e092458516c61512dac541cde0a5e4e5842df00b4e81881ead8f745ddec794", size = 10138374, upload-time = "2026-02-05T01:07:03.804Z" }, + { url = "https://files.pythonhosted.org/packages/d5/8a/48fd81664604848f79d03879b3ca3633762d457a069b07e09fb1b87edd6e/ty-0.0.15-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:79f2e75289eae3cece94c51118b730211af4ba5762906f52a878041b67e54959", size = 9947858, upload-time = "2026-02-05T01:06:47.453Z" }, + { url = "https://files.pythonhosted.org/packages/b6/85/c1ac8e97bcd930946f4c94db85b675561d590b4e72703bf3733419fc3973/ty-0.0.15-py3-none-macosx_11_0_arm64.whl", hash = "sha256:112a7b26e63e48cc72c8c5b03227d1db280cfa57a45f2df0e264c3a016aa8c3c", size = 9443220, upload-time = "2026-02-05T01:06:44.98Z" }, + { url = "https://files.pythonhosted.org/packages/3c/d9/244bc02599d950f7a4298fbc0c1b25cc808646b9577bdf7a83470b2d1cec/ty-0.0.15-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71f62a2644972975a657d9dc867bf901235cde51e8d24c20311067e7afd44a56", size = 9949976, upload-time = "2026-02-05T01:07:01.515Z" }, + { url = "https://files.pythonhosted.org/packages/7e/ab/3a0daad66798c91a33867a3ececf17d314ac65d4ae2bbbd28cbfde94da63/ty-0.0.15-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9e48b42be2d257317c85b78559233273b655dd636fc61e7e1d69abd90fd3cba4", size = 9965918, upload-time = "2026-02-05T01:06:54.283Z" }, + { url = "https://files.pythonhosted.org/packages/39/4e/e62b01338f653059a7c0cd09d1a326e9a9eedc351a0f0de9db0601658c3d/ty-0.0.15-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:27dd5b52a421e6871c5bfe9841160331b60866ed2040250cb161886478ab3e4f", size = 10424943, upload-time = "2026-02-05T01:07:08.777Z" }, + { url = "https://files.pythonhosted.org/packages/65/b5/7aa06655ce69c0d4f3e845d2d85e79c12994b6d84c71699cfb437e0bc8cf/ty-0.0.15-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:76b85c9ec2219e11c358a7db8e21b7e5c6674a1fb9b6f633836949de98d12286", size = 10964692, upload-time = "2026-02-05T01:06:37.103Z" }, + { url = "https://files.pythonhosted.org/packages/13/04/36fdfe1f3c908b471e246e37ce3d011175584c26d3853e6c5d9a0364564c/ty-0.0.15-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a9e8204c61d8ede4f21f2975dce74efdb80fafb2fae1915c666cceb33ea3c90b", size = 10692225, upload-time = "2026-02-05T01:06:49.714Z" }, + { url = "https://files.pythonhosted.org/packages/13/41/5bf882649bd8b64ded5fbce7fb8d77fb3b868de1a3b1a6c4796402b47308/ty-0.0.15-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:af87c3be7c944bb4d6609d6c63e4594944b0028c7bd490a525a82b88fe010d6d", size = 10516776, upload-time = "2026-02-05T01:06:52.047Z" }, + { url = "https://files.pythonhosted.org/packages/56/75/66852d7e004f859839c17ffe1d16513c1e7cc04bcc810edb80ca022a9124/ty-0.0.15-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:50dccf7398505e5966847d366c9e4c650b8c225411c2a68c32040a63b9521eea", size = 9928828, upload-time = "2026-02-05T01:06:56.647Z" }, + { url = "https://files.pythonhosted.org/packages/65/72/96bc16c7b337a3ef358fd227b3c8ef0c77405f3bfbbfb59ee5915f0d9d71/ty-0.0.15-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:bd797b8f231a4f4715110259ad1ad5340a87b802307f3e06d92bfb37b858a8f3", size = 9978960, upload-time = "2026-02-05T01:06:29.567Z" }, + { url = "https://files.pythonhosted.org/packages/a0/18/d2e316a35b626de2227f832cd36d21205e4f5d96fd036a8af84c72ecec1b/ty-0.0.15-py3-none-musllinux_1_2_i686.whl", hash = "sha256:9deb7f20e18b25440a9aa4884f934ba5628ef456dbde91819d5af1a73da48af3", size = 10135903, upload-time = "2026-02-05T01:06:59.256Z" }, + { url = "https://files.pythonhosted.org/packages/02/d3/b617a79c9dad10c888d7c15cd78859e0160b8772273637b9c4241a049491/ty-0.0.15-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:7b31b3de031255b90a5f4d9cb3d050feae246067c87130e5a6861a8061c71754", size = 10615879, upload-time = "2026-02-05T01:07:06.661Z" }, + { url = "https://files.pythonhosted.org/packages/fb/b0/2652a73c71c77296a6343217063f05745da60c67b7e8a8e25f2064167fce/ty-0.0.15-py3-none-win32.whl", hash = "sha256:9362c528ceb62c89d65c216336d28d500bc9f4c10418413f63ebc16886e16cc1", size = 9578058, upload-time = "2026-02-05T01:06:42.928Z" }, + { url = "https://files.pythonhosted.org/packages/84/6e/08a4aedebd2a6ce2784b5bc3760e43d1861f1a184734a78215c2d397c1df/ty-0.0.15-py3-none-win_amd64.whl", hash = "sha256:4db040695ae67c5524f59cb8179a8fa277112e69042d7dfdac862caa7e3b0d9c", size = 10457112, upload-time = "2026-02-05T01:06:39.885Z" }, + { url = "https://files.pythonhosted.org/packages/b3/be/1991f2bc12847ae2d4f1e3ac5dcff8bb7bc1261390645c0755bb55616355/ty-0.0.15-py3-none-win_arm64.whl", hash = "sha256:e5a98d4119e77d6136461e16ae505f8f8069002874ab073de03fbcb1a5e8bf25", size = 9937490, upload-time = "2026-02-05T01:06:32.388Z" }, +] + +[[package]] +name = "urllib3" +version = "2.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, +]