├── .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 | # 
2 |
3 |
4 |
5 |
10 |
11 | 
12 | [](https://github.com/pre-commit/pre-commit)
13 | [](https://github.com/python/black)
14 | [](http://mypy-lang.org/)
15 | [](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 | [](https://github.com/art049/fastapi-odmantic-realworld-example/actions/workflows/ci.yml)
22 | [](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
--------------------------------------------------------------------------------