From 227067561f71591185918b8f3e5cfdda44f54f67 Mon Sep 17 00:00:00 2001 From: Developer Productivity Date: Thu, 7 Dec 2023 22:14:08 +0000 Subject: [PATCH] Initial commit with scaffolded files --- .circleci/config.yml | 20 + .dockerignore | 41 ++ .flake8 | 24 + .github/CODEOWNERS | 1 + .github/PULL_REQUEST_TEMPLATE.md | 25 + .gitignore | 200 ++++++ .python-version | 1 + DEVELOP.md | 168 +++++ MYPY_KNOWN_BAD | 0 Pipfile | 52 ++ Pipfile.lock | 1092 ++++++++++++++++++++++++++++++ README.md | 28 + api/swagger.json | 269 ++++++++ api/swagger.yaml | 178 +++++ app/__init__.py | 0 app/config.py | 20 + app/main.py | 31 + app/models/__init__.py | 0 app/models/check.py | 5 + app/models/unit.py | 8 + app/routes/__init__.py | 0 app/routes/base.py | 10 + app/routes/units.py | 40 ++ app/services/__init__.py | 0 app/services/database.py | 59 ++ build/docker/Dockerfile | 31 + domino.yml | 11 + helm/values.yaml | 2 + my_service/__init__.py | 0 pyproject.toml | 60 ++ renovate.json | 16 + scripts/check-mypy-known-good.sh | 7 + scripts/gen-api-docs.py | 41 ++ scripts/regen-mypy-known-bad.sh | 6 + sonar-project.properties | 4 + tests/__init__.py | 0 tests/fixtures/.gitkeep | 0 tests/unit/__init__.py | 0 tests/unit/app/__init__.py | 0 tests/unit/app/test_main.py | 71 ++ 40 files changed, 2521 insertions(+) create mode 100644 .circleci/config.yml create mode 100644 .dockerignore create mode 100644 .flake8 create mode 100644 .github/CODEOWNERS create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 .gitignore create mode 100644 .python-version create mode 100644 DEVELOP.md create mode 100644 MYPY_KNOWN_BAD create mode 100644 Pipfile create mode 100644 Pipfile.lock create mode 100644 README.md create mode 100644 api/swagger.json create mode 100644 api/swagger.yaml create mode 100644 app/__init__.py create mode 100644 app/config.py create mode 100644 app/main.py create mode 100644 app/models/__init__.py create mode 100644 app/models/check.py create mode 100644 app/models/unit.py create mode 100644 app/routes/__init__.py create mode 100644 app/routes/base.py create mode 100644 app/routes/units.py create mode 100644 app/services/__init__.py create mode 100644 app/services/database.py create mode 100644 build/docker/Dockerfile create mode 100644 domino.yml create mode 100644 helm/values.yaml create mode 100644 my_service/__init__.py create mode 100644 pyproject.toml create mode 100644 renovate.json create mode 100755 scripts/check-mypy-known-good.sh create mode 100755 scripts/gen-api-docs.py create mode 100755 scripts/regen-mypy-known-bad.sh create mode 100644 sonar-project.properties create mode 100644 tests/__init__.py create mode 100644 tests/fixtures/.gitkeep create mode 100644 tests/unit/__init__.py create mode 100644 tests/unit/app/__init__.py create mode 100644 tests/unit/app/test_main.py diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 00000000000..bf66777a9bd --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,20 @@ +version: 2.1 + +setup: true + +orbs: + domino: cerebrotech/domino@0.2 + +parameters: + workflow-type: + type: string + default: "main" + +workflows: + setup: + jobs: + - domino/setup: + context: + - Quay Updater + - workflow + workflow-type: << pipeline.parameters.workflow-type >> diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000000..76c233b3a26 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,41 @@ +**/__pycache__ +**/*.pyc +*.pyc +*.pyo +*.pyd +.Python +.tox +.coverage +.coveragerc +.coverage.* +.circleci +**/.cache +*,cover +*.log +.cache +.dockerignore +.git +.gitignore +.github +.idea +.isort.cfg +.flake8 +.pre-commit-config.yaml +.pytest_cache +.mypy_cache +.venv +__pycache__ +README.md +DEVELOP.md +Dockerfile +MYPY_KNOWN_BAD +build_cache* +coverage.xml +db.sqlite3 +env +pip-delete-this-directory.txt +pip-log.txt +renovate.json +tests +tmp +venv diff --git a/.flake8 b/.flake8 new file mode 100644 index 00000000000..a80f49382a4 --- /dev/null +++ b/.flake8 @@ -0,0 +1,24 @@ +[flake8] +max-complexity = 15 +# Default plus E266 - There should be only one leading # for a block comment +ignore = +# A line is less indented than it should be for hanging indents + E121, +# Closing brackets should match the same indentation level of the line that their opening bracket started on + E123, +# A continuation line is indented farther than it should be for a hanging indent + E126, +# There should be one space before and after an arithmetic operator (+, -, /, and *) + E226, +# Multiple statements of a function definition should be on their own separate lines + E704, +# Line breaks should occur after the binary operator to keep all variable names aligned + W503, +# Line breaks should occur before the binary operator to keep all operators aligned + W504, +# There should be only one leading # for a block comment + E266, +# Colons should not have any space before them + E203, +# Line lengths are recommended to be no greater than 79 characters + E501, diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 00000000000..5a5f382afb9 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @cerebrotech/train diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 00000000000..c0ea9b5594c --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,25 @@ +## PR Type +What kind of change does this PR introduce? + + +- [ ] Bugfix +- [ ] Feature +- [ ] Code style update (formatting) +- [ ] Refactoring (no functional changes) +- [ ] CI related changes +- [ ] Other... Please describe: + +## What is the current behavior? + + +Issue Number: https://dominodatalab.atlassian.net/browse/PLAT-XYZ + +## What is the new behavior? + +## Does this PR introduce a breaking change? +- [ ] Yes +- [ ] No + + + +## Other information \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000000..97f5ce6403b --- /dev/null +++ b/.gitignore @@ -0,0 +1,200 @@ +# macOS +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +# IDEA +.idea/ + +# VS Code +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +!.vscode/*.code-snippets + +# Local History for Visual Studio Code +.history/ + +# Built Visual Studio Code Extensions +*.vsix + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +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/ +cover/ + +# 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 +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .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 + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.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/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# ruff +.ruff_cache/ diff --git a/.python-version b/.python-version new file mode 100644 index 00000000000..2c0733315e4 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.11 diff --git a/DEVELOP.md b/DEVELOP.md new file mode 100644 index 00000000000..659d302f194 --- /dev/null +++ b/DEVELOP.md @@ -0,0 +1,168 @@ +# Development +Notes that can be useful for developing this microservice. + +--- + +#### Table of Contents +- [Local setup](#local-setup) +- [Adding Python Dependencies](#adding-python-dependencies) +- [Running the app locally](#running-the-app-locally) +- [Running unit tests](#running-unit-tests) +- [Building the Docker image and running it in a container](#building-the-docker-image-and-running-it-in-a-container) +- [Saving OpenAPI specs](#saving-openapi-specs) +- [Updating defaults from the repository template](#updating-defaults-from-the-repository-template) +- [Mypy](#mypy) +- [Renovate](#renovate) +- [Additional references](#additional-references) + +--- + +## Local setup +If you want to run things locally, you'll want to do the following steps. + +1. Install Python 3.11 with `pyenv` and use it for this repository +2. Install `pipenv` Python packages as the local build tool +3. Install default and development Python packages in your virtualenv, which should be handled by the build tool of choice + +**Example: pipenv** + +```bash +$ pip install pipenv +$ pipenv install --dev +Creating a virtualenv for this project... +Pipfile: /opt/cerebrotech/train-flyte-domino-agent-service/Pipfile +Using /Users/urianchang/.pyenv/versions/3.11.4/bin/python3 (3.11.4) to create virtualenv... +... +Virtualenv location: /opt/cerebrotech/train-flyte-domino-agent-service/.venv +... +To activate this project's virtualenv, run pipenv shell. +Alternatively, run a command inside the virtualenv with pipenv run. +``` + +## Adding Python dependencies + +Example: We want to add `tqdm` as a development dependency. + +```bash +# pipenv will update Pipfile and Pipfile.lock files +$ pipenv install tqdm==4.65.0 --dev +``` + +To uninstall the same dependency, we can run: + +```bash +# pipenv +$ pipenv uninstall tqdm --dev +``` + + +## Running the app locally +When running the app locally, you'll need to run it in an [ASGI](https://asgi.readthedocs.io/en/latest/) server program. +For this project, we use [`uvicorn`](https://www.uvicorn.org/). +For more information about running a FastAPI app manually, please see: https://fastapi.tiangolo.com/deployment/manually/. + +NOTE: The `--reload` option is fun to use in development, but is resource-intensive and shouldn't be used in production. + +```bash +# pipenv option +$ pipenv run uvicorn app.main:app --reload +Loading .env environment variables... +INFO: Will watch for changes in these directories: ['/opt/cerebrotech/train-flyte-domino-agent-service'] +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +INFO: Started reloader process [10831] using WatchFiles +INFO: Started server process [10859] +INFO: Waiting for application startup. +INFO: Application startup complete. + +You can interact with the app and view OpenAPI (Swagger) docs with a web browser at: http://127.0.0.1:8000/api/units/swagger. + + +## Running unit tests + +```bash +$ pipenv run pytest -v tests/ +Loading .env environment variables... +========================================================================================= test session starts ========================================================================================= +... +tests/unit/app/test_main.py::test_healthcheck PASSED [ 50%] +tests/unit/app/test_main.py::test_units_v1 PASSED [100%] + +========================================================================================== 2 passed in 0.27s ========================================================================================== +``` + +Pass `--cov=cov` to `pytest` generate a code coverage report. + +```bash +$ pipenv run pytest --cov=app -v tests/ +Loading .env environment variables... +========================================================================================= test session starts ========================================================================================= +... +tests/unit/app/test_main.py::test_healthcheck PASSED [ 50%] +tests/unit/app/test_main.py::test_units_v1 PASSED [100%] + +--------- coverage: platform darwin, python 3.10.11-final-0 ---------- +Name Stmts Miss Cover +---------------------------------------------- +app/__init__.py 0 0 100% +app/config.py 11 0 100% +app/main.py 8 0 100% +app/models/__init__.py 0 0 100% +app/models/check.py 3 0 100% +app/models/unit.py 6 0 100% +app/routes/__init__.py 0 0 100% +app/routes/base.py 6 0 100% +app/routes/units.py 27 3 89% +app/services/__init__.py 0 0 100% +app/services/database.py 30 6 80% +---------------------------------------------- +TOTAL 91 9 90% + + +========================================================================================== 2 passed in 0.43s ========================================================================================== +``` + + +## Building the Docker image and running it in a container + +```bash +# Build the Docker image +$ docker build -t train-flyte-domino-agent-service . + +# Run the Docker image in a container ("-d" runs the container in detached mode) +$ docker run -d --name train-flyte-domino-agent-service -p 80:80 train-flyte-domino-agent-service + +# Check that the container is running +$ docker ps +CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES +e38034be13f7 train-flyte-domino-agent-service "uvicorn app.main:ap…" 35 seconds ago Up 34 seconds 0.0.0.0:80->80/tcp train-flyte-domino-agent-service +``` + +With a web browser, you can view the OpenAPI (Swagger) docs served by the app: http://localhost/api/units/swagger + + +## Saving OpenAPI specs +One of the advantages of using the FastAPI framework is that it automatically generates OpenAPI specs. We want to save +these specifications, so other processes can use them without necessarily relying on the app to be running. + +A helper script is available to help generate and save these files into the `/api` directory: [gen-api-docs.py](scripts/gen-api-docs.py). +Run this script with the Python dependency tool of your choice. + +```bash +$ pipenv run python3 scripts/gen-api-docs.py +Loading .env environment variables... +Saving API schema to: api/swagger.json +Saving API schema to: api/swagger.yaml +``` + +## Mypy +[mypy](http://mypy-lang.org/) is used for checking Python code type correctness. For new projects, it is highly encouraged to properly type your code as it can reduce the number of errors in production. + +For existing projects, a `MYPY_KNOWN_BAD` file can be used to exclude untyped or improperly typed files. It will be checked against during CI to ensure correctness. It is also highly encouraged to target an empty `MYPY_KNOWN_BAD` file! + + +## Renovate +[Renovate](https://github.com/apps/renovate) can manage automated updates for your Pipfile and Dockerfile. There is a default configuration stored in `renovate.json`. The Renovate application is not enabled by default. Please create a help ticket with to [IT](https://dominodatalab.atlassian.net/servicedesk/customer/portal/6) to enable the application. + + +## Additional References +- [Creating a Python microservice using fastapi](https://dev.to/paurakhsharma/microservice-in-python-using-fastapi-24cc) diff --git a/MYPY_KNOWN_BAD b/MYPY_KNOWN_BAD new file mode 100644 index 00000000000..e69de29bb2d diff --git a/Pipfile b/Pipfile new file mode 100644 index 00000000000..126ba52c770 --- /dev/null +++ b/Pipfile @@ -0,0 +1,52 @@ +[requires] +python_version = "3.11" + +[[source]] +name = "pypi" +url = "https://pypi.org/simple" +verify_ssl = true + +[packages] +anyio = "==3.6.2" +certifi = "==2022.12.7" +click = "==8.1.3" +colorama = "==0.4.6" +dnspython = "==2.3.0" +email-validator = "==2.0.0.post2" +fastapi = {version = "==0.95.1", extras = ["all"]} +h11 = "==0.14.0" +httpcore = "==0.17.0" +httptools = "==0.5.0" +httpx = "==0.24.0" +idna = "==3.4" +itsdangerous = "==2.1.2" +jinja2 = "==3.1.2" +markupsafe = "==2.1.2" +orjson = "==3.8.11" +pydantic = "==1.10.7" +python-dotenv = "==1.0.0" +python-multipart = "==0.0.6" +pyyaml = "==6.0" +sniffio = "==1.3.0" +starlette = "==0.26.1" +typing-extensions = "==4.5.0" +ujson = "==5.7.0" +uvicorn = {version = "==0.22.0", extras = ["standard"]} +uvloop = "==0.17.0" +watchfiles = "==0.19.0" +websockets = "==11.0.2" +regex = "*" + +[dev-packages] +black = "==23.7.0" +coverage = "*" +flake8 = "*" +httpx = "==0.24.0" +isort = "*" +mypy = "*" +pytest = "==7.4.0" +pytest-cov = "==4.0.0" +pytest-subtests = "*" +ruff = "*" +types-requests = "==2.28.11.12" +types-pyyaml = "==6.0.12.9" diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 00000000000..10c960d23da --- /dev/null +++ b/Pipfile.lock @@ -0,0 +1,1092 @@ +{ + "_meta": { + "hash": { + "sha256": "e60ae543211eb6abed66bc1bd7901f4225172463ded3ade48915eaa2dadc579f" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.11" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "anyio": { + "hashes": [ + "sha256:25ea0d673ae30af41a0c442f81cf3b38c7e79fdc7b60335a4c14e05eb0947421", + "sha256:fbbe32bd270d2a2ef3ed1c5d45041250284e31fc0a4df4a5a6071842051a51e3" + ], + "index": "pypi", + "version": "==3.6.2" + }, + "certifi": { + "hashes": [ + "sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3", + "sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18" + ], + "index": "pypi", + "version": "==2022.12.7" + }, + "click": { + "hashes": [ + "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e", + "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48" + ], + "index": "pypi", + "version": "==8.1.3" + }, + "colorama": { + "hashes": [ + "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", + "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6" + ], + "index": "pypi", + "version": "==0.4.6" + }, + "dnspython": { + "hashes": [ + "sha256:224e32b03eb46be70e12ef6d64e0be123a64e621ab4c0822ff6d450d52a540b9", + "sha256:89141536394f909066cabd112e3e1a37e4e654db00a25308b0f130bc3152eb46" + ], + "index": "pypi", + "version": "==2.3.0" + }, + "email-validator": { + "hashes": [ + "sha256:1ff6e86044200c56ae23595695c54e9614f4a9551e0e393614f764860b3d7900", + "sha256:2466ba57cda361fb7309fd3d5a225723c788ca4bbad32a0ebd5373b99730285c" + ], + "index": "pypi", + "version": "==2.0.0.post2" + }, + "fastapi": { + "extras": [ + "all" + ], + "hashes": [ + "sha256:9569f0a381f8a457ec479d90fa01005cfddaae07546eb1f3fa035bc4797ae7d5", + "sha256:a870d443e5405982e1667dfe372663abf10754f246866056336d7f01c21dab07" + ], + "index": "pypi", + "markers": null, + "version": "==0.95.1" + }, + "h11": { + "hashes": [ + "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d", + "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761" + ], + "index": "pypi", + "version": "==0.14.0" + }, + "httpcore": { + "hashes": [ + "sha256:0fdfea45e94f0c9fd96eab9286077f9ff788dd186635ae61b312693e4d943599", + "sha256:cc045a3241afbf60ce056202301b4d8b6af08845e3294055eb26b09913ef903c" + ], + "index": "pypi", + "version": "==0.17.0" + }, + "httptools": { + "hashes": [ + "sha256:0297822cea9f90a38df29f48e40b42ac3d48a28637368f3ec6d15eebefd182f9", + "sha256:1af91b3650ce518d226466f30bbba5b6376dbd3ddb1b2be8b0658c6799dd450b", + "sha256:1f90cd6fd97c9a1b7fe9215e60c3bd97336742a0857f00a4cb31547bc22560c2", + "sha256:24bb4bb8ac3882f90aa95403a1cb48465de877e2d5298ad6ddcfdebec060787d", + "sha256:295874861c173f9101960bba332429bb77ed4dcd8cdf5cee9922eb00e4f6bc09", + "sha256:3625a55886257755cb15194efbf209584754e31d336e09e2ffe0685a76cb4b60", + "sha256:3a47a34f6015dd52c9eb629c0f5a8a5193e47bf2a12d9a3194d231eaf1bc451a", + "sha256:3cb8acf8f951363b617a8420768a9f249099b92e703c052f9a51b66342eea89b", + "sha256:4b098e4bb1174096a93f48f6193e7d9aa7071506a5877da09a783509ca5fff42", + "sha256:4d9ebac23d2de960726ce45f49d70eb5466725c0087a078866043dad115f850f", + "sha256:50d4613025f15f4b11f1c54bbed4761c0020f7f921b95143ad6d58c151198142", + "sha256:5230a99e724a1bdbbf236a1b58d6e8504b912b0552721c7c6b8570925ee0ccde", + "sha256:54465401dbbec9a6a42cf737627fb0f014d50dc7365a6b6cd57753f151a86ff0", + "sha256:550059885dc9c19a072ca6d6735739d879be3b5959ec218ba3e013fd2255a11b", + "sha256:557be7fbf2bfa4a2ec65192c254e151684545ebab45eca5d50477d562c40f986", + "sha256:5b65be160adcd9de7a7e6413a4966665756e263f0d5ddeffde277ffeee0576a5", + "sha256:64eba6f168803a7469866a9c9b5263a7463fa8b7a25b35e547492aa7322036b6", + "sha256:72ad589ba5e4a87e1d404cc1cb1b5780bfcb16e2aec957b88ce15fe879cc08ca", + "sha256:7d0c1044bce274ec6711f0770fd2d5544fe392591d204c68328e60a46f88843b", + "sha256:7e5eefc58d20e4c2da82c78d91b2906f1a947ef42bd668db05f4ab4201a99f49", + "sha256:850fec36c48df5a790aa735417dca8ce7d4b48d59b3ebd6f83e88a8125cde324", + "sha256:85b392aba273566c3d5596a0a490978c085b79700814fb22bfd537d381dd230c", + "sha256:8c2a56b6aad7cc8f5551d8e04ff5a319d203f9d870398b94702300de50190f63", + "sha256:8f470c79061599a126d74385623ff4744c4e0f4a0997a353a44923c0b561ee51", + "sha256:8ffce9d81c825ac1deaa13bc9694c0562e2840a48ba21cfc9f3b4c922c16f372", + "sha256:9423a2de923820c7e82e18980b937893f4aa8251c43684fa1772e341f6e06887", + "sha256:9b571b281a19762adb3f48a7731f6842f920fa71108aff9be49888320ac3e24d", + "sha256:a04fe458a4597aa559b79c7f48fe3dceabef0f69f562daf5c5e926b153817281", + "sha256:aa47ffcf70ba6f7848349b8a6f9b481ee0f7637931d91a9860a1838bfc586901", + "sha256:bede7ee075e54b9a5bde695b4fc8f569f30185891796b2e4e09e2226801d09bd", + "sha256:c1d2357f791b12d86faced7b5736dea9ef4f5ecdc6c3f253e445ee82da579449", + "sha256:c6eeefd4435055a8ebb6c5cc36111b8591c192c56a95b45fe2af22d9881eee25", + "sha256:ca1b7becf7d9d3ccdbb2f038f665c0f4857e08e1d8481cbcc1a86a0afcfb62b2", + "sha256:e67d4f8734f8054d2c4858570cc4b233bf753f56e85217de4dfb2495904cf02e", + "sha256:e8a34e4c0ab7b1ca17b8763613783e2458e77938092c18ac919420ab8655c8c1", + "sha256:e90491a4d77d0cb82e0e7a9cb35d86284c677402e4ce7ba6b448ccc7325c5421", + "sha256:ef1616b3ba965cd68e6f759eeb5d34fbf596a79e84215eeceebf34ba3f61fdc7", + "sha256:f222e1e9d3f13b68ff8a835574eda02e67277d51631d69d7cf7f8e07df678c86", + "sha256:f5e3088f4ed33947e16fd865b8200f9cfae1144f41b64a8cf19b599508e096bc", + "sha256:f659d7a48401158c59933904040085c200b4be631cb5f23a7d561fbae593ec1f", + "sha256:fe9c766a0c35b7e3d6b6939393c8dfdd5da3ac5dec7f971ec9134f284c6c36d6" + ], + "index": "pypi", + "version": "==0.5.0" + }, + "httpx": { + "hashes": [ + "sha256:447556b50c1921c351ea54b4fe79d91b724ed2b027462ab9a329465d147d5a4e", + "sha256:507d676fc3e26110d41df7d35ebd8b3b8585052450f4097401c9be59d928c63e" + ], + "index": "pypi", + "version": "==0.24.0" + }, + "idna": { + "hashes": [ + "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4", + "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2" + ], + "index": "pypi", + "version": "==3.4" + }, + "itsdangerous": { + "hashes": [ + "sha256:2c2349112351b88699d8d4b6b075022c0808887cb7ad10069318a8b0bc88db44", + "sha256:5dbbc68b317e5e42f327f9021763545dc3fc3bfe22e6deb96aaf1fc38874156a" + ], + "index": "pypi", + "version": "==2.1.2" + }, + "jinja2": { + "hashes": [ + "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852", + "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61" + ], + "index": "pypi", + "version": "==3.1.2" + }, + "markupsafe": { + "hashes": [ + "sha256:0576fe974b40a400449768941d5d0858cc624e3249dfd1e0c33674e5c7ca7aed", + "sha256:085fd3201e7b12809f9e6e9bc1e5c96a368c8523fad5afb02afe3c051ae4afcc", + "sha256:090376d812fb6ac5f171e5938e82e7f2d7adc2b629101cec0db8b267815c85e2", + "sha256:0b462104ba25f1ac006fdab8b6a01ebbfbce9ed37fd37fd4acd70c67c973e460", + "sha256:137678c63c977754abe9086a3ec011e8fd985ab90631145dfb9294ad09c102a7", + "sha256:1bea30e9bf331f3fef67e0a3877b2288593c98a21ccb2cf29b74c581a4eb3af0", + "sha256:22152d00bf4a9c7c83960521fc558f55a1adbc0631fbb00a9471e097b19d72e1", + "sha256:22731d79ed2eb25059ae3df1dfc9cb1546691cc41f4e3130fe6bfbc3ecbbecfa", + "sha256:2298c859cfc5463f1b64bd55cb3e602528db6fa0f3cfd568d3605c50678f8f03", + "sha256:28057e985dace2f478e042eaa15606c7efccb700797660629da387eb289b9323", + "sha256:2e7821bffe00aa6bd07a23913b7f4e01328c3d5cc0b40b36c0bd81d362faeb65", + "sha256:2ec4f2d48ae59bbb9d1f9d7efb9236ab81429a764dedca114f5fdabbc3788013", + "sha256:340bea174e9761308703ae988e982005aedf427de816d1afe98147668cc03036", + "sha256:40627dcf047dadb22cd25ea7ecfe9cbf3bbbad0482ee5920b582f3809c97654f", + "sha256:40dfd3fefbef579ee058f139733ac336312663c6706d1163b82b3003fb1925c4", + "sha256:4cf06cdc1dda95223e9d2d3c58d3b178aa5dacb35ee7e3bbac10e4e1faacb419", + "sha256:50c42830a633fa0cf9e7d27664637532791bfc31c731a87b202d2d8ac40c3ea2", + "sha256:55f44b440d491028addb3b88f72207d71eeebfb7b5dbf0643f7c023ae1fba619", + "sha256:608e7073dfa9e38a85d38474c082d4281f4ce276ac0010224eaba11e929dd53a", + "sha256:63ba06c9941e46fa389d389644e2d8225e0e3e5ebcc4ff1ea8506dce646f8c8a", + "sha256:65608c35bfb8a76763f37036547f7adfd09270fbdbf96608be2bead319728fcd", + "sha256:665a36ae6f8f20a4676b53224e33d456a6f5a72657d9c83c2aa00765072f31f7", + "sha256:6d6607f98fcf17e534162f0709aaad3ab7a96032723d8ac8750ffe17ae5a0666", + "sha256:7313ce6a199651c4ed9d7e4cfb4aa56fe923b1adf9af3b420ee14e6d9a73df65", + "sha256:7668b52e102d0ed87cb082380a7e2e1e78737ddecdde129acadb0eccc5423859", + "sha256:7df70907e00c970c60b9ef2938d894a9381f38e6b9db73c5be35e59d92e06625", + "sha256:7e007132af78ea9df29495dbf7b5824cb71648d7133cf7848a2a5dd00d36f9ff", + "sha256:835fb5e38fd89328e9c81067fd642b3593c33e1e17e2fdbf77f5676abb14a156", + "sha256:8bca7e26c1dd751236cfb0c6c72d4ad61d986e9a41bbf76cb445f69488b2a2bd", + "sha256:8db032bf0ce9022a8e41a22598eefc802314e81b879ae093f36ce9ddf39ab1ba", + "sha256:99625a92da8229df6d44335e6fcc558a5037dd0a760e11d84be2260e6f37002f", + "sha256:9cad97ab29dfc3f0249b483412c85c8ef4766d96cdf9dcf5a1e3caa3f3661cf1", + "sha256:a4abaec6ca3ad8660690236d11bfe28dfd707778e2442b45addd2f086d6ef094", + "sha256:a6e40afa7f45939ca356f348c8e23048e02cb109ced1eb8420961b2f40fb373a", + "sha256:a6f2fcca746e8d5910e18782f976489939d54a91f9411c32051b4aab2bd7c513", + "sha256:a806db027852538d2ad7555b203300173dd1b77ba116de92da9afbc3a3be3eed", + "sha256:abcabc8c2b26036d62d4c746381a6f7cf60aafcc653198ad678306986b09450d", + "sha256:b8526c6d437855442cdd3d87eede9c425c4445ea011ca38d937db299382e6fa3", + "sha256:bb06feb762bade6bf3c8b844462274db0c76acc95c52abe8dbed28ae3d44a147", + "sha256:c0a33bc9f02c2b17c3ea382f91b4db0e6cde90b63b296422a939886a7a80de1c", + "sha256:c4a549890a45f57f1ebf99c067a4ad0cb423a05544accaf2b065246827ed9603", + "sha256:ca244fa73f50a800cf8c3ebf7fd93149ec37f5cb9596aa8873ae2c1d23498601", + "sha256:cf877ab4ed6e302ec1d04952ca358b381a882fbd9d1b07cccbfd61783561f98a", + "sha256:d9d971ec1e79906046aa3ca266de79eac42f1dbf3612a05dc9368125952bd1a1", + "sha256:da25303d91526aac3672ee6d49a2f3db2d9502a4a60b55519feb1a4c7714e07d", + "sha256:e55e40ff0cc8cc5c07996915ad367fa47da6b3fc091fdadca7f5403239c5fec3", + "sha256:f03a532d7dee1bed20bc4884194a16160a2de9ffc6354b3878ec9682bb623c54", + "sha256:f1cd098434e83e656abf198f103a8207a8187c0fc110306691a2e94a78d0abb2", + "sha256:f2bfb563d0211ce16b63c7cb9395d2c682a23187f54c3d79bfec33e6705473c6", + "sha256:f8ffb705ffcf5ddd0e80b65ddf7bed7ee4f5a441ea7d3419e861a12eaf41af58" + ], + "index": "pypi", + "version": "==2.1.2" + }, + "orjson": { + "hashes": [ + "sha256:04b60dfc1251742e79bb075d7a7c4e37078b932a02e6f005c45761bd90c69189", + "sha256:08729e339ff3146e6de56c1166f014c3d2ec3e79ffb76d6c55d52cc892e5e477", + "sha256:0a53b3c02a38aadc5302661c2ca18645093971488992df77ce14fef16f598b2e", + "sha256:0bc3d1b93a73b46a698c054697eb2d27bdedbc5ea0d11ec5f1a6bfbec36346b5", + "sha256:0f9415b86ef154bf247fa78a6918aac50089c296e26fb6cf15bc9d7e6402a1f8", + "sha256:1103e597c16f82c241e1b02beadc9c91cecd93e60433ca73cb6464dcc235f37c", + "sha256:12f647d4da0aab1997e25bed4fa2b76782b5b9d2d1bf3066b5f0a57d34d833c4", + "sha256:173b8f8c750590f432757292cfb197582e5c14347b913b4017561d47af0e759b", + "sha256:176d742f53434541e50a5e659694073aa51dcbd8f29a1708a4fa1a320193c615", + "sha256:1e97fdbb779a3b8f5d9fc7dfddef5325f81ee45897eb7cb4638d5d9734d42514", + "sha256:1fedcc428416e23a6c9de62a000c22ae33bbe0108302ad5d5935e29ea739bf37", + "sha256:235926b38ed9b76ab2bca99ff26ece79c1c46bc10079b06e660b087aecffbe69", + "sha256:2fc050f8e7f2e4061c8c9968ad0be745b11b03913b77ffa8ceca65914696886c", + "sha256:3485c458670c0edb79ca149fe201f199dd9ccfe7ca3acbdef617e3c683e7b97f", + "sha256:358e515b8b19a275b259f5ee1e0efa2859b1d976b5ed5d016ac59f9e6c8788a3", + "sha256:37f38c8194ce086e6a9816b4b8dde5e7f383feeed92feec0385d99baf64f9b6e", + "sha256:382f15861a4bf447ab9d07106010e61b217ef6d4245c6cf64af0c12c4c5e2346", + "sha256:3afccf7f8684dca7f017837a315de0a1ab5c095de22a4eed206d079f9325ed72", + "sha256:3b65424ceee82b94e3613233b67ef110dc58f9d83b0076ec47a506289552a861", + "sha256:3c55065bc2075a5ea6ffb30462d84fd3aa5bbb7ae600855c325ee5753feec715", + "sha256:4118dcd2b5a27a22af5ad92414073f25d93bca1868f1f580056003c84841062f", + "sha256:553fdaf9f4b5060a0dcc517ae0c511c289c184a83d6719d03c5602ed0eef0390", + "sha256:58c068f93d701f9466f667bf3b5cb4e4946aee940df2b07ca5101f1cf1b60ce4", + "sha256:5c3b5405edc3a5f9e34516ee1a729f6c46aecf6de960ae07a7b3e95ebdd0e1d9", + "sha256:5ff10789cbc08a9fd94507c907ba55b9315e99f20345ff8ef34fac432dacd948", + "sha256:62eb8bdcf6f4cdbe12743e88ad98696277a75f91a35e8fb93a7ea2b9f4a7000c", + "sha256:66f0c9e4e8f6641497a7dc50591af3704b11468e9fc90cfb5874f28b0a61edb5", + "sha256:6d7b050135669d2335e40120215ad4120e29958c139f8bab68ce06a1cb1a1b2c", + "sha256:714c3e2be6ed7e4ff6e887926d6e171bfd94fdee76d7d3bfa74ee19237a2d49d", + "sha256:71a656f1c62e84c69060093e20cedff6a92e472d53ff5b8b9026b1b298542a68", + "sha256:7c7b4fae3b8fc69c8e76f1c0694f3decfe8a57f87e7ac7779ebb59cd71135438", + "sha256:7e4ded77ac7432a155d1d27a83bcadf722750aea3b9e6c4d47f2a92054ab71cb", + "sha256:882c77126c42dd93bb35288632d69b1e393863a2b752de3e5fe0112833609496", + "sha256:9486963d2e65482c565dacb366adb36d22aa22acf7274b61490244c3d87fa631", + "sha256:982ab319b7a5ece4199caf2a2b3a28e62a8e289cb6418548ef98bced7e2a6cfe", + "sha256:98befa717efaab7ddb847ebe47d473f6bd6f0cb53e98e6c3d487c7c58ba2e174", + "sha256:9fa900bdd84b4576c8dd6f3e2a00b35797f29283af328c6e3d70addfa4c2d599", + "sha256:b369019e597b59c4b97e9f925a3b725321fa1481c129d76c74c6ea3823f5d1e8", + "sha256:b68a07794834b7bd53ae2a8b4fe4bf010734cae3f0917d434c83b97acf8e5bce", + "sha256:bf48ed8d4b6ab9f23b7ee642462369d7133412d72824bad89f9bf4311c06c6a1", + "sha256:c2d3e6b65458ed71b6797f321d6e8bfeeadee9d3d31cac47806a608ea745edd7", + "sha256:c67ac094a4dde914297543af19f22532d7124f3a35245580d8b756c4ff2f5884", + "sha256:cdf201e77d3fac9d8d6f68d872ef45dccfe46f30b268bb88b6c5af5065b433aa", + "sha256:d47f97b99beb9bcac6e288a76b559543a61e0187443d8089204b757726b1d000", + "sha256:d70b6db9d4e1e6057829cd7fe119c217cebaf989f88d14b2445fa69fc568d03e", + "sha256:d7d5aecccfaf2052cd07ed5bec8efba9ddfea055682fcd346047b1a3e9da3034", + "sha256:e14903bfeb591a9117b7d40d81e3ebca9700b4e77bd829d6f22ea57941bb0ebf", + "sha256:ef52f1d5a2f89ef9049781c90ea35d5edf74374ed6ed515c286a706d1b290267", + "sha256:f2ef933da50b31c112b252be03d1ef59e0d0552c1a08e48295bd529ce42aaab8", + "sha256:f4e4a1001933166fd1c257b920b241b35322bef99ed7329338bf266ac053abe7", + "sha256:f7aeefac55848aeb29f20b91fa55f9e488f446201bb1bb31dc17480d113d8955" + ], + "index": "pypi", + "version": "==3.8.11" + }, + "pydantic": { + "hashes": [ + "sha256:01aea3a42c13f2602b7ecbbea484a98169fb568ebd9e247593ea05f01b884b2e", + "sha256:0cd181f1d0b1d00e2b705f1bf1ac7799a2d938cce3376b8007df62b29be3c2c6", + "sha256:10a86d8c8db68086f1e30a530f7d5f83eb0685e632e411dbbcf2d5c0150e8dcd", + "sha256:193924c563fae6ddcb71d3f06fa153866423ac1b793a47936656e806b64e24ca", + "sha256:464855a7ff7f2cc2cf537ecc421291b9132aa9c79aef44e917ad711b4a93163b", + "sha256:516f1ed9bc2406a0467dd777afc636c7091d71f214d5e413d64fef45174cfc7a", + "sha256:6434b49c0b03a51021ade5c4daa7d70c98f7a79e95b551201fff682fc1661245", + "sha256:64d34ab766fa056df49013bb6e79921a0265204c071984e75a09cbceacbbdd5d", + "sha256:670bb4683ad1e48b0ecb06f0cfe2178dcf74ff27921cdf1606e527d2617a81ee", + "sha256:68792151e174a4aa9e9fc1b4e653e65a354a2fa0fed169f7b3d09902ad2cb6f1", + "sha256:701daea9ffe9d26f97b52f1d157e0d4121644f0fcf80b443248434958fd03dc3", + "sha256:7d45fc99d64af9aaf7e308054a0067fdcd87ffe974f2442312372dfa66e1001d", + "sha256:80b1fab4deb08a8292d15e43a6edccdffa5377a36a4597bb545b93e79c5ff0a5", + "sha256:82dffb306dd20bd5268fd6379bc4bfe75242a9c2b79fec58e1041fbbdb1f7914", + "sha256:8c7f51861d73e8b9ddcb9916ae7ac39fb52761d9ea0df41128e81e2ba42886cd", + "sha256:950ce33857841f9a337ce07ddf46bc84e1c4946d2a3bba18f8280297157a3fd1", + "sha256:976cae77ba6a49d80f461fd8bba183ff7ba79f44aa5cfa82f1346b5626542f8e", + "sha256:9f6f0fd68d73257ad6685419478c5aece46432f4bdd8d32c7345f1986496171e", + "sha256:a7cd2251439988b413cb0a985c4ed82b6c6aac382dbaff53ae03c4b23a70e80a", + "sha256:abfb7d4a7cd5cc4e1d1887c43503a7c5dd608eadf8bc615413fc498d3e4645cd", + "sha256:ae150a63564929c675d7f2303008d88426a0add46efd76c3fc797cd71cb1b46f", + "sha256:b0f85904f73161817b80781cc150f8b906d521fa11e3cdabae19a581c3606209", + "sha256:b4a849d10f211389502059c33332e91327bc154acc1845f375a99eca3afa802d", + "sha256:c15582f9055fbc1bfe50266a19771bbbef33dd28c45e78afbe1996fd70966c2a", + "sha256:c230c0d8a322276d6e7b88c3f7ce885f9ed16e0910354510e0bae84d54991143", + "sha256:cc1dde4e50a5fc1336ee0581c1612215bc64ed6d28d2c7c6f25d2fe3e7c3e918", + "sha256:cf135c46099ff3f919d2150a948ce94b9ce545598ef2c6c7bf55dca98a304b52", + "sha256:cfc83c0678b6ba51b0532bea66860617c4cd4251ecf76e9846fa5a9f3454e97e", + "sha256:d2a5ebb48958754d386195fe9e9c5106f11275867051bf017a8059410e9abf1f", + "sha256:d71e69699498b020ea198468e2480a2f1e7433e32a3a99760058c6520e2bea7e", + "sha256:d75ae19d2a3dbb146b6f324031c24f8a3f52ff5d6a9f22f0683694b3afcb16fb", + "sha256:dfe2507b8ef209da71b6fb5f4e597b50c5a34b78d7e857c4f8f3115effaef5fe", + "sha256:e0cfe895a504c060e5d36b287ee696e2fdad02d89e0d895f83037245218a87fe", + "sha256:e79e999e539872e903767c417c897e729e015872040e56b96e67968c3b918b2d", + "sha256:ecbbc51391248116c0a055899e6c3e7ffbb11fb5e2a4cd6f2d0b93272118a209", + "sha256:f4a2b50e2b03d5776e7f21af73e2070e1b5c0d0df255a827e7c632962f8315af" + ], + "index": "pypi", + "version": "==1.10.7" + }, + "python-dotenv": { + "hashes": [ + "sha256:a8df96034aae6d2d50a4ebe8216326c61c3eb64836776504fcca410e5937a3ba", + "sha256:f5971a9226b701070a4bf2c38c89e5a3f0d64de8debda981d1db98583009122a" + ], + "index": "pypi", + "version": "==1.0.0" + }, + "python-multipart": { + "hashes": [ + "sha256:e9925a80bb668529f1b67c7fdb0a5dacdd7cbfc6fb0bff3ea443fe22bdd62132", + "sha256:ee698bab5ef148b0a760751c261902cd096e57e10558e11aca17646b74ee1c18" + ], + "index": "pypi", + "version": "==0.0.6" + }, + "pyyaml": { + "hashes": [ + "sha256:01b45c0191e6d66c470b6cf1b9531a771a83c1c4208272ead47a3ae4f2f603bf", + "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293", + "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b", + "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57", + "sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b", + "sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4", + "sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07", + "sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba", + "sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9", + "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287", + "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513", + "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0", + "sha256:432557aa2c09802be39460360ddffd48156e30721f5e8d917f01d31694216782", + "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0", + "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92", + "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f", + "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2", + "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc", + "sha256:81957921f441d50af23654aa6c5e5eaf9b06aba7f0a19c18a538dc7ef291c5a1", + "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c", + "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86", + "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4", + "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c", + "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34", + "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b", + "sha256:afa17f5bc4d1b10afd4466fd3a44dc0e245382deca5b3c353d8b757f9e3ecb8d", + "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c", + "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb", + "sha256:bfaef573a63ba8923503d27530362590ff4f576c626d86a9fed95822a8255fd7", + "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737", + "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3", + "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d", + "sha256:d4b0ba9512519522b118090257be113b9468d804b19d63c71dbcf4a48fa32358", + "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53", + "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78", + "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803", + "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a", + "sha256:dbad0e9d368bb989f4515da330b88a057617d16b6a8245084f1b05400f24609f", + "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174", + "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5" + ], + "index": "pypi", + "version": "==6.0" + }, + "regex": { + "hashes": [ + "sha256:0085da0f6c6393428bf0d9c08d8b1874d805bb55e17cb1dfa5ddb7cfb11140bf", + "sha256:06c57e14ac723b04458df5956cfb7e2d9caa6e9d353c0b4c7d5d54fcb1325c46", + "sha256:09b7f4c66aa9d1522b06e31a54f15581c37286237208df1345108fcf4e050c18", + "sha256:0c59122ceccb905a941fb23b087b8eafc5290bf983ebcb14d2301febcbe199c7", + "sha256:1005c60ed7037be0d9dea1f9c53cc42f836188227366370867222bda4c3c6bd7", + "sha256:14898830f0a0eb67cae2bbbc787c1a7d6e34ecc06fbd39d3af5fe29a4468e2c9", + "sha256:14dc6f2d88192a67d708341f3085df6a4f5a0c7b03dec08d763ca2cd86e9f559", + "sha256:1e7d84d64c84ad97bf06f3c8cb5e48941f135ace28f450d86af6b6512f1c9a71", + "sha256:2162ae2eb8b079622176a81b65d486ba50b888271302190870b8cc488587d280", + "sha256:22283c769a7b01c8ac355d5be0715bf6929b6267619505e289f792b01304d898", + "sha256:239c3c2a339d3b3ddd51c2daef10874410917cd2b998f043c13e2084cb191684", + "sha256:293352710172239bf579c90a9864d0df57340b6fd21272345222fb6371bf82b3", + "sha256:2ae54a338191e1356253e7883d9d19f8679b6143703086245fb14d1f20196be9", + "sha256:2e73e5243af12d9cd6a9d6a45a43570dbe2e5b1cdfc862f5ae2b031e44dd95a8", + "sha256:2e9216e0d2cdce7dbc9be48cb3eacb962740a09b011a116fd7af8c832ab116ca", + "sha256:3026cbcf11d79095a32d9a13bbc572a458727bd5b1ca332df4a79faecd45281c", + "sha256:3611576aff55918af2697410ff0293d6071b7e00f4b09e005d614686ac4cd57c", + "sha256:3ae646c35cb9f820491760ac62c25b6d6b496757fda2d51be429e0e7b67ae0ab", + "sha256:3b8e6ea6be6d64104d8e9afc34c151926f8182f84e7ac290a93925c0db004bfd", + "sha256:3d370ff652323c5307d9c8e4c62efd1956fb08051b0e9210212bc51168b4ff56", + "sha256:3f7454aa427b8ab9101f3787eb178057c5250478e39b99540cfc2b889c7d0586", + "sha256:40f029d73b10fac448c73d6eb33d57b34607f40116e9f6e9f0d32e9229b147d7", + "sha256:423adfa872b4908843ac3e7a30f957f5d5282944b81ca0a3b8a7ccbbfaa06103", + "sha256:4873ef92e03a4309b3ccd8281454801b291b689f6ad45ef8c3658b6fa761d7ac", + "sha256:48c640b99213643d141550326f34f0502fedb1798adb3c9eb79650b1ecb2f177", + "sha256:4ae594c66f4a7e1ea67232a0846649a7c94c188d6c071ac0210c3e86a5f92109", + "sha256:4b694430b3f00eb02c594ff5a16db30e054c1b9589a043fe9174584c6efa8033", + "sha256:51d8ea2a3a1a8fe4f67de21b8b93757005213e8ac3917567872f2865185fa7fb", + "sha256:54de2619f5ea58474f2ac211ceea6b615af2d7e4306220d4f3fe690c91988a61", + "sha256:551ad543fa19e94943c5b2cebc54c73353ffff08228ee5f3376bd27b3d5b9800", + "sha256:5543c055d8ec7801901e1193a51570643d6a6ab8751b1f7dd9af71af467538bb", + "sha256:5cd9cd7170459b9223c5e592ac036e0704bee765706445c353d96f2890e816c8", + "sha256:5ec4b3f0aebbbe2fc0134ee30a791af522a92ad9f164858805a77442d7d18570", + "sha256:67ecd894e56a0c6108ec5ab1d8fa8418ec0cff45844a855966b875d1039a2e34", + "sha256:6ab2ed84bf0137927846b37e882745a827458689eb969028af8032b1b3dac78e", + "sha256:704f63b774218207b8ccc6c47fcef5340741e5d839d11d606f70af93ee78e4d4", + "sha256:7098c524ba9f20717a56a8d551d2ed491ea89cbf37e540759ed3b776a4f8d6eb", + "sha256:7aed90a72fc3654fba9bc4b7f851571dcc368120432ad68b226bd593f3f6c0b7", + "sha256:7ce606c14bb195b0e5108544b540e2c5faed6843367e4ab3deb5c6aa5e681208", + "sha256:7eb95fe8222932c10d4436e7a6f7c99991e3fdd9f36c949eff16a69246dee2dc", + "sha256:80b80b889cb767cc47f31d2b2f3dec2db8126fbcd0cff31b3925b4dc6609dcdb", + "sha256:82cd0a69cd28f6cc3789cc6adeb1027f79526b1ab50b1f6062bbc3a0ccb2dbc3", + "sha256:83215147121e15d5f3a45d99abeed9cf1fe16869d5c233b08c56cdf75f43a504", + "sha256:88900f521c645f784260a8d346e12a1590f79e96403971241e64c3a265c8ecdb", + "sha256:91129ff1bb0619bc1f4ad19485718cc623a2dc433dff95baadbf89405c7f6b57", + "sha256:920974009fb37b20d32afcdf0227a2e707eb83fe418713f7a8b7de038b870d0b", + "sha256:9233ac249b354c54146e392e8a451e465dd2d967fc773690811d3a8c240ac601", + "sha256:941460db8fe3bd613db52f05259c9336f5a47ccae7d7def44cc277184030a116", + "sha256:942f8b1f3b223638b02df7df79140646c03938d488fbfb771824f3d05fc083a8", + "sha256:964b16dcc10c79a4a2be9f1273fcc2684a9eedb3906439720598029a797b46e6", + "sha256:9691a549c19c22d26a4f3b948071e93517bdf86e41b81d8c6ac8a964bb71e5a6", + "sha256:96979d753b1dc3b2169003e1854dc67bfc86edf93c01e84757927f810b8c3c93", + "sha256:987b9ac04d0b38ef4f89fbc035e84a7efad9cdd5f1e29024f9289182c8d99e09", + "sha256:988631b9d78b546e284478c2ec15c8a85960e262e247b35ca5eaf7ee22f6050a", + "sha256:9a96edd79661e93327cfeac4edec72a4046e14550a1d22aa0dd2e3ca52aec921", + "sha256:9b7408511fca48a82a119d78a77c2f5eb1b22fe88b0d2450ed0756d194fe7a9a", + "sha256:9dd6082f4e2aec9b6a0927202c85bc1b09dcab113f97265127c1dc20e2e32495", + "sha256:a2ad5add903eb7cdde2b7c64aaca405f3957ab34f16594d2b78d53b8b1a6a7d6", + "sha256:a8c65c17aed7e15a0c824cdc63a6b104dfc530f6fa8cb6ac51c437af52b481c7", + "sha256:aadf28046e77a72f30dcc1ab185639e8de7f4104b8cb5c6dfa5d8ed860e57236", + "sha256:b076da1ed19dc37788f6a934c60adf97bd02c7eea461b73730513921a85d4235", + "sha256:b2aeab3895d778155054abea5238d0eb9a72e9242bd4b43f42fd911ef9a13470", + "sha256:b82edc98d107cbc7357da7a5a695901b47d6eb0420e587256ba3ad24b80b7d0b", + "sha256:b8a0ccc8f2698f120e9e5742f4b38dc944c38744d4bdfc427616f3a163dd9de5", + "sha256:b993b6f524d1e274a5062488a43e3f9f8764ee9745ccd8e8193df743dbe5ee61", + "sha256:bb34d1605f96a245fc39790a117ac1bac8de84ab7691637b26ab2c5efb8f228c", + "sha256:bd3366aceedf274f765a3a4bc95d6cd97b130d1dda524d8f25225d14123c01db", + "sha256:c12f6f67495ea05c3d542d119d270007090bad5b843f642d418eb601ec0fa7be", + "sha256:c662a4cbdd6280ee56f841f14620787215a171c4e2d1744c9528bed8f5816c96", + "sha256:c884d1a59e69e03b93cf0dfee8794c63d7de0ee8f7ffb76e5f75be8131b6400a", + "sha256:ca339088839582d01654e6f83a637a4b8194d0960477b9769d2ff2cfa0fa36d2", + "sha256:cd2b6c5dfe0929b6c23dde9624483380b170b6e34ed79054ad131b20203a1a63", + "sha256:ce0f9fbe7d295f9922c0424a3637b88c6c472b75eafeaff6f910494a1fa719ef", + "sha256:cf0633e4a1b667bfe0bb10b5e53fe0d5f34a6243ea2530eb342491f1adf4f739", + "sha256:cf9273e96f3ee2ac89ffcb17627a78f78e7516b08f94dc435844ae72576a276e", + "sha256:d909b5a3fff619dc7e48b6b1bedc2f30ec43033ba7af32f936c10839e81b9217", + "sha256:d9b6627408021452dcd0d2cdf8da0534e19d93d070bfa8b6b4176f99711e7f90", + "sha256:de35342190deb7b866ad6ba5cbcccb2d22c0487ee0cbb251efef0843d705f0d4", + "sha256:e51c80c168074faa793685656c38eb7a06cbad7774c8cbc3ea05552d615393d8", + "sha256:e6bd1e9b95bc5614a7a9c9c44fde9539cba1c823b43a9f7bc11266446dd568e3", + "sha256:e7a9aaa5a1267125eef22cef3b63484c3241aaec6f48949b366d26c7250e0357", + "sha256:e951d1a8e9963ea51efd7f150450803e3b95db5939f994ad3d5edac2b6f6e2b4", + "sha256:e9941a4ada58f6218694f382e43fdd256e97615db9da135e77359da257a7168b", + "sha256:f0640913d2c1044d97e30d7c41728195fc37e54d190c5385eacb52115127b882", + "sha256:f0ccf3e01afeb412a1a9993049cb160d0352dba635bbca7762b2dc722aa5742a", + "sha256:f2181c20ef18747d5f4a7ea513e09ea03bdd50884a11ce46066bb90fe4213675", + "sha256:f2200e00b62568cfd920127782c61bc1c546062a879cdc741cfcc6976668dfcf", + "sha256:fcbdc5f2b0f1cd0f6a56cdb46fe41d2cce1e644e3b68832f3eeebc5fb0f7712e" + ], + "index": "pypi", + "version": "==2023.8.8" + }, + "sniffio": { + "hashes": [ + "sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101", + "sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384" + ], + "index": "pypi", + "version": "==1.3.0" + }, + "starlette": { + "hashes": [ + "sha256:41da799057ea8620e4667a3e69a5b1923ebd32b1819c8fa75634bbe8d8bea9bd", + "sha256:e87fce5d7cbdde34b76f0ac69013fd9d190d581d80681493016666e6f96c6d5e" + ], + "index": "pypi", + "version": "==0.26.1" + }, + "typing-extensions": { + "hashes": [ + "sha256:5cb5f4a79139d699607b3ef622a1dedafa84e115ab0024e0d9c044a9479ca7cb", + "sha256:fb33085c39dd998ac16d1431ebc293a8b3eedd00fd4a32de0ff79002c19511b4" + ], + "index": "pypi", + "version": "==4.5.0" + }, + "ujson": { + "hashes": [ + "sha256:00343501dbaa5172e78ef0e37f9ebd08040110e11c12420ff7c1f9f0332d939e", + "sha256:0e4e8981c6e7e9e637e637ad8ffe948a09e5434bc5f52ecbb82b4b4cfc092bfb", + "sha256:0ee295761e1c6c30400641f0a20d381633d7622633cdf83a194f3c876a0e4b7e", + "sha256:137831d8a0db302fb6828ee21c67ad63ac537bddc4376e1aab1c8573756ee21c", + "sha256:14f9082669f90e18e64792b3fd0bf19f2b15e7fe467534a35ea4b53f3bf4b755", + "sha256:16b2254a77b310f118717715259a196662baa6b1f63b1a642d12ab1ff998c3d7", + "sha256:18679484e3bf9926342b1c43a3bd640f93a9eeeba19ef3d21993af7b0c44785d", + "sha256:24ad1aa7fc4e4caa41d3d343512ce68e41411fb92adf7f434a4d4b3749dc8f58", + "sha256:26c2b32b489c393106e9cb68d0a02e1a7b9d05a07429d875c46b94ee8405bdb7", + "sha256:2f242eec917bafdc3f73a1021617db85f9958df80f267db69c76d766058f7b19", + "sha256:341f891d45dd3814d31764626c55d7ab3fd21af61fbc99d070e9c10c1190680b", + "sha256:35209cb2c13fcb9d76d249286105b4897b75a5e7f0efb0c0f4b90f222ce48910", + "sha256:3d3b3499c55911f70d4e074c626acdb79a56f54262c3c83325ffb210fb03e44d", + "sha256:4a3d794afbf134df3056a813e5c8a935208cddeae975bd4bc0ef7e89c52f0ce0", + "sha256:4c592eb91a5968058a561d358d0fef59099ed152cfb3e1cd14eee51a7a93879e", + "sha256:4ee997799a23227e2319a3f8817ce0b058923dbd31904761b788dc8f53bd3e30", + "sha256:523ee146cdb2122bbd827f4dcc2a8e66607b3f665186bce9e4f78c9710b6d8ab", + "sha256:54384ce4920a6d35fa9ea8e580bc6d359e3eb961fa7e43f46c78e3ed162d56ff", + "sha256:5593263a7fcfb934107444bcfba9dde8145b282de0ee9f61e285e59a916dda0f", + "sha256:581c945b811a3d67c27566539bfcb9705ea09cb27c4be0002f7a553c8886b817", + "sha256:5eba5e69e4361ac3a311cf44fa71bc619361b6e0626768a494771aacd1c2f09b", + "sha256:6411aea4c94a8e93c2baac096fbf697af35ba2b2ed410b8b360b3c0957a952d3", + "sha256:64772a53f3c4b6122ed930ae145184ebaed38534c60f3d859d8c3f00911eb122", + "sha256:67a19fd8e7d8cc58a169bea99fed5666023adf707a536d8f7b0a3c51dd498abf", + "sha256:6abb8e6d8f1ae72f0ed18287245f5b6d40094e2656d1eab6d99d666361514074", + "sha256:6e80f0d03e7e8646fc3d79ed2d875cebd4c83846e129737fdc4c2532dbd43d9e", + "sha256:6faf46fa100b2b89e4db47206cf8a1ffb41542cdd34dde615b2fc2288954f194", + "sha256:7312731c7826e6c99cdd3ac503cd9acd300598e7a80bcf41f604fee5f49f566c", + "sha256:75204a1dd7ec6158c8db85a2f14a68d2143503f4bafb9a00b63fe09d35762a5e", + "sha256:7592f40175c723c032cdbe9fe5165b3b5903604f774ab0849363386e99e1f253", + "sha256:7b9dc5a90e2149643df7f23634fe202fed5ebc787a2a1be95cf23632b4d90651", + "sha256:7df3fd35ebc14dafeea031038a99232b32f53fa4c3ecddb8bed132a43eefb8ad", + "sha256:800bf998e78dae655008dd10b22ca8dc93bdcfcc82f620d754a411592da4bbf2", + "sha256:8b4257307e3662aa65e2644a277ca68783c5d51190ed9c49efebdd3cbfd5fa44", + "sha256:90712dfc775b2c7a07d4d8e059dd58636bd6ff1776d79857776152e693bddea6", + "sha256:9b0f2680ce8a70f77f5d70aaf3f013d53e6af6d7058727a35d8ceb4a71cdd4e9", + "sha256:a5d2f44331cf04689eafac7a6596c71d6657967c07ac700b0ae1c921178645da", + "sha256:aae4d9e1b4c7b61780f0a006c897a4a1904f862fdab1abb3ea8f45bd11aa58f3", + "sha256:adf445a49d9a97a5a4c9bb1d652a1528de09dd1c48b29f79f3d66cea9f826bf6", + "sha256:af4639f684f425177d09ae409c07602c4096a6287027469157bfb6f83e01448b", + "sha256:afff311e9f065a8f03c3753db7011bae7beb73a66189c7ea5fcb0456b7041ea4", + "sha256:b01a9af52a0d5c46b2c68e3f258fdef2eacaa0ce6ae3e9eb97983f5b1166edb6", + "sha256:b522be14a28e6ac1cf818599aeff1004a28b42df4ed4d7bc819887b9dac915fc", + "sha256:b5ac3d5c5825e30b438ea92845380e812a476d6c2a1872b76026f2e9d8060fc2", + "sha256:b6a6961fc48821d84b1198a09516e396d56551e910d489692126e90bf4887d29", + "sha256:b7316d3edeba8a403686cdcad4af737b8415493101e7462a70ff73dd0609eafc", + "sha256:b738282e12a05f400b291966630a98d622da0938caa4bc93cf65adb5f4281c60", + "sha256:bab10165db6a7994e67001733f7f2caf3400b3e11538409d8756bc9b1c64f7e8", + "sha256:bea8d30e362180aafecabbdcbe0e1f0b32c9fa9e39c38e4af037b9d3ca36f50c", + "sha256:c0d1f7c3908357ee100aa64c4d1cf91edf99c40ac0069422a4fd5fd23b263263", + "sha256:c3af9f9f22a67a8c9466a32115d9073c72a33ae627b11de6f592df0ee09b98b6", + "sha256:c96e3b872bf883090ddf32cc41957edf819c5336ab0007d0cf3854e61841726d", + "sha256:cd90027e6d93e8982f7d0d23acf88c896d18deff1903dd96140613389b25c0dd", + "sha256:d2e43ccdba1cb5c6d3448eadf6fc0dae7be6c77e357a3abc968d1b44e265866d", + "sha256:d36a807a24c7d44f71686685ae6fbc8793d784bca1adf4c89f5f780b835b6243", + "sha256:d7ff6ebb43bc81b057724e89550b13c9a30eda0f29c2f506f8b009895438f5a6", + "sha256:d8cd622c069368d5074bd93817b31bdb02f8d818e57c29e206f10a1f9c6337dd", + "sha256:dda9aa4c33435147262cd2ea87c6b7a1ca83ba9b3933ff7df34e69fee9fced0c", + "sha256:e788e5d5dcae8f6118ac9b45d0b891a0d55f7ac480eddcb7f07263f2bcf37b23", + "sha256:e87cec407ec004cf1b04c0ed7219a68c12860123dfb8902ef880d3d87a71c172", + "sha256:ea7423d8a2f9e160c5e011119741682414c5b8dce4ae56590a966316a07a4618", + "sha256:ed22f9665327a981f288a4f758a432824dc0314e4195a0eaeb0da56a477da94d", + "sha256:ed24406454bb5a31df18f0a423ae14beb27b28cdfa34f6268e7ebddf23da807e", + "sha256:f7f241488879d91a136b299e0c4ce091996c684a53775e63bb442d1a8e9ae22a", + "sha256:ff0004c3f5a9a6574689a553d1b7819d1a496b4f005a7451f339dc2d9f4cf98c" + ], + "index": "pypi", + "version": "==5.7.0" + }, + "uvicorn": { + "extras": [ + "standard" + ], + "hashes": [ + "sha256:79277ae03db57ce7d9aa0567830bbb51d7a612f54d6e1e3e92da3ef24c2c8ed8", + "sha256:e9434d3bbf05f310e762147f769c9f21235ee118ba2d2bf1155a7196448bd996" + ], + "index": "pypi", + "markers": null, + "version": "==0.22.0" + }, + "uvloop": { + "hashes": [ + "sha256:0949caf774b9fcefc7c5756bacbbbd3fc4c05a6b7eebc7c7ad6f825b23998d6d", + "sha256:0ddf6baf9cf11a1a22c71487f39f15b2cf78eb5bde7e5b45fbb99e8a9d91b9e1", + "sha256:1436c8673c1563422213ac6907789ecb2b070f5939b9cbff9ef7113f2b531595", + "sha256:23609ca361a7fc587031429fa25ad2ed7242941adec948f9d10c045bfecab06b", + "sha256:2a6149e1defac0faf505406259561bc14b034cdf1d4711a3ddcdfbaa8d825a05", + "sha256:2deae0b0fb00a6af41fe60a675cec079615b01d68beb4cc7b722424406b126a8", + "sha256:307958f9fc5c8bb01fad752d1345168c0abc5d62c1b72a4a8c6c06f042b45b20", + "sha256:30babd84706115626ea78ea5dbc7dd8d0d01a2e9f9b306d24ca4ed5796c66ded", + "sha256:3378eb62c63bf336ae2070599e49089005771cc651c8769aaad72d1bd9385a7c", + "sha256:3d97672dc709fa4447ab83276f344a165075fd9f366a97b712bdd3fee05efae8", + "sha256:3db8de10ed684995a7f34a001f15b374c230f7655ae840964d51496e2f8a8474", + "sha256:3ebeeec6a6641d0adb2ea71dcfb76017602ee2bfd8213e3fcc18d8f699c5104f", + "sha256:45cea33b208971e87a31c17622e4b440cac231766ec11e5d22c76fab3bf9df62", + "sha256:6708f30db9117f115eadc4f125c2a10c1a50d711461699a0cbfaa45b9a78e376", + "sha256:68532f4349fd3900b839f588972b3392ee56042e440dd5873dfbbcd2cc67617c", + "sha256:6aafa5a78b9e62493539456f8b646f85abc7093dd997f4976bb105537cf2635e", + "sha256:7d37dccc7ae63e61f7b96ee2e19c40f153ba6ce730d8ba4d3b4e9738c1dccc1b", + "sha256:864e1197139d651a76c81757db5eb199db8866e13acb0dfe96e6fc5d1cf45fc4", + "sha256:8887d675a64cfc59f4ecd34382e5b4f0ef4ae1da37ed665adba0c2badf0d6578", + "sha256:8efcadc5a0003d3a6e887ccc1fb44dec25594f117a94e3127954c05cf144d811", + "sha256:9b09e0f0ac29eee0451d71798878eae5a4e6a91aa275e114037b27f7db72702d", + "sha256:a4aee22ece20958888eedbad20e4dbb03c37533e010fb824161b4f05e641f738", + "sha256:a5abddb3558d3f0a78949c750644a67be31e47936042d4f6c888dd6f3c95f4aa", + "sha256:c092a2c1e736086d59ac8e41f9c98f26bbf9b9222a76f21af9dfe949b99b2eb9", + "sha256:c686a47d57ca910a2572fddfe9912819880b8765e2f01dc0dd12a9bf8573e539", + "sha256:cbbe908fda687e39afd6ea2a2f14c2c3e43f2ca88e3a11964b297822358d0e6c", + "sha256:ce9f61938d7155f79d3cb2ffa663147d4a76d16e08f65e2c66b77bd41b356718", + "sha256:dbbaf9da2ee98ee2531e0c780455f2841e4675ff580ecf93fe5c48fe733b5667", + "sha256:f1e507c9ee39c61bfddd79714e4f85900656db1aec4d40c6de55648e85c2799c", + "sha256:ff3d00b70ce95adce264462c930fbaecb29718ba6563db354608f37e49e09024" + ], + "index": "pypi", + "version": "==0.17.0" + }, + "watchfiles": { + "hashes": [ + "sha256:0089c6dc24d436b373c3c57657bf4f9a453b13767150d17284fc6162b2791911", + "sha256:09ea3397aecbc81c19ed7f025e051a7387feefdb789cf768ff994c1228182fda", + "sha256:176a9a7641ec2c97b24455135d58012a5be5c6217fc4d5fef0b2b9f75dbf5154", + "sha256:18b28f6ad871b82df9542ff958d0c86bb0d8310bb09eb8e87d97318a3b5273af", + "sha256:20b44221764955b1e703f012c74015306fb7e79a00c15370785f309b1ed9aa8d", + "sha256:3d7d267d27aceeeaa3de0dd161a0d64f0a282264d592e335fff7958cc0cbae7c", + "sha256:5471582658ea56fca122c0f0d0116a36807c63fefd6fdc92c71ca9a4491b6b48", + "sha256:5569fc7f967429d4bc87e355cdfdcee6aabe4b620801e2cf5805ea245c06097c", + "sha256:68dce92b29575dda0f8d30c11742a8e2b9b8ec768ae414b54f7453f27bdf9545", + "sha256:79c533ff593db861ae23436541f481ec896ee3da4e5db8962429b441bbaae16e", + "sha256:7f3920b1285a7d3ce898e303d84791b7bf40d57b7695ad549dc04e6a44c9f120", + "sha256:91633e64712df3051ca454ca7d1b976baf842d7a3640b87622b323c55f3345e7", + "sha256:945be0baa3e2440151eb3718fd8846751e8b51d8de7b884c90b17d271d34cae8", + "sha256:9afd0d69429172c796164fd7fe8e821ade9be983f51c659a38da3faaaaac44dc", + "sha256:9c75eff897786ee262c9f17a48886f4e98e6cfd335e011c591c305e5d083c056", + "sha256:b538014a87f94d92f98f34d3e6d2635478e6be6423a9ea53e4dd96210065e193", + "sha256:b6577b8c6c8701ba8642ea9335a129836347894b666dd1ec2226830e263909d3", + "sha256:c0376deac92377817e4fb8f347bf559b7d44ff556d9bc6f6208dd3f79f104aaf", + "sha256:cae3dde0b4b2078f31527acff6f486e23abed307ba4d3932466ba7cdd5ecec79", + "sha256:cb5d45c4143c1dd60f98a16187fd123eda7248f84ef22244818c18d531a249d1", + "sha256:d9b073073e048081e502b6c6b0b88714c026a1a4c890569238d04aca5f9ca74b", + "sha256:fac19dc9cbc34052394dbe81e149411a62e71999c0a19e1e09ce537867f95ae0" + ], + "index": "pypi", + "version": "==0.19.0" + }, + "websockets": { + "hashes": [ + "sha256:0fb4480556825e4e6bf2eebdbeb130d9474c62705100c90e59f2f56459ddab42", + "sha256:13bd5bebcd16a4b5e403061b8b9dcc5c77e7a71e3c57e072d8dff23e33f70fba", + "sha256:143782041e95b63083b02107f31cda999f392903ae331de1307441f3a4557d51", + "sha256:1b52def56d2a26e0e9c464f90cadb7e628e04f67b0ff3a76a4d9a18dfc35e3dd", + "sha256:1df2413266bf48430ef2a752c49b93086c6bf192d708e4a9920544c74cd2baa6", + "sha256:2174a75d579d811279855df5824676d851a69f52852edb0e7551e0eeac6f59a4", + "sha256:220d5b93764dd70d7617f1663da64256df7e7ea31fc66bc52c0e3750ee134ae3", + "sha256:232b6ba974f5d09b1b747ac232f3a3d8f86de401d7b565e837cc86988edf37ac", + "sha256:25aae96c1060e85836552a113495db6d857400288161299d77b7b20f2ac569f2", + "sha256:25e265686ea385f22a00cc2b719b880797cd1bb53b46dbde969e554fb458bfde", + "sha256:2abeeae63154b7f63d9f764685b2d299e9141171b8b896688bd8baec6b3e2303", + "sha256:2acdc82099999e44fa7bd8c886f03c70a22b1d53ae74252f389be30d64fd6004", + "sha256:2eb042734e710d39e9bc58deab23a65bd2750e161436101488f8af92f183c239", + "sha256:3178d965ec204773ab67985a09f5696ca6c3869afeed0bb51703ea404a24e975", + "sha256:320ddceefd2364d4afe6576195201a3632a6f2e6d207b0c01333e965b22dbc84", + "sha256:34a6f8996964ccaa40da42ee36aa1572adcb1e213665e24aa2f1037da6080909", + "sha256:3565a8f8c7bdde7c29ebe46146bd191290413ee6f8e94cf350609720c075b0a1", + "sha256:392d409178db1e46d1055e51cc850136d302434e12d412a555e5291ab810f622", + "sha256:3a09cce3dacb6ad638fdfa3154d9e54a98efe7c8f68f000e55ca9c716496ca67", + "sha256:3a2100b02d1aaf66dc48ff1b2a72f34f6ebc575a02bc0350cc8e9fbb35940166", + "sha256:3b87cd302f08ea9e74fdc080470eddbed1e165113c1823fb3ee6328bc40ca1d3", + "sha256:3e79065ff6549dd3c765e7916067e12a9c91df2affea0ac51bcd302aaf7ad207", + "sha256:3ffe251a31f37e65b9b9aca5d2d67fd091c234e530f13d9dce4a67959d5a3fba", + "sha256:46388a050d9e40316e58a3f0838c63caacb72f94129eb621a659a6e49bad27ce", + "sha256:46dda4bc2030c335abe192b94e98686615f9274f6b56f32f2dd661fb303d9d12", + "sha256:4c54086b2d2aec3c3cb887ad97e9c02c6be9f1d48381c7419a4aa932d31661e4", + "sha256:5004c087d17251938a52cce21b3dbdabeecbbe432ce3f5bbbf15d8692c36eac9", + "sha256:502683c5dedfc94b9f0f6790efb26aa0591526e8403ad443dce922cd6c0ec83b", + "sha256:518ed6782d9916c5721ebd61bb7651d244178b74399028302c8617d0620af291", + "sha256:580cc95c58118f8c39106be71e24d0b7e1ad11a155f40a2ee687f99b3e5e432e", + "sha256:58477b041099bb504e1a5ddd8aa86302ed1d5c6995bdd3db2b3084ef0135d277", + "sha256:5875f623a10b9ba154cb61967f940ab469039f0b5e61c80dd153a65f024d9fb7", + "sha256:5c7de298371d913824f71b30f7685bb07ad13969c79679cca5b1f7f94fec012f", + "sha256:634239bc844131863762865b75211a913c536817c0da27f691400d49d256df1d", + "sha256:6d872c972c87c393e6a49c1afbdc596432df8c06d0ff7cd05aa18e885e7cfb7c", + "sha256:752fbf420c71416fb1472fec1b4cb8631c1aa2be7149e0a5ba7e5771d75d2bb9", + "sha256:7742cd4524622cc7aa71734b51294644492a961243c4fe67874971c4d3045982", + "sha256:808b8a33c961bbd6d33c55908f7c137569b09ea7dd024bce969969aa04ecf07c", + "sha256:87c69f50281126dcdaccd64d951fb57fbce272578d24efc59bce72cf264725d0", + "sha256:8df63dcd955eb6b2e371d95aacf8b7c535e482192cff1b6ce927d8f43fb4f552", + "sha256:8f24cd758cbe1607a91b720537685b64e4d39415649cac9177cd1257317cf30c", + "sha256:8f392587eb2767afa8a34e909f2fec779f90b630622adc95d8b5e26ea8823cb8", + "sha256:954eb789c960fa5daaed3cfe336abc066941a5d456ff6be8f0e03dd89886bb4c", + "sha256:955fcdb304833df2e172ce2492b7b47b4aab5dcc035a10e093d911a1916f2c87", + "sha256:95c09427c1c57206fe04277bf871b396476d5a8857fa1b99703283ee497c7a5d", + "sha256:a4fe2442091ff71dee0769a10449420fd5d3b606c590f78dd2b97d94b7455640", + "sha256:aa7b33c1fb2f7b7b9820f93a5d61ffd47f5a91711bc5fa4583bbe0c0601ec0b2", + "sha256:adf6385f677ed2e0b021845b36f55c43f171dab3a9ee0ace94da67302f1bc364", + "sha256:b1a69701eb98ed83dd099de4a686dc892c413d974fa31602bc00aca7cb988ac9", + "sha256:b2a573c8d71b7af937852b61e7ccb37151d719974146b5dc734aad350ef55a02", + "sha256:b444366b605d2885f0034dd889faf91b4b47668dd125591e2c64bfde611ac7e1", + "sha256:b985ba2b9e972cf99ddffc07df1a314b893095f62c75bc7c5354a9c4647c6503", + "sha256:c78ca3037a954a4209b9f900e0eabbc471fb4ebe96914016281df2c974a93e3e", + "sha256:ca9b2dced5cbbc5094678cc1ec62160f7b0fe4defd601cd28a36fde7ee71bbb5", + "sha256:cb46d2c7631b2e6f10f7c8bac7854f7c5e5288f024f1c137d4633c79ead1e3c0", + "sha256:ce69f5c742eefd039dce8622e99d811ef2135b69d10f9aa79fbf2fdcc1e56cd7", + "sha256:cf45d273202b0c1cec0f03a7972c655b93611f2e996669667414557230a87b88", + "sha256:d1881518b488a920434a271a6e8a5c9481a67c4f6352ebbdd249b789c0467ddc", + "sha256:d3cc3e48b6c9f7df8c3798004b9c4b92abca09eeea5e1b0a39698f05b7a33b9d", + "sha256:d6b2bfa1d884c254b841b0ff79373b6b80779088df6704f034858e4d705a4802", + "sha256:d70a438ef2a22a581d65ad7648e949d4ccd20e3c8ed7a90bbc46df4e60320891", + "sha256:daa1e8ea47507555ed7a34f8b49398d33dff5b8548eae3de1dc0ef0607273a33", + "sha256:dca9708eea9f9ed300394d4775beb2667288e998eb6f542cdb6c02027430c599", + "sha256:dd906b0cdc417ea7a5f13bb3c6ca3b5fd563338dc596996cb0fdd7872d691c0a", + "sha256:e0eeeea3b01c97fd3b5049a46c908823f68b59bf0e18d79b231d8d6764bc81ee", + "sha256:e37a76ccd483a6457580077d43bc3dfe1fd784ecb2151fcb9d1c73f424deaeba", + "sha256:e8b967a4849db6b567dec3f7dd5d97b15ce653e3497b8ce0814e470d5e074750", + "sha256:ec00401846569aaf018700249996143f567d50050c5b7b650148989f956547af", + "sha256:ede13a6998ba2568b21825809d96e69a38dc43184bdeebbde3699c8baa21d015", + "sha256:f97e03d4d5a4f0dca739ea274be9092822f7430b77d25aa02da6775e490f6846" + ], + "index": "pypi", + "version": "==11.0.2" + } + }, + "develop": { + "anyio": { + "hashes": [ + "sha256:25ea0d673ae30af41a0c442f81cf3b38c7e79fdc7b60335a4c14e05eb0947421", + "sha256:fbbe32bd270d2a2ef3ed1c5d45041250284e31fc0a4df4a5a6071842051a51e3" + ], + "index": "pypi", + "version": "==3.6.2" + }, + "attrs": { + "hashes": [ + "sha256:1f28b4522cdc2fb4256ac1a020c78acf9cba2c6b461ccd2c126f3aa8e8335d04", + "sha256:6279836d581513a26f1bf235f9acd333bc9115683f14f7e8fae46c98fc50e015" + ], + "markers": "python_version >= '3.7'", + "version": "==23.1.0" + }, + "black": { + "hashes": [ + "sha256:01ede61aac8c154b55f35301fac3e730baf0c9cf8120f65a9cd61a81cfb4a0c3", + "sha256:022a582720b0d9480ed82576c920a8c1dde97cc38ff11d8d8859b3bd6ca9eedb", + "sha256:25cc308838fe71f7065df53aedd20327969d05671bac95b38fdf37ebe70ac087", + "sha256:27eb7a0c71604d5de083757fbdb245b1a4fae60e9596514c6ec497eb63f95320", + "sha256:327a8c2550ddc573b51e2c352adb88143464bb9d92c10416feb86b0f5aee5ff6", + "sha256:47e56d83aad53ca140da0af87678fb38e44fd6bc0af71eebab2d1f59b1acf1d3", + "sha256:501387a9edcb75d7ae8a4412bb8749900386eaef258f1aefab18adddea1936bc", + "sha256:552513d5cd5694590d7ef6f46e1767a4df9af168d449ff767b13b084c020e63f", + "sha256:5c4bc552ab52f6c1c506ccae05681fab58c3f72d59ae6e6639e8885e94fe2587", + "sha256:642496b675095d423f9b8448243336f8ec71c9d4d57ec17bf795b67f08132a91", + "sha256:6d1c6022b86f83b632d06f2b02774134def5d4d4f1dac8bef16d90cda18ba28a", + "sha256:7f3bf2dec7d541b4619b8ce526bda74a6b0bffc480a163fed32eb8b3c9aed8ad", + "sha256:831d8f54c3a8c8cf55f64d0422ee875eecac26f5f649fb6c1df65316b67c8926", + "sha256:8417dbd2f57b5701492cd46edcecc4f9208dc75529bcf76c514864e48da867d9", + "sha256:86cee259349b4448adb4ef9b204bb4467aae74a386bce85d56ba4f5dc0da27be", + "sha256:893695a76b140881531062d48476ebe4a48f5d1e9388177e175d76234ca247cd", + "sha256:9fd59d418c60c0348505f2ddf9609c1e1de8e7493eab96198fc89d9f865e7a96", + "sha256:ad0014efc7acf0bd745792bd0d8857413652979200ab924fbf239062adc12491", + "sha256:b5b0ee6d96b345a8b420100b7d71ebfdd19fab5e8301aff48ec270042cd40ac2", + "sha256:c333286dc3ddca6fdff74670b911cccedacb4ef0a60b34e491b8a67c833b343a", + "sha256:f9062af71c59c004cd519e2fb8f5d25d39e46d3af011b41ab43b9c74e27e236f", + "sha256:fb074d8b213749fa1d077d630db0d5f8cc3b2ae63587ad4116e8a436e9bbe995" + ], + "index": "pypi", + "version": "==23.7.0" + }, + "certifi": { + "hashes": [ + "sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3", + "sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18" + ], + "index": "pypi", + "version": "==2022.12.7" + }, + "click": { + "hashes": [ + "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e", + "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48" + ], + "index": "pypi", + "version": "==8.1.3" + }, + "coverage": { + "hashes": [ + "sha256:07ea61bcb179f8f05ffd804d2732b09d23a1238642bf7e51dad62082b5019b34", + "sha256:1084393c6bda8875c05e04fce5cfe1301a425f758eb012f010eab586f1f3905e", + "sha256:13c6cbbd5f31211d8fdb477f0f7b03438591bdd077054076eec362cf2207b4a7", + "sha256:211a4576e984f96d9fce61766ffaed0115d5dab1419e4f63d6992b480c2bd60b", + "sha256:2d22172f938455c156e9af2612650f26cceea47dc86ca048fa4e0b2d21646ad3", + "sha256:34f9f0763d5fa3035a315b69b428fe9c34d4fc2f615262d6be3d3bf3882fb985", + "sha256:3558e5b574d62f9c46b76120a5c7c16c4612dc2644c3d48a9f4064a705eaee95", + "sha256:36ce5d43a072a036f287029a55b5c6a0e9bd73db58961a273b6dc11a2c6eb9c2", + "sha256:37d5576d35fcb765fca05654f66aa71e2808d4237d026e64ac8b397ffa66a56a", + "sha256:3c9834d5e3df9d2aba0275c9f67989c590e05732439b3318fa37a725dff51e74", + "sha256:438856d3f8f1e27f8e79b5410ae56650732a0dcfa94e756df88c7e2d24851fcd", + "sha256:477c9430ad5d1b80b07f3c12f7120eef40bfbf849e9e7859e53b9c93b922d2af", + "sha256:49ab200acf891e3dde19e5aa4b0f35d12d8b4bd805dc0be8792270c71bd56c54", + "sha256:49dbb19cdcafc130f597d9e04a29d0a032ceedf729e41b181f51cd170e6ee865", + "sha256:4c8e31cf29b60859876474034a83f59a14381af50cbe8a9dbaadbf70adc4b214", + "sha256:4eddd3153d02204f22aef0825409091a91bf2a20bce06fe0f638f5c19a85de54", + "sha256:5247bab12f84a1d608213b96b8af0cbb30d090d705b6663ad794c2f2a5e5b9fe", + "sha256:5492a6ce3bdb15c6ad66cb68a0244854d9917478877a25671d70378bdc8562d0", + "sha256:56afbf41fa4a7b27f6635bc4289050ac3ab7951b8a821bca46f5b024500e6321", + "sha256:59777652e245bb1e300e620ce2bef0d341945842e4eb888c23a7f1d9e143c446", + "sha256:60f64e2007c9144375dd0f480a54d6070f00bb1a28f65c408370544091c9bc9e", + "sha256:63c5b8ecbc3b3d5eb3a9d873dec60afc0cd5ff9d9f1c75981d8c31cfe4df8527", + "sha256:68d8a0426b49c053013e631c0cdc09b952d857efa8f68121746b339912d27a12", + "sha256:74c160285f2dfe0acf0f72d425f3e970b21b6de04157fc65adc9fd07ee44177f", + "sha256:7a9baf8e230f9621f8e1d00c580394a0aa328fdac0df2b3f8384387c44083c0f", + "sha256:7df91fb24c2edaabec4e0eee512ff3bc6ec20eb8dccac2e77001c1fe516c0c84", + "sha256:7f297e0c1ae55300ff688568b04ff26b01c13dfbf4c9d2b7d0cb688ac60df479", + "sha256:80501d1b2270d7e8daf1b64b895745c3e234289e00d5f0e30923e706f110334e", + "sha256:85b7335c22455ec12444cec0d600533a238d6439d8d709d545158c1208483873", + "sha256:887665f00ea4e488501ba755a0e3c2cfd6278e846ada3185f42d391ef95e7e70", + "sha256:8f39c49faf5344af36042b293ce05c0d9004270d811c7080610b3e713251c9b0", + "sha256:90b6e2f0f66750c5a1178ffa9370dec6c508a8ca5265c42fbad3ccac210a7977", + "sha256:96d7d761aea65b291a98c84e1250cd57b5b51726821a6f2f8df65db89363be51", + "sha256:97af9554a799bd7c58c0179cc8dbf14aa7ab50e1fd5fa73f90b9b7215874ba28", + "sha256:97c44f4ee13bce914272589b6b41165bbb650e48fdb7bd5493a38bde8de730a1", + "sha256:a67e6bbe756ed458646e1ef2b0778591ed4d1fcd4b146fc3ba2feb1a7afd4254", + "sha256:ac0dec90e7de0087d3d95fa0533e1d2d722dcc008bc7b60e1143402a04c117c1", + "sha256:ad0f87826c4ebd3ef484502e79b39614e9c03a5d1510cfb623f4a4a051edc6fd", + "sha256:b3eb0c93e2ea6445b2173da48cb548364f8f65bf68f3d090404080d338e3a689", + "sha256:b543302a3707245d454fc49b8ecd2c2d5982b50eb63f3535244fd79a4be0c99d", + "sha256:b859128a093f135b556b4765658d5d2e758e1fae3e7cc2f8c10f26fe7005e543", + "sha256:bac329371d4c0d456e8d5f38a9b0816b446581b5f278474e416ea0c68c47dcd9", + "sha256:c02cfa6c36144ab334d556989406837336c1d05215a9bdf44c0bc1d1ac1cb637", + "sha256:c9737bc49a9255d78da085fa04f628a310c2332b187cd49b958b0e494c125071", + "sha256:ccc51713b5581e12f93ccb9c5e39e8b5d4b16776d584c0f5e9e4e63381356482", + "sha256:ce2ee86ca75f9f96072295c5ebb4ef2a43cecf2870b0ca5e7a1cbdd929cf67e1", + "sha256:d000a739f9feed900381605a12a61f7aaced6beae832719ae0d15058a1e81c1b", + "sha256:db76a1bcb51f02b2007adacbed4c88b6dee75342c37b05d1822815eed19edee5", + "sha256:e2ac9a1de294773b9fa77447ab7e529cf4fe3910f6a0832816e5f3d538cfea9a", + "sha256:e61260ec93f99f2c2d93d264b564ba912bec502f679793c56f678ba5251f0393", + "sha256:fac440c43e9b479d1241fe9d768645e7ccec3fb65dc3a5f6e90675e75c3f3e3a", + "sha256:fc0ed8d310afe013db1eedd37176d0839dc66c96bcfcce8f6607a73ffea2d6ba" + ], + "index": "pypi", + "version": "==7.3.0" + }, + "flake8": { + "hashes": [ + "sha256:d5b3857f07c030bdb5bf41c7f53799571d75c4491748a3adcd47de929e34cd23", + "sha256:ffdfce58ea94c6580c77888a86506937f9a1a227dfcd15f245d694ae20a6b6e5" + ], + "index": "pypi", + "version": "==6.1.0" + }, + "h11": { + "hashes": [ + "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d", + "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761" + ], + "index": "pypi", + "version": "==0.14.0" + }, + "httpcore": { + "hashes": [ + "sha256:0fdfea45e94f0c9fd96eab9286077f9ff788dd186635ae61b312693e4d943599", + "sha256:cc045a3241afbf60ce056202301b4d8b6af08845e3294055eb26b09913ef903c" + ], + "index": "pypi", + "version": "==0.17.0" + }, + "httpx": { + "hashes": [ + "sha256:447556b50c1921c351ea54b4fe79d91b724ed2b027462ab9a329465d147d5a4e", + "sha256:507d676fc3e26110d41df7d35ebd8b3b8585052450f4097401c9be59d928c63e" + ], + "index": "pypi", + "version": "==0.24.0" + }, + "idna": { + "hashes": [ + "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4", + "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2" + ], + "index": "pypi", + "version": "==3.4" + }, + "iniconfig": { + "hashes": [ + "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", + "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374" + ], + "markers": "python_version >= '3.7'", + "version": "==2.0.0" + }, + "isort": { + "hashes": [ + "sha256:8bef7dde241278824a6d83f44a544709b065191b95b6e50894bdc722fcba0504", + "sha256:f84c2818376e66cf843d497486ea8fed8700b340f308f076c6fb1229dff318b6" + ], + "index": "pypi", + "version": "==5.12.0" + }, + "mccabe": { + "hashes": [ + "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325", + "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e" + ], + "markers": "python_version >= '3.6'", + "version": "==0.7.0" + }, + "mypy": { + "hashes": [ + "sha256:159aa9acb16086b79bbb0016145034a1a05360626046a929f84579ce1666b315", + "sha256:258b22210a4a258ccd077426c7a181d789d1121aca6db73a83f79372f5569ae0", + "sha256:26f71b535dfc158a71264e6dc805a9f8d2e60b67215ca0bfa26e2e1aa4d4d373", + "sha256:26fb32e4d4afa205b24bf645eddfbb36a1e17e995c5c99d6d00edb24b693406a", + "sha256:2fc3a600f749b1008cc75e02b6fb3d4db8dbcca2d733030fe7a3b3502902f161", + "sha256:32cb59609b0534f0bd67faebb6e022fe534bdb0e2ecab4290d683d248be1b275", + "sha256:330857f9507c24de5c5724235e66858f8364a0693894342485e543f5b07c8693", + "sha256:361da43c4f5a96173220eb53340ace68cda81845cd88218f8862dfb0adc8cddb", + "sha256:4a465ea2ca12804d5b34bb056be3a29dc47aea5973b892d0417c6a10a40b2d65", + "sha256:51cb1323064b1099e177098cb939eab2da42fea5d818d40113957ec954fc85f4", + "sha256:57b10c56016adce71fba6bc6e9fd45d8083f74361f629390c556738565af8eeb", + "sha256:596fae69f2bfcb7305808c75c00f81fe2829b6236eadda536f00610ac5ec2243", + "sha256:5d627124700b92b6bbaa99f27cbe615c8ea7b3402960f6372ea7d65faf376c14", + "sha256:6ac9c21bfe7bc9f7f1b6fae441746e6a106e48fc9de530dea29e8cd37a2c0cc4", + "sha256:82cb6193de9bbb3844bab4c7cf80e6227d5225cc7625b068a06d005d861ad5f1", + "sha256:8f772942d372c8cbac575be99f9cc9d9fb3bd95c8bc2de6c01411e2c84ebca8a", + "sha256:9fece120dbb041771a63eb95e4896791386fe287fefb2837258925b8326d6160", + "sha256:a156e6390944c265eb56afa67c74c0636f10283429171018446b732f1a05af25", + "sha256:a9ec1f695f0c25986e6f7f8778e5ce61659063268836a38c951200c57479cc12", + "sha256:abed92d9c8f08643c7d831300b739562b0a6c9fcb028d211134fc9ab20ccad5d", + "sha256:b031b9601f1060bf1281feab89697324726ba0c0bae9d7cd7ab4b690940f0b92", + "sha256:c543214ffdd422623e9fedd0869166c2f16affe4ba37463975043ef7d2ea8770", + "sha256:d28ddc3e3dfeab553e743e532fb95b4e6afad51d4706dd22f28e1e5e664828d2", + "sha256:f33592ddf9655a4894aef22d134de7393e95fcbdc2d15c1ab65828eee5c66c70", + "sha256:f6b0e77db9ff4fda74de7df13f30016a0a663928d669c9f2c057048ba44f09bb", + "sha256:f757063a83970d67c444f6e01d9550a7402322af3557ce7630d3c957386fa8f5", + "sha256:ff0cedc84184115202475bbb46dd99f8dcb87fe24d5d0ddfc0fe6b8575c88d2f" + ], + "index": "pypi", + "version": "==1.5.1" + }, + "mypy-extensions": { + "hashes": [ + "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", + "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782" + ], + "markers": "python_version >= '3.5'", + "version": "==1.0.0" + }, + "packaging": { + "hashes": [ + "sha256:994793af429502c4ea2ebf6bf664629d07c1a9fe974af92966e4b8d2df7edc61", + "sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f" + ], + "markers": "python_version >= '3.7'", + "version": "==23.1" + }, + "pathspec": { + "hashes": [ + "sha256:1d6ed233af05e679efb96b1851550ea95bbb64b7c490b0f5aa52996c11e92a20", + "sha256:e0d8d0ac2f12da61956eb2306b69f9469b42f4deb0f3cb6ed47b9cce9996ced3" + ], + "markers": "python_version >= '3.7'", + "version": "==0.11.2" + }, + "platformdirs": { + "hashes": [ + "sha256:b45696dab2d7cc691a3226759c0d3b00c47c8b6e293d96f6436f733303f77f6d", + "sha256:d7c24979f292f916dc9cbf8648319032f551ea8c49a4c9bf2fb556a02070ec1d" + ], + "markers": "python_version >= '3.7'", + "version": "==3.10.0" + }, + "pluggy": { + "hashes": [ + "sha256:cf61ae8f126ac6f7c451172cf30e3e43d3ca77615509771b3a984a0730651e12", + "sha256:d89c696a773f8bd377d18e5ecda92b7a3793cbe66c87060a6fb58c7b6e1061f7" + ], + "markers": "python_version >= '3.8'", + "version": "==1.3.0" + }, + "pycodestyle": { + "hashes": [ + "sha256:259bcc17857d8a8b3b4a2327324b79e5f020a13c16074670f9c8c8f872ea76d0", + "sha256:5d1013ba8dc7895b548be5afb05740ca82454fd899971563d2ef625d090326f8" + ], + "markers": "python_version >= '3.8'", + "version": "==2.11.0" + }, + "pyflakes": { + "hashes": [ + "sha256:4132f6d49cb4dae6819e5379898f2b8cce3c5f23994194c24b77d5da2e36f774", + "sha256:a0aae034c444db0071aa077972ba4768d40c830d9539fd45bf4cd3f8f6992efc" + ], + "markers": "python_version >= '3.8'", + "version": "==3.1.0" + }, + "pytest": { + "hashes": [ + "sha256:78bf16451a2eb8c7a2ea98e32dc119fd2aa758f1d5d66dbf0a59d69a3969df32", + "sha256:b4bf8c45bd59934ed84001ad51e11b4ee40d40a1229d2c79f9c592b0a3f6bd8a" + ], + "index": "pypi", + "version": "==7.4.0" + }, + "pytest-cov": { + "hashes": [ + "sha256:2feb1b751d66a8bd934e5edfa2e961d11309dc37b73b0eabe73b5945fee20f6b", + "sha256:996b79efde6433cdbd0088872dbc5fb3ed7fe1578b68cdbba634f14bb8dd0470" + ], + "index": "pypi", + "version": "==4.0.0" + }, + "pytest-subtests": { + "hashes": [ + "sha256:453389984952eec85ab0ce0c4f026337153df79587048271c7fd0f49119c07e4", + "sha256:51865c88457545f51fb72011942f0a3c6901ee9e24cbfb6d1b9dc1348bafbe37" + ], + "index": "pypi", + "version": "==0.11.0" + }, + "ruff": { + "hashes": [ + "sha256:00d579a011949108c4b4fa04c4f1ee066dab536a9ba94114e8e580c96be2aeb4", + "sha256:022f8bed2dcb5e5429339b7c326155e968a06c42825912481e10be15dafb424b", + "sha256:02dc4f5bf53ef136e459d467f3ce3e04844d509bc46c025a05b018feb37bbc39", + "sha256:06ac5df7dd3ba8bf83bba1490a72f97f1b9b21c7cbcba8406a09de1a83f36083", + "sha256:150007028ad4976ce9a7704f635ead6d0e767f73354ce0137e3e44f3a6c0963b", + "sha256:1cf4d5ad3073af10f186ea22ce24bc5a8afa46151f6896f35c586e40148ba20b", + "sha256:1e0f9ee4c3191444eefeda97d7084721d9b8e29017f67997a20c153457f2eafd", + "sha256:26bd0041d135a883bd6ab3e0b29c42470781fb504cf514e4c17e970e33411d90", + "sha256:2918cb7885fa1611d542de1530bea3fbd63762da793751cc8c8d6e4ba234c3d8", + "sha256:2bfb478e1146a60aa740ab9ebe448b1f9e3c0dfb54be3cc58713310eef059c30", + "sha256:33d7b251afb60bec02a64572b0fd56594b1923ee77585bee1e7e1daf675e7ae7", + "sha256:44bceb3310ac04f0e59d4851e6227f7b1404f753997c7859192e41dbee9f5c8d", + "sha256:66d9d58bcb29afd72d2afe67120afcc7d240efc69a235853813ad556443dc922", + "sha256:8ca1ed11d759a29695aed2bfc7f914b39bcadfe2ef08d98ff69c873f639ad3a8", + "sha256:a24a280db71b0fa2e0de0312b4aecb8e6d08081d1b0b3c641846a9af8e35b4a7", + "sha256:d3a810a79b8029cc92d06c36ea1f10be5298d2323d9024e1d21aedbf0a1a13e5", + "sha256:e9843e5704d4fb44e1a8161b0d31c1a38819723f0942639dfeb53d553be9bfb5" + ], + "index": "pypi", + "version": "==0.0.287" + }, + "sniffio": { + "hashes": [ + "sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101", + "sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384" + ], + "index": "pypi", + "version": "==1.3.0" + }, + "types-pyyaml": { + "hashes": [ + "sha256:5aed5aa66bd2d2e158f75dda22b059570ede988559f030cf294871d3b647e3e8", + "sha256:c51b1bd6d99ddf0aa2884a7a328810ebf70a4262c292195d3f4f9a0005f9eeb6" + ], + "index": "pypi", + "version": "==6.0.12.9" + }, + "types-requests": { + "hashes": [ + "sha256:dbc2933635860e553ffc59f5e264264981358baffe6342b925e3eb8261f866ee", + "sha256:fd530aab3fc4f05ee36406af168f0836e6f00f1ee51a0b96b7311f82cb675230" + ], + "index": "pypi", + "version": "==2.28.11.12" + }, + "types-urllib3": { + "hashes": [ + "sha256:229b7f577c951b8c1b92c1bc2b2fdb0b49847bd2af6d1cc2a2e3dd340f3bda8f", + "sha256:9683bbb7fb72e32bfe9d2be6e04875fbe1b3eeec3cbb4ea231435aa7fd6b4f0e" + ], + "version": "==1.26.25.14" + }, + "typing-extensions": { + "hashes": [ + "sha256:5cb5f4a79139d699607b3ef622a1dedafa84e115ab0024e0d9c044a9479ca7cb", + "sha256:fb33085c39dd998ac16d1431ebc293a8b3eedd00fd4a32de0ff79002c19511b4" + ], + "index": "pypi", + "version": "==4.5.0" + } + } +} diff --git a/README.md b/README.md new file mode 100644 index 00000000000..32a1fec347f --- /dev/null +++ b/README.md @@ -0,0 +1,28 @@ +# train-flyte-domino-agent-service +This repository leverages our "Core Pipeline" framework to publish a Python-based microservice. + +## Features + +* Integrated CircleCI configuration using the [domino-orb](https://github.com/cerebrotech/domino-orb) and [circleci-workflow-builder](https://github.com/cerebrotech/circleci-workflow-builder). +* [Black formatter](https://github.com/psf/black) support +* [ruff](https://github.com/charliermarsh/ruff) lint support +* [mypy](http://mypy-lang.org/) type checking +* [pytest](https://docs.pytest.org/en/7.1.x/)-based unit testing +* Support for building (and caching) Docker images to Domino's repository in [Quay.io](https://quay.io/domino) +* [pipenv](https://pipenv.pypa.io/en/latest/) support +* [FastAPI](https://fastapi.tiangolo.com/lo/) web framework + +## Notes + +### Managing python versions + +Python versions are managed in 3 places +1. The https://github.com/cerebrotech/sdlc-python-executor-docker which has the runtime for building python projects +2. The .python-version file in the base of this repo. This is largely for local development. +3. Pipfile (this should not be specified as it doesn't have any material value with the .python-version) + +Currently, the latest sdlc-python-executor-docker is used, so if you need an older version of python you have to wait for https://dominodatalab.atlassian.net/browse/PLAT-6940 to be finished. + +### Contributing + +Please contact @cerebrotech/dev-productivity [by email](mailto:eng-devprod@dominodatalab.com) or in [#dev-productivity](https://dominodatalab.slack.com/archives/C013UJQ6R7D) for any questions or comments! diff --git a/api/swagger.json b/api/swagger.json new file mode 100644 index 00000000000..08089b63a70 --- /dev/null +++ b/api/swagger.json @@ -0,0 +1,269 @@ +{ + "openapi": "3.0.2", + "info": { + "title": "Python Microservice API", + "description": "\nExample of a Python-based microservice\n", + "termsOfService": "https://swagger.io/terms/", + "contact": { + "name": "Domino Data Lab", + "url": "https://tickets.dominodatalab.com/hc/en-us", + "email": "support@dominodatalab.com" + }, + "license": { + "name": "Apache 2.0", + "url": "https://www.apache.org/licenses/LICENSE-2.0.html" + }, + "version": "1.0" + }, + "paths": { + "/api/units/healthcheck": { + "get": { + "summary": "Healthcheck", + "operationId": "healthcheck_api_units_healthcheck_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HealthCheck" + } + } + } + } + } + } + }, + "/v1/units": { + "get": { + "summary": "Get Units", + "operationId": "get_units_v1_units_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "title": "Response Get Units V1 Units Get", + "type": "array", + "items": { + "$ref": "#/components/schemas/Unit" + } + } + } + } + } + } + } + }, + "/v1/unit": { + "post": { + "summary": "Add Unit", + "operationId": "add_unit_v1_unit_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Unit" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/unit/{name}": { + "put": { + "summary": "Update Unit", + "operationId": "update_unit_v1_unit__name__put", + "parameters": [ + { + "required": true, + "schema": { + "title": "Name", + "type": "string" + }, + "name": "name", + "in": "path" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Unit" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "delete": { + "summary": "Delete Unit", + "operationId": "delete_unit_v1_unit__name__delete", + "parameters": [ + { + "required": true, + "schema": { + "title": "Name", + "type": "string" + }, + "name": "name", + "in": "path" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": { + "$ref": "#/components/schemas/ValidationError" + } + } + } + }, + "HealthCheck": { + "title": "HealthCheck", + "required": [ + "status" + ], + "type": "object", + "properties": { + "status": { + "title": "Status", + "type": "string" + } + } + }, + "Unit": { + "title": "Unit", + "required": [ + "name", + "abbreviation", + "base", + "exponent" + ], + "type": "object", + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "abbreviation": { + "title": "Abbreviation", + "type": "string" + }, + "base": { + "title": "Base", + "type": "integer" + }, + "exponent": { + "title": "Exponent", + "type": "integer" + } + } + }, + "ValidationError": { + "title": "ValidationError", + "required": [ + "loc", + "msg", + "type" + ], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "integer" + } + ] + } + }, + "msg": { + "title": "Message", + "type": "string" + }, + "type": { + "title": "Error Type", + "type": "string" + } + } + } + } + } +} diff --git a/api/swagger.yaml b/api/swagger.yaml new file mode 100644 index 00000000000..0cca4fe225b --- /dev/null +++ b/api/swagger.yaml @@ -0,0 +1,178 @@ +openapi: 3.0.2 +info: + title: Python Microservice API + description: ' + + Example of a Python-based microservice + + ' + termsOfService: https://swagger.io/terms/ + contact: + name: Domino Data Lab + url: !anyurl 'https://tickets.dominodatalab.com/hc/en-us' + email: support@dominodatalab.com + license: + name: Apache 2.0 + url: !anyurl 'https://www.apache.org/licenses/LICENSE-2.0.html' + version: '1.0' +paths: + /api/units/healthcheck: + get: + summary: Healthcheck + operationId: healthcheck_api_units_healthcheck_get + responses: + '200': + description: Successful Response + content: + application/json: + schema: + $ref: '#/components/schemas/HealthCheck' + /v1/units: + get: + summary: Get Units + operationId: get_units_v1_units_get + responses: + '200': + description: Successful Response + content: + application/json: + schema: + title: Response Get Units V1 Units Get + type: array + items: + $ref: '#/components/schemas/Unit' + /v1/unit: + post: + summary: Add Unit + operationId: add_unit_v1_unit_post + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Unit' + required: true + responses: + '201': + description: Successful Response + content: + application/json: + schema: {} + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + /v1/unit/{name}: + put: + summary: Update Unit + operationId: update_unit_v1_unit__name__put + parameters: + - required: true + schema: + title: Name + type: string + name: name + in: path + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Unit' + required: true + responses: + '200': + description: Successful Response + content: + application/json: + schema: {} + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + delete: + summary: Delete Unit + operationId: delete_unit_v1_unit__name__delete + parameters: + - required: true + schema: + title: Name + type: string + name: name + in: path + responses: + '200': + description: Successful Response + content: + application/json: + schema: {} + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' +components: + schemas: + HTTPValidationError: + title: HTTPValidationError + type: object + properties: + detail: + title: Detail + type: array + items: + $ref: '#/components/schemas/ValidationError' + HealthCheck: + title: HealthCheck + required: + - status + type: object + properties: + status: + title: Status + type: string + Unit: + title: Unit + required: + - name + - abbreviation + - base + - exponent + type: object + properties: + name: + title: Name + type: string + abbreviation: + title: Abbreviation + type: string + base: + title: Base + type: integer + exponent: + title: Exponent + type: integer + ValidationError: + title: ValidationError + required: + - loc + - msg + - type + type: object + properties: + loc: + title: Location + type: array + items: + anyOf: + - type: string + - type: integer + msg: + title: Message + type: string + type: + title: Error Type + type: string diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/app/config.py b/app/config.py new file mode 100644 index 00000000000..22d52609b07 --- /dev/null +++ b/app/config.py @@ -0,0 +1,20 @@ +from os import path + +from pydantic import BaseSettings + + +class Settings(BaseSettings): + API_BASE_STR: str = "/api/units" + API_V1_STR: str = "/v1" + + # It is not recommended to hide the API docs + # https://fastapi.tiangolo.com/advanced/conditional-openapi/ + OPENAPI_URL: str = path.join(API_BASE_STR, "openapi.json") + DOCS_URL: str = path.join(API_BASE_STR, "swagger") + REDOC_URL: str = path.join(API_BASE_STR, "redoc") + + class Config: + case_sensitive = True + + +settings = Settings() diff --git a/app/main.py b/app/main.py new file mode 100644 index 00000000000..bf1da41109e --- /dev/null +++ b/app/main.py @@ -0,0 +1,31 @@ +from fastapi import FastAPI + +from app.config import settings +from app.routes.base import base_routes +from app.routes.units import units_v1 + +DESCRIPTION = """ +Example of a Python-based microservice +""" + +app = FastAPI( + title="Python Microservice API", + description=DESCRIPTION, + version="1.0", + terms_of_service="https://swagger.io/terms/", + contact={ + "name": "Domino Data Lab", + "url": "https://tickets.dominodatalab.com/hc/en-us", + "email": "support@dominodatalab.com", + }, + license_info={ + "name": "Apache 2.0", + "url": "https://www.apache.org/licenses/LICENSE-2.0.html", + }, + openapi_url=settings.OPENAPI_URL, + docs_url=settings.DOCS_URL, + redoc_url=settings.REDOC_URL, +) + +app.include_router(base_routes, prefix=settings.API_BASE_STR) +app.include_router(units_v1, prefix=settings.API_V1_STR) diff --git a/app/models/__init__.py b/app/models/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/app/models/check.py b/app/models/check.py new file mode 100644 index 00000000000..7d00fff9611 --- /dev/null +++ b/app/models/check.py @@ -0,0 +1,5 @@ +from pydantic import BaseModel + + +class HealthCheck(BaseModel): + status: str diff --git a/app/models/unit.py b/app/models/unit.py new file mode 100644 index 00000000000..b6002d192bd --- /dev/null +++ b/app/models/unit.py @@ -0,0 +1,8 @@ +from pydantic import BaseModel + + +class Unit(BaseModel): + name: str + abbreviation: str + base: int + exponent: int diff --git a/app/routes/__init__.py b/app/routes/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/app/routes/base.py b/app/routes/base.py new file mode 100644 index 00000000000..405c01b43e2 --- /dev/null +++ b/app/routes/base.py @@ -0,0 +1,10 @@ +from fastapi import APIRouter + +from app.models.check import HealthCheck + +base_routes = APIRouter() + + +@base_routes.get("/healthcheck", response_model=HealthCheck) +def healthcheck(): + return HealthCheck(status="Healthy") diff --git a/app/routes/units.py b/app/routes/units.py new file mode 100644 index 00000000000..7ccc3fcd52e --- /dev/null +++ b/app/routes/units.py @@ -0,0 +1,40 @@ +from typing import List + +from fastapi import APIRouter, HTTPException + +from app.models.unit import Unit +from app.services.database import DatabaseSession + +fake_db = DatabaseSession() + + +units_v1 = APIRouter() + + +@units_v1.get("/units", response_model=List[Unit]) +async def get_units(): + return fake_db.get_units() + + +@units_v1.post("/unit", status_code=201) +async def add_unit(payload: Unit): + result = fake_db.add_unit(payload) + if result is None: + raise HTTPException(status_code=404, detail=f'Unit "{payload.name}" already exists') + return result + + +@units_v1.put("/unit/{name}") +async def update_unit(name: str, payload: Unit): + result = fake_db.update_unit(payload) + if result is None: + raise HTTPException(status_code=404, detail=f'Unit "{name}" not found') + return result + + +@units_v1.delete("/unit/{name}") +async def delete_unit(name: str): + result = fake_db.delete_unit(name) + if result != "success": + raise HTTPException(status_code=404, detail=result) + return None diff --git a/app/services/__init__.py b/app/services/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/app/services/database.py b/app/services/database.py new file mode 100644 index 00000000000..2854e560e85 --- /dev/null +++ b/app/services/database.py @@ -0,0 +1,59 @@ +from typing import ( + Any, + Dict, + List, + Optional, +) + +from app.models.unit import Unit + + +class DatabaseSession: + # Ideally we'd have a database connection + # For now, we'll keep data in memory + units: List[Unit] = [] + + def __init__(self, seed: bool = False): + if seed: + self.units = self._make_units() + + @staticmethod + def _make_units() -> List[Unit]: + return [ + Unit( + name="kilo", + abbreviation="k", + base=10, + exponent=3, + ), + Unit( + name="kibi", + abbreviation="Ki", + base=2, + exponent=10, + ), + ] + + def get_units(self) -> List[Dict[str, Any]]: + return [u.dict() for u in self.units] + + def add_unit(self, new_unit: Unit) -> Optional[Unit]: + for unit in self.units: + if unit.name == new_unit.name: + return None + self.units.append(new_unit) + return new_unit + + def update_unit(self, updates: Unit) -> Optional[Unit]: + for idx, unit in enumerate(self.units): + if unit.name == updates.name: + self.units[idx] = updates + return updates + return None + + def delete_unit(self, name: str) -> str: + for idx, unit in enumerate(self.units): + if unit.name == name: + del self.units[idx] + return "success" + return f'Unit "{name}" not found' diff --git a/build/docker/Dockerfile b/build/docker/Dockerfile new file mode 100644 index 00000000000..5d010e4600b --- /dev/null +++ b/build/docker/Dockerfile @@ -0,0 +1,31 @@ +# Pin to a specific Domino vendored base image and apply OS updates ONLY in final stage of build. +FROM quay.io/domino/python:3.11.5-bullseye-372631 as depbuilder + +ENV APP_DIRECTORY=/app +RUN mkdir $APP_DIRECTORY +WORKDIR $APP_DIRECTORY + +RUN pip install --no-cache-dir --upgrade pip==v23.1.2 pipenv==2023.6.2 + +COPY Pipfile Pipfile +COPY Pipfile.lock Pipfile.lock + +ENV PIPENV_VENV_IN_PROJECT=1 + +RUN pipenv install --deploy + +FROM quay.io/domino/python:3.11.5-slim-bullseye-372633 + +# repo-standards/OSRP +RUN apt-get update && apt-get upgrade --yes && apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* /var/log/*.log + +ENV APP_DIRECTORY=/app +WORKDIR $APP_DIRECTORY + +COPY --from=depbuilder $APP_DIRECTORY/.venv .venv +ENV PATH="$APP_DIRECTORY/.venv/bin:$PATH" + +COPY . . + +# https://fastapi.tiangolo.com/deployment/docker/#build-a-docker-image-for-fastapi +CMD ["uvicorn", "app.main:app", "--proxy-headers", "--host", "0.0.0.0", "--port", "80"] diff --git a/domino.yml b/domino.yml new file mode 100644 index 00000000000..d4751c9abb8 --- /dev/null +++ b/domino.yml @@ -0,0 +1,11 @@ +workflowVersion: v0 +name: train-flyte-domino-agent-service +type: service +version: 0.1 +language: + name: python + version: 3.11.5 +def: + baseChart: + name: service + version: 1.0.3 \ No newline at end of file diff --git a/helm/values.yaml b/helm/values.yaml new file mode 100644 index 00000000000..cfd3b106ae7 --- /dev/null +++ b/helm/values.yaml @@ -0,0 +1,2 @@ +# Placeholder file for configuring values for standardized Helm charts in the Pipeline framework +# In the future, we should be able to set values here and publish a usable Helm chart in CI. diff --git a/my_service/__init__.py b/my_service/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000000..8ee771bd559 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,60 @@ +[tool.black] +line-length = 120 +skip-string-normalization = true +exclude = ''' +/( + \.eggs + | \.git + | \.hg + | \.mypy_cache + | \.tox + | \.venv + | _build + | buck-out + | build + | dist +)/ +''' + +[tool.pytest.ini_options] +filterwarnings = [ + "error" +] + +[tool.mypy] +warn_no_return = false +namespace_packages = true +explicit_package_bases = true +ignore_missing_imports = true +pretty = false +show_error_context = false +no_error_summary = true +check_untyped_defs = true +exclude = [ + "build/", + "resources/" +] + +[tool.pyright] +exclude = [ + "**/__pycache__", + ".venv", + ".git", + "build", + "resources" +] + +[tool.ruff] +select = ["E", "F", "I"] +ignore = ["E501"] # line length +exclude = [ + "**/__pycache__", + ".venv", + ".git", + "build", + "resources" +] + +[tool.coverage.run] +source = ["."] +omit = ["*/venv/*", "*/.venv/*", "*/tests/*", "setup.py"] diff --git a/renovate.json b/renovate.json new file mode 100644 index 00000000000..87d3b3fed1c --- /dev/null +++ b/renovate.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "description": "Renovate config", + "extends": [ + "github>cerebrotech/renovate-config:docker-minimal", + "github>cerebrotech/renovate-config:python-minimal" + ], + "packageRules": [ + { + "matchUpdateTypes": ["patch"], + "matchManagers": ["dockerfile"], + "automerge": true, + "platformAutomerge": true + } + ] +} diff --git a/scripts/check-mypy-known-good.sh b/scripts/check-mypy-known-good.sh new file mode 100755 index 00000000000..80e5852452a --- /dev/null +++ b/scripts/check-mypy-known-good.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash + +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )" +cd "$SCRIPT_DIR/../" || exit 1 + +# shellcheck disable=SC2046 +pipenv run mypy --follow-imports=silent $(comm -3 <(git ls-files '*.py' | sort) <(sort MYPY_KNOWN_BAD)) diff --git a/scripts/gen-api-docs.py b/scripts/gen-api-docs.py new file mode 100755 index 00000000000..f61e4a16318 --- /dev/null +++ b/scripts/gen-api-docs.py @@ -0,0 +1,41 @@ +#!/usr/bin/env python3 +"""Run this script to generate and save OpenAPI specs from the app + +Example: + $ pipenv run python3 bin/gen-api-docs.py +""" +# +import json +import os +import sys + +import yaml +from pydantic import AnyUrl +from pydantic.networks import url_regex + +# Ignore E402 +sys.path.append(os.getcwd()) +from app.main import app # noqa + +API_DOC_DIR = "api" +JSON_FILE_NAME = os.path.join(API_DOC_DIR, "swagger.json") +YAML_FILE_NAME = os.path.join(API_DOC_DIR, "swagger.yaml") + + +def _any_url_representer(dumper, data): + return dumper.represent_scalar("!anyurl", str(data)) + + +print(f"Saving API schema to: {JSON_FILE_NAME}") +with open(JSON_FILE_NAME, "w") as fs: + json.dump(app.openapi(), fs, indent=4) + # NOTE: Add new line at end of file for pre-commit hook + fs.write("\n") + +# https://github.com/tiangolo/fastapi/issues/1140#issuecomment-880743503 +yaml.add_representer(AnyUrl, _any_url_representer, yaml.SafeDumper) +yaml.add_implicit_resolver("!anyurl", url_regex()) + +print(f"Saving API schema to: {YAML_FILE_NAME}") +with open(YAML_FILE_NAME, "w") as fs: + yaml.safe_dump(app.openapi(), fs, sort_keys=False, allow_unicode=True) diff --git a/scripts/regen-mypy-known-bad.sh b/scripts/regen-mypy-known-bad.sh new file mode 100755 index 00000000000..3678c1616c9 --- /dev/null +++ b/scripts/regen-mypy-known-bad.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash + +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )" +cd "$SCRIPT_DIR/../" || exit 1 + +pipenv run mypy . | cut -d ":" -f 1,1 | sort -dsu > MYPY_KNOWN_BAD diff --git a/sonar-project.properties b/sonar-project.properties new file mode 100644 index 00000000000..ab6e5836079 --- /dev/null +++ b/sonar-project.properties @@ -0,0 +1,4 @@ +sonar.projectName=train-flyte-domino-agent-service +sonar.projectKey=train-flyte-domino-agent-service +sonar.test.inclusions=tests/** +sonar.exclusions=bin/**, api/**, tests/** diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/fixtures/.gitkeep b/tests/fixtures/.gitkeep new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/unit/__init__.py b/tests/unit/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/unit/app/__init__.py b/tests/unit/app/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/unit/app/test_main.py b/tests/unit/app/test_main.py new file mode 100644 index 00000000000..ba52f214a14 --- /dev/null +++ b/tests/unit/app/test_main.py @@ -0,0 +1,71 @@ +from copy import deepcopy +from os import path + +from fastapi.testclient import TestClient + +from app.config import settings +from app.main import app + +client = TestClient(app) + + +def test_healthcheck(): + response = client.get(path.join(settings.API_BASE_STR, "healthcheck")) + assert response.status_code == 200 + assert response.json() == { + "status": "Healthy", + } + + +def test_units_v1(): + """Hits a few endpoints: + 1. Create a new unit + 2. Get units + 3. Update unit + 4. Delete unit + """ + unit_name = "foo" + new_unit = { + "name": unit_name, + "abbreviation": "f", + "base": 10, + "exponent": 0, + } + + # Create a new unit + response = client.post( + path.join(settings.API_V1_STR, "unit"), + json=new_unit, + ) + assert response.status_code == 201 + assert response.json() == new_unit + + # Get units + response = client.get(path.join(settings.API_V1_STR, "units")) + assert response.status_code == 200 + assert response.json() == [new_unit] + + # Update a unit + updates = deepcopy(new_unit) + updates["base"] = 2 + + response = client.put( + path.join(settings.API_V1_STR, "unit", unit_name), + json=updates, + ) + assert response.status_code == 200 + assert response.json() == updates + + # Get units to confirm the update + response = client.get(path.join(settings.API_V1_STR, "units")) + assert response.status_code == 200 + assert response.json() == [updates] + + # Delete a unit + response = client.delete(path.join(settings.API_V1_STR, "unit", unit_name)) + assert response.status_code == 200 + + # Get units to confirm the deletion + response = client.get(path.join(settings.API_V1_STR, "units")) + assert response.status_code == 200 + assert response.json() == []