├── .coveragerc ├── .env.test ├── .github └── FUNDING.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .python-version ├── .vscode └── settings.json ├── LICENSE ├── Pipfile ├── Pipfile.lock ├── README.md ├── __tests__ ├── repositories │ ├── test_AuthorRepository.py │ └── test_BookRepository.py └── services │ ├── test_AuthorService.py │ └── test_BookService.py ├── configs ├── Database.py ├── Environment.py ├── GraphQL.py └── __init__.py ├── main.py ├── metadata ├── Tags.py └── __init__.py ├── models ├── AuthorModel.py ├── BaseModel.py ├── BookAuthorAssociation.py ├── BookModel.py └── __init__.py ├── pyproject.toml ├── repositories ├── AuthorRepository.py ├── BookRepository.py ├── RepositoryMeta.py └── __init__.py ├── routers ├── __init__.py └── v1 │ ├── AuthorRouter.py │ ├── BookRouter.py │ └── __init__.py ├── schemas ├── __init__.py ├── graphql │ ├── Author.py │ ├── Book.py │ ├── Mutation.py │ ├── Query.py │ └── __init__.py └── pydantic │ ├── AuthorSchema.py │ ├── BookSchema.py │ └── __init__.py └── services ├── AuthorService.py ├── BookService.py └── __init__.py /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | omit = 3 | __mocks__/* 4 | __tests__/* 5 | **/__init__.py 6 | main.py 7 | -------------------------------------------------------------------------------- /.env.test: -------------------------------------------------------------------------------- 1 | API_VERSION=1.0.0 2 | APP_NAME=fastapi-example 3 | DATABASE_DIALECT=mysql 4 | DATABASE_HOSTNAME=localhost 5 | DATABASE_NAME=test_bookstore 6 | DATABASE_PASSWORD=root1234 7 | DATABASE_PORT=3306 8 | DATABASE_USERNAME=root 9 | DEBUG_MODE=true 10 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # List of Supported Funding Platforms 2 | 3 | github: 0xTheProDev 4 | -------------------------------------------------------------------------------- /.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 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | # .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | # Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | -------------------------------------------------------------------------------- /.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: v3.2.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/psf/black 12 | rev: 22.3.0 13 | hooks: 14 | - id: black 15 | exclude: env 16 | fail_fast: true 17 | -------------------------------------------------------------------------------- /.python-version: -------------------------------------------------------------------------------- 1 | 3.10.9 2 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.formatting.provider": "black", 3 | "editor.formatOnSave": true, 4 | "editor.tabSize": 4, 5 | "files.associations": { 6 | ".coveragerc": "toml", 7 | ".env": "properties", 8 | "pyproject.toml": "toml" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Progyan Bhattacharya 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | url = "https://pypi.org/simple" 3 | verify_ssl = true 4 | name = "pypi" 5 | 6 | [packages] 7 | fastapi = "==0.75.1" 8 | sqlalchemy = "==1.4.35" 9 | mysqlclient = "==2.1.0" 10 | uvicorn = {extras = ["standard"], version = "==0.17.6"} 11 | python-dotenv = "==0.20.0" 12 | strawberry-graphql = {extras = ["fastapi"], version = "==0.114.0"} 13 | 14 | [dev-packages] 15 | pre-commit = "==2.18.1" 16 | pytest = "==7.1.2" 17 | pytest-cov = "==3.0.0" 18 | black = "==22.3.0" 19 | strawberry-graphql = {extras = ["debug-server"], version = "==0.114.0"} 20 | 21 | [requires] 22 | python_version = "3.8" 23 | -------------------------------------------------------------------------------- /Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "8d4e5615df9dd497e6e543d69defb1b77e94a7ee81300130a1f2713dd543ec0c" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3.8" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": { 19 | "anyio": { 20 | "hashes": [ 21 | "sha256:413adf95f93886e442aea925f3ee43baa5a765a64a0f52c6081894f9992fdd0b", 22 | "sha256:cb29b9c70620506a9a8f87a309591713446953302d7d995344d0d7c6c0c9a7be" 23 | ], 24 | "markers": "python_full_version >= '3.6.2'", 25 | "version": "==3.6.1" 26 | }, 27 | "asgiref": { 28 | "hashes": [ 29 | "sha256:1d2880b792ae8757289136f1db2b7b99100ce959b2aa57fd69dab783d05afac4", 30 | "sha256:4a29362a6acebe09bf1d6640db38c1dc3d9217c68e6f9f6204d72667fc19a424" 31 | ], 32 | "markers": "python_version >= '3.7'", 33 | "version": "==3.5.2" 34 | }, 35 | "backports.cached-property": { 36 | "hashes": [ 37 | "sha256:1a5ef1e750f8bc7d0204c807aae8e0f450c655be0cf4b30407a35fd4bb27186c", 38 | "sha256:687b5fe14be40aadcf547cae91337a1fdb84026046a39370274e54d3fe4fb4f9" 39 | ], 40 | "markers": "python_version >= '3.6'", 41 | "version": "==1.0.1" 42 | }, 43 | "click": { 44 | "hashes": [ 45 | "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e", 46 | "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48" 47 | ], 48 | "markers": "python_version >= '3.7'", 49 | "version": "==8.1.3" 50 | }, 51 | "fastapi": { 52 | "hashes": [ 53 | "sha256:8b62bde916d657803fb60fffe88e2b2c9fb854583784607e4347681cae20ad01", 54 | "sha256:f46f8fc81261c2bd956584114da9da98c84e2410c807bc2487532dabf55e7ab8" 55 | ], 56 | "index": "pypi", 57 | "version": "==0.75.1" 58 | }, 59 | "graphql-core": { 60 | "hashes": [ 61 | "sha256:9d1bf141427b7d54be944587c8349df791ce60ade2e3cccaf9c56368c133c201", 62 | "sha256:f83c658e4968998eed1923a2e3e3eddd347e005ac0315fbb7ca4d70ea9156323" 63 | ], 64 | "markers": "python_version >= '3.6' and python_version < '4'", 65 | "version": "==3.2.1" 66 | }, 67 | "greenlet": { 68 | "hashes": [ 69 | "sha256:0051c6f1f27cb756ffc0ffbac7d2cd48cb0362ac1736871399a739b2885134d3", 70 | "sha256:00e44c8afdbe5467e4f7b5851be223be68adb4272f44696ee71fe46b7036a711", 71 | "sha256:013d61294b6cd8fe3242932c1c5e36e5d1db2c8afb58606c5a67efce62c1f5fd", 72 | "sha256:049fe7579230e44daef03a259faa24511d10ebfa44f69411d99e6a184fe68073", 73 | "sha256:14d4f3cd4e8b524ae9b8aa567858beed70c392fdec26dbdb0a8a418392e71708", 74 | "sha256:166eac03e48784a6a6e0e5f041cfebb1ab400b394db188c48b3a84737f505b67", 75 | "sha256:17ff94e7a83aa8671a25bf5b59326ec26da379ace2ebc4411d690d80a7fbcf23", 76 | "sha256:1e12bdc622676ce47ae9abbf455c189e442afdde8818d9da983085df6312e7a1", 77 | "sha256:21915eb821a6b3d9d8eefdaf57d6c345b970ad722f856cd71739493ce003ad08", 78 | "sha256:288c6a76705dc54fba69fbcb59904ae4ad768b4c768839b8ca5fdadec6dd8cfd", 79 | "sha256:2bde6792f313f4e918caabc46532aa64aa27a0db05d75b20edfc5c6f46479de2", 80 | "sha256:32ca72bbc673adbcfecb935bb3fb1b74e663d10a4b241aaa2f5a75fe1d1f90aa", 81 | "sha256:356b3576ad078c89a6107caa9c50cc14e98e3a6c4874a37c3e0273e4baf33de8", 82 | "sha256:40b951f601af999a8bf2ce8c71e8aaa4e8c6f78ff8afae7b808aae2dc50d4c40", 83 | "sha256:572e1787d1460da79590bf44304abbc0a2da944ea64ec549188fa84d89bba7ab", 84 | "sha256:58df5c2a0e293bf665a51f8a100d3e9956febfbf1d9aaf8c0677cf70218910c6", 85 | "sha256:64e6175c2e53195278d7388c454e0b30997573f3f4bd63697f88d855f7a6a1fc", 86 | "sha256:7227b47e73dedaa513cdebb98469705ef0d66eb5a1250144468e9c3097d6b59b", 87 | "sha256:7418b6bfc7fe3331541b84bb2141c9baf1ec7132a7ecd9f375912eca810e714e", 88 | "sha256:7cbd7574ce8e138bda9df4efc6bf2ab8572c9aff640d8ecfece1b006b68da963", 89 | "sha256:7ff61ff178250f9bb3cd89752df0f1dd0e27316a8bd1465351652b1b4a4cdfd3", 90 | "sha256:833e1551925ed51e6b44c800e71e77dacd7e49181fdc9ac9a0bf3714d515785d", 91 | "sha256:8639cadfda96737427330a094476d4c7a56ac03de7265622fcf4cfe57c8ae18d", 92 | "sha256:8c5d5b35f789a030ebb95bff352f1d27a93d81069f2adb3182d99882e095cefe", 93 | "sha256:8c790abda465726cfb8bb08bd4ca9a5d0a7bd77c7ac1ca1b839ad823b948ea28", 94 | "sha256:8d2f1fb53a421b410751887eb4ff21386d119ef9cde3797bf5e7ed49fb51a3b3", 95 | "sha256:903bbd302a2378f984aef528f76d4c9b1748f318fe1294961c072bdc7f2ffa3e", 96 | "sha256:93f81b134a165cc17123626ab8da2e30c0455441d4ab5576eed73a64c025b25c", 97 | "sha256:95e69877983ea39b7303570fa6760f81a3eec23d0e3ab2021b7144b94d06202d", 98 | "sha256:9633b3034d3d901f0a46b7939f8c4d64427dfba6bbc5a36b1a67364cf148a1b0", 99 | "sha256:97e5306482182170ade15c4b0d8386ded995a07d7cc2ca8f27958d34d6736497", 100 | "sha256:9f3cba480d3deb69f6ee2c1825060177a22c7826431458c697df88e6aeb3caee", 101 | "sha256:aa5b467f15e78b82257319aebc78dd2915e4c1436c3c0d1ad6f53e47ba6e2713", 102 | "sha256:abb7a75ed8b968f3061327c433a0fbd17b729947b400747c334a9c29a9af6c58", 103 | "sha256:aec52725173bd3a7b56fe91bc56eccb26fbdff1386ef123abb63c84c5b43b63a", 104 | "sha256:b11548073a2213d950c3f671aa88e6f83cda6e2fb97a8b6317b1b5b33d850e06", 105 | "sha256:b1692f7d6bc45e3200844be0dba153612103db241691088626a33ff1f24a0d88", 106 | "sha256:b336501a05e13b616ef81ce329c0e09ac5ed8c732d9ba7e3e983fcc1a9e86965", 107 | "sha256:b8c008de9d0daba7b6666aa5bbfdc23dcd78cafc33997c9b7741ff6353bafb7f", 108 | "sha256:b92e29e58bef6d9cfd340c72b04d74c4b4e9f70c9fa7c78b674d1fec18896dc4", 109 | "sha256:be5f425ff1f5f4b3c1e33ad64ab994eed12fc284a6ea71c5243fd564502ecbe5", 110 | "sha256:dd0b1e9e891f69e7675ba5c92e28b90eaa045f6ab134ffe70b52e948aa175b3c", 111 | "sha256:e30f5ea4ae2346e62cedde8794a56858a67b878dd79f7df76a0767e356b1744a", 112 | "sha256:e6a36bb9474218c7a5b27ae476035497a6990e21d04c279884eb10d9b290f1b1", 113 | "sha256:e859fcb4cbe93504ea18008d1df98dee4f7766db66c435e4882ab35cf70cac43", 114 | "sha256:eb6ea6da4c787111adf40f697b4e58732ee0942b5d3bd8f435277643329ba627", 115 | "sha256:ec8c433b3ab0419100bd45b47c9c8551248a5aee30ca5e9d399a0b57ac04651b", 116 | "sha256:eff9d20417ff9dcb0d25e2defc2574d10b491bf2e693b4e491914738b7908168", 117 | "sha256:f0214eb2a23b85528310dad848ad2ac58e735612929c8072f6093f3585fd342d", 118 | "sha256:f276df9830dba7a333544bd41070e8175762a7ac20350786b322b714b0e654f5", 119 | "sha256:f3acda1924472472ddd60c29e5b9db0cec629fbe3c5c5accb74d6d6d14773478", 120 | "sha256:f70a9e237bb792c7cc7e44c531fd48f5897961701cdaa06cf22fc14965c496cf", 121 | "sha256:f9d29ca8a77117315101425ec7ec2a47a22ccf59f5593378fc4077ac5b754fce", 122 | "sha256:fa877ca7f6b48054f847b61d6fa7bed5cebb663ebc55e018fda12db09dcc664c", 123 | "sha256:fdcec0b8399108577ec290f55551d926d9a1fa6cad45882093a7a07ac5ec147b" 124 | ], 125 | "markers": "python_version >= '3' and platform_machine == 'aarch64' or (platform_machine == 'ppc64le' or (platform_machine == 'x86_64' or (platform_machine == 'amd64' or (platform_machine == 'AMD64' or (platform_machine == 'win32' or platform_machine == 'WIN32')))))", 126 | "version": "==1.1.2" 127 | }, 128 | "h11": { 129 | "hashes": [ 130 | "sha256:70813c1135087a248a4d38cc0e1a0181ffab2188141a93eaf567940c3957ff06", 131 | "sha256:8ddd78563b633ca55346c8cd41ec0af27d3c79931828beffb46ce70a379e7442" 132 | ], 133 | "markers": "python_version >= '3.6'", 134 | "version": "==0.13.0" 135 | }, 136 | "httptools": { 137 | "hashes": [ 138 | "sha256:1a99346ebcb801b213c591540837340bdf6fd060a8687518d01c607d338b7424", 139 | "sha256:1ee0b459257e222b878a6c09ccf233957d3a4dcb883b0847640af98d2d9aac23", 140 | "sha256:20a45bcf22452a10fa8d58b7dbdb474381f6946bf5b8933e3662d572bc61bae4", 141 | "sha256:29bf97a5c532da9c7a04de2c7a9c31d1d54f3abd65a464119b680206bbbb1055", 142 | "sha256:2c9a930c378b3d15d6b695fb95ebcff81a7395b4f9775c4f10a076beb0b2c1ff", 143 | "sha256:2db44a0b294d317199e9f80123e72c6b005c55b625b57fae36de68670090fa48", 144 | "sha256:3194f6d6443befa8d4db16c1946b2fc428a3ceb8ab32eb6f09a59f86104dc1a0", 145 | "sha256:34d2903dd2a3dd85d33705b6fde40bf91fc44411661283763fd0746723963c83", 146 | "sha256:48e48530d9b995a84d1d89ae6b3ec4e59ea7d494b150ac3bbc5e2ac4acce92cd", 147 | "sha256:54bbd295f031b866b9799dd39cb45deee81aca036c9bff9f58ca06726f6494f1", 148 | "sha256:5d1fe6b6661022fd6cac541f54a4237496b246e6f1c0a6b41998ee08a1135afe", 149 | "sha256:645373c070080e632480a3d251d892cb795be3d3a15f86975d0f1aca56fd230d", 150 | "sha256:6a1a7dfc1f9c78a833e2c4904757a0f47ce25d08634dd2a52af394eefe5f9777", 151 | "sha256:701e66b59dd21a32a274771238025d58db7e2b6ecebbab64ceff51b8e31527ae", 152 | "sha256:72aa3fbe636b16d22e04b5a9d24711b043495e0ecfe58080addf23a1a37f3409", 153 | "sha256:7af6bdbd21a2a25d6784f6d67f44f5df33ef39b6159543b9f9064d365c01f919", 154 | "sha256:7ee9f226acab9085037582c059d66769862706e8e8cd2340470ceb8b3850873d", 155 | "sha256:7f7bfb74718f52d5ed47d608d507bf66d3bc01d4a8b3e6dd7134daaae129357b", 156 | "sha256:8e2eb957787cbb614a0f006bfc5798ff1d90ac7c4dd24854c84edbdc8c02369e", 157 | "sha256:903f739c9fb78dab8970b0f3ea51f21955b24b45afa77b22ff0e172fc11ef111", 158 | "sha256:98993805f1e3cdb53de4eed02b55dcc953cdf017ba7bbb2fd89226c086a6d855", 159 | "sha256:9967d9758df505975913304c434cb9ab21e2c609ad859eb921f2f615a038c8de", 160 | "sha256:a113789e53ac1fa26edf99856a61e4c493868e125ae0dd6354cf518948fbbd5c", 161 | "sha256:a522d12e2ddbc2e91842ffb454a1aeb0d47607972c7d8fc88bd0838d97fb8a2a", 162 | "sha256:abe829275cdd4174b4c4e65ad718715d449e308d59793bf3a931ee1bf7e7b86c", 163 | "sha256:c286985b5e194ca0ebb2908d71464b9be8f17cc66d6d3e330e8d5407248f56ad", 164 | "sha256:cd1295f52971097f757edfbfce827b6dbbfb0f7a74901ee7d4933dff5ad4c9af", 165 | "sha256:ceafd5e960b39c7e0d160a1936b68eb87c5e79b3979d66e774f0c77d4d8faaed", 166 | "sha256:d1f27bb0f75bef722d6e22dc609612bfa2f994541621cd2163f8c943b6463dfe", 167 | "sha256:d3a4e165ca6204f34856b765d515d558dc84f1352033b8721e8d06c3e44930c3", 168 | "sha256:d9b90bf58f3ba04e60321a23a8723a1ff2a9377502535e70495e5ada8e6e6722", 169 | "sha256:f72b5d24d6730035128b238decdc4c0f2104b7056a7ca55cf047c106842ec890", 170 | "sha256:fcddfe70553be717d9745990dfdb194e22ee0f60eb8f48c0794e7bfeda30d2d5", 171 | "sha256:fdb9f9ed79bc6f46b021b3319184699ba1a22410a82204e6e89c774530069683" 172 | ], 173 | "version": "==0.4.0" 174 | }, 175 | "idna": { 176 | "hashes": [ 177 | "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff", 178 | "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d" 179 | ], 180 | "markers": "python_version >= '3.5'", 181 | "version": "==3.3" 182 | }, 183 | "mysqlclient": { 184 | "hashes": [ 185 | "sha256:02c8826e6add9b20f4cb12dcf016485f7b1d6e30356a1204d05431867a1b3947", 186 | "sha256:2c8410f54492a3d2488a6a53e2d85b7e016751a1e7d116e7aea9c763f59f5e8c", 187 | "sha256:973235686f1b720536d417bf0a0d39b4ab3d5086b2b6ad5e6752393428c02b12", 188 | "sha256:b62d23c11c516cedb887377c8807628c1c65d57593b57853186a6ee18b0c6a5b", 189 | "sha256:e6279263d5a9feca3e0edbc2b2a52c057375bf301d47da2089c075ff76331d14" 190 | ], 191 | "index": "pypi", 192 | "version": "==2.1.0" 193 | }, 194 | "pydantic": { 195 | "hashes": [ 196 | "sha256:02eefd7087268b711a3ff4db528e9916ac9aa18616da7bca69c1871d0b7a091f", 197 | "sha256:059b6c1795170809103a1538255883e1983e5b831faea6558ef873d4955b4a74", 198 | "sha256:0bf07cab5b279859c253d26a9194a8906e6f4a210063b84b433cf90a569de0c1", 199 | "sha256:1542636a39c4892c4f4fa6270696902acb186a9aaeac6f6cf92ce6ae2e88564b", 200 | "sha256:177071dfc0df6248fd22b43036f936cfe2508077a72af0933d0c1fa269b18537", 201 | "sha256:18f3e912f9ad1bdec27fb06b8198a2ccc32f201e24174cec1b3424dda605a310", 202 | "sha256:1dd8fecbad028cd89d04a46688d2fcc14423e8a196d5b0a5c65105664901f810", 203 | "sha256:1ed987c3ff29fff7fd8c3ea3a3ea877ad310aae2ef9889a119e22d3f2db0691a", 204 | "sha256:447d5521575f18e18240906beadc58551e97ec98142266e521c34968c76c8761", 205 | "sha256:494f7c8537f0c02b740c229af4cb47c0d39840b829ecdcfc93d91dcbb0779892", 206 | "sha256:4988c0f13c42bfa9ddd2fe2f569c9d54646ce84adc5de84228cfe83396f3bd58", 207 | "sha256:4ce9ae9e91f46c344bec3b03d6ee9612802682c1551aaf627ad24045ce090761", 208 | "sha256:5d93d4e95eacd313d2c765ebe40d49ca9dd2ed90e5b37d0d421c597af830c195", 209 | "sha256:61b6760b08b7c395975d893e0b814a11cf011ebb24f7d869e7118f5a339a82e1", 210 | "sha256:72ccb318bf0c9ab97fc04c10c37683d9eea952ed526707fabf9ac5ae59b701fd", 211 | "sha256:79b485767c13788ee314669008d01f9ef3bc05db9ea3298f6a50d3ef596a154b", 212 | "sha256:7eb57ba90929bac0b6cc2af2373893d80ac559adda6933e562dcfb375029acee", 213 | "sha256:8bc541a405423ce0e51c19f637050acdbdf8feca34150e0d17f675e72d119580", 214 | "sha256:969dd06110cb780da01336b281f53e2e7eb3a482831df441fb65dd30403f4608", 215 | "sha256:985ceb5d0a86fcaa61e45781e567a59baa0da292d5ed2e490d612d0de5796918", 216 | "sha256:9bcf8b6e011be08fb729d110f3e22e654a50f8a826b0575c7196616780683380", 217 | "sha256:9ce157d979f742a915b75f792dbd6aa63b8eccaf46a1005ba03aa8a986bde34a", 218 | "sha256:9f659a5ee95c8baa2436d392267988fd0f43eb774e5eb8739252e5a7e9cf07e0", 219 | "sha256:a4a88dcd6ff8fd47c18b3a3709a89adb39a6373f4482e04c1b765045c7e282fd", 220 | "sha256:a955260d47f03df08acf45689bd163ed9df82c0e0124beb4251b1290fa7ae728", 221 | "sha256:a9af62e9b5b9bc67b2a195ebc2c2662fdf498a822d62f902bf27cccb52dbbf49", 222 | "sha256:ae72f8098acb368d877b210ebe02ba12585e77bd0db78ac04a1ee9b9f5dd2166", 223 | "sha256:b83ba3825bc91dfa989d4eed76865e71aea3a6ca1388b59fc801ee04c4d8d0d6", 224 | "sha256:c11951b404e08b01b151222a1cb1a9f0a860a8153ce8334149ab9199cd198131", 225 | "sha256:c320c64dd876e45254bdd350f0179da737463eea41c43bacbee9d8c9d1021f11", 226 | "sha256:c8098a724c2784bf03e8070993f6d46aa2eeca031f8d8a048dff277703e6e193", 227 | "sha256:d12f96b5b64bec3f43c8e82b4aab7599d0157f11c798c9f9c528a72b9e0b339a", 228 | "sha256:e565a785233c2d03724c4dc55464559639b1ba9ecf091288dd47ad9c629433bd", 229 | "sha256:f0f047e11febe5c3198ed346b507e1d010330d56ad615a7e0a89fae604065a0e", 230 | "sha256:fe4670cb32ea98ffbf5a1262f14c3e102cccd92b1869df3bb09538158ba90fe6" 231 | ], 232 | "markers": "python_full_version >= '3.6.1'", 233 | "version": "==1.9.1" 234 | }, 235 | "pygments": { 236 | "hashes": [ 237 | "sha256:5eb116118f9612ff1ee89ac96437bb6b49e8f04d8a13b514ba26f620208e26eb", 238 | "sha256:dc9c10fb40944260f6ed4c688ece0cd2048414940f1cea51b8b226318411c519" 239 | ], 240 | "markers": "python_version >= '3.6'", 241 | "version": "==2.12.0" 242 | }, 243 | "python-dateutil": { 244 | "hashes": [ 245 | "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86", 246 | "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9" 247 | ], 248 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 249 | "version": "==2.8.2" 250 | }, 251 | "python-dotenv": { 252 | "hashes": [ 253 | "sha256:b7e3b04a59693c42c36f9ab1cc2acc46fa5df8c78e178fc33a8d4cd05c8d498f", 254 | "sha256:d92a187be61fe482e4fd675b6d52200e7be63a12b724abbf931a40ce4fa92938" 255 | ], 256 | "index": "pypi", 257 | "version": "==0.20.0" 258 | }, 259 | "python-multipart": { 260 | "hashes": [ 261 | "sha256:f7bb5f611fc600d15fa47b3974c8aa16e93724513b49b5f95c81e6624c83fa43" 262 | ], 263 | "version": "==0.0.5" 264 | }, 265 | "pyyaml": { 266 | "hashes": [ 267 | "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293", 268 | "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b", 269 | "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57", 270 | "sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b", 271 | "sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4", 272 | "sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07", 273 | "sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba", 274 | "sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9", 275 | "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287", 276 | "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513", 277 | "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0", 278 | "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0", 279 | "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92", 280 | "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f", 281 | "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2", 282 | "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc", 283 | "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c", 284 | "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86", 285 | "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4", 286 | "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c", 287 | "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34", 288 | "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b", 289 | "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c", 290 | "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb", 291 | "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737", 292 | "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3", 293 | "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d", 294 | "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53", 295 | "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78", 296 | "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803", 297 | "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a", 298 | "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174", 299 | "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5" 300 | ], 301 | "version": "==6.0" 302 | }, 303 | "six": { 304 | "hashes": [ 305 | "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", 306 | "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" 307 | ], 308 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 309 | "version": "==1.16.0" 310 | }, 311 | "sniffio": { 312 | "hashes": [ 313 | "sha256:471b71698eac1c2112a40ce2752bb2f4a4814c22a54a3eed3676bc0f5ca9f663", 314 | "sha256:c4666eecec1d3f50960c6bdf61ab7bc350648da6c126e3cf6898d8cd4ddcd3de" 315 | ], 316 | "markers": "python_version >= '3.5'", 317 | "version": "==1.2.0" 318 | }, 319 | "sqlalchemy": { 320 | "hashes": [ 321 | "sha256:093b3109c2747d5dc0fa4314b1caf4c7ca336d5c8c831e3cfbec06a7e861e1e6", 322 | "sha256:186cb3bd77abf2ddcf722f755659559bfb157647b3fd3f32ea1c70e8311e8f6b", 323 | "sha256:1b4eac3933c335d7f375639885765722534bb4e52e51cdc01a667eea822af9b6", 324 | "sha256:1ff9f84b2098ef1b96255a80981ee10f4b5d49b6cfeeccf9632c2078cd86052e", 325 | "sha256:28aa2ef06c904729620cc735262192e622db9136c26d8587f71f29ec7715628a", 326 | "sha256:28b17ebbaee6587013be2f78dc4f6e95115e1ec8dd7647c4e7be048da749e48b", 327 | "sha256:2c6c411d8c59afba95abccd2b418f30ade674186660a2d310d364843049fb2c1", 328 | "sha256:2ffc813b01dc6473990f5e575f210ca5ac2f5465ace3908b78ffd6d20058aab5", 329 | "sha256:48036698f20080462e981b18d77d574631a3d1fc2c33b416c6df299ec1d10b99", 330 | "sha256:48f0eb5bcc87a9b2a95b345ed18d6400daaa86ca414f6840961ed85c342af8f4", 331 | "sha256:4ba2c1f368bcf8551cdaa27eac525022471015633d5bdafbc4297e0511f62f51", 332 | "sha256:53c7469b86a60fe2babca4f70111357e6e3d5150373bc85eb3b914356983e89a", 333 | "sha256:6204d06bfa85f87625e1831ca663f9dba91ac8aec24b8c65d02fb25cbaf4b4d7", 334 | "sha256:63c82c9e8ccc2fb4bfd87c24ffbac320f70b7c93b78f206c1f9c441fa3013a5f", 335 | "sha256:70e571ae9ee0ff36ed37e2b2765445d54981e4d600eccdf6fe3838bc2538d157", 336 | "sha256:95411abc0e36d18f54fa5e24d42960ea3f144fb16caaa5a8c2e492b5424cc82c", 337 | "sha256:9837133b89ad017e50a02a3b46419869cf4e9aa02743e911b2a9e25fa6b05403", 338 | "sha256:9bec63b1e20ef69484f530fb4b4837e050450637ff9acd6dccc7003c5013abf8", 339 | "sha256:9d8edfb09ed2b865485530c13e269833dab62ab2d582fde21026c9039d4d0e62", 340 | "sha256:9dac1924611698f8fe5b2e58601156c01da2b6c0758ba519003013a78280cf4d", 341 | "sha256:9e1a72197529ea00357640f21d92ffc7024e156ef9ac36edf271c8335facbc1a", 342 | "sha256:9e7094cf04e6042c4210a185fa7b9b8b3b789dd6d1de7b4f19452290838e48bd", 343 | "sha256:a4efb70a62cbbbc052c67dc66b5448b0053b509732184af3e7859d05fdf6223c", 344 | "sha256:a5dbdbb39c1b100df4d182c78949158073ca46ba2850c64fe02ffb1eb5b70903", 345 | "sha256:aeea6ace30603ca9a8869853bb4a04c7446856d7789e36694cd887967b7621f6", 346 | "sha256:b2489e70bfa2356f2d421106794507daccf6cc8711753c442fc97272437fc606", 347 | "sha256:babd63fb7cb6b0440abb6d16aca2be63342a6eea3dc7b613bb7a9357dc36920f", 348 | "sha256:c6fb6b9ed1d0be7fa2c90be8ad2442c14cbf84eb0709dd1afeeff1e511550041", 349 | "sha256:cfd8e4c64c30a5219032e64404d468c425bdbc13b397da906fc9bee6591fc0dd", 350 | "sha256:d17316100fcd0b6371ac9211351cb976fd0c2e12a859c1a57965e3ef7f3ed2bc", 351 | "sha256:d38a49aa75a5759d0d118e26701d70c70a37b896379115f8386e91b0444bfa70", 352 | "sha256:da25e75ba9f3fabc271673b6b413ca234994e6d3453424bea36bb5549c5bbaec", 353 | "sha256:e255a8dd5572b0c66d6ee53597d36157ad6cf3bc1114f61c54a65189f996ab03", 354 | "sha256:e8b09e2d90267717d850f2e2323919ea32004f55c40e5d53b41267e382446044", 355 | "sha256:ecc81336b46e31ae9c9bdfa220082079914e31a476d088d3337ecf531d861228", 356 | "sha256:effadcda9a129cc56408dd5b2ea20ee9edcea24bd58e6a1489fa27672d733182" 357 | ], 358 | "index": "pypi", 359 | "version": "==1.4.35" 360 | }, 361 | "starlette": { 362 | "hashes": [ 363 | "sha256:26a18cbda5e6b651c964c12c88b36d9898481cd428ed6e063f5f29c418f73050", 364 | "sha256:57eab3cc975a28af62f6faec94d355a410634940f10b30d68d31cb5ec1b44ae8" 365 | ], 366 | "markers": "python_version >= '3.6'", 367 | "version": "==0.17.1" 368 | }, 369 | "strawberry-graphql": { 370 | "extras": [ 371 | "fastapi" 372 | ], 373 | "hashes": [ 374 | "sha256:93cf617fb830f97ef5fad67655de5e6f9079c37271c696efbdb1fc299a3ff0f0", 375 | "sha256:cedb10b4981b183b318187c78d9b666fd44464fb63fafcbfe55c624372bb33dd" 376 | ], 377 | "index": "pypi", 378 | "version": "==0.114.0" 379 | }, 380 | "typing-extensions": { 381 | "hashes": [ 382 | "sha256:6657594ee297170d19f67d55c05852a874e7eb634f4f753dbd667855e07c1708", 383 | "sha256:f1c24655a0da0d1b67f07e17a5e6b2a105894e6824b92096378bb3668ef02376" 384 | ], 385 | "markers": "python_version >= '3.7'", 386 | "version": "==4.2.0" 387 | }, 388 | "uvicorn": { 389 | "extras": [ 390 | "standard" 391 | ], 392 | "hashes": [ 393 | "sha256:19e2a0e96c9ac5581c01eb1a79a7d2f72bb479691acd2b8921fce48ed5b961a6", 394 | "sha256:5180f9d059611747d841a4a4c4ab675edf54c8489e97f96d0583ee90ac3bfc23" 395 | ], 396 | "index": "pypi", 397 | "version": "==0.17.6" 398 | }, 399 | "uvloop": { 400 | "hashes": [ 401 | "sha256:04ff57aa137230d8cc968f03481176041ae789308b4d5079118331ab01112450", 402 | "sha256:089b4834fd299d82d83a25e3335372f12117a7d38525217c2258e9b9f4578897", 403 | "sha256:1e5f2e2ff51aefe6c19ee98af12b4ae61f5be456cd24396953244a30880ad861", 404 | "sha256:30ba9dcbd0965f5c812b7c2112a1ddf60cf904c1c160f398e7eed3a6b82dcd9c", 405 | "sha256:3a19828c4f15687675ea912cc28bbcb48e9bb907c801873bd1519b96b04fb805", 406 | "sha256:6224f1401025b748ffecb7a6e2652b17768f30b1a6a3f7b44660e5b5b690b12d", 407 | "sha256:647e481940379eebd314c00440314c81ea547aa636056f554d491e40503c8464", 408 | "sha256:6ccd57ae8db17d677e9e06192e9c9ec4bd2066b77790f9aa7dede2cc4008ee8f", 409 | "sha256:772206116b9b57cd625c8a88f2413df2fcfd0b496eb188b82a43bed7af2c2ec9", 410 | "sha256:8e0d26fa5875d43ddbb0d9d79a447d2ace4180d9e3239788208527c4784f7cab", 411 | "sha256:98d117332cc9e5ea8dfdc2b28b0a23f60370d02e1395f88f40d1effd2cb86c4f", 412 | "sha256:b572256409f194521a9895aef274cea88731d14732343da3ecdb175228881638", 413 | "sha256:bd53f7f5db562f37cd64a3af5012df8cac2c464c97e732ed556800129505bd64", 414 | "sha256:bd8f42ea1ea8f4e84d265769089964ddda95eb2bb38b5cbe26712b0616c3edee", 415 | "sha256:e814ac2c6f9daf4c36eb8e85266859f42174a4ff0d71b99405ed559257750382", 416 | "sha256:f74bc20c7b67d1c27c72601c78cf95be99d5c2cdd4514502b4f3eb0933ff1228" 417 | ], 418 | "version": "==0.16.0" 419 | }, 420 | "watchgod": { 421 | "hashes": [ 422 | "sha256:2f3e8137d98f493ff58af54ea00f4d1433a6afe2ed08ab331a657df468c6bfce", 423 | "sha256:cb11ff66657befba94d828e3b622d5fb76f22fbda1376f355f3e6e51e97d9450" 424 | ], 425 | "version": "==0.8.2" 426 | }, 427 | "websockets": { 428 | "hashes": [ 429 | "sha256:07cdc0a5b2549bcfbadb585ad8471ebdc7bdf91e32e34ae3889001c1c106a6af", 430 | "sha256:210aad7fdd381c52e58777560860c7e6110b6174488ef1d4b681c08b68bf7f8c", 431 | "sha256:28dd20b938a57c3124028680dc1600c197294da5db4292c76a0b48efb3ed7f76", 432 | "sha256:2f94fa3ae454a63ea3a19f73b95deeebc9f02ba2d5617ca16f0bbdae375cda47", 433 | "sha256:31564a67c3e4005f27815634343df688b25705cccb22bc1db621c781ddc64c69", 434 | "sha256:347974105bbd4ea068106ec65e8e8ebd86f28c19e529d115d89bd8cc5cda3079", 435 | "sha256:379e03422178436af4f3abe0aa8f401aa77ae2487843738542a75faf44a31f0c", 436 | "sha256:3eda1cb7e9da1b22588cefff09f0951771d6ee9fa8dbe66f5ae04cc5f26b2b55", 437 | "sha256:51695d3b199cd03098ae5b42833006a0f43dc5418d3102972addc593a783bc02", 438 | "sha256:54c000abeaff6d8771a4e2cef40900919908ea7b6b6a30eae72752607c6db559", 439 | "sha256:5b936bf552e4f6357f5727579072ff1e1324717902127ffe60c92d29b67b7be3", 440 | "sha256:6075fd24df23133c1b078e08a9b04a3bc40b31a8def4ee0b9f2c8865acce913e", 441 | "sha256:661f641b44ed315556a2fa630239adfd77bd1b11cb0b9d96ed8ad90b0b1e4978", 442 | "sha256:6ea6b300a6bdd782e49922d690e11c3669828fe36fc2471408c58b93b5535a98", 443 | "sha256:6ed1d6f791eabfd9808afea1e068f5e59418e55721db8b7f3bfc39dc831c42ae", 444 | "sha256:7934e055fd5cd9dee60f11d16c8d79c4567315824bacb1246d0208a47eca9755", 445 | "sha256:7ab36e17af592eec5747c68ef2722a74c1a4a70f3772bc661079baf4ae30e40d", 446 | "sha256:7f6d96fdb0975044fdd7953b35d003b03f9e2bcf85f2d2cf86285ece53e9f991", 447 | "sha256:83e5ca0d5b743cde3d29fda74ccab37bdd0911f25bd4cdf09ff8b51b7b4f2fa1", 448 | "sha256:85506b3328a9e083cc0a0fb3ba27e33c8db78341b3eb12eb72e8afd166c36680", 449 | "sha256:8af75085b4bc0b5c40c4a3c0e113fa95e84c60f4ed6786cbb675aeb1ee128247", 450 | "sha256:8b1359aba0ff810d5830d5ab8e2c4a02bebf98a60aa0124fb29aa78cfdb8031f", 451 | "sha256:8fbd7d77f8aba46d43245e86dd91a8970eac4fb74c473f8e30e9c07581f852b2", 452 | "sha256:907e8247480f287aa9bbc9391bd6de23c906d48af54c8c421df84655eef66af7", 453 | "sha256:93d5ea0b5da8d66d868b32c614d2b52d14304444e39e13a59566d4acb8d6e2e4", 454 | "sha256:97bc9d41e69a7521a358f9b8e44871f6cdeb42af31815c17aed36372d4eec667", 455 | "sha256:994cdb1942a7a4c2e10098d9162948c9e7b235df755de91ca33f6e0481366fdb", 456 | "sha256:a141de3d5a92188234afa61653ed0bbd2dde46ad47b15c3042ffb89548e77094", 457 | "sha256:a1e15b230c3613e8ea82c9fc6941b2093e8eb939dd794c02754d33980ba81e36", 458 | "sha256:aad5e300ab32036eb3fdc350ad30877210e2f51bceaca83fb7fef4d2b6c72b79", 459 | "sha256:b529fdfa881b69fe563dbd98acce84f3e5a67df13de415e143ef053ff006d500", 460 | "sha256:b9c77f0d1436ea4b4dc089ed8335fa141e6a251a92f75f675056dac4ab47a71e", 461 | "sha256:bb621ec2dbbbe8df78a27dbd9dd7919f9b7d32a73fafcb4d9252fc4637343582", 462 | "sha256:c7250848ce69559756ad0086a37b82c986cd33c2d344ab87fea596c5ac6d9442", 463 | "sha256:c8d1d14aa0f600b5be363077b621b1b4d1eb3fbf90af83f9281cda668e6ff7fd", 464 | "sha256:d1655a6fc7aecd333b079d00fb3c8132d18988e47f19740c69303bf02e9883c6", 465 | "sha256:d6353ba89cfc657a3f5beabb3b69be226adbb5c6c7a66398e17809b0ce3c4731", 466 | "sha256:da4377904a3379f0c1b75a965fff23b28315bcd516d27f99a803720dfebd94d4", 467 | "sha256:e49ea4c1a9543d2bd8a747ff24411509c29e4bdcde05b5b0895e2120cb1a761d", 468 | "sha256:e4e08305bfd76ba8edab08dcc6496f40674f44eb9d5e23153efa0a35750337e8", 469 | "sha256:e6fa05a680e35d0fcc1470cb070b10e6fe247af54768f488ed93542e71339d6f", 470 | "sha256:e7e6f2d6fd48422071cc8a6f8542016f350b79cc782752de531577d35e9bd677", 471 | "sha256:e904c0381c014b914136c492c8fa711ca4cced4e9b3d110e5e7d436d0fc289e8", 472 | "sha256:ec2b0ab7edc8cd4b0eb428b38ed89079bdc20c6bdb5f889d353011038caac2f9", 473 | "sha256:ef5ce841e102278c1c2e98f043db99d6755b1c58bde475516aef3a008ed7f28e", 474 | "sha256:f351c7d7d92f67c0609329ab2735eee0426a03022771b00102816a72715bb00b", 475 | "sha256:fab7c640815812ed5f10fbee7abbf58788d602046b7bb3af9b1ac753a6d5e916", 476 | "sha256:fc06cc8073c8e87072138ba1e431300e2d408f054b27047d047b549455066ff4" 477 | ], 478 | "version": "==10.3" 479 | } 480 | }, 481 | "develop": { 482 | "anyio": { 483 | "hashes": [ 484 | "sha256:413adf95f93886e442aea925f3ee43baa5a765a64a0f52c6081894f9992fdd0b", 485 | "sha256:cb29b9c70620506a9a8f87a309591713446953302d7d995344d0d7c6c0c9a7be" 486 | ], 487 | "markers": "python_full_version >= '3.6.2'", 488 | "version": "==3.6.1" 489 | }, 490 | "asgiref": { 491 | "hashes": [ 492 | "sha256:1d2880b792ae8757289136f1db2b7b99100ce959b2aa57fd69dab783d05afac4", 493 | "sha256:4a29362a6acebe09bf1d6640db38c1dc3d9217c68e6f9f6204d72667fc19a424" 494 | ], 495 | "markers": "python_version >= '3.7'", 496 | "version": "==3.5.2" 497 | }, 498 | "attrs": { 499 | "hashes": [ 500 | "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4", 501 | "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd" 502 | ], 503 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", 504 | "version": "==21.4.0" 505 | }, 506 | "backports.cached-property": { 507 | "hashes": [ 508 | "sha256:1a5ef1e750f8bc7d0204c807aae8e0f450c655be0cf4b30407a35fd4bb27186c", 509 | "sha256:687b5fe14be40aadcf547cae91337a1fdb84026046a39370274e54d3fe4fb4f9" 510 | ], 511 | "markers": "python_version >= '3.6'", 512 | "version": "==1.0.1" 513 | }, 514 | "black": { 515 | "hashes": [ 516 | "sha256:06f9d8846f2340dfac80ceb20200ea5d1b3f181dd0556b47af4e8e0b24fa0a6b", 517 | "sha256:10dbe6e6d2988049b4655b2b739f98785a884d4d6b85bc35133a8fb9a2233176", 518 | "sha256:2497f9c2386572e28921fa8bec7be3e51de6801f7459dffd6e62492531c47e09", 519 | "sha256:30d78ba6bf080eeaf0b7b875d924b15cd46fec5fd044ddfbad38c8ea9171043a", 520 | "sha256:328efc0cc70ccb23429d6be184a15ce613f676bdfc85e5fe8ea2a9354b4e9015", 521 | "sha256:35020b8886c022ced9282b51b5a875b6d1ab0c387b31a065b84db7c33085ca79", 522 | "sha256:5795a0375eb87bfe902e80e0c8cfaedf8af4d49694d69161e5bd3206c18618bb", 523 | "sha256:5891ef8abc06576985de8fa88e95ab70641de6c1fca97e2a15820a9b69e51b20", 524 | "sha256:637a4014c63fbf42a692d22b55d8ad6968a946b4a6ebc385c5505d9625b6a464", 525 | "sha256:67c8301ec94e3bcc8906740fe071391bce40a862b7be0b86fb5382beefecd968", 526 | "sha256:6d2fc92002d44746d3e7db7cf9313cf4452f43e9ea77a2c939defce3b10b5c82", 527 | "sha256:6ee227b696ca60dd1c507be80a6bc849a5a6ab57ac7352aad1ffec9e8b805f21", 528 | "sha256:863714200ada56cbc366dc9ae5291ceb936573155f8bf8e9de92aef51f3ad0f0", 529 | "sha256:9b542ced1ec0ceeff5b37d69838106a6348e60db7b8fdd245294dc1d26136265", 530 | "sha256:a6342964b43a99dbc72f72812bf88cad8f0217ae9acb47c0d4f141a6416d2d7b", 531 | "sha256:ad4efa5fad66b903b4a5f96d91461d90b9507a812b3c5de657d544215bb7877a", 532 | "sha256:bc58025940a896d7e5356952228b68f793cf5fcb342be703c3a2669a1488cb72", 533 | "sha256:cc1e1de68c8e5444e8f94c3670bb48a2beef0e91dddfd4fcc29595ebd90bb9ce", 534 | "sha256:cee3e11161dde1b2a33a904b850b0899e0424cc331b7295f2a9698e79f9a69a0", 535 | "sha256:e3556168e2e5c49629f7b0f377070240bd5511e45e25a4497bb0073d9dda776a", 536 | "sha256:e8477ec6bbfe0312c128e74644ac8a02ca06bcdb8982d4ee06f209be28cdf163", 537 | "sha256:ee8f1f7228cce7dffc2b464f07ce769f478968bfb3dd1254a4c2eeed84928aad", 538 | "sha256:fd57160949179ec517d32ac2ac898b5f20d68ed1a9c977346efbac9c2f1e779d" 539 | ], 540 | "index": "pypi", 541 | "version": "==22.3.0" 542 | }, 543 | "cfgv": { 544 | "hashes": [ 545 | "sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426", 546 | "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736" 547 | ], 548 | "markers": "python_full_version >= '3.6.1'", 549 | "version": "==3.3.1" 550 | }, 551 | "click": { 552 | "hashes": [ 553 | "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e", 554 | "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48" 555 | ], 556 | "markers": "python_version >= '3.7'", 557 | "version": "==8.1.3" 558 | }, 559 | "coverage": { 560 | "extras": [ 561 | "toml" 562 | ], 563 | "hashes": [ 564 | "sha256:00c8544510f3c98476bbd58201ac2b150ffbcce46a8c3e4fb89ebf01998f806a", 565 | "sha256:016d7f5cf1c8c84f533a3c1f8f36126fbe00b2ec0ccca47cc5731c3723d327c6", 566 | "sha256:03014a74023abaf5a591eeeaf1ac66a73d54eba178ff4cb1fa0c0a44aae70383", 567 | "sha256:033ebec282793bd9eb988d0271c211e58442c31077976c19c442e24d827d356f", 568 | "sha256:21e6686a95025927775ac501e74f5940cdf6fe052292f3a3f7349b0abae6d00f", 569 | "sha256:26f8f92699756cb7af2b30720de0c5bb8d028e923a95b6d0c891088025a1ac8f", 570 | "sha256:2e76bd16f0e31bc2b07e0fb1379551fcd40daf8cdf7e24f31a29e442878a827c", 571 | "sha256:341e9c2008c481c5c72d0e0dbf64980a4b2238631a7f9780b0fe2e95755fb018", 572 | "sha256:3cfd07c5889ddb96a401449109a8b97a165be9d67077df6802f59708bfb07720", 573 | "sha256:4002f9e8c1f286e986fe96ec58742b93484195defc01d5cc7809b8f7acb5ece3", 574 | "sha256:50ed480b798febce113709846b11f5d5ed1e529c88d8ae92f707806c50297abf", 575 | "sha256:543e172ce4c0de533fa892034cce260467b213c0ea8e39da2f65f9a477425211", 576 | "sha256:5a78cf2c43b13aa6b56003707c5203f28585944c277c1f3f109c7b041b16bd39", 577 | "sha256:5cd698341626f3c77784858427bad0cdd54a713115b423d22ac83a28303d1d95", 578 | "sha256:60c2147921da7f4d2d04f570e1838db32b95c5509d248f3fe6417e91437eaf41", 579 | "sha256:62d382f7d77eeeaff14b30516b17bcbe80f645f5cf02bb755baac376591c653c", 580 | "sha256:69432946f154c6add0e9ede03cc43b96e2ef2733110a77444823c053b1ff5166", 581 | "sha256:727dafd7f67a6e1cad808dc884bd9c5a2f6ef1f8f6d2f22b37b96cb0080d4f49", 582 | "sha256:742fb8b43835078dd7496c3c25a1ec8d15351df49fb0037bffb4754291ef30ce", 583 | "sha256:750e13834b597eeb8ae6e72aa58d1d831b96beec5ad1d04479ae3772373a8088", 584 | "sha256:7b546cf2b1974ddc2cb222a109b37c6ed1778b9be7e6b0c0bc0cf0438d9e45a6", 585 | "sha256:83bd142cdec5e4a5c4ca1d4ff6fa807d28460f9db919f9f6a31babaaa8b88426", 586 | "sha256:8d2e80dd3438e93b19e1223a9850fa65425e77f2607a364b6fd134fcd52dc9df", 587 | "sha256:9229d074e097f21dfe0643d9d0140ee7433814b3f0fc3706b4abffd1e3038632", 588 | "sha256:968ed5407f9460bd5a591cefd1388cc00a8f5099de9e76234655ae48cfdbe2c3", 589 | "sha256:9c82f2cd69c71698152e943f4a5a6b83a3ab1db73b88f6e769fabc86074c3b08", 590 | "sha256:a00441f5ea4504f5abbc047589d09e0dc33eb447dc45a1a527c8b74bfdd32c65", 591 | "sha256:a022394996419142b33a0cf7274cb444c01d2bb123727c4bb0b9acabcb515dea", 592 | "sha256:af5b9ee0fc146e907aa0f5fb858c3b3da9199d78b7bb2c9973d95550bd40f701", 593 | "sha256:b5578efe4038be02d76c344007b13119b2b20acd009a88dde8adec2de4f630b5", 594 | "sha256:b84ab65444dcc68d761e95d4d70f3cfd347ceca5a029f2ffec37d4f124f61311", 595 | "sha256:c53ad261dfc8695062fc8811ac7c162bd6096a05a19f26097f411bdf5747aee7", 596 | "sha256:cc173f1ce9ffb16b299f51c9ce53f66a62f4d975abe5640e976904066f3c835d", 597 | "sha256:d548edacbf16a8276af13063a2b0669d58bbcfca7c55a255f84aac2870786a61", 598 | "sha256:d55fae115ef9f67934e9f1103c9ba826b4c690e4c5bcf94482b8b2398311bf9c", 599 | "sha256:d8099ea680201c2221f8468c372198ceba9338a5fec0e940111962b03b3f716a", 600 | "sha256:e35217031e4b534b09f9b9a5841b9344a30a6357627761d4218818b865d45055", 601 | "sha256:e4f52c272fdc82e7c65ff3f17a7179bc5f710ebc8ce8a5cadac81215e8326740", 602 | "sha256:e637ae0b7b481905358624ef2e81d7fb0b1af55f5ff99f9ba05442a444b11e45", 603 | "sha256:eef5292b60b6de753d6e7f2d128d5841c7915fb1e3321c3a1fe6acfe76c38052", 604 | "sha256:fb45fe08e1abc64eb836d187b20a59172053999823f7f6ef4f18a819c44ba16f" 605 | ], 606 | "markers": "python_version >= '3.7'", 607 | "version": "==6.4" 608 | }, 609 | "distlib": { 610 | "hashes": [ 611 | "sha256:6564fe0a8f51e734df6333d08b8b94d4ea8ee6b99b5ed50613f731fd4089f34b", 612 | "sha256:e4b58818180336dc9c529bfb9a0b58728ffc09ad92027a3f30b7cd91e3458579" 613 | ], 614 | "version": "==0.3.4" 615 | }, 616 | "filelock": { 617 | "hashes": [ 618 | "sha256:37def7b658813cda163b56fc564cdc75e86d338246458c4c28ae84cabefa2404", 619 | "sha256:3a0fd85166ad9dbab54c9aec96737b744106dc5f15c0b09a6744a445299fcf04" 620 | ], 621 | "markers": "python_version >= '3.7'", 622 | "version": "==3.7.1" 623 | }, 624 | "graphql-core": { 625 | "hashes": [ 626 | "sha256:9d1bf141427b7d54be944587c8349df791ce60ade2e3cccaf9c56368c133c201", 627 | "sha256:f83c658e4968998eed1923a2e3e3eddd347e005ac0315fbb7ca4d70ea9156323" 628 | ], 629 | "markers": "python_version >= '3.6' and python_version < '4'", 630 | "version": "==3.2.1" 631 | }, 632 | "h11": { 633 | "hashes": [ 634 | "sha256:70813c1135087a248a4d38cc0e1a0181ffab2188141a93eaf567940c3957ff06", 635 | "sha256:8ddd78563b633ca55346c8cd41ec0af27d3c79931828beffb46ce70a379e7442" 636 | ], 637 | "markers": "python_version >= '3.6'", 638 | "version": "==0.13.0" 639 | }, 640 | "identify": { 641 | "hashes": [ 642 | "sha256:0dca2ea3e4381c435ef9c33ba100a78a9b40c0bab11189c7cf121f75815efeaa", 643 | "sha256:3d11b16f3fe19f52039fb7e39c9c884b21cb1b586988114fbe42671f03de3e82" 644 | ], 645 | "markers": "python_version >= '3.7'", 646 | "version": "==2.5.1" 647 | }, 648 | "idna": { 649 | "hashes": [ 650 | "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff", 651 | "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d" 652 | ], 653 | "markers": "python_version >= '3.5'", 654 | "version": "==3.3" 655 | }, 656 | "iniconfig": { 657 | "hashes": [ 658 | "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3", 659 | "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32" 660 | ], 661 | "version": "==1.1.1" 662 | }, 663 | "mypy-extensions": { 664 | "hashes": [ 665 | "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d", 666 | "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8" 667 | ], 668 | "version": "==0.4.3" 669 | }, 670 | "nodeenv": { 671 | "hashes": [ 672 | "sha256:3ef13ff90291ba2a4a7a4ff9a979b63ffdd00a464dbe04acf0ea6471517a4c2b", 673 | "sha256:621e6b7076565ddcacd2db0294c0381e01fd28945ab36bcf00f41c5daf63bef7" 674 | ], 675 | "version": "==1.6.0" 676 | }, 677 | "packaging": { 678 | "hashes": [ 679 | "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb", 680 | "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522" 681 | ], 682 | "markers": "python_version >= '3.6'", 683 | "version": "==21.3" 684 | }, 685 | "pathspec": { 686 | "hashes": [ 687 | "sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a", 688 | "sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1" 689 | ], 690 | "version": "==0.9.0" 691 | }, 692 | "platformdirs": { 693 | "hashes": [ 694 | "sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788", 695 | "sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7feef19" 696 | ], 697 | "markers": "python_version >= '3.7'", 698 | "version": "==2.5.2" 699 | }, 700 | "pluggy": { 701 | "hashes": [ 702 | "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159", 703 | "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3" 704 | ], 705 | "markers": "python_version >= '3.6'", 706 | "version": "==1.0.0" 707 | }, 708 | "pre-commit": { 709 | "hashes": [ 710 | "sha256:02226e69564ebca1a070bd1f046af866aa1c318dbc430027c50ab832ed2b73f2", 711 | "sha256:5d445ee1fa8738d506881c5d84f83c62bb5be6b2838e32207433647e8e5ebe10" 712 | ], 713 | "index": "pypi", 714 | "version": "==2.18.1" 715 | }, 716 | "py": { 717 | "hashes": [ 718 | "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719", 719 | "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378" 720 | ], 721 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", 722 | "version": "==1.11.0" 723 | }, 724 | "pygments": { 725 | "hashes": [ 726 | "sha256:5eb116118f9612ff1ee89ac96437bb6b49e8f04d8a13b514ba26f620208e26eb", 727 | "sha256:dc9c10fb40944260f6ed4c688ece0cd2048414940f1cea51b8b226318411c519" 728 | ], 729 | "markers": "python_version >= '3.6'", 730 | "version": "==2.12.0" 731 | }, 732 | "pyparsing": { 733 | "hashes": [ 734 | "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb", 735 | "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc" 736 | ], 737 | "markers": "python_full_version >= '3.6.8'", 738 | "version": "==3.0.9" 739 | }, 740 | "pytest": { 741 | "hashes": [ 742 | "sha256:13d0e3ccfc2b6e26be000cb6568c832ba67ba32e719443bfe725814d3c42433c", 743 | "sha256:a06a0425453864a270bc45e71f783330a7428defb4230fb5e6a731fde06ecd45" 744 | ], 745 | "index": "pypi", 746 | "version": "==7.1.2" 747 | }, 748 | "pytest-cov": { 749 | "hashes": [ 750 | "sha256:578d5d15ac4a25e5f961c938b85a05b09fdaae9deef3bb6de9a6e766622ca7a6", 751 | "sha256:e7f0f5b1617d2210a2cabc266dfe2f4c75a8d32fb89eafb7ad9d06f6d076d470" 752 | ], 753 | "index": "pypi", 754 | "version": "==3.0.0" 755 | }, 756 | "python-dateutil": { 757 | "hashes": [ 758 | "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86", 759 | "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9" 760 | ], 761 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 762 | "version": "==2.8.2" 763 | }, 764 | "python-multipart": { 765 | "hashes": [ 766 | "sha256:f7bb5f611fc600d15fa47b3974c8aa16e93724513b49b5f95c81e6624c83fa43" 767 | ], 768 | "version": "==0.0.5" 769 | }, 770 | "pyyaml": { 771 | "hashes": [ 772 | "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293", 773 | "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b", 774 | "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57", 775 | "sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b", 776 | "sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4", 777 | "sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07", 778 | "sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba", 779 | "sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9", 780 | "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287", 781 | "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513", 782 | "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0", 783 | "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0", 784 | "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92", 785 | "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f", 786 | "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2", 787 | "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc", 788 | "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c", 789 | "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86", 790 | "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4", 791 | "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c", 792 | "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34", 793 | "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b", 794 | "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c", 795 | "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb", 796 | "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737", 797 | "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3", 798 | "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d", 799 | "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53", 800 | "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78", 801 | "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803", 802 | "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a", 803 | "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174", 804 | "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5" 805 | ], 806 | "version": "==6.0" 807 | }, 808 | "six": { 809 | "hashes": [ 810 | "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", 811 | "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" 812 | ], 813 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 814 | "version": "==1.16.0" 815 | }, 816 | "sniffio": { 817 | "hashes": [ 818 | "sha256:471b71698eac1c2112a40ce2752bb2f4a4814c22a54a3eed3676bc0f5ca9f663", 819 | "sha256:c4666eecec1d3f50960c6bdf61ab7bc350648da6c126e3cf6898d8cd4ddcd3de" 820 | ], 821 | "markers": "python_version >= '3.5'", 822 | "version": "==1.2.0" 823 | }, 824 | "starlette": { 825 | "hashes": [ 826 | "sha256:26a18cbda5e6b651c964c12c88b36d9898481cd428ed6e063f5f29c418f73050", 827 | "sha256:57eab3cc975a28af62f6faec94d355a410634940f10b30d68d31cb5ec1b44ae8" 828 | ], 829 | "markers": "python_version >= '3.6'", 830 | "version": "==0.17.1" 831 | }, 832 | "strawberry-graphql": { 833 | "extras": [ 834 | "fastapi" 835 | ], 836 | "hashes": [ 837 | "sha256:93cf617fb830f97ef5fad67655de5e6f9079c37271c696efbdb1fc299a3ff0f0", 838 | "sha256:cedb10b4981b183b318187c78d9b666fd44464fb63fafcbfe55c624372bb33dd" 839 | ], 840 | "index": "pypi", 841 | "version": "==0.114.0" 842 | }, 843 | "toml": { 844 | "hashes": [ 845 | "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", 846 | "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f" 847 | ], 848 | "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", 849 | "version": "==0.10.2" 850 | }, 851 | "tomli": { 852 | "hashes": [ 853 | "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc", 854 | "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f" 855 | ], 856 | "markers": "python_version < '3.11'", 857 | "version": "==2.0.1" 858 | }, 859 | "typing-extensions": { 860 | "hashes": [ 861 | "sha256:6657594ee297170d19f67d55c05852a874e7eb634f4f753dbd667855e07c1708", 862 | "sha256:f1c24655a0da0d1b67f07e17a5e6b2a105894e6824b92096378bb3668ef02376" 863 | ], 864 | "markers": "python_version >= '3.7'", 865 | "version": "==4.2.0" 866 | }, 867 | "uvicorn": { 868 | "extras": [ 869 | "standard" 870 | ], 871 | "hashes": [ 872 | "sha256:19e2a0e96c9ac5581c01eb1a79a7d2f72bb479691acd2b8921fce48ed5b961a6", 873 | "sha256:5180f9d059611747d841a4a4c4ab675edf54c8489e97f96d0583ee90ac3bfc23" 874 | ], 875 | "index": "pypi", 876 | "version": "==0.17.6" 877 | }, 878 | "virtualenv": { 879 | "hashes": [ 880 | "sha256:e617f16e25b42eb4f6e74096b9c9e37713cf10bf30168fb4a739f3fa8f898a3a", 881 | "sha256:ef589a79795589aada0c1c5b319486797c03b67ac3984c48c669c0e4f50df3a5" 882 | ], 883 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", 884 | "version": "==20.14.1" 885 | } 886 | } 887 | } 888 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # fastapi-clean-example 2 | 3 | [![Python](https://img.shields.io/badge/python-3670A0?style=for-the-badge&logo=python&logoColor=ffdd54)](https://docs.python.org/3/) 4 | [![FastAPI](https://img.shields.io/badge/FastAPI-005571?style=for-the-badge&logo=fastapi)](https://fastapi.tiangolo.com/) 5 | [![OpenAPI](https://img.shields.io/badge/openapi-6BA539?style=for-the-badge&logo=openapi-initiative&logoColor=fff)](https://www.openapis.org/) 6 | [![Swagger](https://img.shields.io/badge/-Swagger-%23Clojure?style=for-the-badge&logo=swagger&logoColor=white)](https://swagger.io/) 7 | [![GraphQL](https://img.shields.io/badge/-GraphQL-E10098?style=for-the-badge&logo=graphql&logoColor=white)](https://graphql.org/) 8 | [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg?style=for-the-badge)](https://black.readthedocs.io/en/stable/) 9 | [![Typed with: pydantic](https://img.shields.io/badge/typed%20with-pydantic-BA600F.svg?style=for-the-badge)](https://docs.pydantic.dev/) 10 | [![Open Issues](https://img.shields.io/github/issues-raw/0xTheProDev/fastapi-clean-example?style=for-the-badge)](https://github.com/0xTheProDev/fastapi-clean-example/issues) 11 | [![Closed Issues](https://img.shields.io/github/issues-closed-raw/0xTheProDev/fastapi-clean-example?style=for-the-badge)](https://github.com/0xTheProDev/fastapi-clean-example/issues?q=is%3Aissue+is%3Aclosed) 12 | [![Open Pulls](https://img.shields.io/github/issues-pr-raw/0xTheProDev/fastapi-clean-example?style=for-the-badge)](https://github.com/0xTheProDev/fastapi-clean-example/pulls) 13 | [![Closed Pulls](https://img.shields.io/github/issues-pr-closed-raw/0xTheProDev/fastapi-clean-example?style=for-the-badge)](https://github.com/0xTheProDev/fastapi-clean-example/pulls?q=is%3Apr+is%3Aclosed) 14 | [![Contributors](https://img.shields.io/github/contributors/0xTheProDev/fastapi-clean-example?style=for-the-badge)](https://github.com/0xTheProDev/fastapi-clean-example/graphs/contributors) 15 | [![Activity](https://img.shields.io/github/last-commit/0xTheProDev/fastapi-clean-example?style=for-the-badge&label=most%20recent%20activity)](https://github.com/0xTheProDev/fastapi-clean-example/pulse) 16 | 17 | ## Description 18 | 19 | _Example Application Interface using FastAPI framework in Python 3_ 20 | 21 | This example showcases Repository Pattern in Hexagonal Architecture _(also known as Clean Architecture)_. Here we have two Entities - Books and Authors, whose relationships have been exploited to create CRUD endpoint in REST under OpenAPI standard. 22 | 23 | ## Installation 24 | 25 | - Install all the project dependency using [Pipenv](https://pipenv.pypa.io): 26 | 27 | ```sh 28 | $ pipenv install --dev 29 | ``` 30 | 31 | - Run the application from command prompt: 32 | 33 | ```sh 34 | $ pipenv run uvicorn main:app --reload 35 | ``` 36 | 37 | - You can also open a shell inside virtual environment: 38 | 39 | ```sh 40 | $ pipenv shell 41 | ``` 42 | 43 | - Open `localhost:8000/docs` for API Documentation 44 | 45 | - Open `localhost:8000/graphql` for GraphQL Documentation 46 | 47 | _*Note:* In case you are not able to access `pipenv` from you `PATH` locations, replace all instances of `pipenv` with `python3 -m pipenv`._ 48 | 49 | ## Testing 50 | 51 | For Testing, `unittest` module is used for Test Suite and Assertion, whereas `pytest` is being used for Test Runner and Coverage Reporter. 52 | 53 | - Run the following command to initiate test: 54 | ```sh 55 | $ pipenv run pytest 56 | ``` 57 | - To include Coverage Reporting as well: 58 | ```sh 59 | $ pipenv run pytest --cov-report xml --cov . 60 | ``` 61 | 62 | ## License 63 | 64 | © MIT License 65 | -------------------------------------------------------------------------------- /__tests__/repositories/test_AuthorRepository.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy.orm import Session 2 | from unittest import TestCase 3 | from unittest.mock import create_autospec, patch 4 | 5 | from repositories.AuthorRepository import AuthorRepository 6 | 7 | 8 | class TestAuthorRepository(TestCase): 9 | session: Session 10 | authorRepository: AuthorRepository 11 | 12 | def setUp(self): 13 | super().setUp() 14 | self.session = create_autospec(Session) 15 | self.authorRepository = AuthorRepository( 16 | self.session 17 | ) 18 | 19 | @patch("models.AuthorModel.Author", autospec=True) 20 | def test_create(self, Author): 21 | author = Author(name="JK Rowling") 22 | self.authorRepository.create(author) 23 | 24 | # Should call add method on Session 25 | self.session.add.assert_called_once_with(author) 26 | 27 | @patch("models.AuthorModel.Author", autospec=True) 28 | def test_delete(self, Author): 29 | author = Author(id=1) 30 | self.authorRepository.delete(author) 31 | 32 | # Should call delete method on Session 33 | self.session.delete.assert_called_once_with(author) 34 | 35 | @patch("models.AuthorModel.Author", autospec=True) 36 | def test_get(self, Author): 37 | author = Author(id=1) 38 | self.authorRepository.get(author) 39 | 40 | # Should call get method on Session 41 | self.session.get.assert_called_once() 42 | 43 | @patch("models.AuthorModel.Author", autospec=True) 44 | def test_list(self, Author): 45 | self.authorRepository.list(None, 100, 0) 46 | 47 | # Should call query method on Session 48 | self.session.query.assert_called_once() 49 | 50 | self.authorRepository.list("Stephen Knight", 100, 0) 51 | 52 | # Should call filter_by method on QueryResponse 53 | self.session.query( 54 | Author 55 | ).filter_by.assert_called_once_with( 56 | name="Stephen Knight" 57 | ) 58 | 59 | @patch("models.AuthorModel.Author", autospec=True) 60 | def test_update(self, Author): 61 | author = Author(name="Ray Dalio") 62 | self.authorRepository.update(id=1, author=author) 63 | 64 | # Should call merge method on Session 65 | self.session.merge.assert_called_once_with(author) 66 | -------------------------------------------------------------------------------- /__tests__/repositories/test_BookRepository.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy.orm import Session 2 | from unittest import TestCase 3 | from unittest.mock import create_autospec, patch 4 | 5 | from repositories.BookRepository import BookRepository 6 | 7 | 8 | class TestBookRepository(TestCase): 9 | session: Session 10 | bookRepository: BookRepository 11 | 12 | def setUp(self): 13 | super().setUp() 14 | self.session = create_autospec(Session) 15 | self.bookRepository = BookRepository(self.session) 16 | 17 | @patch("models.BookModel.Book", autospec=True) 18 | def test_create(self, Book): 19 | book = Book( 20 | name="Harry Potter and The Order of Phoenix" 21 | ) 22 | self.bookRepository.create(book) 23 | 24 | # Should call add method on Session 25 | self.session.add.assert_called_once_with(book) 26 | 27 | @patch("models.BookModel.Book", autospec=True) 28 | def test_delete(self, Book): 29 | book = Book(id=1) 30 | self.bookRepository.delete(book) 31 | 32 | # Should call delete method on Session 33 | self.session.delete.assert_called_once_with(book) 34 | 35 | @patch("models.BookModel.Book", autospec=True) 36 | def test_get(self, Book): 37 | book = Book(id=1) 38 | self.bookRepository.get(book) 39 | 40 | # Should call get method on Session 41 | self.session.get.assert_called_once() 42 | 43 | @patch("models.BookModel.Book", autospec=True) 44 | def test_list(self, Book): 45 | self.bookRepository.list(None, 100, 0) 46 | 47 | # Should call query method on Session 48 | self.session.query.assert_called_once() 49 | 50 | self.bookRepository.list("Peaky Blinders", 100, 0) 51 | 52 | # Should call filter_by method on QueryResponse 53 | self.session.query( 54 | Book 55 | ).filter_by.assert_called_once_with( 56 | name="Peaky Blinders" 57 | ) 58 | 59 | @patch("models.BookModel.Book", autospec=True) 60 | def test_update(self, Book): 61 | book = Book(name="The Wealth of Nations") 62 | self.bookRepository.update(id=1, book=book) 63 | 64 | # Should call merge method on Session 65 | self.session.merge.assert_called_once_with(book) 66 | -------------------------------------------------------------------------------- /__tests__/services/test_AuthorService.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | from unittest.mock import create_autospec, patch 3 | 4 | from repositories.AuthorRepository import AuthorRepository 5 | from repositories.BookRepository import BookRepository 6 | from services.AuthorService import AuthorService 7 | 8 | 9 | class TestAuthorService(TestCase): 10 | authorRepository: AuthorRepository 11 | authorService: AuthorService 12 | 13 | def setUp(self): 14 | super().setUp() 15 | self.authorRepository = create_autospec( 16 | AuthorRepository 17 | ) 18 | self.authorRepository = create_autospec( 19 | BookRepository 20 | ) 21 | self.authorService = AuthorService( 22 | self.authorRepository 23 | ) 24 | 25 | @patch( 26 | "schemas.pydantic.AuthorSchema.AuthorSchema", 27 | autospec=True, 28 | ) 29 | def test_create(self, AuthorSchema): 30 | author = AuthorSchema() 31 | author.name = "JK Rowling" 32 | 33 | self.authorService.create(author) 34 | 35 | # Should call create method on Author Repository 36 | self.authorRepository.create.assert_called_once() 37 | 38 | def test_delete(self): 39 | self.authorService.delete(author_id=1) 40 | 41 | # Should call delete method on Author Repository 42 | self.authorRepository.delete.assert_called_once() 43 | 44 | def test_get(self): 45 | self.authorService.get(author_id=1) 46 | 47 | # Should call get method on Author Repository 48 | self.authorRepository.get.assert_called_once() 49 | 50 | def test_list(self): 51 | name = "Rowling" 52 | pageSize = 10 53 | startIndex = 2 54 | 55 | self.authorService.list(name, pageSize, startIndex) 56 | 57 | # Should call list method on Author Repository 58 | self.authorRepository.list.assert_called_once_with( 59 | name, pageSize, startIndex 60 | ) 61 | 62 | @patch( 63 | "schemas.pydantic.AuthorSchema.AuthorSchema", 64 | autospec=True, 65 | ) 66 | def test_update(self, AuthorSchema): 67 | author = AuthorSchema() 68 | author.name = "JRR Tokein" 69 | 70 | self.authorService.update( 71 | author_id=1, author_body=author 72 | ) 73 | 74 | # Should call update method on Book Repository 75 | self.authorRepository.update.assert_called_once() 76 | -------------------------------------------------------------------------------- /__tests__/services/test_BookService.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | from unittest.mock import create_autospec, patch 3 | 4 | from repositories.AuthorRepository import AuthorRepository 5 | from repositories.BookRepository import BookRepository 6 | from services.BookService import BookService 7 | 8 | 9 | class TestBookService(TestCase): 10 | authorRepository: AuthorRepository 11 | bookRepository: BookRepository 12 | bookService: BookService 13 | 14 | def setUp(self): 15 | super().setUp() 16 | self.authorRepository = create_autospec( 17 | AuthorRepository 18 | ) 19 | self.bookRepository = create_autospec( 20 | BookRepository 21 | ) 22 | self.bookService = BookService( 23 | self.authorRepository, self.bookRepository 24 | ) 25 | 26 | @patch( 27 | "schemas.pydantic.BookSchema.BookSchema", 28 | autospec=True, 29 | ) 30 | def test_create(self, BookSchema): 31 | book = BookSchema() 32 | book.name = "Harry Potter and The Order of Phoenix" 33 | 34 | self.bookService.create(book) 35 | 36 | # Should call create method on Book Repository 37 | self.bookRepository.create.assert_called_once() 38 | 39 | def test_delete(self): 40 | self.bookService.delete(book_id=1) 41 | 42 | # Should call delete method on Book Repository 43 | self.bookRepository.delete.assert_called_once() 44 | 45 | def test_get(self): 46 | self.bookService.get(book_id=1) 47 | 48 | # Should call get method on Book Repository 49 | self.bookRepository.get.assert_called_once() 50 | 51 | def test_list(self): 52 | name = "The Richest Man in Babylon" 53 | pageSize = 10 54 | startIndex = 2 55 | 56 | self.bookService.list(name, pageSize, startIndex) 57 | 58 | # Should call list method on Book Repository 59 | self.bookRepository.list.assert_called_once_with( 60 | name, pageSize, startIndex 61 | ) 62 | 63 | @patch( 64 | "schemas.pydantic.BookSchema.BookSchema", 65 | autospec=True, 66 | ) 67 | def test_update(self, BookSchema): 68 | book = BookSchema() 69 | book.name = "Harry Potter and The Order of Phoenix" 70 | 71 | self.bookService.update(book_id=1, book_body=book) 72 | 73 | # Should call update method on Book Repository 74 | self.bookRepository.update.assert_called_once() 75 | 76 | def test_get_authors(self): 77 | self.bookService.get_authors(book_id=1) 78 | 79 | # Should call get method on Book Repository 80 | self.bookRepository.get.assert_called_once() 81 | 82 | @patch( 83 | "schemas.pydantic.BookSchema.BookAuthorPostRequestSchema", 84 | autospec=True, 85 | ) 86 | def test_add_author(self, BookAuthorPostRequestSchema): 87 | author = BookAuthorPostRequestSchema() 88 | author.author_id = 3 89 | 90 | self.bookService.add_author( 91 | book_id=1, author_body=author 92 | ) 93 | 94 | # Should call get method on Author Repository 95 | self.authorRepository.get.assert_called_once() 96 | 97 | # Should call get method on Book Repository 98 | self.bookRepository.get.assert_called_once() 99 | 100 | # Should call update method on Book Repository 101 | self.bookRepository.update.assert_called_once() 102 | 103 | def test_remove_author(self): 104 | self.bookService.remove_author( 105 | book_id=1, author_id=2 106 | ) 107 | 108 | # Should call get method on Book Repository 109 | self.bookRepository.get.assert_called_once() 110 | 111 | # Should call update method on Book Repository 112 | self.bookRepository.update.assert_called_once() 113 | -------------------------------------------------------------------------------- /configs/Database.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import create_engine 2 | from sqlalchemy.orm import scoped_session, sessionmaker 3 | 4 | from configs.Environment import get_environment_variables 5 | 6 | # Runtime Environment Configuration 7 | env = get_environment_variables() 8 | 9 | # Generate Database URL 10 | DATABASE_URL = f"{env.DATABASE_DIALECT}://{env.DATABASE_USERNAME}:{env.DATABASE_PASSWORD}@{env.DATABASE_HOSTNAME}:{env.DATABASE_PORT}/{env.DATABASE_NAME}" 11 | 12 | # Create Database Engine 13 | Engine = create_engine( 14 | DATABASE_URL, echo=env.DEBUG_MODE, future=True 15 | ) 16 | 17 | SessionLocal = sessionmaker( 18 | autocommit=False, autoflush=False, bind=Engine 19 | ) 20 | 21 | 22 | def get_db_connection(): 23 | db = scoped_session(SessionLocal) 24 | try: 25 | yield db 26 | finally: 27 | db.close() 28 | -------------------------------------------------------------------------------- /configs/Environment.py: -------------------------------------------------------------------------------- 1 | from functools import lru_cache 2 | import os 3 | 4 | from pydantic import BaseSettings 5 | 6 | 7 | @lru_cache 8 | def get_env_filename(): 9 | runtime_env = os.getenv("ENV") 10 | return f".env.{runtime_env}" if runtime_env else ".env" 11 | 12 | 13 | class EnvironmentSettings(BaseSettings): 14 | API_VERSION: str 15 | APP_NAME: str 16 | DATABASE_DIALECT: str 17 | DATABASE_HOSTNAME: str 18 | DATABASE_NAME: str 19 | DATABASE_PASSWORD: str 20 | DATABASE_PORT: int 21 | DATABASE_USERNAME: str 22 | DEBUG_MODE: bool 23 | 24 | class Config: 25 | env_file = get_env_filename() 26 | env_file_encoding = "utf-8" 27 | 28 | 29 | @lru_cache 30 | def get_environment_variables(): 31 | return EnvironmentSettings() 32 | -------------------------------------------------------------------------------- /configs/GraphQL.py: -------------------------------------------------------------------------------- 1 | from fastapi import Depends 2 | from strawberry.types import Info 3 | 4 | from services.AuthorService import AuthorService 5 | from services.BookService import BookService 6 | 7 | 8 | # GraphQL Dependency Context 9 | async def get_graphql_context( 10 | authorService: AuthorService = Depends(), 11 | bookService: BookService = Depends(), 12 | ): 13 | return { 14 | "authorService": authorService, 15 | "bookService": bookService, 16 | } 17 | 18 | 19 | # Extract AuthorService instance from GraphQL context 20 | def get_AuthorService(info: Info) -> AuthorService: 21 | return info.context["authorService"] 22 | 23 | 24 | # Extract BookService instance from GraphQL context 25 | def get_BookService(info: Info) -> BookService: 26 | return info.context["bookService"] 27 | -------------------------------------------------------------------------------- /configs/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xTheProDev/fastapi-clean-example/181bb46cf7f7e7cdb58844ba65020b1a4aadbdc3/configs/__init__.py -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | from fastapi import Depends, FastAPI 2 | from strawberry import Schema 3 | from strawberry.fastapi import GraphQLRouter 4 | 5 | from configs.Environment import get_environment_variables 6 | from configs.GraphQL import get_graphql_context 7 | from metadata.Tags import Tags 8 | from models.BaseModel import init 9 | from routers.v1.AuthorRouter import AuthorRouter 10 | from routers.v1.BookRouter import BookRouter 11 | from schemas.graphql.Query import Query 12 | from schemas.graphql.Mutation import Mutation 13 | 14 | # Application Environment Configuration 15 | env = get_environment_variables() 16 | 17 | # Core Application Instance 18 | app = FastAPI( 19 | title=env.APP_NAME, 20 | version=env.API_VERSION, 21 | openapi_tags=Tags, 22 | ) 23 | 24 | # Add Routers 25 | app.include_router(AuthorRouter) 26 | app.include_router(BookRouter) 27 | 28 | # GraphQL Schema and Application Instance 29 | schema = Schema(query=Query, mutation=Mutation) 30 | graphql = GraphQLRouter( 31 | schema, 32 | graphiql=env.DEBUG_MODE, 33 | context_getter=get_graphql_context, 34 | ) 35 | 36 | # Integrate GraphQL Application to the Core one 37 | app.include_router( 38 | graphql, 39 | prefix="/graphql", 40 | include_in_schema=False, 41 | ) 42 | 43 | # Initialise Data Model Attributes 44 | init() 45 | -------------------------------------------------------------------------------- /metadata/Tags.py: -------------------------------------------------------------------------------- 1 | Tags = [ 2 | { 3 | "name": "author", 4 | "description": "Contains CRUD operation on Authors", 5 | }, 6 | { 7 | "name": "book", 8 | "description": "Contains CRUD operation on Books", 9 | }, 10 | ] 11 | -------------------------------------------------------------------------------- /metadata/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xTheProDev/fastapi-clean-example/181bb46cf7f7e7cdb58844ba65020b1a4aadbdc3/metadata/__init__.py -------------------------------------------------------------------------------- /models/AuthorModel.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import ( 2 | Column, 3 | Integer, 4 | PrimaryKeyConstraint, 5 | String, 6 | ) 7 | from sqlalchemy.orm import relationship 8 | 9 | from models.BaseModel import EntityMeta 10 | from models.BookAuthorAssociation import ( 11 | book_author_association, 12 | ) 13 | 14 | 15 | class Author(EntityMeta): 16 | __tablename__ = "authors" 17 | 18 | id = Column(Integer) 19 | name = Column(String(16), nullable=False) 20 | books = relationship( 21 | "Book", 22 | lazy="dynamic", 23 | secondary=book_author_association, 24 | ) 25 | 26 | PrimaryKeyConstraint(id) 27 | 28 | def normalize(self): 29 | return { 30 | "id": self.id.__str__(), 31 | "name": self.name.__str__(), 32 | } 33 | -------------------------------------------------------------------------------- /models/BaseModel.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy.ext.declarative import declarative_base 2 | 3 | from configs.Database import Engine 4 | 5 | # Base Entity Model Schema 6 | EntityMeta = declarative_base() 7 | 8 | 9 | def init(): 10 | EntityMeta.metadata.create_all(bind=Engine) 11 | -------------------------------------------------------------------------------- /models/BookAuthorAssociation.py: -------------------------------------------------------------------------------- 1 | # Many-to-Many Relationship between Books and Authors 2 | from sqlalchemy import Column, ForeignKey, Table 3 | 4 | from models.BaseModel import EntityMeta 5 | 6 | 7 | book_author_association = Table( 8 | "book_author_association", 9 | EntityMeta.metadata, 10 | Column("book_id", ForeignKey("books.id")), 11 | Column("author_id", ForeignKey("authors.id")), 12 | ) 13 | -------------------------------------------------------------------------------- /models/BookModel.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import ( 2 | Column, 3 | Integer, 4 | PrimaryKeyConstraint, 5 | String, 6 | ) 7 | from sqlalchemy.orm import relationship 8 | 9 | from models.BaseModel import EntityMeta 10 | from models.BookAuthorAssociation import ( 11 | book_author_association, 12 | ) 13 | 14 | 15 | class Book(EntityMeta): 16 | __tablename__ = "books" 17 | 18 | id = Column(Integer) 19 | name = Column(String(40), nullable=False) 20 | authors = relationship( 21 | "Author", 22 | lazy="dynamic", 23 | secondary=book_author_association, 24 | ) 25 | 26 | PrimaryKeyConstraint(id) 27 | 28 | def normalize(self): 29 | return { 30 | "id": self.id.__str__(), 31 | "name": self.name.__str__(), 32 | } 33 | -------------------------------------------------------------------------------- /models/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xTheProDev/fastapi-clean-example/181bb46cf7f7e7cdb58844ba65020b1a4aadbdc3/models/__init__.py -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.black] 2 | line-length = 60 3 | 4 | [tool.isort] 5 | profile = "black" 6 | src_paths = ["configs", "core", "dependencies", "models", "repositories", "routers", "schemas", "services"] 7 | virtual_env = "env" 8 | 9 | [tool.pytest.ini_options] 10 | pythonpath = [ 11 | "." 12 | ] 13 | testpaths = [ 14 | "__tests__" 15 | ] 16 | -------------------------------------------------------------------------------- /repositories/AuthorRepository.py: -------------------------------------------------------------------------------- 1 | from typing import List, Optional 2 | 3 | from fastapi import Depends 4 | from sqlalchemy.orm import Session, lazyload 5 | 6 | from configs.Database import ( 7 | get_db_connection, 8 | ) 9 | from models.AuthorModel import Author 10 | 11 | 12 | class AuthorRepository: 13 | db: Session 14 | 15 | def __init__( 16 | self, db: Session = Depends(get_db_connection) 17 | ) -> None: 18 | self.db = db 19 | 20 | def list( 21 | self, 22 | name: Optional[str], 23 | limit: Optional[int], 24 | start: Optional[int], 25 | ) -> List[Author]: 26 | query = self.db.query(Author) 27 | 28 | if name: 29 | query = query.filter_by(name=name) 30 | 31 | return query.offset(start).limit(limit).all() 32 | 33 | def get(self, author: Author) -> Author: 34 | return self.db.get( 35 | Author, 36 | author.id, 37 | options=[lazyload(Author.books)], 38 | ) 39 | 40 | def create(self, author: Author) -> Author: 41 | self.db.add(author) 42 | self.db.commit() 43 | self.db.refresh(author) 44 | return author 45 | 46 | def update(self, id: int, author: Author) -> Author: 47 | author.id = id 48 | self.db.merge(author) 49 | self.db.commit() 50 | return author 51 | 52 | def delete(self, author: Author) -> None: 53 | self.db.delete(author) 54 | self.db.commit() 55 | self.db.flush() 56 | -------------------------------------------------------------------------------- /repositories/BookRepository.py: -------------------------------------------------------------------------------- 1 | from typing import List, Optional 2 | 3 | from fastapi import Depends 4 | from sqlalchemy.orm import Session, lazyload 5 | 6 | from configs.Database import ( 7 | get_db_connection, 8 | ) 9 | from models.BookModel import Book 10 | 11 | 12 | class BookRepository: 13 | db: Session 14 | 15 | def __init__( 16 | self, db: Session = Depends(get_db_connection) 17 | ) -> None: 18 | self.db = db 19 | 20 | def list( 21 | self, 22 | name: Optional[str], 23 | limit: Optional[int], 24 | start: Optional[int], 25 | ) -> List[Book]: 26 | query = self.db.query(Book) 27 | 28 | if name: 29 | query = query.filter_by(name=name) 30 | 31 | return query.offset(start).limit(limit).all() 32 | 33 | def get(self, book: Book) -> Book: 34 | return self.db.get( 35 | Book, book.id, options=[lazyload(Book.authors)] 36 | ) 37 | 38 | def create(self, book: Book) -> Book: 39 | self.db.add(book) 40 | self.db.commit() 41 | self.db.refresh(book) 42 | return book 43 | 44 | def update(self, id: int, book: Book) -> Book: 45 | book.id = id 46 | self.db.merge(book) 47 | self.db.commit() 48 | return book 49 | 50 | def delete(self, book: Book) -> None: 51 | self.db.delete(book) 52 | self.db.commit() 53 | self.db.flush() 54 | -------------------------------------------------------------------------------- /repositories/RepositoryMeta.py: -------------------------------------------------------------------------------- 1 | from abc import abstractmethod 2 | from typing import Generic, List, TypeVar 3 | 4 | # Type definition for Model 5 | M = TypeVar("M") 6 | 7 | # Type definition for Unique Id 8 | K = TypeVar("K") 9 | 10 | ################################# 11 | # Abstract Class for Repository # 12 | ################################# 13 | class RepositoryMeta(Generic[M, K]): 14 | 15 | # Create a new instance of the Model 16 | @abstractmethod 17 | def create(self, instance: M) -> M: 18 | pass 19 | 20 | # Delete an existing instance of the Model 21 | @abstractmethod 22 | def delete(self, id: K) -> None: 23 | pass 24 | 25 | # Fetch an existing instance of the Model by it's unique Id 26 | @abstractmethod 27 | def get(self, id: K) -> M: 28 | pass 29 | 30 | # Lists all existing instance of the Model 31 | @abstractmethod 32 | def list(self, limit: int, start: int) -> List[M]: 33 | pass 34 | 35 | # Updates an existing instance of the Model 36 | @abstractmethod 37 | def update(self, id: K, instance: M) -> M: 38 | pass 39 | -------------------------------------------------------------------------------- /repositories/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xTheProDev/fastapi-clean-example/181bb46cf7f7e7cdb58844ba65020b1a4aadbdc3/repositories/__init__.py -------------------------------------------------------------------------------- /routers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xTheProDev/fastapi-clean-example/181bb46cf7f7e7cdb58844ba65020b1a4aadbdc3/routers/__init__.py -------------------------------------------------------------------------------- /routers/v1/AuthorRouter.py: -------------------------------------------------------------------------------- 1 | from typing import List, Optional 2 | 3 | from fastapi import APIRouter, Depends, status 4 | 5 | from schemas.pydantic.AuthorSchema import ( 6 | AuthorPostRequestSchema, 7 | AuthorSchema, 8 | ) 9 | from schemas.pydantic.BookSchema import BookSchema 10 | from services.AuthorService import AuthorService 11 | 12 | AuthorRouter = APIRouter( 13 | prefix="/v1/authors", tags=["author"] 14 | ) 15 | 16 | 17 | @AuthorRouter.get("/", response_model=List[AuthorSchema]) 18 | def index( 19 | name: Optional[str] = None, 20 | pageSize: Optional[int] = 100, 21 | startIndex: Optional[int] = 0, 22 | authorService: AuthorService = Depends(), 23 | ): 24 | return [ 25 | author.normalize() 26 | for author in authorService.list( 27 | name, pageSize, startIndex 28 | ) 29 | ] 30 | 31 | 32 | @AuthorRouter.get("/{id}", response_model=AuthorSchema) 33 | def get(id: int, authorService: AuthorService = Depends()): 34 | return authorService.get(id).normalize() 35 | 36 | 37 | @AuthorRouter.post( 38 | "/", 39 | response_model=AuthorSchema, 40 | status_code=status.HTTP_201_CREATED, 41 | ) 42 | def create( 43 | author: AuthorPostRequestSchema, 44 | authorService: AuthorService = Depends(), 45 | ): 46 | return authorService.create(author).normalize() 47 | 48 | 49 | @AuthorRouter.patch("/{id}", response_model=AuthorSchema) 50 | def update( 51 | id: int, 52 | author: AuthorPostRequestSchema, 53 | authorService: AuthorService = Depends(), 54 | ): 55 | return authorService.update(id, author).normalize() 56 | 57 | 58 | @AuthorRouter.delete( 59 | "/{id}", status_code=status.HTTP_204_NO_CONTENT 60 | ) 61 | def delete( 62 | id: int, authorService: AuthorService = Depends() 63 | ): 64 | return authorService.delete(id) 65 | 66 | 67 | @AuthorRouter.get( 68 | "/{id}/books/", response_model=List[BookSchema] 69 | ) 70 | def get_books( 71 | id: int, authorService: AuthorService = Depends() 72 | ): 73 | return [ 74 | book.normalize() 75 | for book in authorService.get_books(id) 76 | ] 77 | -------------------------------------------------------------------------------- /routers/v1/BookRouter.py: -------------------------------------------------------------------------------- 1 | from typing import List, Optional 2 | 3 | from fastapi import APIRouter, Depends, status 4 | 5 | from schemas.pydantic.AuthorSchema import AuthorSchema 6 | from schemas.pydantic.BookSchema import ( 7 | BookAuthorPostRequestSchema, 8 | BookPostRequestSchema, 9 | BookSchema, 10 | ) 11 | from services.BookService import BookService 12 | 13 | BookRouter = APIRouter(prefix="/v1/books", tags=["book"]) 14 | 15 | 16 | @BookRouter.get("/", response_model=List[BookSchema]) 17 | def index( 18 | name: Optional[str] = None, 19 | pageSize: Optional[int] = 100, 20 | startIndex: Optional[int] = 0, 21 | bookService: BookService = Depends(), 22 | ): 23 | return [ 24 | book.normalize() 25 | for book in bookService.list( 26 | name, pageSize, startIndex 27 | ) 28 | ] 29 | 30 | 31 | @BookRouter.get("/{id}", response_model=BookSchema) 32 | def get(id: int, bookService: BookService = Depends()): 33 | return bookService.get(id).normalize() 34 | 35 | 36 | @BookRouter.post( 37 | "/", 38 | response_model=BookSchema, 39 | status_code=status.HTTP_201_CREATED, 40 | ) 41 | def create( 42 | book: BookPostRequestSchema, 43 | bookService: BookService = Depends(), 44 | ): 45 | return bookService.create(book).normalize() 46 | 47 | 48 | @BookRouter.patch("/{id}", response_model=BookSchema) 49 | def update( 50 | id: int, 51 | book: BookPostRequestSchema, 52 | bookService: BookService = Depends(), 53 | ): 54 | return bookService.update(id, book).normalize() 55 | 56 | 57 | @BookRouter.delete( 58 | "/{id}", status_code=status.HTTP_204_NO_CONTENT 59 | ) 60 | def delete(id: int, bookService: BookService = Depends()): 61 | return bookService.delete(id) 62 | 63 | 64 | @BookRouter.get( 65 | "/{id}/authors/", response_model=List[AuthorSchema] 66 | ) 67 | def get_authors( 68 | id: int, bookService: BookService = Depends() 69 | ): 70 | return [ 71 | author.normalize() 72 | for author in bookService.get_authors(id) 73 | ] 74 | 75 | 76 | @BookRouter.post( 77 | "/{id}/authors/", response_model=List[AuthorSchema] 78 | ) 79 | def add_author( 80 | id: int, 81 | author: BookAuthorPostRequestSchema, 82 | bookService: BookService = Depends(), 83 | ): 84 | return [ 85 | author.normalize() 86 | for author in bookService.add_author(id, author) 87 | ] 88 | 89 | 90 | @BookRouter.delete( 91 | "/{id}/authors/{author_id}", 92 | response_model=List[AuthorSchema], 93 | ) 94 | def remove_author( 95 | id: int, 96 | author_id: int, 97 | bookService: BookService = Depends(), 98 | ): 99 | return [ 100 | author.normalize() 101 | for author in bookService.remove_author( 102 | id, author_id 103 | ) 104 | ] 105 | -------------------------------------------------------------------------------- /routers/v1/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xTheProDev/fastapi-clean-example/181bb46cf7f7e7cdb58844ba65020b1a4aadbdc3/routers/v1/__init__.py -------------------------------------------------------------------------------- /schemas/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xTheProDev/fastapi-clean-example/181bb46cf7f7e7cdb58844ba65020b1a4aadbdc3/schemas/__init__.py -------------------------------------------------------------------------------- /schemas/graphql/Author.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | import strawberry 3 | 4 | from schemas.graphql.Book import BookSchema 5 | 6 | 7 | @strawberry.type(description="Author Schema") 8 | class AuthorSchema: 9 | id: int 10 | name: str 11 | books: List[BookSchema] 12 | 13 | 14 | @strawberry.input(description="Author Mutation Schema") 15 | class AuthorMutationSchema: 16 | name: str 17 | -------------------------------------------------------------------------------- /schemas/graphql/Book.py: -------------------------------------------------------------------------------- 1 | import strawberry 2 | 3 | 4 | @strawberry.type(description="Book Schema") 5 | class BookSchema: 6 | id: int 7 | name: str 8 | 9 | 10 | @strawberry.input(description="Book Mutation Schema") 11 | class BookMutationSchema: 12 | name: str 13 | -------------------------------------------------------------------------------- /schemas/graphql/Mutation.py: -------------------------------------------------------------------------------- 1 | import strawberry 2 | from strawberry.types import Info 3 | from configs.GraphQL import ( 4 | get_AuthorService, 5 | get_BookService, 6 | ) 7 | 8 | from schemas.graphql.Author import ( 9 | AuthorMutationSchema, 10 | AuthorSchema, 11 | ) 12 | from schemas.graphql.Book import ( 13 | BookMutationSchema, 14 | BookSchema, 15 | ) 16 | 17 | 18 | @strawberry.type(description="Mutate all Entity") 19 | class Mutation: 20 | @strawberry.field(description="Adds a new Author") 21 | def add_author( 22 | self, author: AuthorMutationSchema, info: Info 23 | ) -> AuthorSchema: 24 | authorService = get_AuthorService(info) 25 | return authorService.create(author) 26 | 27 | @strawberry.field( 28 | description="Delets an existing Author" 29 | ) 30 | def delete_author( 31 | self, author_id: int, info: Info 32 | ) -> None: 33 | authorService = get_AuthorService(info) 34 | return authorService.delete(author_id) 35 | 36 | @strawberry.field( 37 | description="Updates an existing Author" 38 | ) 39 | def update_author( 40 | self, 41 | author_id: int, 42 | author: AuthorMutationSchema, 43 | info: Info, 44 | ) -> AuthorSchema: 45 | authorService = get_AuthorService(info) 46 | return authorService.update(author_id, author) 47 | 48 | @strawberry.field(description="Add a new Book") 49 | def add_book( 50 | self, book: BookMutationSchema, info: Info 51 | ) -> BookSchema: 52 | bookService = get_BookService(info) 53 | return bookService.create(book) 54 | 55 | @strawberry.field( 56 | description="Deletes an existing Book" 57 | ) 58 | def delete_book(self, book_id: int, info: Info) -> None: 59 | bookService = get_BookService(info) 60 | return bookService.delete(book_id) 61 | 62 | @strawberry.field( 63 | description="Deletes an existing Book" 64 | ) 65 | def update_book( 66 | self, 67 | book_id: int, 68 | book: BookMutationSchema, 69 | info: Info, 70 | ) -> BookSchema: 71 | bookService = get_BookService(info) 72 | return bookService.update(book_id, book) 73 | -------------------------------------------------------------------------------- /schemas/graphql/Query.py: -------------------------------------------------------------------------------- 1 | from typing import List, Optional 2 | 3 | import strawberry 4 | from strawberry.types import Info 5 | from configs.GraphQL import ( 6 | get_AuthorService, 7 | get_BookService, 8 | ) 9 | 10 | from schemas.graphql.Author import AuthorSchema 11 | from schemas.graphql.Book import BookSchema 12 | 13 | 14 | @strawberry.type(description="Query all entities") 15 | class Query: 16 | @strawberry.field(description="Get an Author") 17 | def author( 18 | self, id: int, info: Info 19 | ) -> Optional[AuthorSchema]: 20 | authorService = get_AuthorService(info) 21 | return authorService.get(id) 22 | 23 | @strawberry.field(description="List all Authors") 24 | def authors(self, info: Info) -> List[AuthorSchema]: 25 | authorService = get_AuthorService(info) 26 | return authorService.list() 27 | 28 | @strawberry.field(description="Get a Book") 29 | def book( 30 | self, id: int, info: Info 31 | ) -> Optional[BookSchema]: 32 | bookService = get_BookService(info) 33 | return bookService.get(id) 34 | 35 | @strawberry.field(description="List all Books") 36 | def books(self, info: Info) -> List[BookSchema]: 37 | bookService = get_BookService(info) 38 | return bookService.list() 39 | -------------------------------------------------------------------------------- /schemas/graphql/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xTheProDev/fastapi-clean-example/181bb46cf7f7e7cdb58844ba65020b1a4aadbdc3/schemas/graphql/__init__.py -------------------------------------------------------------------------------- /schemas/pydantic/AuthorSchema.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel 2 | 3 | 4 | class AuthorPostRequestSchema(BaseModel): 5 | name: str 6 | 7 | 8 | class AuthorSchema(AuthorPostRequestSchema): 9 | id: int 10 | -------------------------------------------------------------------------------- /schemas/pydantic/BookSchema.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel 2 | 3 | 4 | class BookPostRequestSchema(BaseModel): 5 | name: str 6 | 7 | 8 | class BookSchema(BookPostRequestSchema): 9 | id: int 10 | 11 | 12 | class BookAuthorPostRequestSchema(BaseModel): 13 | author_id: int 14 | -------------------------------------------------------------------------------- /schemas/pydantic/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xTheProDev/fastapi-clean-example/181bb46cf7f7e7cdb58844ba65020b1a4aadbdc3/schemas/pydantic/__init__.py -------------------------------------------------------------------------------- /services/AuthorService.py: -------------------------------------------------------------------------------- 1 | from typing import List, Optional 2 | 3 | from fastapi import Depends 4 | from models.AuthorModel import Author 5 | from models.BookModel import Book 6 | 7 | from repositories.AuthorRepository import AuthorRepository 8 | from schemas.pydantic.AuthorSchema import AuthorSchema 9 | 10 | 11 | class AuthorService: 12 | authorRepository: AuthorRepository 13 | 14 | def __init__( 15 | self, authorRepository: AuthorRepository = Depends() 16 | ) -> None: 17 | self.authorRepository = authorRepository 18 | 19 | def create(self, author_body: AuthorSchema) -> Author: 20 | return self.authorRepository.create( 21 | Author(name=author_body.name) 22 | ) 23 | 24 | def delete(self, author_id: int) -> None: 25 | return self.authorRepository.delete( 26 | Author(id=author_id) 27 | ) 28 | 29 | def get(self, author_id: int) -> Author: 30 | return self.authorRepository.get( 31 | Author(id=author_id) 32 | ) 33 | 34 | def list( 35 | self, 36 | name: Optional[str] = None, 37 | pageSize: Optional[int] = 100, 38 | startIndex: Optional[int] = 0, 39 | ) -> List[Author]: 40 | return self.authorRepository.list( 41 | name, pageSize, startIndex 42 | ) 43 | 44 | def update( 45 | self, author_id: int, author_body: AuthorSchema 46 | ) -> Author: 47 | return self.authorRepository.update( 48 | author_id, Author(name=author_body.name) 49 | ) 50 | 51 | def get_books(self, author_id: int) -> List[Book]: 52 | return self.authorRepository.get( 53 | Author(id=author_id) 54 | ).books 55 | -------------------------------------------------------------------------------- /services/BookService.py: -------------------------------------------------------------------------------- 1 | from typing import List, Optional 2 | 3 | from fastapi import Depends 4 | from models.AuthorModel import Author 5 | from models.BookModel import Book 6 | 7 | from repositories.AuthorRepository import AuthorRepository 8 | from repositories.BookRepository import BookRepository 9 | from schemas.pydantic.AuthorSchema import AuthorSchema 10 | from schemas.pydantic.BookSchema import ( 11 | BookAuthorPostRequestSchema, 12 | BookSchema, 13 | ) 14 | 15 | 16 | class BookService: 17 | authorRepository: AuthorRepository 18 | bookRepository: BookRepository 19 | 20 | def __init__( 21 | self, 22 | authorRepository: AuthorRepository = Depends(), 23 | bookRepository: BookRepository = Depends(), 24 | ) -> None: 25 | self.authorRepository = authorRepository 26 | self.bookRepository = bookRepository 27 | 28 | def create(self, book_body: BookSchema) -> Book: 29 | return self.bookRepository.create( 30 | Book(name=book_body.name) 31 | ) 32 | 33 | def delete(self, book_id: int) -> None: 34 | return self.bookRepository.delete(Book(id=book_id)) 35 | 36 | def get(self, book_id: int) -> Book: 37 | return self.bookRepository.get(Book(id=book_id)) 38 | 39 | def list( 40 | self, 41 | name: Optional[str] = None, 42 | pageSize: Optional[int] = 100, 43 | startIndex: Optional[int] = 0, 44 | ) -> List[Book]: 45 | return self.bookRepository.list( 46 | name, pageSize, startIndex 47 | ) 48 | 49 | def update( 50 | self, book_id: int, book_body: BookSchema 51 | ) -> Book: 52 | return self.bookRepository.update( 53 | book_id, Book(name=book_body.name) 54 | ) 55 | 56 | def get_authors(self, book_id: int) -> List[Author]: 57 | return self.bookRepository.get( 58 | Book(id=book_id) 59 | ).authors 60 | 61 | def add_author( 62 | self, 63 | book_id: int, 64 | author_body: BookAuthorPostRequestSchema, 65 | ) -> List[Author]: 66 | author = self.authorRepository.get( 67 | Author(id=author_body.author_id) 68 | ) 69 | book = self.bookRepository.get(Book(id=book_id)) 70 | book.authors.append(author) 71 | self.bookRepository.update(book_id, book) 72 | 73 | return book.authors 74 | 75 | def remove_author( 76 | self, book_id: int, author_id: int 77 | ) -> List[Author]: 78 | book = self.bookRepository.get(Book(id=book_id)) 79 | book.authors = filter( 80 | lambda author: author.id != author_id, 81 | book.authors, 82 | ) 83 | self.bookRepository.update(book_id, book) 84 | 85 | return book.authors 86 | -------------------------------------------------------------------------------- /services/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xTheProDev/fastapi-clean-example/181bb46cf7f7e7cdb58844ba65020b1a4aadbdc3/services/__init__.py --------------------------------------------------------------------------------