├── .env ├── .gitignore ├── LICENSE ├── Pipfile ├── Pipfile.lock ├── README.md ├── alembic.ini ├── app ├── __init__.py ├── api │ ├── __init__.py │ ├── deps.py │ └── v1 │ │ ├── __init__.py │ │ └── products.py ├── core │ ├── __init__.py │ └── config.py ├── crud │ ├── __init__.py │ ├── base.py │ └── product.py ├── database │ ├── __init__.py │ ├── base.py │ ├── base_class.py │ ├── initialise.py │ └── session.py ├── initialiser.py ├── main.py ├── models │ ├── __init__.py │ └── product.py ├── schemas │ ├── __init__.py │ ├── message.py │ └── product.py └── tests │ ├── __init__.py │ ├── api │ ├── __init__.py │ └── v1 │ │ ├── __init__.py │ │ └── test_products.py │ └── conftest.py ├── migrations ├── README ├── env.py ├── script.py.mako └── versions │ ├── .gitkeep │ └── eaae517c1e2f_initialise.py └── prestart.sh /.env: -------------------------------------------------------------------------------- 1 | API_V1_STR="/api/v1" 2 | PROJECT_NAME="FastAPI" 3 | SQLALCHEMY_DATABASE_URI="postgresql://localhost:5432/fastapi_db" 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | venv/ 2 | .vscode/ 3 | __pycache__/ 4 | test.db 5 | 6 | # OS generated files # 7 | ###################### 8 | .DS_Store 9 | .DS_Store? 10 | ._* 11 | .Spotlight-V100 12 | .Trashes 13 | ehthumbs.db 14 | Thumbs.db 15 | 16 | # IntelliJ 17 | .idea/ 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Surya Kant Bansal 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 | name = "pypi" 3 | url = "https://pypi.org/simple" 4 | verify_ssl = true 5 | 6 | [dev-packages] 7 | pylint = "*" 8 | pytest = "*" 9 | 10 | [packages] 11 | fastapi = "*" 12 | alembic = "*" 13 | uvicorn = "*" 14 | sqlalchemy = "*" 15 | pydantic = "*" 16 | psycopg2-binary = "*" 17 | python-multipart = "*" 18 | requests = "*" 19 | python-dotenv = "*" 20 | 21 | [requires] 22 | python_version = "3.8" 23 | -------------------------------------------------------------------------------- /Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "beeb089bf9465a167cee308616a7f437217f061ee475c9df67cada8f6052709c" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3.8" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": { 19 | "alembic": { 20 | "hashes": [ 21 | "sha256:4e02ed2aa796bd179965041afa092c55b51fb077de19d61835673cc80672c01c", 22 | "sha256:5334f32314fb2a56d86b4c4dd1ae34b08c03cae4cb888bc699942104d66bc245" 23 | ], 24 | "index": "pypi", 25 | "version": "==1.4.3" 26 | }, 27 | "certifi": { 28 | "hashes": [ 29 | "sha256:5930595817496dd21bb8dc35dad090f1c2cd0adfaf21204bf6732ca5d8ee34d3", 30 | "sha256:8fc0819f1f30ba15bdb34cceffb9ef04d99f420f68eb75d901e9560b8749fc41" 31 | ], 32 | "version": "==2020.6.20" 33 | }, 34 | "chardet": { 35 | "hashes": [ 36 | "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", 37 | "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" 38 | ], 39 | "version": "==3.0.4" 40 | }, 41 | "click": { 42 | "hashes": [ 43 | "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a", 44 | "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc" 45 | ], 46 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", 47 | "version": "==7.1.2" 48 | }, 49 | "fastapi": { 50 | "hashes": [ 51 | "sha256:61ed73b4304413a2ea618d1b95ea866ee386e0e62dd8659c4f5059286f4a39c2", 52 | "sha256:6cc31bb555dd8ca956d1d227477d661e4ac012337242a41d36214ffbda78bfe9" 53 | ], 54 | "index": "pypi", 55 | "version": "==0.61.1" 56 | }, 57 | "h11": { 58 | "hashes": [ 59 | "sha256:311dc5478c2568cc07262e0381cdfc5b9c6ba19775905736c87e81ae6662b9fd", 60 | "sha256:9eecfbafc980976dbff26a01dd3487644dd5d00f8038584451fc64a660f7c502" 61 | ], 62 | "version": "==0.10.0" 63 | }, 64 | "idna": { 65 | "hashes": [ 66 | "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6", 67 | "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0" 68 | ], 69 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 70 | "version": "==2.10" 71 | }, 72 | "mako": { 73 | "hashes": [ 74 | "sha256:8195c8c1400ceb53496064314c6736719c6f25e7479cd24c77be3d9361cddc27", 75 | "sha256:93729a258e4ff0747c876bd9e20df1b9758028946e976324ccd2d68245c7b6a9" 76 | ], 77 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 78 | "version": "==1.1.3" 79 | }, 80 | "markupsafe": { 81 | "hashes": [ 82 | "sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473", 83 | "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161", 84 | "sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235", 85 | "sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5", 86 | "sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42", 87 | "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff", 88 | "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b", 89 | "sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1", 90 | "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e", 91 | "sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183", 92 | "sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66", 93 | "sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b", 94 | "sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1", 95 | "sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15", 96 | "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1", 97 | "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e", 98 | "sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b", 99 | "sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905", 100 | "sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735", 101 | "sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d", 102 | "sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e", 103 | "sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d", 104 | "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c", 105 | "sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21", 106 | "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2", 107 | "sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5", 108 | "sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b", 109 | "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6", 110 | "sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f", 111 | "sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f", 112 | "sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2", 113 | "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7", 114 | "sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be" 115 | ], 116 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 117 | "version": "==1.1.1" 118 | }, 119 | "psycopg2-binary": { 120 | "hashes": [ 121 | "sha256:0deac2af1a587ae12836aa07970f5cb91964f05a7c6cdb69d8425ff4c15d4e2c", 122 | "sha256:0e4dc3d5996760104746e6cfcdb519d9d2cd27c738296525d5867ea695774e67", 123 | "sha256:11b9c0ebce097180129e422379b824ae21c8f2a6596b159c7659e2e5a00e1aa0", 124 | "sha256:1fabed9ea2acc4efe4671b92c669a213db744d2af8a9fc5d69a8e9bc14b7a9db", 125 | "sha256:2dac98e85565d5688e8ab7bdea5446674a83a3945a8f416ad0110018d1501b94", 126 | "sha256:42ec1035841b389e8cc3692277a0bd81cdfe0b65d575a2c8862cec7a80e62e52", 127 | "sha256:6a32f3a4cb2f6e1a0b15215f448e8ce2da192fd4ff35084d80d5e39da683e79b", 128 | "sha256:7312e931b90fe14f925729cde58022f5d034241918a5c4f9797cac62f6b3a9dd", 129 | "sha256:7d92a09b788cbb1aec325af5fcba9fed7203897bbd9269d5691bb1e3bce29550", 130 | "sha256:833709a5c66ca52f1d21d41865a637223b368c0ee76ea54ca5bad6f2526c7679", 131 | "sha256:8cd0fb36c7412996859cb4606a35969dd01f4ea34d9812a141cd920c3b18be77", 132 | "sha256:950bc22bb56ee6ff142a2cb9ee980b571dd0912b0334aa3fe0fe3788d860bea2", 133 | "sha256:a0c50db33c32594305b0ef9abc0cb7db13de7621d2cadf8392a1d9b3c437ef77", 134 | "sha256:a0eb43a07386c3f1f1ebb4dc7aafb13f67188eab896e7397aa1ee95a9c884eb2", 135 | "sha256:aaa4213c862f0ef00022751161df35804127b78adf4a2755b9f991a507e425fd", 136 | "sha256:ac0c682111fbf404525dfc0f18a8b5f11be52657d4f96e9fcb75daf4f3984859", 137 | "sha256:ad20d2eb875aaa1ea6d0f2916949f5c08a19c74d05b16ce6ebf6d24f2c9f75d1", 138 | "sha256:b4afc542c0ac0db720cf516dd20c0846f71c248d2b3d21013aa0d4ef9c71ca25", 139 | "sha256:b8a3715b3c4e604bcc94c90a825cd7f5635417453b253499664f784fc4da0152", 140 | "sha256:ba28584e6bca48c59eecbf7efb1576ca214b47f05194646b081717fa628dfddf", 141 | "sha256:ba381aec3a5dc29634f20692349d73f2d21f17653bda1decf0b52b11d694541f", 142 | "sha256:bd1be66dde2b82f80afb9459fc618216753f67109b859a361cf7def5c7968729", 143 | "sha256:c2507d796fca339c8fb03216364cca68d87e037c1f774977c8fc377627d01c71", 144 | "sha256:cec7e622ebc545dbb4564e483dd20e4e404da17ae07e06f3e780b2dacd5cee66", 145 | "sha256:d14b140a4439d816e3b1229a4a525df917d6ea22a0771a2a78332273fd9528a4", 146 | "sha256:d1b4ab59e02d9008efe10ceabd0b31e79519da6fb67f7d8e8977118832d0f449", 147 | "sha256:d5227b229005a696cc67676e24c214740efd90b148de5733419ac9aaba3773da", 148 | "sha256:e1f57aa70d3f7cc6947fd88636a481638263ba04a742b4a37dd25c373e41491a", 149 | "sha256:e74a55f6bad0e7d3968399deb50f61f4db1926acf4a6d83beaaa7df986f48b1c", 150 | "sha256:e82aba2188b9ba309fd8e271702bd0d0fc9148ae3150532bbb474f4590039ffb", 151 | "sha256:ee69dad2c7155756ad114c02db06002f4cded41132cc51378e57aad79cc8e4f4", 152 | "sha256:f5ab93a2cb2d8338b1674be43b442a7f544a0971da062a5da774ed40587f18f5" 153 | ], 154 | "index": "pypi", 155 | "version": "==2.8.6" 156 | }, 157 | "pydantic": { 158 | "hashes": [ 159 | "sha256:1783c1d927f9e1366e0e0609ae324039b2479a1a282a98ed6a6836c9ed02002c", 160 | "sha256:2dc946b07cf24bee4737ced0ae77e2ea6bc97489ba5a035b603bd1b40ad81f7e", 161 | "sha256:2de562a456c4ecdc80cf1a8c3e70c666625f7d02d89a6174ecf63754c734592e", 162 | "sha256:36dbf6f1be212ab37b5fda07667461a9219c956181aa5570a00edfb0acdfe4a1", 163 | "sha256:3fa799f3cfff3e5f536cbd389368fc96a44bb30308f258c94ee76b73bd60531d", 164 | "sha256:40d765fa2d31d5be8e29c1794657ad46f5ee583a565c83cea56630d3ae5878b9", 165 | "sha256:418b84654b60e44c0cdd5384294b0e4bc1ebf42d6e873819424f3b78b8690614", 166 | "sha256:4900b8820b687c9a3ed753684337979574df20e6ebe4227381d04b3c3c628f99", 167 | "sha256:530d7222a2786a97bc59ee0e0ebbe23728f82974b1f1ad9a11cd966143410633", 168 | "sha256:54122a8ed6b75fe1dd80797f8251ad2063ea348a03b77218d73ea9fe19bd4e73", 169 | "sha256:6c3f162ba175678218629f446a947e3356415b6b09122dcb364e58c442c645a7", 170 | "sha256:b49c86aecde15cde33835d5d6360e55f5e0067bb7143a8303bf03b872935c75b", 171 | "sha256:b5b3489cb303d0f41ad4a7390cf606a5f2c7a94dcba20c051cd1c653694cb14d", 172 | "sha256:cf3933c98cb5e808b62fae509f74f209730b180b1e3c3954ee3f7949e083a7df", 173 | "sha256:eb75dc1809875d5738df14b6566ccf9fd9c0bcde4f36b72870f318f16b9f5c20", 174 | "sha256:f769141ab0abfadf3305d4fcf36660e5cf568a666dd3efab7c3d4782f70946b1", 175 | "sha256:f8af9b840a9074e08c0e6dc93101de84ba95df89b267bf7151d74c553d66833b" 176 | ], 177 | "index": "pypi", 178 | "version": "==1.6.1" 179 | }, 180 | "python-dateutil": { 181 | "hashes": [ 182 | "sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c", 183 | "sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a" 184 | ], 185 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 186 | "version": "==2.8.1" 187 | }, 188 | "python-dotenv": { 189 | "hashes": [ 190 | "sha256:8c10c99a1b25d9a68058a1ad6f90381a62ba68230ca93966882a4dbc3bc9c33d", 191 | "sha256:c10863aee750ad720f4f43436565e4c1698798d763b63234fb5021b6c616e423" 192 | ], 193 | "index": "pypi", 194 | "version": "==0.14.0" 195 | }, 196 | "python-editor": { 197 | "hashes": [ 198 | "sha256:1bf6e860a8ad52a14c3ee1252d5dc25b2030618ed80c022598f00176adc8367d", 199 | "sha256:51fda6bcc5ddbbb7063b2af7509e43bd84bfc32a4ff71349ec7847713882327b", 200 | "sha256:5f98b069316ea1c2ed3f67e7f5df6c0d8f10b689964a4a811ff64f0106819ec8", 201 | "sha256:c3da2053dbab6b29c94e43c486ff67206eafbe7eb52dbec7390b5e2fb05aac77", 202 | "sha256:ea87e17f6ec459e780e4221f295411462e0d0810858e055fc514684350a2f522" 203 | ], 204 | "version": "==1.0.4" 205 | }, 206 | "python-multipart": { 207 | "hashes": [ 208 | "sha256:f7bb5f611fc600d15fa47b3974c8aa16e93724513b49b5f95c81e6624c83fa43" 209 | ], 210 | "index": "pypi", 211 | "version": "==0.0.5" 212 | }, 213 | "requests": { 214 | "hashes": [ 215 | "sha256:b3559a131db72c33ee969480840fff4bb6dd111de7dd27c8ee1f820f4f00231b", 216 | "sha256:fe75cc94a9443b9246fc7049224f75604b113c36acb93f87b80ed42c44cbb898" 217 | ], 218 | "index": "pypi", 219 | "version": "==2.24.0" 220 | }, 221 | "six": { 222 | "hashes": [ 223 | "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", 224 | "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced" 225 | ], 226 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 227 | "version": "==1.15.0" 228 | }, 229 | "sqlalchemy": { 230 | "hashes": [ 231 | "sha256:072766c3bd09294d716b2d114d46ffc5ccf8ea0b714a4e1c48253014b771c6bb", 232 | "sha256:107d4af989831d7b091e382d192955679ec07a9209996bf8090f1f539ffc5804", 233 | "sha256:15c0bcd3c14f4086701c33a9e87e2c7ceb3bcb4a246cd88ec54a49cf2a5bd1a6", 234 | "sha256:26c5ca9d09f0e21b8671a32f7d83caad5be1f6ff45eef5ec2f6fd0db85fc5dc0", 235 | "sha256:276936d41111a501cf4a1a0543e25449108d87e9f8c94714f7660eaea89ae5fe", 236 | "sha256:3292a28344922415f939ee7f4fc0c186f3d5a0bf02192ceabd4f1129d71b08de", 237 | "sha256:33d29ae8f1dc7c75b191bb6833f55a19c932514b9b5ce8c3ab9bc3047da5db36", 238 | "sha256:3bba2e9fbedb0511769780fe1d63007081008c5c2d7d715e91858c94dbaa260e", 239 | "sha256:465c999ef30b1c7525f81330184121521418a67189053bcf585824d833c05b66", 240 | "sha256:51064ee7938526bab92acd049d41a1dc797422256086b39c08bafeffb9d304c6", 241 | "sha256:5a49e8473b1ab1228302ed27365ea0fadd4bf44bc0f9e73fe38e10fdd3d6b4fc", 242 | "sha256:618db68745682f64cedc96ca93707805d1f3a031747b5a0d8e150cfd5055ae4d", 243 | "sha256:6547b27698b5b3bbfc5210233bd9523de849b2bb8a0329cd754c9308fc8a05ce", 244 | "sha256:6557af9e0d23f46b8cd56f8af08eaac72d2e3c632ac8d5cf4e20215a8dca7cea", 245 | "sha256:73a40d4fcd35fdedce07b5885905753d5d4edf413fbe53544dd871f27d48bd4f", 246 | "sha256:8280f9dae4adb5889ce0bb3ec6a541bf05434db5f9ab7673078c00713d148365", 247 | "sha256:83469ad15262402b0e0974e612546bc0b05f379b5aa9072ebf66d0f8fef16bea", 248 | "sha256:860d0fe234922fd5552b7f807fbb039e3e7ca58c18c8d38aa0d0a95ddf4f6c23", 249 | "sha256:883c9fb62cebd1e7126dd683222b3b919657590c3e2db33bdc50ebbad53e0338", 250 | "sha256:8afcb6f4064d234a43fea108859942d9795c4060ed0fbd9082b0f280181a15c1", 251 | "sha256:96f51489ac187f4bab588cf51f9ff2d40b6d170ac9a4270ffaed535c8404256b", 252 | "sha256:9e865835e36dfbb1873b65e722ea627c096c11b05f796831e3a9b542926e979e", 253 | "sha256:aa0554495fe06172b550098909be8db79b5accdf6ffb59611900bea345df5eba", 254 | "sha256:b595e71c51657f9ee3235db8b53d0b57c09eee74dfb5b77edff0e46d2218dc02", 255 | "sha256:b6ff91356354b7ff3bd208adcf875056d3d886ed7cef90c571aef2ab8a554b12", 256 | "sha256:b70bad2f1a5bd3460746c3fb3ab69e4e0eb5f59d977a23f9b66e5bdc74d97b86", 257 | "sha256:c7adb1f69a80573698c2def5ead584138ca00fff4ad9785a4b0b2bf927ba308d", 258 | "sha256:c898b3ebcc9eae7b36bd0b4bbbafce2d8076680f6868bcbacee2d39a7a9726a7", 259 | "sha256:e49947d583fe4d29af528677e4f0aa21f5e535ca2ae69c48270ebebd0d8843c0", 260 | "sha256:eb1d71643e4154398b02e88a42fc8b29db8c44ce4134cf0f4474bfc5cb5d4dac", 261 | "sha256:f2e8a9c0c8813a468aa659a01af6592f71cd30237ec27c4cc0683f089f90dcfc", 262 | "sha256:fe7fe11019fc3e6600819775a7d55abc5446dda07e9795f5954fdbf8a49e1c37" 263 | ], 264 | "index": "pypi", 265 | "version": "==1.3.19" 266 | }, 267 | "starlette": { 268 | "hashes": [ 269 | "sha256:bd2ffe5e37fb75d014728511f8e68ebf2c80b0fa3d04ca1479f4dc752ae31ac9", 270 | "sha256:ebe8ee08d9be96a3c9f31b2cb2a24dbdf845247b745664bd8a3f9bd0c977fdbc" 271 | ], 272 | "markers": "python_version >= '3.6'", 273 | "version": "==0.13.6" 274 | }, 275 | "urllib3": { 276 | "hashes": [ 277 | "sha256:91056c15fa70756691db97756772bb1eb9678fa585d9184f24534b100dc60f4a", 278 | "sha256:e7983572181f5e1522d9c98453462384ee92a0be7fac5f1413a1e35c56cc0461" 279 | ], 280 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'", 281 | "version": "==1.25.10" 282 | }, 283 | "uvicorn": { 284 | "hashes": [ 285 | "sha256:a461e76406088f448f36323f5ac774d50e5a552b6ccb54e4fca8d83ef614a7c2", 286 | "sha256:d06a25caa8dc680ad92eb3ec67363f5281c092059613a1cc0100acba37fc0f45" 287 | ], 288 | "index": "pypi", 289 | "version": "==0.12.1" 290 | } 291 | }, 292 | "develop": { 293 | "astroid": { 294 | "hashes": [ 295 | "sha256:2f4078c2a41bf377eea06d71c9d2ba4eb8f6b1af2135bec27bbbb7d8f12bb703", 296 | "sha256:bc58d83eb610252fd8de6363e39d4f1d0619c894b0ed24603b881c02e64c7386" 297 | ], 298 | "markers": "python_version >= '3.5'", 299 | "version": "==2.4.2" 300 | }, 301 | "attrs": { 302 | "hashes": [ 303 | "sha256:26b54ddbbb9ee1d34d5d3668dd37d6cf74990ab23c828c2888dccdceee395594", 304 | "sha256:fce7fc47dfc976152e82d53ff92fa0407700c21acd20886a13777a0d20e655dc" 305 | ], 306 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 307 | "version": "==20.2.0" 308 | }, 309 | "iniconfig": { 310 | "hashes": [ 311 | "sha256:80cf40c597eb564e86346103f609d74efce0f6b4d4f30ec8ce9e2c26411ba437", 312 | "sha256:e5f92f89355a67de0595932a6c6c02ab4afddc6fcdc0bfc5becd0d60884d3f69" 313 | ], 314 | "version": "==1.0.1" 315 | }, 316 | "isort": { 317 | "hashes": [ 318 | "sha256:36f0c6659b9000597e92618d05b72d4181104cf59472b1c6a039e3783f930c95", 319 | "sha256:ba040c24d20aa302f78f4747df549573ae1eaf8e1084269199154da9c483f07f" 320 | ], 321 | "markers": "python_version >= '3.6' and python_version < '4.0'", 322 | "version": "==5.5.4" 323 | }, 324 | "lazy-object-proxy": { 325 | "hashes": [ 326 | "sha256:0c4b206227a8097f05c4dbdd323c50edf81f15db3b8dc064d08c62d37e1a504d", 327 | "sha256:194d092e6f246b906e8f70884e620e459fc54db3259e60cf69a4d66c3fda3449", 328 | "sha256:1be7e4c9f96948003609aa6c974ae59830a6baecc5376c25c92d7d697e684c08", 329 | "sha256:4677f594e474c91da97f489fea5b7daa17b5517190899cf213697e48d3902f5a", 330 | "sha256:48dab84ebd4831077b150572aec802f303117c8cc5c871e182447281ebf3ac50", 331 | "sha256:5541cada25cd173702dbd99f8e22434105456314462326f06dba3e180f203dfd", 332 | "sha256:59f79fef100b09564bc2df42ea2d8d21a64fdcda64979c0fa3db7bdaabaf6239", 333 | "sha256:8d859b89baf8ef7f8bc6b00aa20316483d67f0b1cbf422f5b4dc56701c8f2ffb", 334 | "sha256:9254f4358b9b541e3441b007a0ea0764b9d056afdeafc1a5569eee1cc6c1b9ea", 335 | "sha256:9651375199045a358eb6741df3e02a651e0330be090b3bc79f6d0de31a80ec3e", 336 | "sha256:97bb5884f6f1cdce0099f86b907aa41c970c3c672ac8b9c8352789e103cf3156", 337 | "sha256:9b15f3f4c0f35727d3a0fba4b770b3c4ebbb1fa907dbcc046a1d2799f3edd142", 338 | "sha256:a2238e9d1bb71a56cd710611a1614d1194dc10a175c1e08d75e1a7bcc250d442", 339 | "sha256:a6ae12d08c0bf9909ce12385803a543bfe99b95fe01e752536a60af2b7797c62", 340 | "sha256:ca0a928a3ddbc5725be2dd1cf895ec0a254798915fb3a36af0964a0a4149e3db", 341 | "sha256:cb2c7c57005a6804ab66f106ceb8482da55f5314b7fcb06551db1edae4ad1531", 342 | "sha256:d74bb8693bf9cf75ac3b47a54d716bbb1a92648d5f781fc799347cfc95952383", 343 | "sha256:d945239a5639b3ff35b70a88c5f2f491913eb94871780ebfabb2568bd58afc5a", 344 | "sha256:eba7011090323c1dadf18b3b689845fd96a61ba0a1dfbd7f24b921398affc357", 345 | "sha256:efa1909120ce98bbb3777e8b6f92237f5d5c8ea6758efea36a473e1d38f7d3e4", 346 | "sha256:f3900e8a5de27447acbf900b4750b0ddfd7ec1ea7fbaf11dfa911141bc522af0" 347 | ], 348 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 349 | "version": "==1.4.3" 350 | }, 351 | "mccabe": { 352 | "hashes": [ 353 | "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", 354 | "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" 355 | ], 356 | "version": "==0.6.1" 357 | }, 358 | "packaging": { 359 | "hashes": [ 360 | "sha256:4357f74f47b9c12db93624a82154e9b120fa8293699949152b22065d556079f8", 361 | "sha256:998416ba6962ae7fbd6596850b80e17859a5753ba17c32284f67bfff33784181" 362 | ], 363 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 364 | "version": "==20.4" 365 | }, 366 | "pluggy": { 367 | "hashes": [ 368 | "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0", 369 | "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d" 370 | ], 371 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 372 | "version": "==0.13.1" 373 | }, 374 | "py": { 375 | "hashes": [ 376 | "sha256:366389d1db726cd2fcfc79732e75410e5fe4d31db13692115529d34069a043c2", 377 | "sha256:9ca6883ce56b4e8da7e79ac18787889fa5206c79dcc67fb065376cd2fe03f342" 378 | ], 379 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 380 | "version": "==1.9.0" 381 | }, 382 | "pylint": { 383 | "hashes": [ 384 | "sha256:bb4a908c9dadbc3aac18860550e870f58e1a02c9f2c204fdf5693d73be061210", 385 | "sha256:bfe68f020f8a0fece830a22dd4d5dddb4ecc6137db04face4c3420a46a52239f" 386 | ], 387 | "index": "pypi", 388 | "version": "==2.6.0" 389 | }, 390 | "pyparsing": { 391 | "hashes": [ 392 | "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1", 393 | "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b" 394 | ], 395 | "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", 396 | "version": "==2.4.7" 397 | }, 398 | "pytest": { 399 | "hashes": [ 400 | "sha256:1cd09785c0a50f9af72220dd12aa78cfa49cbffc356c61eab009ca189e018a33", 401 | "sha256:d010e24666435b39a4cf48740b039885642b6c273a3f77be3e7e03554d2806b7" 402 | ], 403 | "index": "pypi", 404 | "version": "==6.1.0" 405 | }, 406 | "six": { 407 | "hashes": [ 408 | "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", 409 | "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced" 410 | ], 411 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 412 | "version": "==1.15.0" 413 | }, 414 | "toml": { 415 | "hashes": [ 416 | "sha256:926b612be1e5ce0634a2ca03470f95169cf16f939018233a670519cb4ac58b0f", 417 | "sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88" 418 | ], 419 | "version": "==0.10.1" 420 | }, 421 | "wrapt": { 422 | "hashes": [ 423 | "sha256:b62ffa81fb85f4332a4f609cab4ac40709470da05643a082ec1eb88e6d9b97d7" 424 | ], 425 | "version": "==1.12.1" 426 | } 427 | } 428 | } 429 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FastAPI Boilerplate 2 | 3 | A template to start on FastAPI backend projects. 4 | 5 | ## Getting Started 6 | 7 | These instructions will get you a copy of the project up and running on your local machine for development and testing purposes. See deployment for notes on how to deploy the project on a live system. 8 | 9 | ### Prerequisites 10 | 11 | You'll need `python@3.8`, `pipenv` and `postgresql@12` installed on your system to run the project. 12 | 13 | ### Installing 14 | 15 | Run the following command to install all the project dependencies. 16 | ```shell script 17 | pipenv install 18 | ``` 19 | Make sure your local PostgreSQL server is running on `http://localhost:5432`. Then, create a new database called `fastapi_db`. 20 | ```shell script 21 | psql postgres 22 | postgres=# create database fastapi_db; 23 | ``` 24 | **Note:** If you have a different database URL, set it in the `.env` environment file. 25 | 26 | Now, run the `prestart.sh` script that'll create the tables and add initial data. 27 | ```shell script 28 | ./prestart.sh 29 | ``` 30 | If there are any changes to the `SQLALCHEMY_DATABASE_URI` key in the `.env` file, please run the `prestart.sh` script again. 31 | 32 | ### Running 33 | 34 | After all the above mentioned steps, you can start the application using the following command: 35 | ```shell script 36 | python -m app.main 37 | ``` 38 | The application will be available at https://localhost:8000. 39 | 40 | ## Development 41 | 42 | These instructions will provide you some useful information on developing this application. 43 | 44 | ### Migrations 45 | 46 | If there are any changes to the SQLAlchemy ORM models, you can run the following command to generate `alembic` migrations. 47 | ```shell script 48 | alembic revision --autogenerate -m "" 49 | ``` 50 | This command will generate a new migration file in the `migrations` directory. Remember to check the generated migration file before committing. 51 | 52 | ## Testing 53 | 54 | The application unit tests are inside the `app/tests` module. 55 | 56 | Run the following command in the terminal to execute the application unit tests. 57 | ```shell script 58 | pytest app/tests 59 | ``` 60 | 61 | ## Deployment 62 | 63 | The application can be deployed in production using `gunicorn`, you don't need to make any code changes for the same. 64 | Head over to the [Uvicorn Deployment](https://www.uvicorn.org/deployment/) documentation for complete instructions. 65 | 66 | ## Built With 67 | 68 | * [FastAPI](https://fastapi.tiangolo.com/) - The API framework used 69 | * [SQLAlchemy](https://www.sqlalchemy.org/) - Database ORM 70 | * [Pipenv](https://pypi.org/project/pipenv/) - Dependency and virtual environment manager 71 | 72 | ## Authors 73 | 74 | * **Surya Kant Bansal** - *Initial work* - [skb1129](https://github.com/skb1129) 75 | 76 | ## License 77 | 78 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details 79 | -------------------------------------------------------------------------------- /alembic.ini: -------------------------------------------------------------------------------- 1 | # A generic, single database configuration. 2 | 3 | [alembic] 4 | # path to migration scripts 5 | script_location = migrations 6 | 7 | # template used to generate migration files 8 | # file_template = %%(rev)s_%%(slug)s 9 | 10 | # timezone to use when rendering the date 11 | # within the migration file as well as the filename. 12 | # string value is passed to dateutil.tz.gettz() 13 | # leave blank for localtime 14 | # timezone = 15 | 16 | # max length of characters to apply to the 17 | # "slug" field 18 | # truncate_slug_length = 40 19 | 20 | # set to 'true' to run the environment during 21 | # the 'revision' command, regardless of autogenerate 22 | # revision_environment = false 23 | 24 | # set to 'true' to allow .pyc and .pyo files without 25 | # a source .py file to be detected as revisions in the 26 | # versions/ directory 27 | # sourceless = false 28 | 29 | # version location specification; this defaults 30 | # to migrations/versions. When using multiple version 31 | # directories, initial revisions must be specified with --version-path 32 | # version_locations = %(here)s/bar %(here)s/bat migrations/versions 33 | 34 | # the output encoding used when revision files 35 | # are written from script.py.mako 36 | # output_encoding = utf-8 37 | 38 | [post_write_hooks] 39 | # post_write_hooks defines scripts or Python functions that are run 40 | # on newly generated revision scripts. See the documentation for further 41 | # detail and examples 42 | 43 | # format using "black" - use the console_scripts runner, against the "black" entrypoint 44 | # hooks=black 45 | # black.type=console_scripts 46 | # black.entrypoint=black 47 | # black.options=-l 79 48 | 49 | # Logging configuration 50 | [loggers] 51 | keys = root,sqlalchemy,alembic 52 | 53 | [handlers] 54 | keys = console 55 | 56 | [formatters] 57 | keys = generic 58 | 59 | [logger_root] 60 | level = WARN 61 | handlers = console 62 | qualname = 63 | 64 | [logger_sqlalchemy] 65 | level = WARN 66 | handlers = 67 | qualname = sqlalchemy.engine 68 | 69 | [logger_alembic] 70 | level = INFO 71 | handlers = 72 | qualname = alembic 73 | 74 | [handler_console] 75 | class = StreamHandler 76 | args = (sys.stderr,) 77 | level = NOTSET 78 | formatter = generic 79 | 80 | [formatter_generic] 81 | format = %(levelname)-5.5s [%(name)s] %(message)s 82 | datefmt = %H:%M:%S 83 | -------------------------------------------------------------------------------- /app/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skb1129/fastapi-boilerplate/63392b338531ed4ca7f0ab62fb618051235d80c1/app/__init__.py -------------------------------------------------------------------------------- /app/api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skb1129/fastapi-boilerplate/63392b338531ed4ca7f0ab62fb618051235d80c1/app/api/__init__.py -------------------------------------------------------------------------------- /app/api/deps.py: -------------------------------------------------------------------------------- 1 | from typing import Generator 2 | 3 | from app.database.session import SessionLocal 4 | 5 | 6 | def get_db() -> Generator: 7 | try: 8 | db = SessionLocal() 9 | yield db 10 | finally: 11 | db.close() 12 | -------------------------------------------------------------------------------- /app/api/v1/__init__.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter 2 | 3 | from app.api.v1 import products 4 | 5 | api_router = APIRouter() 6 | api_router.include_router(products.router, prefix="/products", tags=["products"]) 7 | -------------------------------------------------------------------------------- /app/api/v1/products.py: -------------------------------------------------------------------------------- 1 | from typing import Any, List 2 | 3 | from fastapi import APIRouter, Depends, HTTPException, status 4 | from sqlalchemy.orm import Session 5 | 6 | from app import schemas, crud 7 | from app.api.deps import get_db 8 | 9 | router = APIRouter() 10 | 11 | 12 | @router.get("", response_model=List[schemas.ProductResponse]) 13 | def read_products(db: Session = Depends(get_db), skip: int = 0, limit: int = 100) -> Any: 14 | """ 15 | Retrieve all products. 16 | """ 17 | products = crud.product.get_multi(db, skip=skip, limit=limit) 18 | return products 19 | 20 | 21 | @router.post("", response_model=schemas.ProductResponse) 22 | def create_product(*, db: Session = Depends(get_db), product_in: schemas.ProductCreate) -> Any: 23 | """ 24 | Create new products. 25 | """ 26 | product = crud.product.create(db, obj_in=product_in) 27 | return product 28 | 29 | 30 | @router.put("", response_model=schemas.ProductResponse) 31 | def update_product(*, db: Session = Depends(get_db), product_in: schemas.ProductUpdate) -> Any: 32 | """ 33 | Update existing products. 34 | """ 35 | product = crud.product.get(db, model_id=product_in.id) 36 | if not product: 37 | raise HTTPException( 38 | status_code=status.HTTP_404_NOT_FOUND, 39 | detail="The product with this ID does not exist in the system.", 40 | ) 41 | product = crud.product.update(db, db_obj=product, obj_in=product_in) 42 | return product 43 | 44 | 45 | @router.delete("", response_model=schemas.Message) 46 | def delete_product(*, db: Session = Depends(get_db), id: int) -> Any: 47 | """ 48 | Delete existing product. 49 | """ 50 | product = crud.product.get(db, model_id=id) 51 | if not product: 52 | raise HTTPException( 53 | status_code=status.HTTP_404_NOT_FOUND, 54 | detail="The product with this ID does not exist in the system.", 55 | ) 56 | crud.product.remove(db, model_id=product.id) 57 | return {"message": f"Product with ID = {id} deleted."} 58 | -------------------------------------------------------------------------------- /app/core/__init__.py: -------------------------------------------------------------------------------- 1 | from app.core.config import settings 2 | -------------------------------------------------------------------------------- /app/core/config.py: -------------------------------------------------------------------------------- 1 | import secrets 2 | 3 | from pydantic import BaseSettings 4 | 5 | 6 | class Settings(BaseSettings): 7 | API_V1_STR: str = "/api/v1" 8 | PROJECT_NAME: str = "FastAPI" 9 | SECRET_KEY: str = secrets.token_urlsafe(32) 10 | SQLALCHEMY_DATABASE_URI: str = "postgresql://localhost:5432/fastapi_db" 11 | 12 | class Config: 13 | env_file = ".env" 14 | 15 | 16 | settings = Settings() 17 | -------------------------------------------------------------------------------- /app/crud/__init__.py: -------------------------------------------------------------------------------- 1 | from app.crud.product import product 2 | -------------------------------------------------------------------------------- /app/crud/base.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Dict, Generic, List, Optional, Type, TypeVar, Union 2 | 3 | from fastapi.encoders import jsonable_encoder 4 | from pydantic import BaseModel 5 | from sqlalchemy.orm import Session 6 | 7 | from app.database.base_class import Base 8 | 9 | ModelType = TypeVar("ModelType", bound=Base) 10 | CreateSchemaType = TypeVar("CreateSchemaType", bound=BaseModel) 11 | UpdateSchemaType = TypeVar("UpdateSchemaType", bound=BaseModel) 12 | 13 | 14 | class CRUDBase(Generic[ModelType, CreateSchemaType, UpdateSchemaType]): 15 | def __init__(self, model: Type[ModelType]): 16 | """ 17 | CRUD object with default methods to Create, Read, Update, Delete (CRUD). 18 | **Parameters** 19 | * `model`: A SQLAlchemy model class 20 | * `schema`: A Pydantic model (schema) class 21 | """ 22 | self.model = model 23 | 24 | def get(self, db: Session, model_id: Any) -> Optional[ModelType]: 25 | return db.query(self.model).get(model_id) 26 | 27 | def get_multi(self, db: Session, *, skip: int = 0, limit: int = 100) -> List[ModelType]: 28 | return db.query(self.model).offset(skip).limit(limit).all() 29 | 30 | def create(self, db: Session, *, obj_in: CreateSchemaType) -> ModelType: 31 | obj_in_data = jsonable_encoder(obj_in) 32 | db_obj = self.model(**obj_in_data) 33 | db.add(db_obj) 34 | db.commit() 35 | db.refresh(db_obj) 36 | return db_obj 37 | 38 | def update(self, db: Session, *, db_obj: ModelType, obj_in: Union[UpdateSchemaType, Dict[str, Any]]) -> ModelType: 39 | obj_data = jsonable_encoder(db_obj) 40 | if isinstance(obj_in, dict): 41 | update_data = obj_in 42 | else: 43 | update_data = obj_in.dict(exclude_unset=True) 44 | for field in obj_data: 45 | if field in update_data: 46 | setattr(db_obj, field, update_data[field]) 47 | db.add(db_obj) 48 | db.commit() 49 | db.refresh(db_obj) 50 | return db_obj 51 | 52 | def remove(self, db: Session, *, model_id: int) -> ModelType: 53 | obj = db.query(self.model).get(model_id) 54 | db.delete(obj) 55 | db.commit() 56 | return obj 57 | -------------------------------------------------------------------------------- /app/crud/product.py: -------------------------------------------------------------------------------- 1 | from typing import Optional, List 2 | 3 | from sqlalchemy.orm import Session 4 | 5 | from app.crud.base import CRUDBase 6 | from app.models.product import Product 7 | from app.schemas import ProductCreate, ProductUpdate 8 | 9 | 10 | class CRUDProduct(CRUDBase[Product, ProductCreate, ProductUpdate]): 11 | # Declare model specific CRUD operation methods. 12 | pass 13 | 14 | 15 | product = CRUDProduct(Product) 16 | -------------------------------------------------------------------------------- /app/database/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skb1129/fastapi-boilerplate/63392b338531ed4ca7f0ab62fb618051235d80c1/app/database/__init__.py -------------------------------------------------------------------------------- /app/database/base.py: -------------------------------------------------------------------------------- 1 | from app.database.base_class import Base 2 | from app.models.product import Product 3 | -------------------------------------------------------------------------------- /app/database/base_class.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | 3 | from sqlalchemy.ext.declarative import as_declarative, declared_attr 4 | 5 | 6 | @as_declarative() 7 | class Base: 8 | id: Any 9 | __name__: str 10 | 11 | # Generate __tablename__ automatically 12 | @declared_attr 13 | def __tablename__(self) -> str: 14 | return self.__name__.lower() 15 | -------------------------------------------------------------------------------- /app/database/initialise.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy.orm import Session 2 | 3 | 4 | def initialise(db: Session) -> None: 5 | # Write database initialisation queries. 6 | pass 7 | -------------------------------------------------------------------------------- /app/database/session.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import create_engine 2 | from sqlalchemy.orm import sessionmaker 3 | 4 | from app.core import settings 5 | 6 | engine = create_engine(settings.SQLALCHEMY_DATABASE_URI, pool_pre_ping=True) 7 | SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) 8 | -------------------------------------------------------------------------------- /app/initialiser.py: -------------------------------------------------------------------------------- 1 | from app.database.initialise import initialise 2 | from app.database.session import SessionLocal 3 | 4 | 5 | def init() -> None: 6 | db = SessionLocal() 7 | initialise(db) 8 | 9 | 10 | def main() -> None: 11 | init() 12 | 13 | 14 | if __name__ == "__main__": 15 | main() 16 | -------------------------------------------------------------------------------- /app/main.py: -------------------------------------------------------------------------------- 1 | import uvicorn 2 | 3 | from fastapi import FastAPI 4 | 5 | from app.api.v1 import api_router 6 | from app.core import settings 7 | 8 | app = FastAPI(title=settings.PROJECT_NAME) 9 | 10 | app.include_router(api_router, prefix=settings.API_V1_STR) 11 | 12 | if __name__ == "__main__": 13 | uvicorn.run(app, host="0.0.0.0", port=8000) 14 | -------------------------------------------------------------------------------- /app/models/__init__.py: -------------------------------------------------------------------------------- 1 | from app.models.product import Product 2 | -------------------------------------------------------------------------------- /app/models/product.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import Column, Integer, String, Float 2 | 3 | from app.database.base_class import Base 4 | 5 | 6 | class Product(Base): 7 | id = Column(Integer, primary_key=True, index=True) 8 | name = Column(String, nullable=False) 9 | price = Column(Float, nullable=False) 10 | -------------------------------------------------------------------------------- /app/schemas/__init__.py: -------------------------------------------------------------------------------- 1 | from app.schemas.message import Message 2 | from app.schemas.product import ProductBase, ProductCreate, ProductUpdate, ProductResponse 3 | -------------------------------------------------------------------------------- /app/schemas/message.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel 2 | 3 | 4 | class Message(BaseModel): 5 | message: str 6 | -------------------------------------------------------------------------------- /app/schemas/product.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from pydantic import BaseModel 4 | 5 | 6 | class ProductBase(BaseModel): 7 | id: Optional[int] 8 | name: Optional[str] 9 | price: Optional[float] 10 | 11 | 12 | class ProductCreate(ProductBase): 13 | name: str 14 | price: float 15 | 16 | 17 | class ProductUpdate(ProductBase): 18 | id: int 19 | pass 20 | 21 | 22 | class ProductResponse(ProductBase): 23 | class Config: 24 | orm_mode = True 25 | -------------------------------------------------------------------------------- /app/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skb1129/fastapi-boilerplate/63392b338531ed4ca7f0ab62fb618051235d80c1/app/tests/__init__.py -------------------------------------------------------------------------------- /app/tests/api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skb1129/fastapi-boilerplate/63392b338531ed4ca7f0ab62fb618051235d80c1/app/tests/api/__init__.py -------------------------------------------------------------------------------- /app/tests/api/v1/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skb1129/fastapi-boilerplate/63392b338531ed4ca7f0ab62fb618051235d80c1/app/tests/api/v1/__init__.py -------------------------------------------------------------------------------- /app/tests/api/v1/test_products.py: -------------------------------------------------------------------------------- 1 | from typing import Dict 2 | 3 | from fastapi.testclient import TestClient 4 | 5 | from app.core import settings 6 | 7 | 8 | def test_create_product(client: TestClient, random_product: Dict[str, str]) -> None: 9 | response = client.post(f"{settings.API_V1_STR}/products", json=random_product) 10 | product = response.json() 11 | assert response.status_code == 200 12 | assert product.get("name") == random_product.get("name") 13 | assert product.get("price") == random_product.get("price") 14 | 15 | 16 | def test_read_products(client: TestClient) -> None: 17 | response = client.get(f"{settings.API_V1_STR}/products") 18 | products = response.json() 19 | assert response.status_code == 200 20 | assert len(products) > 0 21 | 22 | 23 | def test_update_product(client: TestClient, random_product: Dict[str, str]) -> None: 24 | random_product["price"] = 100 25 | response = client.put(f"{settings.API_V1_STR}/products", json=random_product) 26 | product = response.json() 27 | assert response.status_code == 200 28 | assert product.get("price") == random_product.get("price") 29 | 30 | 31 | def test_delete_product(client: TestClient, random_product: Dict[str, str]) -> None: 32 | response = client.delete(f"{settings.API_V1_STR}/products?id={random_product.get('id')}") 33 | message = response.json() 34 | assert response.status_code == 200 35 | assert "message" in message 36 | -------------------------------------------------------------------------------- /app/tests/conftest.py: -------------------------------------------------------------------------------- 1 | from typing import Dict, Generator 2 | 3 | import pytest 4 | 5 | from fastapi.testclient import TestClient 6 | 7 | from app.database.session import SessionLocal 8 | from app.main import app 9 | 10 | 11 | @pytest.fixture(scope="session") 12 | def db() -> Generator: 13 | yield SessionLocal() 14 | 15 | 16 | @pytest.fixture(scope="module") 17 | def client() -> Generator: 18 | with TestClient(app) as c: 19 | yield c 20 | 21 | 22 | @pytest.fixture(scope="module") 23 | def random_product() -> Dict[str, str]: 24 | return { 25 | "id": 1, 26 | "name": "Test Product", 27 | "price": 80, 28 | } 29 | -------------------------------------------------------------------------------- /migrations/README: -------------------------------------------------------------------------------- 1 | Generic single-database configuration. -------------------------------------------------------------------------------- /migrations/env.py: -------------------------------------------------------------------------------- 1 | from alembic import context 2 | from logging.config import fileConfig 3 | from sqlalchemy import engine_from_config, pool 4 | 5 | from app.core import settings 6 | from app.database.base import Base 7 | 8 | # this is the Alembic Config object, which provides 9 | # access to the values within the .ini file in use. 10 | config = context.config 11 | 12 | # Interpret the config file for Python logging. 13 | # This line sets up loggers basically. 14 | fileConfig(config.config_file_name) 15 | 16 | # add your model's MetaData object here 17 | # for 'autogenerate' support 18 | target_metadata = Base.metadata 19 | 20 | 21 | # other values from the config, defined by the needs of env.py, 22 | # can be acquired: 23 | # my_important_option = config.get_main_option("my_important_option") 24 | # ... etc. 25 | 26 | 27 | def run_migrations_offline(): 28 | """Run migrations in 'offline' mode. 29 | 30 | This configures the context with just a URL 31 | and not an Engine, though an Engine is acceptable 32 | here as well. By skipping the Engine creation 33 | we don't even need a DBAPI to be available. 34 | 35 | Calls to context.execute() here emit the given string to the 36 | script output. 37 | 38 | """ 39 | url = settings.SQLALCHEMY_DATABASE_URI 40 | context.configure(url=url, target_metadata=target_metadata, literal_binds=True, 41 | dialect_opts={"paramstyle": "named"}, compare_type=True) 42 | 43 | with context.begin_transaction(): 44 | context.run_migrations() 45 | 46 | 47 | def run_migrations_online(): 48 | """Run migrations in 'online' mode. 49 | 50 | In this scenario we need to create an Engine 51 | and associate a connection with the context. 52 | 53 | """ 54 | configuration = config.get_section(config.config_ini_section) 55 | configuration["sqlalchemy.url"] = settings.SQLALCHEMY_DATABASE_URI 56 | connectable = engine_from_config(configuration, prefix="sqlalchemy.", poolclass=pool.NullPool) 57 | 58 | with connectable.connect() as connection: 59 | context.configure(connection=connection, target_metadata=target_metadata, compare_type=True) 60 | 61 | with context.begin_transaction(): 62 | context.run_migrations() 63 | 64 | 65 | if context.is_offline_mode(): 66 | run_migrations_offline() 67 | else: 68 | run_migrations_online() 69 | -------------------------------------------------------------------------------- /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(): 20 | ${upgrades if upgrades else "pass"} 21 | 22 | 23 | def downgrade(): 24 | ${downgrades if downgrades else "pass"} 25 | -------------------------------------------------------------------------------- /migrations/versions/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skb1129/fastapi-boilerplate/63392b338531ed4ca7f0ab62fb618051235d80c1/migrations/versions/.gitkeep -------------------------------------------------------------------------------- /migrations/versions/eaae517c1e2f_initialise.py: -------------------------------------------------------------------------------- 1 | """initialise 2 | 3 | Revision ID: eaae517c1e2f 4 | Revises: 5 | Create Date: 2020-10-01 12:32:07.701738 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = 'eaae517c1e2f' 14 | down_revision = None 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | op.create_table('product', 22 | sa.Column('id', sa.Integer(), nullable=False), 23 | sa.Column('name', sa.String(), nullable=False), 24 | sa.Column('price', sa.Float(), nullable=False), 25 | sa.PrimaryKeyConstraint('id') 26 | ) 27 | op.create_index(op.f('ix_product_id'), 'product', ['id'], unique=False) 28 | # ### end Alembic commands ### 29 | 30 | 31 | def downgrade(): 32 | # ### commands auto generated by Alembic - please adjust! ### 33 | op.drop_index(op.f('ix_product_id'), table_name='product') 34 | op.drop_table('product') 35 | # ### end Alembic commands ### 36 | -------------------------------------------------------------------------------- /prestart.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | export PYTHONPATH=. 4 | 5 | # Run migrations 6 | alembic upgrade head 7 | 8 | # Create initial data in DB 9 | python -m app.initialiser 10 | --------------------------------------------------------------------------------