diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..aa605c8 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,96 @@ +## Ticket + +- Ticket ID: +- [ ] User Story / Bug Report / Chore Description exists and is complete +- [ ] Technical Document exists and is complete + +--- + +## Description + + + +## Type of Change + +- [ ] Feature (new functionality) +- [ ] Bug fix (fixes an issue) +- [ ] Documentation (docs only) +- [ ] Refactor (code improvement, no behavior change) +- [ ] Performance improvement +- [ ] Hotfix (critical production issue) +- [ ] Infrastructure/tooling + +## Related Issues + +Closes # +Related to # + +--- + +## Design Compliance Checklist + +### Testing + +- [ ] Coverage ≥ 95% (verify: `make test-all`) +- [ ] Unit tests for new/modified code +- [ ] Integration tests for end-to-end flows (if applicable) +- [ ] All tests pass locally (`make test-all`) +- [ ] No flaky tests introduced + +### Documentation + +- [ ] CHANGELOG.md updated with user-facing changes +- [ ] Public APIs have docstrings +- [ ] User documentation updated (if user-facing change) +- [ ] Development docs updated (if process changed) + +### Git Workflow + +- [ ] Branch name follows convention (feature/*, bugfix/*, docs/*, etc.) +- [ ] Commits follow Conventional Commits format +- [ ] No merge commits (rebased on target branch) + +### Code Quality + +- [ ] Code follows existing patterns and conventions +- [ ] No commented-out code or debug statements +- [ ] No TODO comments (convert to issues instead) +- [ ] Error messages are clear and actionable + +### CI/CD + +- [ ] All CI checks passing +- [ ] Linting passes: `make lint` +- [ ] Security scan clean: `make security` +- [ ] Tests pass with ≥95% coverage: `make test-all` + +### Security + +- [ ] No hardcoded secrets or credentials +- [ ] Input validation for user-provided data +- [ ] No command injection vulnerabilities +- [ ] External dependencies reviewed + +--- + +## Verification Commands + +```bash +make lint +make test-all +make pipeline +``` + +## Breaking Changes + +- [ ] No breaking changes +- [ ] Breaking changes documented with migration guide + +--- + +## Self-Review Checklist + +- [ ] I have reviewed my own code +- [ ] I have tested this thoroughly +- [ ] I have updated CHANGELOG.md +- [ ] This PR is ready for review diff --git a/.github/workflows/ci-cache.yml b/.github/workflows/ci-cache.yml new file mode 100644 index 0000000..f406fff --- /dev/null +++ b/.github/workflows/ci-cache.yml @@ -0,0 +1,122 @@ +name: cache Package CI + +on: + push: + branches: ["**"] + paths: + - "packages/cache/**" + - ".github/workflows/ci-cache.yml" + pull_request: + branches: [main, master, develop] + paths: + - "packages/cache/**" + +jobs: + lint: + name: Lint (cache) + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Install uv + run: pip install uv + + - name: Install dependencies + run: uv sync --dev --all-packages + + - name: Ruff check + run: | + cd packages/cache + uv run ruff check --line-length 88 . + + - name: Black check + run: | + cd packages/cache + uv run black --check --line-length=88 . + + - name: isort check + run: | + cd packages/cache + uv run isort --check --line-length=88 . + + # Add mypy step for packages with complex logic (core, orchestrator): + # - name: mypy type check + # run: | + # cd packages/cache + # uv run mypy src/ + + security: + name: Security Scan (cache) + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Install uv + run: pip install uv + + - name: Install dependencies + run: uv sync --dev --all-packages + + - name: Bandit security scan + run: | + cd packages/cache + uv run bandit -r src/ -ll -f json -o bandit-report.json + + - name: Upload security report + if: always() + uses: actions/upload-artifact@v4 + with: + name: bandit-report-cache + path: packages/cache/bandit-report.json + + test: + name: Test (cache) + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, macos-latest] + python-version: ["3.12", "3.13"] + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Install uv + run: pip install uv + + - name: Install dependencies + run: uv sync --dev --all-packages + + - name: Run tests with coverage + run: | + cd packages/cache + uv run pytest -n auto --cov=src --cov-report=xml --cov-report=term + + - name: Check coverage threshold + run: | + cd packages/cache + uv run coverage report --fail-under=95 + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v4 + with: + file: packages/cache/coverage.xml + flags: cache + token: ${{ secrets.CODECOV_TOKEN }} + fail_ci_if_error: false diff --git a/.github/workflows/ci-container-manager.yml b/.github/workflows/ci-container-manager.yml new file mode 100644 index 0000000..cb9be64 --- /dev/null +++ b/.github/workflows/ci-container-manager.yml @@ -0,0 +1,122 @@ +name: container-manager Package CI + +on: + push: + branches: ["**"] + paths: + - "packages/container-manager/**" + - ".github/workflows/ci-container-manager.yml" + pull_request: + branches: [main, master, develop] + paths: + - "packages/container-manager/**" + +jobs: + lint: + name: Lint (container-manager) + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Install uv + run: pip install uv + + - name: Install dependencies + run: uv sync --dev --all-packages + + - name: Ruff check + run: | + cd packages/container-manager + uv run ruff check --line-length 88 . + + - name: Black check + run: | + cd packages/container-manager + uv run black --check --line-length=88 . + + - name: isort check + run: | + cd packages/container-manager + uv run isort --check --line-length=88 . + + # Add mypy step for packages with complex logic (core, orchestrator): + # - name: mypy type check + # run: | + # cd packages/container-manager + # uv run mypy src/ + + security: + name: Security Scan (container-manager) + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Install uv + run: pip install uv + + - name: Install dependencies + run: uv sync --dev --all-packages + + - name: Bandit security scan + run: | + cd packages/container-manager + uv run bandit -r src/ -ll -f json -o bandit-report.json + + - name: Upload security report + if: always() + uses: actions/upload-artifact@v4 + with: + name: bandit-report-container-manager + path: packages/container-manager/bandit-report.json + + test: + name: Test (container-manager) + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, macos-latest] + python-version: ["3.12", "3.13"] + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Install uv + run: pip install uv + + - name: Install dependencies + run: uv sync --dev --all-packages + + - name: Run tests with coverage + run: | + cd packages/container-manager + uv run pytest -n auto --cov=src --cov-report=xml --cov-report=term + + - name: Check coverage threshold + run: | + cd packages/container-manager + uv run coverage report --fail-under=95 + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v4 + with: + file: packages/container-manager/coverage.xml + flags: container-manager + token: ${{ secrets.CODECOV_TOKEN }} + fail_ci_if_error: false diff --git a/.github/workflows/ci-daemon.yml b/.github/workflows/ci-daemon.yml new file mode 100644 index 0000000..3b76a5a --- /dev/null +++ b/.github/workflows/ci-daemon.yml @@ -0,0 +1,122 @@ +name: daemon Package CI + +on: + push: + branches: ["**"] + paths: + - "packages/daemon/**" + - ".github/workflows/ci-daemon.yml" + pull_request: + branches: [main, master, develop] + paths: + - "packages/daemon/**" + +jobs: + lint: + name: Lint (daemon) + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Install uv + run: pip install uv + + - name: Install dependencies + run: uv sync --dev --all-packages + + - name: Ruff check + run: | + cd packages/daemon + uv run ruff check --line-length 88 . + + - name: Black check + run: | + cd packages/daemon + uv run black --check --line-length=88 . + + - name: isort check + run: | + cd packages/daemon + uv run isort --check --line-length=88 . + + # Add mypy step for packages with complex logic (core, orchestrator): + # - name: mypy type check + # run: | + # cd packages/daemon + # uv run mypy src/ + + security: + name: Security Scan (daemon) + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Install uv + run: pip install uv + + - name: Install dependencies + run: uv sync --dev --all-packages + + - name: Bandit security scan + run: | + cd packages/daemon + uv run bandit -r src/ -ll -f json -o bandit-report.json + + - name: Upload security report + if: always() + uses: actions/upload-artifact@v4 + with: + name: bandit-report-daemon + path: packages/daemon/bandit-report.json + + test: + name: Test (daemon) + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, macos-latest] + python-version: ["3.12", "3.13"] + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Install uv + run: pip install uv + + - name: Install dependencies + run: uv sync --dev --all-packages + + - name: Run tests with coverage + run: | + cd packages/daemon + uv run pytest -n auto --cov=src --cov-report=xml --cov-report=term + + - name: Check coverage threshold + run: | + cd packages/daemon + uv run coverage report --fail-under=95 + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v4 + with: + file: packages/daemon/coverage.xml + flags: daemon + token: ${{ secrets.CODECOV_TOKEN }} + fail_ci_if_error: false diff --git a/.github/workflows/ci-event-protocol.yml b/.github/workflows/ci-event-protocol.yml new file mode 100644 index 0000000..922c0f7 --- /dev/null +++ b/.github/workflows/ci-event-protocol.yml @@ -0,0 +1,122 @@ +name: event-protocol Package CI + +on: + push: + branches: ["**"] + paths: + - "packages/event-protocol/**" + - ".github/workflows/ci-event-protocol.yml" + pull_request: + branches: [main, master, develop] + paths: + - "packages/event-protocol/**" + +jobs: + lint: + name: Lint (event-protocol) + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Install uv + run: pip install uv + + - name: Install dependencies + run: uv sync --dev --all-packages + + - name: Ruff check + run: | + cd packages/event-protocol + uv run ruff check --line-length 88 . + + - name: Black check + run: | + cd packages/event-protocol + uv run black --check --line-length=88 . + + - name: isort check + run: | + cd packages/event-protocol + uv run isort --check --line-length=88 . + + # Add mypy step for packages with complex logic (core, orchestrator): + # - name: mypy type check + # run: | + # cd packages/event-protocol + # uv run mypy src/ + + security: + name: Security Scan (event-protocol) + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Install uv + run: pip install uv + + - name: Install dependencies + run: uv sync --dev --all-packages + + - name: Bandit security scan + run: | + cd packages/event-protocol + uv run bandit -r src/ -ll -f json -o bandit-report.json + + - name: Upload security report + if: always() + uses: actions/upload-artifact@v4 + with: + name: bandit-report-event-protocol + path: packages/event-protocol/bandit-report.json + + test: + name: Test (event-protocol) + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, macos-latest] + python-version: ["3.12", "3.13"] + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Install uv + run: pip install uv + + - name: Install dependencies + run: uv sync --dev --all-packages + + - name: Run tests with coverage + run: | + cd packages/event-protocol + uv run pytest -n auto --cov=src --cov-report=xml --cov-report=term + + - name: Check coverage threshold + run: | + cd packages/event-protocol + uv run coverage report --fail-under=95 + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v4 + with: + file: packages/event-protocol/coverage.xml + flags: event-protocol + token: ${{ secrets.CODECOV_TOKEN }} + fail_ci_if_error: false diff --git a/.github/workflows/ci-logging.yml b/.github/workflows/ci-logging.yml new file mode 100644 index 0000000..7377b95 --- /dev/null +++ b/.github/workflows/ci-logging.yml @@ -0,0 +1,122 @@ +name: logging Package CI + +on: + push: + branches: ["**"] + paths: + - "packages/logging/**" + - ".github/workflows/ci-logging.yml" + pull_request: + branches: [main, master, develop] + paths: + - "packages/logging/**" + +jobs: + lint: + name: Lint (logging) + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Install uv + run: pip install uv + + - name: Install dependencies + run: uv sync --dev --all-packages + + - name: Ruff check + run: | + cd packages/logging + uv run ruff check --line-length 88 . + + - name: Black check + run: | + cd packages/logging + uv run black --check --line-length=88 . + + - name: isort check + run: | + cd packages/logging + uv run isort --check --line-length=88 . + + # Add mypy step for packages with complex logic (core, orchestrator): + # - name: mypy type check + # run: | + # cd packages/logging + # uv run mypy src/ + + security: + name: Security Scan (logging) + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Install uv + run: pip install uv + + - name: Install dependencies + run: uv sync --dev --all-packages + + - name: Bandit security scan + run: | + cd packages/logging + uv run bandit -r src/ -ll -f json -o bandit-report.json + + - name: Upload security report + if: always() + uses: actions/upload-artifact@v4 + with: + name: bandit-report-logging + path: packages/logging/bandit-report.json + + test: + name: Test (logging) + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, macos-latest] + python-version: ["3.12", "3.13"] + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Install uv + run: pip install uv + + - name: Install dependencies + run: uv sync --dev --all-packages + + - name: Run tests with coverage + run: | + cd packages/logging + uv run pytest -n auto --cov=src --cov-report=xml --cov-report=term + + - name: Check coverage threshold + run: | + cd packages/logging + uv run coverage report --fail-under=95 + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v4 + with: + file: packages/logging/coverage.xml + flags: logging + token: ${{ secrets.CODECOV_TOKEN }} + fail_ci_if_error: false diff --git a/.github/workflows/ci-pipeline.yml b/.github/workflows/ci-pipeline.yml new file mode 100644 index 0000000..f08b355 --- /dev/null +++ b/.github/workflows/ci-pipeline.yml @@ -0,0 +1,122 @@ +name: pipeline Package CI + +on: + push: + branches: ["**"] + paths: + - "packages/pipeline/**" + - ".github/workflows/ci-pipeline.yml" + pull_request: + branches: [main, master, develop] + paths: + - "packages/pipeline/**" + +jobs: + lint: + name: Lint (pipeline) + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Install uv + run: pip install uv + + - name: Install dependencies + run: uv sync --dev --all-packages + + - name: Ruff check + run: | + cd packages/pipeline + uv run ruff check --line-length 88 . + + - name: Black check + run: | + cd packages/pipeline + uv run black --check --line-length=88 . + + - name: isort check + run: | + cd packages/pipeline + uv run isort --check --line-length=88 . + + # Add mypy step for packages with complex logic (core, orchestrator): + # - name: mypy type check + # run: | + # cd packages/pipeline + # uv run mypy src/ + + security: + name: Security Scan (pipeline) + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Install uv + run: pip install uv + + - name: Install dependencies + run: uv sync --dev --all-packages + + - name: Bandit security scan + run: | + cd packages/pipeline + uv run bandit -r src/ -ll -f json -o bandit-report.json + + - name: Upload security report + if: always() + uses: actions/upload-artifact@v4 + with: + name: bandit-report-pipeline + path: packages/pipeline/bandit-report.json + + test: + name: Test (pipeline) + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, macos-latest] + python-version: ["3.12", "3.13"] + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Install uv + run: pip install uv + + - name: Install dependencies + run: uv sync --dev --all-packages + + - name: Run tests with coverage + run: | + cd packages/pipeline + uv run pytest -n auto --cov=src --cov-report=xml --cov-report=term + + - name: Check coverage threshold + run: | + cd packages/pipeline + uv run coverage report --fail-under=95 + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v4 + with: + file: packages/pipeline/coverage.xml + flags: pipeline + token: ${{ secrets.CODECOV_TOKEN }} + fail_ci_if_error: false diff --git a/.github/workflows/ci-socket.yml b/.github/workflows/ci-socket.yml new file mode 100644 index 0000000..cfd8cdc --- /dev/null +++ b/.github/workflows/ci-socket.yml @@ -0,0 +1,122 @@ +name: socket Package CI + +on: + push: + branches: ["**"] + paths: + - "packages/socket/**" + - ".github/workflows/ci-socket.yml" + pull_request: + branches: [main, master, develop] + paths: + - "packages/socket/**" + +jobs: + lint: + name: Lint (socket) + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Install uv + run: pip install uv + + - name: Install dependencies + run: uv sync --dev --all-packages + + - name: Ruff check + run: | + cd packages/socket + uv run ruff check --line-length 88 . + + - name: Black check + run: | + cd packages/socket + uv run black --check --line-length=88 . + + - name: isort check + run: | + cd packages/socket + uv run isort --check --line-length=88 . + + # Add mypy step for packages with complex logic (core, orchestrator): + # - name: mypy type check + # run: | + # cd packages/socket + # uv run mypy src/ + + security: + name: Security Scan (socket) + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Install uv + run: pip install uv + + - name: Install dependencies + run: uv sync --dev --all-packages + + - name: Bandit security scan + run: | + cd packages/socket + uv run bandit -r src/ -ll -f json -o bandit-report.json + + - name: Upload security report + if: always() + uses: actions/upload-artifact@v4 + with: + name: bandit-report-socket + path: packages/socket/bandit-report.json + + test: + name: Test (socket) + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, macos-latest] + python-version: ["3.12", "3.13"] + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Install uv + run: pip install uv + + - name: Install dependencies + run: uv sync --dev --all-packages + + - name: Run tests with coverage + run: | + cd packages/socket + uv run pytest -n auto --cov=src --cov-report=xml --cov-report=term + + - name: Check coverage threshold + run: | + cd packages/socket + uv run coverage report --fail-under=95 + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v4 + with: + file: packages/socket/coverage.xml + flags: socket + token: ${{ secrets.CODECOV_TOKEN }} + fail_ci_if_error: false diff --git a/.github/workflows/ci-storage.yml b/.github/workflows/ci-storage.yml new file mode 100644 index 0000000..7a1a7d8 --- /dev/null +++ b/.github/workflows/ci-storage.yml @@ -0,0 +1,122 @@ +name: storage Package CI + +on: + push: + branches: ["**"] + paths: + - "packages/storage/**" + - ".github/workflows/ci-storage.yml" + pull_request: + branches: [main, master, develop] + paths: + - "packages/storage/**" + +jobs: + lint: + name: Lint (storage) + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Install uv + run: pip install uv + + - name: Install dependencies + run: uv sync --dev --all-packages + + - name: Ruff check + run: | + cd packages/storage + uv run ruff check --line-length 88 . + + - name: Black check + run: | + cd packages/storage + uv run black --check --line-length=88 . + + - name: isort check + run: | + cd packages/storage + uv run isort --check --line-length=88 . + + # Add mypy step for packages with complex logic (core, orchestrator): + # - name: mypy type check + # run: | + # cd packages/storage + # uv run mypy src/ + + security: + name: Security Scan (storage) + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Install uv + run: pip install uv + + - name: Install dependencies + run: uv sync --dev --all-packages + + - name: Bandit security scan + run: | + cd packages/storage + uv run bandit -r src/ -ll -f json -o bandit-report.json + + - name: Upload security report + if: always() + uses: actions/upload-artifact@v4 + with: + name: bandit-report-storage + path: packages/storage/bandit-report.json + + test: + name: Test (storage) + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, macos-latest] + python-version: ["3.12", "3.13"] + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Install uv + run: pip install uv + + - name: Install dependencies + run: uv sync --dev --all-packages + + - name: Run tests with coverage + run: | + cd packages/storage + uv run pytest -n auto --cov=src --cov-report=xml --cov-report=term + + - name: Check coverage threshold + run: | + cd packages/storage + uv run coverage report --fail-under=95 + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v4 + with: + file: packages/storage/coverage.xml + flags: storage + token: ${{ secrets.CODECOV_TOKEN }} + fail_ci_if_error: false diff --git a/.github/workflows/smoke-test.yml b/.github/workflows/smoke-test.yml new file mode 100644 index 0000000..7380b70 --- /dev/null +++ b/.github/workflows/smoke-test.yml @@ -0,0 +1,45 @@ +name: Smoke Tests (Integration) + +on: + push: + branches: ["**"] + workflow_dispatch: + inputs: + verbose: + description: 'Verbose test output' + required: false + default: false + type: boolean + +jobs: + smoke-test: + name: End-to-End Smoke Tests + runs-on: ubuntu-latest + timeout-minutes: 20 + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Install uv + run: pip install uv + + - name: Install dependencies + run: uv sync --dev --all-packages + + # Add any system dependencies your smoke tests require, e.g.: + # - name: Install system dependencies + # run: sudo apt-get install -y + + - name: Run smoke tests (standard mode) + if: inputs.verbose == false && github.event_name != 'push' + run: bash tests/smoke/run-smoke-tests.sh + + - name: Run smoke tests (verbose mode) + if: inputs.verbose == true || github.event_name == 'push' + run: bash tests/smoke/run-smoke-tests.sh --verbose diff --git a/.gitignore b/.gitignore index 351fce9..f7d8613 100644 --- a/.gitignore +++ b/.gitignore @@ -137,3 +137,4 @@ cython_debug/ # Git worktrees .worktrees/ +bandit-report.json diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..a8713f0 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,150 @@ +# Pre-commit hooks configuration +# Runs automated checks before each commit to catch issues early +# Install: pre-commit install +# Run manually: pre-commit run --all-files + +repos: + # Basic file checks + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.5.0 + hooks: + - id: trailing-whitespace + name: Trim trailing whitespace + - id: end-of-file-fixer + name: Fix end of file + - id: check-yaml + name: Check YAML syntax + - id: check-json + name: Check JSON syntax + - id: check-toml + name: Check TOML syntax + - id: check-merge-conflict + name: Check for merge conflicts + - id: detect-private-key + name: Detect private keys + - id: check-case-conflict + name: Check for case conflicts + - id: check-symlinks + name: Check for broken symlinks + + # Python formatting and linting + - repo: https://github.com/psf/black + rev: 24.10.0 + hooks: + - id: black + name: Format code with Black + language_version: python3.12 + args: [--line-length=88] + stages: [pre-commit] + + # Import sorting + - repo: https://github.com/PyCQA/isort + rev: 5.13.2 + hooks: + - id: isort + name: Sort imports with isort + args: [--profile=black, --line-length=88] + stages: [pre-commit] + + # Ruff linting (checks only, no import sorting) + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.8.4 + hooks: + - id: ruff + name: Lint with Ruff + args: [--fix, --line-length=88] + stages: [pre-commit] + - id: ruff-format + name: Format with Ruff (fallback) + stages: [pre-commit] + + # Type checking with mypy + - repo: https://github.com/pre-commit/mirrors-mypy + rev: v1.13.0 + hooks: + - id: mypy + name: Type check with mypy + language_version: python3.12 + args: [--ignore-missing-imports, --disable-error-code=import-untyped] + stages: [pre-commit] + # add project-specific type stubs here + # e.g. types-requests, types-PyYAML, types-toml + # Exclude test directories to avoid duplicate module errors + exclude: ^(packages|src)/.*/tests/ + + # Security scanning with Bandit + - repo: https://github.com/PyCQA/bandit + rev: 1.8.0 + hooks: + - id: bandit + name: Security check with Bandit + args: [-ll] + stages: [pre-commit] + # Skip test directories and files + exclude: ^(packages|src)/.*/tests/|test_.*\.py$ + files: ^(packages|src)/.*\.py$ + additional_dependencies: [pbr] + + # File size checks + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.5.0 + hooks: + - id: check-added-large-files + name: Check for large files + args: [--maxkb=1000] + exclude: ^tests/fixtures/ + + # Commit message format enforcement + - repo: https://github.com/commitizen-tools/commitizen + rev: v3.29.0 + hooks: + - id: commitizen + name: Validate commit message format + stages: [commit-msg] + + # Branch naming enforcement and full pipeline validation + - repo: local + hooks: + - id: validate-branch-name + name: Validate branch name + entry: scripts/hooks/validate-branch.sh + language: script + stages: [pre-commit] + always_run: true + pass_filenames: false + + - id: validate-ticket-docs + name: Validate ticket documents exist + entry: scripts/hooks/validate-ticket-docs.sh + language: script + stages: [pre-commit] + always_run: true + pass_filenames: false + + - id: pipeline-validation + name: Validate pipeline locally + entry: bash -c 'make pipeline' + language: system + stages: [manual] # Run with: pre-commit run pipeline-validation --all-files + pass_filenames: false + always_run: true + +# Files to exclude from all hooks +exclude: | + (?x)^( + _archive_old_implementation| + htmlcov| + \.venv| + venv| + __pycache__| + \.pytest_cache| + \.mypy_cache| + \.git| + dist| + build| + .*\.egg-info + ) + +# Configuration for pre-commit behavior +default_stages: [pre-commit] +fail_fast: false # Run all hooks even if one fails diff --git a/Makefile b/Makefile index 297c2b4..8f0a304 100644 --- a/Makefile +++ b/Makefile @@ -1,123 +1,278 @@ -MAKEFLAGS += --no-print-directory +.PHONY: help dev install-deps lint format security test-all typecheck smoke-test pipeline push changelog changelog-draft bump ticket clean -# All workspace packages in dependency order -PACKAGES := logging pipeline package-manager container-manager +# ───────────────────────────────────────────────────────────────────────────── +# Variables — configure these for your project +# ───────────────────────────────────────────────────────────────────────────── +PROJECT_NAME := core +PYTHON_VERSION := 3.12 +UV := uv -.PHONY: help sync install dev-shell format lint type-check \ - test test-cov \ - test-logging test-pipeline test-package-manager test-container-manager \ - clean clean-venv pre-commit-install pre-commit-run all-checks +# Directory that contains your packages. For a workspace this is typically +# "packages". For a single-package project set both to ".": +# PACKAGES_DIR := . +# PACKAGES := . +PACKAGES_DIR := packages +# Space-separated list of package directory names under $(PACKAGES_DIR)/. +# Example: PACKAGES := settings core effects orchestrator +PACKAGES := logging pipeline container-manager socket storage cache daemon event-protocol + +# Packages that require mypy type checking. Leave empty to skip mypy everywhere. +# Example: MYPY_PACKAGES := core orchestrator +MYPY_PACKAGES := logging pipeline container-manager socket storage cache daemon event-protocol # + +# Smoke test configuration +# Per-package smoke test scripts at: packages//tests/smoke/run-smoke-tests.sh + +# Color output +BLUE := \033[0;34m +GREEN := \033[0;32m +RED := \033[0;31m +YELLOW := \033[0;33m +NC := \033[0m + +# ───────────────────────────────────────────────────────────────────────────── +# Derived variables — do not edit +# ───────────────────────────────────────────────────────────────────────────── +LINT_TARGETS := $(addprefix lint-,$(PACKAGES)) +FORMAT_TARGETS := $(addprefix format-,$(PACKAGES)) +SECURITY_TARGETS := $(addprefix security-,$(PACKAGES)) +TEST_TARGETS := $(addprefix test-,$(PACKAGES)) + +# ───────────────────────────────────────────────────────────────────────────── +##@ General +# ───────────────────────────────────────────────────────────────────────────── +.DEFAULT_GOAL := help help: ## Show this help message - @echo "Available commands:" - @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf " %-28s %s\n", $$1, $$2}' - -# ─── Workspace sync ───────────────────────────────────────────────────────── - -sync: ## Sync entire workspace (installs all packages into shared .venv) - @echo "Syncing workspace..." - @uv sync - @echo "✅ Workspace sync complete" - -install: sync ## Alias for sync - -dev-shell: ## Activate the shared workspace virtual environment - @echo "Activating virtual environment..." - @echo "Leave the dev shell by typing 'exit'" - @bash -c "source .venv/bin/activate && exec bash" - -# ─── Code quality (workspace-wide) ────────────────────────────────────────── - -format: ## Format all packages with black and isort - @echo "Formatting all packages..." - @uv run black packages/ - @uv run isort packages/ - @echo "✅ Formatting complete" - -lint: ## Lint all packages with ruff - @echo "Linting all packages..." - @uv run ruff check --fix packages/ - @echo "✅ Linting complete" - -type-check: ## Type check each package with mypy (delegated per-package) - @echo "Type checking all packages..." - @for pkg in $(PACKAGES); do \ - echo ""; \ - echo "--- $$pkg ---"; \ - $(MAKE) -C packages/$$pkg type-check; \ - done - @echo "" - @echo "✅ Type checking complete" - -# ─── Tests (must run per-package — workspace-wide run causes namespace collision) ── - -test: ## Run tests for all packages (each package run independently) - @echo "Running all tests..." - @for pkg in $(PACKAGES); do \ - echo ""; \ - echo "--- Testing $$pkg ---"; \ - $(MAKE) -C packages/$$pkg test; \ - done - @echo "" - @echo "✅ All tests complete" - -test-cov: ## Run tests with coverage for all packages - @echo "Running all tests with coverage..." - @for pkg in $(PACKAGES); do \ - echo ""; \ - echo "--- Testing $$pkg (with coverage) ---"; \ - $(MAKE) -C packages/$$pkg test-cov; \ - done - @echo "" - @echo "✅ Coverage complete" - -test-logging: ## Run tests for the logging package only - @$(MAKE) -C packages/logging test - -test-pipeline: ## Run tests for the pipeline package only - @$(MAKE) -C packages/pipeline test - -test-package-manager: ## Run tests for the package-manager package only - @$(MAKE) -C packages/package-manager test - -test-container-manager: ## Run tests for the container-manager package only - @$(MAKE) -C packages/container-manager test - -# ─── Cleanup ───────────────────────────────────────────────────────────────── - -clean: ## Clean cache files and build artifacts across all packages - @echo "Cleaning all packages..." - @for pkg in $(PACKAGES); do \ - $(MAKE) -C packages/$$pkg clean; \ - done - @find . -maxdepth 2 -type d -name "__pycache__" -exec rm -rf {} + 2>/dev/null || true - @find . -maxdepth 2 -type f -name "*.pyc" -delete 2>/dev/null || true - @find . -maxdepth 1 -type d -name ".pytest_cache" -exec rm -rf {} + 2>/dev/null || true - @find . -maxdepth 1 -type d -name ".mypy_cache" -exec rm -rf {} + 2>/dev/null || true - @find . -maxdepth 1 -type d -name ".ruff_cache" -exec rm -rf {} + 2>/dev/null || true - @find . -maxdepth 1 -type d -name "*.egg-info" -exec rm -rf {} + 2>/dev/null || true - @rm -rf build/ dist/ htmlcov/ .coverage 2>/dev/null || true - @echo "✅ Cleanup complete" - -clean-venv: ## Remove the shared workspace virtual environment - @echo "Removing virtual environment..." - @rm -rf .venv - @echo "✅ Virtual environment removed" - @echo "Run 'make sync' to recreate" - -# ─── Pre-commit ─────────────────────────────────────────────────────────────── - -pre-commit-install: ## Install pre-commit hooks - @echo "Installing pre-commit hooks..." - @uv run pre-commit install - @echo "✅ Pre-commit hooks installed" - -pre-commit-run: ## Run pre-commit on all files - @echo "Running pre-commit on all files..." - @uv run pre-commit run --all-files - @echo "✅ Pre-commit checks complete" - -# ─── Composite ──────────────────────────────────────────────────────────────── - -all-checks: format lint type-check test ## Run all checks (format, lint, type-check, test) - @echo "✅ All checks passed!" + @echo -e "$(BLUE)$(PROJECT_NAME) - Development Makefile$(NC)" + @echo -e "" + @echo -e "$(GREEN)Usage:$(NC)" + @echo -e " make $(BLUE)$(NC)" + @echo -e "" + @echo -e "$(GREEN)Targets:$(NC)" + @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf " $(BLUE)%-25s$(NC) %s\n", $$1, $$2}' + +# ───────────────────────────────────────────────────────────────────────────── +##@ Setup & Installation +# ───────────────────────────────────────────────────────────────────────────── +dev: install-deps ## Set up development environment + @$(UV) run pre-commit install --hook-type pre-commit --hook-type commit-msg + @echo -e "$(GREEN)Development environment ready$(NC)" + +install-deps: ## Install all dependencies for development + @echo -e "$(BLUE)Installing packages...$(NC)" + $(UV) sync --dev --all-packages + @echo -e "$(GREEN)All dependencies installed$(NC)" + +# ───────────────────────────────────────────────────────────────────────────── +##@ Code Quality +# ───────────────────────────────────────────────────────────────────────────── +lint: $(LINT_TARGETS) ## Run linting on all packages + +lint-%: ## Lint a specific package + @echo -e "$(BLUE)Linting $* package...$(NC)" + @$(UV) run ruff check --line-length 88 $(PACKAGES_DIR)/$* || (echo -e "$(RED)Ruff found issues in $*. Run: make format-$*$(NC)" && exit 1) + @$(UV) run black --check --line-length 88 $(PACKAGES_DIR)/$* || (echo -e "$(RED)Black formatting needed in $*. Run: make format-$*$(NC)" && exit 1) + @$(UV) run isort --check --profile black --line-length 88 $(PACKAGES_DIR)/$* || (echo -e "$(RED)Import sorting needed in $*. Run: make format-$*$(NC)" && exit 1) + @if echo "$(MYPY_PACKAGES)" | grep -wq "$*"; then \ + echo -e "$(BLUE)Running mypy on $*...$(NC)"; \ + $(UV) run mypy $(PACKAGES_DIR)/$*/src/ || (echo -e "$(RED)Type errors found in $*$(NC)" && exit 1); \ + fi + @echo -e "$(GREEN)$* linting passed$(NC)" + +format: $(FORMAT_TARGETS) ## Format all code in packages + +format-%: ## Format a specific package + @echo -e "$(BLUE)Formatting $* package...$(NC)" + $(UV) run isort --profile black --line-length 88 $(PACKAGES_DIR)/$* + $(UV) run black --line-length 88 $(PACKAGES_DIR)/$* + $(UV) run ruff check --fix --line-length 88 $(PACKAGES_DIR)/$* + @echo -e "$(GREEN)$* formatted$(NC)" + +typecheck: ## Run mypy on all MYPY_PACKAGES (skips if MYPY_PACKAGES is empty) + @if [ -z "$(MYPY_PACKAGES)" ]; then \ + echo -e "$(YELLOW)MYPY_PACKAGES is empty — skipping type checking$(NC)"; \ + else \ + for pkg in $(MYPY_PACKAGES); do \ + echo -e "$(BLUE)Type-checking $$pkg...$(NC)"; \ + $(UV) run mypy $(PACKAGES_DIR)/$$pkg/src/ || exit 1; \ + done; \ + echo -e "$(GREEN)Type checking passed$(NC)"; \ + fi + +# ───────────────────────────────────────────────────────────────────────────── +##@ Security +# ───────────────────────────────────────────────────────────────────────────── +security: $(SECURITY_TARGETS) ## Run security scans on all packages + +security-%: ## Security scan for a specific package + @echo -e "$(BLUE)Running security scan on $* package...$(NC)" + cd $(PACKAGES_DIR)/$* && $(UV) run bandit -r src/ -ll -f json -o bandit-report.json + @echo -e "$(GREEN)$* security scan complete$(NC)" + +# ───────────────────────────────────────────────────────────────────────────── +##@ Testing +# ───────────────────────────────────────────────────────────────────────────── +test-all: $(TEST_TARGETS) ## Run full test suite with coverage + @echo -e "$(GREEN)All package tests completed$(NC)" + +test-%: ## Test a specific package + @echo -e "$(BLUE)Testing $* package...$(NC)" + cd $(PACKAGES_DIR)/$* && $(UV) run pytest -n auto --color=yes --cov=src --cov-report=term + cd $(PACKAGES_DIR)/$* && $(UV) run coverage report --fail-under=65 + +# ───────────────────────────────────────────────────────────────────────────── +##@ Smoke Testing +# ───────────────────────────────────────────────────────────────────────────── +smoke-test: ## Run smoke tests (VERBOSE=true for verbose output) + @echo -e "$(BLUE)Running smoke tests...$(NC)" + @if [ ! -f "$(SMOKE_TEST_SCRIPT)" ]; then \ + echo -e "$(RED)Smoke test script not found at $(SMOKE_TEST_SCRIPT)$(NC)"; \ + exit 1; \ + fi + @if [ "$(VERBOSE)" = "true" ]; then \ + bash $(SMOKE_TEST_SCRIPT) --verbose; \ + else \ + bash $(SMOKE_TEST_SCRIPT); \ + fi + @echo -e "$(GREEN)Smoke tests completed$(NC)" + +# ───────────────────────────────────────────────────────────────────────────── +##@ Documentation +# ───────────────────────────────────────────────────────────────────────────── +changelog: ## Append unreleased commits to docs/changelog.md + @$(UV) run cz changelog + +bump: ## Bump version and update changelog (PKG= for workspace packages, INCREMENT=patch|minor|major) + @if [ -n "$(PKG)" ]; then \ + echo -e "$(BLUE)Bumping version for package: $(PKG)$(NC)"; \ + $(UV) run cz bump --project-root $(PACKAGES_DIR)/$(PKG) $(if $(INCREMENT),--increment $(INCREMENT),); \ + else \ + echo -e "$(BLUE)Bumping root version$(NC)"; \ + $(UV) run cz bump $(if $(INCREMENT),--increment $(INCREMENT),); \ + fi + +ticket: ## Scaffold ticket documents and branch (TYPE=feature|bug|chore ID=NNNNN NAME=) + @if [ -z "$(TYPE)" ] || [ -z "$(ID)" ] || [ -z "$(NAME)" ]; then \ + echo -e "$(RED)Usage: make ticket TYPE=feature|bug|chore ID=NNNNN NAME=$(NC)"; \ + echo -e "$(YELLOW)Example: make ticket TYPE=feature ID=00001 NAME=add-oauth-login$(NC)"; \ + exit 1; \ + fi + @./scripts/scaffold-ticket.sh $(TYPE) $(ID) $(NAME) + +changelog-draft: ## Scaffold docs/changelog-draft.md from git merge commits (manual fallback) + @./scripts/generate-changelog.sh + +# ───────────────────────────────────────────────────────────────────────────── +##@ CI/CD Pipeline +# ───────────────────────────────────────────────────────────────────────────── +pipeline: ## Simulate GitHub Actions locally: lint → security → test + @echo -e "$(BLUE)Running pipeline validation...$(NC)" + @echo -e "" + @echo -e "$(BLUE)Step 1: Linting (All packages)$(NC)" + @$(MAKE) lint + @echo -e "$(GREEN)Linting passed$(NC)" + @echo -e "" + @echo -e "$(BLUE)Step 2: Security Scan (All packages)$(NC)" + @$(MAKE) security + @echo -e "$(GREEN)Security scans passed$(NC)" + @echo -e "" + @echo -e "$(BLUE)Step 3: Testing (All packages)$(NC)" + @$(MAKE) test-all + @echo -e "$(GREEN)Tests passed with 95%+ coverage$(NC)" + @echo -e "" + @echo -e "$(GREEN)Pipeline validation successful!$(NC)" + @echo -e "$(GREEN)Your changes are safe to push to the cloud.$(NC)" + @echo -e "" + +push: ## Run GitHub Actions workflows locally via act (SMOKE=true for smoke tests) + @echo -e "$(BLUE)Setting up GitHub Actions locally...$(NC)" + @if [ ! -f ./bin/act ]; then \ + echo -e "$(BLUE)Downloading act (GitHub Actions CLI)...$(NC)"; \ + mkdir -p ./bin; \ + curl -sL https://github.com/nektos/act/releases/download/v0.2.65/act_Linux_x86_64.tar.gz -o /tmp/act.tar.gz; \ + tar -xzf /tmp/act.tar.gz -C ./bin; \ + rm /tmp/act.tar.gz; \ + echo -e "$(GREEN)act installed to ./bin/act$(NC)"; \ + else \ + echo -e "$(GREEN)act already available$(NC)"; \ + fi + @echo -e "" + @mkdir -p .logs + @TIMESTAMP=$$(date +%Y%m%d-%H%M%S); \ + LOG_FILE=".logs/make-push-$$TIMESTAMP.log"; \ + if [ "$(SMOKE)" = "true" ]; then \ + echo -e "$(BLUE)═══════════════════════════════════════════════════════════$(NC)" | tee "$$LOG_FILE"; \ + echo -e "$(BLUE)Running GitHub Actions with SMOKE TESTS enabled$(NC)" | tee -a "$$LOG_FILE"; \ + echo -e "$(BLUE)═══════════════════════════════════════════════════════════$(NC)" | tee -a "$$LOG_FILE"; \ + echo -e "$(BLUE)Logs: $$LOG_FILE$(NC)" | tee -a "$$LOG_FILE"; \ + echo -e "$(BLUE)═══════════════════════════════════════════════════════════$(NC)" | tee -a "$$LOG_FILE"; \ + echo -e "" | tee -a "$$LOG_FILE"; \ + echo -e "$(BLUE)───────────────────────────────────────────────────────────$(NC)" | tee -a "$$LOG_FILE"; \ + echo -e "$(BLUE)PHASE 1: Standard CI Workflows$(NC)" | tee -a "$$LOG_FILE"; \ + echo -e "$(BLUE)───────────────────────────────────────────────────────────$(NC)" | tee -a "$$LOG_FILE"; \ + ./bin/act push --container-options "--user $$(id -u):$$(id -g)" 2>&1 | tee -a "$$LOG_FILE"; \ + STANDARD_EXIT=$${PIPESTATUS[0]}; \ + echo -e "" | tee -a "$$LOG_FILE"; \ + if [ $$STANDARD_EXIT -eq 0 ]; then \ + echo -e "$(GREEN)Standard CI passed$(NC)" | tee -a "$$LOG_FILE"; \ + echo -e "" | tee -a "$$LOG_FILE"; \ + echo -e "$(BLUE)───────────────────────────────────────────────────────────$(NC)" | tee -a "$$LOG_FILE"; \ + echo -e "$(BLUE)PHASE 2: Smoke Test Workflow$(NC)" | tee -a "$$LOG_FILE"; \ + echo -e "$(BLUE)───────────────────────────────────────────────────────────$(NC)" | tee -a "$$LOG_FILE"; \ + ./bin/act workflow_dispatch -W .github/workflows/smoke-test.yml --container-options "--user $$(id -u):$$(id -g)" 2>&1 | tee -a "$$LOG_FILE"; \ + SMOKE_EXIT=$${PIPESTATUS[0]}; \ + echo -e "" | tee -a "$$LOG_FILE"; \ + if [ $$SMOKE_EXIT -eq 0 ]; then \ + echo -e "$(GREEN)Smoke tests passed$(NC)" | tee -a "$$LOG_FILE"; \ + EXIT_CODE=0; \ + else \ + echo -e "$(RED)Smoke tests failed$(NC)" | tee -a "$$LOG_FILE"; \ + EXIT_CODE=$$SMOKE_EXIT; \ + fi; \ + else \ + echo -e "$(RED)Standard CI failed, skipping smoke tests$(NC)" | tee -a "$$LOG_FILE"; \ + EXIT_CODE=$$STANDARD_EXIT; \ + fi; \ + else \ + echo -e "$(BLUE)═══════════════════════════════════════════════════════════$(NC)" | tee "$$LOG_FILE"; \ + echo -e "$(BLUE)Running Standard GitHub Actions Workflows$(NC)" | tee -a "$$LOG_FILE"; \ + echo -e "$(BLUE)═══════════════════════════════════════════════════════════$(NC)" | tee -a "$$LOG_FILE"; \ + echo -e "$(BLUE)Logs: $$LOG_FILE$(NC)" | tee -a "$$LOG_FILE"; \ + echo -e "$(YELLOW)Tip: Add SMOKE=true to include smoke tests$(NC)" | tee -a "$$LOG_FILE"; \ + echo -e "$(BLUE)═══════════════════════════════════════════════════════════$(NC)" | tee -a "$$LOG_FILE"; \ + echo -e "" | tee -a "$$LOG_FILE"; \ + ./bin/act push --container-options "--user $$(id -u):$$(id -g)" 2>&1 | tee -a "$$LOG_FILE"; \ + EXIT_CODE=$${PIPESTATUS[0]}; \ + fi; \ + echo -e "" | tee -a "$$LOG_FILE"; \ + echo -e "$(BLUE)═══════════════════════════════════════════════════════════$(NC)" | tee -a "$$LOG_FILE"; \ + if [ $$EXIT_CODE -eq 0 ]; then \ + echo -e "$(GREEN)GitHub Actions simulation complete$(NC)" | tee -a "$$LOG_FILE"; \ + else \ + echo -e "$(RED)GitHub Actions simulation failed (exit: $$EXIT_CODE)$(NC)" | tee -a "$$LOG_FILE"; \ + fi; \ + echo -e "$(BLUE)═══════════════════════════════════════════════════════════$(NC)" | tee -a "$$LOG_FILE"; \ + echo -e ""; \ + echo -e "$(GREEN)Full logs: $$LOG_FILE$(NC)"; \ + echo -e "$(GREEN)View logs: cat $$LOG_FILE$(NC)"; \ + echo -e "$(GREEN)Search: grep 'PASSED\|FAILED' $$LOG_FILE$(NC)"; \ + echo -e ""; \ + exit $$EXIT_CODE + +# ───────────────────────────────────────────────────────────────────────────── +##@ Cleanup +# ───────────────────────────────────────────────────────────────────────────── +clean: ## Remove build artifacts and caches + @echo -e "$(BLUE)Cleaning build artifacts...$(NC)" + find . -type d -name __pycache__ -exec rm -rf {} + 2>/dev/null || true + find . -type d -name .pytest_cache -exec rm -rf {} + 2>/dev/null || true + find . -type d -name .mypy_cache -exec rm -rf {} + 2>/dev/null || true + find . -type d -name htmlcov -exec rm -rf {} + 2>/dev/null || true + find . -type d -name dist -exec rm -rf {} + 2>/dev/null || true + find . -type d -name build -exec rm -rf {} + 2>/dev/null || true + find . -type f -name "*.egg-info" -exec rm -rf {} + 2>/dev/null || true + find . -type f -name ".coverage" -delete + find . -type f -name "coverage.xml" -delete + find . -type f -name "bandit-report.json" -delete + @echo -e "$(GREEN)Cleanup complete$(NC)" diff --git a/conftest.py b/conftest.py index 312ebd3..f92d0d5 100644 --- a/conftest.py +++ b/conftest.py @@ -9,8 +9,12 @@ for _pkg_src in [ "packages/logging/src", "packages/pipeline/src", - "packages/package-manager/src", "packages/container-manager/src", + "packages/socket/src", + "packages/storage/src", + "packages/cache/src", + "packages/daemon/src", + "packages/event-protocol/src", ]: _p = str(_root / _pkg_src) if _p not in sys.path: diff --git a/packages/cache/README.md b/packages/cache/README.md new file mode 100644 index 0000000..2b72638 --- /dev/null +++ b/packages/cache/README.md @@ -0,0 +1,20 @@ +# core-cache + +Generic file-path-keyed cache layer over core-storage. + +## Quick Start + +```bash +pip install core-cache +``` + +## Features + +- Transparent caching over storage backends +- Automatic invalidation support +- Pydantic model integration +- Thread-safe operations + +## Documentation + +See `docs/` for architecture and API reference. diff --git a/packages/cache/pyproject.toml b/packages/cache/pyproject.toml index 5cdde84..4649314 100644 --- a/packages/cache/pyproject.toml +++ b/packages/cache/pyproject.toml @@ -41,6 +41,11 @@ ignore = ["E501", "B008"] [tool.ruff.lint.per-file-ignores] "__init__.py" = ["F401"] +[tool.isort] +profile = "black" +line_length = 88 +known_first_party = ["core_cache"] + [tool.mypy] python_version = "3.12" check_untyped_defs = true diff --git a/packages/cache/src/core_cache/cache.py b/packages/cache/src/core_cache/cache.py index 357294b..59761a1 100644 --- a/packages/cache/src/core_cache/cache.py +++ b/packages/cache/src/core_cache/cache.py @@ -7,7 +7,6 @@ from core_storage.exceptions import StorageError from core_storage.store import NamespacedStore -from core_cache.exceptions import CacheError from core_cache.invalidation import build_metadata, check_staleness from core_cache.key import derive_key from core_cache.result import CacheHit, CacheMiss diff --git a/packages/cache/src/core_cache/settings/resolver.py b/packages/cache/src/core_cache/settings/resolver.py index ab29803..5041d71 100644 --- a/packages/cache/src/core_cache/settings/resolver.py +++ b/packages/cache/src/core_cache/settings/resolver.py @@ -34,7 +34,5 @@ def get_settings( env_prefix=ENV_PREFIX, _environ=_environ, ) - validated = ConfigBuilder.build( - CacheSettings, layers, cli_overrides=cli_overrides - ) + validated = ConfigBuilder.build(CacheSettings, layers, cli_overrides=cli_overrides) return validated # type: ignore[return-value] diff --git a/packages/cache/tests/integration/test_cache_integration.py b/packages/cache/tests/integration/test_cache_integration.py index cd3c999..ce3d941 100644 --- a/packages/cache/tests/integration/test_cache_integration.py +++ b/packages/cache/tests/integration/test_cache_integration.py @@ -5,7 +5,6 @@ from pathlib import Path import pytest - from core_storage.backends.json_ import JsonBackend from core_storage.backends.sqlite import SQLiteBackend from core_storage.store import Store diff --git a/packages/cache/tests/unit/test_cache.py b/packages/cache/tests/unit/test_cache.py index 37a9d47..d3a695b 100644 --- a/packages/cache/tests/unit/test_cache.py +++ b/packages/cache/tests/unit/test_cache.py @@ -6,7 +6,6 @@ from pathlib import Path import pytest - from core_storage.backends.sqlite import SQLiteBackend from core_storage.store import Store @@ -23,6 +22,7 @@ def make_cache(tmp_path: Path, strict: bool = False) -> Cache: # --- lookup: miss cases --- + def test_lookup_returns_not_found_when_key_absent(tmp_path: Path) -> None: cache = make_cache(tmp_path) img = tmp_path / "img.jpg" @@ -68,6 +68,7 @@ def test_lookup_returns_stale_when_mtime_changes(tmp_path: Path) -> None: # --- lookup: hit case --- + def test_lookup_returns_hit_when_file_unchanged(tmp_path: Path) -> None: cache = make_cache(tmp_path) img = tmp_path / "img.jpg" @@ -90,6 +91,7 @@ def test_lookup_respects_params_in_key(tmp_path: Path) -> None: # --- store: upsert behaviour --- + def test_store_overwrites_existing_entry(tmp_path: Path) -> None: cache = make_cache(tmp_path) img = tmp_path / "img.jpg" @@ -103,6 +105,7 @@ def test_store_overwrites_existing_entry(tmp_path: Path) -> None: # --- invalidate --- + def test_invalidate_returns_true_when_entry_existed(tmp_path: Path) -> None: cache = make_cache(tmp_path) img = tmp_path / "img.jpg" @@ -131,6 +134,7 @@ def test_invalidate_removes_entry(tmp_path: Path) -> None: # --- clear --- + def test_clear_removes_all_entries(tmp_path: Path) -> None: cache = make_cache(tmp_path) for name in ("a.jpg", "b.jpg", "c.jpg"): @@ -146,6 +150,7 @@ def test_clear_removes_all_entries(tmp_path: Path) -> None: # --- CacheError from non-serializable params --- + def test_lookup_raises_cache_error_for_bad_params(tmp_path: Path) -> None: cache = make_cache(tmp_path) img = tmp_path / "img.jpg" diff --git a/packages/cache/tests/unit/test_public_api.py b/packages/cache/tests/unit/test_public_api.py index 875f392..7526214 100644 --- a/packages/cache/tests/unit/test_public_api.py +++ b/packages/cache/tests/unit/test_public_api.py @@ -8,6 +8,6 @@ def test_public_api_exports() -> None: for name in ("Cache", "CacheHit", "CacheMiss", "CacheError", "CacheSettings"): assert hasattr(core_cache, name), f"Missing export: {name}" - assert set(("Cache", "CacheHit", "CacheMiss", "CacheError", "CacheSettings")).issubset( + assert {"Cache", "CacheHit", "CacheMiss", "CacheError", "CacheSettings"}.issubset( set(core_cache.__all__) ), "Not all symbols are listed in __all__" diff --git a/packages/cache/tests/unit/test_result.py b/packages/cache/tests/unit/test_result.py index 14b739d..190dd05 100644 --- a/packages/cache/tests/unit/test_result.py +++ b/packages/cache/tests/unit/test_result.py @@ -1,22 +1,28 @@ -from datetime import datetime, timezone +from datetime import UTC, datetime import pytest from pydantic import ValidationError +try: + from pydantic import FrozenInstanceError +except ImportError: + # pydantic 2.x moved FrozenInstanceError + FrozenInstanceError = ValueError # type: ignore + from core_cache.result import CacheHit, CacheMiss def test_cache_hit_construction() -> None: - now = datetime.now(tz=timezone.utc) + now = datetime.now(tz=UTC) hit = CacheHit(value=b"hello", cached_at=now) assert hit.value == b"hello" assert hit.cached_at == now def test_cache_hit_is_frozen() -> None: - now = datetime.now(tz=timezone.utc) + now = datetime.now(tz=UTC) hit = CacheHit(value=b"x", cached_at=now) - with pytest.raises(Exception): + with pytest.raises(FrozenInstanceError): hit.value = b"y" # type: ignore[misc] @@ -33,5 +39,5 @@ def test_cache_miss_invalid_reason_raises() -> None: def test_cache_miss_is_frozen() -> None: miss = CacheMiss(reason="not_found") - with pytest.raises(Exception): + with pytest.raises(FrozenInstanceError): miss.reason = "stale" # type: ignore[misc] diff --git a/packages/container-manager/Makefile b/packages/container-manager/Makefile deleted file mode 100644 index 4422dee..0000000 --- a/packages/container-manager/Makefile +++ /dev/null @@ -1,98 +0,0 @@ -MAKEFLAGS += --no-print-directory - -# Standard Makefile for container-manager -# Provides consistent development workflows - -.PHONY: help sync-check ensure-sync dev-shell format lint type-check test test-cov clean clean-venv install build pre-commit-install pre-commit-run all-checks - -help: ## Show this help message - @echo "Available commands:" - @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf " %-20s %s\n", $$1, $$2}' - -sync-check: ## Check if environment sync is needed - @if ! uv run python -c "import sys; sys.exit(0)" 2>/dev/null; then \ - echo "❌ Package not installed - sync needed"; \ - exit 1; \ - elif [ uv.lock -nt .venv/pyvenv.cfg ] 2>/dev/null; then \ - echo "❌ Lock file newer than venv - sync needed"; \ - exit 1; \ - elif [ pyproject.toml -nt .venv/pyvenv.cfg ] 2>/dev/null; then \ - echo "❌ pyproject.toml newer than venv - sync needed"; \ - exit 1; \ - else \ - echo "✅ Environment is up to date"; \ - fi - -ensure-sync: ## Ensure environment is synced - @$(MAKE) sync-check || (echo "Syncing environment..." && uv sync) - -dev-shell: ensure-sync ## Activate development shell - @echo "Activating virtual environment..." - @echo "You can leave the dev shell by typing 'exit'" - @bash -c "source .venv/bin/activate && exec bash" - -format: ensure-sync ## Format code with black and isort - @echo "Formatting code..." - @uv run black . - @uv run isort . - @echo "✅ Formatting complete" - -lint: ensure-sync ## Lint code with ruff - @echo "Linting code..." - @uv run ruff check --fix . - @echo "✅ Linting complete" - -type-check: ensure-sync ## Type check with mypy - @echo "Type checking..." - @uv run mypy . - @echo "✅ Type checking complete" - -test: ensure-sync ## Run tests - @echo "Running tests..." - @uv run pytest -v - @echo "✅ Tests complete" - -test-cov: ensure-sync ## Run tests with coverage - @echo "Running tests with coverage..." - @uv run pytest --cov --cov-report=html --cov-report=term - @echo "✅ Coverage report generated in htmlcov/" - -clean: ## Clean cache files and build artifacts - @echo "Cleaning cache files..." - @find . -type d -name "__pycache__" -exec rm -rf {} + 2>/dev/null || true - @find . -type f -name "*.pyc" -delete 2>/dev/null || true - @find . -type d -name ".pytest_cache" -exec rm -rf {} + 2>/dev/null || true - @find . -type d -name ".mypy_cache" -exec rm -rf {} + 2>/dev/null || true - @find . -type d -name ".ruff_cache" -exec rm -rf {} + 2>/dev/null || true - @find . -type d -name "*.egg-info" -exec rm -rf {} + 2>/dev/null || true - @rm -rf build/ dist/ htmlcov/ .coverage 2>/dev/null || true - @echo "✅ Cleanup complete" - -clean-venv: ## Remove virtual environment - @echo "Removing virtual environment..." - @rm -rf .venv - @echo "✅ Virtual environment removed" - @echo "Run 'make install' or 'uv sync' to recreate" - -install: ## Install project in editable mode - @echo "Installing project..." - @uv sync - @echo "✅ Installation complete" - -build: ensure-sync ## Build the package - @echo "Building package..." - @uv build - @echo "✅ Build complete" - -pre-commit-install: ensure-sync ## Install pre-commit hooks - @echo "Installing pre-commit hooks..." - @uv run pre-commit install - @echo "✅ Pre-commit hooks installed" - -pre-commit-run: ensure-sync ## Run pre-commit on all files - @echo "Running pre-commit on all files..." - @uv run pre-commit run --all-files - @echo "✅ Pre-commit checks complete" - -all-checks: format lint type-check test ## Run all checks (format, lint, type-check, test) - @echo "✅ All checks passed!" diff --git a/packages/container-manager/src/container_manager/core/managers/network.py b/packages/container-manager/src/container_manager/core/managers/network.py index af890e3..62287b1 100644 --- a/packages/container-manager/src/container_manager/core/managers/network.py +++ b/packages/container-manager/src/container_manager/core/managers/network.py @@ -64,9 +64,7 @@ def connect(self, network: str, container: str) -> None: pass @abstractmethod - def disconnect( - self, network: str, container: str, force: bool = False - ) -> None: + def disconnect(self, network: str, container: str, force: bool = False) -> None: """ Disconnect a container from a network. diff --git a/packages/container-manager/src/container_manager/core/types.py b/packages/container-manager/src/container_manager/core/types.py index 31f5ae2..d8752f1 100644 --- a/packages/container-manager/src/container_manager/core/types.py +++ b/packages/container-manager/src/container_manager/core/types.py @@ -37,8 +37,8 @@ class PortMapping: protocol: str = "tcp" """Protocol: 'tcp' or 'udp'""" - host_ip: str = "0.0.0.0" - """Host IP to bind to""" + host_ip: str = "127.0.0.1" + """Host IP to bind to (defaults to localhost for security)""" @dataclass diff --git a/packages/container-manager/src/container_manager/implementations/docker/container.py b/packages/container-manager/src/container_manager/implementations/docker/container.py index a84dffd..7aafedb 100644 --- a/packages/container-manager/src/container_manager/implementations/docker/container.py +++ b/packages/container-manager/src/container_manager/implementations/docker/container.py @@ -3,6 +3,7 @@ from __future__ import annotations import json +import subprocess from ...core.exceptions import ContainerNotFoundError, ContainerRuntimeError from ...core.managers import ContainerManager @@ -171,15 +172,14 @@ def run(self, config: RunConfig) -> str: except Exception as e: raise ContainerRuntimeError( - message=( - f"Failed to run container from image '{config.image}': {e}" - ), + message=(f"Failed to run container from image '{config.image}': {e}"), command=cmd, ) from e - def _run_pty(self, cmd: list[str]) -> "subprocess.CompletedProcess[bytes]": + def _run_pty(self, cmd: list[str]) -> subprocess.CompletedProcess[bytes]: """Run command via host PTY. Exposed for testing.""" from .utils import run_docker_pty + return run_docker_pty(cmd) def start(self, container: str) -> None: @@ -269,9 +269,7 @@ def inspect(self, container: str) -> ContainerInfo: # Parse port mappings ports = [] - port_bindings = cont_data.get("NetworkSettings", {}).get( - "Ports", {} - ) + port_bindings = cont_data.get("NetworkSettings", {}).get("Ports", {}) for container_port, bindings in port_bindings.items(): if bindings: for binding in bindings: @@ -281,7 +279,7 @@ def inspect(self, container: str) -> ContainerInfo: container_port=int(port), host_port=int(binding.get("HostPort", 0)), protocol=protocol, - host_ip=binding.get("HostIp", "0.0.0.0"), + host_ip=binding.get("HostIp", "127.0.0.1"), ) ) @@ -299,9 +297,7 @@ def inspect(self, container: str) -> ContainerInfo: except json.JSONDecodeError as e: raise ContainerRuntimeError( - message=( - f"Failed to parse container info for '{container}': {e}" - ), + message=(f"Failed to parse container info for '{container}': {e}"), command=cmd, ) from e except Exception as e: diff --git a/packages/container-manager/src/container_manager/implementations/docker/image.py b/packages/container-manager/src/container_manager/implementations/docker/image.py index b62d6d3..9a7e11c 100644 --- a/packages/container-manager/src/container_manager/implementations/docker/image.py +++ b/packages/container-manager/src/container_manager/implementations/docker/image.py @@ -107,9 +107,7 @@ def build( cmd.append("--rm=false") try: - result = run_docker_command( - cmd, timeout=timeout, input_data=input_data - ) + result = run_docker_command(cmd, timeout=timeout, input_data=input_data) output = result.stdout.decode("utf-8") # Extract image ID @@ -249,9 +247,7 @@ def list(self, filters: dict[str, str] | None = None) -> list[ImageInfo]: ImageInfo( id=data.get("ID", ""), tags=[ - data.get("Repository", "") - + ":" - + data.get("Tag", "") + data.get("Repository", "") + ":" + data.get("Tag", "") ], size=self._parse_size(data.get("Size", "0B")), created=data.get("CreatedAt"), diff --git a/packages/container-manager/src/container_manager/implementations/docker/network.py b/packages/container-manager/src/container_manager/implementations/docker/network.py index 8b645df..beeb687 100644 --- a/packages/container-manager/src/container_manager/implementations/docker/network.py +++ b/packages/container-manager/src/container_manager/implementations/docker/network.py @@ -76,9 +76,7 @@ def connect(self, network: str, container: str) -> None: command=cmd, ) from e - def disconnect( - self, network: str, container: str, force: bool = False - ) -> None: + def disconnect(self, network: str, container: str, force: bool = False) -> None: """Disconnect a container from a Docker network.""" cmd = [self.command, "network", "disconnect", network, container] if force: diff --git a/packages/container-manager/src/container_manager/implementations/docker/utils.py b/packages/container-manager/src/container_manager/implementations/docker/utils.py index a65e705..59f9a4b 100644 --- a/packages/container-manager/src/container_manager/implementations/docker/utils.py +++ b/packages/container-manager/src/container_manager/implementations/docker/utils.py @@ -31,6 +31,13 @@ def run_docker_command( Raises: ContainerError: If command fails """ + # Security: validate command starts with docker + if not command or command[0] != "docker": + raise ContainerError( + message="run_docker_command only accepts docker commands", + command=command, + ) + try: if stream: # Stream output directly to terminal without capturing @@ -102,10 +109,17 @@ def run_docker_pty(command: list[str]) -> "subprocess.CompletedProcess[bytes]": import select import sys + # Security: validate command starts with docker + if not command or command[0] != "docker": + raise ContainerError( + message="run_docker_pty only accepts docker commands", + command=command, + ) + master_fd, slave_fd = pty.openpty() proc = None try: - proc = subprocess.Popen( # nosec: B603 + proc = subprocess.Popen( command, stdin=slave_fd, stdout=slave_fd, diff --git a/packages/container-manager/tests/test_docker_container_manager.py b/packages/container-manager/tests/test_docker_container_manager.py index 5863f15..7639564 100644 --- a/packages/container-manager/tests/test_docker_container_manager.py +++ b/packages/container-manager/tests/test_docker_container_manager.py @@ -86,9 +86,7 @@ def test_run_with_ports(self, mock_docker_command): # Verify ports are in command call_args = mock_run.call_args[0][0] - assert any( - arg.startswith(("-p", "--publish")) for arg in call_args - ) + assert any(arg.startswith(("-p", "--publish")) for arg in call_args) def test_run_with_environment(self, mock_docker_command): """Test running container with environment variables.""" @@ -441,9 +439,7 @@ def test_inspect_success(self, mock_docker_command): with patch( "container_manager.implementations.docker.container.run_docker_command", - return_value=mock_docker_command( - stdout=json.dumps(inspect_data).encode() - ), + return_value=mock_docker_command(stdout=json.dumps(inspect_data).encode()), ): manager = DockerContainerManager() info = manager.inspect("test-container") @@ -493,9 +489,7 @@ def test_list_success(self, mock_docker_command): with patch( "container_manager.implementations.docker.container.run_docker_command", - return_value=mock_docker_command( - stdout=containers_output.encode() - ), + return_value=mock_docker_command(stdout=containers_output.encode()), ): manager = DockerContainerManager() containers = manager.list() @@ -559,9 +553,7 @@ def test_logs_with_tail(self, mock_docker_command): with patch( "container_manager.implementations.docker.container.run_docker_command" ) as mock_run: - mock_run.return_value = mock_docker_command( - stdout=b"Last 10 lines" - ) + mock_run.return_value = mock_docker_command(stdout=b"Last 10 lines") manager = DockerContainerManager() manager.logs("test-container", tail=10) @@ -572,7 +564,11 @@ def test_logs_with_tail(self, mock_docker_command): assert "10" in call_args def test_logs_not_found(self): - """Test getting logs for non-existent container raises ContainerNotFoundError.""" + """ + Test getting logs for non-existent container. + + Raises ContainerNotFoundError. + """ with patch( "container_manager.implementations.docker.container.run_docker_command", side_effect=Exception("No such container: test-container"), @@ -588,9 +584,7 @@ def test_exec_success(self, mock_docker_command): return_value=mock_docker_command(stdout=b"Command output"), ): manager = DockerContainerManager() - exit_code, output = manager.exec( - "test-container", ["echo", "hello"] - ) + exit_code, output = manager.exec("test-container", ["echo", "hello"]) assert exit_code == 0 assert output == "Command output" @@ -609,9 +603,7 @@ def test_prune_success(self, mock_docker_command): """Test pruning stopped containers.""" with patch( "container_manager.implementations.docker.container.run_docker_command", - return_value=mock_docker_command( - stdout=b"Total reclaimed space: 1GB" - ), + return_value=mock_docker_command(stdout=b"Total reclaimed space: 1GB"), ): manager = DockerContainerManager() result = manager.prune() @@ -712,9 +704,7 @@ def test_inspect_calls_correct_command(self): with patch( "container_manager.implementations.docker.container.run_docker_command" ) as mock_run: - mock_run.return_value = MagicMock( - stdout=json.dumps(inspect_data).encode() - ) + mock_run.return_value = MagicMock(stdout=json.dumps(inspect_data).encode()) manager = DockerContainerManager() manager.inspect("test-container") diff --git a/packages/container-manager/tests/test_docker_container_manager_auto_tty.py b/packages/container-manager/tests/test_docker_container_manager_auto_tty.py index 4aa8a20..033b331 100644 --- a/packages/container-manager/tests/test_docker_container_manager_auto_tty.py +++ b/packages/container-manager/tests/test_docker_container_manager_auto_tty.py @@ -2,8 +2,6 @@ from unittest.mock import patch -import pytest - from container_manager.core.types import RunConfig diff --git a/packages/container-manager/tests/test_docker_container_manager_runtime_flags.py b/packages/container-manager/tests/test_docker_container_manager_runtime_flags.py index debfd60..9284f44 100644 --- a/packages/container-manager/tests/test_docker_container_manager_runtime_flags.py +++ b/packages/container-manager/tests/test_docker_container_manager_runtime_flags.py @@ -1,13 +1,18 @@ -from unittest.mock import patch, MagicMock -import pytest +from unittest.mock import MagicMock, patch -from container_manager.core.types import RunConfig, VolumeMount +from container_manager.core.types import RunConfig from container_manager.implementations.docker.container import DockerContainerManager def _run_config(**kwargs): - defaults = dict(image="alpine:latest", detach=False, remove=True, - restart_policy=None, network=None, log_driver=None) + defaults = { + "image": "alpine:latest", + "detach": False, + "remove": True, + "restart_policy": None, + "network": None, + "log_driver": None, + } defaults.update(kwargs) return RunConfig(**defaults) @@ -29,10 +34,14 @@ def fake_subprocess_run(cmd, **kwargs): captured["cmd"] = cmd return MagicMock(returncode=0, stdout=b"", stderr=b"") - with patch.object(manager, "_run_pty", side_effect=fake_run_pty): - with patch("container_manager.implementations.docker.utils.subprocess.run", - side_effect=fake_subprocess_run): - manager.run(run_config) + with ( + patch.object(manager, "_run_pty", side_effect=fake_run_pty), + patch( + "container_manager.implementations.docker.utils.subprocess.run", + side_effect=fake_subprocess_run, + ), + ): + manager.run(run_config) return captured["cmd"] @@ -42,16 +51,19 @@ def test_tty_flag_added_when_tty_true(): cmd = _captured_cmd(cfg) assert "-t" in cmd + def test_tty_flag_not_added_when_tty_false(): cfg = _run_config(tty=False) cmd = _captured_cmd(cfg) assert "-t" not in cmd + def test_stdin_open_flag_added(): cfg = _run_config(stdin_open=True) cmd = _captured_cmd(cfg) assert "-i" in cmd + def test_runtime_flags_appended_before_image(): cfg = _run_config(runtime_flags=["--userns=keep-id"]) cmd = _captured_cmd(cfg) @@ -59,6 +71,7 @@ def test_runtime_flags_appended_before_image(): assert "--userns=keep-id" in cmd assert cmd.index("--userns=keep-id") < img_idx + def test_multiple_runtime_flags(): cfg = _run_config(runtime_flags=["--flag-a", "--flag-b=val"]) cmd = _captured_cmd(cfg) diff --git a/packages/container-manager/tests/test_docker_container_manager_stream_tty_interaction.py b/packages/container-manager/tests/test_docker_container_manager_stream_tty_interaction.py index 192ad51..f8b3978 100644 --- a/packages/container-manager/tests/test_docker_container_manager_stream_tty_interaction.py +++ b/packages/container-manager/tests/test_docker_container_manager_stream_tty_interaction.py @@ -1,5 +1,4 @@ -from unittest.mock import patch, MagicMock -import pytest +from unittest.mock import MagicMock, patch from container_manager.core.types import RunConfig from container_manager.implementations.docker.container import DockerContainerManager @@ -7,9 +6,16 @@ def test_effective_tty_takes_precedence_over_stream_output(): """When effective_tty=True, PTY path is used regardless of stream_output.""" - cfg = RunConfig(image="alpine:latest", tty=True, stream_output=True, - detach=False, remove=True, - restart_policy=None, network=None, log_driver=None) + cfg = RunConfig( + image="alpine:latest", + tty=True, + stream_output=True, + detach=False, + remove=True, + restart_policy=None, + network=None, + log_driver=None, + ) manager = DockerContainerManager("docker") with patch.object(manager, "_run_pty") as mock_pty: mock_pty.return_value = MagicMock(returncode=0) @@ -19,13 +25,22 @@ def test_effective_tty_takes_precedence_over_stream_output(): def test_stream_output_without_tty_uses_pipe_stream(): """stream_output=True with tty=False uses pass-through (no PTY).""" - cfg = RunConfig(image="alpine:latest", tty=False, stream_output=True, - detach=False, remove=True, - restart_policy=None, network=None, log_driver=None) + cfg = RunConfig( + image="alpine:latest", + tty=False, + stream_output=True, + detach=False, + remove=True, + restart_policy=None, + network=None, + log_driver=None, + ) manager = DockerContainerManager("docker") with patch.object(manager, "_run_pty") as mock_pty: mock_pty.return_value = MagicMock(returncode=0) - with patch("container_manager.implementations.docker.utils.subprocess.run") as mock_run: + with patch( + "container_manager.implementations.docker.utils.subprocess.run" + ) as mock_run: mock_run.return_value = MagicMock(returncode=0, stdout=b"", stderr=b"") manager.run(cfg) assert not mock_pty.called diff --git a/packages/container-manager/tests/test_docker_container_manager_tty.py b/packages/container-manager/tests/test_docker_container_manager_tty.py index 48616bc..97007ef 100644 --- a/packages/container-manager/tests/test_docker_container_manager_tty.py +++ b/packages/container-manager/tests/test_docker_container_manager_tty.py @@ -1,8 +1,6 @@ # tests/test_docker_container_manager_tty.py from dataclasses import fields -import pytest - from container_manager.core.types import RunConfig @@ -10,30 +8,37 @@ def test_runconfig_has_tty_field(): field_names = {f.name for f in fields(RunConfig)} assert "tty" in field_names + def test_runconfig_has_auto_tty_field(): field_names = {f.name for f in fields(RunConfig)} assert "auto_tty" in field_names + def test_runconfig_has_stdin_open_field(): field_names = {f.name for f in fields(RunConfig)} assert "stdin_open" in field_names + def test_runconfig_has_runtime_flags_field(): field_names = {f.name for f in fields(RunConfig)} assert "runtime_flags" in field_names + def test_runconfig_tty_defaults_false(): config = RunConfig(image="alpine:latest") assert config.tty is False + def test_runconfig_stdin_open_defaults_false(): config = RunConfig(image="alpine:latest") assert config.stdin_open is False + def test_runconfig_auto_tty_defaults_false(): config = RunConfig(image="alpine:latest") assert config.auto_tty is False + def test_runconfig_runtime_flags_defaults_empty(): config = RunConfig(image="alpine:latest") assert config.runtime_flags == [] diff --git a/packages/container-manager/tests/test_docker_engine.py b/packages/container-manager/tests/test_docker_engine.py index 627ad8e..e72a87f 100644 --- a/packages/container-manager/tests/test_docker_engine.py +++ b/packages/container-manager/tests/test_docker_engine.py @@ -116,9 +116,7 @@ def test_info_success(self, mock_docker_command): } with patch( "container_manager.implementations.docker.engine.run_docker_command", - return_value=mock_docker_command( - stdout=json.dumps(info_data).encode() - ), + return_value=mock_docker_command(stdout=json.dumps(info_data).encode()), ): engine = DockerEngine() info = engine.info() @@ -197,9 +195,7 @@ def test_info_calls_correct_command(self): with patch( "container_manager.implementations.docker.engine.run_docker_command" ) as mock_run: - mock_run.return_value = MagicMock( - stdout=b'{"ServerVersion": "24.0.7"}' - ) + mock_run.return_value = MagicMock(stdout=b'{"ServerVersion": "24.0.7"}') engine = DockerEngine() engine.info() diff --git a/packages/container-manager/tests/test_docker_image_manager.py b/packages/container-manager/tests/test_docker_image_manager.py index 59efbc3..b67e4c5 100644 --- a/packages/container-manager/tests/test_docker_image_manager.py +++ b/packages/container-manager/tests/test_docker_image_manager.py @@ -170,9 +170,7 @@ def test_pull_success(self, mock_docker_command): with patch( "container_manager.implementations.docker.image.run_docker_command" ) as mock_run: - mock_run.return_value = mock_docker_command( - stdout=b"sha256:abc123" - ) + mock_run.return_value = mock_docker_command(stdout=b"sha256:abc123") manager = DockerImageManager() image_id = manager.pull("alpine:latest") @@ -198,9 +196,7 @@ def test_push_success(self, mock_docker_command): """Test pushing an image successfully.""" with patch( "container_manager.implementations.docker.image.run_docker_command", - return_value=mock_docker_command( - stdout=b"The push refers to repository" - ), + return_value=mock_docker_command(stdout=b"The push refers to repository"), ): manager = DockerImageManager() # Should not raise @@ -306,9 +302,7 @@ def test_inspect_success(self, mock_docker_command): with patch( "container_manager.implementations.docker.image.run_docker_command", - return_value=mock_docker_command( - stdout=json.dumps(inspect_data).encode() - ), + return_value=mock_docker_command(stdout=json.dumps(inspect_data).encode()), ): manager = DockerImageManager() info = manager.inspect("alpine:latest") @@ -396,9 +390,7 @@ def test_prune_success(self, mock_docker_command): """Test pruning unused images.""" with patch( "container_manager.implementations.docker.image.run_docker_command", - return_value=mock_docker_command( - stdout=b"Total reclaimed space: 1.5GB" - ), + return_value=mock_docker_command(stdout=b"Total reclaimed space: 1.5GB"), ): manager = DockerImageManager() result = manager.prune() @@ -440,9 +432,7 @@ def test_build_calls_correct_command(self, sample_build_context): with patch( "container_manager.implementations.docker.image.run_docker_command" ) as mock_run: - mock_run.return_value = MagicMock( - stdout=b"Successfully built abc123" - ) + mock_run.return_value = MagicMock(stdout=b"Successfully built abc123") manager = DockerImageManager() manager.build(sample_build_context, "test:latest") @@ -526,9 +516,7 @@ def test_inspect_calls_correct_command(self): with patch( "container_manager.implementations.docker.image.run_docker_command" ) as mock_run: - mock_run.return_value = MagicMock( - stdout=json.dumps(inspect_data).encode() - ) + mock_run.return_value = MagicMock(stdout=json.dumps(inspect_data).encode()) manager = DockerImageManager() manager.inspect("alpine:latest") @@ -554,9 +542,7 @@ def test_prune_calls_correct_command(self): with patch( "container_manager.implementations.docker.image.run_docker_command" ) as mock_run: - mock_run.return_value = MagicMock( - stdout=b"Total reclaimed space: 0B" - ) + mock_run.return_value = MagicMock(stdout=b"Total reclaimed space: 0B") manager = DockerImageManager() manager.prune() diff --git a/packages/container-manager/tests/test_docker_network_manager.py b/packages/container-manager/tests/test_docker_network_manager.py index 5dd4591..d456208 100644 --- a/packages/container-manager/tests/test_docker_network_manager.py +++ b/packages/container-manager/tests/test_docker_network_manager.py @@ -186,9 +186,7 @@ def test_inspect_success(self, mock_docker_command): with patch( "container_manager.implementations.docker.network.run_docker_command", - return_value=mock_docker_command( - stdout=json.dumps(inspect_data).encode() - ), + return_value=mock_docker_command(stdout=json.dumps(inspect_data).encode()), ): manager = DockerNetworkManager() info = manager.inspect("my-network") @@ -288,9 +286,7 @@ def test_create_calls_correct_command(self): with patch( "container_manager.implementations.docker.network.run_docker_command" ) as mock_run: - mock_run.return_value = type( - "obj", (object,), {"stdout": b"abc123"} - )() + mock_run.return_value = type("obj", (object,), {"stdout": b"abc123"})() manager = DockerNetworkManager() manager.create("my-network") diff --git a/packages/container-manager/tests/test_docker_volume_manager.py b/packages/container-manager/tests/test_docker_volume_manager.py index f320035..edad040 100644 --- a/packages/container-manager/tests/test_docker_volume_manager.py +++ b/packages/container-manager/tests/test_docker_volume_manager.py @@ -145,9 +145,7 @@ def test_inspect_success(self, mock_docker_command): with patch( "container_manager.implementations.docker.volume.run_docker_command", - return_value=mock_docker_command( - stdout=json.dumps(inspect_data).encode() - ), + return_value=mock_docker_command(stdout=json.dumps(inspect_data).encode()), ): manager = DockerVolumeManager() info = manager.inspect("my-volume") @@ -230,9 +228,7 @@ def test_prune_success(self, mock_docker_command): """Test pruning unused volumes.""" with patch( "container_manager.implementations.docker.volume.run_docker_command", - return_value=mock_docker_command( - stdout=b"Total reclaimed space: 1.5GB" - ), + return_value=mock_docker_command(stdout=b"Total reclaimed space: 1.5GB"), ): manager = DockerVolumeManager() result = manager.prune() @@ -245,9 +241,7 @@ def test_create_calls_correct_command(self): with patch( "container_manager.implementations.docker.volume.run_docker_command" ) as mock_run: - mock_run.return_value = type( - "obj", (object,), {"stdout": b"my-volume"} - )() + mock_run.return_value = type("obj", (object,), {"stdout": b"my-volume"})() manager = DockerVolumeManager() manager.create("my-volume") diff --git a/packages/container-manager/tests/test_dockerfile_path.py b/packages/container-manager/tests/test_dockerfile_path.py index a20b266..9ffa694 100644 --- a/packages/container-manager/tests/test_dockerfile_path.py +++ b/packages/container-manager/tests/test_dockerfile_path.py @@ -1,7 +1,6 @@ """Tests for Dockerfile path support.""" -from pathlib import Path -from unittest.mock import MagicMock, patch +from unittest.mock import patch import pytest @@ -29,15 +28,15 @@ def test_build_with_path(image_manager, mock_run_command, tmp_path): """Test building with Dockerfile path.""" dockerfile = tmp_path / "Dockerfile" dockerfile.write_text("FROM alpine") - + context = BuildContext(dockerfile=dockerfile) - + image_manager.build(context, "test-image") - + # Verify command args, kwargs = mock_run_command.call_args cmd = args[0] - + assert "-f" in cmd assert str(dockerfile) in cmd assert str(dockerfile.parent) in cmd # Context should be parent dir @@ -49,20 +48,17 @@ def test_build_with_path_and_context(image_manager, mock_run_command, tmp_path): dockerfile = tmp_path / "other" / "Dockerfile" dockerfile.parent.mkdir() dockerfile.write_text("FROM alpine") - + context_dir = tmp_path / "context" context_dir.mkdir() - - context = BuildContext( - dockerfile=dockerfile, - context_path=context_dir - ) - + + context = BuildContext(dockerfile=dockerfile, context_path=context_dir) + image_manager.build(context, "test-image") - + args, kwargs = mock_run_command.call_args cmd = args[0] - + assert str(dockerfile) in cmd assert str(context_dir) in cmd assert kwargs["input_data"] is None @@ -72,22 +68,19 @@ def test_build_with_string_and_context(image_manager, mock_run_command, tmp_path """Test building with string content and explicit context.""" context_dir = tmp_path / "context" context_dir.mkdir() - - context = BuildContext( - dockerfile="FROM alpine", - context_path=context_dir - ) - + + context = BuildContext(dockerfile="FROM alpine", context_path=context_dir) + image_manager.build(context, "test-image") - + args, kwargs = mock_run_command.call_args cmd = args[0] - + # Should have written Dockerfile to context dir expected_dockerfile = context_dir / "Dockerfile" assert expected_dockerfile.exists() assert expected_dockerfile.read_text() == "FROM alpine" - + assert "-f" in cmd assert str(expected_dockerfile) in cmd assert str(context_dir) in cmd @@ -97,12 +90,12 @@ def test_build_with_string_and_context(image_manager, mock_run_command, tmp_path def test_build_legacy_string(image_manager, mock_run_command): """Test legacy build with string content (tarball).""" context = BuildContext(dockerfile="FROM alpine") - + image_manager.build(context, "test-image") - + args, kwargs = mock_run_command.call_args cmd = args[0] - + assert "-" in cmd # Stdin assert kwargs["input_data"] is not None # Tarball data provided @@ -111,14 +104,11 @@ def test_build_with_extra_files_path_mode(image_manager, mock_run_command, tmp_p """Test writing extra files in path mode.""" dockerfile = tmp_path / "Dockerfile" dockerfile.write_text("FROM alpine") - - context = BuildContext( - dockerfile=dockerfile, - files={"extra.txt": b"content"} - ) - + + context = BuildContext(dockerfile=dockerfile, files={"extra.txt": b"content"}) + image_manager.build(context, "test-image") - + # Should have written extra file to parent dir extra_file = dockerfile.parent / "extra.txt" assert extra_file.exists() diff --git a/packages/container-manager/tests/test_enums.py b/packages/container-manager/tests/test_enums.py index f0f4fe0..1ea3ded 100644 --- a/packages/container-manager/tests/test_enums.py +++ b/packages/container-manager/tests/test_enums.py @@ -1,6 +1,5 @@ """Tests for container management enums.""" - from container_manager.core import ( ContainerRuntime, ContainerState, diff --git a/packages/container-manager/tests/test_exceptions.py b/packages/container-manager/tests/test_exceptions.py index 3377ef5..30e3277 100644 --- a/packages/container-manager/tests/test_exceptions.py +++ b/packages/container-manager/tests/test_exceptions.py @@ -1,6 +1,5 @@ """Tests for container management exceptions.""" - from container_manager.core import ( ContainerError, ContainerNotFoundError, @@ -31,9 +30,7 @@ def test_create_basic(self): def test_create_with_command(self): """Test creating container error with command.""" - error = ContainerError( - "Command failed", command=["docker", "run", "alpine"] - ) + error = ContainerError("Command failed", command=["docker", "run", "alpine"]) assert error.command == ["docker", "run", "alpine"] assert "Command: docker run alpine" in str(error) diff --git a/packages/container-manager/tests/test_factory.py b/packages/container-manager/tests/test_factory.py index 5574dfc..4f0606b 100644 --- a/packages/container-manager/tests/test_factory.py +++ b/packages/container-manager/tests/test_factory.py @@ -80,19 +80,20 @@ def test_create_docker_success(self, mock_docker_command): def test_create_docker_custom_command(self, mock_docker_command): """Test create_docker() with custom command.""" with patch.object(DockerEngine, "ensure_available"): - engine = ContainerEngineFactory.create_docker( - command="custom-docker" - ) + engine = ContainerEngineFactory.create_docker(command="custom-docker") assert engine.command == "custom-docker" def test_create_docker_not_available_raises(self): """Test create_docker() raises when Docker not available.""" - with patch.object( - DockerEngine, - "ensure_available", - side_effect=RuntimeNotAvailableError("docker"), - ), pytest.raises(RuntimeNotAvailableError): + with ( + patch.object( + DockerEngine, + "ensure_available", + side_effect=RuntimeNotAvailableError("docker"), + ), + pytest.raises(RuntimeNotAvailableError), + ): ContainerEngineFactory.create_docker() def test_create_docker_calls_ensure_available(self): @@ -114,19 +115,20 @@ def test_create_podman_success(self, mock_docker_command): def test_create_podman_custom_command(self, mock_docker_command): """Test create_podman() with custom command.""" with patch.object(DockerEngine, "ensure_available"): - engine = ContainerEngineFactory.create_podman( - command="custom-podman" - ) + engine = ContainerEngineFactory.create_podman(command="custom-podman") assert engine.command == "custom-podman" def test_create_podman_not_available_raises(self): """Test create_podman() raises when Podman not available.""" - with patch.object( - DockerEngine, - "ensure_available", - side_effect=RuntimeNotAvailableError("podman"), - ), pytest.raises(RuntimeNotAvailableError): + with ( + patch.object( + DockerEngine, + "ensure_available", + side_effect=RuntimeNotAvailableError("podman"), + ), + pytest.raises(RuntimeNotAvailableError), + ): ContainerEngineFactory.create_podman() def test_create_podman_calls_ensure_available(self): @@ -171,9 +173,11 @@ def mock_is_available(self): call_order.append("is_available") return True - with patch.object(DockerEngine, "__init__", mock_init): - with patch.object(DockerEngine, "is_available", mock_is_available): - ContainerEngineFactory.create(ContainerRuntime.DOCKER) + with ( + patch.object(DockerEngine, "__init__", mock_init), + patch.object(DockerEngine, "is_available", mock_is_available), + ): + ContainerEngineFactory.create(ContainerRuntime.DOCKER) assert call_order == ["init", "is_available"] @@ -208,12 +212,8 @@ def test_factory_creates_independent_instances(self): def test_create_with_both_runtimes(self): """Test creating engines for both Docker and Podman.""" with patch.object(DockerEngine, "is_available", return_value=True): - docker_engine = ContainerEngineFactory.create( - ContainerRuntime.DOCKER - ) - podman_engine = ContainerEngineFactory.create( - ContainerRuntime.PODMAN - ) + docker_engine = ContainerEngineFactory.create(ContainerRuntime.DOCKER) + podman_engine = ContainerEngineFactory.create(ContainerRuntime.PODMAN) assert docker_engine.command == "docker" assert podman_engine.command == "podman" diff --git a/packages/container-manager/tests/test_pty_integration.py b/packages/container-manager/tests/test_pty_integration.py index 73872dd..e702fd9 100644 --- a/packages/container-manager/tests/test_pty_integration.py +++ b/packages/container-manager/tests/test_pty_integration.py @@ -13,7 +13,7 @@ import io import subprocess import sys -from unittest.mock import MagicMock, patch +from unittest.mock import patch import pytest @@ -23,7 +23,6 @@ ) from container_manager.implementations.docker.utils import run_docker_pty - pytestmark = [pytest.mark.docker, pytest.mark.slow] @@ -104,9 +103,9 @@ def test_pty_stdout_isatty_is_true_inside_container() -> None: ] output = _capture_pty_stdout(cmd) text = output.decode("utf-8", errors="replace") - assert "PTY" in text, ( - f"Expected 'PTY' in output when container ran with host PTY, got: {text!r}" - ) + assert ( + "PTY" in text + ), f"Expected 'PTY' in output when container ran with host PTY, got: {text!r}" def test_no_pty_stdout_isatty_is_false_inside_container() -> None: @@ -129,12 +128,12 @@ def test_no_pty_stdout_isatty_is_false_inside_container() -> None: timeout=30, ) text = result.stdout.decode("utf-8", errors="replace") - assert "PLAIN" in text, ( - f"Expected 'PLAIN' in output when container ran without PTY, got: {text!r}" - ) - assert "PTY" not in text, ( - f"Expected no 'PTY' in output when container ran without PTY, got: {text!r}" - ) + assert ( + "PLAIN" in text + ), f"Expected 'PLAIN' in output when container ran without PTY, got: {text!r}" + assert ( + "PTY" not in text + ), f"Expected no 'PTY' in output when container ran without PTY, got: {text!r}" def test_pty_output_contains_ansi_codes() -> None: @@ -155,9 +154,9 @@ def test_pty_output_contains_ansi_codes() -> None: ] output = _capture_pty_stdout(cmd) # PTY may encode ESC as 0x1b; accept either textual representation - assert b"\x1b[32m" in output or b"\033[32m" in output, ( - f"Expected ANSI green escape in PTY output, got: {output!r}" - ) + assert ( + b"\x1b[32m" in output or b"\033[32m" in output + ), f"Expected ANSI green escape in PTY output, got: {output!r}" def test_no_pty_output_lacks_ansi_codes() -> None: @@ -185,9 +184,9 @@ def test_no_pty_output_lacks_ansi_codes() -> None: ) text = result.stdout.decode("utf-8", errors="replace") # ls without a TTY should not emit colour codes - assert "\x1b[" not in text and "\033[" not in text, ( - f"Expected no ANSI codes in non-PTY ls output, got: {text!r}" - ) + assert ( + "\x1b[" not in text and "\033[" not in text + ), f"Expected no ANSI codes in non-PTY ls output, got: {text!r}" def test_run_with_tty_true_dispatches_to_run_pty() -> None: diff --git a/packages/container-manager/tests/test_types.py b/packages/container-manager/tests/test_types.py index 74575a6..42a66e2 100644 --- a/packages/container-manager/tests/test_types.py +++ b/packages/container-manager/tests/test_types.py @@ -36,16 +36,12 @@ def test_create_with_path(self): def test_create_read_only(self): """Test creating read-only volume mount.""" - mount = VolumeMount( - source="/host", target="/container", read_only=True - ) + mount = VolumeMount(source="/host", target="/container", read_only=True) assert mount.read_only is True def test_create_volume_type(self): """Test creating volume mount with volume type.""" - mount = VolumeMount( - source="my-volume", target="/container", type="volume" - ) + mount = VolumeMount(source="my-volume", target="/container", type="volume") assert mount.type == "volume" def test_create_tmpfs_type(self): @@ -63,7 +59,7 @@ def test_create_basic(self): assert port.container_port == 8080 assert port.host_port is None assert port.protocol == "tcp" - assert port.host_ip == "0.0.0.0" + assert port.host_ip == "127.0.0.1" # Secure default (localhost only) def test_create_with_host_port(self): """Test creating port mapping with host port.""" @@ -201,9 +197,7 @@ def test_create_with_network_mode(self): def test_create_with_restart_policy(self): """Test creating run config with restart policy.""" - config = RunConfig( - image="alpine:latest", restart_policy=RestartPolicy.ALWAYS - ) + config = RunConfig(image="alpine:latest", restart_policy=RestartPolicy.ALWAYS) assert config.restart_policy == RestartPolicy.ALWAYS def test_create_detach_false(self): diff --git a/packages/container-manager/verify_real_builds.py b/packages/container-manager/verify_real_builds.py index 2c39b4c..6eaff99 100644 --- a/packages/container-manager/verify_real_builds.py +++ b/packages/container-manager/verify_real_builds.py @@ -4,29 +4,30 @@ Tests real builds with Docker and Podman (if available). """ -import sys -from pathlib import Path import shutil import tempfile -from container_manager import ContainerEngineFactory, BuildContext, ContainerRuntime +from pathlib import Path + +from container_manager import BuildContext, ContainerEngineFactory + def verify_engine(runtime_name: str, command: str): print(f"\n=== Verifying {runtime_name} ({command}) ===") - + try: if runtime_name == "Docker": engine = ContainerEngineFactory.create_docker(command) else: engine = ContainerEngineFactory.create_podman(command) - + if not engine.is_available(): print(f"❌ {runtime_name} is not available (command: {command})") return print(f"✅ {runtime_name} is available: {engine.version()}") - + # Test 1: String Build - print(f"\n[Test 1] Building from String Content...") + print("\n[Test 1] Building from String Content...") context_str = BuildContext( dockerfile="FROM alpine:latest\nRUN echo 'String Build' > /test.txt" ) @@ -38,19 +39,19 @@ def verify_engine(runtime_name: str, command: str): print(f"❌ Failed: {e}") # Test 2: File Path Build - print(f"\n[Test 2] Building from File Path...") + print("\n[Test 2] Building from File Path...") with tempfile.TemporaryDirectory() as tmpdir: tmp_path = Path(tmpdir) - + # Create Dockerfile/Containerfile filename = "Containerfile" if runtime_name == "Podman" else "Dockerfile" dockerfile = tmp_path / filename - dockerfile.write_text("FROM alpine:latest\nRUN echo 'File Build' > /test.txt") - - context_path = BuildContext( - dockerfile=dockerfile + dockerfile.write_text( + "FROM alpine:latest\nRUN echo 'File Build' > /test.txt" ) - + + context_path = BuildContext(dockerfile=dockerfile) + try: img_id = engine.images.build(context_path, f"test-{command}-file") print(f"✅ Built image: {img_id}") @@ -61,10 +62,11 @@ def verify_engine(runtime_name: str, command: str): except Exception as e: print(f"❌ Error initializing engine: {e}") + def main(): # Verify Docker verify_engine("Docker", "docker") - + # Verify Podman # Check if podman is installed first to avoid noise if shutil.which("podman"): @@ -72,5 +74,6 @@ def main(): else: print("\n=== Podman not found, skipping ===") + if __name__ == "__main__": main() diff --git a/packages/daemon/Makefile b/packages/daemon/Makefile deleted file mode 100755 index 7037b97..0000000 --- a/packages/daemon/Makefile +++ /dev/null @@ -1,18 +0,0 @@ -.PHONY: install test clean clean-venv - -install: - uv venv .venv - uv pip install -e ".[dev]" - -test: - .venv/bin/pytest tests/ -v - -clean: - rm -rf build dist *.egg-info - find . -type d -name __pycache__ -exec rm -rf {} + - find . -type f -name "*.pyc" -delete - -clean-venv: - rm -rf .venv - $(MAKE) install - diff --git a/packages/daemon/examples/event_demo.py b/packages/daemon/examples/event_demo.py index 9e920ad..ce88e74 100755 --- a/packages/daemon/examples/event_demo.py +++ b/packages/daemon/examples/event_demo.py @@ -1,5 +1,6 @@ #!/usr/bin/env python3 """Better demo with proper timing.""" + import asyncio import json from datetime import datetime @@ -10,44 +11,44 @@ async def monitor(): """Monitor wallpaper events.""" socket_path = Path.home() / ".cache/dotfiles/sockets/wallpaper_events.sock" - + # Wait for socket for _ in range(20): if socket_path.exists(): break await asyncio.sleep(0.1) - + reader, writer = await asyncio.open_unix_connection(socket_path) print("✅ Monitor: Connected and listening...\n") - + while True: length_bytes = await reader.read(4) if not length_bytes: break - + message_length = int.from_bytes(length_bytes, "big") data = await reader.read(message_length) msg = json.loads(data.decode("utf-8")) - + payload_type = msg["payload"].get("type") - + if payload_type == "operation_started": - print(f"🚀 Operation Started:") + print("🚀 Operation Started:") print(f" Wallpaper: {msg['payload']['wallpaper_path']}") print(f" Monitor: {msg['payload']['monitor']}\n") - + elif payload_type == "operation_progress": - progress = msg['payload']['overall_progress'] - step = msg['payload']['step_id'] + progress = msg["payload"]["overall_progress"] + step = msg["payload"]["step_id"] bar_length = 30 filled = int(bar_length * progress / 100) bar = "█" * filled + "░" * (bar_length - filled) print(f"⏳ [{bar}] {progress:5.1f}% - {step}") - + elif payload_type == "operation_completed": - print(f"\n✅ Operation Completed Successfully!\n") + print("\n✅ Operation Completed Successfully!\n") break - + elif payload_type == "operation_failed": print(f"\n❌ Operation Failed: {msg['payload']['error']}\n") break @@ -57,32 +58,34 @@ async def publisher(): """Publish test events.""" # Give monitor time to connect first await asyncio.sleep(0.5) - + command_socket = Path.home() / ".cache/dotfiles/sockets/command.sock" reader, writer = await asyncio.open_unix_connection(command_socket) - + operation_id = str(uuid4()) - + async def send(msg_dict): data = json.dumps(msg_dict).encode("utf-8") writer.write(len(data).to_bytes(4, "big")) writer.write(data) await writer.drain() - + # Operation started - await send({ - "message_id": str(uuid4()), - "timestamp": datetime.now().isoformat(), - "event_type": "wallpaper", - "payload": { - "type": "operation_started", - "operation_id": operation_id, - "wallpaper_path": "/home/user/Pictures/Wallpapers/mountain-sunset.jpg", - "monitor": "DP-1" + await send( + { + "message_id": str(uuid4()), + "timestamp": datetime.now().isoformat(), + "event_type": "wallpaper", + "payload": { + "type": "operation_started", + "operation_id": operation_id, + "wallpaper_path": "/home/user/Pictures/Wallpapers/mountain-sunset.jpg", + "monitor": "DP-1", + }, } - }) + ) await asyncio.sleep(0.5) - + # Simulate realistic progress steps = [ ("validate_wallpaper", 5.0), @@ -97,34 +100,38 @@ async def send(msg_dict): ("generate_colorscheme_yaml", 90.0), ("set_wallpaper", 100.0), ] - + for step_id, progress in steps: - await send({ + await send( + { + "message_id": str(uuid4()), + "timestamp": datetime.now().isoformat(), + "event_type": "wallpaper", + "payload": { + "type": "operation_progress", + "operation_id": operation_id, + "step_id": step_id, + "step_progress": 100.0, + "overall_progress": progress, + }, + } + ) + await asyncio.sleep(0.3) + + # Completed + await send( + { "message_id": str(uuid4()), "timestamp": datetime.now().isoformat(), "event_type": "wallpaper", "payload": { - "type": "operation_progress", + "type": "operation_completed", "operation_id": operation_id, - "step_id": step_id, - "step_progress": 100.0, - "overall_progress": progress - } - }) - await asyncio.sleep(0.3) - - # Completed - await send({ - "message_id": str(uuid4()), - "timestamp": datetime.now().isoformat(), - "event_type": "wallpaper", - "payload": { - "type": "operation_completed", - "operation_id": operation_id, - "success": True + "success": True, + }, } - }) - + ) + writer.close() await writer.wait_closed() @@ -134,9 +141,9 @@ async def main(): print("🎯 Dotfiles Event System - Live Demo") print("=" * 70) print() - + await asyncio.gather(monitor(), publisher()) - + print("=" * 70) print("✅ Demo Complete - Event system working perfectly!") print("=" * 70) diff --git a/packages/daemon/examples/monitor.py b/packages/daemon/examples/monitor.py index 14b48c9..63133cc 100755 --- a/packages/daemon/examples/monitor.py +++ b/packages/daemon/examples/monitor.py @@ -1,5 +1,6 @@ #!/usr/bin/env python3 """Real-world demo using actual WallpaperService.""" + import asyncio import json from pathlib import Path @@ -8,64 +9,64 @@ async def monitor_wallpaper_events(): """Monitor real wallpaper events from WallpaperService.""" socket_path = Path.home() / ".cache/dotfiles/sockets/wallpaper_events.sock" - + print("📡 Waiting for wallpaper events...") print(" (Run: dotfiles-manager wallpaper change in another terminal)\n") - + # Wait for socket for _ in range(60): if socket_path.exists(): break await asyncio.sleep(1) - + if not socket_path.exists(): print("❌ No wallpaper events socket found") return - + reader, writer = await asyncio.open_unix_connection(socket_path) print("✅ Connected to wallpaper events!\n") print("=" * 70) - + while True: try: length_bytes = await reader.read(4) if not length_bytes: break - + message_length = int.from_bytes(length_bytes, "big") data = await reader.read(message_length) msg = json.loads(data.decode("utf-8")) - + payload_type = msg["payload"].get("type") timestamp = msg["timestamp"] - + if payload_type == "operation_started": print(f"\n🚀 Wallpaper Change Started [{timestamp}]") print(f" Wallpaper: {msg['payload']['wallpaper_path']}") print(f" Monitor: {msg['payload'].get('monitor', 'all')}") print(f" Operation ID: {msg['payload']['operation_id']}\n") - + elif payload_type == "operation_progress": - progress = msg['payload']['overall_progress'] - step = msg['payload']['step_id'] + progress = msg["payload"]["overall_progress"] + step = msg["payload"]["step_id"] bar_length = 40 filled = int(bar_length * progress / 100) bar = "█" * filled + "░" * (bar_length - filled) print(f"⏳ [{bar}] {progress:5.1f}% - {step}") - + elif payload_type == "operation_completed": - success = msg['payload']['success'] + success = msg["payload"]["success"] if success: print(f"\n✅ Wallpaper Changed Successfully! [{timestamp}]") else: print(f"\n❌ Wallpaper Change Failed [{timestamp}]") print("=" * 70) - + elif payload_type == "operation_failed": - error = msg['payload']['error'] + error = msg["payload"]["error"] print(f"\n❌ Operation Failed: {error} [{timestamp}]") print("=" * 70) - + except Exception as e: print(f"Error: {e}") break @@ -76,7 +77,7 @@ async def monitor_wallpaper_events(): print("🎯 Real-World Event Monitor") print("=" * 70) print() - + try: asyncio.run(monitor_wallpaper_events()) except KeyboardInterrupt: diff --git a/packages/daemon/pyproject.toml b/packages/daemon/pyproject.toml index a7ab5fd..0934e89 100755 --- a/packages/daemon/pyproject.toml +++ b/packages/daemon/pyproject.toml @@ -3,13 +3,13 @@ name = "dotfiles-daemon" version = "0.1.0" description = "Persistent event daemon for dotfiles management system" readme = "README.md" -requires-python = ">=3.11" +requires-python = ">=3.12" dependencies = [ "dotfiles-event-protocol", ] [tool.uv.sources] -dotfiles-event-protocol = { path = "../event-protocol", editable = true } +dotfiles-event-protocol = { workspace = true } [project.optional-dependencies] dev = [ diff --git a/packages/daemon/src/dotfiles_daemon/commands/handlers/launch_rofi_wallpaper.py b/packages/daemon/src/dotfiles_daemon/commands/handlers/launch_rofi_wallpaper.py index 67c1af1..bcbe560 100755 --- a/packages/daemon/src/dotfiles_daemon/commands/handlers/launch_rofi_wallpaper.py +++ b/packages/daemon/src/dotfiles_daemon/commands/handlers/launch_rofi_wallpaper.py @@ -52,9 +52,7 @@ async def execute(self, args: dict[str, Any]) -> dict[str, Any]: # Validate config exists if not self._rofi_config_path.exists(): - raise FileNotFoundError( - f"Rofi config not found: {self._rofi_config_path}" - ) + raise FileNotFoundError(f"Rofi config not found: {self._rofi_config_path}") # Launch rofi as detached process process = await asyncio.create_subprocess_exec( diff --git a/packages/daemon/src/dotfiles_daemon/commands/handlers/launch_wlogout.py b/packages/daemon/src/dotfiles_daemon/commands/handlers/launch_wlogout.py index 14e3b4b..314af4a 100755 --- a/packages/daemon/src/dotfiles_daemon/commands/handlers/launch_wlogout.py +++ b/packages/daemon/src/dotfiles_daemon/commands/handlers/launch_wlogout.py @@ -50,14 +50,10 @@ async def execute(self, args: dict[str, Any]) -> dict[str, Any]: """ # Validate paths exist if not self._layout_path.exists(): - raise FileNotFoundError( - f"Wlogout layout not found: {self._layout_path}" - ) + raise FileNotFoundError(f"Wlogout layout not found: {self._layout_path}") if not self._style_path.exists(): - raise FileNotFoundError( - f"Wlogout style not found: {self._style_path}" - ) + raise FileNotFoundError(f"Wlogout style not found: {self._style_path}") # Launch wlogout as detached process # Use asyncio.create_subprocess_exec for non-blocking execution diff --git a/packages/daemon/src/dotfiles_daemon/commands/registry.py b/packages/daemon/src/dotfiles_daemon/commands/registry.py index 5f8488b..cb959d8 100755 --- a/packages/daemon/src/dotfiles_daemon/commands/registry.py +++ b/packages/daemon/src/dotfiles_daemon/commands/registry.py @@ -33,9 +33,7 @@ def register(self, handler: CommandHandler) -> None: ValueError: If handler for command type already registered """ if handler.command_type in self._handlers: - raise ValueError( - f"Handler already registered for {handler.command_type}" - ) + raise ValueError(f"Handler already registered for {handler.command_type}") self._handlers[handler.command_type] = handler self._logger.info(f"Registered handler for {handler.command_type}") diff --git a/packages/daemon/src/dotfiles_daemon/config.py b/packages/daemon/src/dotfiles_daemon/config.py index 10319ed..93e4085 100755 --- a/packages/daemon/src/dotfiles_daemon/config.py +++ b/packages/daemon/src/dotfiles_daemon/config.py @@ -11,21 +11,43 @@ class CommandHandlerConfig(BaseModel): # Wlogout paths wlogout_layout_path: Path = Field( - default=Path.home() / ".tmp" / "inumaki-dotfiles" / "dotfiles" / "wlogout" / "layout", + default=Path.home() + / ".tmp" + / "inumaki-dotfiles" + / "dotfiles" + / "wlogout" + / "layout", description="Path to wlogout layout file", ) wlogout_style_path: Path = Field( - default=Path.home() / ".tmp" / "inumaki-dotfiles" / "dotfiles" / "wlogout" / "style.css", + default=Path.home() + / ".tmp" + / "inumaki-dotfiles" + / "dotfiles" + / "wlogout" + / "style.css", description="Path to wlogout style CSS file", ) # Rofi paths rofi_wallpaper_config_path: Path = Field( - default=Path.home() / ".tmp" / "inumaki-dotfiles" / "dotfiles" / "rofi" / "wallpaper-selector.rasi", + default=Path.home() + / ".tmp" + / "inumaki-dotfiles" + / "dotfiles" + / "rofi" + / "wallpaper-selector.rasi", description="Path to rofi wallpaper selector config", ) rofi_power_menu_script_path: Path = Field( - default=Path.home() / ".tmp" / "inumaki-dotfiles" / "dotfiles" / "eww" / "status-bar" / "scripts" / "power-menu-selector.sh", + default=Path.home() + / ".tmp" + / "inumaki-dotfiles" + / "dotfiles" + / "eww" + / "status-bar" + / "scripts" + / "power-menu-selector.sh", description="Path to power menu selector script", ) diff --git a/packages/daemon/src/dotfiles_daemon/daemon.py b/packages/daemon/src/dotfiles_daemon/daemon.py index f980681..b91cace 100755 --- a/packages/daemon/src/dotfiles_daemon/daemon.py +++ b/packages/daemon/src/dotfiles_daemon/daemon.py @@ -142,9 +142,7 @@ async def run(self) -> None: # Setup signal handlers loop = asyncio.get_running_loop() for sig in (signal.SIGINT, signal.SIGTERM): - loop.add_signal_handler( - sig, lambda: asyncio.create_task(self.stop()) - ) + loop.add_signal_handler(sig, lambda: asyncio.create_task(self.stop())) await self.start() diff --git a/packages/daemon/src/dotfiles_daemon/event_broker.py b/packages/daemon/src/dotfiles_daemon/event_broker.py index 26246a8..8f14964 100755 --- a/packages/daemon/src/dotfiles_daemon/event_broker.py +++ b/packages/daemon/src/dotfiles_daemon/event_broker.py @@ -82,9 +82,7 @@ async def _create_event_server(self, event_type: str) -> None: event_type: Type of event (e.g., "wallpaper", "backup") """ socket_path = self.config.get_event_socket_path(event_type) - self.logger.info( - f"Creating event server for '{event_type}' at {socket_path}" - ) + self.logger.info(f"Creating event server for '{event_type}' at {socket_path}") # Remove existing socket file if it exists if socket_path.exists(): @@ -156,9 +154,7 @@ async def _send_to_server( clients = server_info["clients"] for writer in clients: try: - writer.write( - len(data).to_bytes(4, "big") - ) # Message length prefix + writer.write(len(data).to_bytes(4, "big")) # Message length prefix writer.write(data) await writer.drain() except Exception as e: diff --git a/packages/daemon/src/dotfiles_daemon/logger.py b/packages/daemon/src/dotfiles_daemon/logger.py index 2da3578..27a7df9 100755 --- a/packages/daemon/src/dotfiles_daemon/logger.py +++ b/packages/daemon/src/dotfiles_daemon/logger.py @@ -47,4 +47,3 @@ def error(self, message: str, *args: Any, **kwargs: Any) -> None: def critical(self, message: str, *args: Any, **kwargs: Any) -> None: """Log critical message.""" self.logger.critical(message, *args, **kwargs) - diff --git a/packages/daemon/src/dotfiles_daemon/publisher.py b/packages/daemon/src/dotfiles_daemon/publisher.py index 1681316..c968804 100755 --- a/packages/daemon/src/dotfiles_daemon/publisher.py +++ b/packages/daemon/src/dotfiles_daemon/publisher.py @@ -62,10 +62,8 @@ async def connect(self, timeout: float | None = None) -> bool: self.logger.info("Connected to daemon") return True - except asyncio.TimeoutError: - self.logger.warning( - f"Connection to daemon timed out after {timeout}s" - ) + except TimeoutError: + self.logger.warning(f"Connection to daemon timed out after {timeout}s") return False except FileNotFoundError: self.logger.warning(f"Daemon socket not found: {socket_path}") @@ -111,15 +109,11 @@ async def publish(self, message: Message) -> bool: # Send message if self._writer: - self._writer.write( - len(data).to_bytes(4, "big") - ) # Length prefix + self._writer.write(len(data).to_bytes(4, "big")) # Length prefix self._writer.write(data) await self._writer.drain() - self.logger.debug( - f"Published {message.event_type} event to daemon" - ) + self.logger.debug(f"Published {message.event_type} event to daemon") return True except Exception as e: diff --git a/packages/daemon/tests/test_config.py b/packages/daemon/tests/test_config.py index 466a7ae..840f5ba 100755 --- a/packages/daemon/tests/test_config.py +++ b/packages/daemon/tests/test_config.py @@ -2,8 +2,6 @@ from pathlib import Path -import pytest - from dotfiles_daemon.config import DaemonConfig @@ -69,4 +67,3 @@ def test_model_dump(): assert isinstance(data["socket_dir"], str) assert data["socket_dir"] == "/tmp/sockets" - diff --git a/packages/daemon/tests/test_daemon.py b/packages/daemon/tests/test_daemon.py index dc68fff..2cd45c5 100755 --- a/packages/daemon/tests/test_daemon.py +++ b/packages/daemon/tests/test_daemon.py @@ -3,7 +3,6 @@ import asyncio import pytest - from dotfiles_daemon.config import DaemonConfig from dotfiles_daemon.daemon import DotfilesDaemon from dotfiles_daemon.publisher import DaemonPublisher @@ -98,4 +97,3 @@ async def test_publisher_can_publish_messages(tmp_path): await daemon_task except asyncio.CancelledError: pass - diff --git a/packages/daemon/tests/test_publisher.py b/packages/daemon/tests/test_publisher.py index 5a5caff..47e3a0c 100755 --- a/packages/daemon/tests/test_publisher.py +++ b/packages/daemon/tests/test_publisher.py @@ -1,10 +1,6 @@ """Tests for daemon publisher.""" -import asyncio -from pathlib import Path - import pytest - from dotfiles_daemon.config import DaemonConfig from dotfiles_daemon.publisher import DaemonPublisher from dotfiles_event_protocol import MessageBuilder @@ -60,4 +56,3 @@ async def test_publisher_context_manager(tmp_path): async with DaemonPublisher(config=config) as publisher: # Connection will fail but should not raise assert publisher is not None - diff --git a/packages/event-protocol/Makefile b/packages/event-protocol/Makefile deleted file mode 100755 index e059cf7..0000000 --- a/packages/event-protocol/Makefile +++ /dev/null @@ -1,24 +0,0 @@ -.PHONY: install test clean clean-venv - -VENV_DIR = .venv -PYTHON = $(VENV_DIR)/bin/python -PIP = $(VENV_DIR)/bin/pip -PYTEST = $(VENV_DIR)/bin/pytest - -install: - uv venv $(VENV_DIR) - uv pip install -e ".[dev]" - -test: - $(PYTEST) tests/ -v - -clean: - rm -rf build/ - rm -rf dist/ - rm -rf *.egg-info - find . -type d -name __pycache__ -exec rm -rf {} + - find . -type f -name "*.pyc" -delete - -clean-venv: - rm -rf $(VENV_DIR) - diff --git a/packages/event-protocol/examples/usage_example.py b/packages/event-protocol/examples/usage_example.py index 34c9c94..5541d2c 100755 --- a/packages/event-protocol/examples/usage_example.py +++ b/packages/event-protocol/examples/usage_example.py @@ -158,4 +158,3 @@ def example_error_handling(): example_monitor_querying_state() example_validation() example_error_handling() - diff --git a/packages/event-protocol/pyproject.toml b/packages/event-protocol/pyproject.toml index 356c266..04230a7 100755 --- a/packages/event-protocol/pyproject.toml +++ b/packages/event-protocol/pyproject.toml @@ -2,7 +2,7 @@ name = "dotfiles-event-protocol" version = "0.1.0" description = "Type-safe event protocol for dotfiles event system" -requires-python = ">=3.11" +requires-python = ">=3.12" dependencies = [ "pydantic>=2.0.0", ] diff --git a/packages/event-protocol/src/dotfiles_event_protocol/models.py b/packages/event-protocol/src/dotfiles_event_protocol/models.py index 8dfac4b..0fd736d 100755 --- a/packages/event-protocol/src/dotfiles_event_protocol/models.py +++ b/packages/event-protocol/src/dotfiles_event_protocol/models.py @@ -56,9 +56,7 @@ def from_dict(cls, data: dict[str, Any]) -> "Message": class OperationStartedPayload(BaseModel): """Payload for operation_started message.""" - type: Literal[MessageType.OPERATION_STARTED] = ( - MessageType.OPERATION_STARTED - ) + type: Literal[MessageType.OPERATION_STARTED] = MessageType.OPERATION_STARTED operation_id: str = Field(..., description="Unique operation identifier") operation_name: str = Field(..., description="Name of the operation") parameters: dict[str, Any] = Field( @@ -69,9 +67,7 @@ class OperationStartedPayload(BaseModel): class OperationProgressPayload(BaseModel): """Payload for operation_progress message.""" - type: Literal[MessageType.OPERATION_PROGRESS] = ( - MessageType.OPERATION_PROGRESS - ) + type: Literal[MessageType.OPERATION_PROGRESS] = MessageType.OPERATION_PROGRESS operation_id: str = Field(..., description="Unique operation identifier") step_id: str = Field(..., description="Current step identifier") step_name: str | None = Field(None, description="Human-readable step name") @@ -81,20 +77,14 @@ class OperationProgressPayload(BaseModel): overall_progress: float = Field( ..., ge=0.0, le=100.0, description="Overall progress (0-100)" ) - total_steps: int | None = Field( - None, ge=1, description="Total number of steps" - ) - current_step: int | None = Field( - None, ge=1, description="Current step number" - ) + total_steps: int | None = Field(None, ge=1, description="Total number of steps") + current_step: int | None = Field(None, ge=1, description="Current step number") class OperationCompletedPayload(BaseModel): """Payload for operation_completed message.""" - type: Literal[MessageType.OPERATION_COMPLETED] = ( - MessageType.OPERATION_COMPLETED - ) + type: Literal[MessageType.OPERATION_COMPLETED] = MessageType.OPERATION_COMPLETED operation_id: str = Field(..., description="Unique operation identifier") duration_seconds: float = Field( ..., ge=0.0, description="Operation duration in seconds" @@ -141,9 +131,7 @@ class QueryResponsePayload(BaseModel): type: Literal[MessageType.QUERY_RESPONSE] = MessageType.QUERY_RESPONSE query_id: str = Field(..., description="ID of the query request") result: dict[str, Any] = Field(..., description="Query result") - error: str | None = Field( - None, description="Error message if query failed" - ) + error: str | None = Field(None, description="Error message if query failed") class CommandRequestPayload(BaseModel): @@ -155,9 +143,7 @@ class CommandRequestPayload(BaseModel): type: Literal[MessageType.COMMAND_REQUEST] = MessageType.COMMAND_REQUEST command: CommandType = Field(..., description="Command to execute") - args: dict[str, Any] = Field( - default_factory=dict, description="Command arguments" - ) + args: dict[str, Any] = Field(default_factory=dict, description="Command arguments") fire_and_forget: bool = Field( default=True, description="If True, don't wait for response", @@ -170,12 +156,8 @@ class CommandResponsePayload(BaseModel): type: Literal[MessageType.COMMAND_RESPONSE] = MessageType.COMMAND_RESPONSE command_id: str = Field(..., description="ID of the command request") success: bool = Field(..., description="Whether command succeeded") - result: dict[str, Any] = Field( - default_factory=dict, description="Command result" - ) - error: str | None = Field( - None, description="Error message if command failed" - ) + result: dict[str, Any] = Field(default_factory=dict, description="Command result") + error: str | None = Field(None, description="Error message if command failed") # ============================================================================ diff --git a/packages/event-protocol/src/dotfiles_event_protocol/types.py b/packages/event-protocol/src/dotfiles_event_protocol/types.py index edabd54..8d3e68d 100755 --- a/packages/event-protocol/src/dotfiles_event_protocol/types.py +++ b/packages/event-protocol/src/dotfiles_event_protocol/types.py @@ -1,9 +1,9 @@ """Type aliases and enums for event protocol.""" -from enum import Enum +from enum import StrEnum -class MessageType(str, Enum): +class MessageType(StrEnum): """Message types for event protocol.""" OPERATION_STARTED = "operation_started" @@ -17,7 +17,7 @@ class MessageType(str, Enum): COMMAND_RESPONSE = "command_response" -class QueryType(str, Enum): +class QueryType(StrEnum): """Query types for daemon queries.""" GET_CURRENT_STATE = "get_current_state" @@ -26,7 +26,7 @@ class QueryType(str, Enum): GET_EVENT_HISTORY = "get_event_history" -class CommandType(str, Enum): +class CommandType(StrEnum): """Command types for daemon command execution.""" LAUNCH_WLOGOUT = "launch_wlogout" diff --git a/packages/event-protocol/src/dotfiles_event_protocol/validator.py b/packages/event-protocol/src/dotfiles_event_protocol/validator.py index 31a3eb4..581188c 100755 --- a/packages/event-protocol/src/dotfiles_event_protocol/validator.py +++ b/packages/event-protocol/src/dotfiles_event_protocol/validator.py @@ -14,13 +14,13 @@ class MessageValidator: @staticmethod def validate_message(data: dict[str, Any]) -> Message: """Validate and parse a message. - + Args: data: Raw message data - + Returns: Validated Message - + Raises: ValidationError: If message is invalid """ @@ -29,11 +29,11 @@ def validate_message(data: dict[str, Any]) -> Message: @staticmethod def validate_payload_type(message: Message, expected_type: MessageType) -> bool: """Validate that payload has expected type. - + Args: message: Message to validate expected_type: Expected message type - + Returns: True if payload type matches expected type """ @@ -43,10 +43,10 @@ def validate_payload_type(message: Message, expected_type: MessageType) -> bool: @staticmethod def is_valid_message(data: dict[str, Any]) -> bool: """Check if data is a valid message. - + Args: data: Raw message data - + Returns: True if valid, False otherwise """ @@ -59,10 +59,10 @@ def is_valid_message(data: dict[str, Any]) -> bool: @staticmethod def get_message_type(message: Message) -> MessageType | None: """Get message type from payload. - + Args: message: Message to inspect - + Returns: MessageType if found, None otherwise """ @@ -74,4 +74,3 @@ def get_message_type(message: Message) -> MessageType | None: return MessageType(payload_type) except ValueError: return None - diff --git a/packages/event-protocol/tests/test_builder.py b/packages/event-protocol/tests/test_builder.py index 5a3d8dc..94ed66b 100755 --- a/packages/event-protocol/tests/test_builder.py +++ b/packages/event-protocol/tests/test_builder.py @@ -118,4 +118,3 @@ def test_query_response(): assert msg.payload["query_id"] == "query-123" assert msg.payload["result"]["wallpaper_path"] == "/path/to/image.png" assert msg.payload["error"] is None - diff --git a/packages/event-protocol/tests/test_models.py b/packages/event-protocol/tests/test_models.py index 9d5404f..955dfe5 100755 --- a/packages/event-protocol/tests/test_models.py +++ b/packages/event-protocol/tests/test_models.py @@ -1,14 +1,13 @@ """Tests for event protocol models.""" import pytest -from pydantic import ValidationError - from dotfiles_event_protocol import ( Message, MessageType, OperationProgressMessage, OperationProgressPayload, ) +from pydantic import ValidationError def test_message_creation(): @@ -106,4 +105,3 @@ def test_message_serialization(): msg2 = Message.from_dict(data) assert msg2.event_type == msg.event_type assert msg2.payload == msg.payload - diff --git a/packages/event-protocol/tests/test_validator.py b/packages/event-protocol/tests/test_validator.py index 4329449..e01c9b2 100755 --- a/packages/event-protocol/tests/test_validator.py +++ b/packages/event-protocol/tests/test_validator.py @@ -45,8 +45,14 @@ def test_validate_payload_type(): payload={"type": "operation_progress"}, ) - assert MessageValidator.validate_payload_type(msg, MessageType.OPERATION_PROGRESS) is True - assert MessageValidator.validate_payload_type(msg, MessageType.OPERATION_STARTED) is False + assert ( + MessageValidator.validate_payload_type(msg, MessageType.OPERATION_PROGRESS) + is True + ) + assert ( + MessageValidator.validate_payload_type(msg, MessageType.OPERATION_STARTED) + is False + ) def test_get_message_type(): @@ -65,4 +71,3 @@ def test_get_message_type(): payload={}, ) assert MessageValidator.get_message_type(msg_no_type) is None - diff --git a/packages/logging/Makefile b/packages/logging/Makefile deleted file mode 100644 index 683f078..0000000 --- a/packages/logging/Makefile +++ /dev/null @@ -1,98 +0,0 @@ -MAKEFLAGS += --no-print-directory - -# Standard Makefile for UV Python Projects -# Provides consistent development workflows - -.PHONY: help sync-check ensure-sync dev-shell format lint type-check test test-cov clean clean-venv install build pre-commit-install pre-commit-run all-checks - -help: ## Show this help message - @echo "Available commands:" - @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf " %-20s %s\n", $$1, $$2}' - -sync-check: ## Check if environment sync is needed - @if ! uv run python -c "import sys; sys.exit(0)" 2>/dev/null; then \ - echo "❌ Package not installed - sync needed"; \ - exit 1; \ - elif [ uv.lock -nt .venv/pyvenv.cfg ] 2>/dev/null; then \ - echo "❌ Lock file newer than venv - sync needed"; \ - exit 1; \ - elif [ pyproject.toml -nt .venv/pyvenv.cfg ] 2>/dev/null; then \ - echo "❌ pyproject.toml newer than venv - sync needed"; \ - exit 1; \ - else \ - echo "✅ Environment is up to date"; \ - fi - -ensure-sync: ## Ensure environment is synced - @$(MAKE) sync-check || (echo "Syncing environment..." && uv sync) - -dev-shell: ensure-sync ## Activate development shell - @echo "Activating virtual environment..." - @echo "You can leave the dev shell by typing 'exit'" - @bash -c "source .venv/bin/activate && exec bash" - -format: ensure-sync ## Format code with black and isort - @echo "Formatting code..." - @uv run black . - @uv run isort . - @echo "✅ Formatting complete" - -lint: ensure-sync ## Lint code with ruff - @echo "Linting code..." - @uv run ruff check --fix . - @echo "✅ Linting complete" - -type-check: ensure-sync ## Type check with mypy - @echo "Type checking..." - @uv run mypy . - @echo "✅ Type checking complete" - -test: ensure-sync ## Run tests - @echo "Running tests..." - @uv run pytest -v - @echo "✅ Tests complete" - -test-cov: ensure-sync ## Run tests with coverage - @echo "Running tests with coverage..." - @uv run pytest --cov --cov-report=html --cov-report=term - @echo "✅ Coverage report generated in htmlcov/" - -clean: ## Clean cache files and build artifacts - @echo "Cleaning cache files..." - @find . -type d -name "__pycache__" -exec rm -rf {} + 2>/dev/null || true - @find . -type f -name "*.pyc" -delete 2>/dev/null || true - @find . -type d -name ".pytest_cache" -exec rm -rf {} + 2>/dev/null || true - @find . -type d -name ".mypy_cache" -exec rm -rf {} + 2>/dev/null || true - @find . -type d -name ".ruff_cache" -exec rm -rf {} + 2>/dev/null || true - @find . -type d -name "*.egg-info" -exec rm -rf {} + 2>/dev/null || true - @rm -rf build/ dist/ htmlcov/ .coverage 2>/dev/null || true - @echo "✅ Cleanup complete" - -clean-venv: ## Remove virtual environment - @echo "Removing virtual environment..." - @rm -rf .venv - @echo "✅ Virtual environment removed" - @echo "Run 'make install' or 'uv sync' to recreate" - -install: ## Install project in editable mode - @echo "Installing project..." - @uv sync - @echo "✅ Installation complete" - -build: ensure-sync ## Build the package - @echo "Building package..." - @uv build - @echo "✅ Build complete" - -pre-commit-install: ensure-sync ## Install pre-commit hooks - @echo "Installing pre-commit hooks..." - @uv run pre-commit install - @echo "✅ Pre-commit hooks installed" - -pre-commit-run: ensure-sync ## Run pre-commit on all files - @echo "Running pre-commit on all files..." - @uv run pre-commit run --all-files - @echo "✅ Pre-commit checks complete" - -all-checks: format lint type-check test ## Run all checks (format, lint, type-check, test) - @echo "✅ All checks passed!" diff --git a/packages/logging/src/rich_logging/core/configurator.py b/packages/logging/src/rich_logging/core/configurator.py index 0509cc8..fb0d320 100644 --- a/packages/logging/src/rich_logging/core/configurator.py +++ b/packages/logging/src/rich_logging/core/configurator.py @@ -102,9 +102,7 @@ def update(self, **kwargs) -> LogConfig: RuntimeError: If configurator has not been configured yet """ if self.config is None: - raise RuntimeError( - "Configurator must be configured before updating" - ) + raise RuntimeError("Configurator must be configured before updating") # Merge with existing config config_dict = { @@ -114,22 +112,14 @@ def update(self, **kwargs) -> LogConfig: "formatter_style", self.config.formatter_style ), "format": kwargs.get("format", self.config.format), - "formatter_type": kwargs.get( - "formatter_type", self.config.formatter_type - ), + "formatter_type": kwargs.get("formatter_type", self.config.formatter_type), "colors": kwargs.get("colors", self.config.colors), "console_handler": kwargs.get( "console_handler_type", self.config.console_handler ), - "handler_config": kwargs.get( - "handler_config", self.config.handler_config - ), - "file_handlers": kwargs.get( - "file_handlers", self.config.file_handlers - ), - "rich_features": kwargs.get( - "rich_features", self.config.rich_features - ), + "handler_config": kwargs.get("handler_config", self.config.handler_config), + "file_handlers": kwargs.get("file_handlers", self.config.file_handlers), + "rich_features": kwargs.get("rich_features", self.config.rich_features), } return LogConfig(**config_dict) diff --git a/packages/logging/src/rich_logging/core/log_context.py b/packages/logging/src/rich_logging/core/log_context.py index ec9176d..27523ae 100644 --- a/packages/logging/src/rich_logging/core/log_context.py +++ b/packages/logging/src/rich_logging/core/log_context.py @@ -11,23 +11,20 @@ class LogContext: """Thread-local storage for logging context. - + This class uses threading.local() to store context data per thread, ensuring that parallel tasks can have independent context without interfering with each other. """ - + _thread_local = threading.local() - + @classmethod def set_task_context( - cls, - step_id: str, - task_name: str | None = None, - **extra_context: Any + cls, step_id: str, task_name: str | None = None, **extra_context: Any ) -> None: """Set the task context for the current thread. - + Args: step_id: The unique identifier for the current step/task task_name: Optional human-readable task name @@ -36,39 +33,39 @@ def set_task_context( context = { "step_id": step_id, "task_name": task_name or step_id, - **extra_context + **extra_context, } cls._thread_local.context = context - + @classmethod def get_task_context(cls) -> dict[str, Any] | None: """Get the task context for the current thread. - + Returns: Dictionary containing the task context, or None if no context is set """ return getattr(cls._thread_local, "context", None) - + @classmethod def clear_task_context(cls) -> None: """Clear the task context for the current thread.""" if hasattr(cls._thread_local, "context"): delattr(cls._thread_local, "context") - + @classmethod def get_step_id(cls) -> str | None: """Get the current step ID. - + Returns: The step ID if context is set, None otherwise """ context = cls.get_task_context() return context.get("step_id") if context else None - + @classmethod def get_task_name(cls) -> str | None: """Get the current task name. - + Returns: The task name if context is set, None otherwise """ @@ -78,12 +75,10 @@ def get_task_name(cls) -> str | None: # Convenience functions for easier imports def set_task_context( - step_id: str, - task_name: str | None = None, - **extra_context: Any + step_id: str, task_name: str | None = None, **extra_context: Any ) -> None: """Set the task context for the current thread. - + Args: step_id: The unique identifier for the current step/task task_name: Optional human-readable task name @@ -94,7 +89,7 @@ def set_task_context( def get_task_context() -> dict[str, Any] | None: """Get the task context for the current thread. - + Returns: Dictionary containing the task context, or None if no context is set """ @@ -108,7 +103,7 @@ def clear_task_context() -> None: def get_step_id() -> str | None: """Get the current step ID. - + Returns: The step ID if context is set, None otherwise """ @@ -117,9 +112,8 @@ def get_step_id() -> str | None: def get_task_name() -> str | None: """Get the current task name. - + Returns: The task name if context is set, None otherwise """ return LogContext.get_task_name() - diff --git a/packages/logging/src/rich_logging/core/log_types.py b/packages/logging/src/rich_logging/core/log_types.py index 567b0a4..f8418ef 100644 --- a/packages/logging/src/rich_logging/core/log_types.py +++ b/packages/logging/src/rich_logging/core/log_types.py @@ -105,12 +105,6 @@ class LogConfig: name: str | None = None colors: ColoredFormatterColors | None = None console_handler: ConsoleHandlers = ConsoleHandlers.DEFAULT - handler_config: "RichHandlerSettings | None" = ( - None # Console handler configuration - ) - file_handlers: list[FileHandlerSpec] | None = ( - None # File handler specifications - ) - rich_features: "RichFeatureSettings | None" = ( - None # Rich features configuration - ) + handler_config: "RichHandlerSettings | None" = None # Console handler configuration + file_handlers: list[FileHandlerSpec] | None = None # File handler specifications + rich_features: "RichFeatureSettings | None" = None # Rich features configuration diff --git a/packages/logging/src/rich_logging/core/utils.py b/packages/logging/src/rich_logging/core/utils.py index 19f39a6..f3f2383 100644 --- a/packages/logging/src/rich_logging/core/utils.py +++ b/packages/logging/src/rich_logging/core/utils.py @@ -20,9 +20,7 @@ def get_log_level_map( Dictionary mapping all variants to LogLevels enum values """ level_map = {} - full_name_pattern = re.compile( - r"^[a-zA-Z]+$" - ) # Full names like 'debug', 'info' + full_name_pattern = re.compile(r"^[a-zA-Z]+$") # Full names like 'debug', 'info' abbrev_pattern = re.compile(r"^[a-zA-Z]$") # Single letter abbreviations # Get all class attributes that are lists (our log level options) @@ -50,14 +48,10 @@ def get_log_level_map( # Validate format based on position if i == 0: # Full name if not full_name_pattern.match(variant): - raise ValueError( - f"Invalid full name format: {variant}" - ) + raise ValueError(f"Invalid full name format: {variant}") else: # Abbreviations if not abbrev_pattern.match(variant): - raise ValueError( - f"Invalid abbreviation format: {variant}" - ) + raise ValueError(f"Invalid abbreviation format: {variant}") # Add to map (case-insensitive) level_map[variant.lower()] = log_level_enum diff --git a/packages/logging/src/rich_logging/filters/__init__.py b/packages/logging/src/rich_logging/filters/__init__.py index 707190e..83bf8ce 100644 --- a/packages/logging/src/rich_logging/filters/__init__.py +++ b/packages/logging/src/rich_logging/filters/__init__.py @@ -3,4 +3,3 @@ from .task_context_filter import TaskContextFilter __all__ = ["TaskContextFilter"] - diff --git a/packages/logging/src/rich_logging/filters/task_context_filter.py b/packages/logging/src/rich_logging/filters/task_context_filter.py index db62be7..a3a2c4e 100644 --- a/packages/logging/src/rich_logging/filters/task_context_filter.py +++ b/packages/logging/src/rich_logging/filters/task_context_filter.py @@ -5,23 +5,22 @@ """ import logging -from typing import Any from ..core.log_context import LogContext class TaskContextFilter(logging.Filter): """Filter that adds task context to log messages. - + This filter checks for thread-local task context and prepends the task identifier to log messages. This is particularly useful for parallel execution where multiple tasks run concurrently. - + Example: Without filter: "Installing package..." With filter: "[install_nodejs] Installing package..." """ - + def __init__( self, name: str = "", @@ -31,7 +30,7 @@ def __init__( task_style: str = "cyan", ): """Initialize the task context filter. - + Args: name: Filter name (passed to parent Filter class) enabled: Whether the filter is enabled @@ -45,60 +44,61 @@ def __init__( self.format_template = format_template self.use_rich_markup = use_rich_markup self.task_style = task_style - + def filter(self, record: logging.LogRecord) -> bool: """Filter the log record, adding task context if available. - + Args: record: The log record to filter - + Returns: True to allow the record to be logged """ if not self.enabled: return True - + # Get task context from thread-local storage context = LogContext.get_task_context() - + if context: # Format the task identifier task_identifier = self.format_template.format(**context) - + # Apply Rich markup if enabled if self.use_rich_markup and self.task_style: - task_identifier = f"[{self.task_style}]{task_identifier}[/{self.task_style}]" - + task_identifier = ( + f"[{self.task_style}]{task_identifier}[/{self.task_style}]" + ) + # Prepend to the message record.msg = f"{task_identifier}{record.msg}" - + # Also add to record for potential use by formatters if not hasattr(record, "task_context"): record.task_context = context # type: ignore[attr-defined] - + return True - + def enable(self) -> None: """Enable the filter.""" self.enabled = True - + def disable(self) -> None: """Disable the filter.""" self.enabled = False - + def set_format_template(self, template: str) -> None: """Set the format template. - + Args: template: New format template with placeholders like {step_id}, {task_name} """ self.format_template = template - + def set_style(self, style: str) -> None: """Set the Rich style for task identifiers. - + Args: style: Rich style string (e.g., "cyan", "bold blue", "dim") """ self.task_style = style - diff --git a/packages/logging/src/rich_logging/formatters/colored.py b/packages/logging/src/rich_logging/formatters/colored.py index 283717f..587315e 100644 --- a/packages/logging/src/rich_logging/formatters/colored.py +++ b/packages/logging/src/rich_logging/formatters/colored.py @@ -1,7 +1,6 @@ """Colored formatter implementation.""" import logging as stdlib_logging -import rich_logging from ..core.log_types import ( ColoredFormatterColors, diff --git a/packages/logging/src/rich_logging/formatters/rich.py b/packages/logging/src/rich_logging/formatters/rich.py index 88722e6..d6bc5e2 100644 --- a/packages/logging/src/rich_logging/formatters/rich.py +++ b/packages/logging/src/rich_logging/formatters/rich.py @@ -1,7 +1,6 @@ """Rich formatter implementation.""" import logging as stdlib_logging -import rich_logging from ..core.log_types import ( LogFormatters, @@ -62,8 +61,7 @@ def format(self, record): # we can auto-add colors to the level name if ( "[" not in log_message - or f"[{self.level_colors.get(record.levelname, '')}]" - not in log_message + or f"[{self.level_colors.get(record.levelname, '')}]" not in log_message ): # Auto-colorize level name if not already colored in format string level_color = self.level_colors.get(record.levelname, "white") diff --git a/packages/logging/src/rich_logging/handlers/base.py b/packages/logging/src/rich_logging/handlers/base.py index e0b6cb3..2ec2900 100644 --- a/packages/logging/src/rich_logging/handlers/base.py +++ b/packages/logging/src/rich_logging/handlers/base.py @@ -1,7 +1,6 @@ """Base handler configuration and factory.""" import logging as stdlib_logging -import rich_logging from abc import ABC, abstractmethod from ..core.log_types import ConsoleHandlers diff --git a/packages/logging/src/rich_logging/handlers/console.py b/packages/logging/src/rich_logging/handlers/console.py index 0a244fa..093faef 100644 --- a/packages/logging/src/rich_logging/handlers/console.py +++ b/packages/logging/src/rich_logging/handlers/console.py @@ -1,7 +1,6 @@ """Console handler implementations.""" import logging as stdlib_logging -import rich_logging from ..core.log_types import ConsoleHandlers from ..filters.task_context_filter import TaskContextFilter @@ -77,9 +76,7 @@ def create(self) -> stdlib_logging.Handler: # Register console with manager for sharing if self.logger_name and hasattr(handler, "console"): - console_manager.register_console( - self.logger_name, handler.console - ) + console_manager.register_console(self.logger_name, handler.console) # Add task context filter if enabled if self.settings.show_task_context: diff --git a/packages/logging/src/rich_logging/handlers/file.py b/packages/logging/src/rich_logging/handlers/file.py index a992fe6..be9d53b 100644 --- a/packages/logging/src/rich_logging/handlers/file.py +++ b/packages/logging/src/rich_logging/handlers/file.py @@ -158,9 +158,7 @@ def create( # Register file handlers FileHandlerFactory.register(FileHandlerTypes.FILE, FileHandlerConfig) -FileHandlerFactory.register( - FileHandlerTypes.ROTATING_FILE, RotatingFileHandlerConfig -) +FileHandlerFactory.register(FileHandlerTypes.ROTATING_FILE, RotatingFileHandlerConfig) FileHandlerFactory.register( FileHandlerTypes.TIMED_ROTATING_FILE, TimedRotatingFileHandlerConfig ) diff --git a/packages/logging/src/rich_logging/handlers/file_settings.py b/packages/logging/src/rich_logging/handlers/file_settings.py index 17429fe..38012eb 100644 --- a/packages/logging/src/rich_logging/handlers/file_settings.py +++ b/packages/logging/src/rich_logging/handlers/file_settings.py @@ -71,7 +71,5 @@ class TimedRotatingFileHandlerSettings: # Union type for all file handler settings BaseFileHandlerSettings = ( - FileHandlerSettings - | RotatingFileHandlerSettings - | TimedRotatingFileHandlerSettings + FileHandlerSettings | RotatingFileHandlerSettings | TimedRotatingFileHandlerSettings ) diff --git a/packages/logging/src/rich_logging/handlers/rich_settings.py b/packages/logging/src/rich_logging/handlers/rich_settings.py index 1a9f750..3e3eb97 100644 --- a/packages/logging/src/rich_logging/handlers/rich_settings.py +++ b/packages/logging/src/rich_logging/handlers/rich_settings.py @@ -117,9 +117,9 @@ def to_dict(self) -> dict: # Custom fields that should not be passed to RichHandler # These are used by our custom TaskContextFilter exclude_fields = { - 'show_task_context', - 'task_context_format', - 'task_context_style', + "show_task_context", + "task_context_format", + "task_context_style", } # Convert dataclass to dict, excluding None values for optional diff --git a/packages/logging/src/rich_logging/log.py b/packages/logging/src/rich_logging/log.py index 0a4e60f..436de60 100644 --- a/packages/logging/src/rich_logging/log.py +++ b/packages/logging/src/rich_logging/log.py @@ -1,7 +1,6 @@ """Main logging API facade.""" import logging as stdlib_logging -import rich_logging # Import for type hints from typing import TYPE_CHECKING @@ -55,9 +54,7 @@ def create_logger( name: str | None = None, config: LogConfig | None = None, log_level: LogLevels | None = None, - formatter_style: LogFormatterStyleChoices = ( - LogFormatterStyleChoices.PERCENT - ), + formatter_style: LogFormatterStyleChoices = (LogFormatterStyleChoices.PERCENT), format: str = "%(asctime)s | %(levelname)-8s | %(message)s", formatter_type: LogFormatters = LogFormatters.DEFAULT, colors: type[ColoredFormatterColors] | None = None, @@ -107,9 +104,7 @@ def create_logger( if config is not None: final_config = LogConfig( name=name, # Always use the provided name parameter - log_level=( - log_level if log_level is not None else config.log_level - ), + log_level=(log_level if log_level is not None else config.log_level), formatter_style=( formatter_style if formatter_style != LogFormatterStyleChoices.PERCENT @@ -137,22 +132,16 @@ def create_logger( else config.handler_config ), file_handlers=( - file_handlers - if file_handlers is not None - else config.file_handlers + file_handlers if file_handlers is not None else config.file_handlers ), rich_features=( - rich_features - if rich_features is not None - else config.rich_features + rich_features if rich_features is not None else config.rich_features ), ) else: # Create config from individual parameters - log_level is required if log_level is None: - raise ValueError( - "log_level is required when config is not provided" - ) + raise ValueError("log_level is required when config is not provided") final_config = LogConfig( name=name, @@ -227,8 +216,7 @@ def update( """ if name not in Log._configurators: raise ValueError( - f"Logger '{name}' not found. Create it first with " - f"create_logger()" + f"Logger '{name}' not found. Create it first with " f"create_logger()" ) configurator = Log._configurators[name] @@ -237,9 +225,7 @@ def update( if config is not None: # Start with config values and override with individual parameters update_kwargs = { - "log_level": ( - log_level if log_level is not None else config.log_level - ), + "log_level": (log_level if log_level is not None else config.log_level), "formatter_style": ( formatter_style if formatter_style is not None @@ -263,14 +249,10 @@ def update( else config.handler_config ), "file_handlers": ( - file_handlers - if file_handlers is not None - else config.file_handlers + file_handlers if file_handlers is not None else config.file_handlers ), "rich_features": ( - rich_features - if rich_features is not None - else config.rich_features + rich_features if rich_features is not None else config.rich_features ), } else: diff --git a/packages/logging/src/rich_logging/presets.py b/packages/logging/src/rich_logging/presets.py index 3bf7990..adf38e4 100644 --- a/packages/logging/src/rich_logging/presets.py +++ b/packages/logging/src/rich_logging/presets.py @@ -1,7 +1,7 @@ -from enum import Enum +from enum import StrEnum -class PresetLoggers(str, Enum): +class PresetLoggers(StrEnum): """Preset loggers.""" BASIC_RICH: str = "basic_rich" diff --git a/packages/logging/src/rich_logging/rich/rich_feature_settings.py b/packages/logging/src/rich_logging/rich/rich_feature_settings.py index e4dbf25..ef31555 100644 --- a/packages/logging/src/rich_logging/rich/rich_feature_settings.py +++ b/packages/logging/src/rich_logging/rich/rich_feature_settings.py @@ -222,9 +222,7 @@ def __post_init__(self): ) if len(self.panel_padding) != 2: - raise ValueError( - "panel_padding must be a tuple of (vertical, horizontal)" - ) + raise ValueError("panel_padding must be a tuple of (vertical, horizontal)") if any(p < 0 for p in self.panel_padding): raise ValueError("panel_padding values must be non-negative") diff --git a/packages/logging/src/rich_logging/rich/rich_logger.py b/packages/logging/src/rich_logging/rich/rich_logger.py index fa4898a..51d34ac 100644 --- a/packages/logging/src/rich_logging/rich/rich_logger.py +++ b/packages/logging/src/rich_logging/rich/rich_logger.py @@ -1,7 +1,6 @@ """Enhanced logger with Rich features integration.""" import logging as stdlib_logging -import rich_logging from collections.abc import Iterator from contextlib import contextmanager from typing import Any @@ -104,10 +103,7 @@ def _get_console(self) -> Console | None: # Task context methods for parallel execution def set_task_context( - self, - step_id: str, - task_name: str | None = None, - **extra_context: Any + self, step_id: str, task_name: str | None = None, **extra_context: Any ) -> None: """Set the task context for the current thread. @@ -136,10 +132,7 @@ def clear_task_context(self) -> None: @contextmanager def task_context( - self, - step_id: str, - task_name: str | None = None, - **extra_context: Any + self, step_id: str, task_name: str | None = None, **extra_context: Any ) -> Iterator[None]: """Context manager for task context. @@ -206,13 +199,9 @@ def table( else self._rich_settings.table_show_lines ) show_edge = ( - show_edge - if show_edge is not None - else self._rich_settings.table_show_edge - ) - expand = ( - expand if expand is not None else self._rich_settings.table_expand + show_edge if show_edge is not None else self._rich_settings.table_show_edge ) + expand = expand if expand is not None else self._rich_settings.table_expand table = Table( title=title, @@ -295,18 +284,10 @@ def panel( if border_style is not None else self._rich_settings.panel_border_style ) - expand = ( - expand if expand is not None else self._rich_settings.panel_expand - ) - padding = ( - padding - if padding is not None - else self._rich_settings.panel_padding - ) + expand = expand if expand is not None else self._rich_settings.panel_expand + padding = padding if padding is not None else self._rich_settings.panel_padding box_style = ( - box_style - if box_style is not None - else self._rich_settings.panel_box_style + box_style if box_style is not None else self._rich_settings.panel_box_style ) # Handle box style @@ -446,11 +427,7 @@ def status( return # Use settings defaults - spinner = ( - spinner - if spinner is not None - else self._rich_settings.status_spinner - ) + spinner = spinner if spinner is not None else self._rich_settings.status_spinner refresh_per_second = kwargs.pop( "refresh_per_second", self._rich_settings.status_refresh_per_second ) @@ -511,9 +488,7 @@ def tree( else self._rich_settings.tree_guide_style ) expanded = ( - expanded - if expanded is not None - else self._rich_settings.tree_expanded + expanded if expanded is not None else self._rich_settings.tree_expanded ) # Create root tree @@ -528,9 +503,7 @@ def tree( self._add_tree_nodes(tree, data, expanded) else: # data is a string label - tree = Tree( - data, guide_style=guide_style, expanded=expanded, **kwargs - ) + tree = Tree(data, guide_style=guide_style, expanded=expanded, **kwargs) console.print(tree) @@ -562,9 +535,7 @@ def _add_tree_nodes( self._add_tree_nodes(branch, value, expanded) elif isinstance(value, list): # List of items - branch = parent_node.add( - f"[bold]{key}[/bold]", expanded=expanded - ) + branch = parent_node.add(f"[bold]{key}[/bold]", expanded=expanded) for item in value: if isinstance(item, dict): self._add_tree_nodes(branch, item, expanded) @@ -618,18 +589,10 @@ def columns( return # Use settings defaults for None values - equal = ( - equal if equal is not None else self._rich_settings.columns_equal - ) - expand = ( - expand - if expand is not None - else self._rich_settings.columns_expand - ) + equal = equal if equal is not None else self._rich_settings.columns_equal + expand = expand if expand is not None else self._rich_settings.columns_expand padding = ( - padding - if padding is not None - else self._rich_settings.columns_padding + padding if padding is not None else self._rich_settings.columns_padding ) columns = Columns( @@ -690,18 +653,14 @@ def syntax( return # Use settings defaults for None values - theme = ( - theme if theme is not None else self._rich_settings.syntax_theme - ) + theme = theme if theme is not None else self._rich_settings.syntax_theme line_numbers = ( line_numbers if line_numbers is not None else self._rich_settings.syntax_line_numbers ) word_wrap = ( - word_wrap - if word_wrap is not None - else self._rich_settings.syntax_word_wrap + word_wrap if word_wrap is not None else self._rich_settings.syntax_word_wrap ) background_color = ( background_color @@ -844,18 +803,12 @@ def json( return # Use settings defaults for None values - indent = ( - indent if indent is not None else self._rich_settings.json_indent - ) + indent = indent if indent is not None else self._rich_settings.json_indent highlight = ( - highlight - if highlight is not None - else self._rich_settings.json_highlight + highlight if highlight is not None else self._rich_settings.json_highlight ) sort_keys = ( - sort_keys - if sort_keys is not None - else self._rich_settings.json_sort_keys + sort_keys if sort_keys is not None else self._rich_settings.json_sort_keys ) # Handle different input types @@ -1010,9 +963,7 @@ def bar_chart( return # Use settings defaults for None values - width = ( - width if width is not None else self._rich_settings.bar_chart_width - ) + width = width if width is not None else self._rich_settings.bar_chart_width character = ( character if character is not None @@ -1091,21 +1042,11 @@ def text( return # Use settings defaults for None values - justify = ( - justify - if justify is not None - else self._rich_settings.text_justify - ) + justify = justify if justify is not None else self._rich_settings.text_justify overflow = ( - overflow - if overflow is not None - else self._rich_settings.text_overflow - ) - no_wrap = ( - no_wrap - if no_wrap is not None - else self._rich_settings.text_no_wrap + overflow if overflow is not None else self._rich_settings.text_overflow ) + no_wrap = no_wrap if no_wrap is not None else self._rich_settings.text_no_wrap rich_text = Text( text, @@ -1319,21 +1260,13 @@ def inspect( # Use settings defaults for None values methods = ( - methods - if methods is not None - else self._rich_settings.inspect_methods + methods if methods is not None else self._rich_settings.inspect_methods ) help = help if help is not None else self._rich_settings.inspect_help private = ( - private - if private is not None - else self._rich_settings.inspect_private - ) - dunder = ( - dunder - if dunder is not None - else self._rich_settings.inspect_dunder + private if private is not None else self._rich_settings.inspect_private ) + dunder = dunder if dunder is not None else self._rich_settings.inspect_dunder sort = sort if sort is not None else self._rich_settings.inspect_sort rich_inspect( @@ -1408,9 +1341,7 @@ def pretty( else self._rich_settings.pretty_max_string ) max_depth = ( - max_depth - if max_depth is not None - else self._rich_settings.pretty_max_depth + max_depth if max_depth is not None else self._rich_settings.pretty_max_depth ) pretty = Pretty( diff --git a/packages/logging/tests/conftest.py b/packages/logging/tests/conftest.py index 5b0c8fe..016fa25 100644 --- a/packages/logging/tests/conftest.py +++ b/packages/logging/tests/conftest.py @@ -5,18 +5,17 @@ """ import logging as stdlib_logging -from typing import Generator -from unittest.mock import Mock, MagicMock +from unittest.mock import Mock import pytest from rich_logging import ( + ConsoleHandlers, Log, LogConfig, - LogLevels, - ConsoleHandlers, LogFormatters, LogFormatterStyleChoices, + LogLevels, RichFeatureSettings, RichHandlerSettings, ) @@ -25,20 +24,20 @@ @pytest.fixture(autouse=True) def reset_loggers(): """Reset logger state before each test. - + This ensures tests don't interfere with each other by: - Clearing Log._configurators registry - Removing all handlers from root logger """ # Clear the configurators registry Log._configurators.clear() - + # Clear root logger handlers root_logger = stdlib_logging.getLogger() root_logger.handlers.clear() - + yield - + # Cleanup after test Log._configurators.clear() root_logger.handlers.clear() @@ -47,7 +46,7 @@ def reset_loggers(): @pytest.fixture def mock_console(): """Provide a mock Rich Console for testing. - + Returns: Mock: Mock console with print, log, and other Rich methods """ @@ -77,7 +76,7 @@ def basic_log_config() -> LogConfig: @pytest.fixture def rich_log_config() -> LogConfig: """Provide a Rich-enabled LogConfig for testing. - + Returns: LogConfig: Configuration with Rich handler and features enabled """ @@ -102,7 +101,7 @@ def rich_log_config() -> LogConfig: @pytest.fixture def mock_stdlib_logger() -> Mock: """Provide a mock stdlib Logger for testing. - + Returns: Mock: Mock logger with standard logging methods """ @@ -124,10 +123,10 @@ def mock_stdlib_logger() -> Mock: @pytest.fixture def capture_log_output(caplog): """Capture log output for assertions. - + Args: caplog: pytest's built-in log capture fixture - + Yields: caplog: Configured log capture """ @@ -138,12 +137,11 @@ def capture_log_output(caplog): @pytest.fixture def temp_log_file(tmp_path): """Provide a temporary file path for file logging tests. - + Args: tmp_path: pytest's built-in temporary directory fixture - + Returns: Path: Path to temporary log file """ return tmp_path / "test.log" - diff --git a/packages/logging/tests/contract/test_log_api.py b/packages/logging/tests/contract/test_log_api.py index 843f621..5907a61 100644 --- a/packages/logging/tests/contract/test_log_api.py +++ b/packages/logging/tests/contract/test_log_api.py @@ -12,17 +12,15 @@ """ import logging as stdlib_logging + import pytest from rich_logging import ( + ConsoleHandlers, Log, - LogConfig, LogLevels, - ConsoleHandlers, - LogFormatters, - RichLogger, RichFeatureSettings, - RichHandlerSettings, + RichLogger, ) @@ -39,32 +37,32 @@ def test_create_logger_returns_rich_logger(self): def test_create_logger_with_log_level(self): """Contract: log_level parameter sets logger level.""" logger = Log.create_logger("test_logger", log_level=LogLevels.DEBUG) - + # Access underlying stdlib logger assert logger._logger.level == stdlib_logging.DEBUG def test_create_logger_with_info_level(self): """Contract: INFO level is set correctly.""" logger = Log.create_logger("test_logger", log_level=LogLevels.INFO) - + assert logger._logger.level == stdlib_logging.INFO def test_create_logger_with_warning_level(self): """Contract: WARNING level is set correctly.""" logger = Log.create_logger("test_logger", log_level=LogLevels.WARNING) - + assert logger._logger.level == stdlib_logging.WARNING def test_create_logger_with_error_level(self): """Contract: ERROR level is set correctly.""" logger = Log.create_logger("test_logger", log_level=LogLevels.ERROR) - + assert logger._logger.level == stdlib_logging.ERROR def test_create_logger_with_critical_level(self): """Contract: CRITICAL level is set correctly.""" logger = Log.create_logger("test_logger", log_level=LogLevels.CRITICAL) - + assert logger._logger.level == stdlib_logging.CRITICAL def test_create_logger_with_console_handler_default(self): @@ -72,7 +70,7 @@ def test_create_logger_with_console_handler_default(self): logger = Log.create_logger( "test_logger", log_level=LogLevels.INFO, - console_handler_type=ConsoleHandlers.DEFAULT + console_handler_type=ConsoleHandlers.DEFAULT, ) # Should have at least one handler @@ -83,7 +81,7 @@ def test_create_logger_with_console_handler_rich(self): logger = Log.create_logger( "test_logger", log_level=LogLevels.INFO, - console_handler_type=ConsoleHandlers.RICH + console_handler_type=ConsoleHandlers.RICH, ) # Should have at least one handler @@ -100,11 +98,8 @@ def test_create_logger_with_config_object(self, basic_log_config): def test_create_logger_config_override_with_params(self, basic_log_config): """Contract: Individual parameters override LogConfig values.""" # Config has INFO, but we override with DEBUG - logger = Log.create_logger( - config=basic_log_config, - log_level=LogLevels.DEBUG - ) - + logger = Log.create_logger(config=basic_log_config, log_level=LogLevels.DEBUG) + assert logger._logger.level == stdlib_logging.DEBUG def test_create_logger_stores_configurator(self): @@ -121,9 +116,7 @@ def test_create_logger_with_rich_features(self): ) logger = Log.create_logger( - "test_logger", - log_level=LogLevels.INFO, - rich_features=rich_settings + "test_logger", log_level=LogLevels.INFO, rich_features=rich_settings ) assert logger._rich_settings.enabled is True @@ -192,9 +185,7 @@ def test_update_config_override_with_params(self, basic_log_config): # Update with config (INFO) but override with DEBUG logger = Log.update( - "test_logger", - config=basic_log_config, - log_level=LogLevels.DEBUG + "test_logger", config=basic_log_config, log_level=LogLevels.DEBUG ) assert logger._logger.level == stdlib_logging.DEBUG @@ -213,15 +204,12 @@ def test_update_replaces_handlers(self): logger1 = Log.create_logger( "test_logger", log_level=LogLevels.INFO, - console_handler_type=ConsoleHandlers.DEFAULT + console_handler_type=ConsoleHandlers.DEFAULT, ) initial_handler_count = len(logger1._logger.handlers) # Update with RICH handler - logger2 = Log.update( - "test_logger", - console_handler_type=ConsoleHandlers.RICH - ) + logger2 = Log.update("test_logger", console_handler_type=ConsoleHandlers.RICH) # Should still have handlers (replaced, not added) assert len(logger2._logger.handlers) >= initial_handler_count @@ -255,4 +243,3 @@ def test_update_multiple_times(self): # Third update logger3 = Log.update("test_logger", log_level=LogLevels.ERROR) assert logger3._logger.level == stdlib_logging.ERROR - diff --git a/packages/logging/tests/contract/test_log_level_utils.py b/packages/logging/tests/contract/test_log_level_utils.py index 764676e..38c46c8 100644 --- a/packages/logging/tests/contract/test_log_level_utils.py +++ b/packages/logging/tests/contract/test_log_level_utils.py @@ -15,9 +15,9 @@ from rich_logging import ( LogLevels, + get_log_level_from_verbosity, parse_log_level, validate_log_level_string, - get_log_level_from_verbosity, ) @@ -163,4 +163,3 @@ def test_parse_verbosity_priority(self): result = parse_log_level("critical", verbosity=3, fallback=LogLevels.INFO) # Verbosity 3 = DEBUG assert result == LogLevels.DEBUG - diff --git a/packages/logging/tests/contract/test_rich_logger_api.py b/packages/logging/tests/contract/test_rich_logger_api.py index 0bec9bc..c9ae055 100644 --- a/packages/logging/tests/contract/test_rich_logger_api.py +++ b/packages/logging/tests/contract/test_rich_logger_api.py @@ -11,14 +11,12 @@ """ import logging as stdlib_logging -from unittest.mock import Mock, patch, MagicMock -import pytest +from unittest.mock import MagicMock, Mock, patch from rich_logging import ( + ConsoleHandlers, Log, LogLevels, - ConsoleHandlers, - RichLogger, RichFeatureSettings, ) @@ -29,64 +27,64 @@ class TestRichLoggerStandardLogging: def test_info_delegates_to_stdlib_logger(self): """Contract: info() delegates to stdlib logger.""" logger = Log.create_logger("test", log_level=LogLevels.INFO) - - with patch.object(logger._logger, 'info') as mock_info: + + with patch.object(logger._logger, "info") as mock_info: logger.info("test message") mock_info.assert_called_once_with("test message") def test_debug_delegates_to_stdlib_logger(self): """Contract: debug() delegates to stdlib logger.""" logger = Log.create_logger("test", log_level=LogLevels.DEBUG) - - with patch.object(logger._logger, 'debug') as mock_debug: + + with patch.object(logger._logger, "debug") as mock_debug: logger.debug("debug message") mock_debug.assert_called_once_with("debug message") def test_warning_delegates_to_stdlib_logger(self): """Contract: warning() delegates to stdlib logger.""" logger = Log.create_logger("test", log_level=LogLevels.INFO) - - with patch.object(logger._logger, 'warning') as mock_warning: + + with patch.object(logger._logger, "warning") as mock_warning: logger.warning("warning message") mock_warning.assert_called_once_with("warning message") def test_error_delegates_to_stdlib_logger(self): """Contract: error() delegates to stdlib logger.""" logger = Log.create_logger("test", log_level=LogLevels.INFO) - - with patch.object(logger._logger, 'error') as mock_error: + + with patch.object(logger._logger, "error") as mock_error: logger.error("error message") mock_error.assert_called_once_with("error message") def test_critical_delegates_to_stdlib_logger(self): """Contract: critical() delegates to stdlib logger.""" logger = Log.create_logger("test", log_level=LogLevels.INFO) - - with patch.object(logger._logger, 'critical') as mock_critical: + + with patch.object(logger._logger, "critical") as mock_critical: logger.critical("critical message") mock_critical.assert_called_once_with("critical message") def test_exception_delegates_to_stdlib_logger(self): """Contract: exception() delegates to stdlib logger.""" logger = Log.create_logger("test", log_level=LogLevels.INFO) - - with patch.object(logger._logger, 'exception') as mock_exception: + + with patch.object(logger._logger, "exception") as mock_exception: logger.exception("exception message") mock_exception.assert_called_once_with("exception message") def test_log_delegates_to_stdlib_logger(self): """Contract: log() delegates to stdlib logger.""" logger = Log.create_logger("test", log_level=LogLevels.INFO) - - with patch.object(logger._logger, 'log') as mock_log: + + with patch.object(logger._logger, "log") as mock_log: logger.log(stdlib_logging.INFO, "log message") mock_log.assert_called_once_with(stdlib_logging.INFO, "log message") def test_setLevel_delegates_to_stdlib_logger(self): """Contract: setLevel() delegates to stdlib logger.""" logger = Log.create_logger("test", log_level=LogLevels.INFO) - - with patch.object(logger._logger, 'setLevel') as mock_setLevel: + + with patch.object(logger._logger, "setLevel") as mock_setLevel: logger.setLevel(stdlib_logging.DEBUG) mock_setLevel.assert_called_once_with(stdlib_logging.DEBUG) @@ -94,8 +92,8 @@ def test_addHandler_delegates_to_stdlib_logger(self): """Contract: addHandler() delegates to stdlib logger.""" logger = Log.create_logger("test", log_level=LogLevels.INFO) handler = stdlib_logging.StreamHandler() - - with patch.object(logger._logger, 'addHandler') as mock_addHandler: + + with patch.object(logger._logger, "addHandler") as mock_addHandler: logger.addHandler(handler) mock_addHandler.assert_called_once_with(handler) @@ -103,8 +101,8 @@ def test_removeHandler_delegates_to_stdlib_logger(self): """Contract: removeHandler() delegates to stdlib logger.""" logger = Log.create_logger("test", log_level=LogLevels.INFO) handler = stdlib_logging.StreamHandler() - - with patch.object(logger._logger, 'removeHandler') as mock_removeHandler: + + with patch.object(logger._logger, "removeHandler") as mock_removeHandler: logger.removeHandler(handler) mock_removeHandler.assert_called_once_with(handler) @@ -115,34 +113,34 @@ class TestRichLoggerProperties: def test_name_property_returns_logger_name(self): """Contract: name property returns logger name.""" logger = Log.create_logger("test_logger", log_level=LogLevels.INFO) - + assert logger.name == "test_logger" def test_name_property_for_root_logger(self): """Contract: name property returns 'root' for root logger.""" logger = Log.create_logger(None, log_level=LogLevels.INFO) - + assert logger.name == "root" def test_logger_has_rich_settings(self): """Contract: RichLogger has _rich_settings attribute.""" logger = Log.create_logger("test", log_level=LogLevels.INFO) - - assert hasattr(logger, '_rich_settings') + + assert hasattr(logger, "_rich_settings") assert isinstance(logger._rich_settings, RichFeatureSettings) def test_logger_has_wrapped_logger(self): """Contract: RichLogger has _logger attribute.""" logger = Log.create_logger("test", log_level=LogLevels.INFO) - assert hasattr(logger, '_logger') + assert hasattr(logger, "_logger") assert isinstance(logger._logger, stdlib_logging.Logger) class TestRichLoggerDisplayMethods: """Contract tests for Rich display methods.""" - @patch('rich_logging.rich.rich_logger.console_manager') + @patch("rich_logging.rich.rich_logger.console_manager") def test_table_with_list_data(self, mock_console_manager): """Contract: table() accepts list of rows.""" mock_console = Mock() @@ -152,7 +150,7 @@ def test_table_with_list_data(self, mock_console_manager): "test", log_level=LogLevels.INFO, console_handler_type=ConsoleHandlers.RICH, - rich_features=RichFeatureSettings(enabled=True) + rich_features=RichFeatureSettings(enabled=True), ) data = [["Alice", "30"], ["Bob", "25"]] @@ -161,7 +159,7 @@ def test_table_with_list_data(self, mock_console_manager): # Should call console.print with a Table assert mock_console.print.called - @patch('rich_logging.rich.rich_logger.console_manager') + @patch("rich_logging.rich.rich_logger.console_manager") def test_table_with_dict_data(self, mock_console_manager): """Contract: table() accepts dict of columns.""" mock_console = Mock() @@ -171,7 +169,7 @@ def test_table_with_dict_data(self, mock_console_manager): "test", log_level=LogLevels.INFO, console_handler_type=ConsoleHandlers.RICH, - rich_features=RichFeatureSettings(enabled=True) + rich_features=RichFeatureSettings(enabled=True), ) data = {"Name": ["Alice", "Bob"], "Age": ["30", "25"]} @@ -179,7 +177,7 @@ def test_table_with_dict_data(self, mock_console_manager): assert mock_console.print.called - @patch('rich_logging.rich.rich_logger.console_manager') + @patch("rich_logging.rich.rich_logger.console_manager") def test_panel_displays_message(self, mock_console_manager): """Contract: panel() displays a panel with message.""" mock_console = Mock() @@ -189,14 +187,14 @@ def test_panel_displays_message(self, mock_console_manager): "test", log_level=LogLevels.INFO, console_handler_type=ConsoleHandlers.RICH, - rich_features=RichFeatureSettings(enabled=True) + rich_features=RichFeatureSettings(enabled=True), ) logger.panel("Test message", title="Test") assert mock_console.print.called - @patch('rich_logging.rich.rich_logger.console_manager') + @patch("rich_logging.rich.rich_logger.console_manager") def test_rule_displays_separator(self, mock_console_manager): """Contract: rule() displays a horizontal rule.""" mock_console = Mock() @@ -206,14 +204,14 @@ def test_rule_displays_separator(self, mock_console_manager): "test", log_level=LogLevels.INFO, console_handler_type=ConsoleHandlers.RICH, - rich_features=RichFeatureSettings(enabled=True) + rich_features=RichFeatureSettings(enabled=True), ) logger.rule("Section Title") assert mock_console.print.called - @patch('rich_logging.rich.rich_logger.console_manager') + @patch("rich_logging.rich.rich_logger.console_manager") def test_tree_with_dict_data(self, mock_console_manager): """Contract: tree() accepts dict data.""" mock_console = Mock() @@ -223,7 +221,7 @@ def test_tree_with_dict_data(self, mock_console_manager): "test", log_level=LogLevels.INFO, console_handler_type=ConsoleHandlers.RICH, - rich_features=RichFeatureSettings(enabled=True) + rich_features=RichFeatureSettings(enabled=True), ) data = {"folder": {"file1.txt": "content", "file2.txt": "content"}} @@ -231,7 +229,7 @@ def test_tree_with_dict_data(self, mock_console_manager): assert mock_console.print.called - @patch('rich_logging.rich.rich_logger.console_manager') + @patch("rich_logging.rich.rich_logger.console_manager") def test_syntax_displays_code(self, mock_console_manager): """Contract: syntax() displays syntax-highlighted code.""" mock_console = Mock() @@ -241,14 +239,14 @@ def test_syntax_displays_code(self, mock_console_manager): "test", log_level=LogLevels.INFO, console_handler_type=ConsoleHandlers.RICH, - rich_features=RichFeatureSettings(enabled=True) + rich_features=RichFeatureSettings(enabled=True), ) logger.syntax("print('hello')", "python") assert mock_console.print.called - @patch('rich_logging.rich.rich_logger.console_manager') + @patch("rich_logging.rich.rich_logger.console_manager") def test_markdown_displays_formatted_text(self, mock_console_manager): """Contract: markdown() displays markdown-formatted text.""" mock_console = Mock() @@ -258,14 +256,14 @@ def test_markdown_displays_formatted_text(self, mock_console_manager): "test", log_level=LogLevels.INFO, console_handler_type=ConsoleHandlers.RICH, - rich_features=RichFeatureSettings(enabled=True) + rich_features=RichFeatureSettings(enabled=True), ) logger.markdown("# Title\n\nParagraph") assert mock_console.print.called - @patch('rich_logging.rich.rich_logger.console_manager') + @patch("rich_logging.rich.rich_logger.console_manager") def test_json_displays_formatted_json(self, mock_console_manager): """Contract: json() displays formatted JSON.""" mock_console = Mock() @@ -275,7 +273,7 @@ def test_json_displays_formatted_json(self, mock_console_manager): "test", log_level=LogLevels.INFO, console_handler_type=ConsoleHandlers.RICH, - rich_features=RichFeatureSettings(enabled=True) + rich_features=RichFeatureSettings(enabled=True), ) logger.json({"key": "value"}) @@ -286,7 +284,7 @@ def test_json_displays_formatted_json(self, mock_console_manager): class TestRichLoggerContextManagers: """Contract tests for Rich context managers.""" - @patch('rich_logging.rich.rich_logger.console_manager') + @patch("rich_logging.rich.rich_logger.console_manager") def test_progress_context_manager(self, mock_console_manager): """Contract: progress() returns a context manager.""" mock_console = MagicMock() @@ -298,14 +296,14 @@ def test_progress_context_manager(self, mock_console_manager): "test", log_level=LogLevels.INFO, console_handler_type=ConsoleHandlers.RICH, - rich_features=RichFeatureSettings(enabled=True) + rich_features=RichFeatureSettings(enabled=True), ) with logger.progress("Processing", total=100) as progress: # Should return a progress object (or dummy) assert progress is not None - @patch('rich_logging.rich.rich_logger.console_manager') + @patch("rich_logging.rich.rich_logger.console_manager") def test_status_context_manager(self, mock_console_manager): """Contract: status() returns a context manager.""" mock_console = MagicMock() @@ -317,14 +315,14 @@ def test_status_context_manager(self, mock_console_manager): "test", log_level=LogLevels.INFO, console_handler_type=ConsoleHandlers.RICH, - rich_features=RichFeatureSettings(enabled=True) + rich_features=RichFeatureSettings(enabled=True), ) with logger.status("Loading...") as status: # Should return a status object (or dummy) assert status is not None - @patch('rich_logging.rich.rich_logger.console_manager') + @patch("rich_logging.rich.rich_logger.console_manager") def test_live_context_manager(self, mock_console_manager): """Contract: live() returns a context manager.""" mock_console = MagicMock() @@ -336,10 +334,11 @@ def test_live_context_manager(self, mock_console_manager): "test", log_level=LogLevels.INFO, console_handler_type=ConsoleHandlers.RICH, - rich_features=RichFeatureSettings(enabled=True) + rich_features=RichFeatureSettings(enabled=True), ) from rich.table import Table + table = Table() with logger.live(table): @@ -351,7 +350,7 @@ def test_progress_fallback_when_rich_disabled(self): logger = Log.create_logger( "test", log_level=LogLevels.INFO, - rich_features=RichFeatureSettings(enabled=False) + rich_features=RichFeatureSettings(enabled=False), ) with logger.progress("Processing") as progress: @@ -363,7 +362,7 @@ def test_status_fallback_when_rich_disabled(self): logger = Log.create_logger( "test", log_level=LogLevels.INFO, - rich_features=RichFeatureSettings(enabled=False) + rich_features=RichFeatureSettings(enabled=False), ) with logger.status("Loading...") as status: @@ -374,18 +373,16 @@ def test_status_fallback_when_rich_disabled(self): class TestRichLoggerTaskContext: """Contract tests for task context methods.""" - @patch('rich_logging.rich.rich_logger.LogContext') + @patch("rich_logging.rich.rich_logger.LogContext") def test_set_task_context(self, mock_log_context): """Contract: set_task_context() sets thread-local context.""" logger = Log.create_logger("test", log_level=LogLevels.INFO) logger.set_task_context("task1", "Task One") - mock_log_context.set_task_context.assert_called_once_with( - "task1", "Task One" - ) + mock_log_context.set_task_context.assert_called_once_with("task1", "Task One") - @patch('rich_logging.rich.rich_logger.LogContext') + @patch("rich_logging.rich.rich_logger.LogContext") def test_clear_task_context(self, mock_log_context): """Contract: clear_task_context() clears thread-local context.""" logger = Log.create_logger("test", log_level=LogLevels.INFO) @@ -394,7 +391,7 @@ def test_clear_task_context(self, mock_log_context): mock_log_context.clear_task_context.assert_called_once() - @patch('rich_logging.rich.rich_logger.LogContext') + @patch("rich_logging.rich.rich_logger.LogContext") def test_task_context_manager(self, mock_log_context): """Contract: task_context() is a context manager.""" logger = Log.create_logger("test", log_level=LogLevels.INFO) @@ -408,7 +405,7 @@ def test_task_context_manager(self, mock_log_context): # Context should be cleared after exiting mock_log_context.clear_task_context.assert_called_once() - @patch('rich_logging.rich.rich_logger.LogContext') + @patch("rich_logging.rich.rich_logger.LogContext") def test_task_context_clears_on_exception(self, mock_log_context): """Contract: task_context() clears context even on exception.""" logger = Log.create_logger("test", log_level=LogLevels.INFO) @@ -426,8 +423,8 @@ def test_task_context_clears_on_exception(self, mock_log_context): class TestRichLoggerInteractiveMethods: """Contract tests for interactive methods.""" - @patch('rich_logging.rich.rich_logger.Prompt') - @patch('rich_logging.rich.rich_logger.console_manager') + @patch("rich_logging.rich.rich_logger.Prompt") + @patch("rich_logging.rich.rich_logger.console_manager") def test_prompt_returns_user_input(self, mock_console_manager, mock_prompt): """Contract: prompt() returns user input.""" mock_console = Mock() @@ -438,15 +435,15 @@ def test_prompt_returns_user_input(self, mock_console_manager, mock_prompt): "test", log_level=LogLevels.INFO, console_handler_type=ConsoleHandlers.RICH, - rich_features=RichFeatureSettings(enabled=True) + rich_features=RichFeatureSettings(enabled=True), ) result = logger.prompt("Enter value") assert result == "user input" - @patch('rich_logging.rich.rich_logger.Confirm') - @patch('rich_logging.rich.rich_logger.console_manager') + @patch("rich_logging.rich.rich_logger.Confirm") + @patch("rich_logging.rich.rich_logger.console_manager") def test_confirm_returns_boolean(self, mock_console_manager, mock_confirm): """Contract: confirm() returns boolean.""" mock_console = Mock() @@ -457,7 +454,7 @@ def test_confirm_returns_boolean(self, mock_console_manager, mock_confirm): "test", log_level=LogLevels.INFO, console_handler_type=ConsoleHandlers.RICH, - rich_features=RichFeatureSettings(enabled=True) + rich_features=RichFeatureSettings(enabled=True), ) result = logger.confirm("Continue?") @@ -469,7 +466,7 @@ def test_prompt_returns_default_when_rich_disabled(self): logger = Log.create_logger( "test", log_level=LogLevels.INFO, - rich_features=RichFeatureSettings(enabled=False) + rich_features=RichFeatureSettings(enabled=False), ) result = logger.prompt("Enter value", default="default_value") @@ -481,7 +478,7 @@ def test_confirm_returns_default_when_rich_disabled(self): logger = Log.create_logger( "test", log_level=LogLevels.INFO, - rich_features=RichFeatureSettings(enabled=False) + rich_features=RichFeatureSettings(enabled=False), ) result = logger.confirm("Continue?", default=False) @@ -497,7 +494,7 @@ def test_table_does_not_raise_when_rich_disabled(self): logger = Log.create_logger( "test", log_level=LogLevels.INFO, - rich_features=RichFeatureSettings(enabled=False) + rich_features=RichFeatureSettings(enabled=False), ) # Should not raise @@ -508,7 +505,7 @@ def test_panel_does_not_raise_when_rich_disabled(self): logger = Log.create_logger( "test", log_level=LogLevels.INFO, - rich_features=RichFeatureSettings(enabled=False) + rich_features=RichFeatureSettings(enabled=False), ) # Should not raise @@ -519,7 +516,7 @@ def test_rule_does_not_raise_when_rich_disabled(self): logger = Log.create_logger( "test", log_level=LogLevels.INFO, - rich_features=RichFeatureSettings(enabled=False) + rich_features=RichFeatureSettings(enabled=False), ) # Should not raise @@ -530,7 +527,7 @@ def test_tree_does_not_raise_when_rich_disabled(self): logger = Log.create_logger( "test", log_level=LogLevels.INFO, - rich_features=RichFeatureSettings(enabled=False) + rich_features=RichFeatureSettings(enabled=False), ) # Should not raise @@ -541,7 +538,7 @@ def test_syntax_does_not_raise_when_rich_disabled(self): logger = Log.create_logger( "test", log_level=LogLevels.INFO, - rich_features=RichFeatureSettings(enabled=False) + rich_features=RichFeatureSettings(enabled=False), ) # Should not raise @@ -552,7 +549,7 @@ def test_markdown_does_not_raise_when_rich_disabled(self): logger = Log.create_logger( "test", log_level=LogLevels.INFO, - rich_features=RichFeatureSettings(enabled=False) + rich_features=RichFeatureSettings(enabled=False), ) # Should not raise @@ -563,9 +560,8 @@ def test_json_does_not_raise_when_rich_disabled(self): logger = Log.create_logger( "test", log_level=LogLevels.INFO, - rich_features=RichFeatureSettings(enabled=False) + rich_features=RichFeatureSettings(enabled=False), ) # Should not raise logger.json({"key": "value"}) - diff --git a/packages/logging/tests/integration/test_logger_lifecycle.py b/packages/logging/tests/integration/test_logger_lifecycle.py index a356d25..c22b7cb 100644 --- a/packages/logging/tests/integration/test_logger_lifecycle.py +++ b/packages/logging/tests/integration/test_logger_lifecycle.py @@ -9,19 +9,12 @@ """ import logging as stdlib_logging -import tempfile -from pathlib import Path -import pytest from rich_logging import ( + ConsoleHandlers, Log, LogLevels, - ConsoleHandlers, - LogFormatters, - LogFormatterStyleChoices, RichFeatureSettings, - FileHandlerSpec, - FileHandlerTypes, ) @@ -31,14 +24,14 @@ class TestLoggerCreationAndLogging: def test_create_logger_and_log_messages(self, caplog): """Integration: Create logger and log messages at different levels.""" logger = Log.create_logger("test_app", log_level=LogLevels.DEBUG) - + with caplog.at_level(stdlib_logging.DEBUG, logger="test_app"): logger.debug("Debug message") logger.info("Info message") logger.warning("Warning message") logger.error("Error message") logger.critical("Critical message") - + # Verify all messages were logged assert "Debug message" in caplog.text assert "Info message" in caplog.text @@ -69,12 +62,12 @@ def test_logger_with_rich_handler(self, caplog): "test_app", log_level=LogLevels.INFO, console_handler_type=ConsoleHandlers.RICH, - rich_features=RichFeatureSettings(enabled=True) + rich_features=RichFeatureSettings(enabled=True), ) - + with caplog.at_level(stdlib_logging.INFO, logger="test_app"): logger.info("Test message") - + assert "Test message" in caplog.text @@ -110,15 +103,15 @@ def test_update_logger_level(self, caplog): def test_update_logger_multiple_times(self, caplog): """Integration: Update logger configuration multiple times.""" logger = Log.create_logger("test_app", log_level=LogLevels.INFO) - + # First update logger = Log.update("test_app", log_level=LogLevels.DEBUG) assert logger._logger.level == stdlib_logging.DEBUG - + # Second update logger = Log.update("test_app", log_level=LogLevels.WARNING) assert logger._logger.level == stdlib_logging.WARNING - + # Third update logger = Log.update("test_app", log_level=LogLevels.ERROR) assert logger._logger.level == stdlib_logging.ERROR @@ -131,17 +124,17 @@ def test_multiple_loggers_independent(self, caplog): """Integration: Multiple loggers operate independently.""" logger1 = Log.create_logger("app1", log_level=LogLevels.DEBUG) logger2 = Log.create_logger("app2", log_level=LogLevels.WARNING) - + with caplog.at_level(stdlib_logging.DEBUG): logger1.debug("App1 debug") logger1.info("App1 info") logger2.debug("App2 debug") logger2.warning("App2 warning") - + # Logger1 should log debug and info assert "App1 debug" in caplog.text assert "App1 info" in caplog.text - + # Logger2 should only log warning (debug filtered) assert "App2 debug" not in caplog.text assert "App2 warning" in caplog.text @@ -151,19 +144,16 @@ def test_multiple_loggers_different_handlers(self): logger1 = Log.create_logger( "app1", log_level=LogLevels.INFO, - console_handler_type=ConsoleHandlers.DEFAULT + console_handler_type=ConsoleHandlers.DEFAULT, ) logger2 = Log.create_logger( - "app2", - log_level=LogLevels.INFO, - console_handler_type=ConsoleHandlers.RICH + "app2", log_level=LogLevels.INFO, console_handler_type=ConsoleHandlers.RICH ) - + # Both should have handlers assert len(logger1._logger.handlers) > 0 assert len(logger2._logger.handlers) > 0 - + # They should be independent assert logger1.name == "app1" assert logger2.name == "app2" - diff --git a/packages/logging/tests/integration/test_rich_features.py b/packages/logging/tests/integration/test_rich_features.py index a59a19e..ef454c7 100644 --- a/packages/logging/tests/integration/test_rich_features.py +++ b/packages/logging/tests/integration/test_rich_features.py @@ -8,13 +8,12 @@ - Graceful degradation when Rich disabled """ -import pytest from unittest.mock import patch from rich_logging import ( + ConsoleHandlers, Log, LogLevels, - ConsoleHandlers, RichFeatureSettings, ) @@ -28,13 +27,12 @@ def test_table_displays_data(self): "test_app", log_level=LogLevels.INFO, console_handler_type=ConsoleHandlers.RICH, - rich_features=RichFeatureSettings(enabled=True) + rich_features=RichFeatureSettings(enabled=True), ) # Should not raise - first row is headers when show_header=True logger.table( - [["Name", "Age"], ["Alice", "30"], ["Bob", "25"]], - show_header=True + [["Name", "Age"], ["Alice", "30"], ["Bob", "25"]], show_header=True ) def test_panel_displays_message(self): @@ -43,7 +41,7 @@ def test_panel_displays_message(self): "test_app", log_level=LogLevels.INFO, console_handler_type=ConsoleHandlers.RICH, - rich_features=RichFeatureSettings(enabled=True) + rich_features=RichFeatureSettings(enabled=True), ) # Should not raise @@ -55,7 +53,7 @@ def test_rule_displays_separator(self): "test_app", log_level=LogLevels.INFO, console_handler_type=ConsoleHandlers.RICH, - rich_features=RichFeatureSettings(enabled=True) + rich_features=RichFeatureSettings(enabled=True), ) # Should not raise @@ -67,7 +65,7 @@ def test_syntax_displays_code(self): "test_app", log_level=LogLevels.INFO, console_handler_type=ConsoleHandlers.RICH, - rich_features=RichFeatureSettings(enabled=True) + rich_features=RichFeatureSettings(enabled=True), ) # Should not raise @@ -79,7 +77,7 @@ def test_markdown_displays_formatted_text(self): "test_app", log_level=LogLevels.INFO, console_handler_type=ConsoleHandlers.RICH, - rich_features=RichFeatureSettings(enabled=True) + rich_features=RichFeatureSettings(enabled=True), ) # Should not raise @@ -91,7 +89,7 @@ def test_json_displays_formatted_json(self): "test_app", log_level=LogLevels.INFO, console_handler_type=ConsoleHandlers.RICH, - rich_features=RichFeatureSettings(enabled=True) + rich_features=RichFeatureSettings(enabled=True), ) # Should not raise @@ -101,10 +99,11 @@ def test_json_displays_formatted_json(self): class TestRichContextManagers: """Integration tests for Rich context managers.""" - @patch('rich_logging.rich.rich_logger.console_manager') + @patch("rich_logging.rich.rich_logger.console_manager") def test_progress_context_manager_workflow(self, mock_console_manager): """Integration: progress() context manager workflow.""" from unittest.mock import MagicMock + mock_console = MagicMock() mock_console.__enter__ = MagicMock(return_value=mock_console) mock_console.__exit__ = MagicMock(return_value=False) @@ -114,7 +113,7 @@ def test_progress_context_manager_workflow(self, mock_console_manager): "test_app", log_level=LogLevels.INFO, console_handler_type=ConsoleHandlers.RICH, - rich_features=RichFeatureSettings(enabled=True) + rich_features=RichFeatureSettings(enabled=True), ) # Should work as context manager @@ -126,13 +125,13 @@ def test_progress_fallback_when_disabled(self): logger = Log.create_logger( "test_app", log_level=LogLevels.INFO, - rich_features=RichFeatureSettings(enabled=False) + rich_features=RichFeatureSettings(enabled=False), ) # Should return dummy progress with logger.progress("Processing", total=100) as progress: # Dummy progress should have add_task method - assert hasattr(progress, 'add_task') + assert hasattr(progress, "add_task") class TestTaskContext: @@ -147,4 +146,3 @@ def test_task_context_workflow(self): # Clear task context logger.clear_task_context() - diff --git a/packages/logging/tests/integration_test.py b/packages/logging/tests/integration_test.py index fbf1fff..118c4ee 100644 --- a/packages/logging/tests/integration_test.py +++ b/packages/logging/tests/integration_test.py @@ -65,17 +65,13 @@ def test_all_rich_features(): # immediately) from rich.panel import Panel - system_panel = Panel( - "OS: Linux\nShell: zsh\nTerminal: kitty", title="System Info" - ) + system_panel = Panel("OS: Linux\nShell: zsh\nTerminal: kitty", title="System Info") progress_panel = Panel( "✓ Backup complete\n✓ Files copied\n⧗ Configuring...", title="Progress" ) - next_panel = Panel( - "• Restart shell\n• Test config\n• Enjoy!", title="Next Steps" - ) + next_panel = Panel("• Restart shell\n• Test config\n• Enjoy!", title="Next Steps") logger.columns(system_panel, progress_panel, next_panel, equal=True) @@ -91,9 +87,7 @@ def install_dotfiles(): return True ''' - logger.syntax( - python_code, lexer="python", title="install.py", line_numbers=True - ) + logger.syntax(python_code, lexer="python", title="install.py", line_numbers=True) bash_script = """#!/bin/bash echo "Installing dotfiles..." @@ -163,9 +157,7 @@ def install_dotfiles(): # Test text styling and alignment logger.rule("Text Styling Test") - logger.text( - "🎉 Installation Successful! 🎉", style="bold green", justify="center" - ) + logger.text("🎉 Installation Successful! 🎉", style="bold green", justify="center") logger.text("Warning: Some files were skipped", style="yellow") logger.align("Centered important message", "center", style="bold blue") diff --git a/packages/logging/tests/interactive_demo.py b/packages/logging/tests/interactive_demo.py index deb3a98..4268da7 100644 --- a/packages/logging/tests/interactive_demo.py +++ b/packages/logging/tests/interactive_demo.py @@ -2,7 +2,6 @@ """Interactive demo for Rich features in the logging module.""" import sys - from logging import ( ConsoleHandlers, Log, @@ -23,9 +22,7 @@ def interactive_demo(): handler_config=RichHandlerSettings( show_time=True, show_path=False, markup=True, rich_tracebacks=True ), - rich_features=RichFeatureSettings( - enabled=True, panel_border_style="blue" - ), + rich_features=RichFeatureSettings(enabled=True, panel_border_style="blue"), ) logger.panel( @@ -82,9 +79,7 @@ def interactive_demo(): logger.markdown(summary) - final_confirm = logger.confirm( - "Proceed with these settings?", default=True - ) + final_confirm = logger.confirm("Proceed with these settings?", default=True) if final_confirm: logger.panel( @@ -100,9 +95,7 @@ def interactive_demo(): border_style="yellow", ) else: - logger.text( - "Demo ended early. Thanks for trying it out!", style="yellow" - ) + logger.text("Demo ended early. Thanks for trying it out!", style="yellow") except KeyboardInterrupt: logger.panel( @@ -111,15 +104,11 @@ def interactive_demo(): border_style="red", ) except Exception as e: - logger.panel( - f"An error occurred: {e}", title="Error", border_style="red" - ) + logger.panel(f"An error occurred: {e}", title="Error", border_style="red") if __name__ == "__main__": - print( - "Note: This is an interactive demo. Run it manually to test prompts." - ) + print("Note: This is an interactive demo. Run it manually to test prompts.") print("For automated testing, use the integration_test.py instead.") # Only run interactively if explicitly requested diff --git a/packages/logging/tests/test_rich_features.py b/packages/logging/tests/test_rich_features.py index 7ab2348..623648a 100644 --- a/packages/logging/tests/test_rich_features.py +++ b/packages/logging/tests/test_rich_features.py @@ -1,7 +1,6 @@ """Tests for Rich features in the logging module.""" import logging -import rich_logging import unittest from unittest.mock import Mock, patch @@ -149,9 +148,7 @@ def test_text_styling(self, mock_console_manager): mock_console = Mock() mock_console_manager.get_console.return_value = mock_console - self.rich_logger.text( - "Test text", style="bold green", justify="center" - ) + self.rich_logger.text("Test text", style="bold green", justify="center") mock_console.print.assert_called_once() @@ -203,12 +200,8 @@ def test_settings_defaults_used(self): rich_logger = RichLogger(self.mock_logger, custom_settings) # Verify settings are stored - self.assertEqual( - rich_logger._rich_settings.tree_guide_style, "bold blue" - ) - self.assertEqual( - rich_logger._rich_settings.syntax_theme, "github-dark" - ) + self.assertEqual(rich_logger._rich_settings.tree_guide_style, "bold blue") + self.assertEqual(rich_logger._rich_settings.syntax_theme, "github-dark") self.assertEqual(rich_logger._rich_settings.json_indent, 4) diff --git a/packages/logging/tests/test_rich_interactive.py b/packages/logging/tests/test_rich_interactive.py index c7f8308..0e3a257 100644 --- a/packages/logging/tests/test_rich_interactive.py +++ b/packages/logging/tests/test_rich_interactive.py @@ -1,7 +1,6 @@ """Tests for Rich interactive features (prompts, live updates).""" import logging -import rich_logging import unittest from unittest.mock import Mock, patch @@ -22,9 +21,7 @@ def setUp(self): @patch("rich_logging.rich.rich_logger.RICH_AVAILABLE", True) @patch("rich_logging.rich.rich_logger.console_manager") @patch("rich_logging.rich.rich_logger.Prompt") - def test_prompt_with_choices( - self, mock_prompt_class, mock_console_manager - ): + def test_prompt_with_choices(self, mock_prompt_class, mock_console_manager): """Test prompt functionality with choices.""" mock_console = Mock() mock_console_manager.get_console.return_value = mock_console @@ -74,17 +71,13 @@ def test_prompt_fallback_when_rich_unavailable(self): @patch("rich_logging.rich.rich_logger.RICH_AVAILABLE", True) @patch("rich_logging.rich.rich_logger.console_manager") @patch("rich_logging.rich.rich_logger.Confirm") - def test_confirm_functionality( - self, mock_confirm_class, mock_console_manager - ): + def test_confirm_functionality(self, mock_confirm_class, mock_console_manager): """Test confirm functionality.""" mock_console = Mock() mock_console_manager.get_console.return_value = mock_console mock_confirm_class.ask.return_value = True - result = self.rich_logger.confirm( - "Do you want to continue?", default=False - ) + result = self.rich_logger.confirm("Do you want to continue?", default=False) self.assertTrue(result) mock_confirm_class.ask.assert_called_once_with( @@ -107,16 +100,12 @@ def test_live_context_manager(self, mock_live_class, mock_console_manager): # Create a mock Live instance mock_live_instance = Mock() - mock_live_class.return_value.__enter__ = Mock( - return_value=mock_live_instance - ) + mock_live_class.return_value.__enter__ = Mock(return_value=mock_live_instance) mock_live_class.return_value.__exit__ = Mock(return_value=None) test_renderable = "Test content" - with self.rich_logger.live( - test_renderable, refresh_per_second=2 - ) as live: + with self.rich_logger.live(test_renderable, refresh_per_second=2) as live: self.assertEqual(live, mock_live_instance) # Verify Live was created with correct parameters @@ -213,9 +202,7 @@ def test_custom_settings_override_defaults(self): mock_console = Mock() mock_console_manager.get_console.return_value = mock_console - with patch( - "rich_logging.rich.rich_logger.Prompt" - ) as mock_prompt: + with patch("rich_logging.rich.rich_logger.Prompt") as mock_prompt: mock_prompt.ask.return_value = "test" # Method parameter should override setting diff --git a/packages/logging/tests/test_task_context.py b/packages/logging/tests/test_task_context.py index cec7967..348d09a 100644 --- a/packages/logging/tests/test_task_context.py +++ b/packages/logging/tests/test_task_context.py @@ -2,53 +2,58 @@ """Test task context functionality for parallel execution.""" import sys -import time import threading +import time from pathlib import Path # Add src to path sys.path.insert(0, str(Path(__file__).parent.parent / "src")) -from rich_logging.core.log_context import LogContext, set_task_context, get_task_context, clear_task_context -from rich_logging.filters.task_context_filter import TaskContextFilter import logging +from rich_logging.core.log_context import ( + clear_task_context, + get_task_context, + set_task_context, +) +from rich_logging.filters.task_context_filter import TaskContextFilter + def test_thread_local_context(): """Test that context is thread-local.""" print("\n" + "=" * 80) print("TEST 1: Thread-Local Context Storage") print("=" * 80) - + results = {} - + def worker(thread_id: str): # Set context for this thread set_task_context(f"task_{thread_id}", f"Task {thread_id}") time.sleep(0.1) # Simulate work - + # Get context - should be specific to this thread context = get_task_context() results[thread_id] = context - + clear_task_context() - + # Create multiple threads threads = [] for i in range(3): t = threading.Thread(target=worker, args=(str(i),)) threads.append(t) t.start() - + # Wait for all threads for t in threads: t.join() - + # Verify each thread had its own context print("\nResults:") for thread_id, context in results.items(): print(f" Thread {thread_id}: {context}") - + assert len(results) == 3 assert results["0"]["step_id"] == "task_0" assert results["1"]["step_id"] == "task_1" @@ -61,11 +66,11 @@ def test_task_context_filter(): print("\n" + "=" * 80) print("TEST 2: Task Context Filter") print("=" * 80) - + # Create a logger with the filter logger = logging.getLogger("test_logger") logger.setLevel(logging.DEBUG) - + # Create handler with our filter handler = logging.StreamHandler() task_filter = TaskContextFilter( @@ -76,20 +81,20 @@ def test_task_context_filter(): handler.addFilter(task_filter) handler.setFormatter(logging.Formatter("%(message)s")) logger.addHandler(handler) - + print("\nWithout task context:") logger.info("This is a normal log message") - + print("\nWith task context:") set_task_context("install_nodejs", "Install Node.js") logger.info("Installing package...") logger.debug("Checking dependencies...") logger.info("Installation complete!") clear_task_context() - + print("\nAfter clearing context:") logger.info("Back to normal logging") - + print("\n✓ Task context filter works correctly!") @@ -98,12 +103,12 @@ def test_parallel_logging(): print("\n" + "=" * 80) print("TEST 3: Parallel Logging with Task Context") print("=" * 80) - + # Create a shared logger logger = logging.getLogger("parallel_test") logger.setLevel(logging.INFO) logger.handlers.clear() - + handler = logging.StreamHandler() task_filter = TaskContextFilter( enabled=True, @@ -113,43 +118,43 @@ def test_parallel_logging(): handler.addFilter(task_filter) handler.setFormatter(logging.Formatter("%(levelname)-8s %(message)s")) logger.addHandler(handler) - + def install_package(package_name: str, duration: float): """Simulate package installation.""" step_id = f"install_{package_name.lower()}" set_task_context(step_id, f"Install {package_name}") - + try: logger.info(f"Starting {package_name} installation...") time.sleep(duration * 0.3) - + logger.info(f"Downloading {package_name}...") time.sleep(duration * 0.4) - + logger.info(f"✓ {package_name} installed successfully!") time.sleep(duration * 0.3) finally: clear_task_context() - + print("\nSimulating parallel package installations:\n") - + # Create threads for parallel execution packages = [ ("Node.js", 0.5), ("Python", 0.4), ("Rust", 0.6), ] - + threads = [] for package, duration in packages: t = threading.Thread(target=install_package, args=(package, duration)) threads.append(t) t.start() - + # Wait for all threads for t in threads: t.join() - + print("\n✓ Parallel logging with task context works correctly!") print(" Notice how each log line is prefixed with the task identifier,") print(" making it easy to identify which task produced which log line.") @@ -160,11 +165,11 @@ def main(): print("\n" + "=" * 80) print("TASK CONTEXT FUNCTIONALITY TESTS") print("=" * 80) - + test_thread_local_context() test_task_context_filter() test_parallel_logging() - + print("\n" + "=" * 80) print("ALL TESTS PASSED!") print("=" * 80) @@ -174,4 +179,3 @@ def main(): if __name__ == "__main__": main() - diff --git a/packages/logging/tests/unit/test_formatter_factory.py b/packages/logging/tests/unit/test_formatter_factory.py index 0882192..df386d7 100644 --- a/packages/logging/tests/unit/test_formatter_factory.py +++ b/packages/logging/tests/unit/test_formatter_factory.py @@ -5,13 +5,14 @@ """ import logging as stdlib_logging + import pytest -from rich_logging.formatters import FormatterFactory from rich_logging.core.log_types import ( LogFormatters, LogFormatterStyleChoices, ) +from rich_logging.formatters import FormatterFactory class TestFormatterFactory: @@ -22,7 +23,7 @@ def test_create_default_formatter(self): formatter = FormatterFactory.create( LogFormatters.DEFAULT, format_str="%(levelname)s - %(message)s", - style=LogFormatterStyleChoices.PERCENT + style=LogFormatterStyleChoices.PERCENT, ) assert isinstance(formatter, stdlib_logging.Formatter) @@ -34,7 +35,7 @@ def test_create_colored_formatter(self): formatter = FormatterFactory.create( LogFormatters.COLORED, format_str="%(levelname)s - %(message)s", - style=LogFormatterStyleChoices.PERCENT + style=LogFormatterStyleChoices.PERCENT, ) assert isinstance(formatter, stdlib_logging.Formatter) @@ -44,7 +45,7 @@ def test_create_rich_formatter(self): formatter = FormatterFactory.create( LogFormatters.RICH, format_str="%(message)s", - style=LogFormatterStyleChoices.PERCENT + style=LogFormatterStyleChoices.PERCENT, ) # Rich formatter is a RichFormatter instance @@ -55,7 +56,7 @@ def test_create_formatter_with_brace_style(self): formatter = FormatterFactory.create( LogFormatters.DEFAULT, format_str="{levelname} - {message}", - style=LogFormatterStyleChoices.BRACE + style=LogFormatterStyleChoices.BRACE, ) assert isinstance(formatter, stdlib_logging.Formatter) @@ -66,7 +67,7 @@ def test_create_formatter_with_dollar_style(self): formatter = FormatterFactory.create( LogFormatters.DEFAULT, format_str="$levelname - $message", - style=LogFormatterStyleChoices.DOLLAR + style=LogFormatterStyleChoices.DOLLAR, ) assert isinstance(formatter, stdlib_logging.Formatter) @@ -81,6 +82,5 @@ def test_create_formatter_invalid_type_raises_error(self): FormatterFactory.create( "INVALID", # type: ignore format_str="%(message)s", - style=LogFormatterStyleChoices.PERCENT + style=LogFormatterStyleChoices.PERCENT, ) - diff --git a/packages/pipeline/Makefile b/packages/pipeline/Makefile deleted file mode 100644 index 683f078..0000000 --- a/packages/pipeline/Makefile +++ /dev/null @@ -1,98 +0,0 @@ -MAKEFLAGS += --no-print-directory - -# Standard Makefile for UV Python Projects -# Provides consistent development workflows - -.PHONY: help sync-check ensure-sync dev-shell format lint type-check test test-cov clean clean-venv install build pre-commit-install pre-commit-run all-checks - -help: ## Show this help message - @echo "Available commands:" - @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf " %-20s %s\n", $$1, $$2}' - -sync-check: ## Check if environment sync is needed - @if ! uv run python -c "import sys; sys.exit(0)" 2>/dev/null; then \ - echo "❌ Package not installed - sync needed"; \ - exit 1; \ - elif [ uv.lock -nt .venv/pyvenv.cfg ] 2>/dev/null; then \ - echo "❌ Lock file newer than venv - sync needed"; \ - exit 1; \ - elif [ pyproject.toml -nt .venv/pyvenv.cfg ] 2>/dev/null; then \ - echo "❌ pyproject.toml newer than venv - sync needed"; \ - exit 1; \ - else \ - echo "✅ Environment is up to date"; \ - fi - -ensure-sync: ## Ensure environment is synced - @$(MAKE) sync-check || (echo "Syncing environment..." && uv sync) - -dev-shell: ensure-sync ## Activate development shell - @echo "Activating virtual environment..." - @echo "You can leave the dev shell by typing 'exit'" - @bash -c "source .venv/bin/activate && exec bash" - -format: ensure-sync ## Format code with black and isort - @echo "Formatting code..." - @uv run black . - @uv run isort . - @echo "✅ Formatting complete" - -lint: ensure-sync ## Lint code with ruff - @echo "Linting code..." - @uv run ruff check --fix . - @echo "✅ Linting complete" - -type-check: ensure-sync ## Type check with mypy - @echo "Type checking..." - @uv run mypy . - @echo "✅ Type checking complete" - -test: ensure-sync ## Run tests - @echo "Running tests..." - @uv run pytest -v - @echo "✅ Tests complete" - -test-cov: ensure-sync ## Run tests with coverage - @echo "Running tests with coverage..." - @uv run pytest --cov --cov-report=html --cov-report=term - @echo "✅ Coverage report generated in htmlcov/" - -clean: ## Clean cache files and build artifacts - @echo "Cleaning cache files..." - @find . -type d -name "__pycache__" -exec rm -rf {} + 2>/dev/null || true - @find . -type f -name "*.pyc" -delete 2>/dev/null || true - @find . -type d -name ".pytest_cache" -exec rm -rf {} + 2>/dev/null || true - @find . -type d -name ".mypy_cache" -exec rm -rf {} + 2>/dev/null || true - @find . -type d -name ".ruff_cache" -exec rm -rf {} + 2>/dev/null || true - @find . -type d -name "*.egg-info" -exec rm -rf {} + 2>/dev/null || true - @rm -rf build/ dist/ htmlcov/ .coverage 2>/dev/null || true - @echo "✅ Cleanup complete" - -clean-venv: ## Remove virtual environment - @echo "Removing virtual environment..." - @rm -rf .venv - @echo "✅ Virtual environment removed" - @echo "Run 'make install' or 'uv sync' to recreate" - -install: ## Install project in editable mode - @echo "Installing project..." - @uv sync - @echo "✅ Installation complete" - -build: ensure-sync ## Build the package - @echo "Building package..." - @uv build - @echo "✅ Build complete" - -pre-commit-install: ensure-sync ## Install pre-commit hooks - @echo "Installing pre-commit hooks..." - @uv run pre-commit install - @echo "✅ Pre-commit hooks installed" - -pre-commit-run: ensure-sync ## Run pre-commit on all files - @echo "Running pre-commit on all files..." - @uv run pre-commit run --all-files - @echo "✅ Pre-commit checks complete" - -all-checks: format lint type-check test ## Run all checks (format, lint, type-check, test) - @echo "✅ All checks passed!" diff --git a/packages/pipeline/src/task_pipeline/core/types.py b/packages/pipeline/src/task_pipeline/core/types.py index adb9dfa..47ae5aa 100644 --- a/packages/pipeline/src/task_pipeline/core/types.py +++ b/packages/pipeline/src/task_pipeline/core/types.py @@ -6,7 +6,7 @@ from abc import ABC, abstractmethod from dataclasses import dataclass, field from enum import Enum -from typing import Any, Union +from typing import Any from rich_logging.rich.rich_logger import RichLogger @@ -26,9 +26,7 @@ class PipelineContext[AppConfig]: errors: list[Exception] = field(default_factory=list) # Internal progress tracking (set by Pipeline) - _progress_tracker: "ProgressTracker | None" = field( - default=None, repr=False - ) + _progress_tracker: ProgressTracker | None = field(default=None, repr=False) _current_step_id: str | None = field(default=None, repr=False) def update_step_progress(self, progress: float) -> None: @@ -48,13 +46,9 @@ def run(self, context): return context """ if self._progress_tracker and self._current_step_id: - self._progress_tracker.update_step_progress( - self._current_step_id, progress - ) + self._progress_tracker.update_step_progress(self._current_step_id, progress) - def __deepcopy__( - self, memo: dict[int, Any] - ) -> "PipelineContext[AppConfig]": + def __deepcopy__(self, memo: dict[int, Any]) -> PipelineContext[AppConfig]: """ Custom deep copy that handles non-picklable progress tracker. @@ -149,7 +143,7 @@ class PipelineConfig: # Type alias for pipeline steps -TaskStep = Union[PipelineStep, list[PipelineStep]] +TaskStep = PipelineStep | list[PipelineStep] class ProgressTracker: diff --git a/packages/pipeline/src/task_pipeline/executors/parallel_executor.py b/packages/pipeline/src/task_pipeline/executors/parallel_executor.py index 50bd9ef..82e635e 100644 --- a/packages/pipeline/src/task_pipeline/executors/parallel_executor.py +++ b/packages/pipeline/src/task_pipeline/executors/parallel_executor.py @@ -4,6 +4,7 @@ import copy from concurrent.futures import ThreadPoolExecutor, as_completed +from typing import Any from ..core.types import ( LogicOperator, @@ -31,8 +32,8 @@ def __init__(self, task_executor: TaskExecutor | None = None): def _execute_with_context( self, step: PipelineStep, - step_context: PipelineContext, - ) -> PipelineContext: + step_context: PipelineContext[Any], + ) -> PipelineContext[Any]: """Execute a step with task context set. Args: @@ -62,10 +63,10 @@ def _execute_with_context( def execute( self, steps: list[PipelineStep], - context: PipelineContext, + context: PipelineContext[Any], config: ParallelConfig, - progress_tracker: "ProgressTracker | None" = None, - ) -> PipelineContext: + progress_tracker: ProgressTracker | None = None, + ) -> PipelineContext[Any]: """ Execute a group of steps in parallel with context merging. @@ -123,16 +124,14 @@ def execute( raise RuntimeError("Parallel group failed") # Merge contexts from successful steps - merged_context = self._merge_contexts( - original_context, step_contexts - ) + merged_context = self._merge_contexts(original_context, step_contexts) return merged_context def _merge_contexts( self, - original_context: PipelineContext, - step_contexts: list[PipelineContext], - ) -> PipelineContext: + original_context: PipelineContext[Any], + step_contexts: list[PipelineContext[Any]], + ) -> PipelineContext[Any]: """ Merge contexts from parallel steps. @@ -203,8 +202,7 @@ def _merge_contexts( step_increment = value - original_value if step_increment > 0: # Only add positive increments merged.results[key] = ( - merged.results.get(key, original_value) - + step_increment + merged.results.get(key, original_value) + step_increment ) elif isinstance(value, dict): # For dict values, merge recursively @@ -227,9 +225,7 @@ def _merge_contexts( if hasattr(original_context, "errors") else 0 ) - new_errors = step_context.errors[ - original_error_len: - ] # Only new errors + new_errors = step_context.errors[original_error_len:] # Only new errors merged.errors.extend(new_errors) return merged diff --git a/packages/pipeline/src/task_pipeline/executors/pipeline_executor.py b/packages/pipeline/src/task_pipeline/executors/pipeline_executor.py index a8e2c9c..d4c7fc6 100644 --- a/packages/pipeline/src/task_pipeline/executors/pipeline_executor.py +++ b/packages/pipeline/src/task_pipeline/executors/pipeline_executor.py @@ -2,6 +2,8 @@ from __future__ import annotations +from typing import Any + from ..core.types import ( PipelineConfig, PipelineContext, @@ -36,9 +38,9 @@ def __init__( def execute( self, steps: list[TaskStep], - context: PipelineContext, + context: PipelineContext[Any], config: PipelineConfig, - ) -> PipelineContext: + ) -> PipelineContext[Any]: """ Execute pipeline steps and return final context. @@ -61,9 +63,7 @@ def execute( ) else: # Serial step - current_context = self.task_executor.execute( - step, current_context - ) + current_context = self.task_executor.execute(step, current_context) except Exception as e: if config.fail_fast: raise diff --git a/packages/pipeline/src/task_pipeline/executors/task_executor.py b/packages/pipeline/src/task_pipeline/executors/task_executor.py index de0658a..9506f1b 100644 --- a/packages/pipeline/src/task_pipeline/executors/task_executor.py +++ b/packages/pipeline/src/task_pipeline/executors/task_executor.py @@ -2,6 +2,8 @@ from __future__ import annotations +from typing import Any + from ..core.types import PipelineContext, PipelineStep @@ -9,8 +11,8 @@ class TaskExecutor: """Executes individual pipeline steps with proper error handling.""" def execute( - self, step: PipelineStep, context: PipelineContext - ) -> PipelineContext: + self, step: PipelineStep, context: PipelineContext[Any] + ) -> PipelineContext[Any]: """ Execute a single pipeline step. diff --git a/packages/pipeline/src/task_pipeline/pipeline.py b/packages/pipeline/src/task_pipeline/pipeline.py index 2bfe867..308800e 100644 --- a/packages/pipeline/src/task_pipeline/pipeline.py +++ b/packages/pipeline/src/task_pipeline/pipeline.py @@ -26,9 +26,7 @@ def __init__( self, steps: list[TaskStep], config: PipelineConfig | None = None, - progress_callback: ( - "Callable[[int, int, str, float], None] | None" - ) = None, + progress_callback: Callable[[int, int, str, float], None] | None = None, ): """ Initialize pipeline with steps and configuration. @@ -72,7 +70,7 @@ def on_progress(step_idx, total, name, percent): self._is_running = False self._progress_tracker = ProgressTracker(steps) - def run(self, context: PipelineContext) -> PipelineContext: + def run(self, context: PipelineContext[Any]) -> PipelineContext[Any]: """ Execute the pipeline and return final context. @@ -105,13 +103,11 @@ def run(self, context: PipelineContext) -> PipelineContext: # Set step_id for each parallel step current_context._current_step_id = sub_step.step_id - current_context = ( - self._executor.parallel_executor.execute( - step, - current_context, - self.config.parallel_config, - self._progress_tracker, - ) + current_context = self._executor.parallel_executor.execute( + step, + current_context, + self.config.parallel_config, + self._progress_tracker, ) # Auto-complete all parallel steps to 100% @@ -128,15 +124,11 @@ def run(self, context: PipelineContext) -> PipelineContext: ) # Auto-complete step to 100% - self._progress_tracker.update_step_progress( - step.step_id, 100.0 - ) + self._progress_tracker.update_step_progress(step.step_id, 100.0) # Call progress callback if provided if self._progress_callback: - overall_progress = ( - self._progress_tracker.get_overall_progress() - ) + overall_progress = self._progress_tracker.get_overall_progress() step_name = ( f"parallel_group_{step_index}" if isinstance(step, list) @@ -173,9 +165,7 @@ def get_status(self) -> dict[str, Any]: - step_details: dict[str, dict] - detailed progress for each step """ current_step_name = None - if self._current_step is not None and self._current_step < len( - self.steps - ): + if self._current_step is not None and self._current_step < len(self.steps): step = self.steps[self._current_step] if isinstance(step, list): # For parallel groups, use a generic name @@ -196,9 +186,7 @@ def is_running(self) -> bool: def get_current_step(self) -> str | None: """Return name of current step or None.""" - if self._current_step is not None and self._current_step < len( - self.steps - ): + if self._current_step is not None and self._current_step < len(self.steps): step = self.steps[self._current_step] if isinstance(step, list): # For parallel groups, use a generic name @@ -207,9 +195,7 @@ def get_current_step(self) -> str | None: return None @staticmethod - def create( - steps: list[TaskStep], config: PipelineConfig | None = None - ) -> "Pipeline": + def create(steps: list[TaskStep], config: PipelineConfig | None = None) -> Pipeline: """ Create a new pipeline (factory method following Log.create_logger pattern). diff --git a/packages/pipeline/tests/characterization/test_characterization__step_retry_not_enforced.py b/packages/pipeline/tests/characterization/test_characterization__step_retry_not_enforced.py index 53b8976..552b727 100644 --- a/packages/pipeline/tests/characterization/test_characterization__step_retry_not_enforced.py +++ b/packages/pipeline/tests/characterization/test_characterization__step_retry_not_enforced.py @@ -16,10 +16,11 @@ should be removed/deprecated. """ -import pytest from typing import Any -from task_pipeline import PipelineStep, PipelineContext, Pipeline, PipelineConfig +import pytest + +from task_pipeline import Pipeline, PipelineConfig, PipelineContext, PipelineStep class FlakeyStepWithRetries(PipelineStep): @@ -65,7 +66,8 @@ def run(self, context: PipelineContext[Any]) -> PipelineContext[Any]: if self._attempt_count <= self._fail_count: raise RuntimeError( - f"Attempt {self._attempt_count} failed (will succeed on attempt {self._fail_count + 1})" + f"Attempt {self._attempt_count} failed " + f"(will succeed on attempt {self._fail_count + 1})" ) context.results[self.step_id] = f"succeeded_on_attempt_{self._attempt_count}" @@ -138,9 +140,7 @@ def test_characterization__step_retries_not_enforced_non_critical_step( assert "flakey_step" not in result.results # Did not succeed # If retries were enforced, would have succeeded on attempt 2 - def test_characterization__step_retries_zero_default_works( - self, pipeline_context - ): + def test_characterization__step_retries_zero_default_works(self, pipeline_context): """CHARACTERIZATION: Step with retries=0 (default) fails immediately.""" # Arrange from ..conftest import FailingStep @@ -155,4 +155,3 @@ def test_characterization__step_retries_zero_default_works( # Assert assert retries_value == 0 # Default retries is 0 assert len(result.errors) == 1 # Failed once, no retries - diff --git a/packages/pipeline/tests/characterization/test_characterization__step_timeout_not_enforced.py b/packages/pipeline/tests/characterization/test_characterization__step_timeout_not_enforced.py index 4be56fc..5f4d63a 100644 --- a/packages/pipeline/tests/characterization/test_characterization__step_timeout_not_enforced.py +++ b/packages/pipeline/tests/characterization/test_characterization__step_timeout_not_enforced.py @@ -18,11 +18,10 @@ should be removed/deprecated. """ -import pytest import time from typing import Any -from task_pipeline import PipelineStep, PipelineContext, Pipeline +from task_pipeline import Pipeline, PipelineContext, PipelineStep class SlowStepWithTimeout(PipelineStep): @@ -145,4 +144,3 @@ def test_characterization__step_timeout_none_works_as_expected( # Assert assert timeout_value is None # Default timeout is None assert "key" in result.results - diff --git a/packages/pipeline/tests/conftest.py b/packages/pipeline/tests/conftest.py index 8ebb2d3..657702f 100644 --- a/packages/pipeline/tests/conftest.py +++ b/packages/pipeline/tests/conftest.py @@ -105,9 +105,7 @@ def run(self, context: PipelineContext[Any]) -> PipelineContext[Any]: class FailingStep(PipelineStep): """Test step that always fails.""" - def __init__( - self, step_id: str, error_message: str, critical: bool = True - ): + def __init__(self, step_id: str, error_message: str, critical: bool = True): """Initialize failing step.""" self._step_id = step_id self._error_message = error_message @@ -136,9 +134,7 @@ def run(self, context: PipelineContext[Any]) -> PipelineContext[Any]: class CounterStep(PipelineStep): """Test step that increments a counter.""" - def __init__( - self, step_id: str, counter_key: str = "counter", increment: int = 1 - ): + def __init__(self, step_id: str, counter_key: str = "counter", increment: int = 1): """Initialize counter step.""" self._step_id = step_id self._counter_key = counter_key @@ -203,9 +199,7 @@ def failing_step(): @pytest.fixture def non_critical_failing_step(): """Provide a non-critical failing test step.""" - return FailingStep( - "non_critical_step", "Non-critical error", critical=False - ) + return FailingStep("non_critical_step", "Non-critical error", critical=False) @pytest.fixture diff --git a/packages/pipeline/tests/contract/test_pipeline_api_contract.py b/packages/pipeline/tests/contract/test_pipeline_api_contract.py index 904ab30..a7c746f 100644 --- a/packages/pipeline/tests/contract/test_pipeline_api_contract.py +++ b/packages/pipeline/tests/contract/test_pipeline_api_contract.py @@ -14,20 +14,16 @@ 5. Pipeline.create() factory method works identically to __init__ """ -import pytest -from typing import Any from unittest.mock import MagicMock -from task_pipeline import Pipeline, PipelineStep, PipelineContext, PipelineConfig +from task_pipeline import Pipeline, PipelineConfig, PipelineContext from task_pipeline.core.types import TaskStep class TestPipelineInitializationContract: """Test Pipeline.__init__() contract guarantees.""" - def test_pipeline_accepts_empty_steps_list( - self, pipeline_context, mock_logger - ): + def test_pipeline_accepts_empty_steps_list(self, pipeline_context, mock_logger): """CONTRACT: Pipeline accepts empty list of steps.""" # Arrange & Act pipeline = Pipeline(steps=[]) @@ -84,9 +80,7 @@ def test_pipeline_accepts_parallel_step_group(self, pipeline_context): # Assert assert pipeline is not None - def test_pipeline_accepts_mixed_serial_and_parallel_steps( - self, pipeline_context - ): + def test_pipeline_accepts_mixed_serial_and_parallel_steps(self, pipeline_context): """CONTRACT: Pipeline accepts mix of serial steps and parallel groups.""" # Arrange from ..conftest import SimpleStep diff --git a/packages/pipeline/tests/contract/test_pipelinestep_interface_contract.py b/packages/pipeline/tests/contract/test_pipelinestep_interface_contract.py index 8bc636a..7271421 100644 --- a/packages/pipeline/tests/contract/test_pipelinestep_interface_contract.py +++ b/packages/pipeline/tests/contract/test_pipelinestep_interface_contract.py @@ -15,11 +15,12 @@ 6. description returns str """ -import pytest from abc import ABC from typing import Any -from task_pipeline import PipelineStep, PipelineContext +import pytest + +from task_pipeline import PipelineContext, PipelineStep class TestPipelineStepAbstractContract: @@ -31,7 +32,9 @@ def test_pipelinestep_is_abstract_base_class(self): assert issubclass(PipelineStep, ABC) def test_pipelinestep_cannot_be_instantiated_directly(self): - """CONTRACT: Cannot instantiate PipelineStep without implementing abstract methods.""" + """CONTRACT: Cannot instantiate PipelineStep without implementing abstract + methods. + """ # Act & Assert with pytest.raises(TypeError): # This should raise TypeError because abstract methods not implemented @@ -210,9 +213,7 @@ def description(self) -> str: def timeout(self) -> float | None: return 30.0 - def run( - self, context: PipelineContext[Any] - ) -> PipelineContext[Any]: + def run(self, context: PipelineContext[Any]) -> PipelineContext[Any]: return context step = CustomTimeoutStep() @@ -240,9 +241,7 @@ def description(self) -> str: def retries(self) -> int: return 3 - def run( - self, context: PipelineContext[Any] - ) -> PipelineContext[Any]: + def run(self, context: PipelineContext[Any]) -> PipelineContext[Any]: return context step = CustomRetriesStep() @@ -270,9 +269,7 @@ def description(self) -> str: def critical(self) -> bool: return False - def run( - self, context: PipelineContext[Any] - ) -> PipelineContext[Any]: + def run(self, context: PipelineContext[Any]) -> PipelineContext[Any]: return context step = NonCriticalStep() diff --git a/packages/pipeline/tests/integration/test_advanced_scenarios.py b/packages/pipeline/tests/integration/test_advanced_scenarios.py index 033a38b..5eee028 100644 --- a/packages/pipeline/tests/integration/test_advanced_scenarios.py +++ b/packages/pipeline/tests/integration/test_advanced_scenarios.py @@ -8,14 +8,15 @@ - Mixed execution: executors/pipeline_executor.py:17-48 """ -import pytest from concurrent.futures import TimeoutError +import pytest + from task_pipeline import ( + LogicOperator, + ParallelConfig, Pipeline, PipelineConfig, - ParallelConfig, - LogicOperator, ) @@ -147,12 +148,10 @@ def test_multiple_parallel_groups_in_sequence(self, pipeline_context): assert "g2k1" in result.results assert "g2k2" in result.results - def test_nested_parallel_not_supported_but_sequential_works( - self, pipeline_context - ): + def test_nested_parallel_not_supported_but_sequential_works(self, pipeline_context): """INTEGRATION: Parallel groups execute sequentially (no nesting).""" # Arrange - from ..conftest import SimpleStep, CounterStep + from ..conftest import CounterStep, SimpleStep # This creates two sequential parallel groups, not nested parallelism steps = [ @@ -183,9 +182,7 @@ def test_parallel_execution_is_thread_safe(self, pipeline_context): from ..conftest import CounterStep # Create many parallel steps to stress test thread safety - parallel_steps = [ - CounterStep(f"step{i}", "counter") for i in range(10) - ] + parallel_steps = [CounterStep(f"step{i}", "counter") for i in range(10)] config = ParallelConfig(operator=LogicOperator.AND, max_workers=5) pipeline_config = PipelineConfig(parallel_config=config) pipeline = Pipeline(steps=[parallel_steps], config=pipeline_config) diff --git a/packages/pipeline/tests/integration/test_context_merging.py b/packages/pipeline/tests/integration/test_context_merging.py index ad6f154..eb80d07 100644 --- a/packages/pipeline/tests/integration/test_context_merging.py +++ b/packages/pipeline/tests/integration/test_context_merging.py @@ -14,12 +14,12 @@ from typing import Any from task_pipeline import ( + LogicOperator, + ParallelConfig, Pipeline, - PipelineStep, - PipelineContext, PipelineConfig, - ParallelConfig, - LogicOperator, + PipelineContext, + PipelineStep, ) diff --git a/packages/pipeline/tests/integration/test_error_handling.py b/packages/pipeline/tests/integration/test_error_handling.py index a3a4a72..d3bec32 100644 --- a/packages/pipeline/tests/integration/test_error_handling.py +++ b/packages/pipeline/tests/integration/test_error_handling.py @@ -19,7 +19,7 @@ class TestFailFastMode: def test_fail_fast_stops_on_first_critical_error(self, pipeline_context): """INTEGRATION: Fail-fast stops pipeline on first critical error.""" # Arrange - from ..conftest import SimpleStep, FailingStep + from ..conftest import FailingStep, SimpleStep steps = [ SimpleStep("step1", "key1", "value1"), @@ -36,7 +36,7 @@ def test_fail_fast_stops_on_first_critical_error(self, pipeline_context): def test_fail_fast_continues_on_non_critical_error(self, pipeline_context): """INTEGRATION: Fail-fast continues when non-critical step fails.""" # Arrange - from ..conftest import SimpleStep, FailingStep + from ..conftest import FailingStep, SimpleStep steps = [ SimpleStep("step1", "key1", "value1"), @@ -62,7 +62,7 @@ class TestFailSlowMode: def test_fail_slow_accumulates_all_errors(self, pipeline_context): """INTEGRATION: Fail-slow accumulates all errors from non-critical steps.""" # Arrange - from ..conftest import SimpleStep, FailingStep + from ..conftest import FailingStep, SimpleStep steps = [ SimpleStep("step1", "key1", "value1"), @@ -85,9 +85,7 @@ def test_fail_slow_accumulates_all_errors(self, pipeline_context): assert "Error 1" in str(result.errors[0]) assert "Error 2" in str(result.errors[1]) - def test_fail_slow_continues_even_on_critical_error( - self, pipeline_context - ): + def test_fail_slow_continues_even_on_critical_error(self, pipeline_context): """INTEGRATION: Fail-slow continues even on critical errors. CHARACTERIZATION: Current behavior is that fail_fast=False means @@ -96,7 +94,7 @@ def test_fail_slow_continues_even_on_critical_error( is re-raised when fail_fast=True. """ # Arrange - from ..conftest import SimpleStep, FailingStep + from ..conftest import FailingStep, SimpleStep steps = [ SimpleStep("step1", "key1", "value1"), @@ -149,7 +147,7 @@ def test_multiple_non_critical_failures_all_recorded(self, pipeline_context): def test_successful_steps_after_non_critical_failures(self, pipeline_context): """INTEGRATION: Successful steps execute after non-critical failures.""" # Arrange - from ..conftest import SimpleStep, FailingStep + from ..conftest import FailingStep, SimpleStep steps = [ FailingStep("fail1", "Error 1", critical=False), @@ -169,4 +167,3 @@ def test_successful_steps_after_non_critical_failures(self, pipeline_context): assert result.results["key1"] == "value1" assert result.results["key2"] == "value2" assert len(result.errors) == 2 - diff --git a/packages/pipeline/tests/integration/test_examples_verification.py b/packages/pipeline/tests/integration/test_examples_verification.py index 70906d2..74a4177 100644 --- a/packages/pipeline/tests/integration/test_examples_verification.py +++ b/packages/pipeline/tests/integration/test_examples_verification.py @@ -8,12 +8,7 @@ - docs/guides/parallel-execution.md (parallel steps with progress) """ -import threading -import time from typing import Any -from unittest.mock import MagicMock - -import pytest from task_pipeline import ( Pipeline, @@ -67,9 +62,7 @@ def run(self, context: PipelineContext[Any]) -> PipelineContext[Any]: progress_values.append(progress) return context - context = PipelineContext( - app_config=MockConfig(), logger_instance=MockLogger() - ) + context = PipelineContext(app_config=MockConfig(), logger_instance=MockLogger()) pipeline = Pipeline([ProgressReportingStep()]) pipeline.run(context) @@ -77,9 +70,7 @@ def run(self, context: PipelineContext[Any]) -> PipelineContext[Any]: def test_get_status_returns_step_details(self) -> None: """Pipeline.get_status() returns step-level progress details.""" - context = PipelineContext( - app_config=MockConfig(), logger_instance=MockLogger() - ) + _ = PipelineContext(app_config=MockConfig(), logger_instance=MockLogger()) class SimpleStep(PipelineStep): @property @@ -168,7 +159,7 @@ def test_decorator_works_with_step_update_method(self) -> None: @with_progress_callback def utility_function(items: list, progress_callback) -> int: - for i, item in enumerate(items): + for i, _item in enumerate(items): progress_callback((i + 1) / len(items) * 100) return len(items) @@ -188,9 +179,7 @@ def run(self, context: PipelineContext[Any]) -> PipelineContext[Any]: context.results["count"] = count return context - context = PipelineContext( - app_config=MockConfig(), logger_instance=MockLogger() - ) + context = PipelineContext(app_config=MockConfig(), logger_instance=MockLogger()) pipeline = Pipeline([StepWithUtility()]) result = pipeline.run(context) @@ -263,9 +252,7 @@ def run(self, context: PipelineContext[Any]) -> PipelineContext[Any]: context.results[self._id] = "done" return context - context = PipelineContext( - app_config=MockConfig(), logger_instance=MockLogger() - ) + context = PipelineContext(app_config=MockConfig(), logger_instance=MockLogger()) steps = [[ProgressStep("fast", 2), ProgressStep("slow", 4)]] @@ -304,9 +291,7 @@ def run(self, context: PipelineContext[Any]) -> PipelineContext[Any]: statuses_during_run.append(pipeline_ref[0].get_status()) return context - context = PipelineContext( - app_config=MockConfig(), logger_instance=MockLogger() - ) + context = PipelineContext(app_config=MockConfig(), logger_instance=MockLogger()) pipeline = Pipeline([SlowStep()]) pipeline_ref.append(pipeline) @@ -333,9 +318,7 @@ def description(self) -> str: def run(self, context: PipelineContext[Any]) -> PipelineContext[Any]: return context - context = PipelineContext( - app_config=MockConfig(), logger_instance=MockLogger() - ) + context = PipelineContext(app_config=MockConfig(), logger_instance=MockLogger()) pipeline = Pipeline([CheckingStep()]) diff --git a/packages/pipeline/tests/integration/test_execution_flows.py b/packages/pipeline/tests/integration/test_execution_flows.py index 7e9adfd..5434291 100644 --- a/packages/pipeline/tests/integration/test_execution_flows.py +++ b/packages/pipeline/tests/integration/test_execution_flows.py @@ -12,10 +12,10 @@ import pytest from task_pipeline import ( + LogicOperator, + ParallelConfig, Pipeline, PipelineConfig, - ParallelConfig, - LogicOperator, ) @@ -108,7 +108,7 @@ def test_parallel_and_all_steps_succeed(self, pipeline_context): def test_parallel_and_fails_when_one_step_fails(self, pipeline_context): """INTEGRATION: Parallel AND fails when any step fails.""" # Arrange - from ..conftest import SimpleStep, FailingStep + from ..conftest import FailingStep, SimpleStep parallel_steps = [ SimpleStep("parallel1", "key1", "value1"), @@ -127,9 +127,7 @@ def test_parallel_and_fails_when_one_step_fails(self, pipeline_context): class TestParallelOrLogic: """Integration tests for parallel execution with OR logic.""" - def test_parallel_or_succeeds_when_all_steps_succeed( - self, pipeline_context - ): + def test_parallel_or_succeeds_when_all_steps_succeed(self, pipeline_context): """INTEGRATION: Parallel OR succeeds when all steps succeed.""" # Arrange from ..conftest import SimpleStep @@ -150,12 +148,10 @@ def test_parallel_or_succeeds_when_all_steps_succeed( assert "key2" in result.results assert len(result.errors) == 0 - def test_parallel_or_succeeds_when_one_step_succeeds( - self, pipeline_context - ): + def test_parallel_or_succeeds_when_one_step_succeeds(self, pipeline_context): """INTEGRATION: Parallel OR succeeds when one step succeeds.""" # Arrange - from ..conftest import SimpleStep, FailingStep + from ..conftest import FailingStep, SimpleStep parallel_steps = [ FailingStep("failing1", "Failure 1", critical=False), diff --git a/packages/pipeline/tests/integration/test_progress_tracking.py b/packages/pipeline/tests/integration/test_progress_tracking.py index bd83cbd..c484644 100644 --- a/packages/pipeline/tests/integration/test_progress_tracking.py +++ b/packages/pipeline/tests/integration/test_progress_tracking.py @@ -35,9 +35,7 @@ def test_progress_callback_invoked_for_each_step(self, pipeline_context): # Assert - Callback invoked for each step assert callback.call_count >= 3 # At least once per step - def test_progress_callback_receives_increasing_progress( - self, pipeline_context - ): + def test_progress_callback_receives_increasing_progress(self, pipeline_context): """INTEGRATION: Progress callback receives increasing progress values.""" # Arrange from ..conftest import SimpleStep diff --git a/packages/pipeline/tests/test_decorators.py b/packages/pipeline/tests/test_decorators.py index bd8fb4d..6aacb3e 100644 --- a/packages/pipeline/tests/test_decorators.py +++ b/packages/pipeline/tests/test_decorators.py @@ -117,9 +117,7 @@ def my_function(data: str, progress_callback): return data # Pass callback as keyword arg (recommended way) - result = my_function( - "test", progress_callback=lambda p: updates.append(p) - ) + result = my_function("test", progress_callback=lambda p: updates.append(p)) assert result == "test" assert updates == [100.0] @@ -127,9 +125,7 @@ def test_decorator_with_default_parameters(self): """Test decorator with function that has default parameters.""" @with_progress_callback - def with_defaults( - data: str, multiplier: int = 2, progress_callback=None - ): + def with_defaults(data: str, multiplier: int = 2, progress_callback=None): if progress_callback: progress_callback(50.0) return data * multiplier @@ -144,9 +140,7 @@ def with_defaults( # With callback updates = [] - result = with_defaults( - "x", progress_callback=lambda p: updates.append(p) - ) + result = with_defaults("x", progress_callback=lambda p: updates.append(p)) assert result == "xx" assert updates == [50.0] @@ -170,9 +164,7 @@ def counting_function(progress_callback): # Third call with callback updates = [] - result3 = counting_function( - progress_callback=lambda p: updates.append(p) - ) + result3 = counting_function(progress_callback=lambda p: updates.append(p)) assert result3 == 3 assert updates == [100.0] diff --git a/packages/pipeline/tests/test_parallel_executor.py b/packages/pipeline/tests/test_parallel_executor.py index 48dc764..b3c21d6 100644 --- a/packages/pipeline/tests/test_parallel_executor.py +++ b/packages/pipeline/tests/test_parallel_executor.py @@ -61,17 +61,13 @@ def test_execute_empty_steps_list(self, pipeline_context, parallel_config): # Assert assert result == pipeline_context - def test_execute_single_step( - self, simple_step, pipeline_context, parallel_config - ): + def test_execute_single_step(self, simple_step, pipeline_context, parallel_config): """Test executing single step in parallel.""" # Arrange executor = ParallelTaskExecutor() # Act - result = executor.execute( - [simple_step], pipeline_context, parallel_config - ) + result = executor.execute([simple_step], pipeline_context, parallel_config) # Assert assert result.results["test_key"] == "test_value" @@ -127,9 +123,7 @@ def test_execute_with_max_workers_limit(self, pipeline_context): from .conftest import SimpleStep executor = ParallelTaskExecutor() - steps = [ - SimpleStep(f"step{i}", f"key{i}", f"value{i}") for i in range(10) - ] + steps = [SimpleStep(f"step{i}", f"key{i}", f"value{i}") for i in range(10)] config = ParallelConfig(operator=LogicOperator.AND, max_workers=2) # Act @@ -193,9 +187,7 @@ def test_merge_errors_from_multiple_steps(self, pipeline_context): FailingStep("fail1", "Error 1", critical=False), FailingStep("fail2", "Error 2", critical=False), ] - config = ParallelConfig( - operator=LogicOperator.OR - ) # OR so it doesn't fail + config = ParallelConfig(operator=LogicOperator.OR) # OR so it doesn't fail # Act result = executor.execute(steps, pipeline_context, config) @@ -222,9 +214,7 @@ def step_id(self) -> str: def description(self) -> str: return f"Isolation test: {self._step_id}" - def run( - self, context: PipelineContext[Any] - ) -> PipelineContext[Any]: + def run(self, context: PipelineContext[Any]) -> PipelineContext[Any]: # Try to modify a shared key context.results["shared"] = self._step_id context.results[f"unique_{self._step_id}"] = True @@ -412,9 +402,7 @@ def test_timeout_none_allows_unlimited_time(self, pipeline_context): class TestParallelExecutorEdgeCases: """Test suite for edge cases.""" - def test_execute_with_original_context_preservation( - self, pipeline_context - ): + def test_execute_with_original_context_preservation(self, pipeline_context): """Test original context preserved during parallel execution.""" # Arrange from .conftest import SimpleStep diff --git a/packages/pipeline/tests/test_pipeline_context.py b/packages/pipeline/tests/test_pipeline_context.py index 7b68da0..9c68863 100644 --- a/packages/pipeline/tests/test_pipeline_context.py +++ b/packages/pipeline/tests/test_pipeline_context.py @@ -9,9 +9,7 @@ class TestPipelineContext: """Test suite for PipelineContext.""" - def test_context_creation_with_app_config( - self, mock_app_config, mock_logger - ): + def test_context_creation_with_app_config(self, mock_app_config, mock_logger): """Test creating context with app_config.""" # Arrange & Act context = PipelineContext( @@ -177,9 +175,7 @@ def test_context_is_mutable(self, pipeline_context): assert len(context.results) == 1 assert len(context.errors) == 1 - def test_context_with_prepopulated_results( - self, mock_app_config, mock_logger - ): + def test_context_with_prepopulated_results(self, mock_app_config, mock_logger): """Test creating context with prepopulated results.""" # Arrange initial_results = {"key1": "value1", "key2": 42} @@ -196,9 +192,7 @@ def test_context_with_prepopulated_results( assert context.results["key1"] == "value1" assert context.results["key2"] == 42 - def test_context_with_prepopulated_errors( - self, mock_app_config, mock_logger - ): + def test_context_with_prepopulated_errors(self, mock_app_config, mock_logger): """Test creating context with prepopulated errors.""" # Arrange initial_errors = [RuntimeError("Error 1"), ValueError("Error 2")] @@ -260,9 +254,7 @@ def test_deepcopy_basic_context(self, mock_app_config, mock_logger): assert context_copy.results == context.results assert len(context_copy.errors) == len(context.errors) - def test_deepcopy_with_progress_tracker( - self, mock_app_config, mock_logger - ): + def test_deepcopy_with_progress_tracker(self, mock_app_config, mock_logger): """Test progress tracker is shared (not copied) during deepcopy.""" class DummyStep(PipelineStep): diff --git a/packages/pipeline/tests/test_pipeline_integration.py b/packages/pipeline/tests/test_pipeline_integration.py index 61d8110..0c88990 100644 --- a/packages/pipeline/tests/test_pipeline_integration.py +++ b/packages/pipeline/tests/test_pipeline_integration.py @@ -142,9 +142,7 @@ def step_id(self) -> str: def description(self) -> str: return f"Append {self._value}" - def run( - self, context: PipelineContext[Any] - ) -> PipelineContext[Any]: + def run(self, context: PipelineContext[Any]) -> PipelineContext[Any]: items = context.results.get("items", []) items.append(self._value) context.results["items"] = items @@ -440,9 +438,7 @@ def step_id(self) -> str: def description(self) -> str: return "Load data" - def run( - self, context: PipelineContext[Any] - ) -> PipelineContext[Any]: + def run(self, context: PipelineContext[Any]) -> PipelineContext[Any]: context.results["data"] = [1, 2, 3, 4, 5] return context @@ -458,14 +454,10 @@ def step_id(self) -> str: def description(self) -> str: return f"Process with multiplier {self.multiplier}" - def run( - self, context: PipelineContext[Any] - ) -> PipelineContext[Any]: + def run(self, context: PipelineContext[Any]) -> PipelineContext[Any]: data = context.results.get("data", []) processed = [x * self.multiplier for x in data] - context.results[f"processed_{self.multiplier}"] = sum( - processed - ) + context.results[f"processed_{self.multiplier}"] = sum(processed) return context class AggregateStep(PipelineStep): @@ -477,9 +469,7 @@ def step_id(self) -> str: def description(self) -> str: return "Aggregate results" - def run( - self, context: PipelineContext[Any] - ) -> PipelineContext[Any]: + def run(self, context: PipelineContext[Any]) -> PipelineContext[Any]: total = 0 for key, value in context.results.items(): if key.startswith("processed_"): @@ -524,9 +514,7 @@ def step_id(self) -> str: def description(self) -> str: return "Validate environment" - def run( - self, context: PipelineContext[Any] - ) -> PipelineContext[Any]: + def run(self, context: PipelineContext[Any]) -> PipelineContext[Any]: context.results["validated"] = True return context @@ -539,9 +527,7 @@ def step_id(self) -> str: def description(self) -> str: return "Backup existing files" - def run( - self, context: PipelineContext[Any] - ) -> PipelineContext[Any]: + def run(self, context: PipelineContext[Any]) -> PipelineContext[Any]: context.results["backed_up"] = True return context @@ -557,9 +543,7 @@ def step_id(self) -> str: def description(self) -> str: return f"Install {self.component}" - def run( - self, context: PipelineContext[Any] - ) -> PipelineContext[Any]: + def run(self, context: PipelineContext[Any]) -> PipelineContext[Any]: installed = context.results.get("installed", []) installed.append(self.component) context.results["installed"] = installed @@ -574,9 +558,7 @@ def step_id(self) -> str: def description(self) -> str: return "Finalize installation" - def run( - self, context: PipelineContext[Any] - ) -> PipelineContext[Any]: + def run(self, context: PipelineContext[Any]) -> PipelineContext[Any]: context.results["completed"] = True return context diff --git a/packages/pipeline/tests/test_pipeline_step.py b/packages/pipeline/tests/test_pipeline_step.py index c001b6c..9afecde 100644 --- a/packages/pipeline/tests/test_pipeline_step.py +++ b/packages/pipeline/tests/test_pipeline_step.py @@ -25,9 +25,7 @@ class IncompleteStep(PipelineStep): def description(self) -> str: return "Test" - def run( - self, context: PipelineContext[Any] - ) -> PipelineContext[Any]: + def run(self, context: PipelineContext[Any]) -> PipelineContext[Any]: return context # Act & Assert @@ -43,9 +41,7 @@ class IncompleteStep(PipelineStep): def step_id(self) -> str: return "test" - def run( - self, context: PipelineContext[Any] - ) -> PipelineContext[Any]: + def run(self, context: PipelineContext[Any]) -> PipelineContext[Any]: return context # Act & Assert @@ -82,9 +78,7 @@ def step_id(self) -> str: def description(self) -> str: return "A complete step" - def run( - self, context: PipelineContext[Any] - ) -> PipelineContext[Any]: + def run(self, context: PipelineContext[Any]) -> PipelineContext[Any]: return context # Act @@ -144,9 +138,7 @@ def description(self) -> str: def timeout(self) -> float | None: return 5.0 - def run( - self, context: PipelineContext[Any] - ) -> PipelineContext[Any]: + def run(self, context: PipelineContext[Any]) -> PipelineContext[Any]: return context # Act @@ -172,9 +164,7 @@ def description(self) -> str: def retries(self) -> int: return 3 - def run( - self, context: PipelineContext[Any] - ) -> PipelineContext[Any]: + def run(self, context: PipelineContext[Any]) -> PipelineContext[Any]: return context # Act @@ -213,9 +203,7 @@ def retries(self) -> int: def critical(self) -> bool: return False - def run( - self, context: PipelineContext[Any] - ) -> PipelineContext[Any]: + def run(self, context: PipelineContext[Any]) -> PipelineContext[Any]: return context # Act @@ -249,9 +237,7 @@ def test_run_method_returns_context(self, simple_step, pipeline_context): # Assert assert result == pipeline_context - def test_run_method_can_modify_context( - self, simple_step, pipeline_context - ): + def test_run_method_can_modify_context(self, simple_step, pipeline_context): """Test that run method can modify context.""" # Arrange assert "test_key" not in pipeline_context.results @@ -276,9 +262,7 @@ def step_id(self) -> str: def description(self) -> str: return "Multi result step" - def run( - self, context: PipelineContext[Any] - ) -> PipelineContext[Any]: + def run(self, context: PipelineContext[Any]) -> PipelineContext[Any]: context.results["key1"] = "value1" context.results["key2"] = "value2" context.results["key3"] = "value3" @@ -308,9 +292,7 @@ def step_id(self) -> str: def description(self) -> str: return "Config access step" - def run( - self, context: PipelineContext[Any] - ) -> PipelineContext[Any]: + def run(self, context: PipelineContext[Any]) -> PipelineContext[Any]: context.results["app_name"] = context.app_config.name context.results["app_version"] = context.app_config.version return context @@ -337,9 +319,7 @@ def step_id(self) -> str: def description(self) -> str: return "Logging step" - def run( - self, context: PipelineContext[Any] - ) -> PipelineContext[Any]: + def run(self, context: PipelineContext[Any]) -> PipelineContext[Any]: context.logger_instance.info("Test message") context.results["logged"] = True return context @@ -351,13 +331,9 @@ def run( # Assert assert result.results["logged"] is True - pipeline_context.logger_instance.info.assert_called_once_with( - "Test message" - ) + pipeline_context.logger_instance.info.assert_called_once_with("Test message") - def test_run_method_can_raise_exception( - self, failing_step, pipeline_context - ): + def test_run_method_can_raise_exception(self, failing_step, pipeline_context): """Test that run method can raise exceptions.""" # Act & Assert with pytest.raises(RuntimeError) as exc_info: diff --git a/packages/pipeline/tests/test_progress.py b/packages/pipeline/tests/test_progress.py index 88faf2e..2240217 100644 --- a/packages/pipeline/tests/test_progress.py +++ b/packages/pipeline/tests/test_progress.py @@ -61,9 +61,7 @@ def error(self, msg): class MockConfig: pass - context = PipelineContext( - app_config=MockConfig(), logger_instance=MockLogger() - ) + context = PipelineContext(app_config=MockConfig(), logger_instance=MockLogger()) pipeline = Pipeline(steps=steps, progress_callback=callback) result = pipeline.run(context) @@ -114,9 +112,7 @@ def error(self, msg): class MockConfig: pass - context = PipelineContext( - app_config=MockConfig(), logger_instance=MockLogger() - ) + context = PipelineContext(app_config=MockConfig(), logger_instance=MockLogger()) pipeline = Pipeline(steps=steps) @@ -192,9 +188,7 @@ def error(self, msg): class MockConfig: pass - context = PipelineContext( - app_config=MockConfig(), logger_instance=MockLogger() - ) + context = PipelineContext(app_config=MockConfig(), logger_instance=MockLogger()) pipeline = Pipeline(steps=steps) result = pipeline.run(context) @@ -253,9 +247,7 @@ def error(self, msg): class MockConfig: pass - context = PipelineContext( - app_config=MockConfig(), logger_instance=MockLogger() - ) + context = PipelineContext(app_config=MockConfig(), logger_instance=MockLogger()) pipeline = Pipeline(steps=steps) pipeline.run(context) diff --git a/packages/pipeline/tests/test_progress_tracker.py b/packages/pipeline/tests/test_progress_tracker.py index d19f595..7eb6a20 100644 --- a/packages/pipeline/tests/test_progress_tracker.py +++ b/packages/pipeline/tests/test_progress_tracker.py @@ -186,9 +186,7 @@ def update_progress(step_id: str): threads = [] for i in range(10): - thread = threading.Thread( - target=update_progress, args=(f"step{i}",) - ) + thread = threading.Thread(target=update_progress, args=(f"step{i}",)) threads.append(thread) thread.start() diff --git a/packages/pipeline/tests/test_task_executor.py b/packages/pipeline/tests/test_task_executor.py index 58ab861..af96480 100644 --- a/packages/pipeline/tests/test_task_executor.py +++ b/packages/pipeline/tests/test_task_executor.py @@ -42,9 +42,7 @@ def test_execute_simple_step(self, simple_step, pipeline_context): assert result is not None assert result.results["test_key"] == "test_value" - def test_execute_returns_modified_context( - self, simple_step, pipeline_context - ): + def test_execute_returns_modified_context(self, simple_step, pipeline_context): """Test that execute returns the modified context.""" # Arrange executor = TaskExecutor() @@ -88,12 +86,8 @@ def step_id(self) -> str: def description(self) -> str: return "Modify existing results" - def run( - self, context: PipelineContext[Any] - ) -> PipelineContext[Any]: - context.results["counter"] = ( - context.results.get("counter", 0) + 10 - ) + def run(self, context: PipelineContext[Any]) -> PipelineContext[Any]: + context.results["counter"] = context.results.get("counter", 0) + 10 return context executor = TaskExecutor() @@ -121,9 +115,7 @@ def step_id(self) -> str: def description(self) -> str: return "Step with logging" - def run( - self, context: PipelineContext[Any] - ) -> PipelineContext[Any]: + def run(self, context: PipelineContext[Any]) -> PipelineContext[Any]: context.logger_instance.info("Executing step") context.results["logged"] = True return context @@ -282,9 +274,7 @@ class MinimalContext: class TestTaskExecutorEdgeCases: """Test suite for TaskExecutor edge cases.""" - def test_execute_step_that_returns_different_context( - self, pipeline_context - ): + def test_execute_step_that_returns_different_context(self, pipeline_context): """Test step that returns a different context object.""" # Arrange from typing import Any @@ -300,9 +290,7 @@ def step_id(self) -> str: def description(self) -> str: return "Returns new context" - def run( - self, context: PipelineContext[Any] - ) -> PipelineContext[Any]: + def run(self, context: PipelineContext[Any]) -> PipelineContext[Any]: # Create a new context new_context = PipelineContext( app_config=context.app_config, @@ -321,9 +309,7 @@ def run( assert "new" in result.results assert "old" not in result.results - def test_execute_step_with_empty_context( - self, mock_app_config, mock_logger - ): + def test_execute_step_with_empty_context(self, mock_app_config, mock_logger): """Test executing step with empty context.""" # Arrange from task_pipeline import PipelineContext diff --git a/packages/socket/Makefile b/packages/socket/Makefile deleted file mode 100755 index ac95f61..0000000 --- a/packages/socket/Makefile +++ /dev/null @@ -1,99 +0,0 @@ -MAKEFLAGS += --no-print-directory - -# Standard Makefile for UV Python Projects -# Provides consistent development workflows - -.PHONY: help sync-check ensure-sync dev-shell format lint type-check test test-cov clean clean-venv install build pre-commit-install pre-commit-run all-checks - -help: ## Show this help message - @echo "Available commands:" - @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf " %-20s %s\n", $$1, $$2}' - -sync-check: ## Check if environment sync is needed - @if ! uv run python -c "import sys; sys.exit(0)" 2>/dev/null; then \ - echo "❌ Package not installed - sync needed"; \ - exit 1; \ - elif [ uv.lock -nt .venv/pyvenv.cfg ] 2>/dev/null; then \ - echo "❌ Lock file newer than venv - sync needed"; \ - exit 1; \ - elif [ pyproject.toml -nt .venv/pyvenv.cfg ] 2>/dev/null; then \ - echo "❌ pyproject.toml newer than venv - sync needed"; \ - exit 1; \ - else \ - echo "✅ Environment is up to date"; \ - fi - -ensure-sync: ## Ensure environment is synced - @$(MAKE) sync-check || (echo "Syncing environment..." && uv sync) - -dev-shell: ensure-sync ## Activate development shell - @echo "Activating virtual environment..." - @echo "You can leave the dev shell by typing 'exit'" - @bash -c "source .venv/bin/activate && exec bash" - -format: ensure-sync ## Format code with black and isort - @echo "Formatting code..." - @uv run black . - @uv run isort . - @echo "✅ Formatting complete" - -lint: ensure-sync ## Lint code with ruff - @echo "Linting code..." - @uv run ruff check --fix . - @echo "✅ Linting complete" - -type-check: ensure-sync ## Type check with mypy - @echo "Type checking..." - @uv run mypy . - @echo "✅ Type checking complete" - -test: ensure-sync ## Run tests - @echo "Running tests..." - @uv run pytest -v - @echo "✅ Tests complete" - -test-cov: ensure-sync ## Run tests with coverage - @echo "Running tests with coverage..." - @uv run pytest --cov --cov-report=html --cov-report=term - @echo "✅ Coverage report generated in htmlcov/" - -clean: ## Clean cache files and build artifacts - @echo "Cleaning cache files..." - @find . -type d -name "__pycache__" -exec rm -rf {} + 2>/dev/null || true - @find . -type f -name "*.pyc" -delete 2>/dev/null || true - @find . -type d -name ".pytest_cache" -exec rm -rf {} + 2>/dev/null || true - @find . -type d -name ".mypy_cache" -exec rm -rf {} + 2>/dev/null || true - @find . -type d -name ".ruff_cache" -exec rm -rf {} + 2>/dev/null || true - @find . -type d -name "*.egg-info" -exec rm -rf {} + 2>/dev/null || true - @rm -rf build/ dist/ htmlcov/ .coverage 2>/dev/null || true - @echo "✅ Cleanup complete" - -clean-venv: ## Remove virtual environment - @echo "Removing virtual environment..." - @rm -rf .venv - @echo "✅ Virtual environment removed" - @echo "Run 'make install' or 'uv sync' to recreate" - -install: ## Install project in editable mode - @echo "Installing project..." - @uv sync - @echo "✅ Installation complete" - -build: ensure-sync ## Build the package - @echo "Building package..." - @uv build - @echo "✅ Build complete" - -pre-commit-install: ensure-sync ## Install pre-commit hooks - @echo "Installing pre-commit hooks..." - @uv run pre-commit install - @echo "✅ Pre-commit hooks installed" - -pre-commit-run: ensure-sync ## Run pre-commit on all files - @echo "Running pre-commit on all files..." - @uv run pre-commit run --all-files - @echo "✅ Pre-commit checks complete" - -all-checks: format lint type-check test ## Run all checks (format, lint, type-check, test) - @echo "✅ All checks passed!" - diff --git a/packages/socket/pyproject.toml b/packages/socket/pyproject.toml index e161932..cb4649c 100755 --- a/packages/socket/pyproject.toml +++ b/packages/socket/pyproject.toml @@ -5,7 +5,7 @@ description = "Generic socket communication module for inter-process messaging" readme = "README.md" requires-python = ">=3.12" dependencies = [ - "dotfiles-logging", + "rich-logging", "pydantic>=2.0.0", "dynaconf>=3.2.0", "msgpack>=1.0.0", @@ -33,7 +33,7 @@ packages = ["src/dotfiles_socket"] allow-direct-references = true [tool.uv.sources] -dotfiles-logging = { path = "../logging", editable = true } +rich-logging = { workspace = true } [tool.black] line-length = 79 diff --git a/packages/socket/src/dotfiles_socket/config/config.py b/packages/socket/src/dotfiles_socket/config/config.py index c7ef2ba..2c7e32a 100755 --- a/packages/socket/src/dotfiles_socket/config/config.py +++ b/packages/socket/src/dotfiles_socket/config/config.py @@ -1,15 +1,30 @@ """Configuration models for socket module.""" +import os from pathlib import Path from pydantic import BaseModel, Field, field_validator +def _get_default_socket_dir() -> Path: + """Get secure default socket directory. + + Uses XDG_RUNTIME_DIR if available (Linux standard for user runtime files), + falls back to user-specific directory in /var/run or ~/.local/run. + """ + # Prefer XDG_RUNTIME_DIR (secure, user-specific, cleaned on logout) + if runtime_dir := os.environ.get("XDG_RUNTIME_DIR"): + return Path(runtime_dir) / "sockets" + + # Fallback: user-specific directory + return Path.home() / ".local" / "run" / "sockets" + + class SocketConfig(BaseModel): """Generic socket configuration.""" socket_dir: Path = Field( - default=Path("/tmp/sockets"), + default_factory=_get_default_socket_dir, description="Directory for Unix domain socket files", ) default_timeout: int = Field( diff --git a/packages/socket/src/dotfiles_socket/core/server.py b/packages/socket/src/dotfiles_socket/core/server.py index 6d961a7..33c91ea 100755 --- a/packages/socket/src/dotfiles_socket/core/server.py +++ b/packages/socket/src/dotfiles_socket/core/server.py @@ -49,9 +49,7 @@ def stop(self) -> None: pass @abstractmethod - def send( - self, message: SocketMessage, client_id: str | None = None - ) -> None: + def send(self, message: SocketMessage, client_id: str | None = None) -> None: """Send a message to client(s). Args: @@ -106,9 +104,7 @@ def on_client_disconnected(self, client_id: str) -> None: pass @abstractmethod - def on_message_received( - self, client_id: str, message: SocketMessage - ) -> None: + def on_message_received(self, client_id: str, message: SocketMessage) -> None: """Hook called when a message is received from a client. Only called if allow_client_send is enabled. diff --git a/packages/socket/src/dotfiles_socket/implementations/tcp/client.py b/packages/socket/src/dotfiles_socket/implementations/tcp/client.py index ef19b06..f7e99a5 100755 --- a/packages/socket/src/dotfiles_socket/implementations/tcp/client.py +++ b/packages/socket/src/dotfiles_socket/implementations/tcp/client.py @@ -1,5 +1,7 @@ """TCP socket client implementation.""" +# isort: skip_file + import builtins import logging import socket @@ -64,9 +66,7 @@ def __init__( self._auto_reconnect = auto_reconnect self._buffer_size = ( - buffer_size - if buffer_size is not None - else generic_config.buffer_size + buffer_size if buffer_size is not None else generic_config.buffer_size ) self._timeout = generic_config.default_timeout @@ -76,9 +76,7 @@ def __init__( self._receive_thread: threading.Thread | None = None # Message buffer - self._message_buffer: deque[SocketMessage] = deque( - maxlen=self._buffer_size - ) + self._message_buffer: deque[SocketMessage] = deque(maxlen=self._buffer_size) self._buffer_lock = threading.Lock() self._buffer_condition = threading.Condition(self._buffer_lock) @@ -90,9 +88,7 @@ def event_name(self) -> str: def connect(self) -> None: """Connect to the socket server.""" if self._connected: - self._logger.warning( - f"Client already connected to '{self._event_name}'" - ) + self._logger.warning(f"Client already connected to '{self._event_name}'") return try: @@ -104,9 +100,7 @@ def connect(self) -> None: self._socket.connect((self._host, self._port)) self._connected = True - self._logger.info( - f"Connected to TCP socket: {self._host}:{self._port}" - ) + self._logger.info(f"Connected to TCP socket: {self._host}:{self._port}") # Start receive thread self._receive_thread = threading.Thread( @@ -124,9 +118,7 @@ def disconnect(self) -> None: return self._connected = False - self._logger.info( - f"Disconnecting from TCP socket: {self._host}:{self._port}" - ) + self._logger.info(f"Disconnecting from TCP socket: {self._host}:{self._port}") # Close socket if self._socket: @@ -180,13 +172,9 @@ def receive(self, timeout: float | None = None) -> SocketMessage: if not self._buffer_condition.wait_for( lambda: len(self._message_buffer) > 0, timeout=timeout ): - raise TimeoutError( - f"No message received within {timeout}s" - ) + raise TimeoutError(f"No message received within {timeout}s") else: - self._buffer_condition.wait_for( - lambda: len(self._message_buffer) > 0 - ) + self._buffer_condition.wait_for(lambda: len(self._message_buffer) > 0) # Get message from buffer return self._message_buffer.popleft() diff --git a/packages/socket/src/dotfiles_socket/implementations/tcp/server.py b/packages/socket/src/dotfiles_socket/implementations/tcp/server.py index 3df0555..3e3a1cf 100755 --- a/packages/socket/src/dotfiles_socket/implementations/tcp/server.py +++ b/packages/socket/src/dotfiles_socket/implementations/tcp/server.py @@ -1,5 +1,7 @@ """TCP socket server implementation.""" +# isort: skip_file + import contextlib import logging import socket @@ -72,14 +74,10 @@ def __init__( else tcp_config.port_range_start ) self._port_range_end = ( - port_range_end - if port_range_end is not None - else tcp_config.port_range_end + port_range_end if port_range_end is not None else tcp_config.port_range_end ) self._blocking_mode = ( - blocking_mode - if blocking_mode is not None - else generic_config.blocking_mode + blocking_mode if blocking_mode is not None else generic_config.blocking_mode ) self._allow_client_send = ( allow_client_send @@ -128,19 +126,13 @@ def port(self) -> int | None: def start(self) -> None: """Start the socket server.""" if self._running: - self._logger.warning( - f"Server for '{self._event_name}' already running" - ) + self._logger.warning(f"Server for '{self._event_name}' already running") return try: # Create TCP socket - self._server_socket = socket.socket( - socket.AF_INET, socket.SOCK_STREAM - ) - self._server_socket.setsockopt( - socket.SOL_SOCKET, socket.SO_REUSEADDR, 1 - ) + self._server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self._server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) # Bind to port if self._port is not None: @@ -150,9 +142,7 @@ def start(self) -> None: else: # Try port range bound = False - for port in range( - self._port_range_start, self._port_range_end + 1 - ): + for port in range(self._port_range_start, self._port_range_end + 1): try: self._server_socket.bind((self._host, port)) self._actual_port = port @@ -204,9 +194,7 @@ def stop(self) -> None: try: client_socket.close() except Exception as e: - self._logger.error( - f"Error closing client {client_id}: {e}" - ) + self._logger.error(f"Error closing client {client_id}: {e}") self._clients.clear() # Close server socket @@ -226,9 +214,7 @@ def stop(self) -> None: self._logger.info("TCP socket server stopped") - def send( - self, message: SocketMessage, client_id: str | None = None - ) -> None: + def send(self, message: SocketMessage, client_id: str | None = None) -> None: """Send a message to client(s).""" if not self._running: raise SocketError("Server is not running") @@ -254,12 +240,8 @@ def send( if client_id is not None: # Unicast to specific client if client_id not in self._clients: - raise SocketConnectionError( - f"Client not connected: {client_id}" - ) - self._send_to_client( - self._clients[client_id], packed_data, client_id - ) + raise SocketConnectionError(f"Client not connected: {client_id}") + self._send_to_client(self._clients[client_id], packed_data, client_id) else: # Broadcast to all clients for cid, client_socket in list(self._clients.items()): @@ -304,13 +286,9 @@ def on_client_disconnected(self, client_id: str) -> None: """Hook called when a client disconnects.""" self._logger.info(f"Client disconnected: {client_id}") - def on_message_received( - self, client_id: str, message: SocketMessage - ) -> None: + def on_message_received(self, client_id: str, message: SocketMessage) -> None: """Hook called when a message is received from a client.""" - self._logger.debug( - f"Message from {client_id}: {message.message_type.value}" - ) + self._logger.debug(f"Message from {client_id}: {message.message_type.value}") def get_queue_size(self) -> int: """Get current size of the message queue.""" @@ -384,9 +362,7 @@ def _accept_clients(self) -> None: if self._running: self._logger.error(f"Error accepting client: {e}") - def _handle_client( - self, client_socket: socket.socket, client_id: str - ) -> None: + def _handle_client(self, client_socket: socket.socket, client_id: str) -> None: """Handle communication with a connected client.""" try: while self._running: @@ -430,9 +406,7 @@ def _handle_client( # Call disconnection hook self.on_client_disconnected(client_id) - def _recv_exact( - self, client_socket: socket.socket, num_bytes: int - ) -> bytes: + def _recv_exact(self, client_socket: socket.socket, num_bytes: int) -> bytes: """Receive exactly num_bytes from socket.""" data = b"" while len(data) < num_bytes: diff --git a/packages/socket/src/dotfiles_socket/implementations/unix/client.py b/packages/socket/src/dotfiles_socket/implementations/unix/client.py index 0d3fc66..4f8b761 100755 --- a/packages/socket/src/dotfiles_socket/implementations/unix/client.py +++ b/packages/socket/src/dotfiles_socket/implementations/unix/client.py @@ -1,5 +1,7 @@ """Unix domain socket client implementation.""" +# isort: skip_file + import logging import socket import threading @@ -52,15 +54,11 @@ def __init__( generic_config = get_generic_socket_config() # Set configuration - self._socket_dir = ( - Path(socket_dir) if socket_dir else generic_config.socket_dir - ) + self._socket_dir = Path(socket_dir) if socket_dir else generic_config.socket_dir self._auto_reconnect = auto_reconnect self._reconnect_delay = reconnect_delay self._buffer_size_bytes = ( - buffer_size - if buffer_size is not None - else generic_config.buffer_size + buffer_size if buffer_size is not None else generic_config.buffer_size ) # Client state @@ -83,16 +81,12 @@ def event_name(self) -> str: def connect(self) -> None: """Connect to the socket server.""" if self._connected: - self._logger.warning( - f"Client already connected to '{self._event_name}'" - ) + self._logger.warning(f"Client already connected to '{self._event_name}'") return try: # Create Unix domain socket - self._client_socket = socket.socket( - socket.AF_UNIX, socket.SOCK_STREAM - ) + self._client_socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) self._client_socket.connect(str(self._socket_path)) self._connected = True @@ -117,9 +111,7 @@ def disconnect(self) -> None: self._connected = False self._stop_event.set() - self._logger.info( - f"Disconnecting from Unix socket: {self._socket_path}" - ) + self._logger.info(f"Disconnecting from Unix socket: {self._socket_path}") # Close socket if self._client_socket: @@ -146,9 +138,7 @@ def send(self, message: SocketMessage) -> None: # Send length prefix (4 bytes) + data length = len(packed_data) - self._client_socket.sendall( - length.to_bytes(4, "big") + packed_data - ) + self._client_socket.sendall(length.to_bytes(4, "big") + packed_data) except Exception as e: raise MessageError(f"Failed to send message: {e}") from e @@ -277,9 +267,7 @@ def _recv_exact(self, num_bytes: int) -> bytes: def _attempt_reconnect(self) -> None: """Attempt to reconnect to the server.""" - self._logger.info( - f"Attempting to reconnect in {self._reconnect_delay}s..." - ) + self._logger.info(f"Attempting to reconnect in {self._reconnect_delay}s...") time.sleep(self._reconnect_delay) try: diff --git a/packages/socket/src/dotfiles_socket/implementations/unix/server.py b/packages/socket/src/dotfiles_socket/implementations/unix/server.py index f578eb4..3f2af96 100755 --- a/packages/socket/src/dotfiles_socket/implementations/unix/server.py +++ b/packages/socket/src/dotfiles_socket/implementations/unix/server.py @@ -1,5 +1,7 @@ """Unix domain socket server implementation.""" +# isort: skip_file + import contextlib import logging import socket @@ -63,13 +65,9 @@ def __init__( unix_config = get_unix_socket_config() # Set configuration - self._socket_dir = ( - Path(socket_dir) if socket_dir else generic_config.socket_dir - ) + self._socket_dir = Path(socket_dir) if socket_dir else generic_config.socket_dir self._blocking_mode = ( - blocking_mode - if blocking_mode is not None - else generic_config.blocking_mode + blocking_mode if blocking_mode is not None else generic_config.blocking_mode ) self._allow_client_send = ( allow_client_send @@ -81,9 +79,7 @@ def __init__( if max_connections is not None else unix_config.max_connections ) - self._socket_permissions = ( - socket_permissions or unix_config.socket_permissions - ) + self._socket_permissions = socket_permissions or unix_config.socket_permissions self._auto_remove_socket = ( auto_remove_socket if auto_remove_socket is not None @@ -121,9 +117,7 @@ def event_name(self) -> str: def start(self) -> None: """Start the socket server.""" if self._running: - self._logger.warning( - f"Server for '{self._event_name}' already running" - ) + self._logger.warning(f"Server for '{self._event_name}' already running") return try: @@ -135,9 +129,7 @@ def start(self) -> None: self._socket_path.unlink() # Create Unix domain socket - self._server_socket = socket.socket( - socket.AF_UNIX, socket.SOCK_STREAM - ) + self._server_socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) self._server_socket.bind(str(self._socket_path)) # Set socket permissions @@ -146,9 +138,7 @@ def start(self) -> None: self._server_socket.listen(self._max_connections) self._running = True - self._logger.info( - f"Unix socket server started: {self._socket_path}" - ) + self._logger.info(f"Unix socket server started: {self._socket_path}") if self._blocking_mode: # Blocking mode - run in current thread @@ -178,9 +168,7 @@ def stop(self) -> None: try: client_socket.close() except Exception as e: - self._logger.error( - f"Error closing client {client_id}: {e}" - ) + self._logger.error(f"Error closing client {client_id}: {e}") self._clients.clear() # Close server socket @@ -208,9 +196,7 @@ def stop(self) -> None: self._logger.info("Unix socket server stopped") - def send( - self, message: SocketMessage, client_id: str | None = None - ) -> None: + def send(self, message: SocketMessage, client_id: str | None = None) -> None: """Send a message to client(s).""" if not self._running: raise SocketError("Server is not running") @@ -236,12 +222,8 @@ def send( if client_id is not None: # Unicast to specific client if client_id not in self._clients: - raise SocketConnectionError( - f"Client not connected: {client_id}" - ) - self._send_to_client( - self._clients[client_id], packed_data, client_id - ) + raise SocketConnectionError(f"Client not connected: {client_id}") + self._send_to_client(self._clients[client_id], packed_data, client_id) else: # Broadcast to all clients for cid, client_socket in list(self._clients.items()): @@ -286,13 +268,9 @@ def on_client_disconnected(self, client_id: str) -> None: """Hook called when a client disconnects.""" self._logger.info(f"Client disconnected: {client_id}") - def on_message_received( - self, client_id: str, message: SocketMessage - ) -> None: + def on_message_received(self, client_id: str, message: SocketMessage) -> None: """Hook called when a message is received from a client.""" - self._logger.debug( - f"Message from {client_id}: {message.message_type.value}" - ) + self._logger.debug(f"Message from {client_id}: {message.message_type.value}") def get_queue_size(self) -> int: """Get current size of the message queue.""" @@ -367,8 +345,7 @@ def _send_queued_messages( return self._logger.debug( - f"Sending {len(self._message_queue)} " - f"queued messages to {client_id}" + f"Sending {len(self._message_queue)} " f"queued messages to {client_id}" ) for message in self._message_queue: @@ -376,15 +353,11 @@ def _send_queued_messages( msg_dict = message.to_dict() packed_data = msgpack.packb(msg_dict) length = len(packed_data) - client_socket.sendall( - length.to_bytes(4, "big") + packed_data - ) + client_socket.sendall(length.to_bytes(4, "big") + packed_data) except Exception as e: self._logger.error(f"Failed to send queued message: {e}") - def _handle_client( - self, client_socket: socket.socket, client_id: str - ) -> None: + def _handle_client(self, client_socket: socket.socket, client_id: str) -> None: """Handle communication with a connected client.""" try: while self._running: @@ -424,9 +397,7 @@ def _handle_client( self.on_client_disconnected(client_id) - def _recv_exact( - self, client_socket: socket.socket, num_bytes: int - ) -> bytes: + def _recv_exact(self, client_socket: socket.socket, num_bytes: int) -> bytes: """Receive exact number of bytes from socket.""" data = b"" while len(data) < num_bytes: diff --git a/packages/socket/tests/test_config.py b/packages/socket/tests/test_config.py index 7f1993b..a5fa9a2 100755 --- a/packages/socket/tests/test_config.py +++ b/packages/socket/tests/test_config.py @@ -26,7 +26,8 @@ def test_default_values(self) -> None: """Test default configuration values.""" config = SocketConfig() - assert config.socket_dir == Path("/tmp/sockets") + # Secure default: XDG_RUNTIME_DIR/sockets or ~/.local/run/sockets + assert config.socket_dir.name == "sockets" assert config.default_timeout == 5 assert config.buffer_size == 4096 assert config.timezone == "UTC" diff --git a/packages/socket/tests/test_factory.py b/packages/socket/tests/test_factory.py index 136ac81..c2e5d89 100755 --- a/packages/socket/tests/test_factory.py +++ b/packages/socket/tests/test_factory.py @@ -53,9 +53,7 @@ def test_create_unix_server_with_string( assert server.is_running() server.stop() - def test_create_tcp_server_with_enum( - self, event_name: str, tcp_host: str - ) -> None: + def test_create_tcp_server_with_enum(self, event_name: str, tcp_host: str) -> None: """Test creating TCP server with SocketType enum.""" server = create_server( SocketType.TCP, @@ -120,9 +118,7 @@ def test_create_unix_client_with_string( assert isinstance(client, UnixSocketClient) - def test_create_tcp_client_with_enum( - self, event_name: str, tcp_host: str - ) -> None: + def test_create_tcp_client_with_enum(self, event_name: str, tcp_host: str) -> None: """Test creating TCP client with SocketType enum.""" client = create_client( SocketType.TCP, @@ -192,9 +188,7 @@ def test_unix_socket_communication( client.disconnect() server.stop() - def test_tcp_socket_communication( - self, event_name: str, tcp_host: str - ) -> None: + def test_tcp_socket_communication(self, event_name: str, tcp_host: str) -> None: """Test TCP socket communication using factory.""" server = create_server( SocketType.TCP, diff --git a/packages/socket/tests/test_tcp_socket.py b/packages/socket/tests/test_tcp_socket.py index 9e2d56e..7c0ae67 100755 --- a/packages/socket/tests/test_tcp_socket.py +++ b/packages/socket/tests/test_tcp_socket.py @@ -45,9 +45,7 @@ def test_port_auto_selection(self, event_name: str, tcp_host: str) -> None: server.stop() - def test_specific_port_binding( - self, event_name: str, tcp_host: str - ) -> None: + def test_specific_port_binding(self, event_name: str, tcp_host: str) -> None: """Test binding to specific port.""" server = TcpSocketServer( event_name=event_name, @@ -86,9 +84,7 @@ def test_client_connection(self, event_name: str, tcp_host: str) -> None: client.disconnect() server.stop() - def test_message_send_receive( - self, event_name: str, tcp_host: str - ) -> None: + def test_message_send_receive(self, event_name: str, tcp_host: str) -> None: """Test message sending and receiving.""" server = TcpSocketServer( event_name=event_name, @@ -173,9 +169,7 @@ def test_broadcast_to_multiple_clients( class TestTcpSocketClient: """Tests for TcpSocketClient.""" - def test_client_connect_disconnect( - self, event_name: str, tcp_host: str - ) -> None: + def test_client_connect_disconnect(self, event_name: str, tcp_host: str) -> None: """Test client connect and disconnect.""" server = TcpSocketServer( event_name=event_name, diff --git a/packages/socket/tests/test_unix_socket.py b/packages/socket/tests/test_unix_socket.py index 2b02d5f..3f80832 100755 --- a/packages/socket/tests/test_unix_socket.py +++ b/packages/socket/tests/test_unix_socket.py @@ -13,9 +13,7 @@ class TestUnixSocketServer: """Tests for UnixSocketServer.""" - def test_server_start_stop( - self, temp_socket_dir: Path, event_name: str - ) -> None: + def test_server_start_stop(self, temp_socket_dir: Path, event_name: str) -> None: """Test server start and stop.""" server = UnixSocketServer( event_name=event_name, @@ -31,9 +29,7 @@ def test_server_start_stop( server.stop() assert not server.is_running() - def test_socket_file_creation( - self, temp_socket_dir: Path, event_name: str - ) -> None: + def test_socket_file_creation(self, temp_socket_dir: Path, event_name: str) -> None: """Test socket file is created.""" server = UnixSocketServer( event_name=event_name, @@ -49,9 +45,7 @@ def test_socket_file_creation( server.stop() - def test_client_connection( - self, temp_socket_dir: Path, event_name: str - ) -> None: + def test_client_connection(self, temp_socket_dir: Path, event_name: str) -> None: """Test client can connect to server.""" server = UnixSocketServer( event_name=event_name, @@ -75,9 +69,7 @@ def test_client_connection( client.disconnect() server.stop() - def test_message_send_receive( - self, temp_socket_dir: Path, event_name: str - ) -> None: + def test_message_send_receive(self, temp_socket_dir: Path, event_name: str) -> None: """Test message sending and receiving.""" server = UnixSocketServer( event_name=event_name, diff --git a/packages/storage/.pre-commit-config.yaml b/packages/storage/.pre-commit-config.yaml deleted file mode 100644 index ccba9f7..0000000 --- a/packages/storage/.pre-commit-config.yaml +++ /dev/null @@ -1,93 +0,0 @@ -repos: - - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.5.0 - hooks: - - id: trailing-whitespace - - id: end-of-file-fixer - - id: check-yaml - - id: check-json - - id: check-toml - - id: check-merge-conflict - - id: detect-private-key - - id: check-case-conflict - - id: check-symlinks - - id: check-added-large-files - args: [--maxkb=1000] - exclude: ^tests/fixtures/ - - - repo: https://github.com/psf/black - rev: 24.10.0 - hooks: - - id: black - name: Format code with Black - language_version: python3.12 - args: [--line-length=88] - stages: [pre-commit] - - - repo: https://github.com/PyCQA/isort - rev: 5.13.2 - hooks: - - id: isort - name: Sort imports with isort - args: [--profile=black, --line-length=88] - stages: [pre-commit] - - - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.8.4 - hooks: - - id: ruff - name: Lint with Ruff - args: [--fix, --line-length=88] - stages: [pre-commit] - - id: ruff-format - name: Format with Ruff (fallback) - stages: [pre-commit] - - - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.13.0 - hooks: - - id: mypy - name: Type check with mypy - language_version: python3.12 - args: [--ignore-missing-imports] - stages: [pre-commit] - additional_dependencies: [pydantic>=2.0] - exclude: ^tests/ - - - repo: https://github.com/PyCQA/bandit - rev: 1.8.0 - hooks: - - id: bandit - name: Security check with Bandit - args: [-ll] - stages: [pre-commit] - exclude: ^tests/|test_.*\.py$ - files: ^src/.*\.py$ - additional_dependencies: [pbr] - - - repo: local - hooks: - - id: pipeline-validation - name: Validate pipeline locally - entry: bash -c 'make pipeline' - language: system - stages: [manual] - pass_filenames: false - always_run: true - -exclude: | - (?x)^( - htmlcov| - \.venv| - venv| - __pycache__| - \.pytest_cache| - \.mypy_cache| - \.git| - dist| - build| - .*\.egg-info - ) - -default_stages: [pre-commit] -fail_fast: false diff --git a/packages/storage/Makefile b/packages/storage/Makefile deleted file mode 100644 index 466c584..0000000 --- a/packages/storage/Makefile +++ /dev/null @@ -1,176 +0,0 @@ -# Variables -PROJECT_NAME := core-storage -PYTHON_VERSION := 3.12 -UV := uv -VENV := .venv -VENV_BIN := $(VENV)/bin -SRC := src - -# Colors -BLUE := \033[0;34m -GREEN := \033[0;32m -RED := \033[0;31m -YELLOW := \033[0;33m -NC := \033[0m - -.DEFAULT_GOAL := help -.PHONY: help dev install-deps lint format security test smoke-test pipeline push clean - -help: ## Show available targets - @echo -e "$(BLUE)$(PROJECT_NAME) — available targets:$(NC)" - @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | \ - awk 'BEGIN {FS = ":.*?## "}; {printf " $(YELLOW)%-20s$(NC) %s\n", $$1, $$2}' - -dev: install-deps ## Set up development environment - @$(VENV_BIN)/pre-commit install - @echo -e "$(GREEN)Development environment ready$(NC)" - -install-deps: ## Install all dependencies (including dev) - @echo -e "$(BLUE)Installing dependencies...$(NC)" - $(UV) venv --python $(PYTHON_VERSION) --seed $(VENV) - $(VENV_BIN)/pip install -e ".[redis]" --quiet - $(VENV_BIN)/pip install \ - "ruff>=0.8.0" "black>=24.0.0" "isort>=5.13.0" "mypy>=1.13.0" \ - "bandit[toml]>=1.8.0" "pytest>=8.3.0" "pytest-cov>=6.0.0" \ - "pytest-xdist>=3.6.0" "coverage[toml]>=7.6.0" \ - "fakeredis>=2.26.0" "pre-commit>=3.8.0" --quiet - @echo -e "$(GREEN)All dependencies installed$(NC)" - -lint: ## Run all linters (ruff, black, isort, mypy) - @echo -e "$(BLUE)Linting...$(NC)" - @$(VENV_BIN)/ruff check --line-length 88 $(SRC)/ || (echo -e "$(RED)Ruff found issues. Run: make format$(NC)" && exit 1) - @$(VENV_BIN)/black --check --line-length 88 $(SRC)/ || (echo -e "$(RED)Black formatting needed. Run: make format$(NC)" && exit 1) - @$(VENV_BIN)/isort --check --profile black --line-length 88 $(SRC)/ || (echo -e "$(RED)Import sorting needed. Run: make format$(NC)" && exit 1) - @$(VENV_BIN)/mypy $(SRC)/ || (echo -e "$(RED)Type errors found. Run: $(VENV_BIN)/mypy $(SRC)/ to see details$(NC)" && exit 1) - @echo -e "$(GREEN)Lint passed.$(NC)" - -format: ## Auto-fix formatting (isort → black → ruff) - @echo -e "$(GREEN)Formatting...$(NC)" - $(VENV_BIN)/isort --profile black --line-length 88 $(SRC)/ - $(VENV_BIN)/black --line-length 88 $(SRC)/ - $(VENV_BIN)/ruff check --fix --line-length 88 $(SRC)/ - @echo -e "$(GREEN)Format complete.$(NC)" - -security: ## Run bandit security scan - @echo -e "$(GREEN)Running security scan...$(NC)" - $(VENV_BIN)/bandit -r $(SRC)/ -ll -f json -o bandit-report.json - @echo -e "$(GREEN)Security scan passed.$(NC)" - -test: ## Run tests with coverage (requires 95%) - @echo -e "$(GREEN)Running tests...$(NC)" - $(VENV_BIN)/pytest -n auto --color=yes --cov=$(SRC) --cov-report=term - $(VENV_BIN)/coverage report --fail-under=95 - @echo -e "$(GREEN)Tests passed.$(NC)" - -smoke-test: ## Run smoke tests (end-to-end via pytest) - @echo -e "$(GREEN)Running smoke tests...$(NC)" - $(VENV_BIN)/pytest tests/smoke/ -v --color=yes - @echo -e "$(GREEN)Smoke tests passed.$(NC)" - -pipeline: ## Run full CI pipeline locally (lint → security → test) - @echo -e "$(BLUE)Running pipeline validation...$(NC)" - @echo -e "" - @echo -e "$(BLUE)Step 1: Linting$(NC)" - @$(MAKE) lint - @echo -e "$(GREEN)Linting passed$(NC)" - @echo -e "" - @echo -e "$(BLUE)Step 2: Security Scan$(NC)" - @$(MAKE) security - @echo -e "$(GREEN)Security scan passed$(NC)" - @echo -e "" - @echo -e "$(BLUE)Step 3: Testing$(NC)" - @$(MAKE) test - @echo -e "$(GREEN)Tests passed with 95%+ coverage$(NC)" - @echo -e "" - @echo -e "$(GREEN)Pipeline validation successful!$(NC)" - @echo -e "$(GREEN)Your changes are safe to push to the cloud.$(NC)" - @echo -e "" - -push: ## Simulate GitHub Actions locally with act (SMOKE=true for smoke tests) - @echo -e "$(BLUE)Setting up GitHub Actions locally...$(NC)" - @if [ ! -f ./bin/act ]; then \ - echo -e "$(BLUE)Downloading act (GitHub Actions CLI)...$(NC)"; \ - mkdir -p ./bin; \ - curl -sL https://github.com/nektos/act/releases/download/v0.2.65/act_Linux_x86_64.tar.gz -o /tmp/act.tar.gz; \ - tar -xzf /tmp/act.tar.gz -C ./bin; \ - rm /tmp/act.tar.gz; \ - echo -e "$(GREEN)act installed to ./bin/act$(NC)"; \ - else \ - echo -e "$(GREEN)act already available$(NC)"; \ - fi - @echo -e "" - @mkdir -p .logs - @TIMESTAMP=$$(date +%Y%m%d-%H%M%S); \ - LOG_FILE=".logs/make-push-$$TIMESTAMP.log"; \ - if [ "$(SMOKE)" = "true" ]; then \ - echo -e "$(BLUE)═══════════════════════════════════════════════════════════$(NC)" | tee "$$LOG_FILE"; \ - echo -e "$(BLUE)Running GitHub Actions with SMOKE TESTS enabled$(NC)" | tee -a "$$LOG_FILE"; \ - echo -e "$(BLUE)═══════════════════════════════════════════════════════════$(NC)" | tee -a "$$LOG_FILE"; \ - echo -e "$(BLUE)Logs: $$LOG_FILE$(NC)" | tee -a "$$LOG_FILE"; \ - echo -e "$(BLUE)═══════════════════════════════════════════════════════════$(NC)" | tee -a "$$LOG_FILE"; \ - echo -e "" | tee -a "$$LOG_FILE"; \ - echo -e "$(BLUE)───────────────────────────────────────────────────────────$(NC)" | tee -a "$$LOG_FILE"; \ - echo -e "$(BLUE)PHASE 1: Standard CI Workflows$(NC)" | tee -a "$$LOG_FILE"; \ - echo -e "$(BLUE)───────────────────────────────────────────────────────────$(NC)" | tee -a "$$LOG_FILE"; \ - ./bin/act push --container-options "--user $$(id -u):$$(id -g)" 2>&1 | tee -a "$$LOG_FILE"; \ - STANDARD_EXIT=$${PIPESTATUS[0]}; \ - echo -e "" | tee -a "$$LOG_FILE"; \ - if [ $$STANDARD_EXIT -eq 0 ]; then \ - echo -e "$(GREEN)Standard CI passed$(NC)" | tee -a "$$LOG_FILE"; \ - echo -e "" | tee -a "$$LOG_FILE"; \ - echo -e "$(BLUE)───────────────────────────────────────────────────────────$(NC)" | tee -a "$$LOG_FILE"; \ - echo -e "$(BLUE)PHASE 2: Smoke Test Workflow$(NC)" | tee -a "$$LOG_FILE"; \ - echo -e "$(BLUE)───────────────────────────────────────────────────────────$(NC)" | tee -a "$$LOG_FILE"; \ - ./bin/act workflow_dispatch -W .github/workflows/smoke-test.yml --container-options "--user $$(id -u):$$(id -g)" 2>&1 | tee -a "$$LOG_FILE"; \ - SMOKE_EXIT=$${PIPESTATUS[0]}; \ - echo -e "" | tee -a "$$LOG_FILE"; \ - if [ $$SMOKE_EXIT -eq 0 ]; then \ - echo -e "$(GREEN)Smoke tests passed$(NC)" | tee -a "$$LOG_FILE"; \ - EXIT_CODE=0; \ - else \ - echo -e "$(RED)Smoke tests failed$(NC)" | tee -a "$$LOG_FILE"; \ - EXIT_CODE=$$SMOKE_EXIT; \ - fi; \ - else \ - echo -e "$(RED)Standard CI failed, skipping smoke tests$(NC)" | tee -a "$$LOG_FILE"; \ - EXIT_CODE=$$STANDARD_EXIT; \ - fi; \ - else \ - echo -e "$(BLUE)═══════════════════════════════════════════════════════════$(NC)" | tee "$$LOG_FILE"; \ - echo -e "$(BLUE)Running Standard GitHub Actions Workflows$(NC)" | tee -a "$$LOG_FILE"; \ - echo -e "$(BLUE)═══════════════════════════════════════════════════════════$(NC)" | tee -a "$$LOG_FILE"; \ - echo -e "$(BLUE)Logs: $$LOG_FILE$(NC)" | tee -a "$$LOG_FILE"; \ - echo -e "$(YELLOW)Tip: Add SMOKE=true to include smoke tests$(NC)" | tee -a "$$LOG_FILE"; \ - echo -e "$(BLUE)═══════════════════════════════════════════════════════════$(NC)" | tee -a "$$LOG_FILE"; \ - echo -e "" | tee -a "$$LOG_FILE"; \ - ./bin/act push --container-options "--user $$(id -u):$$(id -g)" 2>&1 | tee -a "$$LOG_FILE"; \ - EXIT_CODE=$${PIPESTATUS[0]}; \ - fi; \ - echo -e "" | tee -a "$$LOG_FILE"; \ - echo -e "$(BLUE)═══════════════════════════════════════════════════════════$(NC)" | tee -a "$$LOG_FILE"; \ - if [ $$EXIT_CODE -eq 0 ]; then \ - echo -e "$(GREEN)GitHub Actions simulation complete$(NC)" | tee -a "$$LOG_FILE"; \ - else \ - echo -e "$(RED)GitHub Actions simulation failed (exit: $$EXIT_CODE)$(NC)" | tee -a "$$LOG_FILE"; \ - fi; \ - echo -e "$(BLUE)═══════════════════════════════════════════════════════════$(NC)" | tee -a "$$LOG_FILE"; \ - echo -e ""; \ - echo -e "$(GREEN)Full logs: $$LOG_FILE$(NC)"; \ - echo -e "$(GREEN)View logs: cat $$LOG_FILE$(NC)"; \ - echo -e "$(GREEN)Search: grep 'PASSED\|FAILED' $$LOG_FILE$(NC)"; \ - echo -e ""; \ - exit $$EXIT_CODE - -clean: ## Remove build artifacts and caches - @echo -e "$(YELLOW)Cleaning...$(NC)" - find . -type d -name "__pycache__" -exec rm -rf {} + 2>/dev/null || true - find . -type d -name ".pytest_cache" -exec rm -rf {} + 2>/dev/null || true - find . -type d -name ".mypy_cache" -exec rm -rf {} + 2>/dev/null || true - find . -type d -name ".ruff_cache" -exec rm -rf {} + 2>/dev/null || true - find . -type d -name "htmlcov" -exec rm -rf {} + 2>/dev/null || true - find . -type d -name "dist" -exec rm -rf {} + 2>/dev/null || true - find . -type d -name "build" -exec rm -rf {} + 2>/dev/null || true - find . -name "*.egg-info" -exec rm -rf {} + 2>/dev/null || true - find . -name ".coverage" -delete 2>/dev/null || true - find . -name "bandit-report.json" -delete 2>/dev/null || true - @echo -e "$(GREEN)Clean complete.$(NC)" diff --git a/pyproject.toml b/pyproject.toml index 4a9cbae..3aadf4b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,21 +6,183 @@ dev-dependencies = [ "pytest>=8.4.2", "pytest-cov>=4.1.0", "pytest-xdist>=3.8.0", - "mypy>=1.11.0", - "black>=24.0.0", - "ruff>=0.6.0", + "pytest-asyncio>=0.21.0", + "mypy>=1.19.0", + "black>=24.10.0", + "ruff>=0.8.0", "isort>=5.13.0", + "bandit>=1.8.0", "pre-commit>=3.8.0", + "commitizen>=3.29.0", + "coverage>=7.0.0", + "fakeredis>=2.0.0", ] +# ───────────────────────────────────────────────────────────────────────────── +# Code Formatting & Linting +# ───────────────────────────────────────────────────────────────────────────── + +[tool.black] +line-length = 88 +target-version = ["py312"] +include = '\.pyi?$' + +[tool.isort] +profile = "black" +line_length = 88 +multi_line_output = 3 +include_trailing_comma = true +force_grid_wrap = 0 +use_parentheses = true +ensure_newline_before_comments = true + +[tool.ruff] +line-length = 88 +target-version = "py312" + +[tool.ruff.lint] +select = [ + "E", # pycodestyle errors + "W", # pycodestyle warnings + "F", # pyflakes + "I", # isort + "UP", # pyupgrade + "ARG", # flake8-unused-arguments + "SIM", # flake8-simplify + "PTH", # flake8-use-pathlib + "N", # pep8-naming +] +ignore = [ + "E501", # line too long (handled by black) + "SIM105", # try-except-pass (contextlib.suppress is style preference) + "ARG002", # unused method argument (pre-existing in daemon handlers) + "SIM102", # nested if (pre-existing in daemon publisher) +] + +[tool.ruff.lint.per-file-ignores] +"__init__.py" = ["F401"] +"packages/cache/tests/unit/test_result.py" = ["B017"] +"packages/cache/tests/unit/test_public_api.py" = ["C405"] +"packages/cache/tests/integration/*" = ["B", "C4"] + +# ───────────────────────────────────────────────────────────────────────────── +# Type Checking +# ───────────────────────────────────────────────────────────────────────────── + +[tool.mypy] +python_version = "3.12" +check_untyped_defs = true +disallow_any_generics = true +disallow_incomplete_defs = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_redundant_casts = true +warn_unused_ignores = true +warn_return_any = true +strict_equality = true +show_error_codes = true + +[[tool.mypy.overrides]] +module = [ + "rich.*", + "redis.*", + "fakeredis.*", + "filelock.*", + "pydantic.*", + "typer.*", + "msgpack.*", + "dynaconf.*", +] +ignore_missing_imports = true + +[[tool.mypy.overrides]] +module = "rich_logging.*" +ignore_errors = true + +[[tool.mypy.overrides]] +module = "task_pipeline.*" +ignore_errors = true + +[[tool.mypy.overrides]] +module = "container_manager.*" +ignore_errors = true + +[[tool.mypy.overrides]] +module = "dotfiles_socket.*" +ignore_errors = true + +[[tool.mypy.overrides]] +module = "core_cache.*" +ignore_errors = true + +[[tool.mypy.overrides]] +module = "dotfiles_event_protocol.*" +ignore_errors = true + +[[tool.mypy.overrides]] +module = "dotfiles_daemon.*" +ignore_errors = true + +# ───────────────────────────────────────────────────────────────────────────── +# Testing +# ───────────────────────────────────────────────────────────────────────────── [tool.pytest.ini_options] -addopts = ["--import-mode=importlib"] +addopts = ["--import-mode=importlib", "-v", "--strict-markers", "--tb=short"] pythonpath = [ "packages/logging/src", "packages/pipeline/src", - "packages/package-manager/src", "packages/container-manager/src", + "packages/socket/src", "packages/storage/src", "packages/cache/src", + "packages/daemon/src", + "packages/event-protocol/src", ] +testpaths = ["packages/*/tests"] + +[tool.coverage.run] +source = ["src"] +omit = ["*/tests/*", "*/test_*.py"] + +[tool.coverage.report] +exclude_lines = [ + "pragma: no cover", + "def __repr__", + "raise AssertionError", + "raise NotImplementedError", + "if __name__ == .__main__.:", + "if TYPE_CHECKING:", +] +fail_under = 65 # Reduced for legacy packages with incomplete coverage + +# ───────────────────────────────────────────────────────────────────────────── +# Version Management & Commit Convention +# ───────────────────────────────────────────────────────────────────────────── + +[tool.commitizen] +name = "cz_customize" +version = "0.1.0" +version_provider = "pep621" +tag_format = "v$version" +update_changelog_on_bump = true +changelog_file = "docs/changelog.md" +changelog_incremental = true + +[tool.commitizen.customize] +schema_pattern = '(✨|🐛|♻️|🗑️|💥|📝|🔧|⚡|🔒|✅|🎨|:[a-z_]+:) [A-Za-z][A-Za-z0-9/_-]*: .{10,}' +changelog_pattern = '(✨|🐛|♻️|🗑️|💥|📝|🔧|⚡|🔒|✅|🎨|:[a-z_]+:).*' +commit_parser = '^(?P✨|🐛|♻️|🗑️|💥|📝|🔧|⚡|🔒|✅|🎨|:[a-z_]+:) (?P[A-Za-z][A-Za-z0-9/_-]*): (?P.+)$' + +[tool.commitizen.customize.change_type_map] +"✨" = "Added" +"🐛" = "Fixed" +"♻️" = "Changed" +"🗑️" = "Removed" +"💥" = "Breaking Changes" +"📝" = "Documentation" +"🔧" = "Infrastructure" +"⚡" = "Performance" +"🔒" = "Security" +"✅" = "Testing" +"🎨" = "Changed" diff --git a/scripts/generate-changelog.sh b/scripts/generate-changelog.sh new file mode 100755 index 0000000..bcba473 --- /dev/null +++ b/scripts/generate-changelog.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env bash +# Scaffolds docs/changelog-draft.md from git merge commits. +# Each section gets empty ### Added / Changed / Fixed stubs and an HTML comment +# listing the commits included in that merge. Fill in the entries manually, +# then move the finished sections into docs/changelog.md. + +set -euo pipefail + +OUTPUT="docs/changelog-draft.md" + +mkdir -p "$(dirname "$OUTPUT")" + +{ + echo "# Changelog Draft" + echo "" + echo "" + echo "" + echo "" +} > "$OUTPUT" + +git log --merges --format="%H|%s|%ad" --date=short | while IFS="|" read -r hash subject date; do + { + echo "## $date — $subject" + echo "" + echo "" + echo "" + echo "### Added" + echo "" + echo "### Changed" + echo "" + echo "### Fixed" + echo "" + echo "---" + echo "" + } >> "$OUTPUT" +done + +echo "Draft written to $OUTPUT" diff --git a/scripts/hooks/validate-branch.sh b/scripts/hooks/validate-branch.sh new file mode 100755 index 0000000..e9fd9c0 --- /dev/null +++ b/scripts/hooks/validate-branch.sh @@ -0,0 +1,50 @@ +#!/usr/bin/env bash +# Validates that the current branch name follows the required convention: +# -__ +# where TYPE is FR (feature), BG (bug), or CH (chore) +# and NNNNN is a zero-padded 5-digit number +# +# e.g. FR-00001__add-oauth-login +# BG-00042__fix-null-pointer-on-export +# CH-00003__upgrade-python-3-13 +# +# Protected branches (main, master, develop, release/*, hotfix/*) are skipped. + +set -euo pipefail + +BRANCH=$(git rev-parse --abbrev-ref HEAD) + +# Skip protected branches +if echo "$BRANCH" | grep -qE '^(main|master|develop|release/.+|hotfix/.+)$'; then + exit 0 +fi + +# Validate format +if ! echo "$BRANCH" | grep -qE '^(FR|BG|CH)-[0-9]{2,}__[a-z0-9][a-z0-9-]*$'; then + echo "" + echo "ERROR: Branch name '$BRANCH' does not follow the required format." + echo "" + echo " Required: -__" + echo "" + echo " Types:" + echo " FR Feature" + echo " BG Bug" + echo " CH Chore" + echo "" + echo " Examples:" + echo " FR-00001__add-oauth-login" + echo " BG-00042__fix-null-pointer-on-export" + echo " CH-00003__upgrade-python-3-13" + echo "" + echo " Rules:" + echo " - Type prefix: FR, BG, or CH (uppercase)" + echo " - Number: at least 2 digits, no upper limit (e.g. 01, 999, 00042)" + echo " - Separator: double underscore __" + echo " - Ticket name: lowercase letters, digits, single hyphens only" + echo "" + echo " Run 'make ticket TYPE=feature|bug|chore ID=NNNNN NAME=' to scaffold." + echo "" + exit 1 +fi + +exit 0 diff --git a/scripts/hooks/validate-ticket-docs.sh b/scripts/hooks/validate-ticket-docs.sh new file mode 100755 index 0000000..48f5915 --- /dev/null +++ b/scripts/hooks/validate-ticket-docs.sh @@ -0,0 +1,146 @@ +#!/usr/bin/env bash +# Validates that both required documents exist for the ticket on the current branch +# and that every required section contains non-empty content. +# +# A line is considered empty if it is: +# - blank or whitespace-only +# - an HTML comment line (starts with ) +# - an empty checkbox: "- [ ]" with no trailing text +# - a bare numbered list item: "1." with no trailing text +# +# Required sections by document type: +# +# FR (Feature) +# US: Story, Acceptance Criteria +# TD: Overview, Approach, Implementation Plan +# +# BG (Bug) +# BR: Description, Steps to Reproduce, Expected Behavior, Actual Behavior, Severity +# TD: Root Cause Analysis, Fix Approach, Implementation Plan +# +# CH (Chore) +# CD: Description, Motivation +# TD: Approach, Implementation Plan +# +# Protected branches are skipped. + +set -euo pipefail + +BRANCH=$(git rev-parse --abbrev-ref HEAD) + +# Skip protected branches +if echo "$BRANCH" | grep -qE '^(main|master|develop|release/.+|hotfix/.+)$'; then + exit 0 +fi + +# Extract ticket ID from branch name +TICKET_ID=$(echo "$BRANCH" | grep -oE '^(FR|BG|CH)-[0-9]{2,}' || true) + +if [ -z "$TICKET_ID" ]; then + # Not a recognized ticket branch — validate-branch.sh handles the error + exit 0 +fi + +PREFIX=$(echo "$TICKET_ID" | cut -d'-' -f1) + +case "$PREFIX" in + FR) + TYPE_DIR="features" + DOC1_SUFFIX="US" + DOC1_LABEL="User Story" + DOC1_SECTIONS=("Story" "Acceptance Criteria") + DOC2_SECTIONS=("Overview" "Approach" "Implementation Plan") + ;; + BG) + TYPE_DIR="bugs" + DOC1_SUFFIX="BR" + DOC1_LABEL="Bug Report" + DOC1_SECTIONS=("Description" "Steps to Reproduce" "Expected Behavior" "Actual Behavior" "Severity") + DOC2_SECTIONS=("Root Cause Analysis" "Fix Approach" "Implementation Plan") + ;; + CH) + TYPE_DIR="chores" + DOC1_SUFFIX="CD" + DOC1_LABEL="Chore Description" + DOC1_SECTIONS=("Description" "Motivation") + DOC2_SECTIONS=("Approach" "Implementation Plan") + ;; +esac + +DOC1_PATH="docs/tickets/${TYPE_DIR}/${TICKET_ID}-${DOC1_SUFFIX}.md" +DOC2_PATH="docs/tickets/${TYPE_DIR}/${TICKET_ID}-TD.md" + +ERRORS=0 + +# ─── Check files exist ──────────────────────────────────────────────────────── + +if [ ! -f "$DOC1_PATH" ]; then + echo "ERROR: Missing ${DOC1_LABEL}: ${DOC1_PATH}" + ERRORS=$((ERRORS + 1)) +fi + +if [ ! -f "$DOC2_PATH" ]; then + echo "ERROR: Missing Technical Document: ${DOC2_PATH}" + ERRORS=$((ERRORS + 1)) +fi + +if [ "$ERRORS" -gt 0 ]; then + echo "" + echo " Run 'make ticket TYPE=feature|bug|chore ID= NAME=' to scaffold them." + echo "" + exit 1 +fi + +# ─── Section content validation ─────────────────────────────────────────────── + +# Returns 0 if the section has at least one non-empty line, 1 otherwise. +section_has_content() { + local file="$1" + local section="$2" + + awk -v s="$section" ' + /^## / { + if (in_section) exit + name = substr($0, 4) + gsub(/[[:space:]]*$/, "", name) + if (name == s) { in_section = 1 } + next + } + !in_section { next } + /^[[:space:]]*$/ { next } + /^[[:space:]]*$/ { in_comment = 0; next } + in_comment { next } + /^-[[:space:]]*\[[[:space:]]\][[:space:]]*$/ { next } + /^[0-9]+\.[[:space:]]*$/ { next } + { found = 1; exit } + END { exit (found ? 0 : 1) } + ' "$file" +} + +# Checks all required sections in a document and accumulates errors. +check_sections() { + local file="$1" + local label="$2" + shift 2 + local sections=("$@") + + for section in "${sections[@]}"; do + if ! section_has_content "$file" "$section"; then + echo "ERROR: Section '${section}' in ${label} is empty: ${file}" + ERRORS=$((ERRORS + 1)) + fi + done +} + +check_sections "$DOC1_PATH" "$DOC1_LABEL" "${DOC1_SECTIONS[@]}" +check_sections "$DOC2_PATH" "Technical Document" "${DOC2_SECTIONS[@]}" + +if [ "$ERRORS" -gt 0 ]; then + echo "" + echo " Fill in all required sections before committing." + echo "" + exit 1 +fi + +exit 0 diff --git a/scripts/scaffold-ticket.sh b/scripts/scaffold-ticket.sh new file mode 100755 index 0000000..37f2b53 --- /dev/null +++ b/scripts/scaffold-ticket.sh @@ -0,0 +1,112 @@ +#!/usr/bin/env bash +# Scaffolds the two required documents for a ticket and checks out the branch. +# +# Usage: ./scripts/scaffold-ticket.sh +# type feature | bug | chore +# id 5-digit zero-padded number (e.g. 00001) +# name lowercase-hyphenated ticket name (e.g. add-oauth-login) +# +# Example: +# ./scripts/scaffold-ticket.sh feature 00001 add-oauth-login +# ./scripts/scaffold-ticket.sh bug 00042 fix-null-pointer-on-export +# ./scripts/scaffold-ticket.sh chore 00003 upgrade-python-3-13 + +set -euo pipefail + +TYPE="${1:-}" +ID="${2:-}" +NAME="${3:-}" + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[0;33m' +BLUE='\033[0;34m' +NC='\033[0m' + +# ─── Validate arguments ──────────────────────────────────────────────────────── + +if [ -z "$TYPE" ] || [ -z "$ID" ] || [ -z "$NAME" ]; then + echo -e "${RED}Usage: $0 ${NC}" + echo -e " type feature | bug | chore" + echo -e " id 5-digit zero-padded number (e.g. 00001)" + echo -e " name lowercase-hyphenated ticket name" + exit 1 +fi + +if ! echo "$ID" | grep -qE '^[0-9]{2,}$'; then + echo -e "${RED}ERROR: ID must be at least 2 digits (e.g. 01, 999, 00042)${NC}" + exit 1 +fi + +if ! echo "$NAME" | grep -qE '^[a-z0-9][a-z0-9-]*$'; then + echo -e "${RED}ERROR: NAME must be lowercase letters, digits, and hyphens only${NC}" + exit 1 +fi + +# ─── Resolve type-specific values ───────────────────────────────────────────── + +case "$TYPE" in + feature) + PREFIX="FR" + TYPE_DIR="features" + DOC1_SUFFIX="US" + DOC1_LABEL="User Story" + ;; + bug) + PREFIX="BG" + TYPE_DIR="bugs" + DOC1_SUFFIX="BR" + DOC1_LABEL="Bug Report" + ;; + chore) + PREFIX="CH" + TYPE_DIR="chores" + DOC1_SUFFIX="CD" + DOC1_LABEL="Chore Description" + ;; + *) + echo -e "${RED}ERROR: type must be feature, bug, or chore (got: $TYPE)${NC}" + exit 1 + ;; +esac + +TICKET_ID="${PREFIX}-${ID}" +BRANCH="${TICKET_ID}__${NAME}" +DOC_DIR="docs/tickets/${TYPE_DIR}" +TEMPLATES_DIR="templates/docs/tickets/${TYPE_DIR}" +DOC1_PATH="${DOC_DIR}/${TICKET_ID}-${DOC1_SUFFIX}.md" +DOC2_PATH="${DOC_DIR}/${TICKET_ID}-TD.md" + +# ─── Create documents ───────────────────────────────────────────────────────── + +mkdir -p "$DOC_DIR" + +if [ ! -f "$DOC1_PATH" ]; then + sed "s/TICKET_ID/${TICKET_ID}/g" "${TEMPLATES_DIR}/TEMPLATE-${DOC1_SUFFIX}.md" > "$DOC1_PATH" + echo -e "${GREEN}Created ${DOC1_LABEL}: ${DOC1_PATH}${NC}" +else + echo -e "${YELLOW}${DOC1_PATH} already exists — skipping${NC}" +fi + +if [ ! -f "$DOC2_PATH" ]; then + sed "s/TICKET_ID/${TICKET_ID}/g" "${TEMPLATES_DIR}/TEMPLATE-TD.md" > "$DOC2_PATH" + echo -e "${GREEN}Created Technical Document: ${DOC2_PATH}${NC}" +else + echo -e "${YELLOW}${DOC2_PATH} already exists — skipping${NC}" +fi + +# ─── Create and check out branch ────────────────────────────────────────────── + +if git rev-parse --verify "$BRANCH" > /dev/null 2>&1; then + echo -e "${YELLOW}Branch ${BRANCH} already exists — switching${NC}" + git checkout "$BRANCH" +else + git checkout -b "$BRANCH" + echo -e "${GREEN}Created and switched to branch: ${BRANCH}${NC}" +fi + +echo "" +echo -e "${BLUE}Next steps:${NC}" +echo -e " 1. Fill in ${DOC1_PATH}" +echo -e " 2. Fill in ${DOC2_PATH}" +echo -e " 3. Commit both documents before making any code changes" diff --git a/uv.lock b/uv.lock index a1954d6..25132a4 100644 --- a/uv.lock +++ b/uv.lock @@ -5,22 +5,39 @@ requires-python = ">=3.12" [manifest] members = [ "container-manager", - "dotfiles-package-manager", - "dotfiles-storage", + "core-cache", + "core-storage", + "dotfiles-daemon", + "dotfiles-event-protocol", + "dotfiles-socket", "rich-logging", "task-pipeline", ] [manifest.dependency-groups] dev = [ - { name = "black", specifier = ">=24.0.0" }, + { name = "bandit", specifier = ">=1.8.0" }, + { name = "black", specifier = ">=24.10.0" }, + { name = "commitizen", specifier = ">=3.29.0" }, + { name = "coverage", specifier = ">=7.0.0" }, + { name = "fakeredis", specifier = ">=2.0.0" }, { name = "isort", specifier = ">=5.13.0" }, - { name = "mypy", specifier = ">=1.11.0" }, + { name = "mypy", specifier = ">=1.19.0" }, { name = "pre-commit", specifier = ">=3.8.0" }, { name = "pytest", specifier = ">=8.4.2" }, + { name = "pytest-asyncio", specifier = ">=0.21.0" }, { name = "pytest-cov", specifier = ">=4.1.0" }, { name = "pytest-xdist", specifier = ">=3.8.0" }, - { name = "ruff", specifier = ">=0.6.0" }, + { name = "ruff", specifier = ">=0.8.0" }, +] + +[[package]] +name = "annotated-doc" +version = "0.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" }, ] [[package]] @@ -32,9 +49,33 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, ] +[[package]] +name = "argcomplete" +version = "3.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/38/61/0b9ae6399dd4a58d8c1b1dc5a27d6f2808023d0b5dd3104bb99f45a33ff6/argcomplete-3.6.3.tar.gz", hash = "sha256:62e8ed4fd6a45864acc8235409461b72c9a28ee785a2011cc5eb78318786c89c", size = 73754, upload-time = "2025-10-20T03:33:34.741Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/74/f5/9373290775639cb67a2fce7f629a1c240dce9f12fe927bc32b2736e16dfc/argcomplete-3.6.3-py3-none-any.whl", hash = "sha256:f5007b3a600ccac5d25bbce33089211dfd49eab4a7718da3f10e3082525a92ce", size = 43846, upload-time = "2025-10-20T03:33:33.021Z" }, +] + +[[package]] +name = "bandit" +version = "1.9.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "pyyaml" }, + { name = "rich" }, + { name = "stevedore" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/aa/c3/0cb80dfe0f3076e5da7e4c5ad8e57bac6ac357ff4a6406205501cade4965/bandit-1.9.4.tar.gz", hash = "sha256:b589e5de2afe70bd4d53fa0c1da6199f4085af666fde00e8a034f152a52cd628", size = 4242677, upload-time = "2026-02-25T06:44:15.503Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/05/a4/a26d5b25671d27e03afb5401a0be5899d94ff8fab6a698b1ac5be3ec29ef/bandit-1.9.4-py3-none-any.whl", hash = "sha256:f89ffa663767f5a0585ea075f01020207e966a9c0f2b9ef56a57c7963a3f6f8e", size = 134741, upload-time = "2026-02-25T06:44:13.694Z" }, +] + [[package]] name = "black" -version = "25.12.0" +version = "26.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, @@ -44,24 +85,24 @@ dependencies = [ { name = "platformdirs" }, { name = "pytokens" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c4/d9/07b458a3f1c525ac392b5edc6b191ff140b596f9d77092429417a54e249d/black-25.12.0.tar.gz", hash = "sha256:8d3dd9cea14bff7ddc0eb243c811cdb1a011ebb4800a5f0335a01a68654796a7", size = 659264, upload-time = "2025-12-08T01:40:52.501Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e1/c5/61175d618685d42b005847464b8fb4743a67b1b8fdb75e50e5a96c31a27a/black-26.3.1.tar.gz", hash = "sha256:2c50f5063a9641c7eed7795014ba37b0f5fa227f3d408b968936e24bc0566b07", size = 666155, upload-time = "2026-03-12T03:36:03.593Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/bd/26083f805115db17fda9877b3c7321d08c647df39d0df4c4ca8f8450593e/black-25.12.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:31f96b7c98c1ddaeb07dc0f56c652e25bdedaac76d5b68a059d998b57c55594a", size = 1924178, upload-time = "2025-12-08T01:49:51.048Z" }, - { url = "https://files.pythonhosted.org/packages/89/6b/ea00d6651561e2bdd9231c4177f4f2ae19cc13a0b0574f47602a7519b6ca/black-25.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:05dd459a19e218078a1f98178c13f861fe6a9a5f88fc969ca4d9b49eb1809783", size = 1742643, upload-time = "2025-12-08T01:49:59.09Z" }, - { url = "https://files.pythonhosted.org/packages/6d/f3/360fa4182e36e9875fabcf3a9717db9d27a8d11870f21cff97725c54f35b/black-25.12.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c1f68c5eff61f226934be6b5b80296cf6939e5d2f0c2f7d543ea08b204bfaf59", size = 1800158, upload-time = "2025-12-08T01:44:27.301Z" }, - { url = "https://files.pythonhosted.org/packages/f8/08/2c64830cb6616278067e040acca21d4f79727b23077633953081c9445d61/black-25.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:274f940c147ddab4442d316b27f9e332ca586d39c85ecf59ebdea82cc9ee8892", size = 1426197, upload-time = "2025-12-08T01:45:51.198Z" }, - { url = "https://files.pythonhosted.org/packages/d4/60/a93f55fd9b9816b7432cf6842f0e3000fdd5b7869492a04b9011a133ee37/black-25.12.0-cp312-cp312-win_arm64.whl", hash = "sha256:169506ba91ef21e2e0591563deda7f00030cb466e747c4b09cb0a9dae5db2f43", size = 1237266, upload-time = "2025-12-08T01:45:10.556Z" }, - { url = "https://files.pythonhosted.org/packages/c8/52/c551e36bc95495d2aa1a37d50566267aa47608c81a53f91daa809e03293f/black-25.12.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a05ddeb656534c3e27a05a29196c962877c83fa5503db89e68857d1161ad08a5", size = 1923809, upload-time = "2025-12-08T01:46:55.126Z" }, - { url = "https://files.pythonhosted.org/packages/a0/f7/aac9b014140ee56d247e707af8db0aae2e9efc28d4a8aba92d0abd7ae9d1/black-25.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9ec77439ef3e34896995503865a85732c94396edcc739f302c5673a2315e1e7f", size = 1742384, upload-time = "2025-12-08T01:49:37.022Z" }, - { url = "https://files.pythonhosted.org/packages/74/98/38aaa018b2ab06a863974c12b14a6266badc192b20603a81b738c47e902e/black-25.12.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e509c858adf63aa61d908061b52e580c40eae0dfa72415fa47ac01b12e29baf", size = 1798761, upload-time = "2025-12-08T01:46:05.386Z" }, - { url = "https://files.pythonhosted.org/packages/16/3a/a8ac542125f61574a3f015b521ca83b47321ed19bb63fe6d7560f348bfe1/black-25.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:252678f07f5bac4ff0d0e9b261fbb029fa530cfa206d0a636a34ab445ef8ca9d", size = 1429180, upload-time = "2025-12-08T01:45:34.903Z" }, - { url = "https://files.pythonhosted.org/packages/e6/2d/bdc466a3db9145e946762d52cd55b1385509d9f9004fec1c97bdc8debbfb/black-25.12.0-cp313-cp313-win_arm64.whl", hash = "sha256:bc5b1c09fe3c931ddd20ee548511c64ebf964ada7e6f0763d443947fd1c603ce", size = 1239350, upload-time = "2025-12-08T01:46:09.458Z" }, - { url = "https://files.pythonhosted.org/packages/35/46/1d8f2542210c502e2ae1060b2e09e47af6a5e5963cb78e22ec1a11170b28/black-25.12.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:0a0953b134f9335c2434864a643c842c44fba562155c738a2a37a4d61f00cad5", size = 1917015, upload-time = "2025-12-08T01:53:27.987Z" }, - { url = "https://files.pythonhosted.org/packages/41/37/68accadf977672beb8e2c64e080f568c74159c1aaa6414b4cd2aef2d7906/black-25.12.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2355bbb6c3b76062870942d8cc450d4f8ac71f9c93c40122762c8784df49543f", size = 1741830, upload-time = "2025-12-08T01:54:36.861Z" }, - { url = "https://files.pythonhosted.org/packages/ac/76/03608a9d8f0faad47a3af3a3c8c53af3367f6c0dd2d23a84710456c7ac56/black-25.12.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9678bd991cc793e81d19aeeae57966ee02909877cb65838ccffef24c3ebac08f", size = 1791450, upload-time = "2025-12-08T01:44:52.581Z" }, - { url = "https://files.pythonhosted.org/packages/06/99/b2a4bd7dfaea7964974f947e1c76d6886d65fe5d24f687df2d85406b2609/black-25.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:97596189949a8aad13ad12fcbb4ae89330039b96ad6742e6f6b45e75ad5cfd83", size = 1452042, upload-time = "2025-12-08T01:46:13.188Z" }, - { url = "https://files.pythonhosted.org/packages/b2/7c/d9825de75ae5dd7795d007681b752275ea85a1c5d83269b4b9c754c2aaab/black-25.12.0-cp314-cp314-win_arm64.whl", hash = "sha256:778285d9ea197f34704e3791ea9404cd6d07595745907dd2ce3da7a13627b29b", size = 1267446, upload-time = "2025-12-08T01:46:14.497Z" }, - { url = "https://files.pythonhosted.org/packages/68/11/21331aed19145a952ad28fca2756a1433ee9308079bd03bd898e903a2e53/black-25.12.0-py3-none-any.whl", hash = "sha256:48ceb36c16dbc84062740049eef990bb2ce07598272e673c17d1a7720c71c828", size = 206191, upload-time = "2025-12-08T01:40:50.963Z" }, + { url = "https://files.pythonhosted.org/packages/dc/f8/da5eae4fc75e78e6dceb60624e1b9662ab00d6b452996046dfa9b8a6025b/black-26.3.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b5e6f89631eb88a7302d416594a32faeee9fb8fb848290da9d0a5f2903519fc1", size = 1895920, upload-time = "2026-03-12T03:40:13.921Z" }, + { url = "https://files.pythonhosted.org/packages/2c/9f/04e6f26534da2e1629b2b48255c264cabf5eedc5141d04516d9d68a24111/black-26.3.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:41cd2012d35b47d589cb8a16faf8a32ef7a336f56356babd9fcf70939ad1897f", size = 1718499, upload-time = "2026-03-12T03:40:15.239Z" }, + { url = "https://files.pythonhosted.org/packages/04/91/a5935b2a63e31b331060c4a9fdb5a6c725840858c599032a6f3aac94055f/black-26.3.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f76ff19ec5297dd8e66eb64deda23631e642c9393ab592826fd4bdc97a4bce7", size = 1794994, upload-time = "2026-03-12T03:40:17.124Z" }, + { url = "https://files.pythonhosted.org/packages/e7/0a/86e462cdd311a3c2a8ece708d22aba17d0b2a0d5348ca34b40cdcbea512e/black-26.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:ddb113db38838eb9f043623ba274cfaf7d51d5b0c22ecb30afe58b1bb8322983", size = 1420867, upload-time = "2026-03-12T03:40:18.83Z" }, + { url = "https://files.pythonhosted.org/packages/5b/e5/22515a19cb7eaee3440325a6b0d95d2c0e88dd180cb011b12ae488e031d1/black-26.3.1-cp312-cp312-win_arm64.whl", hash = "sha256:dfdd51fc3e64ea4f35873d1b3fb25326773d55d2329ff8449139ebaad7357efb", size = 1230124, upload-time = "2026-03-12T03:40:20.425Z" }, + { url = "https://files.pythonhosted.org/packages/f5/77/5728052a3c0450c53d9bb3945c4c46b91baa62b2cafab6801411b6271e45/black-26.3.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:855822d90f884905362f602880ed8b5df1b7e3ee7d0db2502d4388a954cc8c54", size = 1895034, upload-time = "2026-03-12T03:40:21.813Z" }, + { url = "https://files.pythonhosted.org/packages/52/73/7cae55fdfdfbe9d19e9a8d25d145018965fe2079fa908101c3733b0c55a0/black-26.3.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8a33d657f3276328ce00e4d37fe70361e1ec7614da5d7b6e78de5426cb56332f", size = 1718503, upload-time = "2026-03-12T03:40:23.666Z" }, + { url = "https://files.pythonhosted.org/packages/e1/87/af89ad449e8254fdbc74654e6467e3c9381b61472cc532ee350d28cfdafb/black-26.3.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f1cd08e99d2f9317292a311dfe578fd2a24b15dbce97792f9c4d752275c1fa56", size = 1793557, upload-time = "2026-03-12T03:40:25.497Z" }, + { url = "https://files.pythonhosted.org/packages/43/10/d6c06a791d8124b843bf325ab4ac7d2f5b98731dff84d6064eafd687ded1/black-26.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:c7e72339f841b5a237ff14f7d3880ddd0fc7f98a1199e8c4327f9a4f478c1839", size = 1422766, upload-time = "2026-03-12T03:40:27.14Z" }, + { url = "https://files.pythonhosted.org/packages/59/4f/40a582c015f2d841ac24fed6390bd68f0fc896069ff3a886317959c9daf8/black-26.3.1-cp313-cp313-win_arm64.whl", hash = "sha256:afc622538b430aa4c8c853f7f63bc582b3b8030fd8c80b70fb5fa5b834e575c2", size = 1232140, upload-time = "2026-03-12T03:40:28.882Z" }, + { url = "https://files.pythonhosted.org/packages/d5/da/e36e27c9cebc1311b7579210df6f1c86e50f2d7143ae4fcf8a5017dc8809/black-26.3.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2d6bfaf7fd0993b420bed691f20f9492d53ce9a2bcccea4b797d34e947318a78", size = 1889234, upload-time = "2026-03-12T03:40:30.964Z" }, + { url = "https://files.pythonhosted.org/packages/0e/7b/9871acf393f64a5fa33668c19350ca87177b181f44bb3d0c33b2d534f22c/black-26.3.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f89f2ab047c76a9c03f78d0d66ca519e389519902fa27e7a91117ef7611c0568", size = 1720522, upload-time = "2026-03-12T03:40:32.346Z" }, + { url = "https://files.pythonhosted.org/packages/03/87/e766c7f2e90c07fb7586cc787c9ae6462b1eedab390191f2b7fc7f6170a9/black-26.3.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b07fc0dab849d24a80a29cfab8d8a19187d1c4685d8a5e6385a5ce323c1f015f", size = 1787824, upload-time = "2026-03-12T03:40:33.636Z" }, + { url = "https://files.pythonhosted.org/packages/ac/94/2424338fb2d1875e9e83eed4c8e9c67f6905ec25afd826a911aea2b02535/black-26.3.1-cp314-cp314-win_amd64.whl", hash = "sha256:0126ae5b7c09957da2bdbd91a9ba1207453feada9e9fe51992848658c6c8e01c", size = 1445855, upload-time = "2026-03-12T03:40:35.442Z" }, + { url = "https://files.pythonhosted.org/packages/86/43/0c3338bd928afb8ee7471f1a4eec3bdbe2245ccb4a646092a222e8669840/black-26.3.1-cp314-cp314-win_arm64.whl", hash = "sha256:92c0ec1f2cc149551a2b7b47efc32c866406b6891b0ee4625e95967c8f4acfb1", size = 1258109, upload-time = "2026-03-12T03:40:36.832Z" }, + { url = "https://files.pythonhosted.org/packages/8e/0d/52d98722666d6fc6c3dd4c76df339501d6efd40e0ff95e6186a7b7f0befd/black-26.3.1-py3-none-any.whl", hash = "sha256:2bd5aa94fc267d38bb21a70d7410a89f1a1d318841855f698746f8e7f51acd1b", size = 207542, upload-time = "2026-03-12T03:36:01.668Z" }, ] [[package]] @@ -73,6 +114,79 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl", hash = "sha256:a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0", size = 7445, upload-time = "2025-11-19T20:55:50.744Z" }, ] +[[package]] +name = "charset-normalizer" +version = "3.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7b/60/e3bec1881450851b087e301bedc3daa9377a4d45f1c26aa90b0b235e38aa/charset_normalizer-3.4.6.tar.gz", hash = "sha256:1ae6b62897110aa7c79ea2f5dd38d1abca6db663687c0b1ad9aed6f6bae3d9d6", size = 143363, upload-time = "2026-03-15T18:53:25.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/62/c0815c992c9545347aeea7859b50dc9044d147e2e7278329c6e02ac9a616/charset_normalizer-3.4.6-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:2ef7fedc7a6ecbe99969cd09632516738a97eeb8bd7258bf8a0f23114c057dab", size = 295154, upload-time = "2026-03-15T18:50:50.88Z" }, + { url = "https://files.pythonhosted.org/packages/a8/37/bdca6613c2e3c58c7421891d80cc3efa1d32e882f7c4a7ee6039c3fc951a/charset_normalizer-3.4.6-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a4ea868bc28109052790eb2b52a9ab33f3aa7adc02f96673526ff47419490e21", size = 199191, upload-time = "2026-03-15T18:50:52.658Z" }, + { url = "https://files.pythonhosted.org/packages/6c/92/9934d1bbd69f7f398b38c5dae1cbf9cc672e7c34a4adf7b17c0a9c17d15d/charset_normalizer-3.4.6-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:836ab36280f21fc1a03c99cd05c6b7af70d2697e374c7af0b61ed271401a72a2", size = 218674, upload-time = "2026-03-15T18:50:54.102Z" }, + { url = "https://files.pythonhosted.org/packages/af/90/25f6ab406659286be929fd89ab0e78e38aa183fc374e03aa3c12d730af8a/charset_normalizer-3.4.6-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f1ce721c8a7dfec21fcbdfe04e8f68174183cf4e8188e0645e92aa23985c57ff", size = 215259, upload-time = "2026-03-15T18:50:55.616Z" }, + { url = "https://files.pythonhosted.org/packages/4e/ef/79a463eb0fff7f96afa04c1d4c51f8fc85426f918db467854bfb6a569ce3/charset_normalizer-3.4.6-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e28d62a8fc7a1fa411c43bd65e346f3bce9716dc51b897fbe930c5987b402d5", size = 207276, upload-time = "2026-03-15T18:50:57.054Z" }, + { url = "https://files.pythonhosted.org/packages/f7/72/d0426afec4b71dc159fa6b4e68f868cd5a3ecd918fec5813a15d292a7d10/charset_normalizer-3.4.6-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:530d548084c4a9f7a16ed4a294d459b4f229db50df689bfe92027452452943a0", size = 195161, upload-time = "2026-03-15T18:50:58.686Z" }, + { url = "https://files.pythonhosted.org/packages/bf/18/c82b06a68bfcb6ce55e508225d210c7e6a4ea122bfc0748892f3dc4e8e11/charset_normalizer-3.4.6-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:30f445ae60aad5e1f8bdbb3108e39f6fbc09f4ea16c815c66578878325f8f15a", size = 203452, upload-time = "2026-03-15T18:51:00.196Z" }, + { url = "https://files.pythonhosted.org/packages/44/d6/0c25979b92f8adafdbb946160348d8d44aa60ce99afdc27df524379875cb/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ac2393c73378fea4e52aa56285a3d64be50f1a12395afef9cce47772f60334c2", size = 202272, upload-time = "2026-03-15T18:51:01.703Z" }, + { url = "https://files.pythonhosted.org/packages/2e/3d/7fea3e8fe84136bebbac715dd1221cc25c173c57a699c030ab9b8900cbb7/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:90ca27cd8da8118b18a52d5f547859cc1f8354a00cd1e8e5120df3e30d6279e5", size = 195622, upload-time = "2026-03-15T18:51:03.526Z" }, + { url = "https://files.pythonhosted.org/packages/57/8a/d6f7fd5cb96c58ef2f681424fbca01264461336d2a7fc875e4446b1f1346/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8e5a94886bedca0f9b78fecd6afb6629142fd2605aa70a125d49f4edc6037ee6", size = 220056, upload-time = "2026-03-15T18:51:05.269Z" }, + { url = "https://files.pythonhosted.org/packages/16/50/478cdda782c8c9c3fb5da3cc72dd7f331f031e7f1363a893cdd6ca0f8de0/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:695f5c2823691a25f17bc5d5ffe79fa90972cc34b002ac6c843bb8a1720e950d", size = 203751, upload-time = "2026-03-15T18:51:06.858Z" }, + { url = "https://files.pythonhosted.org/packages/75/fc/cc2fcac943939c8e4d8791abfa139f685e5150cae9f94b60f12520feaa9b/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:231d4da14bcd9301310faf492051bee27df11f2bc7549bc0bb41fef11b82daa2", size = 216563, upload-time = "2026-03-15T18:51:08.564Z" }, + { url = "https://files.pythonhosted.org/packages/a8/b7/a4add1d9a5f68f3d037261aecca83abdb0ab15960a3591d340e829b37298/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a056d1ad2633548ca18ffa2f85c202cfb48b68615129143915b8dc72a806a923", size = 209265, upload-time = "2026-03-15T18:51:10.312Z" }, + { url = "https://files.pythonhosted.org/packages/6c/18/c094561b5d64a24277707698e54b7f67bd17a4f857bbfbb1072bba07c8bf/charset_normalizer-3.4.6-cp312-cp312-win32.whl", hash = "sha256:c2274ca724536f173122f36c98ce188fd24ce3dad886ec2b7af859518ce008a4", size = 144229, upload-time = "2026-03-15T18:51:11.694Z" }, + { url = "https://files.pythonhosted.org/packages/ab/20/0567efb3a8fd481b8f34f739ebddc098ed062a59fed41a8d193a61939e8f/charset_normalizer-3.4.6-cp312-cp312-win_amd64.whl", hash = "sha256:c8ae56368f8cc97c7e40a7ee18e1cedaf8e780cd8bc5ed5ac8b81f238614facb", size = 154277, upload-time = "2026-03-15T18:51:13.004Z" }, + { url = "https://files.pythonhosted.org/packages/15/57/28d79b44b51933119e21f65479d0864a8d5893e494cf5daab15df0247c17/charset_normalizer-3.4.6-cp312-cp312-win_arm64.whl", hash = "sha256:899d28f422116b08be5118ef350c292b36fc15ec2daeb9ea987c89281c7bb5c4", size = 142817, upload-time = "2026-03-15T18:51:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/1e/1d/4fdabeef4e231153b6ed7567602f3b68265ec4e5b76d6024cf647d43d981/charset_normalizer-3.4.6-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:11afb56037cbc4b1555a34dd69151e8e069bee82e613a73bef6e714ce733585f", size = 294823, upload-time = "2026-03-15T18:51:15.755Z" }, + { url = "https://files.pythonhosted.org/packages/47/7b/20e809b89c69d37be748d98e84dce6820bf663cf19cf6b942c951a3e8f41/charset_normalizer-3.4.6-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:423fb7e748a08f854a08a222b983f4df1912b1daedce51a72bd24fe8f26a1843", size = 198527, upload-time = "2026-03-15T18:51:17.177Z" }, + { url = "https://files.pythonhosted.org/packages/37/a6/4f8d27527d59c039dce6f7622593cdcd3d70a8504d87d09eb11e9fdc6062/charset_normalizer-3.4.6-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d73beaac5e90173ac3deb9928a74763a6d230f494e4bfb422c217a0ad8e629bf", size = 218388, upload-time = "2026-03-15T18:51:18.934Z" }, + { url = "https://files.pythonhosted.org/packages/f6/9b/4770ccb3e491a9bacf1c46cc8b812214fe367c86a96353ccc6daf87b01ec/charset_normalizer-3.4.6-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d60377dce4511655582e300dc1e5a5f24ba0cb229005a1d5c8d0cb72bb758ab8", size = 214563, upload-time = "2026-03-15T18:51:20.374Z" }, + { url = "https://files.pythonhosted.org/packages/2b/58/a199d245894b12db0b957d627516c78e055adc3a0d978bc7f65ddaf7c399/charset_normalizer-3.4.6-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:530e8cebeea0d76bdcf93357aa5e41336f48c3dc709ac52da2bb167c5b8271d9", size = 206587, upload-time = "2026-03-15T18:51:21.807Z" }, + { url = "https://files.pythonhosted.org/packages/7e/70/3def227f1ec56f5c69dfc8392b8bd63b11a18ca8178d9211d7cc5e5e4f27/charset_normalizer-3.4.6-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:a26611d9987b230566f24a0a125f17fe0de6a6aff9f25c9f564aaa2721a5fb88", size = 194724, upload-time = "2026-03-15T18:51:23.508Z" }, + { url = "https://files.pythonhosted.org/packages/58/ab/9318352e220c05efd31c2779a23b50969dc94b985a2efa643ed9077bfca5/charset_normalizer-3.4.6-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:34315ff4fc374b285ad7f4a0bf7dcbfe769e1b104230d40f49f700d4ab6bbd84", size = 202956, upload-time = "2026-03-15T18:51:25.239Z" }, + { url = "https://files.pythonhosted.org/packages/75/13/f3550a3ac25b70f87ac98c40d3199a8503676c2f1620efbf8d42095cfc40/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5f8ddd609f9e1af8c7bd6e2aca279c931aefecd148a14402d4e368f3171769fd", size = 201923, upload-time = "2026-03-15T18:51:26.682Z" }, + { url = "https://files.pythonhosted.org/packages/1b/db/c5c643b912740b45e8eec21de1bbab8e7fc085944d37e1e709d3dcd9d72f/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:80d0a5615143c0b3225e5e3ef22c8d5d51f3f72ce0ea6fb84c943546c7b25b6c", size = 195366, upload-time = "2026-03-15T18:51:28.129Z" }, + { url = "https://files.pythonhosted.org/packages/5a/67/3b1c62744f9b2448443e0eb160d8b001c849ec3fef591e012eda6484787c/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:92734d4d8d187a354a556626c221cd1a892a4e0802ccb2af432a1d85ec012194", size = 219752, upload-time = "2026-03-15T18:51:29.556Z" }, + { url = "https://files.pythonhosted.org/packages/f6/98/32ffbaf7f0366ffb0445930b87d103f6b406bc2c271563644bde8a2b1093/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:613f19aa6e082cf96e17e3ffd89383343d0d589abda756b7764cf78361fd41dc", size = 203296, upload-time = "2026-03-15T18:51:30.921Z" }, + { url = "https://files.pythonhosted.org/packages/41/12/5d308c1bbe60cabb0c5ef511574a647067e2a1f631bc8634fcafaccd8293/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:2b1a63e8224e401cafe7739f77efd3f9e7f5f2026bda4aead8e59afab537784f", size = 215956, upload-time = "2026-03-15T18:51:32.399Z" }, + { url = "https://files.pythonhosted.org/packages/53/e9/5f85f6c5e20669dbe56b165c67b0260547dea97dba7e187938833d791687/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6cceb5473417d28edd20c6c984ab6fee6c6267d38d906823ebfe20b03d607dc2", size = 208652, upload-time = "2026-03-15T18:51:34.214Z" }, + { url = "https://files.pythonhosted.org/packages/f1/11/897052ea6af56df3eef3ca94edafee410ca699ca0c7b87960ad19932c55e/charset_normalizer-3.4.6-cp313-cp313-win32.whl", hash = "sha256:d7de2637729c67d67cf87614b566626057e95c303bc0a55ffe391f5205e7003d", size = 143940, upload-time = "2026-03-15T18:51:36.15Z" }, + { url = "https://files.pythonhosted.org/packages/a1/5c/724b6b363603e419829f561c854b87ed7c7e31231a7908708ac086cdf3e2/charset_normalizer-3.4.6-cp313-cp313-win_amd64.whl", hash = "sha256:572d7c822caf521f0525ba1bce1a622a0b85cf47ffbdae6c9c19e3b5ac3c4389", size = 154101, upload-time = "2026-03-15T18:51:37.876Z" }, + { url = "https://files.pythonhosted.org/packages/01/a5/7abf15b4c0968e47020f9ca0935fb3274deb87cb288cd187cad92e8cdffd/charset_normalizer-3.4.6-cp313-cp313-win_arm64.whl", hash = "sha256:a4474d924a47185a06411e0064b803c68be044be2d60e50e8bddcc2649957c1f", size = 143109, upload-time = "2026-03-15T18:51:39.565Z" }, + { url = "https://files.pythonhosted.org/packages/25/6f/ffe1e1259f384594063ea1869bfb6be5cdb8bc81020fc36c3636bc8302a1/charset_normalizer-3.4.6-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:9cc6e6d9e571d2f863fa77700701dae73ed5f78881efc8b3f9a4398772ff53e8", size = 294458, upload-time = "2026-03-15T18:51:41.134Z" }, + { url = "https://files.pythonhosted.org/packages/56/60/09bb6c13a8c1016c2ed5c6a6488e4ffef506461aa5161662bd7636936fb1/charset_normalizer-3.4.6-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef5960d965e67165d75b7c7ffc60a83ec5abfc5c11b764ec13ea54fbef8b4421", size = 199277, upload-time = "2026-03-15T18:51:42.953Z" }, + { url = "https://files.pythonhosted.org/packages/00/50/dcfbb72a5138bbefdc3332e8d81a23494bf67998b4b100703fd15fa52d81/charset_normalizer-3.4.6-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b3694e3f87f8ac7ce279d4355645b3c878d24d1424581b46282f24b92f5a4ae2", size = 218758, upload-time = "2026-03-15T18:51:44.339Z" }, + { url = "https://files.pythonhosted.org/packages/03/b3/d79a9a191bb75f5aa81f3aaaa387ef29ce7cb7a9e5074ba8ea095cc073c2/charset_normalizer-3.4.6-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5d11595abf8dd942a77883a39d81433739b287b6aa71620f15164f8096221b30", size = 215299, upload-time = "2026-03-15T18:51:45.871Z" }, + { url = "https://files.pythonhosted.org/packages/76/7e/bc8911719f7084f72fd545f647601ea3532363927f807d296a8c88a62c0d/charset_normalizer-3.4.6-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7bda6eebafd42133efdca535b04ccb338ab29467b3f7bf79569883676fc628db", size = 206811, upload-time = "2026-03-15T18:51:47.308Z" }, + { url = "https://files.pythonhosted.org/packages/e2/40/c430b969d41dda0c465aa36cc7c2c068afb67177bef50905ac371b28ccc7/charset_normalizer-3.4.6-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:bbc8c8650c6e51041ad1be191742b8b421d05bbd3410f43fa2a00c8db87678e8", size = 193706, upload-time = "2026-03-15T18:51:48.849Z" }, + { url = "https://files.pythonhosted.org/packages/48/15/e35e0590af254f7df984de1323640ef375df5761f615b6225ba8deb9799a/charset_normalizer-3.4.6-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:22c6f0c2fbc31e76c3b8a86fba1a56eda6166e238c29cdd3d14befdb4a4e4815", size = 202706, upload-time = "2026-03-15T18:51:50.257Z" }, + { url = "https://files.pythonhosted.org/packages/5e/bd/f736f7b9cc5e93a18b794a50346bb16fbfd6b37f99e8f306f7951d27c17c/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7edbed096e4a4798710ed6bc75dcaa2a21b68b6c356553ac4823c3658d53743a", size = 202497, upload-time = "2026-03-15T18:51:52.012Z" }, + { url = "https://files.pythonhosted.org/packages/9d/ba/2cc9e3e7dfdf7760a6ed8da7446d22536f3d0ce114ac63dee2a5a3599e62/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:7f9019c9cb613f084481bd6a100b12e1547cf2efe362d873c2e31e4035a6fa43", size = 193511, upload-time = "2026-03-15T18:51:53.723Z" }, + { url = "https://files.pythonhosted.org/packages/9e/cb/5be49b5f776e5613be07298c80e1b02a2d900f7a7de807230595c85a8b2e/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:58c948d0d086229efc484fe2f30c2d382c86720f55cd9bc33591774348ad44e0", size = 220133, upload-time = "2026-03-15T18:51:55.333Z" }, + { url = "https://files.pythonhosted.org/packages/83/43/99f1b5dad345accb322c80c7821071554f791a95ee50c1c90041c157ae99/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:419a9d91bd238052642a51938af8ac05da5b3343becde08d5cdeab9046df9ee1", size = 203035, upload-time = "2026-03-15T18:51:56.736Z" }, + { url = "https://files.pythonhosted.org/packages/87/9a/62c2cb6a531483b55dddff1a68b3d891a8b498f3ca555fbcf2978e804d9d/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:5273b9f0b5835ff0350c0828faea623c68bfa65b792720c453e22b25cc72930f", size = 216321, upload-time = "2026-03-15T18:51:58.17Z" }, + { url = "https://files.pythonhosted.org/packages/6e/79/94a010ff81e3aec7c293eb82c28f930918e517bc144c9906a060844462eb/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:0e901eb1049fdb80f5bd11ed5ea1e498ec423102f7a9b9e4645d5b8204ff2815", size = 208973, upload-time = "2026-03-15T18:51:59.998Z" }, + { url = "https://files.pythonhosted.org/packages/2a/57/4ecff6d4ec8585342f0c71bc03efaa99cb7468f7c91a57b105bcd561cea8/charset_normalizer-3.4.6-cp314-cp314-win32.whl", hash = "sha256:b4ff1d35e8c5bd078be89349b6f3a845128e685e751b6ea1169cf2160b344c4d", size = 144610, upload-time = "2026-03-15T18:52:02.213Z" }, + { url = "https://files.pythonhosted.org/packages/80/94/8434a02d9d7f168c25767c64671fead8d599744a05d6a6c877144c754246/charset_normalizer-3.4.6-cp314-cp314-win_amd64.whl", hash = "sha256:74119174722c4349af9708993118581686f343adc1c8c9c007d59be90d077f3f", size = 154962, upload-time = "2026-03-15T18:52:03.658Z" }, + { url = "https://files.pythonhosted.org/packages/46/4c/48f2cdbfd923026503dfd67ccea45c94fd8fe988d9056b468579c66ed62b/charset_normalizer-3.4.6-cp314-cp314-win_arm64.whl", hash = "sha256:e5bcc1a1ae744e0bb59641171ae53743760130600da8db48cbb6e4918e186e4e", size = 143595, upload-time = "2026-03-15T18:52:05.123Z" }, + { url = "https://files.pythonhosted.org/packages/31/93/8878be7569f87b14f1d52032946131bcb6ebbd8af3e20446bc04053dc3f1/charset_normalizer-3.4.6-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:ad8faf8df23f0378c6d527d8b0b15ea4a2e23c89376877c598c4870d1b2c7866", size = 314828, upload-time = "2026-03-15T18:52:06.831Z" }, + { url = "https://files.pythonhosted.org/packages/06/b6/fae511ca98aac69ecc35cde828b0a3d146325dd03d99655ad38fc2cc3293/charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f5ea69428fa1b49573eef0cc44a1d43bebd45ad0c611eb7d7eac760c7ae771bc", size = 208138, upload-time = "2026-03-15T18:52:08.239Z" }, + { url = "https://files.pythonhosted.org/packages/54/57/64caf6e1bf07274a1e0b7c160a55ee9e8c9ec32c46846ce59b9c333f7008/charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:06a7e86163334edfc5d20fe104db92fcd666e5a5df0977cb5680a506fe26cc8e", size = 224679, upload-time = "2026-03-15T18:52:10.043Z" }, + { url = "https://files.pythonhosted.org/packages/aa/cb/9ff5a25b9273ef160861b41f6937f86fae18b0792fe0a8e75e06acb08f1d/charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e1f6e2f00a6b8edb562826e4632e26d063ac10307e80f7461f7de3ad8ef3f077", size = 223475, upload-time = "2026-03-15T18:52:11.854Z" }, + { url = "https://files.pythonhosted.org/packages/fc/97/440635fc093b8d7347502a377031f9605a1039c958f3cd18dcacffb37743/charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:95b52c68d64c1878818687a473a10547b3292e82b6f6fe483808fb1468e2f52f", size = 215230, upload-time = "2026-03-15T18:52:13.325Z" }, + { url = "https://files.pythonhosted.org/packages/cd/24/afff630feb571a13f07c8539fbb502d2ab494019492aaffc78ef41f1d1d0/charset_normalizer-3.4.6-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:7504e9b7dc05f99a9bbb4525c67a2c155073b44d720470a148b34166a69c054e", size = 199045, upload-time = "2026-03-15T18:52:14.752Z" }, + { url = "https://files.pythonhosted.org/packages/e5/17/d1399ecdaf7e0498c327433e7eefdd862b41236a7e484355b8e0e5ebd64b/charset_normalizer-3.4.6-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:172985e4ff804a7ad08eebec0a1640ece87ba5041d565fff23c8f99c1f389484", size = 211658, upload-time = "2026-03-15T18:52:16.278Z" }, + { url = "https://files.pythonhosted.org/packages/b5/38/16baa0affb957b3d880e5ac2144caf3f9d7de7bc4a91842e447fbb5e8b67/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:4be9f4830ba8741527693848403e2c457c16e499100963ec711b1c6f2049b7c7", size = 210769, upload-time = "2026-03-15T18:52:17.782Z" }, + { url = "https://files.pythonhosted.org/packages/05/34/c531bc6ac4c21da9ddfddb3107be2287188b3ea4b53b70fc58f2a77ac8d8/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:79090741d842f564b1b2827c0b82d846405b744d31e84f18d7a7b41c20e473ff", size = 201328, upload-time = "2026-03-15T18:52:19.553Z" }, + { url = "https://files.pythonhosted.org/packages/fa/73/a5a1e9ca5f234519c1953608a03fe109c306b97fdfb25f09182babad51a7/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:87725cfb1a4f1f8c2fc9890ae2f42094120f4b44db9360be5d99a4c6b0e03a9e", size = 225302, upload-time = "2026-03-15T18:52:21.043Z" }, + { url = "https://files.pythonhosted.org/packages/ba/f6/cd782923d112d296294dea4bcc7af5a7ae0f86ab79f8fefbda5526b6cfc0/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:fcce033e4021347d80ed9c66dcf1e7b1546319834b74445f561d2e2221de5659", size = 211127, upload-time = "2026-03-15T18:52:22.491Z" }, + { url = "https://files.pythonhosted.org/packages/0e/c5/0b6898950627af7d6103a449b22320372c24c6feda91aa24e201a478d161/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:ca0276464d148c72defa8bb4390cce01b4a0e425f3b50d1435aa6d7a18107602", size = 222840, upload-time = "2026-03-15T18:52:24.113Z" }, + { url = "https://files.pythonhosted.org/packages/7d/25/c4bba773bef442cbdc06111d40daa3de5050a676fa26e85090fc54dd12f0/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:197c1a244a274bb016dd8b79204850144ef77fe81c5b797dc389327adb552407", size = 216890, upload-time = "2026-03-15T18:52:25.541Z" }, + { url = "https://files.pythonhosted.org/packages/35/1a/05dacadb0978da72ee287b0143097db12f2e7e8d3ffc4647da07a383b0b7/charset_normalizer-3.4.6-cp314-cp314t-win32.whl", hash = "sha256:2a24157fa36980478dd1770b585c0f30d19e18f4fb0c47c13aa568f871718579", size = 155379, upload-time = "2026-03-15T18:52:27.05Z" }, + { url = "https://files.pythonhosted.org/packages/5d/7a/d269d834cb3a76291651256f3b9a5945e81d0a49ab9f4a498964e83c0416/charset_normalizer-3.4.6-cp314-cp314t-win_amd64.whl", hash = "sha256:cd5e2801c89992ed8c0a3f0293ae83c159a60d9a5d685005383ef4caca77f2c4", size = 169043, upload-time = "2026-03-15T18:52:28.502Z" }, + { url = "https://files.pythonhosted.org/packages/23/06/28b29fba521a37a8932c6a84192175c34d49f84a6d4773fa63d05f9aff22/charset_normalizer-3.4.6-cp314-cp314t-win_arm64.whl", hash = "sha256:47955475ac79cc504ef2704b192364e51d0d473ad452caedd0002605f780101c", size = 148523, upload-time = "2026-03-15T18:52:29.956Z" }, + { url = "https://files.pythonhosted.org/packages/2a/68/687187c7e26cb24ccbd88e5069f5ef00eba804d36dde11d99aad0838ab45/charset_normalizer-3.4.6-py3-none-any.whl", hash = "sha256:947cf925bc916d90adba35a64c82aace04fa39b46b52d4630ece166655905a69", size = 61455, upload-time = "2026-03-15T18:53:23.833Z" }, +] + [[package]] name = "click" version = "8.3.1" @@ -94,6 +208,29 @@ 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 = "commitizen" +version = "4.13.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "argcomplete" }, + { name = "charset-normalizer" }, + { name = "colorama" }, + { name = "decli" }, + { name = "deprecated" }, + { name = "jinja2" }, + { name = "packaging" }, + { name = "prompt-toolkit" }, + { name = "pyyaml" }, + { name = "questionary" }, + { name = "termcolor" }, + { name = "tomlkit" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a6/44/10f95e8178ab5a584298726a4a94ceb83a7f77e00741fec4680df05fedd5/commitizen-4.13.9.tar.gz", hash = "sha256:2b4567ed50555e10920e5bd804a6a4e2c42ec70bb74f14a83f2680fe9eaf9727", size = 64145, upload-time = "2026-02-25T02:40:05.326Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/28/22/9b14ee0f17f0aad219a2fb37a293a57b8324d9d195c6ef6807bcd0bf2055/commitizen-4.13.9-py3-none-any.whl", hash = "sha256:d2af3d6a83cacec9d5200e17768942c5de6266f93d932c955986c60c4285f2db", size = 85373, upload-time = "2026-02-25T02:40:03.83Z" }, +] + [[package]] name = "container-manager" version = "0.1.0" @@ -125,78 +262,196 @@ dev = [ { name = "ruff", specifier = ">=0.6.0" }, ] +[[package]] +name = "core-cache" +version = "0.1.0" +source = { editable = "packages/cache" } +dependencies = [ + { name = "core-storage" }, + { name = "pydantic" }, +] + +[package.dev-dependencies] +dev = [ + { name = "mypy" }, + { name = "pytest" }, + { name = "pytest-cov" }, + { name = "pytest-xdist" }, + { name = "ruff" }, +] + +[package.metadata] +requires-dist = [ + { name = "core-storage", editable = "packages/storage" }, + { name = "pydantic", specifier = ">=2.0" }, +] + +[package.metadata.requires-dev] +dev = [ + { name = "mypy", specifier = ">=1.19.0" }, + { name = "pytest", specifier = ">=8.4.2" }, + { name = "pytest-cov", specifier = ">=4.1.0" }, + { name = "pytest-xdist", specifier = ">=3.8.0" }, + { name = "ruff", specifier = ">=0.6.0" }, +] + +[[package]] +name = "core-storage" +version = "0.1.0" +source = { editable = "packages/storage" } +dependencies = [ + { name = "filelock" }, + { name = "pydantic" }, + { name = "typer" }, +] + +[package.optional-dependencies] +redis = [ + { name = "redis" }, +] + +[package.dev-dependencies] +dev = [ + { name = "bandit" }, + { name = "black" }, + { name = "fakeredis" }, + { name = "isort" }, + { name = "mypy" }, + { name = "pre-commit" }, + { name = "pytest" }, + { name = "pytest-cov" }, + { name = "pytest-xdist" }, + { name = "redis" }, + { name = "ruff" }, +] + +[package.metadata] +requires-dist = [ + { name = "filelock", specifier = ">=3.0.0" }, + { name = "pydantic", specifier = ">=2.0" }, + { name = "redis", marker = "extra == 'redis'", specifier = ">=5.0.0" }, + { name = "typer", specifier = ">=0.9.0" }, +] +provides-extras = ["redis"] + +[package.metadata.requires-dev] +dev = [ + { name = "bandit", specifier = ">=1.8.0" }, + { name = "black", specifier = ">=24.0.0" }, + { name = "fakeredis", specifier = ">=2.0.0" }, + { name = "isort", specifier = ">=5.13.0" }, + { name = "mypy", specifier = ">=1.19.0" }, + { name = "pre-commit", specifier = ">=3.8.0" }, + { name = "pytest", specifier = ">=8.4.2" }, + { name = "pytest-cov", specifier = ">=4.1.0" }, + { name = "pytest-xdist", specifier = ">=3.8.0" }, + { name = "redis", specifier = ">=5.0.0" }, + { name = "ruff", specifier = ">=0.6.0" }, +] + [[package]] name = "coverage" -version = "7.13.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b6/45/2c665ca77ec32ad67e25c77daf1cee28ee4558f3bc571cdbaf88a00b9f23/coverage-7.13.0.tar.gz", hash = "sha256:a394aa27f2d7ff9bc04cf703817773a59ad6dfbd577032e690f961d2460ee936", size = 820905, upload-time = "2025-12-08T13:14:38.055Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9b/f1/2619559f17f31ba00fc40908efd1fbf1d0a5536eb75dc8341e7d660a08de/coverage-7.13.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0b3d67d31383c4c68e19a88e28fc4c2e29517580f1b0ebec4a069d502ce1e0bf", size = 218274, upload-time = "2025-12-08T13:12:52.095Z" }, - { url = "https://files.pythonhosted.org/packages/2b/11/30d71ae5d6e949ff93b2a79a2c1b4822e00423116c5c6edfaeef37301396/coverage-7.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:581f086833d24a22c89ae0fe2142cfaa1c92c930adf637ddf122d55083fb5a0f", size = 218638, upload-time = "2025-12-08T13:12:53.418Z" }, - { url = "https://files.pythonhosted.org/packages/79/c2/fce80fc6ded8d77e53207489d6065d0fed75db8951457f9213776615e0f5/coverage-7.13.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0a3a30f0e257df382f5f9534d4ce3d4cf06eafaf5192beb1a7bd066cb10e78fb", size = 250129, upload-time = "2025-12-08T13:12:54.744Z" }, - { url = "https://files.pythonhosted.org/packages/5b/b6/51b5d1eb6fcbb9a1d5d6984e26cbe09018475c2922d554fd724dd0f056ee/coverage-7.13.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:583221913fbc8f53b88c42e8dbb8fca1d0f2e597cb190ce45916662b8b9d9621", size = 252885, upload-time = "2025-12-08T13:12:56.401Z" }, - { url = "https://files.pythonhosted.org/packages/0d/f8/972a5affea41de798691ab15d023d3530f9f56a72e12e243f35031846ff7/coverage-7.13.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f5d9bd30756fff3e7216491a0d6d520c448d5124d3d8e8f56446d6412499e74", size = 253974, upload-time = "2025-12-08T13:12:57.718Z" }, - { url = "https://files.pythonhosted.org/packages/8a/56/116513aee860b2c7968aa3506b0f59b22a959261d1dbf3aea7b4450a7520/coverage-7.13.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a23e5a1f8b982d56fa64f8e442e037f6ce29322f1f9e6c2344cd9e9f4407ee57", size = 250538, upload-time = "2025-12-08T13:12:59.254Z" }, - { url = "https://files.pythonhosted.org/packages/d6/75/074476d64248fbadf16dfafbf93fdcede389ec821f74ca858d7c87d2a98c/coverage-7.13.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9b01c22bc74a7fb44066aaf765224c0d933ddf1f5047d6cdfe4795504a4493f8", size = 251912, upload-time = "2025-12-08T13:13:00.604Z" }, - { url = "https://files.pythonhosted.org/packages/f2/d2/aa4f8acd1f7c06024705c12609d8698c51b27e4d635d717cd1934c9668e2/coverage-7.13.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:898cce66d0836973f48dda4e3514d863d70142bdf6dfab932b9b6a90ea5b222d", size = 250054, upload-time = "2025-12-08T13:13:01.892Z" }, - { url = "https://files.pythonhosted.org/packages/19/98/8df9e1af6a493b03694a1e8070e024e7d2cdc77adedc225a35e616d505de/coverage-7.13.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:3ab483ea0e251b5790c2aac03acde31bff0c736bf8a86829b89382b407cd1c3b", size = 249619, upload-time = "2025-12-08T13:13:03.236Z" }, - { url = "https://files.pythonhosted.org/packages/d8/71/f8679231f3353018ca66ef647fa6fe7b77e6bff7845be54ab84f86233363/coverage-7.13.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1d84e91521c5e4cb6602fe11ece3e1de03b2760e14ae4fcf1a4b56fa3c801fcd", size = 251496, upload-time = "2025-12-08T13:13:04.511Z" }, - { url = "https://files.pythonhosted.org/packages/04/86/9cb406388034eaf3c606c22094edbbb82eea1fa9d20c0e9efadff20d0733/coverage-7.13.0-cp312-cp312-win32.whl", hash = "sha256:193c3887285eec1dbdb3f2bd7fbc351d570ca9c02ca756c3afbc71b3c98af6ef", size = 220808, upload-time = "2025-12-08T13:13:06.422Z" }, - { url = "https://files.pythonhosted.org/packages/1c/59/af483673df6455795daf5f447c2f81a3d2fcfc893a22b8ace983791f6f34/coverage-7.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:4f3e223b2b2db5e0db0c2b97286aba0036ca000f06aca9b12112eaa9af3d92ae", size = 221616, upload-time = "2025-12-08T13:13:07.95Z" }, - { url = "https://files.pythonhosted.org/packages/64/b0/959d582572b30a6830398c60dd419c1965ca4b5fb38ac6b7093a0d50ca8d/coverage-7.13.0-cp312-cp312-win_arm64.whl", hash = "sha256:086cede306d96202e15a4b77ace8472e39d9f4e5f9fd92dd4fecdfb2313b2080", size = 220261, upload-time = "2025-12-08T13:13:09.581Z" }, - { url = "https://files.pythonhosted.org/packages/7c/cc/bce226595eb3bf7d13ccffe154c3c487a22222d87ff018525ab4dd2e9542/coverage-7.13.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:28ee1c96109974af104028a8ef57cec21447d42d0e937c0275329272e370ebcf", size = 218297, upload-time = "2025-12-08T13:13:10.977Z" }, - { url = "https://files.pythonhosted.org/packages/3b/9f/73c4d34600aae03447dff3d7ad1d0ac649856bfb87d1ca7d681cfc913f9e/coverage-7.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d1e97353dcc5587b85986cda4ff3ec98081d7e84dd95e8b2a6d59820f0545f8a", size = 218673, upload-time = "2025-12-08T13:13:12.562Z" }, - { url = "https://files.pythonhosted.org/packages/63/ab/8fa097db361a1e8586535ae5073559e6229596b3489ec3ef2f5b38df8cb2/coverage-7.13.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:99acd4dfdfeb58e1937629eb1ab6ab0899b131f183ee5f23e0b5da5cba2fec74", size = 249652, upload-time = "2025-12-08T13:13:13.909Z" }, - { url = "https://files.pythonhosted.org/packages/90/3a/9bfd4de2ff191feb37ef9465855ca56a6f2f30a3bca172e474130731ac3d/coverage-7.13.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ff45e0cd8451e293b63ced93161e189780baf444119391b3e7d25315060368a6", size = 252251, upload-time = "2025-12-08T13:13:15.553Z" }, - { url = "https://files.pythonhosted.org/packages/df/61/b5d8105f016e1b5874af0d7c67542da780ccd4a5f2244a433d3e20ceb1ad/coverage-7.13.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f4f72a85316d8e13234cafe0a9f81b40418ad7a082792fa4165bd7d45d96066b", size = 253492, upload-time = "2025-12-08T13:13:16.849Z" }, - { url = "https://files.pythonhosted.org/packages/f3/b8/0fad449981803cc47a4694768b99823fb23632150743f9c83af329bb6090/coverage-7.13.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:11c21557d0e0a5a38632cbbaca5f008723b26a89d70db6315523df6df77d6232", size = 249850, upload-time = "2025-12-08T13:13:18.142Z" }, - { url = "https://files.pythonhosted.org/packages/9a/e9/8d68337c3125014d918cf4327d5257553a710a2995a6a6de2ac77e5aa429/coverage-7.13.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:76541dc8d53715fb4f7a3a06b34b0dc6846e3c69bc6204c55653a85dd6220971", size = 251633, upload-time = "2025-12-08T13:13:19.56Z" }, - { url = "https://files.pythonhosted.org/packages/55/14/d4112ab26b3a1bc4b3c1295d8452dcf399ed25be4cf649002fb3e64b2d93/coverage-7.13.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:6e9e451dee940a86789134b6b0ffbe31c454ade3b849bb8a9d2cca2541a8e91d", size = 249586, upload-time = "2025-12-08T13:13:20.883Z" }, - { url = "https://files.pythonhosted.org/packages/2c/a9/22b0000186db663b0d82f86c2f1028099ae9ac202491685051e2a11a5218/coverage-7.13.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:5c67dace46f361125e6b9cace8fe0b729ed8479f47e70c89b838d319375c8137", size = 249412, upload-time = "2025-12-08T13:13:22.22Z" }, - { url = "https://files.pythonhosted.org/packages/a1/2e/42d8e0d9e7527fba439acdc6ed24a2b97613b1dc85849b1dd935c2cffef0/coverage-7.13.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f59883c643cb19630500f57016f76cfdcd6845ca8c5b5ea1f6e17f74c8e5f511", size = 251191, upload-time = "2025-12-08T13:13:23.899Z" }, - { url = "https://files.pythonhosted.org/packages/a4/af/8c7af92b1377fd8860536aadd58745119252aaaa71a5213e5a8e8007a9f5/coverage-7.13.0-cp313-cp313-win32.whl", hash = "sha256:58632b187be6f0be500f553be41e277712baa278147ecb7559983c6d9faf7ae1", size = 220829, upload-time = "2025-12-08T13:13:25.182Z" }, - { url = "https://files.pythonhosted.org/packages/58/f9/725e8bf16f343d33cbe076c75dc8370262e194ff10072c0608b8e5cf33a3/coverage-7.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:73419b89f812f498aca53f757dd834919b48ce4799f9d5cad33ca0ae442bdb1a", size = 221640, upload-time = "2025-12-08T13:13:26.836Z" }, - { url = "https://files.pythonhosted.org/packages/8a/ff/e98311000aa6933cc79274e2b6b94a2fe0fe3434fca778eba82003675496/coverage-7.13.0-cp313-cp313-win_arm64.whl", hash = "sha256:eb76670874fdd6091eedcc856128ee48c41a9bbbb9c3f1c7c3cf169290e3ffd6", size = 220269, upload-time = "2025-12-08T13:13:28.116Z" }, - { url = "https://files.pythonhosted.org/packages/cf/cf/bbaa2e1275b300343ea865f7d424cc0a2e2a1df6925a070b2b2d5d765330/coverage-7.13.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:6e63ccc6e0ad8986386461c3c4b737540f20426e7ec932f42e030320896c311a", size = 218990, upload-time = "2025-12-08T13:13:29.463Z" }, - { url = "https://files.pythonhosted.org/packages/21/1d/82f0b3323b3d149d7672e7744c116e9c170f4957e0c42572f0366dbb4477/coverage-7.13.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:494f5459ffa1bd45e18558cd98710c36c0b8fbfa82a5eabcbe671d80ecffbfe8", size = 219340, upload-time = "2025-12-08T13:13:31.524Z" }, - { url = "https://files.pythonhosted.org/packages/fb/e3/fe3fd4702a3832a255f4d43013eacb0ef5fc155a5960ea9269d8696db28b/coverage-7.13.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:06cac81bf10f74034e055e903f5f946e3e26fc51c09fc9f584e4a1605d977053", size = 260638, upload-time = "2025-12-08T13:13:32.965Z" }, - { url = "https://files.pythonhosted.org/packages/ad/01/63186cb000307f2b4da463f72af9b85d380236965574c78e7e27680a2593/coverage-7.13.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f2ffc92b46ed6e6760f1d47a71e56b5664781bc68986dbd1836b2b70c0ce2071", size = 262705, upload-time = "2025-12-08T13:13:34.378Z" }, - { url = "https://files.pythonhosted.org/packages/7c/a1/c0dacef0cc865f2455d59eed3548573ce47ed603205ffd0735d1d78b5906/coverage-7.13.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0602f701057c6823e5db1b74530ce85f17c3c5be5c85fc042ac939cbd909426e", size = 265125, upload-time = "2025-12-08T13:13:35.73Z" }, - { url = "https://files.pythonhosted.org/packages/ef/92/82b99223628b61300bd382c205795533bed021505eab6dd86e11fb5d7925/coverage-7.13.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:25dc33618d45456ccb1d37bce44bc78cf269909aa14c4db2e03d63146a8a1493", size = 259844, upload-time = "2025-12-08T13:13:37.69Z" }, - { url = "https://files.pythonhosted.org/packages/cf/2c/89b0291ae4e6cd59ef042708e1c438e2290f8c31959a20055d8768349ee2/coverage-7.13.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:71936a8b3b977ddd0b694c28c6a34f4fff2e9dd201969a4ff5d5fc7742d614b0", size = 262700, upload-time = "2025-12-08T13:13:39.525Z" }, - { url = "https://files.pythonhosted.org/packages/bf/f9/a5f992efae1996245e796bae34ceb942b05db275e4b34222a9a40b9fbd3b/coverage-7.13.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:936bc20503ce24770c71938d1369461f0c5320830800933bc3956e2a4ded930e", size = 260321, upload-time = "2025-12-08T13:13:41.172Z" }, - { url = "https://files.pythonhosted.org/packages/4c/89/a29f5d98c64fedbe32e2ac3c227fbf78edc01cc7572eee17d61024d89889/coverage-7.13.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:af0a583efaacc52ae2521f8d7910aff65cdb093091d76291ac5820d5e947fc1c", size = 259222, upload-time = "2025-12-08T13:13:43.282Z" }, - { url = "https://files.pythonhosted.org/packages/b3/c3/940fe447aae302a6701ee51e53af7e08b86ff6eed7631e5740c157ee22b9/coverage-7.13.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f1c23e24a7000da892a312fb17e33c5f94f8b001de44b7cf8ba2e36fbd15859e", size = 261411, upload-time = "2025-12-08T13:13:44.72Z" }, - { url = "https://files.pythonhosted.org/packages/eb/31/12a4aec689cb942a89129587860ed4d0fd522d5fda81237147fde554b8ae/coverage-7.13.0-cp313-cp313t-win32.whl", hash = "sha256:5f8a0297355e652001015e93be345ee54393e45dc3050af4a0475c5a2b767d46", size = 221505, upload-time = "2025-12-08T13:13:46.332Z" }, - { url = "https://files.pythonhosted.org/packages/65/8c/3b5fe3259d863572d2b0827642c50c3855d26b3aefe80bdc9eba1f0af3b0/coverage-7.13.0-cp313-cp313t-win_amd64.whl", hash = "sha256:6abb3a4c52f05e08460bd9acf04fec027f8718ecaa0d09c40ffbc3fbd70ecc39", size = 222569, upload-time = "2025-12-08T13:13:47.79Z" }, - { url = "https://files.pythonhosted.org/packages/b0/39/f71fa8316a96ac72fc3908839df651e8eccee650001a17f2c78cdb355624/coverage-7.13.0-cp313-cp313t-win_arm64.whl", hash = "sha256:3ad968d1e3aa6ce5be295ab5fe3ae1bf5bb4769d0f98a80a0252d543a2ef2e9e", size = 220841, upload-time = "2025-12-08T13:13:49.243Z" }, - { url = "https://files.pythonhosted.org/packages/f8/4b/9b54bedda55421449811dcd5263a2798a63f48896c24dfb92b0f1b0845bd/coverage-7.13.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:453b7ec753cf5e4356e14fe858064e5520c460d3bbbcb9c35e55c0d21155c256", size = 218343, upload-time = "2025-12-08T13:13:50.811Z" }, - { url = "https://files.pythonhosted.org/packages/59/df/c3a1f34d4bba2e592c8979f924da4d3d4598b0df2392fbddb7761258e3dc/coverage-7.13.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:af827b7cbb303e1befa6c4f94fd2bf72f108089cfa0f8abab8f4ca553cf5ca5a", size = 218672, upload-time = "2025-12-08T13:13:52.284Z" }, - { url = "https://files.pythonhosted.org/packages/07/62/eec0659e47857698645ff4e6ad02e30186eb8afd65214fd43f02a76537cb/coverage-7.13.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9987a9e4f8197a1000280f7cc089e3ea2c8b3c0a64d750537809879a7b4ceaf9", size = 249715, upload-time = "2025-12-08T13:13:53.791Z" }, - { url = "https://files.pythonhosted.org/packages/23/2d/3c7ff8b2e0e634c1f58d095f071f52ed3c23ff25be524b0ccae8b71f99f8/coverage-7.13.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3188936845cd0cb114fa6a51842a304cdbac2958145d03be2377ec41eb285d19", size = 252225, upload-time = "2025-12-08T13:13:55.274Z" }, - { url = "https://files.pythonhosted.org/packages/aa/ac/fb03b469d20e9c9a81093575003f959cf91a4a517b783aab090e4538764b/coverage-7.13.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a2bdb3babb74079f021696cb46b8bb5f5661165c385d3a238712b031a12355be", size = 253559, upload-time = "2025-12-08T13:13:57.161Z" }, - { url = "https://files.pythonhosted.org/packages/29/62/14afa9e792383c66cc0a3b872a06ded6e4ed1079c7d35de274f11d27064e/coverage-7.13.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7464663eaca6adba4175f6c19354feea61ebbdd735563a03d1e472c7072d27bb", size = 249724, upload-time = "2025-12-08T13:13:58.692Z" }, - { url = "https://files.pythonhosted.org/packages/31/b7/333f3dab2939070613696ab3ee91738950f0467778c6e5a5052e840646b7/coverage-7.13.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8069e831f205d2ff1f3d355e82f511eb7c5522d7d413f5db5756b772ec8697f8", size = 251582, upload-time = "2025-12-08T13:14:00.642Z" }, - { url = "https://files.pythonhosted.org/packages/81/cb/69162bda9381f39b2287265d7e29ee770f7c27c19f470164350a38318764/coverage-7.13.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:6fb2d5d272341565f08e962cce14cdf843a08ac43bd621783527adb06b089c4b", size = 249538, upload-time = "2025-12-08T13:14:02.556Z" }, - { url = "https://files.pythonhosted.org/packages/e0/76/350387b56a30f4970abe32b90b2a434f87d29f8b7d4ae40d2e8a85aacfb3/coverage-7.13.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:5e70f92ef89bac1ac8a99b3324923b4749f008fdbd7aa9cb35e01d7a284a04f9", size = 249349, upload-time = "2025-12-08T13:14:04.015Z" }, - { url = "https://files.pythonhosted.org/packages/86/0d/7f6c42b8d59f4c7e43ea3059f573c0dcfed98ba46eb43c68c69e52ae095c/coverage-7.13.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4b5de7d4583e60d5fd246dd57fcd3a8aa23c6e118a8c72b38adf666ba8e7e927", size = 251011, upload-time = "2025-12-08T13:14:05.505Z" }, - { url = "https://files.pythonhosted.org/packages/d7/f1/4bb2dff379721bb0b5c649d5c5eaf438462cad824acf32eb1b7ca0c7078e/coverage-7.13.0-cp314-cp314-win32.whl", hash = "sha256:a6c6e16b663be828a8f0b6c5027d36471d4a9f90d28444aa4ced4d48d7d6ae8f", size = 221091, upload-time = "2025-12-08T13:14:07.127Z" }, - { url = "https://files.pythonhosted.org/packages/ba/44/c239da52f373ce379c194b0ee3bcc121020e397242b85f99e0afc8615066/coverage-7.13.0-cp314-cp314-win_amd64.whl", hash = "sha256:0900872f2fdb3ee5646b557918d02279dc3af3dfb39029ac4e945458b13f73bc", size = 221904, upload-time = "2025-12-08T13:14:08.542Z" }, - { url = "https://files.pythonhosted.org/packages/89/1f/b9f04016d2a29c2e4a0307baefefad1a4ec5724946a2b3e482690486cade/coverage-7.13.0-cp314-cp314-win_arm64.whl", hash = "sha256:3a10260e6a152e5f03f26db4a407c4c62d3830b9af9b7c0450b183615f05d43b", size = 220480, upload-time = "2025-12-08T13:14:10.958Z" }, - { url = "https://files.pythonhosted.org/packages/16/d4/364a1439766c8e8647860584171c36010ca3226e6e45b1753b1b249c5161/coverage-7.13.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:9097818b6cc1cfb5f174e3263eba4a62a17683bcfe5c4b5d07f4c97fa51fbf28", size = 219074, upload-time = "2025-12-08T13:14:13.345Z" }, - { url = "https://files.pythonhosted.org/packages/ce/f4/71ba8be63351e099911051b2089662c03d5671437a0ec2171823c8e03bec/coverage-7.13.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0018f73dfb4301a89292c73be6ba5f58722ff79f51593352759c1790ded1cabe", size = 219342, upload-time = "2025-12-08T13:14:15.02Z" }, - { url = "https://files.pythonhosted.org/packages/5e/25/127d8ed03d7711a387d96f132589057213e3aef7475afdaa303412463f22/coverage-7.13.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:166ad2a22ee770f5656e1257703139d3533b4a0b6909af67c6b4a3adc1c98657", size = 260713, upload-time = "2025-12-08T13:14:16.907Z" }, - { url = "https://files.pythonhosted.org/packages/fd/db/559fbb6def07d25b2243663b46ba9eb5a3c6586c0c6f4e62980a68f0ee1c/coverage-7.13.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f6aaef16d65d1787280943f1c8718dc32e9cf141014e4634d64446702d26e0ff", size = 262825, upload-time = "2025-12-08T13:14:18.68Z" }, - { url = "https://files.pythonhosted.org/packages/37/99/6ee5bf7eff884766edb43bd8736b5e1c5144d0fe47498c3779326fe75a35/coverage-7.13.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e999e2dcc094002d6e2c7bbc1fb85b58ba4f465a760a8014d97619330cdbbbf3", size = 265233, upload-time = "2025-12-08T13:14:20.55Z" }, - { url = "https://files.pythonhosted.org/packages/d8/90/92f18fe0356ea69e1f98f688ed80cec39f44e9f09a1f26a1bbf017cc67f2/coverage-7.13.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:00c3d22cf6fb1cf3bf662aaaa4e563be8243a5ed2630339069799835a9cc7f9b", size = 259779, upload-time = "2025-12-08T13:14:22.367Z" }, - { url = "https://files.pythonhosted.org/packages/90/5d/b312a8b45b37a42ea7d27d7d3ff98ade3a6c892dd48d1d503e773503373f/coverage-7.13.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:22ccfe8d9bb0d6134892cbe1262493a8c70d736b9df930f3f3afae0fe3ac924d", size = 262700, upload-time = "2025-12-08T13:14:24.309Z" }, - { url = "https://files.pythonhosted.org/packages/63/f8/b1d0de5c39351eb71c366f872376d09386640840a2e09b0d03973d791e20/coverage-7.13.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:9372dff5ea15930fea0445eaf37bbbafbc771a49e70c0aeed8b4e2c2614cc00e", size = 260302, upload-time = "2025-12-08T13:14:26.068Z" }, - { url = "https://files.pythonhosted.org/packages/aa/7c/d42f4435bc40c55558b3109a39e2d456cddcec37434f62a1f1230991667a/coverage-7.13.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:69ac2c492918c2461bc6ace42d0479638e60719f2a4ef3f0815fa2df88e9f940", size = 259136, upload-time = "2025-12-08T13:14:27.604Z" }, - { url = "https://files.pythonhosted.org/packages/b8/d3/23413241dc04d47cfe19b9a65b32a2edd67ecd0b817400c2843ebc58c847/coverage-7.13.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:739c6c051a7540608d097b8e13c76cfa85263ced467168dc6b477bae3df7d0e2", size = 261467, upload-time = "2025-12-08T13:14:29.09Z" }, - { url = "https://files.pythonhosted.org/packages/13/e6/6e063174500eee216b96272c0d1847bf215926786f85c2bd024cf4d02d2f/coverage-7.13.0-cp314-cp314t-win32.whl", hash = "sha256:fe81055d8c6c9de76d60c94ddea73c290b416e061d40d542b24a5871bad498b7", size = 221875, upload-time = "2025-12-08T13:14:31.106Z" }, - { url = "https://files.pythonhosted.org/packages/3b/46/f4fb293e4cbe3620e3ac2a3e8fd566ed33affb5861a9b20e3dd6c1896cbc/coverage-7.13.0-cp314-cp314t-win_amd64.whl", hash = "sha256:445badb539005283825959ac9fa4a28f712c214b65af3a2c464f1adc90f5fcbc", size = 222982, upload-time = "2025-12-08T13:14:33.1Z" }, - { url = "https://files.pythonhosted.org/packages/68/62/5b3b9018215ed9733fbd1ae3b2ed75c5de62c3b55377a52cae732e1b7805/coverage-7.13.0-cp314-cp314t-win_arm64.whl", hash = "sha256:de7f6748b890708578fc4b7bb967d810aeb6fcc9bff4bb77dbca77dab2f9df6a", size = 221016, upload-time = "2025-12-08T13:14:34.601Z" }, - { url = "https://files.pythonhosted.org/packages/8d/4c/1968f32fb9a2604645827e11ff84a31e59d532e01995f904723b4f5328b3/coverage-7.13.0-py3-none-any.whl", hash = "sha256:850d2998f380b1e266459ca5b47bc9e7daf9af1d070f66317972f382d46f1904", size = 210068, upload-time = "2025-12-08T13:14:36.236Z" }, +version = "7.13.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9d/e0/70553e3000e345daff267cec284ce4cbf3fc141b6da229ac52775b5428f1/coverage-7.13.5.tar.gz", hash = "sha256:c81f6515c4c40141f83f502b07bbfa5c240ba25bbe73da7b33f1e5b6120ff179", size = 915967, upload-time = "2026-03-17T10:33:18.341Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/c3/a396306ba7db865bf96fc1fb3b7fd29bcbf3d829df642e77b13555163cd6/coverage-7.13.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:460cf0114c5016fa841214ff5564aa4864f11948da9440bc97e21ad1f4ba1e01", size = 219554, upload-time = "2026-03-17T10:30:42.208Z" }, + { url = "https://files.pythonhosted.org/packages/a6/16/a68a19e5384e93f811dccc51034b1fd0b865841c390e3c931dcc4699e035/coverage-7.13.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0e223ce4b4ed47f065bfb123687686512e37629be25cc63728557ae7db261422", size = 219908, upload-time = "2026-03-17T10:30:43.906Z" }, + { url = "https://files.pythonhosted.org/packages/29/72/20b917c6793af3a5ceb7fb9c50033f3ec7865f2911a1416b34a7cfa0813b/coverage-7.13.5-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6e3370441f4513c6252bf042b9c36d22491142385049243253c7e48398a15a9f", size = 251419, upload-time = "2026-03-17T10:30:45.545Z" }, + { url = "https://files.pythonhosted.org/packages/8c/49/cd14b789536ac6a4778c453c6a2338bc0a2fb60c5a5a41b4008328b9acc1/coverage-7.13.5-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:03ccc709a17a1de074fb1d11f217342fb0d2b1582ed544f554fc9fc3f07e95f5", size = 254159, upload-time = "2026-03-17T10:30:47.204Z" }, + { url = "https://files.pythonhosted.org/packages/9d/00/7b0edcfe64e2ed4c0340dac14a52ad0f4c9bd0b8b5e531af7d55b703db7c/coverage-7.13.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3f4818d065964db3c1c66dc0fbdac5ac692ecbc875555e13374fdbe7eedb4376", size = 255270, upload-time = "2026-03-17T10:30:48.812Z" }, + { url = "https://files.pythonhosted.org/packages/93/89/7ffc4ba0f5d0a55c1e84ea7cee39c9fc06af7b170513d83fbf3bbefce280/coverage-7.13.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:012d5319e66e9d5a218834642d6c35d265515a62f01157a45bcc036ecf947256", size = 257538, upload-time = "2026-03-17T10:30:50.77Z" }, + { url = "https://files.pythonhosted.org/packages/81/bd/73ddf85f93f7e6fa83e77ccecb6162d9415c79007b4bc124008a4995e4a7/coverage-7.13.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8dd02af98971bdb956363e4827d34425cb3df19ee550ef92855b0acb9c7ce51c", size = 251821, upload-time = "2026-03-17T10:30:52.5Z" }, + { url = "https://files.pythonhosted.org/packages/a0/81/278aff4e8dec4926a0bcb9486320752811f543a3ce5b602cc7a29978d073/coverage-7.13.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f08fd75c50a760c7eb068ae823777268daaf16a80b918fa58eea888f8e3919f5", size = 253191, upload-time = "2026-03-17T10:30:54.543Z" }, + { url = "https://files.pythonhosted.org/packages/70/ee/fe1621488e2e0a58d7e94c4800f0d96f79671553488d401a612bebae324b/coverage-7.13.5-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:843ea8643cf967d1ac7e8ecd4bb00c99135adf4816c0c0593fdcc47b597fcf09", size = 251337, upload-time = "2026-03-17T10:30:56.663Z" }, + { url = "https://files.pythonhosted.org/packages/37/a6/f79fb37aa104b562207cc23cb5711ab6793608e246cae1e93f26b2236ed9/coverage-7.13.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:9d44d7aa963820b1b971dbecd90bfe5fe8f81cff79787eb6cca15750bd2f79b9", size = 255404, upload-time = "2026-03-17T10:30:58.427Z" }, + { url = "https://files.pythonhosted.org/packages/75/f0/ed15262a58ec81ce457ceb717b7f78752a1713556b19081b76e90896e8d4/coverage-7.13.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:7132bed4bd7b836200c591410ae7d97bf7ae8be6fc87d160b2bd881df929e7bf", size = 250903, upload-time = "2026-03-17T10:31:00.093Z" }, + { url = "https://files.pythonhosted.org/packages/0f/e9/9129958f20e7e9d4d56d51d42ccf708d15cac355ff4ac6e736e97a9393d2/coverage-7.13.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a698e363641b98843c517817db75373c83254781426e94ada3197cabbc2c919c", size = 252780, upload-time = "2026-03-17T10:31:01.916Z" }, + { url = "https://files.pythonhosted.org/packages/a4/d7/0ad9b15812d81272db94379fe4c6df8fd17781cc7671fdfa30c76ba5ff7b/coverage-7.13.5-cp312-cp312-win32.whl", hash = "sha256:bdba0a6b8812e8c7df002d908a9a2ea3c36e92611b5708633c50869e6d922fdf", size = 222093, upload-time = "2026-03-17T10:31:03.642Z" }, + { url = "https://files.pythonhosted.org/packages/29/3d/821a9a5799fac2556bcf0bd37a70d1d11fa9e49784b6d22e92e8b2f85f18/coverage-7.13.5-cp312-cp312-win_amd64.whl", hash = "sha256:d2c87e0c473a10bffe991502eac389220533024c8082ec1ce849f4218dded810", size = 222900, upload-time = "2026-03-17T10:31:05.651Z" }, + { url = "https://files.pythonhosted.org/packages/d4/fa/2238c2ad08e35cf4f020ea721f717e09ec3152aea75d191a7faf3ef009a8/coverage-7.13.5-cp312-cp312-win_arm64.whl", hash = "sha256:bf69236a9a81bdca3bff53796237aab096cdbf8d78a66ad61e992d9dac7eb2de", size = 221515, upload-time = "2026-03-17T10:31:07.293Z" }, + { url = "https://files.pythonhosted.org/packages/74/8c/74fedc9663dcf168b0a059d4ea756ecae4da77a489048f94b5f512a8d0b3/coverage-7.13.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5ec4af212df513e399cf11610cc27063f1586419e814755ab362e50a85ea69c1", size = 219576, upload-time = "2026-03-17T10:31:09.045Z" }, + { url = "https://files.pythonhosted.org/packages/0c/c9/44fb661c55062f0818a6ffd2685c67aa30816200d5f2817543717d4b92eb/coverage-7.13.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:941617e518602e2d64942c88ec8499f7fbd49d3f6c4327d3a71d43a1973032f3", size = 219942, upload-time = "2026-03-17T10:31:10.708Z" }, + { url = "https://files.pythonhosted.org/packages/5f/13/93419671cee82b780bab7ea96b67c8ef448f5f295f36bf5031154ec9a790/coverage-7.13.5-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:da305e9937617ee95c2e39d8ff9f040e0487cbf1ac174f777ed5eddd7a7c1f26", size = 250935, upload-time = "2026-03-17T10:31:12.392Z" }, + { url = "https://files.pythonhosted.org/packages/ac/68/1666e3a4462f8202d836920114fa7a5ee9275d1fa45366d336c551a162dd/coverage-7.13.5-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:78e696e1cc714e57e8b25760b33a8b1026b7048d270140d25dafe1b0a1ee05a3", size = 253541, upload-time = "2026-03-17T10:31:14.247Z" }, + { url = "https://files.pythonhosted.org/packages/4e/5e/3ee3b835647be646dcf3c65a7c6c18f87c27326a858f72ab22c12730773d/coverage-7.13.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:02ca0eed225b2ff301c474aeeeae27d26e2537942aa0f87491d3e147e784a82b", size = 254780, upload-time = "2026-03-17T10:31:16.193Z" }, + { url = "https://files.pythonhosted.org/packages/44/b3/cb5bd1a04cfcc49ede6cd8409d80bee17661167686741e041abc7ee1b9a9/coverage-7.13.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:04690832cbea4e4663d9149e05dba142546ca05cb1848816760e7f58285c970a", size = 256912, upload-time = "2026-03-17T10:31:17.89Z" }, + { url = "https://files.pythonhosted.org/packages/1b/66/c1dceb7b9714473800b075f5c8a84f4588f887a90eb8645282031676e242/coverage-7.13.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0590e44dd2745c696a778f7bab6aa95256de2cbc8b8cff4f7db8ff09813d6969", size = 251165, upload-time = "2026-03-17T10:31:19.605Z" }, + { url = "https://files.pythonhosted.org/packages/b7/62/5502b73b97aa2e53ea22a39cf8649ff44827bef76d90bf638777daa27a9d/coverage-7.13.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d7cfad2d6d81dd298ab6b89fe72c3b7b05ec7544bdda3b707ddaecff8d25c161", size = 252908, upload-time = "2026-03-17T10:31:21.312Z" }, + { url = "https://files.pythonhosted.org/packages/7d/37/7792c2d69854397ca77a55c4646e5897c467928b0e27f2d235d83b5d08c6/coverage-7.13.5-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:e092b9499de38ae0fbfbc603a74660eb6ff3e869e507b50d85a13b6db9863e15", size = 250873, upload-time = "2026-03-17T10:31:23.565Z" }, + { url = "https://files.pythonhosted.org/packages/a3/23/bc866fb6163be52a8a9e5d708ba0d3b1283c12158cefca0a8bbb6e247a43/coverage-7.13.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:48c39bc4a04d983a54a705a6389512883d4a3b9862991b3617d547940e9f52b1", size = 255030, upload-time = "2026-03-17T10:31:25.58Z" }, + { url = "https://files.pythonhosted.org/packages/7d/8b/ef67e1c222ef49860701d346b8bbb70881bef283bd5f6cbba68a39a086c7/coverage-7.13.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2d3807015f138ffea1ed9afeeb8624fd781703f2858b62a8dd8da5a0994c57b6", size = 250694, upload-time = "2026-03-17T10:31:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/46/0d/866d1f74f0acddbb906db212e096dee77a8e2158ca5e6bb44729f9d93298/coverage-7.13.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ee2aa19e03161671ec964004fb74b2257805d9710bf14a5c704558b9d8dbaf17", size = 252469, upload-time = "2026-03-17T10:31:29.472Z" }, + { url = "https://files.pythonhosted.org/packages/7a/f5/be742fec31118f02ce42b21c6af187ad6a344fed546b56ca60caacc6a9a0/coverage-7.13.5-cp313-cp313-win32.whl", hash = "sha256:ce1998c0483007608c8382f4ff50164bfc5bd07a2246dd272aa4043b75e61e85", size = 222112, upload-time = "2026-03-17T10:31:31.526Z" }, + { url = "https://files.pythonhosted.org/packages/66/40/7732d648ab9d069a46e686043241f01206348e2bbf128daea85be4d6414b/coverage-7.13.5-cp313-cp313-win_amd64.whl", hash = "sha256:631efb83f01569670a5e866ceb80fe483e7c159fac6f167e6571522636104a0b", size = 222923, upload-time = "2026-03-17T10:31:33.633Z" }, + { url = "https://files.pythonhosted.org/packages/48/af/fea819c12a095781f6ccd504890aaddaf88b8fab263c4940e82c7b770124/coverage-7.13.5-cp313-cp313-win_arm64.whl", hash = "sha256:f4cd16206ad171cbc2470dbea9103cf9a7607d5fe8c242fdf1edf36174020664", size = 221540, upload-time = "2026-03-17T10:31:35.445Z" }, + { url = "https://files.pythonhosted.org/packages/23/d2/17879af479df7fbbd44bd528a31692a48f6b25055d16482fdf5cdb633805/coverage-7.13.5-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0428cbef5783ad91fe240f673cc1f76b25e74bbfe1a13115e4aa30d3f538162d", size = 220262, upload-time = "2026-03-17T10:31:37.184Z" }, + { url = "https://files.pythonhosted.org/packages/5b/4c/d20e554f988c8f91d6a02c5118f9abbbf73a8768a3048cb4962230d5743f/coverage-7.13.5-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e0b216a19534b2427cc201a26c25da4a48633f29a487c61258643e89d28200c0", size = 220617, upload-time = "2026-03-17T10:31:39.245Z" }, + { url = "https://files.pythonhosted.org/packages/29/9c/f9f5277b95184f764b24e7231e166dfdb5780a46d408a2ac665969416d61/coverage-7.13.5-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:972a9cd27894afe4bc2b1480107054e062df08e671df7c2f18c205e805ccd806", size = 261912, upload-time = "2026-03-17T10:31:41.324Z" }, + { url = "https://files.pythonhosted.org/packages/d5/f6/7f1ab39393eeb50cfe4747ae8ef0e4fc564b989225aa1152e13a180d74f8/coverage-7.13.5-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4b59148601efcd2bac8c4dbf1f0ad6391693ccf7a74b8205781751637076aee3", size = 263987, upload-time = "2026-03-17T10:31:43.724Z" }, + { url = "https://files.pythonhosted.org/packages/a0/d7/62c084fb489ed9c6fbdf57e006752e7c516ea46fd690e5ed8b8617c7d52e/coverage-7.13.5-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:505d7083c8b0c87a8fa8c07370c285847c1f77739b22e299ad75a6af6c32c5c9", size = 266416, upload-time = "2026-03-17T10:31:45.769Z" }, + { url = "https://files.pythonhosted.org/packages/a9/f6/df63d8660e1a0bff6125947afda112a0502736f470d62ca68b288ea762d8/coverage-7.13.5-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:60365289c3741e4db327e7baff2a4aaacf22f788e80fa4683393891b70a89fbd", size = 267558, upload-time = "2026-03-17T10:31:48.293Z" }, + { url = "https://files.pythonhosted.org/packages/5b/02/353ca81d36779bd108f6d384425f7139ac3c58c750dcfaafe5d0bee6436b/coverage-7.13.5-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1b88c69c8ef5d4b6fe7dea66d6636056a0f6a7527c440e890cf9259011f5e606", size = 261163, upload-time = "2026-03-17T10:31:50.125Z" }, + { url = "https://files.pythonhosted.org/packages/2c/16/2e79106d5749bcaf3aee6d309123548e3276517cd7851faa8da213bc61bf/coverage-7.13.5-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5b13955d31d1633cf9376908089b7cebe7d15ddad7aeaabcbe969a595a97e95e", size = 263981, upload-time = "2026-03-17T10:31:51.961Z" }, + { url = "https://files.pythonhosted.org/packages/29/c7/c29e0c59ffa6942030ae6f50b88ae49988e7e8da06de7ecdbf49c6d4feae/coverage-7.13.5-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:f70c9ab2595c56f81a89620e22899eea8b212a4041bd728ac6f4a28bf5d3ddd0", size = 261604, upload-time = "2026-03-17T10:31:53.872Z" }, + { url = "https://files.pythonhosted.org/packages/40/48/097cdc3db342f34006a308ab41c3a7c11c3f0d84750d340f45d88a782e00/coverage-7.13.5-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:084b84a8c63e8d6fc7e3931b316a9bcafca1458d753c539db82d31ed20091a87", size = 265321, upload-time = "2026-03-17T10:31:55.997Z" }, + { url = "https://files.pythonhosted.org/packages/bb/1f/4994af354689e14fd03a75f8ec85a9a68d94e0188bbdab3fc1516b55e512/coverage-7.13.5-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:ad14385487393e386e2ea988b09d62dd42c397662ac2dabc3832d71253eee479", size = 260502, upload-time = "2026-03-17T10:31:58.308Z" }, + { url = "https://files.pythonhosted.org/packages/22/c6/9bb9ef55903e628033560885f5c31aa227e46878118b63ab15dc7ba87797/coverage-7.13.5-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:7f2c47b36fe7709a6e83bfadf4eefb90bd25fbe4014d715224c4316f808e59a2", size = 262688, upload-time = "2026-03-17T10:32:00.141Z" }, + { url = "https://files.pythonhosted.org/packages/14/4f/f5df9007e50b15e53e01edea486814783a7f019893733d9e4d6caad75557/coverage-7.13.5-cp313-cp313t-win32.whl", hash = "sha256:67e9bc5449801fad0e5dff329499fb090ba4c5800b86805c80617b4e29809b2a", size = 222788, upload-time = "2026-03-17T10:32:02.246Z" }, + { url = "https://files.pythonhosted.org/packages/e1/98/aa7fccaa97d0f3192bec013c4e6fd6d294a6ed44b640e6bb61f479e00ed5/coverage-7.13.5-cp313-cp313t-win_amd64.whl", hash = "sha256:da86cdcf10d2519e10cabb8ac2de03da1bcb6e4853790b7fbd48523332e3a819", size = 223851, upload-time = "2026-03-17T10:32:04.416Z" }, + { url = "https://files.pythonhosted.org/packages/3d/8b/e5c469f7352651e5f013198e9e21f97510b23de957dd06a84071683b4b60/coverage-7.13.5-cp313-cp313t-win_arm64.whl", hash = "sha256:0ecf12ecb326fe2c339d93fc131816f3a7367d223db37817208905c89bded911", size = 222104, upload-time = "2026-03-17T10:32:06.65Z" }, + { url = "https://files.pythonhosted.org/packages/8e/77/39703f0d1d4b478bfd30191d3c14f53caf596fac00efb3f8f6ee23646439/coverage-7.13.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fbabfaceaeb587e16f7008f7795cd80d20ec548dc7f94fbb0d4ec2e038ce563f", size = 219621, upload-time = "2026-03-17T10:32:08.589Z" }, + { url = "https://files.pythonhosted.org/packages/e2/3e/51dff36d99ae14639a133d9b164d63e628532e2974d8b1edb99dd1ebc733/coverage-7.13.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9bb2a28101a443669a423b665939381084412b81c3f8c0fcfbac57f4e30b5b8e", size = 219953, upload-time = "2026-03-17T10:32:10.507Z" }, + { url = "https://files.pythonhosted.org/packages/6a/6c/1f1917b01eb647c2f2adc9962bd66c79eb978951cab61bdc1acab3290c07/coverage-7.13.5-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bd3a2fbc1c6cccb3c5106140d87cc6a8715110373ef42b63cf5aea29df8c217a", size = 250992, upload-time = "2026-03-17T10:32:12.41Z" }, + { url = "https://files.pythonhosted.org/packages/22/e5/06b1f88f42a5a99df42ce61208bdec3bddb3d261412874280a19796fc09c/coverage-7.13.5-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6c36ddb64ed9d7e496028d1d00dfec3e428e0aabf4006583bb1839958d280510", size = 253503, upload-time = "2026-03-17T10:32:14.449Z" }, + { url = "https://files.pythonhosted.org/packages/80/28/2a148a51e5907e504fa7b85490277734e6771d8844ebcc48764a15e28155/coverage-7.13.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:380e8e9084d8eb38db3a9176a1a4f3c0082c3806fa0dc882d1d87abc3c789247", size = 254852, upload-time = "2026-03-17T10:32:16.56Z" }, + { url = "https://files.pythonhosted.org/packages/61/77/50e8d3d85cc0b7ebe09f30f151d670e302c7ff4a1bf6243f71dd8b0981fa/coverage-7.13.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e808af52a0513762df4d945ea164a24b37f2f518cbe97e03deaa0ee66139b4d6", size = 257161, upload-time = "2026-03-17T10:32:19.004Z" }, + { url = "https://files.pythonhosted.org/packages/3b/c4/b5fd1d4b7bf8d0e75d997afd3925c59ba629fc8616f1b3aae7605132e256/coverage-7.13.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e301d30dd7e95ae068671d746ba8c34e945a82682e62918e41b2679acd2051a0", size = 251021, upload-time = "2026-03-17T10:32:21.344Z" }, + { url = "https://files.pythonhosted.org/packages/f8/66/6ea21f910e92d69ef0b1c3346ea5922a51bad4446c9126db2ae96ee24c4c/coverage-7.13.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:800bc829053c80d240a687ceeb927a94fd108bbdc68dfbe505d0d75ab578a882", size = 252858, upload-time = "2026-03-17T10:32:23.506Z" }, + { url = "https://files.pythonhosted.org/packages/9e/ea/879c83cb5d61aa2a35fb80e72715e92672daef8191b84911a643f533840c/coverage-7.13.5-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:0b67af5492adb31940ee418a5a655c28e48165da5afab8c7fa6fd72a142f8740", size = 250823, upload-time = "2026-03-17T10:32:25.516Z" }, + { url = "https://files.pythonhosted.org/packages/8a/fb/616d95d3adb88b9803b275580bdeee8bd1b69a886d057652521f83d7322f/coverage-7.13.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:c9136ff29c3a91e25b1d1552b5308e53a1e0653a23e53b6366d7c2dcbbaf8a16", size = 255099, upload-time = "2026-03-17T10:32:27.944Z" }, + { url = "https://files.pythonhosted.org/packages/1c/93/25e6917c90ec1c9a56b0b26f6cad6408e5f13bb6b35d484a0d75c9cf000d/coverage-7.13.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:cff784eef7f0b8f6cb28804fbddcfa99f89efe4cc35fb5627e3ac58f91ed3ac0", size = 250638, upload-time = "2026-03-17T10:32:29.914Z" }, + { url = "https://files.pythonhosted.org/packages/fc/7b/dc1776b0464145a929deed214aef9fb1493f159b59ff3c7eeeedf91eddd0/coverage-7.13.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:68a4953be99b17ac3c23b6efbc8a38330d99680c9458927491d18700ef23ded0", size = 252295, upload-time = "2026-03-17T10:32:31.981Z" }, + { url = "https://files.pythonhosted.org/packages/ea/fb/99cbbc56a26e07762a2740713f3c8f9f3f3106e3a3dd8cc4474954bccd34/coverage-7.13.5-cp314-cp314-win32.whl", hash = "sha256:35a31f2b1578185fbe6aa2e74cea1b1d0bbf4c552774247d9160d29b80ed56cc", size = 222360, upload-time = "2026-03-17T10:32:34.233Z" }, + { url = "https://files.pythonhosted.org/packages/8d/b7/4758d4f73fb536347cc5e4ad63662f9d60ba9118cb6785e9616b2ce5d7fa/coverage-7.13.5-cp314-cp314-win_amd64.whl", hash = "sha256:2aa055ae1857258f9e0045be26a6d62bdb47a72448b62d7b55f4820f361a2633", size = 223174, upload-time = "2026-03-17T10:32:36.369Z" }, + { url = "https://files.pythonhosted.org/packages/2c/f2/24d84e1dfe70f8ac9fdf30d338239860d0d1d5da0bda528959d0ebc9da28/coverage-7.13.5-cp314-cp314-win_arm64.whl", hash = "sha256:1b11eef33edeae9d142f9b4358edb76273b3bfd30bc3df9a4f95d0e49caf94e8", size = 221739, upload-time = "2026-03-17T10:32:38.736Z" }, + { url = "https://files.pythonhosted.org/packages/60/5b/4a168591057b3668c2428bff25dd3ebc21b629d666d90bcdfa0217940e84/coverage-7.13.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:10a0c37f0b646eaff7cce1874c31d1f1ccb297688d4c747291f4f4c70741cc8b", size = 220351, upload-time = "2026-03-17T10:32:41.196Z" }, + { url = "https://files.pythonhosted.org/packages/f5/21/1fd5c4dbfe4a58b6b99649125635df46decdfd4a784c3cd6d410d303e370/coverage-7.13.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b5db73ba3c41c7008037fa731ad5459fc3944cb7452fc0aa9f822ad3533c583c", size = 220612, upload-time = "2026-03-17T10:32:43.204Z" }, + { url = "https://files.pythonhosted.org/packages/d6/fe/2a924b3055a5e7e4512655a9d4609781b0d62334fa0140c3e742926834e2/coverage-7.13.5-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:750db93a81e3e5a9831b534be7b1229df848b2e125a604fe6651e48aa070e5f9", size = 261985, upload-time = "2026-03-17T10:32:45.514Z" }, + { url = "https://files.pythonhosted.org/packages/d7/0d/c8928f2bd518c45990fe1a2ab8db42e914ef9b726c975facc4282578c3eb/coverage-7.13.5-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9ddb4f4a5479f2539644be484da179b653273bca1a323947d48ab107b3ed1f29", size = 264107, upload-time = "2026-03-17T10:32:47.971Z" }, + { url = "https://files.pythonhosted.org/packages/ef/ae/4ae35bbd9a0af9d820362751f0766582833c211224b38665c0f8de3d487f/coverage-7.13.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8a7a2049c14f413163e2bdabd37e41179b1d1ccb10ffc6ccc4b7a718429c607", size = 266513, upload-time = "2026-03-17T10:32:50.1Z" }, + { url = "https://files.pythonhosted.org/packages/9c/20/d326174c55af36f74eac6ae781612d9492f060ce8244b570bb9d50d9d609/coverage-7.13.5-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1c85e0b6c05c592ea6d8768a66a254bfb3874b53774b12d4c89c481eb78cb90", size = 267650, upload-time = "2026-03-17T10:32:52.391Z" }, + { url = "https://files.pythonhosted.org/packages/7a/5e/31484d62cbd0eabd3412e30d74386ece4a0837d4f6c3040a653878bfc019/coverage-7.13.5-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:777c4d1eff1b67876139d24288aaf1817f6c03d6bae9c5cc8d27b83bcfe38fe3", size = 261089, upload-time = "2026-03-17T10:32:54.544Z" }, + { url = "https://files.pythonhosted.org/packages/e9/d8/49a72d6de146eebb0b7e48cc0f4bc2c0dd858e3d4790ab2b39a2872b62bd/coverage-7.13.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6697e29b93707167687543480a40f0db8f356e86d9f67ddf2e37e2dfd91a9dab", size = 263982, upload-time = "2026-03-17T10:32:56.803Z" }, + { url = "https://files.pythonhosted.org/packages/06/3b/0351f1bd566e6e4dd39e978efe7958bde1d32f879e85589de147654f57bb/coverage-7.13.5-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:8fdf453a942c3e4d99bd80088141c4c6960bb232c409d9c3558e2dbaa3998562", size = 261579, upload-time = "2026-03-17T10:32:59.466Z" }, + { url = "https://files.pythonhosted.org/packages/5d/ce/796a2a2f4017f554d7810f5c573449b35b1e46788424a548d4d19201b222/coverage-7.13.5-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:32ca0c0114c9834a43f045a87dcebd69d108d8ffb666957ea65aa132f50332e2", size = 265316, upload-time = "2026-03-17T10:33:01.847Z" }, + { url = "https://files.pythonhosted.org/packages/3d/16/d5ae91455541d1a78bc90abf495be600588aff8f6db5c8b0dae739fa39c9/coverage-7.13.5-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:8769751c10f339021e2638cd354e13adeac54004d1941119b2c96fe5276d45ea", size = 260427, upload-time = "2026-03-17T10:33:03.945Z" }, + { url = "https://files.pythonhosted.org/packages/48/11/07f413dba62db21fb3fad5d0de013a50e073cc4e2dc4306e770360f6dfc8/coverage-7.13.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cec2d83125531bd153175354055cdb7a09987af08a9430bd173c937c6d0fba2a", size = 262745, upload-time = "2026-03-17T10:33:06.285Z" }, + { url = "https://files.pythonhosted.org/packages/91/15/d792371332eb4663115becf4bad47e047d16234b1aff687b1b18c58d60ae/coverage-7.13.5-cp314-cp314t-win32.whl", hash = "sha256:0cd9ed7a8b181775459296e402ca4fb27db1279740a24e93b3b41942ebe4b215", size = 223146, upload-time = "2026-03-17T10:33:08.756Z" }, + { url = "https://files.pythonhosted.org/packages/db/51/37221f59a111dca5e85be7dbf09696323b5b9f13ff65e0641d535ed06ea8/coverage-7.13.5-cp314-cp314t-win_amd64.whl", hash = "sha256:301e3b7dfefecaca37c9f1aa6f0049b7d4ab8dd933742b607765d757aca77d43", size = 224254, upload-time = "2026-03-17T10:33:11.174Z" }, + { url = "https://files.pythonhosted.org/packages/54/83/6acacc889de8987441aa7d5adfbdbf33d288dad28704a67e574f1df9bcbb/coverage-7.13.5-cp314-cp314t-win_arm64.whl", hash = "sha256:9dacc2ad679b292709e0f5fc1ac74a6d4d5562e424058962c7bb0c658ad25e45", size = 222276, upload-time = "2026-03-17T10:33:13.466Z" }, + { url = "https://files.pythonhosted.org/packages/9e/ee/a4cf96b8ce1e566ed238f0659ac2d3f007ed1d14b181bcb684e19561a69a/coverage-7.13.5-py3-none-any.whl", hash = "sha256:34b02417cf070e173989b3db962f7ed56d2f644307b2cf9d5a0f258e13084a61", size = 211346, upload-time = "2026-03-17T10:33:15.691Z" }, +] + +[[package]] +name = "decli" +version = "0.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0c/59/d4ffff1dee2c8f6f2dd8f87010962e60f7b7847504d765c91ede5a466730/decli-0.6.3.tar.gz", hash = "sha256:87f9d39361adf7f16b9ca6e3b614badf7519da13092f2db3c80ca223c53c7656", size = 7564, upload-time = "2025-06-01T15:23:41.25Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d8/fa/ec878c28bc7f65b77e7e17af3522c9948a9711b9fa7fc4c5e3140a7e3578/decli-0.6.3-py3-none-any.whl", hash = "sha256:5152347c7bb8e3114ad65db719e5709b28d7f7f45bdb709f70167925e55640f3", size = 7989, upload-time = "2025-06-01T15:23:40.228Z" }, +] + +[[package]] +name = "deprecated" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "wrapt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/49/85/12f0a49a7c4ffb70572b6c2ef13c90c88fd190debda93b23f026b25f9634/deprecated-1.3.1.tar.gz", hash = "sha256:b1b50e0ff0c1fddaa5708a2c6b0a6588bb09b892825ab2b214ac9ea9d92a5223", size = 2932523, upload-time = "2025-10-30T08:19:02.757Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/d0/205d54408c08b13550c733c4b85429e7ead111c7f0014309637425520a9a/deprecated-1.3.1-py2.py3-none-any.whl", hash = "sha256:597bfef186b6f60181535a29fbe44865ce137a5079f295b479886c82729d5f3f", size = 11298, upload-time = "2025-10-30T08:19:00.758Z" }, ] [[package]] @@ -209,72 +464,97 @@ wheels = [ ] [[package]] -name = "dotfiles-package-manager" -version = "0.2.0" -source = { editable = "packages/package-manager" } +name = "dotfiles-daemon" +version = "0.1.0" +source = { editable = "packages/daemon" } +dependencies = [ + { name = "dotfiles-event-protocol" }, +] [package.optional-dependencies] dev = [ - { name = "black" }, - { name = "isort" }, - { name = "mypy" }, - { name = "pre-commit" }, { name = "pytest" }, + { name = "pytest-asyncio" }, { name = "pytest-cov" }, - { name = "ruff" }, ] [package.metadata] requires-dist = [ - { name = "black", marker = "extra == 'dev'", specifier = ">=24.0.0" }, - { name = "isort", marker = "extra == 'dev'", specifier = ">=5.13.0" }, - { name = "mypy", marker = "extra == 'dev'", specifier = ">=1.0.0" }, - { name = "pre-commit", marker = "extra == 'dev'", specifier = ">=3.8.0" }, - { name = "pytest", marker = "extra == 'dev'", specifier = ">=8.0.0" }, - { name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=4.1.0" }, - { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.1.0" }, + { name = "dotfiles-event-protocol", editable = "packages/event-protocol" }, + { name = "pytest", marker = "extra == 'dev'", specifier = ">=7.0.0" }, + { name = "pytest-asyncio", marker = "extra == 'dev'", specifier = ">=0.21.0" }, + { name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=4.0.0" }, ] provides-extras = ["dev"] [[package]] -name = "dotfiles-storage" +name = "dotfiles-event-protocol" version = "0.1.0" -source = { editable = "packages/storage" } +source = { editable = "packages/event-protocol" } dependencies = [ { name = "pydantic" }, ] +[package.optional-dependencies] +dev = [ + { name = "pytest" }, + { name = "pytest-cov" }, +] + +[package.metadata] +requires-dist = [ + { name = "pydantic", specifier = ">=2.0.0" }, + { name = "pytest", marker = "extra == 'dev'", specifier = ">=7.0.0" }, + { name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=4.0.0" }, +] +provides-extras = ["dev"] + +[[package]] +name = "dotfiles-socket" +version = "0.1.0" +source = { editable = "packages/socket" } +dependencies = [ + { name = "dynaconf" }, + { name = "msgpack" }, + { name = "pydantic" }, + { name = "rich-logging" }, +] + [package.optional-dependencies] dev = [ { name = "black" }, - { name = "fakeredis" }, { name = "isort" }, { name = "mypy" }, + { name = "pre-commit" }, { name = "pytest" }, { name = "pytest-cov" }, - { name = "pytest-xdist" }, - { name = "redis" }, { name = "ruff" }, ] -redis = [ - { name = "redis" }, -] [package.metadata] requires-dist = [ { name = "black", marker = "extra == 'dev'", specifier = ">=24.0.0" }, - { name = "fakeredis", marker = "extra == 'dev'", specifier = ">=2.0.0" }, + { name = "dynaconf", specifier = ">=3.2.0" }, { name = "isort", marker = "extra == 'dev'", specifier = ">=5.13.0" }, - { name = "mypy", marker = "extra == 'dev'", specifier = ">=1.11.0" }, - { name = "pydantic", specifier = ">=2.0" }, - { name = "pytest", marker = "extra == 'dev'", specifier = ">=8.4.2" }, + { name = "msgpack", specifier = ">=1.0.0" }, + { name = "mypy", marker = "extra == 'dev'", specifier = ">=1.0.0" }, + { name = "pre-commit", marker = "extra == 'dev'", specifier = ">=3.8.0" }, + { name = "pydantic", specifier = ">=2.0.0" }, + { name = "pytest", marker = "extra == 'dev'", specifier = ">=8.0.0" }, { name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=4.1.0" }, - { name = "pytest-xdist", marker = "extra == 'dev'", specifier = ">=3.8.0" }, - { name = "redis", marker = "extra == 'dev'", specifier = ">=5.0.0" }, - { name = "redis", marker = "extra == 'redis'", specifier = ">=5.0.0" }, - { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.6.0" }, + { name = "rich-logging", editable = "packages/logging" }, + { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.1.0" }, +] +provides-extras = ["dev"] + +[[package]] +name = "dynaconf" +version = "3.2.13" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/57/0e/05927cf459e73f8bf9a9277cbea6f2d5b7db8a5cc9dc1e20e7a5fbac1b90/dynaconf-3.2.13.tar.gz", hash = "sha256:d79e0189d97b3f226b8ebb1717e2ce05d1a05cdf6ea05de66d24625fdb5a0cbd", size = 283507, upload-time = "2026-03-17T19:38:47.632Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/97/43/11d6e5d2c00bf000b5329717c74563bf76a9193f4a41cb0c4ef277dde4fa/dynaconf-3.2.13-py2.py3-none-any.whl", hash = "sha256:4305527aef4834bdba3e39479b23c005186e83fb85f65bcaa4bcea58fa26759b", size = 238041, upload-time = "2026-03-17T19:38:45.337Z" }, ] -provides-extras = ["redis", "dev"] [[package]] name = "execnet" @@ -300,20 +580,20 @@ wheels = [ [[package]] name = "filelock" -version = "3.20.0" +version = "3.25.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/58/46/0028a82567109b5ef6e4d2a1f04a583fb513e6cf9527fcdd09afd817deeb/filelock-3.20.0.tar.gz", hash = "sha256:711e943b4ec6be42e1d4e6690b48dc175c822967466bb31c0c293f34334c13f4", size = 18922, upload-time = "2025-10-08T18:03:50.056Z" } +sdist = { url = "https://files.pythonhosted.org/packages/94/b8/00651a0f559862f3bb7d6f7477b192afe3f583cc5e26403b44e59a55ab34/filelock-3.25.2.tar.gz", hash = "sha256:b64ece2b38f4ca29dd3e810287aa8c48182bbecd1ae6e9ae126c9b35f1382694", size = 40480, upload-time = "2026-03-11T20:45:38.487Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/76/91/7216b27286936c16f5b4d0c530087e4a54eead683e6b0b73dd0c64844af6/filelock-3.20.0-py3-none-any.whl", hash = "sha256:339b4732ffda5cd79b13f4e2711a31b0365ce445d95d243bb996273d072546a2", size = 16054, upload-time = "2025-10-08T18:03:48.35Z" }, + { url = "https://files.pythonhosted.org/packages/a4/a5/842ae8f0c08b61d6484b52f99a03510a3a72d23141942d216ebe81fefbce/filelock-3.25.2-py3-none-any.whl", hash = "sha256:ca8afb0da15f229774c9ad1b455ed96e85a81373065fb10446672f64444ddf70", size = 26759, upload-time = "2026-03-11T20:45:37.437Z" }, ] [[package]] name = "identify" -version = "2.6.15" +version = "2.6.18" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ff/e7/685de97986c916a6d93b3876139e00eef26ad5bbbd61925d670ae8013449/identify-2.6.15.tar.gz", hash = "sha256:e4f4864b96c6557ef2a1e1c951771838f4edc9df3a72ec7118b338801b11c7bf", size = 99311, upload-time = "2025-10-02T17:43:40.631Z" } +sdist = { url = "https://files.pythonhosted.org/packages/46/c4/7fb4db12296cdb11893d61c92048fe617ee853f8523b9b296ac03b43757e/identify-2.6.18.tar.gz", hash = "sha256:873ac56a5e3fd63e7438a7ecbc4d91aca692eb3fefa4534db2b7913f3fc352fd", size = 99580, upload-time = "2026-03-15T18:39:50.319Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0f/1c/e5fd8f973d4f375adb21565739498e2e9a1e54c858a97b9a8ccfdc81da9b/identify-2.6.15-py2.py3-none-any.whl", hash = "sha256:1181ef7608e00704db228516541eb83a88a9f94433a8c80bb9b5bd54b1d81757", size = 99183, upload-time = "2025-10-02T17:43:39.137Z" }, + { url = "https://files.pythonhosted.org/packages/46/33/92ef41c6fad0233e41d3d84ba8e8ad18d1780f1e5d99b3c683e6d7f98b63/identify-2.6.18-py2.py3-none-any.whl", hash = "sha256:8db9d3c8ea9079db92cafb0ebf97abdc09d52e97f4dcf773a2e694048b7cd737", size = 99394, upload-time = "2026-03-15T18:39:48.915Z" }, ] [[package]] @@ -327,63 +607,83 @@ wheels = [ [[package]] name = "isort" -version = "7.0.0" +version = "8.0.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/63/53/4f3c058e3bace40282876f9b553343376ee687f3c35a525dc79dbd450f88/isort-7.0.0.tar.gz", hash = "sha256:5513527951aadb3ac4292a41a16cbc50dd1642432f5e8c20057d414bdafb4187", size = 805049, upload-time = "2025-10-11T13:30:59.107Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ef/7c/ec4ab396d31b3b395e2e999c8f46dec78c5e29209fac49d1f4dace04041d/isort-8.0.1.tar.gz", hash = "sha256:171ac4ff559cdc060bcfff550bc8404a486fee0caab245679c2abe7cb253c78d", size = 769592, upload-time = "2026-02-28T10:08:20.685Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7f/ed/e3705d6d02b4f7aea715a353c8ce193efd0b5db13e204df895d38734c244/isort-7.0.0-py3-none-any.whl", hash = "sha256:1bcabac8bc3c36c7fb7b98a76c8abb18e0f841a3ba81decac7691008592499c1", size = 94672, upload-time = "2025-10-11T13:30:57.665Z" }, + { url = "https://files.pythonhosted.org/packages/3e/95/c7c34aa53c16353c56d0b802fba48d5f5caa2cdee7958acbcb795c830416/isort-8.0.1-py3-none-any.whl", hash = "sha256:28b89bc70f751b559aeca209e6120393d43fbe2490de0559662be7a9787e3d75", size = 89733, upload-time = "2026-02-28T10:08:19.466Z" }, +] + +[[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 = "librt" -version = "0.7.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b3/d9/6f3d3fcf5e5543ed8a60cc70fa7d50508ed60b8a10e9af6d2058159ab54e/librt-0.7.3.tar.gz", hash = "sha256:3ec50cf65235ff5c02c5b747748d9222e564ad48597122a361269dd3aa808798", size = 144549, upload-time = "2025-12-06T19:04:45.553Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/29/90/ed8595fa4e35b6020317b5ea8d226a782dcbac7a997c19ae89fb07a41c66/librt-0.7.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0fa9ac2e49a6bee56e47573a6786cb635e128a7b12a0dc7851090037c0d397a3", size = 55687, upload-time = "2025-12-06T19:03:39.245Z" }, - { url = "https://files.pythonhosted.org/packages/dd/f6/6a20702a07b41006cb001a759440cb6b5362530920978f64a2b2ae2bf729/librt-0.7.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2e980cf1ed1a2420a6424e2ed884629cdead291686f1048810a817de07b5eb18", size = 57127, upload-time = "2025-12-06T19:03:40.3Z" }, - { url = "https://files.pythonhosted.org/packages/79/f3/b0c4703d5ffe9359b67bb2ccb86c42d4e930a363cfc72262ac3ba53cff3e/librt-0.7.3-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e094e445c37c57e9ec612847812c301840239d34ccc5d153a982fa9814478c60", size = 165336, upload-time = "2025-12-06T19:03:41.369Z" }, - { url = "https://files.pythonhosted.org/packages/02/69/3ba05b73ab29ccbe003856232cea4049769be5942d799e628d1470ed1694/librt-0.7.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aca73d70c3f553552ba9133d4a09e767dcfeee352d8d8d3eb3f77e38a3beb3ed", size = 174237, upload-time = "2025-12-06T19:03:42.44Z" }, - { url = "https://files.pythonhosted.org/packages/22/ad/d7c2671e7bf6c285ef408aa435e9cd3fdc06fd994601e1f2b242df12034f/librt-0.7.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c634a0a6db395fdaba0361aa78395597ee72c3aad651b9a307a3a7eaf5efd67e", size = 189017, upload-time = "2025-12-06T19:03:44.01Z" }, - { url = "https://files.pythonhosted.org/packages/f4/94/d13f57193148004592b618555f296b41d2d79b1dc814ff8b3273a0bf1546/librt-0.7.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a59a69deeb458c858b8fea6acf9e2acd5d755d76cd81a655256bc65c20dfff5b", size = 183983, upload-time = "2025-12-06T19:03:45.834Z" }, - { url = "https://files.pythonhosted.org/packages/02/10/b612a9944ebd39fa143c7e2e2d33f2cb790205e025ddd903fb509a3a3bb3/librt-0.7.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:d91e60ac44bbe3a77a67af4a4c13114cbe9f6d540337ce22f2c9eaf7454ca71f", size = 177602, upload-time = "2025-12-06T19:03:46.944Z" }, - { url = "https://files.pythonhosted.org/packages/1f/48/77bc05c4cc232efae6c5592c0095034390992edbd5bae8d6cf1263bb7157/librt-0.7.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:703456146dc2bf430f7832fd1341adac5c893ec3c1430194fdcefba00012555c", size = 199282, upload-time = "2025-12-06T19:03:48.069Z" }, - { url = "https://files.pythonhosted.org/packages/12/aa/05916ccd864227db1ffec2a303ae34f385c6b22d4e7ce9f07054dbcf083c/librt-0.7.3-cp312-cp312-win32.whl", hash = "sha256:b7c1239b64b70be7759554ad1a86288220bbb04d68518b527783c4ad3fb4f80b", size = 47879, upload-time = "2025-12-06T19:03:49.289Z" }, - { url = "https://files.pythonhosted.org/packages/50/92/7f41c42d31ea818b3c4b9cc1562e9714bac3c676dd18f6d5dd3d0f2aa179/librt-0.7.3-cp312-cp312-win_amd64.whl", hash = "sha256:ef59c938f72bdbc6ab52dc50f81d0637fde0f194b02d636987cea2ab30f8f55a", size = 54972, upload-time = "2025-12-06T19:03:50.335Z" }, - { url = "https://files.pythonhosted.org/packages/3f/dc/53582bbfb422311afcbc92adb75711f04e989cec052f08ec0152fbc36c9c/librt-0.7.3-cp312-cp312-win_arm64.whl", hash = "sha256:ff21c554304e8226bf80c3a7754be27c6c3549a9fec563a03c06ee8f494da8fc", size = 48338, upload-time = "2025-12-06T19:03:51.431Z" }, - { url = "https://files.pythonhosted.org/packages/93/7d/e0ce1837dfb452427db556e6d4c5301ba3b22fe8de318379fbd0593759b9/librt-0.7.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:56f2a47beda8409061bc1c865bef2d4bd9ff9255219402c0817e68ab5ad89aed", size = 55742, upload-time = "2025-12-06T19:03:52.459Z" }, - { url = "https://files.pythonhosted.org/packages/be/c0/3564262301e507e1d5cf31c7d84cb12addf0d35e05ba53312494a2eba9a4/librt-0.7.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:14569ac5dd38cfccf0a14597a88038fb16811a6fede25c67b79c6d50fc2c8fdc", size = 57163, upload-time = "2025-12-06T19:03:53.516Z" }, - { url = "https://files.pythonhosted.org/packages/be/ac/245e72b7e443d24a562f6047563c7f59833384053073ef9410476f68505b/librt-0.7.3-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6038ccbd5968325a5d6fd393cf6e00b622a8de545f0994b89dd0f748dcf3e19e", size = 165840, upload-time = "2025-12-06T19:03:54.918Z" }, - { url = "https://files.pythonhosted.org/packages/98/af/587e4491f40adba066ba39a450c66bad794c8d92094f936a201bfc7c2b5f/librt-0.7.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d39079379a9a28e74f4d57dc6357fa310a1977b51ff12239d7271ec7e71d67f5", size = 174827, upload-time = "2025-12-06T19:03:56.082Z" }, - { url = "https://files.pythonhosted.org/packages/78/21/5b8c60ea208bc83dd00421022a3874330685d7e856404128dc3728d5d1af/librt-0.7.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8837d5a52a2d7aa9f4c3220a8484013aed1d8ad75240d9a75ede63709ef89055", size = 189612, upload-time = "2025-12-06T19:03:57.507Z" }, - { url = "https://files.pythonhosted.org/packages/da/2f/8b819169ef696421fb81cd04c6cdf225f6e96f197366001e9d45180d7e9e/librt-0.7.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:399bbd7bcc1633c3e356ae274a1deb8781c7bf84d9c7962cc1ae0c6e87837292", size = 184584, upload-time = "2025-12-06T19:03:58.686Z" }, - { url = "https://files.pythonhosted.org/packages/6c/fc/af9d225a9395b77bd7678362cb055d0b8139c2018c37665de110ca388022/librt-0.7.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:8d8cf653e798ee4c4e654062b633db36984a1572f68c3aa25e364a0ddfbbb910", size = 178269, upload-time = "2025-12-06T19:03:59.769Z" }, - { url = "https://files.pythonhosted.org/packages/6c/d8/7b4fa1683b772966749d5683aa3fd605813defffe157833a8fa69cc89207/librt-0.7.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2f03484b54bf4ae80ab2e504a8d99d20d551bfe64a7ec91e218010b467d77093", size = 199852, upload-time = "2025-12-06T19:04:00.901Z" }, - { url = "https://files.pythonhosted.org/packages/77/e8/4598413aece46ca38d9260ef6c51534bd5f34b5c21474fcf210ce3a02123/librt-0.7.3-cp313-cp313-win32.whl", hash = "sha256:44b3689b040df57f492e02cd4f0bacd1b42c5400e4b8048160c9d5e866de8abe", size = 47936, upload-time = "2025-12-06T19:04:02.054Z" }, - { url = "https://files.pythonhosted.org/packages/af/80/ac0e92d5ef8c6791b3e2c62373863827a279265e0935acdf807901353b0e/librt-0.7.3-cp313-cp313-win_amd64.whl", hash = "sha256:6b407c23f16ccc36614c136251d6b32bf30de7a57f8e782378f1107be008ddb0", size = 54965, upload-time = "2025-12-06T19:04:03.224Z" }, - { url = "https://files.pythonhosted.org/packages/f1/fd/042f823fcbff25c1449bb4203a29919891ca74141b68d3a5f6612c4ce283/librt-0.7.3-cp313-cp313-win_arm64.whl", hash = "sha256:abfc57cab3c53c4546aee31859ef06753bfc136c9d208129bad23e2eca39155a", size = 48350, upload-time = "2025-12-06T19:04:04.234Z" }, - { url = "https://files.pythonhosted.org/packages/3e/ae/c6ecc7bb97134a71b5241e8855d39964c0e5f4d96558f0d60593892806d2/librt-0.7.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:120dd21d46ff875e849f1aae19346223cf15656be489242fe884036b23d39e93", size = 55175, upload-time = "2025-12-06T19:04:05.308Z" }, - { url = "https://files.pythonhosted.org/packages/cf/bc/2cc0cb0ab787b39aa5c7645cd792433c875982bdf12dccca558b89624594/librt-0.7.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1617bea5ab31266e152871208502ee943cb349c224846928a1173c864261375e", size = 56881, upload-time = "2025-12-06T19:04:06.674Z" }, - { url = "https://files.pythonhosted.org/packages/8e/87/397417a386190b70f5bf26fcedbaa1515f19dce33366e2684c6b7ee83086/librt-0.7.3-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:93b2a1f325fefa1482516ced160c8c7b4b8d53226763fa6c93d151fa25164207", size = 163710, upload-time = "2025-12-06T19:04:08.437Z" }, - { url = "https://files.pythonhosted.org/packages/c9/37/7338f85b80e8a17525d941211451199845093ca242b32efbf01df8531e72/librt-0.7.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f3d4801db8354436fd3936531e7f0e4feb411f62433a6b6cb32bb416e20b529f", size = 172471, upload-time = "2025-12-06T19:04:10.124Z" }, - { url = "https://files.pythonhosted.org/packages/3b/e0/741704edabbfae2c852fedc1b40d9ed5a783c70ed3ed8e4fe98f84b25d13/librt-0.7.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11ad45122bbed42cfc8b0597450660126ef28fd2d9ae1a219bc5af8406f95678", size = 186804, upload-time = "2025-12-06T19:04:11.586Z" }, - { url = "https://files.pythonhosted.org/packages/f4/d1/0a82129d6ba242f3be9af34815be089f35051bc79619f5c27d2c449ecef6/librt-0.7.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:6b4e7bff1d76dd2b46443078519dc75df1b5e01562345f0bb740cea5266d8218", size = 181817, upload-time = "2025-12-06T19:04:12.802Z" }, - { url = "https://files.pythonhosted.org/packages/4f/32/704f80bcf9979c68d4357c46f2af788fbf9d5edda9e7de5786ed2255e911/librt-0.7.3-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:d86f94743a11873317094326456b23f8a5788bad9161fd2f0e52088c33564620", size = 175602, upload-time = "2025-12-06T19:04:14.004Z" }, - { url = "https://files.pythonhosted.org/packages/f7/6d/4355cfa0fae0c062ba72f541d13db5bc575770125a7ad3d4f46f4109d305/librt-0.7.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:754a0d09997095ad764ccef050dd5bf26cbf457aab9effcba5890dad081d879e", size = 196497, upload-time = "2025-12-06T19:04:15.487Z" }, - { url = "https://files.pythonhosted.org/packages/2e/eb/ac6d8517d44209e5a712fde46f26d0055e3e8969f24d715f70bd36056230/librt-0.7.3-cp314-cp314-win32.whl", hash = "sha256:fbd7351d43b80d9c64c3cfcb50008f786cc82cba0450e8599fdd64f264320bd3", size = 44678, upload-time = "2025-12-06T19:04:16.688Z" }, - { url = "https://files.pythonhosted.org/packages/e9/93/238f026d141faf9958da588c761a0812a1a21c98cc54a76f3608454e4e59/librt-0.7.3-cp314-cp314-win_amd64.whl", hash = "sha256:d376a35c6561e81d2590506804b428fc1075fcc6298fc5bb49b771534c0ba010", size = 51689, upload-time = "2025-12-06T19:04:17.726Z" }, - { url = "https://files.pythonhosted.org/packages/52/44/43f462ad9dcf9ed7d3172fe2e30d77b980956250bd90e9889a9cca93df2a/librt-0.7.3-cp314-cp314-win_arm64.whl", hash = "sha256:cbdb3f337c88b43c3b49ca377731912c101178be91cb5071aac48faa898e6f8e", size = 44662, upload-time = "2025-12-06T19:04:18.771Z" }, - { url = "https://files.pythonhosted.org/packages/1d/35/fed6348915f96b7323241de97f26e2af481e95183b34991df12fd5ce31b1/librt-0.7.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9f0e0927efe87cd42ad600628e595a1a0aa1c64f6d0b55f7e6059079a428641a", size = 57347, upload-time = "2025-12-06T19:04:19.812Z" }, - { url = "https://files.pythonhosted.org/packages/9a/f2/045383ccc83e3fea4fba1b761796584bc26817b6b2efb6b8a6731431d16f/librt-0.7.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:020c6db391268bcc8ce75105cb572df8cb659a43fd347366aaa407c366e5117a", size = 59223, upload-time = "2025-12-06T19:04:20.862Z" }, - { url = "https://files.pythonhosted.org/packages/77/3f/c081f8455ab1d7f4a10dbe58463ff97119272ff32494f21839c3b9029c2c/librt-0.7.3-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7af7785f5edd1f418da09a8cdb9ec84b0213e23d597413e06525340bcce1ea4f", size = 183861, upload-time = "2025-12-06T19:04:21.963Z" }, - { url = "https://files.pythonhosted.org/packages/1d/f5/73c5093c22c31fbeaebc25168837f05ebfd8bf26ce00855ef97a5308f36f/librt-0.7.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8ccadf260bb46a61b9c7e89e2218f6efea9f3eeaaab4e3d1f58571890e54858e", size = 194594, upload-time = "2025-12-06T19:04:23.14Z" }, - { url = "https://files.pythonhosted.org/packages/78/b8/d5f17d4afe16612a4a94abfded94c16c5a033f183074fb130dfe56fc1a42/librt-0.7.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d9883b2d819ce83f87ba82a746c81d14ada78784db431e57cc9719179847376e", size = 206759, upload-time = "2025-12-06T19:04:24.328Z" }, - { url = "https://files.pythonhosted.org/packages/36/2e/021765c1be85ee23ffd5b5b968bb4cba7526a4db2a0fc27dcafbdfc32da7/librt-0.7.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:59cb0470612d21fa1efddfa0dd710756b50d9c7fb6c1236bbf8ef8529331dc70", size = 203210, upload-time = "2025-12-06T19:04:25.544Z" }, - { url = "https://files.pythonhosted.org/packages/77/f0/9923656e42da4fd18c594bd08cf6d7e152d4158f8b808e210d967f0dcceb/librt-0.7.3-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:1fe603877e1865b5fd047a5e40379509a4a60204aa7aa0f72b16f7a41c3f0712", size = 196708, upload-time = "2025-12-06T19:04:26.725Z" }, - { url = "https://files.pythonhosted.org/packages/fc/0b/0708b886ac760e64d6fbe7e16024e4be3ad1a3629d19489a97e9cf4c3431/librt-0.7.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5460d99ed30f043595bbdc888f542bad2caeb6226b01c33cda3ae444e8f82d42", size = 217212, upload-time = "2025-12-06T19:04:27.892Z" }, - { url = "https://files.pythonhosted.org/packages/5d/7f/12a73ff17bca4351e73d585dd9ebf46723c4a8622c4af7fe11a2e2d011ff/librt-0.7.3-cp314-cp314t-win32.whl", hash = "sha256:d09f677693328503c9e492e33e9601464297c01f9ebd966ea8fc5308f3069bfd", size = 45586, upload-time = "2025-12-06T19:04:29.116Z" }, - { url = "https://files.pythonhosted.org/packages/e2/df/8decd032ac9b995e4f5606cde783711a71094128d88d97a52e397daf2c89/librt-0.7.3-cp314-cp314t-win_amd64.whl", hash = "sha256:25711f364c64cab2c910a0247e90b51421e45dbc8910ceeb4eac97a9e132fc6f", size = 53002, upload-time = "2025-12-06T19:04:30.173Z" }, - { url = "https://files.pythonhosted.org/packages/de/0c/6605b6199de8178afe7efc77ca1d8e6db00453bc1d3349d27605c0f42104/librt-0.7.3-cp314-cp314t-win_arm64.whl", hash = "sha256:a9f9b661f82693eb56beb0605156c7fca57f535704ab91837405913417d6990b", size = 45647, upload-time = "2025-12-06T19:04:31.302Z" }, +version = "0.8.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/56/9c/b4b0c54d84da4a94b37bd44151e46d5e583c9534c7e02250b961b1b6d8a8/librt-0.8.1.tar.gz", hash = "sha256:be46a14693955b3bd96014ccbdb8339ee8c9346fbe11c1b78901b55125f14c73", size = 177471, upload-time = "2026-02-17T16:13:06.101Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/95/21/d39b0a87ac52fc98f621fb6f8060efb017a767ebbbac2f99fbcbc9ddc0d7/librt-0.8.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a28f2612ab566b17f3698b0da021ff9960610301607c9a5e8eaca62f5e1c350a", size = 66516, upload-time = "2026-02-17T16:11:41.604Z" }, + { url = "https://files.pythonhosted.org/packages/69/f1/46375e71441c43e8ae335905e069f1c54febee63a146278bcee8782c84fd/librt-0.8.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:60a78b694c9aee2a0f1aaeaa7d101cf713e92e8423a941d2897f4fa37908dab9", size = 68634, upload-time = "2026-02-17T16:11:43.268Z" }, + { url = "https://files.pythonhosted.org/packages/0a/33/c510de7f93bf1fa19e13423a606d8189a02624a800710f6e6a0a0f0784b3/librt-0.8.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:758509ea3f1eba2a57558e7e98f4659d0ea7670bff49673b0dde18a3c7e6c0eb", size = 198941, upload-time = "2026-02-17T16:11:44.28Z" }, + { url = "https://files.pythonhosted.org/packages/dd/36/e725903416409a533d92398e88ce665476f275081d0d7d42f9c4951999e5/librt-0.8.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:039b9f2c506bd0ab0f8725aa5ba339c6f0cd19d3b514b50d134789809c24285d", size = 209991, upload-time = "2026-02-17T16:11:45.462Z" }, + { url = "https://files.pythonhosted.org/packages/30/7a/8d908a152e1875c9f8eac96c97a480df425e657cdb47854b9efaa4998889/librt-0.8.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5bb54f1205a3a6ab41a6fd71dfcdcbd278670d3a90ca502a30d9da583105b6f7", size = 224476, upload-time = "2026-02-17T16:11:46.542Z" }, + { url = "https://files.pythonhosted.org/packages/a8/b8/a22c34f2c485b8903a06f3fe3315341fe6876ef3599792344669db98fcff/librt-0.8.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:05bd41cdee35b0c59c259f870f6da532a2c5ca57db95b5f23689fcb5c9e42440", size = 217518, upload-time = "2026-02-17T16:11:47.746Z" }, + { url = "https://files.pythonhosted.org/packages/79/6f/5c6fea00357e4f82ba44f81dbfb027921f1ab10e320d4a64e1c408d035d9/librt-0.8.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:adfab487facf03f0d0857b8710cf82d0704a309d8ffc33b03d9302b4c64e91a9", size = 225116, upload-time = "2026-02-17T16:11:49.298Z" }, + { url = "https://files.pythonhosted.org/packages/f2/a0/95ced4e7b1267fe1e2720a111685bcddf0e781f7e9e0ce59d751c44dcfe5/librt-0.8.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:153188fe98a72f206042be10a2c6026139852805215ed9539186312d50a8e972", size = 217751, upload-time = "2026-02-17T16:11:50.49Z" }, + { url = "https://files.pythonhosted.org/packages/93/c2/0517281cb4d4101c27ab59472924e67f55e375bc46bedae94ac6dc6e1902/librt-0.8.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:dd3c41254ee98604b08bd5b3af5bf0a89740d4ee0711de95b65166bf44091921", size = 218378, upload-time = "2026-02-17T16:11:51.783Z" }, + { url = "https://files.pythonhosted.org/packages/43/e8/37b3ac108e8976888e559a7b227d0ceac03c384cfd3e7a1c2ee248dbae79/librt-0.8.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e0d138c7ae532908cbb342162b2611dbd4d90c941cd25ab82084aaf71d2c0bd0", size = 241199, upload-time = "2026-02-17T16:11:53.561Z" }, + { url = "https://files.pythonhosted.org/packages/4b/5b/35812d041c53967fedf551a39399271bbe4257e681236a2cf1a69c8e7fa1/librt-0.8.1-cp312-cp312-win32.whl", hash = "sha256:43353b943613c5d9c49a25aaffdba46f888ec354e71e3529a00cca3f04d66a7a", size = 54917, upload-time = "2026-02-17T16:11:54.758Z" }, + { url = "https://files.pythonhosted.org/packages/de/d1/fa5d5331b862b9775aaf2a100f5ef86854e5d4407f71bddf102f4421e034/librt-0.8.1-cp312-cp312-win_amd64.whl", hash = "sha256:ff8baf1f8d3f4b6b7257fcb75a501f2a5499d0dda57645baa09d4d0d34b19444", size = 62017, upload-time = "2026-02-17T16:11:55.748Z" }, + { url = "https://files.pythonhosted.org/packages/c7/7c/c614252f9acda59b01a66e2ddfd243ed1c7e1deab0293332dfbccf862808/librt-0.8.1-cp312-cp312-win_arm64.whl", hash = "sha256:0f2ae3725904f7377e11cc37722d5d401e8b3d5851fb9273d7f4fe04f6b3d37d", size = 52441, upload-time = "2026-02-17T16:11:56.801Z" }, + { url = "https://files.pythonhosted.org/packages/c5/3c/f614c8e4eaac7cbf2bbdf9528790b21d89e277ee20d57dc6e559c626105f/librt-0.8.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7e6bad1cd94f6764e1e21950542f818a09316645337fd5ab9a7acc45d99a8f35", size = 66529, upload-time = "2026-02-17T16:11:57.809Z" }, + { url = "https://files.pythonhosted.org/packages/ab/96/5836544a45100ae411eda07d29e3d99448e5258b6e9c8059deb92945f5c2/librt-0.8.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cf450f498c30af55551ba4f66b9123b7185362ec8b625a773b3d39aa1a717583", size = 68669, upload-time = "2026-02-17T16:11:58.843Z" }, + { url = "https://files.pythonhosted.org/packages/06/53/f0b992b57af6d5531bf4677d75c44f095f2366a1741fb695ee462ae04b05/librt-0.8.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:eca45e982fa074090057132e30585a7e8674e9e885d402eae85633e9f449ce6c", size = 199279, upload-time = "2026-02-17T16:11:59.862Z" }, + { url = "https://files.pythonhosted.org/packages/f3/ad/4848cc16e268d14280d8168aee4f31cea92bbd2b79ce33d3e166f2b4e4fc/librt-0.8.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c3811485fccfda840861905b8c70bba5ec094e02825598bb9d4ca3936857a04", size = 210288, upload-time = "2026-02-17T16:12:00.954Z" }, + { url = "https://files.pythonhosted.org/packages/52/05/27fdc2e95de26273d83b96742d8d3b7345f2ea2bdbd2405cc504644f2096/librt-0.8.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e4af413908f77294605e28cfd98063f54b2c790561383971d2f52d113d9c363", size = 224809, upload-time = "2026-02-17T16:12:02.108Z" }, + { url = "https://files.pythonhosted.org/packages/7a/d0/78200a45ba3240cb042bc597d6f2accba9193a2c57d0356268cbbe2d0925/librt-0.8.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5212a5bd7fae98dae95710032902edcd2ec4dc994e883294f75c857b83f9aba0", size = 218075, upload-time = "2026-02-17T16:12:03.631Z" }, + { url = "https://files.pythonhosted.org/packages/af/72/a210839fa74c90474897124c064ffca07f8d4b347b6574d309686aae7ca6/librt-0.8.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e692aa2d1d604e6ca12d35e51fdc36f4cda6345e28e36374579f7ef3611b3012", size = 225486, upload-time = "2026-02-17T16:12:04.725Z" }, + { url = "https://files.pythonhosted.org/packages/a3/c1/a03cc63722339ddbf087485f253493e2b013039f5b707e8e6016141130fa/librt-0.8.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4be2a5c926b9770c9e08e717f05737a269b9d0ebc5d2f0060f0fe3fe9ce47acb", size = 218219, upload-time = "2026-02-17T16:12:05.828Z" }, + { url = "https://files.pythonhosted.org/packages/58/f5/fff6108af0acf941c6f274a946aea0e484bd10cd2dc37610287ce49388c5/librt-0.8.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:fd1a720332ea335ceb544cf0a03f81df92abd4bb887679fd1e460976b0e6214b", size = 218750, upload-time = "2026-02-17T16:12:07.09Z" }, + { url = "https://files.pythonhosted.org/packages/71/67/5a387bfef30ec1e4b4f30562c8586566faf87e47d696768c19feb49e3646/librt-0.8.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:93c2af9e01e0ef80d95ae3c720be101227edae5f2fe7e3dc63d8857fadfc5a1d", size = 241624, upload-time = "2026-02-17T16:12:08.43Z" }, + { url = "https://files.pythonhosted.org/packages/d4/be/24f8502db11d405232ac1162eb98069ca49c3306c1d75c6ccc61d9af8789/librt-0.8.1-cp313-cp313-win32.whl", hash = "sha256:086a32dbb71336627e78cc1d6ee305a68d038ef7d4c39aaff41ae8c9aa46e91a", size = 54969, upload-time = "2026-02-17T16:12:09.633Z" }, + { url = "https://files.pythonhosted.org/packages/5c/73/c9fdf6cb2a529c1a092ce769a12d88c8cca991194dfe641b6af12fa964d2/librt-0.8.1-cp313-cp313-win_amd64.whl", hash = "sha256:e11769a1dbda4da7b00a76cfffa67aa47cfa66921d2724539eee4b9ede780b79", size = 62000, upload-time = "2026-02-17T16:12:10.632Z" }, + { url = "https://files.pythonhosted.org/packages/d3/97/68f80ca3ac4924f250cdfa6e20142a803e5e50fca96ef5148c52ee8c10ea/librt-0.8.1-cp313-cp313-win_arm64.whl", hash = "sha256:924817ab3141aca17893386ee13261f1d100d1ef410d70afe4389f2359fea4f0", size = 52495, upload-time = "2026-02-17T16:12:11.633Z" }, + { url = "https://files.pythonhosted.org/packages/c9/6a/907ef6800f7bca71b525a05f1839b21f708c09043b1c6aa77b6b827b3996/librt-0.8.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:6cfa7fe54fd4d1f47130017351a959fe5804bda7a0bc7e07a2cdbc3fdd28d34f", size = 66081, upload-time = "2026-02-17T16:12:12.766Z" }, + { url = "https://files.pythonhosted.org/packages/1b/18/25e991cd5640c9fb0f8d91b18797b29066b792f17bf8493da183bf5caabe/librt-0.8.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:228c2409c079f8c11fb2e5d7b277077f694cb93443eb760e00b3b83cb8b3176c", size = 68309, upload-time = "2026-02-17T16:12:13.756Z" }, + { url = "https://files.pythonhosted.org/packages/a4/36/46820d03f058cfb5a9de5940640ba03165ed8aded69e0733c417bb04df34/librt-0.8.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7aae78ab5e3206181780e56912d1b9bb9f90a7249ce12f0e8bf531d0462dd0fc", size = 196804, upload-time = "2026-02-17T16:12:14.818Z" }, + { url = "https://files.pythonhosted.org/packages/59/18/5dd0d3b87b8ff9c061849fbdb347758d1f724b9a82241aa908e0ec54ccd0/librt-0.8.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:172d57ec04346b047ca6af181e1ea4858086c80bdf455f61994c4aa6fc3f866c", size = 206907, upload-time = "2026-02-17T16:12:16.513Z" }, + { url = "https://files.pythonhosted.org/packages/d1/96/ef04902aad1424fd7299b62d1890e803e6ab4018c3044dca5922319c4b97/librt-0.8.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6b1977c4ea97ce5eb7755a78fae68d87e4102e4aaf54985e8b56806849cc06a3", size = 221217, upload-time = "2026-02-17T16:12:17.906Z" }, + { url = "https://files.pythonhosted.org/packages/6d/ff/7e01f2dda84a8f5d280637a2e5827210a8acca9a567a54507ef1c75b342d/librt-0.8.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:10c42e1f6fd06733ef65ae7bebce2872bcafd8d6e6b0a08fe0a05a23b044fb14", size = 214622, upload-time = "2026-02-17T16:12:19.108Z" }, + { url = "https://files.pythonhosted.org/packages/1e/8c/5b093d08a13946034fed57619742f790faf77058558b14ca36a6e331161e/librt-0.8.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4c8dfa264b9193c4ee19113c985c95f876fae5e51f731494fc4e0cf594990ba7", size = 221987, upload-time = "2026-02-17T16:12:20.331Z" }, + { url = "https://files.pythonhosted.org/packages/d3/cc/86b0b3b151d40920ad45a94ce0171dec1aebba8a9d72bb3fa00c73ab25dd/librt-0.8.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:01170b6729a438f0dedc4a26ed342e3dc4f02d1000b4b19f980e1877f0c297e6", size = 215132, upload-time = "2026-02-17T16:12:21.54Z" }, + { url = "https://files.pythonhosted.org/packages/fc/be/8588164a46edf1e69858d952654e216a9a91174688eeefb9efbb38a9c799/librt-0.8.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:7b02679a0d783bdae30d443025b94465d8c3dc512f32f5b5031f93f57ac32071", size = 215195, upload-time = "2026-02-17T16:12:23.073Z" }, + { url = "https://files.pythonhosted.org/packages/f5/f2/0b9279bea735c734d69344ecfe056c1ba211694a72df10f568745c899c76/librt-0.8.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:190b109bb69592a3401fe1ffdea41a2e73370ace2ffdc4a0e8e2b39cdea81b78", size = 237946, upload-time = "2026-02-17T16:12:24.275Z" }, + { url = "https://files.pythonhosted.org/packages/e9/cc/5f2a34fbc8aeb35314a3641f9956fa9051a947424652fad9882be7a97949/librt-0.8.1-cp314-cp314-win32.whl", hash = "sha256:e70a57ecf89a0f64c24e37f38d3fe217a58169d2fe6ed6d70554964042474023", size = 50689, upload-time = "2026-02-17T16:12:25.766Z" }, + { url = "https://files.pythonhosted.org/packages/a0/76/cd4d010ab2147339ca2b93e959c3686e964edc6de66ddacc935c325883d7/librt-0.8.1-cp314-cp314-win_amd64.whl", hash = "sha256:7e2f3edca35664499fbb36e4770650c4bd4a08abc1f4458eab9df4ec56389730", size = 57875, upload-time = "2026-02-17T16:12:27.465Z" }, + { url = "https://files.pythonhosted.org/packages/84/0f/2143cb3c3ca48bd3379dcd11817163ca50781927c4537345d608b5045998/librt-0.8.1-cp314-cp314-win_arm64.whl", hash = "sha256:0d2f82168e55ddefd27c01c654ce52379c0750ddc31ee86b4b266bcf4d65f2a3", size = 48058, upload-time = "2026-02-17T16:12:28.556Z" }, + { url = "https://files.pythonhosted.org/packages/d2/0e/9b23a87e37baf00311c3efe6b48d6b6c168c29902dfc3f04c338372fd7db/librt-0.8.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2c74a2da57a094bd48d03fa5d196da83d2815678385d2978657499063709abe1", size = 68313, upload-time = "2026-02-17T16:12:29.659Z" }, + { url = "https://files.pythonhosted.org/packages/db/9a/859c41e5a4f1c84200a7d2b92f586aa27133c8243b6cac9926f6e54d01b9/librt-0.8.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a355d99c4c0d8e5b770313b8b247411ed40949ca44e33e46a4789b9293a907ee", size = 70994, upload-time = "2026-02-17T16:12:31.516Z" }, + { url = "https://files.pythonhosted.org/packages/4c/28/10605366ee599ed34223ac2bf66404c6fb59399f47108215d16d5ad751a8/librt-0.8.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2eb345e8b33fb748227409c9f1233d4df354d6e54091f0e8fc53acdb2ffedeb7", size = 220770, upload-time = "2026-02-17T16:12:33.294Z" }, + { url = "https://files.pythonhosted.org/packages/af/8d/16ed8fd452dafae9c48d17a6bc1ee3e818fd40ef718d149a8eff2c9f4ea2/librt-0.8.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9be2f15e53ce4e83cc08adc29b26fb5978db62ef2a366fbdf716c8a6c8901040", size = 235409, upload-time = "2026-02-17T16:12:35.443Z" }, + { url = "https://files.pythonhosted.org/packages/89/1b/7bdf3e49349c134b25db816e4a3db6b94a47ac69d7d46b1e682c2c4949be/librt-0.8.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:785ae29c1f5c6e7c2cde2c7c0e148147f4503da3abc5d44d482068da5322fd9e", size = 246473, upload-time = "2026-02-17T16:12:36.656Z" }, + { url = "https://files.pythonhosted.org/packages/4e/8a/91fab8e4fd2a24930a17188c7af5380eb27b203d72101c9cc000dbdfd95a/librt-0.8.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1d3a7da44baf692f0c6aeb5b2a09c5e6fc7a703bca9ffa337ddd2e2da53f7732", size = 238866, upload-time = "2026-02-17T16:12:37.849Z" }, + { url = "https://files.pythonhosted.org/packages/b9/e0/c45a098843fc7c07e18a7f8a24ca8496aecbf7bdcd54980c6ca1aaa79a8e/librt-0.8.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5fc48998000cbc39ec0d5311312dda93ecf92b39aaf184c5e817d5d440b29624", size = 250248, upload-time = "2026-02-17T16:12:39.445Z" }, + { url = "https://files.pythonhosted.org/packages/82/30/07627de23036640c952cce0c1fe78972e77d7d2f8fd54fa5ef4554ff4a56/librt-0.8.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:e96baa6820280077a78244b2e06e416480ed859bbd8e5d641cf5742919d8beb4", size = 240629, upload-time = "2026-02-17T16:12:40.889Z" }, + { url = "https://files.pythonhosted.org/packages/fb/c1/55bfe1ee3542eba055616f9098eaf6eddb966efb0ca0f44eaa4aba327307/librt-0.8.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:31362dbfe297b23590530007062c32c6f6176f6099646bb2c95ab1b00a57c382", size = 239615, upload-time = "2026-02-17T16:12:42.446Z" }, + { url = "https://files.pythonhosted.org/packages/2b/39/191d3d28abc26c9099b19852e6c99f7f6d400b82fa5a4e80291bd3803e19/librt-0.8.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cc3656283d11540ab0ea01978378e73e10002145117055e03722417aeab30994", size = 263001, upload-time = "2026-02-17T16:12:43.627Z" }, + { url = "https://files.pythonhosted.org/packages/b9/eb/7697f60fbe7042ab4e88f4ee6af496b7f222fffb0a4e3593ef1f29f81652/librt-0.8.1-cp314-cp314t-win32.whl", hash = "sha256:738f08021b3142c2918c03692608baed43bc51144c29e35807682f8070ee2a3a", size = 51328, upload-time = "2026-02-17T16:12:45.148Z" }, + { url = "https://files.pythonhosted.org/packages/7c/72/34bf2eb7a15414a23e5e70ecb9440c1d3179f393d9349338a91e2781c0fb/librt-0.8.1-cp314-cp314t-win_amd64.whl", hash = "sha256:89815a22daf9c51884fb5dbe4f1ef65ee6a146e0b6a8df05f753e2e4a9359bf4", size = 58722, upload-time = "2026-02-17T16:12:46.85Z" }, + { url = "https://files.pythonhosted.org/packages/b2/c8/d148e041732d631fc76036f8b30fae4e77b027a1e95b7a84bb522481a940/librt-0.8.1-cp314-cp314t-win_arm64.whl", hash = "sha256:bf512a71a23504ed08103a13c941f763db13fb11177beb3d9244c98c29fb4a61", size = 48755, upload-time = "2026-02-17T16:12:47.943Z" }, ] [[package]] @@ -398,6 +698,69 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, ] +[[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/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, + { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, + { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, + { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, + { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, + { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, + { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, + { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, + { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, + { 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 = "mdurl" version = "0.1.2" @@ -407,37 +770,81 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, ] +[[package]] +name = "msgpack" +version = "1.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4d/f2/bfb55a6236ed8725a96b0aa3acbd0ec17588e6a2c3b62a93eb513ed8783f/msgpack-1.1.2.tar.gz", hash = "sha256:3b60763c1373dd60f398488069bcdc703cd08a711477b5d480eecc9f9626f47e", size = 173581, upload-time = "2025-10-08T09:15:56.596Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ad/bd/8b0d01c756203fbab65d265859749860682ccd2a59594609aeec3a144efa/msgpack-1.1.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:70a0dff9d1f8da25179ffcf880e10cf1aad55fdb63cd59c9a49a1b82290062aa", size = 81939, upload-time = "2025-10-08T09:15:01.472Z" }, + { url = "https://files.pythonhosted.org/packages/34/68/ba4f155f793a74c1483d4bdef136e1023f7bcba557f0db4ef3db3c665cf1/msgpack-1.1.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:446abdd8b94b55c800ac34b102dffd2f6aa0ce643c55dfc017ad89347db3dbdb", size = 85064, upload-time = "2025-10-08T09:15:03.764Z" }, + { url = "https://files.pythonhosted.org/packages/f2/60/a064b0345fc36c4c3d2c743c82d9100c40388d77f0b48b2f04d6041dbec1/msgpack-1.1.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c63eea553c69ab05b6747901b97d620bb2a690633c77f23feb0c6a947a8a7b8f", size = 417131, upload-time = "2025-10-08T09:15:05.136Z" }, + { url = "https://files.pythonhosted.org/packages/65/92/a5100f7185a800a5d29f8d14041f61475b9de465ffcc0f3b9fba606e4505/msgpack-1.1.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:372839311ccf6bdaf39b00b61288e0557916c3729529b301c52c2d88842add42", size = 427556, upload-time = "2025-10-08T09:15:06.837Z" }, + { url = "https://files.pythonhosted.org/packages/f5/87/ffe21d1bf7d9991354ad93949286f643b2bb6ddbeab66373922b44c3b8cc/msgpack-1.1.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2929af52106ca73fcb28576218476ffbb531a036c2adbcf54a3664de124303e9", size = 404920, upload-time = "2025-10-08T09:15:08.179Z" }, + { url = "https://files.pythonhosted.org/packages/ff/41/8543ed2b8604f7c0d89ce066f42007faac1eaa7d79a81555f206a5cdb889/msgpack-1.1.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:be52a8fc79e45b0364210eef5234a7cf8d330836d0a64dfbb878efa903d84620", size = 415013, upload-time = "2025-10-08T09:15:09.83Z" }, + { url = "https://files.pythonhosted.org/packages/41/0d/2ddfaa8b7e1cee6c490d46cb0a39742b19e2481600a7a0e96537e9c22f43/msgpack-1.1.2-cp312-cp312-win32.whl", hash = "sha256:1fff3d825d7859ac888b0fbda39a42d59193543920eda9d9bea44d958a878029", size = 65096, upload-time = "2025-10-08T09:15:11.11Z" }, + { url = "https://files.pythonhosted.org/packages/8c/ec/d431eb7941fb55a31dd6ca3404d41fbb52d99172df2e7707754488390910/msgpack-1.1.2-cp312-cp312-win_amd64.whl", hash = "sha256:1de460f0403172cff81169a30b9a92b260cb809c4cb7e2fc79ae8d0510c78b6b", size = 72708, upload-time = "2025-10-08T09:15:12.554Z" }, + { url = "https://files.pythonhosted.org/packages/c5/31/5b1a1f70eb0e87d1678e9624908f86317787b536060641d6798e3cf70ace/msgpack-1.1.2-cp312-cp312-win_arm64.whl", hash = "sha256:be5980f3ee0e6bd44f3a9e9dea01054f175b50c3e6cdb692bc9424c0bbb8bf69", size = 64119, upload-time = "2025-10-08T09:15:13.589Z" }, + { url = "https://files.pythonhosted.org/packages/6b/31/b46518ecc604d7edf3a4f94cb3bf021fc62aa301f0cb849936968164ef23/msgpack-1.1.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4efd7b5979ccb539c221a4c4e16aac1a533efc97f3b759bb5a5ac9f6d10383bf", size = 81212, upload-time = "2025-10-08T09:15:14.552Z" }, + { url = "https://files.pythonhosted.org/packages/92/dc/c385f38f2c2433333345a82926c6bfa5ecfff3ef787201614317b58dd8be/msgpack-1.1.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:42eefe2c3e2af97ed470eec850facbe1b5ad1d6eacdbadc42ec98e7dcf68b4b7", size = 84315, upload-time = "2025-10-08T09:15:15.543Z" }, + { url = "https://files.pythonhosted.org/packages/d3/68/93180dce57f684a61a88a45ed13047558ded2be46f03acb8dec6d7c513af/msgpack-1.1.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1fdf7d83102bf09e7ce3357de96c59b627395352a4024f6e2458501f158bf999", size = 412721, upload-time = "2025-10-08T09:15:16.567Z" }, + { url = "https://files.pythonhosted.org/packages/5d/ba/459f18c16f2b3fc1a1ca871f72f07d70c07bf768ad0a507a698b8052ac58/msgpack-1.1.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fac4be746328f90caa3cd4bc67e6fe36ca2bf61d5c6eb6d895b6527e3f05071e", size = 424657, upload-time = "2025-10-08T09:15:17.825Z" }, + { url = "https://files.pythonhosted.org/packages/38/f8/4398c46863b093252fe67368b44edc6c13b17f4e6b0e4929dbf0bdb13f23/msgpack-1.1.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:fffee09044073e69f2bad787071aeec727183e7580443dfeb8556cbf1978d162", size = 402668, upload-time = "2025-10-08T09:15:19.003Z" }, + { url = "https://files.pythonhosted.org/packages/28/ce/698c1eff75626e4124b4d78e21cca0b4cc90043afb80a507626ea354ab52/msgpack-1.1.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5928604de9b032bc17f5099496417f113c45bc6bc21b5c6920caf34b3c428794", size = 419040, upload-time = "2025-10-08T09:15:20.183Z" }, + { url = "https://files.pythonhosted.org/packages/67/32/f3cd1667028424fa7001d82e10ee35386eea1408b93d399b09fb0aa7875f/msgpack-1.1.2-cp313-cp313-win32.whl", hash = "sha256:a7787d353595c7c7e145e2331abf8b7ff1e6673a6b974ded96e6d4ec09f00c8c", size = 65037, upload-time = "2025-10-08T09:15:21.416Z" }, + { url = "https://files.pythonhosted.org/packages/74/07/1ed8277f8653c40ebc65985180b007879f6a836c525b3885dcc6448ae6cb/msgpack-1.1.2-cp313-cp313-win_amd64.whl", hash = "sha256:a465f0dceb8e13a487e54c07d04ae3ba131c7c5b95e2612596eafde1dccf64a9", size = 72631, upload-time = "2025-10-08T09:15:22.431Z" }, + { url = "https://files.pythonhosted.org/packages/e5/db/0314e4e2db56ebcf450f277904ffd84a7988b9e5da8d0d61ab2d057df2b6/msgpack-1.1.2-cp313-cp313-win_arm64.whl", hash = "sha256:e69b39f8c0aa5ec24b57737ebee40be647035158f14ed4b40e6f150077e21a84", size = 64118, upload-time = "2025-10-08T09:15:23.402Z" }, + { url = "https://files.pythonhosted.org/packages/22/71/201105712d0a2ff07b7873ed3c220292fb2ea5120603c00c4b634bcdafb3/msgpack-1.1.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e23ce8d5f7aa6ea6d2a2b326b4ba46c985dbb204523759984430db7114f8aa00", size = 81127, upload-time = "2025-10-08T09:15:24.408Z" }, + { url = "https://files.pythonhosted.org/packages/1b/9f/38ff9e57a2eade7bf9dfee5eae17f39fc0e998658050279cbb14d97d36d9/msgpack-1.1.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6c15b7d74c939ebe620dd8e559384be806204d73b4f9356320632d783d1f7939", size = 84981, upload-time = "2025-10-08T09:15:25.812Z" }, + { url = "https://files.pythonhosted.org/packages/8e/a9/3536e385167b88c2cc8f4424c49e28d49a6fc35206d4a8060f136e71f94c/msgpack-1.1.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:99e2cb7b9031568a2a5c73aa077180f93dd2e95b4f8d3b8e14a73ae94a9e667e", size = 411885, upload-time = "2025-10-08T09:15:27.22Z" }, + { url = "https://files.pythonhosted.org/packages/2f/40/dc34d1a8d5f1e51fc64640b62b191684da52ca469da9cd74e84936ffa4a6/msgpack-1.1.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:180759d89a057eab503cf62eeec0aa61c4ea1200dee709f3a8e9397dbb3b6931", size = 419658, upload-time = "2025-10-08T09:15:28.4Z" }, + { url = "https://files.pythonhosted.org/packages/3b/ef/2b92e286366500a09a67e03496ee8b8ba00562797a52f3c117aa2b29514b/msgpack-1.1.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:04fb995247a6e83830b62f0b07bf36540c213f6eac8e851166d8d86d83cbd014", size = 403290, upload-time = "2025-10-08T09:15:29.764Z" }, + { url = "https://files.pythonhosted.org/packages/78/90/e0ea7990abea5764e4655b8177aa7c63cdfa89945b6e7641055800f6c16b/msgpack-1.1.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8e22ab046fa7ede9e36eeb4cfad44d46450f37bb05d5ec482b02868f451c95e2", size = 415234, upload-time = "2025-10-08T09:15:31.022Z" }, + { url = "https://files.pythonhosted.org/packages/72/4e/9390aed5db983a2310818cd7d3ec0aecad45e1f7007e0cda79c79507bb0d/msgpack-1.1.2-cp314-cp314-win32.whl", hash = "sha256:80a0ff7d4abf5fecb995fcf235d4064b9a9a8a40a3ab80999e6ac1e30b702717", size = 66391, upload-time = "2025-10-08T09:15:32.265Z" }, + { url = "https://files.pythonhosted.org/packages/6e/f1/abd09c2ae91228c5f3998dbd7f41353def9eac64253de3c8105efa2082f7/msgpack-1.1.2-cp314-cp314-win_amd64.whl", hash = "sha256:9ade919fac6a3e7260b7f64cea89df6bec59104987cbea34d34a2fa15d74310b", size = 73787, upload-time = "2025-10-08T09:15:33.219Z" }, + { url = "https://files.pythonhosted.org/packages/6a/b0/9d9f667ab48b16ad4115c1935d94023b82b3198064cb84a123e97f7466c1/msgpack-1.1.2-cp314-cp314-win_arm64.whl", hash = "sha256:59415c6076b1e30e563eb732e23b994a61c159cec44deaf584e5cc1dd662f2af", size = 66453, upload-time = "2025-10-08T09:15:34.225Z" }, + { url = "https://files.pythonhosted.org/packages/16/67/93f80545eb1792b61a217fa7f06d5e5cb9e0055bed867f43e2b8e012e137/msgpack-1.1.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:897c478140877e5307760b0ea66e0932738879e7aa68144d9b78ea4c8302a84a", size = 85264, upload-time = "2025-10-08T09:15:35.61Z" }, + { url = "https://files.pythonhosted.org/packages/87/1c/33c8a24959cf193966ef11a6f6a2995a65eb066bd681fd085afd519a57ce/msgpack-1.1.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a668204fa43e6d02f89dbe79a30b0d67238d9ec4c5bd8a940fc3a004a47b721b", size = 89076, upload-time = "2025-10-08T09:15:36.619Z" }, + { url = "https://files.pythonhosted.org/packages/fc/6b/62e85ff7193663fbea5c0254ef32f0c77134b4059f8da89b958beb7696f3/msgpack-1.1.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5559d03930d3aa0f3aacb4c42c776af1a2ace2611871c84a75afe436695e6245", size = 435242, upload-time = "2025-10-08T09:15:37.647Z" }, + { url = "https://files.pythonhosted.org/packages/c1/47/5c74ecb4cc277cf09f64e913947871682ffa82b3b93c8dad68083112f412/msgpack-1.1.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:70c5a7a9fea7f036b716191c29047374c10721c389c21e9ffafad04df8c52c90", size = 432509, upload-time = "2025-10-08T09:15:38.794Z" }, + { url = "https://files.pythonhosted.org/packages/24/a4/e98ccdb56dc4e98c929a3f150de1799831c0a800583cde9fa022fa90602d/msgpack-1.1.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:f2cb069d8b981abc72b41aea1c580ce92d57c673ec61af4c500153a626cb9e20", size = 415957, upload-time = "2025-10-08T09:15:40.238Z" }, + { url = "https://files.pythonhosted.org/packages/da/28/6951f7fb67bc0a4e184a6b38ab71a92d9ba58080b27a77d3e2fb0be5998f/msgpack-1.1.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d62ce1f483f355f61adb5433ebfd8868c5f078d1a52d042b0a998682b4fa8c27", size = 422910, upload-time = "2025-10-08T09:15:41.505Z" }, + { url = "https://files.pythonhosted.org/packages/f0/03/42106dcded51f0a0b5284d3ce30a671e7bd3f7318d122b2ead66ad289fed/msgpack-1.1.2-cp314-cp314t-win32.whl", hash = "sha256:1d1418482b1ee984625d88aa9585db570180c286d942da463533b238b98b812b", size = 75197, upload-time = "2025-10-08T09:15:42.954Z" }, + { url = "https://files.pythonhosted.org/packages/15/86/d0071e94987f8db59d4eeb386ddc64d0bb9b10820a8d82bcd3e53eeb2da6/msgpack-1.1.2-cp314-cp314t-win_amd64.whl", hash = "sha256:5a46bf7e831d09470ad92dff02b8b1ac92175ca36b087f904a0519857c6be3ff", size = 85772, upload-time = "2025-10-08T09:15:43.954Z" }, + { url = "https://files.pythonhosted.org/packages/81/f2/08ace4142eb281c12701fc3b93a10795e4d4dc7f753911d836675050f886/msgpack-1.1.2-cp314-cp314t-win_arm64.whl", hash = "sha256:d99ef64f349d5ec3293688e91486c5fdb925ed03807f64d98d205d2713c60b46", size = 70868, upload-time = "2025-10-08T09:15:44.959Z" }, +] + [[package]] name = "mypy" -version = "1.19.0" +version = "1.19.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "librt" }, + { name = "librt", marker = "platform_python_implementation != 'PyPy'" }, { name = "mypy-extensions" }, { name = "pathspec" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f9/b5/b58cdc25fadd424552804bf410855d52324183112aa004f0732c5f6324cf/mypy-1.19.0.tar.gz", hash = "sha256:f6b874ca77f733222641e5c46e4711648c4037ea13646fd0cdc814c2eaec2528", size = 3579025, upload-time = "2025-11-28T15:49:01.26Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/11/7e/1afa8fb188b876abeaa14460dc4983f909aaacaa4bf5718c00b2c7e0b3d5/mypy-1.19.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0fb3115cb8fa7c5f887c8a8d81ccdcb94cff334684980d847e5a62e926910e1d", size = 13207728, upload-time = "2025-11-28T15:46:26.463Z" }, - { url = "https://files.pythonhosted.org/packages/b2/13/f103d04962bcbefb1644f5ccb235998b32c337d6c13145ea390b9da47f3e/mypy-1.19.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f3e19e3b897562276bb331074d64c076dbdd3e79213f36eed4e592272dabd760", size = 12202945, upload-time = "2025-11-28T15:48:49.143Z" }, - { url = "https://files.pythonhosted.org/packages/e4/93/a86a5608f74a22284a8ccea8592f6e270b61f95b8588951110ad797c2ddd/mypy-1.19.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b9d491295825182fba01b6ffe2c6fe4e5a49dbf4e2bb4d1217b6ced3b4797bc6", size = 12718673, upload-time = "2025-11-28T15:47:37.193Z" }, - { url = "https://files.pythonhosted.org/packages/3d/58/cf08fff9ced0423b858f2a7495001fda28dc058136818ee9dffc31534ea9/mypy-1.19.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6016c52ab209919b46169651b362068f632efcd5eb8ef9d1735f6f86da7853b2", size = 13608336, upload-time = "2025-11-28T15:48:32.625Z" }, - { url = "https://files.pythonhosted.org/packages/64/ed/9c509105c5a6d4b73bb08733102a3ea62c25bc02c51bca85e3134bf912d3/mypy-1.19.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f188dcf16483b3e59f9278c4ed939ec0254aa8a60e8fc100648d9ab5ee95a431", size = 13833174, upload-time = "2025-11-28T15:45:48.091Z" }, - { url = "https://files.pythonhosted.org/packages/cd/71/01939b66e35c6f8cb3e6fdf0b657f0fd24de2f8ba5e523625c8e72328208/mypy-1.19.0-cp312-cp312-win_amd64.whl", hash = "sha256:0e3c3d1e1d62e678c339e7ade72746a9e0325de42cd2cccc51616c7b2ed1a018", size = 10112208, upload-time = "2025-11-28T15:46:41.702Z" }, - { url = "https://files.pythonhosted.org/packages/cb/0d/a1357e6bb49e37ce26fcf7e3cc55679ce9f4ebee0cd8b6ee3a0e301a9210/mypy-1.19.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7686ed65dbabd24d20066f3115018d2dce030d8fa9db01aa9f0a59b6813e9f9e", size = 13191993, upload-time = "2025-11-28T15:47:22.336Z" }, - { url = "https://files.pythonhosted.org/packages/5d/75/8e5d492a879ec4490e6ba664b5154e48c46c85b5ac9785792a5ec6a4d58f/mypy-1.19.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fd4a985b2e32f23bead72e2fb4bbe5d6aceee176be471243bd831d5b2644672d", size = 12174411, upload-time = "2025-11-28T15:44:55.492Z" }, - { url = "https://files.pythonhosted.org/packages/71/31/ad5dcee9bfe226e8eaba777e9d9d251c292650130f0450a280aec3485370/mypy-1.19.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fc51a5b864f73a3a182584b1ac75c404396a17eced54341629d8bdcb644a5bba", size = 12727751, upload-time = "2025-11-28T15:44:14.169Z" }, - { url = "https://files.pythonhosted.org/packages/77/06/b6b8994ce07405f6039701f4b66e9d23f499d0b41c6dd46ec28f96d57ec3/mypy-1.19.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:37af5166f9475872034b56c5efdcf65ee25394e9e1d172907b84577120714364", size = 13593323, upload-time = "2025-11-28T15:46:34.699Z" }, - { url = "https://files.pythonhosted.org/packages/68/b1/126e274484cccdf099a8e328d4fda1c7bdb98a5e888fa6010b00e1bbf330/mypy-1.19.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:510c014b722308c9bd377993bcbf9a07d7e0692e5fa8fc70e639c1eb19fc6bee", size = 13818032, upload-time = "2025-11-28T15:46:18.286Z" }, - { url = "https://files.pythonhosted.org/packages/f8/56/53a8f70f562dfc466c766469133a8a4909f6c0012d83993143f2a9d48d2d/mypy-1.19.0-cp313-cp313-win_amd64.whl", hash = "sha256:cabbee74f29aa9cd3b444ec2f1e4fa5a9d0d746ce7567a6a609e224429781f53", size = 10120644, upload-time = "2025-11-28T15:47:43.99Z" }, - { url = "https://files.pythonhosted.org/packages/b0/f4/7751f32f56916f7f8c229fe902cbdba3e4dd3f3ea9e8b872be97e7fc546d/mypy-1.19.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:f2e36bed3c6d9b5f35d28b63ca4b727cb0228e480826ffc8953d1892ddc8999d", size = 13185236, upload-time = "2025-11-28T15:45:20.696Z" }, - { url = "https://files.pythonhosted.org/packages/35/31/871a9531f09e78e8d145032355890384f8a5b38c95a2c7732d226b93242e/mypy-1.19.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a18d8abdda14035c5718acb748faec09571432811af129bf0d9e7b2d6699bf18", size = 12213902, upload-time = "2025-11-28T15:46:10.117Z" }, - { url = "https://files.pythonhosted.org/packages/58/b8/af221910dd40eeefa2077a59107e611550167b9994693fc5926a0b0f87c0/mypy-1.19.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f75e60aca3723a23511948539b0d7ed514dda194bc3755eae0bfc7a6b4887aa7", size = 12738600, upload-time = "2025-11-28T15:44:22.521Z" }, - { url = "https://files.pythonhosted.org/packages/11/9f/c39e89a3e319c1d9c734dedec1183b2cc3aefbab066ec611619002abb932/mypy-1.19.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8f44f2ae3c58421ee05fe609160343c25f70e3967f6e32792b5a78006a9d850f", size = 13592639, upload-time = "2025-11-28T15:48:08.55Z" }, - { url = "https://files.pythonhosted.org/packages/97/6d/ffaf5f01f5e284d9033de1267e6c1b8f3783f2cf784465378a86122e884b/mypy-1.19.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:63ea6a00e4bd6822adbfc75b02ab3653a17c02c4347f5bb0cf1d5b9df3a05835", size = 13799132, upload-time = "2025-11-28T15:47:06.032Z" }, - { url = "https://files.pythonhosted.org/packages/fe/b0/c33921e73aaa0106224e5a34822411bea38046188eb781637f5a5b07e269/mypy-1.19.0-cp314-cp314-win_amd64.whl", hash = "sha256:3ad925b14a0bb99821ff6f734553294aa6a3440a8cb082fe1f5b84dfb662afb1", size = 10269832, upload-time = "2025-11-28T15:47:29.392Z" }, - { url = "https://files.pythonhosted.org/packages/09/0e/fe228ed5aeab470c6f4eb82481837fadb642a5aa95cc8215fd2214822c10/mypy-1.19.0-py3-none-any.whl", hash = "sha256:0c01c99d626380752e527d5ce8e69ffbba2046eb8a060db0329690849cf9b6f9", size = 2469714, upload-time = "2025-11-28T15:45:33.22Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/f5/db/4efed9504bc01309ab9c2da7e352cc223569f05478012b5d9ece38fd44d2/mypy-1.19.1.tar.gz", hash = "sha256:19d88bb05303fe63f71dd2c6270daca27cb9401c4ca8255fe50d1d920e0eb9ba", size = 3582404, upload-time = "2025-12-15T05:03:48.42Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/06/8a/19bfae96f6615aa8a0604915512e0289b1fad33d5909bf7244f02935d33a/mypy-1.19.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a8174a03289288c1f6c46d55cef02379b478bfbc8e358e02047487cad44c6ca1", size = 13206053, upload-time = "2025-12-15T05:03:46.622Z" }, + { url = "https://files.pythonhosted.org/packages/a5/34/3e63879ab041602154ba2a9f99817bb0c85c4df19a23a1443c8986e4d565/mypy-1.19.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffcebe56eb09ff0c0885e750036a095e23793ba6c2e894e7e63f6d89ad51f22e", size = 12219134, upload-time = "2025-12-15T05:03:24.367Z" }, + { url = "https://files.pythonhosted.org/packages/89/cc/2db6f0e95366b630364e09845672dbee0cbf0bbe753a204b29a944967cd9/mypy-1.19.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b64d987153888790bcdb03a6473d321820597ab8dd9243b27a92153c4fa50fd2", size = 12731616, upload-time = "2025-12-15T05:02:44.725Z" }, + { url = "https://files.pythonhosted.org/packages/00/be/dd56c1fd4807bc1eba1cf18b2a850d0de7bacb55e158755eb79f77c41f8e/mypy-1.19.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c35d298c2c4bba75feb2195655dfea8124d855dfd7343bf8b8c055421eaf0cf8", size = 13620847, upload-time = "2025-12-15T05:03:39.633Z" }, + { url = "https://files.pythonhosted.org/packages/6d/42/332951aae42b79329f743bf1da088cd75d8d4d9acc18fbcbd84f26c1af4e/mypy-1.19.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:34c81968774648ab5ac09c29a375fdede03ba253f8f8287847bd480782f73a6a", size = 13834976, upload-time = "2025-12-15T05:03:08.786Z" }, + { url = "https://files.pythonhosted.org/packages/6f/63/e7493e5f90e1e085c562bb06e2eb32cae27c5057b9653348d38b47daaecc/mypy-1.19.1-cp312-cp312-win_amd64.whl", hash = "sha256:b10e7c2cd7870ba4ad9b2d8a6102eb5ffc1f16ca35e3de6bfa390c1113029d13", size = 10118104, upload-time = "2025-12-15T05:03:10.834Z" }, + { url = "https://files.pythonhosted.org/packages/de/9f/a6abae693f7a0c697dbb435aac52e958dc8da44e92e08ba88d2e42326176/mypy-1.19.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e3157c7594ff2ef1634ee058aafc56a82db665c9438fd41b390f3bde1ab12250", size = 13201927, upload-time = "2025-12-15T05:02:29.138Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a4/45c35ccf6e1c65afc23a069f50e2c66f46bd3798cbe0d680c12d12935caa/mypy-1.19.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdb12f69bcc02700c2b47e070238f42cb87f18c0bc1fc4cdb4fb2bc5fd7a3b8b", size = 12206730, upload-time = "2025-12-15T05:03:01.325Z" }, + { url = "https://files.pythonhosted.org/packages/05/bb/cdcf89678e26b187650512620eec8368fded4cfd99cfcb431e4cdfd19dec/mypy-1.19.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f859fb09d9583a985be9a493d5cfc5515b56b08f7447759a0c5deaf68d80506e", size = 12724581, upload-time = "2025-12-15T05:03:20.087Z" }, + { url = "https://files.pythonhosted.org/packages/d1/32/dd260d52babf67bad8e6770f8e1102021877ce0edea106e72df5626bb0ec/mypy-1.19.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c9a6538e0415310aad77cb94004ca6482330fece18036b5f360b62c45814c4ef", size = 13616252, upload-time = "2025-12-15T05:02:49.036Z" }, + { url = "https://files.pythonhosted.org/packages/71/d0/5e60a9d2e3bd48432ae2b454b7ef2b62a960ab51292b1eda2a95edd78198/mypy-1.19.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:da4869fc5e7f62a88f3fe0b5c919d1d9f7ea3cef92d3689de2823fd27e40aa75", size = 13840848, upload-time = "2025-12-15T05:02:55.95Z" }, + { url = "https://files.pythonhosted.org/packages/98/76/d32051fa65ecf6cc8c6610956473abdc9b4c43301107476ac03559507843/mypy-1.19.1-cp313-cp313-win_amd64.whl", hash = "sha256:016f2246209095e8eda7538944daa1d60e1e8134d98983b9fc1e92c1fc0cb8dd", size = 10135510, upload-time = "2025-12-15T05:02:58.438Z" }, + { url = "https://files.pythonhosted.org/packages/de/eb/b83e75f4c820c4247a58580ef86fcd35165028f191e7e1ba57128c52782d/mypy-1.19.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:06e6170bd5836770e8104c8fdd58e5e725cfeb309f0a6c681a811f557e97eac1", size = 13199744, upload-time = "2025-12-15T05:03:30.823Z" }, + { url = "https://files.pythonhosted.org/packages/94/28/52785ab7bfa165f87fcbb61547a93f98bb20e7f82f90f165a1f69bce7b3d/mypy-1.19.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:804bd67b8054a85447c8954215a906d6eff9cabeabe493fb6334b24f4bfff718", size = 12215815, upload-time = "2025-12-15T05:02:42.323Z" }, + { url = "https://files.pythonhosted.org/packages/0a/c6/bdd60774a0dbfb05122e3e925f2e9e846c009e479dcec4821dad881f5b52/mypy-1.19.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21761006a7f497cb0d4de3d8ef4ca70532256688b0523eee02baf9eec895e27b", size = 12740047, upload-time = "2025-12-15T05:03:33.168Z" }, + { url = "https://files.pythonhosted.org/packages/32/2a/66ba933fe6c76bd40d1fe916a83f04fed253152f451a877520b3c4a5e41e/mypy-1.19.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:28902ee51f12e0f19e1e16fbe2f8f06b6637f482c459dd393efddd0ec7f82045", size = 13601998, upload-time = "2025-12-15T05:03:13.056Z" }, + { url = "https://files.pythonhosted.org/packages/e3/da/5055c63e377c5c2418760411fd6a63ee2b96cf95397259038756c042574f/mypy-1.19.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:481daf36a4c443332e2ae9c137dfee878fcea781a2e3f895d54bd3002a900957", size = 13807476, upload-time = "2025-12-15T05:03:17.977Z" }, + { url = "https://files.pythonhosted.org/packages/cd/09/4ebd873390a063176f06b0dbf1f7783dd87bd120eae7727fa4ae4179b685/mypy-1.19.1-cp314-cp314-win_amd64.whl", hash = "sha256:8bb5c6f6d043655e055be9b542aa5f3bdd30e4f3589163e85f93f3640060509f", size = 10281872, upload-time = "2025-12-15T05:03:05.549Z" }, + { url = "https://files.pythonhosted.org/packages/8d/f4/4ce9a05ce5ded1de3ec1c1d96cf9f9504a04e54ce0ed55cfa38619a32b8d/mypy-1.19.1-py3-none-any.whl", hash = "sha256:f1235f5ea01b7db5468d53ece6aaddf1ad0b88d9e7462b86ef96fe04995d7247", size = 2471239, upload-time = "2025-12-15T05:03:07.248Z" }, ] [[package]] @@ -451,38 +858,38 @@ wheels = [ [[package]] name = "nodeenv" -version = "1.9.1" +version = "1.10.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437, upload-time = "2024-06-04T18:44:11.171Z" } +sdist = { url = "https://files.pythonhosted.org/packages/24/bf/d1bda4f6168e0b2e9e5958945e01910052158313224ada5ce1fb2e1113b8/nodeenv-1.10.0.tar.gz", hash = "sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb", size = 55611, upload-time = "2025-12-20T14:08:54.006Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314, upload-time = "2024-06-04T18:44:08.352Z" }, + { url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" }, ] [[package]] name = "packaging" -version = "25.0" +version = "26.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } +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/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, + { 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 = "pathspec" -version = "0.12.1" +version = "1.0.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043, upload-time = "2023-12-10T22:30:45Z" } +sdist = { url = "https://files.pythonhosted.org/packages/fa/36/e27608899f9b8d4dff0617b2d9ab17ca5608956ca44461ac14ac48b44015/pathspec-1.0.4.tar.gz", hash = "sha256:0210e2ae8a21a9137c0d470578cb0e595af87edaa6ebf12ff176f14a02e0e645", size = 131200, upload-time = "2026-01-27T03:59:46.938Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191, upload-time = "2023-12-10T22:30:43.14Z" }, + { url = "https://files.pythonhosted.org/packages/ef/3c/2c197d226f9ea224a9ab8d197933f9da0ae0aac5b6e0f884e2b8d9c8e9f7/pathspec-1.0.4-py3-none-any.whl", hash = "sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723", size = 55206, upload-time = "2026-01-27T03:59:45.137Z" }, ] [[package]] name = "platformdirs" -version = "4.5.1" +version = "4.9.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/cf/86/0248f086a84f01b37aaec0fa567b397df1a119f73c16f6c7a9aac73ea309/platformdirs-4.5.1.tar.gz", hash = "sha256:61d5cdcc6065745cdd94f0f878977f8de9437be93de97c1c12f853c9c0cdcbda", size = 21715, upload-time = "2025-12-05T13:52:58.638Z" } +sdist = { url = "https://files.pythonhosted.org/packages/19/56/8d4c30c8a1d07013911a8fdbd8f89440ef9f08d07a1b50ab8ca8be5a20f9/platformdirs-4.9.4.tar.gz", hash = "sha256:1ec356301b7dc906d83f371c8f487070e99d3ccf9e501686456394622a01a934", size = 28737, upload-time = "2026-03-05T18:34:13.271Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/cb/28/3bfe2fa5a7b9c46fe7e13c97bda14c895fb10fa2ebf1d0abb90e0cea7ee1/platformdirs-4.5.1-py3-none-any.whl", hash = "sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31", size = 18731, upload-time = "2025-12-05T13:52:56.823Z" }, + { url = "https://files.pythonhosted.org/packages/63/d7/97f7e3a6abb67d8080dd406fd4df842c2be0efaf712d1c899c32a075027c/platformdirs-4.9.4-py3-none-any.whl", hash = "sha256:68a9a4619a666ea6439f2ff250c12a853cd1cbd5158d258bd824a7df6be2f868", size = 21216, upload-time = "2026-03-05T18:34:12.172Z" }, ] [[package]] @@ -496,7 +903,7 @@ wheels = [ [[package]] name = "pre-commit" -version = "4.5.0" +version = "4.5.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cfgv" }, @@ -505,9 +912,21 @@ dependencies = [ { name = "pyyaml" }, { name = "virtualenv" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f4/9b/6a4ffb4ed980519da959e1cf3122fc6cb41211daa58dbae1c73c0e519a37/pre_commit-4.5.0.tar.gz", hash = "sha256:dc5a065e932b19fc1d4c653c6939068fe54325af8e741e74e88db4d28a4dd66b", size = 198428, upload-time = "2025-11-22T21:02:42.304Z" } +sdist = { url = "https://files.pythonhosted.org/packages/40/f1/6d86a29246dfd2e9b6237f0b5823717f60cad94d47ddc26afa916d21f525/pre_commit-4.5.1.tar.gz", hash = "sha256:eb545fcff725875197837263e977ea257a402056661f09dae08e4b149b030a61", size = 198232, upload-time = "2025-12-16T21:14:33.552Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/19/fd3ef348460c80af7bb4669ea7926651d1f95c23ff2df18b9d24bab4f3fa/pre_commit-4.5.1-py2.py3-none-any.whl", hash = "sha256:3b3afd891e97337708c1674210f8eba659b52a38ea5f822ff142d10786221f77", size = 226437, upload-time = "2025-12-16T21:14:32.409Z" }, +] + +[[package]] +name = "prompt-toolkit" +version = "3.0.51" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "wcwidth" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bb/6e/9d084c929dfe9e3bfe0c6a47e31f78a25c54627d64a66e884a8bf5474f1c/prompt_toolkit-3.0.51.tar.gz", hash = "sha256:931a162e3b27fc90c86f1b48bb1fb2c528c2761475e57c9c06de13311c7b54ed", size = 428940, upload-time = "2025-04-15T09:18:47.731Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5d/c4/b2d28e9d2edf4f1713eb3c29307f1a63f3d67cf09bdda29715a36a68921a/pre_commit-4.5.0-py2.py3-none-any.whl", hash = "sha256:25e2ce09595174d9c97860a95609f9f852c0614ba602de3561e267547f2335e1", size = 226429, upload-time = "2025-11-22T21:02:40.836Z" }, + { url = "https://files.pythonhosted.org/packages/ce/4f/5249960887b1fbe561d9ff265496d170b55a735b76724f10ef19f9e40716/prompt_toolkit-3.0.51-py3-none-any.whl", hash = "sha256:52742911fde84e2d423e2f9a4cf1de7d7ac4e51958f648d9540e0fb8db077b07", size = 387810, upload-time = "2025-04-15T09:18:44.753Z" }, ] [[package]] @@ -621,6 +1040,19 @@ 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" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +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" @@ -648,13 +1080,46 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl", hash = "sha256:202ca578cfeb7370784a8c33d6d05bc6e13b4f25b5053c30a152269fd10f0b88", size = 46396, upload-time = "2025-07-01T13:30:56.632Z" }, ] +[[package]] +name = "python-discovery" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock" }, + { name = "platformdirs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9c/90/bcce6b46823c9bec1757c964dc37ed332579be512e17a30e9698095dcae4/python_discovery-1.2.0.tar.gz", hash = "sha256:7d33e350704818b09e3da2bd419d37e21e7c30db6e0977bb438916e06b41b5b1", size = 58055, upload-time = "2026-03-19T01:43:08.248Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/3c/2005227cb951df502412de2fa781f800663cccbef8d90ec6f1b371ac2c0d/python_discovery-1.2.0-py3-none-any.whl", hash = "sha256:1e108f1bbe2ed0ef089823d28805d5ad32be8e734b86a5f212bf89b71c266e4a", size = 31524, upload-time = "2026-03-19T01:43:07.045Z" }, +] + [[package]] name = "pytokens" -version = "0.3.0" +version = "0.4.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/4e/8d/a762be14dae1c3bf280202ba3172020b2b0b4c537f94427435f19c413b72/pytokens-0.3.0.tar.gz", hash = "sha256:2f932b14ed08de5fcf0b391ace2642f858f1394c0857202959000b68ed7a458a", size = 17644, upload-time = "2025-11-05T13:36:35.34Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b6/34/b4e015b99031667a7b960f888889c5bd34ef585c85e1cb56a594b92836ac/pytokens-0.4.1.tar.gz", hash = "sha256:292052fe80923aae2260c073f822ceba21f3872ced9a68bb7953b348e561179a", size = 23015, upload-time = "2026-01-30T01:03:45.924Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/84/25/d9db8be44e205a124f6c98bc0324b2bb149b7431c53877fc6d1038dddaf5/pytokens-0.3.0-py3-none-any.whl", hash = "sha256:95b2b5eaf832e469d141a378872480ede3f251a5a5041b8ec6e581d3ac71bbf3", size = 12195, upload-time = "2025-11-05T13:36:33.183Z" }, + { url = "https://files.pythonhosted.org/packages/41/5d/e44573011401fb82e9d51e97f1290ceb377800fb4eed650b96f4753b499c/pytokens-0.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:140709331e846b728475786df8aeb27d24f48cbcf7bcd449f8de75cae7a45083", size = 160663, upload-time = "2026-01-30T01:03:06.473Z" }, + { url = "https://files.pythonhosted.org/packages/f0/e6/5bbc3019f8e6f21d09c41f8b8654536117e5e211a85d89212d59cbdab381/pytokens-0.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d6c4268598f762bc8e91f5dbf2ab2f61f7b95bdc07953b602db879b3c8c18e1", size = 255626, upload-time = "2026-01-30T01:03:08.177Z" }, + { url = "https://files.pythonhosted.org/packages/bf/3c/2d5297d82286f6f3d92770289fd439956b201c0a4fc7e72efb9b2293758e/pytokens-0.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:24afde1f53d95348b5a0eb19488661147285ca4dd7ed752bbc3e1c6242a304d1", size = 269779, upload-time = "2026-01-30T01:03:09.756Z" }, + { url = "https://files.pythonhosted.org/packages/20/01/7436e9ad693cebda0551203e0bf28f7669976c60ad07d6402098208476de/pytokens-0.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5ad948d085ed6c16413eb5fec6b3e02fa00dc29a2534f088d3302c47eb59adf9", size = 268076, upload-time = "2026-01-30T01:03:10.957Z" }, + { url = "https://files.pythonhosted.org/packages/2e/df/533c82a3c752ba13ae7ef238b7f8cdd272cf1475f03c63ac6cf3fcfb00b6/pytokens-0.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:3f901fe783e06e48e8cbdc82d631fca8f118333798193e026a50ce1b3757ea68", size = 103552, upload-time = "2026-01-30T01:03:12.066Z" }, + { url = "https://files.pythonhosted.org/packages/cb/dc/08b1a080372afda3cceb4f3c0a7ba2bde9d6a5241f1edb02a22a019ee147/pytokens-0.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8bdb9d0ce90cbf99c525e75a2fa415144fd570a1ba987380190e8b786bc6ef9b", size = 160720, upload-time = "2026-01-30T01:03:13.843Z" }, + { url = "https://files.pythonhosted.org/packages/64/0c/41ea22205da480837a700e395507e6a24425151dfb7ead73343d6e2d7ffe/pytokens-0.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5502408cab1cb18e128570f8d598981c68a50d0cbd7c61312a90507cd3a1276f", size = 254204, upload-time = "2026-01-30T01:03:14.886Z" }, + { url = "https://files.pythonhosted.org/packages/e0/d2/afe5c7f8607018beb99971489dbb846508f1b8f351fcefc225fcf4b2adc0/pytokens-0.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:29d1d8fb1030af4d231789959f21821ab6325e463f0503a61d204343c9b355d1", size = 268423, upload-time = "2026-01-30T01:03:15.936Z" }, + { url = "https://files.pythonhosted.org/packages/68/d4/00ffdbd370410c04e9591da9220a68dc1693ef7499173eb3e30d06e05ed1/pytokens-0.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:970b08dd6b86058b6dc07efe9e98414f5102974716232d10f32ff39701e841c4", size = 266859, upload-time = "2026-01-30T01:03:17.458Z" }, + { url = "https://files.pythonhosted.org/packages/a7/c9/c3161313b4ca0c601eeefabd3d3b576edaa9afdefd32da97210700e47652/pytokens-0.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:9bd7d7f544d362576be74f9d5901a22f317efc20046efe2034dced238cbbfe78", size = 103520, upload-time = "2026-01-30T01:03:18.652Z" }, + { url = "https://files.pythonhosted.org/packages/8f/a7/b470f672e6fc5fee0a01d9e75005a0e617e162381974213a945fcd274843/pytokens-0.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4a14d5f5fc78ce85e426aa159489e2d5961acf0e47575e08f35584009178e321", size = 160821, upload-time = "2026-01-30T01:03:19.684Z" }, + { url = "https://files.pythonhosted.org/packages/80/98/e83a36fe8d170c911f864bfded690d2542bfcfacb9c649d11a9e6eb9dc41/pytokens-0.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97f50fd18543be72da51dd505e2ed20d2228c74e0464e4262e4899797803d7fa", size = 254263, upload-time = "2026-01-30T01:03:20.834Z" }, + { url = "https://files.pythonhosted.org/packages/0f/95/70d7041273890f9f97a24234c00b746e8da86df462620194cef1d411ddeb/pytokens-0.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dc74c035f9bfca0255c1af77ddd2d6ae8419012805453e4b0e7513e17904545d", size = 268071, upload-time = "2026-01-30T01:03:21.888Z" }, + { url = "https://files.pythonhosted.org/packages/da/79/76e6d09ae19c99404656d7db9c35dfd20f2086f3eb6ecb496b5b31163bad/pytokens-0.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f66a6bbe741bd431f6d741e617e0f39ec7257ca1f89089593479347cc4d13324", size = 271716, upload-time = "2026-01-30T01:03:23.633Z" }, + { url = "https://files.pythonhosted.org/packages/79/37/482e55fa1602e0a7ff012661d8c946bafdc05e480ea5a32f4f7e336d4aa9/pytokens-0.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:b35d7e5ad269804f6697727702da3c517bb8a5228afa450ab0fa787732055fc9", size = 104539, upload-time = "2026-01-30T01:03:24.788Z" }, + { url = "https://files.pythonhosted.org/packages/30/e8/20e7db907c23f3d63b0be3b8a4fd1927f6da2395f5bcc7f72242bb963dfe/pytokens-0.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:8fcb9ba3709ff77e77f1c7022ff11d13553f3c30299a9fe246a166903e9091eb", size = 168474, upload-time = "2026-01-30T01:03:26.428Z" }, + { url = "https://files.pythonhosted.org/packages/d6/81/88a95ee9fafdd8f5f3452107748fd04c24930d500b9aba9738f3ade642cc/pytokens-0.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:79fc6b8699564e1f9b521582c35435f1bd32dd06822322ec44afdeba666d8cb3", size = 290473, upload-time = "2026-01-30T01:03:27.415Z" }, + { url = "https://files.pythonhosted.org/packages/cf/35/3aa899645e29b6375b4aed9f8d21df219e7c958c4c186b465e42ee0a06bf/pytokens-0.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d31b97b3de0f61571a124a00ffe9a81fb9939146c122c11060725bd5aea79975", size = 303485, upload-time = "2026-01-30T01:03:28.558Z" }, + { url = "https://files.pythonhosted.org/packages/52/a0/07907b6ff512674d9b201859f7d212298c44933633c946703a20c25e9d81/pytokens-0.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:967cf6e3fd4adf7de8fc73cd3043754ae79c36475c1c11d514fc72cf5490094a", size = 306698, upload-time = "2026-01-30T01:03:29.653Z" }, + { url = "https://files.pythonhosted.org/packages/39/2a/cbbf9250020a4a8dd53ba83a46c097b69e5eb49dd14e708f496f548c6612/pytokens-0.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:584c80c24b078eec1e227079d56dc22ff755e0ba8654d8383b2c549107528918", size = 116287, upload-time = "2026-01-30T01:03:30.912Z" }, + { url = "https://files.pythonhosted.org/packages/c6/78/397db326746f0a342855b81216ae1f0a32965deccfd7c830a2dbc66d2483/pytokens-0.4.1-py3-none-any.whl", hash = "sha256:26cef14744a8385f35d0e095dc8b3a7583f6c953c2e3d269c7f82484bf5ad2de", size = 13729, upload-time = "2026-01-30T01:03:45.029Z" }, ] [[package]] @@ -703,6 +1168,18 @@ wheels = [ { 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 = "questionary" +version = "2.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "prompt-toolkit" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f6/45/eafb0bba0f9988f6a2520f9ca2df2c82ddfa8d67c95d6625452e97b204a5/questionary-2.1.1.tar.gz", hash = "sha256:3d7e980292bb0107abaa79c68dd3eee3c561b83a0f89ae482860b181c8bd412d", size = 25845, upload-time = "2025-08-28T19:00:20.851Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/26/1062c7ec1b053db9e499b4d2d5bc231743201b74051c973dadeac80a8f43/questionary-2.1.1-py3-none-any.whl", hash = "sha256:a51af13f345f1cdea62347589fbb6df3b290306ab8930713bfae4d475a7d4a59", size = 36753, upload-time = "2025-08-28T19:00:19.56Z" }, +] + [[package]] name = "redis" version = "7.3.0" @@ -714,15 +1191,15 @@ wheels = [ [[package]] name = "rich" -version = "14.2.0" +version = "14.3.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markdown-it-py" }, { name = "pygments" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/fb/d2/8920e102050a0de7bfabeb4c4614a49248cf8d5d7a8d01885fbb24dc767a/rich-14.2.0.tar.gz", hash = "sha256:73ff50c7c0c1c77c8243079283f4edb376f0f6442433aecb8ce7e6d0b92d1fe4", size = 219990, upload-time = "2025-10-09T14:16:53.064Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b3/c6/f3b320c27991c46f43ee9d856302c70dc2d0fb2dba4842ff739d5f46b393/rich-14.3.3.tar.gz", hash = "sha256:b8daa0b9e4eef54dd8cf7c86c03713f53241884e814f4e2f5fb342fe520f639b", size = 230582, upload-time = "2026-02-19T17:23:12.474Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/25/7a/b0178788f8dc6cafce37a212c99565fa1fe7872c70c6c9c1e1a372d9d88f/rich-14.2.0-py3-none-any.whl", hash = "sha256:76bc51fe2e57d2b1be1f96c524b890b816e334ab4c1e45888799bfaab0021edd", size = 243393, upload-time = "2025-10-09T14:16:51.245Z" }, + { url = "https://files.pythonhosted.org/packages/14/25/b208c5683343959b670dc001595f2f3737e051da617f66c31f7c4fa93abc/rich-14.3.3-py3-none-any.whl", hash = "sha256:793431c1f8619afa7d3b52b2cdec859562b950ea0d4b6b505397612db8d5362d", size = 310458, upload-time = "2026-02-19T17:23:13.732Z" }, ] [[package]] @@ -759,28 +1236,36 @@ provides-extras = ["dev"] [[package]] name = "ruff" -version = "0.14.9" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f6/1b/ab712a9d5044435be8e9a2beb17cbfa4c241aa9b5e4413febac2a8b79ef2/ruff-0.14.9.tar.gz", hash = "sha256:35f85b25dd586381c0cc053f48826109384c81c00ad7ef1bd977bfcc28119d5b", size = 5809165, upload-time = "2025-12-11T21:39:47.381Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b8/1c/d1b1bba22cffec02351c78ab9ed4f7d7391876e12720298448b29b7229c1/ruff-0.14.9-py3-none-linux_armv6l.whl", hash = "sha256:f1ec5de1ce150ca6e43691f4a9ef5c04574ad9ca35c8b3b0e18877314aba7e75", size = 13576541, upload-time = "2025-12-11T21:39:14.806Z" }, - { url = "https://files.pythonhosted.org/packages/94/ab/ffe580e6ea1fca67f6337b0af59fc7e683344a43642d2d55d251ff83ceae/ruff-0.14.9-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ed9d7417a299fc6030b4f26333bf1117ed82a61ea91238558c0268c14e00d0c2", size = 13779363, upload-time = "2025-12-11T21:39:20.29Z" }, - { url = "https://files.pythonhosted.org/packages/7d/f8/2be49047f929d6965401855461e697ab185e1a6a683d914c5c19c7962d9e/ruff-0.14.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d5dc3473c3f0e4a1008d0ef1d75cee24a48e254c8bed3a7afdd2b4392657ed2c", size = 12925292, upload-time = "2025-12-11T21:39:38.757Z" }, - { url = "https://files.pythonhosted.org/packages/9e/e9/08840ff5127916bb989c86f18924fd568938b06f58b60e206176f327c0fe/ruff-0.14.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:84bf7c698fc8f3cb8278830fb6b5a47f9bcc1ed8cb4f689b9dd02698fa840697", size = 13362894, upload-time = "2025-12-11T21:39:02.524Z" }, - { url = "https://files.pythonhosted.org/packages/31/1c/5b4e8e7750613ef43390bb58658eaf1d862c0cc3352d139cd718a2cea164/ruff-0.14.9-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:aa733093d1f9d88a5d98988d8834ef5d6f9828d03743bf5e338bf980a19fce27", size = 13311482, upload-time = "2025-12-11T21:39:17.51Z" }, - { url = "https://files.pythonhosted.org/packages/5b/3a/459dce7a8cb35ba1ea3e9c88f19077667a7977234f3b5ab197fad240b404/ruff-0.14.9-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6a1cfb04eda979b20c8c19550c8b5f498df64ff8da151283311ce3199e8b3648", size = 14016100, upload-time = "2025-12-11T21:39:41.948Z" }, - { url = "https://files.pythonhosted.org/packages/a6/31/f064f4ec32524f9956a0890fc6a944e5cf06c63c554e39957d208c0ffc45/ruff-0.14.9-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:1e5cb521e5ccf0008bd74d5595a4580313844a42b9103b7388eca5a12c970743", size = 15477729, upload-time = "2025-12-11T21:39:23.279Z" }, - { url = "https://files.pythonhosted.org/packages/7a/6d/f364252aad36ccd443494bc5f02e41bf677f964b58902a17c0b16c53d890/ruff-0.14.9-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cd429a8926be6bba4befa8cdcf3f4dd2591c413ea5066b1e99155ed245ae42bb", size = 15122386, upload-time = "2025-12-11T21:39:33.125Z" }, - { url = "https://files.pythonhosted.org/packages/20/02/e848787912d16209aba2799a4d5a1775660b6a3d0ab3944a4ccc13e64a02/ruff-0.14.9-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ab208c1b7a492e37caeaf290b1378148f75e13c2225af5d44628b95fd7834273", size = 14497124, upload-time = "2025-12-11T21:38:59.33Z" }, - { url = "https://files.pythonhosted.org/packages/f3/51/0489a6a5595b7760b5dbac0dd82852b510326e7d88d51dbffcd2e07e3ff3/ruff-0.14.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:72034534e5b11e8a593f517b2f2f2b273eb68a30978c6a2d40473ad0aaa4cb4a", size = 14195343, upload-time = "2025-12-11T21:39:44.866Z" }, - { url = "https://files.pythonhosted.org/packages/f6/53/3bb8d2fa73e4c2f80acc65213ee0830fa0c49c6479313f7a68a00f39e208/ruff-0.14.9-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:712ff04f44663f1b90a1195f51525836e3413c8a773574a7b7775554269c30ed", size = 14346425, upload-time = "2025-12-11T21:39:05.927Z" }, - { url = "https://files.pythonhosted.org/packages/ad/04/bdb1d0ab876372da3e983896481760867fc84f969c5c09d428e8f01b557f/ruff-0.14.9-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:a111fee1db6f1d5d5810245295527cda1d367c5aa8f42e0fca9a78ede9b4498b", size = 13258768, upload-time = "2025-12-11T21:39:08.691Z" }, - { url = "https://files.pythonhosted.org/packages/40/d9/8bf8e1e41a311afd2abc8ad12be1b6c6c8b925506d9069b67bb5e9a04af3/ruff-0.14.9-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8769efc71558fecc25eb295ddec7d1030d41a51e9dcf127cbd63ec517f22d567", size = 13326939, upload-time = "2025-12-11T21:39:53.842Z" }, - { url = "https://files.pythonhosted.org/packages/f4/56/a213fa9edb6dd849f1cfbc236206ead10913693c72a67fb7ddc1833bf95d/ruff-0.14.9-py3-none-musllinux_1_2_i686.whl", hash = "sha256:347e3bf16197e8a2de17940cd75fd6491e25c0aa7edf7d61aa03f146a1aa885a", size = 13578888, upload-time = "2025-12-11T21:39:35.988Z" }, - { url = "https://files.pythonhosted.org/packages/33/09/6a4a67ffa4abae6bf44c972a4521337ffce9cbc7808faadede754ef7a79c/ruff-0.14.9-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:7715d14e5bccf5b660f54516558aa94781d3eb0838f8e706fb60e3ff6eff03a8", size = 14314473, upload-time = "2025-12-11T21:39:50.78Z" }, - { url = "https://files.pythonhosted.org/packages/12/0d/15cc82da5d83f27a3c6b04f3a232d61bc8c50d38a6cd8da79228e5f8b8d6/ruff-0.14.9-py3-none-win32.whl", hash = "sha256:df0937f30aaabe83da172adaf8937003ff28172f59ca9f17883b4213783df197", size = 13202651, upload-time = "2025-12-11T21:39:26.628Z" }, - { url = "https://files.pythonhosted.org/packages/32/f7/c78b060388eefe0304d9d42e68fab8cffd049128ec466456cef9b8d4f06f/ruff-0.14.9-py3-none-win_amd64.whl", hash = "sha256:c0b53a10e61df15a42ed711ec0bda0c582039cf6c754c49c020084c55b5b0bc2", size = 14702079, upload-time = "2025-12-11T21:39:11.954Z" }, - { url = "https://files.pythonhosted.org/packages/26/09/7a9520315decd2334afa65ed258fed438f070e31f05a2e43dd480a5e5911/ruff-0.14.9-py3-none-win_arm64.whl", hash = "sha256:8e821c366517a074046d92f0e9213ed1c13dbc5b37a7fc20b07f79b64d62cc84", size = 13744730, upload-time = "2025-12-11T21:39:29.659Z" }, +version = "0.15.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/22/9e4f66ee588588dc6c9af6a994e12d26e19efbe874d1a909d09a6dac7a59/ruff-0.15.7.tar.gz", hash = "sha256:04f1ae61fc20fe0b148617c324d9d009b5f63412c0b16474f3d5f1a1a665f7ac", size = 4601277, upload-time = "2026-03-19T16:26:22.605Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/2f/0b08ced94412af091807b6119ca03755d651d3d93a242682bf020189db94/ruff-0.15.7-py3-none-linux_armv6l.whl", hash = "sha256:a81cc5b6910fb7dfc7c32d20652e50fa05963f6e13ead3c5915c41ac5d16668e", size = 10489037, upload-time = "2026-03-19T16:26:32.47Z" }, + { url = "https://files.pythonhosted.org/packages/91/4a/82e0fa632e5c8b1eba5ee86ecd929e8ff327bbdbfb3c6ac5d81631bef605/ruff-0.15.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:722d165bd52403f3bdabc0ce9e41fc47070ac56d7a91b4e0d097b516a53a3477", size = 10955433, upload-time = "2026-03-19T16:27:00.205Z" }, + { url = "https://files.pythonhosted.org/packages/ab/10/12586735d0ff42526ad78c049bf51d7428618c8b5c467e72508c694119df/ruff-0.15.7-py3-none-macosx_11_0_arm64.whl", hash = "sha256:7fbc2448094262552146cbe1b9643a92f66559d3761f1ad0656d4991491af49e", size = 10269302, upload-time = "2026-03-19T16:26:26.183Z" }, + { url = "https://files.pythonhosted.org/packages/eb/5d/32b5c44ccf149a26623671df49cbfbd0a0ae511ff3df9d9d2426966a8d57/ruff-0.15.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b39329b60eba44156d138275323cc726bbfbddcec3063da57caa8a8b1d50adf", size = 10607625, upload-time = "2026-03-19T16:27:03.263Z" }, + { url = "https://files.pythonhosted.org/packages/5d/f1/f0001cabe86173aaacb6eb9bb734aa0605f9a6aa6fa7d43cb49cbc4af9c9/ruff-0.15.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:87768c151808505f2bfc93ae44e5f9e7c8518943e5074f76ac21558ef5627c85", size = 10324743, upload-time = "2026-03-19T16:27:09.791Z" }, + { url = "https://files.pythonhosted.org/packages/7a/87/b8a8f3d56b8d848008559e7c9d8bf367934d5367f6d932ba779456e2f73b/ruff-0.15.7-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fb0511670002c6c529ec66c0e30641c976c8963de26a113f3a30456b702468b0", size = 11138536, upload-time = "2026-03-19T16:27:06.101Z" }, + { url = "https://files.pythonhosted.org/packages/e4/f2/4fd0d05aab0c5934b2e1464784f85ba2eab9d54bffc53fb5430d1ed8b829/ruff-0.15.7-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e0d19644f801849229db8345180a71bee5407b429dd217f853ec515e968a6912", size = 11994292, upload-time = "2026-03-19T16:26:48.718Z" }, + { url = "https://files.pythonhosted.org/packages/64/22/fc4483871e767e5e95d1622ad83dad5ebb830f762ed0420fde7dfa9d9b08/ruff-0.15.7-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4806d8e09ef5e84eb19ba833d0442f7e300b23fe3f0981cae159a248a10f0036", size = 11398981, upload-time = "2026-03-19T16:26:54.513Z" }, + { url = "https://files.pythonhosted.org/packages/b0/99/66f0343176d5eab02c3f7fcd2de7a8e0dd7a41f0d982bee56cd1c24db62b/ruff-0.15.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dce0896488562f09a27b9c91b1f58a097457143931f3c4d519690dea54e624c5", size = 11242422, upload-time = "2026-03-19T16:26:29.277Z" }, + { url = "https://files.pythonhosted.org/packages/5d/3a/a7060f145bfdcce4c987ea27788b30c60e2c81d6e9a65157ca8afe646328/ruff-0.15.7-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:1852ce241d2bc89e5dc823e03cff4ce73d816b5c6cdadd27dbfe7b03217d2a12", size = 11232158, upload-time = "2026-03-19T16:26:42.321Z" }, + { url = "https://files.pythonhosted.org/packages/a7/53/90fbb9e08b29c048c403558d3cdd0adf2668b02ce9d50602452e187cd4af/ruff-0.15.7-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:5f3e4b221fb4bd293f79912fc5e93a9063ebd6d0dcbd528f91b89172a9b8436c", size = 10577861, upload-time = "2026-03-19T16:26:57.459Z" }, + { url = "https://files.pythonhosted.org/packages/2f/aa/5f486226538fe4d0f0439e2da1716e1acf895e2a232b26f2459c55f8ddad/ruff-0.15.7-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:b15e48602c9c1d9bdc504b472e90b90c97dc7d46c7028011ae67f3861ceba7b4", size = 10327310, upload-time = "2026-03-19T16:26:35.909Z" }, + { url = "https://files.pythonhosted.org/packages/99/9e/271afdffb81fe7bfc8c43ba079e9d96238f674380099457a74ccb3863857/ruff-0.15.7-py3-none-musllinux_1_2_i686.whl", hash = "sha256:1b4705e0e85cedc74b0a23cf6a179dbb3df184cb227761979cc76c0440b5ab0d", size = 10840752, upload-time = "2026-03-19T16:26:45.723Z" }, + { url = "https://files.pythonhosted.org/packages/bf/29/a4ae78394f76c7759953c47884eb44de271b03a66634148d9f7d11e721bd/ruff-0.15.7-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:112c1fa316a558bb34319282c1200a8bf0495f1b735aeb78bfcb2991e6087580", size = 11336961, upload-time = "2026-03-19T16:26:39.076Z" }, + { url = "https://files.pythonhosted.org/packages/26/6b/8786ba5736562220d588a2f6653e6c17e90c59ced34a2d7b512ef8956103/ruff-0.15.7-py3-none-win32.whl", hash = "sha256:6d39e2d3505b082323352f733599f28169d12e891f7dd407f2d4f54b4c2886de", size = 10582538, upload-time = "2026-03-19T16:26:15.992Z" }, + { url = "https://files.pythonhosted.org/packages/2b/e9/346d4d3fffc6871125e877dae8d9a1966b254fbd92a50f8561078b88b099/ruff-0.15.7-py3-none-win_amd64.whl", hash = "sha256:4d53d712ddebcd7dace1bc395367aec12c057aacfe9adbb6d832302575f4d3a1", size = 11755839, upload-time = "2026-03-19T16:26:19.897Z" }, + { url = "https://files.pythonhosted.org/packages/8f/e8/726643a3ea68c727da31570bde48c7a10f1aa60eddd628d94078fec586ff/ruff-0.15.7-py3-none-win_arm64.whl", hash = "sha256:18e8d73f1c3fdf27931497972250340f92e8c861722161a9caeb89a58ead6ed2", size = 11023304, upload-time = "2026-03-19T16:26:51.669Z" }, +] + +[[package]] +name = "shellingham" +version = "1.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" }, ] [[package]] @@ -792,6 +1277,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0", size = 29575, upload-time = "2021-05-16T22:03:41.177Z" }, ] +[[package]] +name = "stevedore" +version = "5.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/6d/90764092216fa560f6587f83bb70113a8ba510ba436c6476a2b47359057c/stevedore-5.7.0.tar.gz", hash = "sha256:31dd6fe6b3cbe921e21dcefabc9a5f1cf848cf538a1f27543721b8ca09948aa3", size = 516200, upload-time = "2026-02-20T13:27:06.765Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/06/36d260a695f383345ab5bbc3fd447249594ae2fa8dfd19c533d5ae23f46b/stevedore-5.7.0-py3-none-any.whl", hash = "sha256:fd25efbb32f1abb4c9e502f385f0018632baac11f9ee5d1b70f88cc5e22ad4ed", size = 54483, upload-time = "2026-02-20T13:27:05.561Z" }, +] + [[package]] name = "task-pipeline" version = "0.1.0" @@ -824,6 +1318,39 @@ requires-dist = [ ] provides-extras = ["dev"] +[[package]] +name = "termcolor" +version = "3.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/46/79/cf31d7a93a8fdc6aa0fbb665be84426a8c5a557d9240b6239e9e11e35fc5/termcolor-3.3.0.tar.gz", hash = "sha256:348871ca648ec6a9a983a13ab626c0acce02f515b9e1983332b17af7979521c5", size = 14434, upload-time = "2025-12-29T12:55:21.882Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/d1/8bb87d21e9aeb323cc03034f5eaf2c8f69841e40e4853c2627edf8111ed3/termcolor-3.3.0-py3-none-any.whl", hash = "sha256:cf642efadaf0a8ebbbf4bc7a31cec2f9b5f21a9f726f4ccbb08192c9c26f43a5", size = 7734, upload-time = "2025-12-29T12:55:20.718Z" }, +] + +[[package]] +name = "tomlkit" +version = "0.14.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/af/14b24e41977adb296d6bd1fb59402cf7d60ce364f90c890bd2ec65c43b5a/tomlkit-0.14.0.tar.gz", hash = "sha256:cf00efca415dbd57575befb1f6634c4f42d2d87dbba376128adb42c121b87064", size = 187167, upload-time = "2026-01-13T01:14:53.304Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b5/11/87d6d29fb5d237229d67973a6c9e06e048f01cf4994dee194ab0ea841814/tomlkit-0.14.0-py3-none-any.whl", hash = "sha256:592064ed85b40fa213469f81ac584f67a4f2992509a7c3ea2d632208623a3680", size = 39310, upload-time = "2026-01-13T01:14:51.965Z" }, +] + +[[package]] +name = "typer" +version = "0.24.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-doc" }, + { name = "click" }, + { name = "rich" }, + { name = "shellingham" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f5/24/cb09efec5cc954f7f9b930bf8279447d24618bb6758d4f6adf2574c41780/typer-0.24.1.tar.gz", hash = "sha256:e39b4732d65fbdcde189ae76cf7cd48aeae72919dea1fdfc16593be016256b45", size = 118613, upload-time = "2026-02-21T16:54:40.609Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4a/91/48db081e7a63bb37284f9fbcefda7c44c277b18b0e13fbc36ea2335b71e6/typer-0.24.1-py3-none-any.whl", hash = "sha256:112c1f0ce578bfb4cab9ffdabc68f031416ebcc216536611ba21f04e9aa84c9e", size = 56085, upload-time = "2026-02-21T16:54:41.616Z" }, +] + [[package]] name = "typing-extensions" version = "4.15.0" @@ -847,14 +1374,88 @@ wheels = [ [[package]] name = "virtualenv" -version = "20.35.4" +version = "21.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "distlib" }, { name = "filelock" }, { name = "platformdirs" }, + { name = "python-discovery" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/aa/92/58199fe10049f9703c2666e809c4f686c54ef0a68b0f6afccf518c0b1eb9/virtualenv-21.2.0.tar.gz", hash = "sha256:1720dc3a62ef5b443092e3f499228599045d7fea4c79199770499df8becf9098", size = 5840618, upload-time = "2026-03-09T17:24:38.013Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c6/59/7d02447a55b2e55755011a647479041bc92a82e143f96a8195cb33bd0a1c/virtualenv-21.2.0-py3-none-any.whl", hash = "sha256:1bd755b504931164a5a496d217c014d098426cddc79363ad66ac78125f9d908f", size = 5825084, upload-time = "2026-03-09T17:24:35.378Z" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/20/28/e6f1a6f655d620846bd9df527390ecc26b3805a0c5989048c210e22c5ca9/virtualenv-20.35.4.tar.gz", hash = "sha256:643d3914d73d3eeb0c552cbb12d7e82adf0e504dbf86a3182f8771a153a1971c", size = 6028799, upload-time = "2025-10-29T06:57:40.511Z" } + +[[package]] +name = "wcwidth" +version = "0.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/35/a2/8e3becb46433538a38726c948d3399905a4c7cabd0df578ede5dc51f0ec2/wcwidth-0.6.0.tar.gz", hash = "sha256:cdc4e4262d6ef9a1a57e018384cbeb1208d8abbc64176027e2c2455c81313159", size = 159684, upload-time = "2026-02-06T19:19:40.919Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/68/5a/199c59e0a824a3db2b89c5d2dade7ab5f9624dbf6448dc291b46d5ec94d3/wcwidth-0.6.0-py3-none-any.whl", hash = "sha256:1a3a1e510b553315f8e146c54764f4fb6264ffad731b3d78088cdb1478ffbdad", size = 94189, upload-time = "2026-02-06T19:19:39.646Z" }, +] + +[[package]] +name = "wrapt" +version = "2.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2e/64/925f213fdcbb9baeb1530449ac71a4d57fc361c053d06bf78d0c5c7cd80c/wrapt-2.1.2.tar.gz", hash = "sha256:3996a67eecc2c68fd47b4e3c564405a5777367adfd9b8abb58387b63ee83b21e", size = 81678, upload-time = "2026-03-06T02:53:25.134Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/79/0c/c05523fa3181fdf0c9c52a6ba91a23fbf3246cc095f26f6516f9c60e6771/virtualenv-20.35.4-py3-none-any.whl", hash = "sha256:c21c9cede36c9753eeade68ba7d523529f228a403463376cf821eaae2b650f1b", size = 6005095, upload-time = "2025-10-29T06:57:37.598Z" }, + { url = "https://files.pythonhosted.org/packages/4c/b6/1db817582c49c7fcbb7df6809d0f515af29d7c2fbf57eb44c36e98fb1492/wrapt-2.1.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ff2aad9c4cda28a8f0653fc2d487596458c2a3f475e56ba02909e950a9efa6a9", size = 61255, upload-time = "2026-03-06T02:52:45.663Z" }, + { url = "https://files.pythonhosted.org/packages/a2/16/9b02a6b99c09227c93cd4b73acc3678114154ec38da53043c0ddc1fba0dc/wrapt-2.1.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6433ea84e1cfacf32021d2a4ee909554ade7fd392caa6f7c13f1f4bf7b8e8748", size = 61848, upload-time = "2026-03-06T02:53:48.728Z" }, + { url = "https://files.pythonhosted.org/packages/af/aa/ead46a88f9ec3a432a4832dfedb84092fc35af2d0ba40cd04aea3889f247/wrapt-2.1.2-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c20b757c268d30d6215916a5fa8461048d023865d888e437fab451139cad6c8e", size = 121433, upload-time = "2026-03-06T02:54:40.328Z" }, + { url = "https://files.pythonhosted.org/packages/3a/9f/742c7c7cdf58b59085a1ee4b6c37b013f66ac33673a7ef4aaed5e992bc33/wrapt-2.1.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:79847b83eb38e70d93dc392c7c5b587efe65b3e7afcc167aa8abd5d60e8761c8", size = 123013, upload-time = "2026-03-06T02:53:26.58Z" }, + { url = "https://files.pythonhosted.org/packages/e8/44/2c3dd45d53236b7ed7c646fcf212251dc19e48e599debd3926b52310fafb/wrapt-2.1.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f8fba1bae256186a83d1875b2b1f4e2d1242e8fac0f58ec0d7e41b26967b965c", size = 117326, upload-time = "2026-03-06T02:53:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/74/e2/b17d66abc26bd96f89dec0ecd0ef03da4a1286e6ff793839ec431b9fae57/wrapt-2.1.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e3d3b35eedcf5f7d022291ecd7533321c4775f7b9cd0050a31a68499ba45757c", size = 121444, upload-time = "2026-03-06T02:54:09.5Z" }, + { url = "https://files.pythonhosted.org/packages/3c/62/e2977843fdf9f03daf1586a0ff49060b1b2fc7ff85a7ea82b6217c1ae36e/wrapt-2.1.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:6f2c5390460de57fa9582bc8a1b7a6c86e1a41dfad74c5225fc07044c15cc8d1", size = 116237, upload-time = "2026-03-06T02:54:03.884Z" }, + { url = "https://files.pythonhosted.org/packages/88/dd/27fc67914e68d740bce512f11734aec08696e6b17641fef8867c00c949fc/wrapt-2.1.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7dfa9f2cf65d027b951d05c662cc99ee3bd01f6e4691ed39848a7a5fffc902b2", size = 120563, upload-time = "2026-03-06T02:53:20.412Z" }, + { url = "https://files.pythonhosted.org/packages/ec/9f/b750b3692ed2ef4705cb305bd68858e73010492b80e43d2a4faa5573cbe7/wrapt-2.1.2-cp312-cp312-win32.whl", hash = "sha256:eba8155747eb2cae4a0b913d9ebd12a1db4d860fc4c829d7578c7b989bd3f2f0", size = 58198, upload-time = "2026-03-06T02:53:37.732Z" }, + { url = "https://files.pythonhosted.org/packages/8e/b2/feecfe29f28483d888d76a48f03c4c4d8afea944dbee2b0cd3380f9df032/wrapt-2.1.2-cp312-cp312-win_amd64.whl", hash = "sha256:1c51c738d7d9faa0b3601708e7e2eda9bf779e1b601dce6c77411f2a1b324a63", size = 60441, upload-time = "2026-03-06T02:52:47.138Z" }, + { url = "https://files.pythonhosted.org/packages/44/e1/e328f605d6e208547ea9fd120804fcdec68536ac748987a68c47c606eea8/wrapt-2.1.2-cp312-cp312-win_arm64.whl", hash = "sha256:c8e46ae8e4032792eb2f677dbd0d557170a8e5524d22acc55199f43efedd39bf", size = 58836, upload-time = "2026-03-06T02:53:22.053Z" }, + { url = "https://files.pythonhosted.org/packages/4c/7a/d936840735c828b38d26a854e85d5338894cda544cb7a85a9d5b8b9c4df7/wrapt-2.1.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:787fd6f4d67befa6fe2abdffcbd3de2d82dfc6fb8a6d850407c53332709d030b", size = 61259, upload-time = "2026-03-06T02:53:41.922Z" }, + { url = "https://files.pythonhosted.org/packages/5e/88/9a9b9a90ac8ca11c2fdb6a286cb3a1fc7dd774c00ed70929a6434f6bc634/wrapt-2.1.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4bdf26e03e6d0da3f0e9422fd36bcebf7bc0eeb55fdf9c727a09abc6b9fe472e", size = 61851, upload-time = "2026-03-06T02:52:48.672Z" }, + { url = "https://files.pythonhosted.org/packages/03/a9/5b7d6a16fd6533fed2756900fc8fc923f678179aea62ada6d65c92718c00/wrapt-2.1.2-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bbac24d879aa22998e87f6b3f481a5216311e7d53c7db87f189a7a0266dafffb", size = 121446, upload-time = "2026-03-06T02:54:14.013Z" }, + { url = "https://files.pythonhosted.org/packages/45/bb/34c443690c847835cfe9f892be78c533d4f32366ad2888972c094a897e39/wrapt-2.1.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:16997dfb9d67addc2e3f41b62a104341e80cac52f91110dece393923c0ebd5ca", size = 123056, upload-time = "2026-03-06T02:54:10.829Z" }, + { url = "https://files.pythonhosted.org/packages/93/b9/ff205f391cb708f67f41ea148545f2b53ff543a7ac293b30d178af4d2271/wrapt-2.1.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:162e4e2ba7542da9027821cb6e7c5e068d64f9a10b5f15512ea28e954893a267", size = 117359, upload-time = "2026-03-06T02:53:03.623Z" }, + { url = "https://files.pythonhosted.org/packages/1f/3d/1ea04d7747825119c3c9a5e0874a40b33594ada92e5649347c457d982805/wrapt-2.1.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f29c827a8d9936ac320746747a016c4bc66ef639f5cd0d32df24f5eacbf9c69f", size = 121479, upload-time = "2026-03-06T02:53:45.844Z" }, + { url = "https://files.pythonhosted.org/packages/78/cc/ee3a011920c7a023b25e8df26f306b2484a531ab84ca5c96260a73de76c0/wrapt-2.1.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:a9dd9813825f7ecb018c17fd147a01845eb330254dff86d3b5816f20f4d6aaf8", size = 116271, upload-time = "2026-03-06T02:54:46.356Z" }, + { url = "https://files.pythonhosted.org/packages/98/fd/e5ff7ded41b76d802cf1191288473e850d24ba2e39a6ec540f21ae3b57cb/wrapt-2.1.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6f8dbdd3719e534860d6a78526aafc220e0241f981367018c2875178cf83a413", size = 120573, upload-time = "2026-03-06T02:52:50.163Z" }, + { url = "https://files.pythonhosted.org/packages/47/c5/242cae3b5b080cd09bacef0591691ba1879739050cc7c801ff35c8886b66/wrapt-2.1.2-cp313-cp313-win32.whl", hash = "sha256:5c35b5d82b16a3bc6e0a04349b606a0582bc29f573786aebe98e0c159bc48db6", size = 58205, upload-time = "2026-03-06T02:53:47.494Z" }, + { url = "https://files.pythonhosted.org/packages/12/69/c358c61e7a50f290958809b3c61ebe8b3838ea3e070d7aac9814f95a0528/wrapt-2.1.2-cp313-cp313-win_amd64.whl", hash = "sha256:f8bc1c264d8d1cf5b3560a87bbdd31131573eb25f9f9447bb6252b8d4c44a3a1", size = 60452, upload-time = "2026-03-06T02:53:30.038Z" }, + { url = "https://files.pythonhosted.org/packages/8e/66/c8a6fcfe321295fd8c0ab1bd685b5a01462a9b3aa2f597254462fc2bc975/wrapt-2.1.2-cp313-cp313-win_arm64.whl", hash = "sha256:3beb22f674550d5634642c645aba4c72a2c66fb185ae1aebe1e955fae5a13baf", size = 58842, upload-time = "2026-03-06T02:52:52.114Z" }, + { url = "https://files.pythonhosted.org/packages/da/55/9c7052c349106e0b3f17ae8db4b23a691a963c334de7f9dbd60f8f74a831/wrapt-2.1.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0fc04bc8664a8bc4c8e00b37b5355cffca2535209fba1abb09ae2b7c76ddf82b", size = 63075, upload-time = "2026-03-06T02:53:19.108Z" }, + { url = "https://files.pythonhosted.org/packages/09/a8/ce7b4006f7218248dd71b7b2b732d0710845a0e49213b18faef64811ffef/wrapt-2.1.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a9b9d50c9af998875a1482a038eb05755dfd6fe303a313f6a940bb53a83c3f18", size = 63719, upload-time = "2026-03-06T02:54:33.452Z" }, + { url = "https://files.pythonhosted.org/packages/e4/e5/2ca472e80b9e2b7a17f106bb8f9df1db11e62101652ce210f66935c6af67/wrapt-2.1.2-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2d3ff4f0024dd224290c0eabf0240f1bfc1f26363431505fb1b0283d3b08f11d", size = 152643, upload-time = "2026-03-06T02:52:42.721Z" }, + { url = "https://files.pythonhosted.org/packages/36/42/30f0f2cefca9d9cbf6835f544d825064570203c3e70aa873d8ae12e23791/wrapt-2.1.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3278c471f4468ad544a691b31bb856374fbdefb7fee1a152153e64019379f015", size = 158805, upload-time = "2026-03-06T02:54:25.441Z" }, + { url = "https://files.pythonhosted.org/packages/bb/67/d08672f801f604889dcf58f1a0b424fe3808860ede9e03affc1876b295af/wrapt-2.1.2-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a8914c754d3134a3032601c6984db1c576e6abaf3fc68094bb8ab1379d75ff92", size = 145990, upload-time = "2026-03-06T02:53:57.456Z" }, + { url = "https://files.pythonhosted.org/packages/68/a7/fd371b02e73babec1de6ade596e8cd9691051058cfdadbfd62a5898f3295/wrapt-2.1.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:ff95d4264e55839be37bafe1536db2ab2de19da6b65f9244f01f332b5286cfbf", size = 155670, upload-time = "2026-03-06T02:54:55.309Z" }, + { url = "https://files.pythonhosted.org/packages/86/2d/9fe0095dfdb621009f40117dcebf41d7396c2c22dca6eac779f4c007b86c/wrapt-2.1.2-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:76405518ca4e1b76fbb1b9f686cff93aebae03920cc55ceeec48ff9f719c5f67", size = 144357, upload-time = "2026-03-06T02:54:24.092Z" }, + { url = "https://files.pythonhosted.org/packages/0e/b6/ec7b4a254abbe4cde9fa15c5d2cca4518f6b07d0f1b77d4ee9655e30280e/wrapt-2.1.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c0be8b5a74c5824e9359b53e7e58bef71a729bacc82e16587db1c4ebc91f7c5a", size = 150269, upload-time = "2026-03-06T02:53:31.268Z" }, + { url = "https://files.pythonhosted.org/packages/6e/6b/2fabe8ebf148f4ee3c782aae86a795cc68ffe7d432ef550f234025ce0cfa/wrapt-2.1.2-cp313-cp313t-win32.whl", hash = "sha256:f01277d9a5fc1862f26f7626da9cf443bebc0abd2f303f41c5e995b15887dabd", size = 59894, upload-time = "2026-03-06T02:54:15.391Z" }, + { url = "https://files.pythonhosted.org/packages/ca/fb/9ba66fc2dedc936de5f8073c0217b5d4484e966d87723415cc8262c5d9c2/wrapt-2.1.2-cp313-cp313t-win_amd64.whl", hash = "sha256:84ce8f1c2104d2f6daa912b1b5b039f331febfeee74f8042ad4e04992bd95c8f", size = 63197, upload-time = "2026-03-06T02:54:41.943Z" }, + { url = "https://files.pythonhosted.org/packages/c0/1c/012d7423c95d0e337117723eb8ecf73c622ce15a97847e84cf3f8f26cd7e/wrapt-2.1.2-cp313-cp313t-win_arm64.whl", hash = "sha256:a93cd767e37faeddbe07d8fc4212d5cba660af59bdb0f6372c93faaa13e6e679", size = 60363, upload-time = "2026-03-06T02:54:48.093Z" }, + { url = "https://files.pythonhosted.org/packages/39/25/e7ea0b417db02bb796182a5316398a75792cd9a22528783d868755e1f669/wrapt-2.1.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:1370e516598854e5b4366e09ce81e08bfe94d42b0fd569b88ec46cc56d9164a9", size = 61418, upload-time = "2026-03-06T02:53:55.706Z" }, + { url = "https://files.pythonhosted.org/packages/ec/0f/fa539e2f6a770249907757eaeb9a5ff4deb41c026f8466c1c6d799088a9b/wrapt-2.1.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6de1a3851c27e0bd6a04ca993ea6f80fc53e6c742ee1601f486c08e9f9b900a9", size = 61914, upload-time = "2026-03-06T02:52:53.37Z" }, + { url = "https://files.pythonhosted.org/packages/53/37/02af1867f5b1441aaeda9c82deed061b7cd1372572ddcd717f6df90b5e93/wrapt-2.1.2-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:de9f1a2bbc5ac7f6012ec24525bdd444765a2ff64b5985ac6e0692144838542e", size = 120417, upload-time = "2026-03-06T02:54:30.74Z" }, + { url = "https://files.pythonhosted.org/packages/c3/b7/0138a6238c8ba7476c77cf786a807f871672b37f37a422970342308276e7/wrapt-2.1.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:970d57ed83fa040d8b20c52fe74a6ae7e3775ae8cff5efd6a81e06b19078484c", size = 122797, upload-time = "2026-03-06T02:54:51.539Z" }, + { url = "https://files.pythonhosted.org/packages/e1/ad/819ae558036d6a15b7ed290d5b14e209ca795dd4da9c58e50c067d5927b0/wrapt-2.1.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3969c56e4563c375861c8df14fa55146e81ac11c8db49ea6fb7f2ba58bc1ff9a", size = 117350, upload-time = "2026-03-06T02:54:37.651Z" }, + { url = "https://files.pythonhosted.org/packages/8b/2d/afc18dc57a4600a6e594f77a9ae09db54f55ba455440a54886694a84c71b/wrapt-2.1.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:57d7c0c980abdc5f1d98b11a2aa3bb159790add80258c717fa49a99921456d90", size = 121223, upload-time = "2026-03-06T02:54:35.221Z" }, + { url = "https://files.pythonhosted.org/packages/b9/5b/5ec189b22205697bc56eb3b62aed87a1e0423e9c8285d0781c7a83170d15/wrapt-2.1.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:776867878e83130c7a04237010463372e877c1c994d449ca6aaafeab6aab2586", size = 116287, upload-time = "2026-03-06T02:54:19.654Z" }, + { url = "https://files.pythonhosted.org/packages/f7/2d/f84939a7c9b5e6cdd8a8d0f6a26cabf36a0f7e468b967720e8b0cd2bdf69/wrapt-2.1.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:fab036efe5464ec3291411fabb80a7a39e2dd80bae9bcbeeca5087fdfa891e19", size = 119593, upload-time = "2026-03-06T02:54:16.697Z" }, + { url = "https://files.pythonhosted.org/packages/0b/fe/ccd22a1263159c4ac811ab9374c061bcb4a702773f6e06e38de5f81a1bdc/wrapt-2.1.2-cp314-cp314-win32.whl", hash = "sha256:e6ed62c82ddf58d001096ae84ce7f833db97ae2263bff31c9b336ba8cfe3f508", size = 58631, upload-time = "2026-03-06T02:53:06.498Z" }, + { url = "https://files.pythonhosted.org/packages/65/0a/6bd83be7bff2e7efaac7b4ac9748da9d75a34634bbbbc8ad077d527146df/wrapt-2.1.2-cp314-cp314-win_amd64.whl", hash = "sha256:467e7c76315390331c67073073d00662015bb730c566820c9ca9b54e4d67fd04", size = 60875, upload-time = "2026-03-06T02:53:50.252Z" }, + { url = "https://files.pythonhosted.org/packages/6c/c0/0b3056397fe02ff80e5a5d72d627c11eb885d1ca78e71b1a5c1e8c7d45de/wrapt-2.1.2-cp314-cp314-win_arm64.whl", hash = "sha256:da1f00a557c66225d53b095a97eace0fc5349e3bfda28fa34ffae238978ee575", size = 59164, upload-time = "2026-03-06T02:53:59.128Z" }, + { url = "https://files.pythonhosted.org/packages/71/ed/5d89c798741993b2371396eb9d4634f009ff1ad8a6c78d366fe2883ea7a6/wrapt-2.1.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:62503ffbc2d3a69891cf29beeaccdb4d5e0a126e2b6a851688d4777e01428dbb", size = 63163, upload-time = "2026-03-06T02:52:54.873Z" }, + { url = "https://files.pythonhosted.org/packages/c6/8c/05d277d182bf36b0a13d6bd393ed1dec3468a25b59d01fba2dd70fe4d6ae/wrapt-2.1.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c7e6cd120ef837d5b6f860a6ea3745f8763805c418bb2f12eeb1fa6e25f22d22", size = 63723, upload-time = "2026-03-06T02:52:56.374Z" }, + { url = "https://files.pythonhosted.org/packages/f4/27/6c51ec1eff4413c57e72d6106bb8dec6f0c7cdba6503d78f0fa98767bcc9/wrapt-2.1.2-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3769a77df8e756d65fbc050333f423c01ae012b4f6731aaf70cf2bef61b34596", size = 152652, upload-time = "2026-03-06T02:53:23.79Z" }, + { url = "https://files.pythonhosted.org/packages/db/4c/d7dd662d6963fc7335bfe29d512b02b71cdfa23eeca7ab3ac74a67505deb/wrapt-2.1.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a76d61a2e851996150ba0f80582dd92a870643fa481f3b3846f229de88caf044", size = 158807, upload-time = "2026-03-06T02:53:35.742Z" }, + { url = "https://files.pythonhosted.org/packages/b4/4d/1e5eea1a78d539d346765727422976676615814029522c76b87a95f6bcdd/wrapt-2.1.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6f97edc9842cf215312b75fe737ee7c8adda75a89979f8e11558dfff6343cc4b", size = 146061, upload-time = "2026-03-06T02:52:57.574Z" }, + { url = "https://files.pythonhosted.org/packages/89/bc/62cabea7695cd12a288023251eeefdcb8465056ddaab6227cb78a2de005b/wrapt-2.1.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:4006c351de6d5007aa33a551f600404ba44228a89e833d2fadc5caa5de8edfbf", size = 155667, upload-time = "2026-03-06T02:53:39.422Z" }, + { url = "https://files.pythonhosted.org/packages/e9/99/6f2888cd68588f24df3a76572c69c2de28287acb9e1972bf0c83ce97dbc1/wrapt-2.1.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:a9372fc3639a878c8e7d87e1556fa209091b0a66e912c611e3f833e2c4202be2", size = 144392, upload-time = "2026-03-06T02:54:22.41Z" }, + { url = "https://files.pythonhosted.org/packages/40/51/1dfc783a6c57971614c48e361a82ca3b6da9055879952587bc99fe1a7171/wrapt-2.1.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3144b027ff30cbd2fca07c0a87e67011adb717eb5f5bd8496325c17e454257a3", size = 150296, upload-time = "2026-03-06T02:54:07.848Z" }, + { url = "https://files.pythonhosted.org/packages/6c/38/cbb8b933a0201076c1f64fc42883b0023002bdc14a4964219154e6ff3350/wrapt-2.1.2-cp314-cp314t-win32.whl", hash = "sha256:3b8d15e52e195813efe5db8cec156eebe339aaf84222f4f4f051a6c01f237ed7", size = 60539, upload-time = "2026-03-06T02:54:00.594Z" }, + { url = "https://files.pythonhosted.org/packages/82/dd/e5176e4b241c9f528402cebb238a36785a628179d7d8b71091154b3e4c9e/wrapt-2.1.2-cp314-cp314t-win_amd64.whl", hash = "sha256:08ffa54146a7559f5b8df4b289b46d963a8e74ed16ba3687f99896101a3990c5", size = 63969, upload-time = "2026-03-06T02:54:39Z" }, + { url = "https://files.pythonhosted.org/packages/5c/99/79f17046cf67e4a95b9987ea129632ba8bcec0bc81f3fb3d19bdb0bd60cd/wrapt-2.1.2-cp314-cp314t-win_arm64.whl", hash = "sha256:72aaa9d0d8e4ed0e2e98019cea47a21f823c9dd4b43c7b77bba6679ffcca6a00", size = 60554, upload-time = "2026-03-06T02:53:14.132Z" }, + { url = "https://files.pythonhosted.org/packages/1a/c7/8528ac2dfa2c1e6708f647df7ae144ead13f0a31146f43c7264b4942bf12/wrapt-2.1.2-py3-none-any.whl", hash = "sha256:b8fd6fa2b2c4e7621808f8c62e8317f4aae56e59721ad933bac5239d913cf0e8", size = 43993, upload-time = "2026-03-06T02:53:12.905Z" }, ]