From 07f398921cc94057ed3e8c044e9c6b9733067d27 Mon Sep 17 00:00:00 2001 From: Carsten Scharfenberg Date: Thu, 29 Jan 2026 11:24:28 +0100 Subject: [PATCH 1/9] feat: Remove the advanced middleware API, its Helm chart, client, and shared components, update development environment configurations and CI/CD workflows, and add architectural design documentation. --- .github/workflows/docker-build.yml | 6 +- .github/workflows/docker-release.yml | 2 +- .github/workflows/helm-release.yml | 212 ----- .vscode/settings.json | 4 +- dev_environment/compose-external.yaml | 70 -- dev_environment/compose.yaml | 109 +-- dev_environment/config-external.yaml | 22 - dev_environment/config.yaml | 19 +- dev_environment/middleware-api-config.yaml | 26 - dev_environment/secrets.enc.yaml | 34 - dev_environment/start-external.sh | 41 - dev_environment/start.sh | 36 +- docker/Dockerfile.api | 109 --- docker/container-structure-tests/api.yaml | 88 -- .../ARCHITECTURAL_DESIGN.md | 0 docs/client_certificates.md | 9 - docs/gitlab_related.md | 25 - docs/helmchart_related.md | 137 --- docs/python_related.md | 160 ---- .../.helmignore | 23 - .../Chart.yaml | 24 - .../templates/NOTES.txt | 22 - .../templates/_helpers.tpl | 126 --- .../templates/arc-cache-pvc.yaml | 17 - .../templates/ca-tls-secret.yaml | 19 - .../templates/celery-worker.yaml | 124 --- .../templates/config-secret.yaml | 9 - .../templates/deployment.yaml | 94 -- .../templates/hpa.yaml | 67 -- .../templates/ingress.yaml | 51 -- .../templates/rabbitmq.yaml | 94 -- .../templates/redis.yaml | 85 -- .../templates/server-tls-secret.yaml | 29 - .../templates/service.yaml | 15 - .../templates/serviceaccount.yaml | 29 - .../templates/tests/test-connection.yaml | 15 - .../values.yaml | 175 ---- helmchart/test_deploy/values.yaml | 33 - middleware/api/README.md | 58 -- middleware/api/example_config.yaml | 9 - middleware/api/pyproject.toml | 36 - middleware/api/src/middleware/api/__init__.py | 1 - middleware/api/src/middleware/api/api.py | 555 ------------ .../src/middleware/api/arc_store/__init__.py | 181 ---- .../src/middleware/api/arc_store/git_repo.py | 462 ---------- .../middleware/api/arc_store/gitlab_api.py | 387 --------- .../api/arc_store/remote_git_provider.py | 191 ---- .../api/src/middleware/api/business_logic.py | 217 ----- .../api/src/middleware/api/celery_app.py | 92 -- middleware/api/src/middleware/api/config.py | 76 -- middleware/api/src/middleware/api/main.py | 39 - middleware/api/src/middleware/api/tracing.py | 46 - middleware/api/src/middleware/api/worker.py | 56 -- .../api/src/middleware/api/worker_health.py | 99 --- middleware/api/tests/conftest.py | 102 --- middleware/api/tests/integration/conftest.py | 87 -- .../test_integration_create_or_update_arcs.py | 59 -- .../integration/test_integration_git_repo.py | 135 --- .../api/tests/integration/test_whoami.py | 20 - middleware/api/tests/unit/conftest.py | 113 --- middleware/api/tests/unit/test_arc_store.py | 123 --- middleware/api/tests/unit/test_config.py | 136 --- middleware/api/tests/unit/test_git_repo.py | 479 ---------- .../api/tests/unit/test_git_repo_health.py | 73 -- .../api/tests/unit/test_gitlab_api_config.py | 36 - .../api/tests/unit/test_middleware_api.py | 426 --------- .../tests/unit/test_persistence_gitlab_api.py | 144 --- .../tests/unit/test_remote_repo_manager.py | 159 ---- .../unit/test_unit_create_or_update_arcs.py | 201 ----- middleware/api/tests/unit/test_worker.py | 60 -- .../api/tests/unit/test_worker_health.py | 138 --- middleware/api_client/README.md | 133 --- .../api_client/example_client_config.yaml | 23 - middleware/api_client/pyproject.toml | 41 - .../src/middleware/api_client/__init__.py | 6 - .../src/middleware/api_client/api_client.py | 301 ------- .../src/middleware/api_client/config.py | 38 - middleware/api_client/tests/conftest.py | 110 --- .../api_client/tests/integration/conftest.py | 62 -- .../tests/integration/test_create_arcs.py | 169 ---- .../tests/unit/test_api_client_config.py | 106 --- .../api_client/tests/unit/test_client.py | 354 -------- .../tests/unit/test_client_config.py | 138 --- middleware/shared/README.md | 52 -- middleware/shared/pyproject.toml | 20 - .../shared/src/middleware/shared/__init__.py | 1 - .../middleware/shared/api_models/__init__.py | 1 - .../middleware/shared/api_models/models.py | 107 --- .../src/middleware/shared/api_models/py.typed | 0 .../src/middleware/shared/config/__init__.py | 1 - .../middleware/shared/config/config_base.py | 91 -- .../shared/config/config_wrapper.py | 364 -------- .../src/middleware/shared/config/logging.py | 25 - .../shared/src/middleware/shared/tracing.py | 155 ---- .../shared/tests/unit/test_api_models.py | 52 -- .../shared/tests/unit/test_config_base.py | 25 - .../shared/tests/unit/test_config_wrapper.py | 288 ------ middleware/shared/tests/unit/test_logging.py | 92 -- middleware/sql_to_arc/pyproject.toml | 4 +- middleware/tools/README.md | 92 -- middleware/tools/arc2rocrate.py | 33 - middleware/tools/pyproject.toml | 16 - middleware/tools/rocrate2arc.py | 29 - pyproject.toml | 12 +- ro_crates/edaphobase.json | 45 - ro_crates/minimal.json | 17 - ro_crates/sample.json | 819 ------------------ uv.lock | 677 +-------------- 108 files changed, 48 insertions(+), 11607 deletions(-) delete mode 100644 .github/workflows/helm-release.yml delete mode 100644 dev_environment/compose-external.yaml delete mode 100644 dev_environment/config-external.yaml delete mode 100644 dev_environment/middleware-api-config.yaml delete mode 100644 dev_environment/secrets.enc.yaml delete mode 100755 dev_environment/start-external.sh delete mode 100644 docker/Dockerfile.api delete mode 100644 docker/container-structure-tests/api.yaml rename {middleware/sql_to_arc => docs}/ARCHITECTURAL_DESIGN.md (100%) delete mode 100644 docs/client_certificates.md delete mode 100644 docs/gitlab_related.md delete mode 100644 docs/helmchart_related.md delete mode 100644 docs/python_related.md delete mode 100644 helmchart/fairagro-advanced-middleware-api-chart/.helmignore delete mode 100644 helmchart/fairagro-advanced-middleware-api-chart/Chart.yaml delete mode 100644 helmchart/fairagro-advanced-middleware-api-chart/templates/NOTES.txt delete mode 100644 helmchart/fairagro-advanced-middleware-api-chart/templates/_helpers.tpl delete mode 100644 helmchart/fairagro-advanced-middleware-api-chart/templates/arc-cache-pvc.yaml delete mode 100644 helmchart/fairagro-advanced-middleware-api-chart/templates/ca-tls-secret.yaml delete mode 100644 helmchart/fairagro-advanced-middleware-api-chart/templates/celery-worker.yaml delete mode 100644 helmchart/fairagro-advanced-middleware-api-chart/templates/config-secret.yaml delete mode 100644 helmchart/fairagro-advanced-middleware-api-chart/templates/deployment.yaml delete mode 100644 helmchart/fairagro-advanced-middleware-api-chart/templates/hpa.yaml delete mode 100644 helmchart/fairagro-advanced-middleware-api-chart/templates/ingress.yaml delete mode 100644 helmchart/fairagro-advanced-middleware-api-chart/templates/rabbitmq.yaml delete mode 100644 helmchart/fairagro-advanced-middleware-api-chart/templates/redis.yaml delete mode 100644 helmchart/fairagro-advanced-middleware-api-chart/templates/server-tls-secret.yaml delete mode 100644 helmchart/fairagro-advanced-middleware-api-chart/templates/service.yaml delete mode 100644 helmchart/fairagro-advanced-middleware-api-chart/templates/serviceaccount.yaml delete mode 100644 helmchart/fairagro-advanced-middleware-api-chart/templates/tests/test-connection.yaml delete mode 100644 helmchart/fairagro-advanced-middleware-api-chart/values.yaml delete mode 100644 helmchart/test_deploy/values.yaml delete mode 100644 middleware/api/README.md delete mode 100644 middleware/api/example_config.yaml delete mode 100644 middleware/api/pyproject.toml delete mode 100644 middleware/api/src/middleware/api/__init__.py delete mode 100644 middleware/api/src/middleware/api/api.py delete mode 100644 middleware/api/src/middleware/api/arc_store/__init__.py delete mode 100644 middleware/api/src/middleware/api/arc_store/git_repo.py delete mode 100644 middleware/api/src/middleware/api/arc_store/gitlab_api.py delete mode 100644 middleware/api/src/middleware/api/arc_store/remote_git_provider.py delete mode 100644 middleware/api/src/middleware/api/business_logic.py delete mode 100644 middleware/api/src/middleware/api/celery_app.py delete mode 100644 middleware/api/src/middleware/api/config.py delete mode 100644 middleware/api/src/middleware/api/main.py delete mode 100644 middleware/api/src/middleware/api/tracing.py delete mode 100644 middleware/api/src/middleware/api/worker.py delete mode 100644 middleware/api/src/middleware/api/worker_health.py delete mode 100644 middleware/api/tests/conftest.py delete mode 100644 middleware/api/tests/integration/conftest.py delete mode 100644 middleware/api/tests/integration/test_integration_create_or_update_arcs.py delete mode 100644 middleware/api/tests/integration/test_integration_git_repo.py delete mode 100644 middleware/api/tests/integration/test_whoami.py delete mode 100644 middleware/api/tests/unit/conftest.py delete mode 100644 middleware/api/tests/unit/test_arc_store.py delete mode 100644 middleware/api/tests/unit/test_config.py delete mode 100644 middleware/api/tests/unit/test_git_repo.py delete mode 100644 middleware/api/tests/unit/test_git_repo_health.py delete mode 100644 middleware/api/tests/unit/test_gitlab_api_config.py delete mode 100644 middleware/api/tests/unit/test_middleware_api.py delete mode 100644 middleware/api/tests/unit/test_persistence_gitlab_api.py delete mode 100644 middleware/api/tests/unit/test_remote_repo_manager.py delete mode 100644 middleware/api/tests/unit/test_unit_create_or_update_arcs.py delete mode 100644 middleware/api/tests/unit/test_worker.py delete mode 100644 middleware/api/tests/unit/test_worker_health.py delete mode 100644 middleware/api_client/README.md delete mode 100644 middleware/api_client/example_client_config.yaml delete mode 100644 middleware/api_client/pyproject.toml delete mode 100644 middleware/api_client/src/middleware/api_client/__init__.py delete mode 100644 middleware/api_client/src/middleware/api_client/api_client.py delete mode 100644 middleware/api_client/src/middleware/api_client/config.py delete mode 100644 middleware/api_client/tests/conftest.py delete mode 100644 middleware/api_client/tests/integration/conftest.py delete mode 100644 middleware/api_client/tests/integration/test_create_arcs.py delete mode 100644 middleware/api_client/tests/unit/test_api_client_config.py delete mode 100644 middleware/api_client/tests/unit/test_client.py delete mode 100644 middleware/api_client/tests/unit/test_client_config.py delete mode 100644 middleware/shared/README.md delete mode 100644 middleware/shared/pyproject.toml delete mode 100644 middleware/shared/src/middleware/shared/__init__.py delete mode 100644 middleware/shared/src/middleware/shared/api_models/__init__.py delete mode 100644 middleware/shared/src/middleware/shared/api_models/models.py delete mode 100644 middleware/shared/src/middleware/shared/api_models/py.typed delete mode 100644 middleware/shared/src/middleware/shared/config/__init__.py delete mode 100644 middleware/shared/src/middleware/shared/config/config_base.py delete mode 100644 middleware/shared/src/middleware/shared/config/config_wrapper.py delete mode 100644 middleware/shared/src/middleware/shared/config/logging.py delete mode 100644 middleware/shared/src/middleware/shared/tracing.py delete mode 100644 middleware/shared/tests/unit/test_api_models.py delete mode 100644 middleware/shared/tests/unit/test_config_base.py delete mode 100644 middleware/shared/tests/unit/test_config_wrapper.py delete mode 100644 middleware/shared/tests/unit/test_logging.py delete mode 100644 middleware/tools/README.md delete mode 100644 middleware/tools/arc2rocrate.py delete mode 100644 middleware/tools/pyproject.toml delete mode 100644 middleware/tools/rocrate2arc.py delete mode 100644 ro_crates/edaphobase.json delete mode 100644 ro_crates/minimal.json delete mode 100644 ro_crates/sample.json diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index ad14971..c05e2e4 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -18,7 +18,7 @@ on: description: 'A JSON string array of components to build' required: false type: string - default: '["api"]' + default: '["sql_to_arc"]' outputs: version: description: 'Calculated version' @@ -145,10 +145,6 @@ jobs: id: component-meta run: | case "${{ matrix.component }}" in - api) - echo "title=FairAgro Advanced Middleware API" >> $GITHUB_OUTPUT - echo "description=Advanced middleware API for FairAgro platform" >> $GITHUB_OUTPUT - ;; sql_to_arc) echo "title=FairAgro SQL to ARC Converter" >> $GITHUB_OUTPUT echo "description=Converts SQL database metadata to ARC format for FairAgro platform" >> $GITHUB_OUTPUT diff --git a/.github/workflows/docker-release.yml b/.github/workflows/docker-release.yml index 8befbac..893fe38 100644 --- a/.github/workflows/docker-release.yml +++ b/.github/workflows/docker-release.yml @@ -39,7 +39,7 @@ jobs: with: push_to_registry: true version_bump: ${{ startsWith(github.ref_name, 'feature/') && 'patch' || (github.event.inputs.version_bump || 'patch') }} - components: '["api", "sql_to_arc"]' # Add other components here in the future, e.g., '["api", "worker"]' + components: '["sql_to_arc"]' # Add other components here in the future, e.g., '["worker"]' secrets: inherit security-scan-release: diff --git a/.github/workflows/helm-release.yml b/.github/workflows/helm-release.yml deleted file mode 100644 index 2683bcd..0000000 --- a/.github/workflows/helm-release.yml +++ /dev/null @@ -1,212 +0,0 @@ -name: Create Helm Chart Release - -on: - workflow_dispatch: - inputs: - version_bump: - description: 'Version bump type for Helm chart release' - required: true - default: 'patch' - type: choice - options: - - major - - minor - - patch - -env: - GIT_USER_NAME: ${{ vars.GIT_USER_NAME || 'GitHub Pipeline' }} - GIT_USER_EMAIL: ${{ vars.GIT_USER_EMAIL || 'github_pipeline@fairagro.net' }} - DOCKERHUB_NAMESPACE: ${{ vars.DOCKERHUB_NAMESPACE || 'zalf' }} - IMAGE_NAME: ${{ vars.IMAGE_NAME || 'fairagro-advanced-middleware-api' }} - GITVERSION_TAG_PREFIX: ${{ vars.GITVERSION_TAG_PREFIX || '.*-chart-v' }} - -jobs: - calculate-version: - runs-on: ubuntu-latest - permissions: - contents: read - outputs: - version: ${{ env.IS_FEATURE_BRANCH == 'true' && steps.gitversion.outputs.semVer || steps.gitversion.outputs.majorMinorPatch }} - env: - IS_FEATURE_BRANCH: ${{ startsWith(github.ref_name, 'feature/') }} - - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - uses: gittools/actions/gitversion/setup@v4 - with: - versionSpec: '6.4.x' - - - name: Create GitVersion config - uses: DamianReeves/write-file-action@master - with: - path: GitVersion.yml - write-mode: overwrite - contents: | - mode: ContinuousDeployment - tag-prefix: '${{ env.GITVERSION_TAG_PREFIX }}' - semantic-version-format: Strict - branches: - main: - label: '' - increment: ${{ inputs.version_bump }} - feature: - regex: ^feature/(?.+)$ - label: '{BranchName}' - increment: Inherit - track-merge-target: false - - - name: Execute GitVersion - id: gitversion - uses: gittools/actions/gitversion/execute@v4 - - - name: Debug version info - run: | - echo "🔧 Helm Version Configuration:" - echo " - Branch: ${{ github.ref_name }}" - echo " - Is feature branch: ${{ env.IS_FEATURE_BRANCH }}" - echo " - Version bump input: ${{ github.event.inputs.version_bump || 'patch (default)' }}" - echo "" - echo "📊 GitVersion Results:" - echo " - SemVer: ${{ steps.gitversion.outputs.semVer }}" - echo " - MajorMinorPatch: ${{ steps.gitversion.outputs.majorMinorPatch }}" - echo " - Final version: ${{ env.IS_FEATURE_BRANCH == 'true' && steps.gitversion.outputs.semVer || steps.gitversion.outputs.majorMinorPatch }}" - - helm-release: - needs: calculate-version - runs-on: ubuntu-latest - permissions: - contents: write - - env: - TIMESTAMP: ${{ github.run_id }}${{ github.run_attempt }} - RELEASE_TAG: ${{ github.run_id }}${{ github.run_attempt }}-chart-v${{ needs.calculate-version.outputs.version }} - CHART_VERSION: ${{ needs.calculate-version.outputs.version }} - DOCKERHUB_AVAILABLE: ${{ secrets.DOCKERHUB_USER != '' && secrets.DOCKERHUB_TOKEN != '' }} - - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - uses: azure/setup-helm@v4 - with: - version: '3.14.0' - - - name: Log release info - run: | - echo "📋 Release prepared (all variables defined in YAML):" - echo " - Release tag: ${{ env.RELEASE_TAG }}" - echo " - Chart version: ${{ env.CHART_VERSION }}" - echo " - App version: (read from Chart.yaml by Helm)" - echo " - DockerHub available: ${{ env.DOCKERHUB_AVAILABLE }}" - - - name: Create version tag - id: create_tag - uses: mathieudutour/github-tag-action@v6.2 - with: - github_token: ${{ secrets.GITHUB_TOKEN }} - custom_tag: ${{ env.RELEASE_TAG }} - tag_prefix: "" - create_annotated_tag: true - - - name: Package Helm chart - id: package_chart - run: | - # Package Helm chart (app-version is read from Chart.yaml automatically) - helm package ./helmchart/fairagro-advanced-middleware-api-chart --version "${{ env.CHART_VERSION }}" - - # Find the actual chart package name (helm package creates name based on Chart.yaml) - CHART_PACKAGE=$(ls *.tgz | head -1) - echo "chart-package=$CHART_PACKAGE" >> $GITHUB_OUTPUT - echo "📦 Created Helm chart package: $CHART_PACKAGE" - - - name: Log release info - run: | - if [[ "${{ github.ref_name }}" == "main" ]]; then - echo "Creating GitHub release for final Helm release" - echo "✅ Git tag: ${{ env.RELEASE_TAG }}" - echo "✅ GitHub release: Will be created" - else - echo "Feature branch release - tracking version progression" - echo "✅ Git tag: ${{ env.RELEASE_TAG }} (for GitVersion tracking)" - echo "⏭️ GitHub release: Skipped (feature branch)" - fi - - - name: Create GitHub Release (Draft) - if: github.ref_name == 'main' - id: draft_release - uses: softprops/action-gh-release@v2 - with: - tag_name: ${{ env.RELEASE_TAG }} - name: "chart-v${{ env.CHART_VERSION }}" - body: | - ## Helm Chart Release v${{ env.CHART_VERSION }} - - **Chart Version**: ${{ env.CHART_VERSION }} - - ```bash - helm upgrade --install fairagro-middleware \ - oci://registry-1.docker.io/zalf/fairagro-advanced-middleware-api-chart \ - --version ${{ env.CHART_VERSION }} - ``` - - *(App version and image tag are defined in the Helm chart's Chart.yaml and values.yaml)* - files: ${{ steps.package_chart.outputs.chart-package }} - draft: true - make_latest: true - generate_release_notes: true - append_body: true - - - name: Finalize Release - if: github.ref_name == 'main' - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - echo "🔍 Looking for draft release with tag: ${{ env.RELEASE_TAG }}" - - # Give GitHub API a moment to index the new release - sleep 5 - - # Try to get the release ID from the draft release we just created - if RELEASE_ID=$(gh api repos/${{ github.repository }}/releases/tags/${{ env.RELEASE_TAG }} --jq '.id' 2>/dev/null); then - echo "✅ Found release ID via tag: $RELEASE_ID" - else - echo "⚠️ Release not found via tag, searching in all releases..." - - # Search for the release by version in all releases - RELEASE_ID=$(gh api repos/${{ github.repository }}/releases --jq '.[] | select(.tag_name == "${{ env.RELEASE_TAG }}") | .id') - - if [[ -n "$RELEASE_ID" ]]; then - echo "✅ Found release ID via search: $RELEASE_ID" - else - echo "❌ Release not found at all!" - exit 1 - fi - fi - - # Convert draft to final release - gh api repos/${{ github.repository }}/releases/$RELEASE_ID \ - --method PATCH \ - --field draft=false - echo "🎉 Release finalized successfully" - - - name: Login to Docker Hub and Push Helm chart - if: github.ref_name == 'main' && env.DOCKERHUB_AVAILABLE == 'true' - run: | - # Login to Helm registry with credentials - echo "🔑 Logging in to Docker Hub registry..." - echo "${{ secrets.DOCKERHUB_TOKEN }}" | helm registry login -u "${{ secrets.DOCKERHUB_USER }}" --password-stdin registry-1.docker.io - - # Push Helm chart to OCI registry - echo "📦 Pushing Helm chart to DockerHub OCI registry..." - helm push ${{ steps.package_chart.outputs.chart-package }} oci://registry-1.docker.io/${{ env.DOCKERHUB_NAMESPACE }} - echo "✅ Pushed to DockerHub: ${{ env.DOCKERHUB_NAMESPACE }}/${{ steps.package_chart.outputs.chart-package }}" - - - name: Skip DockerHub push (missing credentials) - if: github.ref_name == 'main' && env.DOCKERHUB_AVAILABLE != 'true' - run: | - echo "⏭️ Skipping DockerHub push - missing credentials (DOCKERHUB_USER and/or DOCKERHUB_TOKEN)" - echo "💡 Configure DockerHub secrets to enable automatic chart publishing" diff --git a/.vscode/settings.json b/.vscode/settings.json index cf92a92..5cf5c61 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -7,7 +7,7 @@ "python.testing.unittestEnabled": false, "python.testing.pytestEnabled": true, "python.testing.autoTestDiscoverOnSaveEnabled": true, - "python.testing.pytestPath": ".venv/bin/pytest", + "python.testing.pytestPath": "${workspaceFolder}/.venv/bin/pytest", "python.testing.cwd": "${workspaceFolder}", "python-envs.pythonProjects": [], @@ -20,7 +20,7 @@ "source.fixAll.ruff": "explicit" } }, - "python.defaultInterpreterPath": ".venv/bin/python", + "python.defaultInterpreterPath": "${workspaceFolder}/.venv/bin/python", "sops-edit.onlyUseButtons": false, "sops-edit.tempFilePreExtension": "decrypted", diff --git a/dev_environment/compose-external.yaml b/dev_environment/compose-external.yaml deleted file mode 100644 index 5072081..0000000 --- a/dev_environment/compose-external.yaml +++ /dev/null @@ -1,70 +0,0 @@ -services: - postgres: - image: postgres:15 - restart: unless-stopped - environment: - POSTGRES_USER: ${POSTGRES_USER:-postgres} - POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-postgres} - POSTGRES_DB: postgres - ports: - - "5432:5432" - volumes: - - postgres_data_external:/var/lib/postgresql/data - healthcheck: - test: ["CMD-SHELL", "pg_isready -U postgres"] - interval: 5s - timeout: 5s - retries: 10 - command: ["postgres", "-c", "listen_addresses=*"] - - db-init: - image: postgres:15 - depends_on: - postgres: - condition: service_healthy - environment: - PGHOST: postgres - PGUSER: ${POSTGRES_USER:-postgres} - PGPASSWORD: ${POSTGRES_PASSWORD:-postgres} - PGDATABASE: postgres - entrypoint: /bin/bash - command: - - -c - - | - set -euo pipefail - - apt-get update && apt-get install -y --no-install-recommends wget ca-certificates - - echo "Dropping and recreating database edaphobase..." - psql -c "DROP DATABASE IF EXISTS edaphobase;" - psql -c "CREATE DATABASE edaphobase;" - - echo "Downloading and importing Edaphobase dump..." - wget -q -O - https://repo.edaphobase.org/rep/dumps/FAIRagro.sql | \ - PGDATABASE=edaphobase psql - - echo "Database initialization complete." - - sql_to_arc: - image: sql_to_arc:latest - build: - context: .. - dockerfile: docker/Dockerfile.sql_to_arc - depends_on: - db-init: - condition: service_completed_successfully - environment: - SQL_TO_ARC_DB_PASSWORD: ${POSTGRES_PASSWORD:-postgres} - CLIENT_KEY_DATA: ${data} - tmpfs: - - /run/secrets:mode=1777 - volumes: - - ./config-external.yaml:/etc/sql_to_arc/config.yaml:ro - - ./client.crt:/etc/sql_to_arc/client.crt:ro - command: > - sh -c "printf '%s' \"$$CLIENT_KEY_DATA\" > /run/secrets/client.key && - /middleware/sql_to_arc/sql_to_arc -c /etc/sql_to_arc/config.yaml" - restart: no - -volumes: - postgres_data_external: diff --git a/dev_environment/compose.yaml b/dev_environment/compose.yaml index d6a2e5c..5072081 100644 --- a/dev_environment/compose.yaml +++ b/dev_environment/compose.yaml @@ -9,7 +9,7 @@ services: ports: - "5432:5432" volumes: - - postgres_data:/var/lib/postgresql/data + - postgres_data_external:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -U postgres"] interval: 5s @@ -44,7 +44,6 @@ services: PGDATABASE=edaphobase psql echo "Database initialization complete." - restart: unless-stopped sql_to_arc: image: sql_to_arc:latest @@ -54,104 +53,18 @@ services: depends_on: db-init: condition: service_completed_successfully - middleware-api: - condition: service_healthy environment: - # DB credentials from env or secrets SQL_TO_ARC_DB_PASSWORD: ${POSTGRES_PASSWORD:-postgres} - # secrets: - # - client_key + CLIENT_KEY_DATA: ${data} + tmpfs: + - /run/secrets:mode=1777 volumes: - # - ./client.crt:/run/secrets/client.crt:ro - - ./config.yaml:/etc/sql_to_arc/config.yaml:ro - command: ["/middleware/sql_to_arc/sql_to_arc", "-c", "/etc/sql_to_arc/config.yaml"] - # command: sleep 3600 - restart: "no" - - rabbitmq: - image: rabbitmq:3-management - ports: - - "5672:5672" - - "15672:15672" - environment: - - RABBITMQ_DEFAULT_USER - - RABBITMQ_DEFAULT_PASS - healthcheck: - test: ["CMD", "rabbitmqctl", "status"] - interval: 10s - timeout: 5s - retries: 5 - restart: unless-stopped - - redis: - image: redis:7 - ports: - - "6379:6379" - healthcheck: - test: ["CMD", "redis-cli", "ping"] - interval: 5s - timeout: 3s - retries: 5 - restart: unless-stopped - - celery-worker: - image: middleware-api:latest - build: - context: .. - dockerfile: docker/Dockerfile.api - depends_on: - rabbitmq: - condition: service_healthy - redis: - condition: service_healthy - middleware-api: - condition: service_started - environment: - - MIDDLEWARE_API_CONFIG=/run/secrets/middleware-api-config - - CELERY_BROKER_URL - - CELERY_RESULT_BACKEND - - GITLAB_API_TOKEN - - GIT_REPO_TOKEN - volumes: - - ./middleware-api-config.yaml:/run/secrets/middleware-api-config:ro - # Command to run celery worker - command: [ - "/api/middleware-api/middleware-api", - "celery", - "-A", "middleware.api.celery_app", - "worker", - "--loglevel=info", - "--concurrency=8",] - restart: unless-stopped - - middleware-api: - image: middleware-api:latest - build: - context: .. - dockerfile: docker/Dockerfile.api - depends_on: - postgres: - condition: service_healthy - rabbitmq: - condition: service_healthy - redis: - condition: service_healthy - ports: - - "8000:8000" - environment: - - CELERY_BROKER_URL - - CELERY_RESULT_BACKEND - - MIDDLEWARE_API_CONFIG=/run/secrets/middleware-api-config - volumes: - - ./middleware-api-config.yaml:/run/secrets/middleware-api-config:ro - # Run uvicorn directly (default command of the binary runs uvicorn) - command: ["/api/middleware-api/middleware-api", "--host", "0.0.0.0", "--port", "8000"] - # command: sleep 3600 - restart: unless-stopped - -# secrets: -# client_key: -# environment: data + - ./config-external.yaml:/etc/sql_to_arc/config.yaml:ro + - ./client.crt:/etc/sql_to_arc/client.crt:ro + command: > + sh -c "printf '%s' \"$$CLIENT_KEY_DATA\" > /run/secrets/client.key && + /middleware/sql_to_arc/sql_to_arc -c /etc/sql_to_arc/config.yaml" + restart: no volumes: - postgres_data: + postgres_data_external: diff --git a/dev_environment/config-external.yaml b/dev_environment/config-external.yaml deleted file mode 100644 index 4f21934..0000000 --- a/dev_environment/config-external.yaml +++ /dev/null @@ -1,22 +0,0 @@ -log_level: INFO - -db_name: edaphobase -db_user: postgres -db_password: ~ -db_host: postgres # Docker service name -db_port: 5432 - -rdi: edaphobase -rdi_url: https://edaphobase.org -max_concurrent_arc_builds: 12 - -api_client: - # NOTE: Change this to the external Middleware API URL - api_url: "https://middleware.fairagro.net" - timeout: 600 - client_cert_path: "/etc/sql_to_arc/client.crt" - client_key_path: "/run/secrets/client.key" - verify_ssl: true - -otel: - log_console_spans: false diff --git a/dev_environment/config.yaml b/dev_environment/config.yaml index b861143..4f21934 100644 --- a/dev_environment/config.yaml +++ b/dev_environment/config.yaml @@ -1,21 +1,22 @@ -log_level: DEBUG +log_level: INFO db_name: edaphobase db_user: postgres db_password: ~ db_host: postgres # Docker service name +db_port: 5432 rdi: edaphobase rdi_url: https://edaphobase.org -max_concurrent_arc_builds: 8 # Build up to 4 ARCs in parallel per batch +max_concurrent_arc_builds: 12 api_client: - api_url: "http://middleware-api:8000" - timeout: 600 # seconds - client_cert_path: ~ - client_key_path: ~ + # NOTE: Change this to the external Middleware API URL + api_url: "https://middleware.fairagro.net" + timeout: 600 + client_cert_path: "/etc/sql_to_arc/client.crt" + client_key_path: "/run/secrets/client.key" + verify_ssl: true otel: - # endpoint: http://signoz:4318 # Uncomment to enable Signoz tracing - log_console_spans: false # Set to false to disable SPAN logging to console - log_level: INFO + log_console_spans: false diff --git a/dev_environment/middleware-api-config.yaml b/dev_environment/middleware-api-config.yaml deleted file mode 100644 index c2d8f20..0000000 --- a/dev_environment/middleware-api-config.yaml +++ /dev/null @@ -1,26 +0,0 @@ -log_level: DEBUG - -# gitlab_api: -# group: FAIRagro-advanced-middleware-dev -# url: https://datahub-dev.ipk-gatersleben.de -# max_workers: 10 -# commit_chunk_size: 200 -git_repo: - url: https://datahub-dev.ipk-gatersleben.de - group: FAIRagro-advanced-middleware-dev - max_workers: 10 -known_rdis: -- bonares -- edal -- edaphobase -- openagrar -- publisso -- thunen_atlas -require_client_cert: false - -celery: {} - -otel: - # endpoint: http://signoz:4318 # Uncomment to enable Signoz tracing - # log_console_spans: true # Enable SPAN logging to console - # log_level: INFO diff --git a/dev_environment/secrets.enc.yaml b/dev_environment/secrets.enc.yaml deleted file mode 100644 index f59651e..0000000 --- a/dev_environment/secrets.enc.yaml +++ /dev/null @@ -1,34 +0,0 @@ -GITLAB_API_TOKEN: ENC[AES256_GCM,data:PsdSbY646I8QGzc4TULV/FSQXzCUkYQlLg7PBuEyFxDJTFB0qbbWn3uA7I4LSBC7K1Wa,iv:kJi4u77/nn5PTATZamMCQFX8Wg1htRyYsbNC7V2xY8E=,tag:lXjgQpsGYm8CuADIeq+Org==,type:str] -GIT_REPO_TOKEN: ENC[AES256_GCM,data:81ucul3QImACQzZ19xMUZCp99wN4aWiMqdKeC3vZxA88lHZuBh+5yNbC5tCe8scimVAT,iv:77iQTDAapvPxYbs9zqMDa62QjQxQA6H56Q3KbnhnEDQ=,tag:CSdOJtjNu9uI9XvpmpSF4g==,type:str] -CELERY_BROKER_URL: ENC[AES256_GCM,data:85FwsqD01riI9k+RnfikbvHFlTgVWAY0PLXS4gC8F5SQhg==,iv:DHmIeafs/BhXfX0MOTMzuU9C4DRcRmV2AEkk7vMtef8=,tag:CC7tqaA/OHuaDSg+7NbhdA==,type:str] -CELERY_RESULT_BACKEND: ENC[AES256_GCM,data:Z9dEOZ3ASgdMfpCEizEcyj8mnOo=,iv:KiuGB2WKRvt+z7q8qUs8sdT8tgx0uKjRHXF7au2EA30=,tag:aDDIa39ou4YYC2j3o9Duxw==,type:str] -RABBITMQ_DEFAULT_USER: ENC[AES256_GCM,data:TxRl2FA=,iv:llvveExHlChtoDv96UMAiRt4+GjgHbrv6M1CnxRkKcE=,tag:FOgz0tAqb05gTbD9T1pH5Q==,type:str] -RABBITMQ_DEFAULT_PASS: ENC[AES256_GCM,data:+4j3q+g=,iv:ArOpiMdap7jKtNyZYQlvfprD+zGitdcpjNOmjkKVBZs=,tag:ARhv5cI4FtJ+DDp8lv+9sQ==,type:str] -sops: - lastmodified: "2026-01-14T14:34:36Z" - mac: ENC[AES256_GCM,data:SNyy53F3Gi/fp6qK3bw3j8C8ICR/vIrXWl0ICXni5oi/2GbZpAb6Eu97AGTCs/nZqq6WR6ySDGlo34DIQkqAPv7UIGLvkjYFJdbD76c++EjHktk2Wmf+JOPvEw48jlrHmlVWic3rxfo1Z+LjvNyxCkwfdhLZ0rYGknF8p4bXPTQ=,iv:RW751TsBBWeZJr5G5OF95rkfqIR81bPbfbr5FUi1IiU=,tag:lqC5O1/KyDafOsH6ksV2sQ==,type:str] - pgp: - - created_at: "2026-01-14T14:34:36Z" - enc: |- - -----BEGIN PGP MESSAGE----- - - hF4Df+t0WwSeCuMSAQdAF4NsQyGi5a+dVPg57TLn5HltyN44crR2hMnvcRtUkEQw - kLWLpTTwWslkFQw+WLAqKQvU2Ta1xflkOdy3Ooo4ynoHXnMpsE1jNZUZD6s8h1w9 - 0lwBdBiFbAmvJGhV8KG0R0ZMLSUx4161OKW/Gx7iemhuvHAROAUpiV7yiJ6FOMhM - p21Qiw8oa9E5M1lqVMHLvJF+SzciGZDvMUtbsbDHNmPpcyORt83cSd4UEQwr6Q== - =4KvP - -----END PGP MESSAGE----- - fp: 37D38A6C0248214B007B6C5685E825F3377228D6 - - created_at: "2026-01-14T14:34:36Z" - enc: |- - -----BEGIN PGP MESSAGE----- - - hF4D5jdJleHfCY0SAQdAExTeQwD7BdHClk5PycSsiEiVpc0Ydgt1rQ+oRU+BRyMw - UjQyaX+FixeCv3khlsje4/ZKW4xpxi4bvs5CsYmWu4C9+io/PK/TphIvCB4LEDmv - 0lwBcuCyKkN+ZrvNFQ+y09/D7eCOUBcll+T6JDBwxvbH48wB9f7jUFvgvq5jBJBU - FpHxi0HoTID6dqBip61QoQN3KmkFgQaUPZBxE50Ux1N0kqzsH+vzWgLgG6++Kg== - =fxgy - -----END PGP MESSAGE----- - fp: CC7B10CE8D78010ABB043F8DB1C462E90012ECFE - unencrypted_suffix: _unencrypted - version: 3.11.0 diff --git a/dev_environment/start-external.sh b/dev_environment/start-external.sh deleted file mode 100755 index 70e7a26..0000000 --- a/dev_environment/start-external.sh +++ /dev/null @@ -1,41 +0,0 @@ -#!/usr/bin/env bash -# -# Start sql_to_arc locally with a local DB, but connecting to an EXTERNAL Middleware API. -# -# Usage: -# ./start-external.sh # Start services -# ./start-external.sh --build # Build images and start -# - -set -euo pipefail - -script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -cd "$script_dir" - -# Parse arguments -BUILD_FLAG="" -if [[ "${1:-}" == "--build" ]]; then - BUILD_FLAG="--build" -fi - -echo "==> Starting SQL-to-ARC with EXTERNAL API..." -echo " - Local PostgreSQL will be started" -echo " - Database will be initialized with Edaphobase dump" -echo " - SQL-to-ARC will connect to the API configured in config-external.yaml" -echo " - Using client certificates: client.crt, client.key" -echo "" - -if [[ ! -f "client.key" ]]; then - echo "ERROR: client.key not found. Please provide your client key." - exit 1 -fi - -# Use sops exec-env to pass the decrypted key as an environment variable -# without writing it to a physical disk file. -sops exec-env "${script_dir}/client.key" \ - "docker compose -f compose-external.yaml up $BUILD_FLAG" - -echo "" -echo "==> Services finished!" -echo " - View logs: docker compose -f compose-external.yaml logs" -echo " - Clean up: docker compose -f compose-external.yaml down" diff --git a/dev_environment/start.sh b/dev_environment/start.sh index 2b642c2..70e7a26 100755 --- a/dev_environment/start.sh +++ b/dev_environment/start.sh @@ -1,10 +1,10 @@ #!/usr/bin/env bash # -# Start the development environment with encrypted secrets +# Start sql_to_arc locally with a local DB, but connecting to an EXTERNAL Middleware API. # # Usage: -# ./start.sh # Start all services -# ./start.sh --build # Build images and start +# ./start-external.sh # Start services +# ./start-external.sh --build # Build images and start # set -euo pipefail @@ -18,30 +18,24 @@ if [[ "${1:-}" == "--build" ]]; then BUILD_FLAG="--build" fi -echo "==> Starting development environment..." -echo " - PostgreSQL will be started" +echo "==> Starting SQL-to-ARC with EXTERNAL API..." +echo " - Local PostgreSQL will be started" echo " - Database will be initialized with Edaphobase dump" -echo " - SQL-to-ARC converter will run after initialization" +echo " - SQL-to-ARC will connect to the API configured in config-external.yaml" +echo " - Using client certificates: client.crt, client.key" echo "" -# Check if sops is available -if ! command -v sops &> /dev/null; then - echo "ERROR: sops is not installed or not in PATH" - echo "Install sops: https://github.com/getsops/sops" +if [[ ! -f "client.key" ]]; then + echo "ERROR: client.key not found. Please provide your client key." exit 1 fi -echo "==> Starting services with sops exec-env..." -echo " Environment variable 'data' will contain decrypted client.key" -echo "" - -# Use sops exec-env to decrypt and run docker compose -# We need to preserve TERM and PATH for proper terminal support -# Use exec-env without --pristine but ensure minimal env pollution -sops exec-env "${script_dir}/secrets.enc.yaml" \ - "docker compose up $BUILD_FLAG" +# Use sops exec-env to pass the decrypted key as an environment variable +# without writing it to a physical disk file. +sops exec-env "${script_dir}/client.key" \ + "docker compose -f compose-external.yaml up $BUILD_FLAG" echo "" echo "==> Services finished!" -echo " - View logs: docker compose logs" -echo " - Clean up: docker compose down" +echo " - View logs: docker compose -f compose-external.yaml logs" +echo " - Clean up: docker compose -f compose-external.yaml down" diff --git a/docker/Dockerfile.api b/docker/Dockerfile.api deleted file mode 100644 index 2d6d673..0000000 --- a/docker/Dockerfile.api +++ /dev/null @@ -1,109 +0,0 @@ -# ---- Package Build Stage ---- -FROM python:3.12.12-alpine3.23 AS package-builder - -WORKDIR /build - -# Copy project files needed for package build -COPY pyproject.toml uv.lock ./ -COPY middleware ./middleware - -# Upgrade pip and install uv -RUN pip install --no-cache-dir --upgrade pip==25.3 uv==0.9.27 - -# Build shared package first (dependency of api) -RUN uv build --package shared --wheel - -# Build the API package as wheel -RUN uv build --package api --wheel - - -# ---- Binary Build Stage ---- -FROM python:3.12.12-alpine3.23 AS binary-builder - -# Install build tools for PyInstaller -RUN apk add --no-cache \ - build-base=0.5-r3 \ - python3-dev=3.12.12-r0 \ - libffi-dev=3.5.2-r0 \ - openssl-dev=3.5.5-r0 \ - cargo=1.91.1-r0 - -WORKDIR /build - -# Install uv and PyInstaller -RUN pip install --no-cache-dir --upgrade pip==25.3 uv==0.9.27 - -# Copy built wheel from package-builder stage -COPY --from=package-builder /build/dist/*.whl /tmp/wheels/ - -# Install the API package from wheel -RUN uv pip install --system /tmp/wheels/*.whl - -# Install PyInstaller -RUN uv pip install --system pyinstaller - -# Build standalone binary with PyInstaller -RUN pyinstaller --onedir \ - --name middleware-api \ - --hidden-import "celery.app.amqp" \ - --hidden-import "celery.app.control" \ - --hidden-import "celery.app.events" \ - --hidden-import "celery.app.log" \ - --hidden-import "celery.apps.worker" \ - --hidden-import "celery.backends.redis" \ - --hidden-import "celery.concurrency.prefork" \ - --hidden-import "celery.events.state" \ - --hidden-import "celery.fixups" \ - --hidden-import "celery.fixups.django" \ - --hidden-import "celery.loaders.app" \ - --hidden-import "celery.worker.autoscale" \ - --hidden-import "celery.worker.components" \ - --hidden-import "celery.worker.consumer" \ - --hidden-import "celery.worker.consumer.delayed_delivery" \ - --hidden-import "celery.worker.strategy" \ - --hidden-import "kombu.transport.pyamqp" \ - --hidden-import "uvicorn.workers" \ - --copy-metadata celery \ - --copy-metadata opentelemetry-api \ - --copy-metadata opentelemetry-instrumentation \ - --copy-metadata opentelemetry-instrumentation-fastapi \ - --copy-metadata opentelemetry-instrumentation-requests \ - --copy-metadata opentelemetry-sdk \ - --copy-metadata requests \ - --copy-metadata starlette \ - $(python -c "import middleware.api; print(middleware.api.__file__.replace('__init__.py', 'main.py'))") - - -# ---- Runtime Stage ---- -FROM alpine:3.23.3 - -WORKDIR /api - -ENV UVICORN_HOST=0.0.0.0 -ENV UVICORN_PORT=8000 -ENV GUNICORN_WORKERS=0 -ENV GUNICORN_LOG_LEVEL=info - -COPY --from=binary-builder /build/dist/middleware-api /api/middleware-api - -# Create non-root user and group and fix permissions -RUN apk add --no-cache curl=8.17.0-r1 git=2.52.0-r0 tzdata \ - && addgroup -S middleware && adduser -S middleware -G middleware \ - && chown -R middleware:middleware /api - -# Configure Git system-wide (before switching user) to ensure settings apply -# http.postBuffer: 500MB buffer for large requests -# http.version: Force HTTP/1.1 as HTTP/2 can be unstable -# http.keepAlive: Disable to prevent unexpected connection drops -RUN git config --system http.postBuffer 524288000 \ - && git config --system http.version HTTP/1.1 \ - && git config --system http.keepAlive false - -USER middleware - -EXPOSE $UVICORN_PORT - -CMD ["/api/middleware-api/middleware-api"] - -HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \ - CMD ["sh", "-c", "curl -f http://127.0.0.1:${UVICORN_PORT}/v1/liveness"] diff --git a/docker/container-structure-tests/api.yaml b/docker/container-structure-tests/api.yaml deleted file mode 100644 index 1a0a203..0000000 --- a/docker/container-structure-tests/api.yaml +++ /dev/null @@ -1,88 +0,0 @@ -# Container Structure Test Configuration -# Tests the built Docker image for security, structure, and functionality -# Documentation: https://github.com/GoogleContainerTools/container-structure-test - -schemaVersion: 2.0.0 - -# File existence tests -fileExistenceTests: - - name: 'Middleware API executable' - path: '/api/middleware-api/middleware-api' - shouldExist: true - permissions: '-rwxr-xr-x' # Executable - - - name: 'No Python wheels in final image' - path: '/tmp/wheels' - shouldExist: false # Wheels should not leak into runtime - - - name: 'No source code in final image' - path: '/build' - shouldExist: false # Build artifacts should not be in runtime - - - name: 'No middleware directory in final image' - path: '/middleware' - shouldExist: false # Source code should not be in runtime - -# File content tests -fileContentTests: - - name: 'Non-root user created' - path: '/etc/passwd' - expectedContents: ['middleware:.*'] - - - name: 'Non-root group created' - path: '/etc/group' - expectedContents: ['middleware:.*'] - -# Command tests -commandTests: - - name: 'Curl is available for health checks' - command: 'sh' - args: ['-lc', 'command -v curl'] - exitCode: 0 - - - name: 'Python is not in final image (security)' - command: 'sh' - args: ['-lc', 'command -v python3'] - exitCode: 127 # Command not found - Python not in final image (security) - - - name: 'Pip is not in final image (security)' - command: 'sh' - args: ['-lc', 'command -v pip'] - exitCode: 127 # pip should not be in runtime for security - - - name: 'UV is not in final image (security)' - command: 'sh' - args: ['-lc', 'command -v uv'] - exitCode: 127 # uv should not be in runtime - - - name: 'PyInstaller is not in final image (security)' - command: 'sh' - args: ['-lc', 'command -v pyinstaller'] - exitCode: 127 # pyinstaller should not be in runtime - - - name: 'Middleware API binary is executable' - command: 'test' - args: ['-x', '/api/middleware-api'] - exitCode: 0 - - - name: 'Minimal package count (Alpine security)' - command: 'sh' - args: ['-lc', 'apk info | wc -l'] - exitCode: 0 - # Alpine should have minimal packages (typically < 20 for this image) - - - name: 'Environment variable UVICORN_HOST is set' - command: 'sh' - args: ['-lc', 'echo $UVICORN_HOST'] - expectedOutput: ['0.0.0.0'] - - - name: 'Environment variable UVICORN_PORT is set' - command: 'sh' - args: ['-lc', 'echo $UVICORN_PORT'] - expectedOutput: ['8000'] - -# Metadata tests -metadataTest: - exposedPorts: ['8000'] - user: 'middleware' - workdir: '/api' diff --git a/middleware/sql_to_arc/ARCHITECTURAL_DESIGN.md b/docs/ARCHITECTURAL_DESIGN.md similarity index 100% rename from middleware/sql_to_arc/ARCHITECTURAL_DESIGN.md rename to docs/ARCHITECTURAL_DESIGN.md diff --git a/docs/client_certificates.md b/docs/client_certificates.md deleted file mode 100644 index 40b6633..0000000 --- a/docs/client_certificates.md +++ /dev/null @@ -1,9 +0,0 @@ -# FAIRagro middleware client certificates - -FAIRagro middleware clients require a client certificate for authentication authorization. - -Creating new certificates require access to the CA (certification authority) database that is stored -in FAIRagro nextcloud. - -For a detailed description of the FAIRagro CA, please refer to the corresponding README file on -nextcloud. diff --git a/docs/gitlab_related.md b/docs/gitlab_related.md deleted file mode 100644 index d853592..0000000 --- a/docs/gitlab_related.md +++ /dev/null @@ -1,25 +0,0 @@ -# Gitlab considerations - -The FAIRagro advanced middleware API will access the gitlab API (in case you use -the `GitlabApi` `ArcStore`) via https. Therefore it needs a group access token. - -To prepare your Gitlab/Datahub instance to interoperate with the FAIRagro advanced -middle, please perform the following steps: - -1. Create a private Gitlab group -2. Navigate to the group settings and create a group access token with the following - features: - - * scopes: api, write_repository - * role: owner/maintainer - - Note: if you would like to run the integration tests defined within this repo, - you're access token needs to have the owner role, because the tests will delete - all repos to be in a deterministic state. In a prodoctive scenario, the maintainer - role will be sufficient. - -3. Add the add group (as well as the URL of your DataHub instance) to your FAIRagro - advanced middleware config file -4. Define the environment variable `GITLAB_API_TOKEN` and assign the created group - access token. Note: for testing purpose you may create a corresponding `.env` file - in the project main directory. diff --git a/docs/helmchart_related.md b/docs/helmchart_related.md deleted file mode 100644 index 727b486..0000000 --- a/docs/helmchart_related.md +++ /dev/null @@ -1,137 +0,0 @@ -# On helm charts - -## Helm chart testing - -This section is about a local test installation of the middleware api using helm. -The needed tools are included in the dev container. - -### Preparations - -Some files need for the test installation can be found in the folder `helmchart/advanced-middleware-api`. -First we will need to create a temporary self-signed server certificate in this folder: - -```bash -FQDN=chart-example.local - -cat > helmchart/fairagro-advanced-middleware-api-chart/server.conf < helmchart/fairagro-advanced-middleware-api-chart/client_ext.conf </dev/null | grep -A 20 "HTTP/1.1" -``` - -**Expected successful response:** - -```json -{"client_id":"chart-example-client","message":"Client authenticated successfully"} -``` diff --git a/docs/python_related.md b/docs/python_related.md deleted file mode 100644 index ff3a866..0000000 --- a/docs/python_related.md +++ /dev/null @@ -1,160 +0,0 @@ -# Python-related technical information on this project - -This project uses [`uv`](https://docs.astral.sh/uv/) for depedency management -and [`uvicorn`](https://www.uvicorn.org/) as minial API server. - -This is a small how to for the case that you've never used these tools before -(just like me). - -## The `uv` workflow - -It's worth to note here, that `uv` distinguishes between "primary" dependencies -and dependencies of dependencies. "Primary" dependencies are those that you -actual intend to use in your own code and that you install on purpose. -The dependencies of these "primary" dependencies are then installed -automatically. - -"Primary" dependencies are managed in the file `pyproject.toml`, whereas -dependencies of dependencies are managed in the file `uv.lock`. - -### First steps after cloning the project - -Create a python virtual environment and activate it: - -```bash -uv venv -. .venv/bin/activate -``` - -Install all standard dependencies (from `uv.lock`): - -```bash -uv sync -``` - -Install also development dependencies: - -```bash -uv sync --dev -``` - -### Modify dependencies - -Add/Delete a primary dependency: - -```bash -uv add -uv delete -``` - -By passing `--dev` you do not modify the standard dependencies, but -development dependencies. - -Update the `uv.lock` file: - -```bash -uv lock -``` - -You can also upgrade the locked dependencies to the newest version -that match those in `pyproject.toml`: - -```bash -uv lock --upgrade -``` - -If you just would like to install an arbitary python package that should -not be part of the project dependencies: - -```bash -uv pip install -``` - -## Testing the FAIRagro middleware API - -There's are bunch of possibilities to run the FAIRagro middleware service in -a test environment. Note that we assume in all cases that the current working -directory is the project base directory (i.e. the one that has been created -by `git clone`). - -The middleware will listen on `http://0.0.0.0:8000` by default. You can -change this by passing the command line args `--host` and/or `--port`. -You will be able to access a swagger API browser by appending `/docs` to the -URL. - -### Configuring the middleware API - -The middleware API needs a config file that is looked for in -`/run/secrets/middleware-api-config` by default. This is not a very practical -path for test runs, though. To override it, please do something like: - -```bash -export MIDDLEWARE_API_CONFIG=example_config.yaml -``` - -Having a look at the file `example_config.yaml` you will notice that the gitlab -API token is missing, as we do not want to commit a secret to git. Nevertheless -the token is needed, otherwise the middleware API won't start. So you can either -define the variable `GITLAB_API_TOKEN` manually, or reuse the the token used for -integration tests that should have already been decrypted by the script `load-env.sh`: - -```bash -source .env -``` - -### Running by executing `main.py` - -We can start the middleware api by executing the python main file: - -```bash -python middleware/api/src/middleware/api/main.py -``` - -### Running the `middleware_api.main` module - -We can also execute the `middleware_api.main` module: - -```bash -python -m middleware.api.main -``` - -### Running via `uvicorn` - -To run the middleware api via `uvicorn` command line tool: - -```bash -uvicorn middleware.api.api:app -``` - -### Running via `fastapi` - -To run the middleware api via `fastapi` command line tool: - -```bash -fastapi run middleware/api/src/middleware/api/api.py --app app -``` - -### Using a local docker image - -We can also build an run the docker image: - -```bash -docker build -f docker/Dockerfile.api . -t middleware-api -docker run \ - -v $(pwd)/example_config.yaml:/run/secrets/middleware-api-config \ - -e GITLAB_API_TOKEN \ - -p 8000:8000 \ - middleware-api -``` - -### Using the official docker image - -To use an official middleware release: - -```bash -docker run \ - -v $(pwd)/example_config.yaml:/run/secrets/middleware-api-config \ - -e GITLAB_API_TOKEN \ - -p 8000:8000 \ - zalf/fairagro-advanced-middleware-api:latest -``` diff --git a/helmchart/fairagro-advanced-middleware-api-chart/.helmignore b/helmchart/fairagro-advanced-middleware-api-chart/.helmignore deleted file mode 100644 index 0e8a0eb..0000000 --- a/helmchart/fairagro-advanced-middleware-api-chart/.helmignore +++ /dev/null @@ -1,23 +0,0 @@ -# Patterns to ignore when building packages. -# This supports shell glob matching, relative path matching, and -# negation (prefixed with !). Only one pattern per line. -.DS_Store -# Common VCS dirs -.git/ -.gitignore -.bzr/ -.bzrignore -.hg/ -.hgignore -.svn/ -# Common backup files -*.swp -*.bak -*.tmp -*.orig -*~ -# Various IDEs -.project -.idea/ -*.tmproj -.vscode/ diff --git a/helmchart/fairagro-advanced-middleware-api-chart/Chart.yaml b/helmchart/fairagro-advanced-middleware-api-chart/Chart.yaml deleted file mode 100644 index 1697816..0000000 --- a/helmchart/fairagro-advanced-middleware-api-chart/Chart.yaml +++ /dev/null @@ -1,24 +0,0 @@ -apiVersion: v2 -name: fairagro-advanced-middleware-api-chart -description: A Helm chart for the FAIRagro advanced middleware API - -# A chart can be either an 'application' or a 'library' chart. -# -# Application charts are a collection of templates that can be packaged into versioned archives -# to be deployed. -# -# Library charts provide useful utilities or functions for the chart developer. They're included as -# a dependency of application charts to inject those utilities and functions into the rendering -# pipeline. Library charts do not define any templates and therefore cannot be deployed. -type: application - -# This is the chart version. This version number should be incremented each time you make changes -# to the chart and its templates, including the app version. -# Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 0.2.0 - -# This is the version number of the application being deployed. This version number should be -# incremented each time you make changes to the application. Versions are not expected to -# follow Semantic Versioning. They should reflect the version the application is using. -# It is recommended to use it with quotes. -appVersion: "0.3.0" diff --git a/helmchart/fairagro-advanced-middleware-api-chart/templates/NOTES.txt b/helmchart/fairagro-advanced-middleware-api-chart/templates/NOTES.txt deleted file mode 100644 index e4b24e2..0000000 --- a/helmchart/fairagro-advanced-middleware-api-chart/templates/NOTES.txt +++ /dev/null @@ -1,22 +0,0 @@ -1. Get the application URL by running these commands: -{{- if .Values.api.ingress.enabled }} -{{- range $host := .Values.api.ingress.hosts }} - {{- range .paths }} - http{{ if $.Values.api.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }} - {{- end }} -{{- end }} -{{- else if contains "NodePort" .Values.api.service.type }} - export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "fairagro-advanced-middleware-api-chart.fullname" . }}) - export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") - echo http://$NODE_IP:$NODE_PORT -{{- else if contains "LoadBalancer" .Values.api.service.type }} - NOTE: It may take a few minutes for the LoadBalancer IP to be available. - You can watch its status by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "fairagro-advanced-middleware-api-chart.fullname" . }}' - export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "fairagro-advanced-middleware-api-chart.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") - echo http://$SERVICE_IP:{{ .Values.api.service.port }} -{{- else if contains "ClusterIP" .Values.api.service.type }} - export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "fairagro-advanced-middleware-api-chart.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") - export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}") - echo "Visit http://127.0.0.1:8080 to use your application" - kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT -{{- end }} diff --git a/helmchart/fairagro-advanced-middleware-api-chart/templates/_helpers.tpl b/helmchart/fairagro-advanced-middleware-api-chart/templates/_helpers.tpl deleted file mode 100644 index dbc5941..0000000 --- a/helmchart/fairagro-advanced-middleware-api-chart/templates/_helpers.tpl +++ /dev/null @@ -1,126 +0,0 @@ -{{/* -Expand the name of the chart. -*/}} -{{- define "fairagro-advanced-middleware-api-chart.name" -}} -{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} -{{- end }} - -{{/* -Create a default fully qualified app name. -We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). -If release name contains chart name it will be used as a full name. -*/}} -{{- define "fairagro-advanced-middleware-api-chart.fullname" -}} -{{- if .Values.fullnameOverride }} -{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} -{{- else }} -{{- $name := default .Chart.Name .Values.nameOverride }} -{{- if contains $name .Release.Name }} -{{- .Release.Name | trunc 63 | trimSuffix "-" }} -{{- else }} -{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} -{{- end }} -{{- end }} -{{- end }} - -{{/* -Create chart name and version as used by the chart label. -*/}} -{{- define "fairagro-advanced-middleware-api-chart.chart" -}} -{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} -{{- end }} - -{{/* -Common labels -*/}} -{{- define "fairagro-advanced-middleware-api-chart.labels" -}} -helm.sh/chart: {{ include "fairagro-advanced-middleware-api-chart.chart" . }} -{{ include "fairagro-advanced-middleware-api-chart.selectorLabels" . }} -{{- if .Chart.AppVersion }} -app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} -{{- end }} -app.kubernetes.io/managed-by: {{ .Release.Service }} -{{- end }} - -{{/* -Selector labels -*/}} -{{- define "fairagro-advanced-middleware-api-chart.selectorLabels" -}} -app.kubernetes.io/name: {{ include "fairagro-advanced-middleware-api-chart.name" . }} -app.kubernetes.io/instance: {{ .Release.Name }} -{{- end }} - -{{/* -Create the name of the service account to use -*/}} -{{- define "fairagro-advanced-middleware-api-chart.serviceAccountName" -}} -{{- default (include "fairagro-advanced-middleware-api-chart.fullname" .) .Values.api.serviceAccount.name }} -{{- end }} - -{{/* -Create the name of the Celery worker service account to use -*/}} -{{- define "fairagro-advanced-middleware-api-chart.celeryServiceAccountName" -}} -{{- if .Values.celery.worker.serviceAccount.name }} -{{- .Values.celery.worker.serviceAccount.name }} -{{- else }} -{{- printf "%s-celery-worker" (include "fairagro-advanced-middleware-api-chart.fullname" .) }} -{{- end }} -{{- end }} - -{{/* -Create the name of the CA TLS secret -*/}} -{{- define "fairagro-advanced-middleware-api-chart.caTlsSecretName" -}} -{{- if .Values.api.tls.caTlsSecretName }} -{{- .Values.api.tls.caTlsSecretName }} -{{- else }} -{{- include "fairagro-advanced-middleware-api-chart.fullname" . }}-ca-secret -{{- end }} -{{- end }} - -{{/* -Compute Celery broker URL based on enabled RabbitMQ or provided override. -*/}} -{{- define "fairagro-advanced-middleware-api-chart.celeryBrokerUrl" -}} -{{- $fullname := include "fairagro-advanced-middleware-api-chart.fullname" . -}} -{{- $brokerOverride := .Values.celery.brokerUrl -}} -{{- if .Values.rabbitmq.enabled -}} - {{- $rabbitAuth := default (dict) .Values.rabbitmq.auth -}} - {{- $user := default "" $rabbitAuth.username -}} - {{- $pass := default "" $rabbitAuth.password -}} - {{- $userEsc := urlquery (default "guest" $user) -}} - {{- $passEsc := urlquery (default "guest" $pass) -}} - {{- $existing := default "" $rabbitAuth.existingSecret -}} - {{- if and $existing (or (eq $user "") (eq $pass "")) -}} - {{- required "Provide rabbitmq.auth.username/password when rabbitmq.auth.existingSecret is set, or set celery.brokerUrl" $brokerOverride -}} - {{- else -}} - {{- printf "amqp://%s:%s@%s-rabbitmq:5672//" $userEsc $passEsc $fullname -}} - {{- end -}} -{{- else -}} -{{- required "Set celery.brokerUrl when rabbitmq.enabled=false" $brokerOverride -}} -{{- end -}} -{{- end }} - -{{/* -Compute Celery result backend based on enabled Redis or provided override. -*/}} -{{- define "fairagro-advanced-middleware-api-chart.celeryResultBackend" -}} -{{- $fullname := include "fairagro-advanced-middleware-api-chart.fullname" . -}} -{{- $backendOverride := default .Values.celery.resultBackend .Values.resultBackend -}} -{{- if .Values.redis.enabled -}} - {{- $redisAuth := default (dict) .Values.redis.auth -}} - {{- $pass := default "" $redisAuth.password -}} - {{- $passEsc := urlquery $pass -}} - {{- $existing := default "" $redisAuth.existingSecret -}} - {{- if and $existing (eq $pass "") -}} - {{- required "Provide redis.auth.password when redis.auth.existingSecret is set, or set resultBackend" $backendOverride -}} - {{- else if $pass -}} - {{- printf "redis://:%s@%s-redis:6379/0" $passEsc $fullname -}} - {{- else -}} - {{- printf "redis://%s-redis:6379/0" $fullname -}} - {{- end -}} -{{- else -}} -{{- required "Set resultBackend when redis.enabled=false" $backendOverride -}} -{{- end -}} -{{- end }} diff --git a/helmchart/fairagro-advanced-middleware-api-chart/templates/arc-cache-pvc.yaml b/helmchart/fairagro-advanced-middleware-api-chart/templates/arc-cache-pvc.yaml deleted file mode 100644 index bdf77ad..0000000 --- a/helmchart/fairagro-advanced-middleware-api-chart/templates/arc-cache-pvc.yaml +++ /dev/null @@ -1,17 +0,0 @@ -{{- if and .Values.celery.worker.persistence .Values.celery.worker.persistence.enabled (not .Values.celery.worker.persistence.useEmptyDir) }} -kind: PersistentVolumeClaim -apiVersion: v1 -metadata: - name: {{ include "fairagro-advanced-middleware-api-chart.fullname" . }}-arc-cache - labels: - {{- include "fairagro-advanced-middleware-api-chart.labels" . | nindent 4 }} -spec: - accessModes: - - {{ .Values.celery.worker.persistence.accessMode | quote }} - resources: - requests: - storage: {{ .Values.celery.worker.persistence.size | quote }} - {{- if .Values.celery.worker.persistence.storageClass }} - storageClassName: {{ .Values.celery.worker.persistence.storageClass | quote }} - {{- end }} -{{- end }} diff --git a/helmchart/fairagro-advanced-middleware-api-chart/templates/ca-tls-secret.yaml b/helmchart/fairagro-advanced-middleware-api-chart/templates/ca-tls-secret.yaml deleted file mode 100644 index ca470b2..0000000 --- a/helmchart/fairagro-advanced-middleware-api-chart/templates/ca-tls-secret.yaml +++ /dev/null @@ -1,19 +0,0 @@ -{{- if .Values.api.ingress.mtlsEnabled }} -{{- $files := .Files }} -{{- $caCrt := "" }} -{{- /* Prefer direct value over file reference */ -}} -{{- if $.Values.api.tls.caCrt }} -{{- $caCrt = $.Values.api.tls.caCrt }} -{{- else if $.Values.api.tls.caCrtFile }} -{{- $caCrt = $files.Get $.Values.api.tls.caCrtFile }} -{{- end }} -{{- if and $.Values.api.tls $caCrt }} -apiVersion: v1 -kind: Secret -metadata: - name: {{ include "fairagro-advanced-middleware-api-chart.fullname" . }}-ca-secret -type: Opaque -stringData: - ca.crt: {{ $caCrt | toYaml | indent 2 }} -{{- end }} -{{- end }} diff --git a/helmchart/fairagro-advanced-middleware-api-chart/templates/celery-worker.yaml b/helmchart/fairagro-advanced-middleware-api-chart/templates/celery-worker.yaml deleted file mode 100644 index b439ba2..0000000 --- a/helmchart/fairagro-advanced-middleware-api-chart/templates/celery-worker.yaml +++ /dev/null @@ -1,124 +0,0 @@ -{{- if and .Values.celery.worker.enabled .Values.rabbitmq.enabled .Values.redis.enabled }} -{{- $repo := default .Values.api.image.repository .Values.celery.image.repository -}} -{{- $tag := default .Chart.AppVersion (default .Values.api.image.tag .Values.celery.image.tag) -}} -{{- $pull := default .Values.api.image.pullPolicy .Values.celery.image.pullPolicy -}} -apiVersion: apps/v1 -kind: Deployment -metadata: - name: {{ include "fairagro-advanced-middleware-api-chart.fullname" . }}-celery-worker - labels: - {{- include "fairagro-advanced-middleware-api-chart.labels" . | nindent 4 }} - app.kubernetes.io/component: celery-worker -spec: - replicas: {{ .Values.celery.worker.replicaCount }} - selector: - matchLabels: - app.kubernetes.io/name: {{ include "fairagro-advanced-middleware-api-chart.name" . }}-celery-worker - app.kubernetes.io/instance: {{ .Release.Name }} - template: - metadata: - labels: - {{- include "fairagro-advanced-middleware-api-chart.labels" . | nindent 8 }} - app.kubernetes.io/name: {{ include "fairagro-advanced-middleware-api-chart.name" . }}-celery-worker - app.kubernetes.io/component: celery-worker - app.kubernetes.io/instance: {{ .Release.Name }} - {{- with .Values.celery.worker.podAnnotations }} - annotations: - {{- toYaml . | nindent 8 }} - {{- end }} - spec: - serviceAccountName: {{ include "fairagro-advanced-middleware-api-chart.celeryServiceAccountName" . }} - {{- with .Values.imagePullSecrets }} - imagePullSecrets: - {{- toYaml . | nindent 8 }} - {{- end }} - {{- with .Values.celery.worker.podSecurityContext }} - securityContext: - {{- toYaml . | nindent 8 }} - {{- end }} - containers: - - name: celery-worker - {{- with .Values.celery.worker.securityContext }} - securityContext: - {{- toYaml . | nindent 12 }} - {{- end }} - image: "{{ $repo }}:{{ $tag }}" - imagePullPolicy: {{ $pull }} - env: - - name: CELERY_BROKER_URL - value: {{ include "fairagro-advanced-middleware-api-chart.celeryBrokerUrl" . | quote }} - - name: CELERY_RESULT_BACKEND - value: {{ include "fairagro-advanced-middleware-api-chart.celeryResultBackend" . | quote }} - - name: MIDDLEWARE_API_CONFIG - value: /run/secrets/middleware-api-config - {{- if and .Values.celery.worker.persistence .Values.celery.worker.persistence.enabled }} - - name: GIT_REPO_CACHE_DIR - value: {{ .Values.celery.worker.persistence.mountPath | quote }} - {{- end }} - volumeMounts: - - name: config - mountPath: /run/secrets/middleware-api-config - readOnly: true - subPath: middleware-api-config - {{- if and .Values.celery.worker.persistence .Values.celery.worker.persistence.enabled }} - - name: arc-cache - mountPath: {{ .Values.celery.worker.persistence.mountPath | quote }} - {{- end }} - {{- with .Values.celery.worker.volumeMounts }} - {{- toYaml . | nindent 12 }} - {{- end }} - command: - - /api/middleware-api/middleware-api - args: - - celery - - -A - - middleware.api.celery_app - - worker - - --loglevel=info - - --concurrency={{ .Values.celery.worker.concurrency }} - {{- with .Values.celery.worker.livenessProbe }} - livenessProbe: - {{- toYaml . | nindent 12 }} - {{- end }} - {{- with .Values.celery.worker.readinessProbe }} - readinessProbe: - {{- toYaml . | nindent 12 }} - {{- end }} - {{- with .Values.celery.worker.resources }} - resources: - {{- toYaml . | nindent 12 }} - {{- end }} - volumes: - - name: config - secret: - secretName: {{ include "fairagro-advanced-middleware-api-chart.fullname" . }}-config - items: - - key: middleware-api-config - path: middleware-api-config - {{- if and .Values.celery.worker.persistence .Values.celery.worker.persistence.enabled }} - - name: arc-cache - {{- if .Values.celery.worker.persistence.useEmptyDir }} - emptyDir: - medium: Memory - sizeLimit: {{ .Values.celery.worker.persistence.size }} - {{- else }} - persistentVolumeClaim: - claimName: {{ include "fairagro-advanced-middleware-api-chart.fullname" . }}-arc-cache - {{- end }} - {{- end }} - {{- with .Values.celery.worker.volumes }} - {{- toYaml . | nindent 8 }} - {{- end }} - {{- with .Values.celery.worker.nodeSelector }} - nodeSelector: - {{- toYaml . | nindent 8 }} - {{- end }} - {{- with .Values.celery.worker.affinity }} - affinity: - {{- toYaml . | nindent 8 }} - {{- end }} - {{- with .Values.celery.worker.tolerations }} - tolerations: - {{- toYaml . | nindent 8 }} - {{- end }} -{{- end }} diff --git a/helmchart/fairagro-advanced-middleware-api-chart/templates/config-secret.yaml b/helmchart/fairagro-advanced-middleware-api-chart/templates/config-secret.yaml deleted file mode 100644 index d1e2283..0000000 --- a/helmchart/fairagro-advanced-middleware-api-chart/templates/config-secret.yaml +++ /dev/null @@ -1,9 +0,0 @@ -apiVersion: v1 -kind: Secret -metadata: - name: {{ include "fairagro-advanced-middleware-api-chart.fullname" . }}-config - labels: - {{- include "fairagro-advanced-middleware-api-chart.labels" . | nindent 4 }} -type: Opaque -stringData: - middleware-api-config: {{ .Values.config | toYaml | quote }} diff --git a/helmchart/fairagro-advanced-middleware-api-chart/templates/deployment.yaml b/helmchart/fairagro-advanced-middleware-api-chart/templates/deployment.yaml deleted file mode 100644 index 51af827..0000000 --- a/helmchart/fairagro-advanced-middleware-api-chart/templates/deployment.yaml +++ /dev/null @@ -1,94 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: {{ include "fairagro-advanced-middleware-api-chart.fullname" . }} - labels: - {{- include "fairagro-advanced-middleware-api-chart.labels" . | nindent 4 }} -spec: - {{- if not .Values.api.autoscaling.enabled }} - replicas: {{ .Values.api.replicaCount }} - {{- end }} - selector: - matchLabels: - {{- include "fairagro-advanced-middleware-api-chart.selectorLabels" . | nindent 6 }} - template: - metadata: - annotations: - checksum/config: {{ include (print $.Template.BasePath "/config-secret.yaml") . | sha256sum }} - {{- with .Values.api.podAnnotations }} - {{- toYaml . | nindent 8 }} - {{- end }} - labels: - {{- include "fairagro-advanced-middleware-api-chart.labels" . | nindent 8 }} - {{- with .Values.api.podLabels }} - {{- toYaml . | nindent 8 }} - {{- end }} - spec: - {{- with .Values.imagePullSecrets }} - imagePullSecrets: - {{- toYaml . | nindent 8 }} - {{- end }} - serviceAccountName: {{ include "fairagro-advanced-middleware-api-chart.serviceAccountName" . }} - {{- with .Values.api.podSecurityContext }} - securityContext: - {{- toYaml . | nindent 8 }} - {{- end }} - containers: - - name: {{ .Chart.Name }} - {{- with .Values.api.securityContext }} - securityContext: - {{- toYaml . | nindent 12 }} - {{- end }} - image: "{{ .Values.api.image.repository }}:{{ .Values.api.image.tag | default .Chart.AppVersion }}" - imagePullPolicy: {{ .Values.api.image.pullPolicy }} - env: - - name: CELERY_BROKER_URL - value: {{ include "fairagro-advanced-middleware-api-chart.celeryBrokerUrl" . | quote }} - - name: CELERY_RESULT_BACKEND - value: {{ include "fairagro-advanced-middleware-api-chart.celeryResultBackend" . | quote }} - ports: - - name: http - containerPort: {{ .Values.api.service.port | int }} - protocol: TCP - {{- with .Values.api.livenessProbe }} - livenessProbe: - {{- toYaml . | nindent 12 }} - {{- end }} - {{- with .Values.api.readinessProbe }} - readinessProbe: - {{- toYaml . | nindent 12 }} - {{- end }} - {{- with .Values.api.resources }} - resources: - {{- toYaml . | nindent 12 }} - {{- end }} - volumeMounts: - - name: config - mountPath: /run/secrets/middleware-api-config - readOnly: true - subPath: middleware-api-config - {{- with .Values.api.volumeMounts }} - {{- toYaml . | nindent 12 }} - {{- end }} - volumes: - - name: config - secret: - secretName: {{ include "fairagro-advanced-middleware-api-chart.fullname" . }}-config - items: - - key: middleware-api-config - path: middleware-api-config - {{- with .Values.api.volumes }} - {{- toYaml . | nindent 8 }} - {{- end }} - {{- with .Values.api.nodeSelector }} - nodeSelector: - {{- toYaml . | nindent 8 }} - {{- end }} - {{- with .Values.api.affinity }} - affinity: - {{- toYaml . | nindent 8 }} - {{- end }} - {{- with .Values.api.tolerations }} - tolerations: - {{- toYaml . | nindent 8 }} - {{- end }} diff --git a/helmchart/fairagro-advanced-middleware-api-chart/templates/hpa.yaml b/helmchart/fairagro-advanced-middleware-api-chart/templates/hpa.yaml deleted file mode 100644 index 17e4590..0000000 --- a/helmchart/fairagro-advanced-middleware-api-chart/templates/hpa.yaml +++ /dev/null @@ -1,67 +0,0 @@ -{{- if .Values.api.autoscaling.enabled }} -apiVersion: autoscaling/v2 -kind: HorizontalPodAutoscaler -metadata: - name: {{ include "fairagro-advanced-middleware-api-chart.fullname" . }} - labels: - {{- include "fairagro-advanced-middleware-api-chart.labels" . | nindent 4 }} -spec: - scaleTargetRef: - apiVersion: apps/v1 - kind: Deployment - name: {{ include "fairagro-advanced-middleware-api-chart.fullname" . }} - minReplicas: {{ .Values.api.autoscaling.minReplicas }} - maxReplicas: {{ .Values.api.autoscaling.maxReplicas }} - metrics: - {{- if .Values.api.autoscaling.targetCPUUtilizationPercentage }} - - type: Resource - resource: - name: cpu - target: - type: Utilization - averageUtilization: {{ .Values.api.autoscaling.targetCPUUtilizationPercentage }} - {{- end }} - {{- if .Values.api.autoscaling.targetMemoryUtilizationPercentage }} - - type: Resource - resource: - name: memory - target: - type: Utilization - averageUtilization: {{ .Values.api.autoscaling.targetMemoryUtilizationPercentage }} - {{- end }} -{{- end }} - -{{- if and .Values.celery.worker.enabled .Values.celery.worker.autoscaling.enabled .Values.rabbitmq.enabled .Values.redis.enabled }} ---- -apiVersion: autoscaling/v2 -kind: HorizontalPodAutoscaler -metadata: - name: {{ include "fairagro-advanced-middleware-api-chart.fullname" . }}-celery-worker - labels: - {{- include "fairagro-advanced-middleware-api-chart.labels" . | nindent 4 }} - app.kubernetes.io/component: celery-worker -spec: - scaleTargetRef: - apiVersion: apps/v1 - kind: Deployment - name: {{ include "fairagro-advanced-middleware-api-chart.fullname" . }}-celery-worker - minReplicas: {{ .Values.celery.worker.autoscaling.minReplicas }} - maxReplicas: {{ .Values.celery.worker.autoscaling.maxReplicas }} - metrics: - {{- if .Values.celery.worker.autoscaling.targetCPUUtilizationPercentage }} - - type: Resource - resource: - name: cpu - target: - type: Utilization - averageUtilization: {{ .Values.celery.worker.autoscaling.targetCPUUtilizationPercentage }} - {{- end }} - {{- if .Values.celery.worker.autoscaling.targetMemoryUtilizationPercentage }} - - type: Resource - resource: - name: memory - target: - type: Utilization - averageUtilization: {{ .Values.celery.worker.autoscaling.targetMemoryUtilizationPercentage }} - {{- end }} -{{- end }} diff --git a/helmchart/fairagro-advanced-middleware-api-chart/templates/ingress.yaml b/helmchart/fairagro-advanced-middleware-api-chart/templates/ingress.yaml deleted file mode 100644 index 8767854..0000000 --- a/helmchart/fairagro-advanced-middleware-api-chart/templates/ingress.yaml +++ /dev/null @@ -1,51 +0,0 @@ -{{- if .Values.api.ingress.enabled -}} -apiVersion: networking.k8s.io/v1 -kind: Ingress -metadata: - name: {{ include "fairagro-advanced-middleware-api-chart.fullname" . }} - labels: - {{- include "fairagro-advanced-middleware-api-chart.labels" . | nindent 4 }} - annotations: - {{- with .Values.api.ingress.annotations }} - {{- toYaml . | nindent 4 }} - {{- end }} - {{- if .Values.api.ingress.mtlsEnabled }} - nginx.ingress.kubernetes.io/auth-tls-secret: "{{ .Release.Namespace }}/{{ include "fairagro-advanced-middleware-api-chart.caTlsSecretName" . }}" - nginx.ingress.kubernetes.io/auth-tls-verify-client: "optional" - nginx.ingress.kubernetes.io/auth-tls-verify-depth: "{{ .Values.api.ingress.mtlsVerifyDepth }}" - nginx.ingress.kubernetes.io/auth-tls-pass-certificate-to-upstream: "true" - {{- end }} -spec: - {{- with .Values.api.ingress.className }} - ingressClassName: {{ . }} - {{- else }} - ingressClassName: nginx - {{- end }} - {{- if .Values.api.ingress.tls }} - tls: - {{- range .Values.api.ingress.tls }} - - hosts: - {{- range .hosts }} - - {{ . | quote }} - {{- end }} - secretName: {{ include "fairagro-advanced-middleware-api-chart.fullname" $ }}-{{ .secretName }} - {{- end }} - {{- end }} - rules: - {{- range .Values.api.ingress.hosts }} - - host: {{ .host | quote }} - http: - paths: - {{- range .paths }} - - path: {{ .path }} - {{- with .pathType }} - pathType: {{ . }} - {{- end }} - backend: - service: - name: {{ include "fairagro-advanced-middleware-api-chart.fullname" $ }} - port: - number: {{ $.Values.api.service.port }} - {{- end }} - {{- end }} -{{- end }} diff --git a/helmchart/fairagro-advanced-middleware-api-chart/templates/rabbitmq.yaml b/helmchart/fairagro-advanced-middleware-api-chart/templates/rabbitmq.yaml deleted file mode 100644 index 5f0e8f3..0000000 --- a/helmchart/fairagro-advanced-middleware-api-chart/templates/rabbitmq.yaml +++ /dev/null @@ -1,94 +0,0 @@ -{{- if .Values.rabbitmq.enabled }} -{{- $rabbitAuth := default (dict) .Values.rabbitmq.auth }} -{{- $secretName := default (printf "%s-rabbitmq-auth" (include "fairagro-advanced-middleware-api-chart.fullname" .)) $rabbitAuth.existingSecret }} -{{- if not $rabbitAuth.existingSecret }} -apiVersion: v1 -kind: Secret -metadata: - name: {{ $secretName }} - labels: - {{- include "fairagro-advanced-middleware-api-chart.labels" . | nindent 4 }} - app.kubernetes.io/component: rabbitmq -type: Opaque -stringData: - username: {{ default "guest" $rabbitAuth.username | quote }} - password: {{ default "guest" $rabbitAuth.password | quote }} - erlang_cookie: {{ default "" $rabbitAuth.erlang_cookie | quote }} ---- -{{- end }} -apiVersion: apps/v1 -kind: Deployment -metadata: - name: {{ include "fairagro-advanced-middleware-api-chart.fullname" . }}-rabbitmq - labels: - {{- include "fairagro-advanced-middleware-api-chart.labels" . | nindent 4 }} - app.kubernetes.io/component: rabbitmq -spec: - replicas: 1 - selector: - matchLabels: - app.kubernetes.io/name: {{ include "fairagro-advanced-middleware-api-chart.name" . }}-rabbitmq - app.kubernetes.io/instance: {{ .Release.Name }} - template: - metadata: - labels: - {{- include "fairagro-advanced-middleware-api-chart.labels" . | nindent 8 }} - app.kubernetes.io/name: {{ include "fairagro-advanced-middleware-api-chart.name" . }}-rabbitmq - app.kubernetes.io/component: rabbitmq - app.kubernetes.io/instance: {{ .Release.Name }} - spec: - containers: - - name: rabbitmq - image: {{ .Values.rabbitmq.image | quote }} - ports: - - containerPort: {{ .Values.rabbitmq.service.port }} - name: amqp - - containerPort: {{ .Values.rabbitmq.service.managementPort }} - name: management - env: - - name: RABBITMQ_DEFAULT_USER - valueFrom: - secretKeyRef: - name: {{ $secretName }} - key: username - - name: RABBITMQ_DEFAULT_PASS - valueFrom: - secretKeyRef: - name: {{ $secretName }} - key: password - - name: RABBITMQ_ERLANG_COOKIE - valueFrom: - secretKeyRef: - name: {{ $secretName }} - key: erlang_cookie - readinessProbe: - tcpSocket: - port: amqp - initialDelaySeconds: 5 - periodSeconds: 10 - livenessProbe: - tcpSocket: - port: amqp - initialDelaySeconds: 10 - periodSeconds: 20 ---- -apiVersion: v1 -kind: Service -metadata: - name: {{ include "fairagro-advanced-middleware-api-chart.fullname" . }}-rabbitmq - labels: - {{- include "fairagro-advanced-middleware-api-chart.labels" . | nindent 4 }} - app.kubernetes.io/component: rabbitmq -spec: - type: ClusterIP - selector: - app.kubernetes.io/name: {{ include "fairagro-advanced-middleware-api-chart.name" . }}-rabbitmq - app.kubernetes.io/instance: {{ .Release.Name }} - ports: - - name: amqp - port: {{ .Values.rabbitmq.service.port }} - targetPort: amqp - - name: management - port: {{ .Values.rabbitmq.service.managementPort }} - targetPort: management -{{- end }} diff --git a/helmchart/fairagro-advanced-middleware-api-chart/templates/redis.yaml b/helmchart/fairagro-advanced-middleware-api-chart/templates/redis.yaml deleted file mode 100644 index 4b434b6..0000000 --- a/helmchart/fairagro-advanced-middleware-api-chart/templates/redis.yaml +++ /dev/null @@ -1,85 +0,0 @@ -{{- if .Values.redis.enabled }} -{{- $redisAuth := default (dict) .Values.redis.auth }} -{{- $redisSecret := default (printf "%s-redis-auth" (include "fairagro-advanced-middleware-api-chart.fullname" .)) $redisAuth.existingSecret }} -{{- $redisPassword := default "" $redisAuth.password }} -{{- if and (not $redisAuth.existingSecret) $redisPassword }} -apiVersion: v1 -kind: Secret -metadata: - name: {{ $redisSecret }} - labels: - {{- include "fairagro-advanced-middleware-api-chart.labels" . | nindent 4 }} - app.kubernetes.io/component: redis -type: Opaque -stringData: - password: {{ $redisPassword | quote }} ---- -{{- end }} -apiVersion: apps/v1 -kind: Deployment -metadata: - name: {{ include "fairagro-advanced-middleware-api-chart.fullname" . }}-redis - labels: - {{- include "fairagro-advanced-middleware-api-chart.labels" . | nindent 4 }} - app.kubernetes.io/component: redis -spec: - replicas: 1 - selector: - matchLabels: - app.kubernetes.io/name: {{ include "fairagro-advanced-middleware-api-chart.name" . }}-redis - app.kubernetes.io/instance: {{ .Release.Name }} - template: - metadata: - labels: - {{- include "fairagro-advanced-middleware-api-chart.labels" . | nindent 8 }} - app.kubernetes.io/name: {{ include "fairagro-advanced-middleware-api-chart.name" . }}-redis - app.kubernetes.io/component: redis - app.kubernetes.io/instance: {{ .Release.Name }} - spec: - containers: - - name: redis - image: {{ .Values.redis.image | quote }} -{{- if or $redisAuth.existingSecret $redisPassword }} - env: - - name: REDIS_PASSWORD - valueFrom: - secretKeyRef: - name: {{ $redisSecret }} - key: password - command: - - /bin/sh - args: - - -c - - redis-server --requirepass "$REDIS_PASSWORD" -{{- end }} - ports: - - containerPort: {{ .Values.redis.service.port }} - name: redis - readinessProbe: - tcpSocket: - port: redis - initialDelaySeconds: 5 - periodSeconds: 10 - livenessProbe: - tcpSocket: - port: redis - initialDelaySeconds: 10 - periodSeconds: 20 ---- -apiVersion: v1 -kind: Service -metadata: - name: {{ include "fairagro-advanced-middleware-api-chart.fullname" . }}-redis - labels: - {{- include "fairagro-advanced-middleware-api-chart.labels" . | nindent 4 }} - app.kubernetes.io/component: redis -spec: - type: ClusterIP - selector: - app.kubernetes.io/name: {{ include "fairagro-advanced-middleware-api-chart.name" . }}-redis - app.kubernetes.io/instance: {{ .Release.Name }} - ports: - - name: redis - port: {{ .Values.redis.service.port }} - targetPort: redis -{{- end }} diff --git a/helmchart/fairagro-advanced-middleware-api-chart/templates/server-tls-secret.yaml b/helmchart/fairagro-advanced-middleware-api-chart/templates/server-tls-secret.yaml deleted file mode 100644 index e948995..0000000 --- a/helmchart/fairagro-advanced-middleware-api-chart/templates/server-tls-secret.yaml +++ /dev/null @@ -1,29 +0,0 @@ -{{- $files := .Files }} -{{- $serverCrt := "" }} -{{- $serverKey := "" }} -{{- /* Prefer direct values over file references */ -}} -{{- if $.Values.api.tls.serverCrt }} -{{- $serverCrt = $.Values.api.tls.serverCrt }} -{{- else if $.Values.api.tls.serverCrtFile }} -{{- $serverCrt = $files.Get $.Values.api.tls.serverCrtFile }} -{{- end }} -{{- if $.Values.api.tls.serverKey }} -{{- $serverKey = $.Values.api.tls.serverKey }} -{{- else if $.Values.api.tls.serverKeyFile }} -{{- $serverKey = $files.Get $.Values.api.tls.serverKeyFile }} -{{- end }} -{{- if and $.Values.api.tls (and $serverCrt $serverKey) }} -{{- range $.Values.api.ingress.tls }} -{{ if .secretName }} ---- -apiVersion: v1 -kind: Secret -metadata: - name: {{ include "fairagro-advanced-middleware-api-chart.fullname" $ }}-{{ .secretName }} -type: kubernetes.io/tls -stringData: - tls.crt: {{ $serverCrt | toYaml | indent 2 }} - tls.key: {{ $serverKey | toYaml | indent 2 }} -{{- end }} -{{- end }} -{{- end }} diff --git a/helmchart/fairagro-advanced-middleware-api-chart/templates/service.yaml b/helmchart/fairagro-advanced-middleware-api-chart/templates/service.yaml deleted file mode 100644 index 435657b..0000000 --- a/helmchart/fairagro-advanced-middleware-api-chart/templates/service.yaml +++ /dev/null @@ -1,15 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - name: {{ include "fairagro-advanced-middleware-api-chart.fullname" . }} - labels: - {{- include "fairagro-advanced-middleware-api-chart.labels" . | nindent 4 }} -spec: - type: {{ .Values.api.service.type }} - ports: - - port: {{ .Values.api.service.port }} - targetPort: http - protocol: TCP - name: http - selector: - {{- include "fairagro-advanced-middleware-api-chart.selectorLabels" . | nindent 4 }} diff --git a/helmchart/fairagro-advanced-middleware-api-chart/templates/serviceaccount.yaml b/helmchart/fairagro-advanced-middleware-api-chart/templates/serviceaccount.yaml deleted file mode 100644 index 4feb484..0000000 --- a/helmchart/fairagro-advanced-middleware-api-chart/templates/serviceaccount.yaml +++ /dev/null @@ -1,29 +0,0 @@ -{{- if .Values.api.serviceAccount.create }} -apiVersion: v1 -kind: ServiceAccount -metadata: - name: {{ include "fairagro-advanced-middleware-api-chart.serviceAccountName" . }} - labels: - {{- include "fairagro-advanced-middleware-api-chart.labels" . | nindent 4 }} - {{- with .Values.api.serviceAccount.annotations }} - annotations: - {{- toYaml . | nindent 4 }} - {{- end }} -automountServiceAccountToken: {{ .Values.api.serviceAccount.automount }} -{{- end }} - -{{- if and .Values.celery.worker.enabled .Values.celery.worker.serviceAccount.create }} ---- -apiVersion: v1 -kind: ServiceAccount -metadata: - name: {{ include "fairagro-advanced-middleware-api-chart.celeryServiceAccountName" . }} - labels: - {{- include "fairagro-advanced-middleware-api-chart.labels" . | nindent 4 }} - app.kubernetes.io/component: celery-worker - {{- with .Values.celery.worker.serviceAccount.annotations }} - annotations: - {{- toYaml . | nindent 4 }} - {{- end }} -automountServiceAccountToken: {{ .Values.celery.worker.serviceAccount.automount }} -{{- end }} diff --git a/helmchart/fairagro-advanced-middleware-api-chart/templates/tests/test-connection.yaml b/helmchart/fairagro-advanced-middleware-api-chart/templates/tests/test-connection.yaml deleted file mode 100644 index d306fa8..0000000 --- a/helmchart/fairagro-advanced-middleware-api-chart/templates/tests/test-connection.yaml +++ /dev/null @@ -1,15 +0,0 @@ -apiVersion: v1 -kind: Pod -metadata: - name: {{ include "fairagro-advanced-middleware-api-chart.fullname" . }}-test-connection - labels: - {{- include "fairagro-advanced-middleware-api-chart.labels" . | nindent 4 }} - annotations: - "helm.sh/hook": test -spec: - containers: - - name: wget - image: busybox - command: ['wget'] - args: ['{{ include "fairagro-advanced-middleware-api-chart.fullname" . }}:{{ .Values.api.service.port }}'] - restartPolicy: Never diff --git a/helmchart/fairagro-advanced-middleware-api-chart/values.yaml b/helmchart/fairagro-advanced-middleware-api-chart/values.yaml deleted file mode 100644 index 7897050..0000000 --- a/helmchart/fairagro-advanced-middleware-api-chart/values.yaml +++ /dev/null @@ -1,175 +0,0 @@ -# Default values for fairagro-advanced-middleware-api-chart. -# This is a YAML-formatted file. -# Declare variables to be passed into your templates. - -config: - log_level: DEBUG - # gitlab_api: - # group: FAIRagro-advanced-middleware-dev - # url: https://datahub-dev.ipk-gatersleben.de - # max_workers: 10 - # commit_chunk_size: 200 - git_repo: - url: https://datahub-dev.ipk-gatersleben.de - group: FAIRagro-advanced-middleware-dev - max_workers: 10 - known_rdis: - - bonares - - edal - - edaphobase - - openagrar - - publisso - - thunen_atlas - celery: {} # all further celery config is set via environment variables - otel: - # endpoint: http://signoz:4318 # Uncomment to enable Signoz tracing - # log_console_spans: false # Set to true to log spans to console - # log_level: INFO - # require_client_cert: false - -rabbitmq: - enabled: true - image: rabbitmq:3.12-management - # auth: - # username: guest - # password: guest - # erlang_cookie: "" - # existingSecret: "" # if set, the secret must contain keys: username, password, erlang_cookie - service: - port: 5672 - managementPort: 15672 - -redis: - enabled: true - image: redis:7 - # auth: - # password: "" - # existingSecret: "" # if set, the secret must contain key: password - service: - port: 6379 - -# API deployment settings -api: - replicaCount: 1 - image: - repository: zalf/fairagro-advanced-middleware-api - pullPolicy: IfNotPresent - tag: "" - service: - type: ClusterIP - port: 8000 - ingress: - enabled: false - className: "" - mtlsEnabled: false - mtlsVerifyDepth: 1 - hosts: [] - tls: [] - tls: - # Direct certificate values (preferred for secrets management) - serverCrt: "" # Base64-encoded or PEM server certificate - serverKey: "" # Base64-encoded or PEM server private key - caCrt: "" # Base64-encoded or PEM CA certificate - # File-based certificate references (used if direct values are not provided) - serverCrtFile: "" # Path to server certificate file (e.g., server.crt) - serverKeyFile: "" # Path to server key file (e.g., server.key) - caCrtFile: "" # Path to CA certificate file (e.g., ca.crt) - caTlsSecretName: "" - podAnnotations: {} - podLabels: {} - podSecurityContext: {} - securityContext: {} - livenessProbe: - httpGet: - path: /v1/liveness - port: http - initialDelaySeconds: 10 # Container darf starten - timeoutSeconds: 3 # genug Zeit für HTTP-Request - periodSeconds: 10 - failureThreshold: 3 - readinessProbe: - httpGet: - path: /v1/health - port: http - initialDelaySeconds: 10 # Container darf starten - timeoutSeconds: 3 # genug Zeit für HTTP-Request - periodSeconds: 10 - failureThreshold: 3 - resources: {} - volumes: [] - volumeMounts: [] - nodeSelector: {} - tolerations: [] - affinity: {} - serviceAccount: - create: true - automount: false - annotations: {} - name: "" - autoscaling: - enabled: false - minReplicas: 1 - maxReplicas: 100 - targetCPUUtilizationPercentage: 80 - # targetMemoryUtilizationPercentage: 80 - -# Celery worker settings -celery: - # brokerUrl: "" # use this setting if you want to override the generated RabbitMQ URL - # resultBackend: "" # use this setting if you want to override the generated Redis URL - image: - repository: zalf/fairagro-advanced-middleware-api - pullPolicy: IfNotPresent - tag: "" - worker: - enabled: true - replicaCount: 1 - concurrency: 4 - persistence: - enabled: true - useEmptyDir: true - size: 100Mi - mountPath: /var/cache/middleware/arc-cache - serviceAccount: - create: true - automount: false - annotations: {} - name: "" - livenessProbe: - exec: - command: ["true"] - # initialDelaySeconds: 25 - # periodSeconds: 20 - # timeoutSeconds: 30 - # failureThreshold: 3 - readinessProbe: - exec: - command: - - /api/middleware-api/middleware-api - - worker-health - initialDelaySeconds: 10 - timeoutSeconds: 3 - periodSeconds: 10 - failureThreshold: 3 - podAnnotations: {} - podLabels: {} - podSecurityContext: {} - securityContext: {} - resources: {} - volumes: [] - volumeMounts: [] - nodeSelector: {} - tolerations: [] - affinity: {} - autoscaling: - enabled: false - minReplicas: 1 - maxReplicas: 100 - targetCPUUtilizationPercentage: 80 - # targetMemoryUtilizationPercentage: 80 - -# This is for the secrets for pulling an image from a private repository more information can be found here: https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/ -imagePullSecrets: [] -# This is to override the chart name. -nameOverride: "" -fullnameOverride: "" diff --git a/helmchart/test_deploy/values.yaml b/helmchart/test_deploy/values.yaml deleted file mode 100644 index 4ce7955..0000000 --- a/helmchart/test_deploy/values.yaml +++ /dev/null @@ -1,33 +0,0 @@ -api: - tls: - serverCrtFile: server.crt - serverKeyFile: server.key - caCrtFile: ca.crt - - image: - repository: fairagro-advanced-middleware-api - tag: test - imagePullPolicy: Never - - ingress: - enabled: true - mtlsEnabled: true - annotations: - nginx.ingress.kubernetes.io/backend-protocol: "HTTP" - hosts: - - host: chart-example.local - paths: - - path: / - pathType: ImplementationSpecific - tls: - - secretName: server-tls - hosts: - - chart-example.local - -celery: - image: - repository: fairagro-advanced-middleware-api - tag: test - imagePullPolicy: Never - worker: - concurrency: 1 diff --git a/middleware/api/README.md b/middleware/api/README.md deleted file mode 100644 index 2f65e04..0000000 --- a/middleware/api/README.md +++ /dev/null @@ -1,58 +0,0 @@ -# FAIRagro Advanced Middleware API - -This is the main REST API for the FAIRagro Advanced Middleware system. - -## Overview - -The API provides endpoints for: - -- **ARC Management**: Create, update, and manage ARC (Annotated Research Context) objects -- **Data Conversion**: Convert data from various sources (SQL, INSPIRE metadata) into ARC format -- **GitLab Integration**: Synchronize ARC objects with GitLab repositories - -## Architecture - -The API is built with: - -- **FastAPI**: Modern, high-performance web framework -- **Uvicorn**: ASGI server for running the application -- **arctrl**: Python library for working with ARC objects -- **python-gitlab**: GitLab API integration - -## Dependencies - -The API depends on: - -- `shared`: Shared utilities and configuration -- `arctrl`: ARC object manipulation -- `fastapi`: Web framework -- `uvicorn`: ASGI server -- `python-gitlab`: GitLab integration -- `pyyaml`: Configuration parsing -- `cryptography`: Security and encryption - -## Development - -Install dependencies: - -```bash -uv sync --package api -``` - -Run tests: - -```bash -uv run pytest middleware/api/tests -``` - -Run the API locally: - -```bash -uv run uvicorn middleware.api.main:app --reload -``` - -## Deployment - -The API is containerized using Docker and can be built as a standalone binary with PyInstaller for minimal runtime dependencies. - -See `docker/Dockerfile.api` for the build configuration. diff --git a/middleware/api/example_config.yaml b/middleware/api/example_config.yaml deleted file mode 100644 index 20b0731..0000000 --- a/middleware/api/example_config.yaml +++ /dev/null @@ -1,9 +0,0 @@ -log_level: DEBUG -client_auth_oid: 1.3.6.1.4.1.64609.1.1 -known_rdis: - - bonares - - edal -gitlab_api: - url: https://datahub-dev.ipk-gatersleben.de - group: FAIRagro-advanced-middleware-integration-tests - max_workers: 5 diff --git a/middleware/api/pyproject.toml b/middleware/api/pyproject.toml deleted file mode 100644 index 521eb8f..0000000 --- a/middleware/api/pyproject.toml +++ /dev/null @@ -1,36 +0,0 @@ -[project] -name = "api" -version = "0.0.0" # currently we disrehgard the version of this subpackage -description = "The FAIRagro advanced middleware API" -readme = "README.md" -requires-python = ">=3.12" -dependencies = [ - "shared", - "arctrl>=3.0.0b15", - "asn1crypto>=1.5.1", - "celery>=5.3.6", - "cryptography>=45.0.6", - "fastapi>=0.124.0", - "gitpython>=3.1.46", - "opentelemetry-instrumentation-celery>=0.47b0", - "opentelemetry-instrumentation-fastapi>=0.47b0", - "opentelemetry-instrumentation-redis>=0.47b0", - "opentelemetry-instrumentation-requests>=0.47b0", - "opentelemetry-instrumentation-starlette>=0.47b0", - "python-gitlab>=6.2.0", - "pyyaml>=6.0.2", - "redis>=5.0.1", - "sse-starlette>=2.0.0", - "uvicorn>=0.35.0", - "celery-types>=0.24.0", -] - -[tool.uv.sources] -shared = { workspace = true } - -[tool.hatch.build.targets.wheel] -packages = ["src/middleware"] - -[build-system] -requires = ["hatchling"] -build-backend = "hatchling.build" diff --git a/middleware/api/src/middleware/api/__init__.py b/middleware/api/src/middleware/api/__init__.py deleted file mode 100644 index 14118e7..0000000 --- a/middleware/api/src/middleware/api/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""The FAIRagro Middleware API package.""" diff --git a/middleware/api/src/middleware/api/api.py b/middleware/api/src/middleware/api/api.py deleted file mode 100644 index b32b7dd..0000000 --- a/middleware/api/src/middleware/api/api.py +++ /dev/null @@ -1,555 +0,0 @@ -"""FastAPI middleware for managing ARC (Advanced Research Context) objects. - -This module provides an API class that handles HTTP requests for creating, reading, -updating and deleting ARC objects. It includes authentication via client certificates -and content type validation. -""" - -import logging -import os -import sys -import tomllib -from collections.abc import AsyncGenerator -from contextlib import asynccontextmanager -from pathlib import Path -from typing import Annotated, cast -from urllib.parse import unquote - -import redis -from asn1crypto.core import Sequence, UTF8String # type: ignore -from cryptography import x509 -from cryptography.x509.extensions import ExtensionNotFound -from cryptography.x509.oid import NameOID -from fastapi import Depends, FastAPI, HTTPException, Request, Response -from fastapi.responses import JSONResponse -from opentelemetry.sdk._logs import LoggerProvider -from opentelemetry.sdk.trace import TracerProvider -from pydantic import ValidationError - -from middleware.shared.api_models.models import ( - CreateOrUpdateArcsRequest, - CreateOrUpdateArcsResponse, - GetTaskStatusResponse, - HealthResponse, - LivenessResponse, - WhoamiResponse, -) -from middleware.shared.tracing import initialize_logging, initialize_tracing - -from .celery_app import celery_app -from .config import Config -from .tracing import instrument_app -from .worker import process_arc - -try: - pyproject_path = Path(__file__).parent.parent / "pyproject.toml" - with pyproject_path.open("rb") as f: - data = tomllib.load(f) - __version__ = data["project"]["version"] -except (FileNotFoundError, KeyError): - # Fallback, falls die Datei nicht gefunden wird oder die Struktur fehlt - __version__ = "0.0.0" - - -loaded_config = None -if "pytest" in sys.modules: - # pytest is executing this file during a test discovery run. - # No config file is available, so we create a dummy config so that pytest does not fail. - loaded_config = Config.from_data( - { - "log_level": "DEBUG", - "celery": { - "broker_url": "memory://", - "result_backend": "cache+memory://", - }, - "gitlab_api": { - "url": "https://localhost/", - "branch": "dummy", - "token": "dummy-token", # nosec B105 - "group": "dummy-group", - }, - } - ) -else: - # Load configuration in production mode - config_file = Path(os.environ.get("MIDDLEWARE_API_CONFIG", "/run/secrets/middleware-api-config")) - if config_file.is_file(): - loaded_config = Config.from_yaml_file(config_file) - else: - logging.getLogger("middleware_api").error( - "Middleware API configuration file not found at %s. Exiting.", config_file - ) - sys.exit(1) - -logging.basicConfig( - level=getattr(logging, loaded_config.log_level), format="%(asctime)s %(levelname)s %(name)s: %(message)s" -) - -logger = logging.getLogger("middleware_api") - - -class PollingLogFilter(logging.Filter): - """Filter to suppress polling task status logs from uvicorn access logger.""" - - def filter(self, record: logging.LogRecord) -> bool: - """Suppress access logs for task status polling at INFO level. - - These logs are shown if the 'middleware_api' logger is set to DEBUG. - """ - msg = record.getMessage() - if "GET /v1/tasks/" in msg: - return logging.getLogger("middleware_api").isEnabledFor(logging.DEBUG) - return True - - -class Api: - """FastAPI middleware for managing ARC (Advanced Research Context) objects. - - This class provides methods and routes for handling HTTP requests related to ARC - objects, including authentication, content validation, and CRUD operations through - FastAPI endpoints. - """ - - # Constants - SUPPORTED_CONTENT_TYPE = "application/json" - SUPPORTED_ACCEPT_TYPE = "application/json" - - def __init__(self, app_config: Config) -> None: - """Initialize the API with optional configuration. - - Args: - app_config (Config): Configuration object. - - """ - self._config = app_config - self._tracer_provider: TracerProvider | None = None - self._logger_provider: LoggerProvider | None = None - logger.debug("API configuration: %s", self._config.model_dump()) - - # Initialize OpenTelemetry tracing with optional OTLP endpoint - otlp_endpoint = str(self._config.otel.endpoint) if self._config.otel.endpoint else None - self._tracer_provider, self._tracer = initialize_tracing( - service_name="middleware-api", - otlp_endpoint=otlp_endpoint, - log_console_spans=self._config.otel.log_console_spans, - ) - # Initialize OTEL log export if configured - self._logger_provider = initialize_logging( - service_name="middleware-api", - otlp_endpoint=otlp_endpoint, - log_level=getattr(logging, self._config.log_level), - otlp_log_level=getattr(logging, self._config.otel.log_level), - ) - - # Apply polling log filter to uvicorn access logger - logging.getLogger("uvicorn.access").addFilter(PollingLogFilter()) - - @asynccontextmanager - async def lifespan(_app: FastAPI) -> AsyncGenerator[None, None]: - yield - if self._tracer_provider is not None: - try: - self._tracer_provider.shutdown() - except (RuntimeError, ValueError, OSError) as exc: - logger.warning("Failed to shutdown tracer provider: %s", exc) - if self._logger_provider is not None: - try: - self._logger_provider.shutdown() - except (RuntimeError, ValueError, OSError) as exc: - logger.warning("Failed to shutdown logger provider: %s", exc) - - self._app = FastAPI( - title="FAIR Middleware API", - description="API for managing ARC (Advanced Research Context) objects", - version=__version__, - lifespan=lifespan, - ) - - # Instrument the FastAPI application with OpenTelemetry - instrument_app(self._app) - - self._setup_routes() - self._setup_exception_handlers() - - @property - def app(self) -> FastAPI: - """Get the FastAPI application instance. - - Returns: - FastAPI: The configured FastAPI application. - - """ - return self._app - - def _validate_client_cert(self, request: Request) -> x509.Certificate | None: - """Extract and parse client certificate from request headers. - - Args: - request (Request): FastAPI request object. - - Returns: - x509.Certificate | None: Parsed client certificate or None if not required/provided. - - Raises: - HTTPException: If certificate is required and missing or invalid. - """ - # check, if we've already cached the cert in the request state - if hasattr(request.state, "cert"): - return getattr(request.state, "cert", None) - - headers = request.headers - logger.debug("Request headers: %s", dict(headers.items())) - - client_cert = headers.get("ssl-client-cert") or headers.get("X-SSL-Client-Cert") - client_verify = headers.get("ssl-client-verify") or headers.get("X-SSL-Client-Verify", "NONE") - logger.debug("Client cert header present: %s", bool(client_cert)) - logger.debug("Client verify status: %s", client_verify) - - if not client_cert: - if self._config.require_client_cert: - msg = "Client certificate required for access" - logger.warning(msg) - raise HTTPException(status_code=401, detail=msg) - logger.debug("Client certificate not required - proceeding without authentication") - request.state.cert = None - return None - - if client_verify != "SUCCESS": - detail_msg = f"Client certificate verification failed: {client_verify}" - logger.warning(detail_msg) - raise HTTPException(status_code=401, detail=detail_msg) - - try: - cert_pem = unquote(client_cert) - logger.debug("URL decoded certificate: %s...", cert_pem[:100]) - cert = x509.load_pem_x509_certificate(cert_pem.encode("utf-8")) - except (ValueError, TypeError) as e: - error_msg = f"Certificate parsing error: {str(e)}" - logger.error(error_msg) - raise HTTPException(status_code=400, detail=error_msg) from e - - request.state.cert = cert - return cert - - def _validate_client_id(self, request: Request) -> str | None: - """Extract client ID from certificate Common Name (CN) attribute. - - Args: - request (Request): FastAPI request object. - - Returns: - str | None: Client identifier extracted from the certificate, or None if not authenticated. - """ - cert = self._validate_client_cert(request) - if cert is None: - logger.debug("No client certificate - client ID is None") - return None - - cn_attributes = cert.subject.get_attributes_for_oid(NameOID.COMMON_NAME) - if not cn_attributes: - msg = "Certificate subject does not contain Common Name (CN) attribute" - logger.warning(msg) - raise HTTPException(status_code=400, detail=msg) - cn = cn_attributes[0].value - logger.debug("Client certificate parsed, CN=%s", cn) - - return cast(str, cn) - - def _get_authorized_rdis(self, request: Request) -> list[str]: - """Extract allowed RDIs from custom extension with configured OID. - - The extension contains RDI identifiers encoded as ASN.1 SEQUENCE of UTF8Strings. - Example: 1.3.6.1.4.1.64609.1.1 = SEQUENCE { UTF8String:"bonares", UTF8String:"edal" } - """ - cert = self._validate_client_cert(request) - if cert is None: - logger.debug("No client certificate - returning empty authorized RDIs") - return [] - - oid = self._config.client_auth_oid - allowed_rdis = self._extract_rdis_from_extension(cert, oid) - - if not allowed_rdis: - logger.warning("No RDIs found in custom extension with OID %s", oid) - - return allowed_rdis - - @staticmethod - def _extract_rdis_from_extension(cert: x509.Certificate, oid: x509.ObjectIdentifier) -> list[str]: - """Extract RDI strings from certificate extension.""" - allowed_rdis = [] - try: - for ext in cert.extensions: - if ext.oid == oid: - allowed_rdis = Api._parse_rdi_sequence(ext) - break - except (ExtensionNotFound, TypeError, ValueError) as e: - logger.warning("Error extracting custom extension: %s", e) - return allowed_rdis - - @staticmethod - def _parse_rdi_sequence(ext: x509.Extension) -> list[str]: - """Parse DER-encoded SEQUENCE of UTF8String RDI values.""" - rdis = [] - try: - der_bytes = ext.value.public_bytes() - seq = Sequence.load(der_bytes) - # pylint: disable=consider-using-enumerate - for i in range(len(seq)): - item = seq[i] - if isinstance(item, UTF8String): - rdis.append(item.native) - logger.debug("Extracted RDIs from extension: %s", rdis) - except (TypeError, ValueError) as e: - logger.warning("Error parsing custom extension: %s", e) - return rdis - - @staticmethod - def _validate_content_type(request: Request) -> None: - content_type = request.headers.get("content-type") - if not content_type: - msg = f"Content-Type header is missing. Expected '{Api.SUPPORTED_CONTENT_TYPE}'." - logger.warning(msg) - raise HTTPException(status_code=415, detail=msg) - if content_type != Api.SUPPORTED_CONTENT_TYPE: - msg = f"Unsupported Media Type. Supported types: '{Api.SUPPORTED_CONTENT_TYPE}'." - logger.warning(msg) - raise HTTPException(status_code=415, detail=msg) - - @staticmethod - def _validate_accept_type(request: Request) -> None: - accept = request.headers.get("accept") - if accept not in [Api.SUPPORTED_ACCEPT_TYPE, "*/*"]: - msg = f"Unsupported Response Type. Supported types: '{Api.SUPPORTED_ACCEPT_TYPE}'." - logger.warning(msg) - raise HTTPException(status_code=406, detail=msg) - - def _get_known_rdis(self) -> list[str]: - return self._config.known_rdis - - def _validate_rdi_known(self, request_body: CreateOrUpdateArcsRequest) -> str: - known_rdis = self._get_known_rdis() - rdi = request_body.rdi - if rdi not in known_rdis: - raise HTTPException(status_code=400, detail=f"RDI '{rdi}' is not recognized.") - return cast(str, rdi) - - def _validate_rdi_authorized(self, request: Request, request_body: CreateOrUpdateArcsRequest) -> str: - rdi = self._validate_rdi_known(request_body) - - # If client certificates are required, check authorized RDIs from certificate - if self._config.require_client_cert: - authorized_rdis = self._get_authorized_rdis(request) - if rdi not in authorized_rdis: - raise HTTPException(status_code=403, detail=f"RDI '{rdi}' not authorized.") - else: - # If client certificates are not required, RDI just needs to be in known_rdis - logger.debug("Client certificates not required - RDI '%s' authorized via known_rdis", rdi) - - return cast(str, rdi) - - def _setup_exception_handlers(self) -> None: - @self._app.exception_handler(Exception) - async def unhandled_exception_handler(_request: Request, _exc: Exception) -> JSONResponse: - logger.error("Unhandled exception: %s", _exc) - return JSONResponse( - status_code=500, - content={"detail": "Internal Server Error. Please contact support if the problem persists."}, - ) - - def _setup_routes(self) -> None: - self._setup_whoami_route() - self._setup_liveness_route() - self._setup_health_route() - self._setup_create_or_update_arcs_route() - self._setup_task_status_route() - - def _setup_whoami_route(self) -> None: - @self._app.get("/v1/whoami", response_model=WhoamiResponse) - async def whoami( - known_rdis: Annotated[list[str], Depends(self._get_known_rdis)], - authorized_rdis: Annotated[list[str], Depends(self._get_authorized_rdis)], - client_id: Annotated[str | None, Depends(self._validate_client_id)], - _accept_validated: Annotated[None, Depends(self._validate_accept_type)], - ) -> WhoamiResponse: - logger.debug("Authorized RDIs: %s", authorized_rdis) - logger.debug("Known RDIs: %s", known_rdis) - accessible_rdis = list(set(authorized_rdis) & set(known_rdis)) - logger.debug("Accessible RDIs: %s", accessible_rdis) - return WhoamiResponse( - client_id=client_id, message="Client authenticated successfully", accessible_rdis=accessible_rdis - ) - - def _setup_liveness_route(self) -> None: - @self._app.get("/v1/liveness", response_model=LivenessResponse) - async def liveness( - _accept_validated: Annotated[None, Depends(self._validate_accept_type)], - ) -> LivenessResponse: - """Check if the API service is running.""" - return LivenessResponse() - - def _setup_health_route(self) -> None: - @self._app.get("/v1/health", response_model=HealthResponse) - def health_check( - response: Response, - _accept_validated: Annotated[None, Depends(self._validate_accept_type)], - ) -> HealthResponse: - """Check health of API and connected services (Redis, RabbitMQ).""" - # Check Redis (result backend) - redis_reachable = False - try: - # Get Redis URL from config - redis_url = ( - self._config.celery.result_backend.get_secret_value() - if self._config.celery - else "redis://localhost:6379/0" - ) - r = redis.from_url(redis_url) - r.ping() - redis_reachable = True - except redis.RedisError as e: - logger.error("Redis health check failed: %s", e) - - # Check RabbitMQ (broker) - rabbitmq_reachable = False - try: - with celery_app.connection_or_acquire() as conn: - conn.ensure_connection(max_retries=1) - rabbitmq_reachable = True - except Exception as e: # pylint: disable=broad-exception-caught - logger.error("RabbitMQ health check failed: %s", e) - - # API only checks its direct dependencies - status = { - "redis_reachable": redis_reachable, - "rabbitmq_reachable": rabbitmq_reachable, - } - - is_healthy = all(status.values()) - - if not is_healthy: - response.status_code = 503 - - return HealthResponse( - status="ok" if is_healthy else "error", - redis_reachable=redis_reachable, - rabbitmq_reachable=rabbitmq_reachable, - ) - - def _setup_create_or_update_arcs_route(self) -> None: - @self._app.post("/v1/arcs", status_code=202) - async def create_or_update_arcs( - request_body: CreateOrUpdateArcsRequest, - client_id: Annotated[str | None, Depends(self._validate_client_id)], - _content_type_validated: Annotated[None, Depends(self._validate_content_type)], - _accept_validated: Annotated[None, Depends(self._validate_accept_type)], - rdi: Annotated[str, Depends(self._validate_rdi_authorized)], - ) -> CreateOrUpdateArcsResponse: - """Submit ARCs for processing asynchronously.""" - logger.info( - "Received POST /v1/arcs request: rdi=%s, num_arcs=%d, client_id=%s", - rdi, - len(request_body.arcs), - client_id or "none", - ) - - if len(request_body.arcs) != 1: - # For now we enforce single ARC per request as per requirements - raise HTTPException( - status_code=400, detail="Currently only single ARC submission is supported per request." - ) - - # Submit task to Celery - # We take the first (and only) ARC - arc_data = request_body.arcs[0] - - # Use rate limiting config if available - # Note: rate limit is usually applied at task definition or globally, - # here we just dispatch. - - task = process_arc.delay(rdi, arc_data, client_id) - - logger.info("Enqueued task %s for ARC processing", task.id) - - return CreateOrUpdateArcsResponse(task_id=task.id, status="processing") - - def _setup_task_status_route(self) -> None: - @self._app.get("/v1/tasks/{task_id}") - async def get_task_status( - task_id: str, - _accept_validated: Annotated[None, Depends(self._validate_accept_type)], - ) -> GetTaskStatusResponse: - """Get the status of an async task.""" - result = celery_app.AsyncResult(task_id) - - task_result = None - error_message = None - - if result.ready(): - if result.successful(): - # If successful, the result is a dict representation of CreateOrUpdateArcsResponse - # (as returned by process_arc's model_dump()) - try: - task_result = CreateOrUpdateArcsResponse.model_validate(result.result) - except ValidationError as e: - # Use more specific exception handling if possible, or just log - logger.error("Failed to validate task result: %s", e) - # Fallback if result is not valid model-dump - pass - elif result.failed(): - error_message = str(result.result) - - return GetTaskStatusResponse(task_id=task_id, status=result.status, result=task_result, error=error_message) - - -middleware_api = Api(loaded_config) -app = middleware_api.app - - -# # ------------------------- -# # READ ARCs -# # ------------------------- -# @app.get("/arcs", response_model=List[ARC]) -# async def get_arcs(): -# return list(ARC_DB.values()) - -# @app.get("/arcs/{arc_id}") -# async def get_arc(arc_id: str, request: Request): -# arc = ARC_DB.get(arc_id) -# if not arc: -# raise HTTPException(status_code=404, detail="ARC not found") -# accept = request.headers.get("accept", "application/json") -# return JSONResponse(content=serialize_arc(arc, accept)) - -# # ------------------------- -# # UPDATE ARC -# # ------------------------- -# @app.put("/arcs/{arc_id}") -# async def update_arc(arc_id: str, updated: ARC): -# if arc_id not in ARC_DB: -# raise HTTPException(status_code=404, detail="ARC not found") -# updated.id = arc_id -# updated.created_at = ARC_DB[arc_id]["created_at"] -# updated.updated_at = datetime.now(UTC).isoformat() + "Z" -# ARC_DB[arc_id] = updated.dict() -# return updated - -# @app.patch("/arcs/{arc_id}") -# async def patch_arc(arc_id: str, patch_data: dict): -# if arc_id not in ARC_DB: -# raise HTTPException(status_code=404, detail="ARC not found") -# arc = ARC_DB[arc_id] -# arc.update(patch_data) -# arc["updated_at"] = datetime.now(UTC).isoformat() + "Z" -# ARC_DB[arc_id] = arc -# return arc - -# # ------------------------- -# # DELETE ARC -# # ------------------------- -# @app.delete("/arcs/{arc_id}", status_code=204) -# async def delete_arc(arc_id: str): -# if arc_id not in ARC_DB: -# raise HTTPException(status_code=404, detail="ARC not found") -# del ARC_DB[arc_id] -# return Response(status_code=204) diff --git a/middleware/api/src/middleware/api/arc_store/__init__.py b/middleware/api/src/middleware/api/arc_store/__init__.py deleted file mode 100644 index c1d412a..0000000 --- a/middleware/api/src/middleware/api/arc_store/__init__.py +++ /dev/null @@ -1,181 +0,0 @@ -"""Contains the ArcStore interface and its implementations.""" - -import hashlib -import logging -from abc import ABC, abstractmethod - -from arctrl import ARC # type: ignore[import-untyped] -from opentelemetry import trace - -logger = logging.getLogger(__name__) - - -class ArcStoreError(Exception): - """Excpetion base class for all ArcStore errors.""" - - -# ----------- Interface ----------- - - -class ArcStore(ABC): - """Abstract base class for ARC storage backends.""" - - def __init__(self) -> None: - """Initialize ArcStore with tracer.""" - self._tracer = trace.get_tracer(__name__) - - def arc_id(self, identifier: str, rdi: str) -> str: - """Generate ARC ID.""" - input_str = f"{identifier}:{rdi}" - return hashlib.sha256(input_str.encode("utf-8")).hexdigest() - - @abstractmethod - async def _create_or_update(self, arc_id: str, arc: ARC) -> None: - """Create or updates an ARC.""" - raise NotImplementedError("`ArcStore._create_or_update` is not implemented") - - @abstractmethod - async def _get(self, arc_id: str) -> ARC | None: - """Return an ARC of a given id.""" - raise NotImplementedError("`ArcStore._get` is not implemented") - - @abstractmethod - async def _delete(self, arc_id: str) -> None: - """Delete an ARC of a given id.""" - raise NotImplementedError("`ArcStore._delete` is not implemented") - - @abstractmethod - async def _exists(self, arc_id: str) -> bool: - """Check if an ARC of a given id already exists.""" - raise NotImplementedError("`ArcStore._exists` is not implemented") - - @abstractmethod - def _check_health(self) -> bool: - """Check connection to the storage backend.""" - raise NotImplementedError("`ArcStore._check_health` is not implemented") - - async def create_or_update(self, arc_id: str, arc: ARC) -> None: - """_Create or update an ARC. - - Args: - arc_id (str): ID of the ARC to create or update. - arc (ARC): ARC object to create or update. - - Raises: - ArcStoreError: If an error occurs during the operation. - - Returns: - _type_: None - - """ - with self._tracer.start_as_current_span( - "api.ArcStore.create_or_update", - attributes={"arc_id": arc_id}, - ) as span: - try: - return await self._create_or_update(arc_id, arc) - except ArcStoreError as e: - span.record_exception(e) - raise - except Exception as e: - logger.exception( - "Caught exception when trying to create or update ARC '%s': %s", - arc_id, - str(e), - ) - span.record_exception(e) - raise ArcStoreError(f"General exception caught in `ArcStore.create_or_update`: {str(e)}") from e - - async def get(self, arc_id: str) -> ARC | None: - """_Get an ARC by its ID. - - Args: - arc_id (str): ID of the ARC to retrieve. - - Returns: - Optional[ARC]: The ARC object if found, otherwise None. - - """ - with self._tracer.start_as_current_span( - "api.ArcStore.get", - attributes={"arc_id": arc_id}, - ) as span: - try: - arc = await self._get(arc_id) - span.set_attribute("found", arc is not None) - return arc - except ArcStoreError as e: - span.record_exception(e) - raise - except Exception as e: - logger.exception("Caught exception when trying to retrieve ARC '%s'", arc_id) - span.record_exception(e) - raise ArcStoreError(f"General exception caught in `ArcStore.get`: {e!r}") from e - - async def delete(self, arc_id: str) -> None: - """_Delete an ARC by its ID. - - Args: - arc_id (str): ID of the ARC to delete. - - Raises: - ArcStoreError: If an error occurs during the operation. - - Returns: - _type_: None - - """ - with self._tracer.start_as_current_span( - "api.ArcStore.delete", - attributes={"arc_id": arc_id}, - ) as span: - try: - return await self._delete(arc_id) - except ArcStoreError as e: - span.record_exception(e) - raise - except Exception as e: - logger.exception("Caught exception when trying to delete ARC '%s'", arc_id) - span.record_exception(e) - raise ArcStoreError(f"General exception caught in `ArcStore.delete`: {e!r}") from e - - async def exists(self, arc_id: str) -> bool: - """_Check if an ARC exists by its ID. - - Args: - arc_id (str): ID of the ARC to check. - - Raises: - ArcStoreError: If an error occurs during the operation. - - Returns: - bool: True if the ARC exists, False otherwise. - - """ - with self._tracer.start_as_current_span( - "api.ArcStore.exists", - attributes={"arc_id": arc_id}, - ) as span: - try: - exists = await self._exists(arc_id) - span.set_attribute("exists", exists) - return exists - except ArcStoreError as e: - span.record_exception(e) - raise - except Exception as e: - logger.exception("Caught exception when trying to check if ARC '%s' exists", arc_id) - span.record_exception(e) - raise ArcStoreError(f"Caught exception when trying to check if ARC '{arc_id}' exists: {e!r}") from e - - def check_health(self) -> bool: - """Check connection to the storage backend. - - Returns: - bool: True if backend is reachable, False otherwise. - """ - try: - return self._check_health() - except (RuntimeError, OSError, ValueError, ConnectionError, TimeoutError) as e: - logger.exception("Caught exception during health check: %s", str(e)) - return False diff --git a/middleware/api/src/middleware/api/arc_store/git_repo.py b/middleware/api/src/middleware/api/arc_store/git_repo.py deleted file mode 100644 index f7bcfb3..0000000 --- a/middleware/api/src/middleware/api/arc_store/git_repo.py +++ /dev/null @@ -1,462 +0,0 @@ -"""Implements an ArcStore using local Git CLI (via GitPython) as backend.""" - -import asyncio -import concurrent.futures -import logging -import shutil -import tempfile -from collections.abc import Callable -from pathlib import Path -from typing import Annotated, Any, TypeVar - -import git.cmd -from arctrl import ARC # type: ignore[import-untyped] -from git import Repo -from git.exc import GitCommandError -from opentelemetry import context, trace -from pydantic import BaseModel, Field, SecretStr, field_validator - -from . import ArcStore -from .remote_git_provider import ( - RemoteGitProvider, -) - -logger = logging.getLogger(__name__) - -T = TypeVar("T") - - -def is_soft_git_error(exc: GitCommandError) -> bool: - """Check if a GitCommandError is an expected 'soft' error (e.g. repo/branch not found).""" - stderr = str(getattr(exc, "stderr", "")) - # Common messages for missing repo or branch - soft_patterns = [ - "not found", - ] - return any(p in stderr.lower() for p in soft_patterns) - - -class GitRepoConfig(BaseModel): - """Configuration for Git CLI based ArcStore.""" - - url: Annotated[str, Field(description="Base URL of the git server (e.g. https://gitlab.com)")] - group: Annotated[str, Field(description="The group/namespace the ARC repos belong to")] - branch: Annotated[str, Field(description="The git branch to use for ARC repos")] = "main" - token: Annotated[SecretStr | None, Field(description="Auth token (for HTTPS auth)")] = None - user_name: Annotated[str, Field(description="Git user.name")] = "Middleware API" - user_email: Annotated[str, Field(description="Git user.email")] = "middleware@fairagro.net" - max_workers: Annotated[int, Field(description="Max threads for git operations")] = 5 - command_timeout: Annotated[float | None, Field(description="Timeout (s) for git commands")] = None - http_low_speed_limit: Annotated[int | None, Field(description="http.lowSpeedLimit in bytes/sec")] = None - http_low_speed_time: Annotated[int | None, Field(description="http.lowSpeedTime in seconds")] = None - cache_dir: Annotated[ - Path, - Field( - description="Local directory to cache git repos.", - validate_default=True, - ), - ] = None # type: ignore[assignment] - - @field_validator("url") - @classmethod - def validate_url_scheme(cls, v: str) -> str: - """Ensure URL uses HTTP, HTTPS or FILE (for tests).""" - valid_schemes = ("https://", "file://", "http://") - if not v.lower().startswith(valid_schemes): - msg = f"Git URL must start with one of: {valid_schemes}" - raise ValueError(msg) - return v - - @field_validator("cache_dir", mode="before") - @classmethod - def set_default_cache_dir(cls, v: Path | str | None) -> Path | str: - """Set default cache dir if None.""" - if v is None: - return Path(tempfile.gettempdir()) / "middleware_git_cache" - return v - - -class GitContextConfig(BaseModel): - """Configuration for a specific GitContext.""" - - repo_url: SecretStr - branch: str - user_name: str | None - user_email: str | None - local_path: Path - command_timeout: float | None = None - http_low_speed_limit: int | None = None - http_low_speed_time: int | None = None - - -_T = TypeVar("_T") - - -class GitContext: - """Context manager for handling a git repository clone.""" - - def __init__(self, config: GitContextConfig) -> None: - """Initialize GitContext.""" - self.config = config - self.repo: Repo | None = None - self._tracer = trace.get_tracer(__name__) - - def _run_git_command(self, action: str, func: Callable[..., _T], *args: object, **kwargs: object) -> _T: - """Run a git command with optional timeout and duration logging.""" - if self.config.command_timeout is not None: - kwargs.setdefault("kill_after_timeout", self.config.command_timeout) - - with self._tracer.start_as_current_span( - f"api.GitContext._run_git_command:{action}", - attributes={"git.action": action}, - set_status_on_exception=False, - ) as span: - try: - result = func(*args, **kwargs) - logger.debug("Git %s succeeded", action) - return result - except GitCommandError as exc: # pragma: no cover - behavior validated indirectly - if is_soft_git_error(exc): - # Soft errors (like 404) are expected in some workflows - # We log them at INFO (LS-remote) or DEBUG and don't mark span as error - level = logging.DEBUG if action == "ls-remote" else logging.INFO - logger.log(level, "Git %s failed as expected: %s", action, exc) - span.add_event("git.expected_failure", attributes={"stderr": str(exc.stderr)}) - span.set_status(trace.Status(trace.StatusCode.OK)) - else: - status = getattr(exc, "status", None) - status_msg = f" (status {status})" if status is not None else "" - logger.warning("Git %s failed%s: %s", action, status_msg, exc) - span.record_exception(exc) - span.set_status(trace.Status(trace.StatusCode.ERROR, str(exc))) - raise - - def _apply_repo_config(self) -> None: - """Apply user and HTTP tuning to the repository config.""" - if not self.repo: - return - - with self.repo.config_writer() as cw: - if self.config.user_name: - cw.set_value("user", "name", self.config.user_name) - if self.config.user_email: - cw.set_value("user", "email", self.config.user_email) - if self.config.http_low_speed_limit is not None: - cw.set_value("http", "lowSpeedLimit", str(self.config.http_low_speed_limit)) - if self.config.http_low_speed_time is not None: - cw.set_value("http", "lowSpeedTime", str(self.config.http_low_speed_time)) - - def _ensure_path(self) -> Path: - repo_path = self.config.local_path - if not repo_path.parent.exists(): - repo_path.parent.mkdir(parents=True, exist_ok=True) - return repo_path - - def _sync_existing_repo(self, repo_path: Path, url: str) -> None: - self.repo = Repo(repo_path) - if "origin" in self.repo.remotes: - self.repo.remotes.origin.set_url(url) - else: - self.repo.create_remote("origin", url) - - try: - self._run_git_command("fetch", self.repo.remotes.origin.fetch) - remote_ref = f"origin/{self.config.branch}" - self._run_git_command("reset", self.repo.git.reset, "--hard", remote_ref) - except GitCommandError: - logger.warning("Failed to sync repo at %s. Assuming clean state needed.", repo_path) - - def _handle_repo_init_error(self, repo_path: Path, url: str) -> None: - if not (repo_path / ".git").exists(): - logger.info("Clone failed. Initializing new repo at %s", repo_path) - self.repo = Repo.init(repo_path) - self.repo.create_remote("origin", url) - # Create a detached head if branch doesn't exist yet (e.g. empty repo) - # We don't need to force HEAD creation if it fails, just init is enough - try: - self.repo.git.checkout("-b", self.config.branch) - except GitCommandError as e: - # If branch already exists or other git error, log and continue - logger.debug("Could not create new branch '%s': %s", self.config.branch, e) - except (OSError, ValueError, IndexError, AttributeError) as e: - logger.warning("Unexpected error during repo init checkout: %s", e) - elif not self.repo: - self.repo = Repo(repo_path) - - def __enter__(self) -> "GitContext": - """Enter context: clone or init repo.""" - repo_path = self._ensure_path() - url = self.config.repo_url.get_secret_value() - - logger.debug("Accessing repo at %s", repo_path) - try: - if (repo_path / ".git").exists(): - self._sync_existing_repo(repo_path, url) - else: - self.repo = self._run_git_command( - "clone", - Repo.clone_from, - url, - repo_path, - branch=self.config.branch, - ) - except GitCommandError: - self._handle_repo_init_error(repo_path, url) - - # Configure user - self._apply_repo_config() - - return self - - def __exit__(self, exc_type: object, exc_val: object, exc_tb: object) -> None: - """Exit context.""" - if self.repo: - self.repo.close() - - @property - def path(self) -> str: - """Return the path to the repository directory.""" - return str(self.config.local_path) - - def commit_and_push(self, message: str) -> None: - """Add all changes, commit and push.""" - if not self.repo: - msg = "Repository not initialized" - raise RuntimeError(msg) - - with self._tracer.start_as_current_span("api.GitContext.commit_and_push") as span: - # Check if dirty or untracked files exist - if not self.repo.is_dirty(untracked_files=True): - logger.info("No changes to commit.") - span.set_attribute("git.dirty", False) - return - - span.set_attribute("git.dirty", True) - - with self._tracer.start_as_current_span("api.GitContext.commit_and_push:add"): - self.repo.git.add(A=True) - - with self._tracer.start_as_current_span("api.GitContext.commit_and_push:commit"): - self.repo.index.commit(message) - - logger.info("Pushing changes to remote branch %s", self.config.branch) - self._run_git_command("push", self.repo.remotes.origin.push, self.config.branch) - - -class GitRepo(ArcStore): - """Implements an ArcStore using Git CLI (GitPython) as backend.""" - - def __init__(self, config: GitRepoConfig) -> None: - """Initialize GitRepo.""" - super().__init__() - self._config = config - self._executor = concurrent.futures.ThreadPoolExecutor(max_workers=self._config.max_workers) - - # Initialize RemoteGitProvider - token = self._config.token.get_secret_value() if self._config.token else None - self._remote_provider = RemoteGitProvider.from_url( - url=self._config.url, - group=self._config.group, - token=token, - ) - - async def _run_in_executor(self, func: Callable[..., T], *args: Any) -> T: - loop = asyncio.get_running_loop() - otel_ctx = context.get_current() - - def _wrapper() -> T: - token = context.attach(otel_ctx) - try: - return func(*args) - finally: - context.detach(token) - - return await loop.run_in_executor(self._executor, _wrapper) - - def _check_health(self) -> bool: - """Check connection to the storage backend.""" - return self._remote_provider.check_health() - - def _get_context_config(self, arc_id: str) -> GitContextConfig: - auth_url = self._remote_provider.get_repo_url(arc_id, authenticated=True) - - # cache_dir is guaranteed to be a Path by Pydantic validation - local_path = self._config.cache_dir / arc_id - - return GitContextConfig( - repo_url=SecretStr(auth_url), - branch=self._config.branch, - user_name=self._config.user_name, - user_email=self._config.user_email, - local_path=local_path, - command_timeout=self._config.command_timeout, - http_low_speed_limit=self._config.http_low_speed_limit, - http_low_speed_time=self._config.http_low_speed_time, - ) - - async def _create_or_update(self, arc_id: str, arc: ARC) -> None: - """Create or update ARC using Git CLI.""" - logger.debug("Creating/updating ARC %s via Git CLI", arc_id) - - def _task() -> None: - with self._tracer.start_as_current_span( - "api.GitRepo._create_or_update", - attributes={"arc_id": arc_id}, - set_status_on_exception=False, - ) as span: - # Ensure remote exists before doing anything else (if manager is configured) - self._remote_provider.ensure_repo_exists(arc_id) - - ctx_config = self._get_context_config(arc_id) - try: - with GitContext(ctx_config) as ctx: - if not ctx.repo: - msg = "Failed to initialize git repo" - raise RuntimeError(msg) - - repo_path = Path(ctx.path) - span.set_attribute("git.local_path", str(repo_path)) - - # Cleanup existing files (except .git) to ensure sync with ARC object - for child in repo_path.iterdir(): - if child.name == ".git": - continue - if child.is_dir(): - shutil.rmtree(child) - else: - child.unlink() - - # Write ARC to repo path - with self._tracer.start_as_current_span("api.GitRepo._create_or_update:arc_write"): - arc.Write(str(repo_path)) - - # Commit and push - ctx.commit_and_push(f"Update ARC {arc_id}") - except GitCommandError as e: - if is_soft_git_error(e): - span.add_event("git.expected_failure", attributes={"stderr": str(e.stderr)}) - span.set_status(trace.Status(trace.StatusCode.OK)) - else: - span.record_exception(e) - span.set_status(trace.Status(trace.StatusCode.ERROR, str(e))) - # Try to diagnose connection issues - self._check_health() - raise - finally: - # Clean up local repository to prevent inode exhaustion - if ctx_config.local_path.exists(): - try: - shutil.rmtree(ctx_config.local_path) - except OSError as e: - logger.warning("Failed to clean up local path %s: %s", ctx_config.local_path, e) - - await self._run_in_executor(_task) - - async def _get(self, arc_id: str) -> ARC | None: - """Get ARC from Git.""" - - def _task() -> ARC | None: - with self._tracer.start_as_current_span( - "api.GitRepo._get", - attributes={"arc_id": arc_id}, - set_status_on_exception=False, - ) as span: - ctx_config = self._get_context_config(arc_id) - try: - with GitContext(ctx_config) as ctx: - if not ctx.repo: - span.set_attribute("found", False) - return None - span.set_attribute("git.local_path", str(ctx.path)) - try: - with self._tracer.start_as_current_span("api.GitRepo._get:arc_load"): - arc = ARC.load(ctx.path) - span.set_attribute("found", arc is not None) - return arc - except (FileNotFoundError, OSError) as e: - logger.warning("File system error loading ARC from repo %s: %s", arc_id, e) - span.record_exception(e) - return None - except GitCommandError as e: - if is_soft_git_error(e): - logger.debug("Failed to clone/access repo for %s: %s", arc_id, e) - span.add_event("git.expected_failure", attributes={"stderr": str(e.stderr)}) - span.set_status(trace.Status(trace.StatusCode.OK)) - else: - logger.warning("Failed to clone/access repo for %s: %s", arc_id, e) - span.record_exception(e) - span.set_status(trace.Status(trace.StatusCode.ERROR, str(e))) - return None - except Exception as e: # pylint: disable=broad-exception-caught # noqa: BLE001 - logger.warning( - "Failed to load ARC from repo %s (might not be an ARC or invalid): %s", - arc_id, - e, - ) - span.record_exception(e) - return None - finally: - # Clean up local repository to prevent inode exhaustion - if ctx_config.local_path.exists(): - try: - shutil.rmtree(ctx_config.local_path) - except OSError as e: - logger.warning("Failed to clean up local path %s: %s", ctx_config.local_path, e) - - return await self._run_in_executor(_task) - - async def _delete(self, arc_id: str) -> None: - """Delete ARC (Not supported via Git CLI easily without platform API).""" - logger.warning( - "Delete operation is not supported by GitRepo (CLI backend). Manual deletion required for %s", - arc_id, - ) - - async def _exists(self, arc_id: str) -> bool: - """Check if ARC repo exists.""" - - def _task() -> bool: - with self._tracer.start_as_current_span( - "api.GitRepo._exists", - attributes={"arc_id": arc_id}, - set_status_on_exception=False, - ) as span: - # We can try to ls-remote using the authenticated URL - url = self._remote_provider.get_repo_url(arc_id, authenticated=True) - span.set_attribute("git.repo_url", url) - - g = git.cmd.Git() - try: - with self._tracer.start_as_current_span( - "api.GitRepo._exists:ls-remote", - set_status_on_exception=False, - ) as inner_span: - try: - if self._config.command_timeout is not None: - g.ls_remote(url, kill_after_timeout=self._config.command_timeout) - else: - g.ls_remote(url) - inner_span.set_status(trace.Status(trace.StatusCode.OK)) - except GitCommandError as e: - if is_soft_git_error(e): - inner_span.set_status(trace.Status(trace.StatusCode.OK)) - inner_span.add_event("git.expected_failure", attributes={"stderr": str(e.stderr)}) - else: - inner_span.set_status(trace.Status(trace.StatusCode.ERROR, str(e))) - inner_span.record_exception(e) - raise - - logger.info("Git ls-remote for %s succeeded", arc_id) - span.set_attribute("exists", True) - return True - except GitCommandError as e: - if is_soft_git_error(e): - logger.debug("Git ls-remote for %s failed (repo not found)", arc_id) - span.set_status(trace.Status(trace.StatusCode.OK)) - else: - logger.warning("Git ls-remote for %s failed: %s", arc_id, e) - span.set_status(trace.Status(trace.StatusCode.ERROR, str(e))) - span.set_attribute("exists", False) - return False - - return await self._run_in_executor(_task) diff --git a/middleware/api/src/middleware/api/arc_store/gitlab_api.py b/middleware/api/src/middleware/api/arc_store/gitlab_api.py deleted file mode 100644 index bafa230..0000000 --- a/middleware/api/src/middleware/api/arc_store/gitlab_api.py +++ /dev/null @@ -1,387 +0,0 @@ -"""Implements an ArcStore using Gitlab API as backend.""" - -import asyncio -import base64 -import concurrent.futures -import hashlib -import logging -import tempfile -from collections.abc import Callable -from pathlib import Path -from typing import Annotated, Any, TypeVar - -import gitlab -from arctrl import ARC # type: ignore[import-untyped] -from gitlab.exceptions import GitlabGetError -from gitlab.v4.objects import Project, ProjectFile -from opentelemetry import context -from pydantic import BaseModel, Field, HttpUrl, SecretStr, field_validator - -from . import ArcStore - -logger = logging.getLogger(__name__) - -T = TypeVar("T") - - -class GitlabApiConfig(BaseModel): - """Configuration for Gitlab API ArcStore.""" - - url: Annotated[HttpUrl, Field(description="URL of the gitlab server to store ARCs in")] - group: Annotated[ - str, - Field( - description="The gitlab group the ARC repos belong to", - min_length=1, # may not be empty - ), - ] - branch: Annotated[str, Field(description="The git branch to use for ARC repos")] = "main" - token: Annotated[ - SecretStr, - Field(description="A gitlab token with CRUD permissions to the gitlab group"), - ] - max_workers: Annotated[ - int, - Field( - description="Maximum number of parallel threads for GitLab API calls", - ge=1, - ), - ] = 5 - commit_chunk_size: Annotated[ - int, - Field( - description="Maximum number of file actions per commit (avoids 'Too many total parameters' error)", - ge=1, - ), - ] = 100 - - @field_validator("group", mode="before") - @classmethod - def to_lowercase(cls, v: str) -> str: - """Ensure group is lowercase and trimmed. - - Args: - v (str): Input value. - - Returns: - str: Normalized value. - - """ - if isinstance(v, str): - return v.lower().strip() - return v - - -class GitlabApi(ArcStore): - """Implements an ArcStore using Gitlab API as backend.""" - - def __init__(self, config: GitlabApiConfig) -> None: - """Konstruktor. - - Args: - config (GitlabApiConfig): Configuration for the Gitlab API ArcStore. - - """ - super().__init__() - logger.info("Initializing ARCPersistenceGitlabAPI") - self._config = config - self._gitlab = gitlab.Gitlab(str(self._config.url), private_token=self._config.token.get_secret_value()) - self._executor = concurrent.futures.ThreadPoolExecutor(max_workers=self._config.max_workers) - - async def _run_in_executor(self, func: Callable[..., T], *args: Any) -> T: - loop = asyncio.get_running_loop() - otel_ctx = context.get_current() - - def _wrapper() -> T: - token = context.attach(otel_ctx) - try: - return func(*args) - finally: - context.detach(token) - - return await loop.run_in_executor(self._executor, _wrapper) - - def _check_health(self) -> bool: - """Check connection to the storage backend.""" - try: - self._gitlab.auth() - # Also check if we can access the group - self._gitlab.groups.get(self._config.group) - return True - except (gitlab.GitlabError, OSError, ValueError): - logger.exception("GitLab health check failed") - return False - - # -------------------------- Project Handling -------------------------- - def _get_or_create_project(self, arc_id: str) -> Project: - with self._tracer.start_as_current_span( - "api.GitlabApi._get_or_create_project", - attributes={"arc_id": arc_id}, - ): - logger.debug("Looking up GitLab project for ARC: %s", arc_id) - projects = self._gitlab.projects.list(search=arc_id) - for project in projects: - if project.path == arc_id: - logger.debug("Found existing project: %s (id=%s)", arc_id, project.id) - return project - logger.info("Creating new GitLab project for ARC: %s", arc_id) - group = self._gitlab.groups.get(self._config.group) - new_project = self._gitlab.projects.create( - { - "name": arc_id, - "path": arc_id, - "namespace_id": group.id, - "initialize_with_readme": False, - } - ) - logger.info("Created project: %s (id=%s)", arc_id, new_project.id) - return new_project - - def _find_project(self, arc_id: str) -> Project | None: - with self._tracer.start_as_current_span( - "api.GitlabApi._find_project", - attributes={"arc_id": arc_id}, - ): - logger.debug("Searching for GitLab project: %s", arc_id) - projects = self._gitlab.projects.list(search=arc_id) - result = next((p for p in projects if p.path == arc_id), None) - if result: - logger.debug("Found project: %s (id=%s)", arc_id, result.id) - else: - logger.debug("Project not found: %s", arc_id) - return result - - # -------------------------- Hashing -------------------------- - def _compute_arc_hash(self, arc_dir: Path) -> str: - sha = hashlib.sha256() - for file_path in sorted(arc_dir.rglob("*")): - if file_path.is_file(): - with open(file_path, "rb") as f: - while chunk := f.read(8192): - sha.update(chunk) - return sha.hexdigest() - - def _load_old_hash(self, project: Project) -> str | None: - with self._tracer.start_as_current_span("api.GitlabApi._load_old_hash"): - try: - old_hash_file = project.files.get(file_path=".arc_hash", ref=self._config.branch) - old_hash = base64.b64decode(old_hash_file.content).decode("utf-8").strip() - logger.debug("Loaded existing ARC hash from GitLab: %s", old_hash[:16]) - return old_hash - except GitlabGetError: - logger.debug("No existing .arc_hash file found in project") - return None - - # -------------------------- File Actions -------------------------- - def _get_existing_files(self, project: Project) -> set[str]: - """Get all existing file paths in the project with a single API call.""" - with self._tracer.start_as_current_span( - "api.GitlabApi._get_existing_files", - attributes={"project_id": project.id}, - ): - try: - logger.debug("Fetching repository tree for project %s", project.id) - tree = project.repository_tree(ref=self._config.branch, all=True, recursive=True, per_page=100) - file_paths = {item["path"] for item in tree if item["type"] == "blob"} - logger.debug("Found %d existing files in repository", len(file_paths)) - return file_paths - except GitlabGetError: - # Branch doesn't exist yet (new project) - logger.debug("Branch %s doesn't exist yet (new project)", self._config.branch) - return set() - - def _prepare_file_actions( - self, project: Project, arc_path: Path, old_hash: str | None, new_hash: str - ) -> list[dict[str, Any]]: - """Prepare file actions with optimized batch file existence check.""" - with self._tracer.start_as_current_span( - "api.GitlabApi._prepare_file_actions", - attributes={"arc_path": str(arc_path)}, - ) as span: - logger.debug("Preparing file actions for ARC at: %s", arc_path) - # Single API call to get all existing files - existing_files = self._get_existing_files(project) - span.set_attribute("existing_files_count", len(existing_files)) - - actions = [] - for file_path in arc_path.rglob("*"): - if not file_path.is_file(): - continue - relative_path = str(file_path.relative_to(arc_path)) - action_type = "update" if relative_path in existing_files else "create" - actions.append(self._build_file_action(file_path, relative_path, action_type)) - - span.set_attribute("actions_count", len(actions)) - logger.debug("Prepared %d file actions (%d existing files)", len(actions), len(existing_files)) - - # ARC hash action separat hinzufügen - actions.append(self._build_hash_action(old_hash, new_hash)) - return actions - - def _build_file_action(self, file_path: Path, relative_path: str, action_type: str) -> dict[str, Any]: - """Erstellt ein Action-Dict für eine Datei (Text oder Binär).""" - content_bytes = file_path.read_bytes() - if self._is_text_file(content_bytes): - return { - "action": action_type, - "file_path": relative_path, - "content": content_bytes.decode("utf-8"), - } - return { - "action": action_type, - "file_path": relative_path, - "content": base64.b64encode(content_bytes).decode("utf-8"), - "encoding": "base64", - } - - def _is_text_file(self, content_bytes: bytes) -> bool: - """Gibt True zurück, wenn Datei UTF-8-dekodierbar ist.""" - try: - content_bytes.decode("utf-8") - return True - except UnicodeDecodeError: - return False - - def _build_hash_action(self, old_hash: str | None, new_hash: str) -> dict[str, Any]: - """Erstellt die Commit-Action für die .arc_hash Datei.""" - return { - "action": "create" if not old_hash else "update", - "file_path": ".arc_hash", - "content": new_hash, - } - - # -------------------------- Commit -------------------------- - def _commit_actions(self, project: Project, actions: list[dict[str, Any]], arc_id: str) -> None: - with self._tracer.start_as_current_span( - "api.GitlabApi._commit_actions", - attributes={"arc_id": arc_id, "num_actions": len(actions)}, - ): - logger.debug("Committing %d actions to GitLab for ARC: %s", len(actions), arc_id) - - # Split actions into chunks to avoid "Too many total parameters" error - chunk_size = self._config.commit_chunk_size - action_chunks = [actions[i : i + chunk_size] for i in range(0, len(actions), chunk_size)] - total_chunks = len(action_chunks) - - if total_chunks > 1: - logger.info( - "Commit for ARC %s is large, splitting into %d chunks (chunk_size=%d)", - arc_id, - total_chunks, - chunk_size, - ) - - for i, chunk in enumerate(action_chunks): - commit_message = ( - f"Add/update ARC {arc_id}" - if total_chunks == 1 - else f"Add/update ARC {arc_id} (part {i + 1}/{total_chunks})" - ) - commit_data = { - "branch": self._config.branch, - "commit_message": commit_message, - "actions": chunk, - } - - with self._tracer.start_as_current_span( - "api.GitlabApi._commit_actions:chunk", - attributes={"arc_id": arc_id, "chunk_num": i + 1, "chunk_size": len(chunk)}, - ): - commit = project.commits.create(commit_data) - logger.info( - "Successfully committed chunk %d/%d for ARC %s to GitLab (commit: %s)", - i + 1, - total_chunks, - arc_id, - commit.id[:8], - ) - - # -------------------------- Create/Update -------------------------- - async def _create_or_update(self, arc_id: str, arc: ARC) -> None: - logger.debug("Creating/updating ARC %s in GitLab", arc_id) - - project = await self._run_in_executor(self._get_or_create_project, arc_id) - - with tempfile.TemporaryDirectory() as tmp_root: - arc_path = Path(tmp_root) / arc_id - arc_path.mkdir(parents=True, exist_ok=True) - - # arc.Write is not async, run in executor - logger.debug("Writing ARC to temporary directory: %s", arc_path) - await self._run_in_executor(arc.Write, str(arc_path)) - - # Compute hash once with tracing - with self._tracer.start_as_current_span("api.GitlabApi._compute_arc_hash"): - new_hash = await self._run_in_executor(self._compute_arc_hash, arc_path) - logger.debug("Computed ARC hash: %s", new_hash[:16]) - - old_hash = await self._run_in_executor(self._load_old_hash, project) - - if new_hash == old_hash: - logger.info("ARC %s unchanged (hash: %s...), skipping commit", arc_id, new_hash[:16]) - return - - logger.debug("ARC %s has changed, preparing commit", arc_id) - actions = await self._run_in_executor(self._prepare_file_actions, project, arc_path, old_hash, new_hash) - await self._run_in_executor(self._commit_actions, project, actions, arc_id) - - # -------------------------- Get -------------------------- - async def _get(self, arc_id: str) -> ARC | None: - def _task() -> ARC | None: - project = self._find_project(arc_id) - if not project: - return None - with tempfile.TemporaryDirectory() as tmp_root: - arc_path = Path(tmp_root) / arc_id - arc_path.mkdir(parents=True, exist_ok=True) - self._download_project_files(project, arc_path) - try: - return ARC.load(str(arc_path)) - except FileNotFoundError as e: - logger.warning("ARC files for %s not found: %s", arc_id, e) - return None - except Exception as e: - logger.error("Unexpected error loading ARC for %s: %s", arc_id, e, exc_info=True) - raise - - return await self._run_in_executor(_task) - - def _download_project_files(self, project: Project, arc_path: Path) -> None: - tree = project.repository_tree(ref=self._config.branch, all=True, recursive=True) - for entry in tree: - if entry["type"] != "blob" or entry["path"] == ".arc_hash": - continue - f = project.files.get(file_path=entry["path"], ref=self._config.branch) - file_path = arc_path / entry["path"] - file_path.parent.mkdir(parents=True, exist_ok=True) - self._write_project_file(f, file_path) - - def _write_project_file(self, f: ProjectFile, file_path: Path) -> None: - content_bytes = base64.b64decode(f.content) - if getattr(f, "encoding", None) == "base64": - file_path.write_bytes(content_bytes) - else: - try: - text_content = content_bytes.decode("utf-8") - file_path.write_text(text_content, encoding="utf-8") - except UnicodeDecodeError: - file_path.write_bytes(content_bytes) - - # -------------------------- Delete -------------------------- - async def _delete(self, arc_id: str) -> None: - def _task() -> None: - project = self._find_project(arc_id) - if project: - project.delete() - else: - logger.warning("Project %s not found for deletion.", arc_id) - - await self._run_in_executor(_task) - - # -------------------------- Exists -------------------------- - async def _exists(self, arc_id: str) -> bool: - def _task() -> bool: - project = self._find_project(arc_id) - return bool(project) - - return await self._run_in_executor(_task) diff --git a/middleware/api/src/middleware/api/arc_store/remote_git_provider.py b/middleware/api/src/middleware/api/arc_store/remote_git_provider.py deleted file mode 100644 index 67c3f86..0000000 --- a/middleware/api/src/middleware/api/arc_store/remote_git_provider.py +++ /dev/null @@ -1,191 +0,0 @@ -"""Abstraction for managing remote git repositories on different platforms.""" - -import http -import logging -import urllib.error -import urllib.request -from abc import ABC, abstractmethod -from pathlib import Path -from urllib.parse import quote, unquote, urlparse - -import gitlab -from git import Repo -from gitlab.exceptions import GitlabError, GitlabGetError -from opentelemetry import trace - -from . import ArcStoreError - -logger = logging.getLogger(__name__) - - -class RemoteGitProvider(ABC): - """Base class for managing remote git repositories.""" - - @abstractmethod - def ensure_repo_exists(self, arc_id: str) -> None: - """Ensure the remote repository exists.""" - pass - - @abstractmethod - def get_repo_url(self, arc_id: str, authenticated: bool = True) -> str: - """Construct the URL for the remote repository.""" - pass - - @abstractmethod - def check_health(self) -> bool: - """Check if the remote storage is reachable.""" - pass - - @staticmethod - def from_url( - url: str, - group: str, - token: str | None = None, - ) -> "RemoteGitProvider": - """Create a provider based on the URL protocol. - - Args: - url (str): Base URL of the git server. - group (str): Group/Namespace name. - token (str | None): Optional auth token. - - Returns: - RemoteGitProvider: An instance of a concrete provider. - - Raises: - ValueError: If no suitable provider can be determined. - - """ - url_lower = url.lower() - - if url_lower.startswith("file://"): - return FileSystemGitProvider(url, group) - - # TODO: any URL that is not file:// is assumed to be GitLab for now, - # but of course, this is a bold assumption. We should improve this later. - # Maybe there is a way to detect GitLab and bailout if not? - if url_lower.startswith(("http://", "https://")): - return GitlabGitProvider(url=url, group_name=group, token=token) - - msg = f"Could not determine git provider for URL '{url}'. Supported protocols: file://, http://, https://" - raise ValueError(msg) - - -class FileSystemGitProvider(RemoteGitProvider): - """Provider for local file-system based 'remotes' (mainly for tests).""" - - def __init__(self, base_url: str, group: str) -> None: - """Initialize with base URL and group.""" - self.base_url = base_url.rstrip("/") - self.group = group.strip("/") - - def ensure_repo_exists(self, arc_id: str) -> None: - """Create a bare repository on the local filesystem if it doesn't exist.""" - parsed_url = urlparse(self.base_url) - if parsed_url.scheme.lower() != "file": - return - - # The path from a file URL might be URL-encoded (e.g. spaces as %20). - # We assume the path is local and can ignore the netloc part. - base_path = Path(unquote(parsed_url.path)) - remote_path = base_path / self.group / f"{arc_id}.git" - - if not remote_path.exists(): - logger.info("Creating local 'remote' bare repository at %s", remote_path) - remote_path.parent.mkdir(parents=True, exist_ok=True) - Repo.init(remote_path, bare=True) - - def get_repo_url(self, arc_id: str, authenticated: bool = True) -> str: # noqa: ARG002 - """Return the file:// URL for the repository.""" - return f"{self.base_url}/{self.group}/{arc_id}.git" - - def check_health(self) -> bool: - """FileSystem 'remote' is always considered healthy if base URL starts with file://.""" - return self.base_url.lower().startswith("file://") - - -class GitlabGitProvider(RemoteGitProvider): - """Provider for GitLab hosted repositories.""" - - def __init__(self, url: str, group_name: str, token: str | None = None) -> None: - """Initialize with GitLab connection details.""" - self.url = url.rstrip("/") - self.group_name = group_name.strip("/") - self.token = token - self._gl = None - if token: - self._gl = gitlab.Gitlab(url=url, private_token=token) - self._tracer = trace.get_tracer(__name__) - - def ensure_repo_exists(self, arc_id: str) -> None: - """Ensure the project exists in the GitLab group.""" - if not self._gl: - logger.debug("Skipping project creation check (no GitLab token provided)") - return - - with self._tracer.start_as_current_span( - "api.GitlabGitProvider.ensure_repo_exists", - attributes={"arc_id": arc_id, "group": self.group_name}, - ): - try: - # 1. Get the group - group = self._gl.groups.get(self.group_name) - - # 2. Check if project exists - project_path = f"{group.full_path}/{arc_id}" - try: - self._gl.projects.get(project_path) - logger.debug("GitLab project %s already exists", project_path) - except GitlabGetError: - # 3. Create project if it doesn't exist - logger.info("Creating new GitLab project: %s in group %s", arc_id, self.group_name) - self._gl.projects.create( - { - "name": arc_id, - "path": arc_id, - "namespace_id": group.id, - "visibility": "private", - } - ) - except GitlabError as e: - msg = f"GitLab API error: {e}" - # Handle 401 specifically to help users find the cause (wrong token) - if hasattr(e, "response_code") and e.response_code == http.HTTPStatus.UNAUTHORIZED: - msg = ( - "GitLab API 401 Unauthorized: Please check your token and its permissions (API scope required)." - ) - - logger.error("Failed to ensure GitLab project exists: %s", msg) - raise ArcStoreError(msg) from e - - def get_repo_url(self, arc_id: str, authenticated: bool = True) -> str: - """Construct the GitLab repository URL, optionally with auth token.""" - repo_url = f"{self.url}/{self.group_name}/{arc_id}.git" - if not authenticated or not self.token: - return repo_url - - safe_token = quote(self.token) - if repo_url.startswith("https://"): - return f"https://oauth2:{safe_token}@{repo_url[8:]}" - if repo_url.startswith("http://"): - return f"http://oauth2:{safe_token}@{repo_url[7:]}" - return repo_url - - def check_health(self) -> bool: - """Check if the GitLab server is reachable.""" - if self._gl: - try: - self._gl.auth() - return True - except GitlabError: - return False - - # Fallback for when no token is provided but we still want to check reachability - try: - with urllib.request.urlopen(self.url, timeout=5) as response: # nosec B310 - return bool(response.status < http.HTTPStatus.BAD_REQUEST) - except (urllib.error.URLError, TimeoutError): - return False - except Exception as e: # pylint: disable=broad-exception-caught - logger.warning("Unexpected error during health check for %s: %s", self.url, e) - return False diff --git a/middleware/api/src/middleware/api/business_logic.py b/middleware/api/src/middleware/api/business_logic.py deleted file mode 100644 index d8de672..0000000 --- a/middleware/api/src/middleware/api/business_logic.py +++ /dev/null @@ -1,217 +0,0 @@ -"""Business logic module for handling ARC (Automated Research Compendium) operations. - -This module provides: -- ARC status management and responses -- JSON validation and processing -- Business logic for creating, updating, and managing ARCs -""" - -import asyncio -import json -import logging -from datetime import UTC, datetime -from typing import Any - -from arctrl import ARC # type: ignore[import-untyped] -from opentelemetry import trace - -from middleware.shared.api_models.models import ( - ArcResponse, - ArcStatus, - CreateOrUpdateArcsResponse, -) - -from .arc_store import ArcStore - -logger = logging.getLogger(__name__) - - -class BusinessLogicError(Exception): - """Base exception class for all business logic errors.""" - - -class InvalidJsonSemanticError(BusinessLogicError): - """Arises when the ARC JSON syntax is valid but semantically incorrect. - - For example, missing required fields or invalid values. - """ - - -class BusinessLogic: - """Core business logic for handling ARC operations.""" - - def __init__(self, store: ArcStore) -> None: - """Initialize the BusinessLogic with the given ArcStore. - - Args: - store (ArcStore): An instance of ArcStore for ARC persistence. - - """ - self._store = store - self._tracer = trace.get_tracer(__name__) - - async def _create_arc_from_rocrate(self, rdi: str, arc_dict: dict) -> ArcResponse: - """Create an ARC from RO-Crate JSON with tracing.""" - with self._tracer.start_as_current_span( - "api.BusinessLogic._create_arc_from_rocrate", - attributes={"rdi": rdi, "arc_index": len(getattr(arc_dict, "__dict__", {}))}, - ) as span: - logger.debug("Processing RO-Crate JSON for RDI: %s", rdi) - try: - with self._tracer.start_as_current_span("api.BusinessLogic._create_arc_from_rocrate:json_serialize"): - arc_json = json.dumps(arc_dict) - - with self._tracer.start_as_current_span("api.BusinessLogic._create_arc_from_rocrate:arc_parse_rocrate"): - arc = ARC.from_rocrate_json_string(arc_json) - - logger.debug("Successfully parsed ARC from RO-Crate JSON") - except Exception as e: - logger.error("Failed to parse RO-Crate JSON: %s", e, exc_info=True) - span.record_exception(e) - raise InvalidJsonSemanticError(f"Error processing RO-Crate JSON: {e!r}") from e - - identifier = getattr(arc, "Identifier", None) - if not identifier or identifier == "": - logger.error("ARC missing identifier in RO-Crate JSON") - raise InvalidJsonSemanticError("RO-Crate JSON must contain an 'Identifier' in the ISA object.") - - arc_id = self._store.arc_id(identifier, rdi) - exists = await self._store.exists(arc_id) - logger.debug("ARC identifier=%s, arc_id=%s, exists=%s", identifier, arc_id, exists) - - span.set_attribute("arc_id", arc_id) - span.set_attribute("arc_exists", exists) - - await self._store.create_or_update(arc_id, arc) - status = ArcStatus.UPDATED if exists else ArcStatus.CREATED - logger.info("ARC %s: %s (id=%s)", status.value, identifier, arc_id) - - return ArcResponse( - id=arc_id, - status=status, - timestamp=datetime.now(UTC).isoformat() + "Z", - ) - - async def _process_arcs(self, rdi: str, arcs: list[Any]) -> list[ArcResponse]: - """Process a batch of ARCs with span for batch timing.""" - logger.debug("Processing batch of %d ARCs for RDI: %s", len(arcs), rdi) - with self._tracer.start_as_current_span( - "api.BusinessLogic._process_arcs", - attributes={"rdi": rdi, "batch_size": len(arcs)}, - ): - tasks = [self._create_arc_from_rocrate(rdi, arc) for arc in arcs] - # Use return_exceptions=True to ensure one failure doesn't stop the whole batch - results = await asyncio.gather(*tasks, return_exceptions=True) - - processed_arcs: list[ArcResponse] = [] - for i, res in enumerate(results): - if isinstance(res, Exception): - logger.error( - "Failed to process ARC at index %d in batch for RDI %s: %s", i, rdi, res, exc_info=True - ) - else: - processed_arcs.append(res) # type: ignore[arg-type] - - logger.debug("Batch processing complete: %d/%d ARCs processed successfully", len(processed_arcs), len(arcs)) - return processed_arcs - - # -------------------------- Create or Update ARCs -------------------------- - # TODO: in the first implementation, we accepted string data for ARC JSON, - # now we accept list[Any] that is already validated using Pydantic in the API layer. - # The question is: do we need validation on the BusinessLogic layer as well? - # Depending on the answer, we need to refactor the current validation approach. - async def create_or_update_arcs( - self, rdi: str, arcs: list[Any], client_id: str | None - ) -> CreateOrUpdateArcsResponse: - """Create or update ARCs based on the provided RO-Crate JSON data. - - Args: - rdi: Research Data Infrastructure identifier. - arcs: List of ARC definitions. - client_id: The client identifier, or None if not authenticated. - - Raises: - InvalidJsonSemanticError: If the JSON is semantically incorrect. - BusinessLogicError: If an error occurs during the operation. - - Returns: - CreateOrUpdateArcsResponse: Response containing details of the processed - ARCs. - - """ - with self._tracer.start_as_current_span( - "api.BusinessLogic.create_or_update_arcs", - attributes={"rdi": rdi, "num_arcs": len(arcs), "client_id": client_id or "none"}, - ) as span: - logger.info( - "Starting ARC creation/update: rdi=%s, num_arcs=%d, client_id=%s", rdi, len(arcs), client_id or "none" - ) - try: - # We do not catch InvalidJsonSemanticError or BusinessLogicError here anymore explicitly - # because individual ARC failures are handled inside _process_arcs now. - # Only if _process_arcs itself crashes (unexpectedly) will we land here. - result = await self._process_arcs(rdi, arcs) - - span.set_attribute("success", True) - span.set_attribute("processed_count", len(result)) - - logger.info("Successfully processed %d/%d ARCs for RDI: %s", len(result), len(arcs), rdi) - - return CreateOrUpdateArcsResponse( - client_id=client_id, - rdi=rdi, - message=f"Processed {len(result)}/{len(arcs)} ARCs successfully", - arcs=result, - ) - except Exception as e: - logger.error("Unexpected error while processing ARCs batch: %s", e, exc_info=True) - span.record_exception(e) - raise BusinessLogicError(f"unexpected error encountered: {str(e)}") from e - - # # ------------------------- - # # READ ARCs - # # ------------------------- - # @app.get("/arcs", response_model=List[ARC]) - # async def get_arcs(): - # return list(ARC_DB.values()) - - # @app.get("/arcs/{arc_id}") - # async def get_arc(arc_id: str, request: Request): - # arc = ARC_DB.get(arc_id) - # if not arc: - # raise HTTPException(status_code=404, detail="ARC not found") - # accept = request.headers.get("accept", "application/json") - # return JSONResponse(content=serialize_arc(arc, accept)) - - # # ------------------------- - # # UPDATE ARC - # # ------------------------- - # @app.put("/arcs/{arc_id}") - # async def update_arc(arc_id: str, updated: ARC): - # if arc_id not in ARC_DB: - # raise HTTPException(status_code=404, detail="ARC not found") - # updated.id = arc_id - # updated.created_at = ARC_DB[arc_id]["created_at"] - # updated.updated_at = datetime.now(UTC).isoformat() + "Z" - # ARC_DB[arc_id] = updated.dict() - # return updated - - # @app.patch("/arcs/{arc_id}") - # async def patch_arc(arc_id: str, patch_data: dict): - # if arc_id not in ARC_DB: - # raise HTTPException(status_code=404, detail="ARC not found") - # arc = ARC_DB[arc_id] - # arc.update(patch_data) - # arc["updated_at"] = datetime.now(UTC).isoformat() + "Z" - # ARC_DB[arc_id] = arc - # return arc - - # # ------------------------- - # # DELETE ARC - # # ------------------------- - # @app.delete("/arcs/{arc_id}", status_code=204) - # async def delete_arc(arc_id: str): - # if arc_id not in ARC_DB: - # raise HTTPException(status_code=404, detail="ARC not found") - # del ARC_DB[arc_id] - # return Response(status_code=204) diff --git a/middleware/api/src/middleware/api/celery_app.py b/middleware/api/src/middleware/api/celery_app.py deleted file mode 100644 index 1453fc7..0000000 --- a/middleware/api/src/middleware/api/celery_app.py +++ /dev/null @@ -1,92 +0,0 @@ -"""Celery application configuration and initialization. - -This module sets up the Celery app for the middleware API, including: -- Broker and backend configuration from YAML config file -- Optional OpenTelemetry instrumentation for distributed tracing -- Task serialization and timezone settings -""" - -import logging -import os -import sys -from pathlib import Path - -from celery import Celery - -from .config import Config - -logger = logging.getLogger(__name__) - -# Load config from YAML file -config_path = Path(os.environ.get("MIDDLEWARE_API_CONFIG", "/run/secrets/middleware-api-config")) - -# Global config instance (can be None in test mode) -loaded_config: Config | None = None - -# Check if running in test environment (pytest sets PYTEST_CURRENT_TEST) or if config file doesn't exist -if "pytest" in sys.modules or not config_path.is_file(): - # Create a dummy celery app for testing - logger.info("Running in test mode - using dummy Celery configuration") - celery_app = Celery( - "middleware_api", - broker="memory://", - backend="cache+memory://", - include=["middleware.api.worker"], - ) - loaded_config = None -else: - loaded_config = Config.from_yaml_file(config_path) - - if not loaded_config.celery: - logger.error("Celery configuration missing in config file") - raise ValueError("Celery configuration missing in config file") - - broker_url = loaded_config.celery.broker_url.get_secret_value() - backend_url = loaded_config.celery.result_backend.get_secret_value() - - logger.info("Celery configured with broker: %s", broker_url) - - celery_app = Celery( - "middleware_api", - broker=broker_url, - backend=backend_url, - include=["middleware.api.worker"], - ) - - # Instrument the Celery app if OTLP endpoint is configured - try: - from .tracing import instrument_celery - - instrument_celery(celery_app) - except ImportError: - # Graceful fallback if dependencies are missing or during build - pass - -celery_app.conf.update( - task_serializer="json", - accept_content=["json"], - result_serializer="json", - timezone="UTC", - enable_utc=True, - task_track_started=True, -) - -# Initialize BusinessLogic for workers (None in test mode) -business_logic = None -if loaded_config is not None: - from .arc_store import ArcStore - from .arc_store.git_repo import GitRepo - from .arc_store.gitlab_api import GitlabApi - from .business_logic import BusinessLogic - - store: ArcStore - if loaded_config.gitlab_api: - store = GitlabApi(loaded_config.gitlab_api) - elif loaded_config.git_repo: - store = GitRepo(loaded_config.git_repo) - else: - logger.error("Invalid ArcStore configuration") - raise ValueError("Invalid ArcStore configuration") - - business_logic = BusinessLogic(store) - logger.info("BusinessLogic initialized for Celery workers") diff --git a/middleware/api/src/middleware/api/config.py b/middleware/api/src/middleware/api/config.py deleted file mode 100644 index d21eeb8..0000000 --- a/middleware/api/src/middleware/api/config.py +++ /dev/null @@ -1,76 +0,0 @@ -"""FAIRagro Middleware API configuration module.""" - -import logging -import re -from typing import Annotated, ClassVar, Self - -from cryptography import x509 -from pydantic import BaseModel, ConfigDict, Field, SecretStr, field_validator, model_validator - -from middleware.shared.config.config_base import ConfigBase - -from .arc_store.git_repo import GitRepoConfig -from .arc_store.gitlab_api import GitlabApiConfig - - -class CeleryConfig(BaseModel): - """Configuration for Celery worker.""" - - broker_url: Annotated[SecretStr, Field(description="RabbitMQ broker URL")] - result_backend: Annotated[SecretStr, Field(description="Redis backend URL")] - task_rate_limit: Annotated[str | None, Field(description="Rate limit for tasks (e.g. '10/m')")] = None - - -class Config(ConfigBase): - """Configuration model for the Middleware API.""" - - known_rdis: Annotated[list[str], Field(description="List of known RDI identifiers")] = [] - client_auth_oid: Annotated[x509.ObjectIdentifier, Field(description="OID for client authentication")] = ( - x509.ObjectIdentifier("1.3.6.1.4.1.64609.1.1") - ) - - git_repo: Annotated[GitRepoConfig | None, Field(description="GitRepo storage backend configuration")] = None - gitlab_api: Annotated[GitlabApiConfig | None, Field(description="GitLab API storage backend configuration")] = None - - celery: Annotated[CeleryConfig, Field(description="Celery configuration")] - - require_client_cert: Annotated[ - bool, Field(description="Require client certificate for API access (set to false for development)") - ] = True - - model_config: ClassVar[ConfigDict] = ConfigDict(arbitrary_types_allowed=True) - - @field_validator("known_rdis") - @classmethod - def validate_known_rdis(cls, rdis: list[str]) -> list[str]: - """Validate that RDI identifiers contain only allowed characters.""" - # This regex allows alphanumeric characters, underscore, hyphen, and dot. - allowed_chars_pattern = re.compile(r"^[a-zA-Z0-9_.-]+$") - for rdi in rdis: - if not allowed_chars_pattern.match(rdi): - msg = ( - f"Invalid RDI identifier '{rdi}'. Only alphanumeric characters, hyphens, " - "underscores, and dots are allowed." - ) - logging.error(msg) - raise ValueError(msg) - return rdis - - @field_validator("client_auth_oid", mode="before") - @classmethod - def parse_client_auth_oid(cls, oid: str | x509.ObjectIdentifier) -> x509.ObjectIdentifier: - """Validate that client_auth_oid is a valid OID (e.g. 1.2.3.4.55516).""" - if isinstance(oid, str): - return x509.ObjectIdentifier(oid) - if isinstance(oid, x509.ObjectIdentifier): - return oid - raise TypeError("client_auth_oid must be a string or x509.ObjectIdentifier") - - @model_validator(mode="after") - def validate_mutual_exclusivity(self) -> Self: - """Validate that exactly one backend is configured.""" - if self.git_repo is None and self.gitlab_api is None: - raise ValueError("Either git_repo or gitlab_api must be configured") - if self.git_repo is not None and self.gitlab_api is not None: - raise ValueError("Only one of git_repo or gitlab_api can be configured") - return self diff --git a/middleware/api/src/middleware/api/main.py b/middleware/api/src/middleware/api/main.py deleted file mode 100644 index 6f00bcf..0000000 --- a/middleware/api/src/middleware/api/main.py +++ /dev/null @@ -1,39 +0,0 @@ -"""Entry point for running the Middleware API with Uvicorn or Celery.""" - -import sys - -import uvicorn -from celery.__main__ import main as celery_main - -from middleware.api.api import middleware_api -from middleware.api.worker_health import check_worker_health - - -def main() -> None: - """Call uvicorn.main() or celery.main() to pass control. - - If the first argument is 'celery', we pass control to celery. - Otherwise we default to uvicorn with the hardcoded app path. - """ - if len(sys.argv) > 1 and sys.argv[1] == "worker-health": - sys.exit(0 if check_worker_health() else 1) - - if len(sys.argv) > 1 and sys.argv[1] == "celery": - # Remove the executable name and the 'celery' command, so sys.argv[0] becomes 'celery' - # effectively mimicking 'python -m celery ...' - - sys.argv = sys.argv[1:] # ['celery', '-A', ...] - celery_main() - sys.exit(0) - - # Construct the app path string - app_path = f"{middleware_api.__module__}:middleware_api.app" - - # Rebuild sys.argv for uvicorn.main() - sys.argv = ["uvicorn", app_path] + sys.argv[1:] - - uvicorn.main() # pylint: disable=no-value-for-parameter - - -if __name__ == "__main__": - main() diff --git a/middleware/api/src/middleware/api/tracing.py b/middleware/api/src/middleware/api/tracing.py deleted file mode 100644 index 4e85bed..0000000 --- a/middleware/api/src/middleware/api/tracing.py +++ /dev/null @@ -1,46 +0,0 @@ -"""OpenTelemetry tracing instrumentation.""" - -import logging -from typing import TYPE_CHECKING, Any - -from opentelemetry.instrumentation.celery import CeleryInstrumentor -from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor -from opentelemetry.instrumentation.redis import RedisInstrumentor -from opentelemetry.instrumentation.requests import RequestsInstrumentor - -if TYPE_CHECKING: - from celery import Celery - from fastapi import FastAPI - - -logger = logging.getLogger(__name__) - - -def instrument_app(app: "FastAPI") -> None: - """Instrument a FastAPI application with OpenTelemetry. - - This includes: - - FastAPI instrumentation (which builds on Starlette) - - Starlette instrumentation directly (for extra coverage) - - Redis instrumentation - - Requests instrumentation - """ - # Instrument FastAPI (handles HTTP requests) - FastAPIInstrumentor.instrument_app(app) - - # Instrument external dependencies - RedisInstrumentor().instrument() - RequestsInstrumentor().instrument() - - logger.info("FastAPI app instrumented for OpenTelemetry (with Redis, Requests)") - - -def instrument_celery(_app: "Celery | None" = None, **kwargs: Any) -> None: - """Instrument a Celery application with OpenTelemetry.""" - CeleryInstrumentor().instrument(**kwargs) - - # Also instrument dependencies that might be used within tasks - RedisInstrumentor().instrument() - RequestsInstrumentor().instrument() - - logger.info("Celery app instrumented for OpenTelemetry") diff --git a/middleware/api/src/middleware/api/worker.py b/middleware/api/src/middleware/api/worker.py deleted file mode 100644 index 20cf158..0000000 --- a/middleware/api/src/middleware/api/worker.py +++ /dev/null @@ -1,56 +0,0 @@ -"""Celery worker module for asynchronous ARC processing tasks. - -This module provides: -- process_arc: Celery task for processing individual ARCs asynchronously -""" - -import asyncio -import logging -from typing import Any - -from middleware.api.celery_app import business_logic, celery_app - -# Initialize logger -logger = logging.getLogger(__name__) - - -@celery_app.task(name="process_arc") -def process_arc(rdi: str, arc_data: dict[str, Any], client_id: str | None) -> dict[str, Any]: - """Process a single ARC asynchronously. - - Args: - rdi: Research Data Infrastructure identifier - arc_data: ARC data dictionary - client_id: Optional client identifier - - Returns: - Task result as dictionary - - Raises: - RuntimeError: If business logic is not initialized - """ - if business_logic is None: - logger.error("BusinessLogic not initialized") - raise RuntimeError("BusinessLogic not initialized") - - logger.info("Starting processing task for RDI %s", rdi) - - # Run the async business logic in a sync wrapper - try: - # We process a list of 1 ARC to reuse the existing batch processing logic - # wrapping it in a coroutine call - loop = asyncio.new_event_loop() - asyncio.set_event_loop(loop) - - # We need to construct a list of one ARC - result = loop.run_until_complete(business_logic.create_or_update_arcs(rdi, [arc_data], client_id)) - loop.close() - - # The result is a CreateOrUpdateArcsResponse object (Pydantic model) - # We return the dict representation - return result.model_dump() - - except Exception as e: - logger.error("Task failed: %s", e, exc_info=True) - # Re-raise to mark task as failed in Celery - raise e diff --git a/middleware/api/src/middleware/api/worker_health.py b/middleware/api/src/middleware/api/worker_health.py deleted file mode 100644 index 025cd40..0000000 --- a/middleware/api/src/middleware/api/worker_health.py +++ /dev/null @@ -1,99 +0,0 @@ -#!/usr/bin/env python3 -"""Health check script for Celery worker and its dependencies. - -This module provides functionality to verify the health of: -- ArcStore backend (Git repository or GitLab API) -- Redis (Celery result backend) -- RabbitMQ (Celery message broker) -""" - -import logging -import os -import sys -from pathlib import Path - -import redis - -from middleware.api.arc_store import ArcStore -from middleware.api.arc_store.git_repo import GitRepo -from middleware.api.arc_store.gitlab_api import GitlabApi -from middleware.api.celery_app import celery_app -from middleware.api.config import Config - -# Configure logging -logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(name)s: %(message)s") -logger = logging.getLogger("worker_health") - - -def check_worker_health() -> bool: - """Check health of the worker and its dependencies.""" - try: - # Load config - config_file = Path(os.environ.get("MIDDLEWARE_API_CONFIG", "/run/secrets/middleware-api-config")) - if not config_file.is_file(): - logger.error("Config file not found: %s", config_file) - return False - - config = Config.from_yaml_file(config_file) - - # Initialize ArcStore - store: ArcStore - if config.gitlab_api: - store = GitlabApi(config.gitlab_api) - elif config.git_repo: - store = GitRepo(config.git_repo) - else: - logger.error("Invalid ArcStore configuration") - return False - - # Check backend (Git/GitLab) - backend_reachable = False - try: - backend_reachable = store.check_health() - except Exception as e: # pylint: disable=broad-exception-caught - logger.error("Backend health check failed: %s", e) - - # Check Redis (result backend) - redis_reachable = False - try: - redis_url = config.celery.result_backend.get_secret_value() if config.celery else "redis://localhost:6379/0" - r = redis.from_url(redis_url) - r.ping() - redis_reachable = True - except redis.RedisError as e: - logger.error("Redis health check failed: %s", e) - - # Check RabbitMQ (broker) - rabbitmq_reachable = False - try: - with celery_app.connection_or_acquire() as conn: - conn.ensure_connection(max_retries=1) - rabbitmq_reachable = True - except Exception as e: # pylint: disable=broad-exception-caught - logger.error("RabbitMQ health check failed: %s", e) - - health_status = { - "backend_reachable": backend_reachable, - "redis_reachable": redis_reachable, - "rabbitmq_reachable": rabbitmq_reachable, - } - - logger.info("Health status: %s", health_status) - - # Return True only if ALL checks pass - if not all(health_status.values()): - logger.error("Some health checks failed") - return False - - return True - - except Exception as e: # pylint: disable=broad-exception-caught - logger.error("Health check exception: %s", e) - return False - - -if __name__ == "__main__": - if check_worker_health(): - sys.exit(0) - else: - sys.exit(1) diff --git a/middleware/api/tests/conftest.py b/middleware/api/tests/conftest.py deleted file mode 100644 index 1ecb862..0000000 --- a/middleware/api/tests/conftest.py +++ /dev/null @@ -1,102 +0,0 @@ -"""Shared fixtures for tests.""" - -import datetime -from collections.abc import Callable - -import pytest -from asn1crypto.core import UTF8String # type: ignore -from cryptography import x509 -from cryptography.hazmat.primitives import hashes, serialization -from cryptography.hazmat.primitives.asymmetric import rsa -from cryptography.x509.oid import NameOID - - -def _create_test_cert(oid: x509.ObjectIdentifier, rdis: list[str]) -> str: - """Create a test certificate with specified RDIs in custom extension. - - Args: - oid: The OID for the custom extension. - rdis: List of RDI identifiers to include in the certificate. - - Returns: - PEM-encoded certificate as string. - - """ - # DER encoding constants - der_sequence_tag = 0x30 - der_short_form_max = 128 - der_long_form_flag = 0x80 - - # Generate private key - private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048) - - # Generate certificate - subject = issuer = x509.Name( - [ - x509.NameAttribute(NameOID.COUNTRY_NAME, "DE"), - x509.NameAttribute(NameOID.STATE_OR_PROVINCE_NAME, "Some-State"), - x509.NameAttribute(NameOID.ORGANIZATION_NAME, "Internet Widgits Pty Ltd"), - x509.NameAttribute(NameOID.COMMON_NAME, "TestClient"), - ] - ) - - # Create custom extension with RDIs as SEQUENCE of UTF8Strings - utf8_bytes = b"".join(UTF8String(rdi).dump() for rdi in rdis) - - # SEQUENCE tag (0x30) + length + content - seq_length = len(utf8_bytes) - if seq_length < der_short_form_max: - extension_value = bytes([der_sequence_tag, seq_length]) + utf8_bytes - else: - # Long form length encoding - length_bytes = seq_length.to_bytes((seq_length.bit_length() + 7) // 8, "big") - extension_value = bytes([der_sequence_tag, der_long_form_flag | len(length_bytes)]) + length_bytes + utf8_bytes - - # Create UnrecognizedExtension with the custom OID - custom_extension = x509.UnrecognizedExtension(oid, extension_value) - - the_cert = ( - x509.CertificateBuilder() - .subject_name(subject) - .issuer_name(issuer) - .public_key(private_key.public_key()) - .serial_number(x509.random_serial_number()) - .not_valid_before(datetime.datetime.now(datetime.UTC)) - .not_valid_after(datetime.datetime.now(datetime.UTC) + datetime.timedelta(days=365)) - .add_extension( - custom_extension, - critical=False, - ) - .sign(private_key, hashes.SHA256()) - ) - - # Convert to PEM format - return the_cert.public_bytes(serialization.Encoding.PEM).decode("utf-8") - - -@pytest.fixture(scope="session") -def create_test_cert() -> Callable[[x509.ObjectIdentifier, list[str]], str]: - """Return the `_create_test_cert` function as a fixture.""" - return _create_test_cert - - -@pytest.fixture(scope="session") -def known_rdis() -> list[str]: - """Return a list of known RDIs for testing.""" - return ["rdi-1", "rdi-2"] - - -@pytest.fixture(scope="session") -def oid() -> x509.ObjectIdentifier: - """Return a test OID.""" - return x509.ObjectIdentifier("1.3.6.1.4.1.64609.1.1") - - -@pytest.fixture(scope="session") -def cert( - create_test_cert: Callable[[x509.ObjectIdentifier, list[str]], str], - oid: x509.ObjectIdentifier, - known_rdis: list[str], -) -> str: - """Create a self-signed client certificate with custom extension for RDIs.""" - return create_test_cert(oid, known_rdis) diff --git a/middleware/api/tests/integration/conftest.py b/middleware/api/tests/integration/conftest.py deleted file mode 100644 index 0310b18..0000000 --- a/middleware/api/tests/integration/conftest.py +++ /dev/null @@ -1,87 +0,0 @@ -"""Integration tests configuration and fixtures.""" - -import os -from collections.abc import Generator -from typing import Any - -import pytest -from cryptography import x509 -from dotenv import load_dotenv -from fastapi.testclient import TestClient -from gitlab import Gitlab, GitlabError - -from middleware.api.api import Api -from middleware.api.config import Config -from middleware.shared.config.config_wrapper import ConfigWrapper, DictType, ListType - -# Load environment variables from .env file (generated by load-env.sh) -load_dotenv() - - -@pytest.fixture(scope="session") -def config(oid: x509.ObjectIdentifier, known_rdis: list[str]) -> "DictType | ListType": - """Provide configuration for tests.""" - config_wrapper = ConfigWrapper.from_data( - { - "log_level": "DEBUG", - "known_rdis": list(known_rdis), - "client_auth_oid": oid.dotted_string, - "gitlab_api": { - "url": "https://datahub-dev.ipk-gatersleben.de", - "group": "FAIRagro-advanced-middleware-integration-tests", - "token": os.getenv("GITLAB_API_TOKEN", ""), - }, - "celery": { - "broker_url": "memory://", - "result_backend": "cache+memory://", - }, - } - ) - return config_wrapper.unwrap() - - -@pytest.fixture(scope="session") -def gitlab_api( - config: dict[str, Any], -) -> Gitlab: # pylint: disable=redefined-outer-name - """Provide a Gitlab API client for tests.""" - token = os.getenv("GITLAB_API_TOKEN") - return Gitlab(config["gitlab_api"]["url"], private_token=token) - - -@pytest.fixture(scope="session") -def gitlab_group(config: dict[str, Any], gitlab_api: Gitlab) -> Any: # pylint: disable=redefined-outer-name - """Provide the Gitlab group for tests.""" - group = gitlab_api.groups.get(config["gitlab_api"]["group"]) - return group - - -@pytest.fixture -def middleware_api( - config: dict[str, Any], -) -> Api: # pylint: disable=redefined-outer-name - """Provide the Middleware API instance for tests.""" - config_validated = Config.from_data(config) - return Api(config_validated) - - -@pytest.fixture -def client( - middleware_api: Api, -) -> Generator[TestClient, None, None]: # pylint: disable=redefined-outer-name - """Provide a TestClient for the Middleware API.""" - with TestClient(middleware_api.app) as c: - yield c - - -@pytest.fixture(scope="session", autouse=True) -def cleanup_gitlab_group(gitlab_group: Any, gitlab_api: Gitlab) -> None: # pylint: disable=redefined-outer-name - """Cleanup the Gitlab group before tests.""" - # delete all projects in the group - for project in gitlab_group.projects.list(all=True): - try: - full_project = gitlab_api.projects.get(project.id) - full_project.delete() - print(f"Deleted test project: {project.name}") - except GitlabError as e: - print(f"Failed to delete project {project.name}: {e}") diff --git a/middleware/api/tests/integration/test_integration_create_or_update_arcs.py b/middleware/api/tests/integration/test_integration_create_or_update_arcs.py deleted file mode 100644 index e8c02de..0000000 --- a/middleware/api/tests/integration/test_integration_create_or_update_arcs.py +++ /dev/null @@ -1,59 +0,0 @@ -"""Integration tests for creating or updating ARCs.""" - -import hashlib -import http -import json -from pathlib import Path -from typing import Any - -import pytest -from fastapi.testclient import TestClient -from gitlab import Gitlab - - -@pytest.mark.asyncio -@pytest.mark.parametrize( - "json_info", - [ - {"file_name": "minimal.json", "identifier": "Test"}, - {"file_name": "sample.json", "identifier": "AthalianaColdStressSugar"}, - ], -) -async def test_create_arcs( - client: TestClient, - cert: str, - json_info: dict[str, Any], -) -> None: - """Test creating ARCs via the /v1/arcs endpoint.""" - cert_with_linebreaks = cert.replace("\\n", "\n") - - headers = { - "ssl-client-cert": cert_with_linebreaks, - "ssl-client-verify": "SUCCESS", - "content-type": "application/json", - } - arc_json_path = Path(__file__).parent.parent.parent.parent.parent / "ro_crates" / json_info["file_name"] - with arc_json_path.open("r", encoding="utf-8") as f: - body = {"rdi": "rdi-1", "arcs": [json.load(f)]} - - response = client.post("/v1/arcs", headers=headers, json=body) - - assert response.status_code == http.HTTPStatus.ACCEPTED # nosec (202 for async processing) - body = response.json() - assert "task_id" in body # nosec - assert body["status"] == "processing" # nosec - - # Note: In integration tests, we would need to poll /v1/tasks/{task_id} to verify completion - # For now, we skip verification as it requires Celery worker to be running - # _verify_gitlab_project(gitlab_api, config, json_info) - - -def _verify_gitlab_project(gitlab_api: Gitlab, config: dict[str, Any], json_info: dict[str, Any]) -> None: - """Verify that the project was created in GitLab and contains the expected file.""" - # check that we have a project/repo that contains the isa.investigation.xlsx file - group_name = config["gitlab_api"]["group"].lower() - group = gitlab_api.groups.get(group_name) - arc_id = hashlib.sha256(f"{json_info['identifier']}:rdi-1".encode()).hexdigest() - project_light = group.projects.list(search=arc_id)[0] - project = gitlab_api.projects.get(project_light.id) - project.files.get(file_path="isa.investigation.xlsx", ref="main") diff --git a/middleware/api/tests/integration/test_integration_git_repo.py b/middleware/api/tests/integration/test_integration_git_repo.py deleted file mode 100644 index 3626876..0000000 --- a/middleware/api/tests/integration/test_integration_git_repo.py +++ /dev/null @@ -1,135 +0,0 @@ -"""Integration tests for GitRepo backend.""" - -import hashlib -import http -import json -import shutil -import tempfile -from collections.abc import Generator -from pathlib import Path -from typing import Any - -import pytest -from cryptography import x509 -from fastapi.testclient import TestClient -from git import Repo - -from middleware.api.api import Api -from middleware.api.config import Config -from middleware.shared.config.config_wrapper import ConfigWrapper - - -@pytest.fixture -def git_server_root() -> Generator[Path, None, None]: - """Create a temporary directory simulating a git server root.""" - temp_dir = tempfile.mkdtemp() - yield Path(temp_dir) - shutil.rmtree(temp_dir) - - -@pytest.fixture -def git_repo_cache_dir() -> Generator[Path, None, None]: - """Create a temporary directory for GitRepo cache.""" - temp_dir = tempfile.mkdtemp() - yield Path(temp_dir) - shutil.rmtree(temp_dir) - - -@pytest.fixture -def git_repo_config(git_server_root: Path, git_repo_cache_dir: Path, oid: x509.ObjectIdentifier) -> dict[str, Any]: - """Provide configuration for GitRepo backend.""" - return { - "log_level": "DEBUG", - "known_rdis": ["rdi-1"], - "client_auth_oid": oid.dotted_string, - "require_client_cert": True, - "git_repo": { - "url": f"file://{git_server_root}", - "group": "test-group", - "branch": "main", - "cache_dir": str(git_repo_cache_dir), - }, - "celery": { - "broker_url": "memory://", - "result_backend": "cache+memory://", - }, - } - - -@pytest.fixture -def api_client(git_repo_config: dict[str, Any]) -> Generator[TestClient, None, None]: - """Provide a TestClient with GitRepo backend.""" - config_wrapper = ConfigWrapper.from_data(git_repo_config) - unwrapped_config = config_wrapper.unwrap() - assert isinstance(unwrapped_config, dict), "Config must be a dictionary" - config = Config.from_data(unwrapped_config) - api = Api(config) - with TestClient(api.app) as c: - yield c - - -@pytest.mark.asyncio -async def test_create_arc_via_git_repo( - api_client: TestClient, - git_server_root: Path, - cert: str, -) -> None: - """Test creating an ARC using the GitRepo backend.""" - # 1. Prepare Request - cert_with_linebreaks = cert.replace("\\n", "\n") - headers = { - "ssl-client-cert": cert_with_linebreaks, - "ssl-client-verify": "SUCCESS", - "content-type": "application/json", - } - - # Load minimal.json - arc_json_path = Path("/workspaces/m4.2_advanced_middleware_api/ro_crates/minimal.json") - with arc_json_path.open("r", encoding="utf-8") as f: - json_content = json.load(f) - - # Wrap in API expected format - body = {"rdi": "rdi-1", "arcs": [json_content]} - - # Calculate expected ARC ID for identifier "Test" - arc_id = hashlib.sha256(b"Test:rdi-1").hexdigest() - - # 2. Pre-create the bare repo - # Expected remote path: base / group / arc_id.git - group_dir = git_server_root / "test-group" - group_dir.mkdir(parents=True, exist_ok=True) - repo_path = group_dir / f"{arc_id}.git" - - # Initialize bare repo - Repo.init(repo_path, bare=True) - - # 3. Call API - response = api_client.post("/v1/arcs", headers=headers, json=body) - - # Assert - # API now returns 202 (Accepted) for async processing - assert response.status_code == http.HTTPStatus.ACCEPTED, f"Response: {response.text}" - response_data = response.json() - assert "task_id" in response_data - assert response_data["status"] == "processing" - - # Note: In integration tests, we would need to poll /v1/tasks/{task_id} and wait for completion - # For now, we skip verification as it requires Celery worker to be running - # _verify_repo_content(repo_path) - - -def _verify_repo_content(repo_path: Path) -> None: - """Verify the content of the pushed repository.""" - with tempfile.TemporaryDirectory() as tmp_clone: - Repo.clone_from(str(repo_path), tmp_clone, branch="main") - - # Check files exist - cloned_path = Path(tmp_clone) - - # The ARC.Write() operation writes the ARC structure (isa.investigation.xlsx, etc.) - # It does not necessarily preserve ro-crate-metadata.json at the root - assert (cloned_path / "isa.investigation.xlsx").exists() - assert (cloned_path / "studies").exists() - assert (cloned_path / "assays").exists() - assert (cloned_path / "workflows").exists() - assert (cloned_path / "runs").exists() diff --git a/middleware/api/tests/integration/test_whoami.py b/middleware/api/tests/integration/test_whoami.py deleted file mode 100644 index d76a10a..0000000 --- a/middleware/api/tests/integration/test_whoami.py +++ /dev/null @@ -1,20 +0,0 @@ -"""Integration tests for the /v1/whoami endpoint.""" - -import http - -import pytest -from fastapi.testclient import TestClient - - -@pytest.mark.asyncio -async def test_whoami_with_client_cert(client: TestClient, cert: str) -> None: - """Test the /v1/whoami endpoint with a client certificate.""" - cert_with_linebreaks = cert.replace("\\n", "\n") - - headers = {"ssl-client-cert": cert_with_linebreaks, "ssl-client-verify": "SUCCESS", "accept": "application/json"} - - response = client.get("/v1/whoami", headers=headers) - - assert response.status_code == http.HTTPStatus.OK # nosec - body = response.json() - assert body["client_id"] == "TestClient" # nosec diff --git a/middleware/api/tests/unit/conftest.py b/middleware/api/tests/unit/conftest.py deleted file mode 100644 index e8e525d..0000000 --- a/middleware/api/tests/unit/conftest.py +++ /dev/null @@ -1,113 +0,0 @@ -"""Unit tests for the FAIRagro middleware API.""" - -import hashlib -import os -import tempfile -from collections.abc import Generator -from pathlib import Path -from unittest.mock import AsyncMock, MagicMock - -import pytest -from cryptography import x509 -from fastapi.testclient import TestClient -from pydantic import HttpUrl, SecretStr - -from middleware.api.api import Api -from middleware.api.arc_store.gitlab_api import GitlabApi, GitlabApiConfig -from middleware.api.business_logic import ( - BusinessLogic, -) -from middleware.api.config import CeleryConfig, Config -from middleware.shared.config.config_base import OtelConfig - - -@pytest.fixture(scope="session", autouse=True) -def setup_test_config() -> Generator[None, None, None]: - """Create a temporary config file for tests and set MIDDLEWARE_API_CONFIG env var.""" - # Create a minimal config file for celery_app to load - config_content = """ -log_level: DEBUG -known_rdis: [] -gitlab_api: - url: http://localhost - token: test-token - group: test-group - branch: main -celery: - broker_url: amqp://guest:guest@localhost:5672// - result_backend: redis://localhost:6379/0 -""" - with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f: - f.write(config_content) - temp_config_path = f.name - - # Set environment variable before any imports happen - os.environ["MIDDLEWARE_API_CONFIG"] = temp_config_path - - yield - - # Cleanup - Path(temp_config_path).unlink(missing_ok=True) - - -@pytest.fixture -def config(oid: x509.ObjectIdentifier, known_rdis: list[str]) -> Config: - """Provide a test Config instance with dummy values.""" - return Config( - log_level="DEBUG", - client_auth_oid=oid, - known_rdis=known_rdis, - gitlab_api=GitlabApiConfig( - url=HttpUrl("http://localhost:8080"), - token=SecretStr("test-token"), - group="test-group", - branch="main", - ), - celery=CeleryConfig( - broker_url=SecretStr("amqp://guest:guest@localhost:5672//"), - result_backend=SecretStr("redis://localhost:6379/0"), - ), - otel=OtelConfig(), - ) - - -@pytest.fixture -def middleware_api(config: Config) -> Api: - """Provide the Middleware API instance for tests.""" - return Api(config) - - -@pytest.fixture -def client( - middleware_api: Api, -) -> Generator[TestClient, None, None]: # pylint: disable=redefined-outer-name - """Provide a TestClient for the Middleware API. - - Also ensure cleanup of dependency overrides. - """ - with TestClient(middleware_api.app) as c: - yield c - middleware_api.app.dependency_overrides.clear() - - -@pytest.fixture -def service() -> BusinessLogic: - """Provide a BusinessLogic instance with a mocked ArcStore.""" - store = MagicMock() - store.arc_id = MagicMock( - side_effect=lambda identifier, rdi: hashlib.sha256(f"{identifier}:{rdi}".encode()).hexdigest() - ) - store.exists = AsyncMock(return_value=False) - store.get = AsyncMock(return_value=None) - store.delete = AsyncMock() - store.create_or_update = AsyncMock() - return BusinessLogic(store) - - -@pytest.fixture -def gitlab_api() -> GitlabApi: - """Provide a GitlabApi instance with a mocked Gitlab client.""" - api_config = GitlabApiConfig(url=HttpUrl("http://gitlab"), token=SecretStr("token"), group="1", branch="main") # nosec - api = GitlabApi(api_config) - api._gitlab = MagicMock() # pylint: disable=protected-access - return api diff --git a/middleware/api/tests/unit/test_arc_store.py b/middleware/api/tests/unit/test_arc_store.py deleted file mode 100644 index c1c68a5..0000000 --- a/middleware/api/tests/unit/test_arc_store.py +++ /dev/null @@ -1,123 +0,0 @@ -"""Tests for ArcStore interface and error handling.""" - -from unittest.mock import MagicMock, patch - -import pytest - -from middleware.api.arc_store import ArcStore, ArcStoreError - - -def create_mock_arc_store() -> ArcStore: - """Create a mock ArcStore instance by patching abstract methods.""" - - class ConcreteArcStore(ArcStore): - arc_id = MagicMock() - - async def _create_or_update(self, *_args: object, **_kwargs: object) -> None: - pass - - async def _delete(self, *_args: object, **_kwargs: object) -> None: - pass - - async def _exists(self, *_args: object, **_kwargs: object) -> bool: - return False - - async def _get(self, *_args: object, **_kwargs: object) -> object: - pass - - def _check_health(self) -> bool: - return True - - return ConcreteArcStore() - - -class TestArcStoreError: - """Test suite for ArcStoreError exception.""" - - def test_arc_store_error_is_exception(self) -> None: - """Test that ArcStoreError is an Exception.""" - error = ArcStoreError("Test error") - assert isinstance(error, Exception) - - def test_arc_store_error_message(self) -> None: - """Test ArcStoreError message.""" - message = "Test error message" - error = ArcStoreError(message) - assert str(error) == message - - -class TestArcStoreWrapperMethods: - """Test suite for ArcStore wrapper methods that handle errors.""" - - @pytest.mark.asyncio - async def test_create_or_update_arc_store_error_passthrough(self) -> None: - """Test create_or_update passes through ArcStoreError.""" - store = create_mock_arc_store() - with patch.object(store, "_create_or_update") as mock_impl: - mock_impl.side_effect = ArcStoreError("Test error") - with pytest.raises(ArcStoreError): - await store.create_or_update("test_id", MagicMock()) - - @pytest.mark.asyncio - async def test_get_arc_store_error_passthrough(self) -> None: - """Test get passes through ArcStoreError.""" - store = create_mock_arc_store() - with patch.object(store, "_get", side_effect=ArcStoreError("Test error")), pytest.raises(ArcStoreError): - await store.get("test_id") - - @pytest.mark.asyncio - async def test_get_generic_exception_logged_and_wrapped(self) -> None: - """Test get logs and wraps generic exceptions.""" - store = create_mock_arc_store() - with ( - patch.object(store, "_get", side_effect=ValueError("Generic error")), - patch("middleware.api.arc_store.logger") as mock_logger, - ): - with pytest.raises(ArcStoreError) as exc_info: - await store.get("test_id") - assert "general exception caught" in str(exc_info.value).lower() - mock_logger.exception.assert_called_once() - - @pytest.mark.asyncio - async def test_delete_arc_store_error_passthrough(self) -> None: - """Test delete passes through ArcStoreError.""" - store = create_mock_arc_store() - with patch.object(store, "_delete", side_effect=ArcStoreError("Test error")), pytest.raises(ArcStoreError): - await store.delete("test_id") - - @pytest.mark.asyncio - async def test_delete_generic_exception_wrapped(self) -> None: - """Test delete wraps generic exceptions in ArcStoreError.""" - store = create_mock_arc_store() - with patch.object(store, "_delete", side_effect=RuntimeError("Generic error")): - with pytest.raises(ArcStoreError) as exc_info: - await store.delete("test_id") - assert "general exception caught" in str(exc_info.value).lower() - - @pytest.mark.asyncio - async def test_exists_arc_store_error_passthrough(self) -> None: - """Test exists passes through ArcStoreError.""" - store = create_mock_arc_store() - with patch.object(store, "_exists", side_effect=ArcStoreError("Test error")), pytest.raises(ArcStoreError): - await store.exists("test_id") - - @pytest.mark.asyncio - async def test_exists_generic_exception_wrapped(self) -> None: - """Test exists wraps generic exceptions in ArcStoreError.""" - store = create_mock_arc_store() - with patch.object(store, "_exists", side_effect=OSError("Generic error")): - with pytest.raises(ArcStoreError) as exc_info: - await store.exists("test_id") - assert "exception" in str(exc_info.value).lower() - - @pytest.mark.asyncio - async def test_exists_generic_exception_logged(self) -> None: - """Test exists logs exceptions.""" - store = create_mock_arc_store() - with ( - patch.object(store, "_exists", side_effect=OSError("Generic error")), - patch("middleware.api.arc_store.logger") as mock_logger, - ): - with pytest.raises(ArcStoreError): - await store.exists("test_id") - mock_logger.exception.assert_called_once() diff --git a/middleware/api/tests/unit/test_config.py b/middleware/api/tests/unit/test_config.py deleted file mode 100644 index 42881b4..0000000 --- a/middleware/api/tests/unit/test_config.py +++ /dev/null @@ -1,136 +0,0 @@ -"""Unit tests for the API configuration module. - -Tests cover: -- RDI identifier validation -- Client authentication OID parsing -- Backend mutual exclusivity (git_repo vs gitlab_api) -- YAML configuration file loading -""" - -import textwrap -from pathlib import Path -from typing import Any - -import pytest -from cryptography import x509 -from pydantic import ValidationError - -from middleware.api.config import Config - - -def _git_repo(tmp_path: Path, group: str = "g") -> dict[str, str]: - repo_dir = tmp_path / "repo" - repo_dir.mkdir(exist_ok=True) - return {"url": repo_dir.as_uri(), "group": group, "path": str(repo_dir)} - - -def test_config_validate_known_rdis_valid(tmp_path: Path) -> None: - """Test valid known RDIs.""" - # We need a minimal valid config - config_data = { - "known_rdis": ["valid-rdi", "rdi.123", "under_score"], - "git_repo": _git_repo(tmp_path), - "celery": {"broker_url": "memory://", "result_backend": "cache+memory://"}, - } - config = Config.model_validate(config_data) - assert len(config.known_rdis) == 3 # noqa: PLR2004 - - -def test_config_validate_known_rdis_invalid(tmp_path: Path) -> None: - """Test invalid known RDIs.""" - config_data = { - "known_rdis": ["invalid rdi"], # space not allowed - "git_repo": _git_repo(tmp_path), - "celery": {"broker_url": "memory://", "result_backend": "cache+memory://"}, - } - with pytest.raises(ValidationError) as exc: - Config.model_validate(config_data) - assert "Invalid RDI identifier" in str(exc.value) - - -def test_config_parse_client_auth_oid_str(tmp_path: Path) -> None: - """Test parsing OID from string.""" - oid_str = "1.2.3.4" - config_data = { - "client_auth_oid": oid_str, - "git_repo": _git_repo(tmp_path), - "celery": {"broker_url": "memory://", "result_backend": "cache+memory://"}, - } - config = Config.model_validate(config_data) - assert isinstance(config.client_auth_oid, x509.ObjectIdentifier) - assert config.client_auth_oid.dotted_string == oid_str - - -def test_config_parse_client_auth_oid_obj(tmp_path: Path) -> None: - """Test parsing OID from ObjectIdentifier.""" - oid = x509.ObjectIdentifier("1.2.3.4") - config_data = { - "client_auth_oid": oid, - "git_repo": _git_repo(tmp_path), - "celery": {"broker_url": "memory://", "result_backend": "cache+memory://"}, - } - config = Config.model_validate(config_data) - assert config.client_auth_oid == oid - - -def test_config_parse_client_auth_oid_invalid_type(tmp_path: Path) -> None: - """Test invalid OID type.""" - config_data = { - "client_auth_oid": 1234, - "git_repo": _git_repo(tmp_path), - "celery": {"broker_url": "memory://", "result_backend": "cache+memory://"}, - } - with pytest.raises(TypeError) as exc: - Config.model_validate(config_data) - assert "client_auth_oid must be a string or x509.ObjectIdentifier" in str(exc.value) - - -def test_config_mutual_exclusivity_none() -> None: - """Test failure when neither backend is configured.""" - config_data: dict[str, Any] = { - "celery": {"broker_url": "memory://", "result_backend": "cache+memory://"}, - } - with pytest.raises(ValidationError) as exc: - Config.model_validate(config_data) - assert "Either git_repo or gitlab_api must be configured" in str(exc.value) - - -def test_config_mutual_exclusivity_both(tmp_path: Path) -> None: - """Test failure when both backends are configured.""" - config_data = { - "git_repo": _git_repo(tmp_path), - "gitlab_api": {"url": "https://gitlab.com", "token": "t", "group": "g", "branch": "b"}, - "celery": {"broker_url": "memory://", "result_backend": "cache+memory://"}, - } - with pytest.raises(ValidationError) as exc: - Config.model_validate(config_data) - assert "Only one of git_repo or gitlab_api can be configured" in str(exc.value) - - -def test_config_from_yaml_file_not_found() -> None: - """Test loading config from non-existent file.""" - with pytest.raises(RuntimeError, match="Config file .* not found"): - Config.from_yaml_file(Path("/non/existent/path.yaml")) - - -def test_config_from_yaml_file_success(tmp_path: Path) -> None: - """Test loading config from a valid file.""" - config_file = tmp_path / "config.yaml" - config_yaml = textwrap.dedent( - f""" - log_level: DEBUG - git_repo: - url: {tmp_path.as_uri()} - group: my-group - path: {tmp_path} - celery: - broker_url: memory:// - result_backend: cache+memory:// - """ - ) - config_file.write_text(config_yaml) - - config = Config.from_yaml_file(config_file) - assert config.log_level == "DEBUG" - assert config.git_repo is not None - assert config.git_repo.url == tmp_path.as_uri() diff --git a/middleware/api/tests/unit/test_git_repo.py b/middleware/api/tests/unit/test_git_repo.py deleted file mode 100644 index efc2aa6..0000000 --- a/middleware/api/tests/unit/test_git_repo.py +++ /dev/null @@ -1,479 +0,0 @@ -"""Unit tests for the GitRepo persistence layer (git_repo.py).""" -# pylint: disable=protected-access - -import asyncio -import tempfile -from collections.abc import Callable -from pathlib import Path -from typing import Any -from unittest.mock import MagicMock, PropertyMock, patch - -import pytest -from git.exc import GitCommandError -from pydantic import SecretStr - -from middleware.api.arc_store.git_repo import GitContext, GitContextConfig, GitRepo, GitRepoConfig, is_soft_git_error - - -@pytest.fixture -def repo_config() -> GitRepoConfig: - """Fixture for GitRepoConfig.""" - return GitRepoConfig( - url="https://gitlab.example.com", - group="mygroup", - token=None, # Updated to avoid SecretStr validation issue in test - cache_dir=Path(tempfile.gettempdir()), - ) - - -@pytest.fixture -def git_repo(repo_config: GitRepoConfig) -> GitRepo: - """Fixture for GitRepo.""" - repo = GitRepo(repo_config) - # Mock executor to avoid threading issues in tests - repo._executor = MagicMock() - # Mock run_in_executor to execute immediately - repo._executor.submit = lambda fn, *args, **kwargs: fn(*args, **kwargs) - return repo - - -def test_git_repo_url_generation(git_repo: GitRepo) -> None: - """Test standard repo URL generation.""" - url = git_repo._remote_provider.get_repo_url("arc123", authenticated=False) - assert url == "https://gitlab.example.com/mygroup/arc123.git" - - -def test_git_repo_context_config_generation(git_repo: GitRepo) -> None: - """Test context config generation with cache dir.""" - config = git_repo._get_context_config("arc123") - assert config.local_path is not None - assert config.local_path == git_repo._config.cache_dir / "arc123" - assert config.repo_url.get_secret_value() == "https://gitlab.example.com/mygroup/arc123.git" - - -def test_git_context_ensure_path(tmp_path: Path) -> None: - """Test that GitContext creates local directories.""" - target_path = tmp_path / "deep" / "nested" / "repo" - config = GitContextConfig( - repo_url=SecretStr("https://example.com/repo.git"), - branch="main", - user_name=None, - user_email=None, - local_path=target_path, - ) - - context = GitContext(config) - path = context._ensure_path() - - assert path == target_path - assert path.parent.exists() - # The actual leaf dir is created by git clone/init usually, but parent must exist - - -@patch("middleware.api.arc_store.git_repo.Repo") -def test_git_context_enter_clone(mock_repo: MagicMock, tmp_path: Path) -> None: - """Test GitContext cloning behavior.""" - target_path = tmp_path / "repo" - target_path.mkdir() - - config = GitContextConfig( - repo_url=SecretStr("https://example.com/repo.git"), - branch="main", - user_name=None, - user_email=None, - local_path=target_path, - ) - - with GitContext(config) as ctx: - assert ctx.path == str(target_path) - # Verify clone called because .git doesn't exist - mock_repo.clone_from.assert_called_once() - - # Verify close called - mock_repo.clone_from.return_value.close.assert_called_once() - - -@patch("middleware.api.arc_store.git_repo.Repo") -def test_git_context_enter_existing(mock_repo: MagicMock, tmp_path: Path) -> None: - """Test GitContext connecting to existing repo.""" - target_path = tmp_path / "repo" - target_path.mkdir() - (target_path / ".git").mkdir() - - config = GitContextConfig( - repo_url=SecretStr("https://example.com/repo.git"), - branch="main", - user_name=None, - user_email=None, - local_path=target_path, - ) - - with GitContext(config) as ctx: - # Verify NO clone called - mock_repo.clone_from.assert_not_called() - # Verify Repo(path) called - mock_repo.assert_called() - assert ctx.repo is not None - - -def test_default_cache_dir_validator() -> None: - """Test Pydantic logic for default cache_dir.""" - # Strict mode test (should have been set by validator) - config = GitRepoConfig( - url="https://a", - group="b", - cache_dir=None, # type: ignore[arg-type] - ) - assert config.cache_dir is not None - - -@pytest.mark.asyncio -async def test_create_or_update(git_repo: GitRepo, tmp_path: Path) -> None: - """Test _create_or_update logic.""" - arc = MagicMock() - arc_id = "test_arc" - - # Mock loop and executor - # Since we mocked repo._executor in fixture, we need to handle run_in_executor - # But git_repo use `loop.run_in_executor`. - # For asyncio tests, we can patch the loop or just rely on the actual loop running the synchronous lambda - - # Let's patch GitContext to avoid real git operations and file system - with patch("middleware.api.arc_store.git_repo.GitContext") as mock_ctx: - mock_ctx_instance = mock_ctx.return_value - mock_ctx_instance.__enter__.return_value = mock_ctx_instance - # Mock repo path - fake_repo_path = tmp_path / "fake_repo" - fake_repo_path.mkdir() - # Create some junk to test cleanup - (fake_repo_path / "junk.txt").write_text("junk") - (fake_repo_path / ".git").mkdir() - - mock_ctx_instance.path = str(fake_repo_path) - mock_ctx_instance.repo = MagicMock() - - # Run the method - # Using real executor would spawn thread. Using sync lambda on mock executor: - # loop.run_in_executor(None, fn) runs in default executor. - # git_repo uses self._executor. - - # We need to ensure run_in_executor calls our function synchronously or awaits it. - # Since we mocked self._executor in the fixture, but loop.run_in_executor implementation - # calls executor.submit. - - # Actually, standard ThreadPoolExecutor logic works fine in tests usually, - # but let's see. - - # For simplicity, we can let it run with the real thread pool (reverting the fixture mock?) - # Or simpler: Patch run_in_executor to execute immediately. - - with patch("asyncio.get_running_loop") as mock_get_loop: - mock_loop = MagicMock() - mock_get_loop.return_value = mock_loop - # Make run_in_executor just call the function AND return a Future (or awaitable) because it is awaited - # But wait, run_in_executor returns a Future. We await it. - # If we make it call the function immediately, it returns the result of the function (None). - # None is not awaitable. - - # We need to return a done future. - - # But the function needs to actually RUN. - # So we can define a side_effect that runs the function and returns a done future. - - def run_and_return_future( - _executor: object, func: Callable[..., Any], *args: object - ) -> asyncio.Future[None]: - # Run the function - func(*args) - # Return done future - f: asyncio.Future[None] = asyncio.Future() - f.set_result(None) - return f - - mock_loop.run_in_executor.side_effect = run_and_return_future - - # Ensure path property mocks correctly - type(mock_ctx_instance).path = PropertyMock(return_value=str(fake_repo_path)) - - await git_repo._create_or_update(arc_id, arc) - - # Check cleanup - assert not (fake_repo_path / "junk.txt").exists() - assert (fake_repo_path / ".git").exists() - - # Check ARC write - # pylint: disable=no-member - arc.Write.assert_called_once_with(str(fake_repo_path)) - - # Check commit/push - mock_ctx_instance.commit_and_push.assert_called() - - -@pytest.mark.asyncio -async def test_get_arc_success(git_repo: GitRepo) -> None: - """Test _get successfully loads ARC.""" - arc_id = "test_arc" - - with ( - patch("middleware.api.arc_store.git_repo.GitContext") as mock_ctx, - patch("middleware.api.arc_store.git_repo.ARC") as mock_arc, - patch("asyncio.get_running_loop") as mock_get_loop, - ): - mock_loop = MagicMock() - mock_get_loop.return_value = mock_loop - - def run_and_return_future(_executor: object, func: Callable[..., Any], *args: object) -> asyncio.Future[Any]: - res = func(*args) - f: asyncio.Future[Any] = asyncio.Future() - f.set_result(res) - return f - - mock_loop.run_in_executor.side_effect = run_and_return_future - - mock_ctx_instance = mock_ctx.return_value - mock_ctx_instance.__enter__.return_value = mock_ctx_instance - mock_ctx_instance.path = "/tmp/fake/path" # nosec B108 - mock_ctx_instance.repo = MagicMock() - - mock_arc.load.return_value = "MyARC" - - result = await git_repo._get(arc_id) - - assert result == "MyARC" - mock_arc.load.assert_called_once_with("/tmp/fake/path") # nosec B108 - - -@pytest.mark.asyncio -async def test_get_arc_repo_fail(git_repo: GitRepo) -> None: - """Test _get handles repo init failure.""" - with ( - patch("middleware.api.arc_store.git_repo.GitContext") as mock_ctx, - patch("asyncio.get_running_loop") as mock_get_loop, - ): - mock_loop = MagicMock() - mock_get_loop.return_value = mock_loop - - def run_and_return_future(_executor: object, func: Callable[..., Any], *args: object) -> asyncio.Future[Any]: - res = func(*args) - f: asyncio.Future[Any] = asyncio.Future() - f.set_result(res) - return f - - mock_loop.run_in_executor.side_effect = run_and_return_future - - mock_ctx_instance = mock_ctx.return_value - mock_ctx_instance.__enter__.return_value = mock_ctx_instance - # Simulate failure to init repo - mock_ctx_instance.repo = None - - result = await git_repo._get("arc1") - assert result is None - - -@pytest.mark.asyncio -async def test_get_arc_load_fail(git_repo: GitRepo) -> None: - """Test _get handles ARC load failure.""" - with ( - patch("middleware.api.arc_store.git_repo.GitContext") as mock_ctx, - patch("middleware.api.arc_store.git_repo.ARC") as mock_arc, - patch("asyncio.get_running_loop") as mock_get_loop, - ): - mock_loop = MagicMock() - mock_get_loop.return_value = mock_loop - - def run_and_return_future(_executor: object, func: Callable[..., Any], *args: object) -> asyncio.Future[Any]: - res = func(*args) - f: asyncio.Future[Any] = asyncio.Future() - f.set_result(res) - return f - - mock_loop.run_in_executor.side_effect = run_and_return_future - - mock_ctx_instance = mock_ctx.return_value - mock_ctx_instance.__enter__.return_value = mock_ctx_instance - mock_ctx_instance.repo = MagicMock() - - mock_arc.load.side_effect = Exception("Bad ARC") - - result = await git_repo._get("arc1") - assert result is None - - -@pytest.mark.asyncio -async def test_exists_true(git_repo: GitRepo) -> None: - """Test _exists returns True.""" - with ( - patch("git.cmd.Git") as mock_git, - patch("asyncio.get_running_loop") as mock_get_loop, - ): - mock_loop = MagicMock() - mock_get_loop.return_value = mock_loop - - def run_and_return_future(_executor: object, func: Callable[..., Any], *args: object) -> asyncio.Future[Any]: - res = func(*args) - f: asyncio.Future[Any] = asyncio.Future() - f.set_result(res) - return f - - mock_loop.run_in_executor.side_effect = run_and_return_future - - mock_git_instance = mock_git.return_value - mock_git_instance.ls_remote.return_value = "hash ref" - - assert await git_repo._exists("arc1") is True - mock_git_instance.ls_remote.assert_called() - - -@pytest.mark.asyncio -async def test_exists_false(git_repo: GitRepo) -> None: - """Test _exists returns False on error.""" - with ( - patch("git.cmd.Git") as mock_git, - patch("asyncio.get_running_loop") as mock_get_loop, - ): - mock_loop = MagicMock() - mock_get_loop.return_value = mock_loop - - def run_and_return_future(_executor: object, func: Callable[..., Any], *args: object) -> asyncio.Future[Any]: - res = func(*args) - f: asyncio.Future[Any] = asyncio.Future() - f.set_result(res) - return f - - mock_loop.run_in_executor.side_effect = run_and_return_future - - mock_git_instance = mock_git.return_value - mock_git_instance.ls_remote.side_effect = GitCommandError("ls-remote", "fail") - - assert await git_repo._exists("arc1") is False - - -@pytest.mark.asyncio -async def test_delete(git_repo: GitRepo) -> None: - """Test _delete just logs warning.""" - # Just ensure it doesn't raise - await git_repo._delete("arc1") - - -@pytest.mark.asyncio -async def test_get_arc_load_os_error(git_repo: GitRepo) -> None: - """Test _get handles ARC load OSError.""" - with ( - patch("middleware.api.arc_store.git_repo.GitContext") as mock_ctx, - patch("middleware.api.arc_store.git_repo.ARC") as mock_arc, - patch("asyncio.get_running_loop") as mock_get_loop, - ): - mock_loop = MagicMock() - mock_get_loop.return_value = mock_loop - - def run_and_return_future(_executor: object, func: Callable[..., Any], *args: object) -> asyncio.Future[Any]: - res = func(*args) - f: asyncio.Future[Any] = asyncio.Future() - f.set_result(res) - return f - - mock_loop.run_in_executor.side_effect = run_and_return_future - - mock_ctx_instance = mock_ctx.return_value - mock_ctx_instance.__enter__.return_value = mock_ctx_instance - mock_ctx_instance.repo = MagicMock() - - mock_arc.load.side_effect = FileNotFoundError("Missing file") - - result = await git_repo._get("arc1") - assert result is None - - -@pytest.mark.asyncio -async def test_get_arc_cleanup_os_error(git_repo: GitRepo) -> None: - """Test _get handles OSError during cleanup.""" - with ( - patch("middleware.api.arc_store.git_repo.GitContext") as mock_ctx, - patch("middleware.api.arc_store.git_repo.ARC") as mock_arc, - patch("middleware.api.arc_store.git_repo.shutil.rmtree") as mock_rmtree, - patch("asyncio.get_running_loop") as mock_get_loop, - ): - mock_loop = MagicMock() - mock_get_loop.return_value = mock_loop - - def run_and_return_future(_executor: object, func: Callable[..., Any], *args: object) -> asyncio.Future[Any]: - res = func(*args) - f: asyncio.Future[Any] = asyncio.Future() - f.set_result(res) - return f - - mock_loop.run_in_executor.side_effect = run_and_return_future - - mock_ctx_instance = mock_ctx.return_value - mock_ctx_instance.__enter__.return_value = mock_ctx_instance - mock_ctx_instance.repo = MagicMock() - - mock_arc.load.return_value = "MyARC" - mock_rmtree.side_effect = OSError("Cleanup failed") - - # We need to make sure the path exists so rmtree is called - with patch("middleware.api.arc_store.git_repo.Path.exists", return_value=True): - result = await git_repo._get("arc1") - - assert result == "MyARC" - mock_rmtree.assert_called_once() - - -def test_is_soft_git_error() -> None: - """Test is_soft_git_error helper.""" - # Positive case - err = GitCommandError("command", 1, "not found") - assert is_soft_git_error(err) is True - - # Negative case - err = GitCommandError("command", 1, "permission denied") - assert is_soft_git_error(err) is False - - -@patch("middleware.api.arc_store.git_repo.Repo") -def test_git_context_sync_fail(mock_repo: MagicMock, tmp_path: Path) -> None: - """Test GitContext._sync_existing_repo handles failures.""" - target_path = tmp_path / "repo" - target_path.mkdir() - (target_path / ".git").mkdir() - - config = GitContextConfig( - repo_url=SecretStr("https://example.com/repo.git"), - branch="main", - user_name=None, - user_email=None, - local_path=target_path, - ) - - # Mock sync failure - mock_repo_instance = mock_repo.return_value - mock_repo_instance.remotes.origin.fetch.side_effect = GitCommandError("fetch", "fail") - - with GitContext(config) as ctx: - assert ctx.repo is not None - # Should log warning and proceed (back to handles_init error logic if called from enter) - - -@pytest.mark.asyncio -async def test_exists_unexpected_error(git_repo: GitRepo) -> None: - """Test _exists handles unexpected (non-soft) git error.""" - with ( - patch("git.cmd.Git") as mock_git, - patch("asyncio.get_running_loop") as mock_get_loop, - ): - mock_loop = MagicMock() - mock_get_loop.return_value = mock_loop - - def run_and_return_future(_executor: object, func: Callable[..., Any], *args: object) -> asyncio.Future[Any]: - res = func(*args) - f: asyncio.Future[Any] = asyncio.Future() - f.set_result(res) - return f - - mock_loop.run_in_executor.side_effect = run_and_return_future - - mock_git_instance = mock_git.return_value - # Unexpected error - mock_git_instance.ls_remote.side_effect = GitCommandError("ls-remote", "severe error") - - assert await git_repo._exists("arc1") is False diff --git a/middleware/api/tests/unit/test_git_repo_health.py b/middleware/api/tests/unit/test_git_repo_health.py deleted file mode 100644 index 270ceaa..0000000 --- a/middleware/api/tests/unit/test_git_repo_health.py +++ /dev/null @@ -1,73 +0,0 @@ -"""Unit tests for GitRepo health checks and validation.""" - -import http -from pathlib import Path -from unittest.mock import MagicMock, patch - -import pytest -from pydantic import ValidationError - -from middleware.api.arc_store.git_repo import GitRepo, GitRepoConfig - - -def test_validate_url_scheme_valid() -> None: - """Test valid URL schemes.""" - GitRepoConfig(url="https://example.com/repo.git", group="group", cache_dir=Path("/tmp")) # nosec B108 - GitRepoConfig(url="file:///tmp/repo.git", group="group", cache_dir=Path("/tmp")) # nosec B108 - GitRepoConfig(url="http://example.com/repo.git", group="group", cache_dir=Path("/tmp")) # nosec B108 - - -def test_validate_url_scheme_invalid() -> None: - """Test invalid URL schemes.""" - with pytest.raises(ValidationError) as excinfo: - GitRepoConfig(url="ftp://example.com/repo.git", group="group", cache_dir=Path("/tmp")) # nosec B108 - assert "Git URL must start with one of: ('https://', 'file://', 'http://')" in str(excinfo.value) - - -def test_check_health_file_scheme() -> None: - """Test health check for file:// scheme returns True regardless of path existence.""" - config = GitRepoConfig(url="file:///non/existent/path", group="group", cache_dir=Path("/tmp")) # nosec B108 - repo = GitRepo(config) - - # Even if path doesn't exist, it should return True as per requirements - assert repo.check_health() is True - - -@patch("urllib.request.urlopen") -def test_check_health_https_success(mock_urlopen: MagicMock) -> None: - """Test health check for https:// scheme success.""" - mock_response = MagicMock() - mock_response.status = http.HTTPStatus.OK - mock_response.__enter__.return_value = mock_response - mock_urlopen.return_value = mock_response - - config = GitRepoConfig(url="https://example.com", group="group", cache_dir=Path("/tmp")) # nosec B108 - repo = GitRepo(config) - - assert repo.check_health() is True - mock_urlopen.assert_called_once() - - -@patch("urllib.request.urlopen") -def test_check_health_https_failure_status(mock_urlopen: MagicMock) -> None: - """Test health check for https:// scheme failure (404).""" - mock_response = MagicMock() - mock_response.status = http.HTTPStatus.NOT_FOUND - mock_response.__enter__.return_value = mock_response - mock_urlopen.return_value = mock_response - - config = GitRepoConfig(url="https://example.com", group="group", cache_dir=Path("/tmp")) # nosec B108 - repo = GitRepo(config) - - assert repo.check_health() is False - - -@patch("urllib.request.urlopen") -def test_check_health_timeout(mock_urlopen: MagicMock) -> None: - """Test health check timeout handling.""" - mock_urlopen.side_effect = TimeoutError("timed out") - - config = GitRepoConfig(url="https://example.com", group="group", cache_dir=Path("/tmp")) # nosec B108 - repo = GitRepo(config) - - assert repo.check_health() is False diff --git a/middleware/api/tests/unit/test_gitlab_api_config.py b/middleware/api/tests/unit/test_gitlab_api_config.py deleted file mode 100644 index 9e72784..0000000 --- a/middleware/api/tests/unit/test_gitlab_api_config.py +++ /dev/null @@ -1,36 +0,0 @@ -"""Unit tests for GitlabApiConfig validation.""" - -import pytest -from pydantic import HttpUrl, SecretStr, ValidationError - -from middleware.api.arc_store.gitlab_api import GitlabApiConfig - - -def test_gitlab_api_config_https_valid() -> None: - """Test valid HTTPS URL.""" - config = GitlabApiConfig( - url=HttpUrl("https://gitlab.example.com/"), - group="mygroup", - token=SecretStr("glpat-token"), - ) - assert str(config.url) == "https://gitlab.example.com/" - - -def test_gitlab_api_config_http_valid() -> None: - """Test valid HTTP URL.""" - config = GitlabApiConfig( - url=HttpUrl("http://gitlab.example.com/"), - group="mygroup", - token=SecretStr("glpat-token"), - ) - assert str(config.url) == "http://gitlab.example.com/" - - -def test_gitlab_api_config_file_invalid() -> None: - """Test invalid file URL (HttpUrl itself usually catches this, but checking strict https validator).""" - with pytest.raises(ValidationError): - GitlabApiConfig( - url=HttpUrl("file:///tmp"), - group="mygroup", - token=SecretStr("glpat-token"), - ) diff --git a/middleware/api/tests/unit/test_middleware_api.py b/middleware/api/tests/unit/test_middleware_api.py deleted file mode 100644 index 13b65eb..0000000 --- a/middleware/api/tests/unit/test_middleware_api.py +++ /dev/null @@ -1,426 +0,0 @@ -"""Unit tests for the FastAPI middleware API endpoints.""" - -import http -import unittest.mock -from collections.abc import Callable -from unittest.mock import MagicMock - -import pytest -import redis -import redis.exceptions -from cryptography import x509 -from fastapi.testclient import TestClient - -from middleware.api.api import Api - - -def test_whoami_success(client: TestClient, middleware_api: Api, cert: str) -> None: - """Test the /v1/whoami endpoint with a valid certificate and accept header.""" - r = client.get( - "/v1/whoami", - headers={"ssl-client-cert": cert, "ssl-client-verify": "SUCCESS", "accept": "application/json"}, - ) - assert r.status_code == http.HTTPStatus.OK - body = r.json() - assert body["client_id"] == "TestClient" - - middleware_api.app.dependency_overrides.clear() - - -def test_whoami_invalid_accept(client: TestClient, cert: str) -> None: - """Test the /v1/whoami endpoint with an invalid accept header.""" - r = client.get( - "/v1/whoami", - headers={"ssl-client-cert": cert, "ssl-client-verify": "SUCCESS", "accept": "application/xml"}, - ) - assert r.status_code == http.HTTPStatus.NOT_ACCEPTABLE - - -def test_whoami_no_cert(client: TestClient) -> None: - """Test the /v1/whoami endpoint without a client certificate.""" - r = client.get( - "/v1/whoami", - headers={"accept": "application/json"}, - ) - assert r.status_code == http.HTTPStatus.UNAUTHORIZED - - -def test_whoami_invalid_cert(client: TestClient, middleware_api: Api) -> None: - """Test the /v1/whoami endpoint with an invalid client certificate.""" - r = client.get( - "/v1/whoami", - headers={"ssl-client-cert": "dummy cert", "ssl-client-verify": "SUCCESS", "accept": "application/json"}, - ) - assert r.status_code == http.HTTPStatus.BAD_REQUEST - - middleware_api.app.dependency_overrides.clear() - - -@pytest.mark.parametrize("verify_status", ["FAILED", "NONE"]) -def test_whoami_cert_verify_not_success(client: TestClient, cert: str, verify_status: str) -> None: - """Test the /v1/whoami endpoint with failed or no certificate verification.""" - r = client.get( - "/v1/whoami", - headers={"ssl-client-cert": cert, "ssl-client-verify": verify_status, "accept": "application/json"}, - ) - assert r.status_code == http.HTTPStatus.UNAUTHORIZED - - -def test_health_check_success(client: TestClient) -> None: - """Test /v1/health success.""" - # Mock Redis - mock_redis = MagicMock() - mock_redis.ping.return_value = True - - # Mock Celery connection - mock_conn = MagicMock() - - with ( - unittest.mock.patch("middleware.api.api.redis.from_url", return_value=mock_redis), - unittest.mock.patch("middleware.api.api.celery_app.connection_or_acquire") as mock_acquire, - ): - mock_acquire.return_value.__enter__.return_value = mock_conn - - r = client.get("/v1/health", headers={"accept": "application/json"}) - assert r.status_code == http.HTTPStatus.OK - assert r.json() == { - "status": "ok", - "redis_reachable": True, - "rabbitmq_reachable": True, - } - - -def test_health_check_failure(client: TestClient) -> None: - """Test /v1/health failure.""" - # Mock Redis failure - with ( - unittest.mock.patch("middleware.api.api.redis.from_url", side_effect=redis.exceptions.RedisError("Redis down")), - unittest.mock.patch( - "middleware.api.api.celery_app.connection_or_acquire", side_effect=Exception("RabbitMQ down") - ), - ): - r = client.get("/v1/health", headers={"accept": "application/json"}) - assert r.status_code == http.HTTPStatus.SERVICE_UNAVAILABLE - assert r.json() == { - "status": "error", - "redis_reachable": False, - "rabbitmq_reachable": False, - } - - -# ------------------------------------------------------------------- -# CREATE / UPDATE ARCS -# ------------------------------------------------------------------- - - -@pytest.mark.parametrize( - "expected_http_status", - [ - (http.HTTPStatus.ACCEPTED), - ], -) -def test_create_or_update_arcs_success(client: TestClient, cert: str, expected_http_status: int) -> None: - """Test creating a new ARC via the /v1/arcs endpoint.""" - # Mock the Celery task - mock_task = MagicMock() - mock_task.id = "task-123" - - # Check where process_arc is imported in api.py. It is imported as: from .worker import process_arc - # We need to patch the one in api.py - with pytest.MonkeyPatch.context() as mp: - mock_process_arc = MagicMock() - mock_process_arc.delay.return_value = mock_task - mp.setattr("middleware.api.api.process_arc", mock_process_arc) - - r = client.post( - "/v1/arcs", - headers={ - "ssl-client-cert": cert, - "ssl-client-verify": "SUCCESS", - "content-type": "application/json", - "accept": "application/json", - }, - json={"rdi": "rdi-1", "arcs": [{"dummy": "crate"}]}, - ) - assert r.status_code == expected_http_status - - -def test_create_or_update_arcs_invalid_cert_format(client: TestClient) -> None: - """Test error handling for invalid certificate format.""" - r = client.post( - "/v1/arcs", - headers={ - "ssl-client-cert": "NOT%20A%20VALID%20CERT", # Properly URL encoded but content invalid - "ssl-client-verify": "SUCCESS", - "content-type": "application/json", - "accept": "application/json", - }, - json={"rdi": "rdi-1", "arcs": [{"dummy": "crate"}]}, - ) - assert r.status_code == http.HTTPStatus.BAD_REQUEST - assert "Certificate parsing error" in r.json()["detail"] - - -def test_create_or_update_arcs_no_cert_allowed(client: TestClient, middleware_api: Api) -> None: - """Test successful submission without cert when not required.""" - # pylint: disable=protected-access - # Disable client cert requirement - middleware_api._config.require_client_cert = False - - # Needs to be known RDI - middleware_api._config.known_rdis = ["rdi-1"] - - # We must mock process_arc.delay since we expect success - mock_task = MagicMock() - mock_task.id = "task-no-cert" - - with unittest.mock.patch("middleware.api.api.process_arc.delay", return_value=mock_task): - r = client.post( - "/v1/arcs", - headers={ - "content-type": "application/json", - "accept": "application/json", - }, - json={"rdi": "rdi-1", "arcs": [{"dummy": "crate"}]}, - ) - assert r.status_code == http.HTTPStatus.ACCEPTED - body = r.json() - assert body["task_id"] == "task-no-cert" - - # Reset config - middleware_api._config.require_client_cert = True - - -def test_get_task_status(client: TestClient) -> None: - """Test getting task status.""" - mock_result = MagicMock() - mock_result.status = "SUCCESS" - mock_result.ready.return_value = True - mock_result.failed.return_value = False - mock_result.result = {"client_id": "test", "message": "ok", "rdi": "rdi-1", "arcs": []} - - with pytest.MonkeyPatch.context() as mp: - mock_async_result = MagicMock(return_value=mock_result) - # Verify import path in api.py: from .celery_app import celery_app - mp.setattr("middleware.api.api.celery_app.AsyncResult", mock_async_result) - - r = client.get( - "/v1/tasks/task-123", - headers={"accept": "application/json"}, - ) - assert r.status_code == http.HTTPStatus.OK - body = r.json() - assert body["task_id"] == "task-123" - assert body["status"] == "SUCCESS" - assert body["result"]["message"] == "ok" - assert body["result"]["client_id"] == "test" - - -# Removed test_create_or_update_arcs_invalid_json_semantic as validation runs async now - - -def test_create_or_update_arcs_invalid_body(client: TestClient, cert: str) -> None: - """Test error handling in the /v1/arcs endpoint.""" - r = client.post( - "/v1/arcs", - headers={ - "ssl-client-cert": cert, - "ssl-client-verify": "SUCCESS", - "content-type": "application/json", - "accept": "application/json", - }, - json=[{"dummy": "crate"}], - ) - assert r.status_code == http.HTTPStatus.UNPROCESSABLE_ENTITY # unprocessable entity by FastAPI - - -def test_create_or_update_arcs_invalid_accept(client: TestClient, cert: str) -> None: - """Test the /v1/arcs endpoint with an invalid accept header.""" - r = client.post( - "/v1/arcs", - headers={ - "ssl-client-cert": cert, - "ssl-client-verify": "SUCCESS", - "content-type": "application/json", - "accept": "application/xml", - }, - json={"rdi": "rdi-1", "arcs": [{"dummy": "crate"}]}, - ) - assert r.status_code == http.HTTPStatus.NOT_ACCEPTABLE - - -def test_create_or_update_arcs_no_cert(client: TestClient) -> None: - """Test the /v1/arcs endpoint without a client certificate.""" - r = client.post( - "/v1/arcs", - headers={ - "content-type": "application/json", - "accept": "application/json", - }, - json={"rdi": "rdi-1", "arcs": [{"dummy": "crate"}]}, - ) - assert r.status_code == http.HTTPStatus.UNAUTHORIZED - - -@pytest.mark.parametrize( - "client_verify, expected_status", - [ - ("FAILED", http.HTTPStatus.UNAUTHORIZED), - ("NONE", http.HTTPStatus.UNAUTHORIZED), - ], -) -def test_create_or_update_arcs_cert_verification_state( - cert: str, client: TestClient, client_verify: str, expected_status: int -) -> None: - """Test the /v1/arcs endpoint with an invalid client certificate.""" - r = client.post( - "/v1/arcs", - headers={ - "ssl-client-cert": cert, - "ssl-client-verify": client_verify, - "content-type": "application/json", - "accept": "application/json", - }, - json={"rdi": "rdi-1", "arcs": [{"dummy": "crate"}]}, - ) - assert r.status_code == expected_status - - -# ------------------------------------------------------------------- -# RDI AUTHORIZATION IN CREATE_OR_UPDATE_ARCS -# ------------------------------------------------------------------- - - -def test_create_or_update_arcs_rdi_not_known(client: TestClient, cert: str) -> None: - """Test that requesting an unknown RDI returns 400.""" - r = client.post( - "/v1/arcs", - headers={ - "ssl-client-cert": cert, - "ssl-client-verify": "SUCCESS", - "content-type": "application/json", - "accept": "application/json", - }, - json={"rdi": "rdi-unknown", "arcs": [{"dummy": "crate"}]}, - ) - assert r.status_code == http.HTTPStatus.BAD_REQUEST - - -def test_create_or_update_arcs_rdi_not_allowed( - client: TestClient, - oid: x509.ObjectIdentifier, - create_test_cert: Callable[[x509.ObjectIdentifier, list[str]], str], -) -> None: - """Test that requesting an RDI not in client certificate returns 403.""" - cert = create_test_cert(oid, ["rdi-2"]) - - r = client.post( - "/v1/arcs", - headers={ - "ssl-client-cert": cert, - "ssl-client-verify": "SUCCESS", - "content-type": "application/json", - "accept": "application/json", - }, - json={"rdi": "rdi-1", "arcs": [{"dummy": "crate"}]}, - ) - assert r.status_code == http.HTTPStatus.FORBIDDEN - - -# ------------------------------------------------------------------- -# ACCESSIBLE RDIS COMPUTATION -# ------------------------------------------------------------------- - - -def test_whoami_accessible_rdis_intersection(client: TestClient, middleware_api: Api) -> None: - """Test that accessible_rdis is the intersection of allowed_rdis and known_rdis.""" - # Create certificate with RDIs that partially overlap with known_rdis - # known_rdis fixture has ["rdi-1", "rdi-2"] - # Let's create a cert with ["rdi-1", "rdi-3"] - only rdi-1 should be accessible - # pylint: disable=protected-access - middleware_api.app.dependency_overrides[middleware_api._validate_client_id] = lambda: "TestClient" - middleware_api.app.dependency_overrides[middleware_api._get_authorized_rdis] = lambda: ["rdi-1", "rdi-3"] - middleware_api.app.dependency_overrides[middleware_api._get_known_rdis] = lambda: ["rdi-1", "rdi-2"] - - r = client.get( - "/v1/whoami", - headers={ - "ssl-client-cert": "dummy-cert", - "ssl-client-verify": "SUCCESS", - "accept": "application/json", - }, - ) - assert r.status_code == http.HTTPStatus.OK - # Only rdi-1 should be in the intersection - assert set(r.json()["accessible_rdis"]) == {"rdi-1"} - - # Cleanup - middleware_api.app.dependency_overrides.clear() - - -def test_whoami_accessible_rdis_no_overlap(client: TestClient, middleware_api: Api) -> None: - """Test that accessible_rdis is empty when there's no overlap between allowed and known RDIs.""" - # Create certificate with RDIs that don't overlap with known_rdis - # known_rdis has ["rdi-1", "rdi-2"], create cert with ["rdi-3", "rdi-4"] - # pylint: disable=protected-access - middleware_api.app.dependency_overrides[middleware_api._validate_client_id] = lambda: "TestClient" - middleware_api.app.dependency_overrides[middleware_api._get_authorized_rdis] = lambda: ["rdi-3", "rdi-4"] - middleware_api.app.dependency_overrides[middleware_api._get_known_rdis] = lambda: ["rdi-1", "rdi-2"] - - r = client.get( - "/v1/whoami", - headers={"ssl-client-cert": "dummy-cert", "ssl-client-verify": "SUCCESS", "accept": "application/json"}, - ) - assert r.status_code == http.HTTPStatus.OK - # No overlap, should be empty - assert r.json()["accessible_rdis"] == [] - - # Cleanup - middleware_api.app.dependency_overrides.clear() - - -def test_whoami_accessible_rdis_complete_overlap(client: TestClient, middleware_api: Api) -> None: - """Test that accessible_rdis contains all RDIs when there's complete overlap.""" - # Create certificate with same RDIs as known_rdis - # pylint: disable=protected-access - middleware_api.app.dependency_overrides[middleware_api._validate_client_id] = lambda: "TestClient" - middleware_api.app.dependency_overrides[middleware_api._get_authorized_rdis] = lambda: ["rdi-1", "rdi-2"] - middleware_api.app.dependency_overrides[middleware_api._get_known_rdis] = lambda: ["rdi-1", "rdi-2"] - - r = client.get( - "/v1/whoami", - headers={"ssl-client-cert": "dummy-cert", "ssl-client-verify": "SUCCESS", "accept": "application/json"}, - ) - assert r.status_code == http.HTTPStatus.OK - # Complete overlap - assert set(r.json()["accessible_rdis"]) == {"rdi-1", "rdi-2"} - - # Cleanup - middleware_api.app.dependency_overrides.clear() - - -def test_whoami_accessible_rdis_superset_in_cert(client: TestClient, middleware_api: Api) -> None: - """Test accessible_rdis when certificate contains more RDIs than known_rdis.""" - # Certificate has ["rdi-1", "rdi-2", "rdi-3", "rdi-4"] - # known_rdis has ["rdi-1", "rdi-2"] - # Result should be ["rdi-1", "rdi-2"] - # pylint: disable=protected-access - middleware_api.app.dependency_overrides[middleware_api._validate_client_id] = lambda: "TestClient" - middleware_api.app.dependency_overrides[middleware_api._get_authorized_rdis] = lambda: [ - "rdi-1", - "rdi-2", - "rdi-3", - "rdi-4", - ] - middleware_api.app.dependency_overrides[middleware_api._get_known_rdis] = lambda: ["rdi-1", "rdi-2"] - - r = client.get( - "/v1/whoami", - headers={"ssl-client-cert": "dummy-cert", "ssl-client-verify": "SUCCESS", "accept": "application/json"}, - ) - assert r.status_code == http.HTTPStatus.OK - # Only the intersection should be returned - assert set(r.json()["accessible_rdis"]) == {"rdi-1", "rdi-2"} - - # Cleanup - middleware_api.app.dependency_overrides.clear() diff --git a/middleware/api/tests/unit/test_persistence_gitlab_api.py b/middleware/api/tests/unit/test_persistence_gitlab_api.py deleted file mode 100644 index 7c1b11f..0000000 --- a/middleware/api/tests/unit/test_persistence_gitlab_api.py +++ /dev/null @@ -1,144 +0,0 @@ -"""Unit tests for the GitLab API persistence layer.""" - -# pylint: disable=protected-access - -import base64 -import http -from pathlib import Path -from typing import Any -from unittest.mock import MagicMock - -import pytest -from gitlab.exceptions import GitlabGetError - -# -------------------- Hilfsfunktionen -------------------- - - -def test_compute_arc_hash(tmp_path: Path, gitlab_api: Any) -> None: - """Tests the hash computation for ARC directories.""" - file = tmp_path / "f.txt" - file.write_text("hello") - h1 = gitlab_api._compute_arc_hash(tmp_path) - file.write_text("world") - h2 = gitlab_api._compute_arc_hash(tmp_path) - assert h1 != h2 # nosec - - -def test_get_or_create_project_found(gitlab_api: Any) -> None: - """Tests finding an existing GitLab project.""" - project = MagicMock() - project.path = "arc1" - gitlab_api._gitlab.projects.list.return_value = [project] - result = gitlab_api._get_or_create_project("arc1") - assert result == project # nosec - - -def test_get_or_create_project_create(gitlab_api: Any) -> None: - """Tests creating a new GitLab project if not found.""" - gitlab_api._gitlab.projects.list.return_value = [] - group = MagicMock() - group.id = 1 - gitlab_api._gitlab.groups.get.return_value = group - project = MagicMock() - gitlab_api._gitlab.projects.create.return_value = project - result = gitlab_api._get_or_create_project("arc1") - assert result == project # nosec - - -# -------------------- Create/Update -------------------- - - -@pytest.mark.asyncio -async def test_create_or_update_no_changes(gitlab_api: Any) -> None: - """Tests that no commit is made if the ARC hash hasn't changed.""" - arc = MagicMock() - arc.Write = lambda path: (Path(path) / "f.txt").write_text("abc") - - project = MagicMock() - # .arc_hash mit "dummyhash" vorhanden - project.files.get.return_value.content = base64.b64encode(b"dummyhash").decode() - gitlab_api._get_or_create_project = lambda _arc_id: project - gitlab_api._compute_arc_hash = lambda _path: "dummyhash" - - await gitlab_api.create_or_update("arc1", arc) - project.commits.create.assert_not_called() - - -@pytest.mark.asyncio -async def test_create_or_update_with_changes(gitlab_api: Any) -> None: - """Tests that a commit is made if the ARC hash has changed.""" - arc = MagicMock() - arc.Write = lambda path: (Path(path) / "f.txt").write_text("abc") - - project = MagicMock() - - # kein .arc_hash vorhanden - def raise_get(*_args: Any, **_kwargs: Any) -> None: - raise GitlabGetError("not found", response_code=http.HTTPStatus.NOT_FOUND) - - project.files.get.side_effect = raise_get - gitlab_api._get_or_create_project = lambda _arc_id: project - gitlab_api._compute_arc_hash = lambda _path: "newhash" - - await gitlab_api.create_or_update("arc1", arc) - project.commits.create.assert_called_once() - args, _kwargs = project.commits.create.call_args - actions = args[0]["actions"] - assert any(a["file_path"] == ".arc_hash" for a in actions) # nosec - - -# -------------------- Get -------------------- - - -@pytest.mark.asyncio -async def test_get_success(gitlab_api: Any, monkeypatch: Any) -> None: - """Tests retrieving an ARC from GitLab.""" - project = MagicMock() - project.path = "arc1" - project.repository_tree.return_value = [ - {"type": "blob", "path": "f.txt"}, - {"type": "blob", "path": ".arc_hash"}, - ] - - fobj = MagicMock() - fobj.content = base64.b64encode(b"hello").decode() - fobj.encoding = None - project.files.get.return_value = fobj - - gitlab_api._gitlab.projects.list.return_value = [project] - - dummy_arc = MagicMock() - monkeypatch.setattr("middleware.api.arc_store.gitlab_api.ARC.load", lambda _path: dummy_arc) - - arc = await gitlab_api.get("arc1") - assert arc == dummy_arc # nosec - project.files.get.assert_any_call(file_path="f.txt", ref=gitlab_api._config.branch) - - -@pytest.mark.asyncio -async def test_get_not_found(gitlab_api: Any) -> None: - """Tests retrieving a non-existing ARC from GitLab.""" - gitlab_api._gitlab.projects.list.return_value = [] - arc = await gitlab_api.get("arcX") - assert arc is None # nosec - - -# -------------------- Delete -------------------- - - -@pytest.mark.asyncio -async def test_delete_found(gitlab_api: Any) -> None: - """Tests deleting an existing ARC from GitLab.""" - project = MagicMock() - project.path = "arc1" - gitlab_api._gitlab.projects.list.return_value = [project] - await gitlab_api.delete("arc1") - project.delete.assert_called_once() - - -@pytest.mark.asyncio -async def test_delete_not_found(gitlab_api: Any) -> None: - """Tests deleting a non-existing ARC from GitLab.""" - gitlab_api._gitlab.projects.list.return_value = [] - # Sollte einfach durchlaufen, kein Fehler - await gitlab_api.delete("arcX") diff --git a/middleware/api/tests/unit/test_remote_repo_manager.py b/middleware/api/tests/unit/test_remote_repo_manager.py deleted file mode 100644 index c408491..0000000 --- a/middleware/api/tests/unit/test_remote_repo_manager.py +++ /dev/null @@ -1,159 +0,0 @@ -"""Unit tests for remote repository providers. - -This module provides tests for: -- FileSystemGitProvider: manages bare repositories in the local file system -- GitlabGitProvider: manages repositories on GitLab using the GitLab API -""" - -from pathlib import Path -from unittest.mock import MagicMock, patch - -import git -import pytest -from gitlab.exceptions import GitlabAuthenticationError, GitlabGetError - -from middleware.api.arc_store import ArcStoreError -from middleware.api.arc_store.remote_git_provider import ( - FileSystemGitProvider, - GitlabGitProvider, - RemoteGitProvider, -) - - -@pytest.fixture -def temp_remote_dir(tmp_path: Path) -> Path: - """Create a temporary directory for remote repositories.""" - remote_dir = tmp_path / "remotes" - remote_dir.mkdir() - return remote_dir - - -class TestFileSystemGitProvider: - """Tests for FileSystemGitProvider.""" - - def test_ensure_repo_exists_creates_bare_repo(self, temp_remote_dir: Path) -> None: - """Test that ensure_repo_exists creates a bare repository if it does not exist.""" - provider = FileSystemGitProvider(base_url=f"file://{temp_remote_dir}", group="my-group") - arc_id = "test-arc" - - provider.ensure_repo_exists(arc_id) - - expected_path = temp_remote_dir / "my-group" / f"{arc_id}.git" - assert expected_path.exists() - assert expected_path.is_dir() - - # Verify it's a bare repo - repo = git.Repo(expected_path) - assert repo.bare - - def test_get_repo_url(self, temp_remote_dir: Path) -> None: - """Test URL construction.""" - provider = FileSystemGitProvider(base_url=f"file://{temp_remote_dir}", group="my-group") - url = provider.get_repo_url("test-arc") - assert url == f"file://{temp_remote_dir}/my-group/test-arc.git" - - def test_check_health(self) -> None: - """Test health check.""" - provider = FileSystemGitProvider(base_url="file:///tmp", group="g") - assert provider.check_health() is True - - provider = FileSystemGitProvider(base_url="http://invalid", group="g") - assert provider.check_health() is False - - -class TestGitlabGitProvider: - """Tests for GitlabGitProvider.""" - - @patch("middleware.api.arc_store.remote_git_provider.gitlab.Gitlab") - def test_ensure_repo_exists_calls_gitlab_api(self, mock_gitlab_class: MagicMock) -> None: - """Test that ensure_repo_exists calls the GitLab API.""" - NAMESPACE_ID = 123 # noqa: N806 - - mock_gl = MagicMock() - mock_gitlab_class.return_value = mock_gl - - mock_group = MagicMock() - mock_group.full_path = "my-group-path" - mock_group.id = NAMESPACE_ID - mock_gl.groups.get.return_value = mock_group - - mock_gl.projects.get.side_effect = GitlabGetError("Not Found", response_code=404) - - provider = GitlabGitProvider(url="https://gitlab.com", group_name="my-group", token="secret") # nosec - arc_id = "test-arc" - - provider.ensure_repo_exists(arc_id) - - mock_gl.groups.get.assert_called_with("my-group") - mock_gl.projects.get.assert_called_with("my-group-path/test-arc") - mock_gl.projects.create.assert_called_once() - args = mock_gl.projects.create.call_args[0][0] - assert args["name"] == arc_id - assert args["namespace_id"] == NAMESPACE_ID - - @patch("middleware.api.arc_store.remote_git_provider.gitlab.Gitlab") - def test_ensure_repo_exists_401(self, mock_gitlab_class: MagicMock) -> None: - """Test that ensure_repo_exists handles 401 Unauthorized correctly.""" - mock_gl = MagicMock() - mock_gitlab_class.return_value = mock_gl - - # Simulate 401 on group retrieval - err = GitlabAuthenticationError("401 Unauthorized", response_code=401) - mock_gl.groups.get.side_effect = err - - provider = GitlabGitProvider(url="https://gitlab.com", group_name="my-group", token="invalid") # nosec - - with pytest.raises(ArcStoreError, match="401 Unauthorized"): - provider.ensure_repo_exists("some-arc") - - def test_get_repo_url(self) -> None: - """Test URL construction with and without auth.""" - url = "https://gitlab.com" - token = "secret-token" # nosec - provider = GitlabGitProvider(url=url, group_name="my-group", token=token) - - # Authenticated - auth_url = provider.get_repo_url("arc123", authenticated=True) - assert auth_url == "https://oauth2:secret-token@gitlab.com/my-group/arc123.git" - - # Not authenticated - plain_url = provider.get_repo_url("arc123", authenticated=False) - assert plain_url == "https://gitlab.com/my-group/arc123.git" - - @patch("middleware.api.arc_store.remote_git_provider.gitlab.Gitlab") - def test_check_health(self, mock_gitlab_class: MagicMock) -> None: - """Test health check using auth() call.""" - mock_gl = MagicMock() - mock_gitlab_class.return_value = mock_gl - - provider = GitlabGitProvider(url="https://gitlab.com", group_name="g", token="t") # nosec - - mock_gl.auth.return_value = True - assert provider.check_health() is True - - mock_gl.auth.side_effect = GitlabAuthenticationError() - assert provider.check_health() is False - - -class TestRemoteGitProviderFactory: - """Tests for RemoteGitProvider factory method.""" - - def test_from_url_file(self) -> None: - """Test factory with file URL.""" - provider = RemoteGitProvider.from_url("file:///tmp", "group") - assert isinstance(provider, FileSystemGitProvider) - - def test_from_url_https_defaults_to_gitlab(self) -> None: - """Test factory with HTTPS URL defaults to GitLab.""" - provider = RemoteGitProvider.from_url("https://git.something.com", "group") - assert isinstance(provider, GitlabGitProvider) - - def test_from_url_http_defaults_to_gitlab(self) -> None: - """Test factory with HTTP URL defaults to GitLab.""" - provider = RemoteGitProvider.from_url("http://localhost:8080", "group") - assert isinstance(provider, GitlabGitProvider) - - def test_from_url_unknown_fails(self) -> None: - """Test that unknown protocols fail.""" - with pytest.raises(ValueError, match="Could not determine git provider"): - RemoteGitProvider.from_url("ftp://server.local", "group") diff --git a/middleware/api/tests/unit/test_unit_create_or_update_arcs.py b/middleware/api/tests/unit/test_unit_create_or_update_arcs.py deleted file mode 100644 index 1fcf5d9..0000000 --- a/middleware/api/tests/unit/test_unit_create_or_update_arcs.py +++ /dev/null @@ -1,201 +0,0 @@ -"""Unit tests for the create_or_update_arcs functionality in BusinessLogic.""" - -from typing import Any -from unittest.mock import AsyncMock - -import pytest - -from middleware.api.business_logic import ( - ArcResponse, - BusinessLogic, - CreateOrUpdateArcsResponse, -) - -SHA256_LENGTH = 64 - - -def is_valid_sha256(s: str) -> bool: - """Check if a string is a valid SHA-256 hash.""" - if len(s) != SHA256_LENGTH: - return False - try: - int(s, 16) - return True - except ValueError: - return False - - -@pytest.mark.asyncio -@pytest.mark.parametrize( - "rocrate", - [ - [], # Empty list - [ - { # One item - "@context": "https://w3id.org/ro/crate/1.1/context", - "@graph": [ - { - "@id": "./", - "@type": "Dataset", - "additionalType": "Investigation", - "identifier": "ARC-001", - }, - { - "@id": "ro-crate-metadata.json", - "@type": "CreativeWork", - "conformsTo": {"@id": "https://w3id.org/ro/crate/1.1"}, - "about": {"@id": "./"}, - }, - ], - } - ], - [ - { # Multiple items - "@context": "https://w3id.org/ro/crate/1.1/context", - "@graph": [ - { - "@id": "./", - "@type": "Dataset", - "additionalType": "Investigation", - "identifier": "ARC-004", - }, - { - "@id": "ro-crate-metadata.json", - "@type": "CreativeWork", - "conformsTo": {"@id": "https://w3id.org/ro/crate/1.1"}, - "about": {"@id": "./"}, - }, - ], - }, - { - "@context": "https://w3id.org/ro/crate/1.1/context", - "@graph": [ - { - "@id": "./", - "@type": "Dataset", - "additionalType": "Investigation", - "identifier": "ARC-005", - }, - { - "@id": "ro-crate-metadata.json", - "@type": "CreativeWork", - "conformsTo": {"@id": "https://w3id.org/ro/crate/1.1"}, - "about": {"@id": "./"}, - }, - ], - }, - ], - ], -) -async def test_create_arc_success(service: BusinessLogic, rocrate: list[dict[str, Any]]) -> None: - """Test creating ARCs with valid RO-Crate JSON.""" - result = await service.create_or_update_arcs(rdi="TestRDI", arcs=rocrate, client_id="TestClient") - - assert isinstance(result, CreateOrUpdateArcsResponse) # nosec - assert result.client_id == "TestClient" # nosec - assert isinstance(result.arcs, list) # nosec - assert all(isinstance(a, ArcResponse) for a in result.arcs) # nosec - assert len(result.arcs) == len(rocrate) # nosec - assert all(is_valid_sha256(a.id) for a in result.arcs) # nosec - assert all(a.status == "created" for a in result.arcs) # nosec - - -@pytest.mark.asyncio -async def test_update_arc_success(service: BusinessLogic) -> None: - """Test updating an existing ARC.""" - rocrate = [ - { # One item - "@context": "https://w3id.org/ro/crate/1.1/context", - "@graph": [ - { - "@id": "./", - "@type": "Dataset", - "additionalType": "Investigation", - "identifier": "ARC-001", - }, - { - "@id": "ro-crate-metadata.json", - "@type": "CreativeWork", - "conformsTo": {"@id": "https://w3id.org/ro/crate/1.1"}, - "about": {"@id": "./"}, - }, - ], - } - ] - - # pylint: disable=protected-access - service._store.exists = AsyncMock(return_value=True) # type: ignore - result = await service.create_or_update_arcs(rdi="TestRDI", arcs=rocrate, client_id="TestClient") - - assert isinstance(result, CreateOrUpdateArcsResponse) # nosec - assert result.client_id == "TestClient" # nosec - assert isinstance(result.arcs, list) # nosec - assert all(isinstance(a, ArcResponse) for a in result.arcs) # nosec - assert len(result.arcs) == len(rocrate) # nosec - assert all(is_valid_sha256(a.id) for a in result.arcs) # nosec - assert all(a.status == "updated" for a in result.arcs) # nosec - - -@pytest.mark.asyncio -@pytest.mark.parametrize( - "rocrate", - [ - [{"@context": "https://w3id.org/ro/crate/1.1/context"}], # No @graph - [ - { # No @context - "@graph": [ - { - "@id": "./", - "@type": "Dataset", - "additionalType": "Investigation", - "identifier": "ARC-006", - }, - { - "@id": "ro-crate-metadata.json", - "@type": "CreativeWork", - "conformsTo": {"@id": "https://w3id.org/ro/crate/1.1"}, - "about": {"@id": "./"}, - }, - ] - } - ], - [ - { # No Dataset - "@context": "https://w3id.org/ro/crate/1.1/context", - "@graph": [ - { - "@id": "ro-crate-metadata.json", - "@type": "CreativeWork", - "about": {"@id": "./"}, - } - ], - } - ], - [ - { - "@context": "https://w3id.org/ro/crate/1.1/context", - "@graph": [ - { - "@id": "./", - "@type": "Dataset", - "additionalType": "Investigation", - # Missing Identifier - }, - { - "@id": "ro-crate-metadata.json", - "@type": "CreativeWork", - "conformsTo": {"@id": "https://w3id.org/ro/crate/1.1"}, - "about": {"@id": "./"}, - }, - ], - } - ], - ], -) -async def test_element_missing(service: BusinessLogic, rocrate: list[dict[str, Any]]) -> None: - """Test handling of RO-Crate JSON missing required elements.""" - # Since we now support partial failures, this should not raise an exception, - # but instead return a result with 0 successful ARCs and log the error. - response = await service.create_or_update_arcs(rdi="TestRDI", arcs=rocrate, client_id="TestClient") - assert len(response.arcs) == 0 - assert "Processed 0/1 ARCs successfully" in response.message diff --git a/middleware/api/tests/unit/test_worker.py b/middleware/api/tests/unit/test_worker.py deleted file mode 100644 index b607e8e..0000000 --- a/middleware/api/tests/unit/test_worker.py +++ /dev/null @@ -1,60 +0,0 @@ -"""Unit tests for Celery worker tasks.""" - -from typing import Any -from unittest.mock import patch - -import pytest - -from middleware.api.worker import process_arc -from middleware.shared.api_models.models import ArcResponse, ArcStatus, CreateOrUpdateArcsResponse - - -def test_process_arc_success() -> None: - """Test successful task execution.""" - # Mock business logic result - mock_result = CreateOrUpdateArcsResponse( - rdi="test-rdi", - client_id="test-client", - message="ok", - arcs=[ArcResponse(id="arc-1", status=ArcStatus.CREATED, timestamp="2024-01-01T00:00:00Z")], - ) - - # Mock the business_logic from celery_app - with patch("middleware.api.worker.business_logic") as mock_bl: - # Define the async return value - async def async_return(*_args: Any, **_kwargs: Any) -> CreateOrUpdateArcsResponse: - return mock_result - - mock_bl.create_or_update_arcs.side_effect = async_return - - # Execute the task - result = process_arc.apply(args=("test-rdi", {"dummy": "data"}, "test-client")).get() - - # Verify result dictionary structure - assert result["rdi"] == "test-rdi" - assert result["client_id"] == "test-client" - assert result["message"] == "ok" - assert len(result["arcs"]) == 1 - assert result["arcs"][0]["id"] == "arc-1" - - -def test_process_arc_failure() -> None: - """Test task failure handling.""" - with patch("middleware.api.worker.business_logic") as mock_bl: - # Define the async return value that raises an exception - async def async_raise(*_args: Any, **_kwargs: Any) -> None: - raise ValueError("Processing failed") - - mock_bl.create_or_update_arcs.side_effect = async_raise - - with pytest.raises(ValueError, match="Processing failed"): - process_arc.apply(args=("test-rdi", {"dummy": "data"}, "test-client")).get() - - -def test_process_arc_no_business_logic() -> None: - """Test task fails when business_logic is not initialized.""" - with ( - patch("middleware.api.worker.business_logic", None), - pytest.raises(RuntimeError, match="BusinessLogic not initialized"), - ): - process_arc.apply(args=("test-rdi", {"dummy": "data"}, "test-client")).get() diff --git a/middleware/api/tests/unit/test_worker_health.py b/middleware/api/tests/unit/test_worker_health.py deleted file mode 100644 index 71cfafa..0000000 --- a/middleware/api/tests/unit/test_worker_health.py +++ /dev/null @@ -1,138 +0,0 @@ -"""Unit tests for worker health check.""" - -from pathlib import Path -from types import SimpleNamespace -from unittest.mock import MagicMock, patch - -from pydantic import SecretStr - -from middleware.api.worker_health import check_worker_health - - -def test_check_worker_health_success(tmp_path: Path) -> None: - """Test worker health check success.""" - mock_config = MagicMock() - mock_config.gitlab_api = None - mock_config.git_repo = str(tmp_path / "repo") - mock_config.celery = SimpleNamespace(result_backend=SecretStr("redis://localhost:6379/0")) - - mock_store = MagicMock() - mock_store.check_health.return_value = True - - mock_redis = MagicMock() - mock_redis.ping.return_value = True - - mock_celery_conn = MagicMock() - - with ( - patch("middleware.api.worker_health.Path") as mock_path, - patch("middleware.api.worker_health.Config") as mock_config_cls, - patch("middleware.api.worker_health.GitRepo", return_value=mock_store), - patch("middleware.api.worker_health.redis.from_url", return_value=mock_redis), - patch("middleware.api.worker_health.celery_app.connection_or_acquire") as mock_conn, - ): - mock_path.return_value.is_file.return_value = True - mock_config_cls.from_yaml_file.return_value = mock_config - mock_conn.return_value.__enter__.return_value = mock_celery_conn - mock_celery_conn.ensure_connection.return_value = None - - assert check_worker_health() is True - - -def test_check_worker_health_backend_failure(tmp_path: Path) -> None: - """Test worker health check when backend fails.""" - mock_config = MagicMock() - mock_config.gitlab_api = None - mock_config.git_repo = str(tmp_path / "repo") - mock_config.celery = SimpleNamespace(result_backend=SecretStr("redis://localhost:6379/0")) - - mock_store = MagicMock() - mock_store.check_health.return_value = False # Backend unreachable - - mock_redis = MagicMock() - mock_redis.ping.return_value = True - - mock_celery_conn = MagicMock() - - with ( - patch("middleware.api.worker_health.Path") as mock_path, - patch("middleware.api.worker_health.Config") as mock_config_cls, - patch("middleware.api.worker_health.GitRepo", return_value=mock_store), - patch("middleware.api.worker_health.redis.from_url", return_value=mock_redis), - patch("middleware.api.worker_health.celery_app.connection_or_acquire") as mock_conn, - ): - mock_path.return_value.is_file.return_value = True - mock_config_cls.from_yaml_file.return_value = mock_config - mock_conn.return_value.__enter__.return_value = mock_celery_conn - mock_celery_conn.ensure_connection.return_value = None - - assert check_worker_health() is False - - -def test_check_worker_health_redis_failure(tmp_path: Path) -> None: - """Test worker health check when Redis fails.""" - mock_config = MagicMock() - mock_config.gitlab_api = None - mock_config.git_repo = str(tmp_path / "repo") - mock_config.celery = SimpleNamespace(result_backend=SecretStr("redis://localhost:6379/0")) - - mock_store = MagicMock() - mock_store.check_health.return_value = True - - mock_celery_conn = MagicMock() - - with ( - patch("middleware.api.worker_health.Path") as mock_path, - patch("middleware.api.worker_health.Config") as mock_config_cls, - patch("middleware.api.worker_health.GitRepo", return_value=mock_store), - patch("middleware.api.worker_health.redis.from_url", side_effect=Exception("Redis connection failed")), - patch("middleware.api.worker_health.celery_app.connection_or_acquire") as mock_conn, - ): - mock_path.return_value.is_file.return_value = True - mock_config_cls.from_yaml_file.return_value = mock_config - mock_conn.return_value.__enter__.return_value = mock_celery_conn - mock_celery_conn.ensure_connection.return_value = None - - assert check_worker_health() is False - - -def test_check_worker_health_rabbitmq_failure(tmp_path: Path) -> None: - """Test worker health check when RabbitMQ fails.""" - mock_config = MagicMock() - mock_config.gitlab_api = None - mock_config.git_repo = str(tmp_path / "repo") - mock_config.celery = SimpleNamespace(result_backend=SecretStr("redis://localhost:6379/0")) - - mock_store = MagicMock() - mock_store.check_health.return_value = True - - mock_redis = MagicMock() - mock_redis.ping.return_value = True - - with ( - patch("middleware.api.worker_health.Path") as mock_path, - patch("middleware.api.worker_health.Config") as mock_config_cls, - patch("middleware.api.worker_health.GitRepo", return_value=mock_store), - patch("middleware.api.worker_health.redis.from_url", return_value=mock_redis), - patch( - "middleware.api.worker_health.celery_app.connection_or_acquire", - side_effect=Exception("RabbitMQ connection failed"), - ), - ): - mock_path.return_value.is_file.return_value = True - mock_config_cls.from_yaml_file.return_value = mock_config - - assert check_worker_health() is False - - -def test_check_worker_health_config_missing() -> None: - """Test worker health check when config missing.""" - with patch("middleware.api.worker_health.Path") as mock_path: - mock_path.return_value.is_file.return_value = False - assert check_worker_health() is False - - -def test_check_worker_health_exception() -> None: - """Test worker health check exception handling.""" - with patch("middleware.api.worker_health.Path", side_effect=Exception("Disk error")): - assert check_worker_health() is False diff --git a/middleware/api_client/README.md b/middleware/api_client/README.md deleted file mode 100644 index e5616cc..0000000 --- a/middleware/api_client/README.md +++ /dev/null @@ -1,133 +0,0 @@ -# Middleware API Client - -Python client for the FAIRagro Middleware API with certificate-based authentication (mTLS). - -## Features - -- ✅ Certificate-based authentication (mutual TLS) -- ✅ Configuration via YAML files, environment variables, or Docker secrets -- ✅ Async context manager support -- ✅ Comprehensive error handling -- ✅ Type-safe with Pydantic models - -## Installation - -This package is part of the FAIRagro Advanced Middleware project and uses local dependencies. - -## Quick Start - -### 1. Create Configuration File - -```yaml -# config.yaml -log_level: INFO -api_url: https://your-api-server:8000 -client_cert_path: /path/to/client-cert.pem -client_key_path: /path/to/client-key.pem -ca_cert_path: /path/to/ca-cert.pem # optional -timeout: 30.0 -verify_ssl: true -``` - -### 2. Use the Client - -```python -import asyncio -from pathlib import Path -from arctrl import ARC, ArcInvestigation -from middleware.api_client import Config, ApiClient - -async def main(): - # Load configuration - config = Config.from_yaml_file(Path("config.yaml")) - - # Create ARC object - inv = ArcInvestigation.create(identifier="my-arc", title="My ARC") - arc = ARC.from_arc_investigation(inv) - - # Use client with context manager - async with ApiClient(config) as client: - # Send request - response = await client.create_or_update_arcs( - rdi="my-rdi", - arcs=[arc] - ) - print(f"Created/Updated {len(response.arcs)} ARCs") - -asyncio.run(main()) -``` - -## Configuration Options - -| Option | Type | Required | Default | Description | -|--------|------|----------|---------|-------------| -| `log_level` | string | No | INFO | Logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL) | -| `api_url` | string | Yes | - | Base URL of the Middleware API | -| `client_cert_path` | string | Yes | - | Path to client certificate (PEM format) | -| `client_key_path` | string | Yes | - | Path to client private key (PEM format) | -| `ca_cert_path` | string | No | null | Path to CA certificate for server verification | -| `timeout` | float | No | 30.0 | Request timeout in seconds | -| `verify_ssl` | bool | No | true | Enable SSL certificate verification | - -## API Methods - -### `create_or_update_arcs(rdi: str, arcs: list[ARC]) -> CreateOrUpdateArcsResponse` - -Create or update ARCs in the Middleware API. - -**Parameters:** - -- `rdi` (str): The RDI identifier (e.g., "edaphobase"). -- `arcs` (list[ARC]): List of ARC objects from arctrl library. - -**Returns:** - -- `CreateOrUpdateArcsResponse`: Contains the result of the operation. - -**Raises:** - -- `ApiClientError`: If the request fails due to HTTP errors or network issues. - -**Example:** - -```python -from arctrl import ARC, ArcInvestigation - -inv = ArcInvestigation.create(identifier="my-arc-001", title="My ARC") -arc = ARC.from_arc_investigation(inv) - -response = await client.create_or_update_arcs( - rdi="edaphobase", - arcs=[arc] -) -``` - -All errors are raised as `ApiClientError` exceptions: - -```python -from middleware.api_client import ApiClientError - -try: - response = await client.create_or_update_arcs( - rdi="my-rdi", - arcs=[arc] - ) -except ApiClientError as e: - print(f"API Error: {e}") -``` - -## Configuration via Environment Variables - -You can override configuration values using environment variables: - -```bash -export API_URL="https://production-api:8000" -export CLIENT_CERT_PATH="/secure/certs/prod-cert.pem" -export CLIENT_KEY_PATH="/secure/certs/prod-key.pem" -``` - -Or use Docker secrets in `/run/secrets/`. - -## License - -This is part of the FAIRagro Advanced Middleware project. diff --git a/middleware/api_client/example_client_config.yaml b/middleware/api_client/example_client_config.yaml deleted file mode 100644 index f09054c..0000000 --- a/middleware/api_client/example_client_config.yaml +++ /dev/null @@ -1,23 +0,0 @@ -# Example Configuration for Middleware API Client - -# Logging level (CRITICAL, ERROR, WARNING, INFO, DEBUG, NOTSET) -log_level: INFO - -# Base URL of the Middleware API -api_url: https://localhost:8000 - -# Path to the client certificate file (PEM format) -client_cert_path: /path/to/client-cert.pem - -# Path to the client private key file (PEM format) -client_key_path: /path/to/client-key.pem - -# Path to the CA certificate file for server verification (optional) -# Set to null or omit if not needed -ca_cert_path: /path/to/ca-cert.pem - -# Request timeout in seconds -timeout: "30.0" - -# Enable SSL certificate verification -verify_ssl: true diff --git a/middleware/api_client/pyproject.toml b/middleware/api_client/pyproject.toml deleted file mode 100644 index 6bffd4c..0000000 --- a/middleware/api_client/pyproject.toml +++ /dev/null @@ -1,41 +0,0 @@ -[project] -name = "api_client" -version = "0.0.0" # currently we disregard the version of this subpackage -description = "The FAIRagro advanced middleware API client" -readme = "README.md" -requires-python = ">=3.12" -dependencies = [ - "httpx>=0.28.1", - "pydantic>=2.12.5", -] - -[dependency-groups] -dev = [ - "pytest>=9.0.2", - "pytest-asyncio>=1.1.0", - "pytest-mock>=3.10.0", - "respx>=0.20.0", - "cryptography>=45.0.6", - "types-pyyaml>=6.0.12.20240917", -] - -[tool.uv.sources] -shared = { path = "../shared", editable = true } - -[tool.pytest.ini_options] -asyncio_mode = "auto" -testpaths = ["tests"] -python_files = ["test_*.py"] -python_classes = ["Test*"] -python_functions = ["test_*"] -addopts = "-v --strict-markers --tb=short" -markers = [ - "asyncio: marks tests as async (deselect with '-m \"not asyncio\"')", -] - -[tool.hatch.build.targets.wheel] -packages = ["src/middleware"] - -[build-system] -requires = ["hatchling"] -build-backend = "hatchling.build" diff --git a/middleware/api_client/src/middleware/api_client/__init__.py b/middleware/api_client/src/middleware/api_client/__init__.py deleted file mode 100644 index 0a4ed79..0000000 --- a/middleware/api_client/src/middleware/api_client/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -"""The FAIRagro Middleware API Client package.""" - -from .api_client import ApiClient, ApiClientError -from .config import Config - -__all__ = ["Config", "ApiClient", "ApiClientError"] diff --git a/middleware/api_client/src/middleware/api_client/api_client.py b/middleware/api_client/src/middleware/api_client/api_client.py deleted file mode 100644 index 1f68863..0000000 --- a/middleware/api_client/src/middleware/api_client/api_client.py +++ /dev/null @@ -1,301 +0,0 @@ -"""Client for the FAIRagro Middleware API.""" - -import asyncio -import json -import logging -import ssl -from typing import TYPE_CHECKING, Any - -import httpx -from pydantic import BaseModel - -from middleware.shared.api_models.models import ( - CreateOrUpdateArcsRequest, - CreateOrUpdateArcsResponse, -) - -from .config import Config - -if TYPE_CHECKING: - from arctrl import ARC # type: ignore[import-untyped] - -logger = logging.getLogger(__name__) - - -class ApiClientError(Exception): - """Base exception for ApiClient errors.""" - - def __init__(self, message: str, status_code: int | None = None) -> None: - """Initialize with message and optional status code.""" - super().__init__(message) - self.status_code = status_code - - -class ApiClient: - """Client for the FAIRagro Middleware API. - - This client provides access to the Middleware API with certificate-based - authentication (mTLS). It supports creating and updating ARCs. - - Example: - ```python - from pathlib import Path - from middleware.api_client import Config, ApiClient - - # Load configuration from YAML file - config = Config.from_yaml_file(Path("config.yaml")) - - # Create client instance - async with ApiClient(config) as client: - # Send request - response = await client.create_or_update_arcs( - rdi="my-rdi", - arcs=[{"@context": "...", "@id": "...", ...}] - ) - print(f"Created/Updated {len(response.arcs)} ARCs") - ``` - """ - - def __init__(self, config: Config) -> None: - """Initialize the ApiClient. - - Args: - config (Config): Configuration object containing API URL and certificate paths. - - Raises: - ApiClientError: If certificate or key files don't exist. - """ - self._config = config - self._client: httpx.AsyncClient | None = None - - # Validate certificate files exist (if provided) - cert_path = config.client_cert_path - key_path = config.client_key_path - - if cert_path is not None and not cert_path.exists(): - raise ApiClientError(f"Client certificate not found: {cert_path}") - if key_path is not None and not key_path.exists(): - raise ApiClientError(f"Client key not found: {key_path}") - - # Validate CA cert if provided - ca_path = config.ca_cert_path - if ca_path and not ca_path.exists(): - raise ApiClientError(f"CA certificate not found: {ca_path}") - - logger.debug( - "ApiClient initialized with API URL: %s, cert: %s, key: %s", - config.api_url, - cert_path, - key_path, - ) - - def _get_client(self) -> httpx.AsyncClient: - """Get or create the HTTP client instance. - - Returns: - httpx.AsyncClient: Configured async HTTP client. - """ - if self._client is None: - # Prepare verify parameter - if not self._config.verify_ssl: - verify: bool | ssl.SSLContext = False - elif self._config.ca_cert_path: - # Create SSL context with CA certificate - ctx = ssl.create_default_context(cafile=str(self._config.ca_cert_path)) - # Load client certificate chain for mTLS - if self._config.client_cert_path and self._config.client_key_path: - ctx.load_cert_chain( - str(self._config.client_cert_path), - str(self._config.client_key_path), - ) - verify = ctx - elif self._config.client_cert_path and self._config.client_key_path: - # No CA cert, but load client certs if available - ctx = ssl.create_default_context() - ctx.load_cert_chain( - str(self._config.client_cert_path), - str(self._config.client_key_path), - ) - verify = ctx - else: - verify = True - - self._client = httpx.AsyncClient( - base_url=self._config.api_url, - verify=verify, - timeout=self._config.timeout, - follow_redirects=self._config.follow_redirects, - headers={ - "accept": "application/json", - }, - ) - logger.debug("Created new httpx.AsyncClient instance") - - return self._client - - async def _post( - self, - path: str, - body: BaseModel, - ) -> Any: - """Send a POST request to the API. - - Args: - path (str): API endpoint path. - body (BaseModel): Request body as Pydantic model. - - Returns: - Any: JSON response data. - - Raises: - ApiClientError: If the request fails. - """ - client = self._get_client() - - try: - path = path.lstrip("/") - logger.debug("Sending POST request to %s", path) - resp = await client.post( - path, - json=body.model_dump(), - headers={"content-type": "application/json"}, - ) - resp.raise_for_status() - logger.debug("POST request successful, status code: %s", resp.status_code) - return resp.json() - except httpx.HTTPStatusError as e: - error_msg = f"HTTP error {e.response.status_code}: {e.response.text}" - logger.error(error_msg) - raise ApiClientError(error_msg, status_code=e.response.status_code) from e - except httpx.RequestError as e: - error_msg = f"Request error: {str(e)}" - logger.error(error_msg) - raise ApiClientError(error_msg) from e - - async def _get(self, path: str) -> Any: - """Send a GET request to the API. - - Args: - path (str): API endpoint path. - - Returns: - Any: JSON response data. - - Raises: - ApiClientError: If the request fails. - """ - client = self._get_client() - try: - path = path.lstrip("/") - logger.debug("Sending GET request to %s", path) - resp = await client.get(path) - resp.raise_for_status() - logger.debug("GET request successful, status code: %s", resp.status_code) - return resp.json() - except httpx.HTTPStatusError as e: - error_msg = f"HTTP error {e.response.status_code}: {e.response.text}" - logger.error(error_msg) - raise ApiClientError(error_msg, status_code=e.response.status_code) from e - except httpx.RequestError as e: - error_msg = f"Request error: {str(e)}" - logger.error(error_msg) - raise ApiClientError(error_msg) from e - - async def create_or_update_arc( - self, - rdi: str, - arc: "ARC | dict[str, Any]", - ) -> CreateOrUpdateArcsResponse: - """Create or update a single ARC in the FAIRagro Middleware API. - - Args: - rdi (str): The RDI identifier. - arc (ARC | dict[str, Any]): ARC object or already serialized ARC (as dict). - - Returns: - CreateOrUpdateArcsResponse: The response containing the result of the operation. - - Raises: - ApiClientError: If the request fails. - """ - # Determine if arc is an ARC object or dict - if isinstance(arc, dict): - # Already serialized - serialized_arc = arc - # If serialized, we might need to extract ID for logging if not provided separately - # But the caller logs it, so we just log a summary here - logger.info("Creating/updating 1 ARC (pre-serialized) for RDI: %s", rdi) - else: - # ARC object, needs serialization - logger.info("Creating/updating 1 ARC for RDI: %s", rdi) - json_str = arc.ToROCrateJsonString() - serialized_arc = json.loads(json_str) - - # The API currently expects a list of ARCs, so we wrap the single ARC - request = CreateOrUpdateArcsRequest(rdi=rdi, arcs=[serialized_arc]) - logger.debug("Request payload: %s", json.dumps(request.model_dump(), indent=2)) - - # 1. Submit task - result = await self._post("v1/arcs", request) - - task_id = result.get("task_id") - if not task_id: - raise ApiClientError("No task_id returned from API") - - logger.info("Task submitted, ID: %s. Polling for results...", task_id) - - # 2. Poll for results with exponential backoff - delay = 1.0 # Start with 1 second delay - max_delay = 30.0 # Max delay of 30 seconds - while True: - await asyncio.sleep(delay) - status_response = await self._get(f"v1/tasks/{task_id}") - status = status_response.get("status") - - logger.debug("Task %s status: %s (next poll in %.1fs)", task_id, status, delay) - - if status == "SUCCESS": - result_data = status_response.get("result") - response = CreateOrUpdateArcsResponse.model_validate(result_data) - logger.info( - "Successfully created/updated %d ARCs for RDI: %s", - len(response.arcs), - response.rdi, - ) - return response - - if status == "FAILURE": - error_msg = status_response.get("error", "Unknown error") - raise ApiClientError(f"Task failed: {error_msg}") - - # Increase delay for next poll iteration - delay = min(delay * 1.5, max_delay) - - async def aclose(self) -> None: - """Close the underlying HTTP client connection. - - This should be called to properly clean up resources when the client - is no longer needed. - """ - if self._client is not None: - logger.debug("Closing httpx.AsyncClient") - await self._client.aclose() - self._client = None - - async def __aenter__(self) -> "ApiClient": - """Async context manager entry. - - Returns: - ApiClient: This client instance. - """ - return self - - async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None: - """Async context manager exit. - - Args: - exc_type: Exception type if an error occurred. - exc_val: Exception value if an error occurred. - exc_tb: Exception traceback if an error occurred. - """ - await self.aclose() diff --git a/middleware/api_client/src/middleware/api_client/config.py b/middleware/api_client/src/middleware/api_client/config.py deleted file mode 100644 index adca55e..0000000 --- a/middleware/api_client/src/middleware/api_client/config.py +++ /dev/null @@ -1,38 +0,0 @@ -"""Configuration module for the Middleware API Client.""" - -from pathlib import Path -from typing import Annotated - -from pydantic import Field, field_validator - -from middleware.shared.config.config_base import ConfigBase - - -class Config(ConfigBase): - """Configuration model for the Middleware API Client. - - This configuration class extends ConfigBase and provides settings - for connecting to the Middleware API with certificate-based authentication. - """ - - api_url: Annotated[str, Field(description="Base URL of the Middleware API (e.g., https://api.example.com)")] - client_cert_path: Annotated[ - Path | None, Field(description="Path to the client certificate file in PEM format (optional)") - ] = None - client_key_path: Annotated[ - Path | None, Field(description="Path to the client private key file in PEM format (optional)") - ] = None - ca_cert_path: Annotated[ - Path | None, Field(description="Path to the CA certificate file for server verification (optional)") - ] = None - timeout: Annotated[float, Field(description="Request timeout in seconds", gt=0)] = 30.0 - verify_ssl: Annotated[bool, Field(description="Enable SSL certificate verification")] = True - follow_redirects: Annotated[bool, Field(description="Follow HTTP redirects for API requests")] = True - - @field_validator("api_url") - @classmethod - def ensure_trailing_slash(cls, v: str) -> str: - """Ensure the API URL ends with a trailing slash.""" - if not v.endswith("/"): - return v + "/" - return v diff --git a/middleware/api_client/tests/conftest.py b/middleware/api_client/tests/conftest.py deleted file mode 100644 index b99d142..0000000 --- a/middleware/api_client/tests/conftest.py +++ /dev/null @@ -1,110 +0,0 @@ -"""Shared test fixtures for API client tests.""" - -import datetime -import tempfile -from collections.abc import Generator -from pathlib import Path - -import pytest -import yaml -from cryptography import x509 -from cryptography.hazmat.primitives import hashes, serialization -from cryptography.hazmat.primitives.asymmetric import rsa -from cryptography.x509.oid import NameOID - - -@pytest.fixture -def temp_dir() -> Generator[Path, None, None]: - """Create a temporary directory for test files.""" - with tempfile.TemporaryDirectory() as tmpdir: - yield Path(tmpdir) - - -@pytest.fixture -def test_cert_pem(temp_dir: Path) -> tuple[Path, Path]: - """Generate a test certificate and key in PEM format. - - Returns: - Tuple of (cert_path, key_path) - """ - # Generate private key - private_key = rsa.generate_private_key( - public_exponent=65537, - key_size=2048, - ) - - # Generate self-signed certificate - subject = issuer = x509.Name( - [ - x509.NameAttribute(NameOID.COUNTRY_NAME, "DE"), - x509.NameAttribute(NameOID.STATE_OR_PROVINCE_NAME, "Test-State"), - x509.NameAttribute(NameOID.ORGANIZATION_NAME, "Test Organization"), - x509.NameAttribute(NameOID.COMMON_NAME, "TestClient"), - ] - ) - - cert = ( - x509.CertificateBuilder() - .subject_name(subject) - .issuer_name(issuer) - .public_key(private_key.public_key()) - .serial_number(x509.random_serial_number()) - .not_valid_before(datetime.datetime.now(datetime.UTC)) - .not_valid_after(datetime.datetime.now(datetime.UTC) + datetime.timedelta(days=1)) - .sign(private_key, hashes.SHA256()) - ) - - # Write certificate to file - cert_path = temp_dir / "test-cert.pem" - cert_path.write_bytes(cert.public_bytes(serialization.Encoding.PEM)) - - # Write private key to file - key_path = temp_dir / "test-key.pem" - key_path.write_bytes( - private_key.private_bytes( - encoding=serialization.Encoding.PEM, - format=serialization.PrivateFormat.TraditionalOpenSSL, - encryption_algorithm=serialization.NoEncryption(), - ) - ) - - return cert_path, key_path - - -@pytest.fixture -def test_config_dict(test_cert_pem: tuple[Path, Path]) -> dict: - """Create a test configuration dictionary. - - Args: - test_cert_pem: Tuple of (cert_path, key_path) - - Returns: - Dictionary with test configuration - """ - cert_path, key_path = test_cert_pem - return { - "log_level": "DEBUG", - "api_url": "https://test-api.example.com", - "client_cert_path": str(cert_path), - "client_key_path": str(key_path), - "timeout": "30.0", # ConfigWrapper requires string values - "verify_ssl": "true", # ConfigWrapper requires string values - } - - -@pytest.fixture -def test_config_yaml(temp_dir: Path, test_config_dict: dict) -> Path: - """Create a test configuration YAML file. - - Args: - temp_dir: Temporary directory - test_config_dict: Configuration dictionary - - Returns: - Path to the YAML configuration file - """ - config_path = temp_dir / "test_config.yaml" - with config_path.open("w") as f: - yaml.dump(test_config_dict, f) - - return config_path diff --git a/middleware/api_client/tests/integration/conftest.py b/middleware/api_client/tests/integration/conftest.py deleted file mode 100644 index d365f7e..0000000 --- a/middleware/api_client/tests/integration/conftest.py +++ /dev/null @@ -1,62 +0,0 @@ -"""Integration test fixtures for API client.""" - -from collections.abc import Generator -from typing import Any - -import pytest -from fastapi.testclient import TestClient - -# Import from the API package -from middleware.api.api import Api -from middleware.api.config import Config as ApiConfig - -# Import from the API client package -from middleware.api_client.config import Config - - -@pytest.fixture -def client_config(test_config_dict: dict) -> Config: - """Create a Config instance for testing. - - Uses the test_config_dict from the parent conftest.py - """ - return Config.from_data(test_config_dict) - - -@pytest.fixture(scope="session") -def api_config(known_rdis: list[str], oid: Any) -> dict[str, Any]: - """Provide API configuration for integration tests. - - Note: Uses the same known_rdis and oid fixtures from the parent conftest.py - """ - return { - "log_level": "DEBUG", - "known_rdis": known_rdis, - "client_auth_oid": oid.dotted_string, - "gitlab_api": { - "url": "https://fake-gitlab.example.com", - "group": "test-group", - "token": "fake-token", - "branch": "main", - }, - } - - -@pytest.fixture -def middleware_api(api_config: dict[str, Any]) -> Api: - """Provide the Middleware API instance for testing. - - This creates a real API instance that can be used with TestClient. - """ - config = ApiConfig.from_data(api_config) - return Api(config) - - -@pytest.fixture -def api_test_client(middleware_api: Api) -> Generator[TestClient, None, None]: - """Provide a TestClient for the Middleware API. - - This allows making HTTP requests to the API without a real server. - """ - with TestClient(middleware_api.app) as client: - yield client diff --git a/middleware/api_client/tests/integration/test_create_arcs.py b/middleware/api_client/tests/integration/test_create_arcs.py deleted file mode 100644 index 247bcc6..0000000 --- a/middleware/api_client/tests/integration/test_create_arcs.py +++ /dev/null @@ -1,169 +0,0 @@ -"""Integration tests for API client against real API instance. - -These tests use FastAPI TestClient to start the Middleware API within the test, -allowing us to test the full request/response cycle without needing nginx or -a real HTTP server. -""" - -import http -import json - -import httpx -import pytest -import respx -from arctrl import ARC, ArcInvestigation # type: ignore[import-untyped] - -from middleware.api_client import ApiClient, Config - - -@pytest.mark.asyncio -@respx.mock -async def test_create_arcs_integration_mock_server(client_config: Config) -> None: - """Test create_or_update_arc with mocked server responses. - - This test uses respx to mock the HTTP responses, allowing us to verify - that the client correctly sends certificates and handles responses. - """ - # Mock successful response - # Mock successful response - task_response = {"task_id": "task-integr-001", "status": "processing"} - - final_result = { - "client_id": "TestClient", - "message": "ARCs created", - "rdi": "test-rdi", - "arcs": [ - { - "id": "arc-id-123", - "status": "created", - "timestamp": "2024-01-01T12:00:00Z", - } - ], - } - - status_response = {"task_id": "task-integr-001", "status": "SUCCESS", "result": final_result} - - route_post = respx.post(f"{client_config.api_url}v1/arcs").mock( - return_value=httpx.Response(http.HTTPStatus.ACCEPTED, json=task_response) - ) - - route_get = respx.get(f"{client_config.api_url}v1/tasks/task-integr-001").mock( - return_value=httpx.Response(http.HTTPStatus.OK, json=status_response) - ) - - # Execute request with ARC object - arc = ARC.from_arc_investigation( - ArcInvestigation.create(identifier="test-arc-001", title="Test ARC", description="Integration test ARC") - ) - async with ApiClient(client_config) as client: - response = await client.create_or_update_arc( - rdi="test-rdi", - arc=arc, - ) - - # Verify - assert route_post.called - assert route_get.called - assert response.rdi == "test-rdi" - assert len(response.arcs) == 1 - assert response.arcs[0].status == "created" - - # Verify request was sent correctly - last_request = route_post.calls.last.request - assert last_request.method == "POST" - assert "application/json" in last_request.headers["content-type"] - - # Verify request body - body = json.loads(last_request.content) - assert body["rdi"] == "test-rdi" - assert len(body["arcs"]) == 1 - - -@pytest.mark.asyncio -@respx.mock -async def test_create_arcs_unauthorized(client_config: Config) -> None: - """Test handling of 401 Unauthorized response.""" - respx.post(f"{client_config.api_url}v1/arcs").mock( - return_value=httpx.Response(http.HTTPStatus.UNAUTHORIZED, text="Unauthorized") - ) - - arc = ARC.from_arc_investigation(ArcInvestigation.create(identifier="test", title="Test")) - async with ApiClient(client_config) as client: - with pytest.raises(Exception, match=str(http.HTTPStatus.UNAUTHORIZED.value)): - await client.create_or_update_arc( - rdi="test-rdi", - arc=arc, - ) - - -@pytest.mark.asyncio -@respx.mock -async def test_create_arcs_forbidden(client_config: Config) -> None: - """Test handling of 403 Forbidden response.""" - respx.post(f"{client_config.api_url}v1/arcs").mock( - return_value=httpx.Response(http.HTTPStatus.FORBIDDEN, text="Forbidden - RDI not authorized") - ) - - arc = ARC.from_arc_investigation(ArcInvestigation.create(identifier="test", title="Test")) - async with ApiClient(client_config) as client: - with pytest.raises(Exception, match=str(http.HTTPStatus.FORBIDDEN.value)): - await client.create_or_update_arc( - rdi="unauthorized-rdi", - arc=arc, - ) - - -@pytest.mark.asyncio -@respx.mock -async def test_create_arcs_validation_error(client_config: Config) -> None: - """Test handling of 422 Validation Error response.""" - respx.post(f"{client_config.api_url}v1/arcs").mock( - return_value=httpx.Response(http.HTTPStatus.UNPROCESSABLE_ENTITY, json={"detail": "Invalid ARC data"}) - ) - - arc = ARC.from_arc_investigation(ArcInvestigation.create(identifier="test", title="Test")) - async with ApiClient(client_config) as client: - with pytest.raises(Exception, match=str(http.HTTPStatus.UNPROCESSABLE_ENTITY.value)): - await client.create_or_update_arc( - rdi="test-rdi", - arc=arc, - ) - - -@pytest.mark.asyncio -@respx.mock -async def test_create_multiple_arcs(client_config: Config) -> None: - """Test that creating multiple ARCs (passing a list) raises a TypeError.""" - # Since the method signature expects a single ARC/dict, passing a list - # generally won't work. We verify it actually fails. - - arc1 = ARC.from_arc_investigation(ArcInvestigation.create(identifier="arc-1", title="ARC 1")) - arc2 = ARC.from_arc_investigation(ArcInvestigation.create(identifier="arc-2", title="ARC 2")) - - async with ApiClient(client_config) as client: - # We expect a failure because we are passing a list where a single item is expected - # This will likely fail in isinstance checks or attribute access inside the method - with pytest.raises((AttributeError, TypeError, Exception)): - await client.create_or_update_arc( - rdi="test-rdi", - arc=[arc1, arc2], # type: ignore - ) - - -@pytest.mark.asyncio -@respx.mock -async def test_timeout_error(client_config: Config) -> None: - """Test handling of timeout errors.""" - # Set a short timeout - client_config.timeout = 0.1 - - # Mock a slow response - respx.post(f"{client_config.api_url}v1/arcs").mock(side_effect=httpx.TimeoutException("Request timeout")) - - arc = ARC.from_arc_investigation(ArcInvestigation.create(identifier="test", title="Test")) - async with ApiClient(client_config) as client: - with pytest.raises(Exception, match="timeout|Timeout"): - await client.create_or_update_arc( - rdi="test-rdi", - arc=arc, - ) diff --git a/middleware/api_client/tests/unit/test_api_client_config.py b/middleware/api_client/tests/unit/test_api_client_config.py deleted file mode 100644 index 7c4bc12..0000000 --- a/middleware/api_client/tests/unit/test_api_client_config.py +++ /dev/null @@ -1,106 +0,0 @@ -"""Unit tests for api_client config module.""" - -from pathlib import Path - -import pytest -from pydantic import ValidationError - -from middleware.api_client.config import Config -from middleware.shared.config.config_base import OtelConfig - - -def test_config_creation_with_required_fields() -> None: - """Test creating an api_client Config with required fields.""" - config = Config( - api_url="https://api.example.com", - client_cert_path=Path("/path/to/cert.pem"), - client_key_path=Path("/path/to/key.pem"), - otel=OtelConfig(), - ) - - assert config.api_url == "https://api.example.com/" - assert config.client_cert_path == Path("/path/to/cert.pem") - assert config.client_key_path == Path("/path/to/key.pem") - - -def test_config_with_all_fields() -> None: - """Test creating an api_client Config with all fields.""" - config = Config( - api_url="https://api.example.com", - client_cert_path=Path("/path/to/cert.pem"), - client_key_path=Path("/path/to/key.pem"), - ca_cert_path=Path("/path/to/ca.pem"), - timeout=60.0, - verify_ssl=False, - follow_redirects=False, - log_level="DEBUG", - otel=OtelConfig(), - ) - - assert config.api_url == "https://api.example.com/" - assert config.ca_cert_path == Path("/path/to/ca.pem") - assert config.timeout == 60.0 # noqa: PLR2004 - assert config.verify_ssl is False - assert config.follow_redirects is False - assert config.log_level == "DEBUG" - - -def test_config_with_defaults() -> None: - """Test creating an api_client Config with default values.""" - config = Config( - api_url="https://api.example.com", - client_cert_path=Path("/path/to/cert.pem"), - client_key_path=Path("/path/to/key.pem"), - otel=OtelConfig(), - ) - - # Check defaults - assert config.ca_cert_path is None - assert config.timeout == 30.0 # noqa: PLR2004 - assert config.verify_ssl is True - assert config.follow_redirects is True - - -def test_config_trailing_slash_validator() -> None: - """Test that api_url always ends with a trailing slash.""" - # Case 1: No trailing slash provided - config1 = Config( - api_url="https://api.example.com", - otel=OtelConfig(), - ) - assert config1.api_url == "https://api.example.com/" - - # Case 2: Trailing slash already provided - config2 = Config( - api_url="https://api.example.com/", - otel=OtelConfig(), - ) - assert config2.api_url == "https://api.example.com/" - - -def test_config_timeout_validation() -> None: - """Test that timeout must be greater than 0.""" - with pytest.raises(ValidationError) as exc_info: - Config( - api_url="https://api.example.com", - client_cert_path=Path("/path/to/cert.pem"), - client_key_path=Path("/path/to/key.pem"), - timeout=0, # Invalid: must be > 0 - otel=OtelConfig(), - ) - - assert "timeout" in str(exc_info.value) - - -def test_config_timeout_negative() -> None: - """Test that timeout cannot be negative.""" - with pytest.raises(ValidationError) as exc_info: - Config( - api_url="https://api.example.com", - client_cert_path=Path("/path/to/cert.pem"), - client_key_path=Path("/path/to/key.pem"), - timeout=-10.0, # Invalid: must be > 0 - otel=OtelConfig(), - ) - - assert "timeout" in str(exc_info.value) diff --git a/middleware/api_client/tests/unit/test_client.py b/middleware/api_client/tests/unit/test_client.py deleted file mode 100644 index 5925c9c..0000000 --- a/middleware/api_client/tests/unit/test_client.py +++ /dev/null @@ -1,354 +0,0 @@ -"""Unit tests for the ApiClient class.""" - -import http -import ssl -from pathlib import Path -from unittest.mock import AsyncMock, patch - -import httpx -import pytest -import respx -from arctrl import ARC, ArcInvestigation # type: ignore[import-untyped] - -from middleware.api_client import ApiClient, ApiClientError, Config -from middleware.shared.api_models.models import CreateOrUpdateArcsResponse - - -@pytest.fixture -def client_config(test_config_dict: dict) -> Config: - """Create a Config instance for testing.""" - return Config.from_data(test_config_dict) - - -@pytest.mark.asyncio -async def test_client_initialization_success(client_config: Config) -> None: - """Test successful client initialization with valid config.""" - client = ApiClient(client_config) - assert client._config == client_config # pylint: disable=protected-access - assert client._client is None # pylint: disable=protected-access - - -@pytest.mark.asyncio -async def test_client_initialization_missing_cert(test_config_dict: dict, temp_dir: Path) -> None: - """Test client initialization fails when certificate file is missing.""" - # Point to non-existent certificate - test_config_dict["client_cert_path"] = str(temp_dir / "nonexistent-cert.pem") - config = Config.from_data(test_config_dict) - - with pytest.raises(ApiClientError, match="Client certificate not found"): - ApiClient(config) - - -@pytest.mark.asyncio -async def test_client_initialization_missing_key(test_config_dict: dict, temp_dir: Path) -> None: - """Test client initialization fails when key file is missing.""" - # Point to non-existent key - test_config_dict["client_key_path"] = str(temp_dir / "nonexistent-key.pem") - config = Config.from_data(test_config_dict) - - with pytest.raises(ApiClientError, match="Client key not found"): - ApiClient(config) - - -@pytest.mark.asyncio -async def test_client_initialization_missing_ca_cert(test_config_dict: dict, temp_dir: Path) -> None: - """Test client initialization fails when CA cert is specified but missing.""" - # Point to non-existent CA cert - test_config_dict["ca_cert_path"] = str(temp_dir / "nonexistent-ca.pem") - config = Config.from_data(test_config_dict) - - with pytest.raises(ApiClientError, match="CA certificate not found"): - ApiClient(config) - - -@pytest.mark.asyncio -@respx.mock -async def test_create_or_update_arc_success(client_config: Config) -> None: - """Test successful create_or_update_arc request.""" - # Mock the API response - # Mock the API response (Task submission) - task_response = {"task_id": "task-123", "status": "processing"} - - # Mock the Task Status response - status_response = { - "task_id": "task-123", - "status": "SUCCESS", - "result": { - "client_id": "TestClient", - "message": "ARCs created successfully", - "rdi": "test-rdi", - "arcs": [ - { - "id": "test-arc-123", - "status": "created", - "timestamp": "2024-01-01T12:00:00Z", - } - ], - }, - } - - route_post = respx.post(f"{client_config.api_url}v1/arcs").mock( - return_value=httpx.Response(http.HTTPStatus.ACCEPTED, json=task_response) - ) - - route_get = respx.get(f"{client_config.api_url}v1/tasks/task-123").mock( - return_value=httpx.Response(http.HTTPStatus.OK, json=status_response) - ) - - # Send request with ARC object - arc = ARC.from_arc_investigation(ArcInvestigation.create(identifier="test-arc", title="Test ARC")) - async with ApiClient(client_config) as client: - response = await client.create_or_update_arc( - rdi="test-rdi", - arc=arc, - ) - - # Verify - assert route_post.called - assert route_get.called - assert isinstance(response, CreateOrUpdateArcsResponse) - assert response.rdi == "test-rdi" - assert len(response.arcs) == 1 - assert response.arcs[0].id == "test-arc-123" - assert response.arcs[0].status == "created" - - -@pytest.mark.asyncio -@respx.mock -async def test_create_or_update_arc_http_error(client_config: Config) -> None: - """Test create_or_update_arc with HTTP error response.""" - # Mock an error response - respx.post(f"{client_config.api_url}v1/arcs").mock( - return_value=httpx.Response(http.HTTPStatus.FORBIDDEN, text="Forbidden") - ) - - # Should raise ApiClientError - arc = ARC.from_arc_investigation(ArcInvestigation.create(identifier="test", title="Test")) - async with ApiClient(client_config) as client: - with pytest.raises(ApiClientError, match=f"HTTP error {http.HTTPStatus.FORBIDDEN.value}"): - await client.create_or_update_arc( - rdi="test-rdi", - arc=arc, - ) - - -@pytest.mark.asyncio -@respx.mock -async def test_create_or_update_arc_network_error(client_config: Config) -> None: - """Test create_or_update_arc with network error.""" - # Mock a network error - respx.post(f"{client_config.api_url}v1/arcs").mock(side_effect=httpx.ConnectError("Connection refused")) - - # Should raise ApiClientError - arc = ARC.from_arc_investigation(ArcInvestigation.create(identifier="test", title="Test")) - async with ApiClient(client_config) as client: - with pytest.raises(ApiClientError, match="Request error"): - await client.create_or_update_arc( - rdi="test-rdi", - arc=arc, - ) - - -@pytest.mark.asyncio -async def test_async_context_manager(client_config: Config) -> None: - """Test that async context manager properly initializes and cleans up.""" - async with ApiClient(client_config) as client: - assert isinstance(client, ApiClient) - - # After context exit, client should be closed - # (we can't easily verify this without accessing private attributes) - - -@pytest.mark.asyncio -async def test_manual_close(client_config: Config) -> None: - """Test manual close of the client.""" - client = ApiClient(client_config) - - # Create the HTTP client by calling _get_client - http_client = client._get_client() # pylint: disable=protected-access - assert http_client is not None - - # Close manually - await client.aclose() - - # Client should be None after close - assert client._client is None # pylint: disable=protected-access - - -@pytest.mark.asyncio -async def test_client_uses_certificates(test_config_dict: dict, test_cert_pem: tuple[Path, Path]) -> None: - """Test that client is configured with the correct certificates.""" - cert_path, key_path = test_cert_pem - - # Update config to use the test certificates - test_config_dict["client_cert_path"] = str(cert_path) - test_config_dict["client_key_path"] = str(key_path) - config = Config.from_data(test_config_dict) - - # Patch httpx.AsyncClient to capture the cert argument - with patch("middleware.api_client.api_client.httpx.AsyncClient") as mock_client_class: - # Configure the mock to return an AsyncMock instance with an async aclose method - mock_instance = AsyncMock() - mock_client_class.return_value = mock_instance - - client = ApiClient(config) - client._get_client() # pylint: disable=protected-access - - # Verify AsyncClient was called with the correct verify parameter - mock_client_class.assert_called_once() - call_kwargs = mock_client_class.call_args.kwargs - - # httpx now expects verify as an ssl.SSLContext with loaded cert chain - assert "verify" in call_kwargs - verify_param = call_kwargs["verify"] - assert isinstance(verify_param, ssl.SSLContext) - - await client.aclose() - - -@pytest.mark.asyncio -@respx.mock -async def test_client_headers(client_config: Config) -> None: - """Test that client sends correct headers.""" - task_response = {"task_id": "task-headers", "status": "processing"} - status_response = { - "task_id": "task-headers", - "status": "SUCCESS", - "result": { - "client_id": "test", - "message": "ok", - "rdi": "test", - "arcs": [], - }, - } - - route_post = respx.post(f"{client_config.api_url}v1/arcs").mock( - return_value=httpx.Response(http.HTTPStatus.ACCEPTED, json=task_response) - ) - - respx.get(f"{client_config.api_url}v1/tasks/task-headers").mock( - return_value=httpx.Response(http.HTTPStatus.OK, json=status_response) - ) - - async with ApiClient(client_config) as client: - # Use a dict so it's treated as pre-serialized, avoiding JSON serialization issues with Mock - await client.create_or_update_arc(rdi="test", arc={"id": "mock-arc"}) - - # Verify headers - assert route_post.called - last_request = route_post.calls.last.request - assert last_request.headers["accept"] == "application/json" - assert last_request.headers["content-type"] == "application/json" - - -@pytest.mark.asyncio -async def test_client_verify_ssl_false(test_config_dict: dict) -> None: - """Test client initialization with verify_ssl=False.""" - test_config_dict["verify_ssl"] = "false" - config = Config.from_data(test_config_dict) - client = ApiClient(config) - - with patch("httpx.AsyncClient") as mock_client: - client._get_client() # pylint: disable=protected-access - mock_client.assert_called_once() - _, kwargs = mock_client.call_args - assert kwargs["verify"] is False - - -@pytest.mark.asyncio -async def test_client_with_ca_cert(test_config_dict: dict, temp_dir: Path) -> None: - """Test client initialization with a CA certificate.""" - ca_cert = temp_dir / "ca.pem" - ca_cert.write_text("fake-ca-cert") - test_config_dict["ca_cert_path"] = str(ca_cert) - config = Config.from_data(test_config_dict) - client = ApiClient(config) - - with patch("httpx.AsyncClient") as mock_client, patch("ssl.create_default_context") as mock_ssl: - mock_ctx = mock_ssl.return_value - client._get_client() # pylint: disable=protected-access - mock_ssl.assert_called_once_with(cafile=str(ca_cert)) - _, kwargs = mock_client.call_args - assert kwargs["verify"] == mock_ctx - - -@pytest.mark.asyncio -async def test_client_with_ca_and_mtls_cert(test_config_dict: dict, temp_dir: Path) -> None: - """Test client initialization with both CA and mTLS certificates.""" - ca_cert = temp_dir / "ca.pem" - ca_cert.write_text("fake-ca-cert") - cert_path = temp_dir / "client.crt" - cert_path.write_text("fake-cert") - key_path = temp_dir / "client.key" - key_path.write_text("fake-key") - - test_config_dict["ca_cert_path"] = str(ca_cert) - test_config_dict["client_cert_path"] = str(cert_path) - test_config_dict["client_key_path"] = str(key_path) - - config = Config.from_data(test_config_dict) - client = ApiClient(config) - - with patch("httpx.AsyncClient") as mock_client, patch("ssl.create_default_context") as mock_ssl: - mock_ctx = mock_ssl.return_value - client._get_client() # pylint: disable=protected-access - mock_ssl.assert_called_once_with(cafile=str(ca_cert)) - mock_ctx.load_cert_chain.assert_called_once_with(str(cert_path), str(key_path)) - _, kwargs = mock_client.call_args - assert kwargs["verify"] == mock_ctx - - -@pytest.mark.asyncio -@respx.mock -async def test_get_http_error(client_config: Config) -> None: - """Test _get with an HTTP error.""" - respx.get(f"{client_config.api_url}v1/test").mock(return_value=httpx.Response(http.HTTPStatus.NOT_FOUND)) - client = ApiClient(client_config) - with pytest.raises(ApiClientError, match="HTTP error 404"): - await client._get("v1/test") # pylint: disable=protected-access - - -@pytest.mark.asyncio -@respx.mock -async def test_get_network_error(client_config: Config) -> None: - """Test _get with a network error.""" - respx.get(f"{client_config.api_url}v1/test").mock(side_effect=httpx.RequestError("Network error")) - client = ApiClient(client_config) - with pytest.raises(ApiClientError, match="Request error: Network error"): - await client._get("v1/test") # pylint: disable=protected-access - - -@pytest.mark.asyncio -@respx.mock -async def test_create_or_update_arc_no_task_id(client_config: Config) -> None: - """Test create_or_update_arc when API returns no task_id.""" - respx.post(f"{client_config.api_url}v1/arcs").mock(return_value=httpx.Response(http.HTTPStatus.ACCEPTED, json={})) - client = ApiClient(client_config) - with pytest.raises(ApiClientError, match="No task_id returned from API"): - await client.create_or_update_arc(rdi="test", arc={"id": "mock-arc"}) - - -@pytest.mark.asyncio -@respx.mock -async def test_create_or_update_arc_task_failure(client_config: Config) -> None: - """Test create_or_update_arc when poll returns FAILURE.""" - task_response = {"task_id": "failed-task"} - status_response = { - "task_id": "failed-task", - "status": "FAILURE", - "error": "Something went wrong", - } - - respx.post(f"{client_config.api_url}v1/arcs").mock( - return_value=httpx.Response(http.HTTPStatus.ACCEPTED, json=task_response) - ) - respx.get(f"{client_config.api_url}v1/tasks/failed-task").mock( - return_value=httpx.Response(http.HTTPStatus.OK, json=status_response) - ) - - client = ApiClient(client_config) - with ( - patch("asyncio.sleep", return_value=None), - pytest.raises(ApiClientError, match="Task failed: Something went wrong"), - ): - await client.create_or_update_arc(rdi="test", arc={"id": "mock-arc"}) diff --git a/middleware/api_client/tests/unit/test_client_config.py b/middleware/api_client/tests/unit/test_client_config.py deleted file mode 100644 index 31d3638..0000000 --- a/middleware/api_client/tests/unit/test_client_config.py +++ /dev/null @@ -1,138 +0,0 @@ -"""Unit tests for the Config class.""" - -import tempfile -from pathlib import Path - -import pytest -from pydantic import ValidationError - -from middleware.api_client.config import Config - - -def test_config_from_yaml_file(test_config_yaml: Path) -> None: - """Test loading configuration from YAML file.""" - config = Config.from_yaml_file(test_config_yaml) - - assert config.log_level == "DEBUG" - assert config.api_url == "https://test-api.example.com/" - assert config.timeout == 30.0 # noqa: PLR2004 - assert config.verify_ssl is True - - -def test_config_from_data(test_config_dict: dict) -> None: - """Test creating configuration from dictionary.""" - config = Config.from_data(test_config_dict) - - assert config.log_level == "DEBUG" - assert config.api_url == "https://test-api.example.com/" - assert config.client_cert_path == Path(test_config_dict["client_cert_path"]) - assert config.client_key_path == Path(test_config_dict["client_key_path"]) - - -def test_config_get_client_cert_path(test_config_dict: dict) -> None: - """Test getting client certificate path as Path object.""" - config = Config.from_data(test_config_dict) - cert_path = config.client_cert_path - - assert isinstance(cert_path, Path) - assert str(cert_path) == test_config_dict["client_cert_path"] - - -def test_config_get_client_key_path(test_config_dict: dict) -> None: - """Test getting client key path as Path object.""" - config = Config.from_data(test_config_dict) - key_path = config.client_key_path - - assert isinstance(key_path, Path) - assert str(key_path) == test_config_dict["client_key_path"] - - -def test_config_get_ca_cert_path_none(test_config_dict: dict) -> None: - """Test getting CA cert path when not configured.""" - config = Config.from_data(test_config_dict) - ca_path = config.ca_cert_path - - assert ca_path is None - - -def test_config_get_ca_cert_path_set(test_config_dict: dict, temp_dir: Path) -> None: - """Test getting CA cert path when configured.""" - ca_cert_path = temp_dir / "ca-cert.pem" - ca_cert_path.write_text("fake ca cert") - - test_config_dict["ca_cert_path"] = str(ca_cert_path) - config = Config.from_data(test_config_dict) - ca_path = config.ca_cert_path - - assert ca_path is not None - assert isinstance(ca_path, Path) - assert ca_path == ca_cert_path - - -def test_config_default_values() -> None: - """Test configuration default values.""" - with tempfile.TemporaryDirectory() as tmpdir: - temp_path = Path(tmpdir) - cert_path = temp_path / "cert.pem" - key_path = temp_path / "key.pem" - cert_path.write_text("fake cert") - key_path.write_text("fake key") - - config = Config.from_data( - { - "api_url": "https://api.example.com", - "client_cert_path": str(cert_path), - "client_key_path": str(key_path), - } - ) - - assert config.log_level == "INFO" # Default from ConfigBase - assert config.timeout == 30.0 # noqa: PLR2004 - assert config.verify_ssl is True - assert config.ca_cert_path is None - - -def test_config_invalid_timeout() -> None: - """Test configuration with invalid timeout.""" - with tempfile.TemporaryDirectory() as tmpdir: - temp_path = Path(tmpdir) - cert_path = temp_path / "cert.pem" - key_path = temp_path / "key.pem" - cert_path.write_text("fake cert") - key_path.write_text("fake key") - - with pytest.raises(ValidationError): # Pydantic ValidationError - Config.from_data( - { - "api_url": "https://api.example.com", - "client_cert_path": str(cert_path), - "client_key_path": str(key_path), - "timeout": "-1.0", # Invalid: must be > 0 - } - ) - - -def test_config_missing_required_fields() -> None: - """Test configuration with only required fields (api_url). - - Client certificates are now optional, so only api_url is required. - """ - config = Config.from_data( - { - "api_url": "https://api.example.com", - # client_cert_path and client_key_path are optional - } - ) - assert config.api_url == "https://api.example.com/" - assert config.client_cert_path is None - assert config.client_key_path is None - - -def test_config_missing_api_url() -> None: - """Test configuration with missing required api_url field.""" - with pytest.raises(ValidationError): # Pydantic ValidationError - Config.from_data( - { - # Missing api_url (the only required field) - } - ) diff --git a/middleware/shared/README.md b/middleware/shared/README.md deleted file mode 100644 index 74e6d44..0000000 --- a/middleware/shared/README.md +++ /dev/null @@ -1,52 +0,0 @@ -# FAIRagro Advanced Middleware - Shared Components - -This package contains shared utilities and components used across the FAIRagro Advanced Middleware system. - -## Overview - -The `shared` package provides: - -- **Configuration Management**: Base classes and utilities for configuration handling -- **Common Models**: Pydantic models used across multiple middleware components -- **Utilities**: Helper functions and classes for common operations - -## Components - -### Configuration (`middleware.shared.config`) - -Configuration utilities including: - -- `ConfigWrapper`: Base class for configuration management -- Environment variable handling -- Configuration validation with Pydantic - -### Models - -Shared Pydantic models for data validation and serialization across the middleware. - -## Usage - -This package is used as a dependency by other middleware components: - -- `api`: The main REST API -- `api_client`: Client library for API interaction -- `sql_to_arc`: SQL to ARC conversion -- `inspire_to_arc`: INSPIRE metadata to ARC conversion - -## Dependencies - -- `pydantic>=2.12.4`: Data validation and settings management - -## Development - -Install in development mode: - -```bash -uv sync --package shared -``` - -Run tests: - -```bash -uv run pytest middleware/shared/tests -``` diff --git a/middleware/shared/pyproject.toml b/middleware/shared/pyproject.toml deleted file mode 100644 index dc2572d..0000000 --- a/middleware/shared/pyproject.toml +++ /dev/null @@ -1,20 +0,0 @@ -[project] -name = "shared" -version = "0.0.0" # currently we disregard the version of this subpackage -description = "The FAIRagro advanced middleware shared components" -readme = "README.md" -requires-python = ">=3.12" -dependencies = [ - "pydantic>=2.12.5", - "pyyaml>=6.0.3", - "opentelemetry-api>=1.26.0", - "opentelemetry-sdk>=1.26.0", - "opentelemetry-exporter-otlp>=1.26.0", -] - -[tool.hatch.build.targets.wheel] -packages = ["src/middleware"] - -[build-system] -requires = ["hatchling"] -build-backend = "hatchling.build" diff --git a/middleware/shared/src/middleware/shared/__init__.py b/middleware/shared/src/middleware/shared/__init__.py deleted file mode 100644 index f0b095f..0000000 --- a/middleware/shared/src/middleware/shared/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Contains same generic utility functions.""" diff --git a/middleware/shared/src/middleware/shared/api_models/__init__.py b/middleware/shared/src/middleware/shared/api_models/__init__.py deleted file mode 100644 index bf3e536..0000000 --- a/middleware/shared/src/middleware/shared/api_models/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""FAIrware Middleware shared API models package.""" diff --git a/middleware/shared/src/middleware/shared/api_models/models.py b/middleware/shared/src/middleware/shared/api_models/models.py deleted file mode 100644 index 7cea9c7..0000000 --- a/middleware/shared/src/middleware/shared/api_models/models.py +++ /dev/null @@ -1,107 +0,0 @@ -"""FAIRagro Middleware API Models package.""" - -from enum import Enum -from typing import Annotated - -from pydantic import BaseModel, Field - - -class ArcStatus(str, Enum): - """Enumeration of possible ARC status values. - - Values: - created: ARC was newly created - updated: ARC was updated - deleted: ARC was deleted - requested: ARC was requested - - """ - - CREATED = "created" - UPDATED = "updated" - DELETED = "deleted" - REQUESTED = "requested" - - -class LivenessResponse(BaseModel): - """Response model for liveness check.""" - - message: Annotated[str, Field(description="Liveness message")] = "ok" - - -class HealthResponse(BaseModel): - """Response model for health check including backend status.""" - - status: Annotated[str, Field(description="Overall service status (ok/error)")] = "ok" - redis_reachable: Annotated[bool, Field(description="True if Redis is reachable")] - rabbitmq_reachable: Annotated[bool, Field(description="True if RabbitMQ is reachable")] - - -class CreateOrUpdateArcsRequest(BaseModel): - """Request model for creating or updating ARCs.""" - - rdi: Annotated[str, Field(description="Research Data Infrastructure identifier")] - arcs: Annotated[list[dict], Field(description="List of ARC definitions in RO-Crate JSON format")] - - -class ApiResponse(BaseModel): - """Base response model for business logic operations. - - Args: - BaseModel (_type_): Pydantic BaseModel for data validation and serialization. - - """ - - client_id: Annotated[ - str | None, - Field( - description="Client identifier which is the CN from the client certificate, " - "or None if client certificates are not required", - ), - ] = None - message: Annotated[str, Field(description="Response message")] = "" - - -class WhoamiResponse(ApiResponse): - """Response model for whoami operation.""" - - accessible_rdis: Annotated[ - list[str], Field(description="List of Research Data Infrastructures the client is authorized for") - ] - - -class ArcResponse(BaseModel): - """Response model for individual ARC operations. - - Args: - BaseModel (_type_): Pydantic BaseModel for data validation and serialization. - - """ - - id: Annotated[str, Field(description="ARC identifier, as hashed value of the original identifier and RDI")] - status: Annotated[ArcStatus, Field(description="Status of the ARC operation")] - timestamp: Annotated[str, Field(description="Timestamp of the ARC operation in ISO 8601 format")] - - -class CreateOrUpdateArcsResponse(ApiResponse): - """Response model for create or update ARC operations (Task Ticket or Result).""" - - rdi: Annotated[str | None, Field(description="Research Data Infrastructure identifier the ARCs belong to")] = None - arcs: Annotated[list[ArcResponse], Field(description="List of ARC responses for the operation")] = Field( - default_factory=list - ) - - # Async task fields - task_id: Annotated[str | None, Field(description="The ID of the background task processing the ARC")] = None - status: Annotated[str | None, Field(description="The status of the task submission")] = None - - -class GetTaskStatusResponse(BaseModel): - """Response model for task status.""" - - task_id: Annotated[str, Field(description="The ID of the background task")] - status: Annotated[str, Field(description="The status of the task")] - result: Annotated[CreateOrUpdateArcsResponse | None, Field(description="The result of the task if completed")] = ( - None - ) - error: Annotated[str | None, Field(description="Error message if task failed")] = None diff --git a/middleware/shared/src/middleware/shared/api_models/py.typed b/middleware/shared/src/middleware/shared/api_models/py.typed deleted file mode 100644 index e69de29..0000000 diff --git a/middleware/shared/src/middleware/shared/config/__init__.py b/middleware/shared/src/middleware/shared/config/__init__.py deleted file mode 100644 index 48a8cb4..0000000 --- a/middleware/shared/src/middleware/shared/config/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""FAIRware Middleware shared config package.""" diff --git a/middleware/shared/src/middleware/shared/config/config_base.py b/middleware/shared/src/middleware/shared/config/config_base.py deleted file mode 100644 index 7319fc0..0000000 --- a/middleware/shared/src/middleware/shared/config/config_base.py +++ /dev/null @@ -1,91 +0,0 @@ -"""FAIRagro Middleware base configuration module.""" - -import logging -from pathlib import Path -from typing import Annotated, Literal, Self, cast - -from pydantic import BaseModel, Field - -from .config_wrapper import ConfigWrapper - -LogLevel = Literal["CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG", "NOTSET"] - - -class OtelConfig(BaseModel): - """OpenTelemetry logging and tracing configuration.""" - - endpoint: Annotated[ - str | None, - Field( - description="OpenTelemetry collector endpoint URL", - examples=["http://signoz:4318"], - ), - ] = None - log_console_spans: Annotated[ - bool, - Field(description="Log OpenTelemetry spans to console"), - ] = False - log_level: Annotated[ - LogLevel, - Field(description="Logging level for OTLP log export"), - ] = "INFO" - - -class ConfigBase(BaseModel): - """Configuration base class for the FAIRagro advanced Middleware.""" - - log_level: Annotated[LogLevel, Field(description="Logging level for console/stdout logging")] = "INFO" - otel: Annotated[ - OtelConfig, - Field(default_factory=OtelConfig, description="OpenTelemetry configuration"), - ] - - @classmethod - def from_config_wrapper(cls, wrapper: ConfigWrapper) -> Self: - """Create Config from ConfigWrapper. - - Args: - wrapper (ConfigWrapper): Wrapped configuration data. - - Returns: - Self: Configuration instance. - - """ - unwrapped = wrapper.unwrap() - # Cast to satisfy MyPy's warn_return_any=true setting - return cast(Self, cls.model_validate(unwrapped)) - - @classmethod - def from_data(cls, data: dict) -> Self: - """Create Config from raw data dictionary. - - Args: - data (dict): Raw configuration data. - - Returns: - Self: Configuration instance. - - """ - wrapper = ConfigWrapper.from_data(data) - return cls.from_config_wrapper(wrapper) - - @classmethod - def from_yaml_file(cls, path: Path) -> Self: - """Create Config from a YAML file. - - Args: - path (Path): Path to the YAML config file. - - Returns: - Config: Configuration instance. - - Raises: - RuntimeError: If the config file is not found. - - """ - if path.is_file(): - wrapper = ConfigWrapper.from_yaml_file(path) - return cls.from_config_wrapper(wrapper) - msg = f"Config file {path} not found." - logging.error(msg) - raise RuntimeError(msg) diff --git a/middleware/shared/src/middleware/shared/config/config_wrapper.py b/middleware/shared/src/middleware/shared/config/config_wrapper.py deleted file mode 100644 index decc943..0000000 --- a/middleware/shared/src/middleware/shared/config/config_wrapper.py +++ /dev/null @@ -1,364 +0,0 @@ -"""Defines the ConfigWrapper class that wraps a yaml file and supports. - -Overriding single entries in the yaml tree by env vars or docker -secret files in /run/secrets. -""" - -import os -from abc import abstractmethod -from collections.abc import Generator -from pathlib import Path -from typing import cast - -import yaml - -type KeyType = str | int -type DictType = dict[str, "ValueType"] -type ListType = list["ValueType"] -type PrimitiveType = str | int | float | bool | None -type ValueType = DictType | ListType | PrimitiveType -type WrapType = "ConfigWrapper | PrimitiveType" - - -class ConfigWrapper: - """Wraps nested dicts and lists (aka loaded yaml). - - Supports Env/Docker-Secret-Overrides. - """ - - def __init__(self, path: str = "") -> None: - """Initialize a ConfigWrapper with an optional path prefix. - - Args: - path: The path prefix used for environment variable and secret lookups. - - """ - self._path = path.upper() - - def _build_path(self, key: str) -> str: - return f"{self._path}_{key}" if self._path else key - - def _wrap(self, value: "ValueType | None", key: str) -> WrapType: - return ConfigWrapper._from_value(value, self._build_path(key)) - - @staticmethod - def _from_value(value: "ValueType | None", path: str) -> WrapType: - if isinstance(value, dict): - return ConfigWrapperDict(value, path) - if isinstance(value, list): - return ConfigWrapperList(value, path) - return value - - @classmethod - def from_data(cls, data: DictType | ListType, prefix: str = "") -> "ConfigWrapper": - """Create a ConfigWrapper from a dictionary or list. - - Args: - data: The dictionary or list to wrap. - prefix: Optional prefix for environment variable and secret lookups. - - Returns: - A new ConfigWrapper instance wrapping the provided data. - - Raises: - TypeError: If data is neither a dictionary nor a list. - - """ - wrapped = cls._from_value(data, prefix) - if not isinstance(wrapped, ConfigWrapper): - raise TypeError(f"'ConfigWrapper' only wraps lists or dicts. You're trying to wrap a '{type(data)}'") - return wrapped - - @classmethod - def from_yaml_file(cls, path: Path, prefix: str = "") -> "ConfigWrapper": - """Create a ConfigWrapper from a yaml file.""" - with open(path, encoding="utf-8") as f: - data = yaml.safe_load(f) or {} - return cls.from_data(data, prefix) - - @staticmethod - def _get_path_str(value: "ValueType | None", key: KeyType) -> str: - if isinstance(value, dict) and "id" in value: - return cast(str, value["id"]) - return str(key) - - @abstractmethod - def __getitem__(self, key: KeyType) -> WrapType: - """Return the value for the given key from the configuration. - - Args: - key: The key to lookup in the configuration. - - Returns: - The value associated with the key, wrapped if necessary. - - Raises: - NotImplementedError: When called on the base class. - - """ - raise NotImplementedError("Please do not use class 'ConfigWrapper' directly, but a derived class") - - def get(self, key: KeyType, default_value: "ValueType | None" = None) -> WrapType: - """Return the value of a config key. - - If the value is a dict or list, it's again wrapped into to ConfigWrapper object. - """ - try: - return self[key] - except KeyError: - key_str = ConfigWrapper._get_path_str(default_value, key) - return self._wrap(default_value, key_str) - - @classmethod - def _unwrap(cls, wrapper: WrapType) -> ValueType: - if isinstance(wrapper, ConfigWrapperDict): - return {k: cls._unwrap(v) for k, v in wrapper.items()} - if isinstance(wrapper, ConfigWrapperList): - return [cls._unwrap(wrapper[i]) for i in range(len(wrapper))] - if isinstance(wrapper, (str, int, float, bool, type(None))): - return wrapper - raise TypeError(f"Cannot unwrap element of type '{type(wrapper)}'") - - def unwrap(self) -> DictType | ListType: - """Convert the wrapped configuration back to a plain dictionary or list. - - Returns: - The unwrapped configuration as a dictionary or list. - - Raises: - TypeError: If the unwrapped value is not a dictionary or list. - - """ - unwrapped = ConfigWrapper._unwrap(self) - if isinstance(unwrapped, dict | list): - return unwrapped - raise TypeError(f"Unwrapped values must be of type list or dict, found '{type(unwrapped)}'") - - @abstractmethod - def __iter__(self) -> Generator[KeyType, None, None]: - """Return an iterator over the configuration keys. - - Returns: - A generator yielding keys of the configuration. - - """ - raise NotImplementedError("Please do not use class 'ConfigWrapper' directly, but a derived class") - - @abstractmethod - def items(self) -> Generator[tuple[KeyType, WrapType], None, None]: - """Return an iterator over the configuration key-value pairs. - - Returns: - A generator yielding tuples of (key, value) pairs from the configuration. - - """ - raise NotImplementedError("Please do not use class 'ConfigWrapper' directly, but a derived class") - - @abstractmethod - def __len__(self) -> int: - """Return the number of items in the configuration. - - Returns: - The number of items in the configuration. - - """ - raise NotImplementedError("Please do not use class 'ConfigWrapper' directly, but a derived class") - - def _override_key_access(self, key: str) -> PrimitiveType: - """Get override value for a key from environment or Docker secrets. - - Attempts to parse the override value as a primitive type (bool, int, float). - Supports the following formats: - - "true", "false" (case-insensitive) -> bool - - Numeric strings -> int or float - - Other strings -> str - - Empty string defaults to None - - Args: - key: The configuration key to lookup. - - Returns: - The override value as a primitive type, or None if not found. - """ - # self._path should alwys be upper case - full_key = self._path + "_" + key.upper() - - override_value = None - - # 1️⃣ Check ENV - if full_key in os.environ: - override_value = os.environ[full_key] - - # 2️⃣ Check Docker secret file - if override_value is None: - secret_file = Path(f"/run/secrets/{full_key.lower()}") - if secret_file.exists(): - override_value = secret_file.read_text(encoding="utf-8").strip() - - if override_value is None: - return None - - # Parse the override value to appropriate primitive type - return self._parse_primitive_value(override_value) - - @staticmethod - def _parse_primitive_value(value: str) -> PrimitiveType: - """Parse a string value into its appropriate primitive type. - - Conversion rules (in order): - 1. Empty string -> None - 2. "true"/"false" (case-insensitive) -> bool - 3. Integer-like strings -> int - 4. Float-like strings -> float - 5. Everything else -> str - - Args: - value: The string value to parse. - - Returns: - The parsed primitive value. - """ - if not value: - return None - - # Try bool - if value.lower() in ("true", "false"): - return value.lower() == "true" - - # Try int - try: - return int(value) - except ValueError: - pass - - # Try float - try: - return float(value) - except ValueError: - pass - - # Default to string - return value - - -class ConfigWrapperDict(ConfigWrapper): - """A ConfigWrapper flavor that specifically wraps dicts.""" - - def __init__(self, data: DictType, path: str = "") -> None: - """Initialize a ConfigWrapperDict with dictionary data and an path prefix. - - Args: - data: The dictionary to wrap. - path: The path prefix used for environment variable and secret lookups. - - """ - super().__init__(path) - self._data = data - - def _all_keys(self) -> set[str]: - """All keys including discovered ENV/Secrets.""" - keys = set(self._data.keys()) - for env_key in os.environ: - if env_key.startswith(self._path + "_"): - key_suffix = env_key[len(self._path) + 1 :] - keys.add(key_suffix.lower()) - secrets_dir = Path("/run/secrets") - path_lower = self._path.lower() - if secrets_dir.exists(): - for secret_file in secrets_dir.iterdir(): - if secret_file.name.startswith(path_lower + "_"): - key_suffix = secret_file.name[len(path_lower) + 1 :] - keys.add(key_suffix.lower()) - return keys - - def __getitem__(self, key: KeyType) -> WrapType: - """Return the value for the given key from the configuration. - - Args: - key: The key to lookup in the configuration. - - Returns: - WrapType: The value associated with the key, wrapped if necessary. - - """ - if not isinstance(key, str): - raise TypeError(f"ConfigWrapperDict only supports string keys, got {type(key)}") - - override_value = self._override_key_access(key) - if override_value is not None: - return override_value - value = self._data[key] - return super()._wrap(value, key) - - def __iter__(self) -> Generator[str, None, None]: - """Iterate over dict keys.""" - yield from self._all_keys() - - def items(self) -> Generator[tuple[str, WrapType], None, None]: - """Iterate over key-value pairs.""" - for key in self._all_keys(): - yield key, self[key] - - def __len__(self) -> int: - """Return the number of keys in the configuration dictionary. - - Returns: - The total count of configuration keys including environment and secret - overrides. - - """ - return len(self._all_keys()) - - -class ConfigWrapperList(ConfigWrapper): - """A ConfigWrapper flavour that specifically wraps lists.""" - - def __init__(self, data: ListType, path: str = "") -> None: - """Initialize a ConfigWrapperList with list data and a path prefix. - - Args: - data: The list to wrap. - path: The path prefix used for environment variable and secret lookups. - - """ - super().__init__(path) - self._data = data - - def __getitem__(self, key: KeyType) -> WrapType: - """Return the value at the specified index in the list. - - Args: - key: The index to lookup in the list. - - Returns: - The value at the specified index, wrapped if necessary. - - """ - if not isinstance(key, int): - raise TypeError(f"ConfigWrapperList only supports integer keys, got {type(key)}") - - value = self._data[key] - key_str = ConfigWrapper._get_path_str(value, key) - override_value = self._override_key_access(key_str) - if override_value is not None: - return override_value - return super()._wrap(value, key_str) - - def __iter__(self) -> Generator[int, None, None]: - """Iterate over list indices.""" - yield from range(len(self._data)) - - def items(self) -> Generator[tuple[int, WrapType], None, None]: - """Iterate over index-value pairs.""" - for idx, value in enumerate(self._data): - key_str = ConfigWrapper._get_path_str(value, idx) - yield idx, super()._wrap(value, key_str) - - def __len__(self) -> int: - """Return the number of items in the list. - - Returns: - The length of the wrapped list. - - """ - return len(self._data) diff --git a/middleware/shared/src/middleware/shared/config/logging.py b/middleware/shared/src/middleware/shared/config/logging.py deleted file mode 100644 index 0ae7291..0000000 --- a/middleware/shared/src/middleware/shared/config/logging.py +++ /dev/null @@ -1,25 +0,0 @@ -"""Logging configuration module. - -This module provides functionality to configure logging levels for all handlers -and the root logger across the application. -""" - -import logging - -from middleware.shared.config.config_base import LogLevel - - -def configure_logging(level: LogLevel) -> None: - """Configure logging level for all handlers. - - Args: - level: Logging level to set for all handlers and root logger. - """ - root = logging.getLogger() - if root.handlers: - # vorhandene Handler neu konfigurieren - for h in root.handlers: - h.setLevel(level) - root.setLevel(level) - else: - logging.basicConfig(level=level) diff --git a/middleware/shared/src/middleware/shared/tracing.py b/middleware/shared/src/middleware/shared/tracing.py deleted file mode 100644 index 2a4922f..0000000 --- a/middleware/shared/src/middleware/shared/tracing.py +++ /dev/null @@ -1,155 +0,0 @@ -""" -OpenTelemetry tracing configuration for the middleware API. - -This module initializes and configures OpenTelemetry for distributed tracing, -with support for FastAPI auto-instrumentation, console logging, and OTLP export to Signoz. -""" - -import logging -from collections.abc import Sequence -from typing import TYPE_CHECKING - -from opentelemetry import trace -from opentelemetry._logs import set_logger_provider -from opentelemetry.exporter.otlp.proto.http._log_exporter import OTLPLogExporter -from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter -from opentelemetry.sdk._logs import LoggerProvider, LoggingHandler -from opentelemetry.sdk._logs.export import ( - BatchLogRecordProcessor, - ConsoleLogRecordExporter, - SimpleLogRecordProcessor, -) -from opentelemetry.sdk.resources import Resource -from opentelemetry.sdk.trace import ReadableSpan, TracerProvider -from opentelemetry.sdk.trace.export import BatchSpanProcessor, SimpleSpanProcessor, SpanExporter, SpanExportResult - -if TYPE_CHECKING: - pass - -logger = logging.getLogger(__name__) - - -class SimpleConsoleSpanExporter(SpanExporter): - """Simple span exporter that logs to console.""" - - def export(self, spans: Sequence[ReadableSpan]) -> SpanExportResult: - """Export spans to console.""" - for span in spans: - if span.end_time is not None and span.start_time is not None: - duration_ms = (span.end_time - span.start_time) / 1e6 - else: - duration_ms = 0.0 - logger.info( - "SPAN: %s (duration=%0.3fms)", - span.name, - duration_ms, - ) - if span.attributes: - logger.info(" Attributes: %s", span.attributes) - return SpanExportResult.SUCCESS - - def shutdown(self) -> None: - """Shutdown the exporter.""" - pass - - def force_flush(self, timeout_millis: int = 30000) -> bool: # noqa: ARG002 - """Flush any pending spans.""" - return True - - -def initialize_tracing( - service_name: str = "middleware-api", - otlp_endpoint: str | None = None, - log_console_spans: bool = True, -) -> tuple[TracerProvider, trace.Tracer]: - """ - Initialize OpenTelemetry tracing with console and optional OTLP exporter. - - Args: - service_name: The service name for traces (default: "middleware-api") - otlp_endpoint: Optional OTLP endpoint URL (e.g. http://signoz:4318) - log_console_spans: Whether to log spans to console (default: True) - - Returns: - Tuple of (TracerProvider, Tracer) for use in the application - """ - # Create a resource describing this service - resource = Resource.create( - { - "service.name": service_name, - "service.version": "0.0.0", - } - ) - - # Create a tracer provider - tracer_provider = TracerProvider(resource=resource) - - # Optionally add console exporter for development/debugging - if log_console_spans: - console_exporter = SimpleConsoleSpanExporter() - tracer_provider.add_span_processor(SimpleSpanProcessor(console_exporter)) - - # Optionally add OTLP exporter for Signoz/Jaeger/etc - if otlp_endpoint: - try: - otlp_exporter = OTLPSpanExporter(endpoint=f"{otlp_endpoint}/v1/traces") - tracer_provider.add_span_processor(BatchSpanProcessor(otlp_exporter)) - logger.info("OpenTelemetry OTLP exporter configured: %s", otlp_endpoint) - except (ValueError, OSError) as e: - logger.warning("Failed to configure OTLP exporter: %s", e) - - # Set the global tracer provider - trace.set_tracer_provider(tracer_provider) - - # Get a tracer for this module - tracer = trace.get_tracer(__name__) - - logger.info( - "OpenTelemetry tracing initialized (console=%s, otlp=%s)", - log_console_spans, - bool(otlp_endpoint), - ) - - return tracer_provider, tracer - - -def initialize_logging( - service_name: str = "middleware-api", - otlp_endpoint: str | None = None, - log_console: bool = False, - log_level: int = logging.INFO, - otlp_log_level: int = logging.INFO, -) -> LoggerProvider: - """ - Initialize OpenTelemetry logging with optional OTLP exporter. - - Args: - service_name: The service name for log records. - service_version: The service version for log records. - otlp_endpoint: Optional OTLP endpoint URL (e.g. http://signoz:4318). - log_console: Whether to also export logs to console via OTLP SDK exporter. - """ - resource = Resource.create({"service.name": service_name, "service.version": "0.0.0"}) - logger_provider = LoggerProvider(resource=resource) - - if otlp_endpoint: - try: - otlp_log_exporter = OTLPLogExporter(endpoint=f"{otlp_endpoint}/v1/logs") - logger_provider.add_log_record_processor(BatchLogRecordProcessor(otlp_log_exporter)) - if log_console: - logger_provider.add_log_record_processor(SimpleLogRecordProcessor(ConsoleLogRecordExporter())) - set_logger_provider(logger_provider) - root_handler = LoggingHandler(level=otlp_log_level, logger_provider=logger_provider) - root_logger = logging.getLogger() - root_logger.addHandler(root_handler) - root_logger.setLevel(min(root_logger.level or log_level, log_level)) - # Prevent self-logging loops: OTEL internal logs and noisy HTTP/SSL logs at WARNING+ only - logging.getLogger("opentelemetry").setLevel(logging.WARNING) - logging.getLogger("urllib3").setLevel(logging.WARNING) - logging.getLogger("httpx").setLevel(logging.WARNING) - logging.getLogger("httpcore").setLevel(logging.WARNING) - logger.info("OpenTelemetry log exporter configured: %s", otlp_endpoint) - except (ValueError, OSError) as e: # pragma: no cover - defensive path - logger.warning("Failed to configure OTLP log exporter: %s", e) - - return logger_provider diff --git a/middleware/shared/tests/unit/test_api_models.py b/middleware/shared/tests/unit/test_api_models.py deleted file mode 100644 index e60442a..0000000 --- a/middleware/shared/tests/unit/test_api_models.py +++ /dev/null @@ -1,52 +0,0 @@ -"""Unit tests for shared API models.""" - -from middleware.shared.api_models.models import ( - ArcStatus, - CreateOrUpdateArcsRequest, - LivenessResponse, -) - - -def test_liveness_response_default() -> None: - """Test creating a LivenessResponse with default message.""" - response = LivenessResponse() - assert response.message == "ok" - - -def test_liveness_response_custom_message() -> None: - """Test creating a LivenessResponse with custom message.""" - response = LivenessResponse(message="service is running") - assert response.message == "service is running" - - -def test_create_or_update_arcs_request() -> None: - """Test creating a CreateOrUpdateArcsRequest.""" - arcs_data = [ - {"identifier": "1", "title": "ARC 1"}, - {"identifier": "2", "title": "ARC 2"}, - ] - - request = CreateOrUpdateArcsRequest(rdi="edaphobase", arcs=arcs_data) - - assert request.rdi == "edaphobase" - assert len(request.arcs) == 2 # noqa: PLR2004 - assert request.arcs[0]["identifier"] == "1" - assert request.arcs[1]["identifier"] == "2" - - -def test_arc_status_enum() -> None: - """Test ArcStatus enum values.""" - assert ArcStatus.CREATED == "created" - assert ArcStatus.UPDATED == "updated" - assert ArcStatus.DELETED == "deleted" - assert ArcStatus.REQUESTED == "requested" - - -def test_arc_status_enum_all_values() -> None: - """Test that all ArcStatus values are present.""" - statuses = [status.value for status in ArcStatus] - assert "created" in statuses - assert "updated" in statuses - assert "deleted" in statuses - assert "requested" in statuses - assert len(statuses) == 4 # noqa: PLR2004 diff --git a/middleware/shared/tests/unit/test_config_base.py b/middleware/shared/tests/unit/test_config_base.py deleted file mode 100644 index 03ceb76..0000000 --- a/middleware/shared/tests/unit/test_config_base.py +++ /dev/null @@ -1,25 +0,0 @@ -"""Unit tests for shared config_base module.""" - -from middleware.shared.config.config_base import ConfigBase, OtelConfig - - -def test_config_base_creation() -> None: - """Test creating a ConfigBase instance.""" - config = ConfigBase(log_level="INFO", otel=OtelConfig()) - - assert config.log_level == "INFO" - - -def test_config_base_default_log_level() -> None: - """Test ConfigBase with default log level.""" - config = ConfigBase(otel=OtelConfig()) - - assert config.log_level == "INFO" # Default log level - - -def test_config_base_different_log_levels() -> None: - """Test ConfigBase with different log levels.""" - log_levels = ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL", "NOTSET"] - for level in log_levels: - config = ConfigBase(log_level=level, otel={}) # type: ignore[arg-type] - assert config.log_level == level diff --git a/middleware/shared/tests/unit/test_config_wrapper.py b/middleware/shared/tests/unit/test_config_wrapper.py deleted file mode 100644 index 082c41d..0000000 --- a/middleware/shared/tests/unit/test_config_wrapper.py +++ /dev/null @@ -1,288 +0,0 @@ -"""Unit tests for the ConfigWrapper utility.""" - -from pathlib import Path -from typing import Any - -import pytest - -from middleware.shared.config.config_wrapper import ConfigWrapper, ConfigWrapperDict, ConfigWrapperList, ListType - - -@pytest.fixture -def sample_dict() -> dict[str, Any]: - """Sample dictionary for testing.""" - return { - "foo": "bar", - "nested": {"key": "value"}, - "list": [{"id": "foo", "key": "value"}], - } - - -@pytest.fixture -def sample_list() -> list[Any]: - """Sample list for testing.""" - return ["a", "b", {"id": "c", "value": 42}] - - -def test_dict_basic_access( - sample_dict: dict[str, Any], -) -> None: # pylint: disable=redefined-outer-name - """Test basic access in ConfigWrapperDict.""" - cfg = ConfigWrapperDict(sample_dict) - assert cfg["foo"] == "bar" # nosec - nested = cfg["nested"] - assert isinstance(nested, ConfigWrapper) # nosec, narrowing for typchecker - assert nested["key"] == "value" # nosec - - -def test_dict_get_method( - sample_dict: dict[str, Any], -) -> None: # pylint: disable=redefined-outer-name - """Test the get method in ConfigWrapperDict.""" - cfg = ConfigWrapperDict(sample_dict) - assert cfg.get("foo") == "bar" # nosec - assert cfg.get("nonexistent", "default") == "default" # nosec - - -def test_dict_override_env( - monkeypatch: Any, -) -> None: # pylint: disable=redefined-outer-name - """Test environment variable override in ConfigWrapperDict.""" - monkeypatch.setenv("FOO_BAR", "env_value") - cfg = ConfigWrapperDict({"bar": "original"}, path="foo") - assert cfg["bar"] == "env_value" # nosec - - -def test_list_override_env(monkeypatch: Any, sample_dict: dict[str, Any]) -> None: # pylint: disable=redefined-outer-name - """Test environment variable override in ConfigWrapperList.""" - monkeypatch.setenv("LIST_FOO_BAR", "baz") - cfg = ConfigWrapper.from_data(sample_dict) - assert isinstance(cfg, ConfigWrapper) # nosec - cfg_list = cfg["list"] - assert isinstance(cfg_list, ConfigWrapper) # nosec - cfg_list_foo = cfg_list[0] - assert isinstance(cfg_list_foo, ConfigWrapper) # nosec - assert "bar" in cfg_list_foo # nosec - assert cfg_list_foo["bar"] == "baz" # nosec - - -def test_dict_override_secret( - tmp_path: Path, -) -> None: # pylint: disable=redefined-outer-name - """Test secret file override in ConfigWrapperDict.""" - secret_file = tmp_path / "foo_secret" - secret_file.write_text("secret_value") - # Patch /run/secrets to tmp_path - monkeypatch = pytest.MonkeyPatch() - original_exists = Path.exists - monkeypatch.setattr(Path, "exists", lambda self: original_exists(tmp_path / self.name)) - original_read_text = Path.read_text - monkeypatch.setattr( - Path, - "read_text", - lambda self, encoding=None: original_read_text(tmp_path / self.name, encoding), - ) - cfg = ConfigWrapperDict({}, path="foo") - # pylint: disable=protected-access - assert cfg._override_key_access("secret") == "secret_value" # nosec - monkeypatch.undo() - - -def test_dict_iteration_and_len(monkeypatch: Any) -> None: - """Test iteration and length in ConfigWrapperDict.""" - monkeypatch.setenv("FOO_NEWKEY", "val") - cfg = ConfigWrapperDict({"bar": "baz"}, path="foo") - keys = set(cfg) - assert "bar" in keys # nosec - assert "newkey" in keys # nosec - assert len(cfg) == 2 # nosec # noqa: PLR2004 - items = dict(cfg.items()) - assert items["bar"] == "baz" # nosec - assert items["newkey"] == "val" # nosec - - -def test_list_access_and_items( - sample_list: ListType, -) -> None: # pylint: disable=redefined-outer-name - """Test access and items in ConfigWrapperList.""" - cfg = ConfigWrapperList(sample_list) - assert cfg[0] == "a" # nosec - item_2 = cfg[2] - assert isinstance(item_2, ConfigWrapper) # nosec, narrowing for typchecker - assert item_2["value"] == 42 # nosec # noqa: PLR2004 - keys = list(cfg) - assert keys == [0, 1, 2] # nosec - items = dict(cfg.items()) - assert items[0] == "a" # nosec - item_2 = cfg[2] - assert isinstance(item_2, ConfigWrapper) # nosec, narrowing for typchecker - assert item_2["value"] == 42 # nosec # noqa: PLR2004 - assert len(cfg) == 3 # nosec # noqa: PLR2004 - - -# New tests for primitive type support - - -def test_parse_primitive_value_bool_true() -> None: - """Test parsing boolean true value.""" - assert ConfigWrapper._parse_primitive_value("true") is True # pylint: disable=protected-access # nosec - assert ConfigWrapper._parse_primitive_value("True") is True # pylint: disable=protected-access # nosec - assert ConfigWrapper._parse_primitive_value("TRUE") is True # pylint: disable=protected-access # nosec - - -def test_parse_primitive_value_bool_false() -> None: - """Test parsing boolean false value.""" - assert ConfigWrapper._parse_primitive_value("false") is False # pylint: disable=protected-access # nosec - assert ConfigWrapper._parse_primitive_value("False") is False # pylint: disable=protected-access # nosec - assert ConfigWrapper._parse_primitive_value("FALSE") is False # pylint: disable=protected-access # nosec - - -def test_parse_primitive_value_int() -> None: - """Test parsing integer values.""" - assert ConfigWrapper._parse_primitive_value("42") == 42 # pylint: disable=protected-access # nosec # noqa: PLR2004 - assert ConfigWrapper._parse_primitive_value("-42") == -42 # pylint: disable=protected-access # nosec # noqa: PLR2004 - assert ConfigWrapper._parse_primitive_value("0") == 0 # pylint: disable=protected-access # nosec - - -def test_parse_primitive_value_float() -> None: - """Test parsing float values.""" - assert ConfigWrapper._parse_primitive_value("3.14") == 3.14 # pylint: disable=protected-access # nosec # noqa: PLR2004 - assert ConfigWrapper._parse_primitive_value("-3.14") == -3.14 # pylint: disable=protected-access # nosec # noqa: PLR2004 - assert ConfigWrapper._parse_primitive_value("0.5") == 0.5 # pylint: disable=protected-access # nosec # noqa: PLR2004 - - -def test_parse_primitive_value_string() -> None: - """Test parsing string values that are not primitives.""" - assert ConfigWrapper._parse_primitive_value("hello") == "hello" # pylint: disable=protected-access # nosec - assert ConfigWrapper._parse_primitive_value("3.14.15") == "3.14.15" # pylint: disable=protected-access # nosec - assert ConfigWrapper._parse_primitive_value("notabool") == "notabool" # pylint: disable=protected-access # nosec - - -def test_parse_primitive_value_empty_string() -> None: - """Test parsing empty string returns None.""" - assert ConfigWrapper._parse_primitive_value("") is None # pylint: disable=protected-access # nosec - - -def test_override_key_access_int_env(monkeypatch: Any) -> None: - """Test environment variable override with integer value.""" - monkeypatch.setenv("FOO_PORT", "8080") - cfg = ConfigWrapperDict({"port": 3000}, path="foo") - result = cfg["port"] - assert result == 8080 # nosec # noqa: PLR2004 - assert isinstance(result, int) # nosec - - -def test_override_key_access_float_env(monkeypatch: Any) -> None: - """Test environment variable override with float value.""" - monkeypatch.setenv("FOO_TIMEOUT", "3.5") - cfg = ConfigWrapperDict({"timeout": 1.0}, path="foo") - result = cfg["timeout"] - assert result == 3.5 # nosec # noqa: PLR2004 - assert isinstance(result, float) # nosec - - -def test_override_key_access_bool_env(monkeypatch: Any) -> None: - """Test environment variable override with boolean value.""" - monkeypatch.setenv("FOO_DEBUG", "true") - cfg = ConfigWrapperDict({"debug": False}, path="foo") - result = cfg["debug"] - assert result is True # nosec - assert isinstance(result, bool) # nosec - - -def test_override_key_access_bool_false_env(monkeypatch: Any) -> None: - """Test environment variable override with boolean false value.""" - monkeypatch.setenv("FOO_ENABLED", "false") - cfg = ConfigWrapperDict({"enabled": True}, path="foo") - result = cfg["enabled"] - assert result is False # nosec - assert isinstance(result, bool) # nosec - - -def test_override_key_access_none_env(monkeypatch: Any) -> None: - """Test environment variable override with empty string returns default value.""" - monkeypatch.setenv("FOO_EMPTY", "") - cfg = ConfigWrapperDict({"empty": "default"}, path="foo") - result = cfg["empty"] - # Empty string from env variable is parsed to None, so the default value is used - assert result == "default" # nosec - - -def test_parse_primitive_value_for_none_case() -> None: - """Test that explicitly None values are preserved in YAML config.""" - cfg = ConfigWrapperDict({"nullable": None}, path="foo") - result = cfg["nullable"] - assert result is None # nosec - - -def test_override_key_access_string_env(monkeypatch: Any) -> None: - """Test environment variable override with string value.""" - monkeypatch.setenv("FOO_NAME", "John") - cfg = ConfigWrapperDict({"name": "default"}, path="foo") - result = cfg["name"] - assert result == "John" # nosec - assert isinstance(result, str) # nosec - - -def test_dict_with_primitive_types() -> None: - """Test ConfigWrapperDict with various primitive types.""" - data: dict[str, Any] = { - "string": "hello", - "integer": 42, - "float": 3.14, - "bool": True, - } - cfg = ConfigWrapperDict(data) - assert cfg["string"] == "hello" # nosec - assert cfg["integer"] == 42 # nosec # noqa: PLR2004 - assert cfg["float"] == 3.14 # nosec # noqa: PLR2004 - assert cfg["bool"] is True # nosec - - -def test_unwrap_with_primitive_types() -> None: - """Test unwrapping ConfigWrapper with primitive types.""" - data: dict[str, Any] = { - "string": "hello", - "integer": 42, - "float": 3.14, - "bool": True, - "null": None, - } - cfg = ConfigWrapper.from_data(data) - unwrapped = cfg.unwrap() - assert isinstance(unwrapped, dict) # nosec - assert unwrapped["string"] == "hello" # nosec - assert unwrapped["integer"] == 42 # nosec # noqa: PLR2004 - assert unwrapped["float"] == 3.14 # nosec # noqa: PLR2004 - assert unwrapped["bool"] is True # nosec - assert unwrapped["null"] is None # nosec - - -def test_nested_dict_with_primitives() -> None: - """Test nested dictionaries with primitive types.""" - data: dict[str, Any] = { - "nested": { - "port": 8080, - "timeout": 5.5, - "debug": False, - "name": "app", - } - } - cfg = ConfigWrapper.from_data(data) # type: ignore[arg-type] - nested = cfg["nested"] - assert isinstance(nested, ConfigWrapper) # nosec - assert nested["port"] == 8080 # nosec # noqa: PLR2004 - assert nested["timeout"] == 5.5 # nosec # noqa: PLR2004 - assert nested["debug"] is False # nosec - assert nested["name"] == "app" # nosec - - -def test_list_with_primitive_types() -> None: - """Test ConfigWrapperList with primitive types.""" - data: ListType = ["string", 42, 3.14, True, None] - cfg = ConfigWrapperList(data) - assert cfg[0] == "string" # nosec - assert cfg[1] == 42 # nosec # noqa: PLR2004 - assert cfg[2] == 3.14 # nosec # noqa: PLR2004 - assert cfg[3] is True # nosec - assert cfg[4] is None # nosec diff --git a/middleware/shared/tests/unit/test_logging.py b/middleware/shared/tests/unit/test_logging.py deleted file mode 100644 index c6f3eb0..0000000 --- a/middleware/shared/tests/unit/test_logging.py +++ /dev/null @@ -1,92 +0,0 @@ -"""Tests for logging configuration module.""" - -import logging -from unittest.mock import MagicMock, patch - -from middleware.shared.config.logging import configure_logging - - -class TestConfigureLogging: - """Test suite for configure_logging function.""" - - def test_configure_logging_with_existing_handlers(self) -> None: - """Test configure_logging with existing handlers.""" - # Setup - root_logger = logging.getLogger() - mock_handler = MagicMock() - root_logger.handlers = [mock_handler] - original_level = root_logger.level - - try: - # Execute - configure_logging("DEBUG") - - # Assert - logging converts strings to level ints - mock_handler.setLevel.assert_called_with("DEBUG") - assert root_logger.level == logging.DEBUG - finally: - root_logger.level = original_level - root_logger.handlers = [] - - def test_configure_logging_with_multiple_handlers(self) -> None: - """Test configure_logging with multiple existing handlers.""" - # Setup - root_logger = logging.getLogger() - mock_handler1 = MagicMock() - mock_handler2 = MagicMock() - root_logger.handlers = [mock_handler1, mock_handler2] - original_level = root_logger.level - - try: - # Execute - configure_logging("INFO") - - # Assert - mock_handler1.setLevel.assert_called_with("INFO") - mock_handler2.setLevel.assert_called_with("INFO") - assert root_logger.level == logging.INFO - finally: - root_logger.level = original_level - root_logger.handlers = [] - - def test_configure_logging_without_existing_handlers(self) -> None: - """Test configure_logging when no handlers exist.""" - # Setup - root_logger = logging.getLogger() - original_handlers = root_logger.handlers.copy() - root_logger.handlers = [] - - try: - # Execute - with patch("logging.basicConfig") as mock_basic_config: - configure_logging("WARNING") - - # Assert - mock_basic_config.assert_called_once_with(level="WARNING") - finally: - root_logger.handlers = original_handlers - - def test_configure_logging_different_levels(self) -> None: - """Test configure_logging with different log levels.""" - # Setup - root_logger = logging.getLogger() - mock_handler = MagicMock() - root_logger.handlers = [mock_handler] - original_level = root_logger.level - - try: - # Test each log level - level_map = { - "DEBUG": logging.DEBUG, - "INFO": logging.INFO, - "WARNING": logging.WARNING, - "ERROR": logging.ERROR, - "CRITICAL": logging.CRITICAL, - } - for level_str, level_int in level_map.items(): - configure_logging(level_str) # type: ignore[arg-type] - mock_handler.setLevel.assert_called_with(level_str) - assert root_logger.level == level_int - finally: - root_logger.level = original_level - root_logger.handlers = [] diff --git a/middleware/sql_to_arc/pyproject.toml b/middleware/sql_to_arc/pyproject.toml index 5cea5d9..75b8ad6 100644 --- a/middleware/sql_to_arc/pyproject.toml +++ b/middleware/sql_to_arc/pyproject.toml @@ -13,8 +13,8 @@ dependencies = [ ] [tool.uv.sources] -shared = { workspace = true } -api_client = { workspace = true } +api_client = { git = "https://github.com/fairagro/m4.2_advanced_middleware_api.git", branch = "feature/split_off_client", subdirectory = "middleware/api_client" } +shared = { git = "https://github.com/fairagro/m4.2_advanced_middleware_api.git", branch = "feature/split_off_client", subdirectory = "middleware/shared" } [tool.hatch.build.targets.wheel] packages = ["src/middleware"] diff --git a/middleware/tools/README.md b/middleware/tools/README.md deleted file mode 100644 index 84839ff..0000000 --- a/middleware/tools/README.md +++ /dev/null @@ -1,92 +0,0 @@ -# ARC Conversion Tools - -A collection of utilities for converting between ARC (Annotated Research Context) and RO-Crate JSON formats. - -## Overview - -This package provides command-line tools to: - -- Convert ARC files to RO-Crate JSON format -- Convert RO-Crate JSON files back to ARC format - -These tools are useful for testing, debugging, and data interchange between ARC-based workflows and RO-Crate consumers. - -## Installation - -```bash -pip install -e . -``` - -Or using uv: - -```bash -uv pip install -e . -``` - -## Tools - -### arc2rocrate.py - -Converts an ARC file to RO-Crate JSON format. - -**Usage:** - -```bash -python arc2rocrate.py -``` - -**Example:** - -```bash -python arc2rocrate.py my_research.arc my_research_rocrate.json -``` - -**Features:** - -- Loads ARC from file -- Exports as RO-Crate JSON string -- Measures and reports conversion time -- Writes JSON to output file - -### rocrate2arc.py - -Converts a RO-Crate JSON file to ARC format. - -**Usage:** - -```bash -python rocrate2arc.py -``` - -**Example:** - -```bash -python rocrate2arc.py my_research_rocrate.json restored_research.arc -``` - -**Features:** - -- Reads RO-Crate JSON from file -- Converts to ARC format -- Includes profiling for performance analysis -- Generates performance statistics (`profile.stats`) - -## Performance Profiling - -The `rocrate2arc.py` tool includes built-in profiling that generates detailed performance statistics. After running the conversion, check the `profile.stats` file and the console output for the top 20 most time-consuming operations. - -## Dependencies - -- `arctrl>=3.0.0b15` - ARC and RO-Crate conversion library - -## Requirements - -- Python >= 3.12 - -## Development - -This package is part of the FAIRagro Advanced Middleware project and is used for testing and development of ARC conversion workflows. - -## License - -See the main project LICENSE file. diff --git a/middleware/tools/arc2rocrate.py b/middleware/tools/arc2rocrate.py deleted file mode 100644 index 32a054c..0000000 --- a/middleware/tools/arc2rocrate.py +++ /dev/null @@ -1,33 +0,0 @@ -"""Tool to convert ARC files to RO-Crate JSON format.""" - -import time - -from arctrl import ARC # type: ignore[import-untyped] - - -def arc_to_rocrate_json(arc_path: str, rocrate_output_path: str) -> None: - """Convert an ARC file to RO-Crate JSON format and save to a file.""" - # Lade ARC aus Datei - start = time.perf_counter() - arc = ARC.load(arc_path) - end = time.perf_counter() - print(f"Loading ARC took {end - start:.2f} seconds") - - # Exportiere als RO-Crate JSON-Objekt - start = time.perf_counter() - rocrate = arc.ToROCrateJsonString() - end = time.perf_counter() - print(f"Converting ARC to RO-Crate JSON took {end - start:.2f} seconds") - - # Schreibe die JSON-Repräsentation in eine Datei - with open(rocrate_output_path, "w", encoding="utf-8") as f: - f.write(rocrate) - - -if __name__ == "__main__": - import sys - - if len(sys.argv) != 3: # noqa: PLR2004 - print("Usage: python arc2rocrate.py ") - sys.exit(1) - arc_to_rocrate_json(sys.argv[1], sys.argv[2]) diff --git a/middleware/tools/pyproject.toml b/middleware/tools/pyproject.toml deleted file mode 100644 index 11af7f8..0000000 --- a/middleware/tools/pyproject.toml +++ /dev/null @@ -1,16 +0,0 @@ -[project] -name = "tools" -version = "0.0.0" -description = "ARC conversion tools" -readme = "README.md" -requires-python = ">=3.12" -dependencies = [ - "arctrl>=3.0.0b15", -] - -[tool.hatch.build.targets.wheel] -packages = ["src/middleware"] - -[build-system] -requires = ["hatchling"] -build-backend = "hatchling.build" diff --git a/middleware/tools/rocrate2arc.py b/middleware/tools/rocrate2arc.py deleted file mode 100644 index e9c8035..0000000 --- a/middleware/tools/rocrate2arc.py +++ /dev/null @@ -1,29 +0,0 @@ -"""Tool to convert RO-Crate JSON files to ARC format.""" - -import cProfile -import pstats - -from arctrl import ARC # type: ignore[import-untyped] - - -def rocrate_json_to_arc(rocrate_input_path: str, arc_path: str) -> None: - """Convert a RO-Crate JSON file to ARC format and save to a file.""" - with open(rocrate_input_path, encoding="utf-8") as f: - rocrate_json = f.read() - arc = ARC.from_rocrate_json_string(rocrate_json) - arc.Write(arc_path) - - -if __name__ == "__main__": - import sys - - if len(sys.argv) != 3: # noqa: PLR2004 - print("Usage: python rocrate2arc.py ") - sys.exit(1) - - # Profiling - cProfile.run("rocrate_json_to_arc(sys.argv[1], sys.argv[2])", "profile.stats") - - # Stats laden - stats = pstats.Stats("profile.stats") - stats.sort_stats(pstats.SortKey.TIME).print_stats(20) diff --git a/pyproject.toml b/pyproject.toml index 654234d..7db9904 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,10 +5,7 @@ description = "The FAIRagro advanced middleware" readme = "README.md" requires-python = ">=3.12" dependencies = [ - "api", - "api_client", "sql_to_arc", - "shared", ] [dependency-groups] @@ -29,18 +26,11 @@ dev = [ ] [tool.uv.sources] -api = { workspace = true } -api_client = { workspace = true } sql_to_arc = { workspace = true } -shared = { workspace = true } [tool.uv.workspace] members = [ - "middleware/api", - "middleware/api_client", "middleware/sql_to_arc", - "middleware/shared", - "middleware/tools", ] [tool.ruff] @@ -105,7 +95,7 @@ disallow_untyped_defs = true disallow_incomplete_defs = true explicit_package_bases = true namespace_packages = true -mypy_path = "middleware/api/src:middleware/api_client/src:middleware/shared/src:middleware/sql_to_arc/src" +mypy_path = "middleware/sql_to_arc/src" # Exclude directories from type checking exclude = [ diff --git a/ro_crates/edaphobase.json b/ro_crates/edaphobase.json deleted file mode 100644 index 0894642..0000000 --- a/ro_crates/edaphobase.json +++ /dev/null @@ -1,45 +0,0 @@ -{ - "@context": [ - "https://w3id.org/ro/crate/1.2/context", - { - "Sample": "https://bioschemas.org/Sample", - "LabProtocol": "https://bioschemas.org/LabProtocol", - "LabProcess": "https://bioschemas.org/LabProcess", - "computationalTool": "https://bioschemas.org/properties/computationalTool", - "labEquipment": "https://bioschemas.org/properties/labEquipment", - "reagent": "https://bioschemas.org/properties/reagent", - "intendedUse": "https://bioschemas.org/properties/intendedUse", - "executesLabProtocol": "https://bioschemas.org/properties/executesLabProtocol", - "parameterValue": "https://bioschemas.org/properties/parameterValue", - "columnIndex": "https://w3id.org/ro/terms/arc#columnIndex" - } - ], - "@graph": [ - { - "@id": "#LICENSE", - "@type": "CreativeWork", - "text": "ALL RIGHTS RESERVED BY THE AUTHORS" - }, - { - "@id": "./", - "@type": "Dataset", - "additionalType": "Investigation", - "identifier": "-1", - "datePublished": "2025-12-09T13:41:46.875", - "name": "Decker, P. et al.. BODENTIER hoch 4 \u2013 Onlineportal mit App zum Erleben, Erkennen, Erfassen und Erforschen (www.bodentierhochvier.de) [Data set].", - "license": { - "@id": "#LICENSE" - } - }, - { - "@id": "ro-crate-metadata.json", - "@type": "CreativeWork", - "conformsTo": { - "@id": "https://w3id.org/ro/crate/1.2" - }, - "about": { - "@id": "./" - } - } - ] -} diff --git a/ro_crates/minimal.json b/ro_crates/minimal.json deleted file mode 100644 index d02303b..0000000 --- a/ro_crates/minimal.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "@context": "https://w3id.org/ro/crate/1.1/context", - "@graph": [ - { - "@id": "./", - "@type": "Dataset", - "additionalType": "Investigation", - "identifier": "Test" - }, - { - "@id": "ro-crate-metadata.json", - "@type": "CreativeWork", - "conformsTo": { "@id": "https://w3id.org/ro/crate/1.1" }, - "about": { "@id": "./" } - } - ] -} diff --git a/ro_crates/sample.json b/ro_crates/sample.json deleted file mode 100644 index 00b4312..0000000 --- a/ro_crates/sample.json +++ /dev/null @@ -1,819 +0,0 @@ -{ - "@context": [ - "https://w3id.org/ro/crate/1.1/context", - { - "Sample": "https://bioschemas.org/Sample", - "additionalProperty": "http://schema.org/additionalProperty", - "intendedUse": "https://bioschemas.org/intendedUse", - "computationalTool": "https://bioschemas.org/computationalTool", - "labEquipment": "https://bioschemas.org/labEquipment", - "reagent": "https://bioschemas.org/reagent", - "LabProtocol": "https://bioschemas.org/LabProtocol", - "executesLabProtocol": "https://bioschemas.org/executesLabProtocol", - "parameterValue": "https://bioschemas.org/parameterValue", - "LabProcess": "https://bioschemas.org/LabProcess", - "measurementMethod": "http://schema.org/measurementMethod" - } - ], - "@graph": [ - { - "@id": "https://bioregistry.io/EFO:EFO_0009736", - "@type": "DefinedTerm", - "name": "principal investigator", - "termCode": "https://bioregistry.io/EFO:EFO_0009736" - }, - { - "@id": "#Person_Jasmine_Beetroot", - "@type": "Person", - "givenName": "Jasmine", - "familyName": "Beetroot", - "jobTitle": { "@id": "https://bioregistry.io/EFO:EFO_0009736" } - }, - { - "@id": "https://bioregistry.io/NCIT:C25936", - "@type": "DefinedTerm", - "name": "Investigator", - "termCode": "https://bioregistry.io/NCIT:C25936" - }, - { - "@id": "#Person_Viola_Canina", - "@type": "Person", - "givenName": "Viola", - "familyName": "Canina", - "jobTitle": { "@id": "https://bioregistry.io/NCIT:C25936" } - }, - { - "@id": "https://bioregistry.io/NCIT:C100128", - "@type": "DefinedTerm", - "name": "Partner", - "termCode": "https://bioregistry.io/NCIT:C100128" - }, - { - "@id": "#Person_Oliver_Sage", - "@type": "Person", - "givenName": "Oliver", - "familyName": "Sage", - "jobTitle": { "@id": "https://bioregistry.io/NCIT:C100128" } - }, - { "@id": "#Person_a", "@type": "Person", "givenName": "a" }, - { - "@id": "#CharacteristicValue_organism_Arabidopsis_thaliana", - "@type": "PropertyValue", - "additionalType": "CharacteristicValue", - "columnIndex": "0", - "name": "organism", - "value": "Arabidopsis thaliana", - "propertyID": "https://bioregistry.io/OBI:0100026", - "valueReference": "http://purl.obolibrary.org/obo/NCBITAXON_3702" - }, - { - "@id": "#CharacteristicValue_growth_time_4", - "@type": "PropertyValue", - "additionalType": "CharacteristicValue", - "columnIndex": "1", - "name": "growth time", - "value": "4", - "propertyID": "http://purl.org/nfdi4plants/ontology/dpbo/DPBO_0000100", - "unitCode": "https://bioregistry.io/UO:0000033", - "unitText": "day" - }, - { - "@id": "#CharacteristicValue_Light_intensity", - "@type": "PropertyValue", - "additionalType": "CharacteristicValue", - "columnIndex": "2", - "name": "Light intensity", - "propertyID": "http://purl.obolibrary.org/obo/MIAPPE_0101", - "unitCode": "https://bioregistry.io/UO:0000156", - "unitText": "einstein per square meter per second" - }, - { - "@id": "#Source_Cold1", - "@type": "Sample", - "additionalType": "Source", - "name": "Cold1", - "additionalProperty": [ - { "@id": "#CharacteristicValue_organism_Arabidopsis_thaliana" }, - { "@id": "#CharacteristicValue_growth_time_4" }, - { "@id": "#CharacteristicValue_Light_intensity" } - ] - }, - { - "@id": "#FactorValue_temperature_day_6", - "@type": "PropertyValue", - "additionalType": "FactorValue", - "columnIndex": "3", - "name": "temperature day", - "value": "6", - "propertyID": "http://purl.org/nfdi4plants/ontology/dpbo/DPBO_0000007", - "unitCode": "https://bioregistry.io/NCIT:C42559", - "unitText": "Degree Celsius" - }, - { - "@id": "#Sample_Cold1_leaf", - "@type": "Sample", - "additionalType": "Sample", - "name": "Cold1_leaf", - "additionalProperty": { "@id": "#FactorValue_temperature_day_6" } - }, - { - "@id": "https://bioregistry.io/NCIT:C41139", - "@type": "DefinedTerm", - "name": "Meter", - "termCode": "https://bioregistry.io/NCIT:C41139" - }, - { - "@id": "#Protocol_./studies/AthalianaColdStress/protocols/growth_protocol.md_NewTable0", - "@type": "LabProtocol", - "name": "./studies/AthalianaColdStress/protocols/growth_protocol.md", - "intendedUse": [ - { "@id": "https://bioregistry.io/NCIT:C71177" }, - { "@id": "https://bioregistry.io/NCIT:C41139" } - ] - }, - { - "@id": "#ParameterValue_Test_Test", - "@type": "PropertyValue", - "additionalType": "ParameterValue", - "columnIndex": ["5", "4"], - "name": "Test", - "value": "Test", - "propertyID": "https://bioregistry.io/NCIT:C47891", - "valueReference": "https://bioregistry.io/NCIT:C47891" - }, - { - "@id": "#ParameterValue_Growth_Test", - "@type": "PropertyValue", - "additionalType": "ParameterValue", - "columnIndex": ["7", "6", "8"], - "name": "Growth", - "value": "Test", - "propertyID": "https://bioregistry.io/NCIT:C64379", - "valueReference": "https://bioregistry.io/NCIT:C47891" - }, - { - "@id": "#ParameterValue_Growth", - "@type": "PropertyValue", - "additionalType": "ParameterValue", - "columnIndex": ["10", "9"], - "name": "Growth", - "propertyID": "https://bioregistry.io/NCIT:C64379", - "unitCode": "https://bioregistry.io/NCIT:C71177", - "unitText": "Kilometer" - }, - { - "@id": "#Process_NewTable0_0", - "@type": "LabProcess", - "name": "NewTable0_0", - "agent": { "@id": "#Person_a" }, - "object": { "@id": "#Source_Cold1" }, - "result": { "@id": "#Sample_Cold1_leaf" }, - "executesLabProtocol": { - "@id": "#Protocol_./studies/AthalianaColdStress/protocols/growth_protocol.md_NewTable0" - }, - "parameterValue": [ - { "@id": "#ParameterValue_Test_Test" }, - { "@id": "#ParameterValue_Test_Test" }, - { "@id": "#ParameterValue_Growth_Test" }, - { "@id": "#ParameterValue_Growth_Test" }, - { "@id": "#ParameterValue_Growth_Test" }, - { "@id": "#ParameterValue_Growth" }, - { "@id": "#ParameterValue_Growth" } - ] - }, - { "@id": "#Person_b", "@type": "Person", "givenName": "b" }, - { - "@id": "#Source_Cold2", - "@type": "Sample", - "additionalType": "Source", - "name": "Cold2", - "additionalProperty": [ - { "@id": "#CharacteristicValue_organism_Arabidopsis_thaliana" }, - { "@id": "#CharacteristicValue_growth_time_4" }, - { "@id": "#CharacteristicValue_Light_intensity" } - ] - }, - { - "@id": "#Sample_Cold2_leaf", - "@type": "Sample", - "additionalType": "Sample", - "name": "Cold2_leaf", - "additionalProperty": { "@id": "#FactorValue_temperature_day_6" } - }, - { - "@id": "https://bioregistry.io/NCIT:C71177", - "@type": "DefinedTerm", - "name": "Kilometer", - "termCode": "https://bioregistry.io/NCIT:C71177" - }, - { - "@id": "#Process_NewTable0_1", - "@type": "LabProcess", - "name": "NewTable0_1", - "agent": { "@id": "#Person_b" }, - "object": { "@id": "#Source_Cold2" }, - "result": { "@id": "#Sample_Cold2_leaf" }, - "executesLabProtocol": { - "@id": "#Protocol_./studies/AthalianaColdStress/protocols/growth_protocol.md_NewTable0" - }, - "parameterValue": [ - { "@id": "#ParameterValue_Test_Test" }, - { "@id": "#ParameterValue_Test_Test" }, - { "@id": "#ParameterValue_Growth_Test" }, - { "@id": "#ParameterValue_Growth_Test" }, - { "@id": "#ParameterValue_Growth_Test" }, - { "@id": "#ParameterValue_Growth" }, - { "@id": "#ParameterValue_Growth" } - ] - }, - { "@id": "#Person_", "@type": "Person", "givenName": "" }, - { - "@id": "#Source_Cold3", - "@type": "Sample", - "additionalType": "Source", - "name": "Cold3", - "additionalProperty": [ - { "@id": "#CharacteristicValue_organism_Arabidopsis_thaliana" }, - { "@id": "#CharacteristicValue_growth_time_4" }, - { "@id": "#CharacteristicValue_Light_intensity" } - ] - }, - { - "@id": "#Sample_Cold3_leaf", - "@type": "Sample", - "additionalType": "Sample", - "name": "Cold3_leaf", - "additionalProperty": { "@id": "#FactorValue_temperature_day_6" } - }, - { - "@id": "#ParameterValue_Growth_5", - "@type": "PropertyValue", - "additionalType": "ParameterValue", - "columnIndex": "10", - "name": "Growth", - "value": "5", - "propertyID": "https://bioregistry.io/NCIT:C64379", - "unitCode": "https://bioregistry.io/NCIT:C71177", - "unitText": "Kilometer" - }, - { - "@id": "#Process_NewTable0_2", - "@type": "LabProcess", - "name": "NewTable0_2", - "agent": { "@id": "#Person_" }, - "object": { "@id": "#Source_Cold3" }, - "result": { "@id": "#Sample_Cold3_leaf" }, - "executesLabProtocol": { - "@id": "#Protocol_./studies/AthalianaColdStress/protocols/growth_protocol.md_NewTable0" - }, - "parameterValue": [ - { "@id": "#ParameterValue_Test_Test" }, - { "@id": "#ParameterValue_Test_Test" }, - { "@id": "#ParameterValue_Growth_Test" }, - { "@id": "#ParameterValue_Growth_Test" }, - { "@id": "#ParameterValue_Growth_Test" }, - { "@id": "#ParameterValue_Growth" }, - { "@id": "#ParameterValue_Growth_5" } - ] - }, - { - "@id": "#Source_RT1", - "@type": "Sample", - "additionalType": "Source", - "name": "RT1", - "additionalProperty": [ - { "@id": "#CharacteristicValue_organism_Arabidopsis_thaliana" }, - { "@id": "#CharacteristicValue_growth_time_4" }, - { "@id": "#CharacteristicValue_Light_intensity" } - ] - }, - { - "@id": "#FactorValue_temperature_day_25", - "@type": "PropertyValue", - "additionalType": "FactorValue", - "columnIndex": "3", - "name": "temperature day", - "value": "25", - "propertyID": "http://purl.org/nfdi4plants/ontology/dpbo/DPBO_0000007", - "unitCode": "https://bioregistry.io/NCIT:C42559", - "unitText": "Degree Celsius" - }, - { - "@id": "#Sample_RT1_leaf", - "@type": "Sample", - "additionalType": "Sample", - "name": "RT1_leaf", - "additionalProperty": { "@id": "#FactorValue_temperature_day_25" } - }, - { - "@id": "#Process_NewTable0_3", - "@type": "LabProcess", - "name": "NewTable0_3", - "agent": { "@id": "#Person_" }, - "object": { "@id": "#Source_RT1" }, - "result": { "@id": "#Sample_RT1_leaf" }, - "executesLabProtocol": { - "@id": "#Protocol_./studies/AthalianaColdStress/protocols/growth_protocol.md_NewTable0" - }, - "parameterValue": [ - { "@id": "#ParameterValue_Test_Test" }, - { "@id": "#ParameterValue_Test_Test" }, - { "@id": "#ParameterValue_Growth_Test" }, - { "@id": "#ParameterValue_Growth_Test" }, - { "@id": "#ParameterValue_Growth_Test" }, - { "@id": "#ParameterValue_Growth" }, - { "@id": "#ParameterValue_Growth" } - ] - }, - { - "@id": "#Source_RT2", - "@type": "Sample", - "additionalType": "Source", - "name": "RT2", - "additionalProperty": [ - { "@id": "#CharacteristicValue_organism_Arabidopsis_thaliana" }, - { "@id": "#CharacteristicValue_growth_time_4" }, - { "@id": "#CharacteristicValue_Light_intensity" } - ] - }, - { - "@id": "#Sample_RT2_leaf", - "@type": "Sample", - "additionalType": "Sample", - "name": "RT2_leaf", - "additionalProperty": { "@id": "#FactorValue_temperature_day_25" } - }, - { - "@id": "#Process_NewTable0_4", - "@type": "LabProcess", - "name": "NewTable0_4", - "agent": { "@id": "#Person_" }, - "object": { "@id": "#Source_RT2" }, - "result": { "@id": "#Sample_RT2_leaf" }, - "executesLabProtocol": { - "@id": "#Protocol_./studies/AthalianaColdStress/protocols/growth_protocol.md_NewTable0" - }, - "parameterValue": [ - { "@id": "#ParameterValue_Test_Test" }, - { "@id": "#ParameterValue_Test_Test" }, - { "@id": "#ParameterValue_Growth_Test" }, - { "@id": "#ParameterValue_Growth_Test" }, - { "@id": "#ParameterValue_Growth_Test" }, - { "@id": "#ParameterValue_Growth" }, - { "@id": "#ParameterValue_Growth" } - ] - }, - { - "@id": "#Source_RT3", - "@type": "Sample", - "additionalType": "Source", - "name": "RT3", - "additionalProperty": [ - { "@id": "#CharacteristicValue_organism_Arabidopsis_thaliana" }, - { "@id": "#CharacteristicValue_growth_time_4" }, - { "@id": "#CharacteristicValue_Light_intensity" } - ] - }, - { - "@id": "#Sample_RT3_leaf", - "@type": "Sample", - "additionalType": "Sample", - "name": "RT3_leaf", - "additionalProperty": { "@id": "#FactorValue_temperature_day_25" } - }, - { - "@id": "#Process_NewTable0_5", - "@type": "LabProcess", - "name": "NewTable0_5", - "agent": { "@id": "#Person_" }, - "object": { "@id": "#Source_RT3" }, - "result": { "@id": "#Sample_RT3_leaf" }, - "executesLabProtocol": { - "@id": "#Protocol_./studies/AthalianaColdStress/protocols/growth_protocol.md_NewTable0" - }, - "parameterValue": [ - { "@id": "#ParameterValue_Test_Test" }, - { "@id": "#ParameterValue_Test_Test" }, - { "@id": "#ParameterValue_Growth_Test" }, - { "@id": "#ParameterValue_Growth_Test" }, - { "@id": "#ParameterValue_Growth_Test" }, - { "@id": "#ParameterValue_Growth" }, - { "@id": "#ParameterValue_Growth" } - ] - }, - { - "@id": "studies/AthalianaColdStress/", - "@type": "Dataset", - "additionalType": "Study", - "identifier": "AthalianaColdStress", - "creator": [ - { "@id": "#Person_Jasmine_Beetroot" }, - { "@id": "#Person_Viola_Canina" }, - { "@id": "#Person_Oliver_Sage" } - ], - "dateModified": "2025-08-11T15:44:37.467", - "description": "Cold acclimation analysis of Arabidopsis leaf tissue", - "hasPart": [], - "about": [ - { "@id": "#Process_NewTable0_0" }, - { "@id": "#Process_NewTable0_1" }, - { "@id": "#Process_NewTable0_2" }, - { "@id": "#Process_NewTable0_3" }, - { "@id": "#Process_NewTable0_4" }, - { "@id": "#Process_NewTable0_5" } - ] - }, - { - "@id": "assays/Proteomics_MS/", - "@type": "Dataset", - "additionalType": "Assay", - "identifier": "Proteomics_MS" - }, - { - "@id": "assays/SugarMeasurement/dataset/./assays/SugarMeasurement/dataset/sugar_result.csv", - "@type": "File", - "name": "assays/SugarMeasurement/dataset/./assays/SugarMeasurement/dataset/sugar_result.csv" - }, - { - "@id": "#Sample_Cold1_sugar-ext", - "@type": "Sample", - "additionalType": "Sample", - "name": "Cold1_sugar-ext" - }, - { "@id": "#Protocol_SugarExtraction", "@type": "LabProtocol" }, - { - "@id": "#ParameterValue_Vortex_Mixer_3", - "@type": "PropertyValue", - "additionalType": "ParameterValue", - "columnIndex": "0", - "name": "Vortex Mixer", - "value": "3", - "propertyID": "https://bioregistry.io/NCIT:C29544", - "unitCode": "https://bioregistry.io/UO:0000010", - "unitText": "second" - }, - { - "@id": "#ParameterValue_Temperature_95", - "@type": "PropertyValue", - "additionalType": "ParameterValue", - "columnIndex": "1", - "name": "Temperature", - "value": "95", - "propertyID": "https://bioregistry.io/NCIT:C25206", - "unitCode": "https://bioregistry.io/UO:0000027", - "unitText": "degree celsius" - }, - { - "@id": "#Process_SugarExtraction_0", - "@type": "LabProcess", - "name": "SugarExtraction_0", - "object": { "@id": "#Sample_Cold1_leaf" }, - "result": { "@id": "#Sample_Cold1_sugar-ext" }, - "executesLabProtocol": { "@id": "#Protocol_SugarExtraction" }, - "parameterValue": [ - { "@id": "#ParameterValue_Vortex_Mixer_3" }, - { "@id": "#ParameterValue_Temperature_95" } - ] - }, - { - "@id": "#Sample_Cold2_sugar-ext", - "@type": "Sample", - "additionalType": "Sample", - "name": "Cold2_sugar-ext" - }, - { - "@id": "#Process_SugarExtraction_1", - "@type": "LabProcess", - "name": "SugarExtraction_1", - "object": { "@id": "#Sample_Cold2_leaf" }, - "result": { "@id": "#Sample_Cold2_sugar-ext" }, - "executesLabProtocol": { "@id": "#Protocol_SugarExtraction" }, - "parameterValue": [ - { "@id": "#ParameterValue_Vortex_Mixer_3" }, - { "@id": "#ParameterValue_Temperature_95" } - ] - }, - { - "@id": "#Sample_Cold3_sugar-ext", - "@type": "Sample", - "additionalType": "Sample", - "name": "Cold3_sugar-ext" - }, - { - "@id": "#Process_SugarExtraction_2", - "@type": "LabProcess", - "name": "SugarExtraction_2", - "object": { "@id": "#Sample_Cold3_leaf" }, - "result": { "@id": "#Sample_Cold3_sugar-ext" }, - "executesLabProtocol": { "@id": "#Protocol_SugarExtraction" }, - "parameterValue": [ - { "@id": "#ParameterValue_Vortex_Mixer_3" }, - { "@id": "#ParameterValue_Temperature_95" } - ] - }, - { - "@id": "#Sample_RT1_sugar-ext", - "@type": "Sample", - "additionalType": "Sample", - "name": "RT1_sugar-ext" - }, - { - "@id": "#Process_SugarExtraction_3", - "@type": "LabProcess", - "name": "SugarExtraction_3", - "object": { "@id": "#Sample_RT1_leaf" }, - "result": { "@id": "#Sample_RT1_sugar-ext" }, - "executesLabProtocol": { "@id": "#Protocol_SugarExtraction" }, - "parameterValue": [ - { "@id": "#ParameterValue_Vortex_Mixer_3" }, - { "@id": "#ParameterValue_Temperature_95" } - ] - }, - { - "@id": "#Sample_RT2_sugar-ext", - "@type": "Sample", - "additionalType": "Sample", - "name": "RT2_sugar-ext" - }, - { - "@id": "#Process_SugarExtraction_4", - "@type": "LabProcess", - "name": "SugarExtraction_4", - "object": { "@id": "#Sample_RT2_leaf" }, - "result": { "@id": "#Sample_RT2_sugar-ext" }, - "executesLabProtocol": { "@id": "#Protocol_SugarExtraction" }, - "parameterValue": [ - { "@id": "#ParameterValue_Vortex_Mixer_3" }, - { "@id": "#ParameterValue_Temperature_95" } - ] - }, - { - "@id": "#Sample_RT3_sugar-ext", - "@type": "Sample", - "additionalType": "Sample", - "name": "RT3_sugar-ext" - }, - { - "@id": "#Process_SugarExtraction_5", - "@type": "LabProcess", - "name": "SugarExtraction_5", - "object": { "@id": "#Sample_RT3_leaf" }, - "result": { "@id": "#Sample_RT3_sugar-ext" }, - "executesLabProtocol": { "@id": "#Protocol_SugarExtraction" }, - "parameterValue": [ - { "@id": "#ParameterValue_Vortex_Mixer_3" }, - { "@id": "#ParameterValue_Temperature_95" } - ] - }, - { "@id": "#OA_", "@type": "DefinedTerm", "name": "" }, - { - "@id": "#Protocol__SugarMeasurement", - "@type": "LabProtocol", - "name": "", - "description": "", - "intendedUse": { "@id": "#OA_" }, - "version": "" - }, - { - "@id": "#ParameterValue_technical_replicate_1", - "@type": "PropertyValue", - "additionalType": "ParameterValue", - "columnIndex": "0", - "name": "technical replicate", - "value": "1", - "propertyID": "https://bioregistry.io/EFO:0002090" - }, - { - "@id": "#ParameterValue_sample_volume_10", - "@type": "PropertyValue", - "additionalType": "ParameterValue", - "columnIndex": "1", - "name": "sample volume", - "value": "10", - "propertyID": "http://purl.org/nfdi4plants/ontology/dpbo/DPBO_0010013", - "unitCode": "https://bioregistry.io/UO:0000101", - "unitText": "microliter" - }, - { - "@id": "#ParameterValue_buffer_volume_150", - "@type": "PropertyValue", - "additionalType": "ParameterValue", - "columnIndex": "2", - "name": "buffer volume", - "value": "150", - "propertyID": "http://purl.org/nfdi4plants/ontology/dpbo/DPBO_0000117", - "unitCode": "https://bioregistry.io/UO:0000101", - "unitText": "microliter" - }, - { - "@id": "#ParameterValue_measurement_device", - "@type": "PropertyValue", - "additionalType": "ParameterValue", - "columnIndex": "3", - "name": "measurement device", - "propertyID": "https://bioregistry.io/OBI:0000832" - }, - { - "@id": "#ParameterValue_cycle_count_5", - "@type": "PropertyValue", - "additionalType": "ParameterValue", - "columnIndex": "4", - "name": "cycle count", - "value": "5", - "propertyID": "http://purl.obolibrary.org/obo/AFR_0002311" - }, - { - "@id": "#Process_SugarMeasurement_0", - "@type": "LabProcess", - "name": "SugarMeasurement_0", - "agent": { "@id": "#Person_" }, - "object": { "@id": "#Sample_Cold1_sugar-ext" }, - "result": { - "@id": "assays/SugarMeasurement/dataset/./assays/SugarMeasurement/dataset/sugar_result.csv" - }, - "executesLabProtocol": { "@id": "#Protocol__SugarMeasurement" }, - "parameterValue": [ - { "@id": "#ParameterValue_technical_replicate_1" }, - { "@id": "#ParameterValue_sample_volume_10" }, - { "@id": "#ParameterValue_buffer_volume_150" }, - { "@id": "#ParameterValue_measurement_device" }, - { "@id": "#ParameterValue_cycle_count_5" } - ] - }, - { - "@id": "#ParameterValue_technical_replicate_2", - "@type": "PropertyValue", - "additionalType": "ParameterValue", - "columnIndex": "0", - "name": "technical replicate", - "value": "2", - "propertyID": "https://bioregistry.io/EFO:0002090" - }, - { - "@id": "#Process_SugarMeasurement_1", - "@type": "LabProcess", - "name": "SugarMeasurement_1", - "agent": { "@id": "#Person_" }, - "object": { "@id": "#Sample_Cold2_sugar-ext" }, - "result": { - "@id": "assays/SugarMeasurement/dataset/./assays/SugarMeasurement/dataset/sugar_result.csv" - }, - "executesLabProtocol": { "@id": "#Protocol__SugarMeasurement" }, - "parameterValue": [ - { "@id": "#ParameterValue_technical_replicate_2" }, - { "@id": "#ParameterValue_sample_volume_10" }, - { "@id": "#ParameterValue_buffer_volume_150" }, - { "@id": "#ParameterValue_measurement_device" }, - { "@id": "#ParameterValue_cycle_count_5" } - ] - }, - { - "@id": "#ParameterValue_technical_replicate_3", - "@type": "PropertyValue", - "additionalType": "ParameterValue", - "columnIndex": "0", - "name": "technical replicate", - "value": "3", - "propertyID": "https://bioregistry.io/EFO:0002090" - }, - { - "@id": "#Process_SugarMeasurement_2", - "@type": "LabProcess", - "name": "SugarMeasurement_2", - "agent": { "@id": "#Person_" }, - "object": { "@id": "#Sample_Cold3_sugar-ext" }, - "result": { - "@id": "assays/SugarMeasurement/dataset/./assays/SugarMeasurement/dataset/sugar_result.csv" - }, - "executesLabProtocol": { "@id": "#Protocol__SugarMeasurement" }, - "parameterValue": [ - { "@id": "#ParameterValue_technical_replicate_3" }, - { "@id": "#ParameterValue_sample_volume_10" }, - { "@id": "#ParameterValue_buffer_volume_150" }, - { "@id": "#ParameterValue_measurement_device" }, - { "@id": "#ParameterValue_cycle_count_5" } - ] - }, - { - "@id": "#Process_SugarMeasurement_3", - "@type": "LabProcess", - "name": "SugarMeasurement_3", - "agent": { "@id": "#Person_" }, - "object": { "@id": "#Sample_RT1_sugar-ext" }, - "result": { - "@id": "assays/SugarMeasurement/dataset/./assays/SugarMeasurement/dataset/sugar_result.csv" - }, - "executesLabProtocol": { "@id": "#Protocol__SugarMeasurement" }, - "parameterValue": [ - { "@id": "#ParameterValue_technical_replicate_1" }, - { "@id": "#ParameterValue_sample_volume_10" }, - { "@id": "#ParameterValue_buffer_volume_150" }, - { "@id": "#ParameterValue_measurement_device" }, - { "@id": "#ParameterValue_cycle_count_5" } - ] - }, - { - "@id": "#Process_SugarMeasurement_4", - "@type": "LabProcess", - "name": "SugarMeasurement_4", - "agent": { "@id": "#Person_" }, - "object": { "@id": "#Sample_RT2_sugar-ext" }, - "result": { - "@id": "assays/SugarMeasurement/dataset/./assays/SugarMeasurement/dataset/sugar_result.csv" - }, - "executesLabProtocol": { "@id": "#Protocol__SugarMeasurement" }, - "parameterValue": [ - { "@id": "#ParameterValue_technical_replicate_2" }, - { "@id": "#ParameterValue_sample_volume_10" }, - { "@id": "#ParameterValue_buffer_volume_150" }, - { "@id": "#ParameterValue_measurement_device" }, - { "@id": "#ParameterValue_cycle_count_5" } - ] - }, - { - "@id": "#Process_SugarMeasurement_5", - "@type": "LabProcess", - "name": "SugarMeasurement_5", - "agent": { "@id": "#Person_" }, - "object": { "@id": "#Sample_RT3_sugar-ext" }, - "result": { - "@id": "assays/SugarMeasurement/dataset/./assays/SugarMeasurement/dataset/sugar_result.csv" - }, - "executesLabProtocol": { "@id": "#Protocol__SugarMeasurement" }, - "parameterValue": [ - { "@id": "#ParameterValue_technical_replicate_3" }, - { "@id": "#ParameterValue_sample_volume_10" }, - { "@id": "#ParameterValue_buffer_volume_150" }, - { "@id": "#ParameterValue_measurement_device" }, - { "@id": "#ParameterValue_cycle_count_5" } - ] - }, - { - "@id": "http://purl.org/nfdi4plants/ontology/dpbo/DPBO_0000116", - "@type": "DefinedTerm", - "name": "Infinite M200 plate reader (Tecan)", - "termCode": "http://purl.org/nfdi4plants/ontology/dpbo/DPBO_0000116" - }, - { - "@id": "assays/SugarMeasurement/", - "@type": "Dataset", - "additionalType": "Assay", - "identifier": "SugarMeasurement", - "hasPart": { - "@id": "assays/SugarMeasurement/dataset/./assays/SugarMeasurement/dataset/sugar_result.csv" - }, - "about": [ - { "@id": "#Process_SugarExtraction_0" }, - { "@id": "#Process_SugarExtraction_1" }, - { "@id": "#Process_SugarExtraction_2" }, - { "@id": "#Process_SugarExtraction_3" }, - { "@id": "#Process_SugarExtraction_4" }, - { "@id": "#Process_SugarExtraction_5" }, - { "@id": "#Process_SugarMeasurement_0" }, - { "@id": "#Process_SugarMeasurement_1" }, - { "@id": "#Process_SugarMeasurement_2" }, - { "@id": "#Process_SugarMeasurement_3" }, - { "@id": "#Process_SugarMeasurement_4" }, - { "@id": "#Process_SugarMeasurement_5" } - ], - "measurementMethod": { - "@id": "http://purl.org/nfdi4plants/ontology/dpbo/DPBO_0000116" - }, - "measurementTechnique": { - "@id": "http://purl.org/nfdi4plants/ontology/dpbo/DPBO_0000116" - } - }, - { "@id": "#OA_draft", "@type": "DefinedTerm", "name": "draft" }, - { "@id": "#LDComment_a_a", "@type": "Comment", "name": "a", "text": "a" }, - { - "@id": "#test", - "@type": "ScholarlyArticle", - "headline": "test", - "identifier": [], - "creativeWorkStatus": { "@id": "#OA_draft" }, - "comment": { "@id": "#LDComment_a_a" } - }, - { - "@id": "./", - "@type": "Dataset", - "additionalType": "Investigation", - "identifier": "AthalianaColdStressSugar", - "datePublished": "2025-08-11T15:44:37.510", - "description": "This experiment investigates the response of Arabidopsis thaliana to cold stress by subjecting plants to low-temperature conditions and monitoring physiological, molecular, and growth-related changes. Cold-treated plants are compared with control groups grown under optimal conditions to assess stress-related markers such as changes in protein abundance, sugar content, and growth rates. The goal is to identify key molecules and physiological responses involved in cold tolerance, contributing to the understanding of plant stress adaptation mechanisms.", - "hasPart": [ - { "@id": "studies/AthalianaColdStress/" }, - { "@id": "assays/Proteomics_MS/" }, - { "@id": "assays/SugarMeasurement/" } - ], - "name": "Arabidopsis thaliana cold acclimation", - "citation": { "@id": "#test" }, - "license": "ALL RIGHTS RESERVED BY THE AUTHORS" - }, - { - "@id": "ro-crate-metadata.json", - "@type": "CreativeWork", - "conformsTo": { "@id": "https://w3id.org/ro/crate/1.1" }, - "about": { "@id": "./" } - } - ] -} diff --git a/uv.lock b/uv.lock index 480efb3..1bfa41a 100644 --- a/uv.lock +++ b/uv.lock @@ -8,24 +8,8 @@ resolution-markers = [ [manifest] members = [ - "api", - "api-client", "m4-2-advanced-middleware", - "shared", "sql-to-arc", - "tools", -] - -[[package]] -name = "amqp" -version = "5.3.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "vine" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/79/fc/ec94a357dfc6683d8c86f8b4cfa5416a4c36b28052ec8260c77aca96a443/amqp-5.3.1.tar.gz", hash = "sha256:cddc00c725449522023bad949f70fff7b48f0b1ade74d170a6f10ab044739432", size = 129013, upload-time = "2024-11-12T19:55:44.051Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/26/99/fc813cd978842c26c82534010ea849eee9ab3a13ea2b74e95cb9c99e747b/amqp-5.3.1-py3-none-any.whl", hash = "sha256:43b3319e1b4e7d1251833a93d672b4af1e40f3d632d479b98661a95f117880a2", size = 50944, upload-time = "2024-11-12T19:55:41.782Z" }, ] [[package]] @@ -59,88 +43,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7f/9c/36c5c37947ebfb8c7f22e0eb6e4d188ee2d53aa3880f3f2744fb894f0cb1/anyio-4.12.0-py3-none-any.whl", hash = "sha256:dad2376a628f98eeca4881fc56cd06affd18f659b17a747d3ff0307ced94b1bb", size = 113362, upload-time = "2025-11-28T23:36:57.897Z" }, ] -[[package]] -name = "api" -version = "0.0.0" -source = { editable = "middleware/api" } -dependencies = [ - { name = "arctrl" }, - { name = "asn1crypto" }, - { name = "celery" }, - { name = "celery-types" }, - { name = "cryptography" }, - { name = "fastapi" }, - { name = "gitpython" }, - { name = "opentelemetry-instrumentation-celery" }, - { name = "opentelemetry-instrumentation-fastapi" }, - { name = "opentelemetry-instrumentation-redis" }, - { name = "opentelemetry-instrumentation-requests" }, - { name = "opentelemetry-instrumentation-starlette" }, - { name = "python-gitlab" }, - { name = "pyyaml" }, - { name = "redis" }, - { name = "shared" }, - { name = "sse-starlette" }, - { name = "uvicorn" }, -] - -[package.metadata] -requires-dist = [ - { name = "arctrl", specifier = ">=3.0.0b15" }, - { name = "asn1crypto", specifier = ">=1.5.1" }, - { name = "celery", specifier = ">=5.3.6" }, - { name = "celery-types", specifier = ">=0.24.0" }, - { name = "cryptography", specifier = ">=45.0.6" }, - { name = "fastapi", specifier = ">=0.124.0" }, - { name = "gitpython", specifier = ">=3.1.46" }, - { name = "opentelemetry-instrumentation-celery", specifier = ">=0.47b0" }, - { name = "opentelemetry-instrumentation-fastapi", specifier = ">=0.47b0" }, - { name = "opentelemetry-instrumentation-redis", specifier = ">=0.47b0" }, - { name = "opentelemetry-instrumentation-requests", specifier = ">=0.47b0" }, - { name = "opentelemetry-instrumentation-starlette", specifier = ">=0.47b0" }, - { name = "python-gitlab", specifier = ">=6.2.0" }, - { name = "pyyaml", specifier = ">=6.0.2" }, - { name = "redis", specifier = ">=5.0.1" }, - { name = "shared", editable = "middleware/shared" }, - { name = "sse-starlette", specifier = ">=2.0.0" }, - { name = "uvicorn", specifier = ">=0.35.0" }, -] - [[package]] name = "api-client" version = "0.0.0" -source = { editable = "middleware/api_client" } +source = { git = "https://github.com/fairagro/m4.2_advanced_middleware_api.git?subdirectory=middleware%2Fapi_client&branch=feature%2Fsplit_off_client#3a8cde5565e2eef861382ed63b8f356227a632ad" } dependencies = [ { name = "httpx" }, { name = "pydantic" }, ] -[package.dev-dependencies] -dev = [ - { name = "cryptography" }, - { name = "pytest" }, - { name = "pytest-asyncio" }, - { name = "pytest-mock" }, - { name = "respx" }, - { name = "types-pyyaml" }, -] - -[package.metadata] -requires-dist = [ - { name = "httpx", specifier = ">=0.28.1" }, - { name = "pydantic", specifier = ">=2.12.5" }, -] - -[package.metadata.requires-dev] -dev = [ - { name = "cryptography", specifier = ">=45.0.6" }, - { name = "pytest", specifier = ">=9.0.2" }, - { name = "pytest-asyncio", specifier = ">=1.1.0" }, - { name = "pytest-mock", specifier = ">=3.10.0" }, - { name = "respx", specifier = ">=0.20.0" }, - { name = "types-pyyaml", specifier = ">=6.0.12.20240917" }, -] - [[package]] name = "arctrl" version = "3.0.0b15" @@ -154,24 +65,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a9/41/efe0d77fa28a3e0bd6b96ef72db2c7f7e660b5a6278ab4265036a42bd17b/arctrl-3.0.0b15-py3-none-any.whl", hash = "sha256:7374520c650bdde842b542f4887b98091bb136792b15fd2bd63d29b6fc82be09", size = 842931, upload-time = "2025-12-04T18:18:42.345Z" }, ] -[[package]] -name = "asgiref" -version = "3.11.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/76/b9/4db2509eabd14b4a8c71d1b24c8d5734c52b8560a7b1e1a8b56c8d25568b/asgiref-3.11.0.tar.gz", hash = "sha256:13acff32519542a1736223fb79a715acdebe24286d98e8b164a73085f40da2c4", size = 37969, upload-time = "2025-11-19T15:32:20.106Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/91/be/317c2c55b8bbec407257d45f5c8d1b6867abc76d12043f2d3d58c538a4ea/asgiref-3.11.0-py3-none-any.whl", hash = "sha256:1db9021efadb0d9512ce8ffaf72fcef601c7b73a8807a1bb2ef143dc6b14846d", size = 24096, upload-time = "2025-11-19T15:32:19.004Z" }, -] - -[[package]] -name = "asn1crypto" -version = "1.5.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/de/cf/d547feed25b5244fcb9392e288ff9fdc3280b10260362fc45d37a798a6ee/asn1crypto-1.5.1.tar.gz", hash = "sha256:13ae38502be632115abf8a24cbe5f4da52e3b5231990aff31123c805306ccb9c", size = 121080, upload-time = "2022-03-15T14:46:52.889Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c9/7f/09065fd9e27da0eda08b4d6897f1c13535066174cc023af248fc2a8d5e5a/asn1crypto-1.5.1-py2.py3-none-any.whl", hash = "sha256:db4e40728b728508912cbb3d44f19ce188f218e9eba635821bb4b68564f8fd67", size = 105045, upload-time = "2022-03-15T14:46:51.055Z" }, -] - [[package]] name = "astroid" version = "4.0.2" @@ -196,47 +89,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e0/0b/8bdc52111c83e2dc2f97403dc87c0830b8989d9ae45732b34b686326fb2c/bandit-1.9.3-py3-none-any.whl", hash = "sha256:4745917c88d2246def79748bde5e08b9d5e9b92f877863d43fab70cd8814ce6a", size = 134451, upload-time = "2026-01-19T04:05:20.938Z" }, ] -[[package]] -name = "billiard" -version = "4.2.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/58/23/b12ac0bcdfb7360d664f40a00b1bda139cbbbced012c34e375506dbd0143/billiard-4.2.4.tar.gz", hash = "sha256:55f542c371209e03cd5862299b74e52e4fbcba8250ba611ad94276b369b6a85f", size = 156537, upload-time = "2025-11-30T13:28:48.52Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/cb/87/8bab77b323f16d67be364031220069f79159117dd5e43eeb4be2fef1ac9b/billiard-4.2.4-py3-none-any.whl", hash = "sha256:525b42bdec68d2b983347ac312f892db930858495db601b5836ac24e6477cde5", size = 87070, upload-time = "2025-11-30T13:28:47.016Z" }, -] - -[[package]] -name = "celery" -version = "5.6.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "billiard" }, - { name = "click" }, - { name = "click-didyoumean" }, - { name = "click-plugins" }, - { name = "click-repl" }, - { name = "kombu" }, - { name = "python-dateutil" }, - { name = "tzlocal" }, - { name = "vine" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/8f/9d/3d13596519cfa7207a6f9834f4b082554845eb3cd2684b5f8535d50c7c44/celery-5.6.2.tar.gz", hash = "sha256:4a8921c3fcf2ad76317d3b29020772103581ed2454c4c042cc55dcc43585009b", size = 1718802, upload-time = "2026-01-04T12:35:58.012Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/dd/bd/9ecd619e456ae4ba73b6583cc313f26152afae13e9a82ac4fe7f8856bfd1/celery-5.6.2-py3-none-any.whl", hash = "sha256:3ffafacbe056951b629c7abcf9064c4a2366de0bdfc9fdba421b97ebb68619a5", size = 445502, upload-time = "2026-01-04T12:35:55.894Z" }, -] - -[[package]] -name = "celery-types" -version = "0.24.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/72/25/2276a1f00f8ab9fc88128c939333933a24db7df1d75aa57ecc27b7dd3a22/celery_types-0.24.0.tar.gz", hash = "sha256:c93fbcd0b04a9e9c2f55d5540aca4aa1ea4cc06a870c0c8dee5062fdd59663fe", size = 33148, upload-time = "2025-12-23T17:16:30.847Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3a/7e/3252cba5f5c9a65a3f52a69734d8e51e023db8981022b503e8183cf0225e/celery_types-0.24.0-py3-none-any.whl", hash = "sha256:a21e04681e68719a208335e556a79909da4be9c5e0d6d2fd0dd4c5615954b3fd", size = 60473, upload-time = "2025-12-23T17:16:29.89Z" }, -] - [[package]] name = "certifi" version = "2025.11.12" @@ -246,63 +98,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/70/7d/9bc192684cea499815ff478dfcdc13835ddf401365057044fb721ec6bddb/certifi-2025.11.12-py3-none-any.whl", hash = "sha256:97de8790030bbd5c2d96b7ec782fc2f7820ef8dba6db909ccf95449f2d062d4b", size = 159438, upload-time = "2025-11-12T02:54:49.735Z" }, ] -[[package]] -name = "cffi" -version = "2.0.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pycparser", marker = "implementation_name != 'PyPy'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" }, - { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" }, - { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" }, - { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" }, - { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" }, - { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" }, - { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" }, - { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" }, - { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" }, - { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" }, - { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" }, - { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" }, - { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" }, - { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" }, - { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" }, - { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" }, - { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" }, - { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" }, - { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" }, - { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" }, - { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" }, - { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" }, - { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" }, - { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" }, - { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" }, - { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" }, - { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" }, - { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" }, - { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" }, - { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" }, - { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" }, - { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" }, - { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" }, - { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" }, - { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" }, - { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" }, - { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" }, - { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" }, - { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" }, - { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" }, - { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" }, - { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" }, - { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" }, - { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" }, - { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" }, - { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, -] - [[package]] name = "cfgv" version = "3.5.0" @@ -381,43 +176,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" }, ] -[[package]] -name = "click-didyoumean" -version = "0.3.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "click" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/30/ce/217289b77c590ea1e7c24242d9ddd6e249e52c795ff10fac2c50062c48cb/click_didyoumean-0.3.1.tar.gz", hash = "sha256:4f82fdff0dbe64ef8ab2279bd6aa3f6a99c3b28c05aa09cbfc07c9d7fbb5a463", size = 3089, upload-time = "2024-03-24T08:22:07.499Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1b/5b/974430b5ffdb7a4f1941d13d83c64a0395114503cc357c6b9ae4ce5047ed/click_didyoumean-0.3.1-py3-none-any.whl", hash = "sha256:5c4bb6007cfea5f2fd6583a2fb6701a22a41eb98957e63d0fac41c10e7c3117c", size = 3631, upload-time = "2024-03-24T08:22:06.356Z" }, -] - -[[package]] -name = "click-plugins" -version = "1.1.1.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "click" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c3/a4/34847b59150da33690a36da3681d6bbc2ec14ee9a846bc30a6746e5984e4/click_plugins-1.1.1.2.tar.gz", hash = "sha256:d7af3984a99d243c131aa1a828331e7630f4a88a9741fd05c927b204bcf92261", size = 8343, upload-time = "2025-06-25T00:47:37.555Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3d/9a/2abecb28ae875e39c8cad711eb1186d8d14eab564705325e77e4e6ab9ae5/click_plugins-1.1.1.2-py2.py3-none-any.whl", hash = "sha256:008d65743833ffc1f5417bf0e78e8d2c23aab04d9745ba817bd3e71b0feb6aa6", size = 11051, upload-time = "2025-06-25T00:47:36.731Z" }, -] - -[[package]] -name = "click-repl" -version = "0.3.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "click" }, - { name = "prompt-toolkit" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/cb/a2/57f4ac79838cfae6912f997b4d1a64a858fb0c86d7fcaae6f7b58d267fca/click-repl-0.3.0.tar.gz", hash = "sha256:17849c23dba3d667247dc4defe1757fff98694e90fe37474f3feebb69ced26a9", size = 10449, upload-time = "2023-06-15T12:43:51.141Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/52/40/9d857001228658f0d59e97ebd4c346fe73e138c6de1bce61dc568a57c7f8/click_repl-0.3.0-py3-none-any.whl", hash = "sha256:fb7e06deb8da8de86180a33a9da97ac316751c094c6899382da7feeeeb51b812", size = 10289, upload-time = "2023-06-15T12:43:48.626Z" }, -] - [[package]] name = "colorama" version = "0.4.6" @@ -501,62 +259,6 @@ wheels = [ { 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" }, ] -[[package]] -name = "cryptography" -version = "46.0.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/9f/33/c00162f49c0e2fe8064a62cb92b93e50c74a72bc370ab92f86112b33ff62/cryptography-46.0.3.tar.gz", hash = "sha256:a8b17438104fed022ce745b362294d9ce35b4c2e45c1d958ad4a4b019285f4a1", size = 749258, upload-time = "2025-10-15T23:18:31.74Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1d/42/9c391dd801d6cf0d561b5890549d4b27bafcc53b39c31a817e69d87c625b/cryptography-46.0.3-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:109d4ddfadf17e8e7779c39f9b18111a09efb969a301a31e987416a0191ed93a", size = 7225004, upload-time = "2025-10-15T23:16:52.239Z" }, - { url = "https://files.pythonhosted.org/packages/1c/67/38769ca6b65f07461eb200e85fc1639b438bdc667be02cf7f2cd6a64601c/cryptography-46.0.3-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:09859af8466b69bc3c27bdf4f5d84a665e0f7ab5088412e9e2ec49758eca5cbc", size = 4296667, upload-time = "2025-10-15T23:16:54.369Z" }, - { url = "https://files.pythonhosted.org/packages/5c/49/498c86566a1d80e978b42f0d702795f69887005548c041636df6ae1ca64c/cryptography-46.0.3-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:01ca9ff2885f3acc98c29f1860552e37f6d7c7d013d7334ff2a9de43a449315d", size = 4450807, upload-time = "2025-10-15T23:16:56.414Z" }, - { url = "https://files.pythonhosted.org/packages/4b/0a/863a3604112174c8624a2ac3c038662d9e59970c7f926acdcfaed8d61142/cryptography-46.0.3-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:6eae65d4c3d33da080cff9c4ab1f711b15c1d9760809dad6ea763f3812d254cb", size = 4299615, upload-time = "2025-10-15T23:16:58.442Z" }, - { url = "https://files.pythonhosted.org/packages/64/02/b73a533f6b64a69f3cd3872acb6ebc12aef924d8d103133bb3ea750dc703/cryptography-46.0.3-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5bf0ed4490068a2e72ac03d786693adeb909981cc596425d09032d372bcc849", size = 4016800, upload-time = "2025-10-15T23:17:00.378Z" }, - { url = "https://files.pythonhosted.org/packages/25/d5/16e41afbfa450cde85a3b7ec599bebefaef16b5c6ba4ec49a3532336ed72/cryptography-46.0.3-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:5ecfccd2329e37e9b7112a888e76d9feca2347f12f37918facbb893d7bb88ee8", size = 4984707, upload-time = "2025-10-15T23:17:01.98Z" }, - { url = "https://files.pythonhosted.org/packages/c9/56/e7e69b427c3878352c2fb9b450bd0e19ed552753491d39d7d0a2f5226d41/cryptography-46.0.3-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a2c0cd47381a3229c403062f764160d57d4d175e022c1df84e168c6251a22eec", size = 4482541, upload-time = "2025-10-15T23:17:04.078Z" }, - { url = "https://files.pythonhosted.org/packages/78/f6/50736d40d97e8483172f1bb6e698895b92a223dba513b0ca6f06b2365339/cryptography-46.0.3-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:549e234ff32571b1f4076ac269fcce7a808d3bf98b76c8dd560e42dbc66d7d91", size = 4299464, upload-time = "2025-10-15T23:17:05.483Z" }, - { url = "https://files.pythonhosted.org/packages/00/de/d8e26b1a855f19d9994a19c702fa2e93b0456beccbcfe437eda00e0701f2/cryptography-46.0.3-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:c0a7bb1a68a5d3471880e264621346c48665b3bf1c3759d682fc0864c540bd9e", size = 4950838, upload-time = "2025-10-15T23:17:07.425Z" }, - { url = "https://files.pythonhosted.org/packages/8f/29/798fc4ec461a1c9e9f735f2fc58741b0daae30688f41b2497dcbc9ed1355/cryptography-46.0.3-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:10b01676fc208c3e6feeb25a8b83d81767e8059e1fe86e1dc62d10a3018fa926", size = 4481596, upload-time = "2025-10-15T23:17:09.343Z" }, - { url = "https://files.pythonhosted.org/packages/15/8d/03cd48b20a573adfff7652b76271078e3045b9f49387920e7f1f631d125e/cryptography-46.0.3-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0abf1ffd6e57c67e92af68330d05760b7b7efb243aab8377e583284dbab72c71", size = 4426782, upload-time = "2025-10-15T23:17:11.22Z" }, - { url = "https://files.pythonhosted.org/packages/fa/b1/ebacbfe53317d55cf33165bda24c86523497a6881f339f9aae5c2e13e57b/cryptography-46.0.3-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a04bee9ab6a4da801eb9b51f1b708a1b5b5c9eb48c03f74198464c66f0d344ac", size = 4698381, upload-time = "2025-10-15T23:17:12.829Z" }, - { url = "https://files.pythonhosted.org/packages/96/92/8a6a9525893325fc057a01f654d7efc2c64b9de90413adcf605a85744ff4/cryptography-46.0.3-cp311-abi3-win32.whl", hash = "sha256:f260d0d41e9b4da1ed1e0f1ce571f97fe370b152ab18778e9e8f67d6af432018", size = 3055988, upload-time = "2025-10-15T23:17:14.65Z" }, - { url = "https://files.pythonhosted.org/packages/7e/bf/80fbf45253ea585a1e492a6a17efcb93467701fa79e71550a430c5e60df0/cryptography-46.0.3-cp311-abi3-win_amd64.whl", hash = "sha256:a9a3008438615669153eb86b26b61e09993921ebdd75385ddd748702c5adfddb", size = 3514451, upload-time = "2025-10-15T23:17:16.142Z" }, - { url = "https://files.pythonhosted.org/packages/2e/af/9b302da4c87b0beb9db4e756386a7c6c5b8003cd0e742277888d352ae91d/cryptography-46.0.3-cp311-abi3-win_arm64.whl", hash = "sha256:5d7f93296ee28f68447397bf5198428c9aeeab45705a55d53a6343455dcb2c3c", size = 2928007, upload-time = "2025-10-15T23:17:18.04Z" }, - { url = "https://files.pythonhosted.org/packages/f5/e2/a510aa736755bffa9d2f75029c229111a1d02f8ecd5de03078f4c18d91a3/cryptography-46.0.3-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:00a5e7e87938e5ff9ff5447ab086a5706a957137e6e433841e9d24f38a065217", size = 7158012, upload-time = "2025-10-15T23:17:19.982Z" }, - { url = "https://files.pythonhosted.org/packages/73/dc/9aa866fbdbb95b02e7f9d086f1fccfeebf8953509b87e3f28fff927ff8a0/cryptography-46.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c8daeb2d2174beb4575b77482320303f3d39b8e81153da4f0fb08eb5fe86a6c5", size = 4288728, upload-time = "2025-10-15T23:17:21.527Z" }, - { url = "https://files.pythonhosted.org/packages/c5/fd/bc1daf8230eaa075184cbbf5f8cd00ba9db4fd32d63fb83da4671b72ed8a/cryptography-46.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:39b6755623145ad5eff1dab323f4eae2a32a77a7abef2c5089a04a3d04366715", size = 4435078, upload-time = "2025-10-15T23:17:23.042Z" }, - { url = "https://files.pythonhosted.org/packages/82/98/d3bd5407ce4c60017f8ff9e63ffee4200ab3e23fe05b765cab805a7db008/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:db391fa7c66df6762ee3f00c95a89e6d428f4d60e7abc8328f4fe155b5ac6e54", size = 4293460, upload-time = "2025-10-15T23:17:24.885Z" }, - { url = "https://files.pythonhosted.org/packages/26/e9/e23e7900983c2b8af7a08098db406cf989d7f09caea7897e347598d4cd5b/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:78a97cf6a8839a48c49271cdcbd5cf37ca2c1d6b7fdd86cc864f302b5e9bf459", size = 3995237, upload-time = "2025-10-15T23:17:26.449Z" }, - { url = "https://files.pythonhosted.org/packages/91/15/af68c509d4a138cfe299d0d7ddb14afba15233223ebd933b4bbdbc7155d3/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:dfb781ff7eaa91a6f7fd41776ec37c5853c795d3b358d4896fdbb5df168af422", size = 4967344, upload-time = "2025-10-15T23:17:28.06Z" }, - { url = "https://files.pythonhosted.org/packages/ca/e3/8643d077c53868b681af077edf6b3cb58288b5423610f21c62aadcbe99f4/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:6f61efb26e76c45c4a227835ddeae96d83624fb0d29eb5df5b96e14ed1a0afb7", size = 4466564, upload-time = "2025-10-15T23:17:29.665Z" }, - { url = "https://files.pythonhosted.org/packages/0e/43/c1e8726fa59c236ff477ff2b5dc071e54b21e5a1e51aa2cee1676f1c986f/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:23b1a8f26e43f47ceb6d6a43115f33a5a37d57df4ea0ca295b780ae8546e8044", size = 4292415, upload-time = "2025-10-15T23:17:31.686Z" }, - { url = "https://files.pythonhosted.org/packages/42/f9/2f8fefdb1aee8a8e3256a0568cffc4e6d517b256a2fe97a029b3f1b9fe7e/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:b419ae593c86b87014b9be7396b385491ad7f320bde96826d0dd174459e54665", size = 4931457, upload-time = "2025-10-15T23:17:33.478Z" }, - { url = "https://files.pythonhosted.org/packages/79/30/9b54127a9a778ccd6d27c3da7563e9f2d341826075ceab89ae3b41bf5be2/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:50fc3343ac490c6b08c0cf0d704e881d0d660be923fd3076db3e932007e726e3", size = 4466074, upload-time = "2025-10-15T23:17:35.158Z" }, - { url = "https://files.pythonhosted.org/packages/ac/68/b4f4a10928e26c941b1b6a179143af9f4d27d88fe84a6a3c53592d2e76bf/cryptography-46.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:22d7e97932f511d6b0b04f2bfd818d73dcd5928db509460aaf48384778eb6d20", size = 4420569, upload-time = "2025-10-15T23:17:37.188Z" }, - { url = "https://files.pythonhosted.org/packages/a3/49/3746dab4c0d1979888f125226357d3262a6dd40e114ac29e3d2abdf1ec55/cryptography-46.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d55f3dffadd674514ad19451161118fd010988540cee43d8bc20675e775925de", size = 4681941, upload-time = "2025-10-15T23:17:39.236Z" }, - { url = "https://files.pythonhosted.org/packages/fd/30/27654c1dbaf7e4a3531fa1fc77986d04aefa4d6d78259a62c9dc13d7ad36/cryptography-46.0.3-cp314-cp314t-win32.whl", hash = "sha256:8a6e050cb6164d3f830453754094c086ff2d0b2f3a897a1d9820f6139a1f0914", size = 3022339, upload-time = "2025-10-15T23:17:40.888Z" }, - { url = "https://files.pythonhosted.org/packages/f6/30/640f34ccd4d2a1bc88367b54b926b781b5a018d65f404d409aba76a84b1c/cryptography-46.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:760f83faa07f8b64e9c33fc963d790a2edb24efb479e3520c14a45741cd9b2db", size = 3494315, upload-time = "2025-10-15T23:17:42.769Z" }, - { url = "https://files.pythonhosted.org/packages/ba/8b/88cc7e3bd0a8e7b861f26981f7b820e1f46aa9d26cc482d0feba0ecb4919/cryptography-46.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:516ea134e703e9fe26bcd1277a4b59ad30586ea90c365a87781d7887a646fe21", size = 2919331, upload-time = "2025-10-15T23:17:44.468Z" }, - { url = "https://files.pythonhosted.org/packages/fd/23/45fe7f376a7df8daf6da3556603b36f53475a99ce4faacb6ba2cf3d82021/cryptography-46.0.3-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:cb3d760a6117f621261d662bccc8ef5bc32ca673e037c83fbe565324f5c46936", size = 7218248, upload-time = "2025-10-15T23:17:46.294Z" }, - { url = "https://files.pythonhosted.org/packages/27/32/b68d27471372737054cbd34c84981f9edbc24fe67ca225d389799614e27f/cryptography-46.0.3-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4b7387121ac7d15e550f5cb4a43aef2559ed759c35df7336c402bb8275ac9683", size = 4294089, upload-time = "2025-10-15T23:17:48.269Z" }, - { url = "https://files.pythonhosted.org/packages/26/42/fa8389d4478368743e24e61eea78846a0006caffaf72ea24a15159215a14/cryptography-46.0.3-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:15ab9b093e8f09daab0f2159bb7e47532596075139dd74365da52ecc9cb46c5d", size = 4440029, upload-time = "2025-10-15T23:17:49.837Z" }, - { url = "https://files.pythonhosted.org/packages/5f/eb/f483db0ec5ac040824f269e93dd2bd8a21ecd1027e77ad7bdf6914f2fd80/cryptography-46.0.3-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:46acf53b40ea38f9c6c229599a4a13f0d46a6c3fa9ef19fc1a124d62e338dfa0", size = 4297222, upload-time = "2025-10-15T23:17:51.357Z" }, - { url = "https://files.pythonhosted.org/packages/fd/cf/da9502c4e1912cb1da3807ea3618a6829bee8207456fbbeebc361ec38ba3/cryptography-46.0.3-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:10ca84c4668d066a9878890047f03546f3ae0a6b8b39b697457b7757aaf18dbc", size = 4012280, upload-time = "2025-10-15T23:17:52.964Z" }, - { url = "https://files.pythonhosted.org/packages/6b/8f/9adb86b93330e0df8b3dcf03eae67c33ba89958fc2e03862ef1ac2b42465/cryptography-46.0.3-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:36e627112085bb3b81b19fed209c05ce2a52ee8b15d161b7c643a7d5a88491f3", size = 4978958, upload-time = "2025-10-15T23:17:54.965Z" }, - { url = "https://files.pythonhosted.org/packages/d1/a0/5fa77988289c34bdb9f913f5606ecc9ada1adb5ae870bd0d1054a7021cc4/cryptography-46.0.3-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1000713389b75c449a6e979ffc7dcc8ac90b437048766cef052d4d30b8220971", size = 4473714, upload-time = "2025-10-15T23:17:56.754Z" }, - { url = "https://files.pythonhosted.org/packages/14/e5/fc82d72a58d41c393697aa18c9abe5ae1214ff6f2a5c18ac470f92777895/cryptography-46.0.3-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:b02cf04496f6576afffef5ddd04a0cb7d49cf6be16a9059d793a30b035f6b6ac", size = 4296970, upload-time = "2025-10-15T23:17:58.588Z" }, - { url = "https://files.pythonhosted.org/packages/78/06/5663ed35438d0b09056973994f1aec467492b33bd31da36e468b01ec1097/cryptography-46.0.3-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:71e842ec9bc7abf543b47cf86b9a743baa95f4677d22baa4c7d5c69e49e9bc04", size = 4940236, upload-time = "2025-10-15T23:18:00.897Z" }, - { url = "https://files.pythonhosted.org/packages/fc/59/873633f3f2dcd8a053b8dd1d38f783043b5fce589c0f6988bf55ef57e43e/cryptography-46.0.3-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:402b58fc32614f00980b66d6e56a5b4118e6cb362ae8f3fda141ba4689bd4506", size = 4472642, upload-time = "2025-10-15T23:18:02.749Z" }, - { url = "https://files.pythonhosted.org/packages/3d/39/8e71f3930e40f6877737d6f69248cf74d4e34b886a3967d32f919cc50d3b/cryptography-46.0.3-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ef639cb3372f69ec44915fafcd6698b6cc78fbe0c2ea41be867f6ed612811963", size = 4423126, upload-time = "2025-10-15T23:18:04.85Z" }, - { url = "https://files.pythonhosted.org/packages/cd/c7/f65027c2810e14c3e7268353b1681932b87e5a48e65505d8cc17c99e36ae/cryptography-46.0.3-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3b51b8ca4f1c6453d8829e1eb7299499ca7f313900dd4d89a24b8b87c0a780d4", size = 4686573, upload-time = "2025-10-15T23:18:06.908Z" }, - { url = "https://files.pythonhosted.org/packages/0a/6e/1c8331ddf91ca4730ab3086a0f1be19c65510a33b5a441cb334e7a2d2560/cryptography-46.0.3-cp38-abi3-win32.whl", hash = "sha256:6276eb85ef938dc035d59b87c8a7dc559a232f954962520137529d77b18ff1df", size = 3036695, upload-time = "2025-10-15T23:18:08.672Z" }, - { url = "https://files.pythonhosted.org/packages/90/45/b0d691df20633eff80955a0fc7695ff9051ffce8b69741444bd9ed7bd0db/cryptography-46.0.3-cp38-abi3-win_amd64.whl", hash = "sha256:416260257577718c05135c55958b674000baef9a1c7d9e8f306ec60d71db850f", size = 3501720, upload-time = "2025-10-15T23:18:10.632Z" }, - { url = "https://files.pythonhosted.org/packages/e8/cb/2da4cc83f5edb9c3257d09e1e7ab7b23f049c7962cae8d842bbef0a9cec9/cryptography-46.0.3-cp38-abi3-win_arm64.whl", hash = "sha256:d89c3468de4cdc4f08a57e214384d0471911a3830fcdaf7a8cc587e42a866372", size = 2918740, upload-time = "2025-10-15T23:18:12.277Z" }, -] - [[package]] name = "dill" version = "0.4.0" @@ -747,30 +449,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b5/36/7fb70f04bf00bc646cd5bb45aa9eddb15e19437a28b8fb2b4a5249fac770/filelock-3.20.3-py3-none-any.whl", hash = "sha256:4b0dda527ee31078689fc205ec4f1c1bf7d56cf88b6dc9426c4f230e46c2dce1", size = 16701, upload-time = "2026-01-09T17:55:04.334Z" }, ] -[[package]] -name = "gitdb" -version = "4.0.12" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "smmap" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/72/94/63b0fc47eb32792c7ba1fe1b694daec9a63620db1e313033d18140c2320a/gitdb-4.0.12.tar.gz", hash = "sha256:5ef71f855d191a3326fcfbc0d5da835f26b13fbcba60c32c21091c349ffdb571", size = 394684, upload-time = "2025-01-02T07:20:46.413Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a0/61/5c78b91c3143ed5c14207f463aecfc8f9dbb5092fb2869baf37c273b2705/gitdb-4.0.12-py3-none-any.whl", hash = "sha256:67073e15955400952c6565cc3e707c554a4eea2e428946f7a4c162fab9bd9bcf", size = 62794, upload-time = "2025-01-02T07:20:43.624Z" }, -] - -[[package]] -name = "gitpython" -version = "3.1.46" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "gitdb" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/df/b5/59d16470a1f0dfe8c793f9ef56fd3826093fc52b3bd96d6b9d6c26c7e27b/gitpython-3.1.46.tar.gz", hash = "sha256:400124c7d0ef4ea03f7310ac2fbf7151e09ff97f2a3288d64a440c584a29c37f", size = 215371, upload-time = "2026-01-01T15:37:32.073Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6a/09/e21df6aef1e1ffc0c816f0522ddc3f6dcded766c3261813131c78a704470/gitpython-3.1.46-py3-none-any.whl", hash = "sha256:79812ed143d9d25b6d176a10bb511de0f9c67b1fa641d82097b0ab90398a2058", size = 208620, upload-time = "2026-01-01T15:37:30.574Z" }, -] - [[package]] name = "googleapis-common-protos" version = "1.72.0" @@ -950,21 +628,6 @@ 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 = "kombu" -version = "5.6.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "amqp" }, - { name = "packaging" }, - { name = "tzdata" }, - { name = "vine" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b6/a5/607e533ed6c83ae1a696969b8e1c137dfebd5759a2e9682e26ff1b97740b/kombu-5.6.2.tar.gz", hash = "sha256:8060497058066c6f5aed7c26d7cd0d3b574990b09de842a8c5aaed0b92cc5a55", size = 472594, upload-time = "2025-12-29T20:30:07.779Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fb/0f/834427d8c03ff1d7e867d3db3d176470c64871753252b21b4f4897d1fa45/kombu-5.6.2-py3-none-any.whl", hash = "sha256:efcfc559da324d41d61ca311b0c64965ea35b4c55cc04ee36e55386145dace93", size = 214219, upload-time = "2025-12-29T20:30:05.74Z" }, -] - [[package]] name = "librt" version = "0.7.3" @@ -1022,9 +685,6 @@ name = "m4-2-advanced-middleware" version = "6.0.0" source = { virtual = "." } dependencies = [ - { name = "api" }, - { name = "api-client" }, - { name = "shared" }, { name = "sql-to-arc" }, ] @@ -1046,12 +706,7 @@ dev = [ ] [package.metadata] -requires-dist = [ - { name = "api", editable = "middleware/api" }, - { name = "api-client", editable = "middleware/api_client" }, - { name = "shared", editable = "middleware/shared" }, - { name = "sql-to-arc", editable = "middleware/sql_to_arc" }, -] +requires-dist = [{ name = "sql-to-arc", editable = "middleware/sql_to_arc" }] [package.metadata.requires-dev] dev = [ @@ -1300,113 +955,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/bc/46/e4a102e17205bb05a50dbf24ef0e92b66b648cd67db9a68865af06a242fd/opentelemetry_exporter_otlp_proto_http-1.39.0-py3-none-any.whl", hash = "sha256:5789cb1375a8b82653328c0ce13a054d285f774099faf9d068032a49de4c7862", size = 19639, upload-time = "2025-12-03T13:19:39.536Z" }, ] -[[package]] -name = "opentelemetry-instrumentation" -version = "0.60b1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "opentelemetry-api" }, - { name = "opentelemetry-semantic-conventions" }, - { name = "packaging" }, - { name = "wrapt" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/41/0f/7e6b713ac117c1f5e4e3300748af699b9902a2e5e34c9cf443dde25a01fa/opentelemetry_instrumentation-0.60b1.tar.gz", hash = "sha256:57ddc7974c6eb35865af0426d1a17132b88b2ed8586897fee187fd5b8944bd6a", size = 31706, upload-time = "2025-12-11T13:36:42.515Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/77/d2/6788e83c5c86a2690101681aeef27eeb2a6bf22df52d3f263a22cee20915/opentelemetry_instrumentation-0.60b1-py3-none-any.whl", hash = "sha256:04480db952b48fb1ed0073f822f0ee26012b7be7c3eac1a3793122737c78632d", size = 33096, upload-time = "2025-12-11T13:35:33.067Z" }, -] - -[[package]] -name = "opentelemetry-instrumentation-asgi" -version = "0.60b1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "asgiref" }, - { name = "opentelemetry-api" }, - { name = "opentelemetry-instrumentation" }, - { name = "opentelemetry-semantic-conventions" }, - { name = "opentelemetry-util-http" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/77/db/851fa88db7441da82d50bd80f2de5ee55213782e25dc858e04d0c9961d60/opentelemetry_instrumentation_asgi-0.60b1.tar.gz", hash = "sha256:16bfbe595cd24cda309a957456d0fc2523f41bc7b076d1f2d7e98a1ad9876d6f", size = 26107, upload-time = "2025-12-11T13:36:47.015Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/76/76/1fb94367cef64420d2171157a6b9509582873bd09a6afe08a78a8d1f59d9/opentelemetry_instrumentation_asgi-0.60b1-py3-none-any.whl", hash = "sha256:d48def2dbed10294c99cfcf41ebbd0c414d390a11773a41f472d20000fcddc25", size = 16933, upload-time = "2025-12-11T13:35:40.462Z" }, -] - -[[package]] -name = "opentelemetry-instrumentation-celery" -version = "0.60b1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "opentelemetry-api" }, - { name = "opentelemetry-instrumentation" }, - { name = "opentelemetry-semantic-conventions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/88/b3/eb0f83e5ef774fc1d65a9ed1b3dd8fbd8d47ec204029794074b76a116d85/opentelemetry_instrumentation_celery-0.60b1.tar.gz", hash = "sha256:896bb9eda2d7c4a39bbc5bee2caae9c06a3a41ba283bafc414b224bc8a0f04c8", size = 14768, upload-time = "2025-12-11T13:36:52.916Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/34/ae/1b868805cf9a9b72450fc5ff6cb36a15735d68bc71c1dc1ffaf2a5ffdabe/opentelemetry_instrumentation_celery-0.60b1-py3-none-any.whl", hash = "sha256:ee946f85a3e6893d8edf09402c2c773cacc09854dcea35ae2a694320f85403cf", size = 13805, upload-time = "2025-12-11T13:35:53.223Z" }, -] - -[[package]] -name = "opentelemetry-instrumentation-fastapi" -version = "0.60b1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "opentelemetry-api" }, - { name = "opentelemetry-instrumentation" }, - { name = "opentelemetry-instrumentation-asgi" }, - { name = "opentelemetry-semantic-conventions" }, - { name = "opentelemetry-util-http" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/9c/e7/e7e5e50218cf488377209d85666b182fa2d4928bf52389411ceeee1b2b60/opentelemetry_instrumentation_fastapi-0.60b1.tar.gz", hash = "sha256:de608955f7ff8eecf35d056578346a5365015fd7d8623df9b1f08d1c74769c01", size = 24958, upload-time = "2025-12-11T13:36:59.35Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7d/cc/6e808328ba54662e50babdcab21138eae4250bc0fddf67d55526a615a2ca/opentelemetry_instrumentation_fastapi-0.60b1-py3-none-any.whl", hash = "sha256:af94b7a239ad1085fc3a820ecf069f67f579d7faf4c085aaa7bd9b64eafc8eaf", size = 13478, upload-time = "2025-12-11T13:36:00.811Z" }, -] - -[[package]] -name = "opentelemetry-instrumentation-redis" -version = "0.60b1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "opentelemetry-api" }, - { name = "opentelemetry-instrumentation" }, - { name = "opentelemetry-semantic-conventions" }, - { name = "wrapt" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/6a/1e/225364fab4db793f6f5024ed9f3dd53774fd7c7c21fa242460234dcdf8d9/opentelemetry_instrumentation_redis-0.60b1.tar.gz", hash = "sha256:ecafa8f81c88917b59f0d842fb3d157f3a8edc71fb4b85bebca3bc19432ce7b8", size = 14774, upload-time = "2025-12-11T13:37:11.201Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/05/bd/d55d3b34fd49df08d9d9fa3701dff0051b216e2c7e9adaaa4ff6aa1de8d7/opentelemetry_instrumentation_redis-0.60b1-py3-none-any.whl", hash = "sha256:33bef0ff9af6f2d88de90c1cd7e25675c10a16d4f9ee5ae7592b28bb08b78139", size = 15502, upload-time = "2025-12-11T13:36:21.481Z" }, -] - -[[package]] -name = "opentelemetry-instrumentation-requests" -version = "0.60b1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "opentelemetry-api" }, - { name = "opentelemetry-instrumentation" }, - { name = "opentelemetry-semantic-conventions" }, - { name = "opentelemetry-util-http" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/9d/4a/bb9d47d7424fc33aeba75275256ae6e6031f44b6a9a3f778d611c0c3ac27/opentelemetry_instrumentation_requests-0.60b1.tar.gz", hash = "sha256:9a1063c16c44a3ba6e81870c4fa42a0fac3ecef5a4d60a11d0976eec9046f3d4", size = 16366, upload-time = "2025-12-11T13:37:12.456Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f2/7f/969b59a5acccb4c35317421843d63d7853ad7a18078ca3a9b80c248be448/opentelemetry_instrumentation_requests-0.60b1-py3-none-any.whl", hash = "sha256:eec9fac3fab84737f663a2e08b12cb095b4bd67643b24587a8ecfa3cf4d0ca4c", size = 13141, upload-time = "2025-12-11T13:36:23.696Z" }, -] - -[[package]] -name = "opentelemetry-instrumentation-starlette" -version = "0.60b1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "opentelemetry-api" }, - { name = "opentelemetry-instrumentation" }, - { name = "opentelemetry-instrumentation-asgi" }, - { name = "opentelemetry-semantic-conventions" }, - { name = "opentelemetry-util-http" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/39/5e/7e5c97ea0d4dcf735fea4d0e8cd91974bcb7d13436cf3b7c85244cf2ace5/opentelemetry_instrumentation_starlette-0.60b1.tar.gz", hash = "sha256:282a25339acd8885e64f7dbaf3efb0e4b9f0bde04b9987ba846ba73d50978faa", size = 14643, upload-time = "2025-12-11T13:37:14.311Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b7/1a/246dd7fcf7dd6c399771e966689cc02d53d6db271f3d3161ca2a755d50c8/opentelemetry_instrumentation_starlette-0.60b1-py3-none-any.whl", hash = "sha256:a5bcf8c75da0501b5c6abb1ea53be699be22698229df59c8478be93ae2e486a8", size = 11765, upload-time = "2025-12-11T13:36:26.693Z" }, -] - [[package]] name = "opentelemetry-proto" version = "1.39.0" @@ -1446,15 +994,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7a/5e/5958555e09635d09b75de3c4f8b9cae7335ca545d77392ffe7331534c402/opentelemetry_semantic_conventions-0.60b1-py3-none-any.whl", hash = "sha256:9fa8c8b0c110da289809292b0591220d3a7b53c1526a23021e977d68597893fb", size = 219982, upload-time = "2025-12-11T13:32:36.955Z" }, ] -[[package]] -name = "opentelemetry-util-http" -version = "0.60b1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/50/fc/c47bb04a1d8a941a4061307e1eddfa331ed4d0ab13d8a9781e6db256940a/opentelemetry_util_http-0.60b1.tar.gz", hash = "sha256:0d97152ca8c8a41ced7172d29d3622a219317f74ae6bb3027cfbdcf22c3cc0d6", size = 11053, upload-time = "2025-12-11T13:37:25.115Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/16/5c/d3f1733665f7cd582ef0842fb1d2ed0bc1fba10875160593342d22bba375/opentelemetry_util_http-0.60b1-py3-none-any.whl", hash = "sha256:66381ba28550c91bee14dcba8979ace443444af1ed609226634596b4b0faf199", size = 8947, upload-time = "2025-12-11T13:36:37.151Z" }, -] - [[package]] name = "packaging" version = "25.0" @@ -1507,18 +1046,6 @@ 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.52" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "wcwidth" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/a1/96/06e01a7b38dce6fe1db213e061a4602dd6032a8a97ef6c1a862537732421/prompt_toolkit-3.0.52.tar.gz", hash = "sha256:28cde192929c8e7321de85de1ddbe736f1375148b02f2e17edd840042b1be855", size = 434198, upload-time = "2025-08-27T15:24:02.057Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl", hash = "sha256:9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955", size = 391431, upload-time = "2025-08-27T15:23:59.498Z" }, -] - [[package]] name = "protobuf" version = "6.33.2" @@ -1592,15 +1119,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/72/f7/212343c1c9cfac35fd943c527af85e9091d633176e2a407a0797856ff7b9/psycopg_binary-3.3.2-cp314-cp314-win_amd64.whl", hash = "sha256:04bb2de4ba69d6f8395b446ede795e8884c040ec71d01dd07ac2b2d18d4153d1", size = 3642122, upload-time = "2025-12-06T17:34:52.506Z" }, ] -[[package]] -name = "pycparser" -version = "2.23" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fe/cf/d2d3b9f5699fb1e4615c8e32ff220203e43b248e1dfcc6736ad9057731ca/pycparser-2.23.tar.gz", hash = "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2", size = 173734, upload-time = "2025-09-09T13:23:47.91Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934", size = 118140, upload-time = "2025-09-09T13:23:46.651Z" }, -] - [[package]] name = "pydantic" version = "2.12.5" @@ -1786,18 +1304,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5a/cc/06253936f4a7fa2e0f48dfe6d851d9c56df896a9ab09ac019d70b760619c/pytest_mock-3.15.1-py3-none-any.whl", hash = "sha256:0a25e2eb88fe5168d535041d09a4529a188176ae608a6d249ee65abc0949630d", size = 10095, upload-time = "2025-09-16T16:37:25.734Z" }, ] -[[package]] -name = "python-dateutil" -version = "2.9.0.post0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "six" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, -] - [[package]] name = "python-dotenv" version = "1.2.1" @@ -1807,19 +1313,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61", size = 21230, upload-time = "2025-10-26T15:12:09.109Z" }, ] -[[package]] -name = "python-gitlab" -version = "7.0.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "requests" }, - { name = "requests-toolbelt" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/5e/c4/0b613303b4f0fcda69b3d2e03d0a1fb1b6b079a7c7832e03a8d92461e9fe/python_gitlab-7.0.0.tar.gz", hash = "sha256:e4d934430f64efc09e6208b782c61cc0a3389527765e03ffbef17f4323dce441", size = 400568, upload-time = "2025-10-29T15:06:02.069Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/4f/9e/811edc46a15f8deb828cba7ef8aab3451dc11ca72d033f3df72a5af865d9/python_gitlab-7.0.0-py3-none-any.whl", hash = "sha256:712a6c8c5e79e7e66f6dabb25d8fe7831a6b238d4a5132f8231df6b3b890ceff", size = 144415, upload-time = "2025-10-29T15:06:00.232Z" }, -] - [[package]] name = "python-multipart" version = "0.0.22" @@ -1875,15 +1368,6 @@ 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 = "redis" -version = "7.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/43/c8/983d5c6579a411d8a99bc5823cc5712768859b5ce2c8afe1a65b37832c81/redis-7.1.0.tar.gz", hash = "sha256:b1cc3cfa5a2cb9c2ab3ba700864fb0ad75617b41f01352ce5779dabf6d5f9c3c", size = 4796669, upload-time = "2025-11-19T15:54:39.961Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/89/f0/8956f8a86b20d7bb9d6ac0187cf4cd54d8065bc9a1a09eb8011d4d326596/redis-7.1.0-py3-none-any.whl", hash = "sha256:23c52b208f92b56103e17c5d06bdc1a6c2c0b3106583985a76a18f83b265de2b", size = 354159, upload-time = "2025-11-19T15:54:38.064Z" }, -] - [[package]] name = "requests" version = "2.32.5" @@ -1899,18 +1383,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, ] -[[package]] -name = "requests-toolbelt" -version = "1.0.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "requests" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/f3/61/d7545dafb7ac2230c70d38d31cbfe4cc64f7144dc41f6e4e4b78ecd9f5bb/requests-toolbelt-1.0.0.tar.gz", hash = "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6", size = 206888, upload-time = "2023-05-01T04:11:33.229Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3f/51/d4db610ef29373b879047326cbf6fa98b6c1969d6f6dc423279de2b1be2c/requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06", size = 54481, upload-time = "2023-05-01T04:11:28.427Z" }, -] - [[package]] name = "respx" version = "0.22.0" @@ -2060,7 +1532,7 @@ wheels = [ [[package]] name = "shared" version = "0.0.0" -source = { editable = "middleware/shared" } +source = { git = "https://github.com/fairagro/m4.2_advanced_middleware_api.git?subdirectory=middleware%2Fshared&branch=feature%2Fsplit_off_client#3a8cde5565e2eef861382ed63b8f356227a632ad" } dependencies = [ { name = "opentelemetry-api" }, { name = "opentelemetry-exporter-otlp" }, @@ -2069,15 +1541,6 @@ dependencies = [ { name = "pyyaml" }, ] -[package.metadata] -requires-dist = [ - { name = "opentelemetry-api", specifier = ">=1.26.0" }, - { name = "opentelemetry-exporter-otlp", specifier = ">=1.26.0" }, - { name = "opentelemetry-sdk", specifier = ">=1.26.0" }, - { name = "pydantic", specifier = ">=2.12.5" }, - { name = "pyyaml", specifier = ">=6.0.3" }, -] - [[package]] name = "shellingham" version = "1.5.4" @@ -2087,24 +1550,6 @@ 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]] -name = "six" -version = "1.17.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, -] - -[[package]] -name = "smmap" -version = "5.0.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/44/cd/a040c4b3119bbe532e5b0732286f805445375489fceaec1f48306068ee3b/smmap-5.0.2.tar.gz", hash = "sha256:26ea65a03958fa0c8a1c7e8c7a58fdc77221b8910f6be2131affade476898ad5", size = 22329, upload-time = "2025-01-02T07:14:40.909Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/04/be/d09147ad1ec7934636ad912901c5fd7667e1c858e19d355237db0d0cd5e4/smmap-5.0.2-py3-none-any.whl", hash = "sha256:b30115f0def7d7531d22a0fb6502488d879e75b260a9db4d0819cfb25403af5e", size = 24303, upload-time = "2025-01-02T07:14:38.724Z" }, -] - [[package]] name = "sql-to-arc" version = "0.0.0" @@ -2119,24 +1564,11 @@ dependencies = [ [package.metadata] requires-dist = [ - { name = "api-client", editable = "middleware/api_client" }, + { name = "api-client", git = "https://github.com/fairagro/m4.2_advanced_middleware_api.git?subdirectory=middleware%2Fapi_client&branch=feature%2Fsplit_off_client" }, { name = "arctrl", specifier = ">=3.0.0b15" }, { name = "psycopg", extras = ["binary"], specifier = ">=3.3.2" }, { name = "pydantic", specifier = ">=2.12.5" }, - { name = "shared", editable = "middleware/shared" }, -] - -[[package]] -name = "sse-starlette" -version = "3.1.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "anyio" }, - { name = "starlette" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/da/34/f5df66cb383efdbf4f2db23cabb27f51b1dcb737efaf8a558f6f1d195134/sse_starlette-3.1.2.tar.gz", hash = "sha256:55eff034207a83a0eb86de9a68099bd0157838f0b8b999a1b742005c71e33618", size = 26303, upload-time = "2025-12-31T08:02:20.023Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b7/95/8c4b76eec9ae574474e5d2997557cebf764bcd3586458956c30631ae08f4/sse_starlette-3.1.2-py3-none-any.whl", hash = "sha256:cd800dd349f4521b317b9391d3796fa97b71748a4da9b9e00aafab32dda375c8", size = 12484, upload-time = "2025-12-31T08:02:18.894Z" }, + { name = "shared", git = "https://github.com/fairagro/m4.2_advanced_middleware_api.git?subdirectory=middleware%2Fshared&branch=feature%2Fsplit_off_client" }, ] [[package]] @@ -2170,17 +1602,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/bd/75/8539d011f6be8e29f339c42e633aae3cb73bffa95dd0f9adec09b9c58e85/tomlkit-0.13.3-py3-none-any.whl", hash = "sha256:c89c649d79ee40629a9fda55f8ace8c6a1b42deb912b2a8fd8d942ddadb606b0", size = 38901, upload-time = "2025-06-05T07:13:43.546Z" }, ] -[[package]] -name = "tools" -version = "0.0.0" -source = { editable = "middleware/tools" } -dependencies = [ - { name = "arctrl" }, -] - -[package.metadata] -requires-dist = [{ name = "arctrl", specifier = ">=3.0.0b15" }] - [[package]] name = "typer" version = "0.20.0" @@ -2196,15 +1617,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/78/64/7713ffe4b5983314e9d436a90d5bd4f63b6054e2aca783a3cfc44cb95bbf/typer-0.20.0-py3-none-any.whl", hash = "sha256:5b463df6793ec1dca6213a3cf4c0f03bc6e322ac5e16e13ddd622a889489784a", size = 47028, upload-time = "2025-10-20T17:03:47.617Z" }, ] -[[package]] -name = "types-pyyaml" -version = "6.0.12.20250915" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7e/69/3c51b36d04da19b92f9e815be12753125bd8bc247ba0470a982e6979e71c/types_pyyaml-6.0.12.20250915.tar.gz", hash = "sha256:0f8b54a528c303f0e6f7165687dd33fafa81c807fcac23f632b63aa624ced1d3", size = 17522, upload-time = "2025-09-15T03:01:00.728Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/bd/e0/1eed384f02555dde685fff1a1ac805c1c7dcb6dd019c916fe659b1c1f9ec/types_pyyaml-6.0.12.20250915-py3-none-any.whl", hash = "sha256:e7d4d9e064e89a3b3cae120b4990cd370874d2bf12fa5f46c97018dd5d3c9ab6", size = 20338, upload-time = "2025-09-15T03:00:59.218Z" }, -] - [[package]] name = "typing-extensions" version = "4.15.0" @@ -2235,18 +1647,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839, upload-time = "2025-03-23T13:54:41.845Z" }, ] -[[package]] -name = "tzlocal" -version = "5.3.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "tzdata", marker = "sys_platform == 'win32'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/8b/2e/c14812d3d4d9cd1773c6be938f89e5735a1f11a9f184ac3639b93cef35d5/tzlocal-5.3.1.tar.gz", hash = "sha256:cceffc7edecefea1f595541dbd6e990cb1ea3d19bf01b2809f362a03dd7921fd", size = 30761, upload-time = "2025-03-05T21:17:41.549Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c2/14/e2a54fabd4f08cd7af1c07030603c3356b74da07f7cc056e600436edfa17/tzlocal-5.3.1-py3-none-any.whl", hash = "sha256:eb1a66c3ef5847adf7a834f1be0800581b683b5608e74f86ecbcef8ab91bb85d", size = 18026, upload-time = "2025-03-05T21:17:39.857Z" }, -] - [[package]] name = "urllib3" version = "2.6.3" @@ -2312,15 +1712,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e4/16/c1fd27e9549f3c4baf1dc9c20c456cd2f822dbf8de9f463824b0c0357e06/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6cde23eeda1a25c75b2e07d39970f3374105d5eafbaab2a4482be82f272d5a5e", size = 4296730, upload-time = "2025-10-16T22:17:00.744Z" }, ] -[[package]] -name = "vine" -version = "5.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/bd/e4/d07b5f29d283596b9727dd5275ccbceb63c44a1a82aa9e4bfd20426762ac/vine-5.1.0.tar.gz", hash = "sha256:8b62e981d35c41049211cf62a0a1242d8c1ee9bd15bb196ce38aefd6799e61e0", size = 48980, upload-time = "2023-11-05T08:46:53.857Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/03/ff/7c0c86c43b3cbb927e0ccc0255cb4057ceba4799cd44ae95174ce8e8b5b2/vine-5.1.0-py3-none-any.whl", hash = "sha256:40fdf3c48b2cfe1c38a49e9ae2da6fda88e4794c810050a728bd7413811fb1dc", size = 9636, upload-time = "2023-11-05T08:46:51.205Z" }, -] - [[package]] name = "virtualenv" version = "20.36.1" @@ -2405,15 +1796,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e3/bd/fa9bb053192491b3867ba07d2343d9f2252e00811567d30ae8d0f78136fe/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:a916a2932da8f8ab582f242c065f5c81bed3462849ca79ee357dd9551b0e9b01", size = 622112, upload-time = "2025-10-14T15:05:50.941Z" }, ] -[[package]] -name = "wcwidth" -version = "0.2.14" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/24/30/6b0809f4510673dc723187aeaf24c7f5459922d01e2f794277a3dfb90345/wcwidth-0.2.14.tar.gz", hash = "sha256:4d478375d31bc5395a3c55c40ccdf3354688364cd61c4f6adacaa9215d0b3605", size = 102293, upload-time = "2025-09-22T16:29:53.023Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/af/b5/123f13c975e9f27ab9c0770f514345bd406d0e8d3b7a0723af9d43f710af/wcwidth-0.2.14-py2.py3-none-any.whl", hash = "sha256:a7bb560c8aee30f9957e5f9895805edd20602f2d7f720186dfd906e82b4982e1", size = 37286, upload-time = "2025-09-22T16:29:51.641Z" }, -] - [[package]] name = "websockets" version = "15.0.1" @@ -2445,55 +1827,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743, upload-time = "2025-03-05T20:03:39.41Z" }, ] -[[package]] -name = "wrapt" -version = "1.17.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/95/8f/aeb76c5b46e273670962298c23e7ddde79916cb74db802131d49a85e4b7d/wrapt-1.17.3.tar.gz", hash = "sha256:f66eb08feaa410fe4eebd17f2a2c8e2e46d3476e9f8c783daa8e09e0faa666d0", size = 55547, upload-time = "2025-08-12T05:53:21.714Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9f/41/cad1aba93e752f1f9268c77270da3c469883d56e2798e7df6240dcb2287b/wrapt-1.17.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ab232e7fdb44cdfbf55fc3afa31bcdb0d8980b9b95c38b6405df2acb672af0e0", size = 53998, upload-time = "2025-08-12T05:51:47.138Z" }, - { url = "https://files.pythonhosted.org/packages/60/f8/096a7cc13097a1869fe44efe68dace40d2a16ecb853141394047f0780b96/wrapt-1.17.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9baa544e6acc91130e926e8c802a17f3b16fbea0fd441b5a60f5cf2cc5c3deba", size = 39020, upload-time = "2025-08-12T05:51:35.906Z" }, - { url = "https://files.pythonhosted.org/packages/33/df/bdf864b8997aab4febb96a9ae5c124f700a5abd9b5e13d2a3214ec4be705/wrapt-1.17.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6b538e31eca1a7ea4605e44f81a48aa24c4632a277431a6ed3f328835901f4fd", size = 39098, upload-time = "2025-08-12T05:51:57.474Z" }, - { url = "https://files.pythonhosted.org/packages/9f/81/5d931d78d0eb732b95dc3ddaeeb71c8bb572fb01356e9133916cd729ecdd/wrapt-1.17.3-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:042ec3bb8f319c147b1301f2393bc19dba6e176b7da446853406d041c36c7828", size = 88036, upload-time = "2025-08-12T05:52:34.784Z" }, - { url = "https://files.pythonhosted.org/packages/ca/38/2e1785df03b3d72d34fc6252d91d9d12dc27a5c89caef3335a1bbb8908ca/wrapt-1.17.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3af60380ba0b7b5aeb329bc4e402acd25bd877e98b3727b0135cb5c2efdaefe9", size = 88156, upload-time = "2025-08-12T05:52:13.599Z" }, - { url = "https://files.pythonhosted.org/packages/b3/8b/48cdb60fe0603e34e05cffda0b2a4adab81fd43718e11111a4b0100fd7c1/wrapt-1.17.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0b02e424deef65c9f7326d8c19220a2c9040c51dc165cddb732f16198c168396", size = 87102, upload-time = "2025-08-12T05:52:14.56Z" }, - { url = "https://files.pythonhosted.org/packages/3c/51/d81abca783b58f40a154f1b2c56db1d2d9e0d04fa2d4224e357529f57a57/wrapt-1.17.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:74afa28374a3c3a11b3b5e5fca0ae03bef8450d6aa3ab3a1e2c30e3a75d023dc", size = 87732, upload-time = "2025-08-12T05:52:36.165Z" }, - { url = "https://files.pythonhosted.org/packages/9e/b1/43b286ca1392a006d5336412d41663eeef1ad57485f3e52c767376ba7e5a/wrapt-1.17.3-cp312-cp312-win32.whl", hash = "sha256:4da9f45279fff3543c371d5ababc57a0384f70be244de7759c85a7f989cb4ebe", size = 36705, upload-time = "2025-08-12T05:53:07.123Z" }, - { url = "https://files.pythonhosted.org/packages/28/de/49493f962bd3c586ab4b88066e967aa2e0703d6ef2c43aa28cb83bf7b507/wrapt-1.17.3-cp312-cp312-win_amd64.whl", hash = "sha256:e71d5c6ebac14875668a1e90baf2ea0ef5b7ac7918355850c0908ae82bcb297c", size = 38877, upload-time = "2025-08-12T05:53:05.436Z" }, - { url = "https://files.pythonhosted.org/packages/f1/48/0f7102fe9cb1e8a5a77f80d4f0956d62d97034bbe88d33e94699f99d181d/wrapt-1.17.3-cp312-cp312-win_arm64.whl", hash = "sha256:604d076c55e2fdd4c1c03d06dc1a31b95130010517b5019db15365ec4a405fc6", size = 36885, upload-time = "2025-08-12T05:52:54.367Z" }, - { url = "https://files.pythonhosted.org/packages/fc/f6/759ece88472157acb55fc195e5b116e06730f1b651b5b314c66291729193/wrapt-1.17.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a47681378a0439215912ef542c45a783484d4dd82bac412b71e59cf9c0e1cea0", size = 54003, upload-time = "2025-08-12T05:51:48.627Z" }, - { url = "https://files.pythonhosted.org/packages/4f/a9/49940b9dc6d47027dc850c116d79b4155f15c08547d04db0f07121499347/wrapt-1.17.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:54a30837587c6ee3cd1a4d1c2ec5d24e77984d44e2f34547e2323ddb4e22eb77", size = 39025, upload-time = "2025-08-12T05:51:37.156Z" }, - { url = "https://files.pythonhosted.org/packages/45/35/6a08de0f2c96dcdd7fe464d7420ddb9a7655a6561150e5fc4da9356aeaab/wrapt-1.17.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:16ecf15d6af39246fe33e507105d67e4b81d8f8d2c6598ff7e3ca1b8a37213f7", size = 39108, upload-time = "2025-08-12T05:51:58.425Z" }, - { url = "https://files.pythonhosted.org/packages/0c/37/6faf15cfa41bf1f3dba80cd3f5ccc6622dfccb660ab26ed79f0178c7497f/wrapt-1.17.3-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6fd1ad24dc235e4ab88cda009e19bf347aabb975e44fd5c2fb22a3f6e4141277", size = 88072, upload-time = "2025-08-12T05:52:37.53Z" }, - { url = "https://files.pythonhosted.org/packages/78/f2/efe19ada4a38e4e15b6dff39c3e3f3f73f5decf901f66e6f72fe79623a06/wrapt-1.17.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ed61b7c2d49cee3c027372df5809a59d60cf1b6c2f81ee980a091f3afed6a2d", size = 88214, upload-time = "2025-08-12T05:52:15.886Z" }, - { url = "https://files.pythonhosted.org/packages/40/90/ca86701e9de1622b16e09689fc24b76f69b06bb0150990f6f4e8b0eeb576/wrapt-1.17.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:423ed5420ad5f5529db9ce89eac09c8a2f97da18eb1c870237e84c5a5c2d60aa", size = 87105, upload-time = "2025-08-12T05:52:17.914Z" }, - { url = "https://files.pythonhosted.org/packages/fd/e0/d10bd257c9a3e15cbf5523025252cc14d77468e8ed644aafb2d6f54cb95d/wrapt-1.17.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e01375f275f010fcbf7f643b4279896d04e571889b8a5b3f848423d91bf07050", size = 87766, upload-time = "2025-08-12T05:52:39.243Z" }, - { url = "https://files.pythonhosted.org/packages/e8/cf/7d848740203c7b4b27eb55dbfede11aca974a51c3d894f6cc4b865f42f58/wrapt-1.17.3-cp313-cp313-win32.whl", hash = "sha256:53e5e39ff71b3fc484df8a522c933ea2b7cdd0d5d15ae82e5b23fde87d44cbd8", size = 36711, upload-time = "2025-08-12T05:53:10.074Z" }, - { url = "https://files.pythonhosted.org/packages/57/54/35a84d0a4d23ea675994104e667ceff49227ce473ba6a59ba2c84f250b74/wrapt-1.17.3-cp313-cp313-win_amd64.whl", hash = "sha256:1f0b2f40cf341ee8cc1a97d51ff50dddb9fcc73241b9143ec74b30fc4f44f6cb", size = 38885, upload-time = "2025-08-12T05:53:08.695Z" }, - { url = "https://files.pythonhosted.org/packages/01/77/66e54407c59d7b02a3c4e0af3783168fff8e5d61def52cda8728439d86bc/wrapt-1.17.3-cp313-cp313-win_arm64.whl", hash = "sha256:7425ac3c54430f5fc5e7b6f41d41e704db073309acfc09305816bc6a0b26bb16", size = 36896, upload-time = "2025-08-12T05:52:55.34Z" }, - { url = "https://files.pythonhosted.org/packages/02/a2/cd864b2a14f20d14f4c496fab97802001560f9f41554eef6df201cd7f76c/wrapt-1.17.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cf30f6e3c077c8e6a9a7809c94551203c8843e74ba0c960f4a98cd80d4665d39", size = 54132, upload-time = "2025-08-12T05:51:49.864Z" }, - { url = "https://files.pythonhosted.org/packages/d5/46/d011725b0c89e853dc44cceb738a307cde5d240d023d6d40a82d1b4e1182/wrapt-1.17.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e228514a06843cae89621384cfe3a80418f3c04aadf8a3b14e46a7be704e4235", size = 39091, upload-time = "2025-08-12T05:51:38.935Z" }, - { url = "https://files.pythonhosted.org/packages/2e/9e/3ad852d77c35aae7ddebdbc3b6d35ec8013af7d7dddad0ad911f3d891dae/wrapt-1.17.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:5ea5eb3c0c071862997d6f3e02af1d055f381b1d25b286b9d6644b79db77657c", size = 39172, upload-time = "2025-08-12T05:51:59.365Z" }, - { url = "https://files.pythonhosted.org/packages/c3/f7/c983d2762bcce2326c317c26a6a1e7016f7eb039c27cdf5c4e30f4160f31/wrapt-1.17.3-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:281262213373b6d5e4bb4353bc36d1ba4084e6d6b5d242863721ef2bf2c2930b", size = 87163, upload-time = "2025-08-12T05:52:40.965Z" }, - { url = "https://files.pythonhosted.org/packages/e4/0f/f673f75d489c7f22d17fe0193e84b41540d962f75fce579cf6873167c29b/wrapt-1.17.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc4a8d2b25efb6681ecacad42fca8859f88092d8732b170de6a5dddd80a1c8fa", size = 87963, upload-time = "2025-08-12T05:52:20.326Z" }, - { url = "https://files.pythonhosted.org/packages/df/61/515ad6caca68995da2fac7a6af97faab8f78ebe3bf4f761e1b77efbc47b5/wrapt-1.17.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:373342dd05b1d07d752cecbec0c41817231f29f3a89aa8b8843f7b95992ed0c7", size = 86945, upload-time = "2025-08-12T05:52:21.581Z" }, - { url = "https://files.pythonhosted.org/packages/d3/bd/4e70162ce398462a467bc09e768bee112f1412e563620adc353de9055d33/wrapt-1.17.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d40770d7c0fd5cbed9d84b2c3f2e156431a12c9a37dc6284060fb4bec0b7ffd4", size = 86857, upload-time = "2025-08-12T05:52:43.043Z" }, - { url = "https://files.pythonhosted.org/packages/2b/b8/da8560695e9284810b8d3df8a19396a6e40e7518059584a1a394a2b35e0a/wrapt-1.17.3-cp314-cp314-win32.whl", hash = "sha256:fbd3c8319de8e1dc79d346929cd71d523622da527cca14e0c1d257e31c2b8b10", size = 37178, upload-time = "2025-08-12T05:53:12.605Z" }, - { url = "https://files.pythonhosted.org/packages/db/c8/b71eeb192c440d67a5a0449aaee2310a1a1e8eca41676046f99ed2487e9f/wrapt-1.17.3-cp314-cp314-win_amd64.whl", hash = "sha256:e1a4120ae5705f673727d3253de3ed0e016f7cd78dc463db1b31e2463e1f3cf6", size = 39310, upload-time = "2025-08-12T05:53:11.106Z" }, - { url = "https://files.pythonhosted.org/packages/45/20/2cda20fd4865fa40f86f6c46ed37a2a8356a7a2fde0773269311f2af56c7/wrapt-1.17.3-cp314-cp314-win_arm64.whl", hash = "sha256:507553480670cab08a800b9463bdb881b2edeed77dc677b0a5915e6106e91a58", size = 37266, upload-time = "2025-08-12T05:52:56.531Z" }, - { url = "https://files.pythonhosted.org/packages/77/ed/dd5cf21aec36c80443c6f900449260b80e2a65cf963668eaef3b9accce36/wrapt-1.17.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:ed7c635ae45cfbc1a7371f708727bf74690daedc49b4dba310590ca0bd28aa8a", size = 56544, upload-time = "2025-08-12T05:51:51.109Z" }, - { url = "https://files.pythonhosted.org/packages/8d/96/450c651cc753877ad100c7949ab4d2e2ecc4d97157e00fa8f45df682456a/wrapt-1.17.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:249f88ed15503f6492a71f01442abddd73856a0032ae860de6d75ca62eed8067", size = 40283, upload-time = "2025-08-12T05:51:39.912Z" }, - { url = "https://files.pythonhosted.org/packages/d1/86/2fcad95994d9b572db57632acb6f900695a648c3e063f2cd344b3f5c5a37/wrapt-1.17.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a03a38adec8066d5a37bea22f2ba6bbf39fcdefbe2d91419ab864c3fb515454", size = 40366, upload-time = "2025-08-12T05:52:00.693Z" }, - { url = "https://files.pythonhosted.org/packages/64/0e/f4472f2fdde2d4617975144311f8800ef73677a159be7fe61fa50997d6c0/wrapt-1.17.3-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5d4478d72eb61c36e5b446e375bbc49ed002430d17cdec3cecb36993398e1a9e", size = 108571, upload-time = "2025-08-12T05:52:44.521Z" }, - { url = "https://files.pythonhosted.org/packages/cc/01/9b85a99996b0a97c8a17484684f206cbb6ba73c1ce6890ac668bcf3838fb/wrapt-1.17.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:223db574bb38637e8230eb14b185565023ab624474df94d2af18f1cdb625216f", size = 113094, upload-time = "2025-08-12T05:52:22.618Z" }, - { url = "https://files.pythonhosted.org/packages/25/02/78926c1efddcc7b3aa0bc3d6b33a822f7d898059f7cd9ace8c8318e559ef/wrapt-1.17.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e405adefb53a435f01efa7ccdec012c016b5a1d3f35459990afc39b6be4d5056", size = 110659, upload-time = "2025-08-12T05:52:24.057Z" }, - { url = "https://files.pythonhosted.org/packages/dc/ee/c414501ad518ac3e6fe184753632fe5e5ecacdcf0effc23f31c1e4f7bfcf/wrapt-1.17.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:88547535b787a6c9ce4086917b6e1d291aa8ed914fdd3a838b3539dc95c12804", size = 106946, upload-time = "2025-08-12T05:52:45.976Z" }, - { url = "https://files.pythonhosted.org/packages/be/44/a1bd64b723d13bb151d6cc91b986146a1952385e0392a78567e12149c7b4/wrapt-1.17.3-cp314-cp314t-win32.whl", hash = "sha256:41b1d2bc74c2cac6f9074df52b2efbef2b30bdfe5f40cb78f8ca22963bc62977", size = 38717, upload-time = "2025-08-12T05:53:15.214Z" }, - { url = "https://files.pythonhosted.org/packages/79/d9/7cfd5a312760ac4dd8bf0184a6ee9e43c33e47f3dadc303032ce012b8fa3/wrapt-1.17.3-cp314-cp314t-win_amd64.whl", hash = "sha256:73d496de46cd2cdbdbcce4ae4bcdb4afb6a11234a1df9c085249d55166b95116", size = 41334, upload-time = "2025-08-12T05:53:14.178Z" }, - { url = "https://files.pythonhosted.org/packages/46/78/10ad9781128ed2f99dbc474f43283b13fea8ba58723e98844367531c18e9/wrapt-1.17.3-cp314-cp314t-win_arm64.whl", hash = "sha256:f38e60678850c42461d4202739f9bf1e3a737c7ad283638251e79cc49effb6b6", size = 38471, upload-time = "2025-08-12T05:52:57.784Z" }, - { url = "https://files.pythonhosted.org/packages/1f/f6/a933bd70f98e9cf3e08167fc5cd7aaaca49147e48411c0bd5ae701bb2194/wrapt-1.17.3-py3-none-any.whl", hash = "sha256:7171ae35d2c33d326ac19dd8facb1e82e5fd04ef8c6c0e394d7af55a55051c22", size = 23591, upload-time = "2025-08-12T05:53:20.674Z" }, -] - [[package]] name = "zipp" version = "3.23.0" From 94cff12b95794e71369bf92230eaf3d06b5b795b Mon Sep 17 00:00:00 2001 From: Carsten Scharfenberg Date: Thu, 29 Jan 2026 13:02:01 +0000 Subject: [PATCH 2/9] feat: enable git-lfs feature in devcontainer.json --- .devcontainer/devcontainer.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 7fefa48..d453576 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -14,11 +14,11 @@ }, // NOTE: this conflicts with out own pre-push git hook. // Currently we do not need git-lfs in this project, anyway. - // "ghcr.io/devcontainers/features/git-lfs:1": { - // "autoPull": true, - // "installDirectlyFromGitHubRelease": true, - // "version": "3.7.1" - // }, + "ghcr.io/devcontainers/features/git-lfs:1": { + "autoPull": true, + "installDirectlyFromGitHubRelease": true, + "version": "3.7.1" + }, "ghcr.io/devcontainers/features/github-cli:1": { "installDirectlyFromGitHubRelease": true, "version": "2.82.1" From 62be033df7cd1f9532a85282d1e013c3d322c775 Mon Sep 17 00:00:00 2001 From: Carsten Scharfenberg Date: Thu, 29 Jan 2026 16:25:40 +0000 Subject: [PATCH 3/9] Refactor code structure for improved readability and maintainability --- dev_environment/start.sh | 8 +- docker/Dockerfile.sql_to_arc | 49 +- middleware/sql_to_arc/pyproject.toml | 6 +- pyproject.toml | 1 - uv.lock | 967 ++++++--------------------- 5 files changed, 213 insertions(+), 818 deletions(-) diff --git a/dev_environment/start.sh b/dev_environment/start.sh index 70e7a26..c292575 100755 --- a/dev_environment/start.sh +++ b/dev_environment/start.sh @@ -21,7 +21,7 @@ fi echo "==> Starting SQL-to-ARC with EXTERNAL API..." echo " - Local PostgreSQL will be started" echo " - Database will be initialized with Edaphobase dump" -echo " - SQL-to-ARC will connect to the API configured in config-external.yaml" +echo " - SQL-to-ARC will connect to the API configured in config.yaml" echo " - Using client certificates: client.crt, client.key" echo "" @@ -33,9 +33,9 @@ fi # Use sops exec-env to pass the decrypted key as an environment variable # without writing it to a physical disk file. sops exec-env "${script_dir}/client.key" \ - "docker compose -f compose-external.yaml up $BUILD_FLAG" + "docker compose -f compose.yaml up $BUILD_FLAG" echo "" echo "==> Services finished!" -echo " - View logs: docker compose -f compose-external.yaml logs" -echo " - Clean up: docker compose -f compose-external.yaml down" +echo " - View logs: docker compose -f compose.yaml logs" +echo " - Clean up: docker compose -f compose.yaml down" diff --git a/docker/Dockerfile.sql_to_arc b/docker/Dockerfile.sql_to_arc index 7b2f2f1..05f438e 100644 --- a/docker/Dockerfile.sql_to_arc +++ b/docker/Dockerfile.sql_to_arc @@ -1,57 +1,40 @@ -# ---- Package Build Stage ---- -FROM python:3.12.12-alpine3.22 AS package-builder - -WORKDIR /build - -# Copy project files needed for package build -COPY pyproject.toml uv.lock ./ -COPY middleware ./middleware - -# Upgrade pip and install uv -RUN pip install --no-cache-dir --upgrade pip==25.0.1 uv==0.9.10 - -# Build shared package first -RUN uv build --package shared --wheel - -# Build the api client package as wheel -RUN uv build --package api_client --wheel - -# Build the sql_to_arc package as wheel -RUN uv build --package sql_to_arc --wheel - # ---- Binary Build Stage ---- -FROM python:3.12.12-alpine3.22 AS binary-builder +FROM python:3.12.12-alpine3.23 AS binary-builder # Install build tools for PyInstaller RUN apk add --no-cache \ build-base=0.5-r3 \ python3-dev=3.12.12-r0 \ - libffi-dev=3.4.8-r0 \ - openssl-dev=3.5.4-r0 \ - cargo=1.87.0-r0 + libffi-dev=3.5.2-r0 \ + openssl-dev=3.5.5-r0 \ + cargo=1.91.1-r0 \ + git=2.52.0-r0 WORKDIR /build # Install uv and PyInstaller -RUN pip install --no-cache-dir --upgrade pip==25.0.1 uv==0.9.10 - -# Copy built wheel from package-builder stage -COPY --from=package-builder /build/dist/*.whl /tmp/wheels/ +RUN pip install --no-cache-dir --upgrade pip==25.3 uv==0.9.27 -# Install the API package from wheel -RUN uv pip install --system /tmp/wheels/*.whl +# Copy project files needed for package build +COPY pyproject.toml uv.lock ./ +COPY middleware ./middleware # Install PyInstaller RUN uv pip install --system pyinstaller +# Install further dependencies for sql_to_arc +RUN uv sync + # Build standalone binary with PyInstaller RUN pyinstaller --onedir \ --name sql_to_arc \ - $(python -c "import middleware.sql_to_arc; print(middleware.sql_to_arc.__file__.replace('__init__.py', 'main.py'))") + --paths .venv/lib/python3.12/site-packages \ + --paths /build/middleware/sql_to_arc/src \ + /build/middleware/sql_to_arc/src/middleware/sql_to_arc/main.py # ---- Runtime Stage ---- -FROM alpine:3.22.2 +FROM alpine:3.23.3 WORKDIR /middleware diff --git a/middleware/sql_to_arc/pyproject.toml b/middleware/sql_to_arc/pyproject.toml index 75b8ad6..b784ce6 100644 --- a/middleware/sql_to_arc/pyproject.toml +++ b/middleware/sql_to_arc/pyproject.toml @@ -8,8 +8,10 @@ dependencies = [ "arctrl>=3.0.0b15", "psycopg[binary]>=3.3.2", "pydantic>=2.12.5", - "shared", - "api_client", + "shared>=0.0.1", + "api_client>=0.0.1", + "opentelemetry-api>=1.39.1", + "opentelemetry-sdk>=1.39.1" ] [tool.uv.sources] diff --git a/pyproject.toml b/pyproject.toml index 7db9904..d2abfdc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,7 +10,6 @@ dependencies = [ [dependency-groups] dev = [ - "fastapi[standard]>=0.124.0", "httpx>=0.28.1", "pylint>=4.0.4", "pytest>=9.0.2", diff --git a/uv.lock b/uv.lock index 1bfa41a..4c9b568 100644 --- a/uv.lock +++ b/uv.lock @@ -12,15 +12,6 @@ members = [ "sql-to-arc", ] -[[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]] name = "annotated-types" version = "0.7.0" @@ -32,21 +23,21 @@ wheels = [ [[package]] name = "anyio" -version = "4.12.0" +version = "4.12.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "idna" }, { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/16/ce/8a777047513153587e5434fd752e89334ac33e379aa3497db860eeb60377/anyio-4.12.0.tar.gz", hash = "sha256:73c693b567b0c55130c104d0b43a9baf3aa6a31fc6110116509f27bf75e21ec0", size = 228266, upload-time = "2025-11-28T23:37:38.911Z" } +sdist = { url = "https://files.pythonhosted.org/packages/96/f0/5eb65b2bb0d09ac6776f2eb54adee6abe8228ea05b20a5ad0e4945de8aac/anyio-4.12.1.tar.gz", hash = "sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703", size = 228685, upload-time = "2026-01-06T11:45:21.246Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7f/9c/36c5c37947ebfb8c7f22e0eb6e4d188ee2d53aa3880f3f2744fb894f0cb1/anyio-4.12.0-py3-none-any.whl", hash = "sha256:dad2376a628f98eeca4881fc56cd06affd18f659b17a747d3ff0307ced94b1bb", size = 113362, upload-time = "2025-11-28T23:36:57.897Z" }, + { url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592, upload-time = "2026-01-06T11:45:19.497Z" }, ] [[package]] name = "api-client" -version = "0.0.0" -source = { git = "https://github.com/fairagro/m4.2_advanced_middleware_api.git?subdirectory=middleware%2Fapi_client&branch=feature%2Fsplit_off_client#3a8cde5565e2eef861382ed63b8f356227a632ad" } +version = "0.0.1" +source = { git = "https://github.com/fairagro/m4.2_advanced_middleware_api.git?subdirectory=middleware%2Fapi_client&branch=feature%2Fsplit_off_client#2028725e3e4c403fe110911df7f0f8bd7b5ee8a4" } dependencies = [ { name = "httpx" }, { name = "pydantic" }, @@ -54,24 +45,24 @@ dependencies = [ [[package]] name = "arctrl" -version = "3.0.0b15" +version = "3.0.0b16" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "openpyxl" }, { name = "requests" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/6b/5b/2a7f447ab2a001e5d38d702bc21503591f2929f38f7acb296fd144d7af85/arctrl-3.0.0b15.tar.gz", hash = "sha256:ce71b8a1766eb717526f4a1b89f22eac0f362ec5191f8f4192e9b3fdc21b5dbd", size = 562143, upload-time = "2025-12-04T18:18:43.899Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7a/d8/ce6b5642a6cbebd7c7cfe1b4818f0695108bc37f78b62c190ff2fcccf689/arctrl-3.0.0b16.tar.gz", hash = "sha256:a4ea987933ad78be485934c46bcfaa47e329085c6bd13bd0d362c6d9bc9a399d", size = 569631, upload-time = "2026-01-15T13:10:42.315Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a9/41/efe0d77fa28a3e0bd6b96ef72db2c7f7e660b5a6278ab4265036a42bd17b/arctrl-3.0.0b15-py3-none-any.whl", hash = "sha256:7374520c650bdde842b542f4887b98091bb136792b15fd2bd63d29b6fc82be09", size = 842931, upload-time = "2025-12-04T18:18:42.345Z" }, + { url = "https://files.pythonhosted.org/packages/f1/f5/c099065f9139d60c07c41cdb138c2c2c00906c2ff35334fa8ff9dd7cbcda/arctrl-3.0.0b16-py3-none-any.whl", hash = "sha256:9ecc032e7ccad75487c2dbf3208dad8accfc6064e1b56c5f8e013e9eb61fac78", size = 851229, upload-time = "2026-01-15T13:10:40.672Z" }, ] [[package]] name = "astroid" -version = "4.0.2" +version = "4.0.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b7/22/97df040e15d964e592d3a180598ace67e91b7c559d8298bdb3c949dc6e42/astroid-4.0.2.tar.gz", hash = "sha256:ac8fb7ca1c08eb9afec91ccc23edbd8ac73bb22cbdd7da1d488d9fb8d6579070", size = 405714, upload-time = "2025-11-09T21:21:18.373Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/ca/c17d0f83016532a1ad87d1de96837164c99d47a3b6bbba28bd597c25b37a/astroid-4.0.3.tar.gz", hash = "sha256:08d1de40d251cc3dc4a7a12726721d475ac189e4e583d596ece7422bc176bda3", size = 406224, upload-time = "2026-01-03T22:14:26.096Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/93/ac/a85b4bfb4cf53221513e27f33cc37ad158fce02ac291d18bee6b49ab477d/astroid-4.0.2-py3-none-any.whl", hash = "sha256:d7546c00a12efc32650b19a2bb66a153883185d3179ab0d4868086f807338b9b", size = 276354, upload-time = "2025-11-09T21:21:16.54Z" }, + { url = "https://files.pythonhosted.org/packages/ce/66/686ac4fc6ef48f5bacde625adac698f41d5316a9753c2b20bb0931c9d4e2/astroid-4.0.3-py3-none-any.whl", hash = "sha256:864a0a34af1bd70e1049ba1e61cee843a7252c826d97825fcee9b2fcbd9e1b14", size = 276443, upload-time = "2026-01-03T22:14:24.412Z" }, ] [[package]] @@ -91,11 +82,11 @@ wheels = [ [[package]] name = "certifi" -version = "2025.11.12" +version = "2026.1.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a2/8c/58f469717fa48465e4a50c014a0400602d3c437d7c0c468e17ada824da3a/certifi-2025.11.12.tar.gz", hash = "sha256:d8ab5478f2ecd78af242878415affce761ca6bc54a22a27e026d7c25357c3316", size = 160538, upload-time = "2025-11-12T02:54:51.517Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/2d/a891ca51311197f6ad14a7ef42e2399f36cf2f9bd44752b3dc4eab60fdc5/certifi-2026.1.4.tar.gz", hash = "sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120", size = 154268, upload-time = "2026-01-04T02:42:41.825Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/70/7d/9bc192684cea499815ff478dfcdc13835ddf401365057044fb721ec6bddb/certifi-2025.11.12-py3-none-any.whl", hash = "sha256:97de8790030bbd5c2d96b7ec782fc2f7820ef8dba6db909ccf95449f2d062d4b", size = 159438, upload-time = "2025-11-12T02:54:49.735Z" }, + { url = "https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl", hash = "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c", size = 152900, upload-time = "2026-01-04T02:42:40.15Z" }, ] [[package]] @@ -164,18 +155,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" }, ] -[[package]] -name = "click" -version = "8.3.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" }, -] - [[package]] name = "colorama" version = "0.4.6" @@ -187,85 +166,85 @@ wheels = [ [[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.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ad/49/349848445b0e53660e258acbcc9b0d014895b6739237920886672240f84b/coverage-7.13.2.tar.gz", hash = "sha256:044c6951ec37146b72a50cc81ef02217d27d4c3640efd2640311393cbbf143d3", size = 826523, upload-time = "2026-01-25T13:00:04.889Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/39/e92a35f7800222d3f7b2cbb7bbc3b65672ae8d501cb31801b2d2bd7acdf1/coverage-7.13.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f106b2af193f965d0d3234f3f83fc35278c7fb935dfbde56ae2da3dd2c03b84d", size = 219142, upload-time = "2026-01-25T12:58:00.448Z" }, + { url = "https://files.pythonhosted.org/packages/45/7a/8bf9e9309c4c996e65c52a7c5a112707ecdd9fbaf49e10b5a705a402bbb4/coverage-7.13.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:78f45d21dc4d5d6bd29323f0320089ef7eae16e4bef712dff79d184fa7330af3", size = 219503, upload-time = "2026-01-25T12:58:02.451Z" }, + { url = "https://files.pythonhosted.org/packages/87/93/17661e06b7b37580923f3f12406ac91d78aeed293fb6da0b69cc7957582f/coverage-7.13.2-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:fae91dfecd816444c74531a9c3d6ded17a504767e97aa674d44f638107265b99", size = 251006, upload-time = "2026-01-25T12:58:04.059Z" }, + { url = "https://files.pythonhosted.org/packages/12/f0/f9e59fb8c310171497f379e25db060abef9fa605e09d63157eebec102676/coverage-7.13.2-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:264657171406c114787b441484de620e03d8f7202f113d62fcd3d9688baa3e6f", size = 253750, upload-time = "2026-01-25T12:58:05.574Z" }, + { url = "https://files.pythonhosted.org/packages/e5/b1/1935e31add2232663cf7edd8269548b122a7d100047ff93475dbaaae673e/coverage-7.13.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ae47d8dcd3ded0155afbb59c62bd8ab07ea0fd4902e1c40567439e6db9dcaf2f", size = 254862, upload-time = "2026-01-25T12:58:07.647Z" }, + { url = "https://files.pythonhosted.org/packages/af/59/b5e97071ec13df5f45da2b3391b6cdbec78ba20757bc92580a5b3d5fa53c/coverage-7.13.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8a0b33e9fd838220b007ce8f299114d406c1e8edb21336af4c97a26ecfd185aa", size = 251420, upload-time = "2026-01-25T12:58:09.309Z" }, + { url = "https://files.pythonhosted.org/packages/3f/75/9495932f87469d013dc515fb0ce1aac5fa97766f38f6b1a1deb1ee7b7f3a/coverage-7.13.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b3becbea7f3ce9a2d4d430f223ec15888e4deb31395840a79e916368d6004cce", size = 252786, upload-time = "2026-01-25T12:58:10.909Z" }, + { url = "https://files.pythonhosted.org/packages/6a/59/af550721f0eb62f46f7b8cb7e6f1860592189267b1c411a4e3a057caacee/coverage-7.13.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:f819c727a6e6eeb8711e4ce63d78c620f69630a2e9d53bc95ca5379f57b6ba94", size = 250928, upload-time = "2026-01-25T12:58:12.449Z" }, + { url = "https://files.pythonhosted.org/packages/9b/b1/21b4445709aae500be4ab43bbcfb4e53dc0811c3396dcb11bf9f23fd0226/coverage-7.13.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:4f7b71757a3ab19f7ba286e04c181004c1d61be921795ee8ba6970fd0ec91da5", size = 250496, upload-time = "2026-01-25T12:58:14.047Z" }, + { url = "https://files.pythonhosted.org/packages/ba/b1/0f5d89dfe0392990e4f3980adbde3eb34885bc1effb2dc369e0bf385e389/coverage-7.13.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b7fc50d2afd2e6b4f6f2f403b70103d280a8e0cb35320cbbe6debcda02a1030b", size = 252373, upload-time = "2026-01-25T12:58:15.976Z" }, + { url = "https://files.pythonhosted.org/packages/01/c9/0cf1a6a57a9968cc049a6b896693faa523c638a5314b1fc374eb2b2ac904/coverage-7.13.2-cp312-cp312-win32.whl", hash = "sha256:292250282cf9bcf206b543d7608bda17ca6fc151f4cbae949fc7e115112fbd41", size = 221696, upload-time = "2026-01-25T12:58:17.517Z" }, + { url = "https://files.pythonhosted.org/packages/4d/05/d7540bf983f09d32803911afed135524570f8c47bb394bf6206c1dc3a786/coverage-7.13.2-cp312-cp312-win_amd64.whl", hash = "sha256:eeea10169fac01549a7921d27a3e517194ae254b542102267bef7a93ed38c40e", size = 222504, upload-time = "2026-01-25T12:58:19.115Z" }, + { url = "https://files.pythonhosted.org/packages/15/8b/1a9f037a736ced0a12aacf6330cdaad5008081142a7070bc58b0f7930cbc/coverage-7.13.2-cp312-cp312-win_arm64.whl", hash = "sha256:2a5b567f0b635b592c917f96b9a9cb3dbd4c320d03f4bf94e9084e494f2e8894", size = 221120, upload-time = "2026-01-25T12:58:21.334Z" }, + { url = "https://files.pythonhosted.org/packages/a7/f0/3d3eac7568ab6096ff23791a526b0048a1ff3f49d0e236b2af6fb6558e88/coverage-7.13.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ed75de7d1217cf3b99365d110975f83af0528c849ef5180a12fd91b5064df9d6", size = 219168, upload-time = "2026-01-25T12:58:23.376Z" }, + { url = "https://files.pythonhosted.org/packages/a3/a6/f8b5cfeddbab95fdef4dcd682d82e5dcff7a112ced57a959f89537ee9995/coverage-7.13.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:97e596de8fa9bada4d88fde64a3f4d37f1b6131e4faa32bad7808abc79887ddc", size = 219537, upload-time = "2026-01-25T12:58:24.932Z" }, + { url = "https://files.pythonhosted.org/packages/7b/e6/8d8e6e0c516c838229d1e41cadcec91745f4b1031d4db17ce0043a0423b4/coverage-7.13.2-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:68c86173562ed4413345410c9480a8d64864ac5e54a5cda236748031e094229f", size = 250528, upload-time = "2026-01-25T12:58:26.567Z" }, + { url = "https://files.pythonhosted.org/packages/8e/78/befa6640f74092b86961f957f26504c8fba3d7da57cc2ab7407391870495/coverage-7.13.2-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7be4d613638d678b2b3773b8f687537b284d7074695a43fe2fbbfc0e31ceaed1", size = 253132, upload-time = "2026-01-25T12:58:28.251Z" }, + { url = "https://files.pythonhosted.org/packages/9d/10/1630db1edd8ce675124a2ee0f7becc603d2bb7b345c2387b4b95c6907094/coverage-7.13.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d7f63ce526a96acd0e16c4af8b50b64334239550402fb1607ce6a584a6d62ce9", size = 254374, upload-time = "2026-01-25T12:58:30.294Z" }, + { url = "https://files.pythonhosted.org/packages/ed/1d/0d9381647b1e8e6d310ac4140be9c428a0277330991e0c35bdd751e338a4/coverage-7.13.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:406821f37f864f968e29ac14c3fccae0fec9fdeba48327f0341decf4daf92d7c", size = 250762, upload-time = "2026-01-25T12:58:32.036Z" }, + { url = "https://files.pythonhosted.org/packages/43/e4/5636dfc9a7c871ee8776af83ee33b4c26bc508ad6cee1e89b6419a366582/coverage-7.13.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ee68e5a4e3e5443623406b905db447dceddffee0dceb39f4e0cd9ec2a35004b5", size = 252502, upload-time = "2026-01-25T12:58:33.961Z" }, + { url = "https://files.pythonhosted.org/packages/02/2a/7ff2884d79d420cbb2d12fed6fff727b6d0ef27253140d3cdbbd03187ee0/coverage-7.13.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2ee0e58cca0c17dd9c6c1cdde02bb705c7b3fbfa5f3b0b5afeda20d4ebff8ef4", size = 250463, upload-time = "2026-01-25T12:58:35.529Z" }, + { url = "https://files.pythonhosted.org/packages/91/c0/ba51087db645b6c7261570400fc62c89a16278763f36ba618dc8657a187b/coverage-7.13.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:6e5bbb5018bf76a56aabdb64246b5288d5ae1b7d0dd4d0534fe86df2c2992d1c", size = 250288, upload-time = "2026-01-25T12:58:37.226Z" }, + { url = "https://files.pythonhosted.org/packages/03/07/44e6f428551c4d9faf63ebcefe49b30e5c89d1be96f6a3abd86a52da9d15/coverage-7.13.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a55516c68ef3e08e134e818d5e308ffa6b1337cc8b092b69b24287bf07d38e31", size = 252063, upload-time = "2026-01-25T12:58:38.821Z" }, + { url = "https://files.pythonhosted.org/packages/c2/67/35b730ad7e1859dd57e834d1bc06080d22d2f87457d53f692fce3f24a5a9/coverage-7.13.2-cp313-cp313-win32.whl", hash = "sha256:5b20211c47a8abf4abc3319d8ce2464864fa9f30c5fcaf958a3eed92f4f1fef8", size = 221716, upload-time = "2026-01-25T12:58:40.484Z" }, + { url = "https://files.pythonhosted.org/packages/0d/82/e5fcf5a97c72f45fc14829237a6550bf49d0ab882ac90e04b12a69db76b4/coverage-7.13.2-cp313-cp313-win_amd64.whl", hash = "sha256:14f500232e521201cf031549fb1ebdfc0a40f401cf519157f76c397e586c3beb", size = 222522, upload-time = "2026-01-25T12:58:43.247Z" }, + { url = "https://files.pythonhosted.org/packages/b1/f1/25d7b2f946d239dd2d6644ca2cc060d24f97551e2af13b6c24c722ae5f97/coverage-7.13.2-cp313-cp313-win_arm64.whl", hash = "sha256:9779310cb5a9778a60c899f075a8514c89fa6d10131445c2207fc893e0b14557", size = 221145, upload-time = "2026-01-25T12:58:45Z" }, + { url = "https://files.pythonhosted.org/packages/9e/f7/080376c029c8f76fadfe43911d0daffa0cbdc9f9418a0eead70c56fb7f4b/coverage-7.13.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:e64fa5a1e41ce5df6b547cbc3d3699381c9e2c2c369c67837e716ed0f549d48e", size = 219861, upload-time = "2026-01-25T12:58:46.586Z" }, + { url = "https://files.pythonhosted.org/packages/42/11/0b5e315af5ab35f4c4a70e64d3314e4eec25eefc6dec13be3a7d5ffe8ac5/coverage-7.13.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b01899e82a04085b6561eb233fd688474f57455e8ad35cd82286463ba06332b7", size = 220207, upload-time = "2026-01-25T12:58:48.277Z" }, + { url = "https://files.pythonhosted.org/packages/b2/0c/0874d0318fb1062117acbef06a09cf8b63f3060c22265adaad24b36306b7/coverage-7.13.2-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:838943bea48be0e2768b0cf7819544cdedc1bbb2f28427eabb6eb8c9eb2285d3", size = 261504, upload-time = "2026-01-25T12:58:49.904Z" }, + { url = "https://files.pythonhosted.org/packages/83/5e/1cd72c22ecb30751e43a72f40ba50fcef1b7e93e3ea823bd9feda8e51f9a/coverage-7.13.2-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:93d1d25ec2b27e90bcfef7012992d1f5121b51161b8bffcda756a816cf13c2c3", size = 263582, upload-time = "2026-01-25T12:58:51.582Z" }, + { url = "https://files.pythonhosted.org/packages/9b/da/8acf356707c7a42df4d0657020308e23e5a07397e81492640c186268497c/coverage-7.13.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:93b57142f9621b0d12349c43fc7741fe578e4bc914c1e5a54142856cfc0bf421", size = 266008, upload-time = "2026-01-25T12:58:53.234Z" }, + { url = "https://files.pythonhosted.org/packages/41/41/ea1730af99960309423c6ea8d6a4f1fa5564b2d97bd1d29dda4b42611f04/coverage-7.13.2-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f06799ae1bdfff7ccb8665d75f8291c69110ba9585253de254688aa8a1ccc6c5", size = 260762, upload-time = "2026-01-25T12:58:55.372Z" }, + { url = "https://files.pythonhosted.org/packages/22/fa/02884d2080ba71db64fdc127b311db60e01fe6ba797d9c8363725e39f4d5/coverage-7.13.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:7f9405ab4f81d490811b1d91c7a20361135a2df4c170e7f0b747a794da5b7f23", size = 263571, upload-time = "2026-01-25T12:58:57.52Z" }, + { url = "https://files.pythonhosted.org/packages/d2/6b/4083aaaeba9b3112f55ac57c2ce7001dc4d8fa3fcc228a39f09cc84ede27/coverage-7.13.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:f9ab1d5b86f8fbc97a5b3cd6280a3fd85fef3b028689d8a2c00918f0d82c728c", size = 261200, upload-time = "2026-01-25T12:58:59.255Z" }, + { url = "https://files.pythonhosted.org/packages/e9/d2/aea92fa36d61955e8c416ede9cf9bf142aa196f3aea214bb67f85235a050/coverage-7.13.2-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:f674f59712d67e841525b99e5e2b595250e39b529c3bda14764e4f625a3fa01f", size = 260095, upload-time = "2026-01-25T12:59:01.066Z" }, + { url = "https://files.pythonhosted.org/packages/0d/ae/04ffe96a80f107ea21b22b2367175c621da920063260a1c22f9452fd7866/coverage-7.13.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c6cadac7b8ace1ba9144feb1ae3cb787a6065ba6d23ffc59a934b16406c26573", size = 262284, upload-time = "2026-01-25T12:59:02.802Z" }, + { url = "https://files.pythonhosted.org/packages/1c/7a/6f354dcd7dfc41297791d6fb4e0d618acb55810bde2c1fd14b3939e05c2b/coverage-7.13.2-cp313-cp313t-win32.whl", hash = "sha256:14ae4146465f8e6e6253eba0cccd57423e598a4cb925958b240c805300918343", size = 222389, upload-time = "2026-01-25T12:59:04.563Z" }, + { url = "https://files.pythonhosted.org/packages/8d/d5/080ad292a4a3d3daf411574be0a1f56d6dee2c4fdf6b005342be9fac807f/coverage-7.13.2-cp313-cp313t-win_amd64.whl", hash = "sha256:9074896edd705a05769e3de0eac0a8388484b503b68863dd06d5e473f874fd47", size = 223450, upload-time = "2026-01-25T12:59:06.677Z" }, + { url = "https://files.pythonhosted.org/packages/88/96/df576fbacc522e9fb8d1c4b7a7fc62eb734be56e2cba1d88d2eabe08ea3f/coverage-7.13.2-cp313-cp313t-win_arm64.whl", hash = "sha256:69e526e14f3f854eda573d3cf40cffd29a1a91c684743d904c33dbdcd0e0f3e7", size = 221707, upload-time = "2026-01-25T12:59:08.363Z" }, + { url = "https://files.pythonhosted.org/packages/55/53/1da9e51a0775634b04fcc11eb25c002fc58ee4f92ce2e8512f94ac5fc5bf/coverage-7.13.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:387a825f43d680e7310e6f325b2167dd093bc8ffd933b83e9aa0983cf6e0a2ef", size = 219213, upload-time = "2026-01-25T12:59:11.909Z" }, + { url = "https://files.pythonhosted.org/packages/46/35/b3caac3ebbd10230fea5a33012b27d19e999a17c9285c4228b4b2e35b7da/coverage-7.13.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f0d7fea9d8e5d778cd5a9e8fc38308ad688f02040e883cdc13311ef2748cb40f", size = 219549, upload-time = "2026-01-25T12:59:13.638Z" }, + { url = "https://files.pythonhosted.org/packages/76/9c/e1cf7def1bdc72c1907e60703983a588f9558434a2ff94615747bd73c192/coverage-7.13.2-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e080afb413be106c95c4ee96b4fffdc9e2fa56a8bbf90b5c0918e5c4449412f5", size = 250586, upload-time = "2026-01-25T12:59:15.808Z" }, + { url = "https://files.pythonhosted.org/packages/ba/49/f54ec02ed12be66c8d8897270505759e057b0c68564a65c429ccdd1f139e/coverage-7.13.2-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a7fc042ba3c7ce25b8a9f097eb0f32a5ce1ccdb639d9eec114e26def98e1f8a4", size = 253093, upload-time = "2026-01-25T12:59:17.491Z" }, + { url = "https://files.pythonhosted.org/packages/fb/5e/aaf86be3e181d907e23c0f61fccaeb38de8e6f6b47aed92bf57d8fc9c034/coverage-7.13.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d0ba505e021557f7f8173ee8cd6b926373d8653e5ff7581ae2efce1b11ef4c27", size = 254446, upload-time = "2026-01-25T12:59:19.752Z" }, + { url = "https://files.pythonhosted.org/packages/28/c8/a5fa01460e2d75b0c853b392080d6829d3ca8b5ab31e158fa0501bc7c708/coverage-7.13.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7de326f80e3451bd5cc7239ab46c73ddb658fe0b7649476bc7413572d36cd548", size = 250615, upload-time = "2026-01-25T12:59:21.928Z" }, + { url = "https://files.pythonhosted.org/packages/86/0b/6d56315a55f7062bb66410732c24879ccb2ec527ab6630246de5fe45a1df/coverage-7.13.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:abaea04f1e7e34841d4a7b343904a3f59481f62f9df39e2cd399d69a187a9660", size = 252452, upload-time = "2026-01-25T12:59:23.592Z" }, + { url = "https://files.pythonhosted.org/packages/30/19/9bc550363ebc6b0ea121977ee44d05ecd1e8bf79018b8444f1028701c563/coverage-7.13.2-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:9f93959ee0c604bccd8e0697be21de0887b1f73efcc3aa73a3ec0fd13feace92", size = 250418, upload-time = "2026-01-25T12:59:25.392Z" }, + { url = "https://files.pythonhosted.org/packages/1f/53/580530a31ca2f0cc6f07a8f2ab5460785b02bb11bdf815d4c4d37a4c5169/coverage-7.13.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:13fe81ead04e34e105bf1b3c9f9cdf32ce31736ee5d90a8d2de02b9d3e1bcb82", size = 250231, upload-time = "2026-01-25T12:59:27.888Z" }, + { url = "https://files.pythonhosted.org/packages/e2/42/dd9093f919dc3088cb472893651884bd675e3df3d38a43f9053656dca9a2/coverage-7.13.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d6d16b0f71120e365741bca2cb473ca6fe38930bc5431c5e850ba949f708f892", size = 251888, upload-time = "2026-01-25T12:59:29.636Z" }, + { url = "https://files.pythonhosted.org/packages/fa/a6/0af4053e6e819774626e133c3d6f70fae4d44884bfc4b126cb647baee8d3/coverage-7.13.2-cp314-cp314-win32.whl", hash = "sha256:9b2f4714bb7d99ba3790ee095b3b4ac94767e1347fe424278a0b10acb3ff04fe", size = 221968, upload-time = "2026-01-25T12:59:31.424Z" }, + { url = "https://files.pythonhosted.org/packages/c4/cc/5aff1e1f80d55862442855517bb8ad8ad3a68639441ff6287dde6a58558b/coverage-7.13.2-cp314-cp314-win_amd64.whl", hash = "sha256:e4121a90823a063d717a96e0a0529c727fb31ea889369a0ee3ec00ed99bf6859", size = 222783, upload-time = "2026-01-25T12:59:33.118Z" }, + { url = "https://files.pythonhosted.org/packages/de/20/09abafb24f84b3292cc658728803416c15b79f9ee5e68d25238a895b07d9/coverage-7.13.2-cp314-cp314-win_arm64.whl", hash = "sha256:6873f0271b4a15a33e7590f338d823f6f66f91ed147a03938d7ce26efd04eee6", size = 221348, upload-time = "2026-01-25T12:59:34.939Z" }, + { url = "https://files.pythonhosted.org/packages/b6/60/a3820c7232db63be060e4019017cd3426751c2699dab3c62819cdbcea387/coverage-7.13.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:f61d349f5b7cd95c34017f1927ee379bfbe9884300d74e07cf630ccf7a610c1b", size = 219950, upload-time = "2026-01-25T12:59:36.624Z" }, + { url = "https://files.pythonhosted.org/packages/fd/37/e4ef5975fdeb86b1e56db9a82f41b032e3d93a840ebaf4064f39e770d5c5/coverage-7.13.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a43d34ce714f4ca674c0d90beb760eb05aad906f2c47580ccee9da8fe8bfb417", size = 220209, upload-time = "2026-01-25T12:59:38.339Z" }, + { url = "https://files.pythonhosted.org/packages/54/df/d40e091d00c51adca1e251d3b60a8b464112efa3004949e96a74d7c19a64/coverage-7.13.2-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bff1b04cb9d4900ce5c56c4942f047dc7efe57e2608cb7c3c8936e9970ccdbee", size = 261576, upload-time = "2026-01-25T12:59:40.446Z" }, + { url = "https://files.pythonhosted.org/packages/c5/44/5259c4bed54e3392e5c176121af9f71919d96dde853386e7730e705f3520/coverage-7.13.2-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6ae99e4560963ad8e163e819e5d77d413d331fd00566c1e0856aa252303552c1", size = 263704, upload-time = "2026-01-25T12:59:42.346Z" }, + { url = "https://files.pythonhosted.org/packages/16/bd/ae9f005827abcbe2c70157459ae86053971c9fa14617b63903abbdce26d9/coverage-7.13.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e79a8c7d461820257d9aa43716c4efc55366d7b292e46b5b37165be1d377405d", size = 266109, upload-time = "2026-01-25T12:59:44.073Z" }, + { url = "https://files.pythonhosted.org/packages/a2/c0/8e279c1c0f5b1eaa3ad9b0fb7a5637fc0379ea7d85a781c0fe0bb3cfc2ab/coverage-7.13.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:060ee84f6a769d40c492711911a76811b4befb6fba50abb450371abb720f5bd6", size = 260686, upload-time = "2026-01-25T12:59:45.804Z" }, + { url = "https://files.pythonhosted.org/packages/b2/47/3a8112627e9d863e7cddd72894171c929e94491a597811725befdcd76bce/coverage-7.13.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3bca209d001fd03ea2d978f8a4985093240a355c93078aee3f799852c23f561a", size = 263568, upload-time = "2026-01-25T12:59:47.929Z" }, + { url = "https://files.pythonhosted.org/packages/92/bc/7ea367d84afa3120afc3ce6de294fd2dcd33b51e2e7fbe4bbfd200f2cb8c/coverage-7.13.2-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:6b8092aa38d72f091db61ef83cb66076f18f02da3e1a75039a4f218629600e04", size = 261174, upload-time = "2026-01-25T12:59:49.717Z" }, + { url = "https://files.pythonhosted.org/packages/33/b7/f1092dcecb6637e31cc2db099581ee5c61a17647849bae6b8261a2b78430/coverage-7.13.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:4a3158dc2dcce5200d91ec28cd315c999eebff355437d2765840555d765a6e5f", size = 260017, upload-time = "2026-01-25T12:59:51.463Z" }, + { url = "https://files.pythonhosted.org/packages/2b/cd/f3d07d4b95fbe1a2ef0958c15da614f7e4f557720132de34d2dc3aa7e911/coverage-7.13.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3973f353b2d70bd9796cc12f532a05945232ccae966456c8ed7034cb96bbfd6f", size = 262337, upload-time = "2026-01-25T12:59:53.407Z" }, + { url = "https://files.pythonhosted.org/packages/e0/db/b0d5b2873a07cb1e06a55d998697c0a5a540dcefbf353774c99eb3874513/coverage-7.13.2-cp314-cp314t-win32.whl", hash = "sha256:79f6506a678a59d4ded048dc72f1859ebede8ec2b9a2d509ebe161f01c2879d3", size = 222749, upload-time = "2026-01-25T12:59:56.316Z" }, + { url = "https://files.pythonhosted.org/packages/e5/2f/838a5394c082ac57d85f57f6aba53093b30d9089781df72412126505716f/coverage-7.13.2-cp314-cp314t-win_amd64.whl", hash = "sha256:196bfeabdccc5a020a57d5a368c681e3a6ceb0447d153aeccc1ab4d70a5032ba", size = 223857, upload-time = "2026-01-25T12:59:58.201Z" }, + { url = "https://files.pythonhosted.org/packages/44/d4/b608243e76ead3a4298824b50922b89ef793e50069ce30316a65c1b4d7ef/coverage-7.13.2-cp314-cp314t-win_arm64.whl", hash = "sha256:69269ab58783e090bfbf5b916ab3d188126e22d6070bbfc93098fdd474ef937c", size = 221881, upload-time = "2026-01-25T13:00:00.449Z" }, + { url = "https://files.pythonhosted.org/packages/d2/db/d291e30fdf7ea617a335531e72294e0c723356d7fdde8fba00610a76bda9/coverage-7.13.2-py3-none-any.whl", hash = "sha256:40ce1ea1e25125556d8e76bd0b61500839a07944cc287ac21d5626f3e620cad5", size = 210943, upload-time = "2026-01-25T13:00:02.388Z" }, ] [[package]] name = "dill" -version = "0.4.0" +version = "0.4.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/12/80/630b4b88364e9a8c8c5797f4602d0f76ef820909ee32f0bacb9f90654042/dill-0.4.0.tar.gz", hash = "sha256:0633f1d2df477324f53a895b02c901fb961bdbf65a17122586ea7019292cbcf0", size = 186976, upload-time = "2025-04-16T00:41:48.867Z" } +sdist = { url = "https://files.pythonhosted.org/packages/81/e1/56027a71e31b02ddc53c7d65b01e68edf64dea2932122fe7746a516f75d5/dill-0.4.1.tar.gz", hash = "sha256:423092df4182177d4d8ba8290c8a5b640c66ab35ec7da59ccfa00f6fa3eea5fa", size = 187315, upload-time = "2026-01-19T02:36:56.85Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/50/3d/9373ad9c56321fdab5b41197068e1d8c25883b3fea29dd361f9b55116869/dill-0.4.0-py3-none-any.whl", hash = "sha256:44f54bf6412c2c8464c14e8243eb163690a9800dbe2c367330883b19c7561049", size = 119668, upload-time = "2025-04-16T00:41:47.671Z" }, + { url = "https://files.pythonhosted.org/packages/1e/77/dc8c558f7593132cf8fefec57c4f60c83b16941c574ac5f619abb3ae7933/dill-0.4.1-py3-none-any.whl", hash = "sha256:1e1ce33e978ae97fcfcff5638477032b801c46c7c65cf717f95fbc2248f79a9d", size = 120019, upload-time = "2026-01-19T02:36:55.663Z" }, ] [[package]] @@ -277,28 +256,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047, upload-time = "2025-07-17T16:51:58.613Z" }, ] -[[package]] -name = "dnspython" -version = "2.8.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8c/8b/57666417c0f90f08bcafa776861060426765fdb422eb10212086fb811d26/dnspython-2.8.0.tar.gz", hash = "sha256:181d3c6996452cb1189c4046c61599b84a5a86e099562ffde77d26984ff26d0f", size = 368251, upload-time = "2025-09-07T18:58:00.022Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ba/5a/18ad964b0086c6e62e2e7500f7edc89e3faa45033c71c1893d34eed2b2de/dnspython-2.8.0-py3-none-any.whl", hash = "sha256:01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af", size = 331094, upload-time = "2025-09-07T18:57:58.071Z" }, -] - -[[package]] -name = "email-validator" -version = "2.3.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "dnspython" }, - { name = "idna" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/f5/22/900cb125c76b7aaa450ce02fd727f452243f2e91a61af068b40adba60ea9/email_validator-2.3.0.tar.gz", hash = "sha256:9fc05c37f2f6cf439ff414f8fc46d917929974a82244c20eb10231ba60c54426", size = 51238, upload-time = "2025-08-26T13:09:06.831Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/de/15/545e2b6cf2e3be84bc1ed85613edd75b8aea69807a71c26f4ca6a9258e82/email_validator-2.3.0-py3-none-any.whl", hash = "sha256:80f13f623413e6b197ae73bb10bf4eb0908faf509ad8362c5edeb0be7fd450b4", size = 35604, upload-time = "2025-08-26T13:09:05.858Z" }, -] - [[package]] name = "et-xmlfile" version = "2.0.0" @@ -308,138 +265,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c1/8b/5fe2cc11fee489817272089c4203e679c63b570a5aaeb18d852ae3cbba6a/et_xmlfile-2.0.0-py3-none-any.whl", hash = "sha256:7a91720bc756843502c3b7504c77b8fe44217c85c537d85037f0f536151b2caa", size = 18059, upload-time = "2024-10-25T17:25:39.051Z" }, ] -[[package]] -name = "fastapi" -version = "0.124.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "annotated-doc" }, - { name = "pydantic" }, - { name = "starlette" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/48/9c/11969bd3e3bc4aa3a711f83dd3720239d3565a934929c74fc32f6c9f3638/fastapi-0.124.0.tar.gz", hash = "sha256:260cd178ad75e6d259991f2fd9b0fee924b224850079df576a3ba604ce58f4e6", size = 357623, upload-time = "2025-12-06T13:11:35.692Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/4d/29/9e1e82e16e9a1763d3b55bfbe9b2fa39d7175a1fd97685c482fa402e111d/fastapi-0.124.0-py3-none-any.whl", hash = "sha256:91596bdc6dde303c318f06e8d2bc75eafb341fc793a0c9c92c0bc1db1ac52480", size = 112505, upload-time = "2025-12-06T13:11:34.392Z" }, -] - -[package.optional-dependencies] -standard = [ - { name = "email-validator" }, - { name = "fastapi-cli", extra = ["standard"] }, - { name = "httpx" }, - { name = "jinja2" }, - { name = "python-multipart" }, - { name = "uvicorn", extra = ["standard"] }, -] - -[[package]] -name = "fastapi-cli" -version = "0.0.16" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "rich-toolkit" }, - { name = "typer" }, - { name = "uvicorn", extra = ["standard"] }, -] -sdist = { url = "https://files.pythonhosted.org/packages/99/75/9407a6b452be4c988feacec9c9d2f58d8f315162a6c7258d5a649d933ebe/fastapi_cli-0.0.16.tar.gz", hash = "sha256:e8a2a1ecf7a4e062e3b2eec63ae34387d1e142d4849181d936b23c4bdfe29073", size = 19447, upload-time = "2025-11-10T19:01:07.856Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/55/43/678528c19318394320ee43757648d5e0a8070cf391b31f69d931e5c840d2/fastapi_cli-0.0.16-py3-none-any.whl", hash = "sha256:addcb6d130b5b9c91adbbf3f2947fe115991495fdb442fe3e51b5fc6327df9f4", size = 12312, upload-time = "2025-11-10T19:01:06.728Z" }, -] - -[package.optional-dependencies] -standard = [ - { name = "fastapi-cloud-cli" }, - { name = "uvicorn", extra = ["standard"] }, -] - -[[package]] -name = "fastapi-cloud-cli" -version = "0.6.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "fastar" }, - { name = "httpx" }, - { name = "pydantic", extra = ["email"] }, - { name = "rich-toolkit" }, - { name = "rignore" }, - { name = "sentry-sdk" }, - { name = "typer" }, - { name = "uvicorn", extra = ["standard"] }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c2/dd/e5890bb4ee63f9d8988660b755490e346cf5769aaa7f5f3ced9afb9f090a/fastapi_cloud_cli-0.6.0.tar.gz", hash = "sha256:2c333fff2e4b93b9efbec7896ce3ffa3e77ce4cf3d8cb14e35b4f823dfddac02", size = 30579, upload-time = "2025-12-04T15:04:07.008Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ac/2f/5ba9b5faa75067e30ff48e3c454263ebc2d2301d5509cfefe12cf9fc8156/fastapi_cloud_cli-0.6.0-py3-none-any.whl", hash = "sha256:b654890b5302c90d2f347b123a35186096328838a526316c470b6005cabd4983", size = 23215, upload-time = "2025-12-04T15:04:08.121Z" }, -] - -[[package]] -name = "fastar" -version = "0.8.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/69/e7/f89d54fb04104114dd0552836dc2b47914f416cc0e200b409dd04a33de5e/fastar-0.8.0.tar.gz", hash = "sha256:f4d4d68dbf1c4c2808f0e730fac5843493fc849f70fe3ad3af60dfbaf68b9a12", size = 68524, upload-time = "2025-11-26T02:36:00.72Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/58/f1/5b2ff898abac7f1a418284aad285e3a4f68d189c572ab2db0f6c9079dd16/fastar-0.8.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:0f10d2adfe40f47ff228f4efaa32d409d732ded98580e03ed37c9535b5fc923d", size = 706369, upload-time = "2025-11-26T02:34:37.783Z" }, - { url = "https://files.pythonhosted.org/packages/23/60/8046a386dca39154f80c927cbbeeb4b1c1267a3271bffe61552eb9995757/fastar-0.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b930da9d598e3bc69513d131f397e6d6be4643926ef3de5d33d1e826631eb036", size = 629097, upload-time = "2025-11-26T02:34:21.888Z" }, - { url = "https://files.pythonhosted.org/packages/22/7e/1ae005addc789924a9268da2394d3bb5c6f96836f7e37b7e3d23c2362675/fastar-0.8.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:9d210da2de733ca801de83e931012349d209f38b92d9630ccaa94bd445bdc9b8", size = 868938, upload-time = "2025-11-26T02:33:51.119Z" }, - { url = "https://files.pythonhosted.org/packages/a6/77/290a892b073b84bf82e6b2259708dfe79c54f356e252c2dd40180b16fe07/fastar-0.8.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa02270721517078a5bd61a38719070ac2537a4aa6b6c48cf369cf2abc59174a", size = 765204, upload-time = "2025-11-26T02:32:47.02Z" }, - { url = "https://files.pythonhosted.org/packages/d0/00/c3155171b976003af3281f5258189f1935b15d1221bfc7467b478c631216/fastar-0.8.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:83c391e5b789a720e4d0029b9559f5d6dee3226693c5b39c0eab8eaece997e0f", size = 764717, upload-time = "2025-11-26T02:33:02.453Z" }, - { url = "https://files.pythonhosted.org/packages/b7/43/405b7ad76207b2c11b7b59335b70eac19e4a2653977f5588a1ac8fed54f4/fastar-0.8.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3258d7a78a72793cdd081545da61cabe85b1f37634a1d0b97ffee0ff11d105ef", size = 931502, upload-time = "2025-11-26T02:33:18.619Z" }, - { url = "https://files.pythonhosted.org/packages/da/8a/a3dde6d37cc3da4453f2845cdf16675b5686b73b164f37e2cc579b057c2c/fastar-0.8.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e6eab95dd985cdb6a50666cbeb9e4814676e59cfe52039c880b69d67cfd44767", size = 821454, upload-time = "2025-11-26T02:33:33.427Z" }, - { url = "https://files.pythonhosted.org/packages/da/c1/904fe2468609c8990dce9fe654df3fbc7324a8d8e80d8240ae2c89757064/fastar-0.8.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:829b1854166141860887273c116c94e31357213fa8e9fe8baeb18bd6c38aa8d9", size = 821647, upload-time = "2025-11-26T02:34:07Z" }, - { url = "https://files.pythonhosted.org/packages/c8/73/a0642ab7a400bc07528091785e868ace598fde06fcd139b8f865ec1b6f3c/fastar-0.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b1667eae13f9457a3c737f4376d68e8c3e548353538b28f7e4273a30cb3965cd", size = 986342, upload-time = "2025-11-26T02:34:53.371Z" }, - { url = "https://files.pythonhosted.org/packages/af/af/60c1bfa6edab72366461a95f053d0f5f7ab1825fe65ca2ca367432cd8629/fastar-0.8.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:b864a95229a7db0814cd9ef7987cb713fd43dce1b0d809dd17d9cd6f02fdde3e", size = 1040207, upload-time = "2025-11-26T02:35:10.65Z" }, - { url = "https://files.pythonhosted.org/packages/f6/a0/0d624290dec622e7fa084b6881f456809f68777d54a314f5dde932714506/fastar-0.8.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c05fbc5618ce17675a42576fa49858d79734627f0a0c74c0875ab45ee8de340c", size = 1045031, upload-time = "2025-11-26T02:35:28.108Z" }, - { url = "https://files.pythonhosted.org/packages/a7/74/cf663af53c4706ba88e6b4af44a6b0c3bd7d7ca09f079dc40647a8f06585/fastar-0.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7f41c51ee96f338662ee3c3df4840511ba3f9969606840f1b10b7cb633a3c716", size = 994877, upload-time = "2025-11-26T02:35:45.797Z" }, - { url = "https://files.pythonhosted.org/packages/52/17/444c8be6e77206050e350da7c338102b6cab384be937fa0b1d6d1f9ede73/fastar-0.8.0-cp312-cp312-win32.whl", hash = "sha256:d949a1a2ea7968b734632c009df0571c94636a5e1622c87a6e2bf712a7334f47", size = 455996, upload-time = "2025-11-26T02:36:26.938Z" }, - { url = "https://files.pythonhosted.org/packages/dc/34/fc3b5e56d71a17b1904800003d9251716e8fd65f662e1b10a26881698a74/fastar-0.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:fc645994d5b927d769121094e8a649b09923b3c13a8b0b98696d8f853f23c532", size = 490429, upload-time = "2025-11-26T02:36:12.707Z" }, - { url = "https://files.pythonhosted.org/packages/35/a8/5608cc837417107c594e2e7be850b9365bcb05e99645966a5d6a156285fe/fastar-0.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:d81ee82e8dc78a0adb81728383bd39611177d642a8fa2d601d4ad5ad59e5f3bd", size = 461297, upload-time = "2025-11-26T02:36:03.546Z" }, - { url = "https://files.pythonhosted.org/packages/d1/a5/79ecba3646e22d03eef1a66fb7fc156567213e2e4ab9faab3bbd4489e483/fastar-0.8.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:a3253a06845462ca2196024c7a18f5c0ba4de1532ab1c4bad23a40b332a06a6a", size = 706112, upload-time = "2025-11-26T02:34:39.237Z" }, - { url = "https://files.pythonhosted.org/packages/0a/03/4f883bce878218a8676c2d7ca09b50c856a5470bb3b7f63baf9521ea6995/fastar-0.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5cbeb3ebfa0980c68ff8b126295cc6b208ccd81b638aebc5a723d810a7a0e5d2", size = 628954, upload-time = "2025-11-26T02:34:23.705Z" }, - { url = "https://files.pythonhosted.org/packages/4f/f1/892e471f156b03d10ba48ace9384f5a896702a54506137462545f38e40b8/fastar-0.8.0-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:1c0d5956b917daac77d333d48b3f0f3ff927b8039d5b32d8125462782369f761", size = 868685, upload-time = "2025-11-26T02:33:53.077Z" }, - { url = "https://files.pythonhosted.org/packages/39/ba/e24915045852e30014ec6840446975c03f4234d1c9270394b51d3ad18394/fastar-0.8.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:27b404db2b786b65912927ce7f3790964a4bcbde42cdd13091b82a89cd655e1c", size = 765044, upload-time = "2025-11-26T02:32:48.187Z" }, - { url = "https://files.pythonhosted.org/packages/14/2c/1aa11ac21a99984864c2fca4994e094319ff3a2046e7a0343c39317bd5b9/fastar-0.8.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0902fc89dcf1e7f07b8563032a4159fe2b835e4c16942c76fd63451d0e5f76a3", size = 764322, upload-time = "2025-11-26T02:33:03.859Z" }, - { url = "https://files.pythonhosted.org/packages/ba/f0/4b91902af39fe2d3bae7c85c6d789586b9fbcf618d7fdb3d37323915906d/fastar-0.8.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:069347e2f0f7a8b99bbac8cd1bc0e06c7b4a31dc964fc60d84b95eab3d869dc1", size = 931016, upload-time = "2025-11-26T02:33:19.902Z" }, - { url = "https://files.pythonhosted.org/packages/c9/97/8fc43a5a9c0a2dc195730f6f7a0f367d171282cd8be2511d0e87c6d2dad0/fastar-0.8.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7fd135306f6bfe9a835918280e0eb440b70ab303e0187d90ab51ca86e143f70d", size = 821308, upload-time = "2025-11-26T02:33:34.664Z" }, - { url = "https://files.pythonhosted.org/packages/0c/e9/058615b63a7fd27965e8c5966f393ed0c169f7ff5012e1674f21684de3ba/fastar-0.8.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78d06d6897f43c27154b5f2d0eb930a43a81b7eec73f6f0b0114814d4a10ab38", size = 821171, upload-time = "2025-11-26T02:34:08.498Z" }, - { url = "https://files.pythonhosted.org/packages/ca/cf/69e16a17961570a755c37ffb5b5aa7610d2e77807625f537989da66f2a9d/fastar-0.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a922f8439231fa0c32b15e8d70ff6d415619b9d40492029dabbc14a0c53b5f18", size = 986227, upload-time = "2025-11-26T02:34:55.06Z" }, - { url = "https://files.pythonhosted.org/packages/fb/83/2100192372e59b56f4ace37d7d9cabda511afd71b5febad1643d1c334271/fastar-0.8.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:a739abd51eb766384b4caff83050888e80cd75bbcfec61e6d1e64875f94e4a40", size = 1039395, upload-time = "2025-11-26T02:35:12.166Z" }, - { url = "https://files.pythonhosted.org/packages/75/15/cdd03aca972f55872efbb7cf7540c3fa7b97a75d626303a3ea46932163dc/fastar-0.8.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:5a65f419d808b23ac89d5cd1b13a2f340f15bc5d1d9af79f39fdb77bba48ff1b", size = 1044766, upload-time = "2025-11-26T02:35:29.62Z" }, - { url = "https://files.pythonhosted.org/packages/3d/29/945e69e4e2652329ace545999334ec31f1431fbae3abb0105587e11af2ae/fastar-0.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7bb2ae6c0cce58f0db1c9f20495e7557cca2c1ee9c69bbd90eafd54f139171c5", size = 994740, upload-time = "2025-11-26T02:35:47.887Z" }, - { url = "https://files.pythonhosted.org/packages/4b/5d/dbfe28f8cd1eb484bba0c62e5259b2cf6fea229d6ef43e05c06b5a78c034/fastar-0.8.0-cp313-cp313-win32.whl", hash = "sha256:b28753e0d18a643272597cb16d39f1053842aa43131ad3e260c03a2417d38401", size = 455990, upload-time = "2025-11-26T02:36:28.502Z" }, - { url = "https://files.pythonhosted.org/packages/e1/01/e965740bd36e60ef4c5aa2cbe42b6c4eb1dc3551009238a97c2e5e96bd23/fastar-0.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:620e5d737dce8321d49a5ebb7997f1fd0047cde3512082c27dc66d6ac8c1927a", size = 490227, upload-time = "2025-11-26T02:36:14.363Z" }, - { url = "https://files.pythonhosted.org/packages/dd/10/c99202719b83e5249f26902ae53a05aea67d840eeb242019322f20fc171c/fastar-0.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:c4c4bd08df563120cd33e854fe0a93b81579e8571b11f9b7da9e84c37da2d6b6", size = 461078, upload-time = "2025-11-26T02:36:04.94Z" }, - { url = "https://files.pythonhosted.org/packages/96/4a/9573b87a0ef07580ed111e7230259aec31bb33ca3667963ebee77022ec61/fastar-0.8.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:50b36ce654ba44b0e13fae607ae17ee6e1597b69f71df1bee64bb8328d881dfc", size = 706041, upload-time = "2025-11-26T02:34:40.638Z" }, - { url = "https://files.pythonhosted.org/packages/4a/19/f95444a1d4f375333af49300aa75ee93afa3335c0e40fda528e460ed859c/fastar-0.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:63a892762683d7ab00df0227d5ea9677c62ff2cde9b875e666c0be569ed940f3", size = 628617, upload-time = "2025-11-26T02:34:24.893Z" }, - { url = "https://files.pythonhosted.org/packages/b3/c9/b51481b38b7e3f16ef2b9e233b1a3623386c939d745d6e41bbd389eaae30/fastar-0.8.0-cp314-cp314-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:4ae6a145c1bff592644bde13f2115e0239f4b7babaf506d14e7d208483cf01a5", size = 869299, upload-time = "2025-11-26T02:33:54.274Z" }, - { url = "https://files.pythonhosted.org/packages/bf/02/3ba1267ee5ba7314e29c431cf82eaa68586f2c40cdfa08be3632b7d07619/fastar-0.8.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ae0ff7c0a1c7e1428404b81faee8aebef466bfd0be25bfe4dabf5d535c68741", size = 764667, upload-time = "2025-11-26T02:32:49.606Z" }, - { url = "https://files.pythonhosted.org/packages/1b/84/bf33530fd015b5d7c2cc69e0bce4a38d736754a6955487005aab1af6adcd/fastar-0.8.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:dbfd87dbd217b45c898b2dbcd0169aae534b2c1c5cbe3119510881f6a5ac8ef5", size = 763993, upload-time = "2025-11-26T02:33:05.782Z" }, - { url = "https://files.pythonhosted.org/packages/da/e0/9564d24e7cea6321a8d921c6d2a457044a476ef197aa4708e179d3d97f0d/fastar-0.8.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a5abd99fcba83ef28c8fe6ae2927edc79053db43a0457a962ed85c9bf150d37", size = 930153, upload-time = "2025-11-26T02:33:21.53Z" }, - { url = "https://files.pythonhosted.org/packages/35/b1/6f57fcd8d6e192cfebf97e58eb27751640ad93784c857b79039e84387b51/fastar-0.8.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:91d4c685620c3a9d6b5ae091dbabab4f98b20049b7ecc7976e19cc9016c0d5d6", size = 821177, upload-time = "2025-11-26T02:33:35.839Z" }, - { url = "https://files.pythonhosted.org/packages/b3/78/9e004ea9f3aa7466f5ddb6f9518780e1d2f0ed3ca55f093632982598bace/fastar-0.8.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f77c2f2cad76e9dc7b6701297adb1eba87d0485944b416fc2ccf5516c01219a3", size = 820652, upload-time = "2025-11-26T02:34:09.776Z" }, - { url = "https://files.pythonhosted.org/packages/42/95/b604ed536544005c9f1aee7c4c74b00150db3d8d535cd8232dc20f947063/fastar-0.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e7f07c4a3dada7757a8fc430a5b4a29e6ef696d2212747213f57086ffd970316", size = 985961, upload-time = "2025-11-26T02:34:56.401Z" }, - { url = "https://files.pythonhosted.org/packages/f2/7b/fa9d4d96a5d494bdb8699363bb9de8178c0c21a02e1d89cd6f913d127018/fastar-0.8.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:90c0c3fe55105c0aed8a83135dbdeb31e683455dbd326a1c48fa44c378b85616", size = 1039316, upload-time = "2025-11-26T02:35:13.807Z" }, - { url = "https://files.pythonhosted.org/packages/4e/f9/8462789243bc3f33e8401378ec6d54de4e20cfa60c96a0e15e3e9d1389bb/fastar-0.8.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:fb9ee51e5bffe0dab3d3126d3a4fac8d8f7235cedcb4b8e74936087ce1c157f3", size = 1045028, upload-time = "2025-11-26T02:35:31.079Z" }, - { url = "https://files.pythonhosted.org/packages/a5/71/9abb128777e616127194b509e98fcda3db797d76288c1a8c23dd22afc14f/fastar-0.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e380b1e8d30317f52406c43b11e98d11e1d68723bbd031e18049ea3497b59a6d", size = 994677, upload-time = "2025-11-26T02:35:49.391Z" }, - { url = "https://files.pythonhosted.org/packages/de/c1/b81b3f194853d7ad232a67a1d768f5f51a016f165cfb56cb31b31bbc6177/fastar-0.8.0-cp314-cp314-win32.whl", hash = "sha256:1c4ffc06e9c4a8ca498c07e094670d8d8c0d25b17ca6465b9774da44ea997ab1", size = 456687, upload-time = "2025-11-26T02:36:30.205Z" }, - { url = "https://files.pythonhosted.org/packages/cb/87/9e0cd4768a98181d56f0cdbab2363404cc15deb93f4aad3b99cd2761bbaa/fastar-0.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:5517a8ad4726267c57a3e0e2a44430b782e00b230bf51c55b5728e758bb3a692", size = 490578, upload-time = "2025-11-26T02:36:16.218Z" }, - { url = "https://files.pythonhosted.org/packages/aa/1e/580a76cf91847654f2ad6520e956e93218f778540975bc4190d363f709e2/fastar-0.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:58030551046ff4a8616931e52a36c83545ff05996db5beb6e0cd2b7e748aa309", size = 461473, upload-time = "2025-11-26T02:36:06.373Z" }, - { url = "https://files.pythonhosted.org/packages/58/4c/bdb5c6efe934f68708529c8c9d4055ebef5c4be370621966438f658b29bd/fastar-0.8.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:1e7d29b6bfecb29db126a08baf3c04a5ab667f6cea2b7067d3e623a67729c4a6", size = 705570, upload-time = "2025-11-26T02:34:42.01Z" }, - { url = "https://files.pythonhosted.org/packages/6d/78/f01ac7e71d5a37621bd13598a26e948a12b85ca8042f7ee1a0a8c9f59cda/fastar-0.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:05eb7b96940f9526b485f1d0b02393839f0f61cac4b1f60024984f8b326d2640", size = 627761, upload-time = "2025-11-26T02:34:26.152Z" }, - { url = "https://files.pythonhosted.org/packages/06/45/6df0ecda86ea9d2e95053c1a655d153dee55fc121b6e13ea6d1e246a50b6/fastar-0.8.0-cp314-cp314t-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:619352d8ac011794e2345c462189dc02ba634750d23cd9d86a9267dd71b1f278", size = 869414, upload-time = "2025-11-26T02:33:55.618Z" }, - { url = "https://files.pythonhosted.org/packages/b2/72/486421f5a8c0c377cc82e7a50c8a8ea899a6ec2aa72bde8f09fb667a2dc8/fastar-0.8.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:74ebfecef3fe6d7a90355fac1402fd30636988332a1d33f3e80019a10782bb24", size = 763863, upload-time = "2025-11-26T02:32:51.051Z" }, - { url = "https://files.pythonhosted.org/packages/d4/64/39f654dbb41a3867fb1f2c8081c014d8f1d32ea10585d84cacbef0b32995/fastar-0.8.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2975aca5a639e26a3ab0d23b4b0628d6dd6d521146c3c11486d782be621a35aa", size = 763065, upload-time = "2025-11-26T02:33:07.274Z" }, - { url = "https://files.pythonhosted.org/packages/4e/bd/c011a34fb3534c4c3301f7c87c4ffd7e47f6113c904c092ddc8a59a303ea/fastar-0.8.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:afc438eaed8ff0dcdd9308268be5cb38c1db7e94c3ccca7c498ca13a4a4535a3", size = 930530, upload-time = "2025-11-26T02:33:23.117Z" }, - { url = "https://files.pythonhosted.org/packages/55/9d/aa6e887a7033c571b1064429222bbe09adc9a3c1e04f3d1788ba5838ebd5/fastar-0.8.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6ced0a5399cc0a84a858ef0a31ca2d0c24d3bbec4bcda506a9192d8119f3590a", size = 820572, upload-time = "2025-11-26T02:33:37.542Z" }, - { url = "https://files.pythonhosted.org/packages/ad/9c/7a3a2278a1052e1a5d98646de7c095a00cffd2492b3b84ce730e2f1cd93a/fastar-0.8.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec9b23da8c4c039da3fe2e358973c66976a0c8508aa06d6626b4403cb5666c19", size = 820649, upload-time = "2025-11-26T02:34:11.108Z" }, - { url = "https://files.pythonhosted.org/packages/02/9e/d38edc1f4438cd047e56137c26d94783ffade42e1b3bde620ccf17b771ef/fastar-0.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:dfba078fcd53478032fd0ceed56960ec6b7ff0511cfc013a8a3a4307e3a7bac4", size = 985653, upload-time = "2025-11-26T02:34:57.884Z" }, - { url = "https://files.pythonhosted.org/packages/69/d9/2147d0c19757e165cd62d41cec3f7b38fad2ad68ab784978b5f81716c7ea/fastar-0.8.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:ade56c94c14be356d295fecb47a3fcd473dd43a8803ead2e2b5b9e58feb6dcfa", size = 1038140, upload-time = "2025-11-26T02:35:15.778Z" }, - { url = "https://files.pythonhosted.org/packages/7f/1d/ec4c717ffb8a308871e9602ec3197d957e238dc0227127ac573ec9bca952/fastar-0.8.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:e48d938f9366db5e59441728f70b7f6c1ccfab7eff84f96f9b7e689b07786c52", size = 1045195, upload-time = "2025-11-26T02:35:32.865Z" }, - { url = "https://files.pythonhosted.org/packages/6a/9f/637334dc8c8f3bb391388b064ae13f0ad9402bc5a6c3e77b8887d0c31921/fastar-0.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:79c441dc1482ff51a54fb3f57ae6f7bb3d2cff88fa2cc5d196c519f8aab64a56", size = 994686, upload-time = "2025-11-26T02:35:51.392Z" }, - { url = "https://files.pythonhosted.org/packages/c9/e2/dfa19a4b260b8ab3581b7484dcb80c09b25324f4daa6b6ae1c7640d1607a/fastar-0.8.0-cp314-cp314t-win32.whl", hash = "sha256:187f61dc739afe45ac8e47ed7fd1adc45d52eac110cf27d579155720507d6fbe", size = 455767, upload-time = "2025-11-26T02:36:34.758Z" }, - { url = "https://files.pythonhosted.org/packages/51/47/df65c72afc1297797b255f90c4778b5d6f1f0f80282a134d5ab610310ed9/fastar-0.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:40e9d763cf8bf85ce2fa256e010aa795c0fe3d3bd1326d5c3084e6ce7857127e", size = 489971, upload-time = "2025-11-26T02:36:22.081Z" }, - { url = "https://files.pythonhosted.org/packages/85/11/0aa8455af26f0ae89e42be67f3a874255ee5d7f0f026fc86e8d56f76b428/fastar-0.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:e59673307b6a08210987059a2bdea2614fe26e3335d0e5d1a3d95f49a05b1418", size = 460467, upload-time = "2025-11-26T02:36:07.978Z" }, -] - [[package]] name = "filelock" version = "3.20.3" @@ -524,35 +349,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, ] -[[package]] -name = "httptools" -version = "0.7.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b5/46/120a669232c7bdedb9d52d4aeae7e6c7dfe151e99dc70802e2fc7a5e1993/httptools-0.7.1.tar.gz", hash = "sha256:abd72556974f8e7c74a259655924a717a2365b236c882c3f6f8a45fe94703ac9", size = 258961, upload-time = "2025-10-10T03:55:08.559Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/53/7f/403e5d787dc4942316e515e949b0c8a013d84078a915910e9f391ba9b3ed/httptools-0.7.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:38e0c83a2ea9746ebbd643bdfb521b9aa4a91703e2cd705c20443405d2fd16a5", size = 206280, upload-time = "2025-10-10T03:54:39.274Z" }, - { url = "https://files.pythonhosted.org/packages/2a/0d/7f3fd28e2ce311ccc998c388dd1c53b18120fda3b70ebb022b135dc9839b/httptools-0.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f25bbaf1235e27704f1a7b86cd3304eabc04f569c828101d94a0e605ef7205a5", size = 110004, upload-time = "2025-10-10T03:54:40.403Z" }, - { url = "https://files.pythonhosted.org/packages/84/a6/b3965e1e146ef5762870bbe76117876ceba51a201e18cc31f5703e454596/httptools-0.7.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2c15f37ef679ab9ecc06bfc4e6e8628c32a8e4b305459de7cf6785acd57e4d03", size = 517655, upload-time = "2025-10-10T03:54:41.347Z" }, - { url = "https://files.pythonhosted.org/packages/11/7d/71fee6f1844e6fa378f2eddde6c3e41ce3a1fb4b2d81118dd544e3441ec0/httptools-0.7.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7fe6e96090df46b36ccfaf746f03034e5ab723162bc51b0a4cf58305324036f2", size = 511440, upload-time = "2025-10-10T03:54:42.452Z" }, - { url = "https://files.pythonhosted.org/packages/22/a5/079d216712a4f3ffa24af4a0381b108aa9c45b7a5cc6eb141f81726b1823/httptools-0.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f72fdbae2dbc6e68b8239defb48e6a5937b12218e6ffc2c7846cc37befa84362", size = 495186, upload-time = "2025-10-10T03:54:43.937Z" }, - { url = "https://files.pythonhosted.org/packages/e9/9e/025ad7b65278745dee3bd0ebf9314934c4592560878308a6121f7f812084/httptools-0.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e99c7b90a29fd82fea9ef57943d501a16f3404d7b9ee81799d41639bdaae412c", size = 499192, upload-time = "2025-10-10T03:54:45.003Z" }, - { url = "https://files.pythonhosted.org/packages/6d/de/40a8f202b987d43afc4d54689600ff03ce65680ede2f31df348d7f368b8f/httptools-0.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:3e14f530fefa7499334a79b0cf7e7cd2992870eb893526fb097d51b4f2d0f321", size = 86694, upload-time = "2025-10-10T03:54:45.923Z" }, - { url = "https://files.pythonhosted.org/packages/09/8f/c77b1fcbfd262d422f12da02feb0d218fa228d52485b77b953832105bb90/httptools-0.7.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6babce6cfa2a99545c60bfef8bee0cc0545413cb0018f617c8059a30ad985de3", size = 202889, upload-time = "2025-10-10T03:54:47.089Z" }, - { url = "https://files.pythonhosted.org/packages/0a/1a/22887f53602feaa066354867bc49a68fc295c2293433177ee90870a7d517/httptools-0.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:601b7628de7504077dd3dcb3791c6b8694bbd967148a6d1f01806509254fb1ca", size = 108180, upload-time = "2025-10-10T03:54:48.052Z" }, - { url = "https://files.pythonhosted.org/packages/32/6a/6aaa91937f0010d288d3d124ca2946d48d60c3a5ee7ca62afe870e3ea011/httptools-0.7.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:04c6c0e6c5fb0739c5b8a9eb046d298650a0ff38cf42537fc372b28dc7e4472c", size = 478596, upload-time = "2025-10-10T03:54:48.919Z" }, - { url = "https://files.pythonhosted.org/packages/6d/70/023d7ce117993107be88d2cbca566a7c1323ccbaf0af7eabf2064fe356f6/httptools-0.7.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:69d4f9705c405ae3ee83d6a12283dc9feba8cc6aaec671b412917e644ab4fa66", size = 473268, upload-time = "2025-10-10T03:54:49.993Z" }, - { url = "https://files.pythonhosted.org/packages/32/4d/9dd616c38da088e3f436e9a616e1d0cc66544b8cdac405cc4e81c8679fc7/httptools-0.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:44c8f4347d4b31269c8a9205d8a5ee2df5322b09bbbd30f8f862185bb6b05346", size = 455517, upload-time = "2025-10-10T03:54:51.066Z" }, - { url = "https://files.pythonhosted.org/packages/1d/3a/a6c595c310b7df958e739aae88724e24f9246a514d909547778d776799be/httptools-0.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:465275d76db4d554918aba40bf1cbebe324670f3dfc979eaffaa5d108e2ed650", size = 458337, upload-time = "2025-10-10T03:54:52.196Z" }, - { url = "https://files.pythonhosted.org/packages/fd/82/88e8d6d2c51edc1cc391b6e044c6c435b6aebe97b1abc33db1b0b24cd582/httptools-0.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:322d00c2068d125bd570f7bf78b2d367dad02b919d8581d7476d8b75b294e3e6", size = 85743, upload-time = "2025-10-10T03:54:53.448Z" }, - { url = "https://files.pythonhosted.org/packages/34/50/9d095fcbb6de2d523e027a2f304d4551855c2f46e0b82befd718b8b20056/httptools-0.7.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:c08fe65728b8d70b6923ce31e3956f859d5e1e8548e6f22ec520a962c6757270", size = 203619, upload-time = "2025-10-10T03:54:54.321Z" }, - { url = "https://files.pythonhosted.org/packages/07/f0/89720dc5139ae54b03f861b5e2c55a37dba9a5da7d51e1e824a1f343627f/httptools-0.7.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7aea2e3c3953521c3c51106ee11487a910d45586e351202474d45472db7d72d3", size = 108714, upload-time = "2025-10-10T03:54:55.163Z" }, - { url = "https://files.pythonhosted.org/packages/b3/cb/eea88506f191fb552c11787c23f9a405f4c7b0c5799bf73f2249cd4f5228/httptools-0.7.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0e68b8582f4ea9166be62926077a3334064d422cf08ab87d8b74664f8e9058e1", size = 472909, upload-time = "2025-10-10T03:54:56.056Z" }, - { url = "https://files.pythonhosted.org/packages/e0/4a/a548bdfae6369c0d078bab5769f7b66f17f1bfaa6fa28f81d6be6959066b/httptools-0.7.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:df091cf961a3be783d6aebae963cc9b71e00d57fa6f149025075217bc6a55a7b", size = 470831, upload-time = "2025-10-10T03:54:57.219Z" }, - { url = "https://files.pythonhosted.org/packages/4d/31/14df99e1c43bd132eec921c2e7e11cda7852f65619bc0fc5bdc2d0cb126c/httptools-0.7.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f084813239e1eb403ddacd06a30de3d3e09a9b76e7894dcda2b22f8a726e9c60", size = 452631, upload-time = "2025-10-10T03:54:58.219Z" }, - { url = "https://files.pythonhosted.org/packages/22/d2/b7e131f7be8d854d48cb6d048113c30f9a46dca0c9a8b08fcb3fcd588cdc/httptools-0.7.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7347714368fb2b335e9063bc2b96f2f87a9ceffcd9758ac295f8bbcd3ffbc0ca", size = 452910, upload-time = "2025-10-10T03:54:59.366Z" }, - { url = "https://files.pythonhosted.org/packages/53/cf/878f3b91e4e6e011eff6d1fa9ca39f7eb17d19c9d7971b04873734112f30/httptools-0.7.1-cp314-cp314-win_amd64.whl", hash = "sha256:cfabda2a5bb85aa2a904ce06d974a3f30fb36cc63d7feaddec05d2050acede96", size = 88205, upload-time = "2025-10-10T03:55:00.389Z" }, -] - [[package]] name = "httpx" version = "0.28.1" @@ -570,11 +366,11 @@ wheels = [ [[package]] name = "identify" -version = "2.6.15" +version = "2.6.16" 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/5b/8d/e8b97e6bd3fb6fb271346f7981362f1e04d6a7463abd0de79e1fda17c067/identify-2.6.16.tar.gz", hash = "sha256:846857203b5511bbe94d5a352a48ef2359532bc8f6727b5544077a0dcfb24980", size = 99360, upload-time = "2026-01-12T18:58:58.201Z" } 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/b8/58/40fbbcefeda82364720eba5cf2270f98496bdfa19ea75b4cccae79c698e6/identify-2.6.16-py2.py3-none-any.whl", hash = "sha256:391ee4d77741d994189522896270b787aed8670389bfd60f326d677d64a6dfb0", size = 99202, upload-time = "2026-01-12T18:58:56.627Z" }, ] [[package]] @@ -588,14 +384,14 @@ wheels = [ [[package]] name = "importlib-metadata" -version = "8.7.0" +version = "8.7.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "zipp" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/76/66/650a33bd90f786193e4de4b3ad86ea60b53c89b669a5c7be931fac31cdb0/importlib_metadata-8.7.0.tar.gz", hash = "sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000", size = 56641, upload-time = "2025-04-27T15:29:01.736Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/49/3b30cad09e7771a4982d9975a8cbf64f00d4a1ececb53297f1d9a7be1b10/importlib_metadata-8.7.1.tar.gz", hash = "sha256:49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb", size = 57107, upload-time = "2025-12-21T10:00:19.278Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/20/b0/36bd937216ec521246249be3bf9855081de4c5e06a0c9b4219dbeda50373/importlib_metadata-8.7.0-py3-none-any.whl", hash = "sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd", size = 27656, upload-time = "2025-04-27T15:29:00.214Z" }, + { url = "https://files.pythonhosted.org/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151", size = 27865, upload-time = "2025-12-21T10:00:18.329Z" }, ] [[package]] @@ -616,68 +412,56 @@ 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" }, ] -[[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.7.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/24/5f3646ff414285e0f7708fa4e946b9bf538345a41d1c375c439467721a5e/librt-0.7.8.tar.gz", hash = "sha256:1a4ede613941d9c3470b0368be851df6bb78ab218635512d0370b27a277a0862", size = 148323, upload-time = "2026-01-14T12:56:16.876Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/56/04/79d8fcb43cae376c7adbab7b2b9f65e48432c9eced62ac96703bcc16e09b/librt-0.7.8-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9b6943885b2d49c48d0cff23b16be830ba46b0152d98f62de49e735c6e655a63", size = 57472, upload-time = "2026-01-14T12:55:08.528Z" }, + { url = "https://files.pythonhosted.org/packages/b4/ba/60b96e93043d3d659da91752689023a73981336446ae82078cddf706249e/librt-0.7.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:46ef1f4b9b6cc364b11eea0ecc0897314447a66029ee1e55859acb3dd8757c93", size = 58986, upload-time = "2026-01-14T12:55:09.466Z" }, + { url = "https://files.pythonhosted.org/packages/7c/26/5215e4cdcc26e7be7eee21955a7e13cbf1f6d7d7311461a6014544596fac/librt-0.7.8-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:907ad09cfab21e3c86e8f1f87858f7049d1097f77196959c033612f532b4e592", size = 168422, upload-time = "2026-01-14T12:55:10.499Z" }, + { url = "https://files.pythonhosted.org/packages/0f/84/e8d1bc86fa0159bfc24f3d798d92cafd3897e84c7fea7fe61b3220915d76/librt-0.7.8-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2991b6c3775383752b3ca0204842743256f3ad3deeb1d0adc227d56b78a9a850", size = 177478, upload-time = "2026-01-14T12:55:11.577Z" }, + { url = "https://files.pythonhosted.org/packages/57/11/d0268c4b94717a18aa91df1100e767b010f87b7ae444dafaa5a2d80f33a6/librt-0.7.8-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:03679b9856932b8c8f674e87aa3c55ea11c9274301f76ae8dc4d281bda55cf62", size = 192439, upload-time = "2026-01-14T12:55:12.7Z" }, + { url = "https://files.pythonhosted.org/packages/8d/56/1e8e833b95fe684f80f8894ae4d8b7d36acc9203e60478fcae599120a975/librt-0.7.8-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3968762fec1b2ad34ce57458b6de25dbb4142713e9ca6279a0d352fa4e9f452b", size = 191483, upload-time = "2026-01-14T12:55:13.838Z" }, + { url = "https://files.pythonhosted.org/packages/17/48/f11cf28a2cb6c31f282009e2208312aa84a5ee2732859f7856ee306176d5/librt-0.7.8-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:bb7a7807523a31f03061288cc4ffc065d684c39db7644c676b47d89553c0d714", size = 185376, upload-time = "2026-01-14T12:55:15.017Z" }, + { url = "https://files.pythonhosted.org/packages/b8/6a/d7c116c6da561b9155b184354a60a3d5cdbf08fc7f3678d09c95679d13d9/librt-0.7.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad64a14b1e56e702e19b24aae108f18ad1bf7777f3af5fcd39f87d0c5a814449", size = 206234, upload-time = "2026-01-14T12:55:16.571Z" }, + { url = "https://files.pythonhosted.org/packages/61/de/1975200bb0285fc921c5981d9978ce6ce11ae6d797df815add94a5a848a3/librt-0.7.8-cp312-cp312-win32.whl", hash = "sha256:0241a6ed65e6666236ea78203a73d800dbed896cf12ae25d026d75dc1fcd1dac", size = 44057, upload-time = "2026-01-14T12:55:18.077Z" }, + { url = "https://files.pythonhosted.org/packages/8e/cd/724f2d0b3461426730d4877754b65d39f06a41ac9d0a92d5c6840f72b9ae/librt-0.7.8-cp312-cp312-win_amd64.whl", hash = "sha256:6db5faf064b5bab9675c32a873436b31e01d66ca6984c6f7f92621656033a708", size = 50293, upload-time = "2026-01-14T12:55:19.179Z" }, + { url = "https://files.pythonhosted.org/packages/bd/cf/7e899acd9ee5727ad8160fdcc9994954e79fab371c66535c60e13b968ffc/librt-0.7.8-cp312-cp312-win_arm64.whl", hash = "sha256:57175aa93f804d2c08d2edb7213e09276bd49097611aefc37e3fa38d1fb99ad0", size = 43574, upload-time = "2026-01-14T12:55:20.185Z" }, + { url = "https://files.pythonhosted.org/packages/a1/fe/b1f9de2829cf7fc7649c1dcd202cfd873837c5cc2fc9e526b0e7f716c3d2/librt-0.7.8-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4c3995abbbb60b3c129490fa985dfe6cac11d88fc3c36eeb4fb1449efbbb04fc", size = 57500, upload-time = "2026-01-14T12:55:21.219Z" }, + { url = "https://files.pythonhosted.org/packages/eb/d4/4a60fbe2e53b825f5d9a77325071d61cd8af8506255067bf0c8527530745/librt-0.7.8-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:44e0c2cbc9bebd074cf2cdbe472ca185e824be4e74b1c63a8e934cea674bebf2", size = 59019, upload-time = "2026-01-14T12:55:22.256Z" }, + { url = "https://files.pythonhosted.org/packages/6a/37/61ff80341ba5159afa524445f2d984c30e2821f31f7c73cf166dcafa5564/librt-0.7.8-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4d2f1e492cae964b3463a03dc77a7fe8742f7855d7258c7643f0ee32b6651dd3", size = 169015, upload-time = "2026-01-14T12:55:23.24Z" }, + { url = "https://files.pythonhosted.org/packages/1c/86/13d4f2d6a93f181ebf2fc953868826653ede494559da8268023fe567fca3/librt-0.7.8-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:451e7ffcef8f785831fdb791bd69211f47e95dc4c6ddff68e589058806f044c6", size = 178161, upload-time = "2026-01-14T12:55:24.826Z" }, + { url = "https://files.pythonhosted.org/packages/88/26/e24ef01305954fc4d771f1f09f3dd682f9eb610e1bec188ffb719374d26e/librt-0.7.8-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3469e1af9f1380e093ae06bedcbdd11e407ac0b303a56bbe9afb1d6824d4982d", size = 193015, upload-time = "2026-01-14T12:55:26.04Z" }, + { url = "https://files.pythonhosted.org/packages/88/a0/92b6bd060e720d7a31ed474d046a69bd55334ec05e9c446d228c4b806ae3/librt-0.7.8-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f11b300027ce19a34f6d24ebb0a25fd0e24a9d53353225a5c1e6cadbf2916b2e", size = 192038, upload-time = "2026-01-14T12:55:27.208Z" }, + { url = "https://files.pythonhosted.org/packages/06/bb/6f4c650253704279c3a214dad188101d1b5ea23be0606628bc6739456624/librt-0.7.8-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4adc73614f0d3c97874f02f2c7fd2a27854e7e24ad532ea6b965459c5b757eca", size = 186006, upload-time = "2026-01-14T12:55:28.594Z" }, + { url = "https://files.pythonhosted.org/packages/dc/00/1c409618248d43240cadf45f3efb866837fa77e9a12a71481912135eb481/librt-0.7.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:60c299e555f87e4c01b2eca085dfccda1dde87f5a604bb45c2906b8305819a93", size = 206888, upload-time = "2026-01-14T12:55:30.214Z" }, + { url = "https://files.pythonhosted.org/packages/d9/83/b2cfe8e76ff5c1c77f8a53da3d5de62d04b5ebf7cf913e37f8bca43b5d07/librt-0.7.8-cp313-cp313-win32.whl", hash = "sha256:b09c52ed43a461994716082ee7d87618096851319bf695d57ec123f2ab708951", size = 44126, upload-time = "2026-01-14T12:55:31.44Z" }, + { url = "https://files.pythonhosted.org/packages/a9/0b/c59d45de56a51bd2d3a401fc63449c0ac163e4ef7f523ea8b0c0dee86ec5/librt-0.7.8-cp313-cp313-win_amd64.whl", hash = "sha256:f8f4a901a3fa28969d6e4519deceab56c55a09d691ea7b12ca830e2fa3461e34", size = 50262, upload-time = "2026-01-14T12:55:33.01Z" }, + { url = "https://files.pythonhosted.org/packages/fc/b9/973455cec0a1ec592395250c474164c4a58ebf3e0651ee920fef1a2623f1/librt-0.7.8-cp313-cp313-win_arm64.whl", hash = "sha256:43d4e71b50763fcdcf64725ac680d8cfa1706c928b844794a7aa0fa9ac8e5f09", size = 43600, upload-time = "2026-01-14T12:55:34.054Z" }, + { url = "https://files.pythonhosted.org/packages/1a/73/fa8814c6ce2d49c3827829cadaa1589b0bf4391660bd4510899393a23ebc/librt-0.7.8-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:be927c3c94c74b05128089a955fba86501c3b544d1d300282cc1b4bd370cb418", size = 57049, upload-time = "2026-01-14T12:55:35.056Z" }, + { url = "https://files.pythonhosted.org/packages/53/fe/f6c70956da23ea235fd2e3cc16f4f0b4ebdfd72252b02d1164dd58b4e6c3/librt-0.7.8-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7b0803e9008c62a7ef79058233db7ff6f37a9933b8f2573c05b07ddafa226611", size = 58689, upload-time = "2026-01-14T12:55:36.078Z" }, + { url = "https://files.pythonhosted.org/packages/1f/4d/7a2481444ac5fba63050d9abe823e6bc16896f575bfc9c1e5068d516cdce/librt-0.7.8-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:79feb4d00b2a4e0e05c9c56df707934f41fcb5fe53fd9efb7549068d0495b758", size = 166808, upload-time = "2026-01-14T12:55:37.595Z" }, + { url = "https://files.pythonhosted.org/packages/ac/3c/10901d9e18639f8953f57c8986796cfbf4c1c514844a41c9197cf87cb707/librt-0.7.8-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b9122094e3f24aa759c38f46bd8863433820654927370250f460ae75488b66ea", size = 175614, upload-time = "2026-01-14T12:55:38.756Z" }, + { url = "https://files.pythonhosted.org/packages/db/01/5cbdde0951a5090a80e5ba44e6357d375048123c572a23eecfb9326993a7/librt-0.7.8-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7e03bea66af33c95ce3addf87a9bf1fcad8d33e757bc479957ddbc0e4f7207ac", size = 189955, upload-time = "2026-01-14T12:55:39.939Z" }, + { url = "https://files.pythonhosted.org/packages/6a/b4/e80528d2f4b7eaf1d437fcbd6fc6ba4cbeb3e2a0cb9ed5a79f47c7318706/librt-0.7.8-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f1ade7f31675db00b514b98f9ab9a7698c7282dad4be7492589109471852d398", size = 189370, upload-time = "2026-01-14T12:55:41.057Z" }, + { url = "https://files.pythonhosted.org/packages/c1/ab/938368f8ce31a9787ecd4becb1e795954782e4312095daf8fd22420227c8/librt-0.7.8-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a14229ac62adcf1b90a15992f1ab9c69ae8b99ffb23cb64a90878a6e8a2f5b81", size = 183224, upload-time = "2026-01-14T12:55:42.328Z" }, + { url = "https://files.pythonhosted.org/packages/3c/10/559c310e7a6e4014ac44867d359ef8238465fb499e7eb31b6bfe3e3f86f5/librt-0.7.8-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5bcaaf624fd24e6a0cb14beac37677f90793a96864c67c064a91458611446e83", size = 203541, upload-time = "2026-01-14T12:55:43.501Z" }, + { url = "https://files.pythonhosted.org/packages/f8/db/a0db7acdb6290c215f343835c6efda5b491bb05c3ddc675af558f50fdba3/librt-0.7.8-cp314-cp314-win32.whl", hash = "sha256:7aa7d5457b6c542ecaed79cec4ad98534373c9757383973e638ccced0f11f46d", size = 40657, upload-time = "2026-01-14T12:55:44.668Z" }, + { url = "https://files.pythonhosted.org/packages/72/e0/4f9bdc2a98a798511e81edcd6b54fe82767a715e05d1921115ac70717f6f/librt-0.7.8-cp314-cp314-win_amd64.whl", hash = "sha256:3d1322800771bee4a91f3b4bd4e49abc7d35e65166821086e5afd1e6c0d9be44", size = 46835, upload-time = "2026-01-14T12:55:45.655Z" }, + { url = "https://files.pythonhosted.org/packages/f9/3d/59c6402e3dec2719655a41ad027a7371f8e2334aa794ed11533ad5f34969/librt-0.7.8-cp314-cp314-win_arm64.whl", hash = "sha256:5363427bc6a8c3b1719f8f3845ea53553d301382928a86e8fab7984426949bce", size = 39885, upload-time = "2026-01-14T12:55:47.138Z" }, + { url = "https://files.pythonhosted.org/packages/4e/9c/2481d80950b83085fb14ba3c595db56330d21bbc7d88a19f20165f3538db/librt-0.7.8-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:ca916919793a77e4a98d4a1701e345d337ce53be4a16620f063191f7322ac80f", size = 59161, upload-time = "2026-01-14T12:55:48.45Z" }, + { url = "https://files.pythonhosted.org/packages/96/79/108df2cfc4e672336765d54e3ff887294c1cc36ea4335c73588875775527/librt-0.7.8-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:54feb7b4f2f6706bb82325e836a01be805770443e2400f706e824e91f6441dde", size = 61008, upload-time = "2026-01-14T12:55:49.527Z" }, + { url = "https://files.pythonhosted.org/packages/46/f2/30179898f9994a5637459d6e169b6abdc982012c0a4b2d4c26f50c06f911/librt-0.7.8-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:39a4c76fee41007070f872b648cc2f711f9abf9a13d0c7162478043377b52c8e", size = 187199, upload-time = "2026-01-14T12:55:50.587Z" }, + { url = "https://files.pythonhosted.org/packages/b4/da/f7563db55cebdc884f518ba3791ad033becc25ff68eb70902b1747dc0d70/librt-0.7.8-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ac9c8a458245c7de80bc1b9765b177055efff5803f08e548dd4bb9ab9a8d789b", size = 198317, upload-time = "2026-01-14T12:55:51.991Z" }, + { url = "https://files.pythonhosted.org/packages/b3/6c/4289acf076ad371471fa86718c30ae353e690d3de6167f7db36f429272f1/librt-0.7.8-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:95b67aa7eff150f075fda09d11f6bfb26edffd300f6ab1666759547581e8f666", size = 210334, upload-time = "2026-01-14T12:55:53.682Z" }, + { url = "https://files.pythonhosted.org/packages/4a/7f/377521ac25b78ac0a5ff44127a0360ee6d5ddd3ce7327949876a30533daa/librt-0.7.8-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:535929b6eff670c593c34ff435d5440c3096f20fa72d63444608a5aef64dd581", size = 211031, upload-time = "2026-01-14T12:55:54.827Z" }, + { url = "https://files.pythonhosted.org/packages/c5/b1/e1e96c3e20b23d00cf90f4aad48f0deb4cdfec2f0ed8380d0d85acf98bbf/librt-0.7.8-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:63937bd0f4d1cb56653dc7ae900d6c52c41f0015e25aaf9902481ee79943b33a", size = 204581, upload-time = "2026-01-14T12:55:56.811Z" }, + { url = "https://files.pythonhosted.org/packages/43/71/0f5d010e92ed9747e14bef35e91b6580533510f1e36a8a09eb79ee70b2f0/librt-0.7.8-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cf243da9e42d914036fd362ac3fa77d80a41cadcd11ad789b1b5eec4daaf67ca", size = 224731, upload-time = "2026-01-14T12:55:58.175Z" }, + { url = "https://files.pythonhosted.org/packages/22/f0/07fb6ab5c39a4ca9af3e37554f9d42f25c464829254d72e4ebbd81da351c/librt-0.7.8-cp314-cp314t-win32.whl", hash = "sha256:171ca3a0a06c643bd0a2f62a8944e1902c94aa8e5da4db1ea9a8daf872685365", size = 41173, upload-time = "2026-01-14T12:55:59.315Z" }, + { url = "https://files.pythonhosted.org/packages/24/d4/7e4be20993dc6a782639625bd2f97f3c66125c7aa80c82426956811cfccf/librt-0.7.8-cp314-cp314t-win_amd64.whl", hash = "sha256:445b7304145e24c60288a2f172b5ce2ca35c0f81605f5299f3fa567e189d2e32", size = 47668, upload-time = "2026-01-14T12:56:00.261Z" }, + { url = "https://files.pythonhosted.org/packages/fc/85/69f92b2a7b3c0f88ffe107c86b952b397004b5b8ea5a81da3d9c04c04422/librt-0.7.8-cp314-cp314t-win_arm64.whl", hash = "sha256:8766ece9de08527deabcd7cb1b4f1a967a385d26e33e536d6d8913db6ef74f06", size = 40550, upload-time = "2026-01-14T12:56:01.542Z" }, ] [[package]] @@ -691,7 +475,6 @@ dependencies = [ [package.dev-dependencies] dev = [ { name = "bandit" }, - { name = "fastapi", extra = ["standard"] }, { name = "httpx" }, { name = "mypy" }, { name = "pre-commit" }, @@ -711,7 +494,6 @@ requires-dist = [{ name = "sql-to-arc", editable = "middleware/sql_to_arc" }] [package.metadata.requires-dev] dev = [ { name = "bandit", specifier = ">=1.7.0" }, - { name = "fastapi", extras = ["standard"], specifier = ">=0.124.0" }, { name = "httpx", specifier = ">=0.28.1" }, { name = "mypy", specifier = ">=1.19.0" }, { name = "pre-commit", specifier = ">=4.0.0" }, @@ -737,69 +519,6 @@ 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 = "mccabe" version = "0.7.0" @@ -862,11 +581,11 @@ 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]] @@ -896,32 +615,32 @@ wheels = [ [[package]] name = "opentelemetry-exporter-otlp" -version = "1.39.0" +version = "1.39.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-exporter-otlp-proto-grpc" }, { name = "opentelemetry-exporter-otlp-proto-http" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/13/be/0e9d889f47e55cadc4041e5b53d4e0cc688f9a74811134fb0ba7cbee6905/opentelemetry_exporter_otlp-1.39.0.tar.gz", hash = "sha256:b405da0287b895fe4e2450dedb2a5b072debba1dfcfed5bdb3d1d183d8daa296", size = 6146, upload-time = "2025-12-03T13:19:58.381Z" } +sdist = { url = "https://files.pythonhosted.org/packages/30/9c/3ab1db90f32da200dba332658f2bbe602369e3d19f6aba394031a42635be/opentelemetry_exporter_otlp-1.39.1.tar.gz", hash = "sha256:7cf7470e9fd0060c8a38a23e4f695ac686c06a48ad97f8d4867bc9b420180b9c", size = 6147, upload-time = "2025-12-11T13:32:40.309Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fc/35/212d2cae4fa9a2c02e74438612268b640ab577b8ccb04590371eb4e0f542/opentelemetry_exporter_otlp-1.39.0-py3-none-any.whl", hash = "sha256:fe155d6968d581b325574ad6dc267c8de299397b18d11feeda2206d0a47928a9", size = 7017, upload-time = "2025-12-03T13:19:35.686Z" }, + { url = "https://files.pythonhosted.org/packages/00/6c/bdc82a066e6fb1dcf9e8cc8d4e026358fe0f8690700cc6369a6bf9bd17a7/opentelemetry_exporter_otlp-1.39.1-py3-none-any.whl", hash = "sha256:68ae69775291f04f000eb4b698ff16ff685fdebe5cb52871bc4e87938a7b00fe", size = 7019, upload-time = "2025-12-11T13:32:19.387Z" }, ] [[package]] name = "opentelemetry-exporter-otlp-proto-common" -version = "1.39.0" +version = "1.39.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-proto" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/11/cb/3a29ce606b10c76d413d6edd42d25a654af03e73e50696611e757d2602f3/opentelemetry_exporter_otlp_proto_common-1.39.0.tar.gz", hash = "sha256:a135fceed1a6d767f75be65bd2845da344dd8b9258eeed6bc48509d02b184409", size = 20407, upload-time = "2025-12-03T13:19:59.003Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e9/9d/22d241b66f7bbde88a3bfa6847a351d2c46b84de23e71222c6aae25c7050/opentelemetry_exporter_otlp_proto_common-1.39.1.tar.gz", hash = "sha256:763370d4737a59741c89a67b50f9e39271639ee4afc999dadfe768541c027464", size = 20409, upload-time = "2025-12-11T13:32:40.885Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ef/c6/215edba62d13a3948c718b289539f70e40965bc37fc82ecd55bb0b749c1a/opentelemetry_exporter_otlp_proto_common-1.39.0-py3-none-any.whl", hash = "sha256:3d77be7c4bdf90f1a76666c934368b8abed730b5c6f0547a2ec57feb115849ac", size = 18367, upload-time = "2025-12-03T13:19:36.906Z" }, + { url = "https://files.pythonhosted.org/packages/8c/02/ffc3e143d89a27ac21fd557365b98bd0653b98de8a101151d5805b5d4c33/opentelemetry_exporter_otlp_proto_common-1.39.1-py3-none-any.whl", hash = "sha256:08f8a5862d64cc3435105686d0216c1365dc5701f86844a8cd56597d0c764fde", size = 18366, upload-time = "2025-12-11T13:32:20.2Z" }, ] [[package]] name = "opentelemetry-exporter-otlp-proto-grpc" -version = "1.39.0" +version = "1.39.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "googleapis-common-protos" }, @@ -932,14 +651,14 @@ dependencies = [ { name = "opentelemetry-sdk" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/7e/62/4db083ee9620da3065eeb559e9fc128f41a1d15e7c48d7c83aafbccd354c/opentelemetry_exporter_otlp_proto_grpc-1.39.0.tar.gz", hash = "sha256:7e7bb3f436006836c0e0a42ac619097746ad5553ad7128a5bd4d3e727f37fc06", size = 24650, upload-time = "2025-12-03T13:20:00.06Z" } +sdist = { url = "https://files.pythonhosted.org/packages/53/48/b329fed2c610c2c32c9366d9dc597202c9d1e58e631c137ba15248d8850f/opentelemetry_exporter_otlp_proto_grpc-1.39.1.tar.gz", hash = "sha256:772eb1c9287485d625e4dbe9c879898e5253fea111d9181140f51291b5fec3ad", size = 24650, upload-time = "2025-12-11T13:32:41.429Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/56/e8/d420b94ffddfd8cff85bb4aa5d98da26ce7935dc3cf3eca6b83cd39ab436/opentelemetry_exporter_otlp_proto_grpc-1.39.0-py3-none-any.whl", hash = "sha256:758641278050de9bb895738f35ff8840e4a47685b7e6ef4a201fe83196ba7a05", size = 19765, upload-time = "2025-12-03T13:19:38.143Z" }, + { url = "https://files.pythonhosted.org/packages/81/a3/cc9b66575bd6597b98b886a2067eea2693408d2d5f39dad9ab7fc264f5f3/opentelemetry_exporter_otlp_proto_grpc-1.39.1-py3-none-any.whl", hash = "sha256:fa1c136a05c7e9b4c09f739469cbdb927ea20b34088ab1d959a849b5cc589c18", size = 19766, upload-time = "2025-12-11T13:32:21.027Z" }, ] [[package]] name = "opentelemetry-exporter-otlp-proto-http" -version = "1.39.0" +version = "1.39.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "googleapis-common-protos" }, @@ -950,21 +669,21 @@ dependencies = [ { name = "requests" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/81/dc/1e9bf3f6a28e29eba516bc0266e052996d02bc7e92675f3cd38169607609/opentelemetry_exporter_otlp_proto_http-1.39.0.tar.gz", hash = "sha256:28d78fc0eb82d5a71ae552263d5012fa3ebad18dfd189bf8d8095ba0e65ee1ed", size = 17287, upload-time = "2025-12-03T13:20:01.134Z" } +sdist = { url = "https://files.pythonhosted.org/packages/80/04/2a08fa9c0214ae38880df01e8bfae12b067ec0793446578575e5080d6545/opentelemetry_exporter_otlp_proto_http-1.39.1.tar.gz", hash = "sha256:31bdab9745c709ce90a49a0624c2bd445d31a28ba34275951a6a362d16a0b9cb", size = 17288, upload-time = "2025-12-11T13:32:42.029Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/bc/46/e4a102e17205bb05a50dbf24ef0e92b66b648cd67db9a68865af06a242fd/opentelemetry_exporter_otlp_proto_http-1.39.0-py3-none-any.whl", hash = "sha256:5789cb1375a8b82653328c0ce13a054d285f774099faf9d068032a49de4c7862", size = 19639, upload-time = "2025-12-03T13:19:39.536Z" }, + { url = "https://files.pythonhosted.org/packages/95/f1/b27d3e2e003cd9a3592c43d099d2ed8d0a947c15281bf8463a256db0b46c/opentelemetry_exporter_otlp_proto_http-1.39.1-py3-none-any.whl", hash = "sha256:d9f5207183dd752a412c4cd564ca8875ececba13be6e9c6c370ffb752fd59985", size = 19641, upload-time = "2025-12-11T13:32:22.248Z" }, ] [[package]] name = "opentelemetry-proto" -version = "1.39.0" +version = "1.39.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "protobuf" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/48/b5/64d2f8c3393cd13ea2092106118f7b98461ba09333d40179a31444c6f176/opentelemetry_proto-1.39.0.tar.gz", hash = "sha256:c1fa48678ad1a1624258698e59be73f990b7fc1f39e73e16a9d08eef65dd838c", size = 46153, upload-time = "2025-12-03T13:20:08.729Z" } +sdist = { url = "https://files.pythonhosted.org/packages/49/1d/f25d76d8260c156c40c97c9ed4511ec0f9ce353f8108ca6e7561f82a06b2/opentelemetry_proto-1.39.1.tar.gz", hash = "sha256:6c8e05144fc0d3ed4d22c2289c6b126e03bcd0e6a7da0f16cedd2e1c2772e2c8", size = 46152, upload-time = "2025-12-11T13:32:48.681Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e3/4d/d500e1862beed68318705732d1976c390f4a72ca8009c4983ff627acff20/opentelemetry_proto-1.39.0-py3-none-any.whl", hash = "sha256:1e086552ac79acb501485ff0ce75533f70f3382d43d0a30728eeee594f7bf818", size = 72534, upload-time = "2025-12-03T13:19:50.251Z" }, + { url = "https://files.pythonhosted.org/packages/51/95/b40c96a7b5203005a0b03d8ce8cd212ff23f1793d5ba289c87a097571b18/opentelemetry_proto-1.39.1-py3-none-any.whl", hash = "sha256:22cdc78efd3b3765d09e68bfbd010d4fc254c9818afd0b6b423387d9dee46007", size = 72535, upload-time = "2025-12-11T13:32:33.866Z" }, ] [[package]] @@ -996,20 +715,20 @@ wheels = [ [[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]] @@ -1048,17 +767,17 @@ wheels = [ [[package]] name = "protobuf" -version = "6.33.2" +version = "6.33.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/34/44/e49ecff446afeec9d1a66d6bbf9adc21e3c7cea7803a920ca3773379d4f6/protobuf-6.33.2.tar.gz", hash = "sha256:56dc370c91fbb8ac85bc13582c9e373569668a290aa2e66a590c2a0d35ddb9e4", size = 444296, upload-time = "2025-12-06T00:17:53.311Z" } +sdist = { url = "https://files.pythonhosted.org/packages/53/b8/cda15d9d46d03d4aa3a67cb6bffe05173440ccf86a9541afaf7ac59a1b6b/protobuf-6.33.4.tar.gz", hash = "sha256:dc2e61bca3b10470c1912d166fe0af67bfc20eb55971dcef8dfa48ce14f0ed91", size = 444346, upload-time = "2026-01-12T18:33:40.109Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/bc/91/1e3a34881a88697a7354ffd177e8746e97a722e5e8db101544b47e84afb1/protobuf-6.33.2-cp310-abi3-win32.whl", hash = "sha256:87eb388bd2d0f78febd8f4c8779c79247b26a5befad525008e49a6955787ff3d", size = 425603, upload-time = "2025-12-06T00:17:41.114Z" }, - { url = "https://files.pythonhosted.org/packages/64/20/4d50191997e917ae13ad0a235c8b42d8c1ab9c3e6fd455ca16d416944355/protobuf-6.33.2-cp310-abi3-win_amd64.whl", hash = "sha256:fc2a0e8b05b180e5fc0dd1559fe8ebdae21a27e81ac77728fb6c42b12c7419b4", size = 436930, upload-time = "2025-12-06T00:17:43.278Z" }, - { url = "https://files.pythonhosted.org/packages/b2/ca/7e485da88ba45c920fb3f50ae78de29ab925d9e54ef0de678306abfbb497/protobuf-6.33.2-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:d9b19771ca75935b3a4422957bc518b0cecb978b31d1dd12037b088f6bcc0e43", size = 427621, upload-time = "2025-12-06T00:17:44.445Z" }, - { url = "https://files.pythonhosted.org/packages/7d/4f/f743761e41d3b2b2566748eb76bbff2b43e14d5fcab694f494a16458b05f/protobuf-6.33.2-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:b5d3b5625192214066d99b2b605f5783483575656784de223f00a8d00754fc0e", size = 324460, upload-time = "2025-12-06T00:17:45.678Z" }, - { url = "https://files.pythonhosted.org/packages/b1/fa/26468d00a92824020f6f2090d827078c09c9c587e34cbfd2d0c7911221f8/protobuf-6.33.2-cp39-abi3-manylinux2014_s390x.whl", hash = "sha256:8cd7640aee0b7828b6d03ae518b5b4806fdfc1afe8de82f79c3454f8aef29872", size = 339168, upload-time = "2025-12-06T00:17:46.813Z" }, - { url = "https://files.pythonhosted.org/packages/56/13/333b8f421738f149d4fe5e49553bc2a2ab75235486259f689b4b91f96cec/protobuf-6.33.2-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:1f8017c48c07ec5859106533b682260ba3d7c5567b1ca1f24297ce03384d1b4f", size = 323270, upload-time = "2025-12-06T00:17:48.253Z" }, - { url = "https://files.pythonhosted.org/packages/0e/15/4f02896cc3df04fc465010a4c6a0cd89810f54617a32a70ef531ed75d61c/protobuf-6.33.2-py3-none-any.whl", hash = "sha256:7636aad9bb01768870266de5dc009de2d1b936771b38a793f73cbbf279c91c5c", size = 170501, upload-time = "2025-12-06T00:17:52.211Z" }, + { url = "https://files.pythonhosted.org/packages/e0/be/24ef9f3095bacdf95b458543334d0c4908ccdaee5130420bf064492c325f/protobuf-6.33.4-cp310-abi3-win32.whl", hash = "sha256:918966612c8232fc6c24c78e1cd89784307f5814ad7506c308ee3cf86662850d", size = 425612, upload-time = "2026-01-12T18:33:29.656Z" }, + { url = "https://files.pythonhosted.org/packages/31/ad/e5693e1974a28869e7cd244302911955c1cebc0161eb32dfa2b25b6e96f0/protobuf-6.33.4-cp310-abi3-win_amd64.whl", hash = "sha256:8f11ffae31ec67fc2554c2ef891dcb561dae9a2a3ed941f9e134c2db06657dbc", size = 436962, upload-time = "2026-01-12T18:33:31.345Z" }, + { url = "https://files.pythonhosted.org/packages/66/15/6ee23553b6bfd82670207ead921f4d8ef14c107e5e11443b04caeb5ab5ec/protobuf-6.33.4-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:2fe67f6c014c84f655ee06f6f66213f9254b3a8b6bda6cda0ccd4232c73c06f0", size = 427612, upload-time = "2026-01-12T18:33:32.646Z" }, + { url = "https://files.pythonhosted.org/packages/2b/48/d301907ce6d0db75f959ca74f44b475a9caa8fcba102d098d3c3dd0f2d3f/protobuf-6.33.4-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:757c978f82e74d75cba88eddec479df9b99a42b31193313b75e492c06a51764e", size = 324484, upload-time = "2026-01-12T18:33:33.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/1c/e53078d3f7fe710572ab2dcffd993e1e3b438ae71cfc031b71bae44fcb2d/protobuf-6.33.4-cp39-abi3-manylinux2014_s390x.whl", hash = "sha256:c7c64f259c618f0bef7bee042075e390debbf9682334be2b67408ec7c1c09ee6", size = 339256, upload-time = "2026-01-12T18:33:35.231Z" }, + { url = "https://files.pythonhosted.org/packages/e8/8e/971c0edd084914f7ee7c23aa70ba89e8903918adca179319ee94403701d5/protobuf-6.33.4-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:3df850c2f8db9934de4cf8f9152f8dc2558f49f298f37f90c517e8e5c84c30e9", size = 323311, upload-time = "2026-01-12T18:33:36.305Z" }, + { url = "https://files.pythonhosted.org/packages/75/b1/1dc83c2c661b4c62d56cc081706ee33a4fc2835bd90f965baa2663ef7676/protobuf-6.33.4-py3-none-any.whl", hash = "sha256:1fe3730068fcf2e595816a6c34fe66eeedd37d51d0400b72fabc848811fdc1bc", size = 170532, upload-time = "2026-01-12T18:33:39.199Z" }, ] [[package]] @@ -1134,11 +853,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" }, ] -[package.optional-dependencies] -email = [ - { name = "email-validator" }, -] - [[package]] name = "pydantic-core" version = "2.41.5" @@ -1304,24 +1018,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5a/cc/06253936f4a7fa2e0f48dfe6d851d9c56df896a9ab09ac019d70b760619c/pytest_mock-3.15.1-py3-none-any.whl", hash = "sha256:0a25e2eb88fe5168d535041d09a4529a188176ae608a6d249ee65abc0949630d", size = 10095, upload-time = "2025-09-16T16:37:25.734Z" }, ] -[[package]] -name = "python-dotenv" -version = "1.2.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f0/26/19cadc79a718c5edbec86fd4919a6b6d3f681039a2f6d66d14be94e75fb9/python_dotenv-1.2.1.tar.gz", hash = "sha256:42667e897e16ab0d66954af0e60a9caa94f0fd4ecf3aaf6d2d260eec1aa36ad6", size = 44221, upload-time = "2025-10-26T15:12:10.434Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61", size = 21230, upload-time = "2025-10-26T15:12:09.109Z" }, -] - -[[package]] -name = "python-multipart" -version = "0.0.22" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/94/01/979e98d542a70714b0cb2b6728ed0b7c46792b695e3eaec3e20711271ca3/python_multipart-0.0.22.tar.gz", hash = "sha256:7340bef99a7e0032613f56dc36027b959fd3b30a787ed62d310e951f7c3a3a58", size = 37612, upload-time = "2026-01-25T10:15:56.219Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1b/d0/397f9626e711ff749a95d96b7af99b9c566a9bb5129b8e4c10fc4d100304/python_multipart-0.0.22-py3-none-any.whl", hash = "sha256:2b2cd894c83d21bf49d702499531c7bafd057d730c201782048f7945d82de155", size = 24579, upload-time = "2026-01-25T10:15:54.811Z" }, -] - [[package]] name = "pyyaml" version = "6.0.3" @@ -1397,97 +1093,15 @@ wheels = [ [[package]] name = "rich" -version = "14.2.0" +version = "14.3.1" 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/a1/84/4831f881aa6ff3c976f6d6809b58cdfa350593ffc0dc3c58f5f6586780fb/rich-14.3.1.tar.gz", hash = "sha256:b8c5f568a3a749f9290ec6bddedf835cec33696bfc1e48bcfecb276c7386e4b8", size = 230125, upload-time = "2026-01-24T21:40:44.847Z" } 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" }, -] - -[[package]] -name = "rich-toolkit" -version = "0.17.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "click" }, - { name = "rich" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ad/d0/8f8de36e1abf8339b497ce700dd7251ca465ffca4a1976969b0eaeb596fb/rich_toolkit-0.17.0.tar.gz", hash = "sha256:17ca7a32e613001aa0945ddea27a246f6de01dfc4c12403254c057a8ee542977", size = 187955, upload-time = "2025-11-27T11:10:24.863Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b2/42/ef2ed40699567661d03b0b511ac46cf6cee736de8f3666819c12d6d20696/rich_toolkit-0.17.0-py3-none-any.whl", hash = "sha256:06fb47a5c5259d6b480287cd38aff5f551b6e1a307f90ed592453dd360e4e71e", size = 31412, upload-time = "2025-11-27T11:10:23.847Z" }, -] - -[[package]] -name = "rignore" -version = "0.7.6" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e5/f5/8bed2310abe4ae04b67a38374a4d311dd85220f5d8da56f47ae9361be0b0/rignore-0.7.6.tar.gz", hash = "sha256:00d3546cd793c30cb17921ce674d2c8f3a4b00501cb0e3dd0e82217dbeba2671", size = 57140, upload-time = "2025-11-05T21:41:21.968Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0b/0e/012556ef3047a2628842b44e753bb15f4dc46806780ff090f1e8fe4bf1eb/rignore-0.7.6-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:03e82348cb7234f8d9b2834f854400ddbbd04c0f8f35495119e66adbd37827a8", size = 883488, upload-time = "2025-11-05T20:42:41.359Z" }, - { url = "https://files.pythonhosted.org/packages/93/b0/d4f1f3fe9eb3f8e382d45ce5b0547ea01c4b7e0b4b4eb87bcd66a1d2b888/rignore-0.7.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b9e624f6be6116ea682e76c5feb71ea91255c67c86cb75befe774365b2931961", size = 820411, upload-time = "2025-11-05T20:42:24.782Z" }, - { url = "https://files.pythonhosted.org/packages/4a/c8/dea564b36dedac8de21c18e1851789545bc52a0c22ece9843444d5608a6a/rignore-0.7.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bda49950d405aa8d0ebe26af807c4e662dd281d926530f03f29690a2e07d649a", size = 897821, upload-time = "2025-11-05T20:40:52.613Z" }, - { url = "https://files.pythonhosted.org/packages/b3/2b/ee96db17ac1835e024c5d0742eefb7e46de60020385ac883dd3d1cde2c1f/rignore-0.7.6-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b5fd5ab3840b8c16851d327ed06e9b8be6459702a53e5ab1fc4073b684b3789e", size = 873963, upload-time = "2025-11-05T20:41:07.49Z" }, - { url = "https://files.pythonhosted.org/packages/a5/8c/ad5a57bbb9d14d5c7e5960f712a8a0b902472ea3f4a2138cbf70d1777b75/rignore-0.7.6-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ced2a248352636a5c77504cb755dc02c2eef9a820a44d3f33061ce1bb8a7f2d2", size = 1169216, upload-time = "2025-11-05T20:41:23.73Z" }, - { url = "https://files.pythonhosted.org/packages/80/e6/5b00bc2a6bc1701e6878fca798cf5d9125eb3113193e33078b6fc0d99123/rignore-0.7.6-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a04a3b73b75ddc12c9c9b21efcdaab33ca3832941d6f1d67bffd860941cd448a", size = 942942, upload-time = "2025-11-05T20:41:39.393Z" }, - { url = "https://files.pythonhosted.org/packages/85/e5/7f99bd0cc9818a91d0e8b9acc65b792e35750e3bdccd15a7ee75e64efca4/rignore-0.7.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d24321efac92140b7ec910ac7c53ab0f0c86a41133d2bb4b0e6a7c94967f44dd", size = 959787, upload-time = "2025-11-05T20:42:09.765Z" }, - { url = "https://files.pythonhosted.org/packages/55/54/2ffea79a7c1eabcede1926347ebc2a81bc6b81f447d05b52af9af14948b9/rignore-0.7.6-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:73c7aa109d41e593785c55fdaa89ad80b10330affa9f9d3e3a51fa695f739b20", size = 984245, upload-time = "2025-11-05T20:41:54.062Z" }, - { url = "https://files.pythonhosted.org/packages/41/f7/e80f55dfe0f35787fa482aa18689b9c8251e045076c35477deb0007b3277/rignore-0.7.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1734dc49d1e9501b07852ef44421f84d9f378da9fbeda729e77db71f49cac28b", size = 1078647, upload-time = "2025-11-05T21:40:13.463Z" }, - { url = "https://files.pythonhosted.org/packages/d4/cf/2c64f0b6725149f7c6e7e5a909d14354889b4beaadddaa5fff023ec71084/rignore-0.7.6-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5719ea14ea2b652c0c0894be5dfde954e1853a80dea27dd2fbaa749618d837f5", size = 1139186, upload-time = "2025-11-05T21:40:31.27Z" }, - { url = "https://files.pythonhosted.org/packages/75/95/a86c84909ccc24af0d094b50d54697951e576c252a4d9f21b47b52af9598/rignore-0.7.6-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:8e23424fc7ce35726854f639cb7968151a792c0c3d9d082f7f67e0c362cfecca", size = 1117604, upload-time = "2025-11-05T21:40:48.07Z" }, - { url = "https://files.pythonhosted.org/packages/7f/5e/13b249613fd5d18d58662490ab910a9f0be758981d1797789913adb4e918/rignore-0.7.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3efdcf1dd84d45f3e2bd2f93303d9be103888f56dfa7c3349b5bf4f0657ec696", size = 1127725, upload-time = "2025-11-05T21:41:05.804Z" }, - { url = "https://files.pythonhosted.org/packages/c7/28/fa5dcd1e2e16982c359128664e3785f202d3eca9b22dd0b2f91c4b3d242f/rignore-0.7.6-cp312-cp312-win32.whl", hash = "sha256:ccca9d1a8b5234c76b71546fc3c134533b013f40495f394a65614a81f7387046", size = 646145, upload-time = "2025-11-05T21:41:51.096Z" }, - { url = "https://files.pythonhosted.org/packages/26/87/69387fb5dd81a0f771936381431780b8cf66fcd2cfe9495e1aaf41548931/rignore-0.7.6-cp312-cp312-win_amd64.whl", hash = "sha256:c96a285e4a8bfec0652e0bfcf42b1aabcdda1e7625f5006d188e3b1c87fdb543", size = 726090, upload-time = "2025-11-05T21:41:36.485Z" }, - { url = "https://files.pythonhosted.org/packages/24/5f/e8418108dcda8087fb198a6f81caadbcda9fd115d61154bf0df4d6d3619b/rignore-0.7.6-cp312-cp312-win_arm64.whl", hash = "sha256:a64a750e7a8277a323f01ca50b7784a764845f6cce2fe38831cb93f0508d0051", size = 656317, upload-time = "2025-11-05T21:41:25.305Z" }, - { url = "https://files.pythonhosted.org/packages/b7/8a/a4078f6e14932ac7edb171149c481de29969d96ddee3ece5dc4c26f9e0c3/rignore-0.7.6-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:2bdab1d31ec9b4fb1331980ee49ea051c0d7f7bb6baa28b3125ef03cdc48fdaf", size = 883057, upload-time = "2025-11-05T20:42:42.741Z" }, - { url = "https://files.pythonhosted.org/packages/f9/8f/f8daacd177db4bf7c2223bab41e630c52711f8af9ed279be2058d2fe4982/rignore-0.7.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:90f0a00ce0c866c275bf888271f1dc0d2140f29b82fcf33cdbda1e1a6af01010", size = 820150, upload-time = "2025-11-05T20:42:26.545Z" }, - { url = "https://files.pythonhosted.org/packages/36/31/b65b837e39c3f7064c426754714ac633b66b8c2290978af9d7f513e14aa9/rignore-0.7.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c1ad295537041dc2ed4b540fb1a3906bd9ede6ccdad3fe79770cd89e04e3c73c", size = 897406, upload-time = "2025-11-05T20:40:53.854Z" }, - { url = "https://files.pythonhosted.org/packages/ca/58/1970ce006c427e202ac7c081435719a076c478f07b3a23f469227788dc23/rignore-0.7.6-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f782dbd3a65a5ac85adfff69e5c6b101285ef3f845c3a3cae56a54bebf9fe116", size = 874050, upload-time = "2025-11-05T20:41:08.922Z" }, - { url = "https://files.pythonhosted.org/packages/d4/00/eb45db9f90137329072a732273be0d383cb7d7f50ddc8e0bceea34c1dfdf/rignore-0.7.6-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65cece3b36e5b0826d946494734c0e6aaf5a0337e18ff55b071438efe13d559e", size = 1167835, upload-time = "2025-11-05T20:41:24.997Z" }, - { url = "https://files.pythonhosted.org/packages/f3/f1/6f1d72ddca41a64eed569680587a1236633587cc9f78136477ae69e2c88a/rignore-0.7.6-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d7e4bb66c13cd7602dc8931822c02dfbbd5252015c750ac5d6152b186f0a8be0", size = 941945, upload-time = "2025-11-05T20:41:40.628Z" }, - { url = "https://files.pythonhosted.org/packages/48/6f/2f178af1c1a276a065f563ec1e11e7a9e23d4996fd0465516afce4b5c636/rignore-0.7.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:297e500c15766e196f68aaaa70e8b6db85fa23fdc075b880d8231fdfba738cd7", size = 959067, upload-time = "2025-11-05T20:42:11.09Z" }, - { url = "https://files.pythonhosted.org/packages/5b/db/423a81c4c1e173877c7f9b5767dcaf1ab50484a94f60a0b2ed78be3fa765/rignore-0.7.6-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a07084211a8d35e1a5b1d32b9661a5ed20669970b369df0cf77da3adea3405de", size = 984438, upload-time = "2025-11-05T20:41:55.443Z" }, - { url = "https://files.pythonhosted.org/packages/31/eb/c4f92cc3f2825d501d3c46a244a671eb737fc1bcf7b05a3ecd34abb3e0d7/rignore-0.7.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:181eb2a975a22256a1441a9d2f15eb1292839ea3f05606620bd9e1938302cf79", size = 1078365, upload-time = "2025-11-05T21:40:15.148Z" }, - { url = "https://files.pythonhosted.org/packages/26/09/99442f02794bd7441bfc8ed1c7319e890449b816a7493b2db0e30af39095/rignore-0.7.6-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:7bbcdc52b5bf9f054b34ce4af5269df5d863d9c2456243338bc193c28022bd7b", size = 1139066, upload-time = "2025-11-05T21:40:32.771Z" }, - { url = "https://files.pythonhosted.org/packages/2c/88/bcfc21e520bba975410e9419450f4b90a2ac8236b9a80fd8130e87d098af/rignore-0.7.6-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:f2e027a6da21a7c8c0d87553c24ca5cc4364def18d146057862c23a96546238e", size = 1118036, upload-time = "2025-11-05T21:40:49.646Z" }, - { url = "https://files.pythonhosted.org/packages/e2/25/d37215e4562cda5c13312636393aea0bafe38d54d4e0517520a4cc0753ec/rignore-0.7.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ee4a18b82cbbc648e4aac1510066682fe62beb5dc88e2c67c53a83954e541360", size = 1127550, upload-time = "2025-11-05T21:41:07.648Z" }, - { url = "https://files.pythonhosted.org/packages/dc/76/a264ab38bfa1620ec12a8ff1c07778da89e16d8c0f3450b0333020d3d6dc/rignore-0.7.6-cp313-cp313-win32.whl", hash = "sha256:a7d7148b6e5e95035d4390396895adc384d37ff4e06781a36fe573bba7c283e5", size = 646097, upload-time = "2025-11-05T21:41:53.201Z" }, - { url = "https://files.pythonhosted.org/packages/62/44/3c31b8983c29ea8832b6082ddb1d07b90379c2d993bd20fce4487b71b4f4/rignore-0.7.6-cp313-cp313-win_amd64.whl", hash = "sha256:b037c4b15a64dced08fc12310ee844ec2284c4c5c1ca77bc37d0a04f7bff386e", size = 726170, upload-time = "2025-11-05T21:41:38.131Z" }, - { url = "https://files.pythonhosted.org/packages/aa/41/e26a075cab83debe41a42661262f606166157df84e0e02e2d904d134c0d8/rignore-0.7.6-cp313-cp313-win_arm64.whl", hash = "sha256:e47443de9b12fe569889bdbe020abe0e0b667516ee2ab435443f6d0869bd2804", size = 656184, upload-time = "2025-11-05T21:41:27.396Z" }, - { url = "https://files.pythonhosted.org/packages/9a/b9/1f5bd82b87e5550cd843ceb3768b4a8ef274eb63f29333cf2f29644b3d75/rignore-0.7.6-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:8e41be9fa8f2f47239ded8920cc283699a052ac4c371f77f5ac017ebeed75732", size = 882632, upload-time = "2025-11-05T20:42:44.063Z" }, - { url = "https://files.pythonhosted.org/packages/e9/6b/07714a3efe4a8048864e8a5b7db311ba51b921e15268b17defaebf56d3db/rignore-0.7.6-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6dc1e171e52cefa6c20e60c05394a71165663b48bca6c7666dee4f778f2a7d90", size = 820760, upload-time = "2025-11-05T20:42:27.885Z" }, - { url = "https://files.pythonhosted.org/packages/ac/0f/348c829ea2d8d596e856371b14b9092f8a5dfbb62674ec9b3f67e4939a9d/rignore-0.7.6-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ce2268837c3600f82ab8db58f5834009dc638ee17103582960da668963bebc5", size = 899044, upload-time = "2025-11-05T20:40:55.336Z" }, - { url = "https://files.pythonhosted.org/packages/f0/30/2e1841a19b4dd23878d73edd5d82e998a83d5ed9570a89675f140ca8b2ad/rignore-0.7.6-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:690a3e1b54bfe77e89c4bacb13f046e642f8baadafc61d68f5a726f324a76ab6", size = 874144, upload-time = "2025-11-05T20:41:10.195Z" }, - { url = "https://files.pythonhosted.org/packages/c2/bf/0ce9beb2e5f64c30e3580bef09f5829236889f01511a125f98b83169b993/rignore-0.7.6-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:09d12ac7a0b6210c07bcd145007117ebd8abe99c8eeb383e9e4673910c2754b2", size = 1168062, upload-time = "2025-11-05T20:41:26.511Z" }, - { url = "https://files.pythonhosted.org/packages/b9/8b/571c178414eb4014969865317da8a02ce4cf5241a41676ef91a59aab24de/rignore-0.7.6-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2a2b2b74a8c60203b08452479b90e5ce3dbe96a916214bc9eb2e5af0b6a9beb0", size = 942542, upload-time = "2025-11-05T20:41:41.838Z" }, - { url = "https://files.pythonhosted.org/packages/19/62/7a3cf601d5a45137a7e2b89d10c05b5b86499190c4b7ca5c3c47d79ee519/rignore-0.7.6-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8fc5a531ef02131e44359419a366bfac57f773ea58f5278c2cdd915f7d10ea94", size = 958739, upload-time = "2025-11-05T20:42:12.463Z" }, - { url = "https://files.pythonhosted.org/packages/5f/1f/4261f6a0d7caf2058a5cde2f5045f565ab91aa7badc972b57d19ce58b14e/rignore-0.7.6-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b7a1f77d9c4cd7e76229e252614d963442686bfe12c787a49f4fe481df49e7a9", size = 984138, upload-time = "2025-11-05T20:41:56.775Z" }, - { url = "https://files.pythonhosted.org/packages/2b/bf/628dfe19c75e8ce1f45f7c248f5148b17dfa89a817f8e3552ab74c3ae812/rignore-0.7.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ead81f728682ba72b5b1c3d5846b011d3e0174da978de87c61645f2ed36659a7", size = 1079299, upload-time = "2025-11-05T21:40:16.639Z" }, - { url = "https://files.pythonhosted.org/packages/af/a5/be29c50f5c0c25c637ed32db8758fdf5b901a99e08b608971cda8afb293b/rignore-0.7.6-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:12ffd50f520c22ffdabed8cd8bfb567d9ac165b2b854d3e679f4bcaef11a9441", size = 1139618, upload-time = "2025-11-05T21:40:34.507Z" }, - { url = "https://files.pythonhosted.org/packages/2a/40/3c46cd7ce4fa05c20b525fd60f599165e820af66e66f2c371cd50644558f/rignore-0.7.6-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:e5a16890fbe3c894f8ca34b0fcacc2c200398d4d46ae654e03bc9b3dbf2a0a72", size = 1117626, upload-time = "2025-11-05T21:40:51.494Z" }, - { url = "https://files.pythonhosted.org/packages/8c/b9/aea926f263b8a29a23c75c2e0d8447965eb1879d3feb53cfcf84db67ed58/rignore-0.7.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3abab3bf99e8a77488ef6c7c9a799fac22224c28fe9f25cc21aa7cc2b72bfc0b", size = 1128144, upload-time = "2025-11-05T21:41:09.169Z" }, - { url = "https://files.pythonhosted.org/packages/a4/f6/0d6242f8d0df7f2ecbe91679fefc1f75e7cd2072cb4f497abaab3f0f8523/rignore-0.7.6-cp314-cp314-win32.whl", hash = "sha256:eeef421c1782953c4375aa32f06ecae470c1285c6381eee2a30d2e02a5633001", size = 646385, upload-time = "2025-11-05T21:41:55.105Z" }, - { url = "https://files.pythonhosted.org/packages/d5/38/c0dcd7b10064f084343d6af26fe9414e46e9619c5f3224b5272e8e5d9956/rignore-0.7.6-cp314-cp314-win_amd64.whl", hash = "sha256:6aeed503b3b3d5af939b21d72a82521701a4bd3b89cd761da1e7dc78621af304", size = 725738, upload-time = "2025-11-05T21:41:39.736Z" }, - { url = "https://files.pythonhosted.org/packages/d9/7a/290f868296c1ece914d565757ab363b04730a728b544beb567ceb3b2d96f/rignore-0.7.6-cp314-cp314-win_arm64.whl", hash = "sha256:104f215b60b3c984c386c3e747d6ab4376d5656478694e22c7bd2f788ddd8304", size = 656008, upload-time = "2025-11-05T21:41:29.028Z" }, - { url = "https://files.pythonhosted.org/packages/ca/d2/3c74e3cd81fe8ea08a8dcd2d755c09ac2e8ad8fe409508904557b58383d3/rignore-0.7.6-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:bb24a5b947656dd94cb9e41c4bc8b23cec0c435b58be0d74a874f63c259549e8", size = 882835, upload-time = "2025-11-05T20:42:45.443Z" }, - { url = "https://files.pythonhosted.org/packages/77/61/a772a34b6b63154877433ac2d048364815b24c2dd308f76b212c408101a2/rignore-0.7.6-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5b1e33c9501cefe24b70a1eafd9821acfd0ebf0b35c3a379430a14df089993e3", size = 820301, upload-time = "2025-11-05T20:42:29.226Z" }, - { url = "https://files.pythonhosted.org/packages/71/30/054880b09c0b1b61d17eeb15279d8bf729c0ba52b36c3ada52fb827cbb3c/rignore-0.7.6-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bec3994665a44454df86deb762061e05cd4b61e3772f5b07d1882a8a0d2748d5", size = 897611, upload-time = "2025-11-05T20:40:56.475Z" }, - { url = "https://files.pythonhosted.org/packages/1e/40/b2d1c169f833d69931bf232600eaa3c7998ba4f9a402e43a822dad2ea9f2/rignore-0.7.6-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:26cba2edfe3cff1dfa72bddf65d316ddebf182f011f2f61538705d6dbaf54986", size = 873875, upload-time = "2025-11-05T20:41:11.561Z" }, - { url = "https://files.pythonhosted.org/packages/55/59/ca5ae93d83a1a60e44b21d87deb48b177a8db1b85e82fc8a9abb24a8986d/rignore-0.7.6-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ffa86694fec604c613696cb91e43892aa22e1fec5f9870e48f111c603e5ec4e9", size = 1167245, upload-time = "2025-11-05T20:41:28.29Z" }, - { url = "https://files.pythonhosted.org/packages/a5/52/cf3dce392ba2af806cba265aad6bcd9c48bb2a6cb5eee448d3319f6e505b/rignore-0.7.6-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48efe2ed95aa8104145004afb15cdfa02bea5cdde8b0344afeb0434f0d989aa2", size = 941750, upload-time = "2025-11-05T20:41:43.111Z" }, - { url = "https://files.pythonhosted.org/packages/ec/be/3f344c6218d779395e785091d05396dfd8b625f6aafbe502746fcd880af2/rignore-0.7.6-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8dcae43eb44b7f2457fef7cc87f103f9a0013017a6f4e62182c565e924948f21", size = 958896, upload-time = "2025-11-05T20:42:13.784Z" }, - { url = "https://files.pythonhosted.org/packages/c9/34/d3fa71938aed7d00dcad87f0f9bcb02ad66c85d6ffc83ba31078ce53646a/rignore-0.7.6-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2cd649a7091c0dad2f11ef65630d30c698d505cbe8660dd395268e7c099cc99f", size = 983992, upload-time = "2025-11-05T20:41:58.022Z" }, - { url = "https://files.pythonhosted.org/packages/24/a4/52a697158e9920705bdbd0748d59fa63e0f3233fb92e9df9a71afbead6ca/rignore-0.7.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:42de84b0289d478d30ceb7ae59023f7b0527786a9a5b490830e080f0e4ea5aeb", size = 1078181, upload-time = "2025-11-05T21:40:18.151Z" }, - { url = "https://files.pythonhosted.org/packages/ac/65/aa76dbcdabf3787a6f0fd61b5cc8ed1e88580590556d6c0207960d2384bb/rignore-0.7.6-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:875a617e57b53b4acbc5a91de418233849711c02e29cc1f4f9febb2f928af013", size = 1139232, upload-time = "2025-11-05T21:40:35.966Z" }, - { url = "https://files.pythonhosted.org/packages/08/44/31b31a49b3233c6842acc1c0731aa1e7fb322a7170612acf30327f700b44/rignore-0.7.6-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:8703998902771e96e49968105207719f22926e4431b108450f3f430b4e268b7c", size = 1117349, upload-time = "2025-11-05T21:40:53.013Z" }, - { url = "https://files.pythonhosted.org/packages/e9/ae/1b199a2302c19c658cf74e5ee1427605234e8c91787cfba0015f2ace145b/rignore-0.7.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:602ef33f3e1b04c1e9a10a3c03f8bc3cef2d2383dcc250d309be42b49923cabc", size = 1127702, upload-time = "2025-11-05T21:41:10.881Z" }, - { url = "https://files.pythonhosted.org/packages/fc/d3/18210222b37e87e36357f7b300b7d98c6dd62b133771e71ae27acba83a4f/rignore-0.7.6-cp314-cp314t-win32.whl", hash = "sha256:c1d8f117f7da0a4a96a8daef3da75bc090e3792d30b8b12cfadc240c631353f9", size = 647033, upload-time = "2025-11-05T21:42:00.095Z" }, - { url = "https://files.pythonhosted.org/packages/3e/87/033eebfbee3ec7d92b3bb1717d8f68c88e6fc7de54537040f3b3a405726f/rignore-0.7.6-cp314-cp314t-win_amd64.whl", hash = "sha256:ca36e59408bec81de75d307c568c2d0d410fb880b1769be43611472c61e85c96", size = 725647, upload-time = "2025-11-05T21:41:44.449Z" }, - { url = "https://files.pythonhosted.org/packages/79/62/b88e5879512c55b8ee979c666ee6902adc4ed05007226de266410ae27965/rignore-0.7.6-cp314-cp314t-win_arm64.whl", hash = "sha256:b83adabeb3e8cf662cabe1931b83e165b88c526fa6af6b3aa90429686e474896", size = 656035, upload-time = "2025-11-05T21:41:31.13Z" }, + { url = "https://files.pythonhosted.org/packages/87/2a/a1810c8627b9ec8c57ec5ec325d306701ae7be50235e8fd81266e002a3cc/rich-14.3.1-py3-none-any.whl", hash = "sha256:da750b1aebbff0b372557426fb3f35ba56de8ef954b3190315eb64076d6fb54e", size = 309952, upload-time = "2026-01-24T21:40:42.969Z" }, ] [[package]] @@ -1516,23 +1130,10 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9e/6a/40fee331a52339926a92e17ae748827270b288a35ef4a15c9c8f2ec54715/ruff-0.14.14-py3-none-win_arm64.whl", hash = "sha256:56e6981a98b13a32236a72a8da421d7839221fa308b223b9283312312e5ac76c", size = 10920448, upload-time = "2026-01-22T22:30:15.417Z" }, ] -[[package]] -name = "sentry-sdk" -version = "2.47.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "certifi" }, - { name = "urllib3" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/4a/2a/d225cbf87b6c8ecce5664db7bcecb82c317e448e3b24a2dcdaacb18ca9a7/sentry_sdk-2.47.0.tar.gz", hash = "sha256:8218891d5e41b4ea8d61d2aed62ed10c80e39d9f2959d6f939efbf056857e050", size = 381895, upload-time = "2025-12-03T14:06:36.846Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/bd/ac/d6286ea0d49e7b58847faf67b00e56bb4ba3d525281e2ac306e1f1f353da/sentry_sdk-2.47.0-py2.py3-none-any.whl", hash = "sha256:d72f8c61025b7d1d9e52510d03a6247b280094a327dd900d987717a4fce93412", size = 411088, upload-time = "2025-12-03T14:06:35.374Z" }, -] - [[package]] name = "shared" -version = "0.0.0" -source = { git = "https://github.com/fairagro/m4.2_advanced_middleware_api.git?subdirectory=middleware%2Fshared&branch=feature%2Fsplit_off_client#3a8cde5565e2eef861382ed63b8f356227a632ad" } +version = "0.0.1" +source = { git = "https://github.com/fairagro/m4.2_advanced_middleware_api.git?subdirectory=middleware%2Fshared&branch=feature%2Fsplit_off_client#2028725e3e4c403fe110911df7f0f8bd7b5ee8a4" } dependencies = [ { name = "opentelemetry-api" }, { name = "opentelemetry-exporter-otlp" }, @@ -1541,15 +1142,6 @@ dependencies = [ { name = "pyyaml" }, ] -[[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]] name = "sql-to-arc" version = "0.0.0" @@ -1557,6 +1149,8 @@ source = { editable = "middleware/sql_to_arc" } dependencies = [ { name = "api-client" }, { name = "arctrl" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-sdk" }, { name = "psycopg", extra = ["binary"] }, { name = "pydantic" }, { name = "shared" }, @@ -1566,24 +1160,13 @@ dependencies = [ requires-dist = [ { name = "api-client", git = "https://github.com/fairagro/m4.2_advanced_middleware_api.git?subdirectory=middleware%2Fapi_client&branch=feature%2Fsplit_off_client" }, { name = "arctrl", specifier = ">=3.0.0b15" }, + { name = "opentelemetry-api", specifier = ">=1.39.1" }, + { name = "opentelemetry-sdk", specifier = ">=1.39.1" }, { name = "psycopg", extras = ["binary"], specifier = ">=3.3.2" }, { name = "pydantic", specifier = ">=2.12.5" }, { name = "shared", git = "https://github.com/fairagro/m4.2_advanced_middleware_api.git?subdirectory=middleware%2Fshared&branch=feature%2Fsplit_off_client" }, ] -[[package]] -name = "starlette" -version = "0.50.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "anyio" }, - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ba/b8/73a0e6a6e079a9d9cfa64113d771e421640b6f679a52eeb9b32f72d871a1/starlette-0.50.0.tar.gz", hash = "sha256:a2a17b22203254bcbc2e1f926d2d55f3f9497f769416b3190768befe598fa3ca", size = 2646985, upload-time = "2025-11-01T15:25:27.516Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d9/52/1064f510b141bd54025f9b55105e26d1fa970b9be67ad766380a3c9b74b0/starlette-0.50.0-py3-none-any.whl", hash = "sha256:9e5391843ec9b6e472eed1365a78c8098cfceb7a74bfd4d6b1c0c0095efb3bca", size = 74033, upload-time = "2025-11-01T15:25:25.461Z" }, -] - [[package]] name = "stevedore" version = "5.6.0" @@ -1595,26 +1178,11 @@ wheels = [ [[package]] name = "tomlkit" -version = "0.13.3" +version = "0.14.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/cc/18/0bbf3884e9eaa38819ebe46a7bd25dcd56b67434402b66a58c4b8e552575/tomlkit-0.13.3.tar.gz", hash = "sha256:430cf247ee57df2b94ee3fbe588e71d362a941ebb545dec29b53961d61add2a1", size = 185207, upload-time = "2025-06-05T07:13:44.947Z" } +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/bd/75/8539d011f6be8e29f339c42e633aae3cb73bffa95dd0f9adec09b9c58e85/tomlkit-0.13.3-py3-none-any.whl", hash = "sha256:c89c649d79ee40629a9fda55f8ace8c6a1b42deb912b2a8fd8d942ddadb606b0", size = 38901, upload-time = "2025-06-05T07:13:43.546Z" }, -] - -[[package]] -name = "typer" -version = "0.20.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "click" }, - { name = "rich" }, - { name = "shellingham" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/8f/28/7c85c8032b91dbe79725b6f17d2fffc595dff06a35c7a30a37bef73a1ab4/typer-0.20.0.tar.gz", hash = "sha256:1aaf6494031793e4876fb0bacfa6a912b551cf43c1e63c800df8b1a866720c37", size = 106492, upload-time = "2025-10-20T17:03:49.445Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/78/64/7713ffe4b5983314e9d436a90d5bd4f63b6054e2aca783a3cfc44cb95bbf/typer-0.20.0-py3-none-any.whl", hash = "sha256:5b463df6793ec1dca6213a3cf4c0f03bc6e322ac5e16e13ddd622a889489784a", size = 47028, upload-time = "2025-10-20T17:03:47.617Z" }, + { 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]] @@ -1640,11 +1208,11 @@ wheels = [ [[package]] name = "tzdata" -version = "2025.2" +version = "2025.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/95/32/1a225d6164441be760d75c2c42e2780dc0873fe382da3e98a2e1e48361e5/tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9", size = 196380, upload-time = "2025-03-23T13:54:43.652Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5e/a7/c202b344c5ca7daf398f3b8a477eeb205cf3b6f32e7ec3a6bac0629ca975/tzdata-2025.3.tar.gz", hash = "sha256:de39c2ca5dc7b0344f2eba86f49d614019d29f060fc4ebc8a417896a620b56a7", size = 196772, upload-time = "2025-12-13T17:45:35.667Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839, upload-time = "2025-03-23T13:54:41.845Z" }, + { url = "https://files.pythonhosted.org/packages/c7/b0/003792df09decd6849a5e39c28b513c06e84436a54440380862b5aeff25d/tzdata-2025.3-py2.py3-none-any.whl", hash = "sha256:06a47e5700f3081aab02b2e513160914ff0694bce9947d6b76ebd6bf57cfc5d1", size = 348521, upload-time = "2025-12-13T17:45:33.889Z" }, ] [[package]] @@ -1656,62 +1224,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, ] -[[package]] -name = "uvicorn" -version = "0.38.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "click" }, - { name = "h11" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/cb/ce/f06b84e2697fef4688ca63bdb2fdf113ca0a3be33f94488f2cadb690b0cf/uvicorn-0.38.0.tar.gz", hash = "sha256:fd97093bdd120a2609fc0d3afe931d4d4ad688b6e75f0f929fde1bc36fe0e91d", size = 80605, upload-time = "2025-10-18T13:46:44.63Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ee/d9/d88e73ca598f4f6ff671fb5fde8a32925c2e08a637303a1d12883c7305fa/uvicorn-0.38.0-py3-none-any.whl", hash = "sha256:48c0afd214ceb59340075b4a052ea1ee91c16fbc2a9b1469cca0e54566977b02", size = 68109, upload-time = "2025-10-18T13:46:42.958Z" }, -] - -[package.optional-dependencies] -standard = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, - { name = "httptools" }, - { name = "python-dotenv" }, - { name = "pyyaml" }, - { name = "uvloop", marker = "platform_python_implementation != 'PyPy' and sys_platform != 'cygwin' and sys_platform != 'win32'" }, - { name = "watchfiles" }, - { name = "websockets" }, -] - -[[package]] -name = "uvloop" -version = "0.22.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/06/f0/18d39dbd1971d6d62c4629cc7fa67f74821b0dc1f5a77af43719de7936a7/uvloop-0.22.1.tar.gz", hash = "sha256:6c84bae345b9147082b17371e3dd5d42775bddce91f885499017f4607fdaf39f", size = 2443250, upload-time = "2025-10-16T22:17:19.342Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3d/ff/7f72e8170be527b4977b033239a83a68d5c881cc4775fca255c677f7ac5d/uvloop-0.22.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:fe94b4564e865d968414598eea1a6de60adba0c040ba4ed05ac1300de402cd42", size = 1359936, upload-time = "2025-10-16T22:16:29.436Z" }, - { url = "https://files.pythonhosted.org/packages/c3/c6/e5d433f88fd54d81ef4be58b2b7b0cea13c442454a1db703a1eea0db1a59/uvloop-0.22.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:51eb9bd88391483410daad430813d982010f9c9c89512321f5b60e2cddbdddd6", size = 752769, upload-time = "2025-10-16T22:16:30.493Z" }, - { url = "https://files.pythonhosted.org/packages/24/68/a6ac446820273e71aa762fa21cdcc09861edd3536ff47c5cd3b7afb10eeb/uvloop-0.22.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:700e674a166ca5778255e0e1dc4e9d79ab2acc57b9171b79e65feba7184b3370", size = 4317413, upload-time = "2025-10-16T22:16:31.644Z" }, - { url = "https://files.pythonhosted.org/packages/5f/6f/e62b4dfc7ad6518e7eff2516f680d02a0f6eb62c0c212e152ca708a0085e/uvloop-0.22.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b5b1ac819a3f946d3b2ee07f09149578ae76066d70b44df3fa990add49a82e4", size = 4426307, upload-time = "2025-10-16T22:16:32.917Z" }, - { url = "https://files.pythonhosted.org/packages/90/60/97362554ac21e20e81bcef1150cb2a7e4ffdaf8ea1e5b2e8bf7a053caa18/uvloop-0.22.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e047cc068570bac9866237739607d1313b9253c3051ad84738cbb095be0537b2", size = 4131970, upload-time = "2025-10-16T22:16:34.015Z" }, - { url = "https://files.pythonhosted.org/packages/99/39/6b3f7d234ba3964c428a6e40006340f53ba37993f46ed6e111c6e9141d18/uvloop-0.22.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:512fec6815e2dd45161054592441ef76c830eddaad55c8aa30952e6fe1ed07c0", size = 4296343, upload-time = "2025-10-16T22:16:35.149Z" }, - { url = "https://files.pythonhosted.org/packages/89/8c/182a2a593195bfd39842ea68ebc084e20c850806117213f5a299dfc513d9/uvloop-0.22.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:561577354eb94200d75aca23fbde86ee11be36b00e52a4eaf8f50fb0c86b7705", size = 1358611, upload-time = "2025-10-16T22:16:36.833Z" }, - { url = "https://files.pythonhosted.org/packages/d2/14/e301ee96a6dc95224b6f1162cd3312f6d1217be3907b79173b06785f2fe7/uvloop-0.22.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cdf5192ab3e674ca26da2eada35b288d2fa49fdd0f357a19f0e7c4e7d5077c8", size = 751811, upload-time = "2025-10-16T22:16:38.275Z" }, - { url = "https://files.pythonhosted.org/packages/b7/02/654426ce265ac19e2980bfd9ea6590ca96a56f10c76e63801a2df01c0486/uvloop-0.22.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e2ea3d6190a2968f4a14a23019d3b16870dd2190cd69c8180f7c632d21de68d", size = 4288562, upload-time = "2025-10-16T22:16:39.375Z" }, - { url = "https://files.pythonhosted.org/packages/15/c0/0be24758891ef825f2065cd5db8741aaddabe3e248ee6acc5e8a80f04005/uvloop-0.22.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0530a5fbad9c9e4ee3f2b33b148c6a64d47bbad8000ea63704fa8260f4cf728e", size = 4366890, upload-time = "2025-10-16T22:16:40.547Z" }, - { url = "https://files.pythonhosted.org/packages/d2/53/8369e5219a5855869bcee5f4d317f6da0e2c669aecf0ef7d371e3d084449/uvloop-0.22.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bc5ef13bbc10b5335792360623cc378d52d7e62c2de64660616478c32cd0598e", size = 4119472, upload-time = "2025-10-16T22:16:41.694Z" }, - { url = "https://files.pythonhosted.org/packages/f8/ba/d69adbe699b768f6b29a5eec7b47dd610bd17a69de51b251126a801369ea/uvloop-0.22.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1f38ec5e3f18c8a10ded09742f7fb8de0108796eb673f30ce7762ce1b8550cad", size = 4239051, upload-time = "2025-10-16T22:16:43.224Z" }, - { url = "https://files.pythonhosted.org/packages/90/cd/b62bdeaa429758aee8de8b00ac0dd26593a9de93d302bff3d21439e9791d/uvloop-0.22.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3879b88423ec7e97cd4eba2a443aa26ed4e59b45e6b76aabf13fe2f27023a142", size = 1362067, upload-time = "2025-10-16T22:16:44.503Z" }, - { url = "https://files.pythonhosted.org/packages/0d/f8/a132124dfda0777e489ca86732e85e69afcd1ff7686647000050ba670689/uvloop-0.22.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4baa86acedf1d62115c1dc6ad1e17134476688f08c6efd8a2ab076e815665c74", size = 752423, upload-time = "2025-10-16T22:16:45.968Z" }, - { url = "https://files.pythonhosted.org/packages/a3/94/94af78c156f88da4b3a733773ad5ba0b164393e357cc4bd0ab2e2677a7d6/uvloop-0.22.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:297c27d8003520596236bdb2335e6b3f649480bd09e00d1e3a99144b691d2a35", size = 4272437, upload-time = "2025-10-16T22:16:47.451Z" }, - { url = "https://files.pythonhosted.org/packages/b5/35/60249e9fd07b32c665192cec7af29e06c7cd96fa1d08b84f012a56a0b38e/uvloop-0.22.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c1955d5a1dd43198244d47664a5858082a3239766a839b2102a269aaff7a4e25", size = 4292101, upload-time = "2025-10-16T22:16:49.318Z" }, - { url = "https://files.pythonhosted.org/packages/02/62/67d382dfcb25d0a98ce73c11ed1a6fba5037a1a1d533dcbb7cab033a2636/uvloop-0.22.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b31dc2fccbd42adc73bc4e7cdbae4fc5086cf378979e53ca5d0301838c5682c6", size = 4114158, upload-time = "2025-10-16T22:16:50.517Z" }, - { url = "https://files.pythonhosted.org/packages/f0/7a/f1171b4a882a5d13c8b7576f348acfe6074d72eaf52cccef752f748d4a9f/uvloop-0.22.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:93f617675b2d03af4e72a5333ef89450dfaa5321303ede6e67ba9c9d26878079", size = 4177360, upload-time = "2025-10-16T22:16:52.646Z" }, - { url = "https://files.pythonhosted.org/packages/79/7b/b01414f31546caf0919da80ad57cbfe24c56b151d12af68cee1b04922ca8/uvloop-0.22.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:37554f70528f60cad66945b885eb01f1bb514f132d92b6eeed1c90fd54ed6289", size = 1454790, upload-time = "2025-10-16T22:16:54.355Z" }, - { url = "https://files.pythonhosted.org/packages/d4/31/0bb232318dd838cad3fa8fb0c68c8b40e1145b32025581975e18b11fab40/uvloop-0.22.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:b76324e2dc033a0b2f435f33eb88ff9913c156ef78e153fb210e03c13da746b3", size = 796783, upload-time = "2025-10-16T22:16:55.906Z" }, - { url = "https://files.pythonhosted.org/packages/42/38/c9b09f3271a7a723a5de69f8e237ab8e7803183131bc57c890db0b6bb872/uvloop-0.22.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:badb4d8e58ee08dad957002027830d5c3b06aea446a6a3744483c2b3b745345c", size = 4647548, upload-time = "2025-10-16T22:16:57.008Z" }, - { url = "https://files.pythonhosted.org/packages/c1/37/945b4ca0ac27e3dc4952642d4c900edd030b3da6c9634875af6e13ae80e5/uvloop-0.22.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b91328c72635f6f9e0282e4a57da7470c7350ab1c9f48546c0f2866205349d21", size = 4467065, upload-time = "2025-10-16T22:16:58.206Z" }, - { url = "https://files.pythonhosted.org/packages/97/cc/48d232f33d60e2e2e0b42f4e73455b146b76ebe216487e862700457fbf3c/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:daf620c2995d193449393d6c62131b3fbd40a63bf7b307a1527856ace637fe88", size = 4328384, upload-time = "2025-10-16T22:16:59.36Z" }, - { url = "https://files.pythonhosted.org/packages/e4/16/c1fd27e9549f3c4baf1dc9c20c456cd2f822dbf8de9f463824b0c0357e06/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6cde23eeda1a25c75b2e07d39970f3374105d5eafbaab2a4482be82f272d5a5e", size = 4296730, upload-time = "2025-10-16T22:17:00.744Z" }, -] - [[package]] name = "virtualenv" version = "20.36.1" @@ -1726,107 +1238,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/6a/2a/dc2228b2888f51192c7dc766106cd475f1b768c10caaf9727659726f7391/virtualenv-20.36.1-py3-none-any.whl", hash = "sha256:575a8d6b124ef88f6f51d56d656132389f961062a9177016a50e4f507bbcc19f", size = 6008258, upload-time = "2026-01-09T18:20:59.425Z" }, ] -[[package]] -name = "watchfiles" -version = "1.1.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "anyio" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c2/c9/8869df9b2a2d6c59d79220a4db37679e74f807c559ffe5265e08b227a210/watchfiles-1.1.1.tar.gz", hash = "sha256:a173cb5c16c4f40ab19cecf48a534c409f7ea983ab8fed0741304a1c0a31b3f2", size = 94440, upload-time = "2025-10-14T15:06:21.08Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/74/d5/f039e7e3c639d9b1d09b07ea412a6806d38123f0508e5f9b48a87b0a76cc/watchfiles-1.1.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:8c89f9f2f740a6b7dcc753140dd5e1ab9215966f7a3530d0c0705c83b401bd7d", size = 404745, upload-time = "2025-10-14T15:04:46.731Z" }, - { url = "https://files.pythonhosted.org/packages/a5/96/a881a13aa1349827490dab2d363c8039527060cfcc2c92cc6d13d1b1049e/watchfiles-1.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bd404be08018c37350f0d6e34676bd1e2889990117a2b90070b3007f172d0610", size = 391769, upload-time = "2025-10-14T15:04:48.003Z" }, - { url = "https://files.pythonhosted.org/packages/4b/5b/d3b460364aeb8da471c1989238ea0e56bec24b6042a68046adf3d9ddb01c/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8526e8f916bb5b9a0a777c8317c23ce65de259422bba5b31325a6fa6029d33af", size = 449374, upload-time = "2025-10-14T15:04:49.179Z" }, - { url = "https://files.pythonhosted.org/packages/b9/44/5769cb62d4ed055cb17417c0a109a92f007114a4e07f30812a73a4efdb11/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2edc3553362b1c38d9f06242416a5d8e9fe235c204a4072e988ce2e5bb1f69f6", size = 459485, upload-time = "2025-10-14T15:04:50.155Z" }, - { url = "https://files.pythonhosted.org/packages/19/0c/286b6301ded2eccd4ffd0041a1b726afda999926cf720aab63adb68a1e36/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30f7da3fb3f2844259cba4720c3fc7138eb0f7b659c38f3bfa65084c7fc7abce", size = 488813, upload-time = "2025-10-14T15:04:51.059Z" }, - { url = "https://files.pythonhosted.org/packages/c7/2b/8530ed41112dd4a22f4dcfdb5ccf6a1baad1ff6eed8dc5a5f09e7e8c41c7/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8979280bdafff686ba5e4d8f97840f929a87ed9cdf133cbbd42f7766774d2aa", size = 594816, upload-time = "2025-10-14T15:04:52.031Z" }, - { url = "https://files.pythonhosted.org/packages/ce/d2/f5f9fb49489f184f18470d4f99f4e862a4b3e9ac2865688eb2099e3d837a/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dcc5c24523771db3a294c77d94771abcfcb82a0e0ee8efd910c37c59ec1b31bb", size = 475186, upload-time = "2025-10-14T15:04:53.064Z" }, - { url = "https://files.pythonhosted.org/packages/cf/68/5707da262a119fb06fbe214d82dd1fe4a6f4af32d2d14de368d0349eb52a/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1db5d7ae38ff20153d542460752ff397fcf5c96090c1230803713cf3147a6803", size = 456812, upload-time = "2025-10-14T15:04:55.174Z" }, - { url = "https://files.pythonhosted.org/packages/66/ab/3cbb8756323e8f9b6f9acb9ef4ec26d42b2109bce830cc1f3468df20511d/watchfiles-1.1.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:28475ddbde92df1874b6c5c8aaeb24ad5be47a11f87cde5a28ef3835932e3e94", size = 630196, upload-time = "2025-10-14T15:04:56.22Z" }, - { url = "https://files.pythonhosted.org/packages/78/46/7152ec29b8335f80167928944a94955015a345440f524d2dfe63fc2f437b/watchfiles-1.1.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:36193ed342f5b9842edd3532729a2ad55c4160ffcfa3700e0d54be496b70dd43", size = 622657, upload-time = "2025-10-14T15:04:57.521Z" }, - { url = "https://files.pythonhosted.org/packages/0a/bf/95895e78dd75efe9a7f31733607f384b42eb5feb54bd2eb6ed57cc2e94f4/watchfiles-1.1.1-cp312-cp312-win32.whl", hash = "sha256:859e43a1951717cc8de7f4c77674a6d389b106361585951d9e69572823f311d9", size = 272042, upload-time = "2025-10-14T15:04:59.046Z" }, - { url = "https://files.pythonhosted.org/packages/87/0a/90eb755f568de2688cb220171c4191df932232c20946966c27a59c400850/watchfiles-1.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:91d4c9a823a8c987cce8fa2690923b069966dabb196dd8d137ea2cede885fde9", size = 288410, upload-time = "2025-10-14T15:05:00.081Z" }, - { url = "https://files.pythonhosted.org/packages/36/76/f322701530586922fbd6723c4f91ace21364924822a8772c549483abed13/watchfiles-1.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:a625815d4a2bdca61953dbba5a39d60164451ef34c88d751f6c368c3ea73d404", size = 278209, upload-time = "2025-10-14T15:05:01.168Z" }, - { url = "https://files.pythonhosted.org/packages/bb/f4/f750b29225fe77139f7ae5de89d4949f5a99f934c65a1f1c0b248f26f747/watchfiles-1.1.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:130e4876309e8686a5e37dba7d5e9bc77e6ed908266996ca26572437a5271e18", size = 404321, upload-time = "2025-10-14T15:05:02.063Z" }, - { url = "https://files.pythonhosted.org/packages/2b/f9/f07a295cde762644aa4c4bb0f88921d2d141af45e735b965fb2e87858328/watchfiles-1.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5f3bde70f157f84ece3765b42b4a52c6ac1a50334903c6eaf765362f6ccca88a", size = 391783, upload-time = "2025-10-14T15:05:03.052Z" }, - { url = "https://files.pythonhosted.org/packages/bc/11/fc2502457e0bea39a5c958d86d2cb69e407a4d00b85735ca724bfa6e0d1a/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:14e0b1fe858430fc0251737ef3824c54027bedb8c37c38114488b8e131cf8219", size = 449279, upload-time = "2025-10-14T15:05:04.004Z" }, - { url = "https://files.pythonhosted.org/packages/e3/1f/d66bc15ea0b728df3ed96a539c777acfcad0eb78555ad9efcaa1274688f0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f27db948078f3823a6bb3b465180db8ebecf26dd5dae6f6180bd87383b6b4428", size = 459405, upload-time = "2025-10-14T15:05:04.942Z" }, - { url = "https://files.pythonhosted.org/packages/be/90/9f4a65c0aec3ccf032703e6db02d89a157462fbb2cf20dd415128251cac0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:059098c3a429f62fc98e8ec62b982230ef2c8df68c79e826e37b895bc359a9c0", size = 488976, upload-time = "2025-10-14T15:05:05.905Z" }, - { url = "https://files.pythonhosted.org/packages/37/57/ee347af605d867f712be7029bb94c8c071732a4b44792e3176fa3c612d39/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bfb5862016acc9b869bb57284e6cb35fdf8e22fe59f7548858e2f971d045f150", size = 595506, upload-time = "2025-10-14T15:05:06.906Z" }, - { url = "https://files.pythonhosted.org/packages/a8/78/cc5ab0b86c122047f75e8fc471c67a04dee395daf847d3e59381996c8707/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:319b27255aacd9923b8a276bb14d21a5f7ff82564c744235fc5eae58d95422ae", size = 474936, upload-time = "2025-10-14T15:05:07.906Z" }, - { url = "https://files.pythonhosted.org/packages/62/da/def65b170a3815af7bd40a3e7010bf6ab53089ef1b75d05dd5385b87cf08/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c755367e51db90e75b19454b680903631d41f9e3607fbd941d296a020c2d752d", size = 456147, upload-time = "2025-10-14T15:05:09.138Z" }, - { url = "https://files.pythonhosted.org/packages/57/99/da6573ba71166e82d288d4df0839128004c67d2778d3b566c138695f5c0b/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c22c776292a23bfc7237a98f791b9ad3144b02116ff10d820829ce62dff46d0b", size = 630007, upload-time = "2025-10-14T15:05:10.117Z" }, - { url = "https://files.pythonhosted.org/packages/a8/51/7439c4dd39511368849eb1e53279cd3454b4a4dbace80bab88feeb83c6b5/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:3a476189be23c3686bc2f4321dd501cb329c0a0469e77b7b534ee10129ae6374", size = 622280, upload-time = "2025-10-14T15:05:11.146Z" }, - { url = "https://files.pythonhosted.org/packages/95/9c/8ed97d4bba5db6fdcdb2b298d3898f2dd5c20f6b73aee04eabe56c59677e/watchfiles-1.1.1-cp313-cp313-win32.whl", hash = "sha256:bf0a91bfb5574a2f7fc223cf95eeea79abfefa404bf1ea5e339c0c1560ae99a0", size = 272056, upload-time = "2025-10-14T15:05:12.156Z" }, - { url = "https://files.pythonhosted.org/packages/1f/f3/c14e28429f744a260d8ceae18bf58c1d5fa56b50d006a7a9f80e1882cb0d/watchfiles-1.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:52e06553899e11e8074503c8e716d574adeeb7e68913115c4b3653c53f9bae42", size = 288162, upload-time = "2025-10-14T15:05:13.208Z" }, - { url = "https://files.pythonhosted.org/packages/dc/61/fe0e56c40d5cd29523e398d31153218718c5786b5e636d9ae8ae79453d27/watchfiles-1.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:ac3cc5759570cd02662b15fbcd9d917f7ecd47efe0d6b40474eafd246f91ea18", size = 277909, upload-time = "2025-10-14T15:05:14.49Z" }, - { url = "https://files.pythonhosted.org/packages/79/42/e0a7d749626f1e28c7108a99fb9bf524b501bbbeb9b261ceecde644d5a07/watchfiles-1.1.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:563b116874a9a7ce6f96f87cd0b94f7faf92d08d0021e837796f0a14318ef8da", size = 403389, upload-time = "2025-10-14T15:05:15.777Z" }, - { url = "https://files.pythonhosted.org/packages/15/49/08732f90ce0fbbc13913f9f215c689cfc9ced345fb1bcd8829a50007cc8d/watchfiles-1.1.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3ad9fe1dae4ab4212d8c91e80b832425e24f421703b5a42ef2e4a1e215aff051", size = 389964, upload-time = "2025-10-14T15:05:16.85Z" }, - { url = "https://files.pythonhosted.org/packages/27/0d/7c315d4bd5f2538910491a0393c56bf70d333d51bc5b34bee8e68e8cea19/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce70f96a46b894b36eba678f153f052967a0d06d5b5a19b336ab0dbbd029f73e", size = 448114, upload-time = "2025-10-14T15:05:17.876Z" }, - { url = "https://files.pythonhosted.org/packages/c3/24/9e096de47a4d11bc4df41e9d1e61776393eac4cb6eb11b3e23315b78b2cc/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cb467c999c2eff23a6417e58d75e5828716f42ed8289fe6b77a7e5a91036ca70", size = 460264, upload-time = "2025-10-14T15:05:18.962Z" }, - { url = "https://files.pythonhosted.org/packages/cc/0f/e8dea6375f1d3ba5fcb0b3583e2b493e77379834c74fd5a22d66d85d6540/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:836398932192dae4146c8f6f737d74baeac8b70ce14831a239bdb1ca882fc261", size = 487877, upload-time = "2025-10-14T15:05:20.094Z" }, - { url = "https://files.pythonhosted.org/packages/ac/5b/df24cfc6424a12deb41503b64d42fbea6b8cb357ec62ca84a5a3476f654a/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:743185e7372b7bc7c389e1badcc606931a827112fbbd37f14c537320fca08620", size = 595176, upload-time = "2025-10-14T15:05:21.134Z" }, - { url = "https://files.pythonhosted.org/packages/8f/b5/853b6757f7347de4e9b37e8cc3289283fb983cba1ab4d2d7144694871d9c/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:afaeff7696e0ad9f02cbb8f56365ff4686ab205fcf9c4c5b6fdfaaa16549dd04", size = 473577, upload-time = "2025-10-14T15:05:22.306Z" }, - { url = "https://files.pythonhosted.org/packages/e1/f7/0a4467be0a56e80447c8529c9fce5b38eab4f513cb3d9bf82e7392a5696b/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f7eb7da0eb23aa2ba036d4f616d46906013a68caf61b7fdbe42fc8b25132e77", size = 455425, upload-time = "2025-10-14T15:05:23.348Z" }, - { url = "https://files.pythonhosted.org/packages/8e/e0/82583485ea00137ddf69bc84a2db88bd92ab4a6e3c405e5fb878ead8d0e7/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:831a62658609f0e5c64178211c942ace999517f5770fe9436be4c2faeba0c0ef", size = 628826, upload-time = "2025-10-14T15:05:24.398Z" }, - { url = "https://files.pythonhosted.org/packages/28/9a/a785356fccf9fae84c0cc90570f11702ae9571036fb25932f1242c82191c/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:f9a2ae5c91cecc9edd47e041a930490c31c3afb1f5e6d71de3dc671bfaca02bf", size = 622208, upload-time = "2025-10-14T15:05:25.45Z" }, - { url = "https://files.pythonhosted.org/packages/c3/f4/0872229324ef69b2c3edec35e84bd57a1289e7d3fe74588048ed8947a323/watchfiles-1.1.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:d1715143123baeeaeadec0528bb7441103979a1d5f6fd0e1f915383fea7ea6d5", size = 404315, upload-time = "2025-10-14T15:05:26.501Z" }, - { url = "https://files.pythonhosted.org/packages/7b/22/16d5331eaed1cb107b873f6ae1b69e9ced582fcf0c59a50cd84f403b1c32/watchfiles-1.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:39574d6370c4579d7f5d0ad940ce5b20db0e4117444e39b6d8f99db5676c52fd", size = 390869, upload-time = "2025-10-14T15:05:27.649Z" }, - { url = "https://files.pythonhosted.org/packages/b2/7e/5643bfff5acb6539b18483128fdc0ef2cccc94a5b8fbda130c823e8ed636/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7365b92c2e69ee952902e8f70f3ba6360d0d596d9299d55d7d386df84b6941fb", size = 449919, upload-time = "2025-10-14T15:05:28.701Z" }, - { url = "https://files.pythonhosted.org/packages/51/2e/c410993ba5025a9f9357c376f48976ef0e1b1aefb73b97a5ae01a5972755/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bfff9740c69c0e4ed32416f013f3c45e2ae42ccedd1167ef2d805c000b6c71a5", size = 460845, upload-time = "2025-10-14T15:05:30.064Z" }, - { url = "https://files.pythonhosted.org/packages/8e/a4/2df3b404469122e8680f0fcd06079317e48db58a2da2950fb45020947734/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b27cf2eb1dda37b2089e3907d8ea92922b673c0c427886d4edc6b94d8dfe5db3", size = 489027, upload-time = "2025-10-14T15:05:31.064Z" }, - { url = "https://files.pythonhosted.org/packages/ea/84/4587ba5b1f267167ee715b7f66e6382cca6938e0a4b870adad93e44747e6/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:526e86aced14a65a5b0ec50827c745597c782ff46b571dbfe46192ab9e0b3c33", size = 595615, upload-time = "2025-10-14T15:05:32.074Z" }, - { url = "https://files.pythonhosted.org/packages/6a/0f/c6988c91d06e93cd0bb3d4a808bcf32375ca1904609835c3031799e3ecae/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:04e78dd0b6352db95507fd8cb46f39d185cf8c74e4cf1e4fbad1d3df96faf510", size = 474836, upload-time = "2025-10-14T15:05:33.209Z" }, - { url = "https://files.pythonhosted.org/packages/b4/36/ded8aebea91919485b7bbabbd14f5f359326cb5ec218cd67074d1e426d74/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c85794a4cfa094714fb9c08d4a218375b2b95b8ed1666e8677c349906246c05", size = 455099, upload-time = "2025-10-14T15:05:34.189Z" }, - { url = "https://files.pythonhosted.org/packages/98/e0/8c9bdba88af756a2fce230dd365fab2baf927ba42cd47521ee7498fd5211/watchfiles-1.1.1-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:74d5012b7630714b66be7b7b7a78855ef7ad58e8650c73afc4c076a1f480a8d6", size = 630626, upload-time = "2025-10-14T15:05:35.216Z" }, - { url = "https://files.pythonhosted.org/packages/2a/84/a95db05354bf2d19e438520d92a8ca475e578c647f78f53197f5a2f17aaf/watchfiles-1.1.1-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:8fbe85cb3201c7d380d3d0b90e63d520f15d6afe217165d7f98c9c649654db81", size = 622519, upload-time = "2025-10-14T15:05:36.259Z" }, - { url = "https://files.pythonhosted.org/packages/1d/ce/d8acdc8de545de995c339be67711e474c77d643555a9bb74a9334252bd55/watchfiles-1.1.1-cp314-cp314-win32.whl", hash = "sha256:3fa0b59c92278b5a7800d3ee7733da9d096d4aabcfabb9a928918bd276ef9b9b", size = 272078, upload-time = "2025-10-14T15:05:37.63Z" }, - { url = "https://files.pythonhosted.org/packages/c4/c9/a74487f72d0451524be827e8edec251da0cc1fcf111646a511ae752e1a3d/watchfiles-1.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:c2047d0b6cea13b3316bdbafbfa0c4228ae593d995030fda39089d36e64fc03a", size = 287664, upload-time = "2025-10-14T15:05:38.95Z" }, - { url = "https://files.pythonhosted.org/packages/df/b8/8ac000702cdd496cdce998c6f4ee0ca1f15977bba51bdf07d872ebdfc34c/watchfiles-1.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:842178b126593addc05acf6fce960d28bc5fae7afbaa2c6c1b3a7b9460e5be02", size = 277154, upload-time = "2025-10-14T15:05:39.954Z" }, - { url = "https://files.pythonhosted.org/packages/47/a8/e3af2184707c29f0f14b1963c0aace6529f9d1b8582d5b99f31bbf42f59e/watchfiles-1.1.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:88863fbbc1a7312972f1c511f202eb30866370ebb8493aef2812b9ff28156a21", size = 403820, upload-time = "2025-10-14T15:05:40.932Z" }, - { url = "https://files.pythonhosted.org/packages/c0/ec/e47e307c2f4bd75f9f9e8afbe3876679b18e1bcec449beca132a1c5ffb2d/watchfiles-1.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:55c7475190662e202c08c6c0f4d9e345a29367438cf8e8037f3155e10a88d5a5", size = 390510, upload-time = "2025-10-14T15:05:41.945Z" }, - { url = "https://files.pythonhosted.org/packages/d5/a0/ad235642118090f66e7b2f18fd5c42082418404a79205cdfca50b6309c13/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f53fa183d53a1d7a8852277c92b967ae99c2d4dcee2bfacff8868e6e30b15f7", size = 448408, upload-time = "2025-10-14T15:05:43.385Z" }, - { url = "https://files.pythonhosted.org/packages/df/85/97fa10fd5ff3332ae17e7e40e20784e419e28521549780869f1413742e9d/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6aae418a8b323732fa89721d86f39ec8f092fc2af67f4217a2b07fd3e93c6101", size = 458968, upload-time = "2025-10-14T15:05:44.404Z" }, - { url = "https://files.pythonhosted.org/packages/47/c2/9059c2e8966ea5ce678166617a7f75ecba6164375f3b288e50a40dc6d489/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f096076119da54a6080e8920cbdaac3dbee667eb91dcc5e5b78840b87415bd44", size = 488096, upload-time = "2025-10-14T15:05:45.398Z" }, - { url = "https://files.pythonhosted.org/packages/94/44/d90a9ec8ac309bc26db808a13e7bfc0e4e78b6fc051078a554e132e80160/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:00485f441d183717038ed2e887a7c868154f216877653121068107b227a2f64c", size = 596040, upload-time = "2025-10-14T15:05:46.502Z" }, - { url = "https://files.pythonhosted.org/packages/95/68/4e3479b20ca305cfc561db3ed207a8a1c745ee32bf24f2026a129d0ddb6e/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a55f3e9e493158d7bfdb60a1165035f1cf7d320914e7b7ea83fe22c6023b58fc", size = 473847, upload-time = "2025-10-14T15:05:47.484Z" }, - { url = "https://files.pythonhosted.org/packages/4f/55/2af26693fd15165c4ff7857e38330e1b61ab8c37d15dc79118cdba115b7a/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c91ed27800188c2ae96d16e3149f199d62f86c7af5f5f4d2c61a3ed8cd3666c", size = 455072, upload-time = "2025-10-14T15:05:48.928Z" }, - { url = "https://files.pythonhosted.org/packages/66/1d/d0d200b10c9311ec25d2273f8aad8c3ef7cc7ea11808022501811208a750/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:311ff15a0bae3714ffb603e6ba6dbfba4065ab60865d15a6ec544133bdb21099", size = 629104, upload-time = "2025-10-14T15:05:49.908Z" }, - { url = "https://files.pythonhosted.org/packages/e3/bd/fa9bb053192491b3867ba07d2343d9f2252e00811567d30ae8d0f78136fe/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:a916a2932da8f8ab582f242c065f5c81bed3462849ca79ee357dd9551b0e9b01", size = 622112, upload-time = "2025-10-14T15:05:50.941Z" }, -] - -[[package]] -name = "websockets" -version = "15.0.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/21/e6/26d09fab466b7ca9c7737474c52be4f76a40301b08362eb2dbc19dcc16c1/websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee", size = 177016, upload-time = "2025-03-05T20:03:41.606Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/51/6b/4545a0d843594f5d0771e86463606a3988b5a09ca5123136f8a76580dd63/websockets-15.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3e90baa811a5d73f3ca0bcbf32064d663ed81318ab225ee4f427ad4e26e5aff3", size = 175437, upload-time = "2025-03-05T20:02:16.706Z" }, - { url = "https://files.pythonhosted.org/packages/f4/71/809a0f5f6a06522af902e0f2ea2757f71ead94610010cf570ab5c98e99ed/websockets-15.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:592f1a9fe869c778694f0aa806ba0374e97648ab57936f092fd9d87f8bc03665", size = 173096, upload-time = "2025-03-05T20:02:18.832Z" }, - { url = "https://files.pythonhosted.org/packages/3d/69/1a681dd6f02180916f116894181eab8b2e25b31e484c5d0eae637ec01f7c/websockets-15.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0701bc3cfcb9164d04a14b149fd74be7347a530ad3bbf15ab2c678a2cd3dd9a2", size = 173332, upload-time = "2025-03-05T20:02:20.187Z" }, - { url = "https://files.pythonhosted.org/packages/a6/02/0073b3952f5bce97eafbb35757f8d0d54812b6174ed8dd952aa08429bcc3/websockets-15.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8b56bdcdb4505c8078cb6c7157d9811a85790f2f2b3632c7d1462ab5783d215", size = 183152, upload-time = "2025-03-05T20:02:22.286Z" }, - { url = "https://files.pythonhosted.org/packages/74/45/c205c8480eafd114b428284840da0b1be9ffd0e4f87338dc95dc6ff961a1/websockets-15.0.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0af68c55afbd5f07986df82831c7bff04846928ea8d1fd7f30052638788bc9b5", size = 182096, upload-time = "2025-03-05T20:02:24.368Z" }, - { url = "https://files.pythonhosted.org/packages/14/8f/aa61f528fba38578ec553c145857a181384c72b98156f858ca5c8e82d9d3/websockets-15.0.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64dee438fed052b52e4f98f76c5790513235efaa1ef7f3f2192c392cd7c91b65", size = 182523, upload-time = "2025-03-05T20:02:25.669Z" }, - { url = "https://files.pythonhosted.org/packages/ec/6d/0267396610add5bc0d0d3e77f546d4cd287200804fe02323797de77dbce9/websockets-15.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d5f6b181bb38171a8ad1d6aa58a67a6aa9d4b38d0f8c5f496b9e42561dfc62fe", size = 182790, upload-time = "2025-03-05T20:02:26.99Z" }, - { url = "https://files.pythonhosted.org/packages/02/05/c68c5adbf679cf610ae2f74a9b871ae84564462955d991178f95a1ddb7dd/websockets-15.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5d54b09eba2bada6011aea5375542a157637b91029687eb4fdb2dab11059c1b4", size = 182165, upload-time = "2025-03-05T20:02:30.291Z" }, - { url = "https://files.pythonhosted.org/packages/29/93/bb672df7b2f5faac89761cb5fa34f5cec45a4026c383a4b5761c6cea5c16/websockets-15.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3be571a8b5afed347da347bfcf27ba12b069d9d7f42cb8c7028b5e98bbb12597", size = 182160, upload-time = "2025-03-05T20:02:31.634Z" }, - { url = "https://files.pythonhosted.org/packages/ff/83/de1f7709376dc3ca9b7eeb4b9a07b4526b14876b6d372a4dc62312bebee0/websockets-15.0.1-cp312-cp312-win32.whl", hash = "sha256:c338ffa0520bdb12fbc527265235639fb76e7bc7faafbb93f6ba80d9c06578a9", size = 176395, upload-time = "2025-03-05T20:02:33.017Z" }, - { url = "https://files.pythonhosted.org/packages/7d/71/abf2ebc3bbfa40f391ce1428c7168fb20582d0ff57019b69ea20fa698043/websockets-15.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcd5cf9e305d7b8338754470cf69cf81f420459dbae8a3b40cee57417f4614a7", size = 176841, upload-time = "2025-03-05T20:02:34.498Z" }, - { url = "https://files.pythonhosted.org/packages/cb/9f/51f0cf64471a9d2b4d0fc6c534f323b664e7095640c34562f5182e5a7195/websockets-15.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931", size = 175440, upload-time = "2025-03-05T20:02:36.695Z" }, - { url = "https://files.pythonhosted.org/packages/8a/05/aa116ec9943c718905997412c5989f7ed671bc0188ee2ba89520e8765d7b/websockets-15.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675", size = 173098, upload-time = "2025-03-05T20:02:37.985Z" }, - { url = "https://files.pythonhosted.org/packages/ff/0b/33cef55ff24f2d92924923c99926dcce78e7bd922d649467f0eda8368923/websockets-15.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151", size = 173329, upload-time = "2025-03-05T20:02:39.298Z" }, - { url = "https://files.pythonhosted.org/packages/31/1d/063b25dcc01faa8fada1469bdf769de3768b7044eac9d41f734fd7b6ad6d/websockets-15.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22", size = 183111, upload-time = "2025-03-05T20:02:40.595Z" }, - { url = "https://files.pythonhosted.org/packages/93/53/9a87ee494a51bf63e4ec9241c1ccc4f7c2f45fff85d5bde2ff74fcb68b9e/websockets-15.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f", size = 182054, upload-time = "2025-03-05T20:02:41.926Z" }, - { url = "https://files.pythonhosted.org/packages/ff/b2/83a6ddf56cdcbad4e3d841fcc55d6ba7d19aeb89c50f24dd7e859ec0805f/websockets-15.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8", size = 182496, upload-time = "2025-03-05T20:02:43.304Z" }, - { url = "https://files.pythonhosted.org/packages/98/41/e7038944ed0abf34c45aa4635ba28136f06052e08fc2168520bb8b25149f/websockets-15.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375", size = 182829, upload-time = "2025-03-05T20:02:48.812Z" }, - { url = "https://files.pythonhosted.org/packages/e0/17/de15b6158680c7623c6ef0db361da965ab25d813ae54fcfeae2e5b9ef910/websockets-15.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d", size = 182217, upload-time = "2025-03-05T20:02:50.14Z" }, - { url = "https://files.pythonhosted.org/packages/33/2b/1f168cb6041853eef0362fb9554c3824367c5560cbdaad89ac40f8c2edfc/websockets-15.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4", size = 182195, upload-time = "2025-03-05T20:02:51.561Z" }, - { url = "https://files.pythonhosted.org/packages/86/eb/20b6cdf273913d0ad05a6a14aed4b9a85591c18a987a3d47f20fa13dcc47/websockets-15.0.1-cp313-cp313-win32.whl", hash = "sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa", size = 176393, upload-time = "2025-03-05T20:02:53.814Z" }, - { url = "https://files.pythonhosted.org/packages/1b/6c/c65773d6cab416a64d191d6ee8a8b1c68a09970ea6909d16965d26bfed1e/websockets-15.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561", size = 176837, upload-time = "2025-03-05T20:02:55.237Z" }, - { url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743, upload-time = "2025-03-05T20:03:39.41Z" }, -] - [[package]] name = "zipp" version = "3.23.0" From 93eae3ead98acec46d3ac73f2cb71193456aca1a Mon Sep 17 00:00:00 2001 From: Carsten Scharfenberg Date: Mon, 2 Feb 2026 10:05:10 +0000 Subject: [PATCH 4/9] fix: update configuration file paths in docker-compose and Dockerfile for consistency --- dev_environment/compose.yaml | 4 ++-- docker/Dockerfile.sql_to_arc | 29 +++++++++++++++++++++++------ 2 files changed, 25 insertions(+), 8 deletions(-) diff --git a/dev_environment/compose.yaml b/dev_environment/compose.yaml index 5072081..6d61fa9 100644 --- a/dev_environment/compose.yaml +++ b/dev_environment/compose.yaml @@ -59,11 +59,11 @@ services: tmpfs: - /run/secrets:mode=1777 volumes: - - ./config-external.yaml:/etc/sql_to_arc/config.yaml:ro + - ./config.yaml:/etc/sql_to_arc/config.yaml:ro - ./client.crt:/etc/sql_to_arc/client.crt:ro command: > sh -c "printf '%s' \"$$CLIENT_KEY_DATA\" > /run/secrets/client.key && - /middleware/sql_to_arc/sql_to_arc -c /etc/sql_to_arc/config.yaml" + /middleware/sql_to_arc/sql_to_arc -c /etc/sql_to_arc/config.yaml" restart: no volumes: diff --git a/docker/Dockerfile.sql_to_arc b/docker/Dockerfile.sql_to_arc index 05f438e..b496abc 100644 --- a/docker/Dockerfile.sql_to_arc +++ b/docker/Dockerfile.sql_to_arc @@ -1,3 +1,20 @@ +# ---- Package Build Stage ---- +FROM python:3.12.12-alpine3.23 AS package-builder + +WORKDIR /build + +# Copy project files needed for package build +COPY pyproject.toml uv.lock ./ +COPY middleware ./middleware + +# Upgrade pip and install uv +RUN pip install --no-cache-dir --upgrade pip==25.3 uv==0.9.27 + +# We prefer to have a real python package for sql_to_arc that will be +# installed in the next stage using uv. +RUN uv build --package sql_to_arc --wheel + + # ---- Binary Build Stage ---- FROM python:3.12.12-alpine3.23 AS binary-builder @@ -14,15 +31,15 @@ WORKDIR /build # Install uv and PyInstaller RUN pip install --no-cache-dir --upgrade pip==25.3 uv==0.9.27 +RUN uv pip install --system pyinstaller -# Copy project files needed for package build +# Copy and install built wheel from package-builder stage +COPY --from=package-builder /build/dist/*.whl /tmp/wheels/ +RUN uv pip install --system /tmp/wheels/*.whl + +# use uv to install dependencies for sql_to_arc that we do not have pre-built wheels for COPY pyproject.toml uv.lock ./ COPY middleware ./middleware - -# Install PyInstaller -RUN uv pip install --system pyinstaller - -# Install further dependencies for sql_to_arc RUN uv sync # Build standalone binary with PyInstaller From 35a624804d601589ac5a7b4b4d54cb0e7e78df56 Mon Sep 17 00:00:00 2001 From: Carsten Scharfenberg Date: Mon, 2 Feb 2026 12:01:54 +0000 Subject: [PATCH 5/9] feat: update test workflow to include integration tests and adjust coverage reporting --- .github/workflows/python-quality.yml | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/.github/workflows/python-quality.yml b/.github/workflows/python-quality.yml index 71f6531..88d0ec5 100644 --- a/.github/workflows/python-quality.yml +++ b/.github/workflows/python-quality.yml @@ -101,15 +101,22 @@ jobs: echo "❌ Failing workflow due to HIGH or MEDIUM severity security issues found by bandit." exit 1 - - name: Run unit tests with coverage + - name: Run tests (Unit & Integration) run: | - uv run pytest middleware/*/tests/unit/ --cov=api --cov=api_client --cov=shared --cov=sql_to_arc --cov-report=xml --cov-report=html --cov-report=term-missing --junitxml=pytest-report.xml + uv run pytest \ + middleware/*/tests/unit/ \ + middleware/*/tests/integration/ \ + --cov=sql_to_arc \ + --cov-report=xml \ + --cov-report=html \ + --cov-report=term-missing \ + --junitxml=pytest-report.xml - name: Upload coverage to Codecov uses: codecov/codecov-action@v4 with: file: ./coverage.xml - flags: unittests + flags: tests name: codecov-umbrella fail_ci_if_error: false @@ -134,4 +141,4 @@ jobs: echo "| pylint | ✅ Code quality | Static analysis (min score: 8.0) |" >> $GITHUB_STEP_SUMMARY echo "| mypy | ✅ Type checking | Static type analysis |" >> $GITHUB_STEP_SUMMARY echo "| bandit | ✅ Security | Security vulnerability scan |" >> $GITHUB_STEP_SUMMARY - echo "| pytest | ✅ Unit tests | Test execution with coverage |" >> $GITHUB_STEP_SUMMARY + echo "| pytest | ✅ Unit & Integration tests | Test execution with coverage |" >> $GITHUB_STEP_SUMMARY From f6c1fe3e5ecc35e50b6e80bfdab9789077f3e5bb Mon Sep 17 00:00:00 2001 From: Carsten Scharfenberg Date: Mon, 2 Feb 2026 12:18:49 +0000 Subject: [PATCH 6/9] fix: update version in pyproject.toml and adjust git source branches for api_client and shared --- .devcontainer/devcontainer.json | 2 -- middleware/sql_to_arc/pyproject.toml | 6 +++--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index d453576..8069bf2 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -12,8 +12,6 @@ "disableIp6tables": true, "version": "28.5.1" }, - // NOTE: this conflicts with out own pre-push git hook. - // Currently we do not need git-lfs in this project, anyway. "ghcr.io/devcontainers/features/git-lfs:1": { "autoPull": true, "installDirectlyFromGitHubRelease": true, diff --git a/middleware/sql_to_arc/pyproject.toml b/middleware/sql_to_arc/pyproject.toml index b784ce6..893d124 100644 --- a/middleware/sql_to_arc/pyproject.toml +++ b/middleware/sql_to_arc/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "sql_to_arc" -version = "0.0.0" # currently we disregard the version of this subpackage +version = "0.1.0" # currently we disregard the version of this subpackage description = "The FAIRagro advanced middleware SQL-to-ARC converter" readme = "README.md" requires-python = ">=3.12" @@ -15,8 +15,8 @@ dependencies = [ ] [tool.uv.sources] -api_client = { git = "https://github.com/fairagro/m4.2_advanced_middleware_api.git", branch = "feature/split_off_client", subdirectory = "middleware/api_client" } -shared = { git = "https://github.com/fairagro/m4.2_advanced_middleware_api.git", branch = "feature/split_off_client", subdirectory = "middleware/shared" } +api_client = { git = "https://github.com/fairagro/m4.2_advanced_middleware_api.git", branch = "main", subdirectory = "middleware/api_client" } +shared = { git = "https://github.com/fairagro/m4.2_advanced_middleware_api.git", branch = "main", subdirectory = "middleware/shared" } [tool.hatch.build.targets.wheel] packages = ["src/middleware"] From dfaab705b4232c2ac041d077155fa3072f9083a2 Mon Sep 17 00:00:00 2001 From: Carsten Scharfenberg Date: Mon, 2 Feb 2026 12:21:24 +0000 Subject: [PATCH 7/9] fix: correct version number and project name in pyproject.toml for sql_to_arc --- middleware/sql_to_arc/pyproject.toml | 2 +- pyproject.toml | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/middleware/sql_to_arc/pyproject.toml b/middleware/sql_to_arc/pyproject.toml index 893d124..2494f24 100644 --- a/middleware/sql_to_arc/pyproject.toml +++ b/middleware/sql_to_arc/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "sql_to_arc" -version = "0.1.0" # currently we disregard the version of this subpackage +version = "0.0.0" # currently we disregard the version of this subpackage description = "The FAIRagro advanced middleware SQL-to-ARC converter" readme = "README.md" requires-python = ">=3.12" diff --git a/pyproject.toml b/pyproject.toml index d2abfdc..0f1a9b8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] -name = "m4-2-advanced-middleware" -version = "6.0.0" -description = "The FAIRagro advanced middleware" +name = "m4-2-sql-to-arc" +version = "0.1.0" +description = "The FAIRagro SQL-to-ARC client" readme = "README.md" requires-python = ">=3.12" dependencies = [ From 6247a53b131b3f73278c0ba6142151ade371b32c Mon Sep 17 00:00:00 2001 From: Carsten Scharfenberg Date: Mon, 2 Feb 2026 13:09:09 +0000 Subject: [PATCH 8/9] refactor: improve Dockerfile for building and installing sql_to_arc package --- docker/Dockerfile.sql_to_arc | 36 +++++++++++++++++++++++------------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/docker/Dockerfile.sql_to_arc b/docker/Dockerfile.sql_to_arc index b496abc..084e6b1 100644 --- a/docker/Dockerfile.sql_to_arc +++ b/docker/Dockerfile.sql_to_arc @@ -10,8 +10,8 @@ COPY middleware ./middleware # Upgrade pip and install uv RUN pip install --no-cache-dir --upgrade pip==25.3 uv==0.9.27 -# We prefer to have a real python package for sql_to_arc that will be -# installed in the next stage using uv. +# We prefer to build a wheel for our package first. This ensures we have a clean, +# distributable artifact that contains only the necessary files. RUN uv build --package sql_to_arc --wheel @@ -29,24 +29,34 @@ RUN apk add --no-cache \ WORKDIR /build -# Install uv and PyInstaller +# Install uv core tool RUN pip install --no-cache-dir --upgrade pip==25.3 uv==0.9.27 -RUN uv pip install --system pyinstaller -# Copy and install built wheel from package-builder stage +# Bring in the pre-built wheel and project metadata COPY --from=package-builder /build/dist/*.whl /tmp/wheels/ -RUN uv pip install --system /tmp/wheels/*.whl - -# use uv to install dependencies for sql_to_arc that we do not have pre-built wheels for COPY pyproject.toml uv.lock ./ + +# We still need the source code because uv sync requires all workspace members (e.g., middleware/sql_to_arc) +# to be physically present to validate the environment against the lockfile. COPY middleware ./middleware -RUN uv sync -# Build standalone binary with PyInstaller -RUN pyinstaller --onedir \ +# Dependency Resolution Strategy: +# 1. We would prefer to install everything as wheels for speed and reliability. +# 2. However, some dependencies (especially git-based ones like api_client) are not available +# as pre-built wheels on PyPI. +# 3. Thus, we use 'uv sync' to create a virtual environment (.venv) and resolve all +# complex dependencies exactly as specified in the uv.lock. +RUN uv sync --no-dev + +# 4. Finally, for packages like sql_to_arc that exist both as a workspace dependency +# and as a pre-built wheel, we explicitly 'uv pip install' the wheel. This ensures +# we use our optimized, pre-built package instead of the 'editable' source install. +RUN uv pip install /tmp/wheels/*.whl pyinstaller + +# Build standalone binary using the .venv's context. +RUN . .venv/bin/activate && \ + python -m PyInstaller --onedir \ --name sql_to_arc \ - --paths .venv/lib/python3.12/site-packages \ - --paths /build/middleware/sql_to_arc/src \ /build/middleware/sql_to_arc/src/middleware/sql_to_arc/main.py From fef39d233cbc1b0597c98d61c7e9dc4de022cedf Mon Sep 17 00:00:00 2001 From: Carsten Scharfenberg Date: Mon, 2 Feb 2026 13:14:40 +0000 Subject: [PATCH 9/9] fix: remove opentelemetry-sdk dependency from pyproject.toml --- middleware/sql_to_arc/pyproject.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/middleware/sql_to_arc/pyproject.toml b/middleware/sql_to_arc/pyproject.toml index 2494f24..9cbbfee 100644 --- a/middleware/sql_to_arc/pyproject.toml +++ b/middleware/sql_to_arc/pyproject.toml @@ -11,7 +11,6 @@ dependencies = [ "shared>=0.0.1", "api_client>=0.0.1", "opentelemetry-api>=1.39.1", - "opentelemetry-sdk>=1.39.1" ] [tool.uv.sources]