From 9668894c29891a2ccbe672e6432a2dd6acb92a50 Mon Sep 17 00:00:00 2001 From: Camill Kaipf <87362681+ckaipf@users.noreply.github.com> Date: Wed, 3 Apr 2024 10:52:30 +0200 Subject: [PATCH] Initial commit --- .devcontainer/.dev_config.yaml | 4 + .devcontainer/Dockerfile | 19 + .devcontainer/dev_install | 16 + .devcontainer/dev_launcher | 4 + .devcontainer/devcontainer.json | 78 ++ .devcontainer/docker-compose.yml | 70 + .devcontainer/license_header.txt | 14 + .gitattributes | 22 + .github/workflows/check_config_docs.yaml | 18 + .github/workflows/check_openapi_spec.yaml | 19 + .github/workflows/check_pyproject.yaml | 16 + .github/workflows/check_readme.yaml | 16 + .github/workflows/check_template_files.yaml | 21 + .github/workflows/ci_release.yaml | 15 + .github/workflows/ci_workflow_dispatch.yaml | 30 + .github/workflows/static_code_analysis.yaml | 27 + .github/workflows/tests.yaml | 31 + .gitignore | 145 ++ .pre-commit-config.yaml | 60 + .pyproject_generation/README.md | 29 + .pyproject_generation/pyproject_custom.toml | 19 + .pyproject_generation/pyproject_template.toml | 109 ++ .readme_generation/README.md | 47 + .readme_generation/description.md | 3 + .readme_generation/design.md | 7 + .readme_generation/readme_template.md | 113 ++ .readme_generation/template_overview.md | 27 + .template/README.md | 47 + .template/deprecated_files.txt | 44 + .template/deprecated_files_ignore.txt | 2 + .template/mandatory_files.txt | 32 + .template/mandatory_files_ignore.txt | 2 + .template/static_files.txt | 59 + .template/static_files_ignore.txt | 2 + Dockerfile | 48 + LICENSE | 202 +++ README.md | 294 ++++ config_schema.json | 201 +++ example_config.yaml | 17 + example_data/README.md | 4 + lock/README.md | 56 + lock/requirements-dev-template.in | 34 + lock/requirements-dev.in | 7 + lock/requirements-dev.txt | 1180 +++++++++++++++++ lock/requirements.txt | 744 +++++++++++ openapi.yaml | 113 ++ pyproject.toml | 154 +++ scripts/README.md | 3 + scripts/__init__.py | 17 + scripts/get_package_name.py | 46 + scripts/license_checker.py | 568 ++++++++ scripts/list_outdated_dependencies.py | 212 +++ scripts/script_utils/__init__.py | 17 + scripts/script_utils/cli.py | 36 + scripts/script_utils/deps.py | 75 ++ scripts/script_utils/fastapi_app_location.py | 23 + scripts/script_utils/lock_deps.py | 47 + scripts/update_all.py | 75 ++ scripts/update_config_docs.py | 160 +++ scripts/update_hook_revs.py | 133 ++ scripts/update_lock.py | 216 +++ scripts/update_openapi_docs.py | 103 ++ scripts/update_pyproject.py | 117 ++ scripts/update_readme.py | 238 ++++ scripts/update_template_files.py | 262 ++++ src/my_microservice/__init__.py | 20 + src/my_microservice/__main__.py | 33 + src/my_microservice/api/__init__.py | 16 + src/my_microservice/api/deps.py | 23 + src/my_microservice/api/main.py | 52 + src/my_microservice/config.py | 42 + src/my_microservice/core/__init__.py | 20 + src/my_microservice/core/greeting.py | 65 + src/my_microservice/models.py | 53 + tests/__init__.py | 16 + tests/fixtures/__init__.py | 16 + tests/fixtures/utils.py | 20 + tests/test_dummy.py | 24 + 78 files changed, 6969 insertions(+) create mode 100644 .devcontainer/.dev_config.yaml create mode 100644 .devcontainer/Dockerfile create mode 100755 .devcontainer/dev_install create mode 100755 .devcontainer/dev_launcher create mode 100644 .devcontainer/devcontainer.json create mode 100644 .devcontainer/docker-compose.yml create mode 100644 .devcontainer/license_header.txt create mode 100644 .gitattributes create mode 100644 .github/workflows/check_config_docs.yaml create mode 100644 .github/workflows/check_openapi_spec.yaml create mode 100644 .github/workflows/check_pyproject.yaml create mode 100644 .github/workflows/check_readme.yaml create mode 100644 .github/workflows/check_template_files.yaml create mode 100644 .github/workflows/ci_release.yaml create mode 100644 .github/workflows/ci_workflow_dispatch.yaml create mode 100644 .github/workflows/static_code_analysis.yaml create mode 100644 .github/workflows/tests.yaml create mode 100644 .gitignore create mode 100644 .pre-commit-config.yaml create mode 100644 .pyproject_generation/README.md create mode 100644 .pyproject_generation/pyproject_custom.toml create mode 100644 .pyproject_generation/pyproject_template.toml create mode 100644 .readme_generation/README.md create mode 100644 .readme_generation/description.md create mode 100644 .readme_generation/design.md create mode 100644 .readme_generation/readme_template.md create mode 100644 .readme_generation/template_overview.md create mode 100644 .template/README.md create mode 100644 .template/deprecated_files.txt create mode 100644 .template/deprecated_files_ignore.txt create mode 100644 .template/mandatory_files.txt create mode 100644 .template/mandatory_files_ignore.txt create mode 100644 .template/static_files.txt create mode 100644 .template/static_files_ignore.txt create mode 100644 Dockerfile create mode 100644 LICENSE create mode 100644 README.md create mode 100644 config_schema.json create mode 100644 example_config.yaml create mode 100644 example_data/README.md create mode 100644 lock/README.md create mode 100644 lock/requirements-dev-template.in create mode 100644 lock/requirements-dev.in create mode 100644 lock/requirements-dev.txt create mode 100644 lock/requirements.txt create mode 100644 openapi.yaml create mode 100644 pyproject.toml create mode 100644 scripts/README.md create mode 100644 scripts/__init__.py create mode 100755 scripts/get_package_name.py create mode 100755 scripts/license_checker.py create mode 100755 scripts/list_outdated_dependencies.py create mode 100644 scripts/script_utils/__init__.py create mode 100644 scripts/script_utils/cli.py create mode 100644 scripts/script_utils/deps.py create mode 100644 scripts/script_utils/fastapi_app_location.py create mode 100644 scripts/script_utils/lock_deps.py create mode 100755 scripts/update_all.py create mode 100755 scripts/update_config_docs.py create mode 100755 scripts/update_hook_revs.py create mode 100755 scripts/update_lock.py create mode 100755 scripts/update_openapi_docs.py create mode 100755 scripts/update_pyproject.py create mode 100755 scripts/update_readme.py create mode 100755 scripts/update_template_files.py create mode 100644 src/my_microservice/__init__.py create mode 100644 src/my_microservice/__main__.py create mode 100644 src/my_microservice/api/__init__.py create mode 100644 src/my_microservice/api/deps.py create mode 100644 src/my_microservice/api/main.py create mode 100644 src/my_microservice/config.py create mode 100644 src/my_microservice/core/__init__.py create mode 100644 src/my_microservice/core/greeting.py create mode 100644 src/my_microservice/models.py create mode 100644 tests/__init__.py create mode 100644 tests/fixtures/__init__.py create mode 100644 tests/fixtures/utils.py create mode 100644 tests/test_dummy.py diff --git a/.devcontainer/.dev_config.yaml b/.devcontainer/.dev_config.yaml new file mode 100644 index 00000000..d8084b29 --- /dev/null +++ b/.devcontainer/.dev_config.yaml @@ -0,0 +1,4 @@ +# Please only mention the non-default settings here: + +language: Greek +service_instance_id: "1" diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 00000000..a3be4194 --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,19 @@ +FROM mcr.microsoft.com/vscode/devcontainers/python:3.9-bullseye + +ENV PYTHONUNBUFFERED 1 + +# Update args in docker-compose.yaml to set the UID/GID of the "vscode" user. +ARG USER_UID=1000 +ARG USER_GID=$USER_UID +RUN if [ "$USER_GID" != "1000" ] || [ "$USER_UID" != "1000" ]; then groupmod --gid $USER_GID vscode && usermod --uid $USER_UID --gid $USER_GID vscode; fi + +# [Option] Install Node.js +ARG INSTALL_NODE="true" +ARG NODE_VERSION="lts/*" +RUN if [ "${INSTALL_NODE}" = "true" ]; then su vscode -c "umask 0002 && . /usr/local/share/nvm/nvm.sh && nvm install ${NODE_VERSION} 2>&1"; fi + +# Copy install and launcher script to bin: +COPY ./dev_install /bin +COPY ./dev_launcher /bin + +CMD ["sleep", "infinity"] diff --git a/.devcontainer/dev_install b/.devcontainer/dev_install new file mode 100755 index 00000000..6a433892 --- /dev/null +++ b/.devcontainer/dev_install @@ -0,0 +1,16 @@ +#!/bin/bash +# install service in dev container + +cd /workspace + +# upgrade pip +python -m pip install --upgrade pip + +# install or upgrade dependencies for development and testing +pip install --no-deps -r ./lock/requirements-dev.txt + +# install the package itself in edit mode: +pip install --no-deps -e . + +# install pre-commit hooks to git +pre-commit install diff --git a/.devcontainer/dev_launcher b/.devcontainer/dev_launcher new file mode 100755 index 00000000..7cdc8166 --- /dev/null +++ b/.devcontainer/dev_launcher @@ -0,0 +1,4 @@ +#!/bin/bash + +# adapt to package name +my-microservice diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 00000000..c49da2b5 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,78 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the README at: +// https://github.com/microsoft/vscode-dev-containers/tree/v0.177.0/containers/python-3-postgres +// Update the VARIANT arg in docker-compose.yml to pick a Python version: 3, 3.8, 3.7, 3.6 +{ + "name": "${localWorkspaceFolderBasename}", + "dockerComposeFile": "docker-compose.yml", + "service": "app", + "workspaceFolder": "/workspace", + "customizations": { + "vscode": { + // Set *default* container specific settings.json values on container create. + "settings": { + "files.eol": "\n", + "terminal.integrated.profiles.linux": { + "bash": { + "path": "/bin/bash" + } + }, + "python.pythonPath": "/usr/local/bin/python", + "python.languageServer": "Pylance", + "python.analysis.typeCheckingMode": "basic", + "python.testing.pytestPath": "/usr/local/py-utils/bin/pytest", + "python.testing.pytestArgs": [ + "--profile" + ], + "python.testing.unittestEnabled": false, + "python.testing.pytestEnabled": true, + "editor.codeActionsOnSave": { + "source.organizeImports": true + }, + "editor.formatOnSave": true, + "editor.renderWhitespace": "all", + "editor.rulers": [ + 88 + ], + "ruff.organizeImports": true, + "editor.defaultFormatter": "charliermarsh.ruff", + "licenser.license": "Custom", + "licenser.customHeaderFile": "/workspace/.devcontainer/license_header.txt" + }, + // Add the IDs of extensions you want installed when the container is created. + "extensions": [ + "mikestead.dotenv", + "ms-azuretools.vscode-docker", + "ms-python.python", + "ms-python.vscode-pylance", + "ms-toolsai.jupyter", + "njpwerner.autodocstring", + "redhat.vscode-yaml", + "42crunch.vscode-openapi", + "arjun.swagger-viewer", + "eamodio.gitlens", + "github.vscode-pull-request-github", + "streetsidesoftware.code-spell-checker", + "yzhang.markdown-all-in-one", + "visualstudioexptteam.vscodeintellicode", + "ymotongpoo.licenser", + "charliermarsh.ruff", + "ms-python.mypy-type-checker" + ] + } + }, + // Use 'forwardPorts' to make a list of ports inside the container available locally. + // "forwardPorts": [5000, 5432], + // Use 'postCreateCommand' to run commands after the container is created. + "postCreateCommand": "dev_install", + // Comment out connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. + "remoteUser": "vscode", + "containerEnv": { + // for testcontainers to connect to the docker host: + "TC_HOST": "host.docker.internal", + "DOCKER_HOST": "unix:///var/run/docker.sock" + }, + "features": { + // details can be found here: https://github.com/devcontainers/features/tree/main/src/docker-outside-of-docker + "ghcr.io/devcontainers/features/docker-outside-of-docker:1": {} + } +} diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml new file mode 100644 index 00000000..1f911d24 --- /dev/null +++ b/.devcontainer/docker-compose.yml @@ -0,0 +1,70 @@ +version: '3' + +services: + app: + build: + context: . + dockerfile: ./Dockerfile + args: + # [Choice] Python version: 3, 3.8, 3.7, 3.6 + VARIANT: 3.9 + # [Choice] Install Node.js + INSTALL_NODE: "true" + NODE_VERSION: "lts/*" + # Please adapt to package name: + PACKAGE_NAME: "my_microservice" + # On Linux, you may need to update USER_UID and USER_GID below if not your local UID is not 1000. + USER_UID: 1000 + USER_GID: 1000 + + init: true + privileged: true + + volumes: + - ..:/workspace:cached + + # Overrides default command so things don't shut down after the process ends. + command: sleep infinity + + # Uncomment the next line to use a non-root user for all processes. + user: vscode + + # define environment variables + environment: + # Please adapt to package name: + MY_MICROSERVICE_CONFIG_YAML: /workspace/.devcontainer/.dev_config.yaml + # Used by db migration: + DB_URL: postgresql://postgres:postgres@postgresql/postgres + # Use "forwardPorts" in **devcontainer.json** to forward an app port locally. + # (Adding the "ports" property to this file will not forward from a Codespace.) + + + # Please remove service dependencies that are not needed: + mongodb: + image: mongo:latest + restart: unless-stopped + volumes: + - mongo_fs:/data/db + + localstack: + image: localstack/localstack + environment: + SERVICES: s3 + DEFAULT_REGION: eu-west-1 + AWS_DEFAULT_REGION: eu-west-1 + # accessible at localhost + HOSTNAME_EXTERNAL: localhost + USE_SSL: "false" + DATA_DIR: /var/lib/localstack/data + DEBUG: 1 + volumes: + - type: volume + source: s3_fs + target: /var/lib/localstack + volume: + nocopy: true + # useful ports: 4566 - AWS API + +volumes: + s3_fs: {} + mongo_fs: {} diff --git a/.devcontainer/license_header.txt b/.devcontainer/license_header.txt new file mode 100644 index 00000000..5757817f --- /dev/null +++ b/.devcontainer/license_header.txt @@ -0,0 +1,14 @@ +Copyright 2021 - 2023 Universität Tübingen, DKFZ, EMBL, and Universität zu Köln +for the German Human Genome-Phenome Archive (GHGA) + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..416f1044 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,22 @@ +# CRLF line endings cause problems in Docker, so we force git +# to check in and out using only LF line endings. + +*.cfg text eol=lf +*.ini text eol=lf +*.json text eol=lf +*.md text eol=lf +*.py text eol=lf +*.txt text eol=lf +*.yaml text eol=lf +*.yml text eol=lf + +.editorconfig text eol=lf +.flake8 text eol=lf +.pylintrc text eol=lf + +.git* text eol=lf +*_files text eol=lf +*_files_ignore text eol=lf + +**/dev_* text eol=lf +**/Dockerfile text eol=lf diff --git a/.github/workflows/check_config_docs.yaml b/.github/workflows/check_config_docs.yaml new file mode 100644 index 00000000..bb88b574 --- /dev/null +++ b/.github/workflows/check_config_docs.yaml @@ -0,0 +1,18 @@ +name: Check if the config schema and the example are up to date. + +on: push + +jobs: + static-code-analysis: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - id: common + uses: ghga-de/gh-action-common@v4 + + - name: Check config docs + run: | + export ${{ steps.common.outputs.CONFIG_YAML_ENV_VAR_NAME }}="${{ steps.common.outputs.CONFIG_YAML }}" + + ./scripts/update_config_docs.py --check diff --git a/.github/workflows/check_openapi_spec.yaml b/.github/workflows/check_openapi_spec.yaml new file mode 100644 index 00000000..f5db7665 --- /dev/null +++ b/.github/workflows/check_openapi_spec.yaml @@ -0,0 +1,19 @@ +# This file is only needed, if your repository uses FastAPI +name: Check if openapi.yaml is up to date + +on: push + +jobs: + static-code-analysis: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - id: common + uses: ghga-de/gh-action-common@v4 + + - name: Check if openapi.yaml is up to date + run: | + export ${{ steps.common.outputs.CONFIG_YAML_ENV_VAR_NAME }}="${{ steps.common.outputs.CONFIG_YAML }}" + + ./scripts/update_openapi_docs.py --check diff --git a/.github/workflows/check_pyproject.yaml b/.github/workflows/check_pyproject.yaml new file mode 100644 index 00000000..d929c799 --- /dev/null +++ b/.github/workflows/check_pyproject.yaml @@ -0,0 +1,16 @@ +name: Check if the config schema and the example are up to date. + +on: push + +jobs: + static-code-analysis: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - id: common + uses: ghga-de/gh-action-common@v4 + + - name: Check pyproject.toml + run: | + ./scripts/update_pyproject.py --check diff --git a/.github/workflows/check_readme.yaml b/.github/workflows/check_readme.yaml new file mode 100644 index 00000000..7414554d --- /dev/null +++ b/.github/workflows/check_readme.yaml @@ -0,0 +1,16 @@ +name: Check if the readme is up to date. + +on: push + +jobs: + static-code-analysis: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - id: common + uses: ghga-de/gh-action-common@v4 + + - name: Check readme + run: | + ./scripts/update_readme.py --check diff --git a/.github/workflows/check_template_files.yaml b/.github/workflows/check_template_files.yaml new file mode 100644 index 00000000..9fb5cbf0 --- /dev/null +++ b/.github/workflows/check_template_files.yaml @@ -0,0 +1,21 @@ +name: Check template files + +on: push + +jobs: + check-template-files: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Set up Python 3.9 + uses: actions/setup-python@v4 + with: + python-version: 3.9 + - name: Check template files + run: | + if [ "${{ github.event.repository.name }}" == "microservice-repository-template" ] + then + echo "Skipping this test as operating on the template repo." + else + ./scripts/update_template_files.py --check + fi diff --git a/.github/workflows/ci_release.yaml b/.github/workflows/ci_release.yaml new file mode 100644 index 00000000..d8547b40 --- /dev/null +++ b/.github/workflows/ci_release.yaml @@ -0,0 +1,15 @@ +name: CI on release + +on: + release: + types: [published] + +jobs: + push_to_docker_hub: + runs-on: ubuntu-latest + steps: + - uses: ghga-de/gh-action-ci@v1 + with: + tag: ${{ github.event.release.tag_name }} + dockerhub_username: ${{ secrets.DOCKERHUB_USERNAME }} + dockerhub_token: ${{ secrets.DOCKERHUB_TOKEN }} diff --git a/.github/workflows/ci_workflow_dispatch.yaml b/.github/workflows/ci_workflow_dispatch.yaml new file mode 100644 index 00000000..eab7d5a3 --- /dev/null +++ b/.github/workflows/ci_workflow_dispatch.yaml @@ -0,0 +1,30 @@ +name: Build on PR or dispatch + +on: + workflow_dispatch: + pull_request: + types: + - opened + - synchronize + - reopened + - labeled + +jobs: + fetch-tag: + runs-on: ubuntu-latest + if: github.event_name == 'workflow_dispatch' || ( github.event.action != 'labeled' && contains(github.event.pull_request.labels.*.name, 'build') ) || ( github.event.action == 'labeled' && github.event.label.name == 'build' ) + steps: + - id: fetch-tag + uses: ghga-de/gh-action-fetch-tag@v1 + outputs: + latest_tag: ${{ steps.fetch-tag.outputs.latest_tag }} + + push_to_docker_hub: + needs: fetch-tag + runs-on: ubuntu-latest + steps: + - uses: ghga-de/gh-action-ci@v1 + with: + tag: ${{ needs.fetch-tag.outputs.latest_tag }}-${{ github.sha }} + dockerhub_username: ${{ secrets.DOCKERHUB_USERNAME }} + dockerhub_token: ${{ secrets.DOCKERHUB_TOKEN }} diff --git a/.github/workflows/static_code_analysis.yaml b/.github/workflows/static_code_analysis.yaml new file mode 100644 index 00000000..86a014df --- /dev/null +++ b/.github/workflows/static_code_analysis.yaml @@ -0,0 +1,27 @@ +name: Static Code Analysis + +on: push + +jobs: + static-code-analysis: + runs-on: ubuntu-latest + name: Static Code Analysis + steps: + - uses: actions/checkout@v3 + + - id: common + uses: ghga-de/gh-action-common@v4 + + - uses: pre-commit/action@v3.0.0 + env: + SKIP: no-commit-to-branch + - name: ruff + run: | + ruff check --output-format=github . + ruff format --check . + - name: mypy + run: | + mypy . + - name: Check license header and file + run: | + ./scripts/license_checker.py diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml new file mode 100644 index 00000000..dacc9f50 --- /dev/null +++ b/.github/workflows/tests.yaml @@ -0,0 +1,31 @@ +name: Tests + +on: push + +jobs: + tests: + runs-on: ubuntu-latest + name: Tests + + steps: + - uses: actions/checkout@v3 + + - id: common + uses: ghga-de/gh-action-common@v4 + + - id: pytest + run: | + export ${{ steps.common.outputs.CONFIG_YAML_ENV_VAR_NAME }}="${{ steps.common.outputs.CONFIG_YAML }}" + + pytest \ + --cov="${{ steps.common.outputs.PACKAGE_NAME }}" \ + --cov-report=xml \ + tests + + - id: coveralls + name: Upload coverage to coveralls + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + pip install --upgrade coveralls + coveralls --service=github diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..d9ba6145 --- /dev/null +++ b/.gitignore @@ -0,0 +1,145 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +.ruff_cache/ +prof/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Virtual environments and environment files +*.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# ignore VS Code settings: +.vscode/ + +# key stores +*.key +*.rnd +.keystore +.ssl/ + +# desktop settings and thumbnails +.DS_Store +desktop.ini +thumbs.db diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000..b6229787 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,60 @@ +# See https://pre-commit.com for more information +# See https://pre-commit.com/hooks.html for more hooks + +default_language_version: + python: python3.9 + +minimum_pre_commit_version: 3.6.0 + +repos: + - repo: local + hooks: + - id: update-hook-revs + name: "ensure hooks are up to date" + language: python + additional_dependencies: + - "packaging" + - "typer" + fail_fast: true + always_run: true + entry: ./scripts/update_hook_revs.py + files: '\.pre-commit-config.yaml' + args: [--check] + pass_filenames: false + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.4.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + exclude: '.*\.json|example_config.yaml' + - id: check-yaml + - id: check-added-large-files + - id: check-ast + - id: check-json + exclude: devcontainer.json + - id: pretty-format-json + args: [--autofix] + exclude: devcontainer.json|config_schema.json + - id: check-merge-conflict + - id: check-symlinks + - id: check-executables-have-shebangs + - id: check-shebang-scripts-are-executable + - id: check-docstring-first + - id: debug-statements + - id: destroyed-symlinks + - id: detect-private-key + - id: mixed-line-ending + args: [--fix=lf] + - id: no-commit-to-branch + args: [--branch, dev, --branch, int, --branch, main] + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.3.2 + hooks: + - id: ruff + args: [--fix, --exit-non-zero-on-fix] + - id: ruff-format + - repo: https://github.com/pre-commit/mirrors-mypy + rev: v1.9.0 + hooks: + - id: mypy + args: [--no-warn-unused-ignores] diff --git a/.pyproject_generation/README.md b/.pyproject_generation/README.md new file mode 100644 index 00000000..93b988fe --- /dev/null +++ b/.pyproject_generation/README.md @@ -0,0 +1,29 @@ + + +# Generating the pyproject.toml + +The pyproject.toml of the service is generated by combining static configuration +captured in [`./pyproject_template.toml`](./pyproject_template.toml) and custom +package metadata specified in [`./pyproject_custom.toml`](./pyproject_custom.toml). + +The `./pyproject_template.toml` is managed by the template, please do not edit manually. + +You may specify properties in the `./pyproject_custom.toml` which are already specified +in the `./pyproject_template.toml`. In that case, the `./pyproject_custom.toml` takes +priority. diff --git a/.pyproject_generation/pyproject_custom.toml b/.pyproject_generation/pyproject_custom.toml new file mode 100644 index 00000000..5f8c2276 --- /dev/null +++ b/.pyproject_generation/pyproject_custom.toml @@ -0,0 +1,19 @@ +[project] +# please adapt to package name +name = "my_microservice" +version = "0.1.0" +description = "My-Microservice - a short description" +dependencies = [ + "typer >= 0.9.0", + "ghga-service-commons[api] >= 1.2.0", + "ghga-event-schemas >= 1.0.0", + "hexkit[akafka,s3,mongodb] >= 1.1.0" +] + +[project.urls] +# please adapt to package name +Repository = "https://github.com/ghga-de/my-microservice" + +[project.scripts] +# please adapt to package name +my-microservice = "my_microservice.__main__:run" diff --git a/.pyproject_generation/pyproject_template.toml b/.pyproject_generation/pyproject_template.toml new file mode 100644 index 00000000..a6201df9 --- /dev/null +++ b/.pyproject_generation/pyproject_template.toml @@ -0,0 +1,109 @@ +[build-system] +requires = ["setuptools>=67.7.2"] +build-backend = "setuptools.build_meta" + +[project] +readme = "README.md" +authors = [ + { name = "German Human Genome Phenome Archive (GHGA)", email = "contact@ghga.de" }, +] +requires-python = ">=3.9" +license = { text = "Apache 2.0" } +classifiers = [ + "Development Status :: 1 - Planning", + "Operating System :: POSIX :: Linux", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "License :: OSI Approved :: Apache Software License", + "Topic :: Internet :: WWW/HTTP :: HTTP Servers", + "Topic :: Software Development :: Libraries", + "Intended Audience :: Developers", +] + +[tool.setuptools.packages.find] +where = ["src"] + +[tool.ruff] +exclude = [ + ".git", + ".devcontainer", + "__pycache__", + "build", + "dist", +] +line-length = 88 +src = ["src", "tests", "examples", "scripts"] +target-version = "py39" + +[tool.ruff.lint] +fixable = [ + "UP", # e.g. List -> list + "I", # sort imports + "D", # pydocstyle +] +ignore = [ + "E", # pycodestyle errors + "W", # pycodestyle warnings - pycodestyle covered by black + "PLW", # pylint warnings + "RUF001", # ambiguous unicode character strings + "RUF010", # explicit conversion to string or repr: !s or !r + "RUF012", # mutable class variables need typing.ClassVar annotation + "N818", # Errors need to have Error suffix + "B008", # function call in arg defaults, + "PLR2004", # magic numbers should be constants + "D205", # blank-line-after-summary + "D400", # first doc line ends in period + "D401", # non-imperative-mood + "D107", # missing docstring in __init__ + "D206", # indent-with-spaces (ignored for formatter) + "D300", # triple-single-quotes (ignored for formatter) +] +select = [ + "C90", # McCabe Complexity + "F", # pyflakes codes + "I", # isort + "S", # flake8-bandit + "B", # flake8-bugbear + "N", # pep8-naming + "UP", # pyupgrade + "PL", # pylint + "RUF", # ruff + "SIM", # flake8-simplify + "D", # pydocstyle +] + +[tool.ruff.lint.mccabe] +max-complexity = 10 + +[tool.ruff.lint.per-file-ignores] +"scripts/*" = ["PL", "S", "SIM", "D"] +"tests/*" = ["S", "SIM", "PLR", "B011"] +".devcontainer/*" = ["S", "SIM", "D"] +"examples/*" = ["S", "D"] +"__init__.py" = ["D"] + +[tool.ruff.lint.pydocstyle] +convention = "pep257" + +[tool.mypy] +disable_error_code = "import" +show_error_codes = true +exclude = [ + 'build/lib/', +] +warn_redundant_casts = true +warn_unused_ignores = true +check_untyped_defs = true +no_site_packages = false + +[tool.pytest.ini_options] +minversion = "7.1" +asyncio_mode = "strict" + +[tool.coverage.paths] +source = [ + "src", + "/workspace/src", + "**/lib/python*/site-packages", +] diff --git a/.readme_generation/README.md b/.readme_generation/README.md new file mode 100644 index 00000000..ed2a2a79 --- /dev/null +++ b/.readme_generation/README.md @@ -0,0 +1,47 @@ + + +# Readme Generation + +The Repository README is generated by collecting information from different sources as +outlined in the following. + +- name: The full name of the package is derived from the remote origin Git repository. +- title: A title case representation of the name. +- shortname: An abbreviation of the full name. This is derived from the name mentioned + in the [`../pyproject.toml`](../pyproject.toml). +- summary: A short 1-2 sentence summary derived from the description in the + [`../pyproject.toml`](../pyproject.toml). +- version: The package version derived from the version specified in the + [`../pyproject.toml`](../pyproject.toml). +- description: A markdown-formatted description of the features and use cases of this + service or package. Obtained from the [`./description.md`](./description.md). +- design_description: A markdown-formatted description of the overall architecture and + design of the package. Obtained from the [`./design.md`](./design.md). +- config_description: A markdown-formatted description of all config parameters. + This is autogenerated from the [`../config_schema.json`](../config_schema.json). +- openapi_doc: A markdown-formatted description of the HTTP API. This is autogenerated + and links to the [`../openapi.yaml`](../openapi.yaml). If the openapi.yaml is not + this documentation is empty. + +The [`./readme_template.md`](./readme_template.md) serves as a template where the +above variable can be filled in using Pythons `string.Template` utility from the +standard library. + +The [`../scripts/update_readme.py`](../scripts/update_readme.py) script can be used to +collect all information and fill it into the template to generate the README file. diff --git a/.readme_generation/description.md b/.readme_generation/description.md new file mode 100644 index 00000000..2ad6dc4f --- /dev/null +++ b/.readme_generation/description.md @@ -0,0 +1,3 @@ + + +Here you should provide a short summary of the purpose of this microservice. diff --git a/.readme_generation/design.md b/.readme_generation/design.md new file mode 100644 index 00000000..f2dfee41 --- /dev/null +++ b/.readme_generation/design.md @@ -0,0 +1,7 @@ + + +This is a Python-based service following the Triple Hexagonal Architecture pattern. +It uses protocol/provider pairs and dependency injection mechanisms provided by the +[hexkit](https://github.com/ghga-de/hexkit) library. diff --git a/.readme_generation/readme_template.md b/.readme_generation/readme_template.md new file mode 100644 index 00000000..98a00c09 --- /dev/null +++ b/.readme_generation/readme_template.md @@ -0,0 +1,113 @@ +[![tests](https://github.com/ghga-de/$repo_name/actions/workflows/tests.yaml/badge.svg)](https://github.com/ghga-de/$repo_name/actions/workflows/tests.yaml) +[![Coverage Status](https://coveralls.io/repos/github/ghga-de/$repo_name/badge.svg?branch=main)](https://coveralls.io/github/ghga-de/$repo_name?branch=main) + +# $title + +$summary + +## Description + +$description + +## Installation + +We recommend using the provided Docker container. + +A pre-build version is available at [docker hub](https://hub.docker.com/repository/docker/ghga/$name): +```bash +docker pull ghga/$name:$version +``` + +Or you can build the container yourself from the [`./Dockerfile`](./Dockerfile): +```bash +# Execute in the repo's root dir: +docker build -t ghga/$name:$version . +``` + +For production-ready deployment, we recommend using Kubernetes, however, +for simple use cases, you could execute the service using docker +on a single server: +```bash +# The entrypoint is preconfigured: +docker run -p 8080:8080 ghga/$name:$version --help +``` + +If you prefer not to use containers, you may install the service from source: +```bash +# Execute in the repo's root dir: +pip install . + +# To run the service: +$shortname --help +``` + +## Configuration + +### Parameters + +The service requires the following configuration parameters: +$config_description + +### Usage: + +A template YAML for configurating the service can be found at +[`./example-config.yaml`](./example-config.yaml). +Please adapt it, rename it to `.$shortname.yaml`, and place it into one of the following locations: +- in the current working directory were you are execute the service (on unix: `./.$shortname.yaml`) +- in your home directory (on unix: `~/.$shortname.yaml`) + +The config yaml will be automatically parsed by the service. + +**Important: If you are using containers, the locations refer to paths within the container.** + +All parameters mentioned in the [`./example-config.yaml`](./example-config.yaml) +could also be set using environment variables or file secrets. + +For naming the environment variables, just prefix the parameter name with `${shortname}_`, +e.g. for the `host` set an environment variable named `${shortname}_host` +(you may use both upper or lower cases, however, it is standard to define all env +variables in upper cases). + +To using file secrets please refer to the +[corresponding section](https://pydantic-docs.helpmanual.io/usage/settings/#secret-support) +of the pydantic documentation. + +$openapi_doc + +## Architecture and Design: +$design_description + +## Development + +For setting up the development environment, we rely on the +[devcontainer feature](https://code.visualstudio.com/docs/remote/containers) of VS Code +in combination with Docker Compose. + +To use it, you have to have Docker Compose as well as VS Code with its "Remote - Containers" +extension (`ms-vscode-remote.remote-containers`) installed. +Then open this repository in VS Code and run the command +`Remote-Containers: Reopen in Container` from the VS Code "Command Palette". + +This will give you a full-fledged, pre-configured development environment including: +- infrastructural dependencies of the service (databases, etc.) +- all relevant VS Code extensions pre-installed +- pre-configured linting and auto-formatting +- a pre-configured debugger +- automatic license-header insertion + +Moreover, inside the devcontainer, a convenience commands `dev_install` is available. +It installs the service with all development dependencies, installs pre-commit. + +The installation is performed automatically when you build the devcontainer. However, +if you update dependencies in the [`./pyproject.toml`](./pyproject.toml) or the +[`./requirements-dev.txt`](./requirements-dev.txt), please run it again. + +## License + +This repository is free to use and modify according to the +[Apache 2.0 License](./LICENSE). + +## README Generation + +This README file is auto-generated, please see [`readme_generation.md`](./readme_generation.md) +for details. diff --git a/.readme_generation/template_overview.md b/.readme_generation/template_overview.md new file mode 100644 index 00000000..6e148ef3 --- /dev/null +++ b/.readme_generation/template_overview.md @@ -0,0 +1,27 @@ +# Microservice Repository Template + +This is a template for GitHub repositories containing one Python-based microservice (optimal for a multirepository setup). + +It features: + +- *Continuous Templation* - A continuous update-delivery mechanism for templated repositories +- A [devcontainer](https://containers.dev/)-based fully-configured development environment for vscode +- Tight linting and formatting using [Ruff](https://docs.astral.sh/ruff/) +- Static type checking using [mypy](https://www.mypy-lang.org/) +- Security scanning using [bandit](https://bandit.readthedocs.io/en/latest/) +- A structure for automated tests using [pytest](https://docs.pytest.org/en/7.4.x/) +- Dependency locking using [pip-tools](https://github.com/jazzband/pip-tools) +- Git hooks checking linting and formatting before committing using [pre-commit](https://pre-commit.com/) +- Automatic container-building and publishing to [Docker Hub](https://hub.docker.com/) +- GitHub Actions for automating or checking all of the above + +It is worth emphasizing the first point, this template is not just a one-time kickstart for your project +but repositories created using this template will continue receiving updates as the template evolves. +For further details, please look at the explanation in [.template/README.md](/.template/README.md). + +Please also refer to [.readme_generation/README.md](/.readme_generation/README.md) for details on how +to adapt this readme. + +Here the intro to the template stops and the actual template for the readme of the microservice starts: + +--- diff --git a/.template/README.md b/.template/README.md new file mode 100644 index 00000000..8a5e7cd6 --- /dev/null +++ b/.template/README.md @@ -0,0 +1,47 @@ + + +# Template File Lists + +This directory contains multiple text files that are listing paths to other files +of this repository. The listed files are affected in different ways by template updates +as explained in the following. + +## `static_files.txt` +The files listed here are synced with their counterparts in the template. They should +never be modified manually. + +## `static_files_ignore.txt` +To opt out of template updates just for individual files declared as static +(e.g. because you would like manually modify them), you may add them to this list. + +## `mandatory_files.txt` +The contents of the files listed here are not synced with the template, however, upon +every template update it is checked that the files exist. You should modify them +manually to the needs of your repository. + +## `mandatory_files_ignore.txt` +To opt out of existence checks for individual files declared as mandatory, you may add +them to this list. + +## `deprecated_files.txt` +Files listed here must not exist in your repository and are automatically deleted upon +a template update. + +## `deprecated_files_ignore.txt` +If you would like to keep files declared as deprecated, you may add them to this list. diff --git a/.template/deprecated_files.txt b/.template/deprecated_files.txt new file mode 100644 index 00000000..df962efe --- /dev/null +++ b/.template/deprecated_files.txt @@ -0,0 +1,44 @@ +# List of all deprecated files and directories that +# should not exist any more in this repo. +# This list is similar to the `./mandatory_files`, +# however, the entries here will be removed if they +# still exist. + +.devcontainer/library-scripts/docker-in-docker-debian.sh +.devcontainer/library-scripts + +.github/workflows/check_mandatory_and_static_files.yaml +.github/workflows/dev_cd.yaml +.github/workflows/unit_and_int_tests.yaml +.github/workflows/cd.yaml + +scripts/check_mandatory_and_static_files.py +scripts/update_static_files.py + +docs + +setup.py +setup.cfg +requirements-dev-common.in +requirements-dev.in +requirements-dev.txt +requirements.txt +pytest.ini +readme_generation.md + +.pylintrc +.flake8 +.mypy.ini +.ruff.toml +.coveragerc +.editorconfig +.deprecated_files +.deprecated_files_ignore +.mandatory_files +.mandatory_files_ignore +.static_files +.static_files_ignore +.description.md +.design.md +.readme_template.md +.readme_generation.md diff --git a/.template/deprecated_files_ignore.txt b/.template/deprecated_files_ignore.txt new file mode 100644 index 00000000..b589813a --- /dev/null +++ b/.template/deprecated_files_ignore.txt @@ -0,0 +1,2 @@ +# Optional list of files which are actually deprecated in the template +# but are still allowed to be used in the current repository diff --git a/.template/mandatory_files.txt b/.template/mandatory_files.txt new file mode 100644 index 00000000..660a15ed --- /dev/null +++ b/.template/mandatory_files.txt @@ -0,0 +1,32 @@ +# List of all mandatory files and directories that +# have to exist in this repo. +# This list is similar to the `./static_files`, +# however, the entries here are just checked for +# existence, their content is not evaluated and +# may differ from that of the template repository. + +.devcontainer/dev_launcher +.devcontainer/docker-compose.yml + +tests/__init__.py +tests/fixtures/__init__.py + +scripts/script_utils/fastapi_app_location.py + +.readme_generation/description.md +.readme_generation/design.md + +.pyproject_generation/pyproject_custom.toml + +lock/requirements-dev.in +lock/requirements-dev.txt +lock/requirements.txt + +Dockerfile +config_schema.json +example_config.yaml +LICENSE +pyproject.toml +README.md + +.pre-commit-config.yaml diff --git a/.template/mandatory_files_ignore.txt b/.template/mandatory_files_ignore.txt new file mode 100644 index 00000000..b8a7ee6e --- /dev/null +++ b/.template/mandatory_files_ignore.txt @@ -0,0 +1,2 @@ +# Optional list of files which are actually mandatory in the template +# but are allowed to be removed in the current repository diff --git a/.template/static_files.txt b/.template/static_files.txt new file mode 100644 index 00000000..46e46bf6 --- /dev/null +++ b/.template/static_files.txt @@ -0,0 +1,59 @@ +# List of all files that are considered static. +# They should never be changed in a service repo +# directly. +# If changes are needed, please first make them in +# the microservice_template_repository at: +# https://github.com/ghga-de/microservice-repository-template +# You pull the updates from the template repository to +# your repo by running the script at: +# ./scripts/update_static_files.py + +.devcontainer/dev_install +.devcontainer/license_header.txt +.devcontainer/Dockerfile +.devcontainer/devcontainer.json + +scripts/script_utils/__init__.py +scripts/script_utils/cli.py + +scripts/__init__.py +scripts/update_all.py +scripts/license_checker.py +scripts/get_package_name.py +scripts/update_config_docs.py +scripts/update_template_files.py +scripts/update_openapi_docs.py +scripts/update_readme.py +scripts/update_lock.py +scripts/update_hook_revs.py +scripts/update_pyproject.py +scripts/list_outdated_dependencies.py +scripts/README.md + +.github/workflows/check_config_docs.yaml +.github/workflows/check_openapi_spec.yaml +.github/workflows/check_readme.yaml +.github/workflows/check_pyproject.yaml +.github/workflows/check_template_files.yaml +.github/workflows/ci_release.yaml +.github/workflows/ci_workflow_dispatch.yaml +.github/workflows/static_code_analysis.yaml +.github/workflows/tests.yaml + +example_data/README.md + +.template/README.md + +.readme_generation/readme_template.md +.readme_generation/README.md + +.pyproject_generation/pyproject_template.toml +.pyproject_generation/README.md + +lock/requirements-dev-template.in +lock/README.md + +.gitattributes +.gitignore + +LICENSE diff --git a/.template/static_files_ignore.txt b/.template/static_files_ignore.txt new file mode 100644 index 00000000..8f374ce0 --- /dev/null +++ b/.template/static_files_ignore.txt @@ -0,0 +1,2 @@ +# Optional list of files which are actually static in the template +# but are allowed to have different content in the current repository diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..4f9f3939 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,48 @@ +# Copyright 2021 - 2023 Universität Tübingen, DKFZ, EMBL, and Universität zu Köln +# for the German Human Genome-Phenome Archive (GHGA) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +## creating building container +FROM python:3.10.9-slim-bullseye AS builder +# update and install dependencies +RUN apt update +RUN apt upgrade -y +RUN pip install build +# copy code +COPY . /service +WORKDIR /service +# build wheel +RUN python -m build + +# creating running container +FROM python:3.10.9-slim-bullseye +# update and install dependencies +RUN apt update +RUN apt upgrade -y +# copy and install requirements and wheel +WORKDIR /service +COPY --from=builder /service/lock/requirements.txt /service +RUN pip install --no-deps -r requirements.txt +RUN rm requirements.txt +COPY --from=builder /service/dist/ /service +RUN pip install --no-deps *.whl +RUN rm *.whl +# create new user and execute as that user +RUN useradd --create-home appuser +WORKDIR /home/appuser +USER appuser +# set environment +ENV PYTHONUNBUFFERED=1 +# Please adapt to package name: +ENTRYPOINT ["my-microservice"] diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..ba3d39f3 --- /dev/null +++ b/LICENSE @@ -0,0 +1,202 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2021 - 2023 Universität Tübingen, DKFZ, EMBL, and Universität zu Köln + for the German Human Genome-Phenome Archive (GHGA) + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md new file mode 100644 index 00000000..f71e139f --- /dev/null +++ b/README.md @@ -0,0 +1,294 @@ +# Microservice Repository Template + +This is a template for GitHub repositories containing one Python-based microservice (optimal for a multirepository setup). + +It features: + +- *Continuous Templation* - A continuous update-delivery mechanism for templated repositories +- A [devcontainer](https://containers.dev/)-based fully-configured development environment for vscode +- Tight linting and formatting using [Ruff](https://docs.astral.sh/ruff/) +- Static type checking using [mypy](https://www.mypy-lang.org/) +- Security scanning using [bandit](https://bandit.readthedocs.io/en/latest/) +- A structure for automated tests using [pytest](https://docs.pytest.org/en/7.4.x/) +- Dependency locking using [pip-tools](https://github.com/jazzband/pip-tools) +- Git hooks checking linting and formatting before committing using [pre-commit](https://pre-commit.com/) +- Automatic container-building and publishing to [Docker Hub](https://hub.docker.com/) +- GitHub Actions for automating or checking all of the above + +It is worth emphasizing the first point, this template is not just a one-time kickstart for your project +but repositories created using this template will continue receiving updates as the template evolves. +For further details, please look at the explanation in [.template/README.md](/.template/README.md). + +Please also refer to [.readme_generation/README.md](/.readme_generation/README.md) for details on how +to adapt this readme. + +Here the intro to the template stops and the actual template for the readme of the microservice starts: + +--- +[![tests](https://github.com/ghga-de/microservice-repository-template/actions/workflows/tests.yaml/badge.svg)](https://github.com/ghga-de/microservice-repository-template/actions/workflows/tests.yaml) +[![Coverage Status](https://coveralls.io/repos/github/ghga-de/microservice-repository-template/badge.svg?branch=main)](https://coveralls.io/github/ghga-de/microservice-repository-template?branch=main) + +# My Microservice + +My-Microservice - a short description + +## Description + + + +Here you should provide a short summary of the purpose of this microservice. + + +## Installation + +We recommend using the provided Docker container. + +A pre-build version is available at [docker hub](https://hub.docker.com/repository/docker/ghga/my-microservice): +```bash +docker pull ghga/my-microservice:0.1.0 +``` + +Or you can build the container yourself from the [`./Dockerfile`](./Dockerfile): +```bash +# Execute in the repo's root dir: +docker build -t ghga/my-microservice:0.1.0 . +``` + +For production-ready deployment, we recommend using Kubernetes, however, +for simple use cases, you could execute the service using docker +on a single server: +```bash +# The entrypoint is preconfigured: +docker run -p 8080:8080 ghga/my-microservice:0.1.0 --help +``` + +If you prefer not to use containers, you may install the service from source: +```bash +# Execute in the repo's root dir: +pip install . + +# To run the service: +my_microservice --help +``` + +## Configuration + +### Parameters + +The service requires the following configuration parameters: +- **`log_level`** *(string)*: The minimum log level to capture. Must be one of: `["CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG", "TRACE"]`. Default: `"INFO"`. + +- **`service_name`** *(string)*: Short name of this service. Default: `"my_microservice"`. + +- **`service_instance_id`** *(string)*: A string that uniquely identifies this instance across all instances of this service. This is included in log messages. + + + Examples: + + ```json + "germany-bw-instance-001" + ``` + + +- **`log_format`**: If set, will replace JSON formatting with the specified string format. If not set, has no effect. In addition to the standard attributes, the following can also be specified: timestamp, service, instance, level, correlation_id, and details. Default: `null`. + + - **Any of** + + - *string* + + - *null* + + + Examples: + + ```json + "%(timestamp)s - %(service)s - %(level)s - %(message)s" + ``` + + + ```json + "%(asctime)s - Severity: %(levelno)s - %(msg)s" + ``` + + +- **`host`** *(string)*: IP of the host. Default: `"127.0.0.1"`. + +- **`port`** *(integer)*: Port to expose the server on the specified host. Default: `8080`. + +- **`auto_reload`** *(boolean)*: A development feature. Set to `True` to automatically reload the server upon code changes. Default: `false`. + +- **`workers`** *(integer)*: Number of workers processes to run. Default: `1`. + +- **`api_root_path`** *(string)*: Root path at which the API is reachable. This is relative to the specified host and port. Default: `""`. + +- **`openapi_url`** *(string)*: Path to get the openapi specification in JSON format. This is relative to the specified host and port. Default: `"/openapi.json"`. + +- **`docs_url`** *(string)*: Path to host the swagger documentation. This is relative to the specified host and port. Default: `"/docs"`. + +- **`cors_allowed_origins`**: A list of origins that should be permitted to make cross-origin requests. By default, cross-origin requests are not allowed. You can use ['*'] to allow any origin. Default: `null`. + + - **Any of** + + - *array* + + - **Items** *(string)* + + - *null* + + + Examples: + + ```json + [ + "https://example.org", + "https://www.example.org" + ] + ``` + + +- **`cors_allow_credentials`**: Indicate that cookies should be supported for cross-origin requests. Defaults to False. Also, cors_allowed_origins cannot be set to ['*'] for credentials to be allowed. The origins must be explicitly specified. Default: `null`. + + - **Any of** + + - *boolean* + + - *null* + + + Examples: + + ```json + [ + "https://example.org", + "https://www.example.org" + ] + ``` + + +- **`cors_allowed_methods`**: A list of HTTP methods that should be allowed for cross-origin requests. Defaults to ['GET']. You can use ['*'] to allow all standard methods. Default: `null`. + + - **Any of** + + - *array* + + - **Items** *(string)* + + - *null* + + + Examples: + + ```json + [ + "*" + ] + ``` + + +- **`cors_allowed_headers`**: A list of HTTP request headers that should be supported for cross-origin requests. Defaults to []. You can use ['*'] to allow all headers. The Accept, Accept-Language, Content-Language and Content-Type headers are always allowed for CORS requests. Default: `null`. + + - **Any of** + + - *array* + + - **Items** *(string)* + + - *null* + + + Examples: + + ```json + [] + ``` + + +- **`generate_correlation_id`** *(boolean)*: A flag, which, if False, will result in an error when inbound requests don't possess a correlation ID. If True, requests without a correlation ID will be assigned a newly generated ID in the correlation ID middleware function. Default: `true`. + + + Examples: + + ```json + true + ``` + + + ```json + false + ``` + + +- **`language`** *(string)*: The language. Must be one of: `["Greek", "Croatian", "French", "German"]`. Default: `"Croatian"`. + + +### Usage: + +A template YAML for configurating the service can be found at +[`./example-config.yaml`](./example-config.yaml). +Please adapt it, rename it to `.my_microservice.yaml`, and place it into one of the following locations: +- in the current working directory were you are execute the service (on unix: `./.my_microservice.yaml`) +- in your home directory (on unix: `~/.my_microservice.yaml`) + +The config yaml will be automatically parsed by the service. + +**Important: If you are using containers, the locations refer to paths within the container.** + +All parameters mentioned in the [`./example-config.yaml`](./example-config.yaml) +could also be set using environment variables or file secrets. + +For naming the environment variables, just prefix the parameter name with `my_microservice_`, +e.g. for the `host` set an environment variable named `my_microservice_host` +(you may use both upper or lower cases, however, it is standard to define all env +variables in upper cases). + +To using file secrets please refer to the +[corresponding section](https://pydantic-docs.helpmanual.io/usage/settings/#secret-support) +of the pydantic documentation. + +## HTTP API +An OpenAPI specification for this service can be found [here](./openapi.yaml). + +## Architecture and Design: + + +This is a Python-based service following the Triple Hexagonal Architecture pattern. +It uses protocol/provider pairs and dependency injection mechanisms provided by the +[hexkit](https://github.com/ghga-de/hexkit) library. + + +## Development + +For setting up the development environment, we rely on the +[devcontainer feature](https://code.visualstudio.com/docs/remote/containers) of VS Code +in combination with Docker Compose. + +To use it, you have to have Docker Compose as well as VS Code with its "Remote - Containers" +extension (`ms-vscode-remote.remote-containers`) installed. +Then open this repository in VS Code and run the command +`Remote-Containers: Reopen in Container` from the VS Code "Command Palette". + +This will give you a full-fledged, pre-configured development environment including: +- infrastructural dependencies of the service (databases, etc.) +- all relevant VS Code extensions pre-installed +- pre-configured linting and auto-formatting +- a pre-configured debugger +- automatic license-header insertion + +Moreover, inside the devcontainer, a convenience commands `dev_install` is available. +It installs the service with all development dependencies, installs pre-commit. + +The installation is performed automatically when you build the devcontainer. However, +if you update dependencies in the [`./pyproject.toml`](./pyproject.toml) or the +[`./requirements-dev.txt`](./requirements-dev.txt), please run it again. + +## License + +This repository is free to use and modify according to the +[Apache 2.0 License](./LICENSE). + +## README Generation + +This README file is auto-generated, please see [`readme_generation.md`](./readme_generation.md) +for details. diff --git a/config_schema.json b/config_schema.json new file mode 100644 index 00000000..91a9d760 --- /dev/null +++ b/config_schema.json @@ -0,0 +1,201 @@ +{ + "additionalProperties": false, + "description": "Modifies the orginal Settings class provided by the user", + "properties": { + "log_level": { + "default": "INFO", + "description": "The minimum log level to capture.", + "enum": [ + "CRITICAL", + "ERROR", + "WARNING", + "INFO", + "DEBUG", + "TRACE" + ], + "title": "Log Level", + "type": "string" + }, + "service_name": { + "default": "my_microservice", + "description": "Short name of this service", + "title": "Service Name", + "type": "string" + }, + "service_instance_id": { + "description": "A string that uniquely identifies this instance across all instances of this service. This is included in log messages.", + "examples": [ + "germany-bw-instance-001" + ], + "title": "Service Instance Id", + "type": "string" + }, + "log_format": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "If set, will replace JSON formatting with the specified string format. If not set, has no effect. In addition to the standard attributes, the following can also be specified: timestamp, service, instance, level, correlation_id, and details", + "examples": [ + "%(timestamp)s - %(service)s - %(level)s - %(message)s", + "%(asctime)s - Severity: %(levelno)s - %(msg)s" + ], + "title": "Log Format" + }, + "host": { + "default": "127.0.0.1", + "description": "IP of the host.", + "title": "Host", + "type": "string" + }, + "port": { + "default": 8080, + "description": "Port to expose the server on the specified host", + "title": "Port", + "type": "integer" + }, + "auto_reload": { + "default": false, + "description": "A development feature. Set to `True` to automatically reload the server upon code changes", + "title": "Auto Reload", + "type": "boolean" + }, + "workers": { + "default": 1, + "description": "Number of workers processes to run.", + "title": "Workers", + "type": "integer" + }, + "api_root_path": { + "default": "", + "description": "Root path at which the API is reachable. This is relative to the specified host and port.", + "title": "Api Root Path", + "type": "string" + }, + "openapi_url": { + "default": "/openapi.json", + "description": "Path to get the openapi specification in JSON format. This is relative to the specified host and port.", + "title": "Openapi Url", + "type": "string" + }, + "docs_url": { + "default": "/docs", + "description": "Path to host the swagger documentation. This is relative to the specified host and port.", + "title": "Docs Url", + "type": "string" + }, + "cors_allowed_origins": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "description": "A list of origins that should be permitted to make cross-origin requests. By default, cross-origin requests are not allowed. You can use ['*'] to allow any origin.", + "examples": [ + [ + "https://example.org", + "https://www.example.org" + ] + ], + "title": "Cors Allowed Origins" + }, + "cors_allow_credentials": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Indicate that cookies should be supported for cross-origin requests. Defaults to False. Also, cors_allowed_origins cannot be set to ['*'] for credentials to be allowed. The origins must be explicitly specified.", + "examples": [ + [ + "https://example.org", + "https://www.example.org" + ] + ], + "title": "Cors Allow Credentials" + }, + "cors_allowed_methods": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "description": "A list of HTTP methods that should be allowed for cross-origin requests. Defaults to ['GET']. You can use ['*'] to allow all standard methods.", + "examples": [ + [ + "*" + ] + ], + "title": "Cors Allowed Methods" + }, + "cors_allowed_headers": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "description": "A list of HTTP request headers that should be supported for cross-origin requests. Defaults to []. You can use ['*'] to allow all headers. The Accept, Accept-Language, Content-Language and Content-Type headers are always allowed for CORS requests.", + "examples": [ + [] + ], + "title": "Cors Allowed Headers" + }, + "generate_correlation_id": { + "default": true, + "description": "A flag, which, if False, will result in an error when inbound requests don't possess a correlation ID. If True, requests without a correlation ID will be assigned a newly generated ID in the correlation ID middleware function.", + "examples": [ + true, + false + ], + "title": "Generate Correlation Id", + "type": "boolean" + }, + "language": { + "default": "Croatian", + "description": "The language.", + "enum": [ + "Greek", + "Croatian", + "French", + "German" + ], + "title": "Language", + "type": "string" + } + }, + "required": [ + "service_instance_id" + ], + "title": "ModSettings", + "type": "object" +} \ No newline at end of file diff --git a/example_config.yaml b/example_config.yaml new file mode 100644 index 00000000..fcc8c1a2 --- /dev/null +++ b/example_config.yaml @@ -0,0 +1,17 @@ +api_root_path: '' +auto_reload: false +cors_allow_credentials: null +cors_allowed_headers: null +cors_allowed_methods: null +cors_allowed_origins: null +docs_url: /docs +generate_correlation_id: true +host: 127.0.0.1 +language: Greek +log_format: null +log_level: INFO +openapi_url: /openapi.json +port: 8080 +service_instance_id: '1' +service_name: my_microservice +workers: 1 diff --git a/example_data/README.md b/example_data/README.md new file mode 100644 index 00000000..9223add8 --- /dev/null +++ b/example_data/README.md @@ -0,0 +1,4 @@ +# Example Data +This folder is may contain data (e.g. as json files) that can be used +during development or testing, to set the application to an initial +state, e.g. by populating the applications database with (realistic) data. diff --git a/lock/README.md b/lock/README.md new file mode 100644 index 00000000..fc255b55 --- /dev/null +++ b/lock/README.md @@ -0,0 +1,56 @@ + + +# Lock Files + +This directory contains two lock files locking the dependencies of this microservice: + +The [`./requirements.txt`](./requirements.txt) contains production dependencies. + +The [`./requirements-dev.txt`](./requirements-dev.txt) additionally contains development +dependencies. + +## Sources + +For generating the production lock file, only the dependencies specified in the +[`../pyproject.toml`](../pyproject.toml) are considered as input. + +For generating the development lock file, additionally, the +[`./requirements-dev-template.in`](./requirements-dev-template.in) as well as +the [`./requirements-dev.in`](./requirements-dev.in) are considered. + +The `./requirements-dev-template.in` is automatically updated from the template +repository and should not be manually modified. + +If you require additional dev dependencies not part of the +`./requirements-dev-template.in`, you can add them to the +`./requirements-dev.in`. + +## Update and Upgrade + +The lock files can be updated running the +[`../scripts/update_lock.py`](../scripts/update_lock.py) script. This will keep the +dependency versions in the lockfile unchanged unless they are in conflict with the +the input sources. In that case, the affected dependencies are updated to the latest +versions compatible with the input. + +If you would like to upgrade all dependencies in the lock file to the latest versions +compatible with the input, you can run `../scripts/update_lock.py --upgrade`. + +If you just want to check if the script would do update, you can run +`../scripts/update_lock.py --check`. diff --git a/lock/requirements-dev-template.in b/lock/requirements-dev-template.in new file mode 100644 index 00000000..b543188a --- /dev/null +++ b/lock/requirements-dev-template.in @@ -0,0 +1,34 @@ +# common requirements for development and testing of services + +pytest>=7.4.0 +pytest-asyncio>=0.23.0 +pytest-cov>=4.1.0 +pytest-profiling>=1.7.0 +snakeviz>=2.2.0 +logot>=1.2.0 + +pre-commit>=3.6.0 + +mypy>=1.8.0 +mypy-extensions>=1.0.0 + +ruff>=0.3.0 + +click>=8.1.0 +typer>=0.9.0 + +httpx>=0.27.0 +pytest-httpx>=0.30.0 + +urllib3>=1.26.18 +requests>=2.31.0 + +stringcase>=1.2.0 +jsonschema2md>=1.0.0 +setuptools>=69.1.0 + +# required since switch to pyproject.toml and pip-tools +tomli>=2.0.1 +tomli_w>=1.0.0 + +uv>=0.1.21 diff --git a/lock/requirements-dev.in b/lock/requirements-dev.in new file mode 100644 index 00000000..4d48a8fd --- /dev/null +++ b/lock/requirements-dev.in @@ -0,0 +1,7 @@ +# requirements for development and testing this service + +# template requirements for development and testing +-r requirements-dev-template.in + +# additional requirements can be listed here +testcontainers[kafka,mongo]>=3.4.1 diff --git a/lock/requirements-dev.txt b/lock/requirements-dev.txt new file mode 100644 index 00000000..d3bda813 --- /dev/null +++ b/lock/requirements-dev.txt @@ -0,0 +1,1180 @@ +# This file was autogenerated by uv via the following command: +# uv pip compile --refresh --generate-hashes --output-file /workspace/lock/requirements-dev.txt /tmp/tmp0timl1mn/pyproject.toml /workspace/lock/requirements-dev.in +aiokafka==0.8.1 \ + --hash=sha256:1e24839088fd6d3ff481cc09a48ea487b997328df11630bc0a1b88255edbcfe9 \ + --hash=sha256:1f43d2afd7d3e4407ada8d754895fad7c344ca00648a8a38418d76564eaaf6cd \ + --hash=sha256:1f6044ed270b946d31f265903b5eb101940ed0ff3a902eaf8178103c943bbcc9 \ + --hash=sha256:24373bb2d519abac036d5b04ebc43452ef4ad1916953b6678b9801a9c93ba237 \ + --hash=sha256:2fa54b8b068d9d8735cb6757a0f48168f8cf9be68860b0bae6b3ed1684cef49b \ + --hash=sha256:3816bcfc3c57dfa4ed77fe1dc3a9a464e17b6400061348155115f282c8150c47 \ + --hash=sha256:45cd28af6590d6a999bb706803166570121ba8a5a0d06c51ebd8a59fab53593c \ + --hash=sha256:4693fbe3c10f125bf3e2df8a8ccbca3eff2bdaaa6589d28c7532c10e7d84598b \ + --hash=sha256:4fccd599ab6b3fda4f4187d854b343f153b40d05d6774be9acf238618da50031 \ + --hash=sha256:6421ee81084532f915501074a132acb2afc8cb88bf5ddb11e584230a30f6f006 \ + --hash=sha256:673c163dee62dfe45146d5250af0e395da5cc92b63f8878c592abc7dc1862899 \ + --hash=sha256:7d327d66b41c4e3bafff7f9efb71936a08f940aa665680717e20862e4272a068 \ + --hash=sha256:7f09784322c0d2c4fcc222add4337a5ac394aa30a248eb4e0e4587a125573c75 \ + --hash=sha256:90960356513f3979754261b132b12a96b0d9e3c6eb44420e3a90a7c31156a81a \ + --hash=sha256:935da8c4da9a00a1e16020d88e578206097b4bb72ebc2a25fbd2cb817907ef28 \ + --hash=sha256:9f19d90b7360bc2239fcd8b147508ae39c3e5b1acfc8e6a2a9b0f306070f7ffe \ + --hash=sha256:a8a641a8102c51422afe111d4bc70c51f335f38fc5906e4c839bd17afeaf3cb2 \ + --hash=sha256:af6df9a41e08b61d7e62c0a416feeabd81bad76fa5c70d499b083d6af9ce72c3 \ + --hash=sha256:b2bf97548fa77ad31062ca580368d346b16ba9fdca5856c435f256f3699ab12b \ + --hash=sha256:bbffc431d9285328c0bc108949132ae11cec863f1dd5a43a1fc3d45a69ffb8a9 \ + --hash=sha256:bf7473c55dc7959d4b7f9d750fa6017b325813d6cb761e488c2d9ea44e922954 \ + --hash=sha256:c4332d37cb9d52181cfda4236566b4028c7c188549277f87bcc3027577d72b1b \ + --hash=sha256:d300188e358cd29989c817f6ee2a2965a039e5a71de8ade6f80f02ebb9bd07b8 \ + --hash=sha256:fd8f9e17bc9cd2ea664a7f5133aede39a8fffebffe0c450252d475dbdedb4a35 \ + --hash=sha256:ff318d29ecbeea8c58d69c91c24d48d7ed4a8d3e829b607e670d118a9a35d5ba + # via hexkit +annotated-types==0.6.0 \ + --hash=sha256:0641064de18ba7a25dee8f96403ebc39113d0cb953a01429249d5c7564666a43 \ + --hash=sha256:563339e807e53ffd9c267e99fc6d9ea23eb8443c08f112651963e24e22f84a5d + # via pydantic +anyio==4.3.0 \ + --hash=sha256:048e05d0f6caeed70d731f3db756d35dcc1f35747c8c403364a8332c630441b8 \ + --hash=sha256:f75253795a87df48568485fd18cdd2a3fa5c4f7c5be8e5e36637733fce06fed6 + # via + # httpx + # starlette + # watchfiles +async-timeout==4.0.3 \ + --hash=sha256:4640d96be84d82d02ed59ea2b7105a0f7b33abe8703703cd0ab0bf87c427522f \ + --hash=sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028 + # via aiokafka +attrs==23.2.0 \ + --hash=sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30 \ + --hash=sha256:99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1 + # via + # jsonschema + # referencing +boto3==1.34.63 \ + --hash=sha256:617174f9051b564a57fb1079186ad15db6519ab3bb0d1fb22cb54767b0c4f46e \ + --hash=sha256:e14f3866f6f372aadc5457ff9a052a45d9e2dd93466122ba86cdd5351cfeafaf + # via hexkit +botocore==1.34.63 \ + --hash=sha256:2237743fc3ed68319bc358b451e7c13a02110242b1522b839806fd64fcee45fb \ + --hash=sha256:8a6cbc3a5c5988725c00815f8f7f6baf81980b19d9a2ee414b031e726759dba9 + # via + # boto3 + # hexkit + # s3transfer +certifi==2024.2.2 \ + --hash=sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f \ + --hash=sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1 + # via + # httpcore + # httpx + # requests +cfgv==3.4.0 \ + --hash=sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9 \ + --hash=sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560 + # via pre-commit +charset-normalizer==3.3.2 \ + --hash=sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027 \ + --hash=sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087 \ + --hash=sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786 \ + --hash=sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8 \ + --hash=sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09 \ + --hash=sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185 \ + --hash=sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574 \ + --hash=sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e \ + --hash=sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519 \ + --hash=sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898 \ + --hash=sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269 \ + --hash=sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3 \ + --hash=sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f \ + --hash=sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6 \ + --hash=sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8 \ + --hash=sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a \ + --hash=sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73 \ + --hash=sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc \ + --hash=sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714 \ + --hash=sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2 \ + --hash=sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc \ + --hash=sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce \ + --hash=sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d \ + --hash=sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e \ + --hash=sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6 \ + --hash=sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269 \ + --hash=sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96 \ + --hash=sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d \ + --hash=sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a \ + --hash=sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4 \ + --hash=sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77 \ + --hash=sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d \ + --hash=sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0 \ + --hash=sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed \ + --hash=sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068 \ + --hash=sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac \ + --hash=sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25 \ + --hash=sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8 \ + --hash=sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab \ + --hash=sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26 \ + --hash=sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2 \ + --hash=sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db \ + --hash=sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f \ + --hash=sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5 \ + --hash=sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99 \ + --hash=sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c \ + --hash=sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d \ + --hash=sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811 \ + --hash=sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa \ + --hash=sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a \ + --hash=sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03 \ + --hash=sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b \ + --hash=sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04 \ + --hash=sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c \ + --hash=sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001 \ + --hash=sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458 \ + --hash=sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389 \ + --hash=sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99 \ + --hash=sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985 \ + --hash=sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537 \ + --hash=sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238 \ + --hash=sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f \ + --hash=sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d \ + --hash=sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796 \ + --hash=sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a \ + --hash=sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143 \ + --hash=sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8 \ + --hash=sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c \ + --hash=sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5 \ + --hash=sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5 \ + --hash=sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711 \ + --hash=sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4 \ + --hash=sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6 \ + --hash=sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c \ + --hash=sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7 \ + --hash=sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4 \ + --hash=sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b \ + --hash=sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae \ + --hash=sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12 \ + --hash=sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c \ + --hash=sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae \ + --hash=sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8 \ + --hash=sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887 \ + --hash=sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b \ + --hash=sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4 \ + --hash=sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f \ + --hash=sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5 \ + --hash=sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33 \ + --hash=sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519 \ + --hash=sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561 + # via requests +click==8.1.7 \ + --hash=sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28 \ + --hash=sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de + # via + # typer + # uvicorn +coverage==7.4.4 \ + --hash=sha256:00838a35b882694afda09f85e469c96367daa3f3f2b097d846a7216993d37f4c \ + --hash=sha256:0513b9508b93da4e1716744ef6ebc507aff016ba115ffe8ecff744d1322a7b63 \ + --hash=sha256:09c3255458533cb76ef55da8cc49ffab9e33f083739c8bd4f58e79fecfe288f7 \ + --hash=sha256:09ef9199ed6653989ebbcaacc9b62b514bb63ea2f90256e71fea3ed74bd8ff6f \ + --hash=sha256:09fa497a8ab37784fbb20ab699c246053ac294d13fc7eb40ec007a5043ec91f8 \ + --hash=sha256:0f9f50e7ef2a71e2fae92774c99170eb8304e3fdf9c8c3c7ae9bab3e7229c5cf \ + --hash=sha256:137eb07173141545e07403cca94ab625cc1cc6bc4c1e97b6e3846270e7e1fea0 \ + --hash=sha256:1f384c3cc76aeedce208643697fb3e8437604b512255de6d18dae3f27655a384 \ + --hash=sha256:201bef2eea65e0e9c56343115ba3814e896afe6d36ffd37bab783261db430f76 \ + --hash=sha256:38dd60d7bf242c4ed5b38e094baf6401faa114fc09e9e6632374388a404f98e7 \ + --hash=sha256:3b799445b9f7ee8bf299cfaed6f5b226c0037b74886a4e11515e569b36fe310d \ + --hash=sha256:3ea79bb50e805cd6ac058dfa3b5c8f6c040cb87fe83de10845857f5535d1db70 \ + --hash=sha256:40209e141059b9370a2657c9b15607815359ab3ef9918f0196b6fccce8d3230f \ + --hash=sha256:41c9c5f3de16b903b610d09650e5e27adbfa7f500302718c9ffd1c12cf9d6818 \ + --hash=sha256:54eb8d1bf7cacfbf2a3186019bcf01d11c666bd495ed18717162f7eb1e9dd00b \ + --hash=sha256:598825b51b81c808cb6f078dcb972f96af96b078faa47af7dfcdf282835baa8d \ + --hash=sha256:5fc1de20b2d4a061b3df27ab9b7c7111e9a710f10dc2b84d33a4ab25065994ec \ + --hash=sha256:623512f8ba53c422fcfb2ce68362c97945095b864cda94a92edbaf5994201083 \ + --hash=sha256:690db6517f09336559dc0b5f55342df62370a48f5469fabf502db2c6d1cffcd2 \ + --hash=sha256:69eb372f7e2ece89f14751fbcbe470295d73ed41ecd37ca36ed2eb47512a6ab9 \ + --hash=sha256:73bfb9c09951125d06ee473bed216e2c3742f530fc5acc1383883125de76d9cd \ + --hash=sha256:742a76a12aa45b44d236815d282b03cfb1de3b4323f3e4ec933acfae08e54ade \ + --hash=sha256:7c95949560050d04d46b919301826525597f07b33beba6187d04fa64d47ac82e \ + --hash=sha256:8130a2aa2acb8788e0b56938786c33c7c98562697bf9f4c7d6e8e5e3a0501e4a \ + --hash=sha256:8a2b2b78c78293782fd3767d53e6474582f62443d0504b1554370bde86cc8227 \ + --hash=sha256:8ce1415194b4a6bd0cdcc3a1dfbf58b63f910dcb7330fe15bdff542c56949f87 \ + --hash=sha256:9ca28a302acb19b6af89e90f33ee3e1906961f94b54ea37de6737b7ca9d8827c \ + --hash=sha256:a4cdc86d54b5da0df6d3d3a2f0b710949286094c3a6700c21e9015932b81447e \ + --hash=sha256:aa5b1c1bfc28384f1f53b69a023d789f72b2e0ab1b3787aae16992a7ca21056c \ + --hash=sha256:aadacf9a2f407a4688d700e4ebab33a7e2e408f2ca04dbf4aef17585389eff3e \ + --hash=sha256:ae71e7ddb7a413dd60052e90528f2f65270aad4b509563af6d03d53e979feafd \ + --hash=sha256:b14706df8b2de49869ae03a5ccbc211f4041750cd4a66f698df89d44f4bd30ec \ + --hash=sha256:b1a93009cb80730c9bca5d6d4665494b725b6e8e157c1cb7f2db5b4b122ea562 \ + --hash=sha256:b2991665420a803495e0b90a79233c1433d6ed77ef282e8e152a324bbbc5e0c8 \ + --hash=sha256:b2c5edc4ac10a7ef6605a966c58929ec6c1bd0917fb8c15cb3363f65aa40e677 \ + --hash=sha256:b4d33f418f46362995f1e9d4f3a35a1b6322cb959c31d88ae56b0298e1c22357 \ + --hash=sha256:b91cbc4b195444e7e258ba27ac33769c41b94967919f10037e6355e998af255c \ + --hash=sha256:c74880fc64d4958159fbd537a091d2a585448a8f8508bf248d72112723974cbd \ + --hash=sha256:c901df83d097649e257e803be22592aedfd5182f07b3cc87d640bbb9afd50f49 \ + --hash=sha256:cac99918c7bba15302a2d81f0312c08054a3359eaa1929c7e4b26ebe41e9b286 \ + --hash=sha256:cc4f1358cb0c78edef3ed237ef2c86056206bb8d9140e73b6b89fbcfcbdd40e1 \ + --hash=sha256:ccd341521be3d1b3daeb41960ae94a5e87abe2f46f17224ba5d6f2b8398016cf \ + --hash=sha256:ce4b94265ca988c3f8e479e741693d143026632672e3ff924f25fab50518dd51 \ + --hash=sha256:cf271892d13e43bc2b51e6908ec9a6a5094a4df1d8af0bfc360088ee6c684409 \ + --hash=sha256:d5ae728ff3b5401cc320d792866987e7e7e880e6ebd24433b70a33b643bb0384 \ + --hash=sha256:d71eec7d83298f1af3326ce0ff1d0ea83c7cb98f72b577097f9083b20bdaf05e \ + --hash=sha256:d898fe162d26929b5960e4e138651f7427048e72c853607f2b200909794ed978 \ + --hash=sha256:d89d7b2974cae412400e88f35d86af72208e1ede1a541954af5d944a8ba46c57 \ + --hash=sha256:dfa8fe35a0bb90382837b238fff375de15f0dcdb9ae68ff85f7a63649c98527e \ + --hash=sha256:e0be5efd5127542ef31f165de269f77560d6cdef525fffa446de6f7e9186cfb2 \ + --hash=sha256:fdfafb32984684eb03c2d83e1e51f64f0906b11e64482df3c5db936ce3839d48 \ + --hash=sha256:ff7687ca3d7028d8a5f0ebae95a6e4827c5616b31a4ee1192bdfde697db110d4 + # via pytest-cov +distlib==0.3.8 \ + --hash=sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784 \ + --hash=sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64 + # via virtualenv +dnspython==2.6.1 \ + --hash=sha256:5ef3b9680161f6fa89daf8ad451b5f1a33b18ae8a1c6778cdf4b43f08c0a6e50 \ + --hash=sha256:e8f0f9c23a7b7cb99ded64e6c3a6f3e701d78f50c55e002b839dea7225cff7cc + # via + # email-validator + # pymongo +docker==7.0.0 \ + --hash=sha256:12ba681f2777a0ad28ffbcc846a69c31b4dfd9752b47eb425a274ee269c5e14b \ + --hash=sha256:323736fb92cd9418fc5e7133bc953e11a9da04f4483f828b527db553f1e7e5a3 + # via testcontainers +email-validator==2.1.1 \ + --hash=sha256:200a70680ba08904be6d1eef729205cc0d687634399a5924d842533efb824b84 \ + --hash=sha256:97d882d174e2a65732fb43bfce81a3a834cbc1bde8bf419e30ef5ea976370a05 + # via pydantic +exceptiongroup==1.2.0 \ + --hash=sha256:4bfd3996ac73b41e9b9628b04e079f193850720ea5945fc96a08633c66912f14 \ + --hash=sha256:91f5c769735f051a4290d52edd0858999b57e5876e9f85937691bd4c9fa3ed68 + # via + # anyio + # pytest +fastapi==0.110.0 \ + --hash=sha256:266775f0dcc95af9d3ef39bad55cff525329a931d5fd51930aadd4f428bf7ff3 \ + --hash=sha256:87a1f6fb632a218222c5984be540055346a8f5d8a68e8f6fb647b1dc9934de4b + # via ghga-service-commons +filelock==3.13.1 \ + --hash=sha256:521f5f56c50f8426f5e03ad3b281b490a87ef15bc6c526f168290f0c7148d44e \ + --hash=sha256:57dbda9b35157b05fb3e58ee91448612eb674172fab98ee235ccb0b5bee19a1c + # via virtualenv +ghga-event-schemas==3.1.0 \ + --hash=sha256:87706784895376314124d30a0ba77dd7cfebdbfbcbb98e88d2a836486f11c385 \ + --hash=sha256:fa0048eda36002e7a79bc9084d2acdcc9eb9d38bcf263d6f68ad6fc453cae130 +ghga-service-commons==3.1.1 \ + --hash=sha256:6f758721a2673ac9c594d9cd052e0204b79950bf9f3e828ca5d870a6637d81b6 \ + --hash=sha256:b4e696c350f14a983d9447c43b16a954f0d2bf0a057c3ecdf0a95e3f115c89b4 +gprof2dot==2022.7.29 \ + --hash=sha256:45b4d298bd36608fccf9511c3fd88a773f7a1abc04d6cd39445b11ba43133ec5 \ + --hash=sha256:f165b3851d3c52ee4915eb1bd6cca571e5759823c2cd0f71a79bda93c2dc85d6 + # via pytest-profiling +h11==0.14.0 \ + --hash=sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d \ + --hash=sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761 + # via + # httpcore + # uvicorn +hexkit==2.1.1 \ + --hash=sha256:1f0a0e20a6d56fe4fa5e0b1c798df4720d2f84e20cbe7f16464bd5107e109c90 \ + --hash=sha256:3ec0f9690eb573125e22bce0c662019c9708cd17f8d2353396dc4496577718c3 + # via ghga-service-commons +httpcore==1.0.4 \ + --hash=sha256:ac418c1db41bade2ad53ae2f3834a3a0f5ae76b56cf5aa497d2d033384fc7d73 \ + --hash=sha256:cb2839ccfcba0d2d3c1131d3c3e26dfc327326fbe7a5dc0dbfe9f6c9151bb022 + # via httpx +httptools==0.6.1 \ + --hash=sha256:00d5d4b68a717765b1fabfd9ca755bd12bf44105eeb806c03d1962acd9b8e563 \ + --hash=sha256:0ac5a0ae3d9f4fe004318d64b8a854edd85ab76cffbf7ef5e32920faef62f142 \ + --hash=sha256:0cf2372e98406efb42e93bfe10f2948e467edfd792b015f1b4ecd897903d3e8d \ + --hash=sha256:1ed99a373e327f0107cb513b61820102ee4f3675656a37a50083eda05dc9541b \ + --hash=sha256:3c3b214ce057c54675b00108ac42bacf2ab8f85c58e3f324a4e963bbc46424f4 \ + --hash=sha256:3e802e0b2378ade99cd666b5bffb8b2a7cc8f3d28988685dc300469ea8dd86cb \ + --hash=sha256:3f30d3ce413088a98b9db71c60a6ada2001a08945cb42dd65a9a9fe228627658 \ + --hash=sha256:405784577ba6540fa7d6ff49e37daf104e04f4b4ff2d1ac0469eaa6a20fde084 \ + --hash=sha256:48ed8129cd9a0d62cf4d1575fcf90fb37e3ff7d5654d3a5814eb3d55f36478c2 \ + --hash=sha256:4bd3e488b447046e386a30f07af05f9b38d3d368d1f7b4d8f7e10af85393db97 \ + --hash=sha256:4f0f8271c0a4db459f9dc807acd0eadd4839934a4b9b892f6f160e94da309837 \ + --hash=sha256:5cceac09f164bcba55c0500a18fe3c47df29b62353198e4f37bbcc5d591172c3 \ + --hash=sha256:639dc4f381a870c9ec860ce5c45921db50205a37cc3334e756269736ff0aac58 \ + --hash=sha256:678fcbae74477a17d103b7cae78b74800d795d702083867ce160fc202104d0da \ + --hash=sha256:6a4f5ccead6d18ec072ac0b84420e95d27c1cdf5c9f1bc8fbd8daf86bd94f43d \ + --hash=sha256:6f58e335a1402fb5a650e271e8c2d03cfa7cea46ae124649346d17bd30d59c90 \ + --hash=sha256:75c8022dca7935cba14741a42744eee13ba05db00b27a4b940f0d646bd4d56d0 \ + --hash=sha256:7a7ea483c1a4485c71cb5f38be9db078f8b0e8b4c4dc0210f531cdd2ddac1ef1 \ + --hash=sha256:7d9ceb2c957320def533671fc9c715a80c47025139c8d1f3797477decbc6edd2 \ + --hash=sha256:7ebaec1bf683e4bf5e9fbb49b8cc36da482033596a415b3e4ebab5a4c0d7ec5e \ + --hash=sha256:85ed077c995e942b6f1b07583e4eb0a8d324d418954fc6af913d36db7c05a5a0 \ + --hash=sha256:8ae5b97f690badd2ca27cbf668494ee1b6d34cf1c464271ef7bfa9ca6b83ffaf \ + --hash=sha256:8b0bb634338334385351a1600a73e558ce619af390c2b38386206ac6a27fecfc \ + --hash=sha256:8e216a038d2d52ea13fdd9b9c9c7459fb80d78302b257828285eca1c773b99b3 \ + --hash=sha256:93ad80d7176aa5788902f207a4e79885f0576134695dfb0fefc15b7a4648d503 \ + --hash=sha256:95658c342529bba4e1d3d2b1a874db16c7cca435e8827422154c9da76ac4e13a \ + --hash=sha256:95fb92dd3649f9cb139e9c56604cc2d7c7bf0fc2e7c8d7fbd58f96e35eddd2a3 \ + --hash=sha256:97662ce7fb196c785344d00d638fc9ad69e18ee4bfb4000b35a52efe5adcc949 \ + --hash=sha256:9bb68d3a085c2174c2477eb3ffe84ae9fb4fde8792edb7bcd09a1d8467e30a84 \ + --hash=sha256:b512aa728bc02354e5ac086ce76c3ce635b62f5fbc32ab7082b5e582d27867bb \ + --hash=sha256:c6e26c30455600b95d94b1b836085138e82f177351454ee841c148f93a9bad5a \ + --hash=sha256:d2f6c3c4cb1948d912538217838f6e9960bc4a521d7f9b323b3da579cd14532f \ + --hash=sha256:dcbab042cc3ef272adc11220517278519adf8f53fd3056d0e68f0a6f891ba94e \ + --hash=sha256:e0b281cf5a125c35f7f6722b65d8542d2e57331be573e9e88bc8b0115c4a7a81 \ + --hash=sha256:e57997ac7fb7ee43140cc03664de5f268813a481dff6245e0075925adc6aa185 \ + --hash=sha256:fe467eb086d80217b7584e61313ebadc8d187a4d95bb62031b7bab4b205c3ba3 + # via uvicorn +httpx==0.27.0 \ + --hash=sha256:71d5465162c13681bff01ad59b2cc68dd838ea1f10e51574bac27103f00c91a5 \ + --hash=sha256:a0cb88a46f32dc874e04ee956e4c2764aba2aa228f650b06788ba6bda2962ab5 + # via pytest-httpx +identify==2.5.35 \ + --hash=sha256:10a7ca245cfcd756a554a7288159f72ff105ad233c7c4b9c6f0f4d108f5f6791 \ + --hash=sha256:c4de0081837b211594f8e877a6b4fad7ca32bbfc1a9307fdd61c28bfe923f13e + # via pre-commit +idna==3.6 \ + --hash=sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca \ + --hash=sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f + # via + # anyio + # email-validator + # httpx + # requests +iniconfig==2.0.0 \ + --hash=sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3 \ + --hash=sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374 + # via pytest +jmespath==1.0.1 \ + --hash=sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980 \ + --hash=sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe + # via + # boto3 + # botocore +jsonschema==4.21.1 \ + --hash=sha256:7996507afae316306f9e2290407761157c6f78002dcf7419acb99822143d1c6f \ + --hash=sha256:85727c00279f5fa6bedbe6238d2aa6403bedd8b4864ab11207d07df3cc1b2ee5 + # via + # ghga-event-schemas + # hexkit +jsonschema-specifications==2023.12.1 \ + --hash=sha256:48a76787b3e70f5ed53f1160d2b81f586e4ca6d1548c5de7085d1682674764cc \ + --hash=sha256:87e4fdf3a94858b8a2ba2778d9ba57d8a9cafca7c7489c46ba0d30a8bc6a9c3c + # via jsonschema +jsonschema2md==1.1.0 \ + --hash=sha256:2386fc4d119330686db3989ea497ab96a4defb6388386fc0ceff756b5c1a66a7 \ + --hash=sha256:e89edf2de1bc7fc3e842915c7c29b7b70888555a87002eccc06350c0412a1458 +kafka-python==2.0.2 \ + --hash=sha256:04dfe7fea2b63726cd6f3e79a2d86e709d608d74406638c5da33a01d45a9d7e3 \ + --hash=sha256:2d92418c7cb1c298fa6c7f0fb3519b520d0d7526ac6cb7ae2a4fc65a51a94b6e + # via + # aiokafka + # testcontainers +logot==1.2.0 \ + --hash=sha256:e4972cc1569322ed6d1e25e8c4507c3d9ebb4fdd93058c420f800bf223bc5f90 \ + --hash=sha256:ed994e50e30fed2378965a859def1eff1c62d97255479196de33f6de7cec2866 +motor==3.3.2 \ + --hash=sha256:6fe7e6f0c4f430b9e030b9d22549b732f7c2226af3ab71ecc309e4a1b7d19953 \ + --hash=sha256:d2fc38de15f1c8058f389c1a44a4d4105c0405c48c061cd492a654496f7bc26a + # via hexkit +mypy==1.9.0 \ + --hash=sha256:0235391f1c6f6ce487b23b9dbd1327b4ec33bb93934aa986efe8a9563d9349e6 \ + --hash=sha256:190da1ee69b427d7efa8aa0d5e5ccd67a4fb04038c380237a0d96829cb157913 \ + --hash=sha256:2418488264eb41f69cc64a69a745fad4a8f86649af4b1041a4c64ee61fc61129 \ + --hash=sha256:3a3c007ff3ee90f69cf0a15cbcdf0995749569b86b6d2f327af01fd1b8aee9dc \ + --hash=sha256:3cc5da0127e6a478cddd906068496a97a7618a21ce9b54bde5bf7e539c7af974 \ + --hash=sha256:48533cdd345c3c2e5ef48ba3b0d3880b257b423e7995dada04248725c6f77374 \ + --hash=sha256:49c87c15aed320de9b438ae7b00c1ac91cd393c1b854c2ce538e2a72d55df150 \ + --hash=sha256:4d3dbd346cfec7cb98e6cbb6e0f3c23618af826316188d587d1c1bc34f0ede03 \ + --hash=sha256:571741dc4194b4f82d344b15e8837e8c5fcc462d66d076748142327626a1b6e9 \ + --hash=sha256:587ce887f75dd9700252a3abbc9c97bbe165a4a630597845c61279cf32dfbf02 \ + --hash=sha256:5d741d3fc7c4da608764073089e5f58ef6352bedc223ff58f2f038c2c4698a89 \ + --hash=sha256:5e6061f44f2313b94f920e91b204ec600982961e07a17e0f6cd83371cb23f5c2 \ + --hash=sha256:61758fabd58ce4b0720ae1e2fea5cfd4431591d6d590b197775329264f86311d \ + --hash=sha256:653265f9a2784db65bfca694d1edd23093ce49740b2244cde583aeb134c008f3 \ + --hash=sha256:68edad3dc7d70f2f17ae4c6c1b9471a56138ca22722487eebacfd1eb5321d612 \ + --hash=sha256:81a10926e5473c5fc3da8abb04119a1f5811a236dc3a38d92015cb1e6ba4cb9e \ + --hash=sha256:85ca5fcc24f0b4aeedc1d02f93707bccc04733f21d41c88334c5482219b1ccb3 \ + --hash=sha256:a260627a570559181a9ea5de61ac6297aa5af202f06fd7ab093ce74e7181e43e \ + --hash=sha256:aceb1db093b04db5cd390821464504111b8ec3e351eb85afd1433490163d60cd \ + --hash=sha256:b685154e22e4e9199fc95f298661deea28aaede5ae16ccc8cbb1045e716b3e04 \ + --hash=sha256:d357423fa57a489e8c47b7c85dfb96698caba13d66e086b412298a1a0ea3b0ed \ + --hash=sha256:d4d5ddc13421ba3e2e082a6c2d74c2ddb3979c39b582dacd53dd5d9431237185 \ + --hash=sha256:e49499be624dead83927e70c756970a0bc8240e9f769389cdf5714b0784ca6bf \ + --hash=sha256:e54396d70be04b34f31d2edf3362c1edd023246c82f1730bbf8768c28db5361b \ + --hash=sha256:f88566144752999351725ac623471661c9d1cd8caa0134ff98cceeea181789f4 \ + --hash=sha256:f8a67616990062232ee4c3952f41c779afac41405806042a8126fe96e098419f \ + --hash=sha256:fe28657de3bfec596bbeef01cb219833ad9d38dd5393fc649f4b366840baefe6 +mypy-extensions==1.0.0 \ + --hash=sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d \ + --hash=sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782 + # via mypy +nodeenv==1.8.0 \ + --hash=sha256:d51e0c37e64fbf47d017feac3145cdbb58836d7eee8c6f6d3b6880c5456227d2 \ + --hash=sha256:df865724bb3c3adc86b3876fa209771517b0cfe596beff01a92700e0e8be4cec + # via pre-commit +packaging==24.0 \ + --hash=sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5 \ + --hash=sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9 + # via + # aiokafka + # docker + # pytest +platformdirs==4.2.0 \ + --hash=sha256:0614df2a2f37e1a662acbd8e2b25b92ccf8632929bc6d43467e17fe89c75e068 \ + --hash=sha256:ef0cc731df711022c174543cb70a9b5bd22e5a9337c8624ef2c2ceb8ddad8768 + # via virtualenv +pluggy==1.4.0 \ + --hash=sha256:7db9f7b503d67d1c5b95f59773ebb58a8c1c288129a88665838012cfb07b8981 \ + --hash=sha256:8c85c2876142a764e5b7548e7d9a0e0ddb46f5185161049a79b7e974454223be + # via pytest +pre-commit==3.6.2 \ + --hash=sha256:ba637c2d7a670c10daedc059f5c49b5bd0aadbccfcd7ec15592cf9665117532c \ + --hash=sha256:c3ef34f463045c88658c5b99f38c1e297abdcc0ff13f98d3370055fbbfabc67e +pydantic==2.6.4 \ + --hash=sha256:b1704e0847db01817624a6b86766967f552dd9dbf3afba4004409f908dcc84e6 \ + --hash=sha256:cc46fce86607580867bdc3361ad462bab9c222ef042d3da86f2fb333e1d916c5 + # via + # fastapi + # ghga-event-schemas + # ghga-service-commons + # hexkit + # pydantic-settings +pydantic-core==2.16.3 \ + --hash=sha256:00ee1c97b5364b84cb0bd82e9bbf645d5e2871fb8c58059d158412fee2d33d8a \ + --hash=sha256:0d32576b1de5a30d9a97f300cc6a3f4694c428d956adbc7e6e2f9cad279e45ed \ + --hash=sha256:0df446663464884297c793874573549229f9eca73b59360878f382a0fc085979 \ + --hash=sha256:0f56ae86b60ea987ae8bcd6654a887238fd53d1384f9b222ac457070b7ac4cff \ + --hash=sha256:13dcc4802961b5f843a9385fc821a0b0135e8c07fc3d9949fd49627c1a5e6ae5 \ + --hash=sha256:162e498303d2b1c036b957a1278fa0899d02b2842f1ff901b6395104c5554a45 \ + --hash=sha256:1b662180108c55dfbf1280d865b2d116633d436cfc0bba82323554873967b340 \ + --hash=sha256:1cac689f80a3abab2d3c0048b29eea5751114054f032a941a32de4c852c59cad \ + --hash=sha256:21b888c973e4f26b7a96491c0965a8a312e13be108022ee510248fe379a5fa23 \ + --hash=sha256:287073c66748f624be4cef893ef9174e3eb88fe0b8a78dc22e88eca4bc357ca6 \ + --hash=sha256:2a1ef6a36fdbf71538142ed604ad19b82f67b05749512e47f247a6ddd06afdc7 \ + --hash=sha256:2a72fb9963cba4cd5793854fd12f4cfee731e86df140f59ff52a49b3552db241 \ + --hash=sha256:2acca2be4bb2f2147ada8cac612f8a98fc09f41c89f87add7256ad27332c2fda \ + --hash=sha256:2f583bd01bbfbff4eaee0868e6fc607efdfcc2b03c1c766b06a707abbc856187 \ + --hash=sha256:33809aebac276089b78db106ee692bdc9044710e26f24a9a2eaa35a0f9fa70ba \ + --hash=sha256:36fa178aacbc277bc6b62a2c3da95226520da4f4e9e206fdf076484363895d2c \ + --hash=sha256:4204e773b4b408062960e65468d5346bdfe139247ee5f1ca2a378983e11388a2 \ + --hash=sha256:4384a8f68ddb31a0b0c3deae88765f5868a1b9148939c3f4121233314ad5532c \ + --hash=sha256:456855f57b413f077dff513a5a28ed838dbbb15082ba00f80750377eed23d132 \ + --hash=sha256:49d5d58abd4b83fb8ce763be7794d09b2f50f10aa65c0f0c1696c677edeb7cbf \ + --hash=sha256:4ac6b4ce1e7283d715c4b729d8f9dab9627586dafce81d9eaa009dd7f25dd972 \ + --hash=sha256:4df8a199d9f6afc5ae9a65f8f95ee52cae389a8c6b20163762bde0426275b7db \ + --hash=sha256:500960cb3a0543a724a81ba859da816e8cf01b0e6aaeedf2c3775d12ee49cade \ + --hash=sha256:519ae0312616026bf4cedc0fe459e982734f3ca82ee8c7246c19b650b60a5ee4 \ + --hash=sha256:578114bc803a4c1ff9946d977c221e4376620a46cf78da267d946397dc9514a8 \ + --hash=sha256:5c5cbc703168d1b7a838668998308018a2718c2130595e8e190220238addc96f \ + --hash=sha256:6162f8d2dc27ba21027f261e4fa26f8bcb3cf9784b7f9499466a311ac284b5b9 \ + --hash=sha256:704d35ecc7e9c31d48926150afada60401c55efa3b46cd1ded5a01bdffaf1d48 \ + --hash=sha256:716b542728d4c742353448765aa7cdaa519a7b82f9564130e2b3f6766018c9ec \ + --hash=sha256:72282ad4892a9fb2da25defeac8c2e84352c108705c972db82ab121d15f14e6d \ + --hash=sha256:7233d65d9d651242a68801159763d09e9ec96e8a158dbf118dc090cd77a104c9 \ + --hash=sha256:732da3243e1b8d3eab8c6ae23ae6a58548849d2e4a4e03a1924c8ddf71a387cb \ + --hash=sha256:75b81e678d1c1ede0785c7f46690621e4c6e63ccd9192af1f0bd9d504bbb6bf4 \ + --hash=sha256:75f76ee558751746d6a38f89d60b6228fa174e5172d143886af0f85aa306fd89 \ + --hash=sha256:7ee8d5f878dccb6d499ba4d30d757111847b6849ae07acdd1205fffa1fc1253c \ + --hash=sha256:7f752826b5b8361193df55afcdf8ca6a57d0232653494ba473630a83ba50d8c9 \ + --hash=sha256:86b3d0033580bd6bbe07590152007275bd7af95f98eaa5bd36f3da219dcd93da \ + --hash=sha256:8d62da299c6ecb04df729e4b5c52dc0d53f4f8430b4492b93aa8de1f541c4aac \ + --hash=sha256:8e47755d8152c1ab5b55928ab422a76e2e7b22b5ed8e90a7d584268dd49e9c6b \ + --hash=sha256:9091632a25b8b87b9a605ec0e61f241c456e9248bfdcf7abdf344fdb169c81cf \ + --hash=sha256:936e5db01dd49476fa8f4383c259b8b1303d5dd5fb34c97de194560698cc2c5e \ + --hash=sha256:99b6add4c0b39a513d323d3b93bc173dac663c27b99860dd5bf491b240d26137 \ + --hash=sha256:9c865a7ee6f93783bd5d781af5a4c43dadc37053a5b42f7d18dc019f8c9d2bd1 \ + --hash=sha256:a425479ee40ff021f8216c9d07a6a3b54b31c8267c6e17aa88b70d7ebd0e5e5b \ + --hash=sha256:a4b2bf78342c40b3dc830880106f54328928ff03e357935ad26c7128bbd66ce8 \ + --hash=sha256:a6b1bb0827f56654b4437955555dc3aeeebeddc47c2d7ed575477f082622c49e \ + --hash=sha256:aaf09e615a0bf98d406657e0008e4a8701b11481840be7d31755dc9f97c44053 \ + --hash=sha256:b1f6f5938d63c6139860f044e2538baeee6f0b251a1816e7adb6cbce106a1f01 \ + --hash=sha256:b29eeb887aa931c2fcef5aa515d9d176d25006794610c264ddc114c053bf96fe \ + --hash=sha256:b3992a322a5617ded0a9f23fd06dbc1e4bd7cf39bc4ccf344b10f80af58beacd \ + --hash=sha256:b5b6079cc452a7c53dd378c6f881ac528246b3ac9aae0f8eef98498a75657805 \ + --hash=sha256:b60cc1a081f80a2105a59385b92d82278b15d80ebb3adb200542ae165cd7d183 \ + --hash=sha256:b926dd38db1519ed3043a4de50214e0d600d404099c3392f098a7f9d75029ff8 \ + --hash=sha256:bd87f48924f360e5d1c5f770d6155ce0e7d83f7b4e10c2f9ec001c73cf475c99 \ + --hash=sha256:bda1ee3e08252b8d41fa5537413ffdddd58fa73107171a126d3b9ff001b9b820 \ + --hash=sha256:be0ec334369316fa73448cc8c982c01e5d2a81c95969d58b8f6e272884df0074 \ + --hash=sha256:c6119dc90483a5cb50a1306adb8d52c66e447da88ea44f323e0ae1a5fcb14256 \ + --hash=sha256:c9803edf8e29bd825f43481f19c37f50d2b01899448273b3a7758441b512acf8 \ + --hash=sha256:c9bd22a2a639e26171068f8ebb5400ce2c1bc7d17959f60a3b753ae13c632975 \ + --hash=sha256:cbcc558401de90a746d02ef330c528f2e668c83350f045833543cd57ecead1ad \ + --hash=sha256:cf6204fe865da605285c34cf1172879d0314ff267b1c35ff59de7154f35fdc2e \ + --hash=sha256:d33dd21f572545649f90c38c227cc8631268ba25c460b5569abebdd0ec5974ca \ + --hash=sha256:d89ca19cdd0dd5f31606a9329e309d4fcbb3df860960acec32630297d61820df \ + --hash=sha256:d8f99b147ff3fcf6b3cc60cb0c39ea443884d5559a30b1481e92495f2310ff2b \ + --hash=sha256:d937653a696465677ed583124b94a4b2d79f5e30b2c46115a68e482c6a591c8a \ + --hash=sha256:dcca5d2bf65c6fb591fff92da03f94cd4f315972f97c21975398bd4bd046854a \ + --hash=sha256:ded1c35f15c9dea16ead9bffcde9bb5c7c031bff076355dc58dcb1cb436c4721 \ + --hash=sha256:e3e70c94a0c3841e6aa831edab1619ad5c511199be94d0c11ba75fe06efe107a \ + --hash=sha256:e56f8186d6210ac7ece503193ec84104da7ceb98f68ce18c07282fcc2452e76f \ + --hash=sha256:e7774b570e61cb998490c5235740d475413a1f6de823169b4cf94e2fe9e9f6b2 \ + --hash=sha256:e7c6ed0dc9d8e65f24f5824291550139fe6f37fac03788d4580da0d33bc00c97 \ + --hash=sha256:ec08be75bb268473677edb83ba71e7e74b43c008e4a7b1907c6d57e940bf34b6 \ + --hash=sha256:ecdf6bf5f578615f2e985a5e1f6572e23aa632c4bd1dc67f8f406d445ac115ed \ + --hash=sha256:ed25e1835c00a332cb10c683cd39da96a719ab1dfc08427d476bce41b92531fc \ + --hash=sha256:f4cb85f693044e0f71f394ff76c98ddc1bc0953e48c061725e540396d5c8a2e1 \ + --hash=sha256:f53aace168a2a10582e570b7736cc5bef12cae9cf21775e3eafac597e8551fbe \ + --hash=sha256:f651dd19363c632f4abe3480a7c87a9773be27cfe1341aef06e8759599454120 \ + --hash=sha256:fc4ad7f7ee1a13d9cb49d8198cd7d7e3aa93e425f371a68235f784e99741561f \ + --hash=sha256:fee427241c2d9fb7192b658190f9f5fd6dfe41e02f3c1489d2ec1e6a5ab1e04a + # via pydantic +pydantic-settings==2.2.1 \ + --hash=sha256:00b9f6a5e95553590434c0fa01ead0b216c3e10bc54ae02e37f359948643c5ed \ + --hash=sha256:0235391d26db4d2190cb9b31051c4b46882d28a51533f97440867f012d4da091 + # via hexkit +pymongo==4.6.2 \ + --hash=sha256:097791d5a8d44e2444e0c8c4d6e14570ac11e22bcb833808885a5db081c3dc2a \ + --hash=sha256:0d002ae456a15b1d790a78bb84f87af21af1cb716a63efb2c446ab6bcbbc48ca \ + --hash=sha256:0fbdbf2fba1b4f5f1522e9f11e21c306e095b59a83340a69e908f8ed9b450070 \ + --hash=sha256:1849fd6f1917b4dc5dbf744b2f18e41e0538d08dd8e9ba9efa811c5149d665a3 \ + --hash=sha256:18c422e6b08fa370ed9d8670c67e78d01f50d6517cec4522aa8627014dfa38b6 \ + --hash=sha256:1f251f287e6d42daa3654b686ce1fcb6d74bf13b3907c3ae25954978c70f2cd4 \ + --hash=sha256:1f5f4cd2969197e25b67e24d5b8aa2452d381861d2791d06c493eaa0b9c9fcfe \ + --hash=sha256:1f706c1a644ed33eaea91df0a8fb687ce572b53eeb4ff9b89270cb0247e5d0e1 \ + --hash=sha256:2160d9c8cd20ce1f76a893f0daf7c0d38af093f36f1b5c9f3dcf3e08f7142814 \ + --hash=sha256:2b575fbe6396bbf21e4d0e5fd2e3cdb656dc90c930b6c5532192e9a89814f72d \ + --hash=sha256:2b65433c90e07dc252b4a55dfd885ca0df94b1cf77c5b8709953ec1983aadc03 \ + --hash=sha256:2f7b98f8d2cf3eeebde738d080ae9b4276d7250912d9751046a9ac1efc9b1ce2 \ + --hash=sha256:311794ef3ccae374aaef95792c36b0e5c06e8d5cf04a1bdb1b2bf14619ac881f \ + --hash=sha256:362a5adf6f3f938a8ff220a4c4aaa93e84ef932a409abecd837c617d17a5990f \ + --hash=sha256:397949a9cc85e4a1452f80b7f7f2175d557237177120954eff00bf79553e89d3 \ + --hash=sha256:3a5280f496297537301e78bde250c96fadf4945e7b2c397d8bb8921861dd236d \ + --hash=sha256:3e03c732cb64b96849310e1d8688fb70d75e2571385485bf2f1e7ad1d309fa53 \ + --hash=sha256:3e9f6e2f3da0a6af854a3e959a6962b5f8b43bbb8113cd0bff0421c5059b3106 \ + --hash=sha256:4522ad69a4ab0e1b46a8367d62ad3865b8cd54cf77518c157631dac1fdc97584 \ + --hash=sha256:477914e13501bb1d4608339ee5bb618be056d2d0e7267727623516cfa902e652 \ + --hash=sha256:4993593de44c741d1e9f230f221fe623179f500765f9855936e4ff6f33571bad \ + --hash=sha256:4d982c6db1da7cf3018183891883660ad085de97f21490d314385373f775915b \ + --hash=sha256:4e2129ec8f72806751b621470ac5d26aaa18fae4194796621508fa0e6068278a \ + --hash=sha256:4fa30494601a6271a8b416554bd7cde7b2a848230f0ec03e3f08d84565b4bf8c \ + --hash=sha256:5379ca6fd325387a34cda440aec2bd031b5ef0b0aa2e23b4981945cff1dab84c \ + --hash=sha256:579508536113dbd4c56e4738955a18847e8a6c41bf3c0b4ab18b51d81a6b7be8 \ + --hash=sha256:57c05f2e310701fc17ae358caafd99b1830014e316f0242d13ab6c01db0ab1c2 \ + --hash=sha256:5c2f258489de12a65b81e1b803a531ee8cf633fa416ae84de65cd5f82d2ceb37 \ + --hash=sha256:5db133d6ec7a4f7fc7e2bd098e4df23d7ad949f7be47b27b515c9fb9301c61e4 \ + --hash=sha256:5f6bcd2d012d82d25191a911a239fd05a8a72e8c5a7d81d056c0f3520cad14d1 \ + --hash=sha256:6125f73503407792c8b3f80165f8ab88a4e448d7d9234c762681a4d0b446fcb4 \ + --hash=sha256:64ec3e2dcab9af61bdbfcb1dd863c70d1b0c220b8e8ac11df8b57f80ee0402b3 \ + --hash=sha256:658f6c028edaeb02761ebcaca8d44d519c22594b2a51dcbc9bd2432aa93319e3 \ + --hash=sha256:68109c13176749fbbbbbdb94dd4a58dcc604db6ea43ee300b2602154aebdd55f \ + --hash=sha256:6ceaaff4b812ae368cf9774989dea81b9bbb71e5bed666feca6a9f3087c03e49 \ + --hash=sha256:707d28a822b918acf941cff590affaddb42a5d640614d71367c8956623a80cbc \ + --hash=sha256:7640d176ee5b0afec76a1bda3684995cb731b2af7fcfd7c7ef8dc271c5d689af \ + --hash=sha256:7dd63f7c2b3727541f7f37d0fb78d9942eb12a866180fbeb898714420aad74e2 \ + --hash=sha256:8110b78fc4b37dced85081d56795ecbee6a7937966e918e05e33a3900e8ea07d \ + --hash=sha256:84593447a5c5fe7a59ba86b72c2c89d813fbac71c07757acdf162fbfd5d005b9 \ + --hash=sha256:8caa73fb19070008e851a589b744aaa38edd1366e2487284c61158c77fdf72af \ + --hash=sha256:91ddf95cedca12f115fbc5f442b841e81197d85aa3cc30b82aee3635a5208af2 \ + --hash=sha256:94637941fe343000f728e28d3fe04f1f52aec6376b67b85583026ff8dab2a0e0 \ + --hash=sha256:97d81d357e1a2a248b3494d52ebc8bf15d223ee89d59ee63becc434e07438a24 \ + --hash=sha256:991e406db5da4d89fb220a94d8caaf974ffe14ce6b095957bae9273c609784a0 \ + --hash=sha256:9aebddb2ec2128d5fc2fe3aee6319afef8697e0374f8a1fcca3449d6f625e7b4 \ + --hash=sha256:9d511db310f43222bc58d811037b176b4b88dc2b4617478c5ef01fea404f8601 \ + --hash=sha256:9eec7140cf7513aa770ea51505d312000c7416626a828de24318fdcc9ac3214c \ + --hash=sha256:9f86ba0c781b497a3c9c886765d7b6402a0e3ae079dd517365044c89cd7abb06 \ + --hash=sha256:a509db602462eb736666989739215b4b7d8f4bb8ac31d0bffd4be9eae96c63ef \ + --hash=sha256:aaecfafb407feb6f562c7f2f5b91f22bfacba6dd739116b1912788cff7124c4a \ + --hash=sha256:ab7d01ac832a1663dad592ccbd92bb0f0775bc8f98a1923c5e1a7d7fead495af \ + --hash=sha256:ac20dd0c7b42555837c86f5ea46505f35af20a08b9cf5770cd1834288d8bd1b4 \ + --hash=sha256:b2d445f1cf147331947cc35ec10342f898329f29dd1947a3f8aeaf7e0e6878d1 \ + --hash=sha256:b2dd8c874927a27995f64a3b44c890e8a944c98dec1ba79eab50e07f1e3f801b \ + --hash=sha256:ba052446a14bd714ec83ca4e77d0d97904f33cd046d7bb60712a6be25eb31dbb \ + --hash=sha256:bea62f03a50f363265a7a651b4e2a4429b4f138c1864b2d83d4bf6f9851994be \ + --hash=sha256:bff601fbfcecd2166d9a2b70777c2985cb9689e2befb3278d91f7f93a0456cae \ + --hash=sha256:c3797e0a628534e07a36544d2bfa69e251a578c6d013e975e9e3ed2ac41f2d95 \ + --hash=sha256:c43205e85cbcbdf03cff62ad8f50426dd9d20134a915cfb626d805bab89a1844 \ + --hash=sha256:c68bf4a399e37798f1b5aa4f6c02886188ef465f4ac0b305a607b7579413e366 \ + --hash=sha256:c9519c9d341983f3a1bd19628fecb1d72a48d8666cf344549879f2e63f54463b \ + --hash=sha256:ca5877754f3fa6e4fe5aacf5c404575f04c2d9efc8d22ed39576ed9098d555c8 \ + --hash=sha256:d0257e0eebb50f242ca28a92ef195889a6ad03dcdde5bf1c7ab9f38b7e810801 \ + --hash=sha256:d788cb5cc947d78934be26eef1623c78cec3729dc93a30c23f049b361aa6d835 \ + --hash=sha256:d7d227a60b00925dd3aeae4675575af89c661a8e89a1f7d1677e57eba4a3693c \ + --hash=sha256:df813f0c2c02281720ccce225edf39dc37855bf72cdfde6f789a1d1cf32ffb4b \ + --hash=sha256:e0b208ebec3b47ee78a5c836e2e885e8c1e10f8ffd101aaec3d63997a4bdcd04 \ + --hash=sha256:e571434633f99a81e081738721bb38e697345281ed2f79c2f290f809ba3fbb2f \ + --hash=sha256:e78af59fd0eb262c2a5f7c7d7e3b95e8596a75480d31087ca5f02f2d4c6acd19 \ + --hash=sha256:e942945e9112075a84d2e2d6e0d0c98833cdcdfe48eb8952b917f996025c7ffa \ + --hash=sha256:ebd343ca44982d480f1e39372c48e8e263fc6f32e9af2be456298f146a3db715 \ + --hash=sha256:ed694c0d1977cb54281cb808bc2b247c17fb64b678a6352d3b77eb678ebe1bd9 \ + --hash=sha256:ee30a9d4c27a88042d0636aca0275788af09cc237ae365cd6ebb34524bddb9cc \ + --hash=sha256:f1febca6f79e91feafc572906871805bd9c271b6a2d98a8bb5499b6ace0befed \ + --hash=sha256:f251db26c239aec2a4d57fbe869e0a27b7f6b5384ec6bf54aeb4a6a5e7408234 \ + --hash=sha256:f3bae553ca39ed52db099d76acd5e8566096064dc7614c34c9359bb239ec4081 \ + --hash=sha256:f673b64a0884edcc56073bda0b363428dc1bf4eb1b5e7d0b689f7ec6173edad6 \ + --hash=sha256:fa0bbbfbd1f8ebbd5facaa10f9f333b20027b240af012748555148943616fdf3 \ + --hash=sha256:fb24abcd50501b25d33a074c1790a1389b6460d2509e4b240d03fd2e5c79f463 \ + --hash=sha256:fbafe3a1df21eeadb003c38fc02c1abf567648b6477ec50c4a3c042dca205371 \ + --hash=sha256:fe010154dfa9e428bd2fb3e9325eff2216ab20a69ccbd6b5cac6785ca2989161 + # via motor +pytest==8.1.1 \ + --hash=sha256:2a8386cfc11fa9d2c50ee7b2a57e7d898ef90470a7a34c4b949ff59662bb78b7 \ + --hash=sha256:ac978141a75948948817d360297b7aae0fcb9d6ff6bc9ec6d514b85d5a65c044 + # via + # pytest-asyncio + # pytest-cov + # pytest-httpx + # pytest-profiling +pytest-asyncio==0.23.5.post1 \ + --hash=sha256:30f54d27774e79ac409778889880242b0403d09cabd65b727ce90fe92dd5d80e \ + --hash=sha256:b9a8806bea78c21276bc34321bbf234ba1b2ea5b30d9f0ce0f2dea45e4685813 +pytest-cov==4.1.0 \ + --hash=sha256:3904b13dfbfec47f003b8e77fd5b589cd11904a21ddf1ab38a64f204d6a10ef6 \ + --hash=sha256:6ba70b9e97e69fcc3fb45bfeab2d0a138fb65c4d0d6a41ef33983ad114be8c3a +pytest-httpx==0.30.0 \ + --hash=sha256:6d47849691faf11d2532565d0c8e0e02b9f4ee730da31687feae315581d7520c \ + --hash=sha256:755b8edca87c974dd4f3605c374fda11db84631de3d163b99c0df5807023a19a +pytest-profiling==1.7.0 \ + --hash=sha256:93938f147662225d2b8bd5af89587b979652426a8a6ffd7e73ec4a23e24b7f29 \ + --hash=sha256:999cc9ac94f2e528e3f5d43465da277429984a1c237ae9818f8cfd0b06acb019 +python-dateutil==2.9.0.post0 \ + --hash=sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3 \ + --hash=sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427 + # via botocore +python-dotenv==1.0.1 \ + --hash=sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca \ + --hash=sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a + # via + # pydantic-settings + # uvicorn +pyyaml==6.0.1 \ + --hash=sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5 \ + --hash=sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc \ + --hash=sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df \ + --hash=sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741 \ + --hash=sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206 \ + --hash=sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27 \ + --hash=sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595 \ + --hash=sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62 \ + --hash=sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98 \ + --hash=sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696 \ + --hash=sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290 \ + --hash=sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9 \ + --hash=sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d \ + --hash=sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6 \ + --hash=sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867 \ + --hash=sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47 \ + --hash=sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486 \ + --hash=sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6 \ + --hash=sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3 \ + --hash=sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007 \ + --hash=sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938 \ + --hash=sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0 \ + --hash=sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c \ + --hash=sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735 \ + --hash=sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d \ + --hash=sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28 \ + --hash=sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4 \ + --hash=sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba \ + --hash=sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8 \ + --hash=sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef \ + --hash=sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5 \ + --hash=sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd \ + --hash=sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3 \ + --hash=sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0 \ + --hash=sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515 \ + --hash=sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c \ + --hash=sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c \ + --hash=sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924 \ + --hash=sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34 \ + --hash=sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43 \ + --hash=sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859 \ + --hash=sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673 \ + --hash=sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54 \ + --hash=sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a \ + --hash=sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b \ + --hash=sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab \ + --hash=sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa \ + --hash=sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c \ + --hash=sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585 \ + --hash=sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d \ + --hash=sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f + # via + # hexkit + # jsonschema2md + # pre-commit + # uvicorn +referencing==0.33.0 \ + --hash=sha256:39240f2ecc770258f28b642dd47fd74bc8b02484de54e1882b74b35ebd779bd5 \ + --hash=sha256:c775fedf74bc0f9189c2a3be1c12fd03e8c23f4d371dce795df44e06c5b412f7 + # via + # jsonschema + # jsonschema-specifications +requests==2.31.0 \ + --hash=sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f \ + --hash=sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1 + # via docker +rpds-py==0.18.0 \ + --hash=sha256:01e36a39af54a30f28b73096dd39b6802eddd04c90dbe161c1b8dbe22353189f \ + --hash=sha256:044a3e61a7c2dafacae99d1e722cc2d4c05280790ec5a05031b3876809d89a5c \ + --hash=sha256:08231ac30a842bd04daabc4d71fddd7e6d26189406d5a69535638e4dcb88fe76 \ + --hash=sha256:08f9ad53c3f31dfb4baa00da22f1e862900f45908383c062c27628754af2e88e \ + --hash=sha256:0ab39c1ba9023914297dd88ec3b3b3c3f33671baeb6acf82ad7ce883f6e8e157 \ + --hash=sha256:0af039631b6de0397ab2ba16eaf2872e9f8fca391b44d3d8cac317860a700a3f \ + --hash=sha256:0b8612cd233543a3781bc659c731b9d607de65890085098986dfd573fc2befe5 \ + --hash=sha256:11a8c85ef4a07a7638180bf04fe189d12757c696eb41f310d2426895356dcf05 \ + --hash=sha256:1374f4129f9bcca53a1bba0bb86bf78325a0374577cf7e9e4cd046b1e6f20e24 \ + --hash=sha256:1d4acf42190d449d5e89654d5c1ed3a4f17925eec71f05e2a41414689cda02d1 \ + --hash=sha256:1d9a5be316c15ffb2b3c405c4ff14448c36b4435be062a7f578ccd8b01f0c4d8 \ + --hash=sha256:1df3659d26f539ac74fb3b0c481cdf9d725386e3552c6fa2974f4d33d78e544b \ + --hash=sha256:22806714311a69fd0af9b35b7be97c18a0fc2826e6827dbb3a8c94eac6cf7eeb \ + --hash=sha256:2644e47de560eb7bd55c20fc59f6daa04682655c58d08185a9b95c1970fa1e07 \ + --hash=sha256:2e6d75ab12b0bbab7215e5d40f1e5b738aa539598db27ef83b2ec46747df90e1 \ + --hash=sha256:30f43887bbae0d49113cbaab729a112251a940e9b274536613097ab8b4899cf6 \ + --hash=sha256:34b18ba135c687f4dac449aa5157d36e2cbb7c03cbea4ddbd88604e076aa836e \ + --hash=sha256:36b3ee798c58ace201289024b52788161e1ea133e4ac93fba7d49da5fec0ef9e \ + --hash=sha256:39514da80f971362f9267c600b6d459bfbbc549cffc2cef8e47474fddc9b45b1 \ + --hash=sha256:39f5441553f1c2aed4de4377178ad8ff8f9d733723d6c66d983d75341de265ab \ + --hash=sha256:3a96e0c6a41dcdba3a0a581bbf6c44bb863f27c541547fb4b9711fd8cf0ffad4 \ + --hash=sha256:3f26b5bd1079acdb0c7a5645e350fe54d16b17bfc5e71f371c449383d3342e17 \ + --hash=sha256:41ef53e7c58aa4ef281da975f62c258950f54b76ec8e45941e93a3d1d8580594 \ + --hash=sha256:42821446ee7a76f5d9f71f9e33a4fb2ffd724bb3e7f93386150b61a43115788d \ + --hash=sha256:43fbac5f22e25bee1d482c97474f930a353542855f05c1161fd804c9dc74a09d \ + --hash=sha256:4457a94da0d5c53dc4b3e4de1158bdab077db23c53232f37a3cb7afdb053a4e3 \ + --hash=sha256:465a3eb5659338cf2a9243e50ad9b2296fa15061736d6e26240e713522b6235c \ + --hash=sha256:482103aed1dfe2f3b71a58eff35ba105289b8d862551ea576bd15479aba01f66 \ + --hash=sha256:4832d7d380477521a8c1644bbab6588dfedea5e30a7d967b5fb75977c45fd77f \ + --hash=sha256:4901165d170a5fde6f589acb90a6b33629ad1ec976d4529e769c6f3d885e3e80 \ + --hash=sha256:5307def11a35f5ae4581a0b658b0af8178c65c530e94893345bebf41cc139d33 \ + --hash=sha256:5417558f6887e9b6b65b4527232553c139b57ec42c64570569b155262ac0754f \ + --hash=sha256:56a737287efecafc16f6d067c2ea0117abadcd078d58721f967952db329a3e5c \ + --hash=sha256:586f8204935b9ec884500498ccc91aa869fc652c40c093bd9e1471fbcc25c022 \ + --hash=sha256:5b4e7d8d6c9b2e8ee2d55c90b59c707ca59bc30058269b3db7b1f8df5763557e \ + --hash=sha256:5ddcba87675b6d509139d1b521e0c8250e967e63b5909a7e8f8944d0f90ff36f \ + --hash=sha256:618a3d6cae6ef8ec88bb76dd80b83cfe415ad4f1d942ca2a903bf6b6ff97a2da \ + --hash=sha256:635dc434ff724b178cb192c70016cc0ad25a275228f749ee0daf0eddbc8183b1 \ + --hash=sha256:661d25cbffaf8cc42e971dd570d87cb29a665f49f4abe1f9e76be9a5182c4688 \ + --hash=sha256:66e6a3af5a75363d2c9a48b07cb27c4ea542938b1a2e93b15a503cdfa8490795 \ + --hash=sha256:67071a6171e92b6da534b8ae326505f7c18022c6f19072a81dcf40db2638767c \ + --hash=sha256:685537e07897f173abcf67258bee3c05c374fa6fff89d4c7e42fb391b0605e98 \ + --hash=sha256:69e64831e22a6b377772e7fb337533c365085b31619005802a79242fee620bc1 \ + --hash=sha256:6b0817e34942b2ca527b0e9298373e7cc75f429e8da2055607f4931fded23e20 \ + --hash=sha256:6c81e5f372cd0dc5dc4809553d34f832f60a46034a5f187756d9b90586c2c307 \ + --hash=sha256:6d7faa6f14017c0b1e69f5e2c357b998731ea75a442ab3841c0dbbbfe902d2c4 \ + --hash=sha256:6ef0befbb5d79cf32d0266f5cff01545602344eda89480e1dd88aca964260b18 \ + --hash=sha256:6ef687afab047554a2d366e112dd187b62d261d49eb79b77e386f94644363294 \ + --hash=sha256:7223a2a5fe0d217e60a60cdae28d6949140dde9c3bcc714063c5b463065e3d66 \ + --hash=sha256:77f195baa60a54ef9d2de16fbbfd3ff8b04edc0c0140a761b56c267ac11aa467 \ + --hash=sha256:793968759cd0d96cac1e367afd70c235867831983f876a53389ad869b043c948 \ + --hash=sha256:7bd339195d84439cbe5771546fe8a4e8a7a045417d8f9de9a368c434e42a721e \ + --hash=sha256:7cd863afe7336c62ec78d7d1349a2f34c007a3cc6c2369d667c65aeec412a5b1 \ + --hash=sha256:7f2facbd386dd60cbbf1a794181e6aa0bd429bd78bfdf775436020172e2a23f0 \ + --hash=sha256:84ffab12db93b5f6bad84c712c92060a2d321b35c3c9960b43d08d0f639d60d7 \ + --hash=sha256:8c8370641f1a7f0e0669ddccca22f1da893cef7628396431eb445d46d893e5cd \ + --hash=sha256:8db715ebe3bb7d86d77ac1826f7d67ec11a70dbd2376b7cc214199360517b641 \ + --hash=sha256:8e8916ae4c720529e18afa0b879473049e95949bf97042e938530e072fde061d \ + --hash=sha256:8f03bccbd8586e9dd37219bce4d4e0d3ab492e6b3b533e973fa08a112cb2ffc9 \ + --hash=sha256:8f2fc11e8fe034ee3c34d316d0ad8808f45bc3b9ce5857ff29d513f3ff2923a1 \ + --hash=sha256:923d39efa3cfb7279a0327e337a7958bff00cc447fd07a25cddb0a1cc9a6d2da \ + --hash=sha256:93df1de2f7f7239dc9cc5a4a12408ee1598725036bd2dedadc14d94525192fc3 \ + --hash=sha256:998e33ad22dc7ec7e030b3df701c43630b5bc0d8fbc2267653577e3fec279afa \ + --hash=sha256:99f70b740dc04d09e6b2699b675874367885217a2e9f782bdf5395632ac663b7 \ + --hash=sha256:9a00312dea9310d4cb7dbd7787e722d2e86a95c2db92fbd7d0155f97127bcb40 \ + --hash=sha256:9d54553c1136b50fd12cc17e5b11ad07374c316df307e4cfd6441bea5fb68496 \ + --hash=sha256:9dbbeb27f4e70bfd9eec1be5477517365afe05a9b2c441a0b21929ee61048124 \ + --hash=sha256:a1ce3ba137ed54f83e56fb983a5859a27d43a40188ba798993812fed73c70836 \ + --hash=sha256:a34d557a42aa28bd5c48a023c570219ba2593bcbbb8dc1b98d8cf5d529ab1434 \ + --hash=sha256:a5f446dd5055667aabaee78487f2b5ab72e244f9bc0b2ffebfeec79051679984 \ + --hash=sha256:ad36cfb355e24f1bd37cac88c112cd7730873f20fb0bdaf8ba59eedf8216079f \ + --hash=sha256:aec493917dd45e3c69d00a8874e7cbed844efd935595ef78a0f25f14312e33c6 \ + --hash=sha256:b316144e85316da2723f9d8dc75bada12fa58489a527091fa1d5a612643d1a0e \ + --hash=sha256:b34ae4636dfc4e76a438ab826a0d1eed2589ca7d9a1b2d5bb546978ac6485461 \ + --hash=sha256:b34b7aa8b261c1dbf7720b5d6f01f38243e9b9daf7e6b8bc1fd4657000062f2c \ + --hash=sha256:bc362ee4e314870a70f4ae88772d72d877246537d9f8cb8f7eacf10884862432 \ + --hash=sha256:bed88b9a458e354014d662d47e7a5baafd7ff81c780fd91584a10d6ec842cb73 \ + --hash=sha256:c0013fe6b46aa496a6749c77e00a3eb07952832ad6166bd481c74bda0dcb6d58 \ + --hash=sha256:c0b5dcf9193625afd8ecc92312d6ed78781c46ecbf39af9ad4681fc9f464af88 \ + --hash=sha256:c4325ff0442a12113a6379af66978c3fe562f846763287ef66bdc1d57925d337 \ + --hash=sha256:c463ed05f9dfb9baebef68048aed8dcdc94411e4bf3d33a39ba97e271624f8f7 \ + --hash=sha256:c8362467a0fdeccd47935f22c256bec5e6abe543bf0d66e3d3d57a8fb5731863 \ + --hash=sha256:cd5bf1af8efe569654bbef5a3e0a56eca45f87cfcffab31dd8dde70da5982475 \ + --hash=sha256:cf1ea2e34868f6fbf070e1af291c8180480310173de0b0c43fc38a02929fc0e3 \ + --hash=sha256:d62dec4976954a23d7f91f2f4530852b0c7608116c257833922a896101336c51 \ + --hash=sha256:d68c93e381010662ab873fea609bf6c0f428b6d0bb00f2c6939782e0818d37bf \ + --hash=sha256:d7c36232a90d4755b720fbd76739d8891732b18cf240a9c645d75f00639a9024 \ + --hash=sha256:dd18772815d5f008fa03d2b9a681ae38d5ae9f0e599f7dda233c439fcaa00d40 \ + --hash=sha256:ddc2f4dfd396c7bfa18e6ce371cba60e4cf9d2e5cdb71376aa2da264605b60b9 \ + --hash=sha256:e003b002ec72c8d5a3e3da2989c7d6065b47d9eaa70cd8808b5384fbb970f4ec \ + --hash=sha256:e32a92116d4f2a80b629778280103d2a510a5b3f6314ceccd6e38006b5e92dcb \ + --hash=sha256:e4461d0f003a0aa9be2bdd1b798a041f177189c1a0f7619fe8c95ad08d9a45d7 \ + --hash=sha256:e541ec6f2ec456934fd279a3120f856cd0aedd209fc3852eca563f81738f6861 \ + --hash=sha256:e546e768d08ad55b20b11dbb78a745151acbd938f8f00d0cfbabe8b0199b9880 \ + --hash=sha256:ea7d4a99f3b38c37eac212dbd6ec42b7a5ec51e2c74b5d3223e43c811609e65f \ + --hash=sha256:ed4eb745efbff0a8e9587d22a84be94a5eb7d2d99c02dacf7bd0911713ed14dd \ + --hash=sha256:f8a2f084546cc59ea99fda8e070be2fd140c3092dc11524a71aa8f0f3d5a55ca \ + --hash=sha256:fcb25daa9219b4cf3a0ab24b0eb9a5cc8949ed4dc72acb8fa16b7e1681aa3c58 \ + --hash=sha256:fdea4952db2793c4ad0bdccd27c1d8fdd1423a92f04598bc39425bcc2b8ee46e + # via + # jsonschema + # referencing +ruff==0.3.2 \ + --hash=sha256:0ac06a3759c3ab9ef86bbeca665d31ad3aa9a4b1c17684aadb7e61c10baa0df4 \ + --hash=sha256:0c1bdd9920cab5707c26c8b3bf33a064a4ca7842d91a99ec0634fec68f9f4037 \ + --hash=sha256:1231eacd4510f73222940727ac927bc5d07667a86b0cbe822024dd00343e77e9 \ + --hash=sha256:2c6d613b19e9a8021be2ee1d0e27710208d1603b56f47203d0abbde906929a9b \ + --hash=sha256:5f65103b1d76e0d600cabd577b04179ff592064eaa451a70a81085930e907d0b \ + --hash=sha256:77f2612752e25f730da7421ca5e3147b213dca4f9a0f7e0b534e9562c5441f01 \ + --hash=sha256:967978ac2d4506255e2f52afe70dda023fc602b283e97685c8447d036863a302 \ + --hash=sha256:9966b964b2dd1107797be9ca7195002b874424d1d5472097701ae8f43eadef5d \ + --hash=sha256:9bd640a8f7dd07a0b6901fcebccedadeb1a705a50350fb86b4003b805c81385a \ + --hash=sha256:b74c3de9103bd35df2bb05d8b2899bf2dbe4efda6474ea9681280648ec4d237d \ + --hash=sha256:b83d17ff166aa0659d1e1deaf9f2f14cbe387293a906de09bc4860717eb2e2da \ + --hash=sha256:bb875c6cc87b3703aeda85f01c9aebdce3d217aeaca3c2e52e38077383f7268a \ + --hash=sha256:be75e468a6a86426430373d81c041b7605137a28f7014a72d2fc749e47f572aa \ + --hash=sha256:c8439338a6303585d27b66b4626cbde89bb3e50fa3cae86ce52c1db7449330a7 \ + --hash=sha256:de8b480d8379620cbb5ea466a9e53bb467d2fb07c7eca54a4aa8576483c35d36 \ + --hash=sha256:f380be9fc15a99765c9cf316b40b9da1f6ad2ab9639e551703e581a5e6da6745 \ + --hash=sha256:fa78ec9418eb1ca3db392811df3376b46471ae93792a81af2d1cbb0e5dcb5142 +s3transfer==0.10.1 \ + --hash=sha256:5683916b4c724f799e600f41dd9e10a9ff19871bf87623cc8f491cb4f5fa0a19 \ + --hash=sha256:ceb252b11bcf87080fb7850a224fb6e05c8a776bab8f2b64b7f25b969464839d + # via boto3 +setuptools==69.2.0 \ + --hash=sha256:0ff4183f8f42cd8fa3acea16c45205521a4ef28f73c6391d8a25e92893134f2e \ + --hash=sha256:c21c49fb1042386df081cb5d86759792ab89efca84cf114889191cd09aacc80c + # via nodeenv +six==1.16.0 \ + --hash=sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926 \ + --hash=sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254 + # via + # pytest-profiling + # python-dateutil +snakeviz==2.2.0 \ + --hash=sha256:569e2d71c47f80a886aa6e70d6405cb6d30aa3520969ad956b06f824c5f02b8e \ + --hash=sha256:7bfd00be7ae147eb4a170a471578e1cd3f41f803238958b6b8efcf2c698a6aa9 +sniffio==1.3.1 \ + --hash=sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2 \ + --hash=sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc + # via + # anyio + # httpx +starlette==0.36.3 \ + --hash=sha256:13d429aa93a61dc40bf503e8c801db1f1bca3dc706b10ef2434a36123568f044 \ + --hash=sha256:90a671733cfb35771d8cc605e0b679d23b992f8dcfad48cc60b38cb29aeb7080 + # via fastapi +stringcase==1.2.0 \ + --hash=sha256:48a06980661908efe8d9d34eab2b6c13aefa2163b3ced26972902e3bdfd87008 +testcontainers==4.0.1 \ + --hash=sha256:0359c1391124d594caeb96f0adddbf16fd07aeec8cea5bbc00f9c44a140e3b25 \ + --hash=sha256:2c91b1fd8fc9901a08054206f1df108cb07685fc30232e6332ee12f292a17ea1 +tomli==2.0.1 \ + --hash=sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc \ + --hash=sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f + # via + # coverage + # mypy + # pytest +tomli-w==1.0.0 \ + --hash=sha256:9f2a07e8be30a0729e533ec968016807069991ae2fd921a78d42f429ae5f4463 \ + --hash=sha256:f463434305e0336248cac9c2dc8076b707d8a12d019dd349f5c1e382dd1ae1b9 +tornado==6.4 \ + --hash=sha256:02ccefc7d8211e5a7f9e8bc3f9e5b0ad6262ba2fbb683a6443ecc804e5224ce0 \ + --hash=sha256:10aeaa8006333433da48dec9fe417877f8bcc21f48dda8d661ae79da357b2a63 \ + --hash=sha256:27787de946a9cffd63ce5814c33f734c627a87072ec7eed71f7fc4417bb16263 \ + --hash=sha256:6f8a6c77900f5ae93d8b4ae1196472d0ccc2775cc1dfdc9e7727889145c45052 \ + --hash=sha256:71ddfc23a0e03ef2df1c1397d859868d158c8276a0603b96cf86892bff58149f \ + --hash=sha256:72291fa6e6bc84e626589f1c29d90a5a6d593ef5ae68052ee2ef000dfd273dee \ + --hash=sha256:88b84956273fbd73420e6d4b8d5ccbe913c65d31351b4c004ae362eba06e1f78 \ + --hash=sha256:e43bc2e5370a6a8e413e1e1cd0c91bedc5bd62a74a532371042a18ef19e10579 \ + --hash=sha256:f0251554cdd50b4b44362f73ad5ba7126fc5b2c2895cc62b14a1c2d7ea32f212 \ + --hash=sha256:f7894c581ecdcf91666a0912f18ce5e757213999e183ebfc2c3fdbf4d5bd764e \ + --hash=sha256:fd03192e287fbd0899dd8f81c6fb9cbbc69194d2074b38f384cb6fa72b80e9c2 + # via snakeviz +typer==0.9.0 \ + --hash=sha256:50922fd79aea2f4751a8e0408ff10d2662bd0c8bbfa84755a699f3bada2978b2 \ + --hash=sha256:5d96d986a21493606a358cae4461bd8cdf83cbf33a5aa950ae629ca3b51467ee +typing-extensions==4.10.0 \ + --hash=sha256:69b1a937c3a517342112fb4c6df7e72fc39a38e7891a5730ed4985b5214b5475 \ + --hash=sha256:b0abd7c89e8fb96f98db18d86106ff1d90ab692004eb746cf6eda2682f91b3cb + # via + # anyio + # fastapi + # logot + # mypy + # pydantic + # pydantic-core + # starlette + # typer + # uvicorn +urllib3==1.26.18 \ + --hash=sha256:34b97092d7e0a3a8cf7cd10e386f401b3737364026c45e622aa02903dffe0f07 \ + --hash=sha256:f8ecc1bba5667413457c529ab955bf8c67b45db799d159066261719e328580a0 + # via + # botocore + # docker + # requests + # testcontainers +uv==0.1.21 \ + --hash=sha256:07816028eaee1d9189dffd37471712145e882fbc3b7d72f0f57dabdb20cfc2a6 \ + --hash=sha256:14a1f98ede89d84e72df197d57eafb1a3b3b1513370830ff33bf779a7ff87c50 \ + --hash=sha256:1565fecf1542162e1fd6f708bf473590c4d6cc4e4529d6cecd26eef7796e2739 \ + --hash=sha256:1be73a717c59427bb3f65a7d38d9a690609fda711043c3ae233334805d5587cd \ + --hash=sha256:5d4d99c1e5ad9ff0a9b353098f3e1fc758a054dcb7400d0f9235b3f22f56a2fc \ + --hash=sha256:6778a27987caa71909adb44acc206e29408b33dee77e7ff7cf13a8af2f725ab0 \ + --hash=sha256:6b0d12953642be8c4238ccb2ef0af224a2cddcae476f7d48cbf56e740f12f846 \ + --hash=sha256:73c9a66f66d21067e57d0d30277dfd4153332b7865eca3c618a24eb951c952ac \ + --hash=sha256:756015a5ae2980cc11403fe9a23b875d824b6dd6d17fac433f7a0726a752e206 \ + --hash=sha256:7ed381cc71b3b718435d6ed8aa7db2373280888bbf2f757ddf8823a02a6274fa \ + --hash=sha256:aef02ebd55ede42dce747e1c31722d94caf27ce2b46be90cdd6a3f683a3caffe \ + --hash=sha256:babe2c43089ff37ec694aa7ddaa23543585d6847d7d6f10ce499ead81db99ede \ + --hash=sha256:bb0c3c452a8a9d9ff735c85ec99d44e6889d11a1b28d3786e50db672df263825 \ + --hash=sha256:c2ccd643badfe300d517bf907b9a8196d5288c1ae510bac1c5f25bd82a1f51b7 \ + --hash=sha256:e0e3684eac2fe1c05b28c0b005a4fdfae79ae4e548b4b82d12cf2a2b826e2de1 \ + --hash=sha256:f97d87ed9160f37be5813631d69ce0a9adbc964e357a8af7ceedfab58972a6dc +uvicorn==0.27.1 \ + --hash=sha256:3d9a267296243532db80c83a959a3400502165ade2c1338dea4e67915fd4745a \ + --hash=sha256:5c89da2f3895767472a35556e539fd59f7edbe9b1e9c0e1c99eebeadc61838e4 + # via ghga-service-commons +uvloop==0.19.0 \ + --hash=sha256:0246f4fd1bf2bf702e06b0d45ee91677ee5c31242f39aab4ea6fe0c51aedd0fd \ + --hash=sha256:02506dc23a5d90e04d4f65c7791e65cf44bd91b37f24cfc3ef6cf2aff05dc7ec \ + --hash=sha256:13dfdf492af0aa0a0edf66807d2b465607d11c4fa48f4a1fd41cbea5b18e8e8b \ + --hash=sha256:2693049be9d36fef81741fddb3f441673ba12a34a704e7b4361efb75cf30befc \ + --hash=sha256:271718e26b3e17906b28b67314c45d19106112067205119dddbd834c2b7ce797 \ + --hash=sha256:2df95fca285a9f5bfe730e51945ffe2fa71ccbfdde3b0da5772b4ee4f2e770d5 \ + --hash=sha256:31e672bb38b45abc4f26e273be83b72a0d28d074d5b370fc4dcf4c4eb15417d2 \ + --hash=sha256:34175c9fd2a4bc3adc1380e1261f60306344e3407c20a4d684fd5f3be010fa3d \ + --hash=sha256:45bf4c24c19fb8a50902ae37c5de50da81de4922af65baf760f7c0c42e1088be \ + --hash=sha256:472d61143059c84947aa8bb74eabbace30d577a03a1805b77933d6bd13ddebbd \ + --hash=sha256:47bf3e9312f63684efe283f7342afb414eea4d3011542155c7e625cd799c3b12 \ + --hash=sha256:492e2c32c2af3f971473bc22f086513cedfc66a130756145a931a90c3958cb17 \ + --hash=sha256:4ce6b0af8f2729a02a5d1575feacb2a94fc7b2e983868b009d51c9a9d2149bef \ + --hash=sha256:5138821e40b0c3e6c9478643b4660bd44372ae1e16a322b8fc07478f92684e24 \ + --hash=sha256:5588bd21cf1fcf06bded085f37e43ce0e00424197e7c10e77afd4bbefffef428 \ + --hash=sha256:570fc0ed613883d8d30ee40397b79207eedd2624891692471808a95069a007c1 \ + --hash=sha256:5a05128d315e2912791de6088c34136bfcdd0c7cbc1cf85fd6fd1bb321b7c849 \ + --hash=sha256:5daa304d2161d2918fa9a17d5635099a2f78ae5b5960e742b2fcfbb7aefaa593 \ + --hash=sha256:5f17766fb6da94135526273080f3455a112f82570b2ee5daa64d682387fe0dcd \ + --hash=sha256:6e3d4e85ac060e2342ff85e90d0c04157acb210b9ce508e784a944f852a40e67 \ + --hash=sha256:7010271303961c6f0fe37731004335401eb9075a12680738731e9c92ddd96ad6 \ + --hash=sha256:7207272c9520203fea9b93843bb775d03e1cf88a80a936ce760f60bb5add92f3 \ + --hash=sha256:78ab247f0b5671cc887c31d33f9b3abfb88d2614b84e4303f1a63b46c046c8bd \ + --hash=sha256:7b1fd71c3843327f3bbc3237bedcdb6504fd50368ab3e04d0410e52ec293f5b8 \ + --hash=sha256:8ca4956c9ab567d87d59d49fa3704cf29e37109ad348f2d5223c9bf761a332e7 \ + --hash=sha256:91ab01c6cd00e39cde50173ba4ec68a1e578fee9279ba64f5221810a9e786533 \ + --hash=sha256:cd81bdc2b8219cb4b2556eea39d2e36bfa375a2dd021404f90a62e44efaaf957 \ + --hash=sha256:da8435a3bd498419ee8c13c34b89b5005130a476bda1d6ca8cfdde3de35cd650 \ + --hash=sha256:de4313d7f575474c8f5a12e163f6d89c0a878bc49219641d49e6f1444369a90e \ + --hash=sha256:e27f100e1ff17f6feeb1f33968bc185bf8ce41ca557deee9d9bbbffeb72030b7 \ + --hash=sha256:f467a5fd23b4fc43ed86342641f3936a68ded707f4627622fa3f82a120e18256 + # via uvicorn +virtualenv==20.25.1 \ + --hash=sha256:961c026ac520bac5f69acb8ea063e8a4f071bcc9457b9c1f28f6b085c511583a \ + --hash=sha256:e08e13ecdca7a0bd53798f356d5831434afa5b07b93f0abdf0797b7a06ffe197 + # via pre-commit +watchfiles==0.21.0 \ + --hash=sha256:02b73130687bc3f6bb79d8a170959042eb56eb3a42df3671c79b428cd73f17cc \ + --hash=sha256:02d91cbac553a3ad141db016e3350b03184deaafeba09b9d6439826ee594b365 \ + --hash=sha256:06247538e8253975bdb328e7683f8515ff5ff041f43be6c40bff62d989b7d0b0 \ + --hash=sha256:08dca260e85ffae975448e344834d765983237ad6dc308231aa16e7933db763e \ + --hash=sha256:0d9ac347653ebd95839a7c607608703b20bc07e577e870d824fa4801bc1cb124 \ + --hash=sha256:0dd5fad9b9c0dd89904bbdea978ce89a2b692a7ee8a0ce19b940e538c88a809c \ + --hash=sha256:11cd0c3100e2233e9c53106265da31d574355c288e15259c0d40a4405cbae317 \ + --hash=sha256:18722b50783b5e30a18a8a5db3006bab146d2b705c92eb9a94f78c72beb94094 \ + --hash=sha256:18d5b4da8cf3e41895b34e8c37d13c9ed294954907929aacd95153508d5d89d7 \ + --hash=sha256:1ad7247d79f9f55bb25ab1778fd47f32d70cf36053941f07de0b7c4e96b5d235 \ + --hash=sha256:1b8d1eae0f65441963d805f766c7e9cd092f91e0c600c820c764a4ff71a0764c \ + --hash=sha256:1bd467213195e76f838caf2c28cd65e58302d0254e636e7c0fca81efa4a2e62c \ + --hash=sha256:1c9198c989f47898b2c22201756f73249de3748e0fc9de44adaf54a8b259cc0c \ + --hash=sha256:1fd9a5205139f3c6bb60d11f6072e0552f0a20b712c85f43d42342d162be1235 \ + --hash=sha256:214cee7f9e09150d4fb42e24919a1e74d8c9b8a9306ed1474ecaddcd5479c293 \ + --hash=sha256:27b4035013f1ea49c6c0b42d983133b136637a527e48c132d368eb19bf1ac6aa \ + --hash=sha256:3a23092a992e61c3a6a70f350a56db7197242f3490da9c87b500f389b2d01eef \ + --hash=sha256:3ad692bc7792be8c32918c699638b660c0de078a6cbe464c46e1340dadb94c19 \ + --hash=sha256:3ccceb50c611c433145502735e0370877cced72a6c70fd2410238bcbc7fe51d8 \ + --hash=sha256:3d0f32ebfaa9c6011f8454994f86108c2eb9c79b8b7de00b36d558cadcedaa3d \ + --hash=sha256:3f92944efc564867bbf841c823c8b71bb0be75e06b8ce45c084b46411475a915 \ + --hash=sha256:40bca549fdc929b470dd1dbfcb47b3295cb46a6d2c90e50588b0a1b3bd98f429 \ + --hash=sha256:43babacef21c519bc6631c5fce2a61eccdfc011b4bcb9047255e9620732c8097 \ + --hash=sha256:4566006aa44cb0d21b8ab53baf4b9c667a0ed23efe4aaad8c227bfba0bf15cbe \ + --hash=sha256:49f56e6ecc2503e7dbe233fa328b2be1a7797d31548e7a193237dcdf1ad0eee0 \ + --hash=sha256:4c48a10d17571d1275701e14a601e36959ffada3add8cdbc9e5061a6e3579a5d \ + --hash=sha256:4ea10a29aa5de67de02256a28d1bf53d21322295cb00bd2d57fcd19b850ebd99 \ + --hash=sha256:511f0b034120cd1989932bf1e9081aa9fb00f1f949fbd2d9cab6264916ae89b1 \ + --hash=sha256:51ddac60b96a42c15d24fbdc7a4bfcd02b5a29c047b7f8bf63d3f6f5a860949a \ + --hash=sha256:57d430f5fb63fea141ab71ca9c064e80de3a20b427ca2febcbfcef70ff0ce895 \ + --hash=sha256:59137c0c6826bd56c710d1d2bda81553b5e6b7c84d5a676747d80caf0409ad94 \ + --hash=sha256:5a03651352fc20975ee2a707cd2d74a386cd303cc688f407296064ad1e6d1562 \ + --hash=sha256:5eb86c6acb498208e7663ca22dbe68ca2cf42ab5bf1c776670a50919a56e64ab \ + --hash=sha256:642d66b75eda909fd1112d35c53816d59789a4b38c141a96d62f50a3ef9b3360 \ + --hash=sha256:6674b00b9756b0af620aa2a3346b01f8e2a3dc729d25617e1b89cf6af4a54eb1 \ + --hash=sha256:668c265d90de8ae914f860d3eeb164534ba2e836811f91fecc7050416ee70aa7 \ + --hash=sha256:66fac0c238ab9a2e72d026b5fb91cb902c146202bbd29a9a1a44e8db7b710b6f \ + --hash=sha256:6c107ea3cf2bd07199d66f156e3ea756d1b84dfd43b542b2d870b77868c98c03 \ + --hash=sha256:6c889025f59884423428c261f212e04d438de865beda0b1e1babab85ef4c0f01 \ + --hash=sha256:6cb8fdc044909e2078c248986f2fc76f911f72b51ea4a4fbbf472e01d14faa58 \ + --hash=sha256:6e9be3ef84e2bb9710f3f777accce25556f4a71e15d2b73223788d528fcc2052 \ + --hash=sha256:7f762a1a85a12cc3484f77eee7be87b10f8c50b0b787bb02f4e357403cad0c0e \ + --hash=sha256:83a696da8922314ff2aec02987eefb03784f473281d740bf9170181829133765 \ + --hash=sha256:853853cbf7bf9408b404754b92512ebe3e3a83587503d766d23e6bf83d092ee6 \ + --hash=sha256:8ad3fe0a3567c2f0f629d800409cd528cb6251da12e81a1f765e5c5345fd0137 \ + --hash=sha256:8c6ed10c2497e5fedadf61e465b3ca12a19f96004c15dcffe4bd442ebadc2d85 \ + --hash=sha256:8d5f400326840934e3507701f9f7269247f7c026d1b6cfd49477d2be0933cfca \ + --hash=sha256:927c589500f9f41e370b0125c12ac9e7d3a2fd166b89e9ee2828b3dda20bfe6f \ + --hash=sha256:9a0aa47f94ea9a0b39dd30850b0adf2e1cd32a8b4f9c7aa443d852aacf9ca214 \ + --hash=sha256:9b37a7ba223b2f26122c148bb8d09a9ff312afca998c48c725ff5a0a632145f7 \ + --hash=sha256:9c873345680c1b87f1e09e0eaf8cf6c891b9851d8b4d3645e7efe2ec20a20cc7 \ + --hash=sha256:9d09869f2c5a6f2d9df50ce3064b3391d3ecb6dced708ad64467b9e4f2c9bef3 \ + --hash=sha256:9d353c4cfda586db2a176ce42c88f2fc31ec25e50212650c89fdd0f560ee507b \ + --hash=sha256:a1e3014a625bcf107fbf38eece0e47fa0190e52e45dc6eee5a8265ddc6dc5ea7 \ + --hash=sha256:a3b9bec9579a15fb3ca2d9878deae789df72f2b0fdaf90ad49ee389cad5edab6 \ + --hash=sha256:ab03a90b305d2588e8352168e8c5a1520b721d2d367f31e9332c4235b30b8994 \ + --hash=sha256:aff06b2cac3ef4616e26ba17a9c250c1fe9dd8a5d907d0193f84c499b1b6e6a9 \ + --hash=sha256:b3cab0e06143768499384a8a5efb9c4dc53e19382952859e4802f294214f36ec \ + --hash=sha256:b4a21f71885aa2744719459951819e7bf5a906a6448a6b2bbce8e9cc9f2c8128 \ + --hash=sha256:b6d45d9b699ecbac6c7bd8e0a2609767491540403610962968d258fd6405c17c \ + --hash=sha256:be6dd5d52b73018b21adc1c5d28ac0c68184a64769052dfeb0c5d9998e7f56a2 \ + --hash=sha256:c550a56bf209a3d987d5a975cdf2063b3389a5d16caf29db4bdddeae49f22078 \ + --hash=sha256:c76c635fabf542bb78524905718c39f736a98e5ab25b23ec6d4abede1a85a6a3 \ + --hash=sha256:c81818595eff6e92535ff32825f31c116f867f64ff8cdf6562cd1d6b2e1e8f3e \ + --hash=sha256:cfb92d49dbb95ec7a07511bc9efb0faff8fe24ef3805662b8d6808ba8409a71a \ + --hash=sha256:d23bcd6c8eaa6324fe109d8cac01b41fe9a54b8c498af9ce464c1aeeb99903d6 \ + --hash=sha256:d5b1dc0e708fad9f92c296ab2f948af403bf201db8fb2eb4c8179db143732e49 \ + --hash=sha256:d78f30cbe8b2ce770160d3c08cff01b2ae9306fe66ce899b73f0409dc1846c1b \ + --hash=sha256:d8f57c4461cd24fda22493109c45b3980863c58a25b8bec885ca8bea6b8d4b28 \ + --hash=sha256:d9792dff410f266051025ecfaa927078b94cc7478954b06796a9756ccc7e14a9 \ + --hash=sha256:e7941bbcfdded9c26b0bf720cb7e6fd803d95a55d2c14b4bd1f6a2772230c586 \ + --hash=sha256:ebe684d7d26239e23d102a2bad2a358dedf18e462e8808778703427d1f584400 \ + --hash=sha256:ec8c8900dc5c83650a63dd48c4d1d245343f904c4b64b48798c67a3767d7e165 \ + --hash=sha256:f564bf68404144ea6b87a78a3f910cc8de216c6b12a4cf0b27718bf4ec38d303 \ + --hash=sha256:fd7ac678b92b29ba630d8c842d8ad6c555abda1b9ef044d6cc092dacbfc9719d + # via uvicorn +websockets==12.0 \ + --hash=sha256:00700340c6c7ab788f176d118775202aadea7602c5cc6be6ae127761c16d6b0b \ + --hash=sha256:0bee75f400895aef54157b36ed6d3b308fcab62e5260703add87f44cee9c82a6 \ + --hash=sha256:0e6e2711d5a8e6e482cacb927a49a3d432345dfe7dea8ace7b5790df5932e4df \ + --hash=sha256:12743ab88ab2af1d17dd4acb4645677cb7063ef4db93abffbf164218a5d54c6b \ + --hash=sha256:1a9d160fd080c6285e202327aba140fc9a0d910b09e423afff4ae5cbbf1c7205 \ + --hash=sha256:1bf386089178ea69d720f8db6199a0504a406209a0fc23e603b27b300fdd6892 \ + --hash=sha256:1df2fbd2c8a98d38a66f5238484405b8d1d16f929bb7a33ed73e4801222a6f53 \ + --hash=sha256:1e4b3f8ea6a9cfa8be8484c9221ec0257508e3a1ec43c36acdefb2a9c3b00aa2 \ + --hash=sha256:1f38a7b376117ef7aff996e737583172bdf535932c9ca021746573bce40165ed \ + --hash=sha256:23509452b3bc38e3a057382c2e941d5ac2e01e251acce7adc74011d7d8de434c \ + --hash=sha256:248d8e2446e13c1d4326e0a6a4e9629cb13a11195051a73acf414812700badbd \ + --hash=sha256:25eb766c8ad27da0f79420b2af4b85d29914ba0edf69f547cc4f06ca6f1d403b \ + --hash=sha256:27a5e9964ef509016759f2ef3f2c1e13f403725a5e6a1775555994966a66e931 \ + --hash=sha256:2c71bd45a777433dd9113847af751aae36e448bc6b8c361a566cb043eda6ec30 \ + --hash=sha256:2cb388a5bfb56df4d9a406783b7f9dbefb888c09b71629351cc6b036e9259370 \ + --hash=sha256:2d225bb6886591b1746b17c0573e29804619c8f755b5598d875bb4235ea639be \ + --hash=sha256:2e5fc14ec6ea568200ea4ef46545073da81900a2b67b3e666f04adf53ad452ec \ + --hash=sha256:363f57ca8bc8576195d0540c648aa58ac18cf85b76ad5202b9f976918f4219cf \ + --hash=sha256:3c6cc1360c10c17463aadd29dd3af332d4a1adaa8796f6b0e9f9df1fdb0bad62 \ + --hash=sha256:3d829f975fc2e527a3ef2f9c8f25e553eb7bc779c6665e8e1d52aa22800bb38b \ + --hash=sha256:3e3aa8c468af01d70332a382350ee95f6986db479ce7af14d5e81ec52aa2b402 \ + --hash=sha256:3f61726cae9f65b872502ff3c1496abc93ffbe31b278455c418492016e2afc8f \ + --hash=sha256:423fc1ed29f7512fceb727e2d2aecb952c46aa34895e9ed96071821309951123 \ + --hash=sha256:46e71dbbd12850224243f5d2aeec90f0aaa0f2dde5aeeb8fc8df21e04d99eff9 \ + --hash=sha256:4d87be612cbef86f994178d5186add3d94e9f31cc3cb499a0482b866ec477603 \ + --hash=sha256:5693ef74233122f8ebab026817b1b37fe25c411ecfca084b29bc7d6efc548f45 \ + --hash=sha256:5aa9348186d79a5f232115ed3fa9020eab66d6c3437d72f9d2c8ac0c6858c558 \ + --hash=sha256:5d873c7de42dea355d73f170be0f23788cf3fa9f7bed718fd2830eefedce01b4 \ + --hash=sha256:5f6ffe2c6598f7f7207eef9a1228b6f5c818f9f4d53ee920aacd35cec8110438 \ + --hash=sha256:604428d1b87edbf02b233e2c207d7d528460fa978f9e391bd8aaf9c8311de137 \ + --hash=sha256:6350b14a40c95ddd53e775dbdbbbc59b124a5c8ecd6fbb09c2e52029f7a9f480 \ + --hash=sha256:6e2df67b8014767d0f785baa98393725739287684b9f8d8a1001eb2839031447 \ + --hash=sha256:6e96f5ed1b83a8ddb07909b45bd94833b0710f738115751cdaa9da1fb0cb66e8 \ + --hash=sha256:6e9e7db18b4539a29cc5ad8c8b252738a30e2b13f033c2d6e9d0549b45841c04 \ + --hash=sha256:70ec754cc2a769bcd218ed8d7209055667b30860ffecb8633a834dde27d6307c \ + --hash=sha256:7b645f491f3c48d3f8a00d1fce07445fab7347fec54a3e65f0725d730d5b99cb \ + --hash=sha256:7fa3d25e81bfe6a89718e9791128398a50dec6d57faf23770787ff441d851967 \ + --hash=sha256:81df9cbcbb6c260de1e007e58c011bfebe2dafc8435107b0537f393dd38c8b1b \ + --hash=sha256:8572132c7be52632201a35f5e08348137f658e5ffd21f51f94572ca6c05ea81d \ + --hash=sha256:87b4aafed34653e465eb77b7c93ef058516cb5acf3eb21e42f33928616172def \ + --hash=sha256:8e332c210b14b57904869ca9f9bf4ca32f5427a03eeb625da9b616c85a3a506c \ + --hash=sha256:9893d1aa45a7f8b3bc4510f6ccf8db8c3b62120917af15e3de247f0780294b92 \ + --hash=sha256:9edf3fc590cc2ec20dc9d7a45108b5bbaf21c0d89f9fd3fd1685e223771dc0b2 \ + --hash=sha256:9fdf06fd06c32205a07e47328ab49c40fc1407cdec801d698a7c41167ea45113 \ + --hash=sha256:a02413bc474feda2849c59ed2dfb2cddb4cd3d2f03a2fedec51d6e959d9b608b \ + --hash=sha256:a1d9697f3337a89691e3bd8dc56dea45a6f6d975f92e7d5f773bc715c15dde28 \ + --hash=sha256:a571f035a47212288e3b3519944f6bf4ac7bc7553243e41eac50dd48552b6df7 \ + --hash=sha256:ab3d732ad50a4fbd04a4490ef08acd0517b6ae6b77eb967251f4c263011a990d \ + --hash=sha256:ae0a5da8f35a5be197f328d4727dbcfafa53d1824fac3d96cdd3a642fe09394f \ + --hash=sha256:b067cb952ce8bf40115f6c19f478dc71c5e719b7fbaa511359795dfd9d1a6468 \ + --hash=sha256:b2ee7288b85959797970114deae81ab41b731f19ebcd3bd499ae9ca0e3f1d2c8 \ + --hash=sha256:b81f90dcc6c85a9b7f29873beb56c94c85d6f0dac2ea8b60d995bd18bf3e2aae \ + --hash=sha256:ba0cab91b3956dfa9f512147860783a1829a8d905ee218a9837c18f683239611 \ + --hash=sha256:baa386875b70cbd81798fa9f71be689c1bf484f65fd6fb08d051a0ee4e79924d \ + --hash=sha256:bbe6013f9f791944ed31ca08b077e26249309639313fff132bfbf3ba105673b9 \ + --hash=sha256:bea88d71630c5900690fcb03161ab18f8f244805c59e2e0dc4ffadae0a7ee0ca \ + --hash=sha256:befe90632d66caaf72e8b2ed4d7f02b348913813c8b0a32fae1cc5fe3730902f \ + --hash=sha256:c3181df4583c4d3994d31fb235dc681d2aaad744fbdbf94c4802485ececdecf2 \ + --hash=sha256:c4e37d36f0d19f0a4413d3e18c0d03d0c268ada2061868c1e6f5ab1a6d575077 \ + --hash=sha256:c588f6abc13f78a67044c6b1273a99e1cf31038ad51815b3b016ce699f0d75c2 \ + --hash=sha256:cbe83a6bbdf207ff0541de01e11904827540aa069293696dd528a6640bd6a5f6 \ + --hash=sha256:d554236b2a2006e0ce16315c16eaa0d628dab009c33b63ea03f41c6107958374 \ + --hash=sha256:dbcf72a37f0b3316e993e13ecf32f10c0e1259c28ffd0a85cee26e8549595fbc \ + --hash=sha256:dc284bbc8d7c78a6c69e0c7325ab46ee5e40bb4d50e494d8131a07ef47500e9e \ + --hash=sha256:dff6cdf35e31d1315790149fee351f9e52978130cef6c87c4b6c9b3baf78bc53 \ + --hash=sha256:e469d01137942849cff40517c97a30a93ae79917752b34029f0ec72df6b46399 \ + --hash=sha256:eb809e816916a3b210bed3c82fb88eaf16e8afcf9c115ebb2bacede1797d2547 \ + --hash=sha256:ed2fcf7a07334c77fc8a230755c2209223a7cc44fc27597729b8ef5425aa61a3 \ + --hash=sha256:f44069528d45a933997a6fef143030d8ca8042f0dfaad753e2906398290e2870 \ + --hash=sha256:f764ba54e33daf20e167915edc443b6f88956f37fb606449b4a5b10ba42235a5 \ + --hash=sha256:fc4e7fa5414512b481a2483775a8e8be7803a35b30ca805afa4998a84f9fd9e8 \ + --hash=sha256:ffefa1374cd508d633646d51a8e9277763a9b78ae71324183693959cf94635a7 + # via uvicorn +wrapt==1.16.0 \ + --hash=sha256:0d2691979e93d06a95a26257adb7bfd0c93818e89b1406f5a28f36e0d8c1e1fc \ + --hash=sha256:14d7dc606219cdd7405133c713f2c218d4252f2a469003f8c46bb92d5d095d81 \ + --hash=sha256:1a5db485fe2de4403f13fafdc231b0dbae5eca4359232d2efc79025527375b09 \ + --hash=sha256:1acd723ee2a8826f3d53910255643e33673e1d11db84ce5880675954183ec47e \ + --hash=sha256:1ca9b6085e4f866bd584fb135a041bfc32cab916e69f714a7d1d397f8c4891ca \ + --hash=sha256:1dd50a2696ff89f57bd8847647a1c363b687d3d796dc30d4dd4a9d1689a706f0 \ + --hash=sha256:2076fad65c6736184e77d7d4729b63a6d1ae0b70da4868adeec40989858eb3fb \ + --hash=sha256:2a88e6010048489cda82b1326889ec075a8c856c2e6a256072b28eaee3ccf487 \ + --hash=sha256:3ebf019be5c09d400cf7b024aa52b1f3aeebeff51550d007e92c3c1c4afc2a40 \ + --hash=sha256:418abb18146475c310d7a6dc71143d6f7adec5b004ac9ce08dc7a34e2babdc5c \ + --hash=sha256:43aa59eadec7890d9958748db829df269f0368521ba6dc68cc172d5d03ed8060 \ + --hash=sha256:44a2754372e32ab315734c6c73b24351d06e77ffff6ae27d2ecf14cf3d229202 \ + --hash=sha256:490b0ee15c1a55be9c1bd8609b8cecd60e325f0575fc98f50058eae366e01f41 \ + --hash=sha256:49aac49dc4782cb04f58986e81ea0b4768e4ff197b57324dcbd7699c5dfb40b9 \ + --hash=sha256:5eb404d89131ec9b4f748fa5cfb5346802e5ee8836f57d516576e61f304f3b7b \ + --hash=sha256:5f15814a33e42b04e3de432e573aa557f9f0f56458745c2074952f564c50e664 \ + --hash=sha256:5f370f952971e7d17c7d1ead40e49f32345a7f7a5373571ef44d800d06b1899d \ + --hash=sha256:66027d667efe95cc4fa945af59f92c5a02c6f5bb6012bff9e60542c74c75c362 \ + --hash=sha256:66dfbaa7cfa3eb707bbfcd46dab2bc6207b005cbc9caa2199bcbc81d95071a00 \ + --hash=sha256:685f568fa5e627e93f3b52fda002c7ed2fa1800b50ce51f6ed1d572d8ab3e7fc \ + --hash=sha256:6906c4100a8fcbf2fa735f6059214bb13b97f75b1a61777fcf6432121ef12ef1 \ + --hash=sha256:6a42cd0cfa8ffc1915aef79cb4284f6383d8a3e9dcca70c445dcfdd639d51267 \ + --hash=sha256:6dcfcffe73710be01d90cae08c3e548d90932d37b39ef83969ae135d36ef3956 \ + --hash=sha256:6f6eac2360f2d543cc875a0e5efd413b6cbd483cb3ad7ebf888884a6e0d2e966 \ + --hash=sha256:72554a23c78a8e7aa02abbd699d129eead8b147a23c56e08d08dfc29cfdddca1 \ + --hash=sha256:73870c364c11f03ed072dda68ff7aea6d2a3a5c3fe250d917a429c7432e15228 \ + --hash=sha256:73aa7d98215d39b8455f103de64391cb79dfcad601701a3aa0dddacf74911d72 \ + --hash=sha256:75ea7d0ee2a15733684badb16de6794894ed9c55aa5e9903260922f0482e687d \ + --hash=sha256:7bd2d7ff69a2cac767fbf7a2b206add2e9a210e57947dd7ce03e25d03d2de292 \ + --hash=sha256:807cc8543a477ab7422f1120a217054f958a66ef7314f76dd9e77d3f02cdccd0 \ + --hash=sha256:8e9723528b9f787dc59168369e42ae1c3b0d3fadb2f1a71de14531d321ee05b0 \ + --hash=sha256:9090c9e676d5236a6948330e83cb89969f433b1943a558968f659ead07cb3b36 \ + --hash=sha256:9153ed35fc5e4fa3b2fe97bddaa7cbec0ed22412b85bcdaf54aeba92ea37428c \ + --hash=sha256:9159485323798c8dc530a224bd3ffcf76659319ccc7bbd52e01e73bd0241a0c5 \ + --hash=sha256:941988b89b4fd6b41c3f0bfb20e92bd23746579736b7343283297c4c8cbae68f \ + --hash=sha256:94265b00870aa407bd0cbcfd536f17ecde43b94fb8d228560a1e9d3041462d73 \ + --hash=sha256:98b5e1f498a8ca1858a1cdbffb023bfd954da4e3fa2c0cb5853d40014557248b \ + --hash=sha256:9b201ae332c3637a42f02d1045e1d0cccfdc41f1f2f801dafbaa7e9b4797bfc2 \ + --hash=sha256:a0ea261ce52b5952bf669684a251a66df239ec6d441ccb59ec7afa882265d593 \ + --hash=sha256:a33a747400b94b6d6b8a165e4480264a64a78c8a4c734b62136062e9a248dd39 \ + --hash=sha256:a452f9ca3e3267cd4d0fcf2edd0d035b1934ac2bd7e0e57ac91ad6b95c0c6389 \ + --hash=sha256:a86373cf37cd7764f2201b76496aba58a52e76dedfaa698ef9e9688bfd9e41cf \ + --hash=sha256:ac83a914ebaf589b69f7d0a1277602ff494e21f4c2f743313414378f8f50a4cf \ + --hash=sha256:aefbc4cb0a54f91af643660a0a150ce2c090d3652cf4052a5397fb2de549cd89 \ + --hash=sha256:b3646eefa23daeba62643a58aac816945cadc0afaf21800a1421eeba5f6cfb9c \ + --hash=sha256:b47cfad9e9bbbed2339081f4e346c93ecd7ab504299403320bf85f7f85c7d46c \ + --hash=sha256:b935ae30c6e7400022b50f8d359c03ed233d45b725cfdd299462f41ee5ffba6f \ + --hash=sha256:bb2dee3874a500de01c93d5c71415fcaef1d858370d405824783e7a8ef5db440 \ + --hash=sha256:bc57efac2da352a51cc4658878a68d2b1b67dbe9d33c36cb826ca449d80a8465 \ + --hash=sha256:bf5703fdeb350e36885f2875d853ce13172ae281c56e509f4e6eca049bdfb136 \ + --hash=sha256:c31f72b1b6624c9d863fc095da460802f43a7c6868c5dda140f51da24fd47d7b \ + --hash=sha256:c5cd603b575ebceca7da5a3a251e69561bec509e0b46e4993e1cac402b7247b8 \ + --hash=sha256:d2efee35b4b0a347e0d99d28e884dfd82797852d62fcd7ebdeee26f3ceb72cf3 \ + --hash=sha256:d462f28826f4657968ae51d2181a074dfe03c200d6131690b7d65d55b0f360f8 \ + --hash=sha256:d5e49454f19ef621089e204f862388d29e6e8d8b162efce05208913dde5b9ad6 \ + --hash=sha256:da4813f751142436b075ed7aa012a8778aa43a99f7b36afe9b742d3ed8bdc95e \ + --hash=sha256:db2e408d983b0e61e238cf579c09ef7020560441906ca990fe8412153e3b291f \ + --hash=sha256:db98ad84a55eb09b3c32a96c576476777e87c520a34e2519d3e59c44710c002c \ + --hash=sha256:dbed418ba5c3dce92619656802cc5355cb679e58d0d89b50f116e4a9d5a9603e \ + --hash=sha256:dcdba5c86e368442528f7060039eda390cc4091bfd1dca41e8046af7c910dda8 \ + --hash=sha256:decbfa2f618fa8ed81c95ee18a387ff973143c656ef800c9f24fb7e9c16054e2 \ + --hash=sha256:e4fdb9275308292e880dcbeb12546df7f3e0f96c6b41197e0cf37d2826359020 \ + --hash=sha256:eb1b046be06b0fce7249f1d025cd359b4b80fc1c3e24ad9eca33e0dcdb2e4a35 \ + --hash=sha256:eb6e651000a19c96f452c85132811d25e9264d836951022d6e81df2fff38337d \ + --hash=sha256:ed867c42c268f876097248e05b6117a65bcd1e63b779e916fe2e33cd6fd0d3c3 \ + --hash=sha256:edfad1d29c73f9b863ebe7082ae9321374ccb10879eeabc84ba3b69f2579d537 \ + --hash=sha256:f2058f813d4f2b5e3a9eb2eb3faf8f1d99b81c3e51aeda4b168406443e8ba809 \ + --hash=sha256:f6b2d0c6703c988d334f297aa5df18c45e97b0af3679bb75059e0e0bd8b1069d \ + --hash=sha256:f8212564d49c50eb4565e502814f694e240c55551a5f1bc841d4fcaabb0a9b8a \ + --hash=sha256:ffa565331890b90056c01db69c0fe634a776f8019c143a5ae265f9c6bc4bd6d4 + # via testcontainers diff --git a/lock/requirements.txt b/lock/requirements.txt new file mode 100644 index 00000000..e5c29d6c --- /dev/null +++ b/lock/requirements.txt @@ -0,0 +1,744 @@ +# This file was autogenerated by uv via the following command: +# uv pip compile --refresh --generate-hashes --output-file /workspace/lock/requirements.txt /tmp/tmp0timl1mn/pyproject.toml -c /workspace/lock/requirements-dev.txt +aiokafka==0.8.1 \ + --hash=sha256:1e24839088fd6d3ff481cc09a48ea487b997328df11630bc0a1b88255edbcfe9 \ + --hash=sha256:1f43d2afd7d3e4407ada8d754895fad7c344ca00648a8a38418d76564eaaf6cd \ + --hash=sha256:1f6044ed270b946d31f265903b5eb101940ed0ff3a902eaf8178103c943bbcc9 \ + --hash=sha256:24373bb2d519abac036d5b04ebc43452ef4ad1916953b6678b9801a9c93ba237 \ + --hash=sha256:2fa54b8b068d9d8735cb6757a0f48168f8cf9be68860b0bae6b3ed1684cef49b \ + --hash=sha256:3816bcfc3c57dfa4ed77fe1dc3a9a464e17b6400061348155115f282c8150c47 \ + --hash=sha256:45cd28af6590d6a999bb706803166570121ba8a5a0d06c51ebd8a59fab53593c \ + --hash=sha256:4693fbe3c10f125bf3e2df8a8ccbca3eff2bdaaa6589d28c7532c10e7d84598b \ + --hash=sha256:4fccd599ab6b3fda4f4187d854b343f153b40d05d6774be9acf238618da50031 \ + --hash=sha256:6421ee81084532f915501074a132acb2afc8cb88bf5ddb11e584230a30f6f006 \ + --hash=sha256:673c163dee62dfe45146d5250af0e395da5cc92b63f8878c592abc7dc1862899 \ + --hash=sha256:7d327d66b41c4e3bafff7f9efb71936a08f940aa665680717e20862e4272a068 \ + --hash=sha256:7f09784322c0d2c4fcc222add4337a5ac394aa30a248eb4e0e4587a125573c75 \ + --hash=sha256:90960356513f3979754261b132b12a96b0d9e3c6eb44420e3a90a7c31156a81a \ + --hash=sha256:935da8c4da9a00a1e16020d88e578206097b4bb72ebc2a25fbd2cb817907ef28 \ + --hash=sha256:9f19d90b7360bc2239fcd8b147508ae39c3e5b1acfc8e6a2a9b0f306070f7ffe \ + --hash=sha256:a8a641a8102c51422afe111d4bc70c51f335f38fc5906e4c839bd17afeaf3cb2 \ + --hash=sha256:af6df9a41e08b61d7e62c0a416feeabd81bad76fa5c70d499b083d6af9ce72c3 \ + --hash=sha256:b2bf97548fa77ad31062ca580368d346b16ba9fdca5856c435f256f3699ab12b \ + --hash=sha256:bbffc431d9285328c0bc108949132ae11cec863f1dd5a43a1fc3d45a69ffb8a9 \ + --hash=sha256:bf7473c55dc7959d4b7f9d750fa6017b325813d6cb761e488c2d9ea44e922954 \ + --hash=sha256:c4332d37cb9d52181cfda4236566b4028c7c188549277f87bcc3027577d72b1b \ + --hash=sha256:d300188e358cd29989c817f6ee2a2965a039e5a71de8ade6f80f02ebb9bd07b8 \ + --hash=sha256:fd8f9e17bc9cd2ea664a7f5133aede39a8fffebffe0c450252d475dbdedb4a35 \ + --hash=sha256:ff318d29ecbeea8c58d69c91c24d48d7ed4a8d3e829b607e670d118a9a35d5ba + # via hexkit +annotated-types==0.6.0 \ + --hash=sha256:0641064de18ba7a25dee8f96403ebc39113d0cb953a01429249d5c7564666a43 \ + --hash=sha256:563339e807e53ffd9c267e99fc6d9ea23eb8443c08f112651963e24e22f84a5d + # via pydantic +anyio==4.3.0 \ + --hash=sha256:048e05d0f6caeed70d731f3db756d35dcc1f35747c8c403364a8332c630441b8 \ + --hash=sha256:f75253795a87df48568485fd18cdd2a3fa5c4f7c5be8e5e36637733fce06fed6 + # via + # starlette + # watchfiles +async-timeout==4.0.3 \ + --hash=sha256:4640d96be84d82d02ed59ea2b7105a0f7b33abe8703703cd0ab0bf87c427522f \ + --hash=sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028 + # via aiokafka +attrs==23.2.0 \ + --hash=sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30 \ + --hash=sha256:99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1 + # via + # jsonschema + # referencing +boto3==1.34.63 \ + --hash=sha256:617174f9051b564a57fb1079186ad15db6519ab3bb0d1fb22cb54767b0c4f46e \ + --hash=sha256:e14f3866f6f372aadc5457ff9a052a45d9e2dd93466122ba86cdd5351cfeafaf + # via hexkit +botocore==1.34.63 \ + --hash=sha256:2237743fc3ed68319bc358b451e7c13a02110242b1522b839806fd64fcee45fb \ + --hash=sha256:8a6cbc3a5c5988725c00815f8f7f6baf81980b19d9a2ee414b031e726759dba9 + # via + # boto3 + # hexkit + # s3transfer +click==8.1.7 \ + --hash=sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28 \ + --hash=sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de + # via + # typer + # uvicorn +dnspython==2.6.1 \ + --hash=sha256:5ef3b9680161f6fa89daf8ad451b5f1a33b18ae8a1c6778cdf4b43f08c0a6e50 \ + --hash=sha256:e8f0f9c23a7b7cb99ded64e6c3a6f3e701d78f50c55e002b839dea7225cff7cc + # via + # email-validator + # pymongo +email-validator==2.1.1 \ + --hash=sha256:200a70680ba08904be6d1eef729205cc0d687634399a5924d842533efb824b84 \ + --hash=sha256:97d882d174e2a65732fb43bfce81a3a834cbc1bde8bf419e30ef5ea976370a05 + # via pydantic +exceptiongroup==1.2.0 \ + --hash=sha256:4bfd3996ac73b41e9b9628b04e079f193850720ea5945fc96a08633c66912f14 \ + --hash=sha256:91f5c769735f051a4290d52edd0858999b57e5876e9f85937691bd4c9fa3ed68 + # via anyio +fastapi==0.110.0 \ + --hash=sha256:266775f0dcc95af9d3ef39bad55cff525329a931d5fd51930aadd4f428bf7ff3 \ + --hash=sha256:87a1f6fb632a218222c5984be540055346a8f5d8a68e8f6fb647b1dc9934de4b + # via ghga-service-commons +ghga-event-schemas==3.1.0 \ + --hash=sha256:87706784895376314124d30a0ba77dd7cfebdbfbcbb98e88d2a836486f11c385 \ + --hash=sha256:fa0048eda36002e7a79bc9084d2acdcc9eb9d38bcf263d6f68ad6fc453cae130 +ghga-service-commons==3.1.1 \ + --hash=sha256:6f758721a2673ac9c594d9cd052e0204b79950bf9f3e828ca5d870a6637d81b6 \ + --hash=sha256:b4e696c350f14a983d9447c43b16a954f0d2bf0a057c3ecdf0a95e3f115c89b4 +h11==0.14.0 \ + --hash=sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d \ + --hash=sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761 + # via uvicorn +hexkit==2.1.1 \ + --hash=sha256:1f0a0e20a6d56fe4fa5e0b1c798df4720d2f84e20cbe7f16464bd5107e109c90 \ + --hash=sha256:3ec0f9690eb573125e22bce0c662019c9708cd17f8d2353396dc4496577718c3 + # via ghga-service-commons +httptools==0.6.1 \ + --hash=sha256:00d5d4b68a717765b1fabfd9ca755bd12bf44105eeb806c03d1962acd9b8e563 \ + --hash=sha256:0ac5a0ae3d9f4fe004318d64b8a854edd85ab76cffbf7ef5e32920faef62f142 \ + --hash=sha256:0cf2372e98406efb42e93bfe10f2948e467edfd792b015f1b4ecd897903d3e8d \ + --hash=sha256:1ed99a373e327f0107cb513b61820102ee4f3675656a37a50083eda05dc9541b \ + --hash=sha256:3c3b214ce057c54675b00108ac42bacf2ab8f85c58e3f324a4e963bbc46424f4 \ + --hash=sha256:3e802e0b2378ade99cd666b5bffb8b2a7cc8f3d28988685dc300469ea8dd86cb \ + --hash=sha256:3f30d3ce413088a98b9db71c60a6ada2001a08945cb42dd65a9a9fe228627658 \ + --hash=sha256:405784577ba6540fa7d6ff49e37daf104e04f4b4ff2d1ac0469eaa6a20fde084 \ + --hash=sha256:48ed8129cd9a0d62cf4d1575fcf90fb37e3ff7d5654d3a5814eb3d55f36478c2 \ + --hash=sha256:4bd3e488b447046e386a30f07af05f9b38d3d368d1f7b4d8f7e10af85393db97 \ + --hash=sha256:4f0f8271c0a4db459f9dc807acd0eadd4839934a4b9b892f6f160e94da309837 \ + --hash=sha256:5cceac09f164bcba55c0500a18fe3c47df29b62353198e4f37bbcc5d591172c3 \ + --hash=sha256:639dc4f381a870c9ec860ce5c45921db50205a37cc3334e756269736ff0aac58 \ + --hash=sha256:678fcbae74477a17d103b7cae78b74800d795d702083867ce160fc202104d0da \ + --hash=sha256:6a4f5ccead6d18ec072ac0b84420e95d27c1cdf5c9f1bc8fbd8daf86bd94f43d \ + --hash=sha256:6f58e335a1402fb5a650e271e8c2d03cfa7cea46ae124649346d17bd30d59c90 \ + --hash=sha256:75c8022dca7935cba14741a42744eee13ba05db00b27a4b940f0d646bd4d56d0 \ + --hash=sha256:7a7ea483c1a4485c71cb5f38be9db078f8b0e8b4c4dc0210f531cdd2ddac1ef1 \ + --hash=sha256:7d9ceb2c957320def533671fc9c715a80c47025139c8d1f3797477decbc6edd2 \ + --hash=sha256:7ebaec1bf683e4bf5e9fbb49b8cc36da482033596a415b3e4ebab5a4c0d7ec5e \ + --hash=sha256:85ed077c995e942b6f1b07583e4eb0a8d324d418954fc6af913d36db7c05a5a0 \ + --hash=sha256:8ae5b97f690badd2ca27cbf668494ee1b6d34cf1c464271ef7bfa9ca6b83ffaf \ + --hash=sha256:8b0bb634338334385351a1600a73e558ce619af390c2b38386206ac6a27fecfc \ + --hash=sha256:8e216a038d2d52ea13fdd9b9c9c7459fb80d78302b257828285eca1c773b99b3 \ + --hash=sha256:93ad80d7176aa5788902f207a4e79885f0576134695dfb0fefc15b7a4648d503 \ + --hash=sha256:95658c342529bba4e1d3d2b1a874db16c7cca435e8827422154c9da76ac4e13a \ + --hash=sha256:95fb92dd3649f9cb139e9c56604cc2d7c7bf0fc2e7c8d7fbd58f96e35eddd2a3 \ + --hash=sha256:97662ce7fb196c785344d00d638fc9ad69e18ee4bfb4000b35a52efe5adcc949 \ + --hash=sha256:9bb68d3a085c2174c2477eb3ffe84ae9fb4fde8792edb7bcd09a1d8467e30a84 \ + --hash=sha256:b512aa728bc02354e5ac086ce76c3ce635b62f5fbc32ab7082b5e582d27867bb \ + --hash=sha256:c6e26c30455600b95d94b1b836085138e82f177351454ee841c148f93a9bad5a \ + --hash=sha256:d2f6c3c4cb1948d912538217838f6e9960bc4a521d7f9b323b3da579cd14532f \ + --hash=sha256:dcbab042cc3ef272adc11220517278519adf8f53fd3056d0e68f0a6f891ba94e \ + --hash=sha256:e0b281cf5a125c35f7f6722b65d8542d2e57331be573e9e88bc8b0115c4a7a81 \ + --hash=sha256:e57997ac7fb7ee43140cc03664de5f268813a481dff6245e0075925adc6aa185 \ + --hash=sha256:fe467eb086d80217b7584e61313ebadc8d187a4d95bb62031b7bab4b205c3ba3 + # via uvicorn +idna==3.6 \ + --hash=sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca \ + --hash=sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f + # via + # anyio + # email-validator +jmespath==1.0.1 \ + --hash=sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980 \ + --hash=sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe + # via + # boto3 + # botocore +jsonschema==4.21.1 \ + --hash=sha256:7996507afae316306f9e2290407761157c6f78002dcf7419acb99822143d1c6f \ + --hash=sha256:85727c00279f5fa6bedbe6238d2aa6403bedd8b4864ab11207d07df3cc1b2ee5 + # via + # ghga-event-schemas + # hexkit +jsonschema-specifications==2023.12.1 \ + --hash=sha256:48a76787b3e70f5ed53f1160d2b81f586e4ca6d1548c5de7085d1682674764cc \ + --hash=sha256:87e4fdf3a94858b8a2ba2778d9ba57d8a9cafca7c7489c46ba0d30a8bc6a9c3c + # via jsonschema +kafka-python==2.0.2 \ + --hash=sha256:04dfe7fea2b63726cd6f3e79a2d86e709d608d74406638c5da33a01d45a9d7e3 \ + --hash=sha256:2d92418c7cb1c298fa6c7f0fb3519b520d0d7526ac6cb7ae2a4fc65a51a94b6e + # via aiokafka +motor==3.3.2 \ + --hash=sha256:6fe7e6f0c4f430b9e030b9d22549b732f7c2226af3ab71ecc309e4a1b7d19953 \ + --hash=sha256:d2fc38de15f1c8058f389c1a44a4d4105c0405c48c061cd492a654496f7bc26a + # via hexkit +packaging==24.0 \ + --hash=sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5 \ + --hash=sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9 + # via aiokafka +pydantic==2.6.4 \ + --hash=sha256:b1704e0847db01817624a6b86766967f552dd9dbf3afba4004409f908dcc84e6 \ + --hash=sha256:cc46fce86607580867bdc3361ad462bab9c222ef042d3da86f2fb333e1d916c5 + # via + # fastapi + # ghga-event-schemas + # ghga-service-commons + # hexkit + # pydantic-settings +pydantic-core==2.16.3 \ + --hash=sha256:00ee1c97b5364b84cb0bd82e9bbf645d5e2871fb8c58059d158412fee2d33d8a \ + --hash=sha256:0d32576b1de5a30d9a97f300cc6a3f4694c428d956adbc7e6e2f9cad279e45ed \ + --hash=sha256:0df446663464884297c793874573549229f9eca73b59360878f382a0fc085979 \ + --hash=sha256:0f56ae86b60ea987ae8bcd6654a887238fd53d1384f9b222ac457070b7ac4cff \ + --hash=sha256:13dcc4802961b5f843a9385fc821a0b0135e8c07fc3d9949fd49627c1a5e6ae5 \ + --hash=sha256:162e498303d2b1c036b957a1278fa0899d02b2842f1ff901b6395104c5554a45 \ + --hash=sha256:1b662180108c55dfbf1280d865b2d116633d436cfc0bba82323554873967b340 \ + --hash=sha256:1cac689f80a3abab2d3c0048b29eea5751114054f032a941a32de4c852c59cad \ + --hash=sha256:21b888c973e4f26b7a96491c0965a8a312e13be108022ee510248fe379a5fa23 \ + --hash=sha256:287073c66748f624be4cef893ef9174e3eb88fe0b8a78dc22e88eca4bc357ca6 \ + --hash=sha256:2a1ef6a36fdbf71538142ed604ad19b82f67b05749512e47f247a6ddd06afdc7 \ + --hash=sha256:2a72fb9963cba4cd5793854fd12f4cfee731e86df140f59ff52a49b3552db241 \ + --hash=sha256:2acca2be4bb2f2147ada8cac612f8a98fc09f41c89f87add7256ad27332c2fda \ + --hash=sha256:2f583bd01bbfbff4eaee0868e6fc607efdfcc2b03c1c766b06a707abbc856187 \ + --hash=sha256:33809aebac276089b78db106ee692bdc9044710e26f24a9a2eaa35a0f9fa70ba \ + --hash=sha256:36fa178aacbc277bc6b62a2c3da95226520da4f4e9e206fdf076484363895d2c \ + --hash=sha256:4204e773b4b408062960e65468d5346bdfe139247ee5f1ca2a378983e11388a2 \ + --hash=sha256:4384a8f68ddb31a0b0c3deae88765f5868a1b9148939c3f4121233314ad5532c \ + --hash=sha256:456855f57b413f077dff513a5a28ed838dbbb15082ba00f80750377eed23d132 \ + --hash=sha256:49d5d58abd4b83fb8ce763be7794d09b2f50f10aa65c0f0c1696c677edeb7cbf \ + --hash=sha256:4ac6b4ce1e7283d715c4b729d8f9dab9627586dafce81d9eaa009dd7f25dd972 \ + --hash=sha256:4df8a199d9f6afc5ae9a65f8f95ee52cae389a8c6b20163762bde0426275b7db \ + --hash=sha256:500960cb3a0543a724a81ba859da816e8cf01b0e6aaeedf2c3775d12ee49cade \ + --hash=sha256:519ae0312616026bf4cedc0fe459e982734f3ca82ee8c7246c19b650b60a5ee4 \ + --hash=sha256:578114bc803a4c1ff9946d977c221e4376620a46cf78da267d946397dc9514a8 \ + --hash=sha256:5c5cbc703168d1b7a838668998308018a2718c2130595e8e190220238addc96f \ + --hash=sha256:6162f8d2dc27ba21027f261e4fa26f8bcb3cf9784b7f9499466a311ac284b5b9 \ + --hash=sha256:704d35ecc7e9c31d48926150afada60401c55efa3b46cd1ded5a01bdffaf1d48 \ + --hash=sha256:716b542728d4c742353448765aa7cdaa519a7b82f9564130e2b3f6766018c9ec \ + --hash=sha256:72282ad4892a9fb2da25defeac8c2e84352c108705c972db82ab121d15f14e6d \ + --hash=sha256:7233d65d9d651242a68801159763d09e9ec96e8a158dbf118dc090cd77a104c9 \ + --hash=sha256:732da3243e1b8d3eab8c6ae23ae6a58548849d2e4a4e03a1924c8ddf71a387cb \ + --hash=sha256:75b81e678d1c1ede0785c7f46690621e4c6e63ccd9192af1f0bd9d504bbb6bf4 \ + --hash=sha256:75f76ee558751746d6a38f89d60b6228fa174e5172d143886af0f85aa306fd89 \ + --hash=sha256:7ee8d5f878dccb6d499ba4d30d757111847b6849ae07acdd1205fffa1fc1253c \ + --hash=sha256:7f752826b5b8361193df55afcdf8ca6a57d0232653494ba473630a83ba50d8c9 \ + --hash=sha256:86b3d0033580bd6bbe07590152007275bd7af95f98eaa5bd36f3da219dcd93da \ + --hash=sha256:8d62da299c6ecb04df729e4b5c52dc0d53f4f8430b4492b93aa8de1f541c4aac \ + --hash=sha256:8e47755d8152c1ab5b55928ab422a76e2e7b22b5ed8e90a7d584268dd49e9c6b \ + --hash=sha256:9091632a25b8b87b9a605ec0e61f241c456e9248bfdcf7abdf344fdb169c81cf \ + --hash=sha256:936e5db01dd49476fa8f4383c259b8b1303d5dd5fb34c97de194560698cc2c5e \ + --hash=sha256:99b6add4c0b39a513d323d3b93bc173dac663c27b99860dd5bf491b240d26137 \ + --hash=sha256:9c865a7ee6f93783bd5d781af5a4c43dadc37053a5b42f7d18dc019f8c9d2bd1 \ + --hash=sha256:a425479ee40ff021f8216c9d07a6a3b54b31c8267c6e17aa88b70d7ebd0e5e5b \ + --hash=sha256:a4b2bf78342c40b3dc830880106f54328928ff03e357935ad26c7128bbd66ce8 \ + --hash=sha256:a6b1bb0827f56654b4437955555dc3aeeebeddc47c2d7ed575477f082622c49e \ + --hash=sha256:aaf09e615a0bf98d406657e0008e4a8701b11481840be7d31755dc9f97c44053 \ + --hash=sha256:b1f6f5938d63c6139860f044e2538baeee6f0b251a1816e7adb6cbce106a1f01 \ + --hash=sha256:b29eeb887aa931c2fcef5aa515d9d176d25006794610c264ddc114c053bf96fe \ + --hash=sha256:b3992a322a5617ded0a9f23fd06dbc1e4bd7cf39bc4ccf344b10f80af58beacd \ + --hash=sha256:b5b6079cc452a7c53dd378c6f881ac528246b3ac9aae0f8eef98498a75657805 \ + --hash=sha256:b60cc1a081f80a2105a59385b92d82278b15d80ebb3adb200542ae165cd7d183 \ + --hash=sha256:b926dd38db1519ed3043a4de50214e0d600d404099c3392f098a7f9d75029ff8 \ + --hash=sha256:bd87f48924f360e5d1c5f770d6155ce0e7d83f7b4e10c2f9ec001c73cf475c99 \ + --hash=sha256:bda1ee3e08252b8d41fa5537413ffdddd58fa73107171a126d3b9ff001b9b820 \ + --hash=sha256:be0ec334369316fa73448cc8c982c01e5d2a81c95969d58b8f6e272884df0074 \ + --hash=sha256:c6119dc90483a5cb50a1306adb8d52c66e447da88ea44f323e0ae1a5fcb14256 \ + --hash=sha256:c9803edf8e29bd825f43481f19c37f50d2b01899448273b3a7758441b512acf8 \ + --hash=sha256:c9bd22a2a639e26171068f8ebb5400ce2c1bc7d17959f60a3b753ae13c632975 \ + --hash=sha256:cbcc558401de90a746d02ef330c528f2e668c83350f045833543cd57ecead1ad \ + --hash=sha256:cf6204fe865da605285c34cf1172879d0314ff267b1c35ff59de7154f35fdc2e \ + --hash=sha256:d33dd21f572545649f90c38c227cc8631268ba25c460b5569abebdd0ec5974ca \ + --hash=sha256:d89ca19cdd0dd5f31606a9329e309d4fcbb3df860960acec32630297d61820df \ + --hash=sha256:d8f99b147ff3fcf6b3cc60cb0c39ea443884d5559a30b1481e92495f2310ff2b \ + --hash=sha256:d937653a696465677ed583124b94a4b2d79f5e30b2c46115a68e482c6a591c8a \ + --hash=sha256:dcca5d2bf65c6fb591fff92da03f94cd4f315972f97c21975398bd4bd046854a \ + --hash=sha256:ded1c35f15c9dea16ead9bffcde9bb5c7c031bff076355dc58dcb1cb436c4721 \ + --hash=sha256:e3e70c94a0c3841e6aa831edab1619ad5c511199be94d0c11ba75fe06efe107a \ + --hash=sha256:e56f8186d6210ac7ece503193ec84104da7ceb98f68ce18c07282fcc2452e76f \ + --hash=sha256:e7774b570e61cb998490c5235740d475413a1f6de823169b4cf94e2fe9e9f6b2 \ + --hash=sha256:e7c6ed0dc9d8e65f24f5824291550139fe6f37fac03788d4580da0d33bc00c97 \ + --hash=sha256:ec08be75bb268473677edb83ba71e7e74b43c008e4a7b1907c6d57e940bf34b6 \ + --hash=sha256:ecdf6bf5f578615f2e985a5e1f6572e23aa632c4bd1dc67f8f406d445ac115ed \ + --hash=sha256:ed25e1835c00a332cb10c683cd39da96a719ab1dfc08427d476bce41b92531fc \ + --hash=sha256:f4cb85f693044e0f71f394ff76c98ddc1bc0953e48c061725e540396d5c8a2e1 \ + --hash=sha256:f53aace168a2a10582e570b7736cc5bef12cae9cf21775e3eafac597e8551fbe \ + --hash=sha256:f651dd19363c632f4abe3480a7c87a9773be27cfe1341aef06e8759599454120 \ + --hash=sha256:fc4ad7f7ee1a13d9cb49d8198cd7d7e3aa93e425f371a68235f784e99741561f \ + --hash=sha256:fee427241c2d9fb7192b658190f9f5fd6dfe41e02f3c1489d2ec1e6a5ab1e04a + # via pydantic +pydantic-settings==2.2.1 \ + --hash=sha256:00b9f6a5e95553590434c0fa01ead0b216c3e10bc54ae02e37f359948643c5ed \ + --hash=sha256:0235391d26db4d2190cb9b31051c4b46882d28a51533f97440867f012d4da091 + # via hexkit +pymongo==4.6.2 \ + --hash=sha256:097791d5a8d44e2444e0c8c4d6e14570ac11e22bcb833808885a5db081c3dc2a \ + --hash=sha256:0d002ae456a15b1d790a78bb84f87af21af1cb716a63efb2c446ab6bcbbc48ca \ + --hash=sha256:0fbdbf2fba1b4f5f1522e9f11e21c306e095b59a83340a69e908f8ed9b450070 \ + --hash=sha256:1849fd6f1917b4dc5dbf744b2f18e41e0538d08dd8e9ba9efa811c5149d665a3 \ + --hash=sha256:18c422e6b08fa370ed9d8670c67e78d01f50d6517cec4522aa8627014dfa38b6 \ + --hash=sha256:1f251f287e6d42daa3654b686ce1fcb6d74bf13b3907c3ae25954978c70f2cd4 \ + --hash=sha256:1f5f4cd2969197e25b67e24d5b8aa2452d381861d2791d06c493eaa0b9c9fcfe \ + --hash=sha256:1f706c1a644ed33eaea91df0a8fb687ce572b53eeb4ff9b89270cb0247e5d0e1 \ + --hash=sha256:2160d9c8cd20ce1f76a893f0daf7c0d38af093f36f1b5c9f3dcf3e08f7142814 \ + --hash=sha256:2b575fbe6396bbf21e4d0e5fd2e3cdb656dc90c930b6c5532192e9a89814f72d \ + --hash=sha256:2b65433c90e07dc252b4a55dfd885ca0df94b1cf77c5b8709953ec1983aadc03 \ + --hash=sha256:2f7b98f8d2cf3eeebde738d080ae9b4276d7250912d9751046a9ac1efc9b1ce2 \ + --hash=sha256:311794ef3ccae374aaef95792c36b0e5c06e8d5cf04a1bdb1b2bf14619ac881f \ + --hash=sha256:362a5adf6f3f938a8ff220a4c4aaa93e84ef932a409abecd837c617d17a5990f \ + --hash=sha256:397949a9cc85e4a1452f80b7f7f2175d557237177120954eff00bf79553e89d3 \ + --hash=sha256:3a5280f496297537301e78bde250c96fadf4945e7b2c397d8bb8921861dd236d \ + --hash=sha256:3e03c732cb64b96849310e1d8688fb70d75e2571385485bf2f1e7ad1d309fa53 \ + --hash=sha256:3e9f6e2f3da0a6af854a3e959a6962b5f8b43bbb8113cd0bff0421c5059b3106 \ + --hash=sha256:4522ad69a4ab0e1b46a8367d62ad3865b8cd54cf77518c157631dac1fdc97584 \ + --hash=sha256:477914e13501bb1d4608339ee5bb618be056d2d0e7267727623516cfa902e652 \ + --hash=sha256:4993593de44c741d1e9f230f221fe623179f500765f9855936e4ff6f33571bad \ + --hash=sha256:4d982c6db1da7cf3018183891883660ad085de97f21490d314385373f775915b \ + --hash=sha256:4e2129ec8f72806751b621470ac5d26aaa18fae4194796621508fa0e6068278a \ + --hash=sha256:4fa30494601a6271a8b416554bd7cde7b2a848230f0ec03e3f08d84565b4bf8c \ + --hash=sha256:5379ca6fd325387a34cda440aec2bd031b5ef0b0aa2e23b4981945cff1dab84c \ + --hash=sha256:579508536113dbd4c56e4738955a18847e8a6c41bf3c0b4ab18b51d81a6b7be8 \ + --hash=sha256:57c05f2e310701fc17ae358caafd99b1830014e316f0242d13ab6c01db0ab1c2 \ + --hash=sha256:5c2f258489de12a65b81e1b803a531ee8cf633fa416ae84de65cd5f82d2ceb37 \ + --hash=sha256:5db133d6ec7a4f7fc7e2bd098e4df23d7ad949f7be47b27b515c9fb9301c61e4 \ + --hash=sha256:5f6bcd2d012d82d25191a911a239fd05a8a72e8c5a7d81d056c0f3520cad14d1 \ + --hash=sha256:6125f73503407792c8b3f80165f8ab88a4e448d7d9234c762681a4d0b446fcb4 \ + --hash=sha256:64ec3e2dcab9af61bdbfcb1dd863c70d1b0c220b8e8ac11df8b57f80ee0402b3 \ + --hash=sha256:658f6c028edaeb02761ebcaca8d44d519c22594b2a51dcbc9bd2432aa93319e3 \ + --hash=sha256:68109c13176749fbbbbbdb94dd4a58dcc604db6ea43ee300b2602154aebdd55f \ + --hash=sha256:6ceaaff4b812ae368cf9774989dea81b9bbb71e5bed666feca6a9f3087c03e49 \ + --hash=sha256:707d28a822b918acf941cff590affaddb42a5d640614d71367c8956623a80cbc \ + --hash=sha256:7640d176ee5b0afec76a1bda3684995cb731b2af7fcfd7c7ef8dc271c5d689af \ + --hash=sha256:7dd63f7c2b3727541f7f37d0fb78d9942eb12a866180fbeb898714420aad74e2 \ + --hash=sha256:8110b78fc4b37dced85081d56795ecbee6a7937966e918e05e33a3900e8ea07d \ + --hash=sha256:84593447a5c5fe7a59ba86b72c2c89d813fbac71c07757acdf162fbfd5d005b9 \ + --hash=sha256:8caa73fb19070008e851a589b744aaa38edd1366e2487284c61158c77fdf72af \ + --hash=sha256:91ddf95cedca12f115fbc5f442b841e81197d85aa3cc30b82aee3635a5208af2 \ + --hash=sha256:94637941fe343000f728e28d3fe04f1f52aec6376b67b85583026ff8dab2a0e0 \ + --hash=sha256:97d81d357e1a2a248b3494d52ebc8bf15d223ee89d59ee63becc434e07438a24 \ + --hash=sha256:991e406db5da4d89fb220a94d8caaf974ffe14ce6b095957bae9273c609784a0 \ + --hash=sha256:9aebddb2ec2128d5fc2fe3aee6319afef8697e0374f8a1fcca3449d6f625e7b4 \ + --hash=sha256:9d511db310f43222bc58d811037b176b4b88dc2b4617478c5ef01fea404f8601 \ + --hash=sha256:9eec7140cf7513aa770ea51505d312000c7416626a828de24318fdcc9ac3214c \ + --hash=sha256:9f86ba0c781b497a3c9c886765d7b6402a0e3ae079dd517365044c89cd7abb06 \ + --hash=sha256:a509db602462eb736666989739215b4b7d8f4bb8ac31d0bffd4be9eae96c63ef \ + --hash=sha256:aaecfafb407feb6f562c7f2f5b91f22bfacba6dd739116b1912788cff7124c4a \ + --hash=sha256:ab7d01ac832a1663dad592ccbd92bb0f0775bc8f98a1923c5e1a7d7fead495af \ + --hash=sha256:ac20dd0c7b42555837c86f5ea46505f35af20a08b9cf5770cd1834288d8bd1b4 \ + --hash=sha256:b2d445f1cf147331947cc35ec10342f898329f29dd1947a3f8aeaf7e0e6878d1 \ + --hash=sha256:b2dd8c874927a27995f64a3b44c890e8a944c98dec1ba79eab50e07f1e3f801b \ + --hash=sha256:ba052446a14bd714ec83ca4e77d0d97904f33cd046d7bb60712a6be25eb31dbb \ + --hash=sha256:bea62f03a50f363265a7a651b4e2a4429b4f138c1864b2d83d4bf6f9851994be \ + --hash=sha256:bff601fbfcecd2166d9a2b70777c2985cb9689e2befb3278d91f7f93a0456cae \ + --hash=sha256:c3797e0a628534e07a36544d2bfa69e251a578c6d013e975e9e3ed2ac41f2d95 \ + --hash=sha256:c43205e85cbcbdf03cff62ad8f50426dd9d20134a915cfb626d805bab89a1844 \ + --hash=sha256:c68bf4a399e37798f1b5aa4f6c02886188ef465f4ac0b305a607b7579413e366 \ + --hash=sha256:c9519c9d341983f3a1bd19628fecb1d72a48d8666cf344549879f2e63f54463b \ + --hash=sha256:ca5877754f3fa6e4fe5aacf5c404575f04c2d9efc8d22ed39576ed9098d555c8 \ + --hash=sha256:d0257e0eebb50f242ca28a92ef195889a6ad03dcdde5bf1c7ab9f38b7e810801 \ + --hash=sha256:d788cb5cc947d78934be26eef1623c78cec3729dc93a30c23f049b361aa6d835 \ + --hash=sha256:d7d227a60b00925dd3aeae4675575af89c661a8e89a1f7d1677e57eba4a3693c \ + --hash=sha256:df813f0c2c02281720ccce225edf39dc37855bf72cdfde6f789a1d1cf32ffb4b \ + --hash=sha256:e0b208ebec3b47ee78a5c836e2e885e8c1e10f8ffd101aaec3d63997a4bdcd04 \ + --hash=sha256:e571434633f99a81e081738721bb38e697345281ed2f79c2f290f809ba3fbb2f \ + --hash=sha256:e78af59fd0eb262c2a5f7c7d7e3b95e8596a75480d31087ca5f02f2d4c6acd19 \ + --hash=sha256:e942945e9112075a84d2e2d6e0d0c98833cdcdfe48eb8952b917f996025c7ffa \ + --hash=sha256:ebd343ca44982d480f1e39372c48e8e263fc6f32e9af2be456298f146a3db715 \ + --hash=sha256:ed694c0d1977cb54281cb808bc2b247c17fb64b678a6352d3b77eb678ebe1bd9 \ + --hash=sha256:ee30a9d4c27a88042d0636aca0275788af09cc237ae365cd6ebb34524bddb9cc \ + --hash=sha256:f1febca6f79e91feafc572906871805bd9c271b6a2d98a8bb5499b6ace0befed \ + --hash=sha256:f251db26c239aec2a4d57fbe869e0a27b7f6b5384ec6bf54aeb4a6a5e7408234 \ + --hash=sha256:f3bae553ca39ed52db099d76acd5e8566096064dc7614c34c9359bb239ec4081 \ + --hash=sha256:f673b64a0884edcc56073bda0b363428dc1bf4eb1b5e7d0b689f7ec6173edad6 \ + --hash=sha256:fa0bbbfbd1f8ebbd5facaa10f9f333b20027b240af012748555148943616fdf3 \ + --hash=sha256:fb24abcd50501b25d33a074c1790a1389b6460d2509e4b240d03fd2e5c79f463 \ + --hash=sha256:fbafe3a1df21eeadb003c38fc02c1abf567648b6477ec50c4a3c042dca205371 \ + --hash=sha256:fe010154dfa9e428bd2fb3e9325eff2216ab20a69ccbd6b5cac6785ca2989161 + # via motor +python-dateutil==2.9.0.post0 \ + --hash=sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3 \ + --hash=sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427 + # via botocore +python-dotenv==1.0.1 \ + --hash=sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca \ + --hash=sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a + # via + # pydantic-settings + # uvicorn +pyyaml==6.0.1 \ + --hash=sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5 \ + --hash=sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc \ + --hash=sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df \ + --hash=sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741 \ + --hash=sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206 \ + --hash=sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27 \ + --hash=sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595 \ + --hash=sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62 \ + --hash=sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98 \ + --hash=sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696 \ + --hash=sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290 \ + --hash=sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9 \ + --hash=sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d \ + --hash=sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6 \ + --hash=sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867 \ + --hash=sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47 \ + --hash=sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486 \ + --hash=sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6 \ + --hash=sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3 \ + --hash=sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007 \ + --hash=sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938 \ + --hash=sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0 \ + --hash=sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c \ + --hash=sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735 \ + --hash=sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d \ + --hash=sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28 \ + --hash=sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4 \ + --hash=sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba \ + --hash=sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8 \ + --hash=sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef \ + --hash=sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5 \ + --hash=sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd \ + --hash=sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3 \ + --hash=sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0 \ + --hash=sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515 \ + --hash=sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c \ + --hash=sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c \ + --hash=sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924 \ + --hash=sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34 \ + --hash=sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43 \ + --hash=sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859 \ + --hash=sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673 \ + --hash=sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54 \ + --hash=sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a \ + --hash=sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b \ + --hash=sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab \ + --hash=sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa \ + --hash=sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c \ + --hash=sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585 \ + --hash=sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d \ + --hash=sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f + # via + # hexkit + # uvicorn +referencing==0.33.0 \ + --hash=sha256:39240f2ecc770258f28b642dd47fd74bc8b02484de54e1882b74b35ebd779bd5 \ + --hash=sha256:c775fedf74bc0f9189c2a3be1c12fd03e8c23f4d371dce795df44e06c5b412f7 + # via + # jsonschema + # jsonschema-specifications +rpds-py==0.18.0 \ + --hash=sha256:01e36a39af54a30f28b73096dd39b6802eddd04c90dbe161c1b8dbe22353189f \ + --hash=sha256:044a3e61a7c2dafacae99d1e722cc2d4c05280790ec5a05031b3876809d89a5c \ + --hash=sha256:08231ac30a842bd04daabc4d71fddd7e6d26189406d5a69535638e4dcb88fe76 \ + --hash=sha256:08f9ad53c3f31dfb4baa00da22f1e862900f45908383c062c27628754af2e88e \ + --hash=sha256:0ab39c1ba9023914297dd88ec3b3b3c3f33671baeb6acf82ad7ce883f6e8e157 \ + --hash=sha256:0af039631b6de0397ab2ba16eaf2872e9f8fca391b44d3d8cac317860a700a3f \ + --hash=sha256:0b8612cd233543a3781bc659c731b9d607de65890085098986dfd573fc2befe5 \ + --hash=sha256:11a8c85ef4a07a7638180bf04fe189d12757c696eb41f310d2426895356dcf05 \ + --hash=sha256:1374f4129f9bcca53a1bba0bb86bf78325a0374577cf7e9e4cd046b1e6f20e24 \ + --hash=sha256:1d4acf42190d449d5e89654d5c1ed3a4f17925eec71f05e2a41414689cda02d1 \ + --hash=sha256:1d9a5be316c15ffb2b3c405c4ff14448c36b4435be062a7f578ccd8b01f0c4d8 \ + --hash=sha256:1df3659d26f539ac74fb3b0c481cdf9d725386e3552c6fa2974f4d33d78e544b \ + --hash=sha256:22806714311a69fd0af9b35b7be97c18a0fc2826e6827dbb3a8c94eac6cf7eeb \ + --hash=sha256:2644e47de560eb7bd55c20fc59f6daa04682655c58d08185a9b95c1970fa1e07 \ + --hash=sha256:2e6d75ab12b0bbab7215e5d40f1e5b738aa539598db27ef83b2ec46747df90e1 \ + --hash=sha256:30f43887bbae0d49113cbaab729a112251a940e9b274536613097ab8b4899cf6 \ + --hash=sha256:34b18ba135c687f4dac449aa5157d36e2cbb7c03cbea4ddbd88604e076aa836e \ + --hash=sha256:36b3ee798c58ace201289024b52788161e1ea133e4ac93fba7d49da5fec0ef9e \ + --hash=sha256:39514da80f971362f9267c600b6d459bfbbc549cffc2cef8e47474fddc9b45b1 \ + --hash=sha256:39f5441553f1c2aed4de4377178ad8ff8f9d733723d6c66d983d75341de265ab \ + --hash=sha256:3a96e0c6a41dcdba3a0a581bbf6c44bb863f27c541547fb4b9711fd8cf0ffad4 \ + --hash=sha256:3f26b5bd1079acdb0c7a5645e350fe54d16b17bfc5e71f371c449383d3342e17 \ + --hash=sha256:41ef53e7c58aa4ef281da975f62c258950f54b76ec8e45941e93a3d1d8580594 \ + --hash=sha256:42821446ee7a76f5d9f71f9e33a4fb2ffd724bb3e7f93386150b61a43115788d \ + --hash=sha256:43fbac5f22e25bee1d482c97474f930a353542855f05c1161fd804c9dc74a09d \ + --hash=sha256:4457a94da0d5c53dc4b3e4de1158bdab077db23c53232f37a3cb7afdb053a4e3 \ + --hash=sha256:465a3eb5659338cf2a9243e50ad9b2296fa15061736d6e26240e713522b6235c \ + --hash=sha256:482103aed1dfe2f3b71a58eff35ba105289b8d862551ea576bd15479aba01f66 \ + --hash=sha256:4832d7d380477521a8c1644bbab6588dfedea5e30a7d967b5fb75977c45fd77f \ + --hash=sha256:4901165d170a5fde6f589acb90a6b33629ad1ec976d4529e769c6f3d885e3e80 \ + --hash=sha256:5307def11a35f5ae4581a0b658b0af8178c65c530e94893345bebf41cc139d33 \ + --hash=sha256:5417558f6887e9b6b65b4527232553c139b57ec42c64570569b155262ac0754f \ + --hash=sha256:56a737287efecafc16f6d067c2ea0117abadcd078d58721f967952db329a3e5c \ + --hash=sha256:586f8204935b9ec884500498ccc91aa869fc652c40c093bd9e1471fbcc25c022 \ + --hash=sha256:5b4e7d8d6c9b2e8ee2d55c90b59c707ca59bc30058269b3db7b1f8df5763557e \ + --hash=sha256:5ddcba87675b6d509139d1b521e0c8250e967e63b5909a7e8f8944d0f90ff36f \ + --hash=sha256:618a3d6cae6ef8ec88bb76dd80b83cfe415ad4f1d942ca2a903bf6b6ff97a2da \ + --hash=sha256:635dc434ff724b178cb192c70016cc0ad25a275228f749ee0daf0eddbc8183b1 \ + --hash=sha256:661d25cbffaf8cc42e971dd570d87cb29a665f49f4abe1f9e76be9a5182c4688 \ + --hash=sha256:66e6a3af5a75363d2c9a48b07cb27c4ea542938b1a2e93b15a503cdfa8490795 \ + --hash=sha256:67071a6171e92b6da534b8ae326505f7c18022c6f19072a81dcf40db2638767c \ + --hash=sha256:685537e07897f173abcf67258bee3c05c374fa6fff89d4c7e42fb391b0605e98 \ + --hash=sha256:69e64831e22a6b377772e7fb337533c365085b31619005802a79242fee620bc1 \ + --hash=sha256:6b0817e34942b2ca527b0e9298373e7cc75f429e8da2055607f4931fded23e20 \ + --hash=sha256:6c81e5f372cd0dc5dc4809553d34f832f60a46034a5f187756d9b90586c2c307 \ + --hash=sha256:6d7faa6f14017c0b1e69f5e2c357b998731ea75a442ab3841c0dbbbfe902d2c4 \ + --hash=sha256:6ef0befbb5d79cf32d0266f5cff01545602344eda89480e1dd88aca964260b18 \ + --hash=sha256:6ef687afab047554a2d366e112dd187b62d261d49eb79b77e386f94644363294 \ + --hash=sha256:7223a2a5fe0d217e60a60cdae28d6949140dde9c3bcc714063c5b463065e3d66 \ + --hash=sha256:77f195baa60a54ef9d2de16fbbfd3ff8b04edc0c0140a761b56c267ac11aa467 \ + --hash=sha256:793968759cd0d96cac1e367afd70c235867831983f876a53389ad869b043c948 \ + --hash=sha256:7bd339195d84439cbe5771546fe8a4e8a7a045417d8f9de9a368c434e42a721e \ + --hash=sha256:7cd863afe7336c62ec78d7d1349a2f34c007a3cc6c2369d667c65aeec412a5b1 \ + --hash=sha256:7f2facbd386dd60cbbf1a794181e6aa0bd429bd78bfdf775436020172e2a23f0 \ + --hash=sha256:84ffab12db93b5f6bad84c712c92060a2d321b35c3c9960b43d08d0f639d60d7 \ + --hash=sha256:8c8370641f1a7f0e0669ddccca22f1da893cef7628396431eb445d46d893e5cd \ + --hash=sha256:8db715ebe3bb7d86d77ac1826f7d67ec11a70dbd2376b7cc214199360517b641 \ + --hash=sha256:8e8916ae4c720529e18afa0b879473049e95949bf97042e938530e072fde061d \ + --hash=sha256:8f03bccbd8586e9dd37219bce4d4e0d3ab492e6b3b533e973fa08a112cb2ffc9 \ + --hash=sha256:8f2fc11e8fe034ee3c34d316d0ad8808f45bc3b9ce5857ff29d513f3ff2923a1 \ + --hash=sha256:923d39efa3cfb7279a0327e337a7958bff00cc447fd07a25cddb0a1cc9a6d2da \ + --hash=sha256:93df1de2f7f7239dc9cc5a4a12408ee1598725036bd2dedadc14d94525192fc3 \ + --hash=sha256:998e33ad22dc7ec7e030b3df701c43630b5bc0d8fbc2267653577e3fec279afa \ + --hash=sha256:99f70b740dc04d09e6b2699b675874367885217a2e9f782bdf5395632ac663b7 \ + --hash=sha256:9a00312dea9310d4cb7dbd7787e722d2e86a95c2db92fbd7d0155f97127bcb40 \ + --hash=sha256:9d54553c1136b50fd12cc17e5b11ad07374c316df307e4cfd6441bea5fb68496 \ + --hash=sha256:9dbbeb27f4e70bfd9eec1be5477517365afe05a9b2c441a0b21929ee61048124 \ + --hash=sha256:a1ce3ba137ed54f83e56fb983a5859a27d43a40188ba798993812fed73c70836 \ + --hash=sha256:a34d557a42aa28bd5c48a023c570219ba2593bcbbb8dc1b98d8cf5d529ab1434 \ + --hash=sha256:a5f446dd5055667aabaee78487f2b5ab72e244f9bc0b2ffebfeec79051679984 \ + --hash=sha256:ad36cfb355e24f1bd37cac88c112cd7730873f20fb0bdaf8ba59eedf8216079f \ + --hash=sha256:aec493917dd45e3c69d00a8874e7cbed844efd935595ef78a0f25f14312e33c6 \ + --hash=sha256:b316144e85316da2723f9d8dc75bada12fa58489a527091fa1d5a612643d1a0e \ + --hash=sha256:b34ae4636dfc4e76a438ab826a0d1eed2589ca7d9a1b2d5bb546978ac6485461 \ + --hash=sha256:b34b7aa8b261c1dbf7720b5d6f01f38243e9b9daf7e6b8bc1fd4657000062f2c \ + --hash=sha256:bc362ee4e314870a70f4ae88772d72d877246537d9f8cb8f7eacf10884862432 \ + --hash=sha256:bed88b9a458e354014d662d47e7a5baafd7ff81c780fd91584a10d6ec842cb73 \ + --hash=sha256:c0013fe6b46aa496a6749c77e00a3eb07952832ad6166bd481c74bda0dcb6d58 \ + --hash=sha256:c0b5dcf9193625afd8ecc92312d6ed78781c46ecbf39af9ad4681fc9f464af88 \ + --hash=sha256:c4325ff0442a12113a6379af66978c3fe562f846763287ef66bdc1d57925d337 \ + --hash=sha256:c463ed05f9dfb9baebef68048aed8dcdc94411e4bf3d33a39ba97e271624f8f7 \ + --hash=sha256:c8362467a0fdeccd47935f22c256bec5e6abe543bf0d66e3d3d57a8fb5731863 \ + --hash=sha256:cd5bf1af8efe569654bbef5a3e0a56eca45f87cfcffab31dd8dde70da5982475 \ + --hash=sha256:cf1ea2e34868f6fbf070e1af291c8180480310173de0b0c43fc38a02929fc0e3 \ + --hash=sha256:d62dec4976954a23d7f91f2f4530852b0c7608116c257833922a896101336c51 \ + --hash=sha256:d68c93e381010662ab873fea609bf6c0f428b6d0bb00f2c6939782e0818d37bf \ + --hash=sha256:d7c36232a90d4755b720fbd76739d8891732b18cf240a9c645d75f00639a9024 \ + --hash=sha256:dd18772815d5f008fa03d2b9a681ae38d5ae9f0e599f7dda233c439fcaa00d40 \ + --hash=sha256:ddc2f4dfd396c7bfa18e6ce371cba60e4cf9d2e5cdb71376aa2da264605b60b9 \ + --hash=sha256:e003b002ec72c8d5a3e3da2989c7d6065b47d9eaa70cd8808b5384fbb970f4ec \ + --hash=sha256:e32a92116d4f2a80b629778280103d2a510a5b3f6314ceccd6e38006b5e92dcb \ + --hash=sha256:e4461d0f003a0aa9be2bdd1b798a041f177189c1a0f7619fe8c95ad08d9a45d7 \ + --hash=sha256:e541ec6f2ec456934fd279a3120f856cd0aedd209fc3852eca563f81738f6861 \ + --hash=sha256:e546e768d08ad55b20b11dbb78a745151acbd938f8f00d0cfbabe8b0199b9880 \ + --hash=sha256:ea7d4a99f3b38c37eac212dbd6ec42b7a5ec51e2c74b5d3223e43c811609e65f \ + --hash=sha256:ed4eb745efbff0a8e9587d22a84be94a5eb7d2d99c02dacf7bd0911713ed14dd \ + --hash=sha256:f8a2f084546cc59ea99fda8e070be2fd140c3092dc11524a71aa8f0f3d5a55ca \ + --hash=sha256:fcb25daa9219b4cf3a0ab24b0eb9a5cc8949ed4dc72acb8fa16b7e1681aa3c58 \ + --hash=sha256:fdea4952db2793c4ad0bdccd27c1d8fdd1423a92f04598bc39425bcc2b8ee46e + # via + # jsonschema + # referencing +s3transfer==0.10.1 \ + --hash=sha256:5683916b4c724f799e600f41dd9e10a9ff19871bf87623cc8f491cb4f5fa0a19 \ + --hash=sha256:ceb252b11bcf87080fb7850a224fb6e05c8a776bab8f2b64b7f25b969464839d + # via boto3 +six==1.16.0 \ + --hash=sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926 \ + --hash=sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254 + # via python-dateutil +sniffio==1.3.1 \ + --hash=sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2 \ + --hash=sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc + # via anyio +starlette==0.36.3 \ + --hash=sha256:13d429aa93a61dc40bf503e8c801db1f1bca3dc706b10ef2434a36123568f044 \ + --hash=sha256:90a671733cfb35771d8cc605e0b679d23b992f8dcfad48cc60b38cb29aeb7080 + # via fastapi +typer==0.9.0 \ + --hash=sha256:50922fd79aea2f4751a8e0408ff10d2662bd0c8bbfa84755a699f3bada2978b2 \ + --hash=sha256:5d96d986a21493606a358cae4461bd8cdf83cbf33a5aa950ae629ca3b51467ee +typing-extensions==4.10.0 \ + --hash=sha256:69b1a937c3a517342112fb4c6df7e72fc39a38e7891a5730ed4985b5214b5475 \ + --hash=sha256:b0abd7c89e8fb96f98db18d86106ff1d90ab692004eb746cf6eda2682f91b3cb + # via + # anyio + # fastapi + # pydantic + # pydantic-core + # starlette + # typer + # uvicorn +urllib3==1.26.18 \ + --hash=sha256:34b97092d7e0a3a8cf7cd10e386f401b3737364026c45e622aa02903dffe0f07 \ + --hash=sha256:f8ecc1bba5667413457c529ab955bf8c67b45db799d159066261719e328580a0 + # via botocore +uvicorn==0.27.1 \ + --hash=sha256:3d9a267296243532db80c83a959a3400502165ade2c1338dea4e67915fd4745a \ + --hash=sha256:5c89da2f3895767472a35556e539fd59f7edbe9b1e9c0e1c99eebeadc61838e4 + # via ghga-service-commons +uvloop==0.19.0 \ + --hash=sha256:0246f4fd1bf2bf702e06b0d45ee91677ee5c31242f39aab4ea6fe0c51aedd0fd \ + --hash=sha256:02506dc23a5d90e04d4f65c7791e65cf44bd91b37f24cfc3ef6cf2aff05dc7ec \ + --hash=sha256:13dfdf492af0aa0a0edf66807d2b465607d11c4fa48f4a1fd41cbea5b18e8e8b \ + --hash=sha256:2693049be9d36fef81741fddb3f441673ba12a34a704e7b4361efb75cf30befc \ + --hash=sha256:271718e26b3e17906b28b67314c45d19106112067205119dddbd834c2b7ce797 \ + --hash=sha256:2df95fca285a9f5bfe730e51945ffe2fa71ccbfdde3b0da5772b4ee4f2e770d5 \ + --hash=sha256:31e672bb38b45abc4f26e273be83b72a0d28d074d5b370fc4dcf4c4eb15417d2 \ + --hash=sha256:34175c9fd2a4bc3adc1380e1261f60306344e3407c20a4d684fd5f3be010fa3d \ + --hash=sha256:45bf4c24c19fb8a50902ae37c5de50da81de4922af65baf760f7c0c42e1088be \ + --hash=sha256:472d61143059c84947aa8bb74eabbace30d577a03a1805b77933d6bd13ddebbd \ + --hash=sha256:47bf3e9312f63684efe283f7342afb414eea4d3011542155c7e625cd799c3b12 \ + --hash=sha256:492e2c32c2af3f971473bc22f086513cedfc66a130756145a931a90c3958cb17 \ + --hash=sha256:4ce6b0af8f2729a02a5d1575feacb2a94fc7b2e983868b009d51c9a9d2149bef \ + --hash=sha256:5138821e40b0c3e6c9478643b4660bd44372ae1e16a322b8fc07478f92684e24 \ + --hash=sha256:5588bd21cf1fcf06bded085f37e43ce0e00424197e7c10e77afd4bbefffef428 \ + --hash=sha256:570fc0ed613883d8d30ee40397b79207eedd2624891692471808a95069a007c1 \ + --hash=sha256:5a05128d315e2912791de6088c34136bfcdd0c7cbc1cf85fd6fd1bb321b7c849 \ + --hash=sha256:5daa304d2161d2918fa9a17d5635099a2f78ae5b5960e742b2fcfbb7aefaa593 \ + --hash=sha256:5f17766fb6da94135526273080f3455a112f82570b2ee5daa64d682387fe0dcd \ + --hash=sha256:6e3d4e85ac060e2342ff85e90d0c04157acb210b9ce508e784a944f852a40e67 \ + --hash=sha256:7010271303961c6f0fe37731004335401eb9075a12680738731e9c92ddd96ad6 \ + --hash=sha256:7207272c9520203fea9b93843bb775d03e1cf88a80a936ce760f60bb5add92f3 \ + --hash=sha256:78ab247f0b5671cc887c31d33f9b3abfb88d2614b84e4303f1a63b46c046c8bd \ + --hash=sha256:7b1fd71c3843327f3bbc3237bedcdb6504fd50368ab3e04d0410e52ec293f5b8 \ + --hash=sha256:8ca4956c9ab567d87d59d49fa3704cf29e37109ad348f2d5223c9bf761a332e7 \ + --hash=sha256:91ab01c6cd00e39cde50173ba4ec68a1e578fee9279ba64f5221810a9e786533 \ + --hash=sha256:cd81bdc2b8219cb4b2556eea39d2e36bfa375a2dd021404f90a62e44efaaf957 \ + --hash=sha256:da8435a3bd498419ee8c13c34b89b5005130a476bda1d6ca8cfdde3de35cd650 \ + --hash=sha256:de4313d7f575474c8f5a12e163f6d89c0a878bc49219641d49e6f1444369a90e \ + --hash=sha256:e27f100e1ff17f6feeb1f33968bc185bf8ce41ca557deee9d9bbbffeb72030b7 \ + --hash=sha256:f467a5fd23b4fc43ed86342641f3936a68ded707f4627622fa3f82a120e18256 + # via uvicorn +watchfiles==0.21.0 \ + --hash=sha256:02b73130687bc3f6bb79d8a170959042eb56eb3a42df3671c79b428cd73f17cc \ + --hash=sha256:02d91cbac553a3ad141db016e3350b03184deaafeba09b9d6439826ee594b365 \ + --hash=sha256:06247538e8253975bdb328e7683f8515ff5ff041f43be6c40bff62d989b7d0b0 \ + --hash=sha256:08dca260e85ffae975448e344834d765983237ad6dc308231aa16e7933db763e \ + --hash=sha256:0d9ac347653ebd95839a7c607608703b20bc07e577e870d824fa4801bc1cb124 \ + --hash=sha256:0dd5fad9b9c0dd89904bbdea978ce89a2b692a7ee8a0ce19b940e538c88a809c \ + --hash=sha256:11cd0c3100e2233e9c53106265da31d574355c288e15259c0d40a4405cbae317 \ + --hash=sha256:18722b50783b5e30a18a8a5db3006bab146d2b705c92eb9a94f78c72beb94094 \ + --hash=sha256:18d5b4da8cf3e41895b34e8c37d13c9ed294954907929aacd95153508d5d89d7 \ + --hash=sha256:1ad7247d79f9f55bb25ab1778fd47f32d70cf36053941f07de0b7c4e96b5d235 \ + --hash=sha256:1b8d1eae0f65441963d805f766c7e9cd092f91e0c600c820c764a4ff71a0764c \ + --hash=sha256:1bd467213195e76f838caf2c28cd65e58302d0254e636e7c0fca81efa4a2e62c \ + --hash=sha256:1c9198c989f47898b2c22201756f73249de3748e0fc9de44adaf54a8b259cc0c \ + --hash=sha256:1fd9a5205139f3c6bb60d11f6072e0552f0a20b712c85f43d42342d162be1235 \ + --hash=sha256:214cee7f9e09150d4fb42e24919a1e74d8c9b8a9306ed1474ecaddcd5479c293 \ + --hash=sha256:27b4035013f1ea49c6c0b42d983133b136637a527e48c132d368eb19bf1ac6aa \ + --hash=sha256:3a23092a992e61c3a6a70f350a56db7197242f3490da9c87b500f389b2d01eef \ + --hash=sha256:3ad692bc7792be8c32918c699638b660c0de078a6cbe464c46e1340dadb94c19 \ + --hash=sha256:3ccceb50c611c433145502735e0370877cced72a6c70fd2410238bcbc7fe51d8 \ + --hash=sha256:3d0f32ebfaa9c6011f8454994f86108c2eb9c79b8b7de00b36d558cadcedaa3d \ + --hash=sha256:3f92944efc564867bbf841c823c8b71bb0be75e06b8ce45c084b46411475a915 \ + --hash=sha256:40bca549fdc929b470dd1dbfcb47b3295cb46a6d2c90e50588b0a1b3bd98f429 \ + --hash=sha256:43babacef21c519bc6631c5fce2a61eccdfc011b4bcb9047255e9620732c8097 \ + --hash=sha256:4566006aa44cb0d21b8ab53baf4b9c667a0ed23efe4aaad8c227bfba0bf15cbe \ + --hash=sha256:49f56e6ecc2503e7dbe233fa328b2be1a7797d31548e7a193237dcdf1ad0eee0 \ + --hash=sha256:4c48a10d17571d1275701e14a601e36959ffada3add8cdbc9e5061a6e3579a5d \ + --hash=sha256:4ea10a29aa5de67de02256a28d1bf53d21322295cb00bd2d57fcd19b850ebd99 \ + --hash=sha256:511f0b034120cd1989932bf1e9081aa9fb00f1f949fbd2d9cab6264916ae89b1 \ + --hash=sha256:51ddac60b96a42c15d24fbdc7a4bfcd02b5a29c047b7f8bf63d3f6f5a860949a \ + --hash=sha256:57d430f5fb63fea141ab71ca9c064e80de3a20b427ca2febcbfcef70ff0ce895 \ + --hash=sha256:59137c0c6826bd56c710d1d2bda81553b5e6b7c84d5a676747d80caf0409ad94 \ + --hash=sha256:5a03651352fc20975ee2a707cd2d74a386cd303cc688f407296064ad1e6d1562 \ + --hash=sha256:5eb86c6acb498208e7663ca22dbe68ca2cf42ab5bf1c776670a50919a56e64ab \ + --hash=sha256:642d66b75eda909fd1112d35c53816d59789a4b38c141a96d62f50a3ef9b3360 \ + --hash=sha256:6674b00b9756b0af620aa2a3346b01f8e2a3dc729d25617e1b89cf6af4a54eb1 \ + --hash=sha256:668c265d90de8ae914f860d3eeb164534ba2e836811f91fecc7050416ee70aa7 \ + --hash=sha256:66fac0c238ab9a2e72d026b5fb91cb902c146202bbd29a9a1a44e8db7b710b6f \ + --hash=sha256:6c107ea3cf2bd07199d66f156e3ea756d1b84dfd43b542b2d870b77868c98c03 \ + --hash=sha256:6c889025f59884423428c261f212e04d438de865beda0b1e1babab85ef4c0f01 \ + --hash=sha256:6cb8fdc044909e2078c248986f2fc76f911f72b51ea4a4fbbf472e01d14faa58 \ + --hash=sha256:6e9be3ef84e2bb9710f3f777accce25556f4a71e15d2b73223788d528fcc2052 \ + --hash=sha256:7f762a1a85a12cc3484f77eee7be87b10f8c50b0b787bb02f4e357403cad0c0e \ + --hash=sha256:83a696da8922314ff2aec02987eefb03784f473281d740bf9170181829133765 \ + --hash=sha256:853853cbf7bf9408b404754b92512ebe3e3a83587503d766d23e6bf83d092ee6 \ + --hash=sha256:8ad3fe0a3567c2f0f629d800409cd528cb6251da12e81a1f765e5c5345fd0137 \ + --hash=sha256:8c6ed10c2497e5fedadf61e465b3ca12a19f96004c15dcffe4bd442ebadc2d85 \ + --hash=sha256:8d5f400326840934e3507701f9f7269247f7c026d1b6cfd49477d2be0933cfca \ + --hash=sha256:927c589500f9f41e370b0125c12ac9e7d3a2fd166b89e9ee2828b3dda20bfe6f \ + --hash=sha256:9a0aa47f94ea9a0b39dd30850b0adf2e1cd32a8b4f9c7aa443d852aacf9ca214 \ + --hash=sha256:9b37a7ba223b2f26122c148bb8d09a9ff312afca998c48c725ff5a0a632145f7 \ + --hash=sha256:9c873345680c1b87f1e09e0eaf8cf6c891b9851d8b4d3645e7efe2ec20a20cc7 \ + --hash=sha256:9d09869f2c5a6f2d9df50ce3064b3391d3ecb6dced708ad64467b9e4f2c9bef3 \ + --hash=sha256:9d353c4cfda586db2a176ce42c88f2fc31ec25e50212650c89fdd0f560ee507b \ + --hash=sha256:a1e3014a625bcf107fbf38eece0e47fa0190e52e45dc6eee5a8265ddc6dc5ea7 \ + --hash=sha256:a3b9bec9579a15fb3ca2d9878deae789df72f2b0fdaf90ad49ee389cad5edab6 \ + --hash=sha256:ab03a90b305d2588e8352168e8c5a1520b721d2d367f31e9332c4235b30b8994 \ + --hash=sha256:aff06b2cac3ef4616e26ba17a9c250c1fe9dd8a5d907d0193f84c499b1b6e6a9 \ + --hash=sha256:b3cab0e06143768499384a8a5efb9c4dc53e19382952859e4802f294214f36ec \ + --hash=sha256:b4a21f71885aa2744719459951819e7bf5a906a6448a6b2bbce8e9cc9f2c8128 \ + --hash=sha256:b6d45d9b699ecbac6c7bd8e0a2609767491540403610962968d258fd6405c17c \ + --hash=sha256:be6dd5d52b73018b21adc1c5d28ac0c68184a64769052dfeb0c5d9998e7f56a2 \ + --hash=sha256:c550a56bf209a3d987d5a975cdf2063b3389a5d16caf29db4bdddeae49f22078 \ + --hash=sha256:c76c635fabf542bb78524905718c39f736a98e5ab25b23ec6d4abede1a85a6a3 \ + --hash=sha256:c81818595eff6e92535ff32825f31c116f867f64ff8cdf6562cd1d6b2e1e8f3e \ + --hash=sha256:cfb92d49dbb95ec7a07511bc9efb0faff8fe24ef3805662b8d6808ba8409a71a \ + --hash=sha256:d23bcd6c8eaa6324fe109d8cac01b41fe9a54b8c498af9ce464c1aeeb99903d6 \ + --hash=sha256:d5b1dc0e708fad9f92c296ab2f948af403bf201db8fb2eb4c8179db143732e49 \ + --hash=sha256:d78f30cbe8b2ce770160d3c08cff01b2ae9306fe66ce899b73f0409dc1846c1b \ + --hash=sha256:d8f57c4461cd24fda22493109c45b3980863c58a25b8bec885ca8bea6b8d4b28 \ + --hash=sha256:d9792dff410f266051025ecfaa927078b94cc7478954b06796a9756ccc7e14a9 \ + --hash=sha256:e7941bbcfdded9c26b0bf720cb7e6fd803d95a55d2c14b4bd1f6a2772230c586 \ + --hash=sha256:ebe684d7d26239e23d102a2bad2a358dedf18e462e8808778703427d1f584400 \ + --hash=sha256:ec8c8900dc5c83650a63dd48c4d1d245343f904c4b64b48798c67a3767d7e165 \ + --hash=sha256:f564bf68404144ea6b87a78a3f910cc8de216c6b12a4cf0b27718bf4ec38d303 \ + --hash=sha256:fd7ac678b92b29ba630d8c842d8ad6c555abda1b9ef044d6cc092dacbfc9719d + # via uvicorn +websockets==12.0 \ + --hash=sha256:00700340c6c7ab788f176d118775202aadea7602c5cc6be6ae127761c16d6b0b \ + --hash=sha256:0bee75f400895aef54157b36ed6d3b308fcab62e5260703add87f44cee9c82a6 \ + --hash=sha256:0e6e2711d5a8e6e482cacb927a49a3d432345dfe7dea8ace7b5790df5932e4df \ + --hash=sha256:12743ab88ab2af1d17dd4acb4645677cb7063ef4db93abffbf164218a5d54c6b \ + --hash=sha256:1a9d160fd080c6285e202327aba140fc9a0d910b09e423afff4ae5cbbf1c7205 \ + --hash=sha256:1bf386089178ea69d720f8db6199a0504a406209a0fc23e603b27b300fdd6892 \ + --hash=sha256:1df2fbd2c8a98d38a66f5238484405b8d1d16f929bb7a33ed73e4801222a6f53 \ + --hash=sha256:1e4b3f8ea6a9cfa8be8484c9221ec0257508e3a1ec43c36acdefb2a9c3b00aa2 \ + --hash=sha256:1f38a7b376117ef7aff996e737583172bdf535932c9ca021746573bce40165ed \ + --hash=sha256:23509452b3bc38e3a057382c2e941d5ac2e01e251acce7adc74011d7d8de434c \ + --hash=sha256:248d8e2446e13c1d4326e0a6a4e9629cb13a11195051a73acf414812700badbd \ + --hash=sha256:25eb766c8ad27da0f79420b2af4b85d29914ba0edf69f547cc4f06ca6f1d403b \ + --hash=sha256:27a5e9964ef509016759f2ef3f2c1e13f403725a5e6a1775555994966a66e931 \ + --hash=sha256:2c71bd45a777433dd9113847af751aae36e448bc6b8c361a566cb043eda6ec30 \ + --hash=sha256:2cb388a5bfb56df4d9a406783b7f9dbefb888c09b71629351cc6b036e9259370 \ + --hash=sha256:2d225bb6886591b1746b17c0573e29804619c8f755b5598d875bb4235ea639be \ + --hash=sha256:2e5fc14ec6ea568200ea4ef46545073da81900a2b67b3e666f04adf53ad452ec \ + --hash=sha256:363f57ca8bc8576195d0540c648aa58ac18cf85b76ad5202b9f976918f4219cf \ + --hash=sha256:3c6cc1360c10c17463aadd29dd3af332d4a1adaa8796f6b0e9f9df1fdb0bad62 \ + --hash=sha256:3d829f975fc2e527a3ef2f9c8f25e553eb7bc779c6665e8e1d52aa22800bb38b \ + --hash=sha256:3e3aa8c468af01d70332a382350ee95f6986db479ce7af14d5e81ec52aa2b402 \ + --hash=sha256:3f61726cae9f65b872502ff3c1496abc93ffbe31b278455c418492016e2afc8f \ + --hash=sha256:423fc1ed29f7512fceb727e2d2aecb952c46aa34895e9ed96071821309951123 \ + --hash=sha256:46e71dbbd12850224243f5d2aeec90f0aaa0f2dde5aeeb8fc8df21e04d99eff9 \ + --hash=sha256:4d87be612cbef86f994178d5186add3d94e9f31cc3cb499a0482b866ec477603 \ + --hash=sha256:5693ef74233122f8ebab026817b1b37fe25c411ecfca084b29bc7d6efc548f45 \ + --hash=sha256:5aa9348186d79a5f232115ed3fa9020eab66d6c3437d72f9d2c8ac0c6858c558 \ + --hash=sha256:5d873c7de42dea355d73f170be0f23788cf3fa9f7bed718fd2830eefedce01b4 \ + --hash=sha256:5f6ffe2c6598f7f7207eef9a1228b6f5c818f9f4d53ee920aacd35cec8110438 \ + --hash=sha256:604428d1b87edbf02b233e2c207d7d528460fa978f9e391bd8aaf9c8311de137 \ + --hash=sha256:6350b14a40c95ddd53e775dbdbbbc59b124a5c8ecd6fbb09c2e52029f7a9f480 \ + --hash=sha256:6e2df67b8014767d0f785baa98393725739287684b9f8d8a1001eb2839031447 \ + --hash=sha256:6e96f5ed1b83a8ddb07909b45bd94833b0710f738115751cdaa9da1fb0cb66e8 \ + --hash=sha256:6e9e7db18b4539a29cc5ad8c8b252738a30e2b13f033c2d6e9d0549b45841c04 \ + --hash=sha256:70ec754cc2a769bcd218ed8d7209055667b30860ffecb8633a834dde27d6307c \ + --hash=sha256:7b645f491f3c48d3f8a00d1fce07445fab7347fec54a3e65f0725d730d5b99cb \ + --hash=sha256:7fa3d25e81bfe6a89718e9791128398a50dec6d57faf23770787ff441d851967 \ + --hash=sha256:81df9cbcbb6c260de1e007e58c011bfebe2dafc8435107b0537f393dd38c8b1b \ + --hash=sha256:8572132c7be52632201a35f5e08348137f658e5ffd21f51f94572ca6c05ea81d \ + --hash=sha256:87b4aafed34653e465eb77b7c93ef058516cb5acf3eb21e42f33928616172def \ + --hash=sha256:8e332c210b14b57904869ca9f9bf4ca32f5427a03eeb625da9b616c85a3a506c \ + --hash=sha256:9893d1aa45a7f8b3bc4510f6ccf8db8c3b62120917af15e3de247f0780294b92 \ + --hash=sha256:9edf3fc590cc2ec20dc9d7a45108b5bbaf21c0d89f9fd3fd1685e223771dc0b2 \ + --hash=sha256:9fdf06fd06c32205a07e47328ab49c40fc1407cdec801d698a7c41167ea45113 \ + --hash=sha256:a02413bc474feda2849c59ed2dfb2cddb4cd3d2f03a2fedec51d6e959d9b608b \ + --hash=sha256:a1d9697f3337a89691e3bd8dc56dea45a6f6d975f92e7d5f773bc715c15dde28 \ + --hash=sha256:a571f035a47212288e3b3519944f6bf4ac7bc7553243e41eac50dd48552b6df7 \ + --hash=sha256:ab3d732ad50a4fbd04a4490ef08acd0517b6ae6b77eb967251f4c263011a990d \ + --hash=sha256:ae0a5da8f35a5be197f328d4727dbcfafa53d1824fac3d96cdd3a642fe09394f \ + --hash=sha256:b067cb952ce8bf40115f6c19f478dc71c5e719b7fbaa511359795dfd9d1a6468 \ + --hash=sha256:b2ee7288b85959797970114deae81ab41b731f19ebcd3bd499ae9ca0e3f1d2c8 \ + --hash=sha256:b81f90dcc6c85a9b7f29873beb56c94c85d6f0dac2ea8b60d995bd18bf3e2aae \ + --hash=sha256:ba0cab91b3956dfa9f512147860783a1829a8d905ee218a9837c18f683239611 \ + --hash=sha256:baa386875b70cbd81798fa9f71be689c1bf484f65fd6fb08d051a0ee4e79924d \ + --hash=sha256:bbe6013f9f791944ed31ca08b077e26249309639313fff132bfbf3ba105673b9 \ + --hash=sha256:bea88d71630c5900690fcb03161ab18f8f244805c59e2e0dc4ffadae0a7ee0ca \ + --hash=sha256:befe90632d66caaf72e8b2ed4d7f02b348913813c8b0a32fae1cc5fe3730902f \ + --hash=sha256:c3181df4583c4d3994d31fb235dc681d2aaad744fbdbf94c4802485ececdecf2 \ + --hash=sha256:c4e37d36f0d19f0a4413d3e18c0d03d0c268ada2061868c1e6f5ab1a6d575077 \ + --hash=sha256:c588f6abc13f78a67044c6b1273a99e1cf31038ad51815b3b016ce699f0d75c2 \ + --hash=sha256:cbe83a6bbdf207ff0541de01e11904827540aa069293696dd528a6640bd6a5f6 \ + --hash=sha256:d554236b2a2006e0ce16315c16eaa0d628dab009c33b63ea03f41c6107958374 \ + --hash=sha256:dbcf72a37f0b3316e993e13ecf32f10c0e1259c28ffd0a85cee26e8549595fbc \ + --hash=sha256:dc284bbc8d7c78a6c69e0c7325ab46ee5e40bb4d50e494d8131a07ef47500e9e \ + --hash=sha256:dff6cdf35e31d1315790149fee351f9e52978130cef6c87c4b6c9b3baf78bc53 \ + --hash=sha256:e469d01137942849cff40517c97a30a93ae79917752b34029f0ec72df6b46399 \ + --hash=sha256:eb809e816916a3b210bed3c82fb88eaf16e8afcf9c115ebb2bacede1797d2547 \ + --hash=sha256:ed2fcf7a07334c77fc8a230755c2209223a7cc44fc27597729b8ef5425aa61a3 \ + --hash=sha256:f44069528d45a933997a6fef143030d8ca8042f0dfaad753e2906398290e2870 \ + --hash=sha256:f764ba54e33daf20e167915edc443b6f88956f37fb606449b4a5b10ba42235a5 \ + --hash=sha256:fc4e7fa5414512b481a2483775a8e8be7803a35b30ca805afa4998a84f9fd9e8 \ + --hash=sha256:ffefa1374cd508d633646d51a8e9277763a9b78ae71324183693959cf94635a7 + # via uvicorn diff --git a/openapi.yaml b/openapi.yaml new file mode 100644 index 00000000..bcc3fab5 --- /dev/null +++ b/openapi.yaml @@ -0,0 +1,113 @@ +components: + schemas: + Greeting: + description: A container storing a greeting for a specific person incl. metadata. + properties: + created_at: + description: The date/time when the message was created + format: date-time + title: Created At + type: string + isinformal: + description: Is the expression used in informal contexts? + title: Isinformal + type: boolean + language: + description: The language. + enum: + - Greek + - Croatian + - French + - German + title: Language + type: string + message: + description: The message content. + title: Message + type: string + required: + - message + - created_at + - language + - isinformal + title: Greeting + type: object + HTTPValidationError: + properties: + detail: + items: + $ref: '#/components/schemas/ValidationError' + title: Detail + type: array + title: HTTPValidationError + type: object + ValidationError: + properties: + loc: + items: + anyOf: + - type: string + - type: integer + title: Location + type: array + msg: + title: Message + type: string + type: + title: Error Type + type: string + required: + - loc + - msg + - type + title: ValidationError + type: object +info: + title: FastAPI + version: 0.1.0 +openapi: 3.1.0 +paths: + /: + get: + description: Greet the World. + operationId: index__get + responses: + '200': + content: + application/json: + schema: {} + description: Successful Response + summary: Greet the world + /greet/{name}: + get: + description: Greet a person by name. You may choose a formal or an informal + greeting.The language for the greeting is configured in the backend. + operationId: greet_greet__name__get + parameters: + - in: path + name: name + required: true + schema: + title: Name + type: string + - in: query + name: isinformal + required: false + schema: + default: true + title: Isinformal + type: boolean + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/Greeting' + description: Successful Response + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + description: Validation Error + summary: Greet a person diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..21d97b62 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,154 @@ +[build-system] +requires = [ + "setuptools>=67.7.2", +] +build-backend = "setuptools.build_meta" + +[project] +readme = "README.md" +authors = [ + { name = "German Human Genome Phenome Archive (GHGA)", email = "contact@ghga.de" }, +] +requires-python = ">=3.9" +classifiers = [ + "Development Status :: 1 - Planning", + "Operating System :: POSIX :: Linux", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "License :: OSI Approved :: Apache Software License", + "Topic :: Internet :: WWW/HTTP :: HTTP Servers", + "Topic :: Software Development :: Libraries", + "Intended Audience :: Developers", +] +name = "my_microservice" +version = "0.1.0" +description = "My-Microservice - a short description" +dependencies = [ + "typer >= 0.9.0", + "ghga-service-commons[api] >= 1.2.0", + "ghga-event-schemas >= 1.0.0", + "hexkit[akafka,s3,mongodb] >= 1.1.0", +] + +[project.license] +text = "Apache 2.0" + +[project.urls] +Repository = "https://github.com/ghga-de/my-microservice" + +[project.scripts] +my-microservice = "my_microservice.__main__:run" + +[tool.setuptools.packages.find] +where = [ + "src", +] + +[tool.ruff] +exclude = [ + ".git", + ".devcontainer", + "__pycache__", + "build", + "dist", +] +line-length = 88 +src = [ + "src", + "tests", + "examples", + "scripts", +] +target-version = "py39" + +[tool.ruff.lint] +fixable = [ + "UP", + "I", + "D", +] +ignore = [ + "E", + "W", + "PLW", + "RUF001", + "RUF010", + "RUF012", + "N818", + "B008", + "PLR2004", + "D205", + "D400", + "D401", + "D107", + "D206", + "D300", +] +select = [ + "C90", + "F", + "I", + "S", + "B", + "N", + "UP", + "PL", + "RUF", + "SIM", + "D", +] + +[tool.ruff.lint.mccabe] +max-complexity = 10 + +[tool.ruff.lint.per-file-ignores] +"scripts/*" = [ + "PL", + "S", + "SIM", + "D", +] +"tests/*" = [ + "S", + "SIM", + "PLR", + "B011", +] +".devcontainer/*" = [ + "S", + "SIM", + "D", +] +"examples/*" = [ + "S", + "D", +] +"__init__.py" = [ + "D", +] + +[tool.ruff.lint.pydocstyle] +convention = "pep257" + +[tool.mypy] +disable_error_code = "import" +show_error_codes = true +exclude = [ + "build/lib/", +] +warn_redundant_casts = true +warn_unused_ignores = true +check_untyped_defs = true +no_site_packages = false + +[tool.pytest.ini_options] +minversion = "7.1" +asyncio_mode = "strict" + +[tool.coverage.paths] +source = [ + "src", + "/workspace/src", + "**/lib/python*/site-packages", +] diff --git a/scripts/README.md b/scripts/README.md new file mode 100644 index 00000000..e09caf3c --- /dev/null +++ b/scripts/README.md @@ -0,0 +1,3 @@ +# Scripts and Development Utilities +This directory contains scripts and other utils that +may be used during development or by an automated CI system. diff --git a/scripts/__init__.py b/scripts/__init__.py new file mode 100644 index 00000000..6222ab0b --- /dev/null +++ b/scripts/__init__.py @@ -0,0 +1,17 @@ +# Copyright 2021 - 2023 Universität Tübingen, DKFZ, EMBL, and Universität zu Köln +# for the German Human Genome-Phenome Archive (GHGA) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +"""Scripts and utils used during development or in CI pipelines.""" diff --git a/scripts/get_package_name.py b/scripts/get_package_name.py new file mode 100755 index 00000000..84d15fd9 --- /dev/null +++ b/scripts/get_package_name.py @@ -0,0 +1,46 @@ +#!/usr/bin/env python3 + +# Copyright 2021 - 2023 Universität Tübingen, DKFZ, EMBL, and Universität zu Köln +# for the German Human Genome-Phenome Archive (GHGA) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Extracts the package name from pyproject.toml""" + +from pathlib import Path + +REPO_ROOT_DIR = Path(__file__).parent.parent.resolve() +PYPROJECT_TOML_PATH = REPO_ROOT_DIR / "pyproject.toml" +NAME_PREFIX = "name = " + + +def get_package_name() -> str: + """Extracts the package name""" + + with open(PYPROJECT_TOML_PATH, encoding="utf8") as pyproject_toml: + for line in pyproject_toml.readlines(): + line_stripped = line.strip() + if line_stripped.startswith(NAME_PREFIX): + package_name = line_stripped[len(NAME_PREFIX) :] + return package_name.strip('"') + raise RuntimeError("Could not find package name.") + + +def run(): + """Run this script.""" + package_name = get_package_name() + print(package_name) + + +if __name__ == "__main__": + run() diff --git a/scripts/license_checker.py b/scripts/license_checker.py new file mode 100755 index 00000000..b6a718df --- /dev/null +++ b/scripts/license_checker.py @@ -0,0 +1,568 @@ +#!/usr/bin/env python3 + +# Copyright 2021 - 2023 Universität Tübingen, DKFZ, EMBL, and Universität zu Köln +# for the German Human Genome-Phenome Archive (GHGA) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""This script checks that the license and license headers +exist and that they are up to date. +""" + +import argparse +import re +import sys +from datetime import date +from pathlib import Path +from typing import Optional, Union + +# root directory of the package: +ROOT_DIR = Path(__file__).parent.parent.resolve() + +# file containing the default global copyright notice: +GLOBAL_COPYRIGHT_FILE_PATH = ROOT_DIR / ".devcontainer" / "license_header.txt" + +# exclude files and dirs from license header check: +EXCLUDE = [ + ".devcontainer", + "eggs", + ".eggs", + "dist", + "build", + "develop-eggs", + "lib", + "lib62", + "parts", + "sdist", + "wheels", + "pip-wheel-metadata", + ".coveragerc", + ".git", + ".github", + ".flake8", + ".gitignore", + ".pylintrc", + ".ruff.toml", + ".ruff_cache", + "example_config.yaml", + "config_schema.json", + "LICENSE", # is checked but not for the license header + ".pre-commit-config.yaml", + "docs", + ".vscode", + ".mypy_cache", + ".mypy.ini", + ".pytest_cache", + ".editorconfig", + ".template/.static_files.txt", + ".template/.static_files_ignore.txt", + ".template/.mandatory_files.txt", + ".template/.mandatory_files_ignore.txt", + ".template/.deprecated_files.txt", + ".template/.deprecated_files_ignore.txt", +] + +# exclude file by file ending from license header check: +EXCLUDE_ENDINGS = [ + "html", + "in", + "ini", + "jinja", + "json", + "md", + "pub", + "pyc", + "sec", + "toml", + "txt", + "xml", + "yaml", + "yml", + "tsv", + "fastq", + "gz", +] + +# exclude any files with names that match any of the following regex: +EXCLUDE_PATTERN = [r".*\.egg-info.*", r".*__cache__.*", r".*\.git.*"] + +# The License header, "{year}" will be replaced by current year: +COPYRIGHT_TEMPLATE = """Copyright {year} {author} + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License.""" + +# A list of all chars that may be used to introduce a comment: +COMMENT_CHARS = ["#"] + +AUTHOR = """Universität Tübingen, DKFZ, EMBL, and Universität zu Köln +for the German Human Genome-Phenome Archive (GHGA)""" + +# The copyright notice should not date earlier than this year: +MIN_YEAR = 2021 + +# The path to the License file relative to target dir +LICENSE_FILE = "LICENSE" + + +class GlobalCopyrightNotice: + """ + This is used to store the copyright notice that should be identical for all checked + files. + The text of the copyright notice is stored in the `text` + property. This property can only be set once. + The property `n_lines` gives the number of lines of the text. It is inferred once + `text` is set. + """ + + def __init__(self): + self._text: Optional[str] = None + self._n_lines: Optional[int] = None + + @property + def text(self) -> Optional[str]: + return self._text + + @text.setter + def text(self, new_text: str): + if self._text is not None: + raise RuntimeError("You can only set the value once.") + self._text = new_text + self._n_lines = len(self._text.split("\n")) + + @property + def n_lines(self) -> int: + if self._n_lines is None: + raise ValueError( + "This property is not yet available." + + " Please set the `text` property first." + ) + return self._n_lines + + +class UnexpectedBinaryFileError(RuntimeError): + """Thrown when trying to read a binary file.""" + + def __init__(self, file_path: Union[str, Path]): + message = f"The file could not be read because it is binary: {str(file_path)}" + super().__init__(message) + + +def get_target_files( + target_dir: Path, + exclude: list[str] = EXCLUDE, + exclude_endings: list[str] = EXCLUDE_ENDINGS, + exclude_pattern: list[str] = EXCLUDE_PATTERN, +) -> list[Path]: + """Get target files that are not match the exclude conditions. + Args: + target_dir (pathlib.Path): The target dir to search. + exclude (List[str], optional): + Overwrite default list of file/dir paths relative to + the target dir that shall be excluded. + exclude_endings (List[str], optional): + Overwrite default list of file endings that shall + be excluded. + exclude_pattern (List[str], optional): + Overwrite default list of regex patterns match file path + for exclusion. + """ + abs_target_dir = Path(target_dir).absolute() + exclude_normalized = [(abs_target_dir / excl).absolute() for excl in exclude] + + # get all files: + all_files = [ + file_.absolute() for file_ in Path(abs_target_dir).rglob("*") if file_.is_file() + ] + + target_files = [ + file_ + for file_ in all_files + if not ( + any([file_.is_relative_to(excl) for excl in exclude_normalized]) + or any([str(file_).endswith(ending) for ending in exclude_endings]) + or any([re.match(pattern, str(file_)) for pattern in exclude_pattern]) + ) + ] + return target_files + + +def normalized_line(line: str, chars_to_trim: list[str] = COMMENT_CHARS) -> str: + norm_line = line.strip() + + for char in chars_to_trim: + norm_line = norm_line.strip(char) + + return norm_line.strip("\n").strip("\t").strip() + + +def normalized_text(text: str, chars_to_trim: list[str] = COMMENT_CHARS) -> str: + "Normalize a license header text." + lines = text.split("\n") + + norm_lines: list[str] = [] + + for line in lines: + stripped_line = line.strip() + # exclude shebang: + if stripped_line.startswith("#!"): + continue + + norm_line = normalized_line(stripped_line) + + # exclude empty lines: + if norm_line == "": + continue + + norm_lines.append(norm_line) + + return "\n".join(norm_lines).strip("\n") + + +def format_copyright_template(copyright_template: str, author: str) -> str: + """Formats license header by inserting the specified author for every occurrence of + "{author}" in the header template. + """ + return normalized_text(copyright_template.replace("{author}", author)) + + +def is_commented_line(line: str, comment_chars: list[str] = COMMENT_CHARS) -> bool: + """Checks whether a line is a comment.""" + line_stripped = line.strip() + for comment_char in comment_chars: + if line_stripped.startswith(comment_char): + return True + + return False + + +def is_empty_line(line: str) -> bool: + """Checks whether a line is empty.""" + return line.strip("\n").strip("\t").strip() == "" + + +def get_header(file_path: Path, comment_chars: list[str] = COMMENT_CHARS): + """Extracts the header from a file and normalizes it.""" + header_lines: list[str] = [] + + try: + with open(file_path) as file: + for line in file: + if is_commented_line( + line, comment_chars=comment_chars + ) or is_empty_line(line): + header_lines.append(line) + else: + break + except UnicodeDecodeError as error: + raise UnexpectedBinaryFileError(file_path=file_path) from error + + # normalize the lines: + header = "".join(header_lines) + return normalized_text(header, chars_to_trim=comment_chars) + + +def validate_year_string(year_string: str, min_year: int = MIN_YEAR) -> bool: + """Check if the specified year string is valid. + Returns `True` if valid or `False` otherwise.""" + + current_year = date.today().year + + # If the year_string is a single number, it must be the current year: + if year_string.isnumeric(): + return int(year_string) == current_year + + # Otherwise, a range (e.g. 2021 - 2023) is expected: + match = re.match("(\d+) - (\d+)", year_string) + + if not match: + return False + + year_1 = int(match.group(1)) + year_2 = int(match.group(2)) + + # Check the validity of the range: + if year_1 >= min_year and year_2 <= year_1: + return False + + # year_2 must be equal to the current year: + return year_2 == current_year + + +def check_copyright_notice( + copyright: str, + global_copyright: GlobalCopyrightNotice, + copyright_template: str = COPYRIGHT_TEMPLATE, + author: str = AUTHOR, + comment_chars: list[str] = COMMENT_CHARS, + min_year: int = MIN_YEAR, +) -> bool: + """Checks the specified copyright text against a template. + + copyright_template (str): + A string containing the copyright text to check against the template. + global_copyright (str, None): + If this is a string, it is checked whether the copyright notice in this file + contains the same year string. + If this is None, the variable is set to the year string present in the + copyright notice of this file. + copyright_template (str, optional): + A string containing a template for the expected license header. + You may include "{year}" which will be replace by the current year. + This defaults to the Apache 2.0 Copyright notice. + author (str, optional): + The author that shall be included in the license header. + It will replace any appearance of "{author}" in the license + header. This defaults to an author info for GHGA. + + """ + # If the global_copyright is already set, check if the current copyright is + # identical to it: + copyright_lines = copyright.split("\n") + if global_copyright.text is not None: + copyright_cleaned = "\n".join(copyright_lines[0 : global_copyright.n_lines]) + return global_copyright.text == copyright_cleaned + + formatted_template = format_copyright_template(copyright_template, author=author) + template_lines = formatted_template.split("\n") + + # The header should be at least as long as the template: + if len(copyright_lines) < len(template_lines): + return False + + for idx, template_line in enumerate(template_lines): + header_line = copyright_lines[idx] + + if "{year}" in template_line: + pattern = template_line.replace("{year}", r"(.+?)") + match = re.match(pattern, header_line) + + if not match: + return False + + year_string = match.group(1) + if not validate_year_string(year_string, min_year=min_year): + return False + + elif template_line != header_line: + return False + + # Take this copyright as the global_copyright from now on: + copyright_cleaned = "\n".join(copyright_lines[0 : len(template_line)]) + global_copyright.text = copyright_cleaned + + return True + + +def check_file_headers( + target_dir: Path, + global_copyright: GlobalCopyrightNotice, + copyright_template: str = COPYRIGHT_TEMPLATE, + author: str = AUTHOR, + exclude: list[str] = EXCLUDE, + exclude_endings: list[str] = EXCLUDE_ENDINGS, + exclude_pattern: list[str] = EXCLUDE_PATTERN, + comment_chars: list[str] = COMMENT_CHARS, + min_year: int = MIN_YEAR, +) -> tuple[list[Path], list[Path]]: + """Check files for presence of a license header and verify that + the copyright notice is up to date (correct year). + + Args: + target_dir (pathlib.Path): The target dir to search. + copyright_template (str, optional): + A string containing a template for the expected license header. + You may include "{year}" which will be replace by the current year. + This defaults to the Apache 2.0 Copyright notice. + global_copyright (str, None): + If this is a string, it is checked whether the copyright notice of these + files contains the same year string. + If this is None, the variable is set to the year string present in the + copyright notice of these files. + author (str, optional): + The author that shall be included in the license header. + It will replace any appearance of "{author}" in the license + header. This defaults to an author info for GHGA. + exclude (List[str], optional): + Overwrite default list of file/dir paths relative to + the target dir that shall be excluded. + exclude_endings (List[str], optional): + Overwrite default list of file endings that shall + be excluded. + exclude_pattern (List[str], optional): + Overwrite default list of regex patterns match file path + for exclusion. + """ + target_files = get_target_files( + target_dir, + exclude=exclude, + exclude_endings=exclude_endings, + exclude_pattern=exclude_pattern, + ) + + # check if license header present in file: + passed_files: list[Path] = [] + failed_files: list[Path] = [] + + for target_file in target_files: + try: + header = get_header(target_file, comment_chars=comment_chars) + if check_copyright_notice( + copyright=header, + global_copyright=global_copyright, + copyright_template=copyright_template, + author=author, + comment_chars=comment_chars, + min_year=min_year, + ): + passed_files.append(target_file) + else: + failed_files.append(target_file) + except UnexpectedBinaryFileError: + # This file is a binary and is therefor skipped. + pass + + return (passed_files, failed_files) + + +def check_license_file( + license_file: Path, + global_copyright: GlobalCopyrightNotice, + copyright_template: str = COPYRIGHT_TEMPLATE, + author: str = AUTHOR, + comment_chars: list[str] = COMMENT_CHARS, + min_year: int = MIN_YEAR, +) -> bool: + """Currently only checks if the copyright notice in the + License file is up to data. + + Args: + license_file (pathlib.Path, optional): Overwrite the default license file. + global_copyright (str, None): + If this is a string, it is checked whether the copyright notice in this file + contains the same year string. + If this is None, the variable is set to the year string present in the + copyright notice of this file. + copyright_template (str, optional): + A string of the copyright notice (usually same as license header). + You may include "{year}" which will be replace by the current year. + This defaults to the Apache 2.0 Copyright notice. + author (str, optional): + The author that shall be included in the copyright notice. + It will replace any appearance of "{author}" in the copyright + notice. This defaults to an author info for GHGA. + """ + + if not license_file.is_file(): + print(f'Could not find license file "{str(license_file)}".') + return False + + with open(license_file) as file_: + license_text = normalized_text(file_.read()) + + # Extract the copyright notice: + # (is expected to be at the end of the file): + formatted_template = format_copyright_template(copyright_template, author=author) + template_lines = formatted_template.split("\n") + license_lines = license_text.split("\n") + copyright = "\n".join(license_lines[-len(template_lines) :]) + + return check_copyright_notice( + copyright=copyright, + global_copyright=global_copyright, + copyright_template=copyright_template, + author=author, + comment_chars=comment_chars, + min_year=min_year, + ) + + +def run(): + """Run checks from CLI.""" + parser = argparse.ArgumentParser( + prog="license-checker", + description=( + "This script checks that the license and license headers " + + "exists and that they are up to date." + ), + ) + + parser.add_argument( + "-L", + "--no-license-file-check", + help="Disables the check of the license file", + action="store_true", + ) + + parser.add_argument( + "-t", + "--target-dir", + help="Specify a custom target dir. Overwrites the default package root.", + ) + + args = parser.parse_args() + + target_dir = Path(args.target_dir).absolute() if args.target_dir else ROOT_DIR + + print(f'Working in "{target_dir}"\n') + + global_copyright = GlobalCopyrightNotice() + + # get global copyright from .devcontainer/license_header.txt file: + with open(GLOBAL_COPYRIGHT_FILE_PATH) as global_copyright_file: + global_copyright.text = normalized_text(global_copyright_file.read()) + + if args.no_license_file_check: + license_file_valid = True + else: + license_file = Path(target_dir / LICENSE_FILE) + print(f'Checking if LICENSE file is up to date: "{license_file}"') + license_file_valid = check_license_file( + license_file, global_copyright=global_copyright + ) + print( + "Copyright notice in license file is " + + ("" if license_file_valid else "not ") + + "up to date.\n" + ) + + print("Checking license headers in files:") + passed_files, failed_files = check_file_headers( + target_dir, global_copyright=global_copyright + ) + print(f"{len(passed_files)} files passed.") + print(f"{len(failed_files)} files failed" + (":" if failed_files else ".")) + for failed_file in failed_files: + print(f' - "{failed_file.relative_to(target_dir)}"') + + print("") + + if failed_files or not license_file_valid: + print("Some checks failed.") + sys.exit(1) + + print("All checks passed.") + sys.exit(0) + + +if __name__ == "__main__": + run() diff --git a/scripts/list_outdated_dependencies.py b/scripts/list_outdated_dependencies.py new file mode 100755 index 00000000..0546abb1 --- /dev/null +++ b/scripts/list_outdated_dependencies.py @@ -0,0 +1,212 @@ +#!/usr/bin/env python3 + +# Copyright 2021 - 2023 Universität Tübingen, DKFZ, EMBL, and Universität zu Köln +# for the German Human Genome-Phenome Archive (GHGA) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +"""Check capped dependencies for newer versions.""" + +import sys +from collections.abc import Sequence +from pathlib import Path +from typing import Any, NamedTuple + +import httpx +from packaging.requirements import Requirement + +from script_utils import cli, deps, lock_deps + +REPO_ROOT_DIR = Path(__file__).parent.parent.resolve() +PYPROJECT_TOML_PATH = REPO_ROOT_DIR / "pyproject.toml" +LOCK_DIR = REPO_ROOT_DIR / "lock" +DEV_DEPS_PATH = LOCK_DIR / "requirements-dev.in" +LOCK_FILE_PATH = LOCK_DIR / "requirements-dev.txt" + + +class OutdatedDep(NamedTuple): + """Encapsulates data of an outdated dependency""" + + name: str + specified_version: str + pypi_version: str + + +def get_main_deps_pyproject(modified_pyproject: dict[str, Any]) -> list[Requirement]: + """Get a list of the dependencies from pyproject.toml""" + + dependencies: list[str] = [] + if "dependencies" in modified_pyproject["project"]: + dependencies = modified_pyproject["project"]["dependencies"] + + return [Requirement(dependency) for dependency in dependencies] + + +def get_optional_deps_pyproject( + modified_pyproject: dict[str, Any], +) -> list[Requirement]: + """Get a list of the optional dependencies from pyproject.toml""" + + dependencies: list[str] = [] + + if "optional-dependencies" in modified_pyproject["project"]: + for optional_dependency_list in modified_pyproject["project"][ + "optional-dependencies" + ]: + dependencies.extend( + modified_pyproject["project"]["optional-dependencies"][ + optional_dependency_list + ] + ) + + return [Requirement(dependency) for dependency in dependencies] + + +def get_deps_dev() -> list[Requirement]: + """Get a list of raw dependency strings from requirements-dev.in""" + with open(DEV_DEPS_PATH, encoding="utf-8") as dev_deps: + dependencies = [ + line + for line in (line.strip() for line in dev_deps) + if line # skip empty lines + and not line.startswith("#") # skip comments + and "requirements-dev-template.in" not in line # skip inclusion line + ] + + return [Requirement(dependency) for dependency in dependencies] + + +def get_version_from_pypi(package_name: str, client: httpx.Client) -> str: + """Make a call to PyPI to get the version information about `package_name`.""" + try: + response = client.get(f"https://pypi.org/pypi/{package_name}/json") + body = response.json() + version = body["info"]["version"] + except (httpx.RequestError, KeyError): + cli.echo_failure(f"Unable to retrieve information for package '{package_name}'") + sys.exit(1) + + return version + + +def get_outdated_deps( + requirements: list[Requirement], strip: bool = False +) -> list[OutdatedDep]: + """Determine which packages have updates available outside of pinned ranges.""" + outdated: list[OutdatedDep] = [] + with httpx.Client(timeout=10) as client: + for requirement in requirements: + pypi_version = get_version_from_pypi(requirement.name, client) + + specified = str(requirement.specifier) + + # Strip the specifier symbols from the front of the string if desired + if strip: + specified = specified.lstrip("<=>!~") + + # append package name, specified version, and latest available version + if not requirement.specifier.contains(pypi_version): + outdated.append(OutdatedDep(requirement.name, specified, pypi_version)) + outdated.sort() + return outdated + + +def print_table( + rows: Sequence[tuple[str, ...]], + headers: tuple[str, ...], + delimiter: str = " | ", +): + """ + List outdated dependencies in a formatted table. + + Args: + `outdated`: A sequence of tuples containing strings. + `headers`: A tuple containing the header strings for the table columns. + """ + if rows and len(rows[0]) != len(headers): + raise RuntimeError("Number of headers doesn't match number of columns") + + header_lengths = [len(header) for header in headers] + + # Find the maximum length of each column + col_widths = [max(len(str(cell)) for cell in col) for col in zip(*rows)] + + # Create a row format based on the maximum column widths + row_format = delimiter.join( + f"{{:<{max(width, header_len)}}}" + for width, header_len in zip(col_widths, header_lengths) + ) + + print(" " + row_format.format(*headers)) + for dependency in rows: + print(" " + row_format.format(*dependency)) + + +def main(transitive: bool = False): + """Check capped dependencies for newer versions. + + Examine `pyproject.toml` and `requirements-dev.in` for capped dependencies. + Make a call to PyPI to see if any newer versions exist. + + Use `transitive` to show outdated transitive dependencies. + """ + modified_pyproject: dict[str, Any] = deps.get_modified_pyproject( + PYPROJECT_TOML_PATH + ) + main_dependencies = get_main_deps_pyproject(modified_pyproject) + optional_dependencies = get_optional_deps_pyproject(modified_pyproject) + dev_dependencies = get_deps_dev() + + outdated_main = get_outdated_deps(main_dependencies) + outdated_optional = get_outdated_deps(optional_dependencies) + outdated_dev = get_outdated_deps(dev_dependencies) + + found_outdated = any([outdated_main, outdated_optional, outdated_dev]) + transitive_headers = ("PACKAGE", "SPECIFIED", "AVAILABLE") + if outdated_main: + location = PYPROJECT_TOML_PATH.name + " - dependencies" + cli.echo_failure(f"Outdated dependencies from {location}:") + print_table(outdated_main, transitive_headers) + if outdated_optional: + location = PYPROJECT_TOML_PATH.name + " - optional-dependencies" + cli.echo_failure(f"Outdated dependencies from {location}:") + print_table(outdated_optional, transitive_headers) + if outdated_dev: + cli.echo_failure(f"Outdated dependencies from {DEV_DEPS_PATH.name}:") + print_table(outdated_dev, transitive_headers) + + if not found_outdated: + cli.echo_success("All top-level dependencies up to date.") + + if transitive: + top_level: set[str] = { + item.name for item in outdated_main + outdated_optional + outdated_dev + } + + print("\nRetrieving transitive dependency information...") + transitive_dependencies = lock_deps.get_lock_file_deps( + LOCK_FILE_PATH, exclude=top_level + ) + outdated_transitive = get_outdated_deps(transitive_dependencies, strip=True) + + if outdated_transitive: + transitive_headers = ("PACKAGE", "PINNED", "AVAILABLE") + + cli.echo_failure("Outdated transitive dependencies:") + print_table(outdated_transitive, transitive_headers) + else: + cli.echo_success("All transitive dependencies up to date.") + + +if __name__ == "__main__": + cli.run(main) diff --git a/scripts/script_utils/__init__.py b/scripts/script_utils/__init__.py new file mode 100644 index 00000000..56e52b5e --- /dev/null +++ b/scripts/script_utils/__init__.py @@ -0,0 +1,17 @@ +# Copyright 2021 - 2023 Universität Tübingen, DKFZ, EMBL, and Universität zu Köln +# for the German Human Genome-Phenome Archive (GHGA) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +"""A collection of utilities used by scripts.""" diff --git a/scripts/script_utils/cli.py b/scripts/script_utils/cli.py new file mode 100644 index 00000000..75c127ff --- /dev/null +++ b/scripts/script_utils/cli.py @@ -0,0 +1,36 @@ +# Copyright 2021 - 2023 Universität Tübingen, DKFZ, EMBL, and Universität zu Köln +# for the German Human Genome-Phenome Archive (GHGA) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +"""A collection of CLI utilities""" + +import typer + + +def echo_success(message: str): + """Print a success message.""" + + styled_message = typer.style(text=message, fg=typer.colors.GREEN) + typer.echo(styled_message) + + +def echo_failure(message: str): + """Print a failure message.""" + + styled_message = typer.style(text=message, fg=typer.colors.RED) + typer.echo(styled_message) + + +run = typer.run diff --git a/scripts/script_utils/deps.py b/scripts/script_utils/deps.py new file mode 100644 index 00000000..cd84c93e --- /dev/null +++ b/scripts/script_utils/deps.py @@ -0,0 +1,75 @@ +# Copyright 2021 - 2023 Universität Tübingen, DKFZ, EMBL, and Universität zu Köln +# for the German Human Genome-Phenome Archive (GHGA) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +"""Contains utils for working with dependencies, lock files, etc.""" + +from copy import deepcopy +from pathlib import Path +from typing import Any + +import stringcase +import tomli + + +def exclude_from_dependency_list(*, package_name: str, dependencies: list) -> list: + """Exclude the specified package from the provided dependency list.""" + + return [ + dependency + for dependency in dependencies + if not dependency.startswith(package_name) + ] + + +def remove_self_dependencies(pyproject: dict) -> dict: + """Filter out self dependencies (dependencies of the package on it self) from the + dependencies and optional-dependencies in the provided pyproject metadata.""" + + if "project" not in pyproject: + return pyproject + + modified_pyproject = deepcopy(pyproject) + + project_metadata = modified_pyproject["project"] + + package_name = stringcase.spinalcase(project_metadata.get("name")) + + if not package_name: + raise ValueError("The provided project metadata does not contain a name.") + + if "dependencies" in project_metadata: + project_metadata["dependencies"] = exclude_from_dependency_list( + package_name=package_name, dependencies=project_metadata["dependencies"] + ) + + if "optional-dependencies" in project_metadata: + for group in project_metadata["optional-dependencies"]: + project_metadata["optional-dependencies"][group] = ( + exclude_from_dependency_list( + package_name=package_name, + dependencies=project_metadata["optional-dependencies"][group], + ) + ) + + return modified_pyproject + + +def get_modified_pyproject(pyproject_toml_path: Path) -> dict[str, Any]: + """Get a copy of pyproject.toml with any self-referencing dependencies removed.""" + with open(pyproject_toml_path, "rb") as pyproject_toml: + pyproject = tomli.load(pyproject_toml) + + modified_pyproject = remove_self_dependencies(pyproject) + return modified_pyproject diff --git a/scripts/script_utils/fastapi_app_location.py b/scripts/script_utils/fastapi_app_location.py new file mode 100644 index 00000000..b2ddb1d9 --- /dev/null +++ b/scripts/script_utils/fastapi_app_location.py @@ -0,0 +1,23 @@ +# Copyright 2021 - 2023 Universität Tübingen, DKFZ, EMBL, and Universität zu Köln +# for the German Human Genome-Phenome Archive (GHGA) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +"""Used to define the location of the main FastAPI app object.""" + +# flake8: noqa +# pylint: skip-file + +# Please adapt to package structure: +from my_microservice.api.main import app diff --git a/scripts/script_utils/lock_deps.py b/scripts/script_utils/lock_deps.py new file mode 100644 index 00000000..6dca8226 --- /dev/null +++ b/scripts/script_utils/lock_deps.py @@ -0,0 +1,47 @@ +# Copyright 2021 - 2023 Universität Tübingen, DKFZ, EMBL, and Universität zu Köln +# for the German Human Genome-Phenome Archive (GHGA) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +"""Provides a function to get all dependencies from the lock file""" + +import re +from pathlib import Path +from typing import Optional + +from packaging.requirements import Requirement + + +def get_lock_file_deps( + lock_file_path: Path, + exclude: Optional[set[str]] = None, +) -> list[Requirement]: + """Inspect the lock file to get the dependencies. + + Return a list of Requirements objects that contain the dependency info. + """ + dependency_pattern = re.compile(r"([^=\s]+==[^\s]*?)\s") + + # Get the set of dependencies from the provided lock file + with open(lock_file_path, encoding="utf-8") as lock_file: + lines = lock_file.readlines() + + dependencies: list[Requirement] = [] + for line in lines: + if match := re.match(dependency_pattern, line): + dependency_string = match.group(1) + requirement = Requirement(dependency_string) + if not exclude or requirement.name not in exclude: + dependencies.append(requirement) + + return dependencies diff --git a/scripts/update_all.py b/scripts/update_all.py new file mode 100755 index 00000000..f319faef --- /dev/null +++ b/scripts/update_all.py @@ -0,0 +1,75 @@ +#!/usr/bin/env python3 + +# Copyright 2021 - 2023 Universität Tübingen, DKFZ, EMBL, and Universität zu Köln +# for the German Human Genome-Phenome Archive (GHGA) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +"""Run all update scripts that are present in the repository in the correct order""" + +try: + from update_template_files import main as update_template +except ImportError: + print("update_template_files script not found") +else: + print("Pulling in updates from template repository") + update_template() + +try: + from update_pyproject import main as update_pyproject +except ImportError: + print("update_pyproject script not found") +else: + print("Updating pyproject.toml file") + update_pyproject() + +try: + from update_lock import main as update_lock +except ImportError: + print("update_lock script not found") +else: + print("Upgrading the lock file") + update_lock(upgrade=True) + +try: + from update_hook_revs import main as update_hook_revs +except ImportError: + print("update_hook_revs script not found") +else: + print("Updating config docs") + update_hook_revs() + +try: + from update_config_docs import main as update_config +except ImportError: + print("update_config_docs script not found") +else: + print("Updating config docs") + update_config() + +try: + from update_openapi_docs import main as update_openapi +except ImportError: + print("update_openapi_docs script not found") +else: + print("Updating OpenAPI docs") + update_openapi() + +try: + from update_readme import main as update_readme +except ImportError: + print("update_readme script not found") +else: + print("Updating README") + update_readme() diff --git a/scripts/update_config_docs.py b/scripts/update_config_docs.py new file mode 100755 index 00000000..90c12081 --- /dev/null +++ b/scripts/update_config_docs.py @@ -0,0 +1,160 @@ +#!/usr/bin/env python3 + +# Copyright 2021 - 2023 Universität Tübingen, DKFZ, EMBL, and Universität zu Köln +# for the German Human Genome-Phenome Archive (GHGA) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Generates a JSON schema from the service's Config class as well as a corresponding +example config yaml (or check whether these files are up to date). +""" + +import importlib +import json +import subprocess +import sys +from difflib import unified_diff +from pathlib import Path +from typing import Any + +import yaml + +from script_utils.cli import echo_failure, echo_success, run + +HERE = Path(__file__).parent.resolve() +REPO_ROOT_DIR = HERE.parent +DEV_CONFIG_YAML = REPO_ROOT_DIR / ".devcontainer" / ".dev_config.yaml" +GET_PACKAGE_NAME_SCRIPT = HERE / "get_package_name.py" +EXAMPLE_CONFIG_YAML = REPO_ROOT_DIR / "example_config.yaml" +CONFIG_SCHEMA_JSON = REPO_ROOT_DIR / "config_schema.json" + + +class ValidationError(RuntimeError): + """Raised when validation of config documentation fails.""" + + +def get_config_class(): + """ + Dynamically imports and returns the Config class from the current service. + This makes the script service repo agnostic. + """ + # get the name of the microservice package + with subprocess.Popen( + args=[GET_PACKAGE_NAME_SCRIPT], + stdout=subprocess.PIPE, + stderr=subprocess.DEVNULL, + ) as process: + assert ( + process.wait() == 0 and process.stdout is not None + ), "Failed to get package name." + package_name = process.stdout.read().decode("utf-8").strip("\n") + + # import the Config class from the microservice package: + config_module: Any = importlib.import_module(f"{package_name}.config") + config_class = config_module.Config + + return config_class + + +def get_dev_config(): + """Get dev config object.""" + config_class = get_config_class() + return config_class(config_yaml=DEV_CONFIG_YAML) + + +def get_schema() -> str: + """Returns a JSON schema generated from a Config class.""" + + config = get_dev_config() + return config.schema_json(indent=2) # change eventually to .model_json_schema(...) + + +def get_example() -> str: + """Returns an example config YAML.""" + + config = get_dev_config() + normalized_config_dict = json.loads( + config.json() # change eventually to .model_dump_json() + ) + return yaml.dump(normalized_config_dict) # pyright: ignore + + +def update_docs(): + """Update the example config and config schema files documenting the config + options.""" + + example = get_example() + with open(EXAMPLE_CONFIG_YAML, "w", encoding="utf-8") as example_file: + example_file.write(example) + + schema = get_schema() + with open(CONFIG_SCHEMA_JSON, "w", encoding="utf-8") as schema_file: + schema_file.write(schema) + + +def print_diff(expected: str, observed: str): + """Print differences between expected and observed files.""" + echo_failure("Differences in Config YAML:") + for line in unified_diff( + expected.splitlines(keepends=True), + observed.splitlines(keepends=True), + fromfile="expected", + tofile="observed", + ): + print(" ", line.rstrip()) + + +def check_docs(): + """Check whether the example config and config schema files documenting the config + options are up to date. + + Raises: + ValidationError: if not up to date. + """ + + example_expected = get_example() + with open(EXAMPLE_CONFIG_YAML, encoding="utf-8") as example_file: + example_observed = example_file.read() + if example_expected != example_observed: + print_diff(example_expected, example_observed) + raise ValidationError( + f"Example config YAML at '{EXAMPLE_CONFIG_YAML}' is not up to date." + ) + + schema_expected = get_schema() + with open(CONFIG_SCHEMA_JSON, encoding="utf-8") as schema_file: + schema_observed = schema_file.read() + if schema_expected != schema_observed: + raise ValidationError( + f"Config schema JSON at '{CONFIG_SCHEMA_JSON}' is not up to date." + ) + + +def main(check: bool = False): + """Update or check the config documentation files.""" + + if check: + try: + check_docs() + except ValidationError as error: + echo_failure(f"Validation failed: {error}") + sys.exit(1) + echo_success("Config docs are up to date.") + return + + update_docs() + echo_success("Successfully updated the config docs.") + + +if __name__ == "__main__": + run(main) diff --git a/scripts/update_hook_revs.py b/scripts/update_hook_revs.py new file mode 100755 index 00000000..0422de2e --- /dev/null +++ b/scripts/update_hook_revs.py @@ -0,0 +1,133 @@ +#!/usr/bin/env python3 + +# Copyright 2021 - 2023 Universität Tübingen, DKFZ, EMBL, and Universität zu Köln +# for the German Human Genome-Phenome Archive (GHGA) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +"""Script to ensure the pre-commit hook revs match what is installed.""" + +import re +import sys +from functools import partial +from pathlib import Path + +from packaging.requirements import Requirement + +from script_utils import cli, lock_deps + +REPO_ROOT_DIR = Path(__file__).parent.parent.resolve() +PRE_COMMIT_CFG_PATH = REPO_ROOT_DIR / ".pre-commit-config.yaml" +LOCK_FILE_PATH = REPO_ROOT_DIR / "lock" / "requirements-dev.txt" + + +def make_dependency_dict(requirements: list[Requirement]) -> dict[str, str]: + """Accept a list of Requirement objects and convert to dict""" + processed = { + req.name: str(req.specifier).removeprefix("==") for req in requirements + } + + return processed + + +def get_repl_value(match, dependencies: dict[str, str], outdated_hooks: list[str]): + """Look up pre-commit hook id in list of dependencies. If there's a match, update + `outdated_hooks` and return the hook version stored in the dictionary""" + ver, name = match.groups() + if name in dependencies: + new_ver = dependencies[name].strip() + + # Use the v prefix if it was used before + if ver.startswith("v"): + new_ver = f"v{new_ver}" + + # Make a list of what's outdated + if new_ver != ver: + msg = f"\t{name} (configured: {ver}, expected: {new_ver})" + outdated_hooks.append(msg) + return new_ver + return ver + + +def get_config(): + """Obtain the current pre-commit hook config from .pre-commit-config.yaml""" + with open(PRE_COMMIT_CFG_PATH, encoding="utf-8") as pre_commit_config: + return pre_commit_config.read() + + +def process_config(dependencies: dict[str, str], config: str) -> tuple[str, list[str]]: + """Compare pre-commit config with lock file dependencies. + + Create a modified copy of the existing config file contents with the hook versions + synchronized to the lock file dependencies. + + Returns: + `new_config`: the updated/synchronized pre-commit config. + + `outdated_hooks`: a list of any outdated hooks with version discrepancy details. + """ + outdated_hooks: list[str] = [] + + hook_rev = re.compile(r"([^\s\n]+)(?=\s*hooks:\s*- id: ([^\s]+))") + + new_config = re.sub( + hook_rev, + repl=partial( + get_repl_value, dependencies=dependencies, outdated_hooks=outdated_hooks + ), + string=config, + ) + + return new_config, outdated_hooks + + +def update_config(new_config: str): + """Write `new_config` to .pre-commit-config.yaml""" + with open(PRE_COMMIT_CFG_PATH, "w", encoding="utf-8") as pre_commit_config: + pre_commit_config.write(new_config) + cli.echo_success(f"Updated '{PRE_COMMIT_CFG_PATH}'") + + +def output_failure(outdated_hooks: list[str]): + """Notify the user that some pre-commit hooks are outdated, and list those hooks.""" + cli.echo_failure("The following pre-commit hook versions are outdated:") + for hook in outdated_hooks: + print(hook) + print("Run 'scripts/update_hook_revs.py' to update") + sys.exit(1) + + +def main(check: bool = False): + """Compare configured pre-commit hooks with the installed dependencies. + + For the set that overlap (e.g. `black`, `mypy`, `ruff`, etc.), make sure the + versions match. If running with `--check`, exit with status code 1 if anything is + outdated. If running without `--check`, update `.pre-commit-config.yaml` as needed. + """ + + dependencies: list[Requirement] = lock_deps.get_lock_file_deps(LOCK_FILE_PATH) + dependency_dict: dict[str, str] = make_dependency_dict(dependencies) + config = get_config() + new_config, outdated_hooks = process_config(dependency_dict, config) + + if config != new_config: + if check: + output_failure(outdated_hooks) + else: + update_config(new_config) + else: + cli.echo_success("Pre-commit hooks are up to date.") + + +if __name__ == "__main__": + cli.run(main) diff --git a/scripts/update_lock.py b/scripts/update_lock.py new file mode 100755 index 00000000..5def8076 --- /dev/null +++ b/scripts/update_lock.py @@ -0,0 +1,216 @@ +#!/usr/bin/env python3 + +# Copyright 2021 - 2023 Universität Tübingen, DKFZ, EMBL, and Universität zu Köln +# for the German Human Genome-Phenome Archive (GHGA) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +"""Update the dependency lock files located at 'requirements.txt' and +'requirements-dev.txt'. +""" + +import os +import re +import subprocess +from itertools import zip_longest +from pathlib import Path +from tempfile import TemporaryDirectory + +import tomli_w + +from script_utils import cli, deps + +REPO_ROOT_DIR = Path(__file__).parent.parent.resolve() +LOCK_DIR = REPO_ROOT_DIR / "lock" + +PYPROJECT_TOML_PATH = REPO_ROOT_DIR / "pyproject.toml" +DEV_DEPS_PATH = LOCK_DIR / "requirements-dev.in" +OUTPUT_LOCK_PATH = LOCK_DIR / "requirements.txt" +OUTPUT_DEV_LOCK_PATH = LOCK_DIR / "requirements-dev.txt" + + +def fix_temp_dir_comments(file_path: Path): + """Fix the temp_dir comments so they don't cause a noisy diff + + This will leave the top compile message intact as a point of sanity to verify that + the requirements are indeed being generated if nothing else changes. + """ + + with open(file_path, encoding="utf-8") as file: + lines = file.readlines() + + with open(file_path, "w", encoding="utf-8") as file: + for line in lines: + # Remove random temp directory name + line = re.sub( + r"\([^\)\(]*?pyproject\.toml\)", + "(pyproject.toml)", + line, + ) + file.write(line) + + +def is_file_outdated(old_file: Path, new_file: Path) -> bool: + """Compares two lock files and returns True if there is a difference, else False""" + + outdated = False + + with open(old_file, encoding="utf-8") as old: + with open(new_file, encoding="utf-8") as new: + outdated = any( + old_line != new_line + for old_line, new_line in zip_longest( + ( + line + for line in (line.strip() for line in old) + if line and not line.startswith("#") + ), + ( + line + for line in (line.strip() for line in new) + if line and not line.startswith("#") + ), + ) + ) + if outdated: + cli.echo_failure(f"{str(old_file)} is out of date!") + return outdated + + +def compile_lock_file( + sources: list[Path], + output: Path, + upgrade: bool, + extras: bool, +) -> None: + """From the specified sources compile a lock file using pip-compile from pip-tools + and write it to the specified output location. + """ + + print(f"Updating '{output.name}'...") + + command = [ + "uv", + "pip", + "compile", + "--refresh", + "--generate-hashes", + ] + + if upgrade: + command.append("--upgrade") + + if extras: + command.append("--all-extras") + + command.extend(["--output-file", str(output.absolute())]) + + command.extend([str(source.absolute()) for source in sources]) + + # constrain the production deps by what's pinned in requirements-dev.txt + if output.name == OUTPUT_LOCK_PATH.name: + command.extend(["-c", str(OUTPUT_DEV_LOCK_PATH)]) + + completed_process = subprocess.run( + args=command, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + check=False, + ) + if completed_process.returncode != 0: + std_out = completed_process.stdout + log = std_out.decode("utf-8") if std_out else "no log available." + raise RuntimeError(f"Failed to compile lock file:\n{log}") + + fix_temp_dir_comments(output.absolute()) + + +def ensure_lock_files_exist(): + """Make sure that the lock files exist if in check mode""" + for output in [OUTPUT_DEV_LOCK_PATH, OUTPUT_LOCK_PATH]: + if not os.path.exists(output): + cli.echo_failure(f"{output} is missing") + return + + +def main(upgrade: bool = False, check: bool = False): + """Update the dependency lock files located at 'requirements.txt' and + 'requirements-dev.txt'. + + For the 'requirements.txt' only the package with its dependencies being defined in + the 'pyproject.toml' are considered. Thereby, all recursive dependencies of the + package on itself are removed. + + For the 'requirements-dev.txt', in addition to the filtered 'pyproject.toml' the + 'requirements-dev.in' is considered. + + The `upgrade` parameter can be used to indicate that dependencies found in existing + lock files should be upgraded. Default pip-compile behavior is to leave them as is. + """ + + # if --check is used, quickly ensure that there is something to compare against + if check: + ensure_lock_files_exist() + + modified_pyproject = deps.get_modified_pyproject(PYPROJECT_TOML_PATH) + + extras = ( + "optional-dependencies" in modified_pyproject["project"] + and modified_pyproject["project"]["optional-dependencies"] + ) + with TemporaryDirectory() as temp_dir: + modified_pyproject_path = Path(temp_dir) / "pyproject.toml" + with open(modified_pyproject_path, "wb") as modified_pyproject_toml: + tomli_w.dump(modified_pyproject, modified_pyproject_toml) + + # make src dir next to TOML to satisfy build system + os.makedirs(Path(temp_dir) / "src") + + # temporary test files + check_dev_path = Path(temp_dir) / OUTPUT_DEV_LOCK_PATH.name + check_prod_path = Path(temp_dir) / OUTPUT_LOCK_PATH.name + + # compile requirements-dev.txt (includes all dependencies) + compile_lock_file( + sources=[modified_pyproject_path, DEV_DEPS_PATH], + output=check_dev_path if check else OUTPUT_DEV_LOCK_PATH, + upgrade=upgrade, + extras=extras, + ) + + if check and is_file_outdated(OUTPUT_DEV_LOCK_PATH, check_dev_path): + return + + # compile requirements.txt (only includes production-related subset of above) + compile_lock_file( + sources=[modified_pyproject_path], + output=check_prod_path if check else OUTPUT_LOCK_PATH, + upgrade=upgrade, + extras=extras, + ) + + if check and is_file_outdated(OUTPUT_LOCK_PATH, check_prod_path): + return + + if check: + cli.echo_success("Lock files are up to date.") + else: + cli.echo_success( + f"Successfully updated lock files at '{OUTPUT_LOCK_PATH}' and" + + f" '{OUTPUT_DEV_LOCK_PATH}'." + ) + + +if __name__ == "__main__": + cli.run(main) diff --git a/scripts/update_openapi_docs.py b/scripts/update_openapi_docs.py new file mode 100755 index 00000000..c7c049c4 --- /dev/null +++ b/scripts/update_openapi_docs.py @@ -0,0 +1,103 @@ +#!/usr/bin/env python3 + +# Copyright 2021 - 2023 Universität Tübingen, DKFZ, EMBL, and Universität zu Köln +# for the German Human Genome-Phenome Archive (GHGA) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Updates OpenAPI-based documentation""" + +import sys +from difflib import unified_diff +from pathlib import Path + +import yaml + +from script_utils.cli import echo_failure, echo_success, run +from script_utils.fastapi_app_location import app + +HERE = Path(__file__).parent.resolve() +REPO_ROOT_DIR = HERE.parent +OPENAPI_YAML = REPO_ROOT_DIR / "openapi.yaml" + + +class ValidationError(RuntimeError): + """Raised when validation of OpenAPI documentation fails.""" + + +def get_openapi_spec() -> str: + """Get an OpenAPI spec in YAML format from the main FastAPI app as defined in the + fastapi_app_location.py file. + """ + + openapi_spec = app.openapi() + return yaml.safe_dump(openapi_spec) + + +def update_docs(): + """Update the OpenAPI YAML file located in the repository's root dir.""" + + openapi_spec = get_openapi_spec() + with open(OPENAPI_YAML, "w", encoding="utf-8") as openapi_file: + openapi_file.write(openapi_spec) + + +def print_diff(expected: str, observed: str): + """Print differences between expected and observed files.""" + echo_failure("Differences in OpenAPI YAML:") + for line in unified_diff( + expected.splitlines(keepends=True), + observed.splitlines(keepends=True), + fromfile="expected", + tofile="observed", + ): + print(" ", line.rstrip()) + + +def check_docs(): + """Checks whether the OpenAPI YAML file located in the repository's root dir is up + to date. + + Raises: + ValidationError: if not up to date. + """ + + openapi_expected = get_openapi_spec() + with open(OPENAPI_YAML, encoding="utf-8") as openapi_file: + openapi_observed = openapi_file.read() + + if openapi_expected != openapi_observed: + print_diff(openapi_expected, openapi_observed) + raise ValidationError( + f"The OpenAPI YAML at '{OPENAPI_YAML}' is not up to date." + ) + + +def main(check: bool = False): + """Update or check the OpenAPI documentation.""" + + if check: + try: + check_docs() + except ValidationError as error: + echo_failure(f"Validation failed: {error}") + sys.exit(1) + echo_success("OpenAPI docs are up to date.") + return + + update_docs() + echo_success("Successfully updated the OpenAPI docs.") + + +if __name__ == "__main__": + run(main) diff --git a/scripts/update_pyproject.py b/scripts/update_pyproject.py new file mode 100755 index 00000000..b68d43da --- /dev/null +++ b/scripts/update_pyproject.py @@ -0,0 +1,117 @@ +#!/usr/bin/env python3 + +# Copyright 2021 - 2023 Universität Tübingen, DKFZ, EMBL, and Universität zu Köln +# for the German Human Genome-Phenome Archive (GHGA) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +"""A script to update the pyproject.toml.""" + +import sys +from pathlib import Path + +import tomli +import tomli_w + +from script_utils import cli + +REPO_ROOT_DIR = Path(__file__).parent.parent.resolve() +PYPROJECT_GENERATION_DIR = REPO_ROOT_DIR / ".pyproject_generation" + +PYPROJECT_TEMPLATE_PATH = PYPROJECT_GENERATION_DIR / "pyproject_template.toml" +PYPROJECT_CUSTOM_PATH = PYPROJECT_GENERATION_DIR / "pyproject_custom.toml" +PYPROJECT_TOML = REPO_ROOT_DIR / "pyproject.toml" + + +def read_template_pyproject() -> dict[str, object]: + """Read the pyproject_template.toml.""" + with open(PYPROJECT_TEMPLATE_PATH, "rb") as file: + return tomli.load(file) + + +def read_custom_pyproject() -> dict[str, object]: + """Read the pyproject_custom.toml.""" + with open(PYPROJECT_CUSTOM_PATH, "rb") as file: + return tomli.load(file) + + +def read_current_pyproject() -> dict[str, object]: + """Read the current pyproject.toml.""" + with open(PYPROJECT_TOML, "rb") as file: + return tomli.load(file) + + +def write_pyproject(pyproject: dict[str, object]) -> None: + """Write the given pyproject dict into the pyproject.toml.""" + with open(PYPROJECT_TOML, "wb") as file: + tomli_w.dump(pyproject, file) + + +def merge_fields(*, source: dict[str, object], dest: dict[str, object]): + """Merge fields existing in both custom and template pyproject definitions. + + If a given field is a dictionary, merge or assign depending on if it's found in dest. + If the field is anything else either assign the value or exit with a message if a + conflict exists. + """ + for field, value in source.items(): + if isinstance(value, dict): + if field in dest: + merge_fields(source=source[field], dest=dest[field]) # type: ignore + else: + dest[field] = value + else: + if field in dest and value != dest[field]: + cli.echo_failure(f"Conflicting values for '{field}'") + exit(1) + elif field not in dest: + dest[field] = value + + +def merge_pyprojects(inputs: list[dict[str, object]]) -> dict[str, object]: + """Compile a pyproject dict from the provided input dicts.""" + pyproject = inputs[0] + + for input in inputs[1:]: + for field, value in input.items(): + if field not in pyproject: + pyproject[field] = value + else: + merge_fields(source=value, dest=pyproject[field]) # type: ignore + + return pyproject + + +def main(*, check: bool = False): + """Update the pyproject.toml or checks for updates if the check flag is specified.""" + template_pyproject = read_template_pyproject() + custom_pyproject = read_custom_pyproject() + merged_pyproject = merge_pyprojects([template_pyproject, custom_pyproject]) + + if check: + current_pyproject = read_current_pyproject() + + if current_pyproject != merged_pyproject: + cli.echo_failure("The pyproject.toml is not up to date.") + sys.exit(1) + + cli.echo_success("The pyproject.toml is up to date.") + return + + write_pyproject(merged_pyproject) + cli.echo_success("Successfully updated the pyproject.toml.") + + +if __name__ == "__main__": + cli.run(main) diff --git a/scripts/update_readme.py b/scripts/update_readme.py new file mode 100755 index 00000000..b8273b02 --- /dev/null +++ b/scripts/update_readme.py @@ -0,0 +1,238 @@ +#!/usr/bin/env python3 + +# Copyright 2021 - 2023 Universität Tübingen, DKFZ, EMBL, and Universität zu Köln +# for the German Human Genome-Phenome Archive (GHGA) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Generate documentation for this package using different sources.""" + +import json +import subprocess # nosec +import sys +from pathlib import Path +from string import Template + +import jsonschema2md +import tomli +from pydantic import BaseModel, Field +from stringcase import spinalcase, titlecase + +from script_utils.cli import echo_failure, echo_success, run + +ROOT_DIR = Path(__file__).parent.parent.resolve() +PYPROJECT_TOML_PATH = ROOT_DIR / "pyproject.toml" +README_GENERATION_DIR = ROOT_DIR / ".readme_generation" +TEMPLATE_OVERVIEW_PATH = README_GENERATION_DIR / "template_overview.md" +DESCRIPTION_PATH = README_GENERATION_DIR / "description.md" +DESIGN_PATH = README_GENERATION_DIR / "design.md" +README_TEMPLATE_PATH = README_GENERATION_DIR / "readme_template.md" +CONFIG_SCHEMA_PATH = ROOT_DIR / "config_schema.json" +OPENAPI_YAML_REL_PATH = "./openapi.yaml" +README_PATH = ROOT_DIR / "README.md" + + +class PackageHeader(BaseModel): + """A basic summary of a package.""" + + shortname: str = Field( + ..., + description=( + "The abbreviation of the package name. Is identical to the package name." + ), + ) + version: str = Field(..., description="The version of the package.") + summary: str = Field( + ..., description="A short 1 or 2 sentence summary of the package." + ) + + +class PackageName(BaseModel): + """The name of a package and it's different representations.""" + + repo_name: str = Field(..., description="The name of the repo") + name: str = Field(..., description="The full name of the package in spinal case.") + title: str = Field(..., description="The name of the package formatted as title.") + + +class PackageDetails(PackageHeader, PackageName): + """A container for details on a package used to build documentation.""" + + description: str = Field( + ..., description="A markdown-formatted description of the package." + ) + design_description: str = Field( + ..., + description=( + "A markdown-formatted description of overall architecture and design of" + + " the package." + ), + ) + config_description: str = Field( + ..., + description=( + "A markdown-formatted list of all configuration parameters of this package." + ), + ) + openapi_doc: str = Field( + ..., + description=( + "A markdown-formatted description rendering or linking to an OpenAPI" + " specification of the package." + ), + ) + + +def read_toml_package_header() -> PackageHeader: + """Read basic information about the package from the pyproject.toml""" + + with open(PYPROJECT_TOML_PATH, "rb") as pyproject_toml: + pyproject = tomli.load(pyproject_toml) + pyproject_project = pyproject["project"] + return PackageHeader( + shortname=pyproject_project["name"], + version=pyproject_project["version"], + summary=pyproject_project["description"], + ) + + +def read_package_name() -> PackageName: + """Infer the package name from the name of the git origin.""" + + with subprocess.Popen( + args="basename -s .git `git config --get remote.origin.url`", + cwd=ROOT_DIR, + stdout=subprocess.PIPE, + shell=True, + ) as process: + stdout, _ = process.communicate() + + if not stdout: + raise RuntimeError("The name of the git origin could not be resolved.") + git_origin_name = stdout.decode("utf-8").strip() + + repo_name = spinalcase(git_origin_name) + name = ( + "my-microservice" + if repo_name == "microservice-repository-template" + else repo_name + ) + title = titlecase(name) + + return PackageName(repo_name=repo_name, name=name, title=title) + + +def read_template_overview() -> str: + """Read the template_overview.""" + + return TEMPLATE_OVERVIEW_PATH.read_text() + + +def read_package_description() -> str: + """Read the package description.""" + + return DESCRIPTION_PATH.read_text() + + +def read_design_description() -> str: + """Read the design description.""" + + return DESIGN_PATH.read_text() + + +def generate_config_docs() -> str: + """Generate markdown-formatted documentation for the configration parameters + listed in the config schema.""" + + parser = jsonschema2md.Parser( + examples_as_yaml=False, + show_examples="all", + ) + with open(CONFIG_SCHEMA_PATH, encoding="utf-8") as json_file: + config_schema = json.load(json_file) + + md_lines = parser.parse_schema(config_schema) + + # ignore everything before the properties header: + properties_index = md_lines.index("## Properties\n\n") + md_lines = md_lines[properties_index + 1 :] + + return "\n".join(md_lines) + + +def generate_openapi_docs() -> str: + """Generate markdown-formatted documentation linking to or rendering an OpenAPI + specification of the package. If no OpenAPI specification is present, return an + empty string.""" + + open_api_yaml_path = ROOT_DIR / OPENAPI_YAML_REL_PATH + + if not open_api_yaml_path.exists(): + return "" + + return ( + "## HTTP API\n" + + "An OpenAPI specification for this service can be found" + + f" [here]({OPENAPI_YAML_REL_PATH})." + ) + + +def get_package_details() -> PackageDetails: + """Get details required to build documentation for the package.""" + + header = read_toml_package_header() + name = read_package_name() + description = read_package_description() + config_description = generate_config_docs() + return PackageDetails( + **header.dict(), + **name.dict(), + description=description, + config_description=config_description, + design_description=read_design_description(), + openapi_doc=generate_openapi_docs(), + ) + + +def generate_single_readme(*, details: PackageDetails) -> str: + """Generate a single markdown-formatted readme file for the package based on the + provided details.""" + + template_content = README_TEMPLATE_PATH.read_text() + template = Template(template_content) + return template.substitute(details.dict()) + + +def main(check: bool = False) -> None: + """Update the readme markdown.""" + + details = get_package_details() + readme_content = generate_single_readme(details=details) + + if details.repo_name == "microservice-repository-template": + template_overview = read_template_overview() + readme_content = template_overview + readme_content + + if check: + if README_PATH.read_text() != readme_content: + echo_failure("README.md is not up to date.") + sys.exit(1) + echo_success("README.md is up to date.") + return + + README_PATH.write_text(readme_content) + echo_success("Successfully updated README.md.") + + +if __name__ == "__main__": + run(main) diff --git a/scripts/update_template_files.py b/scripts/update_template_files.py new file mode 100755 index 00000000..fe29d575 --- /dev/null +++ b/scripts/update_template_files.py @@ -0,0 +1,262 @@ +#!/usr/bin/env python3 + +# Copyright 2021 - 2023 Universität Tübingen, DKFZ, EMBL, and Universität zu Köln +# for the German Human Genome-Phenome Archive (GHGA) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""This script evaluates the entries in .static_files, .mandatory_files and +.deprecated_files and compares them with the microservice template repository, +or verifies their existence or non-existence depending on the list they are in. +""" + +import difflib +import os +import shutil +import stat +import sys +import urllib.error +import urllib.parse +import urllib.request +from pathlib import Path + +try: + from script_utils.cli import echo_failure, echo_success, run +except ImportError: + echo_failure = echo_success = print + + def run(main_fn): + """Run main function without cli tools (typer).""" + main_fn(check="--check" in sys.argv[1:]) + + +REPO_ROOT_DIR = Path(__file__).parent.parent.absolute() + +FILE_LIST_DIR_NAME = ".template" +DEPRECATED_FILES = "deprecated_files" +MANDATORY_FILES = "mandatory_files" +STATIC_FILES = "static_files" + +IGNORE_SUFFIX = "_ignore" + +TEMPLATE_LIST_REL_PATHS = [ + f"{FILE_LIST_DIR_NAME}/{list_name}.txt" + for list_name in [STATIC_FILES, MANDATORY_FILES, DEPRECATED_FILES] +] + +RAW_TEMPLATE_URL = ( + "https://raw.githubusercontent.com/ghga-de/microservice-repository-template/main/" +) + + +class ValidationError(RuntimeError): + """Raised when files need to be updated.""" + + +def get_file_list_path(list_name: str, relative: bool = False) -> Path: + """Get the path to the file list of the given name.""" + return Path(REPO_ROOT_DIR / FILE_LIST_DIR_NAME / f"{list_name}.txt") + + +def get_file_list(list_name: str) -> list[str]: + """Return a list of all file names specified in a given list file.""" + list_path = get_file_list_path(list_name) + with open(list_path, encoding="utf8") as list_file: + file_list = [ + clean_line + for clean_line in ( + line.rstrip() for line in list_file if not line.startswith("#") + ) + if clean_line + ] + if not list_name.endswith(IGNORE_SUFFIX): + ignore_list_name = list_name + IGNORE_SUFFIX + try: + file_set_ignore = set(get_file_list(ignore_list_name)) + except FileNotFoundError: + print(f" - {ignore_list_name} is missing, no exceptions from the template") + else: + file_list = [line for line in file_list if line not in file_set_ignore] + return file_list + + +def get_template_file_content(relative_file_path: str): + """Get the content of the template file corresponding to the given path.""" + remote_file_url = urllib.parse.urljoin(RAW_TEMPLATE_URL, relative_file_path) + remote_file_request = urllib.request.Request(remote_file_url) + try: + with urllib.request.urlopen(remote_file_request) as remote_file_response: + return remote_file_response.read().decode( + remote_file_response.headers.get_content_charset("utf-8") + ) + except urllib.error.HTTPError as remote_file_error: + print( + f" - WARNING: request to remote file {remote_file_url} returned" + f" status code {remote_file_error.code}" + ) + return None + + +def diff_content(local_file_path, local_file_content, template_file_content) -> bool: + """Show diff between given local and remote template file content.""" + if local_file_content != template_file_content: + print(f" - {local_file_path}: differs from template") + for line in difflib.unified_diff( + template_file_content.splitlines(keepends=True), + local_file_content.splitlines(keepends=True), + fromfile="template", + tofile="local", + ): + print(" ", line.rstrip()) + return True + return False + + +def check_file(relative_file_path: str, diff: bool = False) -> bool: + """Compare file at the given path with the given content. + + Returns True if there are differences. + """ + local_file_path = REPO_ROOT_DIR / Path(relative_file_path) + + if not local_file_path.exists(): + print(f" - {local_file_path} does not exist") + return True + + if diff: + template_file_content = get_template_file_content(relative_file_path) + + if template_file_content is None: + print(f" - {local_file_path}: cannot check, remote is missing") + return True + + with open(local_file_path, encoding="utf8") as file: + return diff_content(local_file_path, file.read(), template_file_content) + + return False + + +def update_file(relative_file_path: str, diff: bool = False) -> bool: + """Update file at the given relative path. + + Returns True if there are updates. + """ + + local_file_path = REPO_ROOT_DIR / Path(relative_file_path) + local_parent_dir = local_file_path.parent + + if not local_parent_dir.exists(): + local_parent_dir.mkdir(parents=True) + + if diff or not local_file_path.exists(): + template_file_content = get_template_file_content(relative_file_path) + + if template_file_content is None: + print(f" - {local_file_path}: cannot update, remote is missing") + return True + + if diff and local_file_path.exists(): + with open(local_file_path, encoding="utf8") as file: + if file.read() == template_file_content: + return False + + executable = template_file_content.startswith("#!") + executable_flags = stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH + with open(local_file_path, "w", encoding="utf8") as file: + file.write(template_file_content) + mode = os.fstat(file.fileno()).st_mode + if executable: + mode |= executable_flags + else: + mode &= ~executable_flags + os.fchmod(file.fileno(), stat.S_IMODE(mode)) + + print(f" - {local_file_path}: updated") + return True + + return False + + +def update_files(files: list[str], check: bool = False, diff: bool = False) -> bool: + """Update or check all the files in the given list. + + Returns True if there are updates. + """ + updates = False + update_or_check_file = check_file if check else update_file + for relative_file_path in files: + if update_or_check_file(relative_file_path, diff=diff): + updates = True + return updates + + +def remove_files(files: list[str], check: bool = False) -> bool: + """Remove or check all the files in the given list. + + Returns True if there are updates. + """ + updates = False + for relative_file_path in files: + local_file_path = REPO_ROOT_DIR / Path(relative_file_path) + + if local_file_path.exists(): + if check: + print(f" - {local_file_path}: deprecated, but exists") + else: + if local_file_path.is_dir(): + shutil.rmtree(local_file_path) + else: + local_file_path.unlink() + print(f" - {local_file_path}: removed, since it is deprecated") + updates = True + return updates + + +def main(check: bool = False): + """Update the static files in the service template.""" + updated = False + + print("Template lists...") + if update_files(TEMPLATE_LIST_REL_PATHS, diff=True, check=False): + updated = True + + print("Static files...") + files_to_update = get_file_list(STATIC_FILES) + if update_files(files_to_update, diff=True, check=check): + updated = True + + print("Mandatory files...") + files_to_guarantee = get_file_list(MANDATORY_FILES) + if update_files(files_to_guarantee, check=check): + updated = True + + print("Deprecated files...") + files_to_remove = get_file_list(DEPRECATED_FILES) + if remove_files(files_to_remove, check=check): + updated = True + + if check: + if updated: + echo_failure("Validating files from template failed.") + sys.exit(1) + echo_success("Successfully validated files from template.") + else: + echo_success( + "Successfully updated files from template." + if updated + else "No updates from the template were necessary." + ) + + +if __name__ == "__main__": + run(main) diff --git a/src/my_microservice/__init__.py b/src/my_microservice/__init__.py new file mode 100644 index 00000000..bb514949 --- /dev/null +++ b/src/my_microservice/__init__.py @@ -0,0 +1,20 @@ +# Copyright 2021 - 2023 Universität Tübingen, DKFZ, EMBL, and Universität zu Köln +# for the German Human Genome-Phenome Archive (GHGA) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Short description of package.""" # Please adapt to package + +from importlib.metadata import version + +__version__ = version(__package__) diff --git a/src/my_microservice/__main__.py b/src/my_microservice/__main__.py new file mode 100644 index 00000000..303774aa --- /dev/null +++ b/src/my_microservice/__main__.py @@ -0,0 +1,33 @@ +# Copyright 2021 - 2023 Universität Tübingen, DKFZ, EMBL, and Universität zu Köln +# for the German Human Genome-Phenome Archive (GHGA) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Entrypoint of the package.""" + +import asyncio + +from ghga_service_commons.api import run_server + +from .api.main import app # noqa: F401 pylint: disable=unused-import +from .config import CONFIG, Config + + +def run(config: Config = CONFIG): + """Run the service.""" + # Please adapt to package name + asyncio.run(run_server(app="my_microservice.__main__:app", config=config)) + + +if __name__ == "__main__": + run() diff --git a/src/my_microservice/api/__init__.py b/src/my_microservice/api/__init__.py new file mode 100644 index 00000000..ca43f808 --- /dev/null +++ b/src/my_microservice/api/__init__.py @@ -0,0 +1,16 @@ +# Copyright 2021 - 2023 Universität Tübingen, DKFZ, EMBL, and Universität zu Köln +# for the German Human Genome-Phenome Archive (GHGA) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Subpackage defining a RESTful API.""" diff --git a/src/my_microservice/api/deps.py b/src/my_microservice/api/deps.py new file mode 100644 index 00000000..c5e321bc --- /dev/null +++ b/src/my_microservice/api/deps.py @@ -0,0 +1,23 @@ +# Copyright 2021 - 2023 Universität Tübingen, DKFZ, EMBL, and Universität zu Köln +# for the German Human Genome-Phenome Archive (GHGA) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""FastAPI dependencies (used with the `Depends` feature).""" + +from ..config import CONFIG + + +def get_config(): + """Get runtime configuration.""" + return CONFIG diff --git a/src/my_microservice/api/main.py b/src/my_microservice/api/main.py new file mode 100644 index 00000000..392baec8 --- /dev/null +++ b/src/my_microservice/api/main.py @@ -0,0 +1,52 @@ +# Copyright 2021 - 2023 Universität Tübingen, DKFZ, EMBL, and Universität zu Köln +# for the German Human Genome-Phenome Archive (GHGA) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Module containing the main FastAPI router and (optionally) top-level API enpoints. + +Additional endpoints might be structured in dedicated modules +(each of them having a sub-router). +""" + +from fastapi import Depends, FastAPI +from ghga_service_commons.api import configure_app + +from ..config import CONFIG +from ..core.greeting import generate_greeting +from ..models import Greeting +from .deps import get_config + +app = FastAPI() +configure_app(app, config=CONFIG) + + +@app.get("/", summary="Greet the world") +async def index(): + """Greet the World.""" + return "Hello World." + + +@app.get( + "/greet/{name}", + summary="Greet a person", + description=( + "Greet a person by name. You may choose a formal or an informal greeting." + "The language for the greeting is configured in the backend." + ), + response_model=Greeting, +) +async def greet(name: str, isinformal: bool = True, config=Depends(get_config)): + """Greet a person.""" + return generate_greeting(name=name, language=config.language, isinformal=isinformal) diff --git a/src/my_microservice/config.py b/src/my_microservice/config.py new file mode 100644 index 00000000..db3f0175 --- /dev/null +++ b/src/my_microservice/config.py @@ -0,0 +1,42 @@ +# Copyright 2021 - 2023 Universität Tübingen, DKFZ, EMBL, and Universität zu Köln +# for the German Human Genome-Phenome Archive (GHGA) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Config Parameter Modeling and Parsing.""" + +from ghga_service_commons.api import ApiConfigBase +from hexkit.config import config_from_yaml +from hexkit.log import LoggingConfig +from pydantic import Field + +from .models import SupportedLanguages + +SERVICE_NAME: str = "my_microservice" # Please adapt + + +# Please adapt config prefix and remove unnecessary config bases: +@config_from_yaml(prefix=SERVICE_NAME) +class Config(ApiConfigBase, LoggingConfig): + """Config parameters and their defaults.""" + + service_name: str = Field( + default=SERVICE_NAME, description="Short name of this service" + ) + + language: SupportedLanguages = Field( + default="Croatian", description="The language." + ) + + +CONFIG = Config() diff --git a/src/my_microservice/core/__init__.py b/src/my_microservice/core/__init__.py new file mode 100644 index 00000000..c9f731b4 --- /dev/null +++ b/src/my_microservice/core/__init__.py @@ -0,0 +1,20 @@ +# Copyright 2021 - 2023 Universität Tübingen, DKFZ, EMBL, and Universität zu Köln +# for the German Human Genome-Phenome Archive (GHGA) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +This sub-package contains the main business functionality of this service. + +It should not contain any service API-related code. +""" diff --git a/src/my_microservice/core/greeting.py b/src/my_microservice/core/greeting.py new file mode 100644 index 00000000..4a56f903 --- /dev/null +++ b/src/my_microservice/core/greeting.py @@ -0,0 +1,65 @@ +# Copyright 2021 - 2023 Universität Tübingen, DKFZ, EMBL, and Universität zu Köln +# for the German Human Genome-Phenome Archive (GHGA) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""This module contains functionalities for greeting persons.""" + +import random +from datetime import datetime + +from ..models import Greeting, GreetingExpression + +GREETINGS_EXPRESSIONS = [ + GreetingExpression(expression="Καλημέρα", language="Greek", isinformal=False), + GreetingExpression(expression="Γεια σου", language="Greek", isinformal=True), + GreetingExpression(expression="Γεια", language="Greek", isinformal=True), + GreetingExpression(expression="Dobar dan", language="Croatian", isinformal=False), + GreetingExpression(expression="Bok", language="Croatian", isinformal=True), + GreetingExpression(expression="Zdravo", language="Croatian", isinformal=True), + GreetingExpression(expression="Bonjour", language="French", isinformal=False), + GreetingExpression(expression="Salut", language="French", isinformal=True), + GreetingExpression(expression="Guten Tag", language="German", isinformal=False), + GreetingExpression(expression="Moin moin", language="German", isinformal=True), +] + + +def generate_greeting(name: str, language: str, isinformal: bool): + """Generate a greeting for a specific person.""" + # search for suitable expressions (might be multiple): + expression_hits = [ + expr + for expr in GREETINGS_EXPRESSIONS + if expr.language == language and expr.isinformal == isinformal + ] + + # throw error if no hits were found: + if not expression_hits: + raise ValueError( + f'No greeting expressions found for language=="{language}" ' + 'and isinformal="{isinformal}"' + ) + + # pick a random expression from the list of hits: + expression = random.choice(expression_hits) # nosec # noqa: S311 + + # assemble the greeting phrase: + greeting_phrase = f"{expression.expression} {name}!" + + # return a Greeting object: + return Greeting( + message=greeting_phrase, + created_at=datetime.now(), + language=expression.language, + isinformal=expression.isinformal, + ) diff --git a/src/my_microservice/models.py b/src/my_microservice/models.py new file mode 100644 index 00000000..426b5689 --- /dev/null +++ b/src/my_microservice/models.py @@ -0,0 +1,53 @@ +# Copyright 2021 - 2023 Universität Tübingen, DKFZ, EMBL, and Universität zu Köln +# for the German Human Genome-Phenome Archive (GHGA) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Defines dataclasses for holding business-logic data.""" + +from datetime import datetime +from typing import Literal + +from pydantic import BaseModel, Field + +SupportedLanguages = Literal["Greek", "Croatian", "French", "German"] + + +class MessageBase(BaseModel): + """A message base container.""" + + message: str = Field(..., description="The message content.") + created_at: datetime = Field( + ..., description="The date/time when the message was created" + ) + + +class GreetingBase(BaseModel): + """A container for basic metadata on a greeting phrase/expression.""" + + language: SupportedLanguages = Field(..., description="The language.") + isinformal: bool = Field( + ..., description="Is the expression used in informal contexts?" + ) + + +class GreetingExpression(GreetingBase): + """A container for describing a greeting expression.""" + + expression: str = Field(..., description="The actual greeting expression") + + +class Greeting(GreetingBase, MessageBase): + """A container storing a greeting for a specific person incl. metadata.""" + + pass # pylint: disable=unnecessary-pass diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 00000000..43aaf730 --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1,16 @@ +# Copyright 2021 - 2023 Universität Tübingen, DKFZ, EMBL, and Universität zu Köln +# for the German Human Genome-Phenome Archive (GHGA) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Package containing both unit and integration tests.""" diff --git a/tests/fixtures/__init__.py b/tests/fixtures/__init__.py new file mode 100644 index 00000000..78f4baa0 --- /dev/null +++ b/tests/fixtures/__init__.py @@ -0,0 +1,16 @@ +# Copyright 2021 - 2023 Universität Tübingen, DKFZ, EMBL, and Universität zu Köln +# for the German Human Genome-Phenome Archive (GHGA) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Fixtures that are used in both integration and unit tests.""" diff --git a/tests/fixtures/utils.py b/tests/fixtures/utils.py new file mode 100644 index 00000000..92c10e8f --- /dev/null +++ b/tests/fixtures/utils.py @@ -0,0 +1,20 @@ +# Copyright 2021 - 2023 Universität Tübingen, DKFZ, EMBL, and Universität zu Köln +# for the German Human Genome-Phenome Archive (GHGA) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Utils for Fixture handling.""" + +from pathlib import Path + +BASE_DIR = Path(__file__).parent.resolve() diff --git a/tests/test_dummy.py b/tests/test_dummy.py new file mode 100644 index 00000000..9ff6cebf --- /dev/null +++ b/tests/test_dummy.py @@ -0,0 +1,24 @@ +# Copyright 2021 - 2023 Universität Tübingen, DKFZ, EMBL, and Universität zu Köln +# for the German Human Genome-Phenome Archive (GHGA) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Test dummy.""" + +from my_microservice.core.greeting import generate_greeting + + +def test_dummy(): + """A very simple example test.""" + greeting = generate_greeting("monde", "French", True) + assert greeting.message == "Salut monde!"