├── .gitignore ├── .python-version ├── Pipfile ├── Pipfile.lock ├── README.md ├── migrations ├── README ├── alembic.ini ├── env.py ├── script.py.mako └── versions │ └── 20190504002143_5b5edadb5f5f_create_users.py ├── my_app ├── __init__.py ├── app.py ├── config │ ├── __init__.py │ ├── app_config.py.sample │ └── depot.py ├── error_handling.py └── users │ ├── __init__.py │ ├── models.py │ └── views.py └── tests ├── __init__.py ├── conftest.py ├── support ├── __init__.py └── fixtures │ ├── __init__.py │ └── models.py └── users ├── __init__.py └── test_views.py /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | my_app/config/app_config.py 3 | tests/*.db 4 | .pytest_cache 5 | -------------------------------------------------------------------------------- /.python-version: -------------------------------------------------------------------------------- 1 | 3.10.7 2 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | name = "pypi" 3 | url = "https://pypi.org/simple" 4 | verify_ssl = true 5 | 6 | [dev-packages] 7 | ipython = "*" 8 | mypy = "*" 9 | 10 | [packages] 11 | filedepot = "*" 12 | flask = "*" 13 | boto3 = "*" 14 | flask-sqlalchemy = "*" 15 | alembic = "*" 16 | flask-migrate = "*" 17 | psycopg2 = "*" 18 | webargs = "*" 19 | marshmallow-jsonapi = "*" 20 | pytest = "*" 21 | 22 | [requires] 23 | python_version = "3.10" 24 | -------------------------------------------------------------------------------- /Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "740cfb54fb5d6ccc9afb639f3612dea01a419a0ce3857c562077a8a1333566ca" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3.10" 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:0a024d7f2de88d738d7395ff866997314c837be6104e90c5724350313dee4da4", 22 | "sha256:cd0b5e45b14b706426b833f06369b9a6d5ee03f826ec3238723ce8caaf6e5ffa" 23 | ], 24 | "index": "pypi", 25 | "version": "==1.8.1" 26 | }, 27 | "anyascii": { 28 | "hashes": [ 29 | "sha256:3b3beef6fc43d9036d3b0529050b0c48bfad8bc960e9e562d7223cfb94fe45d4", 30 | "sha256:9d5d32ef844fe225b8bc7cba7f950534fae4da27a9bf3a6bea2cb0ea46ce4730" 31 | ], 32 | "markers": "python_version >= '3.3'", 33 | "version": "==0.3.2" 34 | }, 35 | "attrs": { 36 | "hashes": [ 37 | "sha256:1f28b4522cdc2fb4256ac1a020c78acf9cba2c6b461ccd2c126f3aa8e8335d04", 38 | "sha256:6279836d581513a26f1bf235f9acd333bc9115683f14f7e8fae46c98fc50e015" 39 | ], 40 | "markers": "python_version >= '3.7'", 41 | "version": "==23.1.0" 42 | }, 43 | "boto3": { 44 | "hashes": [ 45 | "sha256:6194763348545bb1669ce8d03ba104be1ba822daa184613aa10b9303a6a79017", 46 | "sha256:be151711bbb4db53e85dd5bbe506002ce6f2f21fc4e45fcf6d2cf356d32cc4c6" 47 | ], 48 | "index": "pypi", 49 | "version": "==1.24.84" 50 | }, 51 | "botocore": { 52 | "hashes": [ 53 | "sha256:e41a81a18511f2f9181b2a9ab302a55c0effecccbef846c55aad0c47bfdbefb9", 54 | "sha256:fc0a13ef6042e890e361cf408759230f8574409bb51f81740d2e5d8ad5d1fbea" 55 | ], 56 | "markers": "python_version >= '3.7'", 57 | "version": "==1.27.96" 58 | }, 59 | "click": { 60 | "hashes": [ 61 | "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e", 62 | "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48" 63 | ], 64 | "markers": "python_version >= '3.7'", 65 | "version": "==8.1.3" 66 | }, 67 | "filedepot": { 68 | "hashes": [ 69 | "sha256:25316ecd352e16524b4d321dbad08e9beb563d5de5447f3ed312daec5d011849" 70 | ], 71 | "index": "pypi", 72 | "version": "==0.8.0" 73 | }, 74 | "flask": { 75 | "hashes": [ 76 | "sha256:58107ed83443e86067e41eff4631b058178191a355886f8e479e347fa1285fdf", 77 | "sha256:edee9b0a7ff26621bd5a8c10ff484ae28737a2410d99b0bb9a6850c7fb977aa0" 78 | ], 79 | "index": "pypi", 80 | "version": "==2.2.5" 81 | }, 82 | "flask-migrate": { 83 | "hashes": [ 84 | "sha256:57d6060839e3a7f150eaab6fe4e726d9e3e7cffe2150fb223d73f92421c6d1d9", 85 | "sha256:a6498706241aba6be7a251078de9cf166d74307bca41a4ca3e403c9d39e2f897" 86 | ], 87 | "index": "pypi", 88 | "version": "==3.1.0" 89 | }, 90 | "flask-sqlalchemy": { 91 | "hashes": [ 92 | "sha256:2bda44b43e7cacb15d4e05ff3cc1f8bc97936cc464623424102bfc2c35e95912", 93 | "sha256:f12c3d4cc5cc7fdcc148b9527ea05671718c3ea45d50c7e732cceb33f574b390" 94 | ], 95 | "index": "pypi", 96 | "version": "==2.5.1" 97 | }, 98 | "greenlet": { 99 | "hashes": [ 100 | "sha256:03a8f4f3430c3b3ff8d10a2a86028c660355ab637cee9333d63d66b56f09d52a", 101 | "sha256:0bf60faf0bc2468089bdc5edd10555bab6e85152191df713e2ab1fcc86382b5a", 102 | "sha256:18a7f18b82b52ee85322d7a7874e676f34ab319b9f8cce5de06067384aa8ff43", 103 | "sha256:18e98fb3de7dba1c0a852731c3070cf022d14f0d68b4c87a19cc1016f3bb8b33", 104 | "sha256:1a819eef4b0e0b96bb0d98d797bef17dc1b4a10e8d7446be32d1da33e095dbb8", 105 | "sha256:26fbfce90728d82bc9e6c38ea4d038cba20b7faf8a0ca53a9c07b67318d46088", 106 | "sha256:2780572ec463d44c1d3ae850239508dbeb9fed38e294c68d19a24d925d9223ca", 107 | "sha256:283737e0da3f08bd637b5ad058507e578dd462db259f7f6e4c5c365ba4ee9343", 108 | "sha256:2d4686f195e32d36b4d7cf2d166857dbd0ee9f3d20ae349b6bf8afc8485b3645", 109 | "sha256:2dd11f291565a81d71dab10b7033395b7a3a5456e637cf997a6f33ebdf06f8db", 110 | "sha256:30bcf80dda7f15ac77ba5af2b961bdd9dbc77fd4ac6105cee85b0d0a5fcf74df", 111 | "sha256:32e5b64b148966d9cccc2c8d35a671409e45f195864560829f395a54226408d3", 112 | "sha256:36abbf031e1c0f79dd5d596bfaf8e921c41df2bdf54ee1eed921ce1f52999a86", 113 | "sha256:3a06ad5312349fec0ab944664b01d26f8d1f05009566339ac6f63f56589bc1a2", 114 | "sha256:3a51c9751078733d88e013587b108f1b7a1fb106d402fb390740f002b6f6551a", 115 | "sha256:3c9b12575734155d0c09d6c3e10dbd81665d5c18e1a7c6597df72fd05990c8cf", 116 | "sha256:3f6ea9bd35eb450837a3d80e77b517ea5bc56b4647f5502cd28de13675ee12f7", 117 | "sha256:4b58adb399c4d61d912c4c331984d60eb66565175cdf4a34792cd9600f21b394", 118 | "sha256:4d2e11331fc0c02b6e84b0d28ece3a36e0548ee1a1ce9ddde03752d9b79bba40", 119 | "sha256:5454276c07d27a740c5892f4907c86327b632127dd9abec42ee62e12427ff7e3", 120 | "sha256:561091a7be172ab497a3527602d467e2b3fbe75f9e783d8b8ce403fa414f71a6", 121 | "sha256:6c3acb79b0bfd4fe733dff8bc62695283b57949ebcca05ae5c129eb606ff2d74", 122 | "sha256:703f18f3fda276b9a916f0934d2fb6d989bf0b4fb5a64825260eb9bfd52d78f0", 123 | "sha256:7492e2b7bd7c9b9916388d9df23fa49d9b88ac0640db0a5b4ecc2b653bf451e3", 124 | "sha256:76ae285c8104046b3a7f06b42f29c7b73f77683df18c49ab5af7983994c2dd91", 125 | "sha256:7cafd1208fdbe93b67c7086876f061f660cfddc44f404279c1585bbf3cdc64c5", 126 | "sha256:7efde645ca1cc441d6dc4b48c0f7101e8d86b54c8530141b09fd31cef5149ec9", 127 | "sha256:88d9ab96491d38a5ab7c56dd7a3cc37d83336ecc564e4e8816dbed12e5aaefc8", 128 | "sha256:8eab883b3b2a38cc1e050819ef06a7e6344d4a990d24d45bc6f2cf959045a45b", 129 | "sha256:910841381caba4f744a44bf81bfd573c94e10b3045ee00de0cbf436fe50673a6", 130 | "sha256:9190f09060ea4debddd24665d6804b995a9c122ef5917ab26e1566dcc712ceeb", 131 | "sha256:937e9020b514ceedb9c830c55d5c9872abc90f4b5862f89c0887033ae33c6f73", 132 | "sha256:94c817e84245513926588caf1152e3b559ff794d505555211ca041f032abbb6b", 133 | "sha256:971ce5e14dc5e73715755d0ca2975ac88cfdaefcaab078a284fea6cfabf866df", 134 | "sha256:9d14b83fab60d5e8abe587d51c75b252bcc21683f24699ada8fb275d7712f5a9", 135 | "sha256:9f35ec95538f50292f6d8f2c9c9f8a3c6540bbfec21c9e5b4b751e0a7c20864f", 136 | "sha256:a1846f1b999e78e13837c93c778dcfc3365902cfb8d1bdb7dd73ead37059f0d0", 137 | "sha256:acd2162a36d3de67ee896c43effcd5ee3de247eb00354db411feb025aa319857", 138 | "sha256:b0ef99cdbe2b682b9ccbb964743a6aca37905fda5e0452e5ee239b1654d37f2a", 139 | "sha256:b80f600eddddce72320dbbc8e3784d16bd3fb7b517e82476d8da921f27d4b249", 140 | "sha256:b864ba53912b6c3ab6bcb2beb19f19edd01a6bfcbdfe1f37ddd1778abfe75a30", 141 | "sha256:b9ec052b06a0524f0e35bd8790686a1da006bd911dd1ef7d50b77bfbad74e292", 142 | "sha256:ba2956617f1c42598a308a84c6cf021a90ff3862eddafd20c3333d50f0edb45b", 143 | "sha256:bdfea8c661e80d3c1c99ad7c3ff74e6e87184895bbaca6ee8cc61209f8b9b85d", 144 | "sha256:be4ed120b52ae4d974aa40215fcdfde9194d63541c7ded40ee12eb4dda57b76b", 145 | "sha256:c4302695ad8027363e96311df24ee28978162cdcdd2006476c43970b384a244c", 146 | "sha256:c48f54ef8e05f04d6eff74b8233f6063cb1ed960243eacc474ee73a2ea8573ca", 147 | "sha256:c9c59a2120b55788e800d82dfa99b9e156ff8f2227f07c5e3012a45a399620b7", 148 | "sha256:cd021c754b162c0fb55ad5d6b9d960db667faad0fa2ff25bb6e1301b0b6e6a75", 149 | "sha256:d27ec7509b9c18b6d73f2f5ede2622441de812e7b1a80bbd446cb0633bd3d5ae", 150 | "sha256:d5508f0b173e6aa47273bdc0a0b5ba055b59662ba7c7ee5119528f466585526b", 151 | "sha256:d75209eed723105f9596807495d58d10b3470fa6732dd6756595e89925ce2470", 152 | "sha256:db1a39669102a1d8d12b57de2bb7e2ec9066a6f2b3da35ae511ff93b01b5d564", 153 | "sha256:dbfcfc0218093a19c252ca8eb9aee3d29cfdcb586df21049b9d777fd32c14fd9", 154 | "sha256:e0f72c9ddb8cd28532185f54cc1453f2c16fb417a08b53a855c4e6a418edd099", 155 | "sha256:e7c8dc13af7db097bed64a051d2dd49e9f0af495c26995c00a9ee842690d34c0", 156 | "sha256:ea9872c80c132f4663822dd2a08d404073a5a9b5ba6155bea72fb2a79d1093b5", 157 | "sha256:eff4eb9b7eb3e4d0cae3d28c283dc16d9bed6b193c2e1ace3ed86ce48ea8df19", 158 | "sha256:f82d4d717d8ef19188687aa32b8363e96062911e63ba22a0cff7802a8e58e5f1", 159 | "sha256:fc3a569657468b6f3fb60587e48356fe512c1754ca05a564f11366ac9e306526" 160 | ], 161 | "markers": "platform_machine == 'aarch64' or (platform_machine == 'ppc64le' or (platform_machine == 'x86_64' or (platform_machine == 'amd64' or (platform_machine == 'AMD64' or (platform_machine == 'win32' or platform_machine == 'WIN32')))))", 162 | "version": "==2.0.2" 163 | }, 164 | "iniconfig": { 165 | "hashes": [ 166 | "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", 167 | "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374" 168 | ], 169 | "markers": "python_version >= '3.7'", 170 | "version": "==2.0.0" 171 | }, 172 | "itsdangerous": { 173 | "hashes": [ 174 | "sha256:2c2349112351b88699d8d4b6b075022c0808887cb7ad10069318a8b0bc88db44", 175 | "sha256:5dbbc68b317e5e42f327f9021763545dc3fc3bfe22e6deb96aaf1fc38874156a" 176 | ], 177 | "markers": "python_version >= '3.7'", 178 | "version": "==2.1.2" 179 | }, 180 | "jinja2": { 181 | "hashes": [ 182 | "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852", 183 | "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61" 184 | ], 185 | "markers": "python_version >= '3.7'", 186 | "version": "==3.1.2" 187 | }, 188 | "jmespath": { 189 | "hashes": [ 190 | "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980", 191 | "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe" 192 | ], 193 | "markers": "python_version >= '3.7'", 194 | "version": "==1.0.1" 195 | }, 196 | "mako": { 197 | "hashes": [ 198 | "sha256:c97c79c018b9165ac9922ae4f32da095ffd3c4e6872b45eded42926deea46818", 199 | "sha256:d60a3903dc3bb01a18ad6a89cdbe2e4eadc69c0bc8ef1e3773ba53d44c3f7a34" 200 | ], 201 | "markers": "python_version >= '3.7'", 202 | "version": "==1.2.4" 203 | }, 204 | "markupsafe": { 205 | "hashes": [ 206 | "sha256:05fb21170423db021895e1ea1e1f3ab3adb85d1c2333cbc2310f2a26bc77272e", 207 | "sha256:0a4e4a1aff6c7ac4cd55792abf96c915634c2b97e3cc1c7129578aa68ebd754e", 208 | "sha256:10bbfe99883db80bdbaff2dcf681dfc6533a614f700da1287707e8a5d78a8431", 209 | "sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686", 210 | "sha256:1577735524cdad32f9f694208aa75e422adba74f1baee7551620e43a3141f559", 211 | "sha256:1b40069d487e7edb2676d3fbdb2b0829ffa2cd63a2ec26c4938b2d34391b4ecc", 212 | "sha256:282c2cb35b5b673bbcadb33a585408104df04f14b2d9b01d4c345a3b92861c2c", 213 | "sha256:2c1b19b3aaacc6e57b7e25710ff571c24d6c3613a45e905b1fde04d691b98ee0", 214 | "sha256:2ef12179d3a291be237280175b542c07a36e7f60718296278d8593d21ca937d4", 215 | "sha256:338ae27d6b8745585f87218a3f23f1512dbf52c26c28e322dbe54bcede54ccb9", 216 | "sha256:3c0fae6c3be832a0a0473ac912810b2877c8cb9d76ca48de1ed31e1c68386575", 217 | "sha256:3fd4abcb888d15a94f32b75d8fd18ee162ca0c064f35b11134be77050296d6ba", 218 | "sha256:42de32b22b6b804f42c5d98be4f7e5e977ecdd9ee9b660fda1a3edf03b11792d", 219 | "sha256:504b320cd4b7eff6f968eddf81127112db685e81f7e36e75f9f84f0df46041c3", 220 | "sha256:525808b8019e36eb524b8c68acdd63a37e75714eac50e988180b169d64480a00", 221 | "sha256:56d9f2ecac662ca1611d183feb03a3fa4406469dafe241673d521dd5ae92a155", 222 | "sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac", 223 | "sha256:65c1a9bcdadc6c28eecee2c119465aebff8f7a584dd719facdd9e825ec61ab52", 224 | "sha256:68e78619a61ecf91e76aa3e6e8e33fc4894a2bebe93410754bd28fce0a8a4f9f", 225 | "sha256:69c0f17e9f5a7afdf2cc9fb2d1ce6aabdb3bafb7f38017c0b77862bcec2bbad8", 226 | "sha256:6b2b56950d93e41f33b4223ead100ea0fe11f8e6ee5f641eb753ce4b77a7042b", 227 | "sha256:787003c0ddb00500e49a10f2844fac87aa6ce977b90b0feaaf9de23c22508b24", 228 | "sha256:7ef3cb2ebbf91e330e3bb937efada0edd9003683db6b57bb108c4001f37a02ea", 229 | "sha256:8023faf4e01efadfa183e863fefde0046de576c6f14659e8782065bcece22198", 230 | "sha256:8758846a7e80910096950b67071243da3e5a20ed2546e6392603c096778d48e0", 231 | "sha256:8afafd99945ead6e075b973fefa56379c5b5c53fd8937dad92c662da5d8fd5ee", 232 | "sha256:8c41976a29d078bb235fea9b2ecd3da465df42a562910f9022f1a03107bd02be", 233 | "sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2", 234 | "sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707", 235 | "sha256:962f82a3086483f5e5f64dbad880d31038b698494799b097bc59c2edf392fce6", 236 | "sha256:9dcdfd0eaf283af041973bff14a2e143b8bd64e069f4c383416ecd79a81aab58", 237 | "sha256:aa7bd130efab1c280bed0f45501b7c8795f9fdbeb02e965371bbef3523627779", 238 | "sha256:ab4a0df41e7c16a1392727727e7998a467472d0ad65f3ad5e6e765015df08636", 239 | "sha256:ad9e82fb8f09ade1c3e1b996a6337afac2b8b9e365f926f5a61aacc71adc5b3c", 240 | "sha256:af598ed32d6ae86f1b747b82783958b1a4ab8f617b06fe68795c7f026abbdcad", 241 | "sha256:b076b6226fb84157e3f7c971a47ff3a679d837cf338547532ab866c57930dbee", 242 | "sha256:b7ff0f54cb4ff66dd38bebd335a38e2c22c41a8ee45aa608efc890ac3e3931bc", 243 | "sha256:bfce63a9e7834b12b87c64d6b155fdd9b3b96191b6bd334bf37db7ff1fe457f2", 244 | "sha256:c011a4149cfbcf9f03994ec2edffcb8b1dc2d2aede7ca243746df97a5d41ce48", 245 | "sha256:c9c804664ebe8f83a211cace637506669e7890fec1b4195b505c214e50dd4eb7", 246 | "sha256:ca379055a47383d02a5400cb0d110cef0a776fc644cda797db0c5696cfd7e18e", 247 | "sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b", 248 | "sha256:cd0f502fe016460680cd20aaa5a76d241d6f35a1c3350c474bac1273803893fa", 249 | "sha256:ceb01949af7121f9fc39f7d27f91be8546f3fb112c608bc4029aef0bab86a2a5", 250 | "sha256:d080e0a5eb2529460b30190fcfcc4199bd7f827663f858a226a81bc27beaa97e", 251 | "sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb", 252 | "sha256:df0be2b576a7abbf737b1575f048c23fb1d769f267ec4358296f31c2479db8f9", 253 | "sha256:e09031c87a1e51556fdcb46e5bd4f59dfb743061cf93c4d6831bf894f125eb57", 254 | "sha256:e4dd52d80b8c83fdce44e12478ad2e85c64ea965e75d66dbeafb0a3e77308fcc", 255 | "sha256:fec21693218efe39aa7f8599346e90c705afa52c5b31ae019b2e57e8f6542bb2" 256 | ], 257 | "markers": "python_version >= '3.7'", 258 | "version": "==2.1.3" 259 | }, 260 | "marshmallow": { 261 | "hashes": [ 262 | "sha256:90032c0fd650ce94b6ec6dc8dfeb0e3ff50c144586462c389b81a07205bedb78", 263 | "sha256:93f0958568da045b0021ec6aeb7ac37c81bfcccbb9a0e7ed8559885070b3a19b" 264 | ], 265 | "markers": "python_version >= '3.7'", 266 | "version": "==3.19.0" 267 | }, 268 | "marshmallow-jsonapi": { 269 | "hashes": [ 270 | "sha256:b7403688297dfe8b89173582811989badbe1328ac36447c5a151c006fbe34d24", 271 | "sha256:bd88c0ac0e2ddeb0a3ceb86229963b9f828d898041f29d92a68f585a1feb37b5" 272 | ], 273 | "index": "pypi", 274 | "version": "==0.24.0" 275 | }, 276 | "packaging": { 277 | "hashes": [ 278 | "sha256:994793af429502c4ea2ebf6bf664629d07c1a9fe974af92966e4b8d2df7edc61", 279 | "sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f" 280 | ], 281 | "markers": "python_version >= '3.7'", 282 | "version": "==23.1" 283 | }, 284 | "pluggy": { 285 | "hashes": [ 286 | "sha256:c2fd55a7d7a3863cba1a013e4e2414658b1d07b6bc57b3919e0c63c9abb99849", 287 | "sha256:d12f0c4b579b15f5e054301bb226ee85eeeba08ffec228092f8defbaa3a4c4b3" 288 | ], 289 | "markers": "python_version >= '3.7'", 290 | "version": "==1.2.0" 291 | }, 292 | "psycopg2": { 293 | "hashes": [ 294 | "sha256:06f32425949bd5fe8f625c49f17ebb9784e1e4fe928b7cce72edc36fb68e4c0c", 295 | "sha256:0762c27d018edbcb2d34d51596e4346c983bd27c330218c56c4dc25ef7e819bf", 296 | "sha256:083707a696e5e1c330af2508d8fab36f9700b26621ccbcb538abe22e15485362", 297 | "sha256:34b33e0162cfcaad151f249c2649fd1030010c16f4bbc40a604c1cb77173dcf7", 298 | "sha256:4295093a6ae3434d33ec6baab4ca5512a5082cc43c0505293087b8a46d108461", 299 | "sha256:8cf3878353cc04b053822896bc4922b194792df9df2f1ad8da01fb3043602126", 300 | "sha256:8e841d1bf3434da985cc5ef13e6f75c8981ced601fd70cc6bf33351b91562981", 301 | "sha256:9572e08b50aed176ef6d66f15a21d823bb6f6d23152d35e8451d7d2d18fdac56", 302 | "sha256:a81e3866f99382dfe8c15a151f1ca5fde5815fde879348fe5a9884a7c092a305", 303 | "sha256:cb10d44e6694d763fa1078a26f7f6137d69f555a78ec85dc2ef716c37447e4b2", 304 | "sha256:d3ca6421b942f60c008f81a3541e8faf6865a28d5a9b48544b0ee4f40cac7fca" 305 | ], 306 | "index": "pypi", 307 | "version": "==2.9.3" 308 | }, 309 | "py": { 310 | "hashes": [ 311 | "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719", 312 | "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378" 313 | ], 314 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", 315 | "version": "==1.11.0" 316 | }, 317 | "pytest": { 318 | "hashes": [ 319 | "sha256:1377bda3466d70b55e3f5cecfa55bb7cfcf219c7964629b967c37cf0bda818b7", 320 | "sha256:4f365fec2dff9c1162f834d9f18af1ba13062db0c708bf7b946f8a5c76180c39" 321 | ], 322 | "index": "pypi", 323 | "version": "==7.1.3" 324 | }, 325 | "python-dateutil": { 326 | "hashes": [ 327 | "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86", 328 | "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9" 329 | ], 330 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 331 | "version": "==2.8.2" 332 | }, 333 | "s3transfer": { 334 | "hashes": [ 335 | "sha256:3c0da2d074bf35d6870ef157158641178a4204a6e689e82546083e31e0311346", 336 | "sha256:640bb492711f4c0c0905e1f62b6aaeb771881935ad27884852411f8e9cacbca9" 337 | ], 338 | "markers": "python_version >= '3.7'", 339 | "version": "==0.6.1" 340 | }, 341 | "six": { 342 | "hashes": [ 343 | "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", 344 | "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" 345 | ], 346 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 347 | "version": "==1.16.0" 348 | }, 349 | "sqlalchemy": { 350 | "hashes": [ 351 | "sha256:0db6734cb5644c55d0262a813b764c6e2cda1e66e939a488b3d6298cdc7344c2", 352 | "sha256:0e4645b260cfe375a0603aa117f0a47680864cf37833129da870919e88b08d8f", 353 | "sha256:131f0c894c6572cb1bdcf97c92d999d3128c4ff1ca13061296057072f61afe13", 354 | "sha256:1e2caba78e7d1f5003e88817b7a1754d4e58f4a8f956dc423bf8e304c568ab09", 355 | "sha256:2de1477af7f48c633b8ecb88245aedd811dca88e88aee9e9d787b388abe74c44", 356 | "sha256:2f3b6c31b915159b96b68372212fa77f69230b0a32acab40cf539d2823954f5a", 357 | "sha256:3ef876615ff4b53e2033022195830ec4941a6e21068611f8d77de60203b90a98", 358 | "sha256:43e69c8c1cea0188b7094e22fb93ae1a1890aac748628b7e925024a206f75368", 359 | "sha256:53081c6fce0d49bb36d05f12dc87e008c9b0df58a163b792c5fc4ac638925f98", 360 | "sha256:5a934eff1a2882137be3384826f997db8441d43b61fda3094923e69fffe474be", 361 | "sha256:5e8522b49e0e640287308b68f71cc338446bbe1c226c8f81743baa91b0246e92", 362 | "sha256:61f2035dea56ff1a429077e481496f813378beb02b823d2e3e7eb05bc1a7a8ca", 363 | "sha256:63ea36c08792a7a8a08958bc806ecff6b491386feeaf14607c3d9d2d9325e67f", 364 | "sha256:6e85e315725807c127ad8ba3d628fdb861cf9ebfb0e10c39a97c01e257cdd71b", 365 | "sha256:7641f6ed2682de84d77c4894cf2e43700f3cf7a729361d7f9cac98febf3d8614", 366 | "sha256:7be04dbe3470fe8dd332fdb48c979887c381ef6c635eddf2dec43d2766111be4", 367 | "sha256:81d867c1be5abd49f7e547c108391f371a9d980ba7ec34666c50d683f782b754", 368 | "sha256:8544c6e62eacb77d5106e2055ef10f2407fc0dbd547e879f8745b2032eefd2bc", 369 | "sha256:8d3cbdb2f07fb0e4b897dc1df39166735e194fb946f28f26f4c9f9801c8b24f7", 370 | "sha256:8d6ef848e5afcd1bda3e9a843751f845c0ca888b61e669237680e913d84ec206", 371 | "sha256:8e2569dac4e3cb85365b91ab569d06a221e0e17e65ce59949d00c3958946282b", 372 | "sha256:90d320fde566b864adbc19abb40ecb80f4e25d6f084639969bb972d5cca16858", 373 | "sha256:91eb8f89fcce8f709f8a4d65d265bc48a80264ee14c7c9e955f3222f19b4b39c", 374 | "sha256:a08a791c75d6154d46914d1e23bd81d9455f2950ec1de81f2723848c593d2c8b", 375 | "sha256:a2e9f50a906d0b81292576a9fb458f8cace904c81a67088f4a2ca9ff2856f55d", 376 | "sha256:a5a2856e12cf5f54301ddf043bcbf0552561d61555e1bcf348b63f42b8e1eec2", 377 | "sha256:b2801f85c5c0293aa710f8aa5262c707a83c1c203962ae5a22b4d9095e71aa9d", 378 | "sha256:b72f4e4def50414164a1d899f2ce4e782a029fad0ed5585981d1611e8ae29a74", 379 | "sha256:bdaf89dd82f4a0e1b8b5ffc9cdc0c9551be6175f7eee5af6a838e92ba2e57100", 380 | "sha256:c5e333b81fe10d14efebd4e9429b7bb865ed9463ca8bef07a7136dfa1fd4a37b", 381 | "sha256:ce1fc3f64fd42d5f763d6b83651471f32920338a1ba107a3186211474861af57", 382 | "sha256:d0c96592f54edd571e00ba6b1ed5df8263328ca1da9e78088c0ebc93c2e6562c", 383 | "sha256:dc97238fa44be86971270943a0c21c19ce18b8d1596919048e57912e8abc02cc", 384 | "sha256:e19546924f0cf2ec930d1faf318b7365e5827276410a513340f31a2b423e96a4", 385 | "sha256:f2938edc512dd1fa48653e14c1655ab46144d4450f0e6b33da7acd8ba77fbfd7", 386 | "sha256:f387b496a4c9474d8580195bb2660264a3f295a04d3a9d00f4fa15e9e597427e", 387 | "sha256:f409f35a0330ab0cb18ece736b86d8b8233c64f4461fcb10993f67afc0ac7e5a", 388 | "sha256:f662cf69484c59f8a3435902c40dfc34d86050bdb15e23d437074ce9f153306b", 389 | "sha256:fbcc51fdbc89fafe4f4fe66f59372a8be88ded04de34ef438ab04f980beb12d4", 390 | "sha256:fc1dae11bd5167f9eb53b3ccad24a79813004612141e76de21cf4c028dc30b34", 391 | "sha256:ff6496ad5e9dc8baeb93a151cc2f599d01e5f8928a2aaf0b09a06428fdbaf553" 392 | ], 393 | "markers": "python_version >= '3.7'", 394 | "version": "==2.0.16" 395 | }, 396 | "tomli": { 397 | "hashes": [ 398 | "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc", 399 | "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f" 400 | ], 401 | "markers": "python_version >= '3.7'", 402 | "version": "==2.0.1" 403 | }, 404 | "typing-extensions": { 405 | "hashes": [ 406 | "sha256:88a4153d8505aabbb4e13aacb7c486c2b4a33ca3b3f807914a9b4c844c471c26", 407 | "sha256:d91d5919357fe7f681a9f2b5b4cb2a5f1ef0a1e9f59c4d8ff0d3491e05c0ffd5" 408 | ], 409 | "markers": "python_version >= '3.7'", 410 | "version": "==4.6.3" 411 | }, 412 | "urllib3": { 413 | "hashes": [ 414 | "sha256:8d36afa7616d8ab714608411b4a3b13e58f463aee519024578e062e141dce20f", 415 | "sha256:8f135f6502756bde6b2a9b28989df5fbe87c9970cecaa69041edcce7f0589b14" 416 | ], 417 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", 418 | "version": "==1.26.16" 419 | }, 420 | "webargs": { 421 | "hashes": [ 422 | "sha256:6746327faf549533bf30be7333f99541b6c60a85f23acf1bb0bea68498e3bcd7", 423 | "sha256:99d68940c452e07726485a15fef43f12f8ae6c0c5b391bcba76065d4527fb85d" 424 | ], 425 | "index": "pypi", 426 | "version": "==8.2.0" 427 | }, 428 | "werkzeug": { 429 | "hashes": [ 430 | "sha256:935539fa1413afbb9195b24880778422ed620c0fc09670945185cce4d91a8890", 431 | "sha256:98c774df2f91b05550078891dee5f0eb0cb797a522c757a2452b9cee5b202330" 432 | ], 433 | "markers": "python_version >= '3.8'", 434 | "version": "==2.3.6" 435 | } 436 | }, 437 | "develop": { 438 | "asttokens": { 439 | "hashes": [ 440 | "sha256:4622110b2a6f30b77e1473affaa97e711bc2f07d3f10848420ff1898edbe94f3", 441 | "sha256:6b0ac9e93fb0335014d382b8fa9b3afa7df546984258005da0b9e7095b3deb1c" 442 | ], 443 | "version": "==2.2.1" 444 | }, 445 | "backcall": { 446 | "hashes": [ 447 | "sha256:5cbdbf27be5e7cfadb448baf0aa95508f91f2bbc6c6437cd9cd06e2a4c215e1e", 448 | "sha256:fbbce6a29f263178a1f7915c1940bde0ec2b2a967566fe1c65c1dfb7422bd255" 449 | ], 450 | "version": "==0.2.0" 451 | }, 452 | "decorator": { 453 | "hashes": [ 454 | "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330", 455 | "sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186" 456 | ], 457 | "markers": "python_version >= '3.5'", 458 | "version": "==5.1.1" 459 | }, 460 | "executing": { 461 | "hashes": [ 462 | "sha256:0314a69e37426e3608aada02473b4161d4caf5a4b244d1d0c48072b8fee7bacc", 463 | "sha256:19da64c18d2d851112f09c287f8d3dbbdf725ab0e569077efb6cdcbd3497c107" 464 | ], 465 | "version": "==1.2.0" 466 | }, 467 | "ipython": { 468 | "hashes": [ 469 | "sha256:b13a1d6c1f5818bd388db53b7107d17454129a70de2b87481d555daede5eb49e", 470 | "sha256:b38c31e8fc7eff642fc7c597061fff462537cf2314e3225a19c906b7b0d8a345" 471 | ], 472 | "index": "pypi", 473 | "version": "==8.10.0" 474 | }, 475 | "jedi": { 476 | "hashes": [ 477 | "sha256:203c1fd9d969ab8f2119ec0a3342e0b49910045abe6af0a3ae83a5764d54639e", 478 | "sha256:bae794c30d07f6d910d32a7048af09b5a39ed740918da923c6b780790ebac612" 479 | ], 480 | "markers": "python_version >= '3.6'", 481 | "version": "==0.18.2" 482 | }, 483 | "matplotlib-inline": { 484 | "hashes": [ 485 | "sha256:f1f41aab5328aa5aaea9b16d083b128102f8712542f819fe7e6a420ff581b311", 486 | "sha256:f887e5f10ba98e8d2b150ddcf4702c1e5f8b3a20005eb0f74bfdbd360ee6f304" 487 | ], 488 | "markers": "python_version >= '3.5'", 489 | "version": "==0.1.6" 490 | }, 491 | "mypy": { 492 | "hashes": [ 493 | "sha256:06e1eac8d99bd404ed8dd34ca29673c4346e76dd8e612ea507763dccd7e13c7a", 494 | "sha256:2ee3dbc53d4df7e6e3b1c68ac6a971d3a4fb2852bf10a05fda228721dd44fae1", 495 | "sha256:4bc460e43b7785f78862dab78674e62ec3cd523485baecfdf81a555ed29ecfa0", 496 | "sha256:64e1f6af81c003f85f0dfed52db632817dabb51b65c0318ffbf5ff51995bbb08", 497 | "sha256:6e35d764784b42c3e256848fb8ed1d4292c9fc0098413adb28d84974c095b279", 498 | "sha256:6ee196b1d10b8b215e835f438e06965d7a480f6fe016eddbc285f13955cca659", 499 | "sha256:756fad8b263b3ba39e4e204ee53042671b660c36c9017412b43af210ddee7b08", 500 | "sha256:77f8fcf7b4b3cc0c74fb33ae54a4cd00bb854d65645c48beccf65fa10b17882c", 501 | "sha256:794f385653e2b749387a42afb1e14c2135e18daeb027e0d97162e4b7031210f8", 502 | "sha256:8ad21d4c9d3673726cf986ea1d0c9fb66905258709550ddf7944c8f885f208be", 503 | "sha256:8e8e49aa9cc23aa4c926dc200ce32959d3501c4905147a66ce032f05cb5ecb92", 504 | "sha256:9f362470a3480165c4c6151786b5379351b790d56952005be18bdbdd4c7ce0ae", 505 | "sha256:a16a0145d6d7d00fbede2da3a3096dcc9ecea091adfa8da48fa6a7b75d35562d", 506 | "sha256:ad77c13037d3402fbeffda07d51e3f228ba078d1c7096a73759c9419ea031bf4", 507 | "sha256:b6ede64e52257931315826fdbfc6ea878d89a965580d1a65638ef77cb551f56d", 508 | "sha256:c9e0efb95ed6ca1654951bd5ec2f3fa91b295d78bf6527e026529d4aaa1e0c30", 509 | "sha256:ce65f70b14a21fdac84c294cde75e6dbdabbcff22975335e20827b3b94bdbf49", 510 | "sha256:d1debb09043e1f5ee845fa1e96d180e89115b30e47c5d3ce53bc967bab53f62d", 511 | "sha256:e178eaffc3c5cd211a87965c8c0df6da91ed7d258b5fc72b8e047c3771317ddb", 512 | "sha256:e1acf62a8c4f7c092462c738aa2c2489e275ed386320c10b2e9bff31f6f7e8d6", 513 | "sha256:e53773073c864d5f5cec7f3fc72fbbcef65410cde8cc18d4f7242dea60dac52e", 514 | "sha256:eb3978b191b9fa0488524bb4ffedf2c573340e8c2b4206fc191d44c7093abfb7", 515 | "sha256:f64d2ce043a209a297df322eb4054dfbaa9de9e8738291706eaafda81ab2b362", 516 | "sha256:fa38f82f53e1e7beb45557ff167c177802ba7b387ad017eab1663d567017c8ee" 517 | ], 518 | "index": "pypi", 519 | "version": "==0.981" 520 | }, 521 | "mypy-extensions": { 522 | "hashes": [ 523 | "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", 524 | "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782" 525 | ], 526 | "markers": "python_version >= '3.5'", 527 | "version": "==1.0.0" 528 | }, 529 | "parso": { 530 | "hashes": [ 531 | "sha256:8c07be290bb59f03588915921e29e8a50002acaf2cdc5fa0e0114f91709fafa0", 532 | "sha256:c001d4636cd3aecdaf33cbb40aebb59b094be2a74c556778ef5576c175e19e75" 533 | ], 534 | "markers": "python_version >= '3.6'", 535 | "version": "==0.8.3" 536 | }, 537 | "pexpect": { 538 | "hashes": [ 539 | "sha256:0b48a55dcb3c05f3329815901ea4fc1537514d6ba867a152b581d69ae3710937", 540 | "sha256:fc65a43959d153d0114afe13997d439c22823a27cefceb5ff35c2178c6784c0c" 541 | ], 542 | "markers": "sys_platform != 'win32'", 543 | "version": "==4.8.0" 544 | }, 545 | "pickleshare": { 546 | "hashes": [ 547 | "sha256:87683d47965c1da65cdacaf31c8441d12b8044cdec9aca500cd78fc2c683afca", 548 | "sha256:9649af414d74d4df115d5d718f82acb59c9d418196b7b4290ed47a12ce62df56" 549 | ], 550 | "version": "==0.7.5" 551 | }, 552 | "prompt-toolkit": { 553 | "hashes": [ 554 | "sha256:23ac5d50538a9a38c8bde05fecb47d0b403ecd0662857a86f886f798563d5b9b", 555 | "sha256:45ea77a2f7c60418850331366c81cf6b5b9cf4c7fd34616f733c5427e6abbb1f" 556 | ], 557 | "markers": "python_full_version >= '3.7.0'", 558 | "version": "==3.0.38" 559 | }, 560 | "ptyprocess": { 561 | "hashes": [ 562 | "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35", 563 | "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220" 564 | ], 565 | "version": "==0.7.0" 566 | }, 567 | "pure-eval": { 568 | "hashes": [ 569 | "sha256:01eaab343580944bc56080ebe0a674b39ec44a945e6d09ba7db3cb8cec289350", 570 | "sha256:2b45320af6dfaa1750f543d714b6d1c520a1688dec6fd24d339063ce0aaa9ac3" 571 | ], 572 | "version": "==0.2.2" 573 | }, 574 | "pygments": { 575 | "hashes": [ 576 | "sha256:8ace4d3c1dd481894b2005f560ead0f9f19ee64fe983366be1a21e171d12775c", 577 | "sha256:db2db3deb4b4179f399a09054b023b6a586b76499d36965813c71aa8ed7b5fd1" 578 | ], 579 | "markers": "python_full_version >= '3.7.0'", 580 | "version": "==2.15.1" 581 | }, 582 | "six": { 583 | "hashes": [ 584 | "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", 585 | "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" 586 | ], 587 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 588 | "version": "==1.16.0" 589 | }, 590 | "stack-data": { 591 | "hashes": [ 592 | "sha256:32d2dd0376772d01b6cb9fc996f3c8b57a357089dec328ed4b6553d037eaf815", 593 | "sha256:cbb2a53eb64e5785878201a97ed7c7b94883f48b87bfb0bbe8b623c74679e4a8" 594 | ], 595 | "version": "==0.6.2" 596 | }, 597 | "tomli": { 598 | "hashes": [ 599 | "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc", 600 | "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f" 601 | ], 602 | "markers": "python_version >= '3.7'", 603 | "version": "==2.0.1" 604 | }, 605 | "traitlets": { 606 | "hashes": [ 607 | "sha256:9e6ec080259b9a5940c797d58b613b5e31441c2257b87c2e795c5228ae80d2d8", 608 | "sha256:f6cde21a9c68cf756af02035f72d5a723bf607e862e7be33ece505abf4a3bad9" 609 | ], 610 | "markers": "python_full_version >= '3.7.0'", 611 | "version": "==5.9.0" 612 | }, 613 | "typing-extensions": { 614 | "hashes": [ 615 | "sha256:88a4153d8505aabbb4e13aacb7c486c2b4a33ca3b3f807914a9b4c844c471c26", 616 | "sha256:d91d5919357fe7f681a9f2b5b4cb2a5f1ef0a1e9f59c4d8ff0d3491e05c0ffd5" 617 | ], 618 | "markers": "python_version >= '3.7'", 619 | "version": "==4.6.3" 620 | }, 621 | "wcwidth": { 622 | "hashes": [ 623 | "sha256:795b138f6875577cd91bba52baf9e445cd5118fd32723b460e30a0af30ea230e", 624 | "sha256:a5220780a404dbe3353789870978e472cfe477761f06ee55077256e509b156d0" 625 | ], 626 | "version": "==0.2.6" 627 | } 628 | } 629 | } 630 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # "Uploading files to Google Cloud Storage using a Flask API" – Example App 2 | 3 | This is the example app for our 3-part post series on Medium: 4 | 5 | * [Part 1](https://medium.com/p/7a4e379911d7?source=friends_link&sk=dd460418de3d1829c056db5c069f9b6d): basic app setup, configuring testing & production filedepots (see [part-1](https://github.com/paulgoetze/flask-gcs-upload-example-app/tree/part-1) branch) 6 | * [Part 2](https://medium.com/p/6b203a0e392c?source=friends_link&sk=e7274af2488285dd51756d81de9cf671): testing and implementing the User model & file upload endpoint (see [part-2](https://github.com/paulgoetze/flask-gcs-upload-example-app/tree/part-2) branch) 7 | * [Part 3](https://medium.com/p/897c302916e7?source=friends_link&sk=e9fca9639697051be296c140932884e0): customising the upload & allowing multiple depots (see [part-3](https://github.com/paulgoetze/flask-gcs-upload-example-app/tree/part-3) branch) 8 | 9 | 10 | ## Running the App 11 | 12 | In order to run the Flask app: 13 | 14 | * make sure you have Python v3.10+ and [pipenv](https://pipenv.pypa.io/en/latest/install/#installing-pipenv) installed 15 | * run `pipenv install` to install the dependencies 16 | * run `pipenv shell` to activate the projects virtualenv 17 | * copy `my_app/config/app_config.py.sample` to `my_app/config/app_config.py` and adjust the config variables to your needs 18 | * run `FLASK_APP=my_app flask db upgrade` to init the configured database 19 | * run `FLASK_APP=my_app flask run` to start the local server 20 | * run `pytest` to run the test suite 21 | -------------------------------------------------------------------------------- /migrations/README: -------------------------------------------------------------------------------- 1 | Generic single-database configuration. -------------------------------------------------------------------------------- /migrations/alembic.ini: -------------------------------------------------------------------------------- 1 | # A generic, single database configuration. 2 | 3 | [alembic] 4 | # template used to generate migration files 5 | file_template = %%(year)d%%(month).2d%%(day).2d%%(hour).2d%%(minute).2d%%(second).2d_%%(rev)s_%%(slug)s 6 | 7 | # set to 'true' to run the environment during 8 | # the 'revision' command, regardless of autogenerate 9 | # revision_environment = false 10 | 11 | 12 | # Logging configuration 13 | [loggers] 14 | keys = root,sqlalchemy,alembic 15 | 16 | [handlers] 17 | keys = console 18 | 19 | [formatters] 20 | keys = generic 21 | 22 | [logger_root] 23 | level = WARN 24 | handlers = console 25 | qualname = 26 | 27 | [logger_sqlalchemy] 28 | level = WARN 29 | handlers = 30 | qualname = sqlalchemy.engine 31 | 32 | [logger_alembic] 33 | level = INFO 34 | handlers = 35 | qualname = alembic 36 | 37 | [handler_console] 38 | class = StreamHandler 39 | args = (sys.stderr,) 40 | level = NOTSET 41 | formatter = generic 42 | 43 | [formatter_generic] 44 | format = %(levelname)-5.5s [%(name)s] %(message)s 45 | datefmt = %H:%M:%S 46 | -------------------------------------------------------------------------------- /migrations/env.py: -------------------------------------------------------------------------------- 1 | from __future__ import with_statement 2 | 3 | import logging 4 | from logging.config import fileConfig 5 | 6 | from alembic import context 7 | from sqlalchemy import engine_from_config 8 | from sqlalchemy import pool 9 | 10 | # this is the Alembic Config object, which provides 11 | # access to the values within the .ini file in use. 12 | config = context.config 13 | 14 | # Interpret the config file for Python logging. 15 | # This line sets up loggers basically. 16 | fileConfig(config.config_file_name) 17 | logger = logging.getLogger('alembic.env') 18 | 19 | # add your model's MetaData object here 20 | # for 'autogenerate' support 21 | # from myapp import mymodel 22 | # target_metadata = mymodel.Base.metadata 23 | from flask import current_app 24 | 25 | config.set_main_option('sqlalchemy.url', 26 | current_app.config.get('SQLALCHEMY_DATABASE_URI')) 27 | target_metadata = current_app.extensions['migrate'].db.metadata 28 | 29 | 30 | # other values from the config, defined by the needs of env.py, 31 | # can be acquired: 32 | # my_important_option = config.get_main_option("my_important_option") 33 | # ... etc. 34 | 35 | 36 | def run_migrations_offline(): 37 | """Run migrations in 'offline' mode. 38 | 39 | This configures the context with just a URL 40 | and not an Engine, though an Engine is acceptable 41 | here as well. By skipping the Engine creation 42 | we don't even need a DBAPI to be available. 43 | 44 | Calls to context.execute() here emit the given string to the 45 | script output. 46 | 47 | """ 48 | url = config.get_main_option("sqlalchemy.url") 49 | context.configure( 50 | url=url, target_metadata=target_metadata, literal_binds=True 51 | ) 52 | 53 | with context.begin_transaction(): 54 | context.run_migrations() 55 | 56 | 57 | def run_migrations_online(): 58 | """Run migrations in 'online' mode. 59 | 60 | In this scenario we need to create an Engine 61 | and associate a connection with the context. 62 | 63 | """ 64 | 65 | # this callback is used to prevent an auto-migration from being generated 66 | # when there are no changes to the schema 67 | # reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html 68 | def process_revision_directives(context, revision, directives): 69 | if getattr(config.cmd_opts, 'autogenerate', False): 70 | script = directives[0] 71 | if script.upgrade_ops.is_empty(): 72 | directives[:] = [] 73 | logger.info('No changes in schema detected.') 74 | 75 | connectable = engine_from_config( 76 | config.get_section(config.config_ini_section), 77 | prefix='sqlalchemy.', 78 | poolclass=pool.NullPool, 79 | ) 80 | 81 | with connectable.connect() as connection: 82 | context.configure( 83 | connection=connection, 84 | target_metadata=target_metadata, 85 | process_revision_directives=process_revision_directives, 86 | **current_app.extensions['migrate'].configure_args 87 | ) 88 | 89 | with context.begin_transaction(): 90 | context.run_migrations() 91 | 92 | 93 | if context.is_offline_mode(): 94 | run_migrations_offline() 95 | else: 96 | run_migrations_online() 97 | -------------------------------------------------------------------------------- /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/20190504002143_5b5edadb5f5f_create_users.py: -------------------------------------------------------------------------------- 1 | """create_users 2 | 3 | Revision ID: 5b5edadb5f5f 4 | Revises: 5 | Create Date: 2019-05-04 00:21:43.029010 6 | 7 | """ 8 | import sqlalchemy as sa 9 | from alembic import op 10 | 11 | # revision identifiers, used by Alembic. 12 | revision = '5b5edadb5f5f' 13 | down_revision = None 14 | branch_labels = None 15 | depends_on = None 16 | 17 | 18 | def upgrade(): 19 | op.create_table( 20 | 'users', 21 | sa.Column('id', sa.Integer(), nullable=False), 22 | sa.Column('email', sa.String(length=256), nullable=False), 23 | sa.PrimaryKeyConstraint('id'), 24 | sa.UniqueConstraint('email') 25 | ) 26 | 27 | 28 | def downgrade(): 29 | op.drop_table('users') 30 | -------------------------------------------------------------------------------- /my_app/__init__.py: -------------------------------------------------------------------------------- 1 | from my_app.app import App, db, migrate 2 | 3 | 4 | def create_app() -> App: 5 | return App() 6 | -------------------------------------------------------------------------------- /my_app/app.py: -------------------------------------------------------------------------------- 1 | from os import path 2 | from typing import Dict, Optional 3 | 4 | from flask import Flask 5 | from flask_migrate import Migrate 6 | from flask_sqlalchemy import SQLAlchemy 7 | 8 | from my_app.error_handling import register_error_handlers 9 | 10 | APP_ROOT = path.dirname(path.abspath(__file__)) 11 | 12 | db = SQLAlchemy() 13 | migrate = Migrate() 14 | 15 | 16 | class App(Flask): 17 | 18 | def __init__(self, config: Optional[Dict] = None): 19 | Flask.__init__(self, __name__) 20 | 21 | self.url_map.strict_slashes = False 22 | self.load_config() 23 | 24 | if config: 25 | self.config.update(config) 26 | 27 | register_error_handlers(self) 28 | self.register_blueprints() 29 | self.setup_depots() 30 | 31 | db.init_app(self) 32 | migrate.init_app(self, db) 33 | 34 | def load_config(self): 35 | """Load the app's config from app_config.py""" 36 | 37 | file_path = path.join(APP_ROOT, 'config', 'app_config.py') 38 | self.config.from_pyfile(file_path) 39 | 40 | def register_blueprints(self): 41 | """Register all blueprints""" 42 | 43 | from .users import users 44 | self.register_blueprint(users) 45 | 46 | def setup_depots(self): 47 | """Setup the file depots""" 48 | 49 | from .config import depot 50 | depot.init_depots(self) 51 | -------------------------------------------------------------------------------- /my_app/config/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paulgoetze/flask-gcs-upload-example-app/676c2f055f040f47d4d73d3597b826e9719ca882/my_app/config/__init__.py -------------------------------------------------------------------------------- /my_app/config/app_config.py.sample: -------------------------------------------------------------------------------- 1 | TESTING = True 2 | 3 | SQLALCHEMY_DATABASE_URI = 'your-db-uri' 4 | SQLALCHEMY_TRACK_MODIFICATIONS = False 5 | 6 | GOOGLE_CLOUD_STORAGE_ACCESS_KEY = 'my-storage-access-key' 7 | GOOGLE_CLOUD_STORAGE_SECRET_KEY = 'my-storage-secret-key' 8 | GOOGLE_CLOUD_STORAGE_BUCKET = 'my-bucket-name' 9 | -------------------------------------------------------------------------------- /my_app/config/depot.py: -------------------------------------------------------------------------------- 1 | from depot.manager import DepotManager 2 | from flask import Flask 3 | 4 | 5 | def init_depots(app: Flask): 6 | """Setup all configured depots""" 7 | 8 | depot_name = 'avatar' 9 | depot_config = default_config(app) 10 | 11 | DepotManager.configure(depot_name, depot_config) 12 | 13 | 14 | def default_config(app: Flask): 15 | """Return a default config that is used by all depots""" 16 | 17 | if app.testing: 18 | return test_config() 19 | else: 20 | return production_config(app) 21 | 22 | 23 | def test_config(): 24 | """Return the default test config that is used by all depots""" 25 | 26 | return {'depot.backend': 'depot.io.memory.MemoryFileStorage'} 27 | 28 | 29 | def production_config(app: Flask): 30 | """Return the default production config that is used by all depots""" 31 | 32 | return { 33 | 'depot.backend': 'depot.io.boto3.S3Storage', 34 | 'depot.endpoint_url': 'https://storage.googleapis.com', 35 | 'depot.access_key_id': app.config.get('GOOGLE_CLOUD_STORAGE_ACCESS_KEY'), 36 | 'depot.secret_access_key': app.config.get('GOOGLE_CLOUD_STORAGE_SECRET_KEY'), 37 | 'depot.bucket': app.config.get('GOOGLE_CLOUD_STORAGE_BUCKET') 38 | } 39 | -------------------------------------------------------------------------------- /my_app/error_handling.py: -------------------------------------------------------------------------------- 1 | from http import HTTPStatus 2 | 3 | from flask import jsonify 4 | from marshmallow_jsonapi import fields 5 | from marshmallow import Schema 6 | from werkzeug.exceptions import default_exceptions, HTTPException 7 | 8 | 9 | class ErrorSchema(Schema): 10 | code = fields.Int(required=True) 11 | detail = fields.Str(required=True, attribute='description') 12 | 13 | class Meta: 14 | type_ = 'error' 15 | strict = True 16 | 17 | 18 | def register_error_handlers(app): 19 | """ Register error handlers for the given app """ 20 | 21 | for code in default_exceptions: 22 | app.register_error_handler(code, json_error) 23 | 24 | 25 | def json_error(error): 26 | """Generate a json response from an exception""" 27 | 28 | errors = [ErrorSchema().dump(error)] 29 | response = jsonify(errors=errors) 30 | 31 | code = status_code(error) 32 | response.status_code = code 33 | 34 | return response 35 | 36 | 37 | def status_code(error): 38 | """Return the errors status code""" 39 | 40 | if isinstance(error, HTTPException): 41 | return error.code 42 | else: 43 | return HTTPStatus.INTERNAL_SERVER_ERROR 44 | -------------------------------------------------------------------------------- /my_app/users/__init__.py: -------------------------------------------------------------------------------- 1 | from .views import users 2 | -------------------------------------------------------------------------------- /my_app/users/models.py: -------------------------------------------------------------------------------- 1 | from marshmallow_jsonapi import fields 2 | from marshmallow_jsonapi.flask import Schema 3 | 4 | from my_app import db 5 | 6 | 7 | class User(db.Model): 8 | __tablename__ = 'users' 9 | 10 | id = db.Column(db.Integer, primary_key=True) 11 | email = db.Column(db.String(256), nullable=False, unique=True) 12 | 13 | def __repr__(self): 14 | return ''.format(self=self) 15 | 16 | 17 | class UserSchema(Schema): 18 | id = fields.Int(required=True) 19 | email = fields.Str(required=True) 20 | 21 | class Meta: 22 | type_ = 'users' 23 | self_view = 'users.get_user' 24 | self_view_many = 'users.get_users' 25 | self_view_kwargs = {'user_id': ''} 26 | strict = True 27 | -------------------------------------------------------------------------------- /my_app/users/views.py: -------------------------------------------------------------------------------- 1 | from http import HTTPStatus 2 | 3 | from flask import Blueprint, jsonify, abort 4 | from sqlalchemy.exc import IntegrityError 5 | from webargs import fields 6 | from webargs.flaskparser import use_args 7 | 8 | from my_app import db 9 | from .models import User, UserSchema 10 | 11 | users = Blueprint('users', __name__, url_prefix='/users') 12 | 13 | 14 | @users.route('') 15 | def get_users(): 16 | """List all users""" 17 | 18 | records = User.query.all() 19 | data = UserSchema().dump(records, many=True) 20 | return jsonify(data), HTTPStatus.OK 21 | 22 | 23 | @users.route('/') 24 | def get_user(user_id): 25 | """Get a user""" 26 | 27 | try: 28 | user = User.query.get(int(user_id)) 29 | 30 | if user: 31 | data = UserSchema().dump(user) 32 | return jsonify(data), HTTPStatus.OK 33 | else: 34 | return abort(HTTPStatus.NOT_FOUND) 35 | except ValueError: 36 | return abort(HTTPStatus.NOT_FOUND) 37 | 38 | 39 | @users.route('', methods=['POST']) 40 | @use_args({'email': fields.Str(required=True)}) 41 | def create_user(args): 42 | """Create a new user""" 43 | 44 | email = args.get('email') 45 | user = User(email=email) 46 | db.session.add(user) 47 | 48 | try: 49 | db.session.commit() 50 | data = UserSchema().dump(user) 51 | return jsonify(data), HTTPStatus.CREATED 52 | except IntegrityError: 53 | db.session.rollback() 54 | return abort(HTTPStatus.BAD_REQUEST, 'user already exists') 55 | 56 | 57 | @users.route('/', methods=['PUT']) 58 | @use_args({'email': fields.Str(required=True)}) 59 | def update(args, user_id): 60 | """Update a user""" 61 | 62 | user = User.query.get(user_id) 63 | 64 | if not user: 65 | abort(HTTPStatus.NOT_FOUND) 66 | 67 | user.email = args.get('email') 68 | db.session.commit() 69 | 70 | data = UserSchema().dump(user) 71 | return jsonify(data) 72 | 73 | 74 | @users.route('/', methods=['DELETE']) 75 | def delete_user(user_id): 76 | """Delete a user""" 77 | 78 | user = User.query.get(user_id) 79 | 80 | if not user: 81 | return abort(HTTPStatus.NOT_FOUND) 82 | 83 | db.session.delete(user) 84 | db.session.commit() 85 | 86 | return jsonify(), HTTPStatus.NO_CONTENT 87 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paulgoetze/flask-gcs-upload-example-app/676c2f055f040f47d4d73d3597b826e9719ca882/tests/__init__.py -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | from os import path 2 | 3 | from flask_migrate import Migrate, upgrade 4 | 5 | from my_app import App, db as database 6 | from .support.fixtures.models import * 7 | 8 | 9 | @pytest.fixture(scope='session') 10 | def app(): 11 | """Session-wide test Flask application""" 12 | 13 | config_override = { 14 | 'TESTING': True 15 | } 16 | 17 | app = App(config=config_override) 18 | configure_test_db(app) 19 | 20 | with app.app_context(): 21 | yield app 22 | 23 | 24 | def configure_test_db(app: App): 25 | """Set local sqlite test db""" 26 | 27 | db_path = path.dirname(path.abspath(__file__)) 28 | test_db = 'sqlite:///{}/test.db'.format(db_path) 29 | app.config['SQLALCHEMY_DATABASE_URI'] = test_db 30 | 31 | 32 | @pytest.fixture(scope='session') 33 | def db(app: App): 34 | """Session-wide test database""" 35 | 36 | database.app = app 37 | cleanup_db(database) 38 | 39 | # bring database schema up to date 40 | Migrate(app, database) 41 | 42 | with app.app_context(): 43 | upgrade() 44 | 45 | yield database 46 | cleanup_db(database) 47 | 48 | 49 | def cleanup_db(db): 50 | """Clean up the given database""" 51 | 52 | db.reflect() 53 | db.drop_all() 54 | 55 | 56 | @pytest.fixture(scope='function') 57 | def session(db): 58 | """Create a new database session for a test""" 59 | 60 | connection = db.engine.connect() 61 | transaction = connection.begin() 62 | 63 | options = dict(bind=connection, binds={}) 64 | session = db.create_scoped_session(options=options) 65 | 66 | db.session = session 67 | 68 | yield session 69 | 70 | if transaction.is_active: 71 | transaction.rollback() 72 | 73 | connection.close() 74 | session.remove() 75 | 76 | 77 | @pytest.fixture(scope='function') 78 | def client(app, session): 79 | """Function-wide test client""" 80 | 81 | yield app.test_client() 82 | -------------------------------------------------------------------------------- /tests/support/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paulgoetze/flask-gcs-upload-example-app/676c2f055f040f47d4d73d3597b826e9719ca882/tests/support/__init__.py -------------------------------------------------------------------------------- /tests/support/fixtures/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paulgoetze/flask-gcs-upload-example-app/676c2f055f040f47d4d73d3597b826e9719ca882/tests/support/fixtures/__init__.py -------------------------------------------------------------------------------- /tests/support/fixtures/models.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from sqlalchemy.orm import Session 3 | 4 | from my_app import db 5 | from my_app.users.models import User 6 | 7 | 8 | @pytest.fixture 9 | def user(session: Session) -> db.Model: 10 | user = User(email='user@example.com') 11 | session.add(user) 12 | session.commit() 13 | 14 | return user 15 | 16 | 17 | -------------------------------------------------------------------------------- /tests/users/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paulgoetze/flask-gcs-upload-example-app/676c2f055f040f47d4d73d3597b826e9719ca882/tests/users/__init__.py -------------------------------------------------------------------------------- /tests/users/test_views.py: -------------------------------------------------------------------------------- 1 | import json 2 | from http import HTTPStatus 3 | 4 | from my_app.users.models import User 5 | 6 | 7 | def test_get_all_users(session, client): 8 | """Test getting all users""" 9 | 10 | user_a = User(email='user-a@example.com') 11 | user_b = User(email='user-b@example.com') 12 | 13 | session.add(user_a) 14 | session.add(user_b) 15 | session.commit() 16 | 17 | response = client.get('/users', content_type='application/json') 18 | data = response.json['data'] 19 | assert isinstance(data, list) 20 | assert len(data) == 2 21 | 22 | 23 | def test_get_user(user, client): 24 | """Test getting a user""" 25 | 26 | response = client.get(f'/users/{user.id}', content_type='application/json') 27 | data = response.json['data'] 28 | assert data['type'] == 'users' 29 | assert data['id'] == user.id 30 | assert data['attributes']['email'] == user.email 31 | 32 | 33 | def test_create_a_user(client): 34 | """Test creating a user""" 35 | 36 | assert User.query.count() == 0 37 | 38 | email = 'user@example.com' 39 | response = client.post( 40 | '/users', 41 | data=json.dumps({'email': email}), 42 | content_type='application/json', 43 | ) 44 | data = response.json 45 | 46 | assert response.status_code == HTTPStatus.CREATED 47 | assert User.query.filter_by(email=email).count() == 1 48 | assert data['data']['attributes']['email'] == email 49 | 50 | 51 | def test_create_a_duplicate_user(user, client): 52 | """Test creating an already existing user""" 53 | 54 | response = client.post( 55 | '/users', 56 | data=json.dumps({'email': user.email}), 57 | content_type='application/json' 58 | ) 59 | 60 | assert response.status_code == HTTPStatus.BAD_REQUEST 61 | 62 | 63 | def test_updating_a_user(user, client): 64 | """Test updating a user""" 65 | 66 | email = user.email 67 | assert User.query.filter_by(email=email).count() == 1 68 | 69 | new_email = f'new-{email}' 70 | response = client.put( 71 | f'/users/{user.id}', 72 | data=json.dumps({'email': new_email}), 73 | content_type='application/json', 74 | ) 75 | data = response.json 76 | 77 | assert response.status_code == HTTPStatus.OK 78 | assert user.email == new_email 79 | assert User.query.filter_by(email=email).count() == 0 80 | assert data['data']['id'] == user.id 81 | assert data['data']['attributes']['email'] == new_email 82 | 83 | 84 | def test_updating_a_not_existing_user(client): 85 | """Test updating a not existing user""" 86 | 87 | assert User.query.count() == 0 88 | 89 | response = client.put( 90 | '/users/-1', 91 | data=json.dumps({'email': 'email@example.com'}), 92 | content_type='application/json', 93 | ) 94 | 95 | assert response.status_code == HTTPStatus.NOT_FOUND 96 | assert User.query.count() == 0 97 | 98 | 99 | def test_deleting_a_user(user, client): 100 | """Test deleting a user""" 101 | 102 | response = client.delete(f'/users/{user.id}') 103 | 104 | assert response.status_code == HTTPStatus.NO_CONTENT 105 | assert User.query.get(user.id) is None 106 | 107 | 108 | def test_deleting_a_not_existing_user(client): 109 | """Test deleting a not existing user""" 110 | 111 | response = client.delete('/users/0') 112 | assert response.status_code == HTTPStatus.NOT_FOUND 113 | --------------------------------------------------------------------------------