├── .env ├── .flake8 ├── .github └── workflows │ ├── ci.yml │ └── realworld-tests.yml ├── .gitignore ├── .gitmodules ├── .isort.cfg ├── .pre-commit-config.yaml ├── .vscode ├── extensions.json ├── launch.json └── settings.json ├── README.md ├── logo.png ├── mypy.ini ├── poetry.lock ├── pyproject.toml ├── pytest.ini ├── scripts ├── coverage.sh ├── format.sh ├── lint.sh ├── setup.sh ├── start-mongo.sh ├── start.sh ├── stop-mongo.sh └── test.sh ├── src ├── __init__.py ├── api.py ├── core │ ├── __init__.py │ ├── article.py │ ├── comment.py │ ├── exceptions.py │ ├── tag.py │ └── user.py ├── endpoints │ ├── __init__.py │ ├── article.py │ ├── comment.py │ ├── profile.py │ ├── tag.py │ └── user.py ├── models │ ├── __init__.py │ ├── article.py │ └── user.py ├── schemas │ ├── __init__.py │ ├── article.py │ ├── base.py │ ├── comment.py │ ├── tag.py │ └── user.py ├── settings.py └── utils │ ├── __init__.py │ └── security.py └── tests └── .gitkeep /.env: -------------------------------------------------------------------------------- 1 | PYTHONPATH=./src 2 | COVERAGE_RCFILE=./coverage.ini 3 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 88 3 | ignore = 4 | E203, # See https://github.com/PyCQA/pycodestyle/issues/373 5 | W503 6 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push] 4 | 5 | jobs: 6 | static-analysis: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v2 10 | - name: Set up Python 3.10 11 | uses: actions/setup-python@v2 12 | with: 13 | python-version: "3.10" 14 | - uses: pre-commit/action@v2.0.0 15 | with: 16 | extra_args: --all-files 17 | -------------------------------------------------------------------------------- /.github/workflows/realworld-tests.yml: -------------------------------------------------------------------------------- 1 | name: Realworld Tests 2 | 3 | on: [push] 4 | 5 | jobs: 6 | realworld-tests: 7 | timeout-minutes: 10 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - uses: actions/checkout@v1 12 | with: 13 | submodules: recursive 14 | - name: Install poetry 15 | run: pipx install poetry 16 | - name: Set up Python 3.10 17 | uses: actions/setup-python@v4 18 | with: 19 | python-version: "3.10" 20 | cache: "poetry" 21 | - name: Install dependencies 22 | run: poetry install 23 | - name: Start the MongoDB instance 24 | uses: art049/mongodb-cluster-action@v0 25 | id: mongodb-cluster-action 26 | - name: Start the FastAPI server 27 | run: | 28 | ./scripts/start.sh & 29 | # Wait for the server 30 | while ! curl "http://localhost:8000/health" > /dev/null 2>&1 31 | do 32 | sleep 1; 33 | done 34 | echo "Server ready." 35 | env: 36 | MONGO_URI: ${{ steps.mongodb-cluster-action.outputs.connection-string }} 37 | - name: Run realworld backend tests 38 | run: ./realworld/api/run-api-tests.sh 39 | env: 40 | APIURL: http://localhost:8000 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .venv 86 | env/ 87 | venv/ 88 | ENV/ 89 | env.bak/ 90 | venv.bak/ 91 | 92 | # Spyder project settings 93 | .spyderproject 94 | .spyproject 95 | 96 | # Rope project settings 97 | .ropeproject 98 | 99 | # mkdocs documentation 100 | /site 101 | 102 | # mypy 103 | .mypy_cache/ 104 | 105 | #Pipenv lock file to allow slightly different python versions 106 | Pipfile.lock 107 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "realworld"] 2 | path = realworld 3 | url = https://github.com/gothinkster/realworld.git 4 | -------------------------------------------------------------------------------- /.isort.cfg: -------------------------------------------------------------------------------- 1 | [settings] 2 | line_length=88 3 | multi_line_output=3 4 | include_trailing_comma=True 5 | use_parentheses=True 6 | force_grid_wrap=0 7 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # See https://pre-commit.com for more information 2 | # See https://pre-commit.com/hooks.html for more hooks 3 | repos: 4 | - repo: https://github.com/pre-commit/pre-commit-hooks 5 | rev: v2.4.0 6 | hooks: 7 | - id: trailing-whitespace 8 | - id: end-of-file-fixer 9 | - id: check-yaml 10 | - id: check-added-large-files 11 | - repo: https://github.com/pre-commit/mirrors-prettier 12 | rev: "v2.7.1" 13 | hooks: 14 | - id: prettier 15 | - repo: https://github.com/psf/black 16 | rev: 22.6.0 17 | hooks: 18 | - id: black 19 | - repo: https://github.com/timothycrosley/isort.git 20 | rev: "5.10.1" 21 | hooks: 22 | - id: isort 23 | - repo: https://github.com/pre-commit/mirrors-mypy 24 | rev: v0.971 25 | hooks: 26 | - id: mypy 27 | - repo: https://gitlab.com/pycqa/flake8.git 28 | rev: 5.0.4 29 | hooks: 30 | - id: flake8 31 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "njpwerner.autodocstring", 4 | "ryanluker.vscode-coverage-gutters", 5 | "ms-python.python", 6 | "littlefoxteam.vscode-python-test-adapter", 7 | "hbenl.vscode-test-explorer" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Debug Tests", 9 | "type": "python", 10 | "request": "test", 11 | "console": "integratedTerminal", 12 | "justMyCode": false 13 | } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.formatting.provider": "black", 3 | "editor.rulers": [88], 4 | "python.linting.enabled": true, 5 | "python.linting.flake8Enabled": true, 6 | "python.linting.mypyEnabled": true, 7 | "python.envFile": "${workspaceFolder}/.env", 8 | "python.pythonPath": "${workspaceFolder}/.venv/bin/python3.8", 9 | "editor.codeActionsOnSave": { 10 | "source.organizeImports": true 11 | }, 12 | "python.testing.pytestEnabled": true, 13 | "editor.formatOnSave": true, 14 | "files.exclude": { 15 | ".venv/": false, 16 | ".pytest_cache/": true, 17 | ".mypy_cache/": true 18 | }, 19 | "python.linting.mypyArgs": [ 20 | "--config-file=${workspaceFolder}/mypy.ini", 21 | "--no-pretty" 22 | ], 23 | "python.languageServer": "Pylance", 24 | "python.linting.pydocstyleArgs": ["--config=./.pydocstyle"] 25 | } 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ![RealWorld FastAPI + ODMantic App](logo.png) 2 | 3 |
4 | 5 | 10 | 11 | ![Python: 3.10](https://img.shields.io/badge/python-3.10-informational.svg) 12 | [![pre-commit](https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit&logoColor=white)](https://github.com/pre-commit/pre-commit) 13 | [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/python/black) 14 | [![mypy: checked](https://img.shields.io/badge/mypy-checked-informational.svg)](http://mypy-lang.org/) 15 | [![Manager: poetry](https://img.shields.io/badge/manager-poetry-blueviolet.svg)](https://poetry.eustace.io/) 16 | 17 |
18 | 19 | > ### [FastAPI](https://github.com/tiangolo/fastapi) + [ODMantic](https://github.com/art049/odmantic) codebase containing real world examples (CRUD, auth, advanced patterns, etc) that adheres to the [RealWorld](https://github.com/gothinkster/realworld) spec and API. 20 | 21 | [![CI](https://github.com/art049/fastapi-odmantic-realworld-example/actions/workflows/ci.yml/badge.svg)](https://github.com/art049/fastapi-odmantic-realworld-example/actions/workflows/ci.yml) 22 | [![Realworld Tests](https://github.com/art049/fastapi-odmantic-realworld-example/actions/workflows/realworld-tests.yml/badge.svg)](https://github.com/art049/fastapi-odmantic-realworld-example/actions/workflows/realworld-tests.yml) 23 | 24 | ## Getting Started 25 | 26 | ### :hammer: Installation 27 | 28 | - [Install Docker](https://docs.docker.com/engine/install/) (necessary to run a local MongoDB instance) 29 | - Make sure Python 3.10 is available on your system 30 | - Install [poetry](https://poetry.eustace.io/) 31 | - Setup the environment `./scripts/setup.sh` 32 | 33 | ### :bulb: Useful scripts 34 | 35 | - Start the MongoDB instance `./scripts/start-mongo.sh` 36 | - Stop the MongoDB instance `./scripts/stop-mongo.sh` 37 | - Start the FastAPI server `./scripts/start.sh` 38 | - Format the code `./scripts/format.sh` 39 | - Manually run the linter `./scripts/lint.sh` 40 | - Manually run the tests `./scripts/test.sh` 41 | 42 | ## Coming Soon 43 | 44 | - [ ] Articles with details on every single step required to build this app 45 | - [ ] Testing 46 | - [ ] Deployment on AWS with MongoDB Atlas 47 | -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/art049/fastapi-odmantic-realworld-example/54725da134654b6874e57dd021d8737770b4e175/logo.png -------------------------------------------------------------------------------- /mypy.ini: -------------------------------------------------------------------------------- 1 | [mypy] 2 | python_version=3.10 3 | pretty=True 4 | ignore_missing_imports = True 5 | namespace_packages=True 6 | disallow_untyped_defs=False 7 | check_untyped_defs=True 8 | # allow returning Any as a consequence of the options above 9 | warn_return_any=True 10 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. 2 | 3 | [[package]] 4 | name = "annotated-types" 5 | version = "0.6.0" 6 | description = "Reusable constraint types to use with typing.Annotated" 7 | optional = false 8 | python-versions = ">=3.8" 9 | files = [ 10 | {file = "annotated_types-0.6.0-py3-none-any.whl", hash = "sha256:0641064de18ba7a25dee8f96403ebc39113d0cb953a01429249d5c7564666a43"}, 11 | {file = "annotated_types-0.6.0.tar.gz", hash = "sha256:563339e807e53ffd9c267e99fc6d9ea23eb8443c08f112651963e24e22f84a5d"}, 12 | ] 13 | 14 | [[package]] 15 | name = "anyio" 16 | version = "3.7.1" 17 | description = "High level compatibility layer for multiple asynchronous event loop implementations" 18 | optional = false 19 | python-versions = ">=3.7" 20 | files = [ 21 | {file = "anyio-3.7.1-py3-none-any.whl", hash = "sha256:91dee416e570e92c64041bd18b900d1d6fa78dff7048769ce5ac5ddad004fbb5"}, 22 | {file = "anyio-3.7.1.tar.gz", hash = "sha256:44a3c9aba0f5defa43261a8b3efb97891f2bd7d804e0e1f56419befa1adfc780"}, 23 | ] 24 | 25 | [package.dependencies] 26 | idna = ">=2.8" 27 | sniffio = ">=1.1" 28 | 29 | [package.extras] 30 | doc = ["Sphinx", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme (>=1.2.2)", "sphinxcontrib-jquery"] 31 | test = ["anyio[trio]", "coverage[toml] (>=4.5)", "hypothesis (>=4.0)", "mock (>=4)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"] 32 | trio = ["trio (<0.22)"] 33 | 34 | [[package]] 35 | name = "bcrypt" 36 | version = "4.1.1" 37 | description = "Modern password hashing for your software and your servers" 38 | optional = false 39 | python-versions = ">=3.7" 40 | files = [ 41 | {file = "bcrypt-4.1.1-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:196008d91201bbb1aa4e666fee5e610face25d532e433a560cabb33bfdff958b"}, 42 | {file = "bcrypt-4.1.1-cp37-abi3-macosx_13_0_universal2.whl", hash = "sha256:2e197534c884336f9020c1f3a8efbaab0aa96fc798068cb2da9c671818b7fbb0"}, 43 | {file = "bcrypt-4.1.1-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d573885b637815a7f3a3cd5f87724d7d0822da64b0ab0aa7f7c78bae534e86dc"}, 44 | {file = "bcrypt-4.1.1-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bab33473f973e8058d1b2df8d6e095d237c49fbf7a02b527541a86a5d1dc4444"}, 45 | {file = "bcrypt-4.1.1-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:fb931cd004a7ad36a89789caf18a54c20287ec1cd62161265344b9c4554fdb2e"}, 46 | {file = "bcrypt-4.1.1-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:12f40f78dcba4aa7d1354d35acf45fae9488862a4fb695c7eeda5ace6aae273f"}, 47 | {file = "bcrypt-4.1.1-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:2ade10e8613a3b8446214846d3ddbd56cfe9205a7d64742f0b75458c868f7492"}, 48 | {file = "bcrypt-4.1.1-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:f33b385c3e80b5a26b3a5e148e6165f873c1c202423570fdf45fe34e00e5f3e5"}, 49 | {file = "bcrypt-4.1.1-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:755b9d27abcab678e0b8fb4d0abdebeea1f68dd1183b3f518bad8d31fa77d8be"}, 50 | {file = "bcrypt-4.1.1-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a7a7b8a87e51e5e8ca85b9fdaf3a5dc7aaf123365a09be7a27883d54b9a0c403"}, 51 | {file = "bcrypt-4.1.1-cp37-abi3-win32.whl", hash = "sha256:3d6c4e0d6963c52f8142cdea428e875042e7ce8c84812d8e5507bd1e42534e07"}, 52 | {file = "bcrypt-4.1.1-cp37-abi3-win_amd64.whl", hash = "sha256:14d41933510717f98aac63378b7956bbe548986e435df173c841d7f2bd0b2de7"}, 53 | {file = "bcrypt-4.1.1-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:24c2ebd287b5b11016f31d506ca1052d068c3f9dc817160628504690376ff050"}, 54 | {file = "bcrypt-4.1.1-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:476aa8e8aca554260159d4c7a97d6be529c8e177dbc1d443cb6b471e24e82c74"}, 55 | {file = "bcrypt-4.1.1-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:12611c4b0a8b1c461646228344784a1089bc0c49975680a2f54f516e71e9b79e"}, 56 | {file = "bcrypt-4.1.1-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c6450538a0fc32fb7ce4c6d511448c54c4ff7640b2ed81badf9898dcb9e5b737"}, 57 | {file = "bcrypt-4.1.1.tar.gz", hash = "sha256:df37f5418d4f1cdcff845f60e747a015389fa4e63703c918330865e06ad80007"}, 58 | ] 59 | 60 | [package.extras] 61 | tests = ["pytest (>=3.2.1,!=3.3.0)"] 62 | typecheck = ["mypy"] 63 | 64 | [[package]] 65 | name = "black" 66 | version = "22.12.0" 67 | description = "The uncompromising code formatter." 68 | optional = false 69 | python-versions = ">=3.7" 70 | files = [ 71 | {file = "black-22.12.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9eedd20838bd5d75b80c9f5487dbcb06836a43833a37846cf1d8c1cc01cef59d"}, 72 | {file = "black-22.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:159a46a4947f73387b4d83e87ea006dbb2337eab6c879620a3ba52699b1f4351"}, 73 | {file = "black-22.12.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d30b212bffeb1e252b31dd269dfae69dd17e06d92b87ad26e23890f3efea366f"}, 74 | {file = "black-22.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:7412e75863aa5c5411886804678b7d083c7c28421210180d67dfd8cf1221e1f4"}, 75 | {file = "black-22.12.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c116eed0efb9ff870ded8b62fe9f28dd61ef6e9ddd28d83d7d264a38417dcee2"}, 76 | {file = "black-22.12.0-cp37-cp37m-win_amd64.whl", hash = "sha256:1f58cbe16dfe8c12b7434e50ff889fa479072096d79f0a7f25e4ab8e94cd8350"}, 77 | {file = "black-22.12.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77d86c9f3db9b1bf6761244bc0b3572a546f5fe37917a044e02f3166d5aafa7d"}, 78 | {file = "black-22.12.0-cp38-cp38-win_amd64.whl", hash = "sha256:82d9fe8fee3401e02e79767016b4907820a7dc28d70d137eb397b92ef3cc5bfc"}, 79 | {file = "black-22.12.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:101c69b23df9b44247bd88e1d7e90154336ac4992502d4197bdac35dd7ee3320"}, 80 | {file = "black-22.12.0-cp39-cp39-win_amd64.whl", hash = "sha256:559c7a1ba9a006226f09e4916060982fd27334ae1998e7a38b3f33a37f7a2148"}, 81 | {file = "black-22.12.0-py3-none-any.whl", hash = "sha256:436cc9167dd28040ad90d3b404aec22cedf24a6e4d7de221bec2730ec0c97bcf"}, 82 | {file = "black-22.12.0.tar.gz", hash = "sha256:229351e5a18ca30f447bf724d007f890f97e13af070bb6ad4c0a441cd7596a2f"}, 83 | ] 84 | 85 | [package.dependencies] 86 | click = ">=8.0.0" 87 | mypy-extensions = ">=0.4.3" 88 | pathspec = ">=0.9.0" 89 | platformdirs = ">=2" 90 | 91 | [package.extras] 92 | colorama = ["colorama (>=0.4.3)"] 93 | d = ["aiohttp (>=3.7.4)"] 94 | jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] 95 | uvloop = ["uvloop (>=0.15.2)"] 96 | 97 | [[package]] 98 | name = "cffi" 99 | version = "1.16.0" 100 | description = "Foreign Function Interface for Python calling C code." 101 | optional = false 102 | python-versions = ">=3.8" 103 | files = [ 104 | {file = "cffi-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6b3d6606d369fc1da4fd8c357d026317fbb9c9b75d36dc16e90e84c26854b088"}, 105 | {file = "cffi-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ac0f5edd2360eea2f1daa9e26a41db02dd4b0451b48f7c318e217ee092a213e9"}, 106 | {file = "cffi-1.16.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7e61e3e4fa664a8588aa25c883eab612a188c725755afff6289454d6362b9673"}, 107 | {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a72e8961a86d19bdb45851d8f1f08b041ea37d2bd8d4fd19903bc3083d80c896"}, 108 | {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b50bf3f55561dac5438f8e70bfcdfd74543fd60df5fa5f62d94e5867deca684"}, 109 | {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7651c50c8c5ef7bdb41108b7b8c5a83013bfaa8a935590c5d74627c047a583c7"}, 110 | {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4108df7fe9b707191e55f33efbcb2d81928e10cea45527879a4749cbe472614"}, 111 | {file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:32c68ef735dbe5857c810328cb2481e24722a59a2003018885514d4c09af9743"}, 112 | {file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:673739cb539f8cdaa07d92d02efa93c9ccf87e345b9a0b556e3ecc666718468d"}, 113 | {file = "cffi-1.16.0-cp310-cp310-win32.whl", hash = "sha256:9f90389693731ff1f659e55c7d1640e2ec43ff725cc61b04b2f9c6d8d017df6a"}, 114 | {file = "cffi-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:e6024675e67af929088fda399b2094574609396b1decb609c55fa58b028a32a1"}, 115 | {file = "cffi-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b84834d0cf97e7d27dd5b7f3aca7b6e9263c56308ab9dc8aae9784abb774d404"}, 116 | {file = "cffi-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b8ebc27c014c59692bb2664c7d13ce7a6e9a629be20e54e7271fa696ff2b417"}, 117 | {file = "cffi-1.16.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ee07e47c12890ef248766a6e55bd38ebfb2bb8edd4142d56db91b21ea68b7627"}, 118 | {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8a9d3ebe49f084ad71f9269834ceccbf398253c9fac910c4fd7053ff1386936"}, 119 | {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e70f54f1796669ef691ca07d046cd81a29cb4deb1e5f942003f401c0c4a2695d"}, 120 | {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5bf44d66cdf9e893637896c7faa22298baebcd18d1ddb6d2626a6e39793a1d56"}, 121 | {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b78010e7b97fef4bee1e896df8a4bbb6712b7f05b7ef630f9d1da00f6444d2e"}, 122 | {file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c6a164aa47843fb1b01e941d385aab7215563bb8816d80ff3a363a9f8448a8dc"}, 123 | {file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e09f3ff613345df5e8c3667da1d918f9149bd623cd9070c983c013792a9a62eb"}, 124 | {file = "cffi-1.16.0-cp311-cp311-win32.whl", hash = "sha256:2c56b361916f390cd758a57f2e16233eb4f64bcbeee88a4881ea90fca14dc6ab"}, 125 | {file = "cffi-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:db8e577c19c0fda0beb7e0d4e09e0ba74b1e4c092e0e40bfa12fe05b6f6d75ba"}, 126 | {file = "cffi-1.16.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:fa3a0128b152627161ce47201262d3140edb5a5c3da88d73a1b790a959126956"}, 127 | {file = "cffi-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:68e7c44931cc171c54ccb702482e9fc723192e88d25a0e133edd7aff8fcd1f6e"}, 128 | {file = "cffi-1.16.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:abd808f9c129ba2beda4cfc53bde801e5bcf9d6e0f22f095e45327c038bfe68e"}, 129 | {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88e2b3c14bdb32e440be531ade29d3c50a1a59cd4e51b1dd8b0865c54ea5d2e2"}, 130 | {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcc8eb6d5902bb1cf6dc4f187ee3ea80a1eba0a89aba40a5cb20a5087d961357"}, 131 | {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b7be2d771cdba2942e13215c4e340bfd76398e9227ad10402a8767ab1865d2e6"}, 132 | {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e715596e683d2ce000574bae5d07bd522c781a822866c20495e52520564f0969"}, 133 | {file = "cffi-1.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2d92b25dbf6cae33f65005baf472d2c245c050b1ce709cc4588cdcdd5495b520"}, 134 | {file = "cffi-1.16.0-cp312-cp312-win32.whl", hash = "sha256:b2ca4e77f9f47c55c194982e10f058db063937845bb2b7a86c84a6cfe0aefa8b"}, 135 | {file = "cffi-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:68678abf380b42ce21a5f2abde8efee05c114c2fdb2e9eef2efdb0257fba1235"}, 136 | {file = "cffi-1.16.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0c9ef6ff37e974b73c25eecc13952c55bceed9112be2d9d938ded8e856138bcc"}, 137 | {file = "cffi-1.16.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a09582f178759ee8128d9270cd1344154fd473bb77d94ce0aeb2a93ebf0feaf0"}, 138 | {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e760191dd42581e023a68b758769e2da259b5d52e3103c6060ddc02c9edb8d7b"}, 139 | {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:80876338e19c951fdfed6198e70bc88f1c9758b94578d5a7c4c91a87af3cf31c"}, 140 | {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a6a14b17d7e17fa0d207ac08642c8820f84f25ce17a442fd15e27ea18d67c59b"}, 141 | {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6602bc8dc6f3a9e02b6c22c4fc1e47aa50f8f8e6d3f78a5e16ac33ef5fefa324"}, 142 | {file = "cffi-1.16.0-cp38-cp38-win32.whl", hash = "sha256:131fd094d1065b19540c3d72594260f118b231090295d8c34e19a7bbcf2e860a"}, 143 | {file = "cffi-1.16.0-cp38-cp38-win_amd64.whl", hash = "sha256:31d13b0f99e0836b7ff893d37af07366ebc90b678b6664c955b54561fc36ef36"}, 144 | {file = "cffi-1.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:582215a0e9adbe0e379761260553ba11c58943e4bbe9c36430c4ca6ac74b15ed"}, 145 | {file = "cffi-1.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b29ebffcf550f9da55bec9e02ad430c992a87e5f512cd63388abb76f1036d8d2"}, 146 | {file = "cffi-1.16.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dc9b18bf40cc75f66f40a7379f6a9513244fe33c0e8aa72e2d56b0196a7ef872"}, 147 | {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cb4a35b3642fc5c005a6755a5d17c6c8b6bcb6981baf81cea8bfbc8903e8ba8"}, 148 | {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b86851a328eedc692acf81fb05444bdf1891747c25af7529e39ddafaf68a4f3f"}, 149 | {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c0f31130ebc2d37cdd8e44605fb5fa7ad59049298b3f745c74fa74c62fbfcfc4"}, 150 | {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f8e709127c6c77446a8c0a8c8bf3c8ee706a06cd44b1e827c3e6a2ee6b8c098"}, 151 | {file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:748dcd1e3d3d7cd5443ef03ce8685043294ad6bd7c02a38d1bd367cfd968e000"}, 152 | {file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8895613bcc094d4a1b2dbe179d88d7fb4a15cee43c052e8885783fac397d91fe"}, 153 | {file = "cffi-1.16.0-cp39-cp39-win32.whl", hash = "sha256:ed86a35631f7bfbb28e108dd96773b9d5a6ce4811cf6ea468bb6a359b256b1e4"}, 154 | {file = "cffi-1.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:3686dffb02459559c74dd3d81748269ffb0eb027c39a6fc99502de37d501faa8"}, 155 | {file = "cffi-1.16.0.tar.gz", hash = "sha256:bcb3ef43e58665bbda2fb198698fcae6776483e0c4a631aa5647806c25e02cc0"}, 156 | ] 157 | 158 | [package.dependencies] 159 | pycparser = "*" 160 | 161 | [[package]] 162 | name = "cfgv" 163 | version = "3.4.0" 164 | description = "Validate configuration and produce human readable error messages." 165 | optional = false 166 | python-versions = ">=3.8" 167 | files = [ 168 | {file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"}, 169 | {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"}, 170 | ] 171 | 172 | [[package]] 173 | name = "click" 174 | version = "8.1.7" 175 | description = "Composable command line interface toolkit" 176 | optional = false 177 | python-versions = ">=3.7" 178 | files = [ 179 | {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, 180 | {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, 181 | ] 182 | 183 | [package.dependencies] 184 | colorama = {version = "*", markers = "platform_system == \"Windows\""} 185 | 186 | [[package]] 187 | name = "colorama" 188 | version = "0.4.6" 189 | description = "Cross-platform colored terminal text." 190 | optional = false 191 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" 192 | files = [ 193 | {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, 194 | {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, 195 | ] 196 | 197 | [[package]] 198 | name = "cryptography" 199 | version = "41.0.7" 200 | description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." 201 | optional = false 202 | python-versions = ">=3.7" 203 | files = [ 204 | {file = "cryptography-41.0.7-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:3c78451b78313fa81607fa1b3f1ae0a5ddd8014c38a02d9db0616133987b9cdf"}, 205 | {file = "cryptography-41.0.7-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:928258ba5d6f8ae644e764d0f996d61a8777559f72dfeb2eea7e2fe0ad6e782d"}, 206 | {file = "cryptography-41.0.7-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5a1b41bc97f1ad230a41657d9155113c7521953869ae57ac39ac7f1bb471469a"}, 207 | {file = "cryptography-41.0.7-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:841df4caa01008bad253bce2a6f7b47f86dc9f08df4b433c404def869f590a15"}, 208 | {file = "cryptography-41.0.7-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:5429ec739a29df2e29e15d082f1d9ad683701f0ec7709ca479b3ff2708dae65a"}, 209 | {file = "cryptography-41.0.7-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:43f2552a2378b44869fe8827aa19e69512e3245a219104438692385b0ee119d1"}, 210 | {file = "cryptography-41.0.7-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:af03b32695b24d85a75d40e1ba39ffe7db7ffcb099fe507b39fd41a565f1b157"}, 211 | {file = "cryptography-41.0.7-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:49f0805fc0b2ac8d4882dd52f4a3b935b210935d500b6b805f321addc8177406"}, 212 | {file = "cryptography-41.0.7-cp37-abi3-win32.whl", hash = "sha256:f983596065a18a2183e7f79ab3fd4c475205b839e02cbc0efbbf9666c4b3083d"}, 213 | {file = "cryptography-41.0.7-cp37-abi3-win_amd64.whl", hash = "sha256:90452ba79b8788fa380dfb587cca692976ef4e757b194b093d845e8d99f612f2"}, 214 | {file = "cryptography-41.0.7-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:079b85658ea2f59c4f43b70f8119a52414cdb7be34da5d019a77bf96d473b960"}, 215 | {file = "cryptography-41.0.7-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:b640981bf64a3e978a56167594a0e97db71c89a479da8e175d8bb5be5178c003"}, 216 | {file = "cryptography-41.0.7-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:e3114da6d7f95d2dee7d3f4eec16dacff819740bbab931aff8648cb13c5ff5e7"}, 217 | {file = "cryptography-41.0.7-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d5ec85080cce7b0513cfd233914eb8b7bbd0633f1d1703aa28d1dd5a72f678ec"}, 218 | {file = "cryptography-41.0.7-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:7a698cb1dac82c35fcf8fe3417a3aaba97de16a01ac914b89a0889d364d2f6be"}, 219 | {file = "cryptography-41.0.7-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:37a138589b12069efb424220bf78eac59ca68b95696fc622b6ccc1c0a197204a"}, 220 | {file = "cryptography-41.0.7-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:68a2dec79deebc5d26d617bfdf6e8aab065a4f34934b22d3b5010df3ba36612c"}, 221 | {file = "cryptography-41.0.7-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:09616eeaef406f99046553b8a40fbf8b1e70795a91885ba4c96a70793de5504a"}, 222 | {file = "cryptography-41.0.7-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:48a0476626da912a44cc078f9893f292f0b3e4c739caf289268168d8f4702a39"}, 223 | {file = "cryptography-41.0.7-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c7f3201ec47d5207841402594f1d7950879ef890c0c495052fa62f58283fde1a"}, 224 | {file = "cryptography-41.0.7-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c5ca78485a255e03c32b513f8c2bc39fedb7f5c5f8535545bdc223a03b24f248"}, 225 | {file = "cryptography-41.0.7-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:d6c391c021ab1f7a82da5d8d0b3cee2f4b2c455ec86c8aebbc84837a631ff309"}, 226 | {file = "cryptography-41.0.7.tar.gz", hash = "sha256:13f93ce9bea8016c253b34afc6bd6a75993e5c40672ed5405a9c832f0d4a00bc"}, 227 | ] 228 | 229 | [package.dependencies] 230 | cffi = ">=1.12" 231 | 232 | [package.extras] 233 | docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=1.1.1)"] 234 | docstest = ["pyenchant (>=1.6.11)", "sphinxcontrib-spelling (>=4.0.1)", "twine (>=1.12.0)"] 235 | nox = ["nox"] 236 | pep8test = ["black", "check-sdist", "mypy", "ruff"] 237 | sdist = ["build"] 238 | ssh = ["bcrypt (>=3.1.5)"] 239 | test = ["pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"] 240 | test-randomorder = ["pytest-randomly"] 241 | 242 | [[package]] 243 | name = "distlib" 244 | version = "0.3.7" 245 | description = "Distribution utilities" 246 | optional = false 247 | python-versions = "*" 248 | files = [ 249 | {file = "distlib-0.3.7-py2.py3-none-any.whl", hash = "sha256:2e24928bc811348f0feb63014e97aaae3037f2cf48712d51ae61df7fd6075057"}, 250 | {file = "distlib-0.3.7.tar.gz", hash = "sha256:9dafe54b34a028eafd95039d5e5d4851a13734540f1331060d31c9916e7147a8"}, 251 | ] 252 | 253 | [[package]] 254 | name = "dnspython" 255 | version = "2.4.2" 256 | description = "DNS toolkit" 257 | optional = false 258 | python-versions = ">=3.8,<4.0" 259 | files = [ 260 | {file = "dnspython-2.4.2-py3-none-any.whl", hash = "sha256:57c6fbaaeaaf39c891292012060beb141791735dbb4004798328fc2c467402d8"}, 261 | {file = "dnspython-2.4.2.tar.gz", hash = "sha256:8dcfae8c7460a2f84b4072e26f1c9f4101ca20c071649cb7c34e8b6a93d58984"}, 262 | ] 263 | 264 | [package.extras] 265 | dnssec = ["cryptography (>=2.6,<42.0)"] 266 | doh = ["h2 (>=4.1.0)", "httpcore (>=0.17.3)", "httpx (>=0.24.1)"] 267 | doq = ["aioquic (>=0.9.20)"] 268 | idna = ["idna (>=2.1,<4.0)"] 269 | trio = ["trio (>=0.14,<0.23)"] 270 | wmi = ["wmi (>=1.5.1,<2.0.0)"] 271 | 272 | [[package]] 273 | name = "ecdsa" 274 | version = "0.18.0" 275 | description = "ECDSA cryptographic signature library (pure python)" 276 | optional = false 277 | python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" 278 | files = [ 279 | {file = "ecdsa-0.18.0-py2.py3-none-any.whl", hash = "sha256:80600258e7ed2f16b9aa1d7c295bd70194109ad5a30fdee0eaeefef1d4c559dd"}, 280 | {file = "ecdsa-0.18.0.tar.gz", hash = "sha256:190348041559e21b22a1d65cee485282ca11a6f81d503fddb84d5017e9ed1e49"}, 281 | ] 282 | 283 | [package.dependencies] 284 | six = ">=1.9.0" 285 | 286 | [package.extras] 287 | gmpy = ["gmpy"] 288 | gmpy2 = ["gmpy2"] 289 | 290 | [[package]] 291 | name = "fastapi" 292 | version = "0.104.1" 293 | description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" 294 | optional = false 295 | python-versions = ">=3.8" 296 | files = [ 297 | {file = "fastapi-0.104.1-py3-none-any.whl", hash = "sha256:752dc31160cdbd0436bb93bad51560b57e525cbb1d4bbf6f4904ceee75548241"}, 298 | {file = "fastapi-0.104.1.tar.gz", hash = "sha256:e5e4540a7c5e1dcfbbcf5b903c234feddcdcd881f191977a1c5dfd917487e7ae"}, 299 | ] 300 | 301 | [package.dependencies] 302 | anyio = ">=3.7.1,<4.0.0" 303 | pydantic = ">=1.7.4,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0 || >2.0.0,<2.0.1 || >2.0.1,<2.1.0 || >2.1.0,<3.0.0" 304 | starlette = ">=0.27.0,<0.28.0" 305 | typing-extensions = ">=4.8.0" 306 | 307 | [package.extras] 308 | all = ["email-validator (>=2.0.0)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=2.11.2)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.5)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"] 309 | 310 | [[package]] 311 | name = "filelock" 312 | version = "3.13.1" 313 | description = "A platform independent file lock." 314 | optional = false 315 | python-versions = ">=3.8" 316 | files = [ 317 | {file = "filelock-3.13.1-py3-none-any.whl", hash = "sha256:57dbda9b35157b05fb3e58ee91448612eb674172fab98ee235ccb0b5bee19a1c"}, 318 | {file = "filelock-3.13.1.tar.gz", hash = "sha256:521f5f56c50f8426f5e03ad3b281b490a87ef15bc6c526f168290f0c7148d44e"}, 319 | ] 320 | 321 | [package.extras] 322 | docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.24)"] 323 | testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)"] 324 | typing = ["typing-extensions (>=4.8)"] 325 | 326 | [[package]] 327 | name = "flake8" 328 | version = "5.0.4" 329 | description = "the modular source code checker: pep8 pyflakes and co" 330 | optional = false 331 | python-versions = ">=3.6.1" 332 | files = [ 333 | {file = "flake8-5.0.4-py2.py3-none-any.whl", hash = "sha256:7a1cf6b73744f5806ab95e526f6f0d8c01c66d7bbe349562d22dfca20610b248"}, 334 | {file = "flake8-5.0.4.tar.gz", hash = "sha256:6fbe320aad8d6b95cec8b8e47bc933004678dc63095be98528b7bdd2a9f510db"}, 335 | ] 336 | 337 | [package.dependencies] 338 | mccabe = ">=0.7.0,<0.8.0" 339 | pycodestyle = ">=2.9.0,<2.10.0" 340 | pyflakes = ">=2.5.0,<2.6.0" 341 | 342 | [[package]] 343 | name = "h11" 344 | version = "0.14.0" 345 | description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" 346 | optional = false 347 | python-versions = ">=3.7" 348 | files = [ 349 | {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, 350 | {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, 351 | ] 352 | 353 | [[package]] 354 | name = "identify" 355 | version = "2.5.33" 356 | description = "File identification library for Python" 357 | optional = false 358 | python-versions = ">=3.8" 359 | files = [ 360 | {file = "identify-2.5.33-py2.py3-none-any.whl", hash = "sha256:d40ce5fcd762817627670da8a7d8d8e65f24342d14539c59488dc603bf662e34"}, 361 | {file = "identify-2.5.33.tar.gz", hash = "sha256:161558f9fe4559e1557e1bff323e8631f6a0e4837f7497767c1782832f16b62d"}, 362 | ] 363 | 364 | [package.extras] 365 | license = ["ukkonen"] 366 | 367 | [[package]] 368 | name = "idna" 369 | version = "3.6" 370 | description = "Internationalized Domain Names in Applications (IDNA)" 371 | optional = false 372 | python-versions = ">=3.5" 373 | files = [ 374 | {file = "idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f"}, 375 | {file = "idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca"}, 376 | ] 377 | 378 | [[package]] 379 | name = "iniconfig" 380 | version = "2.0.0" 381 | description = "brain-dead simple config-ini parsing" 382 | optional = false 383 | python-versions = ">=3.7" 384 | files = [ 385 | {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, 386 | {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, 387 | ] 388 | 389 | [[package]] 390 | name = "isort" 391 | version = "5.13.1" 392 | description = "A Python utility / library to sort Python imports." 393 | optional = false 394 | python-versions = ">=3.8.0" 395 | files = [ 396 | {file = "isort-5.13.1-py3-none-any.whl", hash = "sha256:56a51732c25f94ca96f6721be206dd96a95f42950502eb26c1015d333bc6edb7"}, 397 | {file = "isort-5.13.1.tar.gz", hash = "sha256:aaed790b463e8703fb1eddb831dfa8e8616bacde2c083bd557ef73c8189b7263"}, 398 | ] 399 | 400 | [[package]] 401 | name = "mccabe" 402 | version = "0.7.0" 403 | description = "McCabe checker, plugin for flake8" 404 | optional = false 405 | python-versions = ">=3.6" 406 | files = [ 407 | {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, 408 | {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, 409 | ] 410 | 411 | [[package]] 412 | name = "motor" 413 | version = "3.3.2" 414 | description = "Non-blocking MongoDB driver for Tornado or asyncio" 415 | optional = false 416 | python-versions = ">=3.7" 417 | files = [ 418 | {file = "motor-3.3.2-py3-none-any.whl", hash = "sha256:6fe7e6f0c4f430b9e030b9d22549b732f7c2226af3ab71ecc309e4a1b7d19953"}, 419 | {file = "motor-3.3.2.tar.gz", hash = "sha256:d2fc38de15f1c8058f389c1a44a4d4105c0405c48c061cd492a654496f7bc26a"}, 420 | ] 421 | 422 | [package.dependencies] 423 | pymongo = ">=4.5,<5" 424 | 425 | [package.extras] 426 | aws = ["pymongo[aws] (>=4.5,<5)"] 427 | encryption = ["pymongo[encryption] (>=4.5,<5)"] 428 | gssapi = ["pymongo[gssapi] (>=4.5,<5)"] 429 | ocsp = ["pymongo[ocsp] (>=4.5,<5)"] 430 | snappy = ["pymongo[snappy] (>=4.5,<5)"] 431 | srv = ["pymongo[srv] (>=4.5,<5)"] 432 | test = ["aiohttp (<3.8.6)", "mockupdb", "motor[encryption]", "pytest (>=7)", "tornado (>=5)"] 433 | zstd = ["pymongo[zstd] (>=4.5,<5)"] 434 | 435 | [[package]] 436 | name = "mypy" 437 | version = "0.971" 438 | description = "Optional static typing for Python" 439 | optional = false 440 | python-versions = ">=3.6" 441 | files = [ 442 | {file = "mypy-0.971-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f2899a3cbd394da157194f913a931edfd4be5f274a88041c9dc2d9cdcb1c315c"}, 443 | {file = "mypy-0.971-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:98e02d56ebe93981c41211c05adb630d1d26c14195d04d95e49cd97dbc046dc5"}, 444 | {file = "mypy-0.971-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:19830b7dba7d5356d3e26e2427a2ec91c994cd92d983142cbd025ebe81d69cf3"}, 445 | {file = "mypy-0.971-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:02ef476f6dcb86e6f502ae39a16b93285fef97e7f1ff22932b657d1ef1f28655"}, 446 | {file = "mypy-0.971-cp310-cp310-win_amd64.whl", hash = "sha256:25c5750ba5609a0c7550b73a33deb314ecfb559c350bb050b655505e8aed4103"}, 447 | {file = "mypy-0.971-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:d3348e7eb2eea2472db611486846742d5d52d1290576de99d59edeb7cd4a42ca"}, 448 | {file = "mypy-0.971-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:3fa7a477b9900be9b7dd4bab30a12759e5abe9586574ceb944bc29cddf8f0417"}, 449 | {file = "mypy-0.971-cp36-cp36m-win_amd64.whl", hash = "sha256:2ad53cf9c3adc43cf3bea0a7d01a2f2e86db9fe7596dfecb4496a5dda63cbb09"}, 450 | {file = "mypy-0.971-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:855048b6feb6dfe09d3353466004490b1872887150c5bb5caad7838b57328cc8"}, 451 | {file = "mypy-0.971-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:23488a14a83bca6e54402c2e6435467a4138785df93ec85aeff64c6170077fb0"}, 452 | {file = "mypy-0.971-cp37-cp37m-win_amd64.whl", hash = "sha256:4b21e5b1a70dfb972490035128f305c39bc4bc253f34e96a4adf9127cf943eb2"}, 453 | {file = "mypy-0.971-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:9796a2ba7b4b538649caa5cecd398d873f4022ed2333ffde58eaf604c4d2cb27"}, 454 | {file = "mypy-0.971-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:5a361d92635ad4ada1b1b2d3630fc2f53f2127d51cf2def9db83cba32e47c856"}, 455 | {file = "mypy-0.971-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b793b899f7cf563b1e7044a5c97361196b938e92f0a4343a5d27966a53d2ec71"}, 456 | {file = "mypy-0.971-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:d1ea5d12c8e2d266b5fb8c7a5d2e9c0219fedfeb493b7ed60cd350322384ac27"}, 457 | {file = "mypy-0.971-cp38-cp38-win_amd64.whl", hash = "sha256:23c7ff43fff4b0df93a186581885c8512bc50fc4d4910e0f838e35d6bb6b5e58"}, 458 | {file = "mypy-0.971-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:1f7656b69974a6933e987ee8ffb951d836272d6c0f81d727f1d0e2696074d9e6"}, 459 | {file = "mypy-0.971-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d2022bfadb7a5c2ef410d6a7c9763188afdb7f3533f22a0a32be10d571ee4bbe"}, 460 | {file = "mypy-0.971-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ef943c72a786b0f8d90fd76e9b39ce81fb7171172daf84bf43eaf937e9f220a9"}, 461 | {file = "mypy-0.971-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:d744f72eb39f69312bc6c2abf8ff6656973120e2eb3f3ec4f758ed47e414a4bf"}, 462 | {file = "mypy-0.971-cp39-cp39-win_amd64.whl", hash = "sha256:77a514ea15d3007d33a9e2157b0ba9c267496acf12a7f2b9b9f8446337aac5b0"}, 463 | {file = "mypy-0.971-py3-none-any.whl", hash = "sha256:0d054ef16b071149917085f51f89555a576e2618d5d9dd70bd6eea6410af3ac9"}, 464 | {file = "mypy-0.971.tar.gz", hash = "sha256:40b0f21484238269ae6a57200c807d80debc6459d444c0489a102d7c6a75fa56"}, 465 | ] 466 | 467 | [package.dependencies] 468 | mypy-extensions = ">=0.4.3" 469 | typing-extensions = ">=3.10" 470 | 471 | [package.extras] 472 | dmypy = ["psutil (>=4.0)"] 473 | python2 = ["typed-ast (>=1.4.0,<2)"] 474 | reports = ["lxml"] 475 | 476 | [[package]] 477 | name = "mypy-extensions" 478 | version = "1.0.0" 479 | description = "Type system extensions for programs checked with the mypy type checker." 480 | optional = false 481 | python-versions = ">=3.5" 482 | files = [ 483 | {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, 484 | {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, 485 | ] 486 | 487 | [[package]] 488 | name = "nodeenv" 489 | version = "1.8.0" 490 | description = "Node.js virtual environment builder" 491 | optional = false 492 | python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*" 493 | files = [ 494 | {file = "nodeenv-1.8.0-py2.py3-none-any.whl", hash = "sha256:df865724bb3c3adc86b3876fa209771517b0cfe596beff01a92700e0e8be4cec"}, 495 | {file = "nodeenv-1.8.0.tar.gz", hash = "sha256:d51e0c37e64fbf47d017feac3145cdbb58836d7eee8c6f6d3b6880c5456227d2"}, 496 | ] 497 | 498 | [package.dependencies] 499 | setuptools = "*" 500 | 501 | [[package]] 502 | name = "odmantic" 503 | version = "0.9.2" 504 | description = "ODMantic, an AsyncIO MongoDB Object Document Mapper for Python using type hints" 505 | optional = false 506 | python-versions = ">=3.8" 507 | files = [] 508 | develop = true 509 | 510 | [package.dependencies] 511 | motor = ">=3.1.1" 512 | pydantic = ">=2.5.2" 513 | pymongo = ">=4.1.0" 514 | 515 | [package.extras] 516 | dev = ["ipython (>=7.16.1,<7.17.0)"] 517 | doc = ["mkdocs-macros-plugin (>=1.0.4,<1.1.0)", "mkdocs-material (>=9.5.2,<9.6.0)", "mkdocstrings[python] (>=0.24.0,<0.25.0)", "pydocstyle[toml] (>=6.3.0,<6.4.0)"] 518 | fastapi = ["fastapi (>=0.100.0)"] 519 | test = ["async-asgi-testclient (>=1.4.11,<1.5.0)", "asyncmock (>=0.4.2,<0.5.0)", "coverage[toml] (>=6.2,<7.0)", "darglint (>=1.8.1,<1.9.0)", "fastapi (>=0.104.0)", "httpx (>=0.24.1,<0.25.0)", "inline-snapshot (>=0.6.0,<0.7.0)", "mypy (>=1.4.1,<1.5.0)", "pytest (>=7.0,<8.0)", "pytest-asyncio (>=0.16.0,<0.17.0)", "pytest-benchmark (>=4.0.0,<4.1.0)", "pytest-codspeed (>=2.1.0,<2.2.0)", "pytest-sugar (>=0.9.5,<0.10.0)", "pytest-xdist (>=2.1.0,<2.2.0)", "pytz (>=2023.3,<2024.0)", "requests (>=2.24,<3.0)", "ruff (>=0.0.277,<0.1.0)", "semver (>=2.13.0,<2.14.0)", "typer (>=0.4.1,<0.5.0)", "types-pytz (>=2023.3.0.0,<2023.4.0.0)", "uvicorn (>=0.17.0,<0.18.0)"] 520 | 521 | [package.source] 522 | type = "directory" 523 | url = "../odmantic" 524 | 525 | [[package]] 526 | name = "packaging" 527 | version = "23.2" 528 | description = "Core utilities for Python packages" 529 | optional = false 530 | python-versions = ">=3.7" 531 | files = [ 532 | {file = "packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"}, 533 | {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"}, 534 | ] 535 | 536 | [[package]] 537 | name = "passlib" 538 | version = "1.7.4" 539 | description = "comprehensive password hashing framework supporting over 30 schemes" 540 | optional = false 541 | python-versions = "*" 542 | files = [ 543 | {file = "passlib-1.7.4-py2.py3-none-any.whl", hash = "sha256:aa6bca462b8d8bda89c70b382f0c298a20b5560af6cbfa2dce410c0a2fb669f1"}, 544 | {file = "passlib-1.7.4.tar.gz", hash = "sha256:defd50f72b65c5402ab2c573830a6978e5f202ad0d984793c8dde2c4152ebe04"}, 545 | ] 546 | 547 | [package.dependencies] 548 | bcrypt = {version = ">=3.1.0", optional = true, markers = "extra == \"bcrypt\""} 549 | 550 | [package.extras] 551 | argon2 = ["argon2-cffi (>=18.2.0)"] 552 | bcrypt = ["bcrypt (>=3.1.0)"] 553 | build-docs = ["cloud-sptheme (>=1.10.1)", "sphinx (>=1.6)", "sphinxcontrib-fulltoc (>=1.2.0)"] 554 | totp = ["cryptography"] 555 | 556 | [[package]] 557 | name = "pathspec" 558 | version = "0.12.1" 559 | description = "Utility library for gitignore style pattern matching of file paths." 560 | optional = false 561 | python-versions = ">=3.8" 562 | files = [ 563 | {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, 564 | {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, 565 | ] 566 | 567 | [[package]] 568 | name = "platformdirs" 569 | version = "4.1.0" 570 | description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." 571 | optional = false 572 | python-versions = ">=3.8" 573 | files = [ 574 | {file = "platformdirs-4.1.0-py3-none-any.whl", hash = "sha256:11c8f37bcca40db96d8144522d925583bdb7a31f7b0e37e3ed4318400a8e2380"}, 575 | {file = "platformdirs-4.1.0.tar.gz", hash = "sha256:906d548203468492d432bcb294d4bc2fff751bf84971fbb2c10918cc206ee420"}, 576 | ] 577 | 578 | [package.extras] 579 | docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.1)", "sphinx-autodoc-typehints (>=1.24)"] 580 | test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4)", "pytest-cov (>=4.1)", "pytest-mock (>=3.11.1)"] 581 | 582 | [[package]] 583 | name = "pluggy" 584 | version = "1.3.0" 585 | description = "plugin and hook calling mechanisms for python" 586 | optional = false 587 | python-versions = ">=3.8" 588 | files = [ 589 | {file = "pluggy-1.3.0-py3-none-any.whl", hash = "sha256:d89c696a773f8bd377d18e5ecda92b7a3793cbe66c87060a6fb58c7b6e1061f7"}, 590 | {file = "pluggy-1.3.0.tar.gz", hash = "sha256:cf61ae8f126ac6f7c451172cf30e3e43d3ca77615509771b3a984a0730651e12"}, 591 | ] 592 | 593 | [package.extras] 594 | dev = ["pre-commit", "tox"] 595 | testing = ["pytest", "pytest-benchmark"] 596 | 597 | [[package]] 598 | name = "pre-commit" 599 | version = "2.21.0" 600 | description = "A framework for managing and maintaining multi-language pre-commit hooks." 601 | optional = false 602 | python-versions = ">=3.7" 603 | files = [ 604 | {file = "pre_commit-2.21.0-py2.py3-none-any.whl", hash = "sha256:e2f91727039fc39a92f58a588a25b87f936de6567eed4f0e673e0507edc75bad"}, 605 | {file = "pre_commit-2.21.0.tar.gz", hash = "sha256:31ef31af7e474a8d8995027fefdfcf509b5c913ff31f2015b4ec4beb26a6f658"}, 606 | ] 607 | 608 | [package.dependencies] 609 | cfgv = ">=2.0.0" 610 | identify = ">=1.0.0" 611 | nodeenv = ">=0.11.1" 612 | pyyaml = ">=5.1" 613 | virtualenv = ">=20.10.0" 614 | 615 | [[package]] 616 | name = "pyasn1" 617 | version = "0.5.1" 618 | description = "Pure-Python implementation of ASN.1 types and DER/BER/CER codecs (X.208)" 619 | optional = false 620 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" 621 | files = [ 622 | {file = "pyasn1-0.5.1-py2.py3-none-any.whl", hash = "sha256:4439847c58d40b1d0a573d07e3856e95333f1976294494c325775aeca506eb58"}, 623 | {file = "pyasn1-0.5.1.tar.gz", hash = "sha256:6d391a96e59b23130a5cfa74d6fd7f388dbbe26cc8f1edf39fdddf08d9d6676c"}, 624 | ] 625 | 626 | [[package]] 627 | name = "pycodestyle" 628 | version = "2.9.1" 629 | description = "Python style guide checker" 630 | optional = false 631 | python-versions = ">=3.6" 632 | files = [ 633 | {file = "pycodestyle-2.9.1-py2.py3-none-any.whl", hash = "sha256:d1735fc58b418fd7c5f658d28d943854f8a849b01a5d0a1e6f3f3fdd0166804b"}, 634 | {file = "pycodestyle-2.9.1.tar.gz", hash = "sha256:2c9607871d58c76354b697b42f5d57e1ada7d261c261efac224b664affdc5785"}, 635 | ] 636 | 637 | [[package]] 638 | name = "pycparser" 639 | version = "2.21" 640 | description = "C parser in Python" 641 | optional = false 642 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 643 | files = [ 644 | {file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"}, 645 | {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"}, 646 | ] 647 | 648 | [[package]] 649 | name = "pydantic" 650 | version = "2.5.2" 651 | description = "Data validation using Python type hints" 652 | optional = false 653 | python-versions = ">=3.7" 654 | files = [ 655 | {file = "pydantic-2.5.2-py3-none-any.whl", hash = "sha256:80c50fb8e3dcecfddae1adbcc00ec5822918490c99ab31f6cf6140ca1c1429f0"}, 656 | {file = "pydantic-2.5.2.tar.gz", hash = "sha256:ff177ba64c6faf73d7afa2e8cad38fd456c0dbe01c9954e71038001cd15a6edd"}, 657 | ] 658 | 659 | [package.dependencies] 660 | annotated-types = ">=0.4.0" 661 | pydantic-core = "2.14.5" 662 | typing-extensions = ">=4.6.1" 663 | 664 | [package.extras] 665 | email = ["email-validator (>=2.0.0)"] 666 | 667 | [[package]] 668 | name = "pydantic-core" 669 | version = "2.14.5" 670 | description = "" 671 | optional = false 672 | python-versions = ">=3.7" 673 | files = [ 674 | {file = "pydantic_core-2.14.5-cp310-cp310-macosx_10_7_x86_64.whl", hash = "sha256:7e88f5696153dc516ba6e79f82cc4747e87027205f0e02390c21f7cb3bd8abfd"}, 675 | {file = "pydantic_core-2.14.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4641e8ad4efb697f38a9b64ca0523b557c7931c5f84e0fd377a9a3b05121f0de"}, 676 | {file = "pydantic_core-2.14.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:774de879d212db5ce02dfbf5b0da9a0ea386aeba12b0b95674a4ce0593df3d07"}, 677 | {file = "pydantic_core-2.14.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ebb4e035e28f49b6f1a7032920bb9a0c064aedbbabe52c543343d39341a5b2a3"}, 678 | {file = "pydantic_core-2.14.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b53e9ad053cd064f7e473a5f29b37fc4cc9dc6d35f341e6afc0155ea257fc911"}, 679 | {file = "pydantic_core-2.14.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8aa1768c151cf562a9992462239dfc356b3d1037cc5a3ac829bb7f3bda7cc1f9"}, 680 | {file = "pydantic_core-2.14.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eac5c82fc632c599f4639a5886f96867ffced74458c7db61bc9a66ccb8ee3113"}, 681 | {file = "pydantic_core-2.14.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d2ae91f50ccc5810b2f1b6b858257c9ad2e08da70bf890dee02de1775a387c66"}, 682 | {file = "pydantic_core-2.14.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:6b9ff467ffbab9110e80e8c8de3bcfce8e8b0fd5661ac44a09ae5901668ba997"}, 683 | {file = "pydantic_core-2.14.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:61ea96a78378e3bd5a0be99b0e5ed00057b71f66115f5404d0dae4819f495093"}, 684 | {file = "pydantic_core-2.14.5-cp310-none-win32.whl", hash = "sha256:bb4c2eda937a5e74c38a41b33d8c77220380a388d689bcdb9b187cf6224c9720"}, 685 | {file = "pydantic_core-2.14.5-cp310-none-win_amd64.whl", hash = "sha256:b7851992faf25eac90bfcb7bfd19e1f5ffa00afd57daec8a0042e63c74a4551b"}, 686 | {file = "pydantic_core-2.14.5-cp311-cp311-macosx_10_7_x86_64.whl", hash = "sha256:4e40f2bd0d57dac3feb3a3aed50f17d83436c9e6b09b16af271b6230a2915459"}, 687 | {file = "pydantic_core-2.14.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ab1cdb0f14dc161ebc268c09db04d2c9e6f70027f3b42446fa11c153521c0e88"}, 688 | {file = "pydantic_core-2.14.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aae7ea3a1c5bb40c93cad361b3e869b180ac174656120c42b9fadebf685d121b"}, 689 | {file = "pydantic_core-2.14.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:60b7607753ba62cf0739177913b858140f11b8af72f22860c28eabb2f0a61937"}, 690 | {file = "pydantic_core-2.14.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2248485b0322c75aee7565d95ad0e16f1c67403a470d02f94da7344184be770f"}, 691 | {file = "pydantic_core-2.14.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:823fcc638f67035137a5cd3f1584a4542d35a951c3cc68c6ead1df7dac825c26"}, 692 | {file = "pydantic_core-2.14.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:96581cfefa9123accc465a5fd0cc833ac4d75d55cc30b633b402e00e7ced00a6"}, 693 | {file = "pydantic_core-2.14.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a33324437018bf6ba1bb0f921788788641439e0ed654b233285b9c69704c27b4"}, 694 | {file = "pydantic_core-2.14.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:9bd18fee0923ca10f9a3ff67d4851c9d3e22b7bc63d1eddc12f439f436f2aada"}, 695 | {file = "pydantic_core-2.14.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:853a2295c00f1d4429db4c0fb9475958543ee80cfd310814b5c0ef502de24dda"}, 696 | {file = "pydantic_core-2.14.5-cp311-none-win32.whl", hash = "sha256:cb774298da62aea5c80a89bd58c40205ab4c2abf4834453b5de207d59d2e1651"}, 697 | {file = "pydantic_core-2.14.5-cp311-none-win_amd64.whl", hash = "sha256:e87fc540c6cac7f29ede02e0f989d4233f88ad439c5cdee56f693cc9c1c78077"}, 698 | {file = "pydantic_core-2.14.5-cp311-none-win_arm64.whl", hash = "sha256:57d52fa717ff445cb0a5ab5237db502e6be50809b43a596fb569630c665abddf"}, 699 | {file = "pydantic_core-2.14.5-cp312-cp312-macosx_10_7_x86_64.whl", hash = "sha256:e60f112ac88db9261ad3a52032ea46388378034f3279c643499edb982536a093"}, 700 | {file = "pydantic_core-2.14.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6e227c40c02fd873c2a73a98c1280c10315cbebe26734c196ef4514776120aeb"}, 701 | {file = "pydantic_core-2.14.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f0cbc7fff06a90bbd875cc201f94ef0ee3929dfbd5c55a06674b60857b8b85ed"}, 702 | {file = "pydantic_core-2.14.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:103ef8d5b58596a731b690112819501ba1db7a36f4ee99f7892c40da02c3e189"}, 703 | {file = "pydantic_core-2.14.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c949f04ecad823f81b1ba94e7d189d9dfb81edbb94ed3f8acfce41e682e48cef"}, 704 | {file = "pydantic_core-2.14.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c1452a1acdf914d194159439eb21e56b89aa903f2e1c65c60b9d874f9b950e5d"}, 705 | {file = "pydantic_core-2.14.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cb4679d4c2b089e5ef89756bc73e1926745e995d76e11925e3e96a76d5fa51fc"}, 706 | {file = "pydantic_core-2.14.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cf9d3fe53b1ee360e2421be95e62ca9b3296bf3f2fb2d3b83ca49ad3f925835e"}, 707 | {file = "pydantic_core-2.14.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:70f4b4851dbb500129681d04cc955be2a90b2248d69273a787dda120d5cf1f69"}, 708 | {file = "pydantic_core-2.14.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:59986de5710ad9613ff61dd9b02bdd2f615f1a7052304b79cc8fa2eb4e336d2d"}, 709 | {file = "pydantic_core-2.14.5-cp312-none-win32.whl", hash = "sha256:699156034181e2ce106c89ddb4b6504c30db8caa86e0c30de47b3e0654543260"}, 710 | {file = "pydantic_core-2.14.5-cp312-none-win_amd64.whl", hash = "sha256:5baab5455c7a538ac7e8bf1feec4278a66436197592a9bed538160a2e7d11e36"}, 711 | {file = "pydantic_core-2.14.5-cp312-none-win_arm64.whl", hash = "sha256:e47e9a08bcc04d20975b6434cc50bf82665fbc751bcce739d04a3120428f3e27"}, 712 | {file = "pydantic_core-2.14.5-cp37-cp37m-macosx_10_7_x86_64.whl", hash = "sha256:af36f36538418f3806048f3b242a1777e2540ff9efaa667c27da63d2749dbce0"}, 713 | {file = "pydantic_core-2.14.5-cp37-cp37m-macosx_11_0_arm64.whl", hash = "sha256:45e95333b8418ded64745f14574aa9bfc212cb4fbeed7a687b0c6e53b5e188cd"}, 714 | {file = "pydantic_core-2.14.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e47a76848f92529879ecfc417ff88a2806438f57be4a6a8bf2961e8f9ca9ec7"}, 715 | {file = "pydantic_core-2.14.5-cp37-cp37m-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d81e6987b27bc7d101c8597e1cd2bcaa2fee5e8e0f356735c7ed34368c471550"}, 716 | {file = "pydantic_core-2.14.5-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:34708cc82c330e303f4ce87758828ef6e457681b58ce0e921b6e97937dd1e2a3"}, 717 | {file = "pydantic_core-2.14.5-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:652c1988019752138b974c28f43751528116bcceadad85f33a258869e641d753"}, 718 | {file = "pydantic_core-2.14.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e4d090e73e0725b2904fdbdd8d73b8802ddd691ef9254577b708d413bf3006e"}, 719 | {file = "pydantic_core-2.14.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5c7d5b5005f177764e96bd584d7bf28d6e26e96f2a541fdddb934c486e36fd59"}, 720 | {file = "pydantic_core-2.14.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:a71891847f0a73b1b9eb86d089baee301477abef45f7eaf303495cd1473613e4"}, 721 | {file = "pydantic_core-2.14.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:a717aef6971208f0851a2420b075338e33083111d92041157bbe0e2713b37325"}, 722 | {file = "pydantic_core-2.14.5-cp37-none-win32.whl", hash = "sha256:de790a3b5aa2124b8b78ae5faa033937a72da8efe74b9231698b5a1dd9be3405"}, 723 | {file = "pydantic_core-2.14.5-cp37-none-win_amd64.whl", hash = "sha256:6c327e9cd849b564b234da821236e6bcbe4f359a42ee05050dc79d8ed2a91588"}, 724 | {file = "pydantic_core-2.14.5-cp38-cp38-macosx_10_7_x86_64.whl", hash = "sha256:ef98ca7d5995a82f43ec0ab39c4caf6a9b994cb0b53648ff61716370eadc43cf"}, 725 | {file = "pydantic_core-2.14.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c6eae413494a1c3f89055da7a5515f32e05ebc1a234c27674a6956755fb2236f"}, 726 | {file = "pydantic_core-2.14.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dcf4e6d85614f7a4956c2de5a56531f44efb973d2fe4a444d7251df5d5c4dcfd"}, 727 | {file = "pydantic_core-2.14.5-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6637560562134b0e17de333d18e69e312e0458ee4455bdad12c37100b7cad706"}, 728 | {file = "pydantic_core-2.14.5-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:77fa384d8e118b3077cccfcaf91bf83c31fe4dc850b5e6ee3dc14dc3d61bdba1"}, 729 | {file = "pydantic_core-2.14.5-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:16e29bad40bcf97aac682a58861249ca9dcc57c3f6be22f506501833ddb8939c"}, 730 | {file = "pydantic_core-2.14.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:531f4b4252fac6ca476fbe0e6f60f16f5b65d3e6b583bc4d87645e4e5ddde331"}, 731 | {file = "pydantic_core-2.14.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:074f3d86f081ce61414d2dc44901f4f83617329c6f3ab49d2bc6c96948b2c26b"}, 732 | {file = "pydantic_core-2.14.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:c2adbe22ab4babbca99c75c5d07aaf74f43c3195384ec07ccbd2f9e3bddaecec"}, 733 | {file = "pydantic_core-2.14.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:0f6116a558fd06d1b7c2902d1c4cf64a5bd49d67c3540e61eccca93f41418124"}, 734 | {file = "pydantic_core-2.14.5-cp38-none-win32.whl", hash = "sha256:fe0a5a1025eb797752136ac8b4fa21aa891e3d74fd340f864ff982d649691867"}, 735 | {file = "pydantic_core-2.14.5-cp38-none-win_amd64.whl", hash = "sha256:079206491c435b60778cf2b0ee5fd645e61ffd6e70c47806c9ed51fc75af078d"}, 736 | {file = "pydantic_core-2.14.5-cp39-cp39-macosx_10_7_x86_64.whl", hash = "sha256:a6a16f4a527aae4f49c875da3cdc9508ac7eef26e7977952608610104244e1b7"}, 737 | {file = "pydantic_core-2.14.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:abf058be9517dc877227ec3223f0300034bd0e9f53aebd63cf4456c8cb1e0863"}, 738 | {file = "pydantic_core-2.14.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:49b08aae5013640a3bfa25a8eebbd95638ec3f4b2eaf6ed82cf0c7047133f03b"}, 739 | {file = "pydantic_core-2.14.5-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c2d97e906b4ff36eb464d52a3bc7d720bd6261f64bc4bcdbcd2c557c02081ed2"}, 740 | {file = "pydantic_core-2.14.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3128e0bbc8c091ec4375a1828d6118bc20404883169ac95ffa8d983b293611e6"}, 741 | {file = "pydantic_core-2.14.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88e74ab0cdd84ad0614e2750f903bb0d610cc8af2cc17f72c28163acfcf372a4"}, 742 | {file = "pydantic_core-2.14.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c339dabd8ee15f8259ee0f202679b6324926e5bc9e9a40bf981ce77c038553db"}, 743 | {file = "pydantic_core-2.14.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3387277f1bf659caf1724e1afe8ee7dbc9952a82d90f858ebb931880216ea955"}, 744 | {file = "pydantic_core-2.14.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ba6b6b3846cfc10fdb4c971980a954e49d447cd215ed5a77ec8190bc93dd7bc5"}, 745 | {file = "pydantic_core-2.14.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ca61d858e4107ce5e1330a74724fe757fc7135190eb5ce5c9d0191729f033209"}, 746 | {file = "pydantic_core-2.14.5-cp39-none-win32.whl", hash = "sha256:ec1e72d6412f7126eb7b2e3bfca42b15e6e389e1bc88ea0069d0cc1742f477c6"}, 747 | {file = "pydantic_core-2.14.5-cp39-none-win_amd64.whl", hash = "sha256:c0b97ec434041827935044bbbe52b03d6018c2897349670ff8fe11ed24d1d4ab"}, 748 | {file = "pydantic_core-2.14.5-pp310-pypy310_pp73-macosx_10_7_x86_64.whl", hash = "sha256:79e0a2cdbdc7af3f4aee3210b1172ab53d7ddb6a2d8c24119b5706e622b346d0"}, 749 | {file = "pydantic_core-2.14.5-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:678265f7b14e138d9a541ddabbe033012a2953315739f8cfa6d754cc8063e8ca"}, 750 | {file = "pydantic_core-2.14.5-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:95b15e855ae44f0c6341ceb74df61b606e11f1087e87dcb7482377374aac6abe"}, 751 | {file = "pydantic_core-2.14.5-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:09b0e985fbaf13e6b06a56d21694d12ebca6ce5414b9211edf6f17738d82b0f8"}, 752 | {file = "pydantic_core-2.14.5-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3ad873900297bb36e4b6b3f7029d88ff9829ecdc15d5cf20161775ce12306f8a"}, 753 | {file = "pydantic_core-2.14.5-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:2d0ae0d8670164e10accbeb31d5ad45adb71292032d0fdb9079912907f0085f4"}, 754 | {file = "pydantic_core-2.14.5-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:d37f8ec982ead9ba0a22a996129594938138a1503237b87318392a48882d50b7"}, 755 | {file = "pydantic_core-2.14.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:35613015f0ba7e14c29ac6c2483a657ec740e5ac5758d993fdd5870b07a61d8b"}, 756 | {file = "pydantic_core-2.14.5-pp37-pypy37_pp73-macosx_10_7_x86_64.whl", hash = "sha256:ab4ea451082e684198636565224bbb179575efc1658c48281b2c866bfd4ddf04"}, 757 | {file = "pydantic_core-2.14.5-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ce601907e99ea5b4adb807ded3570ea62186b17f88e271569144e8cca4409c7"}, 758 | {file = "pydantic_core-2.14.5-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fb2ed8b3fe4bf4506d6dab3b93b83bbc22237e230cba03866d561c3577517d18"}, 759 | {file = "pydantic_core-2.14.5-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:70f947628e074bb2526ba1b151cee10e4c3b9670af4dbb4d73bc8a89445916b5"}, 760 | {file = "pydantic_core-2.14.5-pp37-pypy37_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:4bc536201426451f06f044dfbf341c09f540b4ebdb9fd8d2c6164d733de5e634"}, 761 | {file = "pydantic_core-2.14.5-pp37-pypy37_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f4791cf0f8c3104ac668797d8c514afb3431bc3305f5638add0ba1a5a37e0d88"}, 762 | {file = "pydantic_core-2.14.5-pp38-pypy38_pp73-macosx_10_7_x86_64.whl", hash = "sha256:038c9f763e650712b899f983076ce783175397c848da04985658e7628cbe873b"}, 763 | {file = "pydantic_core-2.14.5-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:27548e16c79702f1e03f5628589c6057c9ae17c95b4c449de3c66b589ead0520"}, 764 | {file = "pydantic_core-2.14.5-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c97bee68898f3f4344eb02fec316db93d9700fb1e6a5b760ffa20d71d9a46ce3"}, 765 | {file = "pydantic_core-2.14.5-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b9b759b77f5337b4ea024f03abc6464c9f35d9718de01cfe6bae9f2e139c397e"}, 766 | {file = "pydantic_core-2.14.5-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:439c9afe34638ace43a49bf72d201e0ffc1a800295bed8420c2a9ca8d5e3dbb3"}, 767 | {file = "pydantic_core-2.14.5-pp38-pypy38_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:ba39688799094c75ea8a16a6b544eb57b5b0f3328697084f3f2790892510d144"}, 768 | {file = "pydantic_core-2.14.5-pp38-pypy38_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:ccd4d5702bb90b84df13bd491be8d900b92016c5a455b7e14630ad7449eb03f8"}, 769 | {file = "pydantic_core-2.14.5-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:81982d78a45d1e5396819bbb4ece1fadfe5f079335dd28c4ab3427cd95389944"}, 770 | {file = "pydantic_core-2.14.5-pp39-pypy39_pp73-macosx_10_7_x86_64.whl", hash = "sha256:7f8210297b04e53bc3da35db08b7302a6a1f4889c79173af69b72ec9754796b8"}, 771 | {file = "pydantic_core-2.14.5-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:8c8a8812fe6f43a3a5b054af6ac2d7b8605c7bcab2804a8a7d68b53f3cd86e00"}, 772 | {file = "pydantic_core-2.14.5-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:206ed23aecd67c71daf5c02c3cd19c0501b01ef3cbf7782db9e4e051426b3d0d"}, 773 | {file = "pydantic_core-2.14.5-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c2027d05c8aebe61d898d4cffd774840a9cb82ed356ba47a90d99ad768f39789"}, 774 | {file = "pydantic_core-2.14.5-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:40180930807ce806aa71eda5a5a5447abb6b6a3c0b4b3b1b1962651906484d68"}, 775 | {file = "pydantic_core-2.14.5-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:615a0a4bff11c45eb3c1996ceed5bdaa2f7b432425253a7c2eed33bb86d80abc"}, 776 | {file = "pydantic_core-2.14.5-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f5e412d717366e0677ef767eac93566582518fe8be923361a5c204c1a62eaafe"}, 777 | {file = "pydantic_core-2.14.5-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:513b07e99c0a267b1d954243845d8a833758a6726a3b5d8948306e3fe14675e3"}, 778 | {file = "pydantic_core-2.14.5.tar.gz", hash = "sha256:6d30226dfc816dd0fdf120cae611dd2215117e4f9b124af8c60ab9093b6e8e71"}, 779 | ] 780 | 781 | [package.dependencies] 782 | typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" 783 | 784 | [[package]] 785 | name = "pydantic-settings" 786 | version = "2.1.0" 787 | description = "Settings management using Pydantic" 788 | optional = false 789 | python-versions = ">=3.8" 790 | files = [ 791 | {file = "pydantic_settings-2.1.0-py3-none-any.whl", hash = "sha256:7621c0cb5d90d1140d2f0ef557bdf03573aac7035948109adf2574770b77605a"}, 792 | {file = "pydantic_settings-2.1.0.tar.gz", hash = "sha256:26b1492e0a24755626ac5e6d715e9077ab7ad4fb5f19a8b7ed7011d52f36141c"}, 793 | ] 794 | 795 | [package.dependencies] 796 | pydantic = ">=2.3.0" 797 | python-dotenv = ">=0.21.0" 798 | 799 | [[package]] 800 | name = "pyflakes" 801 | version = "2.5.0" 802 | description = "passive checker of Python programs" 803 | optional = false 804 | python-versions = ">=3.6" 805 | files = [ 806 | {file = "pyflakes-2.5.0-py2.py3-none-any.whl", hash = "sha256:4579f67d887f804e67edb544428f264b7b24f435b263c4614f384135cea553d2"}, 807 | {file = "pyflakes-2.5.0.tar.gz", hash = "sha256:491feb020dca48ccc562a8c0cbe8df07ee13078df59813b83959cbdada312ea3"}, 808 | ] 809 | 810 | [[package]] 811 | name = "pymongo" 812 | version = "4.6.1" 813 | description = "Python driver for MongoDB " 814 | optional = false 815 | python-versions = ">=3.7" 816 | files = [ 817 | {file = "pymongo-4.6.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4344c30025210b9fa80ec257b0e0aab5aa1d5cca91daa70d82ab97b482cc038e"}, 818 | {file = "pymongo-4.6.1-cp310-cp310-manylinux1_i686.whl", hash = "sha256:1c5654bb8bb2bdb10e7a0bc3c193dd8b49a960b9eebc4381ff5a2043f4c3c441"}, 819 | {file = "pymongo-4.6.1-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:eaf2f65190c506def2581219572b9c70b8250615dc918b3b7c218361a51ec42e"}, 820 | {file = "pymongo-4.6.1-cp310-cp310-manylinux2014_i686.whl", hash = "sha256:262356ea5fcb13d35fb2ab6009d3927bafb9504ef02339338634fffd8a9f1ae4"}, 821 | {file = "pymongo-4.6.1-cp310-cp310-manylinux2014_ppc64le.whl", hash = "sha256:2dd2f6960ee3c9360bed7fb3c678be0ca2d00f877068556785ec2eb6b73d2414"}, 822 | {file = "pymongo-4.6.1-cp310-cp310-manylinux2014_s390x.whl", hash = "sha256:ff925f1cca42e933376d09ddc254598f8c5fcd36efc5cac0118bb36c36217c41"}, 823 | {file = "pymongo-4.6.1-cp310-cp310-manylinux2014_x86_64.whl", hash = "sha256:3cadf7f4c8e94d8a77874b54a63c80af01f4d48c4b669c8b6867f86a07ba994f"}, 824 | {file = "pymongo-4.6.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55dac73316e7e8c2616ba2e6f62b750918e9e0ae0b2053699d66ca27a7790105"}, 825 | {file = "pymongo-4.6.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:154b361dcb358ad377d5d40df41ee35f1cc14c8691b50511547c12404f89b5cb"}, 826 | {file = "pymongo-4.6.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2940aa20e9cc328e8ddeacea8b9a6f5ddafe0b087fedad928912e787c65b4909"}, 827 | {file = "pymongo-4.6.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:010bc9aa90fd06e5cc52c8fac2c2fd4ef1b5f990d9638548dde178005770a5e8"}, 828 | {file = "pymongo-4.6.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e470fa4bace5f50076c32f4b3cc182b31303b4fefb9b87f990144515d572820b"}, 829 | {file = "pymongo-4.6.1-cp310-cp310-win32.whl", hash = "sha256:da08ea09eefa6b960c2dd9a68ec47949235485c623621eb1d6c02b46765322ac"}, 830 | {file = "pymongo-4.6.1-cp310-cp310-win_amd64.whl", hash = "sha256:13d613c866f9f07d51180f9a7da54ef491d130f169e999c27e7633abe8619ec9"}, 831 | {file = "pymongo-4.6.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6a0ae7a48a6ef82ceb98a366948874834b86c84e288dbd55600c1abfc3ac1d88"}, 832 | {file = "pymongo-4.6.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5bd94c503271e79917b27c6e77f7c5474da6930b3fb9e70a12e68c2dff386b9a"}, 833 | {file = "pymongo-4.6.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2d4ccac3053b84a09251da8f5350bb684cbbf8c8c01eda6b5418417d0a8ab198"}, 834 | {file = "pymongo-4.6.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:349093675a2d3759e4fb42b596afffa2b2518c890492563d7905fac503b20daa"}, 835 | {file = "pymongo-4.6.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88beb444fb438385e53dc9110852910ec2a22f0eab7dd489e827038fdc19ed8d"}, 836 | {file = "pymongo-4.6.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d8e62d06e90f60ea2a3d463ae51401475568b995bafaffd81767d208d84d7bb1"}, 837 | {file = "pymongo-4.6.1-cp311-cp311-win32.whl", hash = "sha256:5556e306713e2522e460287615d26c0af0fe5ed9d4f431dad35c6624c5d277e9"}, 838 | {file = "pymongo-4.6.1-cp311-cp311-win_amd64.whl", hash = "sha256:b10d8cda9fc2fcdcfa4a000aa10413a2bf8b575852cd07cb8a595ed09689ca98"}, 839 | {file = "pymongo-4.6.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:b435b13bb8e36be11b75f7384a34eefe487fe87a6267172964628e2b14ecf0a7"}, 840 | {file = "pymongo-4.6.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e438417ce1dc5b758742e12661d800482200b042d03512a8f31f6aaa9137ad40"}, 841 | {file = "pymongo-4.6.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8b47ebd89e69fbf33d1c2df79759d7162fc80c7652dacfec136dae1c9b3afac7"}, 842 | {file = "pymongo-4.6.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bbed8cccebe1169d45cedf00461b2842652d476d2897fd1c42cf41b635d88746"}, 843 | {file = "pymongo-4.6.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c30a9e06041fbd7a7590693ec5e407aa8737ad91912a1e70176aff92e5c99d20"}, 844 | {file = "pymongo-4.6.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b8729dbf25eb32ad0dc0b9bd5e6a0d0b7e5c2dc8ec06ad171088e1896b522a74"}, 845 | {file = "pymongo-4.6.1-cp312-cp312-win32.whl", hash = "sha256:3177f783ae7e08aaf7b2802e0df4e4b13903520e8380915e6337cdc7a6ff01d8"}, 846 | {file = "pymongo-4.6.1-cp312-cp312-win_amd64.whl", hash = "sha256:00c199e1c593e2c8b033136d7a08f0c376452bac8a896c923fcd6f419e07bdd2"}, 847 | {file = "pymongo-4.6.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:13552ca505366df74e3e2f0a4f27c363928f3dff0eef9f281eb81af7f29bc3c5"}, 848 | {file = "pymongo-4.6.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:77e0df59b1a4994ad30c6d746992ae887f9756a43fc25dec2db515d94cf0222d"}, 849 | {file = "pymongo-4.6.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:3a7f02a58a0c2912734105e05dedbee4f7507e6f1bd132ebad520be0b11d46fd"}, 850 | {file = "pymongo-4.6.1-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:026a24a36394dc8930cbcb1d19d5eb35205ef3c838a7e619e04bd170713972e7"}, 851 | {file = "pymongo-4.6.1-cp37-cp37m-manylinux2014_ppc64le.whl", hash = "sha256:3b287e814a01deddb59b88549c1e0c87cefacd798d4afc0c8bd6042d1c3d48aa"}, 852 | {file = "pymongo-4.6.1-cp37-cp37m-manylinux2014_s390x.whl", hash = "sha256:9a710c184ba845afb05a6f876edac8f27783ba70e52d5eaf939f121fc13b2f59"}, 853 | {file = "pymongo-4.6.1-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:30b2c9caf3e55c2e323565d1f3b7e7881ab87db16997dc0cbca7c52885ed2347"}, 854 | {file = "pymongo-4.6.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ff62ba8ff70f01ab4fe0ae36b2cb0b5d1f42e73dfc81ddf0758cd9f77331ad25"}, 855 | {file = "pymongo-4.6.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:547dc5d7f834b1deefda51aedb11a7af9c51c45e689e44e14aa85d44147c7657"}, 856 | {file = "pymongo-4.6.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1de3c6faf948f3edd4e738abdb4b76572b4f4fdfc1fed4dad02427e70c5a6219"}, 857 | {file = "pymongo-4.6.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a2831e05ce0a4df10c4ac5399ef50b9a621f90894c2a4d2945dc5658765514ed"}, 858 | {file = "pymongo-4.6.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:144a31391a39a390efce0c5ebcaf4bf112114af4384c90163f402cec5ede476b"}, 859 | {file = "pymongo-4.6.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:33bb16a07d3cc4e0aea37b242097cd5f7a156312012455c2fa8ca396953b11c4"}, 860 | {file = "pymongo-4.6.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:b7b1a83ce514700276a46af3d9e481ec381f05b64939effc9065afe18456a6b9"}, 861 | {file = "pymongo-4.6.1-cp37-cp37m-win32.whl", hash = "sha256:3071ec998cc3d7b4944377e5f1217c2c44b811fae16f9a495c7a1ce9b42fb038"}, 862 | {file = "pymongo-4.6.1-cp37-cp37m-win_amd64.whl", hash = "sha256:2346450a075625c4d6166b40a013b605a38b6b6168ce2232b192a37fb200d588"}, 863 | {file = "pymongo-4.6.1-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:061598cbc6abe2f382ab64c9caa83faa2f4c51256f732cdd890bcc6e63bfb67e"}, 864 | {file = "pymongo-4.6.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:d483793a384c550c2d12cb794ede294d303b42beff75f3b3081f57196660edaf"}, 865 | {file = "pymongo-4.6.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:f9756f1d25454ba6a3c2f1ef8b7ddec23e5cdeae3dc3c3377243ae37a383db00"}, 866 | {file = "pymongo-4.6.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:1ed23b0e2dac6f84f44c8494fbceefe6eb5c35db5c1099f56ab78fc0d94ab3af"}, 867 | {file = "pymongo-4.6.1-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:3d18a9b9b858ee140c15c5bfcb3e66e47e2a70a03272c2e72adda2482f76a6ad"}, 868 | {file = "pymongo-4.6.1-cp38-cp38-manylinux2014_ppc64le.whl", hash = "sha256:c258dbacfff1224f13576147df16ce3c02024a0d792fd0323ac01bed5d3c545d"}, 869 | {file = "pymongo-4.6.1-cp38-cp38-manylinux2014_s390x.whl", hash = "sha256:f7acc03a4f1154ba2643edeb13658d08598fe6e490c3dd96a241b94f09801626"}, 870 | {file = "pymongo-4.6.1-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:76013fef1c9cd1cd00d55efde516c154aa169f2bf059b197c263a255ba8a9ddf"}, 871 | {file = "pymongo-4.6.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f0e6a6c807fa887a0c51cc24fe7ea51bb9e496fe88f00d7930063372c3664c3"}, 872 | {file = "pymongo-4.6.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dd1fa413f8b9ba30140de198e4f408ffbba6396864c7554e0867aa7363eb58b2"}, 873 | {file = "pymongo-4.6.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d219b4508f71d762368caec1fc180960569766049bbc4d38174f05e8ef2fe5b"}, 874 | {file = "pymongo-4.6.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:27b81ecf18031998ad7db53b960d1347f8f29e8b7cb5ea7b4394726468e4295e"}, 875 | {file = "pymongo-4.6.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:56816e43c92c2fa8c11dc2a686f0ca248bea7902f4a067fa6cbc77853b0f041e"}, 876 | {file = "pymongo-4.6.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ef801027629c5b511cf2ba13b9be29bfee36ae834b2d95d9877818479cdc99ea"}, 877 | {file = "pymongo-4.6.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:d4c2be9760b112b1caf649b4977b81b69893d75aa86caf4f0f398447be871f3c"}, 878 | {file = "pymongo-4.6.1-cp38-cp38-win32.whl", hash = "sha256:39d77d8bbb392fa443831e6d4ae534237b1f4eee6aa186f0cdb4e334ba89536e"}, 879 | {file = "pymongo-4.6.1-cp38-cp38-win_amd64.whl", hash = "sha256:4497d49d785482cc1a44a0ddf8830b036a468c088e72a05217f5b60a9e025012"}, 880 | {file = "pymongo-4.6.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:69247f7a2835fc0984bbf0892e6022e9a36aec70e187fcfe6cae6a373eb8c4de"}, 881 | {file = "pymongo-4.6.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:7bb0e9049e81def6829d09558ad12d16d0454c26cabe6efc3658e544460688d9"}, 882 | {file = "pymongo-4.6.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:6a1810c2cbde714decf40f811d1edc0dae45506eb37298fd9d4247b8801509fe"}, 883 | {file = "pymongo-4.6.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:e2aced6fb2f5261b47d267cb40060b73b6527e64afe54f6497844c9affed5fd0"}, 884 | {file = "pymongo-4.6.1-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:d0355cff58a4ed6d5e5f6b9c3693f52de0784aa0c17119394e2a8e376ce489d4"}, 885 | {file = "pymongo-4.6.1-cp39-cp39-manylinux2014_ppc64le.whl", hash = "sha256:3c74f4725485f0a7a3862cfd374cc1b740cebe4c133e0c1425984bcdcce0f4bb"}, 886 | {file = "pymongo-4.6.1-cp39-cp39-manylinux2014_s390x.whl", hash = "sha256:9c79d597fb3a7c93d7c26924db7497eba06d58f88f58e586aa69b2ad89fee0f8"}, 887 | {file = "pymongo-4.6.1-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:8ec75f35f62571a43e31e7bd11749d974c1b5cd5ea4a8388725d579263c0fdf6"}, 888 | {file = "pymongo-4.6.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5e641f931c5cd95b376fd3c59db52770e17bec2bf86ef16cc83b3906c054845"}, 889 | {file = "pymongo-4.6.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9aafd036f6f2e5ad109aec92f8dbfcbe76cff16bad683eb6dd18013739c0b3ae"}, 890 | {file = "pymongo-4.6.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f2b856518bfcfa316c8dae3d7b412aecacf2e8ba30b149f5eb3b63128d703b9"}, 891 | {file = "pymongo-4.6.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5ec31adc2e988fd7db3ab509954791bbc5a452a03c85e45b804b4bfc31fa221d"}, 892 | {file = "pymongo-4.6.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9167e735379ec43d8eafa3fd675bfbb12e2c0464f98960586e9447d2cf2c7a83"}, 893 | {file = "pymongo-4.6.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1461199b07903fc1424709efafe379205bf5f738144b1a50a08b0396357b5abf"}, 894 | {file = "pymongo-4.6.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:3094c7d2f820eecabadae76bfec02669567bbdd1730eabce10a5764778564f7b"}, 895 | {file = "pymongo-4.6.1-cp39-cp39-win32.whl", hash = "sha256:c91ea3915425bd4111cb1b74511cdc56d1d16a683a48bf2a5a96b6a6c0f297f7"}, 896 | {file = "pymongo-4.6.1-cp39-cp39-win_amd64.whl", hash = "sha256:ef102a67ede70e1721fe27f75073b5314911dbb9bc27cde0a1c402a11531e7bd"}, 897 | {file = "pymongo-4.6.1.tar.gz", hash = "sha256:31dab1f3e1d0cdd57e8df01b645f52d43cc1b653ed3afd535d2891f4fc4f9712"}, 898 | ] 899 | 900 | [package.dependencies] 901 | dnspython = ">=1.16.0,<3.0.0" 902 | 903 | [package.extras] 904 | aws = ["pymongo-auth-aws (<2.0.0)"] 905 | encryption = ["certifi", "pymongo[aws]", "pymongocrypt (>=1.6.0,<2.0.0)"] 906 | gssapi = ["pykerberos", "winkerberos (>=0.5.0)"] 907 | ocsp = ["certifi", "cryptography (>=2.5)", "pyopenssl (>=17.2.0)", "requests (<3.0.0)", "service-identity (>=18.1.0)"] 908 | snappy = ["python-snappy"] 909 | test = ["pytest (>=7)"] 910 | zstd = ["zstandard"] 911 | 912 | [[package]] 913 | name = "pytest" 914 | version = "7.4.3" 915 | description = "pytest: simple powerful testing with Python" 916 | optional = false 917 | python-versions = ">=3.7" 918 | files = [ 919 | {file = "pytest-7.4.3-py3-none-any.whl", hash = "sha256:0d009c083ea859a71b76adf7c1d502e4bc170b80a8ef002da5806527b9591fac"}, 920 | {file = "pytest-7.4.3.tar.gz", hash = "sha256:d989d136982de4e3b29dabcc838ad581c64e8ed52c11fbe86ddebd9da0818cd5"}, 921 | ] 922 | 923 | [package.dependencies] 924 | colorama = {version = "*", markers = "sys_platform == \"win32\""} 925 | iniconfig = "*" 926 | packaging = "*" 927 | pluggy = ">=0.12,<2.0" 928 | 929 | [package.extras] 930 | testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] 931 | 932 | [[package]] 933 | name = "pytest-asyncio" 934 | version = "0.19.0" 935 | description = "Pytest support for asyncio" 936 | optional = false 937 | python-versions = ">=3.7" 938 | files = [ 939 | {file = "pytest-asyncio-0.19.0.tar.gz", hash = "sha256:ac4ebf3b6207259750bc32f4c1d8fcd7e79739edbc67ad0c58dd150b1d072fed"}, 940 | {file = "pytest_asyncio-0.19.0-py3-none-any.whl", hash = "sha256:7a97e37cfe1ed296e2e84941384bdd37c376453912d397ed39293e0916f521fa"}, 941 | ] 942 | 943 | [package.dependencies] 944 | pytest = ">=6.1.0" 945 | 946 | [package.extras] 947 | testing = ["coverage (>=6.2)", "flaky (>=3.5.0)", "hypothesis (>=5.7.1)", "mypy (>=0.931)", "pytest-trio (>=0.7.0)"] 948 | 949 | [[package]] 950 | name = "python-dotenv" 951 | version = "1.0.0" 952 | description = "Read key-value pairs from a .env file and set them as environment variables" 953 | optional = false 954 | python-versions = ">=3.8" 955 | files = [ 956 | {file = "python-dotenv-1.0.0.tar.gz", hash = "sha256:a8df96034aae6d2d50a4ebe8216326c61c3eb64836776504fcca410e5937a3ba"}, 957 | {file = "python_dotenv-1.0.0-py3-none-any.whl", hash = "sha256:f5971a9226b701070a4bf2c38c89e5a3f0d64de8debda981d1db98583009122a"}, 958 | ] 959 | 960 | [package.extras] 961 | cli = ["click (>=5.0)"] 962 | 963 | [[package]] 964 | name = "python-jose" 965 | version = "3.3.0" 966 | description = "JOSE implementation in Python" 967 | optional = false 968 | python-versions = "*" 969 | files = [ 970 | {file = "python-jose-3.3.0.tar.gz", hash = "sha256:55779b5e6ad599c6336191246e95eb2293a9ddebd555f796a65f838f07e5d78a"}, 971 | {file = "python_jose-3.3.0-py2.py3-none-any.whl", hash = "sha256:9b1376b023f8b298536eedd47ae1089bcdb848f1535ab30555cd92002d78923a"}, 972 | ] 973 | 974 | [package.dependencies] 975 | cryptography = {version = ">=3.4.0", optional = true, markers = "extra == \"cryptography\""} 976 | ecdsa = "!=0.15" 977 | pyasn1 = "*" 978 | rsa = "*" 979 | 980 | [package.extras] 981 | cryptography = ["cryptography (>=3.4.0)"] 982 | pycrypto = ["pyasn1", "pycrypto (>=2.6.0,<2.7.0)"] 983 | pycryptodome = ["pyasn1", "pycryptodome (>=3.3.1,<4.0.0)"] 984 | 985 | [[package]] 986 | name = "pyyaml" 987 | version = "6.0.1" 988 | description = "YAML parser and emitter for Python" 989 | optional = false 990 | python-versions = ">=3.6" 991 | files = [ 992 | {file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"}, 993 | {file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"}, 994 | {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, 995 | {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, 996 | {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, 997 | {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"}, 998 | {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, 999 | {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, 1000 | {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, 1001 | {file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"}, 1002 | {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, 1003 | {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, 1004 | {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, 1005 | {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, 1006 | {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, 1007 | {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, 1008 | {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, 1009 | {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, 1010 | {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, 1011 | {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, 1012 | {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, 1013 | {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, 1014 | {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, 1015 | {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, 1016 | {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, 1017 | {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd"}, 1018 | {file = "PyYAML-6.0.1-cp36-cp36m-win32.whl", hash = "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585"}, 1019 | {file = "PyYAML-6.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa"}, 1020 | {file = "PyYAML-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3"}, 1021 | {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27"}, 1022 | {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3"}, 1023 | {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c"}, 1024 | {file = "PyYAML-6.0.1-cp37-cp37m-win32.whl", hash = "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba"}, 1025 | {file = "PyYAML-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867"}, 1026 | {file = "PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595"}, 1027 | {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, 1028 | {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, 1029 | {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, 1030 | {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"}, 1031 | {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, 1032 | {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, 1033 | {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, 1034 | {file = "PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859"}, 1035 | {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, 1036 | {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, 1037 | {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, 1038 | {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"}, 1039 | {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, 1040 | {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, 1041 | {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, 1042 | ] 1043 | 1044 | [[package]] 1045 | name = "rsa" 1046 | version = "4.9" 1047 | description = "Pure-Python RSA implementation" 1048 | optional = false 1049 | python-versions = ">=3.6,<4" 1050 | files = [ 1051 | {file = "rsa-4.9-py3-none-any.whl", hash = "sha256:90260d9058e514786967344d0ef75fa8727eed8a7d2e43ce9f4bcf1b536174f7"}, 1052 | {file = "rsa-4.9.tar.gz", hash = "sha256:e38464a49c6c85d7f1351b0126661487a7e0a14a50f1675ec50eb34d4f20ef21"}, 1053 | ] 1054 | 1055 | [package.dependencies] 1056 | pyasn1 = ">=0.1.3" 1057 | 1058 | [[package]] 1059 | name = "setuptools" 1060 | version = "69.0.2" 1061 | description = "Easily download, build, install, upgrade, and uninstall Python packages" 1062 | optional = false 1063 | python-versions = ">=3.8" 1064 | files = [ 1065 | {file = "setuptools-69.0.2-py3-none-any.whl", hash = "sha256:1e8fdff6797d3865f37397be788a4e3cba233608e9b509382a2777d25ebde7f2"}, 1066 | {file = "setuptools-69.0.2.tar.gz", hash = "sha256:735896e78a4742605974de002ac60562d286fa8051a7e2299445e8e8fbb01aa6"}, 1067 | ] 1068 | 1069 | [package.extras] 1070 | docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] 1071 | testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] 1072 | testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.1)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] 1073 | 1074 | [[package]] 1075 | name = "six" 1076 | version = "1.16.0" 1077 | description = "Python 2 and 3 compatibility utilities" 1078 | optional = false 1079 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" 1080 | files = [ 1081 | {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, 1082 | {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, 1083 | ] 1084 | 1085 | [[package]] 1086 | name = "sniffio" 1087 | version = "1.3.0" 1088 | description = "Sniff out which async library your code is running under" 1089 | optional = false 1090 | python-versions = ">=3.7" 1091 | files = [ 1092 | {file = "sniffio-1.3.0-py3-none-any.whl", hash = "sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384"}, 1093 | {file = "sniffio-1.3.0.tar.gz", hash = "sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101"}, 1094 | ] 1095 | 1096 | [[package]] 1097 | name = "starlette" 1098 | version = "0.27.0" 1099 | description = "The little ASGI library that shines." 1100 | optional = false 1101 | python-versions = ">=3.7" 1102 | files = [ 1103 | {file = "starlette-0.27.0-py3-none-any.whl", hash = "sha256:918416370e846586541235ccd38a474c08b80443ed31c578a418e2209b3eef91"}, 1104 | {file = "starlette-0.27.0.tar.gz", hash = "sha256:6a6b0d042acb8d469a01eba54e9cda6cbd24ac602c4cd016723117d6a7e73b75"}, 1105 | ] 1106 | 1107 | [package.dependencies] 1108 | anyio = ">=3.4.0,<5" 1109 | 1110 | [package.extras] 1111 | full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart", "pyyaml"] 1112 | 1113 | [[package]] 1114 | name = "typing-extensions" 1115 | version = "4.9.0" 1116 | description = "Backported and Experimental Type Hints for Python 3.8+" 1117 | optional = false 1118 | python-versions = ">=3.8" 1119 | files = [ 1120 | {file = "typing_extensions-4.9.0-py3-none-any.whl", hash = "sha256:af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd"}, 1121 | {file = "typing_extensions-4.9.0.tar.gz", hash = "sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783"}, 1122 | ] 1123 | 1124 | [[package]] 1125 | name = "uvicorn" 1126 | version = "0.24.0.post1" 1127 | description = "The lightning-fast ASGI server." 1128 | optional = false 1129 | python-versions = ">=3.8" 1130 | files = [ 1131 | {file = "uvicorn-0.24.0.post1-py3-none-any.whl", hash = "sha256:7c84fea70c619d4a710153482c0d230929af7bcf76c7bfa6de151f0a3a80121e"}, 1132 | {file = "uvicorn-0.24.0.post1.tar.gz", hash = "sha256:09c8e5a79dc466bdf28dead50093957db184de356fcdc48697bad3bde4c2588e"}, 1133 | ] 1134 | 1135 | [package.dependencies] 1136 | click = ">=7.0" 1137 | h11 = ">=0.8" 1138 | 1139 | [package.extras] 1140 | standard = ["colorama (>=0.4)", "httptools (>=0.5.0)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1)", "watchfiles (>=0.13)", "websockets (>=10.4)"] 1141 | 1142 | [[package]] 1143 | name = "virtualenv" 1144 | version = "20.25.0" 1145 | description = "Virtual Python Environment builder" 1146 | optional = false 1147 | python-versions = ">=3.7" 1148 | files = [ 1149 | {file = "virtualenv-20.25.0-py3-none-any.whl", hash = "sha256:4238949c5ffe6876362d9c0180fc6c3a824a7b12b80604eeb8085f2ed7460de3"}, 1150 | {file = "virtualenv-20.25.0.tar.gz", hash = "sha256:bf51c0d9c7dd63ea8e44086fa1e4fb1093a31e963b86959257378aef020e1f1b"}, 1151 | ] 1152 | 1153 | [package.dependencies] 1154 | distlib = ">=0.3.7,<1" 1155 | filelock = ">=3.12.2,<4" 1156 | platformdirs = ">=3.9.1,<5" 1157 | 1158 | [package.extras] 1159 | docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] 1160 | test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"] 1161 | 1162 | [metadata] 1163 | lock-version = "2.0" 1164 | python-versions = "^3.11" 1165 | content-hash = "f13a74242807f392fec8666ddfcabda0420297b9d281cba5ded3a562c52ec9e2" 1166 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "fastapi-odmantic-realworld-example" 3 | version = "0.1.0" 4 | description = "" 5 | authors = ["Arthur Pastel "] 6 | license = "ISC" 7 | 8 | [tool.poetry.dependencies] 9 | python = "^3.11" 10 | fastapi = "0.104.1" 11 | passlib = { extras = ["bcrypt"], version = "^1.7.4" } 12 | python-jose = { extras = ["cryptography"], version = "^3.3.0" } 13 | odmantic = { git = "https://github.com/art049/odmantic.git" } 14 | pydantic-settings = "^2.1.0" 15 | uvicorn = "^0.24.0.post1" 16 | 17 | [tool.poetry.dev-dependencies] 18 | pytest = "^7.1.2" 19 | isort = "^5.10.1" 20 | black = "^22.6.0" 21 | flake8 = "^5.0.4" 22 | mypy = "^0.971" 23 | pytest-asyncio = "^0.19.0" 24 | pre-commit = "^2.20.0" 25 | 26 | [build-system] 27 | requires = ["poetry-core>=1.0.0"] 28 | build-backend = "poetry.core.masonry.api" 29 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | python_paths = src/ 3 | addopts = -n auto 4 | -------------------------------------------------------------------------------- /scripts/coverage.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | coverage run -m pytest -c pytest.ini 4 | coverage xml 5 | -------------------------------------------------------------------------------- /scripts/format.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | isort -y 3 | black ./ 4 | -------------------------------------------------------------------------------- /scripts/lint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | flake8 src/ tests/ 4 | find "$(pwd)/src" -type f -name "\"*.py\"" ! -name "\"*test_*\"" \ 5 | -exec python -m mypy {} + 6 | -------------------------------------------------------------------------------- /scripts/setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | poetry install 4 | poetry run pre-commit install 5 | echo -e "\e[1mProject has been setup successfully\e[0m" 6 | -------------------------------------------------------------------------------- /scripts/start-mongo.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | docker run --rm -p 27017:27017 --name fastapi-odmantic-example -d mongo:4 3 | -------------------------------------------------------------------------------- /scripts/start.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | poetry run uvicorn --app-dir ./src/ api:app 3 | -------------------------------------------------------------------------------- /scripts/stop-mongo.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | docker stop fastapi-odmantic-example 3 | -------------------------------------------------------------------------------- /scripts/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # pytest -n auto -s -c pytest.ini 3 | export APIURL=http://localhost:8000 4 | ./realworld/api/run-api-tests.sh 5 | -------------------------------------------------------------------------------- /src/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/art049/fastapi-odmantic-realworld-example/54725da134654b6874e57dd021d8737770b4e175/src/__init__.py -------------------------------------------------------------------------------- /src/api.py: -------------------------------------------------------------------------------- 1 | import uvicorn 2 | from fastapi import FastAPI 3 | from starlette.middleware.cors import CORSMiddleware 4 | 5 | from endpoints.article import router as article_router 6 | from endpoints.comment import router as comment_router 7 | from endpoints.profile import router as profile_router 8 | from endpoints.tag import router as tag_router 9 | from endpoints.user import router as user_router 10 | 11 | app = FastAPI() 12 | 13 | 14 | app.add_middleware( 15 | CORSMiddleware, 16 | allow_origins=["*"], 17 | allow_credentials=True, 18 | allow_methods=["*"], 19 | allow_headers=["*"], 20 | ) 21 | 22 | 23 | @app.get("/health", tags=["health"]) 24 | async def health_check(): 25 | return {"status": "ok"} 26 | 27 | 28 | app.include_router(user_router, tags=["user"]) 29 | app.include_router(article_router, tags=["article"]) 30 | app.include_router(comment_router, tags=["article"]) 31 | app.include_router(tag_router, tags=["tag"]) 32 | app.include_router(profile_router, tags=["profile"]) 33 | 34 | if __name__ == "__main__": 35 | uvicorn.run(app) 36 | -------------------------------------------------------------------------------- /src/core/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/art049/fastapi-odmantic-realworld-example/54725da134654b6874e57dd021d8737770b4e175/src/core/__init__.py -------------------------------------------------------------------------------- /src/core/article.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from odmantic import AIOEngine 4 | from odmantic.query import QueryExpression, desc 5 | 6 | from core.exceptions import ArticleNotFoundException, NotArticleAuthorException 7 | from models.article import ArticleModel 8 | from models.user import UserModel 9 | from schemas.article import MultipleArticlesResponse 10 | 11 | 12 | async def build_get_articles_query( 13 | engine: AIOEngine, 14 | author: Optional[str], 15 | favorited: Optional[str], 16 | tag: Optional[str], 17 | ) -> Optional[QueryExpression]: 18 | query = QueryExpression() 19 | 20 | if author is not None: 21 | author_user = await engine.find_one(UserModel, UserModel.username == author) 22 | if author_user is None: 23 | return None 24 | query &= ArticleModel.author == author_user.id 25 | 26 | if tag is not None: 27 | query &= {+ArticleModel.tag_list: {"$elemMatch": {"$eq": tag}}} 28 | 29 | if favorited is not None: 30 | favorited_user = await engine.find_one( 31 | UserModel, UserModel.username == favorited 32 | ) 33 | if favorited_user is None: 34 | return None 35 | query &= { 36 | +ArticleModel.favorited_user_ids: {"$elemMatch": {"$eq": favorited_user.id}} 37 | } 38 | 39 | return query 40 | 41 | 42 | def build_get_feed_articles_query(user: UserModel) -> QueryExpression: 43 | return ArticleModel.id.in_(user.following_ids) 44 | 45 | 46 | async def get_multiple_articles_response( 47 | engine: AIOEngine, 48 | user: Optional[UserModel], 49 | query: QueryExpression, 50 | limit: int, 51 | offset: int, 52 | ) -> MultipleArticlesResponse: 53 | articles = await engine.find( 54 | ArticleModel, 55 | query, 56 | skip=offset, 57 | limit=limit, 58 | sort=desc(ArticleModel.created_at), 59 | ) 60 | count = await engine.count(ArticleModel, query) 61 | return MultipleArticlesResponse.from_article_instances(articles, count, user) 62 | 63 | 64 | async def get_article_by_slug(engine: AIOEngine, slug: str) -> ArticleModel: 65 | article = await engine.find_one(ArticleModel, ArticleModel.slug == slug) 66 | if article is None: 67 | raise ArticleNotFoundException() 68 | return article 69 | 70 | 71 | async def add_article_to_favorite( 72 | engine: AIOEngine, user: UserModel, article: ArticleModel 73 | ) -> ArticleModel: 74 | favorited_set = {*article.favorited_user_ids, user.id} 75 | article.favorited_user_ids = tuple(favorited_set) 76 | await engine.save(article) 77 | return article 78 | 79 | 80 | async def remove_article_from_favorites( 81 | engine: AIOEngine, user: UserModel, article: ArticleModel 82 | ) -> ArticleModel: 83 | favorited_set = {*article.favorited_user_ids} - {user.id} 84 | article.favorited_user_ids = tuple(favorited_set) 85 | await engine.save(article) 86 | return article 87 | 88 | 89 | def ensure_is_author(user: UserModel, article: ArticleModel): 90 | if user != article.author: 91 | raise NotArticleAuthorException() 92 | -------------------------------------------------------------------------------- /src/core/comment.py: -------------------------------------------------------------------------------- 1 | from typing import List, Tuple 2 | 3 | from odmantic.bson import ObjectId 4 | from odmantic.engine import AIOEngine 5 | 6 | from core.exceptions import ( 7 | ArticleNotFoundException, 8 | CommentAuthorNotFoundException, 9 | CommentNotFoundException, 10 | NotCommentAuthorException, 11 | ) 12 | from models.article import ArticleModel, CommentModel 13 | from models.user import UserModel 14 | 15 | 16 | def ensure_is_comment_author(user: UserModel, comment: CommentModel): 17 | if comment.authorId != user.id: 18 | raise NotCommentAuthorException() 19 | 20 | 21 | async def add_new_comment( 22 | engine: AIOEngine, article: ArticleModel, comment: CommentModel 23 | ): 24 | article.comments += (comment,) 25 | await engine.save(article) 26 | 27 | 28 | async def get_article_comments_and_authors_by_slug( 29 | engine: AIOEngine, slug: str 30 | ) -> List[Tuple[CommentModel, UserModel]]: 31 | article = await engine.find_one(ArticleModel, ArticleModel.slug == slug) 32 | if article is None: 33 | raise ArticleNotFoundException() 34 | comment_authors = await engine.find( 35 | UserModel, 36 | UserModel.id.in_(comment.authorId for comment in article.comments), 37 | ) 38 | try: 39 | return list( 40 | [ 41 | ( 42 | comment, 43 | next( 44 | user for user in comment_authors if user.id == comment.authorId 45 | ), 46 | ) 47 | for comment in article.comments 48 | ] 49 | ) 50 | except StopIteration: 51 | raise CommentAuthorNotFoundException() 52 | 53 | 54 | def get_comment_and_index_from_id( 55 | article: ArticleModel, comment_id: ObjectId 56 | ) -> Tuple[CommentModel, int]: 57 | for index, comment in enumerate(article.comments): 58 | if comment.id == comment_id: 59 | return comment, index 60 | raise CommentNotFoundException() 61 | 62 | 63 | async def delete_comment_by_index(engine: AIOEngine, article: ArticleModel, index: int): 64 | article.comments = article.comments[:index] + article.comments[index + 1 :] 65 | await engine.save(article) 66 | -------------------------------------------------------------------------------- /src/core/exceptions.py: -------------------------------------------------------------------------------- 1 | from fastapi import HTTPException, status 2 | 3 | 4 | class UserNotFoundException(HTTPException): 5 | def __init__(self) -> None: 6 | super().__init__(status_code=status.HTTP_404_NOT_FOUND, detail="User not found") 7 | 8 | 9 | class ArticleNotFoundException(HTTPException): 10 | def __init__(self) -> None: 11 | super().__init__( 12 | status_code=status.HTTP_404_NOT_FOUND, detail="Article not found" 13 | ) 14 | 15 | 16 | class CommentNotFoundException(HTTPException): 17 | def __init__(self) -> None: 18 | super().__init__( 19 | status_code=status.HTTP_404_NOT_FOUND, detail="Comment not found" 20 | ) 21 | 22 | 23 | class CommentAuthorNotFoundException(HTTPException): 24 | def __init__(self) -> None: 25 | super().__init__( 26 | status_code=status.HTTP_404_NOT_FOUND, detail="Comment author not found" 27 | ) 28 | 29 | 30 | class ProfileNotFoundException(HTTPException): 31 | def __init__(self) -> None: 32 | super().__init__( 33 | status_code=status.HTTP_404_NOT_FOUND, detail="Profile not found" 34 | ) 35 | 36 | 37 | class NotArticleAuthorException(HTTPException): 38 | def __init__(self) -> None: 39 | super().__init__( 40 | status_code=status.HTTP_403_FORBIDDEN, 41 | detail="User is not author of the article", 42 | ) 43 | 44 | 45 | class NotCommentAuthorException(HTTPException): 46 | def __init__(self) -> None: 47 | super().__init__( 48 | status_code=status.HTTP_403_FORBIDDEN, 49 | detail="User is not author of the comment", 50 | ) 51 | 52 | 53 | class NotAuthenticatedException(HTTPException): 54 | def __init__(self) -> None: 55 | super().__init__( 56 | status_code=status.HTTP_401_UNAUTHORIZED, 57 | detail="Not authenticated", 58 | headers={"WWW-Authenticate": "Token"}, 59 | ) 60 | 61 | 62 | class CredentialsException(HTTPException): 63 | def __init__(self) -> None: 64 | super().__init__( 65 | status_code=status.HTTP_401_UNAUTHORIZED, 66 | detail="Could not validate credentials", 67 | headers={"WWW-Authenticate": "Token"}, 68 | ) 69 | 70 | 71 | class InvalidCredentialsException(HTTPException): 72 | def __init__(self) -> None: 73 | super().__init__( 74 | status_code=status.HTTP_401_UNAUTHORIZED, 75 | detail="Incorrect username or password", 76 | headers={"WWW-Authenticate": "Token"}, 77 | ) 78 | -------------------------------------------------------------------------------- /src/core/tag.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from odmantic import AIOEngine 4 | 5 | from models.article import ArticleModel 6 | 7 | 8 | async def get_all_tags(engine: AIOEngine) -> List[str]: 9 | pipeline = [ 10 | { 11 | "$unwind": { 12 | "path": ++ArticleModel.tag_list, 13 | "preserveNullAndEmptyArrays": True, 14 | } 15 | }, 16 | { 17 | "$group": { 18 | "_id": "all", 19 | "all_tags": {"$addToSet": ++ArticleModel.tag_list}, 20 | } 21 | }, 22 | ] 23 | col = engine.get_collection(ArticleModel) 24 | result = await col.aggregate(pipeline).to_list(length=1) 25 | if len(result) > 0: 26 | tags: List[str] = result[0]["all_tags"] 27 | else: 28 | tags = [] 29 | return tags 30 | -------------------------------------------------------------------------------- /src/core/user.py: -------------------------------------------------------------------------------- 1 | from odmantic import AIOEngine 2 | 3 | from core.exceptions import UserNotFoundException 4 | from models.user import UserModel 5 | 6 | 7 | async def get_user_by_username(engine: AIOEngine, username: str) -> UserModel: 8 | user = await engine.find_one(UserModel, UserModel.username == username) 9 | if user is None: 10 | raise UserNotFoundException() 11 | return user 12 | -------------------------------------------------------------------------------- /src/endpoints/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/art049/fastapi-odmantic-realworld-example/54725da134654b6874e57dd021d8737770b4e175/src/endpoints/__init__.py -------------------------------------------------------------------------------- /src/endpoints/article.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from typing import Optional 3 | 4 | from fastapi import APIRouter, Body, Depends 5 | 6 | from core.article import ( 7 | add_article_to_favorite, 8 | build_get_articles_query, 9 | build_get_feed_articles_query, 10 | ensure_is_author, 11 | get_article_by_slug, 12 | get_multiple_articles_response, 13 | remove_article_from_favorites, 14 | ) 15 | from models.article import ArticleModel 16 | from models.user import UserModel 17 | from schemas.article import ( 18 | MultipleArticlesResponse, 19 | NewArticle, 20 | SingleArticleResponse, 21 | UpdateArticle, 22 | ) 23 | from settings import Engine 24 | from utils.security import get_current_user_instance, get_current_user_optional_instance 25 | 26 | router = APIRouter() 27 | 28 | 29 | @router.get("/articles", response_model=MultipleArticlesResponse) 30 | async def get_articles( 31 | author: str | None = None, 32 | favorited: str | None = None, 33 | tag: str | None = None, 34 | limit: int = 20, 35 | offset: int = 0, 36 | user_instance: Optional[UserModel] = Depends(get_current_user_optional_instance), 37 | ): 38 | query = await build_get_articles_query(Engine, author, favorited, tag) 39 | if query is None: 40 | return MultipleArticlesResponse(articles=[], articles_count=0) 41 | response = await get_multiple_articles_response( 42 | Engine, user_instance, query, limit, offset 43 | ) 44 | return response 45 | 46 | 47 | @router.get("/articles/feed", response_model=MultipleArticlesResponse) 48 | async def get_feed_articles( 49 | limit: int = 20, 50 | offset: int = 0, 51 | user_instance: UserModel = Depends(get_current_user_instance), 52 | ): 53 | query = build_get_feed_articles_query(user_instance) 54 | response = await get_multiple_articles_response( 55 | Engine, user_instance, query, limit, offset 56 | ) 57 | return response 58 | 59 | 60 | @router.post("/articles", response_model=SingleArticleResponse) 61 | async def create_article( 62 | new_article: NewArticle = Body(..., embed=True, alias="article"), 63 | user_instance: UserModel = Depends(get_current_user_instance), 64 | ): 65 | print(new_article.tag_list) 66 | article = ArticleModel(author=user_instance, **new_article.model_dump()) 67 | article.tag_list.sort() 68 | await Engine.save(article) 69 | return SingleArticleResponse.from_article_instance(article, user_instance) 70 | 71 | 72 | @router.get("/articles/{slug}", response_model=SingleArticleResponse) 73 | async def get_single_article( 74 | slug: str, 75 | user_instance: Optional[UserModel] = Depends(get_current_user_optional_instance), 76 | ): 77 | article = await get_article_by_slug(Engine, slug) 78 | return SingleArticleResponse.from_article_instance(article, user_instance) 79 | 80 | 81 | @router.put("/articles/{slug}", response_model=SingleArticleResponse) 82 | async def update_article( 83 | slug: str, 84 | update_data: UpdateArticle = Body(..., embed=True, alias="article"), 85 | current_user: UserModel = Depends(get_current_user_instance), 86 | ): 87 | article = await get_article_by_slug(Engine, slug) 88 | ensure_is_author(current_user, article) 89 | 90 | patch_dict = update_data.dict(exclude_unset=True) 91 | for name, value in patch_dict.items(): 92 | setattr(article, name, value) 93 | article.updated_at = datetime.utcnow() 94 | await Engine.save(article) 95 | return SingleArticleResponse.from_article_instance(article, current_user) 96 | 97 | 98 | @router.delete("/articles/{slug}") 99 | async def delete_article( 100 | slug: str, 101 | current_user: UserModel = Depends(get_current_user_instance), 102 | ): 103 | article = await get_article_by_slug(Engine, slug) 104 | ensure_is_author(current_user, article) 105 | await Engine.delete(article) 106 | 107 | 108 | @router.post("/articles/{slug}/favorite", response_model=SingleArticleResponse) 109 | async def favorite_article( 110 | slug: str, 111 | current_user: UserModel = Depends(get_current_user_instance), 112 | ): 113 | article = await get_article_by_slug(Engine, slug) 114 | await add_article_to_favorite(Engine, user=current_user, article=article) 115 | return SingleArticleResponse.from_article_instance(article, current_user) 116 | 117 | 118 | @router.delete("/articles/{slug}/favorite", response_model=SingleArticleResponse) 119 | async def unfavorite_article( 120 | slug: str, 121 | current_user: UserModel = Depends(get_current_user_instance), 122 | ): 123 | article = await get_article_by_slug(Engine, slug) 124 | await remove_article_from_favorites(Engine, user=current_user, article=article) 125 | return SingleArticleResponse.from_article_instance(article, current_user) 126 | -------------------------------------------------------------------------------- /src/endpoints/comment.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter, Body, Depends 2 | from odmantic.bson import ObjectId 3 | 4 | from core.article import get_article_by_slug 5 | from core.comment import ( 6 | add_new_comment, 7 | delete_comment_by_index, 8 | ensure_is_comment_author, 9 | get_article_comments_and_authors_by_slug, 10 | get_comment_and_index_from_id, 11 | ) 12 | from models.article import CommentModel 13 | from models.user import UserModel 14 | from schemas.comment import MultipleCommentsResponse, NewComment, SingleCommentResponse 15 | from schemas.user import User 16 | from settings import Engine 17 | from utils.security import get_current_user_instance 18 | 19 | router = APIRouter() 20 | 21 | 22 | @router.get("/articles/{slug}/comments", response_model=MultipleCommentsResponse) 23 | async def get_article_comments( 24 | slug: str, 25 | ): 26 | data = await get_article_comments_and_authors_by_slug(Engine, slug) 27 | return MultipleCommentsResponse.from_comments_and_authors(data) 28 | 29 | 30 | @router.post("/articles/{slug}/comments", response_model=SingleCommentResponse) 31 | async def add_article_comment( 32 | slug: str, 33 | new_comment: NewComment = Body(..., embed=True, alias="comment"), 34 | user_instance: UserModel = Depends(get_current_user_instance), 35 | ): 36 | article = await get_article_by_slug(Engine, slug) 37 | comment_instance = CommentModel( 38 | authorId=user_instance.id, **new_comment.model_dump() 39 | ) 40 | await add_new_comment(Engine, article, comment_instance) 41 | return SingleCommentResponse( 42 | comment={**comment_instance.dict(), "author": user_instance} 43 | ) 44 | 45 | 46 | @router.delete("/articles/{slug}/comments/{id}") 47 | async def delete_article_comment( 48 | slug: str, 49 | id: ObjectId, 50 | user_instance: User = Depends(get_current_user_instance), 51 | ): 52 | article = await get_article_by_slug(Engine, slug) 53 | comment, index = get_comment_and_index_from_id(article, id) 54 | ensure_is_comment_author(user_instance, comment) 55 | await delete_comment_by_index(Engine, article, index) 56 | -------------------------------------------------------------------------------- /src/endpoints/profile.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from fastapi import APIRouter, Depends 4 | 5 | from core.user import get_user_by_username 6 | from models.user import UserModel 7 | from schemas.user import Profile, ProfileResponse 8 | from settings import Engine 9 | from utils.security import get_current_user_instance, get_current_user_optional_instance 10 | 11 | router = APIRouter() 12 | 13 | 14 | @router.get("/profiles/{username}", response_model=ProfileResponse) 15 | async def get_profile( 16 | username: str, 17 | logged_user: Optional[UserModel] = Depends(get_current_user_optional_instance), 18 | ): 19 | user = await get_user_by_username(Engine, username) 20 | following = False 21 | if logged_user is not None and user.id in logged_user.following_ids: 22 | following = True 23 | return ProfileResponse(profile=Profile(following=following, **user.dict())) 24 | 25 | 26 | @router.post("/profiles/{username}/follow", response_model=ProfileResponse) 27 | async def follow_user( 28 | username: str, 29 | user_instance: UserModel = Depends(get_current_user_instance), 30 | ): 31 | user_to_follow = await get_user_by_username(Engine, username) 32 | following_set = set(user_instance.following_ids) | set((user_to_follow.id,)) 33 | user_instance.following_ids = tuple(following_set) 34 | await Engine.save(user_instance) 35 | profile = Profile(following=True, **user_to_follow.dict()) 36 | return ProfileResponse(profile=profile) 37 | 38 | 39 | @router.delete("/profiles/{username}/follow", response_model=ProfileResponse) 40 | async def unfollow_user( 41 | username: str, 42 | user_instance: UserModel = Depends(get_current_user_instance), 43 | ): 44 | user_to_unfollow = await get_user_by_username(Engine, username) 45 | following_set = set(user_instance.following_ids) - set((user_to_unfollow.id,)) 46 | user_instance.following_ids = tuple(following_set) 47 | await Engine.save(user_instance) 48 | profile = Profile(following=False, **user_to_unfollow.dict()) 49 | return ProfileResponse(profile=profile) 50 | -------------------------------------------------------------------------------- /src/endpoints/tag.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter 2 | 3 | from core.tag import get_all_tags 4 | from schemas.tag import TagsResponse 5 | from settings import Engine 6 | 7 | router = APIRouter() 8 | 9 | 10 | @router.get("/tags", response_model=TagsResponse) 11 | async def get_tags(): 12 | tags = await get_all_tags(Engine) 13 | return TagsResponse(tags=tags) 14 | -------------------------------------------------------------------------------- /src/endpoints/user.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter, Body, Depends 2 | 3 | from core.exceptions import InvalidCredentialsException 4 | from models.user import UserModel 5 | from schemas.user import LoginUser, NewUser, UpdateUser, User, UserResponse 6 | from settings import Engine 7 | from utils.security import ( 8 | OAUTH2_SCHEME, 9 | authenticate_user, 10 | create_access_token, 11 | get_current_user, 12 | get_current_user_instance, 13 | get_password_hash, 14 | ) 15 | 16 | router = APIRouter() 17 | 18 | 19 | @router.post("/users", response_model=UserResponse) 20 | async def register_user( 21 | user: NewUser = Body(..., embed=True), 22 | ): 23 | instance = UserModel( 24 | **user.dict(), hashed_password=get_password_hash(user.password) 25 | ) 26 | await Engine.save(instance) 27 | token = create_access_token(instance) 28 | return UserResponse(user=User(token=token, **user.dict())) 29 | 30 | 31 | @router.post("/users/login", response_model=UserResponse) 32 | async def login_user( 33 | user_input: LoginUser = Body(..., embed=True, alias="user"), 34 | ): 35 | user = await authenticate_user( 36 | Engine, user_input.email, user_input.password.get_secret_value() 37 | ) 38 | if user is None: 39 | raise InvalidCredentialsException() 40 | 41 | token = create_access_token(user) 42 | return UserResponse(user=User(token=token, **user.dict())) 43 | 44 | 45 | @router.get("/user", response_model=UserResponse) 46 | async def current_user( 47 | current_user: User = Depends(get_current_user), 48 | ): 49 | return UserResponse(user=current_user) 50 | 51 | 52 | @router.put("/user", response_model=UserResponse) 53 | async def update_user( 54 | update_user: UpdateUser = Body(..., embed=True, alias="user"), 55 | user_instance: User = Depends(get_current_user_instance), 56 | token: str = Depends(OAUTH2_SCHEME), 57 | ): 58 | patch_dict = update_user.dict(exclude_unset=True) 59 | for name, value in patch_dict.items(): 60 | setattr(user_instance, name, value) 61 | await Engine.save(user_instance) 62 | return UserResponse(user=User(token=token, **user_instance.dict())) 63 | -------------------------------------------------------------------------------- /src/models/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/art049/fastapi-odmantic-realworld-example/54725da134654b6874e57dd021d8737770b4e175/src/models/__init__.py -------------------------------------------------------------------------------- /src/models/article.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from typing import List, Tuple 3 | from uuid import uuid4 4 | 5 | from odmantic import EmbeddedModel, Field, Model, ObjectId, Reference 6 | from pydantic import root_validator 7 | 8 | from models.user import UserModel 9 | 10 | 11 | def generate_random_str(): 12 | s = str(uuid4()) 13 | return s.split("-")[0] 14 | 15 | 16 | class CommentModel(EmbeddedModel): 17 | """Comment embedded model with a unique id field""" 18 | 19 | id: ObjectId = Field(default_factory=ObjectId) 20 | body: str 21 | created_at: datetime = Field(default_factory=datetime.utcnow) 22 | updated_at: datetime = Field(default_factory=datetime.utcnow) 23 | authorId: ObjectId 24 | 25 | 26 | class ArticleModel(Model): 27 | slug: str 28 | # NOTE: slug is not a primary field because it could change and this would imply to 29 | # change all the references 30 | title: str 31 | description: str 32 | body: str 33 | tag_list: List[str] = [] 34 | created_at: datetime = Field(default_factory=datetime.utcnow) 35 | updated_at: datetime = Field(default_factory=datetime.utcnow) 36 | author: UserModel = Reference() 37 | favorited_user_ids: Tuple[ObjectId, ...] = () 38 | comments: Tuple[CommentModel, ...] = () 39 | 40 | @root_validator(pre=True) 41 | def generate_slug(cls, values): 42 | if values.get("slug") is not None: 43 | return values 44 | title = values.get("title", "") 45 | words = title.split()[:5] 46 | words = [w.lower() for w in words] 47 | slug = "-".join(words) + f"-{generate_random_str()}" 48 | values["slug"] = slug 49 | # Note on why the tag_list is sorted: 50 | # https://github.com/gothinkster/realworld/issues/839 51 | if values.get("tag_list") is not None and isinstance(values["tag_list"], list): 52 | values["tag_list"].sort() 53 | return values 54 | -------------------------------------------------------------------------------- /src/models/user.py: -------------------------------------------------------------------------------- 1 | from typing import Optional, Tuple 2 | 3 | from odmantic import Model 4 | from odmantic.bson import ObjectId 5 | 6 | 7 | class UserModel(Model): 8 | username: str 9 | email: str 10 | hashed_password: str 11 | bio: Optional[str] = None 12 | image: Optional[str] = None 13 | following_ids: Tuple[ObjectId, ...] = () 14 | -------------------------------------------------------------------------------- /src/schemas/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/art049/fastapi-odmantic-realworld-example/54725da134654b6874e57dd021d8737770b4e175/src/schemas/__init__.py -------------------------------------------------------------------------------- /src/schemas/article.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from typing import List, Optional, Sequence 3 | 4 | from pydantic import Field 5 | 6 | from models.article import ArticleModel 7 | from models.user import UserModel 8 | from schemas.user import Profile 9 | 10 | from .base import BaseSchema 11 | 12 | 13 | class Article(BaseSchema): 14 | slug: str 15 | title: str 16 | description: str 17 | body: str 18 | tag_list: List[str] = Field(..., alias="tagList") 19 | created_at: datetime = Field(..., alias="createdAt") 20 | updated_at: datetime = Field(..., alias="updatedAt") 21 | favorited: bool = False 22 | favorites_count: int = Field(0, alias="favoritesCount") 23 | author: Profile 24 | 25 | @classmethod 26 | def from_article_instance( 27 | cls, article: ArticleModel, user: Optional[UserModel] = None 28 | ) -> "Article": 29 | if user is None: 30 | favorited = False 31 | else: 32 | favorited = user.id in article.favorited_user_ids 33 | 34 | return cls( 35 | favorited=favorited, 36 | favorites_count=len(article.favorited_user_ids), 37 | **article.dict() 38 | ) 39 | 40 | 41 | class SingleArticleResponse(BaseSchema): 42 | article: Article 43 | 44 | @classmethod 45 | def from_article_instance( 46 | cls, article: ArticleModel, user: Optional[UserModel] = None 47 | ) -> "SingleArticleResponse": 48 | return cls(article=Article.from_article_instance(article=article, user=user)) 49 | 50 | 51 | class MultipleArticlesResponse(BaseSchema): 52 | articles: List[Article] 53 | articles_count: int = Field(..., alias="articlesCount") 54 | 55 | @classmethod 56 | def from_article_instances( 57 | cls, 58 | articles: Sequence[ArticleModel], 59 | total_count: int, 60 | user: Optional[UserModel] = None, 61 | ) -> "MultipleArticlesResponse": 62 | articles = [Article.from_article_instance(a, user) for a in articles] 63 | return cls(articles=articles, articles_count=total_count) 64 | 65 | 66 | class NewArticle(BaseSchema): 67 | title: str 68 | description: str 69 | body: str 70 | tag_list: Optional[List[str]] = Field(None, alias="tagList") 71 | 72 | 73 | class UpdateArticle(BaseSchema): 74 | title: Optional[str] = None 75 | description: Optional[str] = None 76 | body: Optional[str] = None 77 | -------------------------------------------------------------------------------- /src/schemas/base.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | from odmantic.bson import BSON_TYPES_ENCODERS 4 | from pydantic.main import BaseModel 5 | 6 | 7 | class BaseSchema(BaseModel): 8 | model_config = { 9 | "populate_by_name": True, 10 | "json_encoders": { 11 | datetime: lambda d: d.strftime("%Y-%m-%dT%H:%M:%S.%fZ"), 12 | **BSON_TYPES_ENCODERS, 13 | }, 14 | "from_attributes": True, 15 | } 16 | -------------------------------------------------------------------------------- /src/schemas/comment.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from typing import List, Tuple 3 | 4 | from odmantic.bson import ObjectId 5 | from pydantic import Field 6 | 7 | from models.article import CommentModel 8 | from models.user import UserModel 9 | from schemas.user import Profile 10 | 11 | from .base import BaseSchema 12 | 13 | 14 | class Comment(BaseSchema): 15 | id: ObjectId 16 | created_at: datetime = Field(..., alias="createdAt") 17 | updated_at: datetime = Field(..., alias="updatedAt") 18 | body: str 19 | author: Profile 20 | 21 | 22 | class SingleCommentResponse(BaseSchema): 23 | comment: Comment 24 | 25 | 26 | class MultipleCommentsResponse(BaseSchema): 27 | comments: List[Comment] 28 | 29 | @classmethod 30 | def from_comments_and_authors(cls, data: List[Tuple[CommentModel, UserModel]]): 31 | return cls( 32 | comments=[{**comment.dict(), "author": author} for comment, author in data] 33 | ) 34 | 35 | 36 | class NewComment(BaseSchema): 37 | body: str 38 | 39 | 40 | class ProfileResponse(BaseSchema): 41 | profile: Profile 42 | -------------------------------------------------------------------------------- /src/schemas/tag.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from .base import BaseSchema 4 | 5 | 6 | class TagsResponse(BaseSchema): 7 | tags: List[str] 8 | -------------------------------------------------------------------------------- /src/schemas/user.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from pydantic import SecretStr 4 | 5 | from .base import BaseSchema 6 | 7 | 8 | class LoginUser(BaseSchema): 9 | email: str 10 | password: SecretStr 11 | 12 | 13 | class NewUser(BaseSchema): 14 | username: str 15 | email: str 16 | password: str 17 | 18 | 19 | class User(BaseSchema): 20 | email: str 21 | token: str 22 | username: str 23 | bio: Optional[str] = None 24 | image: Optional[str] = None 25 | 26 | 27 | class UserResponse(BaseSchema): 28 | user: User 29 | 30 | 31 | class UpdateUser(BaseSchema): 32 | email: Optional[str] = None 33 | token: Optional[str] = None 34 | username: Optional[str] = None 35 | bio: Optional[str] = None 36 | image: Optional[str] = None 37 | 38 | 39 | class Profile(BaseSchema): 40 | username: str 41 | bio: Optional[str] 42 | image: Optional[str] 43 | following: bool = False 44 | 45 | 46 | class ProfileResponse(BaseSchema): 47 | profile: Profile 48 | -------------------------------------------------------------------------------- /src/settings.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from motor.motor_asyncio import AsyncIOMotorClient 4 | from odmantic import AIOEngine 5 | from pydantic import Field 6 | from pydantic.types import SecretStr 7 | from pydantic_settings import BaseSettings 8 | 9 | 10 | class _Settings(BaseSettings): 11 | SECRET_KEY: SecretStr = Field( 12 | "09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7" 13 | ) 14 | ALGORITHM: str = "HS256" 15 | ACCESS_TOKEN_EXPIRE_MINUTES: int = 30 16 | MONGO_URI: Optional[str] = None 17 | 18 | 19 | # Make this a singleton to avoid reloading it from the env everytime 20 | SETTINGS = _Settings() 21 | 22 | MotorClient = AsyncIOMotorClient(SETTINGS.MONGO_URI) 23 | Engine = AIOEngine(MotorClient, database="test") 24 | -------------------------------------------------------------------------------- /src/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/art049/fastapi-odmantic-realworld-example/54725da134654b6874e57dd021d8737770b4e175/src/utils/__init__.py -------------------------------------------------------------------------------- /src/utils/security.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime, timedelta 2 | from typing import Optional, cast 3 | 4 | from fastapi import Depends, FastAPI, HTTPException 5 | from fastapi.openapi.models import OAuthFlows as OAuthFlowsModel 6 | from fastapi.security import OAuth2 7 | from fastapi.security.utils import get_authorization_scheme_param 8 | from jose import JWTError, jwt 9 | from odmantic import AIOEngine 10 | from passlib.context import CryptContext 11 | from pydantic import BaseModel, ValidationError 12 | from starlette.requests import Request 13 | 14 | from core.exceptions import CredentialsException, NotAuthenticatedException 15 | from models.user import UserModel 16 | from schemas.user import User 17 | from settings import SETTINGS, Engine 18 | 19 | 20 | class Token(BaseModel): 21 | access_token: str 22 | token_type: str 23 | 24 | 25 | class TokenContent(BaseModel): 26 | username: str 27 | 28 | 29 | PWD_CONTEXT = CryptContext(schemes=["bcrypt"], deprecated="auto") 30 | 31 | 32 | class OAuth2PasswordToken(OAuth2): 33 | def __init__( 34 | self, 35 | tokenUrl: str, 36 | scheme_name: Optional[str] = None, 37 | scopes: Optional[dict] = None, 38 | ): 39 | if not scopes: 40 | scopes = {} 41 | flows = OAuthFlowsModel(password={"tokenUrl": tokenUrl, "scopes": scopes}) 42 | super().__init__(flows=flows, scheme_name=scheme_name, auto_error=False) 43 | 44 | async def __call__(self, request: Request) -> Optional[str]: 45 | authorization: str = request.headers.get("Authorization") 46 | scheme, param = get_authorization_scheme_param(authorization) 47 | if not authorization or scheme.lower() != "token": 48 | return None 49 | return cast(str, param) 50 | 51 | 52 | OAUTH2_SCHEME = OAuth2PasswordToken(tokenUrl="/users") 53 | 54 | app = FastAPI() 55 | 56 | 57 | def verify_password(plain_password, hashed_password): 58 | return PWD_CONTEXT.verify(plain_password, hashed_password) 59 | 60 | 61 | def get_password_hash(password): 62 | return PWD_CONTEXT.hash(password) 63 | 64 | 65 | async def get_user_instance( 66 | engine: AIOEngine, username: Optional[str] = None, email: Optional[str] = None 67 | ) -> Optional[UserModel]: 68 | """Get a user instance from its username""" 69 | if username is not None: 70 | query = UserModel.username == username 71 | elif email is not None: 72 | query = UserModel.email == email 73 | else: 74 | return None 75 | user = await engine.find_one(UserModel, query) 76 | return user 77 | 78 | 79 | async def authenticate_user( 80 | engine: AIOEngine, email: str, password: str 81 | ) -> Optional[UserModel]: 82 | """Verify the User/Password pair against the DB content""" 83 | user = await get_user_instance(engine, email=email) 84 | if user is None: 85 | return None 86 | if not verify_password(password, user.hashed_password): 87 | return None 88 | return user 89 | 90 | 91 | def create_access_token(user: UserModel) -> str: 92 | token_content = TokenContent(username=user.username) 93 | expire = datetime.utcnow() + timedelta(minutes=SETTINGS.ACCESS_TOKEN_EXPIRE_MINUTES) 94 | to_encode = {"exp": expire, "sub": token_content.json()} 95 | encoded_jwt = jwt.encode( 96 | to_encode, SETTINGS.SECRET_KEY.get_secret_value(), algorithm=SETTINGS.ALGORITHM 97 | ) 98 | return str(encoded_jwt) 99 | 100 | 101 | async def get_current_user_instance( 102 | token: Optional[str] = Depends(OAUTH2_SCHEME), 103 | ) -> UserModel: 104 | """Decode the JWT and return the associated User""" 105 | if token is None: 106 | raise NotAuthenticatedException() 107 | try: 108 | payload = jwt.decode( 109 | token, 110 | SETTINGS.SECRET_KEY.get_secret_value(), 111 | algorithms=[SETTINGS.ALGORITHM], 112 | ) 113 | except JWTError: 114 | raise CredentialsException() 115 | 116 | try: 117 | token_content = TokenContent.parse_raw(payload.get("sub")) 118 | except ValidationError: 119 | raise CredentialsException() 120 | 121 | user = await get_user_instance(Engine, username=token_content.username) 122 | if user is None: 123 | raise CredentialsException() 124 | return user 125 | 126 | 127 | async def get_current_user_optional_instance( 128 | token: str = Depends(OAUTH2_SCHEME), 129 | ) -> Optional[UserModel]: 130 | try: 131 | user = await get_current_user_instance(token) 132 | return user 133 | except HTTPException: 134 | return None 135 | 136 | 137 | async def get_current_user( 138 | user_instance: UserModel = Depends(get_current_user_instance), 139 | token: str = Depends(OAUTH2_SCHEME), 140 | ) -> User: 141 | return User(token=token, **user_instance.dict()) 142 | -------------------------------------------------------------------------------- /tests/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/art049/fastapi-odmantic-realworld-example/54725da134654b6874e57dd021d8737770b4e175/tests/.gitkeep --------------------------------------------------------------------------------