├── .gitignore ├── LICENSE ├── Pipfile ├── Pipfile.lock ├── README.md ├── alembic.ini ├── pyproject.toml └── src ├── application ├── authentication │ ├── __init__.py │ └── dependency_injection.py └── orders.py ├── config └── __init__.py ├── domain ├── authentication │ ├── __init__.py │ └── models.py ├── orders │ ├── __init__.py │ ├── models.py │ └── repository.py ├── products │ ├── __init__.py │ ├── models.py │ └── repository.py └── users │ ├── __init__.py │ ├── models.py │ └── repository.py ├── infrastructure ├── application │ ├── __init__.py │ └── factory.py ├── database │ ├── __init__.py │ ├── migrations │ │ ├── README │ │ ├── env.py │ │ ├── script.py.mako │ │ └── versions │ │ │ └── 69888b706120_.py │ ├── repository.py │ ├── session.py │ ├── tables.py │ └── transaction.py ├── errors │ ├── __init__.py │ ├── base.py │ └── handlers.py └── models │ ├── __init__.py │ ├── base.py │ └── response.py ├── main.py └── presentation ├── __init__.py └── rest ├── __init__.py ├── orders.py └── products.py /.gitignore: -------------------------------------------------------------------------------- 1 | ## Local configurations 2 | 3 | # Virtual environments 4 | .venv 5 | env/ 6 | venv/ 7 | 8 | # Environment variables 9 | .env 10 | 11 | 12 | 13 | ## Editors 14 | 15 | # VS code 16 | .vscode 17 | 18 | # PyCharm 19 | .idea 20 | 21 | 22 | 23 | ## Operating system specific 24 | 25 | # Mac 26 | .DS_Store 27 | 28 | 29 | ## Storages 30 | 31 | # Sqlite 32 | *.sqlite3 33 | 34 | # Databases dumps 35 | dump.sql 36 | 37 | # Volumes 38 | data/ 39 | 40 | 41 | 42 | ## Log files 43 | *log 44 | 45 | 46 | 47 | ## Kubernetes configmaps 48 | infra/configmaps.yaml 49 | 50 | 51 | 52 | ## Python stuff 53 | 54 | # Byte-compiled / optimized / DLL files 55 | __pycache__/ 56 | 57 | # Unit test / coverage reports 58 | .coverage 59 | .coverage.* 60 | 61 | # Hypothesis 62 | .hypothesis/ 63 | 64 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Dmytro Parfeniuk 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 | SQLAlchemy = {version = "~=2.0", extras=["asyncio", "mypy"]} 8 | aiosqlite = "~=0.19" 9 | alembic = "~=1.10" 10 | fastapi = "==0.92.0" 11 | greenlet = "~=2.0" # required by SQLAlchemy: https://docs.sqlalchemy.org/en/20/orm/extensions/asyncio.html 12 | gunicorn = "~=20.1" 13 | loguru = "~=0.6" 14 | passlib = {version = "~=1.7", extras = ["bcrypt"]} 15 | pydantic = {version = "~=1.10", extras=["dotenv"]} 16 | python-jose = {version = "~=3.3", extras = ["cryptography"]} 17 | uvicorn = "~=0.21.0" 18 | websockets = "~=11.0" 19 | 20 | 21 | 22 | [dev-packages] 23 | black = "~=23.1" 24 | httpx = "~=0.23" 25 | hypothesis = "~=6.68" 26 | isort = "~=5.12" 27 | mypy = "~=1.0" 28 | pre-commit = "~=3.1" 29 | pydantic-factories = "~=1.17" 30 | pytest = "~=7.2" 31 | pytest-cov = "~=4.0" 32 | pytest-env = "~=0.8" 33 | pytest-lazy-fixture = "~=0.6" 34 | pytest-mock = "~=3.10" 35 | ruff = "~=0.0.261" 36 | sqlalchemy-stubs = "~=0.4" 37 | types-passlib = "~=1.7" 38 | types-python-jose = "~=3.3" 39 | 40 | 41 | [requires] 42 | python_version = "3.11" 43 | -------------------------------------------------------------------------------- /Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "4ec9a1929ccc91bc71b702eb22bd7aad009a87c68d35ad9bbb9161a469ee0811" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3.11" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": { 19 | "aiosqlite": { 20 | "hashes": [ 21 | "sha256:95ee77b91c8d2808bd08a59fbebf66270e9090c3d92ffbf260dc0db0b979577d", 22 | "sha256:edba222e03453e094a3ce605db1b970c4b3376264e56f32e2a4959f948d66a96" 23 | ], 24 | "index": "pypi", 25 | "version": "==0.19.0" 26 | }, 27 | "alembic": { 28 | "hashes": [ 29 | "sha256:6a810a6b012c88b33458fceb869aef09ac75d6ace5291915ba7fae44de372c01", 30 | "sha256:dc871798a601fab38332e38d6ddb38d5e734f60034baeb8e2db5b642fccd8ab8" 31 | ], 32 | "index": "pypi", 33 | "version": "==1.11.1" 34 | }, 35 | "anyio": { 36 | "hashes": [ 37 | "sha256:44a3c9aba0f5defa43261a8b3efb97891f2bd7d804e0e1f56419befa1adfc780", 38 | "sha256:91dee416e570e92c64041bd18b900d1d6fa78dff7048769ce5ac5ddad004fbb5" 39 | ], 40 | "markers": "python_version >= '3.7'", 41 | "version": "==3.7.1" 42 | }, 43 | "bcrypt": { 44 | "hashes": [ 45 | "sha256:089098effa1bc35dc055366740a067a2fc76987e8ec75349eb9484061c54f535", 46 | "sha256:08d2947c490093a11416df18043c27abe3921558d2c03e2076ccb28a116cb6d0", 47 | "sha256:0eaa47d4661c326bfc9d08d16debbc4edf78778e6aaba29c1bc7ce67214d4410", 48 | "sha256:27d375903ac8261cfe4047f6709d16f7d18d39b1ec92aaf72af989552a650ebd", 49 | "sha256:2b3ac11cf45161628f1f3733263e63194f22664bf4d0c0f3ab34099c02134665", 50 | "sha256:2caffdae059e06ac23fce178d31b4a702f2a3264c20bfb5ff541b338194d8fab", 51 | "sha256:3100851841186c25f127731b9fa11909ab7b1df6fc4b9f8353f4f1fd952fbf71", 52 | "sha256:5ad4d32a28b80c5fa6671ccfb43676e8c1cc232887759d1cd7b6f56ea4355215", 53 | "sha256:67a97e1c405b24f19d08890e7ae0c4f7ce1e56a712a016746c8b2d7732d65d4b", 54 | "sha256:705b2cea8a9ed3d55b4491887ceadb0106acf7c6387699fca771af56b1cdeeda", 55 | "sha256:8a68f4341daf7522fe8d73874de8906f3a339048ba406be6ddc1b3ccb16fc0d9", 56 | "sha256:a522427293d77e1c29e303fc282e2d71864579527a04ddcfda6d4f8396c6c36a", 57 | "sha256:ae88eca3024bb34bb3430f964beab71226e761f51b912de5133470b649d82344", 58 | "sha256:b1023030aec778185a6c16cf70f359cbb6e0c289fd564a7cfa29e727a1c38f8f", 59 | "sha256:b3b85202d95dd568efcb35b53936c5e3b3600c7cdcc6115ba461df3a8e89f38d", 60 | "sha256:b57adba8a1444faf784394de3436233728a1ecaeb6e07e8c22c8848f179b893c", 61 | "sha256:bf4fa8b2ca74381bb5442c089350f09a3f17797829d958fad058d6e44d9eb83c", 62 | "sha256:ca3204d00d3cb2dfed07f2d74a25f12fc12f73e606fcaa6975d1f7ae69cacbb2", 63 | "sha256:cbb03eec97496166b704ed663a53680ab57c5084b2fc98ef23291987b525cb7d", 64 | "sha256:e9a51bbfe7e9802b5f3508687758b564069ba937748ad7b9e890086290d2f79e", 65 | "sha256:fbdaec13c5105f0c4e5c52614d04f0bca5f5af007910daa8b6b12095edaa67b3" 66 | ], 67 | "version": "==4.0.1" 68 | }, 69 | "cffi": { 70 | "hashes": [ 71 | "sha256:00a9ed42e88df81ffae7a8ab6d9356b371399b91dbdf0c3cb1e84c03a13aceb5", 72 | "sha256:03425bdae262c76aad70202debd780501fabeaca237cdfddc008987c0e0f59ef", 73 | "sha256:04ed324bda3cda42b9b695d51bb7d54b680b9719cfab04227cdd1e04e5de3104", 74 | "sha256:0e2642fe3142e4cc4af0799748233ad6da94c62a8bec3a6648bf8ee68b1c7426", 75 | "sha256:173379135477dc8cac4bc58f45db08ab45d228b3363adb7af79436135d028405", 76 | "sha256:198caafb44239b60e252492445da556afafc7d1e3ab7a1fb3f0584ef6d742375", 77 | "sha256:1e74c6b51a9ed6589199c787bf5f9875612ca4a8a0785fb2d4a84429badaf22a", 78 | "sha256:2012c72d854c2d03e45d06ae57f40d78e5770d252f195b93f581acf3ba44496e", 79 | "sha256:21157295583fe8943475029ed5abdcf71eb3911894724e360acff1d61c1d54bc", 80 | "sha256:2470043b93ff09bf8fb1d46d1cb756ce6132c54826661a32d4e4d132e1977adf", 81 | "sha256:285d29981935eb726a4399badae8f0ffdff4f5050eaa6d0cfc3f64b857b77185", 82 | "sha256:30d78fbc8ebf9c92c9b7823ee18eb92f2e6ef79b45ac84db507f52fbe3ec4497", 83 | "sha256:320dab6e7cb2eacdf0e658569d2575c4dad258c0fcc794f46215e1e39f90f2c3", 84 | "sha256:33ab79603146aace82c2427da5ca6e58f2b3f2fb5da893ceac0c42218a40be35", 85 | "sha256:3548db281cd7d2561c9ad9984681c95f7b0e38881201e157833a2342c30d5e8c", 86 | "sha256:3799aecf2e17cf585d977b780ce79ff0dc9b78d799fc694221ce814c2c19db83", 87 | "sha256:39d39875251ca8f612b6f33e6b1195af86d1b3e60086068be9cc053aa4376e21", 88 | "sha256:3b926aa83d1edb5aa5b427b4053dc420ec295a08e40911296b9eb1b6170f6cca", 89 | "sha256:3bcde07039e586f91b45c88f8583ea7cf7a0770df3a1649627bf598332cb6984", 90 | "sha256:3d08afd128ddaa624a48cf2b859afef385b720bb4b43df214f85616922e6a5ac", 91 | "sha256:3eb6971dcff08619f8d91607cfc726518b6fa2a9eba42856be181c6d0d9515fd", 92 | "sha256:40f4774f5a9d4f5e344f31a32b5096977b5d48560c5592e2f3d2c4374bd543ee", 93 | "sha256:4289fc34b2f5316fbb762d75362931e351941fa95fa18789191b33fc4cf9504a", 94 | "sha256:470c103ae716238bbe698d67ad020e1db9d9dba34fa5a899b5e21577e6d52ed2", 95 | "sha256:4f2c9f67e9821cad2e5f480bc8d83b8742896f1242dba247911072d4fa94c192", 96 | "sha256:50a74364d85fd319352182ef59c5c790484a336f6db772c1a9231f1c3ed0cbd7", 97 | "sha256:54a2db7b78338edd780e7ef7f9f6c442500fb0d41a5a4ea24fff1c929d5af585", 98 | "sha256:5635bd9cb9731e6d4a1132a498dd34f764034a8ce60cef4f5319c0541159392f", 99 | "sha256:59c0b02d0a6c384d453fece7566d1c7e6b7bae4fc5874ef2ef46d56776d61c9e", 100 | "sha256:5d598b938678ebf3c67377cdd45e09d431369c3b1a5b331058c338e201f12b27", 101 | "sha256:5df2768244d19ab7f60546d0c7c63ce1581f7af8b5de3eb3004b9b6fc8a9f84b", 102 | "sha256:5ef34d190326c3b1f822a5b7a45f6c4535e2f47ed06fec77d3d799c450b2651e", 103 | "sha256:6975a3fac6bc83c4a65c9f9fcab9e47019a11d3d2cf7f3c0d03431bf145a941e", 104 | "sha256:6c9a799e985904922a4d207a94eae35c78ebae90e128f0c4e521ce339396be9d", 105 | "sha256:70df4e3b545a17496c9b3f41f5115e69a4f2e77e94e1d2a8e1070bc0c38c8a3c", 106 | "sha256:7473e861101c9e72452f9bf8acb984947aa1661a7704553a9f6e4baa5ba64415", 107 | "sha256:8102eaf27e1e448db915d08afa8b41d6c7ca7a04b7d73af6514df10a3e74bd82", 108 | "sha256:87c450779d0914f2861b8526e035c5e6da0a3199d8f1add1a665e1cbc6fc6d02", 109 | "sha256:8b7ee99e510d7b66cdb6c593f21c043c248537a32e0bedf02e01e9553a172314", 110 | "sha256:91fc98adde3d7881af9b59ed0294046f3806221863722ba7d8d120c575314325", 111 | "sha256:94411f22c3985acaec6f83c6df553f2dbe17b698cc7f8ae751ff2237d96b9e3c", 112 | "sha256:98d85c6a2bef81588d9227dde12db8a7f47f639f4a17c9ae08e773aa9c697bf3", 113 | "sha256:9ad5db27f9cabae298d151c85cf2bad1d359a1b9c686a275df03385758e2f914", 114 | "sha256:a0b71b1b8fbf2b96e41c4d990244165e2c9be83d54962a9a1d118fd8657d2045", 115 | "sha256:a0f100c8912c114ff53e1202d0078b425bee3649ae34d7b070e9697f93c5d52d", 116 | "sha256:a591fe9e525846e4d154205572a029f653ada1a78b93697f3b5a8f1f2bc055b9", 117 | "sha256:a5c84c68147988265e60416b57fc83425a78058853509c1b0629c180094904a5", 118 | "sha256:a66d3508133af6e8548451b25058d5812812ec3798c886bf38ed24a98216fab2", 119 | "sha256:a8c4917bd7ad33e8eb21e9a5bbba979b49d9a97acb3a803092cbc1133e20343c", 120 | "sha256:b3bbeb01c2b273cca1e1e0c5df57f12dce9a4dd331b4fa1635b8bec26350bde3", 121 | "sha256:cba9d6b9a7d64d4bd46167096fc9d2f835e25d7e4c121fb2ddfc6528fb0413b2", 122 | "sha256:cc4d65aeeaa04136a12677d3dd0b1c0c94dc43abac5860ab33cceb42b801c1e8", 123 | "sha256:ce4bcc037df4fc5e3d184794f27bdaab018943698f4ca31630bc7f84a7b69c6d", 124 | "sha256:cec7d9412a9102bdc577382c3929b337320c4c4c4849f2c5cdd14d7368c5562d", 125 | "sha256:d400bfb9a37b1351253cb402671cea7e89bdecc294e8016a707f6d1d8ac934f9", 126 | "sha256:d61f4695e6c866a23a21acab0509af1cdfd2c013cf256bbf5b6b5e2695827162", 127 | "sha256:db0fbb9c62743ce59a9ff687eb5f4afbe77e5e8403d6697f7446e5f609976f76", 128 | "sha256:dd86c085fae2efd48ac91dd7ccffcfc0571387fe1193d33b6394db7ef31fe2a4", 129 | "sha256:e00b098126fd45523dd056d2efba6c5a63b71ffe9f2bbe1a4fe1716e1d0c331e", 130 | "sha256:e229a521186c75c8ad9490854fd8bbdd9a0c9aa3a524326b55be83b54d4e0ad9", 131 | "sha256:e263d77ee3dd201c3a142934a086a4450861778baaeeb45db4591ef65550b0a6", 132 | "sha256:ed9cb427ba5504c1dc15ede7d516b84757c3e3d7868ccc85121d9310d27eed0b", 133 | "sha256:fa6693661a4c91757f4412306191b6dc88c1703f780c8234035eac011922bc01", 134 | "sha256:fcd131dd944808b5bdb38e6f5b53013c5aa4f334c5cad0c72742f6eba4b73db0" 135 | ], 136 | "version": "==1.15.1" 137 | }, 138 | "click": { 139 | "hashes": [ 140 | "sha256:48ee849951919527a045bfe3bf7baa8a959c423134e1a5b98c05c20ba75a1cbd", 141 | "sha256:fa244bb30b3b5ee2cae3da8f55c9e5e0c0e86093306301fb418eb9dc40fbded5" 142 | ], 143 | "markers": "python_version >= '3.7'", 144 | "version": "==8.1.6" 145 | }, 146 | "cryptography": { 147 | "hashes": [ 148 | "sha256:01f1d9e537f9a15b037d5d9ee442b8c22e3ae11ce65ea1f3316a41c78756b711", 149 | "sha256:079347de771f9282fbfe0e0236c716686950c19dee1b76240ab09ce1624d76d7", 150 | "sha256:182be4171f9332b6741ee818ec27daff9fb00349f706629f5cbf417bd50e66fd", 151 | "sha256:192255f539d7a89f2102d07d7375b1e0a81f7478925b3bc2e0549ebf739dae0e", 152 | "sha256:2a034bf7d9ca894720f2ec1d8b7b5832d7e363571828037f9e0c4f18c1b58a58", 153 | "sha256:342f3767e25876751e14f8459ad85e77e660537ca0a066e10e75df9c9e9099f0", 154 | "sha256:439c3cc4c0d42fa999b83ded80a9a1fb54d53c58d6e59234cfe97f241e6c781d", 155 | "sha256:49c3222bb8f8e800aead2e376cbef687bc9e3cb9b58b29a261210456a7783d83", 156 | "sha256:674b669d5daa64206c38e507808aae49904c988fa0a71c935e7006a3e1e83831", 157 | "sha256:7a9a3bced53b7f09da251685224d6a260c3cb291768f54954e28f03ef14e3766", 158 | "sha256:7af244b012711a26196450d34f483357e42aeddb04128885d95a69bd8b14b69b", 159 | "sha256:7d230bf856164de164ecb615ccc14c7fc6de6906ddd5b491f3af90d3514c925c", 160 | "sha256:84609ade00a6ec59a89729e87a503c6e36af98ddcd566d5f3be52e29ba993182", 161 | "sha256:9a6673c1828db6270b76b22cc696f40cde9043eb90373da5c2f8f2158957f42f", 162 | "sha256:9b6d717393dbae53d4e52684ef4f022444fc1cce3c48c38cb74fca29e1f08eaa", 163 | "sha256:9c3fe6534d59d071ee82081ca3d71eed3210f76ebd0361798c74abc2bcf347d4", 164 | "sha256:a719399b99377b218dac6cf547b6ec54e6ef20207b6165126a280b0ce97e0d2a", 165 | "sha256:b332cba64d99a70c1e0836902720887fb4529ea49ea7f5462cf6640e095e11d2", 166 | "sha256:d124682c7a23c9764e54ca9ab5b308b14b18eba02722b8659fb238546de83a76", 167 | "sha256:d73f419a56d74fef257955f51b18d046f3506270a5fd2ac5febbfa259d6c0fa5", 168 | "sha256:f0dc40e6f7aa37af01aba07277d3d64d5a03dc66d682097541ec4da03cc140ee", 169 | "sha256:f14ad275364c8b4e525d018f6716537ae7b6d369c094805cae45300847e0894f", 170 | "sha256:f772610fe364372de33d76edcd313636a25684edb94cee53fd790195f5989d14" 171 | ], 172 | "version": "==41.0.2" 173 | }, 174 | "ecdsa": { 175 | "hashes": [ 176 | "sha256:190348041559e21b22a1d65cee485282ca11a6f81d503fddb84d5017e9ed1e49", 177 | "sha256:80600258e7ed2f16b9aa1d7c295bd70194109ad5a30fdee0eaeefef1d4c559dd" 178 | ], 179 | "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", 180 | "version": "==0.18.0" 181 | }, 182 | "fastapi": { 183 | "hashes": [ 184 | "sha256:023a0f5bd2c8b2609014d3bba1e14a1d7df96c6abea0a73070621c9862b9a4de", 185 | "sha256:ae7b97c778e2f2ec3fb3cb4fb14162129411d99907fb71920f6d69a524340ebf" 186 | ], 187 | "index": "pypi", 188 | "version": "==0.92.0" 189 | }, 190 | "greenlet": { 191 | "hashes": [ 192 | "sha256:03a8f4f3430c3b3ff8d10a2a86028c660355ab637cee9333d63d66b56f09d52a", 193 | "sha256:0bf60faf0bc2468089bdc5edd10555bab6e85152191df713e2ab1fcc86382b5a", 194 | "sha256:18a7f18b82b52ee85322d7a7874e676f34ab319b9f8cce5de06067384aa8ff43", 195 | "sha256:18e98fb3de7dba1c0a852731c3070cf022d14f0d68b4c87a19cc1016f3bb8b33", 196 | "sha256:1a819eef4b0e0b96bb0d98d797bef17dc1b4a10e8d7446be32d1da33e095dbb8", 197 | "sha256:26fbfce90728d82bc9e6c38ea4d038cba20b7faf8a0ca53a9c07b67318d46088", 198 | "sha256:2780572ec463d44c1d3ae850239508dbeb9fed38e294c68d19a24d925d9223ca", 199 | "sha256:283737e0da3f08bd637b5ad058507e578dd462db259f7f6e4c5c365ba4ee9343", 200 | "sha256:2d4686f195e32d36b4d7cf2d166857dbd0ee9f3d20ae349b6bf8afc8485b3645", 201 | "sha256:2dd11f291565a81d71dab10b7033395b7a3a5456e637cf997a6f33ebdf06f8db", 202 | "sha256:30bcf80dda7f15ac77ba5af2b961bdd9dbc77fd4ac6105cee85b0d0a5fcf74df", 203 | "sha256:32e5b64b148966d9cccc2c8d35a671409e45f195864560829f395a54226408d3", 204 | "sha256:36abbf031e1c0f79dd5d596bfaf8e921c41df2bdf54ee1eed921ce1f52999a86", 205 | "sha256:3a06ad5312349fec0ab944664b01d26f8d1f05009566339ac6f63f56589bc1a2", 206 | "sha256:3a51c9751078733d88e013587b108f1b7a1fb106d402fb390740f002b6f6551a", 207 | "sha256:3c9b12575734155d0c09d6c3e10dbd81665d5c18e1a7c6597df72fd05990c8cf", 208 | "sha256:3f6ea9bd35eb450837a3d80e77b517ea5bc56b4647f5502cd28de13675ee12f7", 209 | "sha256:4b58adb399c4d61d912c4c331984d60eb66565175cdf4a34792cd9600f21b394", 210 | "sha256:4d2e11331fc0c02b6e84b0d28ece3a36e0548ee1a1ce9ddde03752d9b79bba40", 211 | "sha256:5454276c07d27a740c5892f4907c86327b632127dd9abec42ee62e12427ff7e3", 212 | "sha256:561091a7be172ab497a3527602d467e2b3fbe75f9e783d8b8ce403fa414f71a6", 213 | "sha256:6c3acb79b0bfd4fe733dff8bc62695283b57949ebcca05ae5c129eb606ff2d74", 214 | "sha256:703f18f3fda276b9a916f0934d2fb6d989bf0b4fb5a64825260eb9bfd52d78f0", 215 | "sha256:7492e2b7bd7c9b9916388d9df23fa49d9b88ac0640db0a5b4ecc2b653bf451e3", 216 | "sha256:76ae285c8104046b3a7f06b42f29c7b73f77683df18c49ab5af7983994c2dd91", 217 | "sha256:7cafd1208fdbe93b67c7086876f061f660cfddc44f404279c1585bbf3cdc64c5", 218 | "sha256:7efde645ca1cc441d6dc4b48c0f7101e8d86b54c8530141b09fd31cef5149ec9", 219 | "sha256:88d9ab96491d38a5ab7c56dd7a3cc37d83336ecc564e4e8816dbed12e5aaefc8", 220 | "sha256:8eab883b3b2a38cc1e050819ef06a7e6344d4a990d24d45bc6f2cf959045a45b", 221 | "sha256:910841381caba4f744a44bf81bfd573c94e10b3045ee00de0cbf436fe50673a6", 222 | "sha256:9190f09060ea4debddd24665d6804b995a9c122ef5917ab26e1566dcc712ceeb", 223 | "sha256:937e9020b514ceedb9c830c55d5c9872abc90f4b5862f89c0887033ae33c6f73", 224 | "sha256:94c817e84245513926588caf1152e3b559ff794d505555211ca041f032abbb6b", 225 | "sha256:971ce5e14dc5e73715755d0ca2975ac88cfdaefcaab078a284fea6cfabf866df", 226 | "sha256:9d14b83fab60d5e8abe587d51c75b252bcc21683f24699ada8fb275d7712f5a9", 227 | "sha256:9f35ec95538f50292f6d8f2c9c9f8a3c6540bbfec21c9e5b4b751e0a7c20864f", 228 | "sha256:a1846f1b999e78e13837c93c778dcfc3365902cfb8d1bdb7dd73ead37059f0d0", 229 | "sha256:acd2162a36d3de67ee896c43effcd5ee3de247eb00354db411feb025aa319857", 230 | "sha256:b0ef99cdbe2b682b9ccbb964743a6aca37905fda5e0452e5ee239b1654d37f2a", 231 | "sha256:b80f600eddddce72320dbbc8e3784d16bd3fb7b517e82476d8da921f27d4b249", 232 | "sha256:b864ba53912b6c3ab6bcb2beb19f19edd01a6bfcbdfe1f37ddd1778abfe75a30", 233 | "sha256:b9ec052b06a0524f0e35bd8790686a1da006bd911dd1ef7d50b77bfbad74e292", 234 | "sha256:ba2956617f1c42598a308a84c6cf021a90ff3862eddafd20c3333d50f0edb45b", 235 | "sha256:bdfea8c661e80d3c1c99ad7c3ff74e6e87184895bbaca6ee8cc61209f8b9b85d", 236 | "sha256:be4ed120b52ae4d974aa40215fcdfde9194d63541c7ded40ee12eb4dda57b76b", 237 | "sha256:c4302695ad8027363e96311df24ee28978162cdcdd2006476c43970b384a244c", 238 | "sha256:c48f54ef8e05f04d6eff74b8233f6063cb1ed960243eacc474ee73a2ea8573ca", 239 | "sha256:c9c59a2120b55788e800d82dfa99b9e156ff8f2227f07c5e3012a45a399620b7", 240 | "sha256:cd021c754b162c0fb55ad5d6b9d960db667faad0fa2ff25bb6e1301b0b6e6a75", 241 | "sha256:d27ec7509b9c18b6d73f2f5ede2622441de812e7b1a80bbd446cb0633bd3d5ae", 242 | "sha256:d5508f0b173e6aa47273bdc0a0b5ba055b59662ba7c7ee5119528f466585526b", 243 | "sha256:d75209eed723105f9596807495d58d10b3470fa6732dd6756595e89925ce2470", 244 | "sha256:db1a39669102a1d8d12b57de2bb7e2ec9066a6f2b3da35ae511ff93b01b5d564", 245 | "sha256:dbfcfc0218093a19c252ca8eb9aee3d29cfdcb586df21049b9d777fd32c14fd9", 246 | "sha256:e0f72c9ddb8cd28532185f54cc1453f2c16fb417a08b53a855c4e6a418edd099", 247 | "sha256:e7c8dc13af7db097bed64a051d2dd49e9f0af495c26995c00a9ee842690d34c0", 248 | "sha256:ea9872c80c132f4663822dd2a08d404073a5a9b5ba6155bea72fb2a79d1093b5", 249 | "sha256:eff4eb9b7eb3e4d0cae3d28c283dc16d9bed6b193c2e1ace3ed86ce48ea8df19", 250 | "sha256:f82d4d717d8ef19188687aa32b8363e96062911e63ba22a0cff7802a8e58e5f1", 251 | "sha256:fc3a569657468b6f3fb60587e48356fe512c1754ca05a564f11366ac9e306526" 252 | ], 253 | "index": "pypi", 254 | "version": "==2.0.2" 255 | }, 256 | "gunicorn": { 257 | "hashes": [ 258 | "sha256:9dcc4547dbb1cb284accfb15ab5667a0e5d1881cc443e0677b4882a4067a807e", 259 | "sha256:e0a968b5ba15f8a328fdfd7ab1fcb5af4470c28aaf7e55df02a99bc13138e6e8" 260 | ], 261 | "index": "pypi", 262 | "version": "==20.1.0" 263 | }, 264 | "h11": { 265 | "hashes": [ 266 | "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d", 267 | "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761" 268 | ], 269 | "markers": "python_version >= '3.7'", 270 | "version": "==0.14.0" 271 | }, 272 | "idna": { 273 | "hashes": [ 274 | "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4", 275 | "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2" 276 | ], 277 | "markers": "python_version >= '3.5'", 278 | "version": "==3.4" 279 | }, 280 | "loguru": { 281 | "hashes": [ 282 | "sha256:1612053ced6ae84d7959dd7d5e431a0532642237ec21f7fd83ac73fe539e03e1", 283 | "sha256:b93aa30099fa6860d4727f1b81f8718e965bb96253fa190fab2077aaad6d15d3" 284 | ], 285 | "index": "pypi", 286 | "version": "==0.7.0" 287 | }, 288 | "mako": { 289 | "hashes": [ 290 | "sha256:c97c79c018b9165ac9922ae4f32da095ffd3c4e6872b45eded42926deea46818", 291 | "sha256:d60a3903dc3bb01a18ad6a89cdbe2e4eadc69c0bc8ef1e3773ba53d44c3f7a34" 292 | ], 293 | "markers": "python_version >= '3.7'", 294 | "version": "==1.2.4" 295 | }, 296 | "markupsafe": { 297 | "hashes": [ 298 | "sha256:05fb21170423db021895e1ea1e1f3ab3adb85d1c2333cbc2310f2a26bc77272e", 299 | "sha256:0a4e4a1aff6c7ac4cd55792abf96c915634c2b97e3cc1c7129578aa68ebd754e", 300 | "sha256:10bbfe99883db80bdbaff2dcf681dfc6533a614f700da1287707e8a5d78a8431", 301 | "sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686", 302 | "sha256:1577735524cdad32f9f694208aa75e422adba74f1baee7551620e43a3141f559", 303 | "sha256:1b40069d487e7edb2676d3fbdb2b0829ffa2cd63a2ec26c4938b2d34391b4ecc", 304 | "sha256:282c2cb35b5b673bbcadb33a585408104df04f14b2d9b01d4c345a3b92861c2c", 305 | "sha256:2c1b19b3aaacc6e57b7e25710ff571c24d6c3613a45e905b1fde04d691b98ee0", 306 | "sha256:2ef12179d3a291be237280175b542c07a36e7f60718296278d8593d21ca937d4", 307 | "sha256:338ae27d6b8745585f87218a3f23f1512dbf52c26c28e322dbe54bcede54ccb9", 308 | "sha256:3c0fae6c3be832a0a0473ac912810b2877c8cb9d76ca48de1ed31e1c68386575", 309 | "sha256:3fd4abcb888d15a94f32b75d8fd18ee162ca0c064f35b11134be77050296d6ba", 310 | "sha256:42de32b22b6b804f42c5d98be4f7e5e977ecdd9ee9b660fda1a3edf03b11792d", 311 | "sha256:504b320cd4b7eff6f968eddf81127112db685e81f7e36e75f9f84f0df46041c3", 312 | "sha256:525808b8019e36eb524b8c68acdd63a37e75714eac50e988180b169d64480a00", 313 | "sha256:56d9f2ecac662ca1611d183feb03a3fa4406469dafe241673d521dd5ae92a155", 314 | "sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac", 315 | "sha256:65c1a9bcdadc6c28eecee2c119465aebff8f7a584dd719facdd9e825ec61ab52", 316 | "sha256:68e78619a61ecf91e76aa3e6e8e33fc4894a2bebe93410754bd28fce0a8a4f9f", 317 | "sha256:69c0f17e9f5a7afdf2cc9fb2d1ce6aabdb3bafb7f38017c0b77862bcec2bbad8", 318 | "sha256:6b2b56950d93e41f33b4223ead100ea0fe11f8e6ee5f641eb753ce4b77a7042b", 319 | "sha256:787003c0ddb00500e49a10f2844fac87aa6ce977b90b0feaaf9de23c22508b24", 320 | "sha256:7ef3cb2ebbf91e330e3bb937efada0edd9003683db6b57bb108c4001f37a02ea", 321 | "sha256:8023faf4e01efadfa183e863fefde0046de576c6f14659e8782065bcece22198", 322 | "sha256:8758846a7e80910096950b67071243da3e5a20ed2546e6392603c096778d48e0", 323 | "sha256:8afafd99945ead6e075b973fefa56379c5b5c53fd8937dad92c662da5d8fd5ee", 324 | "sha256:8c41976a29d078bb235fea9b2ecd3da465df42a562910f9022f1a03107bd02be", 325 | "sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2", 326 | "sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707", 327 | "sha256:962f82a3086483f5e5f64dbad880d31038b698494799b097bc59c2edf392fce6", 328 | "sha256:9dcdfd0eaf283af041973bff14a2e143b8bd64e069f4c383416ecd79a81aab58", 329 | "sha256:aa7bd130efab1c280bed0f45501b7c8795f9fdbeb02e965371bbef3523627779", 330 | "sha256:ab4a0df41e7c16a1392727727e7998a467472d0ad65f3ad5e6e765015df08636", 331 | "sha256:ad9e82fb8f09ade1c3e1b996a6337afac2b8b9e365f926f5a61aacc71adc5b3c", 332 | "sha256:af598ed32d6ae86f1b747b82783958b1a4ab8f617b06fe68795c7f026abbdcad", 333 | "sha256:b076b6226fb84157e3f7c971a47ff3a679d837cf338547532ab866c57930dbee", 334 | "sha256:b7ff0f54cb4ff66dd38bebd335a38e2c22c41a8ee45aa608efc890ac3e3931bc", 335 | "sha256:bfce63a9e7834b12b87c64d6b155fdd9b3b96191b6bd334bf37db7ff1fe457f2", 336 | "sha256:c011a4149cfbcf9f03994ec2edffcb8b1dc2d2aede7ca243746df97a5d41ce48", 337 | "sha256:c9c804664ebe8f83a211cace637506669e7890fec1b4195b505c214e50dd4eb7", 338 | "sha256:ca379055a47383d02a5400cb0d110cef0a776fc644cda797db0c5696cfd7e18e", 339 | "sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b", 340 | "sha256:cd0f502fe016460680cd20aaa5a76d241d6f35a1c3350c474bac1273803893fa", 341 | "sha256:ceb01949af7121f9fc39f7d27f91be8546f3fb112c608bc4029aef0bab86a2a5", 342 | "sha256:d080e0a5eb2529460b30190fcfcc4199bd7f827663f858a226a81bc27beaa97e", 343 | "sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb", 344 | "sha256:df0be2b576a7abbf737b1575f048c23fb1d769f267ec4358296f31c2479db8f9", 345 | "sha256:e09031c87a1e51556fdcb46e5bd4f59dfb743061cf93c4d6831bf894f125eb57", 346 | "sha256:e4dd52d80b8c83fdce44e12478ad2e85c64ea965e75d66dbeafb0a3e77308fcc", 347 | "sha256:fec21693218efe39aa7f8599346e90c705afa52c5b31ae019b2e57e8f6542bb2" 348 | ], 349 | "markers": "python_version >= '3.7'", 350 | "version": "==2.1.3" 351 | }, 352 | "mypy": { 353 | "hashes": [ 354 | "sha256:01fd2e9f85622d981fd9063bfaef1aed6e336eaacca00892cd2d82801ab7c042", 355 | "sha256:0dde1d180cd84f0624c5dcaaa89c89775550a675aff96b5848de78fb11adabcd", 356 | "sha256:141dedfdbfe8a04142881ff30ce6e6653c9685b354876b12e4fe6c78598b45e2", 357 | "sha256:16f0db5b641ba159eff72cff08edc3875f2b62b2fa2bc24f68c1e7a4e8232d01", 358 | "sha256:190b6bab0302cec4e9e6767d3eb66085aef2a1cc98fe04936d8a42ed2ba77bb7", 359 | "sha256:2460a58faeea905aeb1b9b36f5065f2dc9a9c6e4c992a6499a2360c6c74ceca3", 360 | "sha256:34a9239d5b3502c17f07fd7c0b2ae6b7dd7d7f6af35fbb5072c6208e76295816", 361 | "sha256:43b592511672017f5b1a483527fd2684347fdffc041c9ef53428c8dc530f79a3", 362 | "sha256:43d24f6437925ce50139a310a64b2ab048cb2d3694c84c71c3f2a1626d8101dc", 363 | "sha256:45d32cec14e7b97af848bddd97d85ea4f0db4d5a149ed9676caa4eb2f7402bb4", 364 | "sha256:470c969bb3f9a9efcedbadcd19a74ffb34a25f8e6b0e02dae7c0e71f8372f97b", 365 | "sha256:566e72b0cd6598503e48ea610e0052d1b8168e60a46e0bfd34b3acf2d57f96a8", 366 | "sha256:5703097c4936bbb9e9bce41478c8d08edd2865e177dc4c52be759f81ee4dd26c", 367 | "sha256:7549fbf655e5825d787bbc9ecf6028731973f78088fbca3a1f4145c39ef09462", 368 | "sha256:8207b7105829eca6f3d774f64a904190bb2231de91b8b186d21ffd98005f14a7", 369 | "sha256:8c4d8e89aa7de683e2056a581ce63c46a0c41e31bd2b6d34144e2c80f5ea53dc", 370 | "sha256:98324ec3ecf12296e6422939e54763faedbfcc502ea4a4c38502082711867258", 371 | "sha256:9bbcd9ab8ea1f2e1c8031c21445b511442cc45c89951e49bbf852cbb70755b1b", 372 | "sha256:9d40652cc4fe33871ad3338581dca3297ff5f2213d0df345bcfbde5162abf0c9", 373 | "sha256:a2746d69a8196698146a3dbe29104f9eb6a2a4d8a27878d92169a6c0b74435b6", 374 | "sha256:ae704dcfaa180ff7c4cfbad23e74321a2b774f92ca77fd94ce1049175a21c97f", 375 | "sha256:bfdca17c36ae01a21274a3c387a63aa1aafe72bff976522886869ef131b937f1", 376 | "sha256:c482e1246726616088532b5e964e39765b6d1520791348e6c9dc3af25b233828", 377 | "sha256:ca637024ca67ab24a7fd6f65d280572c3794665eaf5edcc7e90a866544076878", 378 | "sha256:e02d700ec8d9b1859790c0475df4e4092c7bf3272a4fd2c9f33d87fac4427b8f", 379 | "sha256:e5952d2d18b79f7dc25e62e014fe5a23eb1a3d2bc66318df8988a01b1a037c5b" 380 | ], 381 | "version": "==1.4.1" 382 | }, 383 | "mypy-extensions": { 384 | "hashes": [ 385 | "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", 386 | "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782" 387 | ], 388 | "markers": "python_version >= '3.5'", 389 | "version": "==1.0.0" 390 | }, 391 | "passlib": { 392 | "extras": [ 393 | "bcrypt" 394 | ], 395 | "hashes": [ 396 | "sha256:aa6bca462b8d8bda89c70b382f0c298a20b5560af6cbfa2dce410c0a2fb669f1", 397 | "sha256:defd50f72b65c5402ab2c573830a6978e5f202ad0d984793c8dde2c4152ebe04" 398 | ], 399 | "index": "pypi", 400 | "version": "==1.7.4" 401 | }, 402 | "pyasn1": { 403 | "hashes": [ 404 | "sha256:87a2121042a1ac9358cabcaf1d07680ff97ee6404333bacca15f76aa8ad01a57", 405 | "sha256:97b7290ca68e62a832558ec3976f15cbf911bf5d7c7039d8b861c2a0ece69fde" 406 | ], 407 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", 408 | "version": "==0.5.0" 409 | }, 410 | "pycparser": { 411 | "hashes": [ 412 | "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9", 413 | "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206" 414 | ], 415 | "version": "==2.21" 416 | }, 417 | "pydantic": { 418 | "extras": [ 419 | "dotenv" 420 | ], 421 | "hashes": [ 422 | "sha256:008c5e266c8aada206d0627a011504e14268a62091450210eda7c07fabe6963e", 423 | "sha256:0588788a9a85f3e5e9ebca14211a496409cb3deca5b6971ff37c556d581854e7", 424 | "sha256:08a6c32e1c3809fbc49debb96bf833164f3438b3696abf0fbeceb417d123e6eb", 425 | "sha256:16928fdc9cb273c6af00d9d5045434c39afba5f42325fb990add2c241402d151", 426 | "sha256:174899023337b9fc685ac8adaa7b047050616136ccd30e9070627c1aaab53a13", 427 | "sha256:192c608ad002a748e4a0bed2ddbcd98f9b56df50a7c24d9a931a8c5dd053bd3d", 428 | "sha256:1954f8778489a04b245a1e7b8b22a9d3ea8ef49337285693cf6959e4b757535e", 429 | "sha256:2417de68290434461a266271fc57274a138510dca19982336639484c73a07af6", 430 | "sha256:265a60da42f9f27e0b1014eab8acd3e53bd0bad5c5b4884e98a55f8f596b2c19", 431 | "sha256:331c031ba1554b974c98679bd0780d89670d6fd6f53f5d70b10bdc9addee1713", 432 | "sha256:373c0840f5c2b5b1ccadd9286782852b901055998136287828731868027a724f", 433 | "sha256:3f34739a89260dfa420aa3cbd069fbcc794b25bbe5c0a214f8fb29e363484b66", 434 | "sha256:41e0bb6efe86281623abbeeb0be64eab740c865388ee934cd3e6a358784aca6e", 435 | "sha256:4400015f15c9b464c9db2d5d951b6a780102cfa5870f2c036d37c23b56f7fc1b", 436 | "sha256:44e51ba599c3ef227e168424e220cd3e544288c57829520dc90ea9cb190c3248", 437 | "sha256:469adf96c8e2c2bbfa655fc7735a2a82f4c543d9fee97bd113a7fb509bf5e622", 438 | "sha256:5b02d24f7b2b365fed586ed73582c20f353a4c50e4be9ba2c57ab96f8091ddae", 439 | "sha256:7522a7666157aa22b812ce14c827574ddccc94f361237ca6ea8bb0d5c38f1629", 440 | "sha256:787cf23e5a0cde753f2eabac1b2e73ae3844eb873fd1f5bdbff3048d8dbb7604", 441 | "sha256:8268a735a14c308923e8958363e3a3404f6834bb98c11f5ab43251a4e410170c", 442 | "sha256:8dc77064471780262b6a68fe67e013298d130414d5aaf9b562c33987dbd2cf4f", 443 | "sha256:a451ccab49971af043ec4e0d207cbc8cbe53dbf148ef9f19599024076fe9c25b", 444 | "sha256:a6c098d4ab5e2d5b3984d3cb2527e2d6099d3de85630c8934efcfdc348a9760e", 445 | "sha256:abade85268cc92dff86d6effcd917893130f0ff516f3d637f50dadc22ae93999", 446 | "sha256:bc64eab9b19cd794a380179ac0e6752335e9555d214cfcb755820333c0784cb3", 447 | "sha256:c3339a46bbe6013ef7bdd2844679bfe500347ac5742cd4019a88312aa58a9847", 448 | "sha256:d185819a7a059550ecb85d5134e7d40f2565f3dd94cfd870132c5f91a89cf58c", 449 | "sha256:d7781f1d13b19700b7949c5a639c764a077cbbdd4322ed505b449d3ca8edcb36", 450 | "sha256:e297897eb4bebde985f72a46a7552a7556a3dd11e7f76acda0c1093e3dbcf216", 451 | "sha256:e6cbfbd010b14c8a905a7b10f9fe090068d1744d46f9e0c021db28daeb8b6de1", 452 | "sha256:e9738b0f2e6c70f44ee0de53f2089d6002b10c33264abee07bdb5c7f03038303", 453 | "sha256:e9baf78b31da2dc3d3f346ef18e58ec5f12f5aaa17ac517e2ffd026a92a87588", 454 | "sha256:ef55392ec4bb5721f4ded1096241e4b7151ba6d50a50a80a2526c854f42e6a2f", 455 | "sha256:f66d479cf7eb331372c470614be6511eae96f1f120344c25f3f9bb59fb1b5528", 456 | "sha256:fe429898f2c9dd209bd0632a606bddc06f8bce081bbd03d1c775a45886e2c1cb", 457 | "sha256:ff44c5e89315b15ff1f7fdaf9853770b810936d6b01a7bcecaa227d2f8fe444f" 458 | ], 459 | "index": "pypi", 460 | "version": "==1.10.11" 461 | }, 462 | "python-dotenv": { 463 | "hashes": [ 464 | "sha256:a8df96034aae6d2d50a4ebe8216326c61c3eb64836776504fcca410e5937a3ba", 465 | "sha256:f5971a9226b701070a4bf2c38c89e5a3f0d64de8debda981d1db98583009122a" 466 | ], 467 | "version": "==1.0.0" 468 | }, 469 | "python-jose": { 470 | "extras": [ 471 | "cryptography" 472 | ], 473 | "hashes": [ 474 | "sha256:55779b5e6ad599c6336191246e95eb2293a9ddebd555f796a65f838f07e5d78a", 475 | "sha256:9b1376b023f8b298536eedd47ae1089bcdb848f1535ab30555cd92002d78923a" 476 | ], 477 | "index": "pypi", 478 | "version": "==3.3.0" 479 | }, 480 | "rsa": { 481 | "hashes": [ 482 | "sha256:90260d9058e514786967344d0ef75fa8727eed8a7d2e43ce9f4bcf1b536174f7", 483 | "sha256:e38464a49c6c85d7f1351b0126661487a7e0a14a50f1675ec50eb34d4f20ef21" 484 | ], 485 | "markers": "python_version >= '3.6' and python_version < '4'", 486 | "version": "==4.9" 487 | }, 488 | "setuptools": { 489 | "hashes": [ 490 | "sha256:11e52c67415a381d10d6b462ced9cfb97066179f0e871399e006c4ab101fc85f", 491 | "sha256:baf1fdb41c6da4cd2eae722e135500da913332ab3f2f5c7d33af9b492acb5235" 492 | ], 493 | "markers": "python_version >= '3.7'", 494 | "version": "==68.0.0" 495 | }, 496 | "six": { 497 | "hashes": [ 498 | "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", 499 | "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" 500 | ], 501 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 502 | "version": "==1.16.0" 503 | }, 504 | "sniffio": { 505 | "hashes": [ 506 | "sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101", 507 | "sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384" 508 | ], 509 | "markers": "python_version >= '3.7'", 510 | "version": "==1.3.0" 511 | }, 512 | "sqlalchemy": { 513 | "extras": [ 514 | "asyncio", 515 | "mypy" 516 | ], 517 | "hashes": [ 518 | "sha256:024d2f67fb3ec697555e48caeb7147cfe2c08065a4f1a52d93c3d44fc8e6ad1c", 519 | "sha256:0bf0fd65b50a330261ec7fe3d091dfc1c577483c96a9fa1e4323e932961aa1b5", 520 | "sha256:16a310f5bc75a5b2ce7cb656d0e76eb13440b8354f927ff15cbaddd2523ee2d1", 521 | "sha256:1d90ccc15ba1baa345796a8fb1965223ca7ded2d235ccbef80a47b85cea2d71a", 522 | "sha256:22bafb1da60c24514c141a7ff852b52f9f573fb933b1e6b5263f0daa28ce6db9", 523 | "sha256:2c69ce70047b801d2aba3e5ff3cba32014558966109fecab0c39d16c18510f15", 524 | "sha256:2e7b69d9ced4b53310a87117824b23c509c6fc1f692aa7272d47561347e133b6", 525 | "sha256:314145c1389b021a9ad5aa3a18bac6f5d939f9087d7fc5443be28cba19d2c972", 526 | "sha256:3afa8a21a9046917b3a12ffe016ba7ebe7a55a6fc0c7d950beb303c735c3c3ad", 527 | "sha256:430614f18443b58ceb9dedec323ecddc0abb2b34e79d03503b5a7579cd73a531", 528 | "sha256:43699eb3f80920cc39a380c159ae21c8a8924fe071bccb68fc509e099420b148", 529 | "sha256:539010665c90e60c4a1650afe4ab49ca100c74e6aef882466f1de6471d414be7", 530 | "sha256:57d100a421d9ab4874f51285c059003292433c648df6abe6c9c904e5bd5b0828", 531 | "sha256:5831138f0cc06b43edf5f99541c64adf0ab0d41f9a4471fd63b54ae18399e4de", 532 | "sha256:584f66e5e1979a7a00f4935015840be627e31ca29ad13f49a6e51e97a3fb8cae", 533 | "sha256:5d6afc41ca0ecf373366fd8e10aee2797128d3ae45eb8467b19da4899bcd1ee0", 534 | "sha256:61ada5831db36d897e28eb95f0f81814525e0d7927fb51145526c4e63174920b", 535 | "sha256:6b54d1ad7a162857bb7c8ef689049c7cd9eae2f38864fc096d62ae10bc100c7d", 536 | "sha256:7351c05db355da112e056a7b731253cbeffab9dfdb3be1e895368513c7d70106", 537 | "sha256:77a14fa20264af73ddcdb1e2b9c5a829b8cc6b8304d0f093271980e36c200a3f", 538 | "sha256:851a37898a8a39783aab603c7348eb5b20d83c76a14766a43f56e6ad422d1ec8", 539 | "sha256:89bc2b374ebee1a02fd2eae6fd0570b5ad897ee514e0f84c5c137c942772aa0c", 540 | "sha256:8e712cfd2e07b801bc6b60fdf64853bc2bd0af33ca8fa46166a23fe11ce0dbb0", 541 | "sha256:8f9eb4575bfa5afc4b066528302bf12083da3175f71b64a43a7c0badda2be365", 542 | "sha256:8fc05b59142445a4efb9c1fd75c334b431d35c304b0e33f4fa0ff1ea4890f92e", 543 | "sha256:96f0463573469579d32ad0c91929548d78314ef95c210a8115346271beeeaaa2", 544 | "sha256:9deaae357edc2091a9ed5d25e9ee8bba98bcfae454b3911adeaf159c2e9ca9e3", 545 | "sha256:a752b7a9aceb0ba173955d4f780c64ee15a1a991f1c52d307d6215c6c73b3a4c", 546 | "sha256:ae7473a67cd82a41decfea58c0eac581209a0aa30f8bc9190926fbf628bb17f7", 547 | "sha256:b15afbf5aa76f2241184c1d3b61af1a72ba31ce4161013d7cb5c4c2fca04fd6e", 548 | "sha256:c896d4e6ab2eba2afa1d56be3d0b936c56d4666e789bfc59d6ae76e9fcf46145", 549 | "sha256:cb4e688f6784427e5f9479d1a13617f573de8f7d4aa713ba82813bcd16e259d1", 550 | "sha256:cda283700c984e699e8ef0fcc5c61f00c9d14b6f65a4f2767c97242513fcdd84", 551 | "sha256:cf7b5e3856cbf1876da4e9d9715546fa26b6e0ba1a682d5ed2fc3ca4c7c3ec5b", 552 | "sha256:d6894708eeb81f6d8193e996257223b6bb4041cb05a17cd5cf373ed836ef87a2", 553 | "sha256:d8f2afd1aafded7362b397581772c670f20ea84d0a780b93a1a1529da7c3d369", 554 | "sha256:dd4d410a76c3762511ae075d50f379ae09551d92525aa5bb307f8343bf7c2c12", 555 | "sha256:eb60699de43ba1a1f77363f563bb2c652f7748127ba3a774f7cf2c7804aa0d3d", 556 | "sha256:f469f15068cd8351826df4080ffe4cc6377c5bf7d29b5a07b0e717dddb4c7ea2", 557 | "sha256:f82c310ddf97b04e1392c33cf9a70909e0ae10a7e2ddc1d64495e3abdc5d19fb", 558 | "sha256:fa51ce4aea583b0c6b426f4b0563d3535c1c75986c4373a0987d84d22376585b" 559 | ], 560 | "index": "pypi", 561 | "version": "==2.0.19" 562 | }, 563 | "starlette": { 564 | "hashes": [ 565 | "sha256:774f1df1983fd594b9b6fb3ded39c2aa1979d10ac45caac0f4255cbe2acb8628", 566 | "sha256:854c71e73736c429c2bdb07801f2c76c9cba497e7c3cf4988fde5e95fe4cdb3c" 567 | ], 568 | "markers": "python_version >= '3.7'", 569 | "version": "==0.25.0" 570 | }, 571 | "typing-extensions": { 572 | "hashes": [ 573 | "sha256:440d5dd3af93b060174bf433bccd69b0babc3b15b1a8dca43789fd7f61514b36", 574 | "sha256:b75ddc264f0ba5615db7ba217daeb99701ad295353c45f9e95963337ceeeffb2" 575 | ], 576 | "markers": "python_version >= '3.7'", 577 | "version": "==4.7.1" 578 | }, 579 | "uvicorn": { 580 | "hashes": [ 581 | "sha256:0fac9cb342ba099e0d582966005f3fdba5b0290579fed4a6266dc702ca7bb032", 582 | "sha256:e47cac98a6da10cd41e6fd036d472c6f58ede6c5dbee3dbee3ef7a100ed97742" 583 | ], 584 | "index": "pypi", 585 | "version": "==0.21.1" 586 | }, 587 | "websockets": { 588 | "hashes": [ 589 | "sha256:01f5567d9cf6f502d655151645d4e8b72b453413d3819d2b6f1185abc23e82dd", 590 | "sha256:03aae4edc0b1c68498f41a6772d80ac7c1e33c06c6ffa2ac1c27a07653e79d6f", 591 | "sha256:0ac56b661e60edd453585f4bd68eb6a29ae25b5184fd5ba51e97652580458998", 592 | "sha256:0ee68fe502f9031f19d495dae2c268830df2760c0524cbac5d759921ba8c8e82", 593 | "sha256:1553cb82942b2a74dd9b15a018dce645d4e68674de2ca31ff13ebc2d9f283788", 594 | "sha256:1a073fc9ab1c8aff37c99f11f1641e16da517770e31a37265d2755282a5d28aa", 595 | "sha256:1d2256283fa4b7f4c7d7d3e84dc2ece74d341bce57d5b9bf385df109c2a1a82f", 596 | "sha256:1d5023a4b6a5b183dc838808087033ec5df77580485fc533e7dab2567851b0a4", 597 | "sha256:1fdf26fa8a6a592f8f9235285b8affa72748dc12e964a5518c6c5e8f916716f7", 598 | "sha256:2529338a6ff0eb0b50c7be33dc3d0e456381157a31eefc561771ee431134a97f", 599 | "sha256:279e5de4671e79a9ac877427f4ac4ce93751b8823f276b681d04b2156713b9dd", 600 | "sha256:2d903ad4419f5b472de90cd2d40384573b25da71e33519a67797de17ef849b69", 601 | "sha256:332d126167ddddec94597c2365537baf9ff62dfcc9db4266f263d455f2f031cb", 602 | "sha256:34fd59a4ac42dff6d4681d8843217137f6bc85ed29722f2f7222bd619d15e95b", 603 | "sha256:3580dd9c1ad0701169e4d6fc41e878ffe05e6bdcaf3c412f9d559389d0c9e016", 604 | "sha256:3ccc8a0c387629aec40f2fc9fdcb4b9d5431954f934da3eaf16cdc94f67dbfac", 605 | "sha256:41f696ba95cd92dc047e46b41b26dd24518384749ed0d99bea0a941ca87404c4", 606 | "sha256:42cc5452a54a8e46a032521d7365da775823e21bfba2895fb7b77633cce031bb", 607 | "sha256:4841ed00f1026dfbced6fca7d963c4e7043aa832648671b5138008dc5a8f6d99", 608 | "sha256:4b253869ea05a5a073ebfdcb5cb3b0266a57c3764cf6fe114e4cd90f4bfa5f5e", 609 | "sha256:54c6e5b3d3a8936a4ab6870d46bdd6ec500ad62bde9e44462c32d18f1e9a8e54", 610 | "sha256:619d9f06372b3a42bc29d0cd0354c9bb9fb39c2cbc1a9c5025b4538738dbffaf", 611 | "sha256:6505c1b31274723ccaf5f515c1824a4ad2f0d191cec942666b3d0f3aa4cb4007", 612 | "sha256:660e2d9068d2bedc0912af508f30bbeb505bbbf9774d98def45f68278cea20d3", 613 | "sha256:6681ba9e7f8f3b19440921e99efbb40fc89f26cd71bf539e45d8c8a25c976dc6", 614 | "sha256:68b977f21ce443d6d378dbd5ca38621755f2063d6fdb3335bda981d552cfff86", 615 | "sha256:69269f3a0b472e91125b503d3c0b3566bda26da0a3261c49f0027eb6075086d1", 616 | "sha256:6f1a3f10f836fab6ca6efa97bb952300b20ae56b409414ca85bff2ad241d2a61", 617 | "sha256:7622a89d696fc87af8e8d280d9b421db5133ef5b29d3f7a1ce9f1a7bf7fcfa11", 618 | "sha256:777354ee16f02f643a4c7f2b3eff8027a33c9861edc691a2003531f5da4f6bc8", 619 | "sha256:84d27a4832cc1a0ee07cdcf2b0629a8a72db73f4cf6de6f0904f6661227f256f", 620 | "sha256:8531fdcad636d82c517b26a448dcfe62f720e1922b33c81ce695d0edb91eb931", 621 | "sha256:86d2a77fd490ae3ff6fae1c6ceaecad063d3cc2320b44377efdde79880e11526", 622 | "sha256:88fc51d9a26b10fc331be344f1781224a375b78488fc343620184e95a4b27016", 623 | "sha256:8a34e13a62a59c871064dfd8ffb150867e54291e46d4a7cf11d02c94a5275bae", 624 | "sha256:8c82f11964f010053e13daafdc7154ce7385ecc538989a354ccc7067fd7028fd", 625 | "sha256:92b2065d642bf8c0a82d59e59053dd2fdde64d4ed44efe4870fa816c1232647b", 626 | "sha256:97b52894d948d2f6ea480171a27122d77af14ced35f62e5c892ca2fae9344311", 627 | "sha256:9d9acd80072abcc98bd2c86c3c9cd4ac2347b5a5a0cae7ed5c0ee5675f86d9af", 628 | "sha256:9f59a3c656fef341a99e3d63189852be7084c0e54b75734cde571182c087b152", 629 | "sha256:aa5003845cdd21ac0dc6c9bf661c5beddd01116f6eb9eb3c8e272353d45b3288", 630 | "sha256:b16fff62b45eccb9c7abb18e60e7e446998093cdcb50fed33134b9b6878836de", 631 | "sha256:b30c6590146e53149f04e85a6e4fcae068df4289e31e4aee1fdf56a0dead8f97", 632 | "sha256:b58cbf0697721120866820b89f93659abc31c1e876bf20d0b3d03cef14faf84d", 633 | "sha256:b67c6f5e5a401fc56394f191f00f9b3811fe843ee93f4a70df3c389d1adf857d", 634 | "sha256:bceab846bac555aff6427d060f2fcfff71042dba6f5fca7dc4f75cac815e57ca", 635 | "sha256:bee9fcb41db2a23bed96c6b6ead6489702c12334ea20a297aa095ce6d31370d0", 636 | "sha256:c114e8da9b475739dde229fd3bc6b05a6537a88a578358bc8eb29b4030fac9c9", 637 | "sha256:c1f0524f203e3bd35149f12157438f406eff2e4fb30f71221c8a5eceb3617b6b", 638 | "sha256:c792ea4eabc0159535608fc5658a74d1a81020eb35195dd63214dcf07556f67e", 639 | "sha256:c7f3cb904cce8e1be667c7e6fef4516b98d1a6a0635a58a57528d577ac18a128", 640 | "sha256:d67ac60a307f760c6e65dad586f556dde58e683fab03323221a4e530ead6f74d", 641 | "sha256:dcacf2c7a6c3a84e720d1bb2b543c675bf6c40e460300b628bab1b1efc7c034c", 642 | "sha256:de36fe9c02995c7e6ae6efe2e205816f5f00c22fd1fbf343d4d18c3d5ceac2f5", 643 | "sha256:def07915168ac8f7853812cc593c71185a16216e9e4fa886358a17ed0fd9fcf6", 644 | "sha256:df41b9bc27c2c25b486bae7cf42fccdc52ff181c8c387bfd026624a491c2671b", 645 | "sha256:e052b8467dd07d4943936009f46ae5ce7b908ddcac3fda581656b1b19c083d9b", 646 | "sha256:e063b1865974611313a3849d43f2c3f5368093691349cf3c7c8f8f75ad7cb280", 647 | "sha256:e1459677e5d12be8bbc7584c35b992eea142911a6236a3278b9b5ce3326f282c", 648 | "sha256:e1a99a7a71631f0efe727c10edfba09ea6bee4166a6f9c19aafb6c0b5917d09c", 649 | "sha256:e590228200fcfc7e9109509e4d9125eace2042fd52b595dd22bbc34bb282307f", 650 | "sha256:e6316827e3e79b7b8e7d8e3b08f4e331af91a48e794d5d8b099928b6f0b85f20", 651 | "sha256:e7837cb169eca3b3ae94cc5787c4fed99eef74c0ab9506756eea335e0d6f3ed8", 652 | "sha256:e848f46a58b9fcf3d06061d17be388caf70ea5b8cc3466251963c8345e13f7eb", 653 | "sha256:ed058398f55163a79bb9f06a90ef9ccc063b204bb346c4de78efc5d15abfe602", 654 | "sha256:f2e58f2c36cc52d41f2659e4c0cbf7353e28c8c9e63e30d8c6d3494dc9fdedcf", 655 | "sha256:f467ba0050b7de85016b43f5a22b46383ef004c4f672148a8abf32bc999a87f0", 656 | "sha256:f61bdb1df43dc9c131791fbc2355535f9024b9a04398d3bd0684fc16ab07df74", 657 | "sha256:fb06eea71a00a7af0ae6aefbb932fb8a7df3cb390cc217d51a9ad7343de1b8d0", 658 | "sha256:ffd7dcaf744f25f82190856bc26ed81721508fc5cbf2a330751e135ff1283564" 659 | ], 660 | "index": "pypi", 661 | "version": "==11.0.3" 662 | } 663 | }, 664 | "develop": { 665 | "annotated-types": { 666 | "hashes": [ 667 | "sha256:47cdc3490d9ac1506ce92c7aaa76c579dc3509ff11e098fc867e5130ab7be802", 668 | "sha256:58da39888f92c276ad970249761ebea80ba544b77acddaa1a4d6cf78287d45fd" 669 | ], 670 | "markers": "python_version >= '3.7'", 671 | "version": "==0.5.0" 672 | }, 673 | "anyio": { 674 | "hashes": [ 675 | "sha256:44a3c9aba0f5defa43261a8b3efb97891f2bd7d804e0e1f56419befa1adfc780", 676 | "sha256:91dee416e570e92c64041bd18b900d1d6fa78dff7048769ce5ac5ddad004fbb5" 677 | ], 678 | "markers": "python_version >= '3.7'", 679 | "version": "==3.7.1" 680 | }, 681 | "attrs": { 682 | "hashes": [ 683 | "sha256:1f28b4522cdc2fb4256ac1a020c78acf9cba2c6b461ccd2c126f3aa8e8335d04", 684 | "sha256:6279836d581513a26f1bf235f9acd333bc9115683f14f7e8fae46c98fc50e015" 685 | ], 686 | "markers": "python_version >= '3.7'", 687 | "version": "==23.1.0" 688 | }, 689 | "black": { 690 | "hashes": [ 691 | "sha256:01ede61aac8c154b55f35301fac3e730baf0c9cf8120f65a9cd61a81cfb4a0c3", 692 | "sha256:022a582720b0d9480ed82576c920a8c1dde97cc38ff11d8d8859b3bd6ca9eedb", 693 | "sha256:25cc308838fe71f7065df53aedd20327969d05671bac95b38fdf37ebe70ac087", 694 | "sha256:27eb7a0c71604d5de083757fbdb245b1a4fae60e9596514c6ec497eb63f95320", 695 | "sha256:327a8c2550ddc573b51e2c352adb88143464bb9d92c10416feb86b0f5aee5ff6", 696 | "sha256:47e56d83aad53ca140da0af87678fb38e44fd6bc0af71eebab2d1f59b1acf1d3", 697 | "sha256:501387a9edcb75d7ae8a4412bb8749900386eaef258f1aefab18adddea1936bc", 698 | "sha256:552513d5cd5694590d7ef6f46e1767a4df9af168d449ff767b13b084c020e63f", 699 | "sha256:5c4bc552ab52f6c1c506ccae05681fab58c3f72d59ae6e6639e8885e94fe2587", 700 | "sha256:642496b675095d423f9b8448243336f8ec71c9d4d57ec17bf795b67f08132a91", 701 | "sha256:6d1c6022b86f83b632d06f2b02774134def5d4d4f1dac8bef16d90cda18ba28a", 702 | "sha256:7f3bf2dec7d541b4619b8ce526bda74a6b0bffc480a163fed32eb8b3c9aed8ad", 703 | "sha256:831d8f54c3a8c8cf55f64d0422ee875eecac26f5f649fb6c1df65316b67c8926", 704 | "sha256:8417dbd2f57b5701492cd46edcecc4f9208dc75529bcf76c514864e48da867d9", 705 | "sha256:86cee259349b4448adb4ef9b204bb4467aae74a386bce85d56ba4f5dc0da27be", 706 | "sha256:893695a76b140881531062d48476ebe4a48f5d1e9388177e175d76234ca247cd", 707 | "sha256:9fd59d418c60c0348505f2ddf9609c1e1de8e7493eab96198fc89d9f865e7a96", 708 | "sha256:ad0014efc7acf0bd745792bd0d8857413652979200ab924fbf239062adc12491", 709 | "sha256:b5b0ee6d96b345a8b420100b7d71ebfdd19fab5e8301aff48ec270042cd40ac2", 710 | "sha256:c333286dc3ddca6fdff74670b911cccedacb4ef0a60b34e491b8a67c833b343a", 711 | "sha256:f9062af71c59c004cd519e2fb8f5d25d39e46d3af011b41ab43b9c74e27e236f", 712 | "sha256:fb074d8b213749fa1d077d630db0d5f8cc3b2ae63587ad4116e8a436e9bbe995" 713 | ], 714 | "index": "pypi", 715 | "version": "==23.7.0" 716 | }, 717 | "certifi": { 718 | "hashes": [ 719 | "sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082", 720 | "sha256:92d6037539857d8206b8f6ae472e8b77db8058fec5937a1ef3f54304089edbb9" 721 | ], 722 | "markers": "python_version >= '3.6'", 723 | "version": "==2023.7.22" 724 | }, 725 | "cfgv": { 726 | "hashes": [ 727 | "sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426", 728 | "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736" 729 | ], 730 | "markers": "python_full_version >= '3.6.1'", 731 | "version": "==3.3.1" 732 | }, 733 | "click": { 734 | "hashes": [ 735 | "sha256:48ee849951919527a045bfe3bf7baa8a959c423134e1a5b98c05c20ba75a1cbd", 736 | "sha256:fa244bb30b3b5ee2cae3da8f55c9e5e0c0e86093306301fb418eb9dc40fbded5" 737 | ], 738 | "markers": "python_version >= '3.7'", 739 | "version": "==8.1.6" 740 | }, 741 | "coverage": { 742 | "extras": [ 743 | "toml" 744 | ], 745 | "hashes": [ 746 | "sha256:06a9a2be0b5b576c3f18f1a241f0473575c4a26021b52b2a85263a00f034d51f", 747 | "sha256:06fb182e69f33f6cd1d39a6c597294cff3143554b64b9825d1dc69d18cc2fff2", 748 | "sha256:0a5f9e1dbd7fbe30196578ca36f3fba75376fb99888c395c5880b355e2875f8a", 749 | "sha256:0e1f928eaf5469c11e886fe0885ad2bf1ec606434e79842a879277895a50942a", 750 | "sha256:171717c7cb6b453aebac9a2ef603699da237f341b38eebfee9be75d27dc38e01", 751 | "sha256:1e9d683426464e4a252bf70c3498756055016f99ddaec3774bf368e76bbe02b6", 752 | "sha256:201e7389591af40950a6480bd9edfa8ed04346ff80002cec1a66cac4549c1ad7", 753 | "sha256:245167dd26180ab4c91d5e1496a30be4cd721a5cf2abf52974f965f10f11419f", 754 | "sha256:2aee274c46590717f38ae5e4650988d1af340fe06167546cc32fe2f58ed05b02", 755 | "sha256:2e07b54284e381531c87f785f613b833569c14ecacdcb85d56b25c4622c16c3c", 756 | "sha256:31563e97dae5598556600466ad9beea39fb04e0229e61c12eaa206e0aa202063", 757 | "sha256:33d6d3ea29d5b3a1a632b3c4e4f4ecae24ef170b0b9ee493883f2df10039959a", 758 | "sha256:3d376df58cc111dc8e21e3b6e24606b5bb5dee6024f46a5abca99124b2229ef5", 759 | "sha256:419bfd2caae268623dd469eff96d510a920c90928b60f2073d79f8fe2bbc5959", 760 | "sha256:48c19d2159d433ccc99e729ceae7d5293fbffa0bdb94952d3579983d1c8c9d97", 761 | "sha256:49969a9f7ffa086d973d91cec8d2e31080436ef0fb4a359cae927e742abfaaa6", 762 | "sha256:52edc1a60c0d34afa421c9c37078817b2e67a392cab17d97283b64c5833f427f", 763 | "sha256:537891ae8ce59ef63d0123f7ac9e2ae0fc8b72c7ccbe5296fec45fd68967b6c9", 764 | "sha256:54b896376ab563bd38453cecb813c295cf347cf5906e8b41d340b0321a5433e5", 765 | "sha256:58c2ccc2f00ecb51253cbe5d8d7122a34590fac9646a960d1430d5b15321d95f", 766 | "sha256:5b7540161790b2f28143191f5f8ec02fb132660ff175b7747b95dcb77ac26562", 767 | "sha256:5baa06420f837184130752b7c5ea0808762083bf3487b5038d68b012e5937dbe", 768 | "sha256:5e330fc79bd7207e46c7d7fd2bb4af2963f5f635703925543a70b99574b0fea9", 769 | "sha256:61b9a528fb348373c433e8966535074b802c7a5d7f23c4f421e6c6e2f1697a6f", 770 | "sha256:63426706118b7f5cf6bb6c895dc215d8a418d5952544042c8a2d9fe87fcf09cb", 771 | "sha256:6d040ef7c9859bb11dfeb056ff5b3872436e3b5e401817d87a31e1750b9ae2fb", 772 | "sha256:6f48351d66575f535669306aa7d6d6f71bc43372473b54a832222803eb956fd1", 773 | "sha256:7ee7d9d4822c8acc74a5e26c50604dff824710bc8de424904c0982e25c39c6cb", 774 | "sha256:81c13a1fc7468c40f13420732805a4c38a105d89848b7c10af65a90beff25250", 775 | "sha256:8d13c64ee2d33eccf7437961b6ea7ad8673e2be040b4f7fd4fd4d4d28d9ccb1e", 776 | "sha256:8de8bb0e5ad103888d65abef8bca41ab93721647590a3f740100cd65c3b00511", 777 | "sha256:8fa03bce9bfbeeef9f3b160a8bed39a221d82308b4152b27d82d8daa7041fee5", 778 | "sha256:924d94291ca674905fe9481f12294eb11f2d3d3fd1adb20314ba89e94f44ed59", 779 | "sha256:975d70ab7e3c80a3fe86001d8751f6778905ec723f5b110aed1e450da9d4b7f2", 780 | "sha256:976b9c42fb2a43ebf304fa7d4a310e5f16cc99992f33eced91ef6f908bd8f33d", 781 | "sha256:9e31cb64d7de6b6f09702bb27c02d1904b3aebfca610c12772452c4e6c21a0d3", 782 | "sha256:a342242fe22407f3c17f4b499276a02b01e80f861f1682ad1d95b04018e0c0d4", 783 | "sha256:a3d33a6b3eae87ceaefa91ffdc130b5e8536182cd6dfdbfc1aa56b46ff8c86de", 784 | "sha256:a895fcc7b15c3fc72beb43cdcbdf0ddb7d2ebc959edac9cef390b0d14f39f8a9", 785 | "sha256:afb17f84d56068a7c29f5fa37bfd38d5aba69e3304af08ee94da8ed5b0865833", 786 | "sha256:b1c546aca0ca4d028901d825015dc8e4d56aac4b541877690eb76490f1dc8ed0", 787 | "sha256:b29019c76039dc3c0fd815c41392a044ce555d9bcdd38b0fb60fb4cd8e475ba9", 788 | "sha256:b46517c02ccd08092f4fa99f24c3b83d8f92f739b4657b0f146246a0ca6a831d", 789 | "sha256:b7aa5f8a41217360e600da646004f878250a0d6738bcdc11a0a39928d7dc2050", 790 | "sha256:b7b4c971f05e6ae490fef852c218b0e79d4e52f79ef0c8475566584a8fb3e01d", 791 | "sha256:ba90a9563ba44a72fda2e85302c3abc71c5589cea608ca16c22b9804262aaeb6", 792 | "sha256:cb017fd1b2603ef59e374ba2063f593abe0fc45f2ad9abdde5b4d83bd922a353", 793 | "sha256:d22656368f0e6189e24722214ed8d66b8022db19d182927b9a248a2a8a2f67eb", 794 | "sha256:d2c2db7fd82e9b72937969bceac4d6ca89660db0a0967614ce2481e81a0b771e", 795 | "sha256:d39b5b4f2a66ccae8b7263ac3c8170994b65266797fb96cbbfd3fb5b23921db8", 796 | "sha256:d62a5c7dad11015c66fbb9d881bc4caa5b12f16292f857842d9d1871595f4495", 797 | "sha256:e7d9405291c6928619403db1d10bd07888888ec1abcbd9748fdaa971d7d661b2", 798 | "sha256:e84606b74eb7de6ff581a7915e2dab7a28a0517fbe1c9239eb227e1354064dcd", 799 | "sha256:eb393e5ebc85245347950143969b241d08b52b88a3dc39479822e073a1a8eb27", 800 | "sha256:ebba1cd308ef115925421d3e6a586e655ca5a77b5bf41e02eb0e4562a111f2d1", 801 | "sha256:ee57190f24fba796e36bb6d3aa8a8783c643d8fa9760c89f7a98ab5455fbf818", 802 | "sha256:f2f67fe12b22cd130d34d0ef79206061bfb5eda52feb6ce0dba0644e20a03cf4", 803 | "sha256:f6951407391b639504e3b3be51b7ba5f3528adbf1a8ac3302b687ecababf929e", 804 | "sha256:f75f7168ab25dd93110c8a8117a22450c19976afbc44234cbf71481094c1b850", 805 | "sha256:fdec9e8cbf13a5bf63290fc6013d216a4c7232efb51548594ca3631a7f13c3a3" 806 | ], 807 | "markers": "python_version >= '3.7'", 808 | "version": "==7.2.7" 809 | }, 810 | "distlib": { 811 | "hashes": [ 812 | "sha256:2e24928bc811348f0feb63014e97aaae3037f2cf48712d51ae61df7fd6075057", 813 | "sha256:9dafe54b34a028eafd95039d5e5d4851a13734540f1331060d31c9916e7147a8" 814 | ], 815 | "version": "==0.3.7" 816 | }, 817 | "faker": { 818 | "hashes": [ 819 | "sha256:78840b94843f3aa32a34a220b2b5e8b309e3ffff3a231b0c54e841bb68e0757d", 820 | "sha256:c6c1218482faf79ae1d791bb7124067d12339e0b8f400de855e2c281bcf78c77" 821 | ], 822 | "markers": "python_version >= '3.8'", 823 | "version": "==19.2.0" 824 | }, 825 | "filelock": { 826 | "hashes": [ 827 | "sha256:002740518d8aa59a26b0c76e10fb8c6e15eae825d34b6fdf670333fd7b938d81", 828 | "sha256:cbb791cdea2a72f23da6ac5b5269ab0a0d161e9ef0100e653b69049a7706d1ec" 829 | ], 830 | "markers": "python_version >= '3.7'", 831 | "version": "==3.12.2" 832 | }, 833 | "h11": { 834 | "hashes": [ 835 | "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d", 836 | "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761" 837 | ], 838 | "markers": "python_version >= '3.7'", 839 | "version": "==0.14.0" 840 | }, 841 | "httpcore": { 842 | "hashes": [ 843 | "sha256:a6f30213335e34c1ade7be6ec7c47f19f50c56db36abef1a9dfa3815b1cb3888", 844 | "sha256:c2789b767ddddfa2a5782e3199b2b7f6894540b17b16ec26b2c4d8e103510b87" 845 | ], 846 | "markers": "python_version >= '3.7'", 847 | "version": "==0.17.3" 848 | }, 849 | "httpx": { 850 | "hashes": [ 851 | "sha256:06781eb9ac53cde990577af654bd990a4949de37a28bdb4a230d434f3a30b9bd", 852 | "sha256:5853a43053df830c20f8110c5e69fe44d035d850b2dfe795e196f00fdb774bdd" 853 | ], 854 | "index": "pypi", 855 | "version": "==0.24.1" 856 | }, 857 | "hypothesis": { 858 | "hashes": [ 859 | "sha256:fa8eee429b99f7d3c953fb2b57de415fd39b472b09328b86c1978f12669ef395", 860 | "sha256:ffece8e40a34329e7112f7408f2c45fe587761978fdbc6f4f91bf0d683a7d4d9" 861 | ], 862 | "index": "pypi", 863 | "version": "==6.82.0" 864 | }, 865 | "identify": { 866 | "hashes": [ 867 | "sha256:7243800bce2f58404ed41b7c002e53d4d22bcf3ae1b7900c2d7aefd95394bf7f", 868 | "sha256:c22a8ead0d4ca11f1edd6c9418c3220669b3b7533ada0a0ffa6cc0ef85cf9b54" 869 | ], 870 | "markers": "python_version >= '3.8'", 871 | "version": "==2.5.26" 872 | }, 873 | "idna": { 874 | "hashes": [ 875 | "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4", 876 | "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2" 877 | ], 878 | "markers": "python_version >= '3.5'", 879 | "version": "==3.4" 880 | }, 881 | "iniconfig": { 882 | "hashes": [ 883 | "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", 884 | "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374" 885 | ], 886 | "markers": "python_version >= '3.7'", 887 | "version": "==2.0.0" 888 | }, 889 | "isort": { 890 | "hashes": [ 891 | "sha256:8bef7dde241278824a6d83f44a544709b065191b95b6e50894bdc722fcba0504", 892 | "sha256:f84c2818376e66cf843d497486ea8fed8700b340f308f076c6fb1229dff318b6" 893 | ], 894 | "index": "pypi", 895 | "version": "==5.12.0" 896 | }, 897 | "mypy": { 898 | "hashes": [ 899 | "sha256:01fd2e9f85622d981fd9063bfaef1aed6e336eaacca00892cd2d82801ab7c042", 900 | "sha256:0dde1d180cd84f0624c5dcaaa89c89775550a675aff96b5848de78fb11adabcd", 901 | "sha256:141dedfdbfe8a04142881ff30ce6e6653c9685b354876b12e4fe6c78598b45e2", 902 | "sha256:16f0db5b641ba159eff72cff08edc3875f2b62b2fa2bc24f68c1e7a4e8232d01", 903 | "sha256:190b6bab0302cec4e9e6767d3eb66085aef2a1cc98fe04936d8a42ed2ba77bb7", 904 | "sha256:2460a58faeea905aeb1b9b36f5065f2dc9a9c6e4c992a6499a2360c6c74ceca3", 905 | "sha256:34a9239d5b3502c17f07fd7c0b2ae6b7dd7d7f6af35fbb5072c6208e76295816", 906 | "sha256:43b592511672017f5b1a483527fd2684347fdffc041c9ef53428c8dc530f79a3", 907 | "sha256:43d24f6437925ce50139a310a64b2ab048cb2d3694c84c71c3f2a1626d8101dc", 908 | "sha256:45d32cec14e7b97af848bddd97d85ea4f0db4d5a149ed9676caa4eb2f7402bb4", 909 | "sha256:470c969bb3f9a9efcedbadcd19a74ffb34a25f8e6b0e02dae7c0e71f8372f97b", 910 | "sha256:566e72b0cd6598503e48ea610e0052d1b8168e60a46e0bfd34b3acf2d57f96a8", 911 | "sha256:5703097c4936bbb9e9bce41478c8d08edd2865e177dc4c52be759f81ee4dd26c", 912 | "sha256:7549fbf655e5825d787bbc9ecf6028731973f78088fbca3a1f4145c39ef09462", 913 | "sha256:8207b7105829eca6f3d774f64a904190bb2231de91b8b186d21ffd98005f14a7", 914 | "sha256:8c4d8e89aa7de683e2056a581ce63c46a0c41e31bd2b6d34144e2c80f5ea53dc", 915 | "sha256:98324ec3ecf12296e6422939e54763faedbfcc502ea4a4c38502082711867258", 916 | "sha256:9bbcd9ab8ea1f2e1c8031c21445b511442cc45c89951e49bbf852cbb70755b1b", 917 | "sha256:9d40652cc4fe33871ad3338581dca3297ff5f2213d0df345bcfbde5162abf0c9", 918 | "sha256:a2746d69a8196698146a3dbe29104f9eb6a2a4d8a27878d92169a6c0b74435b6", 919 | "sha256:ae704dcfaa180ff7c4cfbad23e74321a2b774f92ca77fd94ce1049175a21c97f", 920 | "sha256:bfdca17c36ae01a21274a3c387a63aa1aafe72bff976522886869ef131b937f1", 921 | "sha256:c482e1246726616088532b5e964e39765b6d1520791348e6c9dc3af25b233828", 922 | "sha256:ca637024ca67ab24a7fd6f65d280572c3794665eaf5edcc7e90a866544076878", 923 | "sha256:e02d700ec8d9b1859790c0475df4e4092c7bf3272a4fd2c9f33d87fac4427b8f", 924 | "sha256:e5952d2d18b79f7dc25e62e014fe5a23eb1a3d2bc66318df8988a01b1a037c5b" 925 | ], 926 | "version": "==1.4.1" 927 | }, 928 | "mypy-extensions": { 929 | "hashes": [ 930 | "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", 931 | "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782" 932 | ], 933 | "markers": "python_version >= '3.5'", 934 | "version": "==1.0.0" 935 | }, 936 | "nodeenv": { 937 | "hashes": [ 938 | "sha256:d51e0c37e64fbf47d017feac3145cdbb58836d7eee8c6f6d3b6880c5456227d2", 939 | "sha256:df865724bb3c3adc86b3876fa209771517b0cfe596beff01a92700e0e8be4cec" 940 | ], 941 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5, 3.6'", 942 | "version": "==1.8.0" 943 | }, 944 | "packaging": { 945 | "hashes": [ 946 | "sha256:994793af429502c4ea2ebf6bf664629d07c1a9fe974af92966e4b8d2df7edc61", 947 | "sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f" 948 | ], 949 | "markers": "python_version >= '3.7'", 950 | "version": "==23.1" 951 | }, 952 | "pathspec": { 953 | "hashes": [ 954 | "sha256:2798de800fa92780e33acca925945e9a19a133b715067cf165b8866c15a31687", 955 | "sha256:d8af70af76652554bd134c22b3e8a1cc46ed7d91edcdd721ef1a0c51a84a5293" 956 | ], 957 | "markers": "python_version >= '3.7'", 958 | "version": "==0.11.1" 959 | }, 960 | "platformdirs": { 961 | "hashes": [ 962 | "sha256:1b42b450ad933e981d56e59f1b97495428c9bd60698baab9f3eb3d00d5822421", 963 | "sha256:ad8291ae0ae5072f66c16945166cb11c63394c7a3ad1b1bc9828ca3162da8c2f" 964 | ], 965 | "markers": "python_version >= '3.7'", 966 | "version": "==3.9.1" 967 | }, 968 | "pluggy": { 969 | "hashes": [ 970 | "sha256:c2fd55a7d7a3863cba1a013e4e2414658b1d07b6bc57b3919e0c63c9abb99849", 971 | "sha256:d12f0c4b579b15f5e054301bb226ee85eeeba08ffec228092f8defbaa3a4c4b3" 972 | ], 973 | "markers": "python_version >= '3.7'", 974 | "version": "==1.2.0" 975 | }, 976 | "pre-commit": { 977 | "hashes": [ 978 | "sha256:10badb65d6a38caff29703362271d7dca483d01da88f9d7e05d0b97171c136cb", 979 | "sha256:a2256f489cd913d575c145132ae196fe335da32d91a8294b7afe6622335dd023" 980 | ], 981 | "index": "pypi", 982 | "version": "==3.3.3" 983 | }, 984 | "pydantic": { 985 | "extras": [ 986 | "dotenv" 987 | ], 988 | "hashes": [ 989 | "sha256:008c5e266c8aada206d0627a011504e14268a62091450210eda7c07fabe6963e", 990 | "sha256:0588788a9a85f3e5e9ebca14211a496409cb3deca5b6971ff37c556d581854e7", 991 | "sha256:08a6c32e1c3809fbc49debb96bf833164f3438b3696abf0fbeceb417d123e6eb", 992 | "sha256:16928fdc9cb273c6af00d9d5045434c39afba5f42325fb990add2c241402d151", 993 | "sha256:174899023337b9fc685ac8adaa7b047050616136ccd30e9070627c1aaab53a13", 994 | "sha256:192c608ad002a748e4a0bed2ddbcd98f9b56df50a7c24d9a931a8c5dd053bd3d", 995 | "sha256:1954f8778489a04b245a1e7b8b22a9d3ea8ef49337285693cf6959e4b757535e", 996 | "sha256:2417de68290434461a266271fc57274a138510dca19982336639484c73a07af6", 997 | "sha256:265a60da42f9f27e0b1014eab8acd3e53bd0bad5c5b4884e98a55f8f596b2c19", 998 | "sha256:331c031ba1554b974c98679bd0780d89670d6fd6f53f5d70b10bdc9addee1713", 999 | "sha256:373c0840f5c2b5b1ccadd9286782852b901055998136287828731868027a724f", 1000 | "sha256:3f34739a89260dfa420aa3cbd069fbcc794b25bbe5c0a214f8fb29e363484b66", 1001 | "sha256:41e0bb6efe86281623abbeeb0be64eab740c865388ee934cd3e6a358784aca6e", 1002 | "sha256:4400015f15c9b464c9db2d5d951b6a780102cfa5870f2c036d37c23b56f7fc1b", 1003 | "sha256:44e51ba599c3ef227e168424e220cd3e544288c57829520dc90ea9cb190c3248", 1004 | "sha256:469adf96c8e2c2bbfa655fc7735a2a82f4c543d9fee97bd113a7fb509bf5e622", 1005 | "sha256:5b02d24f7b2b365fed586ed73582c20f353a4c50e4be9ba2c57ab96f8091ddae", 1006 | "sha256:7522a7666157aa22b812ce14c827574ddccc94f361237ca6ea8bb0d5c38f1629", 1007 | "sha256:787cf23e5a0cde753f2eabac1b2e73ae3844eb873fd1f5bdbff3048d8dbb7604", 1008 | "sha256:8268a735a14c308923e8958363e3a3404f6834bb98c11f5ab43251a4e410170c", 1009 | "sha256:8dc77064471780262b6a68fe67e013298d130414d5aaf9b562c33987dbd2cf4f", 1010 | "sha256:a451ccab49971af043ec4e0d207cbc8cbe53dbf148ef9f19599024076fe9c25b", 1011 | "sha256:a6c098d4ab5e2d5b3984d3cb2527e2d6099d3de85630c8934efcfdc348a9760e", 1012 | "sha256:abade85268cc92dff86d6effcd917893130f0ff516f3d637f50dadc22ae93999", 1013 | "sha256:bc64eab9b19cd794a380179ac0e6752335e9555d214cfcb755820333c0784cb3", 1014 | "sha256:c3339a46bbe6013ef7bdd2844679bfe500347ac5742cd4019a88312aa58a9847", 1015 | "sha256:d185819a7a059550ecb85d5134e7d40f2565f3dd94cfd870132c5f91a89cf58c", 1016 | "sha256:d7781f1d13b19700b7949c5a639c764a077cbbdd4322ed505b449d3ca8edcb36", 1017 | "sha256:e297897eb4bebde985f72a46a7552a7556a3dd11e7f76acda0c1093e3dbcf216", 1018 | "sha256:e6cbfbd010b14c8a905a7b10f9fe090068d1744d46f9e0c021db28daeb8b6de1", 1019 | "sha256:e9738b0f2e6c70f44ee0de53f2089d6002b10c33264abee07bdb5c7f03038303", 1020 | "sha256:e9baf78b31da2dc3d3f346ef18e58ec5f12f5aaa17ac517e2ffd026a92a87588", 1021 | "sha256:ef55392ec4bb5721f4ded1096241e4b7151ba6d50a50a80a2526c854f42e6a2f", 1022 | "sha256:f66d479cf7eb331372c470614be6511eae96f1f120344c25f3f9bb59fb1b5528", 1023 | "sha256:fe429898f2c9dd209bd0632a606bddc06f8bce081bbd03d1c775a45886e2c1cb", 1024 | "sha256:ff44c5e89315b15ff1f7fdaf9853770b810936d6b01a7bcecaa227d2f8fe444f" 1025 | ], 1026 | "index": "pypi", 1027 | "version": "==1.10.11" 1028 | }, 1029 | "pydantic-core": { 1030 | "hashes": [ 1031 | "sha256:019c5c41941438570dfc7d3f0ae389b2425add1775a357ce1e83ed1434f943d6", 1032 | "sha256:01f56d5ee70b1d39c0fd08372cc5142274070ab7181d17c86035f130eebc05b8", 1033 | "sha256:055f7ea6b1fbb37880d66d70eefd22dd319b09c79d2cb99b1dbfeb34b653b0b2", 1034 | "sha256:05b4bf8c58409586a7a04c858a86ab10f28c6c1a7c33da65e0326c59d5b0ab16", 1035 | "sha256:06884c07956526ac9ebfef40fe21a11605569b8fc0e2054a375fb39c978bf48f", 1036 | "sha256:06f33f695527f5a86e090f208978f9fd252c9cfc7e869d3b679bd71f7cb2c1fa", 1037 | "sha256:0aa429578e23885b3984c49d687cd05ab06f0b908ea1711a8bf7e503b7f97160", 1038 | "sha256:0b3d781c71b8bfb621ef23b9c874933e2cd33237c1a65cc20eeb37437f8e7e18", 1039 | "sha256:0dc5f516b24d24bc9e8dd9305460899f38302b3c4f9752663b396ef9848557bf", 1040 | "sha256:0fc7e0b056b66cc536e97ef60f48b3b289f6b3b62ac225afd4b22a42434617bf", 1041 | "sha256:12be3b5f54f8111ca38e6b7277f26c23ba5cb3344fae06f879a0a93dfc8b479e", 1042 | "sha256:1624baa76d1740711b2048f302ae9a6d73d277c55a8c3e88b53b773ebf73a971", 1043 | "sha256:1aefebb506bc1fe355d91d25f12bcdea7f4d7c2d9f0f6716dd025543777c99a5", 1044 | "sha256:1bcfb7be905aa849bd882262e1df3f75b564e2f708b4b4c7ad2d3deaf5410562", 1045 | "sha256:1c119e9227487ad3d7c3c737d896afe548a6be554091f9745da1f4b489c40561", 1046 | "sha256:20d710c1f79af930b8891bcebd84096798e4387ab64023ef41521d58f21277d3", 1047 | "sha256:2183a9e18cdc0de53bdaa1675f237259162abeb62d6ac9e527c359c1074dc55d", 1048 | "sha256:27babb9879bf2c45ed655d02639f4c30e2b9ef1b71ce59c2305bbf7287910a18", 1049 | "sha256:27c1bbfb9d84a75cf33b7f19b53c29eb7ead99b235fce52aced5507174ab8f98", 1050 | "sha256:2b79f3681481f4424d7845cc7a261d5a4baa810d656b631fa844dc9967b36a7b", 1051 | "sha256:2f10aa5452b865818dd0137f568d443f5e93b60a27080a01aa4b7512c7ba13a3", 1052 | "sha256:309f45d4d7481d6f09cb9e35c72caa0e50add4a30bb08c04c5fe5956a0158633", 1053 | "sha256:31acc37288b8e69e4849f618c3d5cf13b58077c1a1ff9ade0b3065ba974cd385", 1054 | "sha256:37c5028cebdf731298724070838fb3a71ef1fbd201d193d311ac2cbdbca25a23", 1055 | "sha256:38a0e7ee65c8999394d92d9c724434cb629279d19844f2b69d9bbc46dc8b8b61", 1056 | "sha256:39aa09ed7ce2a648c904f79032d16dda29e6913112af8465a7bf710eef23c7ca", 1057 | "sha256:3cd7ee8bbfab277ab56e272221886fd33a1b5943fbf45ae9195aa6a48715a8a0", 1058 | "sha256:3d642e5c029e2acfacf6aa0a7a3e822086b3b777c70d364742561f9ca64c1ffc", 1059 | "sha256:41bbc2678a5b6a19371b2cb51f30ccea71f0c14b26477d2d884fed761cea42c7", 1060 | "sha256:45327fc57afbe3f2c3d7f54a335d5cecee8a9fdb3906a2fbed8af4092f4926df", 1061 | "sha256:4542c98b8364b976593703a2dda97377433b102f380b61bc3a2cbc2fbdae1d1f", 1062 | "sha256:45fa1e8ad6f4367ad73674ca560da8e827cc890eaf371f3ee063d6d7366a207b", 1063 | "sha256:4638ebc17de08c2f3acba557efeb6f195c88b7299d8c55c0bb4e20638bbd4d03", 1064 | "sha256:464bf799b422be662e5e562e62beeffc9eaa907d381a9d63a2556615bbda286d", 1065 | "sha256:4788135db4bd83a5edc3522b11544b013be7d25b74b155e08dd3b20cd6663bbb", 1066 | "sha256:47e8f034be31390a8f525431eb5e803a78ce7e2e11b32abf5361a972e14e6b61", 1067 | "sha256:4824eb018f0a4680b1e434697a9bf3f41c7799b80076d06530cbbd212e040ccc", 1068 | "sha256:4bf20c9722821fce766e685718e739deeccc60d6bc7be5029281db41f999ee0c", 1069 | "sha256:4d3097c39d7d4e8dba2ef86de171dcccad876c36d8379415ba18a5a4d0533510", 1070 | "sha256:4d889d498fce64bfcd8adf1a78579a7f626f825cbeb2956a24a29b35f9a1df32", 1071 | "sha256:4d965c7c4b40d1cedec9188782e98bd576f9a04868835604200c3a6e817b824f", 1072 | "sha256:4e26944e64ecc1d7b19db954c0f7b471f3b141ec8e1a9f57cfe27671525cd248", 1073 | "sha256:534f3f63c000f08050c6f7f4378bf2b52d7ba9214e9d35e3f60f7ad24a4d6425", 1074 | "sha256:539432f911686cb80284c30b33eaf9f4fd9a11e1111fe0dc98fdbdce69b49821", 1075 | "sha256:5af2d43b1978958d91351afbcc9b4d0cfe144c46c61740e82aaac8bb39ab1a4d", 1076 | "sha256:5cfb5ac4e82c47d5dc25b209dd4c3989e284b80109f9e08b33c895080c424b4f", 1077 | "sha256:616b3451b05ca63b8f433c627f68046b39543faeaa4e50d8c6699a2a1e4b85a5", 1078 | "sha256:6441a29f42585f085db0c04cd0557d4cbbb46fa68a0972409b1cfe9f430280c1", 1079 | "sha256:64bfd2c35a2c350f73ac52dc134d8775f93359c4c969280a6fe5301b5b6e7431", 1080 | "sha256:6ca34c29fbd6592de5fd39e80c1993634d704c4e7e14ba54c87b2c7c53da68fe", 1081 | "sha256:73929a2fb600a2333fce2efd92596cff5e6bf8946e20e93c067b220760064862", 1082 | "sha256:73f62bb7fd862d9bcd886e10612bade6fe042eda8b47e8c129892bcfb7b45e84", 1083 | "sha256:7584171eb3115acd4aba699bc836634783f5bd5aab131e88d8eeb8a3328a4a72", 1084 | "sha256:78b1ac0151271ce62bc2b33755f1043eda6a310373143a2f27e2bcd3d5fc8633", 1085 | "sha256:7cb496e934b71f1ade844ab91d6ccac78a3520e5df02fdb2357f85a71e541e69", 1086 | "sha256:7d55e38a89ec2ae17b2fa7ffeda6b70f63afab1888bd0d57aaa7b7879760acb4", 1087 | "sha256:7ecf0a67b212900e92f328181fed02840d74ed39553cdb38d27314e2b9c89dfa", 1088 | "sha256:85cd9c0af34e371390e3cb2f3a470b0b40cc07568c1e966c638c49062be6352d", 1089 | "sha256:8ba3073eb38a1294e8c7902989fb80a7a147a69db2396818722bd078476586a0", 1090 | "sha256:8d0dbcc57839831ae79fd24b1b83d42bc9448d79feaf3ed3fb5cbf94ffbf3eb7", 1091 | "sha256:9342de50824b40f55d2600f66c6f9a91a3a24851eca39145a749a3dc804ee599", 1092 | "sha256:937c0fe9538f1212b62df6a68f8d78df3572fe3682d9a0dd8851eac8a4e46063", 1093 | "sha256:9eff3837d447fccf2ac38c259b14ab9cbde700df355a45a1f3ff244d5e78f8b6", 1094 | "sha256:9ff322c7e1030543d35d83bb521b69114d3d150750528d7757544f639def9ad6", 1095 | "sha256:a3e9a18401a28db4358da2e191508702dbf065f2664c710708cdf9552b9fa50c", 1096 | "sha256:a439fd0d45d51245bbde799726adda5bd18aed3fa2b01ab2e6a64d6d13776fa3", 1097 | "sha256:a666134b41712e30a71afaa26deeb4da374179f769fa49784cdf0e7698880fab", 1098 | "sha256:ad442b8585ed4a3c2d22e4bf7b465d9b7d281e055b09719a8aeb5b576422dc9b", 1099 | "sha256:ad46027dbd5c1db87dc0b49becbe23093b143a20302028d387dae37ee5ef95f5", 1100 | "sha256:ad814864aba263be9c83ada44a95f72d10caabbf91589321f95c29c902bdcff0", 1101 | "sha256:adcb9c8848e15c613e483e0b99767ae325af27fe0dbd866df01fe5849d06e6e1", 1102 | "sha256:af693a89db6d6ac97dd84dd7769b3f2bd9007b578127d0e7dda03053f4d3b34b", 1103 | "sha256:afa8808159169368b66e4fbeafac6c6fd8f26246dc4d0dcc2caf94bd9cf1b828", 1104 | "sha256:ba2b807d2b62c446120906b8580cddae1d76d3de4efbb95ccc87f5e35c75b4b2", 1105 | "sha256:ba6a8cf089222a171b8f84e6ec2d10f7a9d14f26be3a347b14775a8741810676", 1106 | "sha256:bf3ed993bdf4754909f175ff348cf8f78d4451215b8aa338633f149ca3b1f37a", 1107 | "sha256:bf6a1d2c920cc9528e884850a4b2ee7629e3d362d5c44c66526d4097bbb07a1a", 1108 | "sha256:c089d8e7f1b4db08b2f8e4107304eec338df046275dad432635a9be9531e2fc8", 1109 | "sha256:c24465dd11b65c8510f251b095fc788c7c91481c81840112fe3f76c30793a455", 1110 | "sha256:cb08fab0fc1db15c277b72e33ac74ad9c0c789413da8984a3eacb22a94b42ef4", 1111 | "sha256:cd782807d35c8a41aaa7d30b5107784420eefd9fdc1c760d86007d43ae00b15d", 1112 | "sha256:d5146a6749b1905e04e62e0ad4622f079e5582f8b3abef5fb64516c623127908", 1113 | "sha256:dcbff997f47d45bf028bda4c3036bb3101e89a3df271281d392b6175f71c71d1", 1114 | "sha256:dd3b023f3317dbbbc775e43651ce1a31a9cea46216ad0b5be37afc18a2007699", 1115 | "sha256:deeb64335f489c3c11949cbd1d1668b3f1fb2d1c6a5bf40e126ef7bf95f9fa40", 1116 | "sha256:e09d9f6d722de9d4c1c5f122ea9bc6b25a05f975457805af4dcab7b0128aacbf", 1117 | "sha256:e33fcbea3b63a339dd94de0fc442fefacfe681cc7027ce63f67af9f7ceec7422", 1118 | "sha256:e3ed6834cc005798187a56c248a2240207cb8ffdda1c89e9afda4c3d526c2ea0", 1119 | "sha256:e4208f23f12d0ad206a07a489ef4cb15722c10b62774c4460ee4123250be938e", 1120 | "sha256:e427b66596a6441a5607dfc0085b47d36073f88da7ac48afd284263b9b99e6ce", 1121 | "sha256:e72ac299a6bf732a60852d052acf3999d234686755a02ba111e85e7ebf8155b1", 1122 | "sha256:ea955e4ed21f4bbb9b83fea09fc6af0bed82e69ecf6b35ec89237a0a49633033", 1123 | "sha256:ed5babdcd3d052ba5cf8832561f18df20778c7ccf12587b2d82f7bf3bf259a0e", 1124 | "sha256:eda1a89c4526826c0a87d33596a4cd15b8f58e9250f503e39af1699ba9c878e8", 1125 | "sha256:ef1fd1b24e9bcddcb168437686677104e205c8e25b066e73ffdf331d3bb8792b", 1126 | "sha256:ef6a222d54f742c24f6b143aab088702db3a827b224e75b9dd28b38597c595fe", 1127 | "sha256:f3dd5333049b5b3faa739e0f40b77cc8b7a1aded2f2da0e28794c81586d7b08a", 1128 | "sha256:f60e31e3e15e8c294bf70c60f8ae4d0c3caf3af8f26466e9aa8ea4c01302749b", 1129 | "sha256:f642313d559f9d9a00c4de6820124059cc3342a0d0127b18301de2c680d5ea40", 1130 | "sha256:f868e731a18b403b88aa434d960489ceeed0ddeb44ebc02389540731a67705e0", 1131 | "sha256:f93c867e5e85584a28c6a6feb6f2086d717266eb5d1210d096dd717b7f4dec04" 1132 | ], 1133 | "markers": "python_version >= '3.7'", 1134 | "version": "==2.3.0" 1135 | }, 1136 | "pydantic-factories": { 1137 | "hashes": [ 1138 | "sha256:5a1522a31d27e1af414719c510a4a934365292f3ea6fdc843ed65d0564242636", 1139 | "sha256:de36e0db7108af5f4328308da9a4049311c4d5e0814553d2f39078b08b05e48d" 1140 | ], 1141 | "index": "pypi", 1142 | "version": "==1.17.3" 1143 | }, 1144 | "pytest": { 1145 | "hashes": [ 1146 | "sha256:78bf16451a2eb8c7a2ea98e32dc119fd2aa758f1d5d66dbf0a59d69a3969df32", 1147 | "sha256:b4bf8c45bd59934ed84001ad51e11b4ee40d40a1229d2c79f9c592b0a3f6bd8a" 1148 | ], 1149 | "index": "pypi", 1150 | "version": "==7.4.0" 1151 | }, 1152 | "pytest-cov": { 1153 | "hashes": [ 1154 | "sha256:3904b13dfbfec47f003b8e77fd5b589cd11904a21ddf1ab38a64f204d6a10ef6", 1155 | "sha256:6ba70b9e97e69fcc3fb45bfeab2d0a138fb65c4d0d6a41ef33983ad114be8c3a" 1156 | ], 1157 | "index": "pypi", 1158 | "version": "==4.1.0" 1159 | }, 1160 | "pytest-env": { 1161 | "hashes": [ 1162 | "sha256:5e533273f4d9e6a41c3a3120e0c7944aae5674fa773b329f00a5eb1f23c53a38", 1163 | "sha256:baed9b3b6bae77bd75b9238e0ed1ee6903a42806ae9d6aeffb8754cd5584d4ff" 1164 | ], 1165 | "index": "pypi", 1166 | "version": "==0.8.2" 1167 | }, 1168 | "pytest-lazy-fixture": { 1169 | "hashes": [ 1170 | "sha256:0e7d0c7f74ba33e6e80905e9bfd81f9d15ef9a790de97993e34213deb5ad10ac", 1171 | "sha256:e0b379f38299ff27a653f03eaa69b08a6fd4484e46fd1c9907d984b9f9daeda6" 1172 | ], 1173 | "index": "pypi", 1174 | "version": "==0.6.3" 1175 | }, 1176 | "pytest-mock": { 1177 | "hashes": [ 1178 | "sha256:21c279fff83d70763b05f8874cc9cfb3fcacd6d354247a976f9529d19f9acf39", 1179 | "sha256:7f6b125602ac6d743e523ae0bfa71e1a697a2f5534064528c6ff84c2f7c2fc7f" 1180 | ], 1181 | "index": "pypi", 1182 | "version": "==3.11.1" 1183 | }, 1184 | "python-dateutil": { 1185 | "hashes": [ 1186 | "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86", 1187 | "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9" 1188 | ], 1189 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 1190 | "version": "==2.8.2" 1191 | }, 1192 | "pyyaml": { 1193 | "hashes": [ 1194 | "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc", 1195 | "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741", 1196 | "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206", 1197 | "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27", 1198 | "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595", 1199 | "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62", 1200 | "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98", 1201 | "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696", 1202 | "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d", 1203 | "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867", 1204 | "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47", 1205 | "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486", 1206 | "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6", 1207 | "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3", 1208 | "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007", 1209 | "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938", 1210 | "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c", 1211 | "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735", 1212 | "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d", 1213 | "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba", 1214 | "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8", 1215 | "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5", 1216 | "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd", 1217 | "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3", 1218 | "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0", 1219 | "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515", 1220 | "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c", 1221 | "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c", 1222 | "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924", 1223 | "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34", 1224 | "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43", 1225 | "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859", 1226 | "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673", 1227 | "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a", 1228 | "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab", 1229 | "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa", 1230 | "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c", 1231 | "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585", 1232 | "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d", 1233 | "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f" 1234 | ], 1235 | "markers": "python_version >= '3.6'", 1236 | "version": "==6.0.1" 1237 | }, 1238 | "ruff": { 1239 | "hashes": [ 1240 | "sha256:2dae8f2d9c44c5c49af01733c2f7956f808db682a4193180dedb29dd718d7bbe", 1241 | "sha256:2e7c15828d09f90e97bea8feefcd2907e8c8ce3a1f959c99f9b4b3469679f33c", 1242 | "sha256:37359cd67d2af8e09110a546507c302cbea11c66a52d2a9b6d841d465f9962d4", 1243 | "sha256:48ed5aca381050a4e2f6d232db912d2e4e98e61648b513c350990c351125aaec", 1244 | "sha256:4a7d52457b5dfcd3ab24b0b38eefaead8e2dca62b4fbf10de4cd0938cf20ce30", 1245 | "sha256:581c43e4ac5e5a7117ad7da2120d960a4a99e68ec4021ec3cd47fe1cf78f8380", 1246 | "sha256:5f972567163a20fb8c2d6afc60c2ea5ef8b68d69505760a8bd0377de8984b4f6", 1247 | "sha256:7008fc6ca1df18b21fa98bdcfc711dad5f94d0fc3c11791f65e460c48ef27c82", 1248 | "sha256:7784e3606352fcfb193f3cd22b2e2117c444cb879ef6609ec69deabd662b0763", 1249 | "sha256:7a37dab70114671d273f203268f6c3366c035fe0c8056614069e90a65e614bfc", 1250 | "sha256:83e8f372fa5627eeda5b83b5a9632d2f9c88fc6d78cead7e2a1f6fb05728d137", 1251 | "sha256:8ffa7347ad11643f29de100977c055e47c988cd6d9f5f5ff83027600b11b9189", 1252 | "sha256:b7de5b8689575918e130e4384ed9f539ce91d067c0a332aedef6ca7188adac2d", 1253 | "sha256:bd58af46b0221efb95966f1f0f7576df711cb53e50d2fdb0e83c2f33360116a4", 1254 | "sha256:d878370f7e9463ac40c253724229314ff6ebe4508cdb96cb536e1af4d5a9cd4f", 1255 | "sha256:ef6ee3e429fd29d6a5ceed295809e376e6ece5b0f13c7e703efaf3d3bcb30b96", 1256 | "sha256:fe7118c1eae3fda17ceb409629c7f3b5a22dffa7caf1f6796776936dca1fe653" 1257 | ], 1258 | "index": "pypi", 1259 | "version": "==0.0.280" 1260 | }, 1261 | "setuptools": { 1262 | "hashes": [ 1263 | "sha256:11e52c67415a381d10d6b462ced9cfb97066179f0e871399e006c4ab101fc85f", 1264 | "sha256:baf1fdb41c6da4cd2eae722e135500da913332ab3f2f5c7d33af9b492acb5235" 1265 | ], 1266 | "markers": "python_version >= '3.7'", 1267 | "version": "==68.0.0" 1268 | }, 1269 | "six": { 1270 | "hashes": [ 1271 | "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", 1272 | "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" 1273 | ], 1274 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 1275 | "version": "==1.16.0" 1276 | }, 1277 | "sniffio": { 1278 | "hashes": [ 1279 | "sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101", 1280 | "sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384" 1281 | ], 1282 | "markers": "python_version >= '3.7'", 1283 | "version": "==1.3.0" 1284 | }, 1285 | "sortedcontainers": { 1286 | "hashes": [ 1287 | "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88", 1288 | "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0" 1289 | ], 1290 | "version": "==2.4.0" 1291 | }, 1292 | "sqlalchemy-stubs": { 1293 | "hashes": [ 1294 | "sha256:5eec7aa110adf9b957b631799a72fef396b23ff99fe296df726645d01e312aa5", 1295 | "sha256:c665d6dd4482ef642f01027fa06c3d5e91befabb219dc71fc2a09e7d7695f7ae" 1296 | ], 1297 | "index": "pypi", 1298 | "version": "==0.4" 1299 | }, 1300 | "types-passlib": { 1301 | "hashes": [ 1302 | "sha256:6abbf2400a8f1cba48639753e3a034af507a765489bb070974d7f68d9ceef883", 1303 | "sha256:7a4df64b53c2746f804aa29fb361974e5894e0df30ff18cf60b9518696ffc9d3" 1304 | ], 1305 | "index": "pypi", 1306 | "version": "==1.7.7.12" 1307 | }, 1308 | "types-pyasn1": { 1309 | "hashes": [ 1310 | "sha256:8f1965d0b79152f9d1efc89f9aa9a8cdda7cd28b2619df6737c095cbedeff98b", 1311 | "sha256:dd5fc818864e63a66cd714be0a7a59a493f4a81b87ee9ac978c41f1eaa9a0cef" 1312 | ], 1313 | "version": "==0.4.0.6" 1314 | }, 1315 | "types-python-jose": { 1316 | "hashes": [ 1317 | "sha256:3c316675c3cee059ccb9aff87358254344915239fa7f19cee2787155a7db14ac", 1318 | "sha256:95592273443b45dc5cc88f7c56aa5a97725428753fb738b794e63ccb4904954e" 1319 | ], 1320 | "index": "pypi", 1321 | "version": "==3.3.4.8" 1322 | }, 1323 | "typing-extensions": { 1324 | "hashes": [ 1325 | "sha256:440d5dd3af93b060174bf433bccd69b0babc3b15b1a8dca43789fd7f61514b36", 1326 | "sha256:b75ddc264f0ba5615db7ba217daeb99701ad295353c45f9e95963337ceeeffb2" 1327 | ], 1328 | "markers": "python_version >= '3.7'", 1329 | "version": "==4.7.1" 1330 | }, 1331 | "virtualenv": { 1332 | "hashes": [ 1333 | "sha256:01aacf8decd346cf9a865ae85c0cdc7f64c8caa07ff0d8b1dfc1733d10677442", 1334 | "sha256:2ef6a237c31629da6442b0bcaa3999748108c7166318d1f55cc9f8d7294e97bd" 1335 | ], 1336 | "markers": "python_version >= '3.7'", 1337 | "version": "==20.24.1" 1338 | } 1339 | } 1340 | } 1341 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # medium_fastapi_layered_2023 2 | This project contains the source code for the medium article 3 | 4 | 🔗 https://medium.com/@parfeniukink/python-backend-project-advanced-setup-fastapi-example-7b7e73a52aec 5 | 6 | 7 | ## SQLAlchemy2 implementation 8 | 🔗 https://github.com/parfeniukink/medium_fastapi_layered_2023/pull/1 9 | -------------------------------------------------------------------------------- /alembic.ini: -------------------------------------------------------------------------------- 1 | # A generic, single database configuration. 2 | 3 | [alembic] 4 | # path to migration scripts 5 | script_location = ./src/infrastructure/database/migrations 6 | 7 | # template used to generate migration file names; The default value is %%(rev)s_%%(slug)s 8 | # Uncomment the line below if you want the files to be prepended with date and time 9 | # file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s 10 | 11 | # sys.path path, will be prepended to sys.path if present. 12 | # defaults to the current working directory. 13 | prepend_sys_path = . 14 | 15 | # timezone to use when rendering the date within the migration file 16 | # as well as the filename. 17 | # If specified, requires the python-dateutil library that can be 18 | # installed by adding `alembic[tz]` to the pip requirements 19 | # string value is passed to dateutil.tz.gettz() 20 | # leave blank for localtime 21 | # timezone = 22 | 23 | # max length of characters to apply to the 24 | # "slug" field 25 | # truncate_slug_length = 40 26 | 27 | # set to 'true' to run the environment during 28 | # the 'revision' command, regardless of autogenerate 29 | # revision_environment = false 30 | 31 | # set to 'true' to allow .pyc and .pyo files without 32 | # a source .py file to be detected as revisions in the 33 | # versions/ directory 34 | # sourceless = false 35 | 36 | # version location specification; This defaults 37 | # to ./src/infrastructure/database/migrations/versions. When using multiple version 38 | # directories, initial revisions must be specified with --version-path. 39 | # The path separator used here should be the separator specified by "version_path_separator" below. 40 | # version_locations = %(here)s/bar:%(here)s/bat:./src/infrastructure/database/migrations/versions 41 | 42 | # version path separator; As mentioned above, this is the character used to split 43 | # version_locations. The default within new alembic.ini files is "os", which uses os.pathsep. 44 | # If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas. 45 | # Valid values for version_path_separator are: 46 | # 47 | # version_path_separator = : 48 | # version_path_separator = ; 49 | # version_path_separator = space 50 | version_path_separator = os # Use os.pathsep. Default configuration used for new projects. 51 | 52 | # set to 'true' to search source files recursively 53 | # in each "version_locations" directory 54 | # new in Alembic version 1.10 55 | # recursive_version_locations = false 56 | 57 | # the output encoding used when revision files 58 | # are written from script.py.mako 59 | # output_encoding = utf-8 60 | 61 | sqlalchemy.url = driver://user:pass@localhost/dbname 62 | 63 | 64 | [post_write_hooks] 65 | # post_write_hooks defines scripts or Python functions that are run 66 | # on newly generated revision scripts. See the documentation for further 67 | # detail and examples 68 | 69 | # format using "black" - use the console_scripts runner, against the "black" entrypoint 70 | # hooks = black 71 | # black.type = console_scripts 72 | # black.entrypoint = black 73 | # black.options = -l 79 REVISION_SCRIPT_FILENAME 74 | 75 | # Logging configuration 76 | [loggers] 77 | keys = root,sqlalchemy,alembic 78 | 79 | [handlers] 80 | keys = console 81 | 82 | [formatters] 83 | keys = generic 84 | 85 | [logger_root] 86 | level = WARN 87 | handlers = console 88 | qualname = 89 | 90 | [logger_sqlalchemy] 91 | level = WARN 92 | handlers = 93 | qualname = sqlalchemy.engine 94 | 95 | [logger_alembic] 96 | level = INFO 97 | handlers = 98 | qualname = alembic 99 | 100 | [handler_console] 101 | class = StreamHandler 102 | args = (sys.stderr,) 103 | level = NOTSET 104 | formatter = generic 105 | 106 | [formatter_generic] 107 | format = %(levelname)-5.5s [%(name)s] %(message)s 108 | datefmt = %H:%M:%S 109 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.ruff] 2 | # Enable pycodestyle (`E`) and Pyflakes (`F`) codes by default. 3 | select = ["E", "F"] 4 | ignore = [] 5 | 6 | # Allow autofix for all enabled rules (when `--fix`) is provided. 7 | fixable = ["A", "B", "C", "D", "E", "F", "G", "I", "N", "Q", "S", "T", "W", "ANN", "ARG", "BLE", "COM", "DJ", "DTZ", "EM", "ERA", "EXE", "FBT", "ICN", "INP", "ISC", "NPY", "PD", "PGH", "PIE", "PL", "PT", "PTH", "PYI", "RET", "RSE", "RUF", "SIM", "SLF", "TCH", "TID", "TRY", "UP", "YTT"] 8 | unfixable = [] 9 | 10 | # Exclude a variety of commonly ignored directories. 11 | exclude = [ 12 | ".bzr", 13 | ".direnv", 14 | ".eggs", 15 | ".git", 16 | ".hg", 17 | ".mypy_cache", 18 | ".nox", 19 | ".pants.d", 20 | ".pytype", 21 | ".mypy_cache", 22 | ".ruff_cache", 23 | ".svn", 24 | ".tox", 25 | ".venv", 26 | "__pypackages__", 27 | "__pycache_", 28 | "_build", 29 | "buck-out", 30 | "build", 31 | "dist", 32 | "venv", 33 | "migrations", 34 | ] 35 | 36 | 37 | # Same as Black. 38 | line-length = 79 39 | 40 | # Allow unused variables when underscore-prefixed. 41 | dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" 42 | 43 | # Assume Python 3.10. 44 | target-version = "py311" 45 | 46 | 47 | [tool.ruff.mccabe] 48 | # Unlike Flake8, default to a complexity level of 10. 49 | max-complexity = 10 50 | 51 | 52 | [tool.black] 53 | target-version = ['py311'] 54 | line-length = 79 55 | exclude = ''' 56 | ( 57 | /( 58 | \.eggs # exclude a few common directories in the 59 | | \.git # root of the project 60 | | \.hg 61 | | \.mypy_cache 62 | | \.tox 63 | | \.venv 64 | | venv 65 | | _build 66 | | buck-out 67 | | build 68 | | dist 69 | | migrations 70 | )/ 71 | # the root of the project 72 | ) 73 | ''' 74 | 75 | [tool.isort] 76 | profile = "black" 77 | multi_line_output = 3 78 | include_trailing_comma = true 79 | force_grid_wrap = 0 80 | use_parentheses = true 81 | line_length = 79 82 | skip = '.venv,venv,env' 83 | src_paths = ["src"] 84 | 85 | [tool.pytest.ini_options] 86 | addopts = '-s -vvv --cache-clear' 87 | asyncio_mode = 'auto' 88 | cache_dir = '/tmp' 89 | python_files = 'tests.py test_*.py *_test.py' 90 | python_functions = 'test_* *_test' 91 | filterwarnings = ['ignore::RuntimeWarning', 'ignore::UserWarning'] 92 | 93 | [tool.coverage.run] 94 | omit = [ 95 | "*/conftest.py", 96 | "*/test_*.py", 97 | "*/migrations/", 98 | ] 99 | 100 | [tool.mypy] 101 | plugins = [ 102 | "pydantic.mypy" 103 | ] 104 | python_version = '3.11' 105 | files = ['*.py',] 106 | warn_redundant_casts = true 107 | warn_unused_ignores = true 108 | show_error_codes = true 109 | namespace_packages = true 110 | exclude = ["migrations"] 111 | 112 | # Silint "type import errors" as our 3rd-party libs does not have types 113 | # Check: https://mypy.readthedocs.io/en/latest/config_file.html#import-discovery 114 | follow_imports = 'silent' 115 | 116 | [[tool.mypy.overrides]] 117 | module = [ 118 | "websockets.exceptions", 119 | ] 120 | ignore_missing_imports = true 121 | -------------------------------------------------------------------------------- /src/application/authentication/__init__.py: -------------------------------------------------------------------------------- 1 | from src.application.authentication.dependency_injection import * # noqa: F401, F403 2 | -------------------------------------------------------------------------------- /src/application/authentication/dependency_injection.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | from fastapi import Depends 4 | from fastapi.security import OAuth2PasswordBearer 5 | from jose import JWTError, jwt 6 | from pydantic import ValidationError 7 | 8 | from src.config import settings 9 | from src.domain.authentication import TokenPayload 10 | from src.domain.users import User, UsersRepository 11 | from src.infrastructure.errors import AuthenticationError 12 | 13 | __all__ = ("get_current_user",) 14 | 15 | oauth2_oauth = OAuth2PasswordBearer( 16 | tokenUrl="/auth/openapi", 17 | scheme_name=settings.authentication.scheme, 18 | ) 19 | 20 | 21 | async def get_current_user(token: str = Depends(oauth2_oauth)) -> User: 22 | try: 23 | payload = jwt.decode( 24 | token, 25 | settings.authentication.access_token.secret_key, 26 | algorithms=[settings.authentication.algorithm], 27 | ) 28 | token_payload = TokenPayload(**payload) 29 | 30 | if datetime.fromtimestamp(token_payload.exp) < datetime.now(): 31 | raise AuthenticationError 32 | except (JWTError, ValidationError): 33 | raise AuthenticationError 34 | 35 | user = await UsersRepository().get(id_=token_payload.sub) 36 | 37 | # TODO: Check if the token is in the blacklist 38 | 39 | return user 40 | -------------------------------------------------------------------------------- /src/application/orders.py: -------------------------------------------------------------------------------- 1 | from src.domain.orders import Order, OrdersRepository, OrderUncommited 2 | from src.domain.users import User 3 | from src.infrastructure.database.transaction import transaction 4 | 5 | 6 | @transaction 7 | async def create(payload: dict, user: User) -> Order: 8 | payload.update(user_id=user.id) 9 | 10 | order = await OrdersRepository().create(OrderUncommited(**payload)) 11 | 12 | # Do som other stuff... 13 | 14 | return order 15 | -------------------------------------------------------------------------------- /src/config/__init__.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | from pydantic import BaseConfig, BaseModel, BaseSettings 4 | 5 | 6 | # API Settings 7 | class APIUrlsSettings(BaseModel): 8 | """Configure public urls.""" 9 | 10 | docs: str = "/docs" 11 | redoc: str = "/redoc" 12 | 13 | 14 | class PublicApiSettings(BaseModel): 15 | """Configure public API settings.""" 16 | 17 | name: str = "Backend" 18 | urls: APIUrlsSettings = APIUrlsSettings() 19 | 20 | 21 | # Database Settings 22 | class DatabaseSettings(BaseModel): 23 | name: str = "db.sqlite3" 24 | 25 | @property 26 | def url(self) -> str: 27 | return f"sqlite+aiosqlite:///./{self.name}" 28 | 29 | 30 | class KafkaSettings(BaseModel): 31 | bootstrap_servers: str = "localhost:9092" 32 | 33 | 34 | # Logging Settings 35 | class LoggingSettings(BaseModel): 36 | """Configure the logging engine.""" 37 | 38 | # The time field can be formatted using more human-friendly tokens. 39 | # These constitute a subset of the one used by the Pendulum library 40 | # https://pendulum.eustace.io/docs/#tokens 41 | format: str = "{time:YYYY-MM-DD HH:mm:ss} | {level: <5} | {message}" 42 | 43 | # The .log filename 44 | file: str = "backend" 45 | 46 | # The .log file Rotation 47 | rotation: str = "1MB" 48 | 49 | # The type of compression 50 | compression: str = "zip" 51 | 52 | 53 | class AccessTokenSettings(BaseModel): 54 | secret_key: str = "invaliad" 55 | ttl: int = 100 # seconds 56 | 57 | 58 | class RefreshTokenSettings(BaseModel): 59 | secret_key: str = "invaliad" 60 | ttl: int = 100 # seconds 61 | 62 | 63 | class AuthenticationSettings(BaseModel): 64 | access_token: AccessTokenSettings = AccessTokenSettings() 65 | refresh_token: RefreshTokenSettings = RefreshTokenSettings() 66 | algorithm: str = "HS256" 67 | scheme: str = "Bearer" 68 | 69 | 70 | # Settings are powered by pydantic 71 | # https://pydantic-docs.helpmanual.io/usage/settings/ 72 | class Settings(BaseSettings): 73 | debug: bool = True 74 | 75 | # Project file system 76 | root_dir: Path 77 | src_dir: Path 78 | 79 | # Infrastructure settings 80 | database: DatabaseSettings = DatabaseSettings() 81 | 82 | # Application configuration 83 | public_api: PublicApiSettings = PublicApiSettings() 84 | logging: LoggingSettings = LoggingSettings() 85 | authentication: AuthenticationSettings = AuthenticationSettings() 86 | 87 | class Config(BaseConfig): 88 | env_nested_delimiter: str = "__" 89 | env_file: str = ".env" 90 | 91 | 92 | # Define the root path 93 | # -------------------------------------- 94 | ROOT_PATH = Path(__file__).parent.parent 95 | 96 | # ====================================== 97 | # Load settings 98 | # ====================================== 99 | settings = Settings( 100 | # NOTE: We would like to hard-code the root and applications directories 101 | # to avoid overriding via environment variables 102 | root_dir=ROOT_PATH, 103 | src_dir=ROOT_PATH / "src", 104 | ) 105 | -------------------------------------------------------------------------------- /src/domain/authentication/__init__.py: -------------------------------------------------------------------------------- 1 | from src.domain.authentication.models import * # noqa: F401, F403 2 | -------------------------------------------------------------------------------- /src/domain/authentication/models.py: -------------------------------------------------------------------------------- 1 | from pydantic import Field 2 | 3 | from src.infrastructure.models import InternalModel, PublicModel 4 | 5 | __all__ = ( 6 | "TokenClaimRequestBody", 7 | "TokenClaimPublic", 8 | "TokenPayload", 9 | "AccessToken", 10 | "RefreshToken", 11 | ) 12 | 13 | 14 | # Public models 15 | # ------------------------------------------------------ 16 | class TokenClaimRequestBody(PublicModel): 17 | login: str = Field("OpenAPI documentation") 18 | password: str = Field("OpenAPI documentation") 19 | 20 | 21 | class TokenClaimPublic(PublicModel): 22 | access: str = Field("OpenAPI documentation") 23 | refresh: str = Field("OpenAPI documentation") 24 | 25 | 26 | # Internal models 27 | # ------------------------------------------------------ 28 | class TokenPayload(InternalModel): 29 | sub: int 30 | exp: int 31 | 32 | 33 | class AccessToken(InternalModel): 34 | payload: TokenPayload 35 | raw_token: str 36 | 37 | 38 | class RefreshToken(InternalModel): 39 | payload: TokenPayload 40 | raw_token: str 41 | -------------------------------------------------------------------------------- /src/domain/orders/__init__.py: -------------------------------------------------------------------------------- 1 | from src.domain.orders.models import * # noqa: F401, F403 2 | from src.domain.orders.repository import * # noqa: F401, F403 3 | -------------------------------------------------------------------------------- /src/domain/orders/models.py: -------------------------------------------------------------------------------- 1 | from pydantic import Field 2 | 3 | from src.infrastructure.models import InternalModel, PublicModel 4 | 5 | __all__ = ("OrderCreateRequestBody", "OrderPublic", "OrderUncommited", "Order") 6 | 7 | 8 | # Public models 9 | # ------------------------------------------------------ 10 | class _OrderPublic(PublicModel): 11 | amount: int = Field(description="OpenAPI description") 12 | product_id: int = Field(description="OpenAPI description") 13 | 14 | 15 | class OrderCreateRequestBody(_OrderPublic): 16 | """Order create request body.""" 17 | 18 | pass 19 | 20 | 21 | class OrderPublic(_OrderPublic): 22 | """The internal application representation.""" 23 | 24 | id: int 25 | 26 | 27 | # Internal models 28 | # ------------------------------------------------------ 29 | class _OrderInternal(InternalModel): 30 | amount: int 31 | product_id: int 32 | user_id: int 33 | 34 | 35 | class OrderUncommited(_OrderInternal): 36 | """This schema is used for creating instance in the database.""" 37 | 38 | pass 39 | 40 | 41 | class Order(_OrderInternal): 42 | """Existed order representation.""" 43 | 44 | id: int 45 | -------------------------------------------------------------------------------- /src/domain/orders/repository.py: -------------------------------------------------------------------------------- 1 | from typing import AsyncGenerator 2 | 3 | from src.infrastructure.database import BaseRepository, OrdersTable 4 | 5 | from .models import Order, OrderUncommited 6 | 7 | all = ("OrdersRepository",) 8 | 9 | 10 | class OrdersRepository(BaseRepository[OrdersTable]): 11 | schema_class = OrdersTable 12 | 13 | async def all(self) -> AsyncGenerator[Order, None]: 14 | async for instance in self._all(): 15 | yield Order.from_orm(instance) 16 | 17 | async def get(self, id_: int) -> Order: 18 | instance = await self._get(key="id", value=id_) 19 | return Order.from_orm(instance) 20 | 21 | async def create(self, schema: OrderUncommited) -> Order: 22 | instance: OrdersTable = await self._save(schema.dict()) 23 | return Order.from_orm(instance) 24 | -------------------------------------------------------------------------------- /src/domain/products/__init__.py: -------------------------------------------------------------------------------- 1 | from src.domain.products.models import * # noqa: F401, F403 2 | from src.domain.products.repository import * # noqa: F401, F403 3 | -------------------------------------------------------------------------------- /src/domain/products/models.py: -------------------------------------------------------------------------------- 1 | from pydantic import Field 2 | 3 | from src.infrastructure.models import InternalModel, PublicModel 4 | 5 | __all__ = ( 6 | "ProductCreateRequestBody", 7 | "ProductPublic", 8 | "ProductUncommited", 9 | "Product", 10 | ) 11 | 12 | 13 | # Public models 14 | # ------------------------------------------------------ 15 | class _ProductPublic(PublicModel): 16 | name: str = Field(description="OpenAPI description") 17 | price: int = Field(description="OpenAPI description") 18 | 19 | 20 | class ProductCreateRequestBody(_ProductPublic): 21 | """Product create request body.""" 22 | 23 | pass 24 | 25 | 26 | class ProductPublic(_ProductPublic): 27 | """The internal application representation.""" 28 | 29 | id: int 30 | 31 | 32 | # Internal models 33 | # ------------------------------------------------------ 34 | class _ProductInternal(InternalModel): 35 | name: str 36 | price: int 37 | 38 | 39 | class ProductUncommited(_ProductInternal): 40 | """This schema is used for creating instance in the database.""" 41 | 42 | pass 43 | 44 | 45 | class Product(_ProductInternal): 46 | """Existed product representation.""" 47 | 48 | id: int 49 | -------------------------------------------------------------------------------- /src/domain/products/repository.py: -------------------------------------------------------------------------------- 1 | from typing import AsyncGenerator 2 | 3 | from src.domain.products.models import Product, ProductUncommited 4 | from src.infrastructure.database import BaseRepository, ProductsTable 5 | 6 | all = ("ProductRepository",) 7 | 8 | 9 | class ProductRepository(BaseRepository[ProductsTable]): 10 | schema_class = ProductsTable 11 | 12 | async def all(self) -> AsyncGenerator[Product, None]: 13 | async for instance in self._all(): 14 | yield Product.from_orm(instance) 15 | 16 | async def get(self, id_: int) -> Product: 17 | instance = await self._get(key="id", value=id_) 18 | return Product.from_orm(instance) 19 | 20 | async def create(self, schema: ProductUncommited) -> Product: 21 | instance: ProductsTable = await self._save(schema.dict()) 22 | return Product.from_orm(instance) 23 | -------------------------------------------------------------------------------- /src/domain/users/__init__.py: -------------------------------------------------------------------------------- 1 | from src.domain.users.models import * # noqa: F401, F403 2 | from src.domain.users.repository import * # noqa: F401, F403 3 | -------------------------------------------------------------------------------- /src/domain/users/models.py: -------------------------------------------------------------------------------- 1 | from src.infrastructure.models import InternalModel 2 | 3 | __all__ = ("UserUncommited", "User") 4 | 5 | 6 | # Internal models 7 | # ------------------------------------------------------ 8 | class UserUncommited(InternalModel): 9 | """This schema is used for creating instance in the database.""" 10 | 11 | usernae: str 12 | password: str 13 | 14 | 15 | class User(UserUncommited): 16 | """Existed product representation.""" 17 | 18 | id: int 19 | -------------------------------------------------------------------------------- /src/domain/users/repository.py: -------------------------------------------------------------------------------- 1 | from typing import AsyncGenerator 2 | 3 | from src.infrastructure.database import BaseRepository, UsersTable 4 | 5 | from .models import User, UserUncommited 6 | 7 | all = ("UsersRepository",) 8 | 9 | 10 | class UsersRepository(BaseRepository[UsersTable]): 11 | schema_class = UsersTable 12 | 13 | async def all(self) -> AsyncGenerator[User, None]: 14 | async for instance in self._all(): 15 | yield User.from_orm(instance) 16 | 17 | async def get(self, id_: int) -> User: 18 | instance = await self._get(key="id", value=id_) 19 | return User.from_orm(instance) 20 | 21 | async def create(self, schema: UserUncommited) -> User: 22 | instance: UsersTable = await self._save(schema.dict()) 23 | return User.from_orm(instance) 24 | -------------------------------------------------------------------------------- /src/infrastructure/application/__init__.py: -------------------------------------------------------------------------------- 1 | from src.infrastructure.application.factory import * # noqa: F401, F403 2 | -------------------------------------------------------------------------------- /src/infrastructure/application/factory.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from functools import partial 3 | from typing import Callable, Coroutine, Iterable 4 | 5 | from fastapi import APIRouter, FastAPI 6 | from fastapi.exceptions import RequestValidationError 7 | from pydantic import ValidationError 8 | 9 | from src.infrastructure.errors import ( 10 | BaseError, 11 | custom_base_errors_handler, 12 | pydantic_validation_errors_handler, 13 | python_base_error_handler, 14 | ) 15 | 16 | __all__ = ("create",) 17 | 18 | 19 | def create( 20 | *_, 21 | rest_routers: Iterable[APIRouter], 22 | startup_tasks: Iterable[Callable[[], Coroutine]] | None = None, 23 | shutdown_tasks: Iterable[Callable[[], Coroutine]] | None = None, 24 | **kwargs, 25 | ) -> FastAPI: 26 | """The application factory using FastAPI framework. 27 | 🎉 Only passing routes is mandatory to start. 28 | """ 29 | 30 | # Initialize the base FastAPI application 31 | app = FastAPI(**kwargs) 32 | 33 | # Include REST API routers 34 | for router in rest_routers: 35 | app.include_router(router) 36 | 37 | # Extend FastAPI default error handlers 38 | app.exception_handler(RequestValidationError)( 39 | pydantic_validation_errors_handler 40 | ) 41 | app.exception_handler(BaseError)(custom_base_errors_handler) 42 | app.exception_handler(ValidationError)(pydantic_validation_errors_handler) 43 | app.exception_handler(Exception)(python_base_error_handler) 44 | 45 | # Define startup tasks that are running asynchronous using FastAPI hook 46 | if startup_tasks: 47 | for task in startup_tasks: 48 | coro = partial(asyncio.create_task, task()) 49 | app.on_event("startup")(coro) 50 | 51 | # Define shutdown tasks using FastAPI hook 52 | if shutdown_tasks: 53 | for task in shutdown_tasks: 54 | app.on_event("shutdown")(task) 55 | 56 | return app 57 | -------------------------------------------------------------------------------- /src/infrastructure/database/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module includes all shared utils and tools for the database interaction. 3 | """ 4 | 5 | from src.infrastructure.database.repository import * # noqa: F401, F403 6 | from src.infrastructure.database.tables import * # noqa: F401, F403 7 | -------------------------------------------------------------------------------- /src/infrastructure/database/migrations/README: -------------------------------------------------------------------------------- 1 | Generic single-database configuration with an async dbapi. -------------------------------------------------------------------------------- /src/infrastructure/database/migrations/env.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from logging.config import fileConfig 3 | 4 | from alembic import context 5 | from sqlalchemy import pool 6 | from sqlalchemy.engine import Connection 7 | from sqlalchemy.ext.asyncio import async_engine_from_config 8 | 9 | from src.config import settings 10 | from src.infrastructure.database.tables import Base, OrdersTable, ProductsTable 11 | 12 | # this is the Alembic Config object, which provides 13 | # access to the values within the .ini file in use. 14 | config = context.config 15 | 16 | # Interpret the config file for Python logging. 17 | # This line sets up loggers basically. 18 | if config.config_file_name is not None: 19 | fileConfig(config.config_file_name) 20 | 21 | # add your model's MetaData object here 22 | # for 'autogenerate' support 23 | # from myapp import mymodel 24 | # target_metadata = mymodel.Base.metadata 25 | target_metadata = Base.metadata 26 | 27 | # other values from the config, defined by the needs of env.py, 28 | # can be acquired: 29 | # my_important_option = config.get_main_option("my_important_option") 30 | # ... etc. 31 | 32 | config.set_main_option("sqlalchemy.url", settings.database.url) 33 | 34 | 35 | def run_migrations_offline() -> None: 36 | """Run migrations in 'offline' mode. 37 | 38 | This configures the context with just a URL 39 | and not an Engine, though an Engine is acceptable 40 | here as well. By skipping the Engine creation 41 | we don't even need a DBAPI to be available. 42 | 43 | Calls to context.execute() here emit the given string to the 44 | script output. 45 | 46 | """ 47 | url = config.get_main_option("sqlalchemy.url") 48 | context.configure( 49 | url=url, 50 | target_metadata=target_metadata, 51 | literal_binds=True, 52 | dialect_opts={"paramstyle": "named"}, 53 | ) 54 | 55 | with context.begin_transaction(): 56 | context.run_migrations() 57 | 58 | 59 | def do_run_migrations(connection: Connection) -> None: 60 | context.configure(connection=connection, target_metadata=target_metadata) 61 | 62 | with context.begin_transaction(): 63 | context.run_migrations() 64 | 65 | 66 | async def run_async_migrations() -> None: 67 | """In this scenario we need to create an Engine 68 | and associate a connection with the context. 69 | 70 | """ 71 | 72 | connectable = async_engine_from_config( 73 | config.get_section(config.config_ini_section, {}), 74 | prefix="sqlalchemy.", 75 | poolclass=pool.NullPool, 76 | ) 77 | 78 | async with connectable.connect() as connection: 79 | await connection.run_sync(do_run_migrations) 80 | 81 | await connectable.dispose() 82 | 83 | 84 | def run_migrations_online() -> None: 85 | """Run migrations in 'online' mode.""" 86 | 87 | asyncio.run(run_async_migrations()) 88 | 89 | 90 | if context.is_offline_mode(): 91 | run_migrations_offline() 92 | else: 93 | run_migrations_online() 94 | -------------------------------------------------------------------------------- /src/infrastructure/database/migrations/script.py.mako: -------------------------------------------------------------------------------- 1 | """${message} 2 | 3 | Revision ID: ${up_revision} 4 | Revises: ${down_revision | comma,n} 5 | Create Date: ${create_date} 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | ${imports if imports else ""} 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = ${repr(up_revision)} 14 | down_revision = ${repr(down_revision)} 15 | branch_labels = ${repr(branch_labels)} 16 | depends_on = ${repr(depends_on)} 17 | 18 | 19 | def upgrade() -> None: 20 | ${upgrades if upgrades else "pass"} 21 | 22 | 23 | def downgrade() -> None: 24 | ${downgrades if downgrades else "pass"} 25 | -------------------------------------------------------------------------------- /src/infrastructure/database/migrations/versions/69888b706120_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: 69888b706120 4 | Revises: 5 | Create Date: 2023-07-23 20:56:26.430715 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = '69888b706120' 14 | down_revision = None 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade() -> None: 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | op.create_table('products', 22 | sa.Column('name', sa.String(), nullable=False), 23 | sa.Column('price', sa.Integer(), nullable=False), 24 | sa.Column('id', sa.Integer(), nullable=False), 25 | sa.PrimaryKeyConstraint('id', name=op.f('pk_products')) 26 | ) 27 | op.create_table('users', 28 | sa.Column('username', sa.String(), nullable=False), 29 | sa.Column('password', sa.String(), nullable=False), 30 | sa.Column('id', sa.Integer(), nullable=False), 31 | sa.PrimaryKeyConstraint('id', name=op.f('pk_users')) 32 | ) 33 | op.create_table('orders', 34 | sa.Column('amount', sa.Integer(), nullable=False), 35 | sa.Column('product_id', sa.Integer(), nullable=False), 36 | sa.Column('user_id', sa.Integer(), nullable=False), 37 | sa.Column('id', sa.Integer(), nullable=False), 38 | sa.ForeignKeyConstraint(['product_id'], ['products.id'], name=op.f('fk_orders_product_id_products')), 39 | sa.ForeignKeyConstraint(['user_id'], ['users.id'], name=op.f('fk_orders_user_id_users')), 40 | sa.PrimaryKeyConstraint('id', name=op.f('pk_orders')) 41 | ) 42 | # ### end Alembic commands ### 43 | 44 | 45 | def downgrade() -> None: 46 | # ### commands auto generated by Alembic - please adjust! ### 47 | op.drop_table('orders') 48 | op.drop_table('users') 49 | op.drop_table('products') 50 | # ### end Alembic commands ### 51 | -------------------------------------------------------------------------------- /src/infrastructure/database/repository.py: -------------------------------------------------------------------------------- 1 | from typing import Any, AsyncGenerator, Generic, Type 2 | 3 | from sqlalchemy import Result, asc, delete, desc, func, select, update 4 | 5 | from src.infrastructure.database.session import Session 6 | from src.infrastructure.database.tables import ConcreteTable 7 | from src.infrastructure.errors import ( 8 | DatabaseError, 9 | NotFoundError, 10 | UnprocessableError, 11 | ) 12 | 13 | __all__ = ("BaseRepository",) 14 | 15 | 16 | # Mypy error: https://github.com/python/mypy/issues/13755 17 | class BaseRepository(Session, Generic[ConcreteTable]): # type: ignore 18 | """This class implements the base interface for working with database 19 | # and makes it easier to work with type annotations. 20 | """ 21 | 22 | schema_class: Type[ConcreteTable] 23 | 24 | def __init__(self) -> None: 25 | super().__init__() 26 | 27 | if not self.schema_class: 28 | raise UnprocessableError( 29 | message=( 30 | "Can not initiate the class without schema_class attribute" 31 | ) 32 | ) 33 | 34 | async def _update( 35 | self, key: str, value: Any, payload: dict[str, Any] 36 | ) -> ConcreteTable: 37 | """Updates an existed instance of the model in the related table. 38 | If some data is not exist in the payload then the null value will 39 | be passed to the schema class.""" 40 | 41 | query = ( 42 | update(self.schema_class) 43 | .where(getattr(self.schema_class, key) == value) 44 | .values(payload) 45 | .returning(self.schema_class) 46 | ) 47 | result: Result = await self.execute(query) 48 | await self._session.flush() 49 | 50 | if not (schema := result.scalar_one_or_none()): 51 | raise DatabaseError 52 | 53 | return schema 54 | 55 | async def _get(self, key: str, value: Any) -> ConcreteTable: 56 | """Return only one result by filters""" 57 | 58 | query = select(self.schema_class).where( 59 | getattr(self.schema_class, key) == value 60 | ) 61 | result: Result = await self.execute(query) 62 | 63 | if not (_result := result.scalars().one_or_none()): 64 | raise NotFoundError 65 | 66 | return _result 67 | 68 | async def count(self) -> int: 69 | result: Result = await self.execute(func.count(self.schema_class.id)) 70 | value = result.scalar() 71 | 72 | if not isinstance(value, int): 73 | raise UnprocessableError( 74 | message=( 75 | "For some reason count function returned not an integer." 76 | f"Value: {value}" 77 | ), 78 | ) 79 | 80 | return value 81 | 82 | async def _first(self, by: str = "id") -> ConcreteTable: 83 | result: Result = await self.execute( 84 | select(self.schema_class).order_by(asc(by)).limit(1) 85 | ) 86 | 87 | if not (_result := result.scalar_one_or_none()): 88 | raise NotFoundError 89 | 90 | return _result 91 | 92 | async def _last(self, by: str = "id") -> ConcreteTable: 93 | result: Result = await self.execute( 94 | select(self.schema_class).order_by(desc(by)).limit(1) 95 | ) 96 | 97 | if not (_result := result.scalar_one_or_none()): 98 | raise NotFoundError 99 | 100 | return _result 101 | 102 | async def _save(self, payload: dict[str, Any]) -> ConcreteTable: 103 | try: 104 | schema = self.schema_class(**payload) 105 | self._session.add(schema) 106 | await self._session.flush() 107 | await self._session.refresh(schema) 108 | return schema 109 | except self._ERRORS: 110 | raise DatabaseError 111 | 112 | async def _all(self) -> AsyncGenerator[ConcreteTable, None]: 113 | result: Result = await self.execute(select(self.schema_class)) 114 | schemas = result.scalars().all() 115 | 116 | for schema in schemas: 117 | yield schema 118 | 119 | async def delete(self, id_: int) -> None: 120 | await self.execute( 121 | delete(self.schema_class).where(self.schema_class.id == id_) 122 | ) 123 | await self._session.flush() 124 | -------------------------------------------------------------------------------- /src/infrastructure/database/session.py: -------------------------------------------------------------------------------- 1 | from contextvars import ContextVar 2 | 3 | from sqlalchemy import Result 4 | from sqlalchemy.exc import IntegrityError, PendingRollbackError 5 | from sqlalchemy.ext.asyncio import ( 6 | AsyncEngine, 7 | AsyncSession, 8 | async_sessionmaker, 9 | create_async_engine, 10 | ) 11 | 12 | from src.config import settings 13 | from src.infrastructure.errors import DatabaseError 14 | 15 | __all__ = ("get_session", "engine", "CTX_SESSION") 16 | 17 | 18 | engine: AsyncEngine = create_async_engine( 19 | settings.database.url, future=True, pool_pre_ping=True, echo=False 20 | ) 21 | 22 | 23 | def get_session(engine: AsyncEngine | None = engine) -> AsyncSession: 24 | Session: async_sessionmaker = async_sessionmaker( 25 | engine, expire_on_commit=False, autoflush=False 26 | ) 27 | 28 | return Session() 29 | 30 | 31 | CTX_SESSION: ContextVar[AsyncSession] = ContextVar( 32 | "session", default=get_session() 33 | ) 34 | 35 | 36 | class Session: 37 | # All sqlalchemy errors that can be raised 38 | _ERRORS = (IntegrityError, PendingRollbackError) 39 | 40 | def __init__(self) -> None: 41 | self._session: AsyncSession = CTX_SESSION.get() 42 | 43 | async def execute(self, query) -> Result: 44 | try: 45 | result = await self._session.execute(query) 46 | return result 47 | except self._ERRORS: 48 | raise DatabaseError 49 | -------------------------------------------------------------------------------- /src/infrastructure/database/tables.py: -------------------------------------------------------------------------------- 1 | from typing import TypeVar 2 | 3 | from sqlalchemy import Column, ForeignKey, Integer, MetaData, String 4 | from sqlalchemy.orm import declarative_base 5 | 6 | __all__ = ("UsersTable", "ProductsTable", "OrdersTable") 7 | 8 | meta = MetaData( 9 | naming_convention={ 10 | "ix": "ix_%(column_0_label)s", 11 | "uq": "uq_%(table_name)s_%(column_0_name)s", 12 | "ck": "ck_%(table_name)s_`%(constraint_name)s`", 13 | "fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s", 14 | "pk": "pk_%(table_name)s", 15 | } 16 | ) 17 | 18 | 19 | class _Base: 20 | """Base class for all database models.""" 21 | 22 | id = Column(Integer, primary_key=True) 23 | 24 | 25 | Base = declarative_base(cls=_Base, metadata=meta) 26 | 27 | ConcreteTable = TypeVar("ConcreteTable", bound=Base) # type: ignore 28 | 29 | 30 | class UsersTable(Base): 31 | __tablename__ = "users" 32 | 33 | username: str = Column(String, nullable=False) # type: ignore 34 | password: str = Column(String, nullable=False) # type: ignore 35 | 36 | 37 | class ProductsTable(Base): 38 | __tablename__ = "products" 39 | 40 | name: str = Column(String, nullable=False) # type: ignore 41 | price: int = Column(Integer, nullable=False) # type: ignore 42 | 43 | 44 | class OrdersTable(Base): 45 | __tablename__ = "orders" 46 | 47 | amount: int = Column(Integer, nullable=False, default=1) # type: ignore 48 | 49 | product_id: int = Column( 50 | ForeignKey(ProductsTable.id), 51 | nullable=False, 52 | ) # type: ignore[var-annotated] 53 | user_id: int = Column( 54 | ForeignKey(UsersTable.id), 55 | nullable=False, 56 | ) # type: ignore[var-annotated] 57 | -------------------------------------------------------------------------------- /src/infrastructure/database/transaction.py: -------------------------------------------------------------------------------- 1 | from functools import wraps 2 | 3 | from loguru import logger 4 | from sqlalchemy.exc import IntegrityError, PendingRollbackError 5 | from sqlalchemy.ext.asyncio import AsyncSession 6 | 7 | from src.infrastructure.database.session import CTX_SESSION, get_session 8 | from src.infrastructure.errors import DatabaseError 9 | 10 | 11 | def transaction(coro): 12 | """This decorator should be used with all coroutines 13 | that want's access the database for saving a new data. 14 | """ 15 | 16 | @wraps(coro) 17 | async def inner(*args, **kwargs): 18 | session: AsyncSession = get_session() 19 | CTX_SESSION.set(session) 20 | 21 | try: 22 | result = await coro(*args, **kwargs) 23 | await session.commit() 24 | return result 25 | except DatabaseError as error: 26 | # NOTE: If any sort of issues are occurred in the code 27 | # they are handled on the BaseCRUD level and raised 28 | # as a DatabseError. 29 | # If the DatabseError is handled within domain/application 30 | # levels it is possible that `await session.commit()` 31 | # would raise an error. 32 | logger.error(f"Rolling back changes.\n{error}") 33 | await session.rollback() 34 | raise DatabaseError 35 | except (IntegrityError, PendingRollbackError) as error: 36 | # NOTE: Since there is a session commit on this level it should 37 | # be handled because it can raise some errors also 38 | logger.error(f"Rolling back changes.\n{error}") 39 | await session.rollback() 40 | finally: 41 | await session.close() 42 | 43 | return inner 44 | -------------------------------------------------------------------------------- /src/infrastructure/errors/__init__.py: -------------------------------------------------------------------------------- 1 | from src.infrastructure.errors.base import * # noqa: F401, F403 2 | from src.infrastructure.errors.handlers import * # noqa: F401, F403 3 | -------------------------------------------------------------------------------- /src/infrastructure/errors/base.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module is responsible for describing shared errors 3 | that are handled on the last step of processing the request. 4 | """ 5 | 6 | 7 | from typing import Any 8 | 9 | from starlette import status 10 | 11 | __all__ = ( 12 | "BaseError", 13 | "BadRequestError", 14 | "UnprocessableError", 15 | "NotFoundError", 16 | "DatabaseError", 17 | "AuthenticationError", 18 | "AuthorizationError", 19 | ) 20 | 21 | 22 | class BaseError(Exception): 23 | def __init__( 24 | self, 25 | *_: tuple[Any], 26 | message: str = "", 27 | status_code: int = status.HTTP_500_INTERNAL_SERVER_ERROR, 28 | ) -> None: 29 | self.message: str = message 30 | self.status_code: int = status_code 31 | 32 | super().__init__(message) 33 | 34 | 35 | class BadRequestError(BaseError): 36 | def __init__(self, *_: tuple[Any], message: str = "Bad request") -> None: 37 | super().__init__( 38 | message=message, 39 | status_code=status.HTTP_400_BAD_REQUEST, 40 | ) 41 | 42 | 43 | class UnprocessableError(BaseError): 44 | def __init__( 45 | self, *_: tuple[Any], message: str = "Validation error" 46 | ) -> None: 47 | super().__init__( 48 | message=message, 49 | status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, 50 | ) 51 | 52 | 53 | class NotFoundError(BaseError): 54 | def __init__(self, *_: tuple[Any], message: str = "Not found") -> None: 55 | super().__init__( 56 | message=message, status_code=status.HTTP_404_NOT_FOUND 57 | ) 58 | 59 | 60 | class DatabaseError(BaseError): 61 | def __init__( 62 | self, *_: tuple[Any], message: str = "Database error" 63 | ) -> None: 64 | super().__init__( 65 | message=message, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR 66 | ) 67 | 68 | 69 | class AuthenticationError(BaseError): 70 | def __init__( 71 | self, *_: tuple[Any], message: str = "Authentication error" 72 | ) -> None: 73 | super().__init__( 74 | message=message, 75 | status_code=status.HTTP_401_UNAUTHORIZED, 76 | ) 77 | 78 | 79 | class AuthorizationError(BaseError): 80 | def __init__( 81 | self, *_: tuple[Any], message: str = "Authorization error" 82 | ) -> None: 83 | super().__init__( 84 | message=message, status_code=status.HTTP_403_FORBIDDEN 85 | ) 86 | -------------------------------------------------------------------------------- /src/infrastructure/errors/handlers.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module is used for representing FastAPI error handlers 3 | that are dispatched automatically by fastapi engine. 4 | """ 5 | 6 | from fastapi.encoders import jsonable_encoder 7 | from fastapi.exceptions import RequestValidationError 8 | from fastapi.responses import JSONResponse 9 | from starlette import status 10 | from starlette.requests import Request 11 | 12 | from src.infrastructure.errors.base import BaseError 13 | from src.infrastructure.models import ErrorResponse, ErrorResponseMulti 14 | 15 | __all__ = ( 16 | "custom_base_errors_handler", 17 | "python_base_error_handler", 18 | "pydantic_validation_errors_handler", 19 | ) 20 | 21 | 22 | def custom_base_errors_handler(_: Request, error: BaseError) -> JSONResponse: 23 | """This function is called if the BaseError was raised.""" 24 | 25 | response = ErrorResponseMulti( 26 | results=[ErrorResponse(message=error.message.capitalize())] 27 | ) 28 | 29 | return JSONResponse( 30 | response.dict(by_alias=True), 31 | status_code=error.status_code, 32 | ) 33 | 34 | 35 | def python_base_error_handler(_: Request, error: Exception) -> JSONResponse: 36 | """This function is called if the Exception was raised.""" 37 | 38 | response = ErrorResponseMulti( 39 | results=[ErrorResponse(message=f"Unhandled error: {error}")] 40 | ) 41 | 42 | return JSONResponse( 43 | content=jsonable_encoder(response.dict(by_alias=True)), 44 | status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, 45 | ) 46 | 47 | 48 | def pydantic_validation_errors_handler( 49 | _: Request, error: RequestValidationError 50 | ) -> JSONResponse: 51 | """This function is called if the Pydantic validation error was raised.""" 52 | 53 | response = ErrorResponseMulti( 54 | results=[ 55 | ErrorResponse( 56 | message=err["msg"], 57 | path=list(err["loc"]), 58 | ) 59 | for err in error.errors() 60 | ] 61 | ) 62 | 63 | return JSONResponse( 64 | content=jsonable_encoder(response.dict(by_alias=True)), 65 | status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, 66 | ) 67 | -------------------------------------------------------------------------------- /src/infrastructure/models/__init__.py: -------------------------------------------------------------------------------- 1 | from src.infrastructure.models.base import * # noqa: F401, F403 2 | from src.infrastructure.models.response import * # noqa: F401, F403 3 | -------------------------------------------------------------------------------- /src/infrastructure/models/base.py: -------------------------------------------------------------------------------- 1 | """ 2 | This model includes basic data models that are used in the whole application. 3 | """ 4 | 5 | import json 6 | from typing import TypeVar 7 | 8 | from pydantic import BaseModel, Extra 9 | 10 | __all__ = ( 11 | "InternalModel", 12 | "_InternalModel", 13 | "PublicModel", 14 | "_PublicModel", 15 | "FrozenModel", 16 | ) 17 | 18 | 19 | def to_camelcase(string: str) -> str: 20 | """The alias generator for PublicModel.""" 21 | 22 | resp = "".join( 23 | word.capitalize() if index else word 24 | for index, word in enumerate(string.split("_")) 25 | ) 26 | return resp 27 | 28 | 29 | _json_encoders = { 30 | # np.float32: lambda v: float(v) if v else None, 31 | } 32 | 33 | 34 | class FrozenModel(BaseModel): 35 | class Config: 36 | json_encoders = _json_encoders 37 | orm_mode = True 38 | use_enum_values = True 39 | allow_population_by_field_name = True 40 | arbitrary_types_allowed = True 41 | allow_mutation = False 42 | 43 | 44 | class InternalModel(BaseModel): 45 | class Config: 46 | json_encoders = _json_encoders 47 | extra = Extra.forbid 48 | orm_mode = True 49 | use_enum_values = True 50 | allow_population_by_field_name = True 51 | validate_assignment = True 52 | arbitrary_types_allowed = True 53 | 54 | 55 | _InternalModel = TypeVar("_InternalModel", bound=InternalModel) 56 | 57 | 58 | class PublicModel(BaseModel): 59 | class Config: 60 | json_encoders = _json_encoders 61 | extra = Extra.ignore 62 | orm_mode = True 63 | use_enum_values = True 64 | validate_assignment = True 65 | alias_generator = to_camelcase 66 | allow_population_by_field_name = True 67 | arbitrary_types_allowed = True 68 | 69 | def encoded_dict(self, by_alias=True): 70 | """This method might be useful is the data should be passed 71 | only with primitives that are allowed by JSON format. 72 | The regular .dict() does not return the ISO datetime format 73 | but the .json() - does. This method is a combination of them. 74 | """ 75 | return json.loads(self.json(by_alias=by_alias)) 76 | 77 | 78 | _PublicModel = TypeVar("_PublicModel", bound=PublicModel) 79 | -------------------------------------------------------------------------------- /src/infrastructure/models/response.py: -------------------------------------------------------------------------------- 1 | from collections.abc import Mapping 2 | from typing import Any, Generic 3 | 4 | from pydantic import Field, conlist 5 | from pydantic.generics import GenericModel 6 | 7 | from src.infrastructure.models.base import PublicModel, _PublicModel 8 | 9 | __all__ = ( 10 | "ResponseMulti", 11 | "Response", 12 | "_Response", 13 | "ErrorResponse", 14 | "ErrorResponseMulti", 15 | ) 16 | 17 | 18 | class ResponseMulti(PublicModel, GenericModel, Generic[_PublicModel]): 19 | """Generic response model that consist multiple results.""" 20 | 21 | result: list[_PublicModel] 22 | 23 | 24 | class Response(PublicModel, GenericModel, Generic[_PublicModel]): 25 | """Generic response model that consist only one result.""" 26 | 27 | result: _PublicModel 28 | 29 | 30 | _Response = Mapping[int | str, dict[str, Any]] 31 | 32 | 33 | class ErrorResponse(PublicModel): 34 | """Error response model.""" 35 | 36 | message: str = Field(description="This field represent the message") 37 | path: list = Field( 38 | description="The path to the field that raised the error", 39 | default_factory=list, 40 | ) 41 | 42 | 43 | class ErrorResponseMulti(PublicModel): 44 | """The public error respnse model that includes multiple objects.""" 45 | 46 | results: conlist(ErrorResponse, min_items=1) # type: ignore 47 | -------------------------------------------------------------------------------- /src/main.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI 2 | from loguru import logger 3 | 4 | from src.config import settings 5 | from src.infrastructure import application 6 | from src.presentation import rest 7 | 8 | # Adjust the logging 9 | # ------------------------------- 10 | logger.add( 11 | "".join( 12 | [ 13 | str(settings.root_dir), 14 | "/logs/", 15 | settings.logging.file.lower(), 16 | ".log", 17 | ] 18 | ), 19 | format=settings.logging.format, 20 | rotation=settings.logging.rotation, 21 | compression=settings.logging.compression, 22 | level="INFO", 23 | ) 24 | 25 | 26 | # Adjust the application 27 | # ------------------------------- 28 | app: FastAPI = application.create( 29 | debug=settings.debug, 30 | rest_routers=(rest.products.router, rest.orders.router), 31 | startup_tasks=[], 32 | shutdown_tasks=[], 33 | ) 34 | -------------------------------------------------------------------------------- /src/presentation/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/parfeniukink/medium_fastapi_layered_2023/05502fcb28e1a0efc90913b821a905c1a42b66a9/src/presentation/__init__.py -------------------------------------------------------------------------------- /src/presentation/rest/__init__.py: -------------------------------------------------------------------------------- 1 | from src.presentation.rest import orders, products 2 | -------------------------------------------------------------------------------- /src/presentation/rest/orders.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter, Depends, Request, status 2 | 3 | from src.application import orders 4 | from src.application.authentication import get_current_user 5 | from src.domain.orders import ( 6 | Order, 7 | OrderCreateRequestBody, 8 | OrderPublic, 9 | OrdersRepository, 10 | ) 11 | from src.domain.users import User 12 | from src.infrastructure.database.transaction import transaction 13 | from src.infrastructure.models import Response, ResponseMulti 14 | 15 | router = APIRouter(prefix="/orders", tags=["Orders"]) 16 | 17 | 18 | @router.get("", status_code=status.HTTP_200_OK) 19 | @transaction 20 | async def orders_list( 21 | request: Request, user: User = Depends(get_current_user) 22 | ) -> ResponseMulti[OrderPublic]: 23 | """Get all orders.""" 24 | 25 | # Get all products from the database 26 | orders_public = [ 27 | OrderPublic.from_orm(order) async for order in OrdersRepository().all() 28 | ] 29 | 30 | return ResponseMulti[OrderPublic](result=orders_public) 31 | 32 | 33 | @router.post("", status_code=status.HTTP_201_CREATED) 34 | async def order_create( 35 | request: Request, 36 | schema: OrderCreateRequestBody, 37 | user: User = Depends(get_current_user), 38 | ) -> Response[OrderPublic]: 39 | """Create a new order.""" 40 | 41 | # Save product to the database 42 | order: Order = await orders.create(payload=schema.dict(), user=user) 43 | order_public = OrderPublic.from_orm(order) 44 | 45 | return Response[OrderPublic](result=order_public) 46 | -------------------------------------------------------------------------------- /src/presentation/rest/products.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter, Depends, Request, status 2 | 3 | from src.application.authentication import get_current_user 4 | from src.domain.products import ( 5 | Product, 6 | ProductCreateRequestBody, 7 | ProductPublic, 8 | ProductRepository, 9 | ProductUncommited, 10 | ) 11 | from src.domain.users import User 12 | from src.infrastructure.database.transaction import transaction 13 | from src.infrastructure.models import Response, ResponseMulti 14 | 15 | router = APIRouter(prefix="/products", tags=["Products"]) 16 | 17 | 18 | @router.get("", status_code=status.HTTP_200_OK) 19 | @transaction 20 | async def products_list(request: Request) -> ResponseMulti[ProductPublic]: 21 | """Get all products.""" 22 | 23 | # Get all products from the database 24 | products_public = [ 25 | ProductPublic.from_orm(product) 26 | async for product in ProductRepository().all() 27 | ] 28 | 29 | return ResponseMulti[ProductPublic](result=products_public) 30 | 31 | 32 | @router.post("", status_code=status.HTTP_201_CREATED) 33 | @transaction 34 | async def product_create( 35 | _: Request, 36 | schema: ProductCreateRequestBody, 37 | user: User = Depends(get_current_user), 38 | ) -> Response[ProductPublic]: 39 | """Create a new product.""" 40 | 41 | # Save product to the database 42 | product: Product = await ProductRepository().create( 43 | ProductUncommited(**schema.dict()) 44 | ) 45 | product_public = ProductPublic.from_orm(product) 46 | 47 | return Response[ProductPublic](result=product_public) 48 | --------------------------------------------------------------------------------