diff --git a/.devcontainer/.dev_config.yaml b/.devcontainer/.dev_config.yaml new file mode 100644 index 0000000..5341bae --- /dev/null +++ b/.devcontainer/.dev_config.yaml @@ -0,0 +1,14 @@ +service_name: dins + +service_instance_id: '1' +kafka_servers: ["kafka:9092"] + +db_connection_str: "mongodb://mongodb:27017" +db_name: "dev_db" + +dataset_change_event_topic: metadata_datasets +dataset_deletion_event_type: dataset_deleted +dataset_upsertion_event_type: dataset_created +files_to_delete_topic: file_deletions +file_registered_event_topic: internal_file_registry +file_registered_event_type: file_registered diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 0000000..b21af00 --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,19 @@ +FROM mcr.microsoft.com/devcontainers/python:1-3.12-bookworm + +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="false" +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 0000000..6a43389 --- /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 0000000..c93f0e6 --- /dev/null +++ b/.devcontainer/dev_launcher @@ -0,0 +1,3 @@ +#!/bin/bash + +dins diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..49098d6 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,75 @@ +// 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 +{ + "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.unittestEnabled": false, + "python.testing.pytestEnabled": true, + "editor.codeActionsOnSave": { + "source.organizeImports": "explicit" + }, + "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", + "-ms-python.autopep8" + ] + } + }, + // 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 0000000..1f45338 --- /dev/null +++ b/.devcontainer/docker-compose.yml @@ -0,0 +1,43 @@ +services: + app: + build: + context: . + dockerfile: ./Dockerfile + args: + # [Choice] Install Node.js + INSTALL_NODE: "false" + NODE_VERSION: "lts/*" + # Please adapt to package name: + PACKAGE_NAME: "dataset_information_service" + # 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 + + # Makes testcontainers work on linux based hosts + extra_hosts: + - host.docker.internal:host-gateway + + 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: + DINS_CONFIG_YAML: /workspace/.devcontainer/.dev_config.yaml + + mongodb: + image: mongo:latest + restart: unless-stopped + volumes: + - mongo_fs:/data/db + +volumes: + mongo_fs: {} diff --git a/.devcontainer/license_header.txt b/.devcontainer/license_header.txt new file mode 100644 index 0000000..3e70646 --- /dev/null +++ b/.devcontainer/license_header.txt @@ -0,0 +1,14 @@ +Copyright 2021 - 2024 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 0000000..416f104 --- /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 0000000..b2d08c8 --- /dev/null +++ b/.github/workflows/check_config_docs.yaml @@ -0,0 +1,25 @@ +name: Check if the config schema and the example are up to date + +on: push + +jobs: + static-code-analysis: + name: Check config schema and example + + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + id: checkout + uses: actions/checkout@v4 + + - name: Common steps + id: common + uses: ghga-de/gh-action-common@v6 + + - name: Check config docs + id: 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 0000000..ba4b8e0 --- /dev/null +++ b/.github/workflows/check_openapi_spec.yaml @@ -0,0 +1,26 @@ +# This file is only needed if your repository uses FastAPI +name: Check if OpenAPI spec is up to date + +on: push + +jobs: + static-code-analysis: + name: Check OpenAPI spec + + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + id: checkout + uses: actions/checkout@v4 + + - name: Common steps + id: common + uses: ghga-de/gh-action-common@v6 + + - name: Check openapi.yaml + id: check-openapi-docs + 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 0000000..0c7abdc --- /dev/null +++ b/.github/workflows/check_pyproject.yaml @@ -0,0 +1,23 @@ +name: Check if pyproject.toml file is up to date + +on: push + +jobs: + static-code-analysis: + name: Check pyproject file + + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + id: checkout + uses: actions/checkout@v4 + + - name: Common steps + id: common + uses: ghga-de/gh-action-common@v6 + + - name: Check pyproject.toml + id: check-pyproject + 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 0000000..5d06e3f --- /dev/null +++ b/.github/workflows/check_readme.yaml @@ -0,0 +1,23 @@ +name: Check if the README file is up to date + +on: push + +jobs: + static-code-analysis: + name: Check README file + + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + id: checkout + uses: actions/checkout@v4 + + - name: Common steps + id: common + uses: ghga-de/gh-action-common@v6 + + - name: Check README + id: 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 0000000..e6b7e60 --- /dev/null +++ b/.github/workflows/check_template_files.yaml @@ -0,0 +1,30 @@ +name: Check template files + +on: push + +jobs: + check-template-files: + name: Check template files + + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + id: checkout + uses: actions/checkout@v4 + + - name: Set up Python 3.12 + id: setup-python + uses: actions/setup-python@v5 + with: + python-version: '3.12' + + - name: Check template files + id: 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 0000000..ee2f060 --- /dev/null +++ b/.github/workflows/ci_release.yaml @@ -0,0 +1,23 @@ +name: CI on release + +on: + release: + types: [published] + +jobs: + push_to_docker_hub: + name: Push to Docker Hub + + strategy: + matrix: + flavor: ["", "debian"] + + 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 }} + flavor: ${{ matrix.flavor }} diff --git a/.github/workflows/ci_workflow_dispatch.yaml b/.github/workflows/ci_workflow_dispatch.yaml new file mode 100644 index 0000000..a49be97 --- /dev/null +++ b/.github/workflows/ci_workflow_dispatch.yaml @@ -0,0 +1,32 @@ +name: Build on PR or dispatch + +on: + workflow_dispatch: + pull_request: + types: + - opened + - synchronize + - reopened + - labeled + +jobs: + fetch-tag: + name: 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: + name: 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 0000000..052dfdc --- /dev/null +++ b/.github/workflows/static_code_analysis.yaml @@ -0,0 +1,39 @@ +name: Static Code Analysis + +on: push + +jobs: + static-code-analysis: + name: Static Code Analysis + + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + id: checkout + uses: actions/checkout@v4 + + - name: Common steps + id: common + uses: ghga-de/gh-action-common@v6 + + - name: Run pre-commit + uses: pre-commit/action@v3.0.1 + env: + SKIP: no-commit-to-branch + + - name: Run ruff + id: ruff + run: | + ruff check --output-format=github . + ruff format --check . + + - name: Run mypy + id: mypy + run: | + mypy . + + - name: Check license header and file + id: license-checker + run: | + ./scripts/check_license.py diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml new file mode 100644 index 0000000..56a9c19 --- /dev/null +++ b/.github/workflows/tests.yaml @@ -0,0 +1,36 @@ +name: Run test suite and measure coverage + +on: push + +jobs: + tests: + name: Run test suite + + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + id: checkout + uses: actions/checkout@v4 + + - name: Common steps + id: common + uses: ghga-de/gh-action-common@v6 + + - name: Run tests + 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 + + - name: Upload coverage to coveralls + id: 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 0000000..d9ba614 --- /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 0000000..25bf528 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,57 @@ +# See https://pre-commit.com for more information +# See https://pre-commit.com/hooks.html for more hooks + +minimum_pre_commit_version: 3.7.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.6.5 + hooks: + - id: ruff + args: [--fix, --exit-non-zero-on-fix] + - id: ruff-format + - repo: https://github.com/pre-commit/mirrors-mypy + rev: v1.11.2 + 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 0000000..4434a8f --- /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 0000000..ce46ed3 --- /dev/null +++ b/.pyproject_generation/pyproject_custom.toml @@ -0,0 +1,16 @@ +[project] +name = "dins" +version = "1.1.0" +description = "Dataset Information Service - Providing public metadata about files registered with the Internal File Registry" +dependencies = [ + "typer >= 0.12", + "ghga-service-commons[api] >= 3.1", + "ghga-event-schemas >= 3.3", + "hexkit[akafka,s3,mongodb] >= 3.2", +] + +[project.urls] +Repository = "https://github.com/ghga-de/dataset-information-service" + +[project.scripts] +dins = "dins.__main__:run" diff --git a/.pyproject_generation/pyproject_template.toml b/.pyproject_generation/pyproject_template.toml new file mode 100644 index 0000000..07a11ff --- /dev/null +++ b/.pyproject_generation/pyproject_template.toml @@ -0,0 +1,110 @@ +[build-system] +requires = ["setuptools>=69"] +build-backend = "setuptools.build_meta" + +[project] +readme = "README.md" +authors = [ + { name = "German Human Genome Phenome Archive (GHGA)", email = "contact@ghga.de" }, +] +requires-python = ">=3.12" +license = { text = "Apache 2.0" } +classifiers = [ + "Development Status :: 1 - Planning", + "Operating System :: POSIX :: Linux", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.12", + "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 = "py312" + +[tool.ruff.lint] +fixable = [ + "UP", # e.g. List -> list + "I", # sort imports + "D", # pydocstyle +] +ignore = [ + "E111", # indentation with invalid multiple (for formatter) + "E114", # indentation with invalid multiple comment (for formatter) + "E116", # over indentation (for formatter) + "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 (for formatter) + "D300", # triple-single-quotes (for formatter) + "UP040", # type statement (not yet supported by mypy) +] +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 = "8.0" +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 0000000..725df74 --- /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 0000000..2d5e9bd --- /dev/null +++ b/.readme_generation/description.md @@ -0,0 +1,2 @@ +The File Information Service serves publicly available metadata about files registered with the Internal File Registry service. +Currently this includes the SHA256 checksum of the unencrypted file content and the size of the unencrypted file in bytes. diff --git a/.readme_generation/design.md b/.readme_generation/design.md new file mode 100644 index 0000000..f2dfee4 --- /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 0000000..98a00c0 --- /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 0000000..6e148ef --- /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 0000000..56ada89 --- /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 0000000..152ea26 --- /dev/null +++ b/.template/deprecated_files.txt @@ -0,0 +1,45 @@ +# 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/license_checker.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 0000000..b589813 --- /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 0000000..fcfb0f8 --- /dev/null +++ b/.template/mandatory_files.txt @@ -0,0 +1,33 @@ +# 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 +Dockerfile.debian +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 0000000..b8a7ee6 --- /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 0000000..1cf9790 --- /dev/null +++ b/.template/static_files.txt @@ -0,0 +1,61 @@ +# 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/script_utils/deps.py +scripts/script_utils/lock_deps.py + +scripts/__init__.py +scripts/update_all.py +scripts/check_license.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 0000000..8f374ce --- /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 0000000..d019668 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,50 @@ +# Copyright 2021 - 2024 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. + +# BASE: a base image with updated packages +FROM python:3.12-alpine AS base +RUN apk upgrade --no-cache --available + +# BUILDER: a container to build the service wheel +FROM base AS builder +RUN pip install build +COPY . /service +WORKDIR /service +RUN python -m build + +# DEP-BUILDER: a container to (build and) install dependencies +FROM base AS dep-builder +RUN apk update +RUN apk add build-base gcc g++ libffi-dev zlib-dev +RUN apk upgrade --available +WORKDIR /service +COPY --from=builder /service/lock/requirements.txt /service +RUN pip install --no-deps -r requirements.txt + +# RUNNER: a container to run the service +FROM base AS runner +WORKDIR /service +RUN rm -rf /usr/local/lib/python3.12 +COPY --from=dep-builder /usr/local/lib/python3.12 /usr/local/lib/python3.12 +COPY --from=builder /service/dist/ /service +RUN pip install --no-deps *.whl +RUN rm *.whl +RUN adduser -D appuser +WORKDIR /home/appuser +USER appuser +ENV PYTHONUNBUFFERED=1 + +# Please adapt to package name: +ENTRYPOINT ["dins"] diff --git a/Dockerfile.debian b/Dockerfile.debian new file mode 100644 index 0000000..88d79dd --- /dev/null +++ b/Dockerfile.debian @@ -0,0 +1,48 @@ +# Copyright 2021 - 2024 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.12-slim-bookworm 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.12-slim-bookworm +# 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 ["dins"] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f459a36 --- /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 - 2024 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 0000000..f71e139 --- /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 0000000..18c12f1 --- /dev/null +++ b/config_schema.json @@ -0,0 +1,329 @@ +{ + "additionalProperties": false, + "description": "Modifies the original Settings class provided by the user", + "properties": { + "files_to_delete_topic": { + "description": "The name of the topic for events informing about files to be deleted.", + "examples": [ + "file-deletions" + ], + "title": "Files To Delete Topic", + "type": "string" + }, + "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": "dins", + "title": "Service Name", + "type": "string" + }, + "service_instance_id": { + "description": "A string that uniquely identifies this instance across all instances of this service. A globally unique Kafka client ID will be created by concatenating the service_name and the service_instance_id.", + "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" + }, + "log_traceback": { + "default": true, + "description": "Whether to include exception tracebacks in log messages.", + "title": "Log Traceback", + "type": "boolean" + }, + "kafka_servers": { + "description": "A list of connection strings to connect to Kafka bootstrap servers.", + "examples": [ + [ + "localhost:9092" + ] + ], + "items": { + "type": "string" + }, + "title": "Kafka Servers", + "type": "array" + }, + "kafka_security_protocol": { + "default": "PLAINTEXT", + "description": "Protocol used to communicate with brokers. Valid values are: PLAINTEXT, SSL.", + "enum": [ + "PLAINTEXT", + "SSL" + ], + "title": "Kafka Security Protocol", + "type": "string" + }, + "kafka_ssl_cafile": { + "default": "", + "description": "Certificate Authority file path containing certificates used to sign broker certificates. If a CA is not specified, the default system CA will be used if found by OpenSSL.", + "title": "Kafka Ssl Cafile", + "type": "string" + }, + "kafka_ssl_certfile": { + "default": "", + "description": "Optional filename of client certificate, as well as any CA certificates needed to establish the certificate's authenticity.", + "title": "Kafka Ssl Certfile", + "type": "string" + }, + "kafka_ssl_keyfile": { + "default": "", + "description": "Optional filename containing the client private key.", + "title": "Kafka Ssl Keyfile", + "type": "string" + }, + "kafka_ssl_password": { + "default": "", + "description": "Optional password to be used for the client private key.", + "format": "password", + "title": "Kafka Ssl Password", + "type": "string", + "writeOnly": true + }, + "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" + }, + "kafka_max_message_size": { + "default": 1048576, + "description": "The largest message size that can be transmitted, in bytes. Only services that have a need to send/receive larger messages should set this.", + "examples": [ + 1048576, + 16777216 + ], + "exclusiveMinimum": 0, + "title": "Kafka Max Message Size", + "type": "integer" + }, + "db_connection_str": { + "description": "MongoDB connection string. Might include credentials. For more information see: https://naiveskill.com/mongodb-connection-string/", + "examples": [ + "mongodb://localhost:27017" + ], + "format": "password", + "title": "Db Connection Str", + "type": "string", + "writeOnly": true + }, + "db_name": { + "description": "Name of the database located on the MongoDB server.", + "examples": [ + "my-database" + ], + "title": "Db Name", + "type": "string" + }, + "dataset_change_event_topic": { + "description": "Name of the topic for events that inform about datasets.", + "examples": [ + "metadata_datasets" + ], + "title": "Dataset Change Event Topic", + "type": "string" + }, + "dataset_upsertion_event_type": { + "description": "The type of events that inform about new and changed datasets.", + "examples": [ + "dataset_created" + ], + "title": "Dataset Upsertion Event Type", + "type": "string" + }, + "dataset_deletion_event_type": { + "description": "The type of events that inform about deleted datasets.", + "examples": [ + "dataset_deleted" + ], + "title": "Dataset Deletion Event Type", + "type": "string" + }, + "file_registered_event_topic": { + "description": "The name of the topic for events informing about new registered files for which the metadata should be made available.", + "examples": [ + "internal-file-registry" + ], + "title": "File Registered Event Topic", + "type": "string" + }, + "file_registered_event_type": { + "description": "The name of the type used for events informing about new registered files for which the metadata should be made available.", + "examples": [ + "file_registered" + ], + "title": "File Registered Event Type", + "type": "string" + }, + "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" + } + }, + "required": [ + "files_to_delete_topic", + "service_instance_id", + "kafka_servers", + "db_connection_str", + "db_name", + "dataset_change_event_topic", + "dataset_upsertion_event_type", + "dataset_deletion_event_type", + "file_registered_event_topic", + "file_registered_event_type" + ], + "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 0000000..ce29448 --- /dev/null +++ b/example_config.yaml @@ -0,0 +1,33 @@ +api_root_path: '' +auto_reload: false +cors_allow_credentials: null +cors_allowed_headers: null +cors_allowed_methods: null +cors_allowed_origins: null +dataset_change_event_topic: metadata_datasets +dataset_deletion_event_type: dataset_deleted +dataset_upsertion_event_type: dataset_created +db_connection_str: '**********' +db_name: dev_db +docs_url: /docs +file_registered_event_topic: internal_file_registry +file_registered_event_type: file_registered +files_to_delete_topic: file_deletions +generate_correlation_id: true +host: 127.0.0.1 +kafka_max_message_size: 1048576 +kafka_security_protocol: PLAINTEXT +kafka_servers: +- kafka:9092 +kafka_ssl_cafile: '' +kafka_ssl_certfile: '' +kafka_ssl_keyfile: '' +kafka_ssl_password: '' +log_format: null +log_level: INFO +log_traceback: true +openapi_url: /openapi.json +port: 8080 +service_instance_id: '1' +service_name: dins +workers: 1 diff --git a/example_data/README.md b/example_data/README.md new file mode 100644 index 0000000..9223add --- /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 0000000..090a32e --- /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 0000000..97ce387 --- /dev/null +++ b/lock/requirements-dev-template.in @@ -0,0 +1,32 @@ +# common requirements for development and testing of services + +pytest>=8.2 +pytest-asyncio>=0.23.7 +pytest-cov>=5 +snakeviz>=2.2 +logot>=1.3 + +pre-commit>=3.7 + +mypy>=1.10 +mypy-extensions>=1.0 + +ruff>=0.4 + +click>=8.1 +typer>=0.12 + +httpx>=0.27 +pytest-httpx>=0.30 + +urllib3>=1.26.18 +requests>=2.31 + +stringcase>=1.2 +jsonschema2md>=1.1 +setuptools>=69.5 + +# required since switch to pyproject.toml and pip-tools +tomli_w>=1.0 + +uv>=0.2.13 diff --git a/lock/requirements-dev.in b/lock/requirements-dev.in new file mode 100644 index 0000000..fe5e26b --- /dev/null +++ b/lock/requirements-dev.in @@ -0,0 +1,8 @@ +# 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]>=4.6.0 +pymongo >=4.8, <4.9 diff --git a/lock/requirements-dev.txt b/lock/requirements-dev.txt new file mode 100644 index 0000000..4044e08 --- /dev/null +++ b/lock/requirements-dev.txt @@ -0,0 +1,1313 @@ +aiokafka==0.11.0 \ + --hash=sha256:0973a245b8b9daf8ef6814253a80a700f1f54d2da7d88f6fe479f46e0fd83053 \ + --hash=sha256:0d80590c4ef0ba546a299cee22ea27c3360c14241ec43a8e6904653f7b22d328 \ + --hash=sha256:0e957b42ae959365efbb45c9b5de38032c573608553c3670ad8695cc210abec9 \ + --hash=sha256:1d519bf9875ac867fb19d55de3750833b1eb6379a08de29a68618e24e6a49fc0 \ + --hash=sha256:1f8ae91f0373830e4664376157fe61b611ca7e573d8a559b151aef5bf53df46c \ + --hash=sha256:224db2447f6c1024198d8342e7099198f90401e2fa29c0762afbc51eadf5c490 \ + --hash=sha256:230170ce2e8a0eb852e2e8b78b08ce2e29b77dfe2c51bd56f5ab4be0f332a63b \ + --hash=sha256:3711fa64ee8640dcd4cb640f1030f9439d02e85acd57010d09053017092d8cc2 \ + --hash=sha256:38e1917e706c1158d5e1f612d1fc1b40f706dc46c534e73ab4de8ae2868a31be \ + --hash=sha256:419dd28c8ed6e926061bdc60929af08a6b52f1721e1179d9d21cc72ae28fd6f6 \ + --hash=sha256:4e0cc080a7f4c659ee4e1baa1c32adedcccb105a52156d4909f357d76fac0dc1 \ + --hash=sha256:516e1d68d9a377860b2e17453580afe304605bc71894f684d3e7b6618f6f939f \ + --hash=sha256:55a07a39d82c595223a17015ea738d152544cee979d3d6d822707a082465621c \ + --hash=sha256:560839ae6bc13e71025d71e94df36980f5c6e36a64916439e598b6457267a37f \ + --hash=sha256:59f4b935589ebb244620afad8bf3320e3bc86879a8b1c692ad06bd324f6c6127 \ + --hash=sha256:6ef3e7c8a923e502caa4d24041f2be778fd7f9ee4587bf0bcb4f74cac05122fa \ + --hash=sha256:702aec15b63bad5e4476294bcb1cb177559149fce3e59335794f004c279cbd6a \ + --hash=sha256:73584be8ba7906e3f33ca0f08f6af21a9ae31b86c6b635b93db3b1e6f452657b \ + --hash=sha256:807f699cf916369b1a512e4f2eaec714398c202d8803328ef8711967d99a56ce \ + --hash=sha256:818a6f8e44b02113b9e795bee6029c8a4e525ab38f29d7adb0201f3fec74c808 \ + --hash=sha256:8ba981956243767b37c929845c398fda2a2e35a4034d218badbe2b62e6f98f96 \ + --hash=sha256:926f93fb6a39891fd4364494432b479c0602f9cac708778d4a262a2c2e20d3b4 \ + --hash=sha256:9a478a14fd23fd1ffe9c7a21238d818b5f5e0626f7f06146b687f3699298391b \ + --hash=sha256:acfd0a5d0aec762ba73eeab73b23edce14f315793f063b6a4b223b6f79e36bb8 \ + --hash=sha256:d59fc7aec088c9ffc02d37e61591f053459bd11912cf04c70ac4f7e60405667d \ + --hash=sha256:d724b6fc484e453b373052813e4e543fc028a22c3fbda10e13b6829740000b8a \ + --hash=sha256:eaafe134de57b184f3c030e1a11051590caff7953c8bf58048eefd8d828e39d7 \ + --hash=sha256:eac78a009b713e28b5b4c4daae9d062acbf2b7980e5734467643a810134583b5 \ + --hash=sha256:ee0c61a2dcabbe4474ff237d708f9bd663dd2317e03a9cb7239a212c9ee05b12 \ + --hash=sha256:f1c85f66eb3564c5e74d8e4c25df4ac1fd94f1a6f6e66f005aafa6f791bde215 \ + --hash=sha256:f2def07fe1720c4fe37c0309e355afa9ff4a28e0aabfe847be0692461ac69352 + # via hexkit +annotated-types==0.7.0 \ + --hash=sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53 \ + --hash=sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89 + # via pydantic +anyio==4.4.0 \ + --hash=sha256:5aadc6a1bbb7cdb0bede386cac5e2940f5e2ff3aa20277e991cf028e0585ce94 \ + --hash=sha256:c1b2d8f46a8a812513012e1107cb0e68c17159a7a594208005a57dc776e1bdc7 + # via + # httpx + # starlette + # watchfiles +async-timeout==4.0.3 \ + --hash=sha256:4640d96be84d82d02ed59ea2b7105a0f7b33abe8703703cd0ab0bf87c427522f \ + --hash=sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028 + # via aiokafka +attrs==24.2.0 \ + --hash=sha256:5cfb1b9148b5b086569baec03f20d7b6bf3bcacc9a42bebf87ffaaca362f6346 \ + --hash=sha256:81921eb96de3191c8258c199618104dd27ac608d9366f5e35d011eae1867ede2 + # via + # jsonschema + # referencing +boto3==1.35.21 \ + --hash=sha256:247f88eedce9ae4e014a8fc14a9473759bb8e391460d49396a3b600fb649f33b \ + --hash=sha256:db5fbbd10248db060f2ccce3ae17764f1641c99c8b9f51d422c26ebe25703a1e + # via hexkit +botocore==1.35.21 \ + --hash=sha256:3db9ddfe521edc0753fc8c68caef71c7806e1d2d21ce8cbabc2065b7d79192f2 \ + --hash=sha256:db917e7d7b3a2eed1310c6496784bc813c91f020a021c2ab5f9df7d28cdb4f1d + # via + # boto3 + # hexkit + # s3transfer +certifi==2024.8.30 \ + --hash=sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8 \ + --hash=sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9 + # 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 + # -r lock/requirements-dev-template.in + # typer + # uvicorn +coverage==7.6.1 \ + --hash=sha256:06a737c882bd26d0d6ee7269b20b12f14a8704807a01056c80bb881a4b2ce6ca \ + --hash=sha256:07e2ca0ad381b91350c0ed49d52699b625aab2b44b65e1b4e02fa9df0e92ad2d \ + --hash=sha256:0c0420b573964c760df9e9e86d1a9a622d0d27f417e1a949a8a66dd7bcee7bc6 \ + --hash=sha256:0dbde0f4aa9a16fa4d754356a8f2e36296ff4d83994b2c9d8398aa32f222f989 \ + --hash=sha256:1125ca0e5fd475cbbba3bb67ae20bd2c23a98fac4e32412883f9bcbaa81c314c \ + --hash=sha256:13b0a73a0896988f053e4fbb7de6d93388e6dd292b0d87ee51d106f2c11b465b \ + --hash=sha256:166811d20dfea725e2e4baa71fffd6c968a958577848d2131f39b60043400223 \ + --hash=sha256:170d444ab405852903b7d04ea9ae9b98f98ab6d7e63e1115e82620807519797f \ + --hash=sha256:1f4aa8219db826ce6be7099d559f8ec311549bfc4046f7f9fe9b5cea5c581c56 \ + --hash=sha256:225667980479a17db1048cb2bf8bfb39b8e5be8f164b8f6628b64f78a72cf9d3 \ + --hash=sha256:260933720fdcd75340e7dbe9060655aff3af1f0c5d20f46b57f262ab6c86a5e8 \ + --hash=sha256:2bdb062ea438f22d99cba0d7829c2ef0af1d768d1e4a4f528087224c90b132cb \ + --hash=sha256:2c09f4ce52cb99dd7505cd0fc8e0e37c77b87f46bc9c1eb03fe3bc9991085388 \ + --hash=sha256:3115a95daa9bdba70aea750db7b96b37259a81a709223c8448fa97727d546fe0 \ + --hash=sha256:3e0cadcf6733c09154b461f1ca72d5416635e5e4ec4e536192180d34ec160f8a \ + --hash=sha256:3f1156e3e8f2872197af3840d8ad307a9dd18e615dc64d9ee41696f287c57ad8 \ + --hash=sha256:4421712dbfc5562150f7554f13dde997a2e932a6b5f352edcce948a815efee6f \ + --hash=sha256:44df346d5215a8c0e360307d46ffaabe0f5d3502c8a1cefd700b34baf31d411a \ + --hash=sha256:502753043567491d3ff6d08629270127e0c31d4184c4c8d98f92c26f65019962 \ + --hash=sha256:547f45fa1a93154bd82050a7f3cddbc1a7a4dd2a9bf5cb7d06f4ae29fe94eaf8 \ + --hash=sha256:5621a9175cf9d0b0c84c2ef2b12e9f5f5071357c4d2ea6ca1cf01814f45d2391 \ + --hash=sha256:609b06f178fe8e9f89ef676532760ec0b4deea15e9969bf754b37f7c40326dbc \ + --hash=sha256:645786266c8f18a931b65bfcefdbf6952dd0dea98feee39bd188607a9d307ed2 \ + --hash=sha256:6878ef48d4227aace338d88c48738a4258213cd7b74fd9a3d4d7582bb1d8a155 \ + --hash=sha256:6a89ecca80709d4076b95f89f308544ec8f7b4727e8a547913a35f16717856cb \ + --hash=sha256:6db04803b6c7291985a761004e9060b2bca08da6d04f26a7f2294b8623a0c1a0 \ + --hash=sha256:6e2cd258d7d927d09493c8df1ce9174ad01b381d4729a9d8d4e38670ca24774c \ + --hash=sha256:6e81d7a3e58882450ec4186ca59a3f20a5d4440f25b1cff6f0902ad890e6748a \ + --hash=sha256:702855feff378050ae4f741045e19a32d57d19f3e0676d589df0575008ea5004 \ + --hash=sha256:78b260de9790fd81e69401c2dc8b17da47c8038176a79092a89cb2b7d945d060 \ + --hash=sha256:7bb65125fcbef8d989fa1dd0e8a060999497629ca5b0efbca209588a73356232 \ + --hash=sha256:7dea0889685db8550f839fa202744652e87c60015029ce3f60e006f8c4462c93 \ + --hash=sha256:8284cf8c0dd272a247bc154eb6c95548722dce90d098c17a883ed36e67cdb129 \ + --hash=sha256:877abb17e6339d96bf08e7a622d05095e72b71f8afd8a9fefc82cf30ed944163 \ + --hash=sha256:8929543a7192c13d177b770008bc4e8119f2e1f881d563fc6b6305d2d0ebe9de \ + --hash=sha256:8ae539519c4c040c5ffd0632784e21b2f03fc1340752af711f33e5be83a9d6c6 \ + --hash=sha256:8f59d57baca39b32db42b83b2a7ba6f47ad9c394ec2076b084c3f029b7afca23 \ + --hash=sha256:9054a0754de38d9dbd01a46621636689124d666bad1936d76c0341f7d71bf569 \ + --hash=sha256:953510dfb7b12ab69d20135a0662397f077c59b1e6379a768e97c59d852ee51d \ + --hash=sha256:95cae0efeb032af8458fc27d191f85d1717b1d4e49f7cb226cf526ff28179778 \ + --hash=sha256:9bc572be474cafb617672c43fe989d6e48d3c83af02ce8de73fff1c6bb3c198d \ + --hash=sha256:9c56863d44bd1c4fe2abb8a4d6f5371d197f1ac0ebdee542f07f35895fc07f36 \ + --hash=sha256:9e0b2df163b8ed01d515807af24f63de04bebcecbd6c3bfeff88385789fdf75a \ + --hash=sha256:a09ece4a69cf399510c8ab25e0950d9cf2b42f7b3cb0374f95d2e2ff594478a6 \ + --hash=sha256:a1ac0ae2b8bd743b88ed0502544847c3053d7171a3cff9228af618a068ed9c34 \ + --hash=sha256:a318d68e92e80af8b00fa99609796fdbcdfef3629c77c6283566c6f02c6d6704 \ + --hash=sha256:a4acd025ecc06185ba2b801f2de85546e0b8ac787cf9d3b06e7e2a69f925b106 \ + --hash=sha256:a6d3adcf24b624a7b778533480e32434a39ad8fa30c315208f6d3e5542aeb6e9 \ + --hash=sha256:a78d169acd38300060b28d600344a803628c3fd585c912cacc9ea8790fe96862 \ + --hash=sha256:a95324a9de9650a729239daea117df21f4b9868ce32e63f8b650ebe6cef5595b \ + --hash=sha256:abd5fd0db5f4dc9289408aaf34908072f805ff7792632250dcb36dc591d24255 \ + --hash=sha256:b06079abebbc0e89e6163b8e8f0e16270124c154dc6e4a47b413dd538859af16 \ + --hash=sha256:b43c03669dc4618ec25270b06ecd3ee4fa94c7f9b3c14bae6571ca00ef98b0d3 \ + --hash=sha256:b48f312cca9621272ae49008c7f613337c53fadca647d6384cc129d2996d1133 \ + --hash=sha256:b5d7b556859dd85f3a541db6a4e0167b86e7273e1cdc973e5b175166bb634fdb \ + --hash=sha256:b9f222de8cded79c49bf184bdbc06630d4c58eec9459b939b4a690c82ed05657 \ + --hash=sha256:c3c02d12f837d9683e5ab2f3d9844dc57655b92c74e286c262e0fc54213c216d \ + --hash=sha256:c44fee9975f04b33331cb8eb272827111efc8930cfd582e0320613263ca849ca \ + --hash=sha256:cf4b19715bccd7ee27b6b120e7e9dd56037b9c0681dcc1adc9ba9db3d417fa36 \ + --hash=sha256:d0c212c49b6c10e6951362f7c6df3329f04c2b1c28499563d4035d964ab8e08c \ + --hash=sha256:d3296782ca4eab572a1a4eca686d8bfb00226300dcefdf43faa25b5242ab8a3e \ + --hash=sha256:d85f5e9a5f8b73e2350097c3756ef7e785f55bd71205defa0bfdaf96c31616ff \ + --hash=sha256:da511e6ad4f7323ee5702e6633085fb76c2f893aaf8ce4c51a0ba4fc07580ea7 \ + --hash=sha256:e05882b70b87a18d937ca6768ff33cc3f72847cbc4de4491c8e73880766718e5 \ + --hash=sha256:e61c0abb4c85b095a784ef23fdd4aede7a2628478e7baba7c5e3deba61070a02 \ + --hash=sha256:e6a08c0be454c3b3beb105c0596ebdc2371fab6bb90c0c0297f4e58fd7e1012c \ + --hash=sha256:e9a6e0eb86070e8ccaedfbd9d38fec54864f3125ab95419970575b42af7541df \ + --hash=sha256:ed37bd3c3b063412f7620464a9ac1314d33100329f39799255fb8d3027da50d3 \ + --hash=sha256:f1adfc8ac319e1a348af294106bc6a8458a0f1633cc62a1446aebc30c5fa186a \ + --hash=sha256:f5796e664fe802da4f57a168c85359a8fbf3eab5e55cd4e4569fbacecc903959 \ + --hash=sha256:fc5a77d0c516700ebad189b587de289a20a78324bc54baee03dd486f0855d234 \ + --hash=sha256:fd21f6ae3f08b41004dfb433fa895d858f3f5979e7762d052b12aef444e29afc + # 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.1.0 \ + --hash=sha256:ad8c70e6e3f8926cb8a92619b832b4ea5299e2831c14284663184e200546fa6c \ + --hash=sha256:c96b93b7f0a746f9e77d325bcfb87422a3d8bd4f03136ae8a85b37f1898d5fc0 + # via testcontainers +email-validator==2.2.0 \ + --hash=sha256:561977c2d73ce3611850a06fa56b414621e0c8faa9d66f2611407d87465da631 \ + --hash=sha256:cb690f344c617a714f22e66ae771445a1ceb46821152df8e165c5f9a364582b7 + # via + # fastapi + # pydantic +fastapi==0.111.1 \ + --hash=sha256:4f51cfa25d72f9fbc3280832e84b32494cf186f50158d364a8765aabf22587bf \ + --hash=sha256:ddd1ac34cb1f76c2e2d7f8545a4bcb5463bce4834e81abf0b189e0c359ab2413 + # via ghga-service-commons +fastapi-cli==0.0.5 \ + --hash=sha256:d30e1239c6f46fcb95e606f02cdda59a1e2fa778a54b64686b3ff27f6211ff9f \ + --hash=sha256:e94d847524648c748a5350673546bbf9bcaeb086b33c24f2e82e021436866a46 + # via fastapi +filelock==3.16.1 \ + --hash=sha256:2082e5703d51fbf98ea75855d9d5527e33d8ff23099bec374a134febee6946b0 \ + --hash=sha256:c249fbfcd5db47e5e2d6d62198e565475ee65e4831e2561c8e313fa7eb961435 + # via virtualenv +ghga-event-schemas==3.3.1 \ + --hash=sha256:6b21b45efd8a1a5dbbc3b671f3d23390b728dd19669ce39a7153acb5487f1256 \ + --hash=sha256:845e72aa8045c52fbb42a215dcd95dd8436c841788b1d8edb80794584faab09a + # via dins (pyproject.toml) +ghga-service-commons==3.1.5 \ + --hash=sha256:6650167f9fe21d683d7f7bd81320f3301f6624f6c71d7e633fbf5f0a3453d973 \ + --hash=sha256:eeae99a976d2c81bce754e20cc0156a49b76ef3f2f660175d654de0abcb62c6e + # via dins (pyproject.toml) +h11==0.14.0 \ + --hash=sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d \ + --hash=sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761 + # via + # httpcore + # uvicorn +hexkit==3.6.0 \ + --hash=sha256:0f650bf87a95c91ddffa20f1c5c130b2c5aad719cb5031bf6c7ea60fa8f136c1 \ + --hash=sha256:70d8159565b43ce4fa54bb471a0cd873517162fb27a056e30613c4332822500b + # via + # dins (pyproject.toml) + # ghga-service-commons +httpcore==1.0.5 \ + --hash=sha256:34a38e2f9291467ee3b44e89dd52615370e152954ba21721378a87b2960f7a61 \ + --hash=sha256:421f18bac248b25d310f3cacd198d55b8e6125c107797b609ff9b7a6ba7991b5 + # 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.2 \ + --hash=sha256:7bb2708e112d8fdd7829cd4243970f0c223274051cb35ee80c03301ee29a3df0 \ + --hash=sha256:f7c2be1d2f3c3c3160d441802406b206c2b76f5947b11115e6df10c6c65e66c2 + # via + # -r lock/requirements-dev-template.in + # fastapi + # pytest-httpx +identify==2.6.1 \ + --hash=sha256:53863bcac7caf8d2ed85bd20312ea5dcfc22226800f6d6881f232d861db5a8f0 \ + --hash=sha256:91478c5fb7c3aac5ff7bf9b4344f803843dc586832d5f110d672b19aa1984c98 + # via pre-commit +idna==3.10 \ + --hash=sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9 \ + --hash=sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3 + # via + # anyio + # email-validator + # httpx + # requests +iniconfig==2.0.0 \ + --hash=sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3 \ + --hash=sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374 + # via pytest +jinja2==3.1.4 \ + --hash=sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369 \ + --hash=sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d + # via fastapi +jmespath==1.0.1 \ + --hash=sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980 \ + --hash=sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe + # via + # boto3 + # botocore +jsonschema==4.23.0 \ + --hash=sha256:d71497fef26351a33265337fa77ffeb82423f3ea21283cd9467bb03999266bc4 \ + --hash=sha256:fbadb6f8b144a8f8cf9f0b89ba94501d143e50411a1278633f56a7acf7fd5566 + # via + # ghga-event-schemas + # hexkit +jsonschema-specifications==2023.12.1 \ + --hash=sha256:48a76787b3e70f5ed53f1160d2b81f586e4ca6d1548c5de7085d1682674764cc \ + --hash=sha256:87e4fdf3a94858b8a2ba2778d9ba57d8a9cafca7c7489c46ba0d30a8bc6a9c3c + # via jsonschema +jsonschema2md==1.3.0 \ + --hash=sha256:5ee8f6674c9fec7303daa24c79023805caf2f2fefb99834813bd746227d146ea \ + --hash=sha256:ba089d46a3ac6f43b10caeaf8cd1b3978c7bd15c8e287de78d247666cb8857c0 + # via -r lock/requirements-dev-template.in +logot==1.3.0 \ + --hash=sha256:bb2e8cf8ca949015e1e096e45023095ebd5df06ea4627f5df47d53dcdf62b74e \ + --hash=sha256:de392d182308828a0a9a442120e25e4ad2258fef52c4ed275e012aaffb0514a5 + # via -r lock/requirements-dev-template.in +markdown-it-py==3.0.0 \ + --hash=sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1 \ + --hash=sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb + # via rich +markupsafe==2.1.5 \ + --hash=sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf \ + --hash=sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff \ + --hash=sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f \ + --hash=sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3 \ + --hash=sha256:1f3fbcb7ef1f16e48246f704ab79d79da8a46891e2da03f8783a5b6fa41a9532 \ + --hash=sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f \ + --hash=sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617 \ + --hash=sha256:2d2d793e36e230fd32babe143b04cec8a8b3eb8a3122d2aceb4a371e6b09b8df \ + --hash=sha256:30b600cf0a7ac9234b2638fbc0fb6158ba5bdcdf46aeb631ead21248b9affbc4 \ + --hash=sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906 \ + --hash=sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f \ + --hash=sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4 \ + --hash=sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8 \ + --hash=sha256:4096e9de5c6fdf43fb4f04c26fb114f61ef0bf2e5604b6ee3019d51b69e8c371 \ + --hash=sha256:4275d846e41ecefa46e2015117a9f491e57a71ddd59bbead77e904dc02b1bed2 \ + --hash=sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465 \ + --hash=sha256:4f11aa001c540f62c6166c7726f71f7573b52c68c31f014c25cc7901deea0b52 \ + --hash=sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6 \ + --hash=sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169 \ + --hash=sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad \ + --hash=sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2 \ + --hash=sha256:5dedb4db619ba5a2787a94d877bc8ffc0566f92a01c0ef214865e54ecc9ee5e0 \ + --hash=sha256:619bc166c4f2de5caa5a633b8b7326fbe98e0ccbfacabd87268a2b15ff73a029 \ + --hash=sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f \ + --hash=sha256:656f7526c69fac7f600bd1f400991cc282b417d17539a1b228617081106feb4a \ + --hash=sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced \ + --hash=sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5 \ + --hash=sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c \ + --hash=sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf \ + --hash=sha256:7b2e5a267c855eea6b4283940daa6e88a285f5f2a67f2220203786dfa59b37e9 \ + --hash=sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb \ + --hash=sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad \ + --hash=sha256:8dd717634f5a044f860435c1d8c16a270ddf0ef8588d4887037c5028b859b0c3 \ + --hash=sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1 \ + --hash=sha256:97cafb1f3cbcd3fd2b6fbfb99ae11cdb14deea0736fc2b0952ee177f2b813a46 \ + --hash=sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc \ + --hash=sha256:a549b9c31bec33820e885335b451286e2969a2d9e24879f83fe904a5ce59d70a \ + --hash=sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee \ + --hash=sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900 \ + --hash=sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5 \ + --hash=sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea \ + --hash=sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f \ + --hash=sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5 \ + --hash=sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e \ + --hash=sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a \ + --hash=sha256:c8b29db45f8fe46ad280a7294f5c3ec36dbac9491f2d1c17345be8e69cc5928f \ + --hash=sha256:ce409136744f6521e39fd8e2a24c53fa18ad67aa5bc7c2cf83645cce5b5c4e50 \ + --hash=sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a \ + --hash=sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b \ + --hash=sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4 \ + --hash=sha256:daa4ee5a243f0f20d528d939d06670a298dd39b1ad5f8a72a4275124a7819eff \ + --hash=sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2 \ + --hash=sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46 \ + --hash=sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b \ + --hash=sha256:ec6a563cff360b50eed26f13adc43e61bc0c04d94b8be985e6fb24b81f6dcfdf \ + --hash=sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5 \ + --hash=sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5 \ + --hash=sha256:fa9db3f79de01457b03d4f01b34cf91bc0048eb2c3846ff26f66687c2f6d16ab \ + --hash=sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd \ + --hash=sha256:ffee1f21e5ef0d712f9033568f8344d5da8cc2869dbd08d87c84656e6a2d2f68 + # via jinja2 +mdurl==0.1.2 \ + --hash=sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8 \ + --hash=sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba + # via markdown-it-py +motor==3.5.1 \ + --hash=sha256:1622bd7b39c3e6375607c14736f6e1d498128eadf6f5f93f8786cf17d37062ac \ + --hash=sha256:f95a9ea0f011464235e0bd72910baa291db3a6009e617ac27b82f57885abafb8 + # via hexkit +mypy==1.11.2 \ + --hash=sha256:06d26c277962f3fb50e13044674aa10553981ae514288cb7d0a738f495550b36 \ + --hash=sha256:2ff93107f01968ed834f4256bc1fc4475e2fecf6c661260066a985b52741ddce \ + --hash=sha256:36383a4fcbad95f2657642a07ba22ff797de26277158f1cc7bd234821468b1b6 \ + --hash=sha256:37c7fa6121c1cdfcaac97ce3d3b5588e847aa79b580c1e922bb5d5d2902df19b \ + --hash=sha256:3a66169b92452f72117e2da3a576087025449018afc2d8e9bfe5ffab865709ca \ + --hash=sha256:3f14cd3d386ac4d05c5a39a51b84387403dadbd936e17cb35882134d4f8f0d24 \ + --hash=sha256:41ea707d036a5307ac674ea172875f40c9d55c5394f888b168033177fce47383 \ + --hash=sha256:478db5f5036817fe45adb7332d927daa62417159d49783041338921dcf646fc7 \ + --hash=sha256:4a8a53bc3ffbd161b5b2a4fff2f0f1e23a33b0168f1c0778ec70e1a3d66deb86 \ + --hash=sha256:539c570477a96a4e6fb718b8d5c3e0c0eba1f485df13f86d2970c91f0673148d \ + --hash=sha256:57555a7715c0a34421013144a33d280e73c08df70f3a18a552938587ce9274f4 \ + --hash=sha256:6e658bd2d20565ea86da7d91331b0eed6d2eee22dc031579e6297f3e12c758c8 \ + --hash=sha256:6e7184632d89d677973a14d00ae4d03214c8bc301ceefcdaf5c474866814c987 \ + --hash=sha256:75746e06d5fa1e91bfd5432448d00d34593b52e7e91a187d981d08d1f33d4385 \ + --hash=sha256:7f9993ad3e0ffdc95c2a14b66dee63729f021968bff8ad911867579c65d13a79 \ + --hash=sha256:801780c56d1cdb896eacd5619a83e427ce436d86a3bdf9112527f24a66618fef \ + --hash=sha256:801ca29f43d5acce85f8e999b1e431fb479cb02d0e11deb7d2abb56bdaf24fd6 \ + --hash=sha256:969ea3ef09617aff826885a22ece0ddef69d95852cdad2f60c8bb06bf1f71f70 \ + --hash=sha256:a976775ab2256aadc6add633d44f100a2517d2388906ec4f13231fafbb0eccca \ + --hash=sha256:af8d155170fcf87a2afb55b35dc1a0ac21df4431e7d96717621962e4b9192e70 \ + --hash=sha256:b499bc07dbdcd3de92b0a8b29fdf592c111276f6a12fe29c30f6c417dd546d12 \ + --hash=sha256:cd953f221ac1379050a8a646585a29574488974f79d8082cedef62744f0a0104 \ + --hash=sha256:d42a6dd818ffce7be66cce644f1dff482f1d97c53ca70908dff0b9ddc120b77a \ + --hash=sha256:e8960dbbbf36906c5c0b7f4fbf2f0c7ffb20f4898e6a879fcf56a41a08b0d318 \ + --hash=sha256:edb91dded4df17eae4537668b23f0ff6baf3707683734b6a818d5b9d0c0c31a1 \ + --hash=sha256:ee23de8530d99b6db0573c4ef4bd8f39a2a6f9b60655bf7a1357e585a3486f2b \ + --hash=sha256:f7821776e5c4286b6a13138cc935e2e9b6fde05e081bdebf5cdb2bb97c9df81d + # via -r lock/requirements-dev-template.in +mypy-extensions==1.0.0 \ + --hash=sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d \ + --hash=sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782 + # via + # -r lock/requirements-dev-template.in + # mypy +nodeenv==1.9.1 \ + --hash=sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f \ + --hash=sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9 + # via pre-commit +packaging==24.1 \ + --hash=sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002 \ + --hash=sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124 + # via + # aiokafka + # pytest +platformdirs==4.3.6 \ + --hash=sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907 \ + --hash=sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb + # via virtualenv +pluggy==1.5.0 \ + --hash=sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1 \ + --hash=sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669 + # via pytest +pre-commit==3.8.0 \ + --hash=sha256:8bb6494d4a20423842e198980c9ecf9f96607a07ea29549e180eef9ae80fe7af \ + --hash=sha256:9a90a53bf82fdd8778d58085faf8d83df56e40dfe18f45b19446e26bf1b3a63f + # via -r lock/requirements-dev-template.in +pydantic==2.9.2 \ + --hash=sha256:d155cef71265d1e9807ed1c32b4c8deec042a44a50a4188b25ac67ecd81a9c0f \ + --hash=sha256:f048cec7b26778210e28a0459867920654d48e5e62db0958433636cde4254f12 + # via + # fastapi + # ghga-event-schemas + # ghga-service-commons + # hexkit + # pydantic-settings +pydantic-core==2.23.4 \ + --hash=sha256:0a7df63886be5e270da67e0966cf4afbae86069501d35c8c1b3b6c168f42cb36 \ + --hash=sha256:0cb3da3fd1b6a5d0279a01877713dbda118a2a4fc6f0d821a57da2e464793f05 \ + --hash=sha256:0dbd8dbed2085ed23b5c04afa29d8fd2771674223135dc9bc937f3c09284d071 \ + --hash=sha256:0dff76e0602ca7d4cdaacc1ac4c005e0ce0dcfe095d5b5259163a80d3a10d327 \ + --hash=sha256:1278e0d324f6908e872730c9102b0112477a7f7cf88b308e4fc36ce1bdb6d58c \ + --hash=sha256:128585782e5bfa515c590ccee4b727fb76925dd04a98864182b22e89a4e6ed36 \ + --hash=sha256:1498bec4c05c9c787bde9125cfdcc63a41004ff167f495063191b863399b1a29 \ + --hash=sha256:19442362866a753485ba5e4be408964644dd6a09123d9416c54cd49171f50744 \ + --hash=sha256:1b84d168f6c48fabd1f2027a3d1bdfe62f92cade1fb273a5d68e621da0e44e6d \ + --hash=sha256:1e90d2e3bd2c3863d48525d297cd143fe541be8bbf6f579504b9712cb6b643ec \ + --hash=sha256:20152074317d9bed6b7a95ade3b7d6054845d70584216160860425f4fbd5ee9e \ + --hash=sha256:216f9b2d7713eb98cb83c80b9c794de1f6b7e3145eef40400c62e86cee5f4e1e \ + --hash=sha256:233710f069d251feb12a56da21e14cca67994eab08362207785cf8c598e74577 \ + --hash=sha256:255a8ef062cbf6674450e668482456abac99a5583bbafb73f9ad469540a3a232 \ + --hash=sha256:2584f7cf844ac4d970fba483a717dbe10c1c1c96a969bf65d61ffe94df1b2863 \ + --hash=sha256:2971bb5ffe72cc0f555c13e19b23c85b654dd2a8f7ab493c262071377bfce9f6 \ + --hash=sha256:29d2c342c4bc01b88402d60189f3df065fb0dda3654744d5a165a5288a657368 \ + --hash=sha256:2e203fdf807ac7e12ab59ca2bfcabb38c7cf0b33c41efeb00f8e5da1d86af480 \ + --hash=sha256:33e3d65a85a2a4a0dc3b092b938a4062b1a05f3a9abde65ea93b233bca0e03f2 \ + --hash=sha256:374a5e5049eda9e0a44c696c7ade3ff355f06b1fe0bb945ea3cac2bc336478a2 \ + --hash=sha256:37b0fe330e4a58d3c58b24d91d1eb102aeec675a3db4c292ec3928ecd892a9a6 \ + --hash=sha256:3d5639516376dce1940ea36edf408c554475369f5da2abd45d44621cb616f769 \ + --hash=sha256:42c6dcb030aefb668a2b7009c85b27f90e51e6a3b4d5c9bc4c57631292015b0d \ + --hash=sha256:4a7cd62e831afe623fbb7aabbb4fe583212115b3ef38a9f6b71869ba644624a2 \ + --hash=sha256:4ba762ed58e8d68657fc1281e9bb72e1c3e79cc5d464be146e260c541ec12d84 \ + --hash=sha256:4fc714bdbfb534f94034efaa6eadd74e5b93c8fa6315565a222f7b6f42ca1166 \ + --hash=sha256:4ffa2ebd4c8530079140dd2d7f794a9d9a73cbb8e9d59ffe24c63436efa8f271 \ + --hash=sha256:5a1504ad17ba4210df3a045132a7baeeba5a200e930f57512ee02909fc5c4cb5 \ + --hash=sha256:5c364564d17da23db1106787675fc7af45f2f7b58b4173bfdd105564e132e6fb \ + --hash=sha256:5e11661ce0fd30a6790e8bcdf263b9ec5988e95e63cf901972107efc49218b13 \ + --hash=sha256:5f54b118ce5de9ac21c363d9b3caa6c800341e8c47a508787e5868c6b79c9323 \ + --hash=sha256:5f5ff8d839f4566a474a969508fe1c5e59c31c80d9e140566f9a37bba7b8d556 \ + --hash=sha256:61817945f2fe7d166e75fbfb28004034b48e44878177fc54d81688e7b85a3665 \ + --hash=sha256:624e278a7d29b6445e4e813af92af37820fafb6dcc55c012c834f9e26f9aaaef \ + --hash=sha256:63e46b3169866bd62849936de036f901a9356e36376079b05efa83caeaa02ceb \ + --hash=sha256:6531b7ca5f951d663c339002e91aaebda765ec7d61b7d1e3991051906ddde119 \ + --hash=sha256:68665f4c17edcceecc112dfed5dbe6f92261fb9d6054b47d01bf6371a6196126 \ + --hash=sha256:696dd8d674d6ce621ab9d45b205df149399e4bb9aa34102c970b721554828510 \ + --hash=sha256:6f783e0ec4803c787bcea93e13e9932edab72068f68ecffdf86a99fd5918878b \ + --hash=sha256:723314c1d51722ab28bfcd5240d858512ffd3116449c557a1336cbe3919beb87 \ + --hash=sha256:74b9127ffea03643e998e0c5ad9bd3811d3dac8c676e47db17b0ee7c3c3bf35f \ + --hash=sha256:7530e201d10d7d14abce4fb54cfe5b94a0aefc87da539d0346a484ead376c3cc \ + --hash=sha256:77733e3892bb0a7fa797826361ce8a9184d25c8dffaec60b7ffe928153680ba8 \ + --hash=sha256:78ddaaa81421a29574a682b3179d4cf9e6d405a09b99d93ddcf7e5239c742e21 \ + --hash=sha256:7c9129eb40958b3d4500fa2467e6a83356b3b61bfff1b414c7361d9220f9ae8f \ + --hash=sha256:7d32706badfe136888bdea71c0def994644e09fff0bfe47441deaed8e96fdbc6 \ + --hash=sha256:81965a16b675b35e1d09dd14df53f190f9129c0202356ed44ab2728b1c905658 \ + --hash=sha256:8394d940e5d400d04cad4f75c0598665cbb81aecefaca82ca85bd28264af7f9b \ + --hash=sha256:86d2f57d3e1379a9525c5ab067b27dbb8a0642fb5d454e17a9ac434f9ce523e3 \ + --hash=sha256:883a91b5dd7d26492ff2f04f40fbb652de40fcc0afe07e8129e8ae779c2110eb \ + --hash=sha256:88ad334a15b32a791ea935af224b9de1bf99bcd62fabf745d5f3442199d86d59 \ + --hash=sha256:9261d3ce84fa1d38ed649c3638feefeae23d32ba9182963e465d58d62203bd24 \ + --hash=sha256:97df63000f4fea395b2824da80e169731088656d1818a11b95f3b173747b6cd9 \ + --hash=sha256:98d134c954828488b153d88ba1f34e14259284f256180ce659e8d83e9c05eaa3 \ + --hash=sha256:996a38a83508c54c78a5f41456b0103c30508fed9abcad0a59b876d7398f25fd \ + --hash=sha256:9a5bce9d23aac8f0cf0836ecfc033896aa8443b501c58d0602dbfd5bd5b37753 \ + --hash=sha256:9a6b5099eeec78827553827f4c6b8615978bb4b6a88e5d9b93eddf8bb6790f55 \ + --hash=sha256:9d18368b137c6295db49ce7218b1a9ba15c5bc254c96d7c9f9e924a9bc7825ad \ + --hash=sha256:a4fa4fc04dff799089689f4fd502ce7d59de529fc2f40a2c8836886c03e0175a \ + --hash=sha256:a5c7ba8ffb6d6f8f2ab08743be203654bb1aaa8c9dcb09f82ddd34eadb695605 \ + --hash=sha256:aea443fffa9fbe3af1a9ba721a87f926fe548d32cab71d188a6ede77d0ff244e \ + --hash=sha256:b10bd51f823d891193d4717448fab065733958bdb6a6b351967bd349d48d5c9b \ + --hash=sha256:ba1a0996f6c2773bd83e63f18914c1de3c9dd26d55f4ac302a7efe93fb8e7433 \ + --hash=sha256:bb2802e667b7051a1bebbfe93684841cc9351004e2badbd6411bf357ab8d5ac8 \ + --hash=sha256:cfdd16ab5e59fc31b5e906d1a3f666571abc367598e3e02c83403acabc092e07 \ + --hash=sha256:d06b0c8da4f16d1d1e352134427cb194a0a6e19ad5db9161bf32b2113409e728 \ + --hash=sha256:d0776dea117cf5272382634bd2a5c1b6eb16767c223c6a5317cd3e2a757c61a0 \ + --hash=sha256:d18ca8148bebe1b0a382a27a8ee60350091a6ddaf475fa05ef50dc35b5df6327 \ + --hash=sha256:d4488a93b071c04dc20f5cecc3631fc78b9789dd72483ba15d423b5b3689b555 \ + --hash=sha256:d5f7a395a8cf1621939692dba2a6b6a830efa6b3cee787d82c7de1ad2930de64 \ + --hash=sha256:d7a80d21d613eec45e3d41eb22f8f94ddc758a6c4720842dc74c0581f54993d6 \ + --hash=sha256:d97683ddee4723ae8c95d1eddac7c192e8c552da0c73a925a89fa8649bf13eea \ + --hash=sha256:dcedcd19a557e182628afa1d553c3895a9f825b936415d0dbd3cd0bbcfd29b4b \ + --hash=sha256:de6d1d1b9e5101508cb37ab0d972357cac5235f5c6533d1071964c47139257df \ + --hash=sha256:df49e7a0861a8c36d089c1ed57d308623d60416dab2647a4a17fe050ba85de0e \ + --hash=sha256:df933278128ea1cd77772673c73954e53a1c95a4fdf41eef97c2b779271bd0bd \ + --hash=sha256:e08277a400de01bc72436a0ccd02bdf596631411f592ad985dcee21445bd0068 \ + --hash=sha256:e38e63e6f3d1cec5a27e0afe90a085af8b6806ee208b33030e65b6516353f1a3 \ + --hash=sha256:e55541f756f9b3ee346b840103f32779c695a19826a4c442b7954550a0972040 \ + --hash=sha256:ec4e55f79b1c4ffb2eecd8a0cfba9955a2588497d96851f4c8f99aa4a1d39b12 \ + --hash=sha256:ed1a53de42fbe34853ba90513cea21673481cd81ed1be739f7f2efb931b24916 \ + --hash=sha256:ed541d70698978a20eb63d8c5d72f2cc6d7079d9d90f6b50bad07826f1320f5f \ + --hash=sha256:f09e2ff1f17c2b51f2bc76d1cc33da96298f0a036a137f5440ab3ec5360b624f \ + --hash=sha256:f220b0eea5965dec25480b6333c788fb72ce5f9129e8759ef876a1d805d00801 \ + --hash=sha256:f3e0da4ebaef65158d4dfd7d3678aad692f7666877df0002b8a522cdf088f231 \ + --hash=sha256:f455ee30a9d61d3e1a15abd5068827773d6e4dc513e795f380cdd59932c782d5 \ + --hash=sha256:f5ef8f42bec47f21d07668a043f077d507e5bf4e668d5c6dfe6aaba89de1a5b8 \ + --hash=sha256:f69a8e0b033b747bb3e36a44e7732f0c99f7edd5cea723d45bc0d6e95377ffee \ + --hash=sha256:ff02b6d461a6de369f07ec15e465a88895f3223eb75073ffea56b84d9331f607 + # via pydantic +pydantic-settings==2.5.2 \ + --hash=sha256:2c912e55fd5794a59bf8c832b9de832dcfdf4778d79ff79b708744eed499a907 \ + --hash=sha256:f90b139682bee4d2065273d5185d71d37ea46cfe57e1b5ae184fc6a0b2484ca0 + # via hexkit +pygments==2.18.0 \ + --hash=sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199 \ + --hash=sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a + # via rich +pymongo==4.8.0 \ + --hash=sha256:0fc18b3a093f3db008c5fea0e980dbd3b743449eee29b5718bc2dc15ab5088bb \ + --hash=sha256:16e5019f75f6827bb5354b6fef8dfc9d6c7446894a27346e03134d290eb9e758 \ + --hash=sha256:180d5eb1dc28b62853e2f88017775c4500b07548ed28c0bd9c005c3d7bc52526 \ + --hash=sha256:18c9d8f975dd7194c37193583fd7d1eb9aea0c21ee58955ecf35362239ff31ac \ + --hash=sha256:236bbd7d0aef62e64caf4b24ca200f8c8670d1a6f5ea828c39eccdae423bc2b2 \ + --hash=sha256:284d0717d1a7707744018b0b6ee7801b1b1ff044c42f7be7a01bb013de639470 \ + --hash=sha256:2ecd71b9226bd1d49416dc9f999772038e56f415a713be51bf18d8676a0841c8 \ + --hash=sha256:31e4d21201bdf15064cf47ce7b74722d3e1aea2597c6785882244a3bb58c7eab \ + --hash=sha256:3b5802151fc2b51cd45492c80ed22b441d20090fb76d1fd53cd7760b340ff554 \ + --hash=sha256:3c68fe128a171493018ca5c8020fc08675be130d012b7ab3efe9e22698c612a1 \ + --hash=sha256:3ed1c316718a2836f7efc3d75b4b0ffdd47894090bc697de8385acd13c513a70 \ + --hash=sha256:408b2f8fdbeca3c19e4156f28fff1ab11c3efb0407b60687162d49f68075e63c \ + --hash=sha256:417369ce39af2b7c2a9c7152c1ed2393edfd1cbaf2a356ba31eb8bcbd5c98dd7 \ + --hash=sha256:454f2295875744dc70f1881e4b2eb99cdad008a33574bc8aaf120530f66c0cde \ + --hash=sha256:47ec8c3f0a7b2212dbc9be08d3bf17bc89abd211901093e3ef3f2adea7de7a69 \ + --hash=sha256:4bf58e6825b93da63e499d1a58de7de563c31e575908d4e24876234ccb910eba \ + --hash=sha256:519d1bab2b5e5218c64340b57d555d89c3f6c9d717cecbf826fb9d42415e7750 \ + --hash=sha256:52b4108ac9469febba18cea50db972605cc43978bedaa9fea413378877560ef8 \ + --hash=sha256:658d0170f27984e0d89c09fe5c42296613b711a3ffd847eb373b0dbb5b648d5f \ + --hash=sha256:6b50040d9767197b77ed420ada29b3bf18a638f9552d80f2da817b7c4a4c9c68 \ + --hash=sha256:7148419eedfea9ecb940961cfe465efaba90595568a1fb97585fb535ea63fe2b \ + --hash=sha256:77f53429515d2b3e86dcc83dadecf7ff881e538c168d575f3688698a8707b80a \ + --hash=sha256:87075a1feb1e602e539bdb1ef8f4324a3427eb0d64208c3182e677d2c0718b6f \ + --hash=sha256:8b18c8324809539c79bd6544d00e0607e98ff833ca21953df001510ca25915d1 \ + --hash=sha256:9097c331577cecf8034422956daaba7ec74c26f7b255d718c584faddd7fa2e3c \ + --hash=sha256:920d4f8f157a71b3cb3f39bc09ce070693d6e9648fb0e30d00e2657d1dca4e49 \ + --hash=sha256:9365166aa801c63dff1a3cb96e650be270da06e3464ab106727223123405510f \ + --hash=sha256:940d456774b17814bac5ea7fc28188c7a1338d4a233efbb6ba01de957bded2e8 \ + --hash=sha256:aec2b9088cdbceb87e6ca9c639d0ff9b9d083594dda5ca5d3c4f6774f4c81b33 \ + --hash=sha256:af3e98dd9702b73e4e6fd780f6925352237f5dce8d99405ff1543f3771201704 \ + --hash=sha256:b6564780cafd6abeea49759fe661792bd5a67e4f51bca62b88faab497ab5fe89 \ + --hash=sha256:b747c0e257b9d3e6495a018309b9e0c93b7f0d65271d1d62e572747f4ffafc88 \ + --hash=sha256:bf821bd3befb993a6db17229a2c60c1550e957de02a6ff4dd0af9476637b2e4d \ + --hash=sha256:c6b804bb4f2d9dc389cc9e827d579fa327272cdb0629a99bfe5b83cb3e269ebf \ + --hash=sha256:cc8b8582f4209c2459b04b049ac03c72c618e011d3caa5391ff86d1bda0cc486 \ + --hash=sha256:cd39455b7ee70aabee46f7399b32ab38b86b236c069ae559e22be6b46b2bbfc4 \ + --hash=sha256:d0cf61450feadca81deb1a1489cb1a3ae1e4266efd51adafecec0e503a8dcd84 \ + --hash=sha256:d18d86bc9e103f4d3d4f18b85a0471c0e13ce5b79194e4a0389a224bb70edd53 \ + --hash=sha256:d5428dbcd43d02f6306e1c3c95f692f68b284e6ee5390292242f509004c9e3a8 \ + --hash=sha256:de3a860f037bb51f968de320baef85090ff0bbb42ec4f28ec6a5ddf88be61871 \ + --hash=sha256:e0061af6e8c5e68b13f1ec9ad5251247726653c5af3c0bbdfbca6cf931e99216 \ + --hash=sha256:e5df28f74002e37bcbdfdc5109799f670e4dfef0fb527c391ff84f078050e7b5 \ + --hash=sha256:e6a720a3d22b54183352dc65f08cd1547204d263e0651b213a0a2e577e838526 \ + --hash=sha256:e8400587d594761e5136a3423111f499574be5fd53cf0aefa0d0f05b180710b0 \ + --hash=sha256:e84bc7707492f06fbc37a9f215374d2977d21b72e10a67f1b31893ec5a140ad8 \ + --hash=sha256:ef7225755ed27bfdb18730c68f6cb023d06c28f2b734597480fb4c0e500feb6f \ + --hash=sha256:f2b7bec27e047e84947fbd41c782f07c54c30c76d14f3b8bf0c89f7413fac67a \ + --hash=sha256:f2fbdb87fe5075c8beb17a5c16348a1ea3c8b282a5cb72d173330be2fecf22f5 \ + --hash=sha256:f5bf0eb8b6ef40fa22479f09375468c33bebb7fe49d14d9c96c8fd50355188b0 \ + --hash=sha256:fdc20cd1e1141b04696ffcdb7c71e8a4a665db31fe72e51ec706b3bdd2d09f36 + # via + # -r lock/requirements-dev.in + # motor +pytest==8.3.3 \ + --hash=sha256:70b98107bd648308a7952b06e6ca9a50bc660be218d53c257cc1fc94fda10181 \ + --hash=sha256:a6853c7375b2663155079443d2e45de913a911a11d669df02a50814944db57b2 + # via + # -r lock/requirements-dev-template.in + # pytest-asyncio + # pytest-cov + # pytest-httpx +pytest-asyncio==0.24.0 \ + --hash=sha256:a811296ed596b69bf0b6f3dc40f83bcaf341b155a269052d82efa2b25ac7037b \ + --hash=sha256:d081d828e576d85f875399194281e92bf8a68d60d72d1a2faf2feddb6c46b276 + # via -r lock/requirements-dev-template.in +pytest-cov==5.0.0 \ + --hash=sha256:4f0764a1219df53214206bf1feea4633c3b558a2925c8b59f144f682861ce652 \ + --hash=sha256:5837b58e9f6ebd335b0f8060eecce69b662415b16dc503883a02f45dfeb14857 + # via -r lock/requirements-dev-template.in +pytest-httpx==0.30.0 \ + --hash=sha256:6d47849691faf11d2532565d0c8e0e02b9f4ee730da31687feae315581d7520c \ + --hash=sha256:755b8edca87c974dd4f3605c374fda11db84631de3d163b99c0df5807023a19a + # via -r lock/requirements-dev-template.in +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 +python-multipart==0.0.9 \ + --hash=sha256:03f54688c663f1b7977105f021043b0793151e4cb1c1a9d4a11fc13d622c4026 \ + --hash=sha256:97ca7b8ea7b05f977dc3849c3ba99d51689822fab725c3703af7c866a0c2b215 + # via fastapi +pyyaml==6.0.2 \ + --hash=sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff \ + --hash=sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48 \ + --hash=sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086 \ + --hash=sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e \ + --hash=sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133 \ + --hash=sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5 \ + --hash=sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484 \ + --hash=sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee \ + --hash=sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5 \ + --hash=sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68 \ + --hash=sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a \ + --hash=sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf \ + --hash=sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99 \ + --hash=sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8 \ + --hash=sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85 \ + --hash=sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19 \ + --hash=sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc \ + --hash=sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a \ + --hash=sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1 \ + --hash=sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317 \ + --hash=sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c \ + --hash=sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631 \ + --hash=sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d \ + --hash=sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652 \ + --hash=sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5 \ + --hash=sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e \ + --hash=sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b \ + --hash=sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8 \ + --hash=sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476 \ + --hash=sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706 \ + --hash=sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563 \ + --hash=sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237 \ + --hash=sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b \ + --hash=sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083 \ + --hash=sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180 \ + --hash=sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425 \ + --hash=sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e \ + --hash=sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f \ + --hash=sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725 \ + --hash=sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183 \ + --hash=sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab \ + --hash=sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774 \ + --hash=sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725 \ + --hash=sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e \ + --hash=sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5 \ + --hash=sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d \ + --hash=sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290 \ + --hash=sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44 \ + --hash=sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed \ + --hash=sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4 \ + --hash=sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba \ + --hash=sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12 \ + --hash=sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4 + # via + # hexkit + # jsonschema2md + # pre-commit + # uvicorn +referencing==0.35.1 \ + --hash=sha256:25b42124a6c8b632a425174f24087783efb348a6f1e0008e63cd4466fedf703c \ + --hash=sha256:eda6d3234d62814d1c64e305c1331c9a3a6132da475ab6382eaa997b21ee75de + # via + # jsonschema + # jsonschema-specifications +requests==2.32.3 \ + --hash=sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760 \ + --hash=sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6 + # via + # -r lock/requirements-dev-template.in + # docker +rich==13.8.1 \ + --hash=sha256:1760a3c0848469b97b558fc61c85233e3dafb69c7a071b4d60c38099d3cd4c06 \ + --hash=sha256:8260cda28e3db6bf04d2d1ef4dbc03ba80a824c88b0e7668a0f23126a424844a + # via typer +rpds-py==0.20.0 \ + --hash=sha256:06db23d43f26478303e954c34c75182356ca9aa7797d22c5345b16871ab9c45c \ + --hash=sha256:0e13e6952ef264c40587d510ad676a988df19adea20444c2b295e536457bc585 \ + --hash=sha256:11ef6ce74616342888b69878d45e9f779b95d4bd48b382a229fe624a409b72c5 \ + --hash=sha256:1259c7b3705ac0a0bd38197565a5d603218591d3f6cee6e614e380b6ba61c6f6 \ + --hash=sha256:18d7585c463087bddcfa74c2ba267339f14f2515158ac4db30b1f9cbdb62c8ef \ + --hash=sha256:1e0f80b739e5a8f54837be5d5c924483996b603d5502bfff79bf33da06164ee2 \ + --hash=sha256:1e5f3cd7397c8f86c8cc72d5a791071431c108edd79872cdd96e00abd8497d29 \ + --hash=sha256:220002c1b846db9afd83371d08d239fdc865e8f8c5795bbaec20916a76db3318 \ + --hash=sha256:22e6c9976e38f4d8c4a63bd8a8edac5307dffd3ee7e6026d97f3cc3a2dc02a0b \ + --hash=sha256:238a2d5b1cad28cdc6ed15faf93a998336eb041c4e440dd7f902528b8891b399 \ + --hash=sha256:2580b0c34583b85efec8c5c5ec9edf2dfe817330cc882ee972ae650e7b5ef739 \ + --hash=sha256:28527c685f237c05445efec62426d285e47a58fb05ba0090a4340b73ecda6dee \ + --hash=sha256:2cf126d33a91ee6eedc7f3197b53e87a2acdac63602c0f03a02dd69e4b138174 \ + --hash=sha256:338ca4539aad4ce70a656e5187a3a31c5204f261aef9f6ab50e50bcdffaf050a \ + --hash=sha256:39ed0d010457a78f54090fafb5d108501b5aa5604cc22408fc1c0c77eac14344 \ + --hash=sha256:3ad0fda1635f8439cde85c700f964b23ed5fc2d28016b32b9ee5fe30da5c84e2 \ + --hash=sha256:3d2b1ad682a3dfda2a4e8ad8572f3100f95fad98cb99faf37ff0ddfe9cbf9d03 \ + --hash=sha256:3d61339e9f84a3f0767b1995adfb171a0d00a1185192718a17af6e124728e0f5 \ + --hash=sha256:3fde368e9140312b6e8b6c09fb9f8c8c2f00999d1823403ae90cc00480221b22 \ + --hash=sha256:40ce74fc86ee4645d0a225498d091d8bc61f39b709ebef8204cb8b5a464d3c0e \ + --hash=sha256:49a8063ea4296b3a7e81a5dfb8f7b2d73f0b1c20c2af401fb0cdf22e14711a96 \ + --hash=sha256:4a1f1d51eccb7e6c32ae89243cb352389228ea62f89cd80823ea7dd1b98e0b91 \ + --hash=sha256:4b16aa0107ecb512b568244ef461f27697164d9a68d8b35090e9b0c1c8b27752 \ + --hash=sha256:4f1ed4749a08379555cebf4650453f14452eaa9c43d0a95c49db50c18b7da075 \ + --hash=sha256:4fe84294c7019456e56d93e8ababdad5a329cd25975be749c3f5f558abb48253 \ + --hash=sha256:50eccbf054e62a7b2209b28dc7a22d6254860209d6753e6b78cfaeb0075d7bee \ + --hash=sha256:514b3293b64187172bc77c8fb0cdae26981618021053b30d8371c3a902d4d5ad \ + --hash=sha256:54b43a2b07db18314669092bb2de584524d1ef414588780261e31e85846c26a5 \ + --hash=sha256:55fea87029cded5df854ca7e192ec7bdb7ecd1d9a3f63d5c4eb09148acf4a7ce \ + --hash=sha256:569b3ea770c2717b730b61998b6c54996adee3cef69fc28d444f3e7920313cf7 \ + --hash=sha256:56e27147a5a4c2c21633ff8475d185734c0e4befd1c989b5b95a5d0db699b21b \ + --hash=sha256:57eb94a8c16ab08fef6404301c38318e2c5a32216bf5de453e2714c964c125c8 \ + --hash=sha256:5a35df9f5548fd79cb2f52d27182108c3e6641a4feb0f39067911bf2adaa3e57 \ + --hash=sha256:5a8c94dad2e45324fc74dce25e1645d4d14df9a4e54a30fa0ae8bad9a63928e3 \ + --hash=sha256:5b4f105deeffa28bbcdff6c49b34e74903139afa690e35d2d9e3c2c2fba18cec \ + --hash=sha256:5c1dc0f53856b9cc9a0ccca0a7cc61d3d20a7088201c0937f3f4048c1718a209 \ + --hash=sha256:614fdafe9f5f19c63ea02817fa4861c606a59a604a77c8cdef5aa01d28b97921 \ + --hash=sha256:617c7357272c67696fd052811e352ac54ed1d9b49ab370261a80d3b6ce385045 \ + --hash=sha256:65794e4048ee837494aea3c21a28ad5fc080994dfba5b036cf84de37f7ad5074 \ + --hash=sha256:6632f2d04f15d1bd6fe0eedd3b86d9061b836ddca4c03d5cf5c7e9e6b7c14580 \ + --hash=sha256:6c8ef2ebf76df43f5750b46851ed1cdf8f109d7787ca40035fe19fbdc1acc5a7 \ + --hash=sha256:758406267907b3781beee0f0edfe4a179fbd97c0be2e9b1154d7f0a1279cf8e5 \ + --hash=sha256:7e60cb630f674a31f0368ed32b2a6b4331b8350d67de53c0359992444b116dd3 \ + --hash=sha256:89c19a494bf3ad08c1da49445cc5d13d8fefc265f48ee7e7556839acdacf69d0 \ + --hash=sha256:8a86a9b96070674fc88b6f9f71a97d2c1d3e5165574615d1f9168ecba4cecb24 \ + --hash=sha256:8bc7690f7caee50b04a79bf017a8d020c1f48c2a1077ffe172abec59870f1139 \ + --hash=sha256:8d7919548df3f25374a1f5d01fbcd38dacab338ef5f33e044744b5c36729c8db \ + --hash=sha256:9426133526f69fcaba6e42146b4e12d6bc6c839b8b555097020e2b78ce908dcc \ + --hash=sha256:9824fb430c9cf9af743cf7aaf6707bf14323fb51ee74425c380f4c846ea70789 \ + --hash=sha256:9bb4a0d90fdb03437c109a17eade42dfbf6190408f29b2744114d11586611d6f \ + --hash=sha256:9bc2d153989e3216b0559251b0c260cfd168ec78b1fac33dd485750a228db5a2 \ + --hash=sha256:9d35cef91e59ebbeaa45214861874bc6f19eb35de96db73e467a8358d701a96c \ + --hash=sha256:a1862d2d7ce1674cffa6d186d53ca95c6e17ed2b06b3f4c476173565c862d232 \ + --hash=sha256:a84ab91cbe7aab97f7446652d0ed37d35b68a465aeef8fc41932a9d7eee2c1a6 \ + --hash=sha256:aa7f429242aae2947246587d2964fad750b79e8c233a2367f71b554e9447949c \ + --hash=sha256:aa9a0521aeca7d4941499a73ad7d4f8ffa3d1affc50b9ea11d992cd7eff18a29 \ + --hash=sha256:ac2f4f7a98934c2ed6505aead07b979e6f999389f16b714448fb39bbaa86a489 \ + --hash=sha256:ae94bd0b2f02c28e199e9bc51485d0c5601f58780636185660f86bf80c89af94 \ + --hash=sha256:af0fc424a5842a11e28956e69395fbbeab2c97c42253169d87e90aac2886d751 \ + --hash=sha256:b2a5db5397d82fa847e4c624b0c98fe59d2d9b7cf0ce6de09e4d2e80f8f5b3f2 \ + --hash=sha256:b4c29cbbba378759ac5786730d1c3cb4ec6f8ababf5c42a9ce303dc4b3d08cda \ + --hash=sha256:b74b25f024b421d5859d156750ea9a65651793d51b76a2e9238c05c9d5f203a9 \ + --hash=sha256:b7f19250ceef892adf27f0399b9e5afad019288e9be756d6919cb58892129f51 \ + --hash=sha256:b80d4a7900cf6b66bb9cee5c352b2d708e29e5a37fe9bf784fa97fc11504bf6c \ + --hash=sha256:b8c00a3b1e70c1d3891f0db1b05292747f0dbcfb49c43f9244d04c70fbc40eb8 \ + --hash=sha256:bb273176be34a746bdac0b0d7e4e2c467323d13640b736c4c477881a3220a989 \ + --hash=sha256:c3c20f0ddeb6e29126d45f89206b8291352b8c5b44384e78a6499d68b52ae511 \ + --hash=sha256:c3e130fd0ec56cb76eb49ef52faead8ff09d13f4527e9b0c400307ff72b408e1 \ + --hash=sha256:c52d3f2f82b763a24ef52f5d24358553e8403ce05f893b5347098014f2d9eff2 \ + --hash=sha256:c6377e647bbfd0a0b159fe557f2c6c602c159fc752fa316572f012fc0bf67150 \ + --hash=sha256:c638144ce971df84650d3ed0096e2ae7af8e62ecbbb7b201c8935c370df00a2c \ + --hash=sha256:ce9845054c13696f7af7f2b353e6b4f676dab1b4b215d7fe5e05c6f8bb06f965 \ + --hash=sha256:cf258ede5bc22a45c8e726b29835b9303c285ab46fc7c3a4cc770736b5304c9f \ + --hash=sha256:d0a26ffe9d4dd35e4dfdd1e71f46401cff0181c75ac174711ccff0459135fa58 \ + --hash=sha256:d0b67d87bb45ed1cd020e8fbf2307d449b68abc45402fe1a4ac9e46c3c8b192b \ + --hash=sha256:d20277fd62e1b992a50c43f13fbe13277a31f8c9f70d59759c88f644d66c619f \ + --hash=sha256:d454b8749b4bd70dd0a79f428731ee263fa6995f83ccb8bada706e8d1d3ff89d \ + --hash=sha256:d4c7d1a051eeb39f5c9547e82ea27cbcc28338482242e3e0b7768033cb083821 \ + --hash=sha256:d72278a30111e5b5525c1dd96120d9e958464316f55adb030433ea905866f4de \ + --hash=sha256:d72a210824facfdaf8768cf2d7ca25a042c30320b3020de2fa04640920d4e121 \ + --hash=sha256:d807dc2051abe041b6649681dce568f8e10668e3c1c6543ebae58f2d7e617855 \ + --hash=sha256:dbe982f38565bb50cb7fb061ebf762c2f254ca3d8c20d4006878766e84266272 \ + --hash=sha256:dcedf0b42bcb4cfff4101d7771a10532415a6106062f005ab97d1d0ab5681c60 \ + --hash=sha256:deb62214c42a261cb3eb04d474f7155279c1a8a8c30ac89b7dcb1721d92c3c02 \ + --hash=sha256:def7400461c3a3f26e49078302e1c1b38f6752342c77e3cf72ce91ca69fb1bc1 \ + --hash=sha256:df3de6b7726b52966edf29663e57306b23ef775faf0ac01a3e9f4012a24a4140 \ + --hash=sha256:e1940dae14e715e2e02dfd5b0f64a52e8374a517a1e531ad9412319dc3ac7879 \ + --hash=sha256:e4df1e3b3bec320790f699890d41c59d250f6beda159ea3c44c3f5bac1976940 \ + --hash=sha256:e6900ecdd50ce0facf703f7a00df12374b74bbc8ad9fe0f6559947fb20f82364 \ + --hash=sha256:ea438162a9fcbee3ecf36c23e6c68237479f89f962f82dae83dc15feeceb37e4 \ + --hash=sha256:eb851b7df9dda52dc1415ebee12362047ce771fc36914586b2e9fcbd7d293b3e \ + --hash=sha256:ec31a99ca63bf3cd7f1a5ac9fe95c5e2d060d3c768a09bc1d16e235840861420 \ + --hash=sha256:f0475242f447cc6cb8a9dd486d68b2ef7fbee84427124c232bff5f63b1fe11e5 \ + --hash=sha256:f2fbf7db2012d4876fb0d66b5b9ba6591197b0f165db8d99371d976546472a24 \ + --hash=sha256:f60012a73aa396be721558caa3a6fd49b3dd0033d1675c6d59c4502e870fcf0c \ + --hash=sha256:f8e604fe73ba048c06085beaf51147eaec7df856824bfe7b98657cf436623daf \ + --hash=sha256:f90a4cd061914a60bd51c68bcb4357086991bd0bb93d8aa66a6da7701370708f \ + --hash=sha256:f918a1a130a6dfe1d7fe0f105064141342e7dd1611f2e6a21cd2f5c8cb1cfb3e \ + --hash=sha256:fa518bcd7600c584bf42e6617ee8132869e877db2f76bcdc281ec6a4113a53ab \ + --hash=sha256:faefcc78f53a88f3076b7f8be0a8f8d35133a3ecf7f3770895c25f8813460f08 \ + --hash=sha256:fcaeb7b57f1a1e071ebd748984359fef83ecb026325b9d4ca847c95bc7311c92 \ + --hash=sha256:fd2d84f40633bc475ef2d5490b9c19543fbf18596dcb1b291e3a12ea5d722f7a \ + --hash=sha256:fdfc3a892927458d98f3d55428ae46b921d1f7543b89382fdb483f5640daaec8 + # via + # jsonschema + # referencing +ruff==0.6.5 \ + --hash=sha256:005256d977021790cc52aa23d78f06bb5090dc0bfbd42de46d49c201533982ae \ + --hash=sha256:09c72a833fd3551135ceddcba5ebdb68ff89225d30758027280968c9acdc7810 \ + --hash=sha256:381413ec47f71ce1d1c614f7779d88886f406f1fd53d289c77e4e533dc6ea200 \ + --hash=sha256:3a8d42d11fff8d3143ff4da41742a98f8f233bf8890e9fe23077826818f8d680 \ + --hash=sha256:3e42a57b58e3612051a636bc1ac4e6b838679530235520e8f095f7c44f706ff9 \ + --hash=sha256:482c1e6bfeb615eafc5899127b805d28e387bd87db38b2c0c41d271f5e58d8cc \ + --hash=sha256:4d32d87fab433c0cf285c3683dd4dae63be05fd7a1d65b3f5bf7cdd05a6b96fb \ + --hash=sha256:51935067740773afdf97493ba9b8231279e9beef0f2a8079188c4776c25688e0 \ + --hash=sha256:52e75a82bbc9b42e63c08d22ad0ac525117e72aee9729a069d7c4f235fc4d276 \ + --hash=sha256:7291e64d7129f24d1b0c947ec3ec4c0076e958d1475c61202497c6aced35dd19 \ + --hash=sha256:794ada3400a0d0b89e3015f1a7e01f4c97320ac665b7bc3ade24b50b54cb2972 \ + --hash=sha256:7e4e308f16e07c95fc7753fc1aaac690a323b2bb9f4ec5e844a97bb7fbebd748 \ + --hash=sha256:800c50371bdcb99b3c1551d5691e14d16d6f07063a518770254227f7f6e8c178 \ + --hash=sha256:8e25ddd9cd63ba1f3bd51c1f09903904a6adf8429df34f17d728a8fa11174253 \ + --hash=sha256:932cd69eefe4daf8c7d92bd6689f7e8182571cb934ea720af218929da7bd7d69 \ + --hash=sha256:9ad7dfbd138d09d9a7e6931e6a7e797651ce29becd688be8a0d4d5f8177b4b0c \ + --hash=sha256:a50af6e828ee692fb10ff2dfe53f05caecf077f4210fae9677e06a808275754f \ + --hash=sha256:cf4d3fa53644137f6a4a27a2b397381d16454a1566ae5335855c187fbf67e4f5 + # via -r lock/requirements-dev-template.in +s3transfer==0.10.2 \ + --hash=sha256:0711534e9356d3cc692fdde846b4a1e4b0cb6519971860796e6bc4c7aea00ef6 \ + --hash=sha256:eca1c20de70a39daee580aef4986996620f365c4e0fda6a86100231d62f1bf69 + # via boto3 +setuptools==75.1.0 \ + --hash=sha256:35ab7fd3bcd95e6b7fd704e4a1539513edad446c097797f2985e0e4b960772f2 \ + --hash=sha256:d59a21b17a275fb872a9c3dae73963160ae079f1049ed956880cd7c09b120538 + # via -r lock/requirements-dev-template.in +shellingham==1.5.4 \ + --hash=sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686 \ + --hash=sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de + # via typer +six==1.16.0 \ + --hash=sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926 \ + --hash=sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254 + # via python-dateutil +snakeviz==2.2.0 \ + --hash=sha256:569e2d71c47f80a886aa6e70d6405cb6d30aa3520969ad956b06f824c5f02b8e \ + --hash=sha256:7bfd00be7ae147eb4a170a471578e1cd3f41f803238958b6b8efcf2c698a6aa9 + # via -r lock/requirements-dev-template.in +sniffio==1.3.1 \ + --hash=sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2 \ + --hash=sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc + # via + # anyio + # httpx +starlette==0.37.2 \ + --hash=sha256:6fe59f29268538e5d0d182f2791a479a0c64638e6935d1c6989e63fb2699c6ee \ + --hash=sha256:9af890290133b79fc3db55474ade20f6220a364a0402e0b556e7cd5e1e093823 + # via fastapi +stringcase==1.2.0 \ + --hash=sha256:48a06980661908efe8d9d34eab2b6c13aefa2163b3ced26972902e3bdfd87008 + # via -r lock/requirements-dev-template.in +testcontainers==4.8.1 \ + --hash=sha256:5ded4820b7227ad526857eb3caaafcabce1bbac05d22ad194849b136ffae3cb0 \ + --hash=sha256:d8ae43e8fe34060fcd5c3f494e0b7652b7774beabe94568a2283d0881e94d489 + # via -r lock/requirements-dev.in +tomli-w==1.0.0 \ + --hash=sha256:9f2a07e8be30a0729e533ec968016807069991ae2fd921a78d42f429ae5f4463 \ + --hash=sha256:f463434305e0336248cac9c2dc8076b707d8a12d019dd349f5c1e382dd1ae1b9 + # via -r lock/requirements-dev-template.in +tornado==6.4.1 \ + --hash=sha256:163b0aafc8e23d8cdc3c9dfb24c5368af84a81e3364745ccb4427669bf84aec8 \ + --hash=sha256:25486eb223babe3eed4b8aecbac33b37e3dd6d776bc730ca14e1bf93888b979f \ + --hash=sha256:454db8a7ecfcf2ff6042dde58404164d969b6f5d58b926da15e6b23817950fc4 \ + --hash=sha256:613bf4ddf5c7a95509218b149b555621497a6cc0d46ac341b30bd9ec19eac7f3 \ + --hash=sha256:6d5ce3437e18a2b66fbadb183c1d3364fb03f2be71299e7d10dbeeb69f4b2a14 \ + --hash=sha256:8ae50a504a740365267b2a8d1a90c9fbc86b780a39170feca9bcc1787ff80842 \ + --hash=sha256:92d3ab53183d8c50f8204a51e6f91d18a15d5ef261e84d452800d4ff6fc504e9 \ + --hash=sha256:a02a08cc7a9314b006f653ce40483b9b3c12cda222d6a46d4ac63bb6c9057698 \ + --hash=sha256:b24b8982ed444378d7f21d563f4180a2de31ced9d8d84443907a0a64da2072e7 \ + --hash=sha256:d9a566c40b89757c9aa8e6f032bcdb8ca8795d7c1a9762910c722b1635c9de4d \ + --hash=sha256:e2e20b9113cd7293f164dc46fffb13535266e713cdb87bd2d15ddb336e96cfc4 + # via snakeviz +typer==0.12.5 \ + --hash=sha256:62fe4e471711b147e3365034133904df3e235698399bc4de2b36c8579298d52b \ + --hash=sha256:f592f089bedcc8ec1b974125d64851029c3b1af145f04aca64d69410f0c9b722 + # via + # -r lock/requirements-dev-template.in + # dins (pyproject.toml) + # fastapi-cli +typing-extensions==4.12.2 \ + --hash=sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d \ + --hash=sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8 + # via + # aiokafka + # fastapi + # mypy + # pydantic + # pydantic-core + # testcontainers + # typer +urllib3==2.2.3 \ + --hash=sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac \ + --hash=sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9 + # via + # -r lock/requirements-dev-template.in + # botocore + # docker + # requests + # testcontainers +uv==0.4.12 \ + --hash=sha256:0840d0141f54f64474c9dbd46787971859fac9deacc701091b44f1c47d066823 \ + --hash=sha256:0d548c090bf38fb76b6493c90bbfbad30bfc4b41365019953bffbc54d32394ed \ + --hash=sha256:0f00d15108af7b17f49d70714a31927eed27e192d5e5410822c098399d61196d \ + --hash=sha256:31f7689c6f49b0489dc727b1e6f0f008f7db21388c3cf374577a445bd7d727b8 \ + --hash=sha256:56901b53c9bcce81305826c89378058922b405d0fbfb5c2742dda7dc5fdf891c \ + --hash=sha256:649d2974da5d867ca0230a15aa75d6e4625c2a71eddc0abaeebe7a167038f56b \ + --hash=sha256:67327c5997a9c4531c0e13be8545aa6568a15c99a97770ac65f6dcc5600e8a9c \ + --hash=sha256:6922ca516056069a6c835f0cf60053241bb3438e4ccc0356c223d4f5c0d92254 \ + --hash=sha256:86635a9dd024d08499405c9e1c1087aa24ffbfe89eb6dde010e5a60855e661bc \ + --hash=sha256:8a102ee30a41909634b28cb9d7d5a03af2953aa86ff941e24916093f4a74d44f \ + --hash=sha256:8cbfa5ed4ea167291260416d71d54ffb949b0b98bcf945190adb8c65e30492be \ + --hash=sha256:9aa768f4b94335a4145d74e73ff4721cb1a3e1fd1269f4bb95187a9f8d41f8e1 \ + --hash=sha256:a1d2ada46563178cacfeb2ff8a3b2764381a953cee87002fad0b9181f4a35e0d \ + --hash=sha256:a3c1b7b4a6e5258c0b20079beb1d22c3d306f7695eab8a3d3aea93b37db01b3a \ + --hash=sha256:c081b13c7789b518a2077ed0c49d33c9d855e110a2f670e4f354696245089edc \ + --hash=sha256:c6861b3c92da1cdc2cb18c76b0e05004413ce1cc95782a4b34b7ee002006efb8 \ + --hash=sha256:dc638ff81e817a1c049c8bd51c623238dccf9bfbfb17e20878eaece6c74338bb \ + --hash=sha256:e931a2add4dfec717184164a54608b99d37e0000b9c151bb020a0a2dcc6d5cc1 + # via -r lock/requirements-dev-template.in +uvicorn==0.29.0 \ + --hash=sha256:2c2aac7ff4f4365c206fd773a39bf4ebd1047c238f8b8268ad996829323473de \ + --hash=sha256:6a69214c0b6a087462412670b3ef21224fa48cae0e452b5883e8e8bdfdd11dd0 + # via + # fastapi + # fastapi-cli + # ghga-service-commons +uvloop==0.20.0 \ + --hash=sha256:265a99a2ff41a0fd56c19c3838b29bf54d1d177964c300dad388b27e84fd7847 \ + --hash=sha256:2beee18efd33fa6fdb0976e18475a4042cd31c7433c866e8a09ab604c7c22ff2 \ + --hash=sha256:35968fc697b0527a06e134999eef859b4034b37aebca537daeb598b9d45a137b \ + --hash=sha256:36c530d8fa03bfa7085af54a48f2ca16ab74df3ec7108a46ba82fd8b411a2315 \ + --hash=sha256:3a609780e942d43a275a617c0839d85f95c334bad29c4c0918252085113285b5 \ + --hash=sha256:4603ca714a754fc8d9b197e325db25b2ea045385e8a3ad05d3463de725fdf469 \ + --hash=sha256:4b75f2950ddb6feed85336412b9a0c310a2edbcf4cf931aa5cfe29034829676d \ + --hash=sha256:4f44af67bf39af25db4c1ac27e82e9665717f9c26af2369c404be865c8818dcf \ + --hash=sha256:6462c95f48e2d8d4c993a2950cd3d31ab061864d1c226bbf0ee2f1a8f36674b9 \ + --hash=sha256:649c33034979273fa71aa25d0fe120ad1777c551d8c4cd2c0c9851d88fcb13ab \ + --hash=sha256:746242cd703dc2b37f9d8b9f173749c15e9a918ddb021575a0205ec29a38d31e \ + --hash=sha256:77fbc69c287596880ecec2d4c7a62346bef08b6209749bf6ce8c22bbaca0239e \ + --hash=sha256:80dc1b139516be2077b3e57ce1cb65bfed09149e1d175e0478e7a987863b68f0 \ + --hash=sha256:82edbfd3df39fb3d108fc079ebc461330f7c2e33dbd002d146bf7c445ba6e756 \ + --hash=sha256:89e8d33bb88d7263f74dc57d69f0063e06b5a5ce50bb9a6b32f5fcbe655f9e73 \ + --hash=sha256:94707205efbe809dfa3a0d09c08bef1352f5d3d6612a506f10a319933757c006 \ + --hash=sha256:95720bae002ac357202e0d866128eb1ac82545bcf0b549b9abe91b5178d9b541 \ + --hash=sha256:9b04d96188d365151d1af41fa2d23257b674e7ead68cfd61c725a422764062ae \ + --hash=sha256:9d0fba61846f294bce41eb44d60d58136090ea2b5b99efd21cbdf4e21927c56a \ + --hash=sha256:9ebafa0b96c62881d5cafa02d9da2e44c23f9f0cd829f3a32a6aff771449c996 \ + --hash=sha256:a0fac7be202596c7126146660725157d4813aa29a4cc990fe51346f75ff8fde7 \ + --hash=sha256:aea15c78e0d9ad6555ed201344ae36db5c63d428818b4b2a42842b3870127c00 \ + --hash=sha256:b10c2956efcecb981bf9cfb8184d27d5d64b9033f917115a960b83f11bfa0d6b \ + --hash=sha256:b16696f10e59d7580979b420eedf6650010a4a9c3bd8113f24a103dfdb770b10 \ + --hash=sha256:d8c36fdf3e02cec92aed2d44f63565ad1522a499c654f07935c8f9d04db69e95 \ + --hash=sha256:e237f9c1e8a00e7d9ddaa288e535dc337a39bcbf679f290aee9d26df9e72bce9 \ + --hash=sha256:e50289c101495e0d1bb0bfcb4a60adde56e32f4449a67216a1ab2750aa84f037 \ + --hash=sha256:e7d61fe8e8d9335fac1bf8d5d82820b4808dd7a43020c149b63a1ada953d48a6 \ + --hash=sha256:e97152983442b499d7a71e44f29baa75b3b02e65d9c44ba53b10338e98dedb66 \ + --hash=sha256:f0e94b221295b5e69de57a1bd4aeb0b3a29f61be6e1b478bb8a69a73377db7ba \ + --hash=sha256:fee6044b64c965c425b65a4e17719953b96e065c5b7e09b599ff332bb2744bdf + # via uvicorn +virtualenv==20.26.5 \ + --hash=sha256:4f3ac17b81fba3ce3bd6f4ead2749a72da5929c01774948e243db9ba41df4ff6 \ + --hash=sha256:ce489cac131aa58f4b25e321d6d186171f78e6cb13fafbf32a840cee67733ff4 + # via pre-commit +watchfiles==0.24.0 \ + --hash=sha256:01550ccf1d0aed6ea375ef259706af76ad009ef5b0203a3a4cce0f6024f9b68a \ + --hash=sha256:01def80eb62bd5db99a798d5e1f5f940ca0a05986dcfae21d833af7a46f7ee22 \ + --hash=sha256:07cdef0c84c03375f4e24642ef8d8178e533596b229d32d2bbd69e5128ede02a \ + --hash=sha256:083dc77dbdeef09fa44bb0f4d1df571d2e12d8a8f985dccde71ac3ac9ac067a0 \ + --hash=sha256:1cf1f6dd7825053f3d98f6d33f6464ebdd9ee95acd74ba2c34e183086900a827 \ + --hash=sha256:21ab23fdc1208086d99ad3f69c231ba265628014d4aed31d4e8746bd59e88cd1 \ + --hash=sha256:2dadf8a8014fde6addfd3c379e6ed1a981c8f0a48292d662e27cabfe4239c83c \ + --hash=sha256:2e28d91ef48eab0afb939fa446d8ebe77e2f7593f5f463fd2bb2b14132f95b6e \ + --hash=sha256:2efec17819b0046dde35d13fb8ac7a3ad877af41ae4640f4109d9154ed30a188 \ + --hash=sha256:30bbd525c3262fd9f4b1865cb8d88e21161366561cd7c9e1194819e0a33ea86b \ + --hash=sha256:316449aefacf40147a9efaf3bd7c9bdd35aaba9ac5d708bd1eb5763c9a02bef5 \ + --hash=sha256:327763da824817b38ad125dcd97595f942d720d32d879f6c4ddf843e3da3fe90 \ + --hash=sha256:32aa53a9a63b7f01ed32e316e354e81e9da0e6267435c7243bf8ae0f10b428ef \ + --hash=sha256:34e19e56d68b0dad5cff62273107cf5d9fbaf9d75c46277aa5d803b3ef8a9e9b \ + --hash=sha256:3770e260b18e7f4e576edca4c0a639f704088602e0bc921c5c2e721e3acb8d15 \ + --hash=sha256:3d2e3ab79a1771c530233cadfd277fcc762656d50836c77abb2e5e72b88e3a48 \ + --hash=sha256:41face41f036fee09eba33a5b53a73e9a43d5cb2c53dad8e61fa6c9f91b5a51e \ + --hash=sha256:43e3e37c15a8b6fe00c1bce2473cfa8eb3484bbeecf3aefbf259227e487a03df \ + --hash=sha256:449f43f49c8ddca87c6b3980c9284cab6bd1f5c9d9a2b00012adaaccd5e7decd \ + --hash=sha256:4933a508d2f78099162da473841c652ad0de892719043d3f07cc83b33dfd9d91 \ + --hash=sha256:49d617df841a63b4445790a254013aea2120357ccacbed00253f9c2b5dc24e2d \ + --hash=sha256:49fb58bcaa343fedc6a9e91f90195b20ccb3135447dc9e4e2570c3a39565853e \ + --hash=sha256:4a7fa2bc0efef3e209a8199fd111b8969fe9db9c711acc46636686331eda7dd4 \ + --hash=sha256:4abf4ad269856618f82dee296ac66b0cd1d71450fc3c98532d93798e73399b7a \ + --hash=sha256:4b8693502d1967b00f2fb82fc1e744df128ba22f530e15b763c8d82baee15370 \ + --hash=sha256:4d28cea3c976499475f5b7a2fec6b3a36208656963c1a856d328aeae056fc5c1 \ + --hash=sha256:5148c2f1ea043db13ce9b0c28456e18ecc8f14f41325aa624314095b6aa2e9ea \ + --hash=sha256:54ca90a9ae6597ae6dc00e7ed0a040ef723f84ec517d3e7ce13e63e4bc82fa04 \ + --hash=sha256:551ec3ee2a3ac9cbcf48a4ec76e42c2ef938a7e905a35b42a1267fa4b1645896 \ + --hash=sha256:5c51749f3e4e269231510da426ce4a44beb98db2dce9097225c338f815b05d4f \ + --hash=sha256:632676574429bee8c26be8af52af20e0c718cc7f5f67f3fb658c71928ccd4f7f \ + --hash=sha256:6509ed3f467b79d95fc62a98229f79b1a60d1b93f101e1c61d10c95a46a84f43 \ + --hash=sha256:6bdcfa3cd6fdbdd1a068a52820f46a815401cbc2cb187dd006cb076675e7b735 \ + --hash=sha256:7138eff8baa883aeaa074359daabb8b6c1e73ffe69d5accdc907d62e50b1c0da \ + --hash=sha256:7211b463695d1e995ca3feb38b69227e46dbd03947172585ecb0588f19b0d87a \ + --hash=sha256:73bde715f940bea845a95247ea3e5eb17769ba1010efdc938ffcb967c634fa61 \ + --hash=sha256:78470906a6be5199524641f538bd2c56bb809cd4bf29a566a75051610bc982c3 \ + --hash=sha256:7ae3e208b31be8ce7f4c2c0034f33406dd24fbce3467f77223d10cd86778471c \ + --hash=sha256:7e4bd963a935aaf40b625c2499f3f4f6bbd0c3776f6d3bc7c853d04824ff1c9f \ + --hash=sha256:82ae557a8c037c42a6ef26c494d0631cacca040934b101d001100ed93d43f361 \ + --hash=sha256:82b2509f08761f29a0fdad35f7e1638b8ab1adfa2666d41b794090361fb8b855 \ + --hash=sha256:8360f7314a070c30e4c976b183d1d8d1585a4a50c5cb603f431cebcbb4f66327 \ + --hash=sha256:85d5f0c7771dcc7a26c7a27145059b6bb0ce06e4e751ed76cdf123d7039b60b5 \ + --hash=sha256:88bcd4d0fe1d8ff43675360a72def210ebad3f3f72cabfeac08d825d2639b4ab \ + --hash=sha256:9301c689051a4857d5b10777da23fafb8e8e921bcf3abe6448a058d27fb67633 \ + --hash=sha256:951088d12d339690a92cef2ec5d3cfd957692834c72ffd570ea76a6790222777 \ + --hash=sha256:95cf3b95ea665ab03f5a54765fa41abf0529dbaf372c3b83d91ad2cfa695779b \ + --hash=sha256:96619302d4374de5e2345b2b622dc481257a99431277662c30f606f3e22f42be \ + --hash=sha256:999928c6434372fde16c8f27143d3e97201160b48a614071261701615a2a156f \ + --hash=sha256:9a60e2bf9dc6afe7f743e7c9b149d1fdd6dbf35153c78fe3a14ae1a9aee3d98b \ + --hash=sha256:9f895d785eb6164678ff4bb5cc60c5996b3ee6df3edb28dcdeba86a13ea0465e \ + --hash=sha256:a2a9891723a735d3e2540651184be6fd5b96880c08ffe1a98bae5017e65b544b \ + --hash=sha256:a974231b4fdd1bb7f62064a0565a6b107d27d21d9acb50c484d2cdba515b9366 \ + --hash=sha256:aa0fd7248cf533c259e59dc593a60973a73e881162b1a2f73360547132742823 \ + --hash=sha256:acbfa31e315a8f14fe33e3542cbcafc55703b8f5dcbb7c1eecd30f141df50db3 \ + --hash=sha256:afb72325b74fa7a428c009c1b8be4b4d7c2afedafb2982827ef2156646df2fe1 \ + --hash=sha256:b3ef2c69c655db63deb96b3c3e587084612f9b1fa983df5e0c3379d41307467f \ + --hash=sha256:b52a65e4ea43c6d149c5f8ddb0bef8d4a1e779b77591a458a893eb416624a418 \ + --hash=sha256:b665caeeda58625c3946ad7308fbd88a086ee51ccb706307e5b1fa91556ac886 \ + --hash=sha256:b74fdffce9dfcf2dc296dec8743e5b0332d15df19ae464f0e249aa871fc1c571 \ + --hash=sha256:b995bfa6bf01a9e09b884077a6d37070464b529d8682d7691c2d3b540d357a0c \ + --hash=sha256:bd82010f8ab451dabe36054a1622870166a67cf3fce894f68895db6f74bbdc94 \ + --hash=sha256:bdcd5538e27f188dd3c804b4a8d5f52a7fc7f87e7fd6b374b8e36a4ca03db428 \ + --hash=sha256:c79d7719d027b7a42817c5d96461a99b6a49979c143839fc37aa5748c322f234 \ + --hash=sha256:cdab9555053399318b953a1fe1f586e945bc8d635ce9d05e617fd9fe3a4687d6 \ + --hash=sha256:ce72dba6a20e39a0c628258b5c308779b8697f7676c254a845715e2a1039b968 \ + --hash=sha256:d337193bbf3e45171c8025e291530fb7548a93c45253897cd764a6a71c937ed9 \ + --hash=sha256:d3dcb774e3568477275cc76554b5a565024b8ba3a0322f77c246bc7111c5bb9c \ + --hash=sha256:d64ba08db72e5dfd5c33be1e1e687d5e4fcce09219e8aee893a4862034081d4e \ + --hash=sha256:d7a2e3b7f5703ffbd500dabdefcbc9eafeff4b9444bbdd5d83d79eedf8428fab \ + --hash=sha256:d831ee0a50946d24a53821819b2327d5751b0c938b12c0653ea5be7dea9c82ec \ + --hash=sha256:d9018153cf57fc302a2a34cb7564870b859ed9a732d16b41a9b5cb2ebed2d444 \ + --hash=sha256:e5171ef898299c657685306d8e1478a45e9303ddcd8ac5fed5bd52ad4ae0b69b \ + --hash=sha256:e94e98c7cb94cfa6e071d401ea3342767f28eb5a06a58fafdc0d2a4974f4f35c \ + --hash=sha256:ec39698c45b11d9694a1b635a70946a5bad066b593af863460a8e600f0dff1ca \ + --hash=sha256:ed9aba6e01ff6f2e8285e5aa4154e2970068fe0fc0998c4380d0e6278222269b \ + --hash=sha256:edf71b01dec9f766fb285b73930f95f730bb0943500ba0566ae234b5c1618c18 \ + --hash=sha256:ee82c98bed9d97cd2f53bdb035e619309a098ea53ce525833e26b93f673bc318 \ + --hash=sha256:f4c96283fca3ee09fb044f02156d9570d156698bc3734252175a38f0e8975f07 \ + --hash=sha256:f7d9b87c4c55e3ea8881dfcbf6d61ea6775fffed1fedffaa60bd047d3c08c430 \ + --hash=sha256:f83df90191d67af5a831da3a33dd7628b02a95450e168785586ed51e6d28943c \ + --hash=sha256:fca9433a45f18b7c779d2bae7beeec4f740d28b788b117a48368d95a3233ed83 \ + --hash=sha256:fd92bbaa2ecdb7864b7600dcdb6f2f1db6e0346ed425fbd01085be04c63f0b05 + # via uvicorn +websockets==13.0.1 \ + --hash=sha256:00fd961943b6c10ee6f0b1130753e50ac5dcd906130dcd77b0003c3ab797d026 \ + --hash=sha256:03d3f9ba172e0a53e37fa4e636b86cc60c3ab2cfee4935e66ed1d7acaa4625ad \ + --hash=sha256:0513c727fb8adffa6d9bf4a4463b2bade0186cbd8c3604ae5540fae18a90cb99 \ + --hash=sha256:05e70fec7c54aad4d71eae8e8cab50525e899791fc389ec6f77b95312e4e9920 \ + --hash=sha256:0617fd0b1d14309c7eab6ba5deae8a7179959861846cbc5cb528a7531c249448 \ + --hash=sha256:06c0a667e466fcb56a0886d924b5f29a7f0886199102f0a0e1c60a02a3751cb4 \ + --hash=sha256:0f52504023b1480d458adf496dc1c9e9811df4ba4752f0bc1f89ae92f4f07d0c \ + --hash=sha256:10a0dc7242215d794fb1918f69c6bb235f1f627aaf19e77f05336d147fce7c37 \ + --hash=sha256:11f9976ecbc530248cf162e359a92f37b7b282de88d1d194f2167b5e7ad80ce3 \ + --hash=sha256:132511bfd42e77d152c919147078460c88a795af16b50e42a0bd14f0ad71ddd2 \ + --hash=sha256:139add0f98206cb74109faf3611b7783ceafc928529c62b389917a037d4cfdf4 \ + --hash=sha256:14b9c006cac63772b31abbcd3e3abb6228233eec966bf062e89e7fa7ae0b7333 \ + --hash=sha256:15c7d62ee071fa94a2fc52c2b472fed4af258d43f9030479d9c4a2de885fd543 \ + --hash=sha256:165bedf13556f985a2aa064309baa01462aa79bf6112fbd068ae38993a0e1f1b \ + --hash=sha256:17118647c0ea14796364299e942c330d72acc4b248e07e639d34b75067b3cdd8 \ + --hash=sha256:1841c9082a3ba4a05ea824cf6d99570a6a2d8849ef0db16e9c826acb28089e8f \ + --hash=sha256:1a678532018e435396e37422a95e3ab87f75028ac79570ad11f5bf23cd2a7d8c \ + --hash=sha256:1ee4cc030a4bdab482a37462dbf3ffb7e09334d01dd37d1063be1136a0d825fa \ + --hash=sha256:1f3cf6d6ec1142412d4535adabc6bd72a63f5f148c43fe559f06298bc21953c9 \ + --hash=sha256:1f613289f4a94142f914aafad6c6c87903de78eae1e140fa769a7385fb232fdf \ + --hash=sha256:1fa082ea38d5de51dd409434edc27c0dcbd5fed2b09b9be982deb6f0508d25bc \ + --hash=sha256:249aab278810bee585cd0d4de2f08cfd67eed4fc75bde623be163798ed4db2eb \ + --hash=sha256:254ecf35572fca01a9f789a1d0f543898e222f7b69ecd7d5381d8d8047627bdb \ + --hash=sha256:2a02b0161c43cc9e0232711eff846569fad6ec836a7acab16b3cf97b2344c060 \ + --hash=sha256:30d3a1f041360f029765d8704eae606781e673e8918e6b2c792e0775de51352f \ + --hash=sha256:3624fd8664f2577cf8de996db3250662e259bfbc870dd8ebdcf5d7c6ac0b5185 \ + --hash=sha256:3f55b36d17ac50aa8a171b771e15fbe1561217510c8768af3d546f56c7576cdc \ + --hash=sha256:46af561eba6f9b0848b2c9d2427086cabadf14e0abdd9fde9d72d447df268418 \ + --hash=sha256:47236c13be337ef36546004ce8c5580f4b1150d9538b27bf8a5ad8edf23ccfab \ + --hash=sha256:4a365bcb7be554e6e1f9f3ed64016e67e2fa03d7b027a33e436aecf194febb63 \ + --hash=sha256:4d6ece65099411cfd9a48d13701d7438d9c34f479046b34c50ff60bb8834e43e \ + --hash=sha256:4e85f46ce287f5c52438bb3703d86162263afccf034a5ef13dbe4318e98d86e7 \ + --hash=sha256:4f0426d51c8f0926a4879390f53c7f5a855e42d68df95fff6032c82c888b5f36 \ + --hash=sha256:518f90e6dd089d34eaade01101fd8a990921c3ba18ebbe9b0165b46ebff947f0 \ + --hash=sha256:52aed6ef21a0f1a2a5e310fb5c42d7555e9c5855476bbd7173c3aa3d8a0302f2 \ + --hash=sha256:556e70e4f69be1082e6ef26dcb70efcd08d1850f5d6c5f4f2bcb4e397e68f01f \ + --hash=sha256:56a952fa2ae57a42ba7951e6b2605e08a24801a4931b5644dfc68939e041bc7f \ + --hash=sha256:59197afd478545b1f73367620407b0083303569c5f2d043afe5363676f2697c9 \ + --hash=sha256:5df891c86fe68b2c38da55b7aea7095beca105933c697d719f3f45f4220a5e0e \ + --hash=sha256:63848cdb6fcc0bf09d4a155464c46c64ffdb5807ede4fb251da2c2692559ce75 \ + --hash=sha256:64a11aae1de4c178fa653b07d90f2fb1a2ed31919a5ea2361a38760192e1858b \ + --hash=sha256:6724b554b70d6195ba19650fef5759ef11346f946c07dbbe390e039bcaa7cc3d \ + --hash=sha256:67494e95d6565bf395476e9d040037ff69c8b3fa356a886b21d8422ad86ae075 \ + --hash=sha256:67648f5e50231b5a7f6d83b32f9c525e319f0ddc841be0de64f24928cd75a603 \ + --hash=sha256:68264802399aed6fe9652e89761031acc734fc4c653137a5911c2bfa995d6d6d \ + --hash=sha256:699ba9dd6a926f82a277063603fc8d586b89f4cb128efc353b749b641fcddda7 \ + --hash=sha256:6aa74a45d4cdc028561a7d6ab3272c8b3018e23723100b12e58be9dfa5a24491 \ + --hash=sha256:6b41a1b3b561f1cba8321fb32987552a024a8f67f0d05f06fcf29f0090a1b956 \ + --hash=sha256:71e6e5a3a3728886caee9ab8752e8113670936a193284be9d6ad2176a137f376 \ + --hash=sha256:7d20516990d8ad557b5abeb48127b8b779b0b7e6771a265fa3e91767596d7d97 \ + --hash=sha256:80e4ba642fc87fa532bac07e5ed7e19d56940b6af6a8c61d4429be48718a380f \ + --hash=sha256:872afa52a9f4c414d6955c365b6588bc4401272c629ff8321a55f44e3f62b553 \ + --hash=sha256:8eb2b9a318542153674c6e377eb8cb9ca0fc011c04475110d3477862f15d29f0 \ + --hash=sha256:9bbc525f4be3e51b89b2a700f5746c2a6907d2e2ef4513a8daafc98198b92237 \ + --hash=sha256:a1a2e272d067030048e1fe41aa1ec8cfbbaabce733b3d634304fa2b19e5c897f \ + --hash=sha256:a5dc0c42ded1557cc7c3f0240b24129aefbad88af4f09346164349391dea8e58 \ + --hash=sha256:acab3539a027a85d568c2573291e864333ec9d912675107d6efceb7e2be5d980 \ + --hash=sha256:acbebec8cb3d4df6e2488fbf34702cbc37fc39ac7abf9449392cefb3305562e9 \ + --hash=sha256:ad327ac80ba7ee61da85383ca8822ff808ab5ada0e4a030d66703cc025b021c4 \ + --hash=sha256:b448a0690ef43db5ef31b3a0d9aea79043882b4632cfc3eaab20105edecf6097 \ + --hash=sha256:b5a06d7f60bc2fc378a333978470dfc4e1415ee52f5f0fce4f7853eb10c1e9df \ + --hash=sha256:b74593e9acf18ea5469c3edaa6b27fa7ecf97b30e9dabd5a94c4c940637ab96e \ + --hash=sha256:b79915a1179a91f6c5f04ece1e592e2e8a6bd245a0e45d12fd56b2b59e559a32 \ + --hash=sha256:b80f0c51681c517604152eb6a572f5a9378f877763231fddb883ba2f968e8817 \ + --hash=sha256:b8ac5b46fd798bbbf2ac6620e0437c36a202b08e1f827832c4bf050da081b501 \ + --hash=sha256:c3c493d0e5141ec055a7d6809a28ac2b88d5b878bb22df8c621ebe79a61123d0 \ + --hash=sha256:c44ca9ade59b2e376612df34e837013e2b273e6c92d7ed6636d0556b6f4db93d \ + --hash=sha256:c4a6343e3b0714e80da0b0893543bf9a5b5fa71b846ae640e56e9abc6fbc4c83 \ + --hash=sha256:c5870b4a11b77e4caa3937142b650fbbc0914a3e07a0cf3131f35c0587489c1c \ + --hash=sha256:ca48914cdd9f2ccd94deab5bcb5ac98025a5ddce98881e5cce762854a5de330b \ + --hash=sha256:cf2fae6d85e5dc384bf846f8243ddaa9197f3a1a70044f59399af001fd1f51d4 \ + --hash=sha256:d450f5a7a35662a9b91a64aefa852f0c0308ee256122f5218a42f1d13577d71e \ + --hash=sha256:d6716c087e4aa0b9260c4e579bb82e068f84faddb9bfba9906cb87726fa2e870 \ + --hash=sha256:d93572720d781331fb10d3da9ca1067817d84ad1e7c31466e9f5e59965618096 \ + --hash=sha256:dbb0b697cc0655719522406c059eae233abaa3243821cfdfab1215d02ac10231 \ + --hash=sha256:e33505534f3f673270dd67f81e73550b11de5b538c56fe04435d63c02c3f26b5 \ + --hash=sha256:e801ca2f448850685417d723ec70298feff3ce4ff687c6f20922c7474b4746ae \ + --hash=sha256:e82db3756ccb66266504f5a3de05ac6b32f287faacff72462612120074103329 \ + --hash=sha256:ef48e4137e8799998a343706531e656fdec6797b80efd029117edacb74b0a10a \ + --hash=sha256:f1d3d1f2eb79fe7b0fb02e599b2bf76a7619c79300fc55f0b5e2d382881d4f7f \ + --hash=sha256:f3fea72e4e6edb983908f0db373ae0732b275628901d909c382aae3b592589f2 \ + --hash=sha256:f40de079779acbcdbb6ed4c65af9f018f8b77c5ec4e17a4b737c05c2db554491 \ + --hash=sha256:f73e676a46b0fe9426612ce8caeca54c9073191a77c3e9d5c94697aef99296af \ + --hash=sha256:f9c9e258e3d5efe199ec23903f5da0eeaad58cf6fccb3547b74fd4750e5ac47a \ + --hash=sha256:fac2d146ff30d9dd2fcf917e5d147db037a5c573f0446c564f16f1f94cf87462 \ + --hash=sha256:faef9ec6354fe4f9a2c0bbb52fb1ff852effc897e2a4501e25eb3a47cb0a4f89 + # 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 0000000..dfc62e0 --- /dev/null +++ b/lock/requirements.txt @@ -0,0 +1,957 @@ +aiokafka==0.11.0 \ + --hash=sha256:0973a245b8b9daf8ef6814253a80a700f1f54d2da7d88f6fe479f46e0fd83053 \ + --hash=sha256:0d80590c4ef0ba546a299cee22ea27c3360c14241ec43a8e6904653f7b22d328 \ + --hash=sha256:0e957b42ae959365efbb45c9b5de38032c573608553c3670ad8695cc210abec9 \ + --hash=sha256:1d519bf9875ac867fb19d55de3750833b1eb6379a08de29a68618e24e6a49fc0 \ + --hash=sha256:1f8ae91f0373830e4664376157fe61b611ca7e573d8a559b151aef5bf53df46c \ + --hash=sha256:224db2447f6c1024198d8342e7099198f90401e2fa29c0762afbc51eadf5c490 \ + --hash=sha256:230170ce2e8a0eb852e2e8b78b08ce2e29b77dfe2c51bd56f5ab4be0f332a63b \ + --hash=sha256:3711fa64ee8640dcd4cb640f1030f9439d02e85acd57010d09053017092d8cc2 \ + --hash=sha256:38e1917e706c1158d5e1f612d1fc1b40f706dc46c534e73ab4de8ae2868a31be \ + --hash=sha256:419dd28c8ed6e926061bdc60929af08a6b52f1721e1179d9d21cc72ae28fd6f6 \ + --hash=sha256:4e0cc080a7f4c659ee4e1baa1c32adedcccb105a52156d4909f357d76fac0dc1 \ + --hash=sha256:516e1d68d9a377860b2e17453580afe304605bc71894f684d3e7b6618f6f939f \ + --hash=sha256:55a07a39d82c595223a17015ea738d152544cee979d3d6d822707a082465621c \ + --hash=sha256:560839ae6bc13e71025d71e94df36980f5c6e36a64916439e598b6457267a37f \ + --hash=sha256:59f4b935589ebb244620afad8bf3320e3bc86879a8b1c692ad06bd324f6c6127 \ + --hash=sha256:6ef3e7c8a923e502caa4d24041f2be778fd7f9ee4587bf0bcb4f74cac05122fa \ + --hash=sha256:702aec15b63bad5e4476294bcb1cb177559149fce3e59335794f004c279cbd6a \ + --hash=sha256:73584be8ba7906e3f33ca0f08f6af21a9ae31b86c6b635b93db3b1e6f452657b \ + --hash=sha256:807f699cf916369b1a512e4f2eaec714398c202d8803328ef8711967d99a56ce \ + --hash=sha256:818a6f8e44b02113b9e795bee6029c8a4e525ab38f29d7adb0201f3fec74c808 \ + --hash=sha256:8ba981956243767b37c929845c398fda2a2e35a4034d218badbe2b62e6f98f96 \ + --hash=sha256:926f93fb6a39891fd4364494432b479c0602f9cac708778d4a262a2c2e20d3b4 \ + --hash=sha256:9a478a14fd23fd1ffe9c7a21238d818b5f5e0626f7f06146b687f3699298391b \ + --hash=sha256:acfd0a5d0aec762ba73eeab73b23edce14f315793f063b6a4b223b6f79e36bb8 \ + --hash=sha256:d59fc7aec088c9ffc02d37e61591f053459bd11912cf04c70ac4f7e60405667d \ + --hash=sha256:d724b6fc484e453b373052813e4e543fc028a22c3fbda10e13b6829740000b8a \ + --hash=sha256:eaafe134de57b184f3c030e1a11051590caff7953c8bf58048eefd8d828e39d7 \ + --hash=sha256:eac78a009b713e28b5b4c4daae9d062acbf2b7980e5734467643a810134583b5 \ + --hash=sha256:ee0c61a2dcabbe4474ff237d708f9bd663dd2317e03a9cb7239a212c9ee05b12 \ + --hash=sha256:f1c85f66eb3564c5e74d8e4c25df4ac1fd94f1a6f6e66f005aafa6f791bde215 \ + --hash=sha256:f2def07fe1720c4fe37c0309e355afa9ff4a28e0aabfe847be0692461ac69352 + # via + # -c lock/requirements-dev.txt + # hexkit +annotated-types==0.7.0 \ + --hash=sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53 \ + --hash=sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89 + # via + # -c lock/requirements-dev.txt + # pydantic +anyio==4.4.0 \ + --hash=sha256:5aadc6a1bbb7cdb0bede386cac5e2940f5e2ff3aa20277e991cf028e0585ce94 \ + --hash=sha256:c1b2d8f46a8a812513012e1107cb0e68c17159a7a594208005a57dc776e1bdc7 + # via + # -c lock/requirements-dev.txt + # httpx + # starlette + # watchfiles +async-timeout==4.0.3 \ + --hash=sha256:4640d96be84d82d02ed59ea2b7105a0f7b33abe8703703cd0ab0bf87c427522f \ + --hash=sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028 + # via + # -c lock/requirements-dev.txt + # aiokafka +attrs==24.2.0 \ + --hash=sha256:5cfb1b9148b5b086569baec03f20d7b6bf3bcacc9a42bebf87ffaaca362f6346 \ + --hash=sha256:81921eb96de3191c8258c199618104dd27ac608d9366f5e35d011eae1867ede2 + # via + # -c lock/requirements-dev.txt + # jsonschema + # referencing +boto3==1.35.21 \ + --hash=sha256:247f88eedce9ae4e014a8fc14a9473759bb8e391460d49396a3b600fb649f33b \ + --hash=sha256:db5fbbd10248db060f2ccce3ae17764f1641c99c8b9f51d422c26ebe25703a1e + # via + # -c lock/requirements-dev.txt + # hexkit +botocore==1.35.21 \ + --hash=sha256:3db9ddfe521edc0753fc8c68caef71c7806e1d2d21ce8cbabc2065b7d79192f2 \ + --hash=sha256:db917e7d7b3a2eed1310c6496784bc813c91f020a021c2ab5f9df7d28cdb4f1d + # via + # -c lock/requirements-dev.txt + # boto3 + # hexkit + # s3transfer +certifi==2024.8.30 \ + --hash=sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8 \ + --hash=sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9 + # via + # -c lock/requirements-dev.txt + # httpcore + # httpx +click==8.1.7 \ + --hash=sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28 \ + --hash=sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de + # via + # -c lock/requirements-dev.txt + # typer + # uvicorn +dnspython==2.6.1 \ + --hash=sha256:5ef3b9680161f6fa89daf8ad451b5f1a33b18ae8a1c6778cdf4b43f08c0a6e50 \ + --hash=sha256:e8f0f9c23a7b7cb99ded64e6c3a6f3e701d78f50c55e002b839dea7225cff7cc + # via + # -c lock/requirements-dev.txt + # email-validator + # pymongo +email-validator==2.2.0 \ + --hash=sha256:561977c2d73ce3611850a06fa56b414621e0c8faa9d66f2611407d87465da631 \ + --hash=sha256:cb690f344c617a714f22e66ae771445a1ceb46821152df8e165c5f9a364582b7 + # via + # -c lock/requirements-dev.txt + # fastapi + # pydantic +fastapi==0.111.1 \ + --hash=sha256:4f51cfa25d72f9fbc3280832e84b32494cf186f50158d364a8765aabf22587bf \ + --hash=sha256:ddd1ac34cb1f76c2e2d7f8545a4bcb5463bce4834e81abf0b189e0c359ab2413 + # via + # -c lock/requirements-dev.txt + # ghga-service-commons +fastapi-cli==0.0.5 \ + --hash=sha256:d30e1239c6f46fcb95e606f02cdda59a1e2fa778a54b64686b3ff27f6211ff9f \ + --hash=sha256:e94d847524648c748a5350673546bbf9bcaeb086b33c24f2e82e021436866a46 + # via + # -c lock/requirements-dev.txt + # fastapi +ghga-event-schemas==3.3.1 \ + --hash=sha256:6b21b45efd8a1a5dbbc3b671f3d23390b728dd19669ce39a7153acb5487f1256 \ + --hash=sha256:845e72aa8045c52fbb42a215dcd95dd8436c841788b1d8edb80794584faab09a + # via + # -c lock/requirements-dev.txt + # dins (pyproject.toml) +ghga-service-commons==3.1.5 \ + --hash=sha256:6650167f9fe21d683d7f7bd81320f3301f6624f6c71d7e633fbf5f0a3453d973 \ + --hash=sha256:eeae99a976d2c81bce754e20cc0156a49b76ef3f2f660175d654de0abcb62c6e + # via + # -c lock/requirements-dev.txt + # dins (pyproject.toml) +h11==0.14.0 \ + --hash=sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d \ + --hash=sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761 + # via + # -c lock/requirements-dev.txt + # httpcore + # uvicorn +hexkit==3.6.0 \ + --hash=sha256:0f650bf87a95c91ddffa20f1c5c130b2c5aad719cb5031bf6c7ea60fa8f136c1 \ + --hash=sha256:70d8159565b43ce4fa54bb471a0cd873517162fb27a056e30613c4332822500b + # via + # -c lock/requirements-dev.txt + # dins (pyproject.toml) + # ghga-service-commons +httpcore==1.0.5 \ + --hash=sha256:34a38e2f9291467ee3b44e89dd52615370e152954ba21721378a87b2960f7a61 \ + --hash=sha256:421f18bac248b25d310f3cacd198d55b8e6125c107797b609ff9b7a6ba7991b5 + # via + # -c lock/requirements-dev.txt + # 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 + # -c lock/requirements-dev.txt + # uvicorn +httpx==0.27.2 \ + --hash=sha256:7bb2708e112d8fdd7829cd4243970f0c223274051cb35ee80c03301ee29a3df0 \ + --hash=sha256:f7c2be1d2f3c3c3160d441802406b206c2b76f5947b11115e6df10c6c65e66c2 + # via + # -c lock/requirements-dev.txt + # fastapi +idna==3.10 \ + --hash=sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9 \ + --hash=sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3 + # via + # -c lock/requirements-dev.txt + # anyio + # email-validator + # httpx +jinja2==3.1.4 \ + --hash=sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369 \ + --hash=sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d + # via + # -c lock/requirements-dev.txt + # fastapi +jmespath==1.0.1 \ + --hash=sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980 \ + --hash=sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe + # via + # -c lock/requirements-dev.txt + # boto3 + # botocore +jsonschema==4.23.0 \ + --hash=sha256:d71497fef26351a33265337fa77ffeb82423f3ea21283cd9467bb03999266bc4 \ + --hash=sha256:fbadb6f8b144a8f8cf9f0b89ba94501d143e50411a1278633f56a7acf7fd5566 + # via + # -c lock/requirements-dev.txt + # ghga-event-schemas + # hexkit +jsonschema-specifications==2023.12.1 \ + --hash=sha256:48a76787b3e70f5ed53f1160d2b81f586e4ca6d1548c5de7085d1682674764cc \ + --hash=sha256:87e4fdf3a94858b8a2ba2778d9ba57d8a9cafca7c7489c46ba0d30a8bc6a9c3c + # via + # -c lock/requirements-dev.txt + # jsonschema +markdown-it-py==3.0.0 \ + --hash=sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1 \ + --hash=sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb + # via + # -c lock/requirements-dev.txt + # rich +markupsafe==2.1.5 \ + --hash=sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf \ + --hash=sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff \ + --hash=sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f \ + --hash=sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3 \ + --hash=sha256:1f3fbcb7ef1f16e48246f704ab79d79da8a46891e2da03f8783a5b6fa41a9532 \ + --hash=sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f \ + --hash=sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617 \ + --hash=sha256:2d2d793e36e230fd32babe143b04cec8a8b3eb8a3122d2aceb4a371e6b09b8df \ + --hash=sha256:30b600cf0a7ac9234b2638fbc0fb6158ba5bdcdf46aeb631ead21248b9affbc4 \ + --hash=sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906 \ + --hash=sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f \ + --hash=sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4 \ + --hash=sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8 \ + --hash=sha256:4096e9de5c6fdf43fb4f04c26fb114f61ef0bf2e5604b6ee3019d51b69e8c371 \ + --hash=sha256:4275d846e41ecefa46e2015117a9f491e57a71ddd59bbead77e904dc02b1bed2 \ + --hash=sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465 \ + --hash=sha256:4f11aa001c540f62c6166c7726f71f7573b52c68c31f014c25cc7901deea0b52 \ + --hash=sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6 \ + --hash=sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169 \ + --hash=sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad \ + --hash=sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2 \ + --hash=sha256:5dedb4db619ba5a2787a94d877bc8ffc0566f92a01c0ef214865e54ecc9ee5e0 \ + --hash=sha256:619bc166c4f2de5caa5a633b8b7326fbe98e0ccbfacabd87268a2b15ff73a029 \ + --hash=sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f \ + --hash=sha256:656f7526c69fac7f600bd1f400991cc282b417d17539a1b228617081106feb4a \ + --hash=sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced \ + --hash=sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5 \ + --hash=sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c \ + --hash=sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf \ + --hash=sha256:7b2e5a267c855eea6b4283940daa6e88a285f5f2a67f2220203786dfa59b37e9 \ + --hash=sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb \ + --hash=sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad \ + --hash=sha256:8dd717634f5a044f860435c1d8c16a270ddf0ef8588d4887037c5028b859b0c3 \ + --hash=sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1 \ + --hash=sha256:97cafb1f3cbcd3fd2b6fbfb99ae11cdb14deea0736fc2b0952ee177f2b813a46 \ + --hash=sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc \ + --hash=sha256:a549b9c31bec33820e885335b451286e2969a2d9e24879f83fe904a5ce59d70a \ + --hash=sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee \ + --hash=sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900 \ + --hash=sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5 \ + --hash=sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea \ + --hash=sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f \ + --hash=sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5 \ + --hash=sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e \ + --hash=sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a \ + --hash=sha256:c8b29db45f8fe46ad280a7294f5c3ec36dbac9491f2d1c17345be8e69cc5928f \ + --hash=sha256:ce409136744f6521e39fd8e2a24c53fa18ad67aa5bc7c2cf83645cce5b5c4e50 \ + --hash=sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a \ + --hash=sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b \ + --hash=sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4 \ + --hash=sha256:daa4ee5a243f0f20d528d939d06670a298dd39b1ad5f8a72a4275124a7819eff \ + --hash=sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2 \ + --hash=sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46 \ + --hash=sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b \ + --hash=sha256:ec6a563cff360b50eed26f13adc43e61bc0c04d94b8be985e6fb24b81f6dcfdf \ + --hash=sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5 \ + --hash=sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5 \ + --hash=sha256:fa9db3f79de01457b03d4f01b34cf91bc0048eb2c3846ff26f66687c2f6d16ab \ + --hash=sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd \ + --hash=sha256:ffee1f21e5ef0d712f9033568f8344d5da8cc2869dbd08d87c84656e6a2d2f68 + # via + # -c lock/requirements-dev.txt + # jinja2 +mdurl==0.1.2 \ + --hash=sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8 \ + --hash=sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba + # via + # -c lock/requirements-dev.txt + # markdown-it-py +motor==3.5.1 \ + --hash=sha256:1622bd7b39c3e6375607c14736f6e1d498128eadf6f5f93f8786cf17d37062ac \ + --hash=sha256:f95a9ea0f011464235e0bd72910baa291db3a6009e617ac27b82f57885abafb8 + # via + # -c lock/requirements-dev.txt + # hexkit +packaging==24.1 \ + --hash=sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002 \ + --hash=sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124 + # via + # -c lock/requirements-dev.txt + # aiokafka +pydantic==2.9.2 \ + --hash=sha256:d155cef71265d1e9807ed1c32b4c8deec042a44a50a4188b25ac67ecd81a9c0f \ + --hash=sha256:f048cec7b26778210e28a0459867920654d48e5e62db0958433636cde4254f12 + # via + # -c lock/requirements-dev.txt + # fastapi + # ghga-event-schemas + # ghga-service-commons + # hexkit + # pydantic-settings +pydantic-core==2.23.4 \ + --hash=sha256:0a7df63886be5e270da67e0966cf4afbae86069501d35c8c1b3b6c168f42cb36 \ + --hash=sha256:0cb3da3fd1b6a5d0279a01877713dbda118a2a4fc6f0d821a57da2e464793f05 \ + --hash=sha256:0dbd8dbed2085ed23b5c04afa29d8fd2771674223135dc9bc937f3c09284d071 \ + --hash=sha256:0dff76e0602ca7d4cdaacc1ac4c005e0ce0dcfe095d5b5259163a80d3a10d327 \ + --hash=sha256:1278e0d324f6908e872730c9102b0112477a7f7cf88b308e4fc36ce1bdb6d58c \ + --hash=sha256:128585782e5bfa515c590ccee4b727fb76925dd04a98864182b22e89a4e6ed36 \ + --hash=sha256:1498bec4c05c9c787bde9125cfdcc63a41004ff167f495063191b863399b1a29 \ + --hash=sha256:19442362866a753485ba5e4be408964644dd6a09123d9416c54cd49171f50744 \ + --hash=sha256:1b84d168f6c48fabd1f2027a3d1bdfe62f92cade1fb273a5d68e621da0e44e6d \ + --hash=sha256:1e90d2e3bd2c3863d48525d297cd143fe541be8bbf6f579504b9712cb6b643ec \ + --hash=sha256:20152074317d9bed6b7a95ade3b7d6054845d70584216160860425f4fbd5ee9e \ + --hash=sha256:216f9b2d7713eb98cb83c80b9c794de1f6b7e3145eef40400c62e86cee5f4e1e \ + --hash=sha256:233710f069d251feb12a56da21e14cca67994eab08362207785cf8c598e74577 \ + --hash=sha256:255a8ef062cbf6674450e668482456abac99a5583bbafb73f9ad469540a3a232 \ + --hash=sha256:2584f7cf844ac4d970fba483a717dbe10c1c1c96a969bf65d61ffe94df1b2863 \ + --hash=sha256:2971bb5ffe72cc0f555c13e19b23c85b654dd2a8f7ab493c262071377bfce9f6 \ + --hash=sha256:29d2c342c4bc01b88402d60189f3df065fb0dda3654744d5a165a5288a657368 \ + --hash=sha256:2e203fdf807ac7e12ab59ca2bfcabb38c7cf0b33c41efeb00f8e5da1d86af480 \ + --hash=sha256:33e3d65a85a2a4a0dc3b092b938a4062b1a05f3a9abde65ea93b233bca0e03f2 \ + --hash=sha256:374a5e5049eda9e0a44c696c7ade3ff355f06b1fe0bb945ea3cac2bc336478a2 \ + --hash=sha256:37b0fe330e4a58d3c58b24d91d1eb102aeec675a3db4c292ec3928ecd892a9a6 \ + --hash=sha256:3d5639516376dce1940ea36edf408c554475369f5da2abd45d44621cb616f769 \ + --hash=sha256:42c6dcb030aefb668a2b7009c85b27f90e51e6a3b4d5c9bc4c57631292015b0d \ + --hash=sha256:4a7cd62e831afe623fbb7aabbb4fe583212115b3ef38a9f6b71869ba644624a2 \ + --hash=sha256:4ba762ed58e8d68657fc1281e9bb72e1c3e79cc5d464be146e260c541ec12d84 \ + --hash=sha256:4fc714bdbfb534f94034efaa6eadd74e5b93c8fa6315565a222f7b6f42ca1166 \ + --hash=sha256:4ffa2ebd4c8530079140dd2d7f794a9d9a73cbb8e9d59ffe24c63436efa8f271 \ + --hash=sha256:5a1504ad17ba4210df3a045132a7baeeba5a200e930f57512ee02909fc5c4cb5 \ + --hash=sha256:5c364564d17da23db1106787675fc7af45f2f7b58b4173bfdd105564e132e6fb \ + --hash=sha256:5e11661ce0fd30a6790e8bcdf263b9ec5988e95e63cf901972107efc49218b13 \ + --hash=sha256:5f54b118ce5de9ac21c363d9b3caa6c800341e8c47a508787e5868c6b79c9323 \ + --hash=sha256:5f5ff8d839f4566a474a969508fe1c5e59c31c80d9e140566f9a37bba7b8d556 \ + --hash=sha256:61817945f2fe7d166e75fbfb28004034b48e44878177fc54d81688e7b85a3665 \ + --hash=sha256:624e278a7d29b6445e4e813af92af37820fafb6dcc55c012c834f9e26f9aaaef \ + --hash=sha256:63e46b3169866bd62849936de036f901a9356e36376079b05efa83caeaa02ceb \ + --hash=sha256:6531b7ca5f951d663c339002e91aaebda765ec7d61b7d1e3991051906ddde119 \ + --hash=sha256:68665f4c17edcceecc112dfed5dbe6f92261fb9d6054b47d01bf6371a6196126 \ + --hash=sha256:696dd8d674d6ce621ab9d45b205df149399e4bb9aa34102c970b721554828510 \ + --hash=sha256:6f783e0ec4803c787bcea93e13e9932edab72068f68ecffdf86a99fd5918878b \ + --hash=sha256:723314c1d51722ab28bfcd5240d858512ffd3116449c557a1336cbe3919beb87 \ + --hash=sha256:74b9127ffea03643e998e0c5ad9bd3811d3dac8c676e47db17b0ee7c3c3bf35f \ + --hash=sha256:7530e201d10d7d14abce4fb54cfe5b94a0aefc87da539d0346a484ead376c3cc \ + --hash=sha256:77733e3892bb0a7fa797826361ce8a9184d25c8dffaec60b7ffe928153680ba8 \ + --hash=sha256:78ddaaa81421a29574a682b3179d4cf9e6d405a09b99d93ddcf7e5239c742e21 \ + --hash=sha256:7c9129eb40958b3d4500fa2467e6a83356b3b61bfff1b414c7361d9220f9ae8f \ + --hash=sha256:7d32706badfe136888bdea71c0def994644e09fff0bfe47441deaed8e96fdbc6 \ + --hash=sha256:81965a16b675b35e1d09dd14df53f190f9129c0202356ed44ab2728b1c905658 \ + --hash=sha256:8394d940e5d400d04cad4f75c0598665cbb81aecefaca82ca85bd28264af7f9b \ + --hash=sha256:86d2f57d3e1379a9525c5ab067b27dbb8a0642fb5d454e17a9ac434f9ce523e3 \ + --hash=sha256:883a91b5dd7d26492ff2f04f40fbb652de40fcc0afe07e8129e8ae779c2110eb \ + --hash=sha256:88ad334a15b32a791ea935af224b9de1bf99bcd62fabf745d5f3442199d86d59 \ + --hash=sha256:9261d3ce84fa1d38ed649c3638feefeae23d32ba9182963e465d58d62203bd24 \ + --hash=sha256:97df63000f4fea395b2824da80e169731088656d1818a11b95f3b173747b6cd9 \ + --hash=sha256:98d134c954828488b153d88ba1f34e14259284f256180ce659e8d83e9c05eaa3 \ + --hash=sha256:996a38a83508c54c78a5f41456b0103c30508fed9abcad0a59b876d7398f25fd \ + --hash=sha256:9a5bce9d23aac8f0cf0836ecfc033896aa8443b501c58d0602dbfd5bd5b37753 \ + --hash=sha256:9a6b5099eeec78827553827f4c6b8615978bb4b6a88e5d9b93eddf8bb6790f55 \ + --hash=sha256:9d18368b137c6295db49ce7218b1a9ba15c5bc254c96d7c9f9e924a9bc7825ad \ + --hash=sha256:a4fa4fc04dff799089689f4fd502ce7d59de529fc2f40a2c8836886c03e0175a \ + --hash=sha256:a5c7ba8ffb6d6f8f2ab08743be203654bb1aaa8c9dcb09f82ddd34eadb695605 \ + --hash=sha256:aea443fffa9fbe3af1a9ba721a87f926fe548d32cab71d188a6ede77d0ff244e \ + --hash=sha256:b10bd51f823d891193d4717448fab065733958bdb6a6b351967bd349d48d5c9b \ + --hash=sha256:ba1a0996f6c2773bd83e63f18914c1de3c9dd26d55f4ac302a7efe93fb8e7433 \ + --hash=sha256:bb2802e667b7051a1bebbfe93684841cc9351004e2badbd6411bf357ab8d5ac8 \ + --hash=sha256:cfdd16ab5e59fc31b5e906d1a3f666571abc367598e3e02c83403acabc092e07 \ + --hash=sha256:d06b0c8da4f16d1d1e352134427cb194a0a6e19ad5db9161bf32b2113409e728 \ + --hash=sha256:d0776dea117cf5272382634bd2a5c1b6eb16767c223c6a5317cd3e2a757c61a0 \ + --hash=sha256:d18ca8148bebe1b0a382a27a8ee60350091a6ddaf475fa05ef50dc35b5df6327 \ + --hash=sha256:d4488a93b071c04dc20f5cecc3631fc78b9789dd72483ba15d423b5b3689b555 \ + --hash=sha256:d5f7a395a8cf1621939692dba2a6b6a830efa6b3cee787d82c7de1ad2930de64 \ + --hash=sha256:d7a80d21d613eec45e3d41eb22f8f94ddc758a6c4720842dc74c0581f54993d6 \ + --hash=sha256:d97683ddee4723ae8c95d1eddac7c192e8c552da0c73a925a89fa8649bf13eea \ + --hash=sha256:dcedcd19a557e182628afa1d553c3895a9f825b936415d0dbd3cd0bbcfd29b4b \ + --hash=sha256:de6d1d1b9e5101508cb37ab0d972357cac5235f5c6533d1071964c47139257df \ + --hash=sha256:df49e7a0861a8c36d089c1ed57d308623d60416dab2647a4a17fe050ba85de0e \ + --hash=sha256:df933278128ea1cd77772673c73954e53a1c95a4fdf41eef97c2b779271bd0bd \ + --hash=sha256:e08277a400de01bc72436a0ccd02bdf596631411f592ad985dcee21445bd0068 \ + --hash=sha256:e38e63e6f3d1cec5a27e0afe90a085af8b6806ee208b33030e65b6516353f1a3 \ + --hash=sha256:e55541f756f9b3ee346b840103f32779c695a19826a4c442b7954550a0972040 \ + --hash=sha256:ec4e55f79b1c4ffb2eecd8a0cfba9955a2588497d96851f4c8f99aa4a1d39b12 \ + --hash=sha256:ed1a53de42fbe34853ba90513cea21673481cd81ed1be739f7f2efb931b24916 \ + --hash=sha256:ed541d70698978a20eb63d8c5d72f2cc6d7079d9d90f6b50bad07826f1320f5f \ + --hash=sha256:f09e2ff1f17c2b51f2bc76d1cc33da96298f0a036a137f5440ab3ec5360b624f \ + --hash=sha256:f220b0eea5965dec25480b6333c788fb72ce5f9129e8759ef876a1d805d00801 \ + --hash=sha256:f3e0da4ebaef65158d4dfd7d3678aad692f7666877df0002b8a522cdf088f231 \ + --hash=sha256:f455ee30a9d61d3e1a15abd5068827773d6e4dc513e795f380cdd59932c782d5 \ + --hash=sha256:f5ef8f42bec47f21d07668a043f077d507e5bf4e668d5c6dfe6aaba89de1a5b8 \ + --hash=sha256:f69a8e0b033b747bb3e36a44e7732f0c99f7edd5cea723d45bc0d6e95377ffee \ + --hash=sha256:ff02b6d461a6de369f07ec15e465a88895f3223eb75073ffea56b84d9331f607 + # via + # -c lock/requirements-dev.txt + # pydantic +pydantic-settings==2.5.2 \ + --hash=sha256:2c912e55fd5794a59bf8c832b9de832dcfdf4778d79ff79b708744eed499a907 \ + --hash=sha256:f90b139682bee4d2065273d5185d71d37ea46cfe57e1b5ae184fc6a0b2484ca0 + # via + # -c lock/requirements-dev.txt + # hexkit +pygments==2.18.0 \ + --hash=sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199 \ + --hash=sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a + # via + # -c lock/requirements-dev.txt + # rich +pymongo==4.8.0 \ + --hash=sha256:0fc18b3a093f3db008c5fea0e980dbd3b743449eee29b5718bc2dc15ab5088bb \ + --hash=sha256:16e5019f75f6827bb5354b6fef8dfc9d6c7446894a27346e03134d290eb9e758 \ + --hash=sha256:180d5eb1dc28b62853e2f88017775c4500b07548ed28c0bd9c005c3d7bc52526 \ + --hash=sha256:18c9d8f975dd7194c37193583fd7d1eb9aea0c21ee58955ecf35362239ff31ac \ + --hash=sha256:236bbd7d0aef62e64caf4b24ca200f8c8670d1a6f5ea828c39eccdae423bc2b2 \ + --hash=sha256:284d0717d1a7707744018b0b6ee7801b1b1ff044c42f7be7a01bb013de639470 \ + --hash=sha256:2ecd71b9226bd1d49416dc9f999772038e56f415a713be51bf18d8676a0841c8 \ + --hash=sha256:31e4d21201bdf15064cf47ce7b74722d3e1aea2597c6785882244a3bb58c7eab \ + --hash=sha256:3b5802151fc2b51cd45492c80ed22b441d20090fb76d1fd53cd7760b340ff554 \ + --hash=sha256:3c68fe128a171493018ca5c8020fc08675be130d012b7ab3efe9e22698c612a1 \ + --hash=sha256:3ed1c316718a2836f7efc3d75b4b0ffdd47894090bc697de8385acd13c513a70 \ + --hash=sha256:408b2f8fdbeca3c19e4156f28fff1ab11c3efb0407b60687162d49f68075e63c \ + --hash=sha256:417369ce39af2b7c2a9c7152c1ed2393edfd1cbaf2a356ba31eb8bcbd5c98dd7 \ + --hash=sha256:454f2295875744dc70f1881e4b2eb99cdad008a33574bc8aaf120530f66c0cde \ + --hash=sha256:47ec8c3f0a7b2212dbc9be08d3bf17bc89abd211901093e3ef3f2adea7de7a69 \ + --hash=sha256:4bf58e6825b93da63e499d1a58de7de563c31e575908d4e24876234ccb910eba \ + --hash=sha256:519d1bab2b5e5218c64340b57d555d89c3f6c9d717cecbf826fb9d42415e7750 \ + --hash=sha256:52b4108ac9469febba18cea50db972605cc43978bedaa9fea413378877560ef8 \ + --hash=sha256:658d0170f27984e0d89c09fe5c42296613b711a3ffd847eb373b0dbb5b648d5f \ + --hash=sha256:6b50040d9767197b77ed420ada29b3bf18a638f9552d80f2da817b7c4a4c9c68 \ + --hash=sha256:7148419eedfea9ecb940961cfe465efaba90595568a1fb97585fb535ea63fe2b \ + --hash=sha256:77f53429515d2b3e86dcc83dadecf7ff881e538c168d575f3688698a8707b80a \ + --hash=sha256:87075a1feb1e602e539bdb1ef8f4324a3427eb0d64208c3182e677d2c0718b6f \ + --hash=sha256:8b18c8324809539c79bd6544d00e0607e98ff833ca21953df001510ca25915d1 \ + --hash=sha256:9097c331577cecf8034422956daaba7ec74c26f7b255d718c584faddd7fa2e3c \ + --hash=sha256:920d4f8f157a71b3cb3f39bc09ce070693d6e9648fb0e30d00e2657d1dca4e49 \ + --hash=sha256:9365166aa801c63dff1a3cb96e650be270da06e3464ab106727223123405510f \ + --hash=sha256:940d456774b17814bac5ea7fc28188c7a1338d4a233efbb6ba01de957bded2e8 \ + --hash=sha256:aec2b9088cdbceb87e6ca9c639d0ff9b9d083594dda5ca5d3c4f6774f4c81b33 \ + --hash=sha256:af3e98dd9702b73e4e6fd780f6925352237f5dce8d99405ff1543f3771201704 \ + --hash=sha256:b6564780cafd6abeea49759fe661792bd5a67e4f51bca62b88faab497ab5fe89 \ + --hash=sha256:b747c0e257b9d3e6495a018309b9e0c93b7f0d65271d1d62e572747f4ffafc88 \ + --hash=sha256:bf821bd3befb993a6db17229a2c60c1550e957de02a6ff4dd0af9476637b2e4d \ + --hash=sha256:c6b804bb4f2d9dc389cc9e827d579fa327272cdb0629a99bfe5b83cb3e269ebf \ + --hash=sha256:cc8b8582f4209c2459b04b049ac03c72c618e011d3caa5391ff86d1bda0cc486 \ + --hash=sha256:cd39455b7ee70aabee46f7399b32ab38b86b236c069ae559e22be6b46b2bbfc4 \ + --hash=sha256:d0cf61450feadca81deb1a1489cb1a3ae1e4266efd51adafecec0e503a8dcd84 \ + --hash=sha256:d18d86bc9e103f4d3d4f18b85a0471c0e13ce5b79194e4a0389a224bb70edd53 \ + --hash=sha256:d5428dbcd43d02f6306e1c3c95f692f68b284e6ee5390292242f509004c9e3a8 \ + --hash=sha256:de3a860f037bb51f968de320baef85090ff0bbb42ec4f28ec6a5ddf88be61871 \ + --hash=sha256:e0061af6e8c5e68b13f1ec9ad5251247726653c5af3c0bbdfbca6cf931e99216 \ + --hash=sha256:e5df28f74002e37bcbdfdc5109799f670e4dfef0fb527c391ff84f078050e7b5 \ + --hash=sha256:e6a720a3d22b54183352dc65f08cd1547204d263e0651b213a0a2e577e838526 \ + --hash=sha256:e8400587d594761e5136a3423111f499574be5fd53cf0aefa0d0f05b180710b0 \ + --hash=sha256:e84bc7707492f06fbc37a9f215374d2977d21b72e10a67f1b31893ec5a140ad8 \ + --hash=sha256:ef7225755ed27bfdb18730c68f6cb023d06c28f2b734597480fb4c0e500feb6f \ + --hash=sha256:f2b7bec27e047e84947fbd41c782f07c54c30c76d14f3b8bf0c89f7413fac67a \ + --hash=sha256:f2fbdb87fe5075c8beb17a5c16348a1ea3c8b282a5cb72d173330be2fecf22f5 \ + --hash=sha256:f5bf0eb8b6ef40fa22479f09375468c33bebb7fe49d14d9c96c8fd50355188b0 \ + --hash=sha256:fdc20cd1e1141b04696ffcdb7c71e8a4a665db31fe72e51ec706b3bdd2d09f36 + # via + # -c lock/requirements-dev.txt + # motor +python-dateutil==2.9.0.post0 \ + --hash=sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3 \ + --hash=sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427 + # via + # -c lock/requirements-dev.txt + # botocore +python-dotenv==1.0.1 \ + --hash=sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca \ + --hash=sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a + # via + # -c lock/requirements-dev.txt + # pydantic-settings + # uvicorn +python-multipart==0.0.9 \ + --hash=sha256:03f54688c663f1b7977105f021043b0793151e4cb1c1a9d4a11fc13d622c4026 \ + --hash=sha256:97ca7b8ea7b05f977dc3849c3ba99d51689822fab725c3703af7c866a0c2b215 + # via + # -c lock/requirements-dev.txt + # fastapi +pyyaml==6.0.2 \ + --hash=sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff \ + --hash=sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48 \ + --hash=sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086 \ + --hash=sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e \ + --hash=sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133 \ + --hash=sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5 \ + --hash=sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484 \ + --hash=sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee \ + --hash=sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5 \ + --hash=sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68 \ + --hash=sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a \ + --hash=sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf \ + --hash=sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99 \ + --hash=sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8 \ + --hash=sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85 \ + --hash=sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19 \ + --hash=sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc \ + --hash=sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a \ + --hash=sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1 \ + --hash=sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317 \ + --hash=sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c \ + --hash=sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631 \ + --hash=sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d \ + --hash=sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652 \ + --hash=sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5 \ + --hash=sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e \ + --hash=sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b \ + --hash=sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8 \ + --hash=sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476 \ + --hash=sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706 \ + --hash=sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563 \ + --hash=sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237 \ + --hash=sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b \ + --hash=sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083 \ + --hash=sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180 \ + --hash=sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425 \ + --hash=sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e \ + --hash=sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f \ + --hash=sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725 \ + --hash=sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183 \ + --hash=sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab \ + --hash=sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774 \ + --hash=sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725 \ + --hash=sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e \ + --hash=sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5 \ + --hash=sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d \ + --hash=sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290 \ + --hash=sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44 \ + --hash=sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed \ + --hash=sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4 \ + --hash=sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba \ + --hash=sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12 \ + --hash=sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4 + # via + # -c lock/requirements-dev.txt + # hexkit + # uvicorn +referencing==0.35.1 \ + --hash=sha256:25b42124a6c8b632a425174f24087783efb348a6f1e0008e63cd4466fedf703c \ + --hash=sha256:eda6d3234d62814d1c64e305c1331c9a3a6132da475ab6382eaa997b21ee75de + # via + # -c lock/requirements-dev.txt + # jsonschema + # jsonschema-specifications +rich==13.8.1 \ + --hash=sha256:1760a3c0848469b97b558fc61c85233e3dafb69c7a071b4d60c38099d3cd4c06 \ + --hash=sha256:8260cda28e3db6bf04d2d1ef4dbc03ba80a824c88b0e7668a0f23126a424844a + # via + # -c lock/requirements-dev.txt + # typer +rpds-py==0.20.0 \ + --hash=sha256:06db23d43f26478303e954c34c75182356ca9aa7797d22c5345b16871ab9c45c \ + --hash=sha256:0e13e6952ef264c40587d510ad676a988df19adea20444c2b295e536457bc585 \ + --hash=sha256:11ef6ce74616342888b69878d45e9f779b95d4bd48b382a229fe624a409b72c5 \ + --hash=sha256:1259c7b3705ac0a0bd38197565a5d603218591d3f6cee6e614e380b6ba61c6f6 \ + --hash=sha256:18d7585c463087bddcfa74c2ba267339f14f2515158ac4db30b1f9cbdb62c8ef \ + --hash=sha256:1e0f80b739e5a8f54837be5d5c924483996b603d5502bfff79bf33da06164ee2 \ + --hash=sha256:1e5f3cd7397c8f86c8cc72d5a791071431c108edd79872cdd96e00abd8497d29 \ + --hash=sha256:220002c1b846db9afd83371d08d239fdc865e8f8c5795bbaec20916a76db3318 \ + --hash=sha256:22e6c9976e38f4d8c4a63bd8a8edac5307dffd3ee7e6026d97f3cc3a2dc02a0b \ + --hash=sha256:238a2d5b1cad28cdc6ed15faf93a998336eb041c4e440dd7f902528b8891b399 \ + --hash=sha256:2580b0c34583b85efec8c5c5ec9edf2dfe817330cc882ee972ae650e7b5ef739 \ + --hash=sha256:28527c685f237c05445efec62426d285e47a58fb05ba0090a4340b73ecda6dee \ + --hash=sha256:2cf126d33a91ee6eedc7f3197b53e87a2acdac63602c0f03a02dd69e4b138174 \ + --hash=sha256:338ca4539aad4ce70a656e5187a3a31c5204f261aef9f6ab50e50bcdffaf050a \ + --hash=sha256:39ed0d010457a78f54090fafb5d108501b5aa5604cc22408fc1c0c77eac14344 \ + --hash=sha256:3ad0fda1635f8439cde85c700f964b23ed5fc2d28016b32b9ee5fe30da5c84e2 \ + --hash=sha256:3d2b1ad682a3dfda2a4e8ad8572f3100f95fad98cb99faf37ff0ddfe9cbf9d03 \ + --hash=sha256:3d61339e9f84a3f0767b1995adfb171a0d00a1185192718a17af6e124728e0f5 \ + --hash=sha256:3fde368e9140312b6e8b6c09fb9f8c8c2f00999d1823403ae90cc00480221b22 \ + --hash=sha256:40ce74fc86ee4645d0a225498d091d8bc61f39b709ebef8204cb8b5a464d3c0e \ + --hash=sha256:49a8063ea4296b3a7e81a5dfb8f7b2d73f0b1c20c2af401fb0cdf22e14711a96 \ + --hash=sha256:4a1f1d51eccb7e6c32ae89243cb352389228ea62f89cd80823ea7dd1b98e0b91 \ + --hash=sha256:4b16aa0107ecb512b568244ef461f27697164d9a68d8b35090e9b0c1c8b27752 \ + --hash=sha256:4f1ed4749a08379555cebf4650453f14452eaa9c43d0a95c49db50c18b7da075 \ + --hash=sha256:4fe84294c7019456e56d93e8ababdad5a329cd25975be749c3f5f558abb48253 \ + --hash=sha256:50eccbf054e62a7b2209b28dc7a22d6254860209d6753e6b78cfaeb0075d7bee \ + --hash=sha256:514b3293b64187172bc77c8fb0cdae26981618021053b30d8371c3a902d4d5ad \ + --hash=sha256:54b43a2b07db18314669092bb2de584524d1ef414588780261e31e85846c26a5 \ + --hash=sha256:55fea87029cded5df854ca7e192ec7bdb7ecd1d9a3f63d5c4eb09148acf4a7ce \ + --hash=sha256:569b3ea770c2717b730b61998b6c54996adee3cef69fc28d444f3e7920313cf7 \ + --hash=sha256:56e27147a5a4c2c21633ff8475d185734c0e4befd1c989b5b95a5d0db699b21b \ + --hash=sha256:57eb94a8c16ab08fef6404301c38318e2c5a32216bf5de453e2714c964c125c8 \ + --hash=sha256:5a35df9f5548fd79cb2f52d27182108c3e6641a4feb0f39067911bf2adaa3e57 \ + --hash=sha256:5a8c94dad2e45324fc74dce25e1645d4d14df9a4e54a30fa0ae8bad9a63928e3 \ + --hash=sha256:5b4f105deeffa28bbcdff6c49b34e74903139afa690e35d2d9e3c2c2fba18cec \ + --hash=sha256:5c1dc0f53856b9cc9a0ccca0a7cc61d3d20a7088201c0937f3f4048c1718a209 \ + --hash=sha256:614fdafe9f5f19c63ea02817fa4861c606a59a604a77c8cdef5aa01d28b97921 \ + --hash=sha256:617c7357272c67696fd052811e352ac54ed1d9b49ab370261a80d3b6ce385045 \ + --hash=sha256:65794e4048ee837494aea3c21a28ad5fc080994dfba5b036cf84de37f7ad5074 \ + --hash=sha256:6632f2d04f15d1bd6fe0eedd3b86d9061b836ddca4c03d5cf5c7e9e6b7c14580 \ + --hash=sha256:6c8ef2ebf76df43f5750b46851ed1cdf8f109d7787ca40035fe19fbdc1acc5a7 \ + --hash=sha256:758406267907b3781beee0f0edfe4a179fbd97c0be2e9b1154d7f0a1279cf8e5 \ + --hash=sha256:7e60cb630f674a31f0368ed32b2a6b4331b8350d67de53c0359992444b116dd3 \ + --hash=sha256:89c19a494bf3ad08c1da49445cc5d13d8fefc265f48ee7e7556839acdacf69d0 \ + --hash=sha256:8a86a9b96070674fc88b6f9f71a97d2c1d3e5165574615d1f9168ecba4cecb24 \ + --hash=sha256:8bc7690f7caee50b04a79bf017a8d020c1f48c2a1077ffe172abec59870f1139 \ + --hash=sha256:8d7919548df3f25374a1f5d01fbcd38dacab338ef5f33e044744b5c36729c8db \ + --hash=sha256:9426133526f69fcaba6e42146b4e12d6bc6c839b8b555097020e2b78ce908dcc \ + --hash=sha256:9824fb430c9cf9af743cf7aaf6707bf14323fb51ee74425c380f4c846ea70789 \ + --hash=sha256:9bb4a0d90fdb03437c109a17eade42dfbf6190408f29b2744114d11586611d6f \ + --hash=sha256:9bc2d153989e3216b0559251b0c260cfd168ec78b1fac33dd485750a228db5a2 \ + --hash=sha256:9d35cef91e59ebbeaa45214861874bc6f19eb35de96db73e467a8358d701a96c \ + --hash=sha256:a1862d2d7ce1674cffa6d186d53ca95c6e17ed2b06b3f4c476173565c862d232 \ + --hash=sha256:a84ab91cbe7aab97f7446652d0ed37d35b68a465aeef8fc41932a9d7eee2c1a6 \ + --hash=sha256:aa7f429242aae2947246587d2964fad750b79e8c233a2367f71b554e9447949c \ + --hash=sha256:aa9a0521aeca7d4941499a73ad7d4f8ffa3d1affc50b9ea11d992cd7eff18a29 \ + --hash=sha256:ac2f4f7a98934c2ed6505aead07b979e6f999389f16b714448fb39bbaa86a489 \ + --hash=sha256:ae94bd0b2f02c28e199e9bc51485d0c5601f58780636185660f86bf80c89af94 \ + --hash=sha256:af0fc424a5842a11e28956e69395fbbeab2c97c42253169d87e90aac2886d751 \ + --hash=sha256:b2a5db5397d82fa847e4c624b0c98fe59d2d9b7cf0ce6de09e4d2e80f8f5b3f2 \ + --hash=sha256:b4c29cbbba378759ac5786730d1c3cb4ec6f8ababf5c42a9ce303dc4b3d08cda \ + --hash=sha256:b74b25f024b421d5859d156750ea9a65651793d51b76a2e9238c05c9d5f203a9 \ + --hash=sha256:b7f19250ceef892adf27f0399b9e5afad019288e9be756d6919cb58892129f51 \ + --hash=sha256:b80d4a7900cf6b66bb9cee5c352b2d708e29e5a37fe9bf784fa97fc11504bf6c \ + --hash=sha256:b8c00a3b1e70c1d3891f0db1b05292747f0dbcfb49c43f9244d04c70fbc40eb8 \ + --hash=sha256:bb273176be34a746bdac0b0d7e4e2c467323d13640b736c4c477881a3220a989 \ + --hash=sha256:c3c20f0ddeb6e29126d45f89206b8291352b8c5b44384e78a6499d68b52ae511 \ + --hash=sha256:c3e130fd0ec56cb76eb49ef52faead8ff09d13f4527e9b0c400307ff72b408e1 \ + --hash=sha256:c52d3f2f82b763a24ef52f5d24358553e8403ce05f893b5347098014f2d9eff2 \ + --hash=sha256:c6377e647bbfd0a0b159fe557f2c6c602c159fc752fa316572f012fc0bf67150 \ + --hash=sha256:c638144ce971df84650d3ed0096e2ae7af8e62ecbbb7b201c8935c370df00a2c \ + --hash=sha256:ce9845054c13696f7af7f2b353e6b4f676dab1b4b215d7fe5e05c6f8bb06f965 \ + --hash=sha256:cf258ede5bc22a45c8e726b29835b9303c285ab46fc7c3a4cc770736b5304c9f \ + --hash=sha256:d0a26ffe9d4dd35e4dfdd1e71f46401cff0181c75ac174711ccff0459135fa58 \ + --hash=sha256:d0b67d87bb45ed1cd020e8fbf2307d449b68abc45402fe1a4ac9e46c3c8b192b \ + --hash=sha256:d20277fd62e1b992a50c43f13fbe13277a31f8c9f70d59759c88f644d66c619f \ + --hash=sha256:d454b8749b4bd70dd0a79f428731ee263fa6995f83ccb8bada706e8d1d3ff89d \ + --hash=sha256:d4c7d1a051eeb39f5c9547e82ea27cbcc28338482242e3e0b7768033cb083821 \ + --hash=sha256:d72278a30111e5b5525c1dd96120d9e958464316f55adb030433ea905866f4de \ + --hash=sha256:d72a210824facfdaf8768cf2d7ca25a042c30320b3020de2fa04640920d4e121 \ + --hash=sha256:d807dc2051abe041b6649681dce568f8e10668e3c1c6543ebae58f2d7e617855 \ + --hash=sha256:dbe982f38565bb50cb7fb061ebf762c2f254ca3d8c20d4006878766e84266272 \ + --hash=sha256:dcedf0b42bcb4cfff4101d7771a10532415a6106062f005ab97d1d0ab5681c60 \ + --hash=sha256:deb62214c42a261cb3eb04d474f7155279c1a8a8c30ac89b7dcb1721d92c3c02 \ + --hash=sha256:def7400461c3a3f26e49078302e1c1b38f6752342c77e3cf72ce91ca69fb1bc1 \ + --hash=sha256:df3de6b7726b52966edf29663e57306b23ef775faf0ac01a3e9f4012a24a4140 \ + --hash=sha256:e1940dae14e715e2e02dfd5b0f64a52e8374a517a1e531ad9412319dc3ac7879 \ + --hash=sha256:e4df1e3b3bec320790f699890d41c59d250f6beda159ea3c44c3f5bac1976940 \ + --hash=sha256:e6900ecdd50ce0facf703f7a00df12374b74bbc8ad9fe0f6559947fb20f82364 \ + --hash=sha256:ea438162a9fcbee3ecf36c23e6c68237479f89f962f82dae83dc15feeceb37e4 \ + --hash=sha256:eb851b7df9dda52dc1415ebee12362047ce771fc36914586b2e9fcbd7d293b3e \ + --hash=sha256:ec31a99ca63bf3cd7f1a5ac9fe95c5e2d060d3c768a09bc1d16e235840861420 \ + --hash=sha256:f0475242f447cc6cb8a9dd486d68b2ef7fbee84427124c232bff5f63b1fe11e5 \ + --hash=sha256:f2fbf7db2012d4876fb0d66b5b9ba6591197b0f165db8d99371d976546472a24 \ + --hash=sha256:f60012a73aa396be721558caa3a6fd49b3dd0033d1675c6d59c4502e870fcf0c \ + --hash=sha256:f8e604fe73ba048c06085beaf51147eaec7df856824bfe7b98657cf436623daf \ + --hash=sha256:f90a4cd061914a60bd51c68bcb4357086991bd0bb93d8aa66a6da7701370708f \ + --hash=sha256:f918a1a130a6dfe1d7fe0f105064141342e7dd1611f2e6a21cd2f5c8cb1cfb3e \ + --hash=sha256:fa518bcd7600c584bf42e6617ee8132869e877db2f76bcdc281ec6a4113a53ab \ + --hash=sha256:faefcc78f53a88f3076b7f8be0a8f8d35133a3ecf7f3770895c25f8813460f08 \ + --hash=sha256:fcaeb7b57f1a1e071ebd748984359fef83ecb026325b9d4ca847c95bc7311c92 \ + --hash=sha256:fd2d84f40633bc475ef2d5490b9c19543fbf18596dcb1b291e3a12ea5d722f7a \ + --hash=sha256:fdfc3a892927458d98f3d55428ae46b921d1f7543b89382fdb483f5640daaec8 + # via + # -c lock/requirements-dev.txt + # jsonschema + # referencing +s3transfer==0.10.2 \ + --hash=sha256:0711534e9356d3cc692fdde846b4a1e4b0cb6519971860796e6bc4c7aea00ef6 \ + --hash=sha256:eca1c20de70a39daee580aef4986996620f365c4e0fda6a86100231d62f1bf69 + # via + # -c lock/requirements-dev.txt + # boto3 +shellingham==1.5.4 \ + --hash=sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686 \ + --hash=sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de + # via + # -c lock/requirements-dev.txt + # typer +six==1.16.0 \ + --hash=sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926 \ + --hash=sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254 + # via + # -c lock/requirements-dev.txt + # python-dateutil +sniffio==1.3.1 \ + --hash=sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2 \ + --hash=sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc + # via + # -c lock/requirements-dev.txt + # anyio + # httpx +starlette==0.37.2 \ + --hash=sha256:6fe59f29268538e5d0d182f2791a479a0c64638e6935d1c6989e63fb2699c6ee \ + --hash=sha256:9af890290133b79fc3db55474ade20f6220a364a0402e0b556e7cd5e1e093823 + # via + # -c lock/requirements-dev.txt + # fastapi +typer==0.12.5 \ + --hash=sha256:62fe4e471711b147e3365034133904df3e235698399bc4de2b36c8579298d52b \ + --hash=sha256:f592f089bedcc8ec1b974125d64851029c3b1af145f04aca64d69410f0c9b722 + # via + # -c lock/requirements-dev.txt + # dins (pyproject.toml) + # fastapi-cli +typing-extensions==4.12.2 \ + --hash=sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d \ + --hash=sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8 + # via + # -c lock/requirements-dev.txt + # aiokafka + # fastapi + # pydantic + # pydantic-core + # typer +urllib3==2.2.3 \ + --hash=sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac \ + --hash=sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9 + # via + # -c lock/requirements-dev.txt + # botocore +uvicorn==0.29.0 \ + --hash=sha256:2c2aac7ff4f4365c206fd773a39bf4ebd1047c238f8b8268ad996829323473de \ + --hash=sha256:6a69214c0b6a087462412670b3ef21224fa48cae0e452b5883e8e8bdfdd11dd0 + # via + # -c lock/requirements-dev.txt + # fastapi + # fastapi-cli + # ghga-service-commons +uvloop==0.20.0 \ + --hash=sha256:265a99a2ff41a0fd56c19c3838b29bf54d1d177964c300dad388b27e84fd7847 \ + --hash=sha256:2beee18efd33fa6fdb0976e18475a4042cd31c7433c866e8a09ab604c7c22ff2 \ + --hash=sha256:35968fc697b0527a06e134999eef859b4034b37aebca537daeb598b9d45a137b \ + --hash=sha256:36c530d8fa03bfa7085af54a48f2ca16ab74df3ec7108a46ba82fd8b411a2315 \ + --hash=sha256:3a609780e942d43a275a617c0839d85f95c334bad29c4c0918252085113285b5 \ + --hash=sha256:4603ca714a754fc8d9b197e325db25b2ea045385e8a3ad05d3463de725fdf469 \ + --hash=sha256:4b75f2950ddb6feed85336412b9a0c310a2edbcf4cf931aa5cfe29034829676d \ + --hash=sha256:4f44af67bf39af25db4c1ac27e82e9665717f9c26af2369c404be865c8818dcf \ + --hash=sha256:6462c95f48e2d8d4c993a2950cd3d31ab061864d1c226bbf0ee2f1a8f36674b9 \ + --hash=sha256:649c33034979273fa71aa25d0fe120ad1777c551d8c4cd2c0c9851d88fcb13ab \ + --hash=sha256:746242cd703dc2b37f9d8b9f173749c15e9a918ddb021575a0205ec29a38d31e \ + --hash=sha256:77fbc69c287596880ecec2d4c7a62346bef08b6209749bf6ce8c22bbaca0239e \ + --hash=sha256:80dc1b139516be2077b3e57ce1cb65bfed09149e1d175e0478e7a987863b68f0 \ + --hash=sha256:82edbfd3df39fb3d108fc079ebc461330f7c2e33dbd002d146bf7c445ba6e756 \ + --hash=sha256:89e8d33bb88d7263f74dc57d69f0063e06b5a5ce50bb9a6b32f5fcbe655f9e73 \ + --hash=sha256:94707205efbe809dfa3a0d09c08bef1352f5d3d6612a506f10a319933757c006 \ + --hash=sha256:95720bae002ac357202e0d866128eb1ac82545bcf0b549b9abe91b5178d9b541 \ + --hash=sha256:9b04d96188d365151d1af41fa2d23257b674e7ead68cfd61c725a422764062ae \ + --hash=sha256:9d0fba61846f294bce41eb44d60d58136090ea2b5b99efd21cbdf4e21927c56a \ + --hash=sha256:9ebafa0b96c62881d5cafa02d9da2e44c23f9f0cd829f3a32a6aff771449c996 \ + --hash=sha256:a0fac7be202596c7126146660725157d4813aa29a4cc990fe51346f75ff8fde7 \ + --hash=sha256:aea15c78e0d9ad6555ed201344ae36db5c63d428818b4b2a42842b3870127c00 \ + --hash=sha256:b10c2956efcecb981bf9cfb8184d27d5d64b9033f917115a960b83f11bfa0d6b \ + --hash=sha256:b16696f10e59d7580979b420eedf6650010a4a9c3bd8113f24a103dfdb770b10 \ + --hash=sha256:d8c36fdf3e02cec92aed2d44f63565ad1522a499c654f07935c8f9d04db69e95 \ + --hash=sha256:e237f9c1e8a00e7d9ddaa288e535dc337a39bcbf679f290aee9d26df9e72bce9 \ + --hash=sha256:e50289c101495e0d1bb0bfcb4a60adde56e32f4449a67216a1ab2750aa84f037 \ + --hash=sha256:e7d61fe8e8d9335fac1bf8d5d82820b4808dd7a43020c149b63a1ada953d48a6 \ + --hash=sha256:e97152983442b499d7a71e44f29baa75b3b02e65d9c44ba53b10338e98dedb66 \ + --hash=sha256:f0e94b221295b5e69de57a1bd4aeb0b3a29f61be6e1b478bb8a69a73377db7ba \ + --hash=sha256:fee6044b64c965c425b65a4e17719953b96e065c5b7e09b599ff332bb2744bdf + # via + # -c lock/requirements-dev.txt + # uvicorn +watchfiles==0.24.0 \ + --hash=sha256:01550ccf1d0aed6ea375ef259706af76ad009ef5b0203a3a4cce0f6024f9b68a \ + --hash=sha256:01def80eb62bd5db99a798d5e1f5f940ca0a05986dcfae21d833af7a46f7ee22 \ + --hash=sha256:07cdef0c84c03375f4e24642ef8d8178e533596b229d32d2bbd69e5128ede02a \ + --hash=sha256:083dc77dbdeef09fa44bb0f4d1df571d2e12d8a8f985dccde71ac3ac9ac067a0 \ + --hash=sha256:1cf1f6dd7825053f3d98f6d33f6464ebdd9ee95acd74ba2c34e183086900a827 \ + --hash=sha256:21ab23fdc1208086d99ad3f69c231ba265628014d4aed31d4e8746bd59e88cd1 \ + --hash=sha256:2dadf8a8014fde6addfd3c379e6ed1a981c8f0a48292d662e27cabfe4239c83c \ + --hash=sha256:2e28d91ef48eab0afb939fa446d8ebe77e2f7593f5f463fd2bb2b14132f95b6e \ + --hash=sha256:2efec17819b0046dde35d13fb8ac7a3ad877af41ae4640f4109d9154ed30a188 \ + --hash=sha256:30bbd525c3262fd9f4b1865cb8d88e21161366561cd7c9e1194819e0a33ea86b \ + --hash=sha256:316449aefacf40147a9efaf3bd7c9bdd35aaba9ac5d708bd1eb5763c9a02bef5 \ + --hash=sha256:327763da824817b38ad125dcd97595f942d720d32d879f6c4ddf843e3da3fe90 \ + --hash=sha256:32aa53a9a63b7f01ed32e316e354e81e9da0e6267435c7243bf8ae0f10b428ef \ + --hash=sha256:34e19e56d68b0dad5cff62273107cf5d9fbaf9d75c46277aa5d803b3ef8a9e9b \ + --hash=sha256:3770e260b18e7f4e576edca4c0a639f704088602e0bc921c5c2e721e3acb8d15 \ + --hash=sha256:3d2e3ab79a1771c530233cadfd277fcc762656d50836c77abb2e5e72b88e3a48 \ + --hash=sha256:41face41f036fee09eba33a5b53a73e9a43d5cb2c53dad8e61fa6c9f91b5a51e \ + --hash=sha256:43e3e37c15a8b6fe00c1bce2473cfa8eb3484bbeecf3aefbf259227e487a03df \ + --hash=sha256:449f43f49c8ddca87c6b3980c9284cab6bd1f5c9d9a2b00012adaaccd5e7decd \ + --hash=sha256:4933a508d2f78099162da473841c652ad0de892719043d3f07cc83b33dfd9d91 \ + --hash=sha256:49d617df841a63b4445790a254013aea2120357ccacbed00253f9c2b5dc24e2d \ + --hash=sha256:49fb58bcaa343fedc6a9e91f90195b20ccb3135447dc9e4e2570c3a39565853e \ + --hash=sha256:4a7fa2bc0efef3e209a8199fd111b8969fe9db9c711acc46636686331eda7dd4 \ + --hash=sha256:4abf4ad269856618f82dee296ac66b0cd1d71450fc3c98532d93798e73399b7a \ + --hash=sha256:4b8693502d1967b00f2fb82fc1e744df128ba22f530e15b763c8d82baee15370 \ + --hash=sha256:4d28cea3c976499475f5b7a2fec6b3a36208656963c1a856d328aeae056fc5c1 \ + --hash=sha256:5148c2f1ea043db13ce9b0c28456e18ecc8f14f41325aa624314095b6aa2e9ea \ + --hash=sha256:54ca90a9ae6597ae6dc00e7ed0a040ef723f84ec517d3e7ce13e63e4bc82fa04 \ + --hash=sha256:551ec3ee2a3ac9cbcf48a4ec76e42c2ef938a7e905a35b42a1267fa4b1645896 \ + --hash=sha256:5c51749f3e4e269231510da426ce4a44beb98db2dce9097225c338f815b05d4f \ + --hash=sha256:632676574429bee8c26be8af52af20e0c718cc7f5f67f3fb658c71928ccd4f7f \ + --hash=sha256:6509ed3f467b79d95fc62a98229f79b1a60d1b93f101e1c61d10c95a46a84f43 \ + --hash=sha256:6bdcfa3cd6fdbdd1a068a52820f46a815401cbc2cb187dd006cb076675e7b735 \ + --hash=sha256:7138eff8baa883aeaa074359daabb8b6c1e73ffe69d5accdc907d62e50b1c0da \ + --hash=sha256:7211b463695d1e995ca3feb38b69227e46dbd03947172585ecb0588f19b0d87a \ + --hash=sha256:73bde715f940bea845a95247ea3e5eb17769ba1010efdc938ffcb967c634fa61 \ + --hash=sha256:78470906a6be5199524641f538bd2c56bb809cd4bf29a566a75051610bc982c3 \ + --hash=sha256:7ae3e208b31be8ce7f4c2c0034f33406dd24fbce3467f77223d10cd86778471c \ + --hash=sha256:7e4bd963a935aaf40b625c2499f3f4f6bbd0c3776f6d3bc7c853d04824ff1c9f \ + --hash=sha256:82ae557a8c037c42a6ef26c494d0631cacca040934b101d001100ed93d43f361 \ + --hash=sha256:82b2509f08761f29a0fdad35f7e1638b8ab1adfa2666d41b794090361fb8b855 \ + --hash=sha256:8360f7314a070c30e4c976b183d1d8d1585a4a50c5cb603f431cebcbb4f66327 \ + --hash=sha256:85d5f0c7771dcc7a26c7a27145059b6bb0ce06e4e751ed76cdf123d7039b60b5 \ + --hash=sha256:88bcd4d0fe1d8ff43675360a72def210ebad3f3f72cabfeac08d825d2639b4ab \ + --hash=sha256:9301c689051a4857d5b10777da23fafb8e8e921bcf3abe6448a058d27fb67633 \ + --hash=sha256:951088d12d339690a92cef2ec5d3cfd957692834c72ffd570ea76a6790222777 \ + --hash=sha256:95cf3b95ea665ab03f5a54765fa41abf0529dbaf372c3b83d91ad2cfa695779b \ + --hash=sha256:96619302d4374de5e2345b2b622dc481257a99431277662c30f606f3e22f42be \ + --hash=sha256:999928c6434372fde16c8f27143d3e97201160b48a614071261701615a2a156f \ + --hash=sha256:9a60e2bf9dc6afe7f743e7c9b149d1fdd6dbf35153c78fe3a14ae1a9aee3d98b \ + --hash=sha256:9f895d785eb6164678ff4bb5cc60c5996b3ee6df3edb28dcdeba86a13ea0465e \ + --hash=sha256:a2a9891723a735d3e2540651184be6fd5b96880c08ffe1a98bae5017e65b544b \ + --hash=sha256:a974231b4fdd1bb7f62064a0565a6b107d27d21d9acb50c484d2cdba515b9366 \ + --hash=sha256:aa0fd7248cf533c259e59dc593a60973a73e881162b1a2f73360547132742823 \ + --hash=sha256:acbfa31e315a8f14fe33e3542cbcafc55703b8f5dcbb7c1eecd30f141df50db3 \ + --hash=sha256:afb72325b74fa7a428c009c1b8be4b4d7c2afedafb2982827ef2156646df2fe1 \ + --hash=sha256:b3ef2c69c655db63deb96b3c3e587084612f9b1fa983df5e0c3379d41307467f \ + --hash=sha256:b52a65e4ea43c6d149c5f8ddb0bef8d4a1e779b77591a458a893eb416624a418 \ + --hash=sha256:b665caeeda58625c3946ad7308fbd88a086ee51ccb706307e5b1fa91556ac886 \ + --hash=sha256:b74fdffce9dfcf2dc296dec8743e5b0332d15df19ae464f0e249aa871fc1c571 \ + --hash=sha256:b995bfa6bf01a9e09b884077a6d37070464b529d8682d7691c2d3b540d357a0c \ + --hash=sha256:bd82010f8ab451dabe36054a1622870166a67cf3fce894f68895db6f74bbdc94 \ + --hash=sha256:bdcd5538e27f188dd3c804b4a8d5f52a7fc7f87e7fd6b374b8e36a4ca03db428 \ + --hash=sha256:c79d7719d027b7a42817c5d96461a99b6a49979c143839fc37aa5748c322f234 \ + --hash=sha256:cdab9555053399318b953a1fe1f586e945bc8d635ce9d05e617fd9fe3a4687d6 \ + --hash=sha256:ce72dba6a20e39a0c628258b5c308779b8697f7676c254a845715e2a1039b968 \ + --hash=sha256:d337193bbf3e45171c8025e291530fb7548a93c45253897cd764a6a71c937ed9 \ + --hash=sha256:d3dcb774e3568477275cc76554b5a565024b8ba3a0322f77c246bc7111c5bb9c \ + --hash=sha256:d64ba08db72e5dfd5c33be1e1e687d5e4fcce09219e8aee893a4862034081d4e \ + --hash=sha256:d7a2e3b7f5703ffbd500dabdefcbc9eafeff4b9444bbdd5d83d79eedf8428fab \ + --hash=sha256:d831ee0a50946d24a53821819b2327d5751b0c938b12c0653ea5be7dea9c82ec \ + --hash=sha256:d9018153cf57fc302a2a34cb7564870b859ed9a732d16b41a9b5cb2ebed2d444 \ + --hash=sha256:e5171ef898299c657685306d8e1478a45e9303ddcd8ac5fed5bd52ad4ae0b69b \ + --hash=sha256:e94e98c7cb94cfa6e071d401ea3342767f28eb5a06a58fafdc0d2a4974f4f35c \ + --hash=sha256:ec39698c45b11d9694a1b635a70946a5bad066b593af863460a8e600f0dff1ca \ + --hash=sha256:ed9aba6e01ff6f2e8285e5aa4154e2970068fe0fc0998c4380d0e6278222269b \ + --hash=sha256:edf71b01dec9f766fb285b73930f95f730bb0943500ba0566ae234b5c1618c18 \ + --hash=sha256:ee82c98bed9d97cd2f53bdb035e619309a098ea53ce525833e26b93f673bc318 \ + --hash=sha256:f4c96283fca3ee09fb044f02156d9570d156698bc3734252175a38f0e8975f07 \ + --hash=sha256:f7d9b87c4c55e3ea8881dfcbf6d61ea6775fffed1fedffaa60bd047d3c08c430 \ + --hash=sha256:f83df90191d67af5a831da3a33dd7628b02a95450e168785586ed51e6d28943c \ + --hash=sha256:fca9433a45f18b7c779d2bae7beeec4f740d28b788b117a48368d95a3233ed83 \ + --hash=sha256:fd92bbaa2ecdb7864b7600dcdb6f2f1db6e0346ed425fbd01085be04c63f0b05 + # via + # -c lock/requirements-dev.txt + # uvicorn +websockets==13.0.1 \ + --hash=sha256:00fd961943b6c10ee6f0b1130753e50ac5dcd906130dcd77b0003c3ab797d026 \ + --hash=sha256:03d3f9ba172e0a53e37fa4e636b86cc60c3ab2cfee4935e66ed1d7acaa4625ad \ + --hash=sha256:0513c727fb8adffa6d9bf4a4463b2bade0186cbd8c3604ae5540fae18a90cb99 \ + --hash=sha256:05e70fec7c54aad4d71eae8e8cab50525e899791fc389ec6f77b95312e4e9920 \ + --hash=sha256:0617fd0b1d14309c7eab6ba5deae8a7179959861846cbc5cb528a7531c249448 \ + --hash=sha256:06c0a667e466fcb56a0886d924b5f29a7f0886199102f0a0e1c60a02a3751cb4 \ + --hash=sha256:0f52504023b1480d458adf496dc1c9e9811df4ba4752f0bc1f89ae92f4f07d0c \ + --hash=sha256:10a0dc7242215d794fb1918f69c6bb235f1f627aaf19e77f05336d147fce7c37 \ + --hash=sha256:11f9976ecbc530248cf162e359a92f37b7b282de88d1d194f2167b5e7ad80ce3 \ + --hash=sha256:132511bfd42e77d152c919147078460c88a795af16b50e42a0bd14f0ad71ddd2 \ + --hash=sha256:139add0f98206cb74109faf3611b7783ceafc928529c62b389917a037d4cfdf4 \ + --hash=sha256:14b9c006cac63772b31abbcd3e3abb6228233eec966bf062e89e7fa7ae0b7333 \ + --hash=sha256:15c7d62ee071fa94a2fc52c2b472fed4af258d43f9030479d9c4a2de885fd543 \ + --hash=sha256:165bedf13556f985a2aa064309baa01462aa79bf6112fbd068ae38993a0e1f1b \ + --hash=sha256:17118647c0ea14796364299e942c330d72acc4b248e07e639d34b75067b3cdd8 \ + --hash=sha256:1841c9082a3ba4a05ea824cf6d99570a6a2d8849ef0db16e9c826acb28089e8f \ + --hash=sha256:1a678532018e435396e37422a95e3ab87f75028ac79570ad11f5bf23cd2a7d8c \ + --hash=sha256:1ee4cc030a4bdab482a37462dbf3ffb7e09334d01dd37d1063be1136a0d825fa \ + --hash=sha256:1f3cf6d6ec1142412d4535adabc6bd72a63f5f148c43fe559f06298bc21953c9 \ + --hash=sha256:1f613289f4a94142f914aafad6c6c87903de78eae1e140fa769a7385fb232fdf \ + --hash=sha256:1fa082ea38d5de51dd409434edc27c0dcbd5fed2b09b9be982deb6f0508d25bc \ + --hash=sha256:249aab278810bee585cd0d4de2f08cfd67eed4fc75bde623be163798ed4db2eb \ + --hash=sha256:254ecf35572fca01a9f789a1d0f543898e222f7b69ecd7d5381d8d8047627bdb \ + --hash=sha256:2a02b0161c43cc9e0232711eff846569fad6ec836a7acab16b3cf97b2344c060 \ + --hash=sha256:30d3a1f041360f029765d8704eae606781e673e8918e6b2c792e0775de51352f \ + --hash=sha256:3624fd8664f2577cf8de996db3250662e259bfbc870dd8ebdcf5d7c6ac0b5185 \ + --hash=sha256:3f55b36d17ac50aa8a171b771e15fbe1561217510c8768af3d546f56c7576cdc \ + --hash=sha256:46af561eba6f9b0848b2c9d2427086cabadf14e0abdd9fde9d72d447df268418 \ + --hash=sha256:47236c13be337ef36546004ce8c5580f4b1150d9538b27bf8a5ad8edf23ccfab \ + --hash=sha256:4a365bcb7be554e6e1f9f3ed64016e67e2fa03d7b027a33e436aecf194febb63 \ + --hash=sha256:4d6ece65099411cfd9a48d13701d7438d9c34f479046b34c50ff60bb8834e43e \ + --hash=sha256:4e85f46ce287f5c52438bb3703d86162263afccf034a5ef13dbe4318e98d86e7 \ + --hash=sha256:4f0426d51c8f0926a4879390f53c7f5a855e42d68df95fff6032c82c888b5f36 \ + --hash=sha256:518f90e6dd089d34eaade01101fd8a990921c3ba18ebbe9b0165b46ebff947f0 \ + --hash=sha256:52aed6ef21a0f1a2a5e310fb5c42d7555e9c5855476bbd7173c3aa3d8a0302f2 \ + --hash=sha256:556e70e4f69be1082e6ef26dcb70efcd08d1850f5d6c5f4f2bcb4e397e68f01f \ + --hash=sha256:56a952fa2ae57a42ba7951e6b2605e08a24801a4931b5644dfc68939e041bc7f \ + --hash=sha256:59197afd478545b1f73367620407b0083303569c5f2d043afe5363676f2697c9 \ + --hash=sha256:5df891c86fe68b2c38da55b7aea7095beca105933c697d719f3f45f4220a5e0e \ + --hash=sha256:63848cdb6fcc0bf09d4a155464c46c64ffdb5807ede4fb251da2c2692559ce75 \ + --hash=sha256:64a11aae1de4c178fa653b07d90f2fb1a2ed31919a5ea2361a38760192e1858b \ + --hash=sha256:6724b554b70d6195ba19650fef5759ef11346f946c07dbbe390e039bcaa7cc3d \ + --hash=sha256:67494e95d6565bf395476e9d040037ff69c8b3fa356a886b21d8422ad86ae075 \ + --hash=sha256:67648f5e50231b5a7f6d83b32f9c525e319f0ddc841be0de64f24928cd75a603 \ + --hash=sha256:68264802399aed6fe9652e89761031acc734fc4c653137a5911c2bfa995d6d6d \ + --hash=sha256:699ba9dd6a926f82a277063603fc8d586b89f4cb128efc353b749b641fcddda7 \ + --hash=sha256:6aa74a45d4cdc028561a7d6ab3272c8b3018e23723100b12e58be9dfa5a24491 \ + --hash=sha256:6b41a1b3b561f1cba8321fb32987552a024a8f67f0d05f06fcf29f0090a1b956 \ + --hash=sha256:71e6e5a3a3728886caee9ab8752e8113670936a193284be9d6ad2176a137f376 \ + --hash=sha256:7d20516990d8ad557b5abeb48127b8b779b0b7e6771a265fa3e91767596d7d97 \ + --hash=sha256:80e4ba642fc87fa532bac07e5ed7e19d56940b6af6a8c61d4429be48718a380f \ + --hash=sha256:872afa52a9f4c414d6955c365b6588bc4401272c629ff8321a55f44e3f62b553 \ + --hash=sha256:8eb2b9a318542153674c6e377eb8cb9ca0fc011c04475110d3477862f15d29f0 \ + --hash=sha256:9bbc525f4be3e51b89b2a700f5746c2a6907d2e2ef4513a8daafc98198b92237 \ + --hash=sha256:a1a2e272d067030048e1fe41aa1ec8cfbbaabce733b3d634304fa2b19e5c897f \ + --hash=sha256:a5dc0c42ded1557cc7c3f0240b24129aefbad88af4f09346164349391dea8e58 \ + --hash=sha256:acab3539a027a85d568c2573291e864333ec9d912675107d6efceb7e2be5d980 \ + --hash=sha256:acbebec8cb3d4df6e2488fbf34702cbc37fc39ac7abf9449392cefb3305562e9 \ + --hash=sha256:ad327ac80ba7ee61da85383ca8822ff808ab5ada0e4a030d66703cc025b021c4 \ + --hash=sha256:b448a0690ef43db5ef31b3a0d9aea79043882b4632cfc3eaab20105edecf6097 \ + --hash=sha256:b5a06d7f60bc2fc378a333978470dfc4e1415ee52f5f0fce4f7853eb10c1e9df \ + --hash=sha256:b74593e9acf18ea5469c3edaa6b27fa7ecf97b30e9dabd5a94c4c940637ab96e \ + --hash=sha256:b79915a1179a91f6c5f04ece1e592e2e8a6bd245a0e45d12fd56b2b59e559a32 \ + --hash=sha256:b80f0c51681c517604152eb6a572f5a9378f877763231fddb883ba2f968e8817 \ + --hash=sha256:b8ac5b46fd798bbbf2ac6620e0437c36a202b08e1f827832c4bf050da081b501 \ + --hash=sha256:c3c493d0e5141ec055a7d6809a28ac2b88d5b878bb22df8c621ebe79a61123d0 \ + --hash=sha256:c44ca9ade59b2e376612df34e837013e2b273e6c92d7ed6636d0556b6f4db93d \ + --hash=sha256:c4a6343e3b0714e80da0b0893543bf9a5b5fa71b846ae640e56e9abc6fbc4c83 \ + --hash=sha256:c5870b4a11b77e4caa3937142b650fbbc0914a3e07a0cf3131f35c0587489c1c \ + --hash=sha256:ca48914cdd9f2ccd94deab5bcb5ac98025a5ddce98881e5cce762854a5de330b \ + --hash=sha256:cf2fae6d85e5dc384bf846f8243ddaa9197f3a1a70044f59399af001fd1f51d4 \ + --hash=sha256:d450f5a7a35662a9b91a64aefa852f0c0308ee256122f5218a42f1d13577d71e \ + --hash=sha256:d6716c087e4aa0b9260c4e579bb82e068f84faddb9bfba9906cb87726fa2e870 \ + --hash=sha256:d93572720d781331fb10d3da9ca1067817d84ad1e7c31466e9f5e59965618096 \ + --hash=sha256:dbb0b697cc0655719522406c059eae233abaa3243821cfdfab1215d02ac10231 \ + --hash=sha256:e33505534f3f673270dd67f81e73550b11de5b538c56fe04435d63c02c3f26b5 \ + --hash=sha256:e801ca2f448850685417d723ec70298feff3ce4ff687c6f20922c7474b4746ae \ + --hash=sha256:e82db3756ccb66266504f5a3de05ac6b32f287faacff72462612120074103329 \ + --hash=sha256:ef48e4137e8799998a343706531e656fdec6797b80efd029117edacb74b0a10a \ + --hash=sha256:f1d3d1f2eb79fe7b0fb02e599b2bf76a7619c79300fc55f0b5e2d382881d4f7f \ + --hash=sha256:f3fea72e4e6edb983908f0db373ae0732b275628901d909c382aae3b592589f2 \ + --hash=sha256:f40de079779acbcdbb6ed4c65af9f018f8b77c5ec4e17a4b737c05c2db554491 \ + --hash=sha256:f73e676a46b0fe9426612ce8caeca54c9073191a77c3e9d5c94697aef99296af \ + --hash=sha256:f9c9e258e3d5efe199ec23903f5da0eeaad58cf6fccb3547b74fd4750e5ac47a \ + --hash=sha256:fac2d146ff30d9dd2fcf917e5d147db037a5c573f0446c564f16f1f94cf87462 \ + --hash=sha256:faef9ec6354fe4f9a2c0bbb52fb1ff852effc897e2a4501e25eb3a47cb0a4f89 + # via + # -c lock/requirements-dev.txt + # uvicorn diff --git a/openapi.yaml b/openapi.yaml new file mode 100644 index 0000000..e3eff55 --- /dev/null +++ b/openapi.yaml @@ -0,0 +1,239 @@ +components: + schemas: + DatasetInformation: + description: Container bundling public information for a dataset. + properties: + dataset_id: + description: Public accession of a dataset. + title: Dataset Id + type: string + file_information: + description: Public information on all files belonging to a dataset. + items: + $ref: '#/components/schemas/FileInformation' + title: File Information + type: array + required: + - dataset_id + - file_information + title: DatasetInformation + type: object + FileInformation: + description: 'Public information container for files registered with the Internal + File + + Registry service.' + properties: + file_id: + description: Public identifier of the file associated with the given information + title: File Id + type: string + sha256_hash: + description: SHA256 hash of the unencrypted file content encoded as hexadecimal values + as produced by hashlib.hexdigest(). + title: Sha256 Hash + type: string + size: + description: Size of the unencrypted file in bytes. + exclusiveMinimum: 0.0 + title: Size + type: integer + required: + - file_id + - size + - sha256_hash + title: FileInformation + type: object + HTTPValidationError: + properties: + detail: + items: + $ref: '#/components/schemas/ValidationError' + title: Detail + type: array + title: HTTPValidationError + type: object + HttpDatasetMissingInformationError: + additionalProperties: false + properties: + data: + $ref: '#/components/schemas/HttpDatasetMissingInformationErrorData' + description: + description: A human readable message to the client explaining the cause + of the exception. + title: Description + type: string + exception_id: + const: datasetInformationNotFound + enum: + - datasetInformationNotFound + title: Exception Id + type: string + required: + - data + - description + - exception_id + title: HttpDatasetMissingInformationError + type: object + HttpDatasetMissingInformationErrorData: + properties: + dataset_id: + title: Dataset Id + type: string + missing_file_ids: + items: + type: string + title: Missing File Ids + type: array + required: + - dataset_id + - missing_file_ids + title: HttpDatasetMissingInformationErrorData + type: object + HttpInformationNotFoundError: + additionalProperties: false + properties: + data: + $ref: '#/components/schemas/HttpInformationNotFoundErrorData' + description: + description: A human readable message to the client explaining the cause + of the exception. + title: Description + type: string + exception_id: + const: informationNotFound + enum: + - informationNotFound + title: Exception Id + type: string + required: + - data + - description + - exception_id + title: HttpInformationNotFoundError + type: object + HttpInformationNotFoundErrorData: + properties: + file_id: + title: File Id + type: string + required: + - file_id + title: HttpInformationNotFoundErrorData + 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: + /dataset_information/{dataset_id}: + get: + description: Retrieve and serve stored dataset information. + operationId: getDatasetInformation + parameters: + - in: path + name: dataset_id + required: true + schema: + title: Dataset Id + type: string + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/DatasetInformation' + description: + - File information consisting of file id, sha256 checksum of the unencrypted + file content and file size of the unencrypted file in bytes for all files + in a dataset. + '404': + content: + application/json: + schema: + $ref: '#/components/schemas/HttpDatasetMissingInformationError' + description: 'Exceptions by ID: + + - datasetInformationNotFound: Information for one or more of the dataset + files is not registered.' + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + description: Validation Error + summary: Return public file information for the given dataset id, i.e. public + accession. + tags: + - FileInformationService + /file_information/{file_id}: + get: + description: Retrieve and serve stored file information. + operationId: getFileInformation + parameters: + - in: path + name: file_id + required: true + schema: + title: File Id + type: string + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/FileInformation' + description: + - File information consisting of file id, sha256 checksum of the unencryptedfile + content and file size of the unencrypted file in bytes. + '404': + content: + application/json: + schema: + $ref: '#/components/schemas/HttpInformationNotFoundError' + description: 'Exceptions by ID: + + - informationNotFound: No information registered for the given file ID.' + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + description: Validation Error + summary: Return public file information for the given file id, i.e. public accession. + tags: + - FileInformationService + /health: + get: + description: Used to test if this service is alive + operationId: health_health_get + responses: + '200': + content: + application/json: + schema: {} + description: Successful Response + summary: health + tags: + - FileInformationService diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..b39c620 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,155 @@ +[build-system] +requires = [ + "setuptools>=69", +] +build-backend = "setuptools.build_meta" + +[project] +readme = "README.md" +authors = [ + { name = "German Human Genome Phenome Archive (GHGA)", email = "contact@ghga.de" }, +] +requires-python = ">=3.12" +classifiers = [ + "Development Status :: 1 - Planning", + "Operating System :: POSIX :: Linux", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.12", + "License :: OSI Approved :: Apache Software License", + "Topic :: Internet :: WWW/HTTP :: HTTP Servers", + "Topic :: Software Development :: Libraries", + "Intended Audience :: Developers", +] +name = "dins" +version = "1.1.0" +description = "Dataset Information Service - Providing public metadata about files registered with the Internal File Registry" +dependencies = [ + "typer >= 0.12", + "ghga-service-commons[api] >= 3.1", + "ghga-event-schemas >= 3.3", + "hexkit[akafka,s3,mongodb] >= 3.2", +] + +[project.license] +text = "Apache 2.0" + +[project.urls] +Repository = "https://github.com/ghga-de/dataset-information-service" + +[project.scripts] +dins = "dins.__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 = "py312" + +[tool.ruff.lint] +fixable = [ + "UP", + "I", + "D", +] +ignore = [ + "E111", + "E114", + "E116", + "PLW", + "RUF001", + "RUF010", + "RUF012", + "N818", + "B008", + "PLR2004", + "D205", + "D400", + "D401", + "D107", + "D206", + "D300", + "UP040", +] +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 = "8.0" +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 0000000..e09caf3 --- /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 0000000..f05ff70 --- /dev/null +++ b/scripts/__init__.py @@ -0,0 +1,17 @@ +# Copyright 2021 - 2024 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/check_license.py b/scripts/check_license.py new file mode 100755 index 0000000..37103d8 --- /dev/null +++ b/scripts/check_license.py @@ -0,0 +1,568 @@ +#!/usr/bin/env python3 + +# Copyright 2021 - 2024 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 + +# 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 = [ + ".coveragerc", + ".devcontainer", + ".editorconfig", + ".eggs", + ".git", + ".github", + ".flake8", + ".gitignore", + ".mypy_cache", + ".mypy.ini", + ".pylintrc", + ".pytest_cache", + ".ruff.toml", + ".ruff_cache", + ".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", + ".tox", + ".venv", + ".vscode", + "eggs", + "build", + "config_schema.json", + "dist", + "docs", + "develop-eggs", + "example_config.yaml", + "htmlcov", + "lib", + "lib62", + "parts", + "pip-wheel-metadata", + "sdist", + "venv", + "wheels", + "LICENSE", # is checked but not for the license header +] + +# exclude file by file ending from license header check: +EXCLUDE_ENDINGS = [ + "html", + "in", + "ini", + "jinja", + "json", + "md", + "pub", + "pyc", + "pyd", + "typed", + "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 strings that may be used to introduce a line comment: +LINE_COMMENTS = ["#"] + +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: str | None = None + self._n_lines: int | None = None + + @property + def text(self) -> str | None: + 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: 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, line_comments: list[str] = LINE_COMMENTS) -> str: + line = line.strip() + for line_comment in line_comments: + line_without_comment = line.removeprefix(line_comment) + if line_without_comment != line: + line = line_without_comment.lstrip() + break + return line + + +def normalized_text(text: str, line_comments: list[str] = LINE_COMMENTS) -> 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, line_comments=line_comments) + + # 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, line_comments: list[str] = LINE_COMMENTS) -> bool: + """Checks whether a line is a comment.""" + return line.lstrip().startswith(tuple(line_comments)) + + +def is_empty_line(line: str) -> bool: + """Checks whether a line is empty.""" + return not line.strip() + + +def get_header(file_path: Path, line_comments: list[str] = LINE_COMMENTS): + """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, line_comments=line_comments + ) 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, line_comments=line_comments) + + +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 - 2024) is expected: + match = re.match(r"(\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, + line_comments: list[str] = LINE_COMMENTS, + 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, + line_comments: list[str] = LINE_COMMENTS, + 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, line_comments=line_comments) + if check_copyright_notice( + copyright=header, + global_copyright=global_copyright, + copyright_template=copyright_template, + author=author, + line_comments=line_comments, + 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, + line_comments: list[str] = LINE_COMMENTS, + 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, + line_comments=line_comments, + 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/get_package_name.py b/scripts/get_package_name.py new file mode 100755 index 0000000..505a921 --- /dev/null +++ b/scripts/get_package_name.py @@ -0,0 +1,46 @@ +#!/usr/bin/env python3 + +# Copyright 2021 - 2024 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/list_outdated_dependencies.py b/scripts/list_outdated_dependencies.py new file mode 100755 index 0000000..894818c --- /dev/null +++ b/scripts/list_outdated_dependencies.py @@ -0,0 +1,214 @@ +#!/usr/bin/env python3 + +# Copyright 2021 - 2024 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, strict=True) + ] + + # 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, strict=True) + ) + + 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 0000000..d0b0aa9 --- /dev/null +++ b/scripts/script_utils/__init__.py @@ -0,0 +1,17 @@ +# Copyright 2021 - 2024 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 0000000..c7d6af4 --- /dev/null +++ b/scripts/script_utils/cli.py @@ -0,0 +1,43 @@ +# Copyright 2021 - 2024 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_warning(message: str): + """Print a warning message.""" + + styled_message = typer.style(text=message, fg=typer.colors.YELLOW) + 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 0000000..4e2f8e7 --- /dev/null +++ b/scripts/script_utils/deps.py @@ -0,0 +1,75 @@ +# Copyright 2021 - 2024 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.""" + +import tomllib +from copy import deepcopy +from pathlib import Path +from typing import Any + +import stringcase + + +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 = tomllib.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 0000000..3760fa4 --- /dev/null +++ b/scripts/script_utils/fastapi_app_location.py @@ -0,0 +1,45 @@ +# Copyright 2021 - 2024 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.""" + +import json +from typing import Any + +from fastapi import FastAPI + +from dins.adapters.inbound.fastapi_.configure import get_openapi_schema +from dins.adapters.inbound.fastapi_.routes import router + +app = FastAPI() +app.include_router(router) + + +def custom_openapi() -> dict[str, Any]: + if app.openapi_schema: + return app.openapi_schema + openapi_schema = get_openapi_schema(app) + app.openapi_schema = openapi_schema + return openapi_schema + + +def main(): + """Print the openapi""" + print(json.dumps(custom_openapi())) + + +if __name__ == "__main__": + main() diff --git a/scripts/script_utils/lock_deps.py b/scripts/script_utils/lock_deps.py new file mode 100644 index 0000000..f800d14 --- /dev/null +++ b/scripts/script_utils/lock_deps.py @@ -0,0 +1,46 @@ +# Copyright 2021 - 2024 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 packaging.requirements import Requirement + + +def get_lock_file_deps( + lock_file_path: Path, + exclude: set[str] | None = 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 0000000..856253f --- /dev/null +++ b/scripts/update_all.py @@ -0,0 +1,75 @@ +#!/usr/bin/env python3 + +# Copyright 2021 - 2024 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 0000000..8156192 --- /dev/null +++ b/scripts/update_config_docs.py @@ -0,0 +1,160 @@ +#!/usr/bin/env python3 + +# Copyright 2021 - 2024 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 0000000..c915cb0 --- /dev/null +++ b/scripts/update_hook_revs.py @@ -0,0 +1,133 @@ +#!/usr/bin/env python3 + +# Copyright 2021 - 2024 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 0000000..2f263a3 --- /dev/null +++ b/scripts/update_lock.py @@ -0,0 +1,210 @@ +#!/usr/bin/env python3 + +# Copyright 2021 - 2024 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", "--no-header"] + + 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 0000000..b2a99ca --- /dev/null +++ b/scripts/update_openapi_docs.py @@ -0,0 +1,103 @@ +#!/usr/bin/env python3 + +# Copyright 2021 - 2024 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 0000000..a09c6f0 --- /dev/null +++ b/scripts/update_pyproject.py @@ -0,0 +1,119 @@ +#!/usr/bin/env python3 + +# Copyright 2021 - 2024 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 +import tomllib +from pathlib import Path + +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 tomllib.load(file) + + +def read_custom_pyproject() -> dict[str, object]: + """Read the pyproject_custom.toml.""" + with open(PYPROJECT_CUSTOM_PATH, "rb") as file: + return tomllib.load(file) + + +def read_current_pyproject() -> dict[str, object]: + """Read the current pyproject.toml.""" + with open(PYPROJECT_TOML, "rb") as file: + return tomllib.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, multiline_strings=True) + + +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 an error message + if the values have different types. + """ + 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: + if type(value) == type(dest[field]): + cli.echo_warning(f"Overriding value for '{field}'...") + else: + cli.echo_failure(f"Conflicting types for '{field}'...") + sys.exit(1) + 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 in pyproject: + merge_fields(source=value, dest=pyproject[field]) # type: ignore + else: + pyproject[field] = value + + 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 0000000..41052d2 --- /dev/null +++ b/scripts/update_readme.py @@ -0,0 +1,238 @@ +#!/usr/bin/env python3 + +# Copyright 2021 - 2024 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 +import tomllib +from pathlib import Path +from string import Template + +import jsonschema2md +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 = tomllib.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 0000000..0df6a74 --- /dev/null +++ b/scripts/update_template_files.py @@ -0,0 +1,262 @@ +#!/usr/bin/env python3 + +# Copyright 2021 - 2024 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/dins/__init__.py b/src/dins/__init__.py new file mode 100644 index 0000000..5423f05 --- /dev/null +++ b/src/dins/__init__.py @@ -0,0 +1,19 @@ +# Copyright 2021 - 2024 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""" + +from importlib.metadata import version + +__version__ = version(__package__) diff --git a/src/dins/__main__.py b/src/dins/__main__.py new file mode 100644 index 0000000..69d4b2e --- /dev/null +++ b/src/dins/__main__.py @@ -0,0 +1,26 @@ +# Copyright 2021 - 2024 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""" + +from dins.cli import cli + + +def run(): + """Run the service""" + cli() + + +if __name__ == "__main__": + run() diff --git a/src/dins/adapters/__init__.py b/src/dins/adapters/__init__.py new file mode 100644 index 0000000..77fa790 --- /dev/null +++ b/src/dins/adapters/__init__.py @@ -0,0 +1,15 @@ +# Copyright 2021 - 2024 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. +"""Adapter implementations""" diff --git a/src/dins/adapters/inbound/__init__.py b/src/dins/adapters/inbound/__init__.py new file mode 100644 index 0000000..05891c8 --- /dev/null +++ b/src/dins/adapters/inbound/__init__.py @@ -0,0 +1,16 @@ +# Copyright 2021 - 2024 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 inbound/primary/driving adapters according to the +Hexagonal Architecture Concept.""" diff --git a/src/dins/adapters/inbound/dao.py b/src/dins/adapters/inbound/dao.py new file mode 100644 index 0000000..5a872c2 --- /dev/null +++ b/src/dins/adapters/inbound/dao.py @@ -0,0 +1,40 @@ +# Copyright 2021 - 2024 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. +"""DAO translators for database access.""" + +from hexkit.protocols.dao import DaoFactoryProtocol + +from dins.core import models +from dins.ports.inbound.dao import DatasetDaoPort, FileInformationDaoPort + + +async def get_file_information_dao( + *, dao_factory: DaoFactoryProtocol +) -> FileInformationDaoPort: + """Setup the DAOs using the specified provider of the DaoFactoryProtocol.""" + return await dao_factory.get_dao( + name="file_information", + dto_model=models.FileInformation, + id_field="file_id", + ) + + +async def get_dataset_dao(*, dao_factory: DaoFactoryProtocol) -> DatasetDaoPort: + """Setup the DAOs using the specified provider of the DaoFactoryProtocol.""" + return await dao_factory.get_dao( + name="dataset_file_ids", + dto_model=models.DatasetFileIDs, + id_field="dataset_id", + ) diff --git a/src/dins/adapters/inbound/event_sub.py b/src/dins/adapters/inbound/event_sub.py new file mode 100644 index 0000000..22aadd4 --- /dev/null +++ b/src/dins/adapters/inbound/event_sub.py @@ -0,0 +1,185 @@ +# Copyright 2021 - 2024 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. + +"""Receive events informing about files that are expected to be uploaded.""" + +import logging + +from ghga_event_schemas import pydantic_ as event_schemas +from ghga_event_schemas.validation import get_validated_payload +from hexkit.custom_types import Ascii, JsonObject +from hexkit.protocols.daosub import DaoSubscriberProtocol +from hexkit.protocols.eventsub import EventSubscriberProtocol +from pydantic import Field +from pydantic_settings import BaseSettings + +from dins.ports.inbound.information_service import InformationServicePort + +log = logging.getLogger(__name__) + + +class EventSubTranslatorConfig(BaseSettings): + """Config for publishing file upload-related events.""" + + dataset_change_event_topic: str = Field( + default=..., + description="Name of the topic for events that inform about datasets.", + examples=["metadata_datasets"], + ) + dataset_upsertion_event_type: str = Field( + default=..., + description="The type of events that inform about new and changed datasets.", + examples=["dataset_created"], + ) + dataset_deletion_event_type: str = Field( + default=..., + description="The type of events that inform about deleted datasets.", + examples=["dataset_deleted"], + ) + file_registered_event_topic: str = Field( + default=..., + description="The name of the topic for events informing about new registered files" + " for which the metadata should be made available.", + examples=["internal-file-registry"], + ) + file_registered_event_type: str = Field( + default=..., + description="The name of the type used for events informing about new registered files" + " for which the metadata should be made available.", + examples=["file_registered"], + ) + + +class EventSubTranslator(EventSubscriberProtocol): + """A triple hexagonal translator compatible with the EventSubscriberProtocol that + is used to received events relevant for file uploads. + """ + + def __init__( + self, + config: EventSubTranslatorConfig, + information_service: InformationServicePort, + ): + """Initialize with config parameters and core dependencies.""" + self._config = config + self._information_service = information_service + + self.topics_of_interest = [ + config.dataset_change_event_topic, + config.file_registered_event_topic, + ] + self.types_of_interest = [ + config.dataset_deletion_event_type, + config.dataset_upsertion_event_type, + config.file_registered_event_type, + ] + + async def _consume_validated( + self, *, payload: JsonObject, type_: Ascii, topic: Ascii, key: str + ) -> None: + """ + Receive and process an event with already validated topic and type. + + Args: + payload (JsonObject): The data/payload to send with the event. + type_ (str): The type of the event. + topic (str): Name of the topic the event was published to. + key (str): The key associated with the event. + """ + if type_ == self._config.file_registered_event_type: + await self._consume_file_internally_registered(payload=payload) + elif type_ == self._config.dataset_upsertion_event_type: + await self._consume_dataset_upserted(payload=payload) + elif type_ == self._config.dataset_deletion_event_type: + await self._consume_dataset_deleted(payload=payload) + else: + raise RuntimeError(f"Unexpected event of type: {type_}") + + async def _consume_file_internally_registered(self, *, payload: JsonObject): + """ + Consume confirmation event that object data has been moved to permanent storage + and the associated relevant metadata should be presented by this service. + """ + validated_payload = get_validated_payload( + payload=payload, + schema=event_schemas.FileInternallyRegistered, + ) + + await self._information_service.register_information( + file_registered=validated_payload + ) + + async def _consume_dataset_upserted(self, *, payload: JsonObject): + """Consume newly registered dataset to store file ID mapping.""" + validated_payload = get_validated_payload( + payload=payload, + schema=event_schemas.MetadataDatasetOverview, + ) + + await self._information_service.register_dataset_information( + metadata_dataset=validated_payload + ) + + async def _consume_dataset_deleted(self, *, payload: JsonObject): + """Delete information for registered dataset mappings when a dataset is deleted.""" + validated_payload = get_validated_payload( + payload=payload, + schema=event_schemas.MetadataDatasetID, + ) + + await self._information_service.delete_dataset_information( + metadata_dataset_id=validated_payload.accession + ) + + +class OutboxSubTranslatorConfig(BaseSettings): + """Config for the outbox subscriber""" + + files_to_delete_topic: str = Field( + default=..., + description="The name of the topic for events informing about files to be deleted.", + examples=["file-deletions"], + ) + + +class InformationDeletionRequestedListener( + DaoSubscriberProtocol[event_schemas.FileDeletionRequested] +): + """A class that consumes FileDeletionRequested events.""" + + event_topic: str + dto_model = event_schemas.FileDeletionRequested + + def __init__( + self, + *, + config: OutboxSubTranslatorConfig, + information_service: InformationServicePort, + ): + self.event_topic = config.files_to_delete_topic + self.information_service = information_service + + async def changed( + self, resource_id: str, update: event_schemas.FileDeletionRequested + ) -> None: + """Consume change event for File Deletion Requests.""" + await self.information_service.deletion_requested(file_id=update.file_id) + + async def deleted(self, resource_id: str) -> None: + """Consume event indicating the deletion of a File Deletion Request.""" + log.warning( + "Received DELETED-type event for FileDeletionRequested with resource ID '%s'", + resource_id, + ) diff --git a/src/dins/adapters/inbound/fastapi_/__init__.py b/src/dins/adapters/inbound/fastapi_/__init__.py new file mode 100644 index 0000000..7596220 --- /dev/null +++ b/src/dins/adapters/inbound/fastapi_/__init__.py @@ -0,0 +1,15 @@ +# Copyright 2021 - 2024 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-based Adapter Code""" diff --git a/src/dins/adapters/inbound/fastapi_/configure.py b/src/dins/adapters/inbound/fastapi_/configure.py new file mode 100644 index 0000000..8919291 --- /dev/null +++ b/src/dins/adapters/inbound/fastapi_/configure.py @@ -0,0 +1,57 @@ +# Copyright 2021 - 2024 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 to customize openAPI script""" + +from typing import Any + +from fastapi import FastAPI +from fastapi.openapi.utils import get_openapi +from ghga_service_commons.api import ApiConfigBase, configure_app + +from dins import __version__ +from dins.adapters.inbound.fastapi_.routes import router +from dins.config import Config + +config = Config() + + +def get_openapi_schema(app: FastAPI) -> dict[str, Any]: + """Generates a custom openapi schema for the service""" + return get_openapi( + title="File Information Service", + version=__version__, + description="Providing public metadata about files registered with the Internal File Registry", + tags=[{"name": "FileInformationService"}], + routes=app.routes, + ) + + +def get_configured_app(*, config: ApiConfigBase) -> FastAPI: + """Create and configure a REST API application.""" + app = FastAPI() + app.include_router(router) + configure_app(app, config=config) + + def custom_openapi(): + if app.openapi_schema: + return app.openapi_schema + openapi_schema = get_openapi_schema(app) + app.openapi_schema = openapi_schema + return app.openapi_schema + + app.openapi = custom_openapi # type: ignore [method-assign] + + return app diff --git a/src/dins/adapters/inbound/fastapi_/dummies.py b/src/dins/adapters/inbound/fastapi_/dummies.py new file mode 100644 index 0000000..c07d359 --- /dev/null +++ b/src/dins/adapters/inbound/fastapi_/dummies.py @@ -0,0 +1,31 @@ +# Copyright 2021 - 2024 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 dependency dummies that are used in view definitions but need to be +replaced at runtime by actual dependencies. +""" + +from typing import Annotated + +from fastapi import Depends +from ghga_service_commons.api.di import DependencyDummy + +from dins.ports.inbound.information_service import InformationServicePort + +information_service_port = DependencyDummy("information_service") + +InformationServiceDummy = Annotated[ + InformationServicePort, Depends(information_service_port) +] diff --git a/src/dins/adapters/inbound/fastapi_/http_exceptions.py b/src/dins/adapters/inbound/fastapi_/http_exceptions.py new file mode 100644 index 0000000..a618d3a --- /dev/null +++ b/src/dins/adapters/inbound/fastapi_/http_exceptions.py @@ -0,0 +1,88 @@ +# Copyright 2021 - 2024 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 collextion of http exceptions.""" + +from ghga_service_commons.httpyexpect.server import HttpCustomExceptionBase +from pydantic import BaseModel + + +class HttpDatasetNotFoundError(HttpCustomExceptionBase): + """Raised when a file with given ID could not be found.""" + + exception_id = "datasetNotFound" + + class DataModel(BaseModel): + """Model for exception data""" + + file_id: str + + def __init__(self, *, dataset_id: str, status_code: int = 404): + """Construct message and init the exception.""" + super().__init__( + status_code=status_code, + description=( + f"Information for the dataset with ID { + dataset_id} is not registered." + ), + data={"dataset_id": dataset_id}, + ) + + +class HttpDatasetMissingInformationError(HttpCustomExceptionBase): + """Raised when information for one or more files in a dataset is missing.""" + + exception_id = "datasetInformationNotFound" + + class DataModel(BaseModel): + """Model for exception data""" + + dataset_id: str + missing_file_ids: list[str] + + def __init__( + self, *, dataset_id: str, missing_file_ids: list[str], status_code: int = 404 + ): + """Construct message and init the exception.""" + super().__init__( + status_code=status_code, + description=( + f"Not all information for the dataset with ID { + dataset_id} is registered." + ), + data={"dataset_id": dataset_id, "missing_file_ids": missing_file_ids}, + ) + + +class HttpInformationNotFoundError(HttpCustomExceptionBase): + """Raised when a file with given ID could not be found.""" + + exception_id = "informationNotFound" + + class DataModel(BaseModel): + """Model for exception data""" + + file_id: str + + def __init__(self, *, file_id: str, status_code: int = 404): + """Construct message and init the exception.""" + super().__init__( + status_code=status_code, + description=( + f"Information for the file with ID { + file_id} is not registered." + ), + data={"file_id": file_id}, + ) diff --git a/src/dins/adapters/inbound/fastapi_/http_responses.py b/src/dins/adapters/inbound/fastapi_/http_responses.py new file mode 100644 index 0000000..11c8a30 --- /dev/null +++ b/src/dins/adapters/inbound/fastapi_/http_responses.py @@ -0,0 +1,43 @@ +# Copyright 2021 - 2024 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 http responses.""" + +from fastapi.responses import JSONResponse + +from dins.core.models import DatasetInformation, FileInformation + + +class HttpDatasetInformationResponse(JSONResponse): + """Return relevant public information for the requested dataset.""" + + response_id = "datasetInformation" + + def __init__( + self, *, dataset_information: DatasetInformation, status_code: int = 200 + ): + """Construct message and init the response.""" + super().__init__( + content=dataset_information.model_dump(), status_code=status_code + ) + + +class HttpFileInformationResponse(JSONResponse): + """Return relevant public information for the requested file.""" + + response_id = "fileInformation" + + def __init__(self, *, file_information: FileInformation, status_code: int = 200): + """Construct message and init the response.""" + super().__init__(content=file_information.model_dump(), status_code=status_code) diff --git a/src/dins/adapters/inbound/fastapi_/routes.py b/src/dins/adapters/inbound/fastapi_/routes.py new file mode 100644 index 0000000..33a62f7 --- /dev/null +++ b/src/dins/adapters/inbound/fastapi_/routes.py @@ -0,0 +1,138 @@ +# Copyright 2021 - 2024 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 routes for S3 upload metadata ingest""" + +from typing import Annotated + +from fastapi import APIRouter, Depends, status + +from dins.adapters.inbound.fastapi_ import dummies, http_exceptions, http_responses +from dins.core import models +from dins.ports.inbound.information_service import InformationServicePort + +router = APIRouter() + +RESPONSES = { + "datasetInformation": { + "description": ( + "File information consisting of file id, sha256 checksum of the unencrypted " + "file content and file size of the unencrypted file in bytes for all files in a dataset.", + ), + "model": models.DatasetInformation, + }, + "datasetInformationNotFound": { + "description": ( + "Exceptions by ID:\n- datasetInformationNotFound: Information for one or more of the dataset files is not registered." + ), + "model": http_exceptions.HttpDatasetMissingInformationError.get_body_model(), + }, + "datasetNotFound": { + "description": ( + "Exceptions by ID:\n- datasetNotFound: No information registered for the given dataset ID." + ), + "model": http_exceptions.HttpDatasetNotFoundError.get_body_model(), + }, + "fileInformation": { + "description": ( + "File information consisting of file id, sha256 checksum of the unencrypted" + "file content and file size of the unencrypted file in bytes.", + ), + "model": models.FileInformation, + }, + "informationNotFound": { + "description": ( + "Exceptions by ID:\n- informationNotFound: No information registered for the given file ID." + ), + "model": http_exceptions.HttpInformationNotFoundError.get_body_model(), + }, +} + + +@router.get( + "/health", + summary="health", + tags=["FileInformationService"], + status_code=200, +) +async def health(): + """Used to test if this service is alive""" + return {"status": "OK"} + + +@router.get( + "/dataset_information/{dataset_id}", + summary="Return public file information for the given dataset id, i.e. public accession.", + operation_id="getDatasetInformation", + tags=["FileInformationService"], + status_code=status.HTTP_200_OK, + response_description="File information consisting of file id, sha256 checksum of the unencrypted file content and file size of the unencrypted file in bytes for all files in a dataset.", + responses={ + status.HTTP_200_OK: RESPONSES["datasetInformation"], + status.HTTP_404_NOT_FOUND: RESPONSES["datasetNotFound"] + | RESPONSES["datasetInformationNotFound"], + }, +) +async def get_dataset_information( + dataset_id: str, + information_service: Annotated[ + InformationServicePort, Depends(dummies.information_service_port) + ], +): + """Retrieve and serve stored dataset information.""" + try: + ( + found_information, + missing_file_ids, + ) = await information_service.batch_serve_information(dataset_id=dataset_id) + except information_service.DatasetMappingNotFoundError as error: + raise http_exceptions.HttpDatasetNotFoundError(dataset_id=dataset_id) from error + + if missing_file_ids: + # This should only happen if a dataset is already registered + # but not all files have hit the internal file registry yet + raise http_exceptions.HttpDatasetMissingInformationError( + dataset_id=dataset_id, missing_file_ids=missing_file_ids + ) + + return http_responses.DatasetInformation( + dataset_id=dataset_id, file_information=found_information + ) + + +@router.get( + "/file_information/{file_id}", + summary="Return public file information for the given file id, i.e. public accession.", + operation_id="getFileInformation", + tags=["FileInformationService"], + status_code=status.HTTP_200_OK, + response_description="File information consisting of file id, sha256 checksum of the unencrypted file content and file size of the unencrypted file in bytes.", + responses={ + status.HTTP_200_OK: RESPONSES["fileInformation"], + status.HTTP_404_NOT_FOUND: RESPONSES["informationNotFound"], + }, +) +async def get_file_information( + file_id: str, + information_service: Annotated[ + InformationServicePort, Depends(dummies.information_service_port) + ], +): + """Retrieve and serve stored file information.""" + try: + file_information = await information_service.serve_information(file_id=file_id) + except information_service.InformationNotFoundError as error: + raise http_exceptions.HttpInformationNotFoundError(file_id=file_id) from error + + return http_responses.HttpFileInformationResponse(file_information=file_information) diff --git a/src/dins/cli.py b/src/dins/cli.py new file mode 100644 index 0000000..56f8f99 --- /dev/null +++ b/src/dins/cli.py @@ -0,0 +1,51 @@ +# Copyright 2021 - 2024 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. +# +# Copyright 2021 - 2024 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 + +import typer + +from dins.main import consume_events, run_rest + +cli = typer.Typer() + + +@cli.command(name="run-rest") +def sync_run_api(): + """Run the HTTP REST API.""" + asyncio.run(run_rest()) + + +@cli.command(name="consume-events") +def sync_run_consume_events(): + """Consume pending events.""" + asyncio.run(consume_events()) diff --git a/src/dins/config.py b/src/dins/config.py new file mode 100644 index 0000000..5640e5d --- /dev/null +++ b/src/dins/config.py @@ -0,0 +1,43 @@ +# Copyright 2021 - 2024 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 hexkit.providers.mongokafka import MongoKafkaConfig + +from dins.adapters.inbound.event_sub import ( + EventSubTranslatorConfig, + OutboxSubTranslatorConfig, +) + +SERVICE_NAME = "dins" + + +@config_from_yaml(prefix=SERVICE_NAME) +class Config( + ApiConfigBase, + EventSubTranslatorConfig, + MongoKafkaConfig, + LoggingConfig, + OutboxSubTranslatorConfig, +): + """Config parameters and their defaults.""" + + service_name: str = SERVICE_NAME + + +CONFIG = Config() diff --git a/src/dins/core/__init__.py b/src/dins/core/__init__.py new file mode 100644 index 0000000..664c0a7 --- /dev/null +++ b/src/dins/core/__init__.py @@ -0,0 +1,19 @@ +# Copyright 2021 - 2024 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/dins/core/information_service.py b/src/dins/core/information_service.py new file mode 100644 index 0000000..13acf77 --- /dev/null +++ b/src/dins/core/information_service.py @@ -0,0 +1,153 @@ +# Copyright 2021 - 2024 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 logic for public file information storage, retrieval and deletion.""" + +import logging + +import ghga_event_schemas.pydantic_ as event_schemas +from hexkit.protocols.dao import ResourceNotFoundError + +from dins.adapters.inbound.dao import DatasetDaoPort, FileInformationDaoPort +from dins.core.models import DatasetFileIDs, FileInformation +from dins.ports.inbound.information_service import InformationServicePort + +log = logging.getLogger(__name__) + + +class InformationService(InformationServicePort): + """A service that handles storage and deletion of relevant metadata for files + registered with the Internal File Registry service. + """ + + def __init__( + self, + *, + dataset_dao: DatasetDaoPort, + file_information_dao: FileInformationDaoPort, + ): + self._dataset_dao = dataset_dao + self._file_information_dao = file_information_dao + + async def delete_dataset_information(self, dataset_id: str): + """Delete dataset to file ID mapping when the corresponding dataset is deleted.""" + try: + await self._dataset_dao.get_by_id(id_=dataset_id) + except ResourceNotFoundError: + log.info(f"Mapping for dataset with id {dataset_id} does not exist.") + return + + await self._dataset_dao.delete(id_=dataset_id) + log.info(f"Successfully deleted mapping for dataset with id { + dataset_id}.") + + async def deletion_requested(self, file_id: str): + """Handle deletion requests for information associated with the given file ID.""" + try: + await self._file_information_dao.get_by_id(id_=file_id) + except ResourceNotFoundError: + log.info(f"Information for file with id {file_id} does not exist.") + return + + await self._file_information_dao.delete(id_=file_id) + log.info(f"Successfully deleted entries for file with id {file_id}.") + + async def register_dataset_information( + self, metadata_dataset: event_schemas.MetadataDatasetOverview + ): + """Extract dataset to file ID mapping and store it.""" + dataset_id = metadata_dataset.accession + file_ids = [file.accession for file in metadata_dataset.files] + + dataset_mapping = DatasetFileIDs(dataset_id=dataset_id, file_ids=file_ids) + + # inverted logic due to raw pymongo exception exposed by hexkit + try: + existing_mapping = await self._dataset_dao.get_by_id(id_=dataset_id) + log.debug(f"Found existing information for dataset {dataset_id}") + # Only log if information to be inserted is a mismatch + if existing_mapping != dataset_mapping: + information_exists = self.MismatchingDatasetAlreadyRegistered( + dataset_id=dataset_id + ) + log.error(information_exists) + except ResourceNotFoundError: + await self._dataset_dao.insert(dataset_mapping) + log.debug(f"Successfully inserted file id mapping for dataset { + dataset_id}.") + + async def register_information( + self, file_registered: event_schemas.FileInternallyRegistered + ): + """Store information for a file newly registered with the Internal File Registry.""" + file_information = FileInformation( + file_id=file_registered.file_id, + size=file_registered.decrypted_size, + sha256_hash=file_registered.decrypted_sha256, + ) + file_id = file_information.file_id + + # inverted logic due to raw pymongo exception exposed by hexkit + try: + existing_information = await self._file_information_dao.get_by_id( + id_=file_id + ) + log.debug(f"Found existing information for file {file_id}") + # Only log if information to be inserted is a mismatch + if existing_information != file_information: + information_exists = self.MismatchingInformationAlreadyRegistered( + file_id=file_id + ) + log.error(information_exists) + except ResourceNotFoundError: + await self._file_information_dao.insert(file_information) + log.debug(f"Successfully inserted information for file {file_id} ") + + async def batch_serve_information( + self, dataset_id: str + ) -> tuple[list[FileInformation], list[str]]: + """Retrieve stored public information for the given dataset ID to be served by the API.""" + try: + dataset_mapping = await self._dataset_dao.get_by_id(dataset_id) + log.debug(f"Found mapping for dataset {dataset_id}.") + except ResourceNotFoundError as error: + dataset_mapping_not_found = self.DatasetMappingNotFoundError( + dataset_id=dataset_id + ) + log.warning(dataset_mapping_not_found) + raise dataset_mapping_not_found from error + + found_information = [] + missing_file_ids = [] + + for file_id in dataset_mapping.file_ids: + try: + file_information = await self.serve_information(file_id) + found_information.append(file_information) + except self.InformationNotFoundError: + missing_file_ids.append(file_id) + + return found_information, missing_file_ids + + async def serve_information(self, file_id: str) -> FileInformation: + """Retrieve stored public information for the given file ID to be served by the API.""" + try: + file_information = await self._file_information_dao.get_by_id(file_id) + log.debug(f"Foudn information for file {file_id}.") + except ResourceNotFoundError as error: + information_not_found = self.InformationNotFoundError(file_id=file_id) + log.warning(information_not_found) + raise information_not_found from error + + return file_information diff --git a/src/dins/core/models.py b/src/dins/core/models.py new file mode 100644 index 0000000..40a481a --- /dev/null +++ b/src/dins/core/models.py @@ -0,0 +1,53 @@ +# Copyright 2021 - 2024 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. +"""Models for internal representation""" + +from pydantic import BaseModel, Field, PositiveInt + + +class FileInformation(BaseModel): + """Public information container for files registered with the Internal File + Registry service. + """ + + file_id: str = Field( + ..., + description="Public identifier of the file associated with the given information", + ) + size: PositiveInt = Field(..., description="Size of the unencrypted file in bytes.") + sha256_hash: str = Field( + ..., + description="SHA256 hash of the unencrypted file content encoded as hexadecimal " + " values as produced by hashlib.hexdigest().", + ) + + +class DatasetFileIDs(BaseModel): + """Contains ID of a dataset and its contained files.""" + + dataset_id: str = Field(..., description="Public accesion of a dataset.") + file_ids: list[str] = Field( + ..., + description="Public accesions for all files included in the corresponding dataset.", + ) + + +class DatasetInformation(BaseModel): + """Container bundling public information for a dataset.""" + + dataset_id: str = Field(..., description="Public accession of a dataset.") + file_information: list[FileInformation] = Field( + ..., description="Public information on all files belonging to a dataset." + ) diff --git a/src/dins/inject.py b/src/dins/inject.py new file mode 100644 index 0000000..d5a25e2 --- /dev/null +++ b/src/dins/inject.py @@ -0,0 +1,130 @@ +# Copyright 2021 - 2024 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 hosting the dependency injection container.""" + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager + +from fastapi import FastAPI +from ghga_service_commons.utils.context import asyncnullcontext +from hexkit.providers.akafka import KafkaEventSubscriber, KafkaOutboxSubscriber +from hexkit.providers.mongodb import MongoDbDaoFactory + +from dins.adapters.inbound import dao +from dins.adapters.inbound.event_sub import ( + EventSubTranslator, + InformationDeletionRequestedListener, +) +from dins.adapters.inbound.fastapi_ import dummies +from dins.adapters.inbound.fastapi_.configure import get_configured_app +from dins.config import Config +from dins.core.information_service import InformationService +from dins.ports.inbound.information_service import InformationServicePort + + +@asynccontextmanager +async def prepare_core( + *, config: Config +) -> AsyncGenerator[InformationServicePort, None]: + """Constructs and initializes all core components and their outbound dependencies.""" + dao_factory = MongoDbDaoFactory(config=config) + dataset_dao = await dao.get_dataset_dao(dao_factory=dao_factory) + file_information_dao = await dao.get_file_information_dao(dao_factory=dao_factory) + + yield InformationService( + dataset_dao=dataset_dao, file_information_dao=file_information_dao + ) + + +def prepare_core_with_override( + *, + config: Config, + information_service_override: InformationServicePort | None = None, +): + """Resolve the prepare_core context manager based on config and override (if any).""" + return ( + asyncnullcontext(information_service_override) + if information_service_override + else prepare_core(config=config) + ) + + +@asynccontextmanager +async def prepare_event_subscriber( + *, + config: Config, + information_service_override: InformationServicePort | None = None, +) -> AsyncGenerator[KafkaEventSubscriber, None]: + """Construct and initialize an event subscriber with all its dependencies. + By default, the core dependencies are automatically prepared but you can also + provide them using the information_service_override parameter. + """ + async with prepare_core_with_override( + config=config, information_service_override=information_service_override + ) as information_service: + event_sub_translator = EventSubTranslator( + config=config, information_service=information_service + ) + async with KafkaEventSubscriber.construct( + config=config, translator=event_sub_translator + ) as event_subscriber: + yield event_subscriber + + +@asynccontextmanager +async def prepare_outbox_subscriber( + *, + config: Config, + information_service_override: InformationServicePort | None = None, +) -> AsyncGenerator[KafkaOutboxSubscriber, None]: + """Construct and initialize an event subscriber with all its dependencies. + By default, the core dependencies are automatically prepared but you can also + provide them using the information_service_override parameter. + """ + async with prepare_core_with_override( + config=config, information_service_override=information_service_override + ) as information_service: + outbox_translators = [ + InformationDeletionRequestedListener( + config=config, information_service=information_service + ) + ] + async with KafkaOutboxSubscriber.construct( + config=config, translators=outbox_translators + ) as kafka_outbox_subscriber: + yield kafka_outbox_subscriber + + +@asynccontextmanager +async def prepare_rest_app( + *, + config: Config, + information_service_override: InformationServicePort | None = None, +) -> AsyncGenerator[FastAPI, None]: + """Construct and initialize an REST API app along with all its dependencies. + By default, the core dependencies are automatically prepared but you can also + provide them using the information_service_override parameter. + """ + app = get_configured_app(config=config) + + async with prepare_core_with_override( + config=config, information_service_override=information_service_override + ) as information_service: + app.dependency_overrides[dummies.information_service_port] = ( + lambda: information_service + ) + yield app diff --git a/src/dins/main.py b/src/dins/main.py new file mode 100644 index 0000000..6875053 --- /dev/null +++ b/src/dins/main.py @@ -0,0 +1,51 @@ +# Copyright 2021 - 2024 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. +"""REST API configuration and function for CLI""" + +import asyncio + +from ghga_service_commons.api import run_server +from hexkit.log import configure_logging + +from dins.config import Config +from dins.inject import ( + prepare_event_subscriber, + prepare_outbox_subscriber, + prepare_rest_app, +) + + +async def run_rest(): + """Run the HTTP REST API.""" + config = Config() + configure_logging(config=config) + + async with prepare_rest_app(config=config) as app: + await run_server(app=app, config=config) + + +async def consume_events(run_forever: bool = True): + """Consume events for both the normal and outbox subscribers""" + config = Config() + configure_logging(config=config) + + async with ( + prepare_event_subscriber(config=config) as event_subscriber, + prepare_outbox_subscriber(config=config) as outbox_subscriber, + ): + await asyncio.gather( + outbox_subscriber.run(forever=run_forever), + event_subscriber.run(forever=run_forever), + ) diff --git a/src/dins/ports/__init__.py b/src/dins/ports/__init__.py new file mode 100644 index 0000000..149ce92 --- /dev/null +++ b/src/dins/ports/__init__.py @@ -0,0 +1,15 @@ +# Copyright 2021 - 2024 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. +"""Inbound and outbound hexagonal port definitions.""" diff --git a/src/dins/ports/inbound/__init__.py b/src/dins/ports/inbound/__init__.py new file mode 100644 index 0000000..c70a66f --- /dev/null +++ b/src/dins/ports/inbound/__init__.py @@ -0,0 +1,15 @@ +# Copyright 2021 - 2024 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. +"""Inbound hexagonal port definitions.""" diff --git a/src/dins/ports/inbound/dao.py b/src/dins/ports/inbound/dao.py new file mode 100644 index 0000000..abc838e --- /dev/null +++ b/src/dins/ports/inbound/dao.py @@ -0,0 +1,22 @@ +# Copyright 2021 - 2024 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. +"""DAO interfaces for database access.""" + +from hexkit.protocols.dao import DaoNaturalId + +from dins.core import models + +DatasetDaoPort = DaoNaturalId[models.DatasetFileIDs] +FileInformationDaoPort = DaoNaturalId[models.FileInformation] diff --git a/src/dins/ports/inbound/information_service.py b/src/dins/ports/inbound/information_service.py new file mode 100644 index 0000000..bbc8140 --- /dev/null +++ b/src/dins/ports/inbound/information_service.py @@ -0,0 +1,89 @@ +# Copyright 2021 - 2024 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 interfaces for public file information storage, retrieval and deletion.""" + +from abc import ABC, abstractmethod + +import ghga_event_schemas.pydantic_ as event_schemas + +from dins.core.models import FileInformation + + +class InformationServicePort(ABC): + """Abstract baseclass for a service that handles storage and deletion of relevant + metadata for files registered with the Internal File Registry service. + """ + + class MismatchingDatasetAlreadyRegistered(RuntimeError): + """Raised when the given file ID is already registered but the info doesn't match.""" + + def __init__(self, *, dataset_id: str): + message = f"Mismatching mapping has already been registered for the dataset with ID { + dataset_id} ." + super().__init__(message) + + class MismatchingInformationAlreadyRegistered(RuntimeError): + """Raised when the given file ID is already registered but the info doesn't match.""" + + def __init__(self, *, file_id: str): + message = f"Mismatching information for the file with ID { + file_id} has already been registered." + super().__init__(message) + + class DatasetMappingNotFoundError(RuntimeError): + """Raised when information for a given file ID is not registered.""" + + def __init__(self, *, dataset_id: str): + message = f"Mapping for the dataset with ID { + dataset_id} is not registered." + super().__init__(message) + + class InformationNotFoundError(RuntimeError): + """Raised when information for a given file ID is not registered.""" + + def __init__(self, *, file_id: str): + message = f"Information for the file with ID { + file_id} is not registered." + super().__init__(message) + + @abstractmethod + async def delete_dataset_information(self, metadata_dataset_id: str): + """Delete dataset to file ID mapping when the corresponding dataset is deleted.""" + + @abstractmethod + async def deletion_requested(self, file_id: str): + """Handle deletion requests for information associated with the given file ID.""" + + @abstractmethod + async def register_dataset_information( + self, metadata_dataset: event_schemas.MetadataDatasetOverview + ): + """Extract dataset to file ID mapping and store it.""" + + @abstractmethod + async def register_information( + self, file_registered: event_schemas.FileInternallyRegistered + ): + """Store information for a file newly registered with the Internal File Registry.""" + + @abstractmethod + async def batch_serve_information( + self, dataset_id: str + ) -> tuple[list[FileInformation], list[str]]: + """Retrieve stored public information for the given dataset ID to be served by the API.""" + + @abstractmethod + async def serve_information(self, file_id: str) -> FileInformation: + """Retrieve stored public information for the given file ID to be served by the API.""" diff --git a/src/dins/py.typed b/src/dins/py.typed new file mode 100644 index 0000000..d3245e7 --- /dev/null +++ b/src/dins/py.typed @@ -0,0 +1 @@ +# Marker file for PEP 561. This package uses inline types. diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..a047337 --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1,16 @@ +# Copyright 2021 - 2024 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 0000000..955d32d --- /dev/null +++ b/tests/fixtures/__init__.py @@ -0,0 +1,16 @@ +# Copyright 2021 - 2024 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/config.py b/tests/fixtures/config.py new file mode 100644 index 0000000..b59b001 --- /dev/null +++ b/tests/fixtures/config.py @@ -0,0 +1,41 @@ +# Copyright 2021 - 2024 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 config""" + +from pathlib import Path + +from pydantic_settings import BaseSettings + +from dins.config import Config +from tests.fixtures.utils import BASE_DIR + +TEST_CONFIG_YAML = BASE_DIR / "test_config.yaml" + + +def get_config( + sources: list[BaseSettings] | None = None, + default_config_yaml: Path = TEST_CONFIG_YAML, +) -> Config: + """Merges parameters from the default TEST_CONFIG_YAML with params inferred + from testcontainers. + """ + sources_dict: dict[str, object] = {} + + if sources is not None: + for source in sources: + sources_dict.update(**source.model_dump()) + + return Config(config_yaml=default_config_yaml, **sources_dict) diff --git a/tests/fixtures/joint.py b/tests/fixtures/joint.py new file mode 100644 index 0000000..2957fc5 --- /dev/null +++ b/tests/fixtures/joint.py @@ -0,0 +1,112 @@ +# Copyright 2021 - 2024 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. + +"""Join the functionality of all fixtures for API-level integration testing.""" + +__all__ = [ + "joint_fixture", + "JointFixture", + "kafka_container_fixture", + "kafka_fixture", + "mongodb_container_fixture", + "mongodb_fixture", +] +from collections.abc import AsyncGenerator +from dataclasses import dataclass + +import httpx +import pytest_asyncio +from ghga_service_commons.api.testing import AsyncTestClient +from hexkit.providers.akafka import KafkaEventSubscriber, KafkaOutboxSubscriber +from hexkit.providers.akafka.testutils import ( + KafkaFixture, + kafka_container_fixture, + kafka_fixture, +) +from hexkit.providers.mongodb import MongoDbDaoFactory +from hexkit.providers.mongodb.testutils import ( + MongoDbFixture, + mongodb_container_fixture, + mongodb_fixture, +) + +from dins.adapters.inbound.dao import get_file_information_dao +from dins.config import Config +from dins.inject import ( + prepare_core, + prepare_event_subscriber, + prepare_outbox_subscriber, + prepare_rest_app, +) +from dins.ports.inbound.dao import FileInformationDaoPort +from dins.ports.inbound.information_service import InformationServicePort +from tests.fixtures.config import get_config + + +@dataclass +class JointFixture: + """Returned by the `joint_fixture`.""" + + config: Config + information_service: InformationServicePort + file_information_dao: FileInformationDaoPort + rest_client: httpx.AsyncClient + event_subscriber: KafkaEventSubscriber + outbox_subscriber: KafkaOutboxSubscriber + mongodb: MongoDbFixture + kafka: KafkaFixture + + +@pytest_asyncio.fixture +async def joint_fixture( + mongodb: MongoDbFixture, + kafka: KafkaFixture, +) -> AsyncGenerator[JointFixture, None]: + """A fixture that embeds all other fixtures for API-level integration testing""" + config = get_config( + sources=[ + mongodb.config, + kafka.config, + ] + ) + + dao_factory = MongoDbDaoFactory(config=config) + file_information_dao = await get_file_information_dao(dao_factory=dao_factory) + + # prepare everything except the outbox subscriber + async with ( + prepare_core(config=config) as information_service, + prepare_rest_app( + config=config, information_service_override=information_service + ) as app, + prepare_event_subscriber( + config=config, information_service_override=information_service + ) as event_subscriber, + prepare_outbox_subscriber( + config=config, + information_service_override=information_service, + ) as outbox_subscriber, + AsyncTestClient(app=app) as rest_client, + ): + yield JointFixture( + config=config, + information_service=information_service, + file_information_dao=file_information_dao, + rest_client=rest_client, + event_subscriber=event_subscriber, + outbox_subscriber=outbox_subscriber, + mongodb=mongodb, + kafka=kafka, + ) diff --git a/tests/fixtures/test_config.yaml b/tests/fixtures/test_config.yaml new file mode 100644 index 0000000..3cc6acf --- /dev/null +++ b/tests/fixtures/test_config.yaml @@ -0,0 +1,14 @@ +service_name: fins + +service_instance_id: '1' +kafka_servers: ["kafka:9092"] + +db_connection_str: "mongodb://mongodb:27017" +db_name: "dev_db" + +dataset_change_event_topic: metadata_datasets +dataset_deletion_event_type: dataset_deleted +dataset_upsertion_event_type: dataset_created +files_to_delete_topic: file_deletions +file_registered_event_topic: internal_file_registry +file_registered_event_type: file_registered diff --git a/tests/fixtures/utils.py b/tests/fixtures/utils.py new file mode 100644 index 0000000..85997a9 --- /dev/null +++ b/tests/fixtures/utils.py @@ -0,0 +1,20 @@ +# Copyright 2021 - 2024 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. + +"""General testing utilities""" + +from pathlib import Path + +BASE_DIR = Path(__file__).parent.resolve() diff --git a/tests/test_typical_journey.py b/tests/test_typical_journey.py new file mode 100644 index 0000000..c06f634 --- /dev/null +++ b/tests/test_typical_journey.py @@ -0,0 +1,139 @@ +# Copyright 2021 - 2024 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. + +"""Tests typical user journeys""" + +import logging + +import ghga_event_schemas.pydantic_ as event_schemas +import pytest +from ghga_service_commons.utils.utc_dates import now_as_utc +from hexkit.protocols.dao import ResourceNotFoundError + +from dins.core import models +from tests.fixtures.joint import ( + JointFixture, + joint_fixture, # noqa: F401 + kafka_container_fixture, # noqa: F401 + kafka_fixture, # noqa: F401 + mongodb_container_fixture, # noqa: F401 + mongodb_fixture, # noqa: F401 +) + +FILE_ID = "test-file" +CHANGED_TYPE = "upserted" +DECRYPTED_SHA256 = "fake-checksum" +DECRYPTED_SIZE = 12345678 + +INCOMING_PAYLOAD_MOCK = event_schemas.FileInternallyRegistered( + s3_endpoint_alias="test-node", + file_id=FILE_ID, + object_id="test-object", + bucket_id="test-bucket", + upload_date=now_as_utc().isoformat(), + decrypted_size=DECRYPTED_SIZE, + decrypted_sha256=DECRYPTED_SHA256, + encrypted_part_size=1, + encrypted_parts_md5=["some", "checksum"], + encrypted_parts_sha256=["some", "checksum"], + content_offset=1234, + decryption_secret_id="some-secret", +) + +FILE_INFORMATION_MOCK = models.FileInformation( + file_id=FILE_ID, sha256_hash=DECRYPTED_SHA256, size=DECRYPTED_SIZE +) + +pytestmark = pytest.mark.asyncio() + + +async def test_normal_journey( + joint_fixture: JointFixture, # noqa: F811 + caplog, +): + """Simulates a typical, successful API journey.""" + # Test population path + file_id = INCOMING_PAYLOAD_MOCK.file_id + + await joint_fixture.kafka.publish_event( + payload=INCOMING_PAYLOAD_MOCK.model_dump(), + type_=joint_fixture.config.file_registered_event_type, + topic=joint_fixture.config.file_registered_event_topic, + ) + await joint_fixture.event_subscriber.run(forever=False) + + file_information = await joint_fixture.file_information_dao.get_by_id(file_id) + assert file_information == FILE_INFORMATION_MOCK + + # Test reregistration of identical content + expected_message = f"Found existing information for file {file_id}" + + caplog.clear() + with caplog.at_level(level=logging.DEBUG, logger="fins.core.information_service"): + await joint_fixture.kafka.publish_event( + payload=INCOMING_PAYLOAD_MOCK.model_dump(), + type_=joint_fixture.config.file_registered_event_type, + topic=joint_fixture.config.file_registered_event_topic, + ) + await joint_fixture.event_subscriber.run(forever=False) + assert len(caplog.messages) == 1 + assert expected_message in caplog.messages + + # Test reregistration of mismatching content + mismatch_message = f"Mismatching information for the file with ID { + file_id} has already been registered." + mismatch_mock = INCOMING_PAYLOAD_MOCK.model_copy( + update={"decrypted_sha256": "other-fake-checksum"} + ) + + caplog.clear() + with caplog.at_level(level=logging.DEBUG, logger="fins.core.information_service"): + await joint_fixture.kafka.publish_event( + payload=mismatch_mock.model_dump(), + type_=joint_fixture.config.file_registered_event_type, + topic=joint_fixture.config.file_registered_event_topic, + ) + await joint_fixture.event_subscriber.run(forever=False) + assert len(caplog.messages) == 2 + assert expected_message in caplog.messages + assert mismatch_message in caplog.messages + + # Test requesting existing file information + base_url = f"{joint_fixture.config.api_root_path}/file_information" + url = f"{base_url}/{file_id}" + response = await joint_fixture.rest_client.get(url) + assert response.status_code == 200 + assert models.FileInformation(**response.json()) == FILE_INFORMATION_MOCK + + # Test requesting invalid file information + url = f"{base_url}/invalid" + response = await joint_fixture.rest_client.get(url) + assert response.status_code == 404 + + # requst deletion + deletion_requested = event_schemas.FileDeletionRequested(file_id=file_id) + + await joint_fixture.kafka.publish_event( + payload=deletion_requested.model_dump(), + type_=CHANGED_TYPE, + topic=joint_fixture.config.files_to_delete_topic, + ) + await joint_fixture.outbox_subscriber.run(forever=False) + + # assert information is gone + with pytest.raises(ResourceNotFoundError): + file_information = await joint_fixture.file_information_dao.get_by_id( + id_=file_id + )