├── .gitignore ├── Pipfile ├── Pipfile.lock ├── README.md ├── app ├── __init__.py ├── api │ ├── __init__.py │ └── user │ │ ├── __init__.py │ │ ├── controller.py │ │ ├── dto.py │ │ ├── service.py │ │ └── utils.py ├── auth │ ├── __init__.py │ ├── controller.py │ ├── dto.py │ ├── service.py │ └── utils.py ├── extensions.py ├── models │ ├── __init__.py │ ├── schemas.py │ └── user.py └── utils.py ├── config.py ├── giya.py ├── requirements.txt └── tests ├── __init__.py ├── test_auth_api.py ├── test_config.py ├── test_user_api.py ├── test_user_model.py └── utils ├── __init__.py ├── base.py └── common.py /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/ 2 | __pycache__ 3 | migrations/ 4 | .env 5 | *.sqlite -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | name = "pypi" 3 | url = "https://pypi.org/simple" 4 | verify_ssl = true 5 | 6 | [dev-packages] 7 | 8 | [packages] 9 | flask-sqlalchemy = "*" 10 | flask-restx = "*" 11 | flask-migrate = "*" 12 | flask-bcrypt = "*" 13 | flask-cors = "*" 14 | flask-jwt-extended = "*" 15 | flask-marshmallow = "*" 16 | marshmallow-sqlalchemy = "*" 17 | python-dotenv = "*" 18 | 19 | [requires] 20 | python_version = "3.9" 21 | -------------------------------------------------------------------------------- /Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "2cbc72aeae99e5427250fd584fa1ccaa67ddfd1894bc5a1bda9bb7893a70697d" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3.9" 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:bc5bdf03d1b9814ee4d72adc0b19df2123f6c50a60c1ea761733f3640feedb8d", 22 | "sha256:d0c580041f9f6487d5444df672a83da9be57398f39d6c1802bbedec6fefbeef6" 23 | ], 24 | "markers": "python_version >= '3.6'", 25 | "version": "==1.7.3" 26 | }, 27 | "aniso8601": { 28 | "hashes": [ 29 | "sha256:1d2b7ef82963909e93c4f24ce48d4de9e66009a21bf1c1e1c85bdd0812fe412f", 30 | "sha256:72e3117667eedf66951bb2d93f4296a56b94b078a8a95905a052611fb3f1b973" 31 | ], 32 | "markers": "python_version >= '3.5'", 33 | "version": "==9.0.1" 34 | }, 35 | "attrs": { 36 | "hashes": [ 37 | "sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1", 38 | "sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb" 39 | ], 40 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", 41 | "version": "==21.2.0" 42 | }, 43 | "bcrypt": { 44 | "hashes": [ 45 | "sha256:5b93c1726e50a93a033c36e5ca7fdcd29a5c7395af50a6892f5d9e7c6cfbfb29", 46 | "sha256:63d4e3ff96188e5898779b6057878fecf3f11cfe6ec3b313ea09955d587ec7a7", 47 | "sha256:81fec756feff5b6818ea7ab031205e1d323d8943d237303baca2c5f9c7846f34", 48 | "sha256:a67fb841b35c28a59cebed05fbd3e80eea26e6d75851f0574a9273c80f3e9b55", 49 | "sha256:c95d4cbebffafcdd28bd28bb4e25b31c50f6da605c81ffd9ad8a3d1b2ab7b1b6", 50 | "sha256:cd1ea2ff3038509ea95f687256c46b79f5fc382ad0aa3664d200047546d511d1", 51 | "sha256:cdcdcb3972027f83fe24a48b1e90ea4b584d35f1cc279d76de6fc4b13376239d" 52 | ], 53 | "markers": "python_version >= '3.6'", 54 | "version": "==3.2.0" 55 | }, 56 | "cffi": { 57 | "hashes": [ 58 | "sha256:06c54a68935738d206570b20da5ef2b6b6d92b38ef3ec45c5422c0ebaf338d4d", 59 | "sha256:0c0591bee64e438883b0c92a7bed78f6290d40bf02e54c5bf0978eaf36061771", 60 | "sha256:19ca0dbdeda3b2615421d54bef8985f72af6e0c47082a8d26122adac81a95872", 61 | "sha256:22b9c3c320171c108e903d61a3723b51e37aaa8c81255b5e7ce102775bd01e2c", 62 | "sha256:26bb2549b72708c833f5abe62b756176022a7b9a7f689b571e74c8478ead51dc", 63 | "sha256:33791e8a2dc2953f28b8d8d300dde42dd929ac28f974c4b4c6272cb2955cb762", 64 | "sha256:3c8d896becff2fa653dc4438b54a5a25a971d1f4110b32bd3068db3722c80202", 65 | "sha256:4373612d59c404baeb7cbd788a18b2b2a8331abcc84c3ba40051fcd18b17a4d5", 66 | "sha256:487d63e1454627c8e47dd230025780e91869cfba4c753a74fda196a1f6ad6548", 67 | "sha256:48916e459c54c4a70e52745639f1db524542140433599e13911b2f329834276a", 68 | "sha256:4922cd707b25e623b902c86188aca466d3620892db76c0bdd7b99a3d5e61d35f", 69 | "sha256:55af55e32ae468e9946f741a5d51f9896da6b9bf0bbdd326843fec05c730eb20", 70 | "sha256:57e555a9feb4a8460415f1aac331a2dc833b1115284f7ded7278b54afc5bd218", 71 | "sha256:5d4b68e216fc65e9fe4f524c177b54964af043dde734807586cf5435af84045c", 72 | "sha256:64fda793737bc4037521d4899be780534b9aea552eb673b9833b01f945904c2e", 73 | "sha256:6d6169cb3c6c2ad50db5b868db6491a790300ade1ed5d1da29289d73bbe40b56", 74 | "sha256:7bcac9a2b4fdbed2c16fa5681356d7121ecabf041f18d97ed5b8e0dd38a80224", 75 | "sha256:80b06212075346b5546b0417b9f2bf467fea3bfe7352f781ffc05a8ab24ba14a", 76 | "sha256:818014c754cd3dba7229c0f5884396264d51ffb87ec86e927ef0be140bfdb0d2", 77 | "sha256:8eb687582ed7cd8c4bdbff3df6c0da443eb89c3c72e6e5dcdd9c81729712791a", 78 | "sha256:99f27fefe34c37ba9875f224a8f36e31d744d8083e00f520f133cab79ad5e819", 79 | "sha256:9f3e33c28cd39d1b655ed1ba7247133b6f7fc16fa16887b120c0c670e35ce346", 80 | "sha256:a8661b2ce9694ca01c529bfa204dbb144b275a31685a075ce123f12331be790b", 81 | "sha256:a9da7010cec5a12193d1af9872a00888f396aba3dc79186604a09ea3ee7c029e", 82 | "sha256:aedb15f0a5a5949ecb129a82b72b19df97bbbca024081ed2ef88bd5c0a610534", 83 | "sha256:b315d709717a99f4b27b59b021e6207c64620790ca3e0bde636a6c7f14618abb", 84 | "sha256:ba6f2b3f452e150945d58f4badd92310449876c4c954836cfb1803bdd7b422f0", 85 | "sha256:c33d18eb6e6bc36f09d793c0dc58b0211fccc6ae5149b808da4a62660678b156", 86 | "sha256:c9a875ce9d7fe32887784274dd533c57909b7b1dcadcc128a2ac21331a9765dd", 87 | "sha256:c9e005e9bd57bc987764c32a1bee4364c44fdc11a3cc20a40b93b444984f2b87", 88 | "sha256:d2ad4d668a5c0645d281dcd17aff2be3212bc109b33814bbb15c4939f44181cc", 89 | "sha256:d950695ae4381ecd856bcaf2b1e866720e4ab9a1498cba61c602e56630ca7195", 90 | "sha256:e22dcb48709fc51a7b58a927391b23ab37eb3737a98ac4338e2448bef8559b33", 91 | "sha256:e8c6a99be100371dbb046880e7a282152aa5d6127ae01783e37662ef73850d8f", 92 | "sha256:e9dc245e3ac69c92ee4c167fbdd7428ec1956d4e754223124991ef29eb57a09d", 93 | "sha256:eb687a11f0a7a1839719edd80f41e459cc5366857ecbed383ff376c4e3cc6afd", 94 | "sha256:eb9e2a346c5238a30a746893f23a9535e700f8192a68c07c0258e7ece6ff3728", 95 | "sha256:ed38b924ce794e505647f7c331b22a693bee1538fdf46b0222c4717b42f744e7", 96 | "sha256:f0010c6f9d1a4011e429109fda55a225921e3206e7f62a0c22a35344bfd13cca", 97 | "sha256:f0c5d1acbfca6ebdd6b1e3eded8d261affb6ddcf2186205518f1428b8569bb99", 98 | "sha256:f10afb1004f102c7868ebfe91c28f4a712227fe4cb24974350ace1f90e1febbf", 99 | "sha256:f174135f5609428cc6e1b9090f9268f5c8935fddb1b25ccb8255a2d50de6789e", 100 | "sha256:f3ebe6e73c319340830a9b2825d32eb6d8475c1dac020b4f0aa774ee3b898d1c", 101 | "sha256:f627688813d0a4140153ff532537fbe4afea5a3dffce1f9deb7f91f848a832b5", 102 | "sha256:fd4305f86f53dfd8cd3522269ed7fc34856a8ee3709a5e28b2836b2db9d4cd69" 103 | ], 104 | "version": "==1.14.6" 105 | }, 106 | "click": { 107 | "hashes": [ 108 | "sha256:8c04c11192119b1ef78ea049e0a6f0463e4c48ef00a30160c704337586f3ad7a", 109 | "sha256:fba402a4a47334742d782209a7c79bc448911afe1149d07bdabdf480b3e2f4b6" 110 | ], 111 | "markers": "python_version >= '3.6'", 112 | "version": "==8.0.1" 113 | }, 114 | "flask": { 115 | "hashes": [ 116 | "sha256:1c4c257b1892aec1398784c63791cbaa43062f1f7aeb555c4da961b20ee68f55", 117 | "sha256:a6209ca15eb63fc9385f38e452704113d679511d9574d09b2cf9183ae7d20dc9" 118 | ], 119 | "markers": "python_version >= '3.6'", 120 | "version": "==2.0.1" 121 | }, 122 | "flask-bcrypt": { 123 | "hashes": [ 124 | "sha256:d71c8585b2ee1c62024392ebdbc447438564e2c8c02b4e57b56a4cafd8d13c5f" 125 | ], 126 | "index": "pypi", 127 | "version": "==0.7.1" 128 | }, 129 | "flask-cors": { 130 | "hashes": [ 131 | "sha256:74efc975af1194fc7891ff5cd85b0f7478be4f7f59fe158102e91abb72bb4438", 132 | "sha256:b60839393f3b84a0f3746f6cdca56c1ad7426aa738b70d6c61375857823181de" 133 | ], 134 | "index": "pypi", 135 | "version": "==3.0.10" 136 | }, 137 | "flask-jwt-extended": { 138 | "hashes": [ 139 | "sha256:6e2b40d548b9dfc6051740c4552c097ac38e514e500c16c682d9a533d17ca418", 140 | "sha256:80d06d3893089824659c26d0cb261999a12f425a66f09c3685f993065bc47b3a" 141 | ], 142 | "index": "pypi", 143 | "version": "==4.3.0" 144 | }, 145 | "flask-marshmallow": { 146 | "hashes": [ 147 | "sha256:2adcd782b5a4a6c5ae3c96701f320d8ca6997995a52b2661093c56cc3ed24754", 148 | "sha256:bd01a6372cbe50e36f205cfff0fc5dab0b7b662c4c8b2c4fc06a3151b2950950" 149 | ], 150 | "index": "pypi", 151 | "version": "==0.14.0" 152 | }, 153 | "flask-migrate": { 154 | "hashes": [ 155 | "sha256:57d6060839e3a7f150eaab6fe4e726d9e3e7cffe2150fb223d73f92421c6d1d9", 156 | "sha256:a6498706241aba6be7a251078de9cf166d74307bca41a4ca3e403c9d39e2f897" 157 | ], 158 | "index": "pypi", 159 | "version": "==3.1.0" 160 | }, 161 | "flask-restx": { 162 | "hashes": [ 163 | "sha256:63c69a61999a34f1774eaccc6fc8c7f504b1aad7d56a8ec672264e52d9ac05f4", 164 | "sha256:96157547acaa8892adcefd8c60abf9040212ac2a8634937a82946e07b46147fd" 165 | ], 166 | "index": "pypi", 167 | "version": "==0.5.1" 168 | }, 169 | "flask-sqlalchemy": { 170 | "hashes": [ 171 | "sha256:2bda44b43e7cacb15d4e05ff3cc1f8bc97936cc464623424102bfc2c35e95912", 172 | "sha256:f12c3d4cc5cc7fdcc148b9527ea05671718c3ea45d50c7e732cceb33f574b390" 173 | ], 174 | "index": "pypi", 175 | "version": "==2.5.1" 176 | }, 177 | "greenlet": { 178 | "hashes": [ 179 | "sha256:00e44c8afdbe5467e4f7b5851be223be68adb4272f44696ee71fe46b7036a711", 180 | "sha256:013d61294b6cd8fe3242932c1c5e36e5d1db2c8afb58606c5a67efce62c1f5fd", 181 | "sha256:049fe7579230e44daef03a259faa24511d10ebfa44f69411d99e6a184fe68073", 182 | "sha256:14d4f3cd4e8b524ae9b8aa567858beed70c392fdec26dbdb0a8a418392e71708", 183 | "sha256:166eac03e48784a6a6e0e5f041cfebb1ab400b394db188c48b3a84737f505b67", 184 | "sha256:17ff94e7a83aa8671a25bf5b59326ec26da379ace2ebc4411d690d80a7fbcf23", 185 | "sha256:1e12bdc622676ce47ae9abbf455c189e442afdde8818d9da983085df6312e7a1", 186 | "sha256:21915eb821a6b3d9d8eefdaf57d6c345b970ad722f856cd71739493ce003ad08", 187 | "sha256:288c6a76705dc54fba69fbcb59904ae4ad768b4c768839b8ca5fdadec6dd8cfd", 188 | "sha256:32ca72bbc673adbcfecb935bb3fb1b74e663d10a4b241aaa2f5a75fe1d1f90aa", 189 | "sha256:356b3576ad078c89a6107caa9c50cc14e98e3a6c4874a37c3e0273e4baf33de8", 190 | "sha256:40b951f601af999a8bf2ce8c71e8aaa4e8c6f78ff8afae7b808aae2dc50d4c40", 191 | "sha256:572e1787d1460da79590bf44304abbc0a2da944ea64ec549188fa84d89bba7ab", 192 | "sha256:58df5c2a0e293bf665a51f8a100d3e9956febfbf1d9aaf8c0677cf70218910c6", 193 | "sha256:64e6175c2e53195278d7388c454e0b30997573f3f4bd63697f88d855f7a6a1fc", 194 | "sha256:7227b47e73dedaa513cdebb98469705ef0d66eb5a1250144468e9c3097d6b59b", 195 | "sha256:7418b6bfc7fe3331541b84bb2141c9baf1ec7132a7ecd9f375912eca810e714e", 196 | "sha256:7cbd7574ce8e138bda9df4efc6bf2ab8572c9aff640d8ecfece1b006b68da963", 197 | "sha256:7ff61ff178250f9bb3cd89752df0f1dd0e27316a8bd1465351652b1b4a4cdfd3", 198 | "sha256:833e1551925ed51e6b44c800e71e77dacd7e49181fdc9ac9a0bf3714d515785d", 199 | "sha256:8639cadfda96737427330a094476d4c7a56ac03de7265622fcf4cfe57c8ae18d", 200 | "sha256:8c790abda465726cfb8bb08bd4ca9a5d0a7bd77c7ac1ca1b839ad823b948ea28", 201 | "sha256:8d2f1fb53a421b410751887eb4ff21386d119ef9cde3797bf5e7ed49fb51a3b3", 202 | "sha256:903bbd302a2378f984aef528f76d4c9b1748f318fe1294961c072bdc7f2ffa3e", 203 | "sha256:93f81b134a165cc17123626ab8da2e30c0455441d4ab5576eed73a64c025b25c", 204 | "sha256:95e69877983ea39b7303570fa6760f81a3eec23d0e3ab2021b7144b94d06202d", 205 | "sha256:9633b3034d3d901f0a46b7939f8c4d64427dfba6bbc5a36b1a67364cf148a1b0", 206 | "sha256:97e5306482182170ade15c4b0d8386ded995a07d7cc2ca8f27958d34d6736497", 207 | "sha256:9f3cba480d3deb69f6ee2c1825060177a22c7826431458c697df88e6aeb3caee", 208 | "sha256:aa5b467f15e78b82257319aebc78dd2915e4c1436c3c0d1ad6f53e47ba6e2713", 209 | "sha256:abb7a75ed8b968f3061327c433a0fbd17b729947b400747c334a9c29a9af6c58", 210 | "sha256:aec52725173bd3a7b56fe91bc56eccb26fbdff1386ef123abb63c84c5b43b63a", 211 | "sha256:b11548073a2213d950c3f671aa88e6f83cda6e2fb97a8b6317b1b5b33d850e06", 212 | "sha256:b1692f7d6bc45e3200844be0dba153612103db241691088626a33ff1f24a0d88", 213 | "sha256:b92e29e58bef6d9cfd340c72b04d74c4b4e9f70c9fa7c78b674d1fec18896dc4", 214 | "sha256:be5f425ff1f5f4b3c1e33ad64ab994eed12fc284a6ea71c5243fd564502ecbe5", 215 | "sha256:dd0b1e9e891f69e7675ba5c92e28b90eaa045f6ab134ffe70b52e948aa175b3c", 216 | "sha256:e30f5ea4ae2346e62cedde8794a56858a67b878dd79f7df76a0767e356b1744a", 217 | "sha256:e6a36bb9474218c7a5b27ae476035497a6990e21d04c279884eb10d9b290f1b1", 218 | "sha256:e859fcb4cbe93504ea18008d1df98dee4f7766db66c435e4882ab35cf70cac43", 219 | "sha256:eb6ea6da4c787111adf40f697b4e58732ee0942b5d3bd8f435277643329ba627", 220 | "sha256:ec8c433b3ab0419100bd45b47c9c8551248a5aee30ca5e9d399a0b57ac04651b", 221 | "sha256:eff9d20417ff9dcb0d25e2defc2574d10b491bf2e693b4e491914738b7908168", 222 | "sha256:f0214eb2a23b85528310dad848ad2ac58e735612929c8072f6093f3585fd342d", 223 | "sha256:f276df9830dba7a333544bd41070e8175762a7ac20350786b322b714b0e654f5", 224 | "sha256:f3acda1924472472ddd60c29e5b9db0cec629fbe3c5c5accb74d6d6d14773478", 225 | "sha256:f70a9e237bb792c7cc7e44c531fd48f5897961701cdaa06cf22fc14965c496cf", 226 | "sha256:f9d29ca8a77117315101425ec7ec2a47a22ccf59f5593378fc4077ac5b754fce", 227 | "sha256:fa877ca7f6b48054f847b61d6fa7bed5cebb663ebc55e018fda12db09dcc664c", 228 | "sha256:fdcec0b8399108577ec290f55551d926d9a1fa6cad45882093a7a07ac5ec147b" 229 | ], 230 | "markers": "python_version >= '3' and platform_machine == 'aarch64' or (platform_machine == 'ppc64le' or (platform_machine == 'x86_64' or (platform_machine == 'amd64' or (platform_machine == 'AMD64' or (platform_machine == 'win32' or platform_machine == 'WIN32')))))", 231 | "version": "==1.1.2" 232 | }, 233 | "itsdangerous": { 234 | "hashes": [ 235 | "sha256:5174094b9637652bdb841a3029700391451bd092ba3db90600dea710ba28e97c", 236 | "sha256:9e724d68fc22902a1435351f84c3fb8623f303fffcc566a4cb952df8c572cff0" 237 | ], 238 | "markers": "python_version >= '3.6'", 239 | "version": "==2.0.1" 240 | }, 241 | "jinja2": { 242 | "hashes": [ 243 | "sha256:1f06f2da51e7b56b8f238affdd6b4e2c61e39598a378cc49345bc1bd42a978a4", 244 | "sha256:703f484b47a6af502e743c9122595cc812b0271f661722403114f71a79d0f5a4" 245 | ], 246 | "markers": "python_version >= '3.6'", 247 | "version": "==3.0.1" 248 | }, 249 | "jsonschema": { 250 | "hashes": [ 251 | "sha256:48f4e74f8bec0c2f75e9fcfffa264e78342873e1b57e2cfeae54864cc5e9e4dd", 252 | "sha256:9938802041347f2c62cad2aef59e9a0826cd34584f3609db950efacb4dbf6518" 253 | ], 254 | "markers": "python_version >= '3.7'", 255 | "version": "==4.0.1" 256 | }, 257 | "mako": { 258 | "hashes": [ 259 | "sha256:169fa52af22a91900d852e937400e79f535496191c63712e3b9fda5a9bed6fc3", 260 | "sha256:6804ee66a7f6a6416910463b00d76a7b25194cd27f1918500c5bd7be2a088a23" 261 | ], 262 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 263 | "version": "==1.1.5" 264 | }, 265 | "markupsafe": { 266 | "hashes": [ 267 | "sha256:01a9b8ea66f1658938f65b93a85ebe8bc016e6769611be228d797c9d998dd298", 268 | "sha256:023cb26ec21ece8dc3907c0e8320058b2e0cb3c55cf9564da612bc325bed5e64", 269 | "sha256:0446679737af14f45767963a1a9ef7620189912317d095f2d9ffa183a4d25d2b", 270 | "sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567", 271 | "sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff", 272 | "sha256:0d4b31cc67ab36e3392bbf3862cfbadac3db12bdd8b02a2731f509ed5b829724", 273 | "sha256:10f82115e21dc0dfec9ab5c0223652f7197feb168c940f3ef61563fc2d6beb74", 274 | "sha256:168cd0a3642de83558a5153c8bd34f175a9a6e7f6dc6384b9655d2697312a646", 275 | "sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35", 276 | "sha256:1f2ade76b9903f39aa442b4aadd2177decb66525062db244b35d71d0ee8599b6", 277 | "sha256:2a7d351cbd8cfeb19ca00de495e224dea7e7d919659c2841bbb7f420ad03e2d6", 278 | "sha256:2d7d807855b419fc2ed3e631034685db6079889a1f01d5d9dac950f764da3dad", 279 | "sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26", 280 | "sha256:36bc903cbb393720fad60fc28c10de6acf10dc6cc883f3e24ee4012371399a38", 281 | "sha256:37205cac2a79194e3750b0af2a5720d95f786a55ce7df90c3af697bfa100eaac", 282 | "sha256:3c112550557578c26af18a1ccc9e090bfe03832ae994343cfdacd287db6a6ae7", 283 | "sha256:3dd007d54ee88b46be476e293f48c85048603f5f516008bee124ddd891398ed6", 284 | "sha256:47ab1e7b91c098ab893b828deafa1203de86d0bc6ab587b160f78fe6c4011f75", 285 | "sha256:49e3ceeabbfb9d66c3aef5af3a60cc43b85c33df25ce03d0031a608b0a8b2e3f", 286 | "sha256:4efca8f86c54b22348a5467704e3fec767b2db12fc39c6d963168ab1d3fc9135", 287 | "sha256:53edb4da6925ad13c07b6d26c2a852bd81e364f95301c66e930ab2aef5b5ddd8", 288 | "sha256:5855f8438a7d1d458206a2466bf82b0f104a3724bf96a1c781ab731e4201731a", 289 | "sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a", 290 | "sha256:5bb28c636d87e840583ee3adeb78172efc47c8b26127267f54a9c0ec251d41a9", 291 | "sha256:60bf42e36abfaf9aff1f50f52644b336d4f0a3fd6d8a60ca0d054ac9f713a864", 292 | "sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914", 293 | "sha256:6557b31b5e2c9ddf0de32a691f2312a32f77cd7681d8af66c2692efdbef84c18", 294 | "sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8", 295 | "sha256:6a7fae0dd14cf60ad5ff42baa2e95727c3d81ded453457771d02b7d2b3f9c0c2", 296 | "sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d", 297 | "sha256:6fcf051089389abe060c9cd7caa212c707e58153afa2c649f00346ce6d260f1b", 298 | "sha256:7d91275b0245b1da4d4cfa07e0faedd5b0812efc15b702576d103293e252af1b", 299 | "sha256:905fec760bd2fa1388bb5b489ee8ee5f7291d692638ea5f67982d968366bef9f", 300 | "sha256:97383d78eb34da7e1fa37dd273c20ad4320929af65d156e35a5e2d89566d9dfb", 301 | "sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833", 302 | "sha256:99df47edb6bda1249d3e80fdabb1dab8c08ef3975f69aed437cb69d0a5de1e28", 303 | "sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415", 304 | "sha256:ab3ef638ace319fa26553db0624c4699e31a28bb2a835c5faca8f8acf6a5a902", 305 | "sha256:add36cb2dbb8b736611303cd3bfcee00afd96471b09cda130da3581cbdc56a6d", 306 | "sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9", 307 | "sha256:b7f2d075102dc8c794cbde1947378051c4e5180d52d276987b8d28a3bd58c17d", 308 | "sha256:baa1a4e8f868845af802979fcdbf0bb11f94f1cb7ced4c4b8a351bb60d108145", 309 | "sha256:be98f628055368795d818ebf93da628541e10b75b41c559fdf36d104c5787066", 310 | "sha256:bf5d821ffabf0ef3533c39c518f3357b171a1651c1ff6827325e4489b0e46c3c", 311 | "sha256:c47adbc92fc1bb2b3274c4b3a43ae0e4573d9fbff4f54cd484555edbf030baf1", 312 | "sha256:d7f9850398e85aba693bb640262d3611788b1f29a79f0c93c565694658f4071f", 313 | "sha256:d8446c54dc28c01e5a2dbac5a25f071f6653e6e40f3a8818e8b45d790fe6ef53", 314 | "sha256:e0f138900af21926a02425cf736db95be9f4af72ba1bb21453432a07f6082134", 315 | "sha256:e9936f0b261d4df76ad22f8fee3ae83b60d7c3e871292cd42f40b81b70afae85", 316 | "sha256:f5653a225f31e113b152e56f154ccbe59eeb1c7487b39b9d9f9cdb58e6c79dc5", 317 | "sha256:f826e31d18b516f653fe296d967d700fddad5901ae07c622bb3705955e1faa94", 318 | "sha256:f8ba0e8349a38d3001fae7eadded3f6606f0da5d748ee53cc1dab1d6527b9509", 319 | "sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51", 320 | "sha256:fa130dd50c57d53368c9d59395cb5526eda596d3ffe36666cd81a44d56e48872" 321 | ], 322 | "markers": "python_version >= '3.6'", 323 | "version": "==2.0.1" 324 | }, 325 | "marshmallow": { 326 | "hashes": [ 327 | "sha256:c67929438fd73a2be92128caa0325b1b5ed8b626d91a094d2f7f2771bf1f1c0e", 328 | "sha256:dd4724335d3c2b870b641ffe4a2f8728a1380cd2e7e2312756715ffeaa82b842" 329 | ], 330 | "markers": "python_version >= '3.5'", 331 | "version": "==3.13.0" 332 | }, 333 | "marshmallow-sqlalchemy": { 334 | "hashes": [ 335 | "sha256:ba7493eeb8669a3bf00d8f906b657feaa87a740ae9e4ecf829cfd6ddf763d276", 336 | "sha256:d8525f74de51554b5c8491effe036f60629a426229befa33ff614c8569a16a73" 337 | ], 338 | "index": "pypi", 339 | "version": "==0.26.1" 340 | }, 341 | "pycparser": { 342 | "hashes": [ 343 | "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0", 344 | "sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705" 345 | ], 346 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 347 | "version": "==2.20" 348 | }, 349 | "pyjwt": { 350 | "hashes": [ 351 | "sha256:934d73fbba91b0483d3857d1aff50e96b2a892384ee2c17417ed3203f173fca1", 352 | "sha256:fba44e7898bbca160a2b2b501f492824fc8382485d3a6f11ba5d0c1937ce6130" 353 | ], 354 | "markers": "python_version >= '3.6'", 355 | "version": "==2.1.0" 356 | }, 357 | "pyrsistent": { 358 | "hashes": [ 359 | "sha256:097b96f129dd36a8c9e33594e7ebb151b1515eb52cceb08474c10a5479e799f2", 360 | "sha256:2aaf19dc8ce517a8653746d98e962ef480ff34b6bc563fc067be6401ffb457c7", 361 | "sha256:404e1f1d254d314d55adb8d87f4f465c8693d6f902f67eb6ef5b4526dc58e6ea", 362 | "sha256:48578680353f41dca1ca3dc48629fb77dfc745128b56fc01096b2530c13fd426", 363 | "sha256:4916c10896721e472ee12c95cdc2891ce5890898d2f9907b1b4ae0f53588b710", 364 | "sha256:527be2bfa8dc80f6f8ddd65242ba476a6c4fb4e3aedbf281dfbac1b1ed4165b1", 365 | "sha256:58a70d93fb79dc585b21f9d72487b929a6fe58da0754fa4cb9f279bb92369396", 366 | "sha256:5e4395bbf841693eaebaa5bb5c8f5cdbb1d139e07c975c682ec4e4f8126e03d2", 367 | "sha256:6b5eed00e597b5b5773b4ca30bd48a5774ef1e96f2a45d105db5b4ebb4bca680", 368 | "sha256:73ff61b1411e3fb0ba144b8f08d6749749775fe89688093e1efef9839d2dcc35", 369 | "sha256:772e94c2c6864f2cd2ffbe58bb3bdefbe2a32afa0acb1a77e472aac831f83427", 370 | "sha256:773c781216f8c2900b42a7b638d5b517bb134ae1acbebe4d1e8f1f41ea60eb4b", 371 | "sha256:a0c772d791c38bbc77be659af29bb14c38ced151433592e326361610250c605b", 372 | "sha256:b29b869cf58412ca5738d23691e96d8aff535e17390128a1a52717c9a109da4f", 373 | "sha256:c1a9ff320fa699337e05edcaae79ef8c2880b52720bc031b219e5b5008ebbdef", 374 | "sha256:cd3caef37a415fd0dae6148a1b6957a8c5f275a62cca02e18474608cb263640c", 375 | "sha256:d5ec194c9c573aafaceebf05fc400656722793dac57f254cd4741f3c27ae57b4", 376 | "sha256:da6e5e818d18459fa46fac0a4a4e543507fe1110e808101277c5a2b5bab0cd2d", 377 | "sha256:e79d94ca58fcafef6395f6352383fa1a76922268fa02caa2272fff501c2fdc78", 378 | "sha256:f3ef98d7b76da5eb19c37fda834d50262ff9167c65658d1d8f974d2e4d90676b", 379 | "sha256:f4c8cabb46ff8e5d61f56a037974228e978f26bfefce4f61a4b1ac0ba7a2ab72" 380 | ], 381 | "markers": "python_version >= '3.6'", 382 | "version": "==0.18.0" 383 | }, 384 | "python-dotenv": { 385 | "hashes": [ 386 | "sha256:aae25dc1ebe97c420f50b81fb0e5c949659af713f31fdb63c749ca68748f34b1", 387 | "sha256:f521bc2ac9a8e03c736f62911605c5d83970021e3fa95b37d769e2bbbe9b6172" 388 | ], 389 | "index": "pypi", 390 | "version": "==0.19.0" 391 | }, 392 | "pytz": { 393 | "hashes": [ 394 | "sha256:83a4a90894bf38e243cf052c8b58f381bfe9a7a483f6a9cab140bc7f702ac4da", 395 | "sha256:eb10ce3e7736052ed3623d49975ce333bcd712c7bb19a58b9e2089d4057d0798" 396 | ], 397 | "version": "==2021.1" 398 | }, 399 | "six": { 400 | "hashes": [ 401 | "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", 402 | "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" 403 | ], 404 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 405 | "version": "==1.16.0" 406 | }, 407 | "sqlalchemy": { 408 | "hashes": [ 409 | "sha256:0566a6e90951590c0307c75f9176597c88ef4be2724958ca1d28e8ae05ec8822", 410 | "sha256:08d9396a2a38e672133266b31ed39b2b1f2b5ec712b5bff5e08033970563316a", 411 | "sha256:0b08a53e40b34205acfeb5328b832f44437956d673a6c09fce55c66ab0e54916", 412 | "sha256:16ef07e102d2d4f974ba9b0d4ac46345a411ad20ad988b3654d59ff08e553b1c", 413 | "sha256:1adf3d25e2e33afbcd48cfad8076f9378793be43e7fec3e4334306cac6bec138", 414 | "sha256:1b38db2417b9f7005d6ceba7ce2a526bf10e3f6f635c0f163e6ed6a42b5b62b2", 415 | "sha256:1ebd69365717becaa1b618220a3df97f7c08aa68e759491de516d1c3667bba54", 416 | "sha256:26b0cd2d5c7ea96d3230cb20acac3d89de3b593339c1447b4d64bfcf4eac1110", 417 | "sha256:2ed67aae8cde4d32aacbdba4f7f38183d14443b714498eada5e5a7a37769c0b7", 418 | "sha256:33a1e86abad782e90976de36150d910748b58e02cd7d35680d441f9a76806c18", 419 | "sha256:41a916d815a3a23cb7fff8d11ad0c9b93369ac074e91e428075e088fe57d5358", 420 | "sha256:6003771ea597346ab1e97f2f58405c6cacbf6a308af3d28a9201a643c0ac7bb3", 421 | "sha256:6400b22e4e41cc27623a9a75630b7719579cd9a3a2027bcf16ad5aaa9a7806c0", 422 | "sha256:6b602e3351f59f3999e9fb8b87e5b95cb2faab6a6ecdb482382ac6fdfbee5266", 423 | "sha256:75cd5d48389a7635393ff5a9214b90695c06b3d74912109c3b00ce7392b69c6c", 424 | "sha256:7ad59e2e16578b6c1a2873e4888134112365605b08a6067dd91e899e026efa1c", 425 | "sha256:7b7778a205f956755e05721eebf9f11a6ac18b2409bff5db53ce5fe7ede79831", 426 | "sha256:842c49dd584aedd75c2ee05f6c950730c3ffcddd21c5824ed0f820808387e1e3", 427 | "sha256:90fe429285b171bcc252e21515703bdc2a4721008d1f13aa5b7150336f8a8493", 428 | "sha256:91cd87d1de0111eaca11ccc3d31af441c753fa2bc22df72e5009cfb0a1af5b03", 429 | "sha256:9a1df8c93a0dd9cef0839917f0c6c49f46c75810cf8852be49884da4a7de3c59", 430 | "sha256:9ebe49c3960aa2219292ea2e5df6acdc425fc828f2f3d50b4cfae1692bcb5f02", 431 | "sha256:a28fe28c359835f3be20c89efd517b35e8f97dbb2ca09c6cf0d9ac07f62d7ef6", 432 | "sha256:a36ea43919e51b0de0c0bc52bcfdad7683f6ea9fb81b340cdabb9df0e045e0f7", 433 | "sha256:a505ecc0642f52e7c65afb02cc6181377d833b7df0994ecde15943b18d0fa89c", 434 | "sha256:a79abdb404d9256afb8aeaa0d3a4bc7d3b6d8b66103d8b0f2f91febd3909976e", 435 | "sha256:c211e8ec81522ce87b0b39f0cf0712c998d4305a030459a0e115a2b3dc71598f", 436 | "sha256:dd4ed12a775f2cde4519f4267d3601990a97d8ecde5c944ab06bfd6e8e8ea177", 437 | "sha256:e37621b37c73b034997b5116678862f38ee70e5a054821c7b19d0e55df270dec", 438 | "sha256:e93978993a2ad0af43f132be3ea8805f56b2f2cd223403ec28d3e7d5c6d39ed1" 439 | ], 440 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", 441 | "version": "==1.4.25" 442 | }, 443 | "werkzeug": { 444 | "hashes": [ 445 | "sha256:1de1db30d010ff1af14a009224ec49ab2329ad2cde454c8a708130642d579c42", 446 | "sha256:6c1ec500dcdba0baa27600f6a22f6333d8b662d22027ff9f6202e3367413caa8" 447 | ], 448 | "markers": "python_version >= '3.6'", 449 | "version": "==2.0.1" 450 | } 451 | }, 452 | "develop": {} 453 | } 454 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Giya 2 | 3 | An awesome REST boilerplate that uses Flask-RESTX (formerly Flask-RESTPlus). 4 | It has the usual API features to get you started and off the ground, 5 | it's also designed to be easily scalable and extendable. 6 | 7 | I wrote this boilerplate because I found that a lot of Flask REST boilerplates are either 8 | doing too much, is lacking, or it simply doesn't fit my needs. 9 | 10 | 11 | # Features 12 | 13 | * Full featured framework for fast, easy, and documented API with [Flask-RESTX](https://flask-restx.readthedocs.io/en/latest/) 14 | * JSON Web Token Authentication with [Flask-JWT-Extended](https://flask-jwt-extended.readthedocs.io/en/stable/) 15 | * Swagger Documentation (Part of Flask-RESTX). 16 | * Unit Testing. 17 | * Database ORM with [Flask-SQLAlchemy](https://flask-sqlalchemy.palletsprojects.com/en/2.x/) 18 | * Database Migrations using [Flask-Migrate](https://github.com/miguelgrinberg/flask-migrate) 19 | * Object serialization/deserialization with [Flask-Marshmallow](https://flask-marshmallow.readthedocs.io/en/latest/) 20 | * Data validations with Marshmallow [Marshmallow](https://marshmallow.readthedocs.io/en/stable/quickstart.html#validation) 21 | 22 | ## Flask CLI help command output: 23 | ```sh 24 | Usage: flask [OPTIONS] COMMAND [ARGS]... 25 | 26 | A general utility script for Flask applications. 27 | 28 | Provides commands from Flask, extensions, and the application. Loads the 29 | application defined in the FLASK_APP environment variable, or from a 30 | wsgi.py file. Setting the FLASK_ENV environment variable to 'development' 31 | will enable debug mode. 32 | 33 | $ export FLASK_APP=giya.py 34 | $ export FLASK_ENV=development 35 | $ flask run 36 | 37 | Options: 38 | --version Show the flask version 39 | --help Show this message and exit. 40 | 41 | Commands: 42 | db Perform database migrations. 43 | routes Show the routes for the app. 44 | run Run a development server. 45 | shell Run a shell in the app context. 46 | test Run unit tests 47 | ``` 48 | 49 | # Pre-requisites 50 | 51 | This boilerplate uses `SQLite` as its database, make sure you have it installed. 52 | `Pipenv` is recommended to help manage the dependencies and virtualenv. 53 | 54 | You can also use other DBs like `PostGreSQL`, make sure you have it setup and update your `DATABASE_URL` in your configs. 55 | Read more at [Flask-SQLAlchemy's](https://flask-sqlalchemy.palletsprojects.com/en/2.x/) documentations. 56 | 57 | It uses [Black](https://github.com/psf/black) for code styling/formatting. 58 | 59 | # Usage 60 | 61 | ## Notes 62 | 63 | By default the `/` route is used by the `auth` blueprint. 64 | 65 | The rest of the resources are found in `/api` (This is the docs route by default, this can be changed easily). 66 | 67 | **Note**: Pipenv seems to have been becoming unmaintained or unsupported, so `virtualenv` is recommended to manage your packages and Python environment, hence why `requirements.txt` has been generated. 68 | 69 | ## Installing with Pipenv 70 | ```sh 71 | # Clone the repo 72 | $ git clone https://github.com/X1Zeth2X/flask-restx-boilerplate.git 73 | 74 | # Install packages with pipenv 75 | $ pipenv install 76 | ``` 77 | 78 | ## Running 79 | Please specify your app's environment variables in a `.env` file, otherwise Flask CLI wouldn't find your app. 80 | 81 | ```sh 82 | # .env file example 83 | export FLASK_APP=giya 84 | 85 | # configs: production, testing, development, and default (uses DevelopmentConfig) 86 | export FLASK_CONFIG=development 87 | 88 | # Another way of assigning environment variables is: 89 | FLASK_APP=giya 90 | FLASK_CONFIG=development 91 | 92 | # Read more at https://github.com/theskumar/python-dotenv 93 | ``` 94 | 95 | ```sh 96 | # Enter the virtualenv 97 | $ pipenv shell 98 | 99 | # (Optional for development, recommended) 100 | $ flask db init # Initializes a new SQLite database. 101 | $ flask db migrate # Creates the tables in the database. 102 | 103 | # Run the app 104 | $ flask run 105 | ``` 106 | 107 | ## Unit testing 108 | Giya has already some unit tests written, we encourage adding more unit tests as you scale. 109 | 110 | ```sh 111 | # Unit testing 112 | $ flask test 113 | 114 | # Run specific unit test(s) 115 | $ flask test tests.test_auth_api tests.test_user_model ... 116 | ``` 117 | -------------------------------------------------------------------------------- /app/__init__.py: -------------------------------------------------------------------------------- 1 | """ Top level module 2 | 3 | This module: 4 | 5 | - Contains create_app() 6 | - Registers extensions 7 | """ 8 | 9 | from flask import Flask 10 | 11 | # Import extensions 12 | from .extensions import bcrypt, cors, db, jwt, ma 13 | 14 | # Import config 15 | from config import config_by_name 16 | 17 | 18 | def create_app(config_name): 19 | app = Flask(__name__) 20 | app.config.from_object(config_by_name[config_name]) 21 | 22 | register_extensions(app) 23 | 24 | # Register blueprints 25 | from .auth import auth_bp 26 | 27 | app.register_blueprint(auth_bp) 28 | 29 | from .api import api_bp 30 | 31 | app.register_blueprint(api_bp, url_prefix="/api") 32 | 33 | return app 34 | 35 | 36 | def register_extensions(app): 37 | # Registers flask extensions 38 | db.init_app(app) 39 | ma.init_app(app) 40 | jwt.init_app(app) 41 | bcrypt.init_app(app) 42 | cors.init_app(app) 43 | -------------------------------------------------------------------------------- /app/api/__init__.py: -------------------------------------------------------------------------------- 1 | from flask_restx import Api 2 | from flask import Blueprint 3 | 4 | from .user.controller import api as user_ns 5 | 6 | # Import controller APIs as namespaces. 7 | api_bp = Blueprint("api", __name__) 8 | 9 | api = Api(api_bp, title="API", description="Main routes.") 10 | 11 | # API namespaces 12 | api.add_namespace(user_ns) 13 | -------------------------------------------------------------------------------- /app/api/user/__init__.py: -------------------------------------------------------------------------------- 1 | # Entry point 2 | -------------------------------------------------------------------------------- /app/api/user/controller.py: -------------------------------------------------------------------------------- 1 | from flask_restx import Resource 2 | from flask_jwt_extended import jwt_required 3 | 4 | from .service import UserService 5 | from .dto import UserDto 6 | 7 | api = UserDto.api 8 | data_resp = UserDto.data_resp 9 | 10 | 11 | @api.route("/") 12 | class UserGet(Resource): 13 | @api.doc( 14 | "Get a specific user", 15 | responses={ 16 | 200: ("User data successfully sent", data_resp), 17 | 404: "User not found!", 18 | }, 19 | ) 20 | @jwt_required() 21 | def get(self, username): 22 | """ Get a specific user's data by their username """ 23 | return UserService.get_user_data(username) 24 | -------------------------------------------------------------------------------- /app/api/user/dto.py: -------------------------------------------------------------------------------- 1 | from flask_restx import Namespace, fields 2 | 3 | 4 | class UserDto: 5 | 6 | api = Namespace("user", description="User related operations.") 7 | user = api.model( 8 | "User object", 9 | { 10 | "email": fields.String, 11 | "name": fields.String, 12 | "username": fields.String, 13 | "joined_date": fields.DateTime, 14 | "role_id": fields.Integer, 15 | }, 16 | ) 17 | 18 | data_resp = api.model( 19 | "User Data Response", 20 | { 21 | "status": fields.Boolean, 22 | "message": fields.String, 23 | "user": fields.Nested(user), 24 | }, 25 | ) 26 | -------------------------------------------------------------------------------- /app/api/user/service.py: -------------------------------------------------------------------------------- 1 | from flask import current_app 2 | 3 | from app.utils import err_resp, message, internal_err_resp 4 | from app.models.user import User 5 | 6 | 7 | class UserService: 8 | @staticmethod 9 | def get_user_data(username): 10 | """ Get user data by username """ 11 | if not (user := User.query.filter_by(username=username).first()): 12 | return err_resp("User not found!", "user_404", 404) 13 | 14 | from .utils import load_data 15 | 16 | try: 17 | user_data = load_data(user) 18 | 19 | resp = message(True, "User data sent") 20 | resp["user"] = user_data 21 | return resp, 200 22 | 23 | except Exception as error: 24 | current_app.logger.error(error) 25 | return internal_err_resp() 26 | -------------------------------------------------------------------------------- /app/api/user/utils.py: -------------------------------------------------------------------------------- 1 | def load_data(user_db_obj): 2 | """ Load user's data 3 | 4 | Parameters: 5 | - User db object 6 | """ 7 | from app.models.schemas import UserSchema 8 | 9 | user_schema = UserSchema() 10 | 11 | data = user_schema.dump(user_db_obj) 12 | 13 | return data 14 | -------------------------------------------------------------------------------- /app/auth/__init__.py: -------------------------------------------------------------------------------- 1 | from flask_restx import Api 2 | from flask import Blueprint 3 | 4 | # Import auth namespace 5 | from .controller import api as auth_ns 6 | 7 | auth_bp = Blueprint("auth", __name__) 8 | 9 | auth = Api( 10 | auth_bp, title="G-Authenticate", description="Authenticate and receive tokens." 11 | ) 12 | 13 | # API namespaces 14 | auth.add_namespace(auth_ns) 15 | -------------------------------------------------------------------------------- /app/auth/controller.py: -------------------------------------------------------------------------------- 1 | from flask import request 2 | from flask_restx import Resource 3 | 4 | from app.utils import validation_error 5 | 6 | # Auth modules 7 | from .service import AuthService 8 | from .dto import AuthDto 9 | from .utils import LoginSchema, RegisterSchema 10 | 11 | api = AuthDto.api 12 | auth_success = AuthDto.auth_success 13 | 14 | login_schema = LoginSchema() 15 | register_schema = RegisterSchema() 16 | 17 | 18 | @api.route("/login") 19 | class AuthLogin(Resource): 20 | """ User login endpoint 21 | User registers then receives the user's information and access_token 22 | """ 23 | 24 | auth_login = AuthDto.auth_login 25 | 26 | @api.doc( 27 | "Auth login", 28 | responses={ 29 | 200: ("Logged in", auth_success), 30 | 400: "Validations failed.", 31 | 403: "Incorrect password or incomplete credentials.", 32 | 404: "Email does not match any account.", 33 | }, 34 | ) 35 | @api.expect(auth_login, validate=True) 36 | def post(self): 37 | """ Login using email and password """ 38 | # Grab the json data 39 | login_data = request.get_json() 40 | 41 | # Validate data 42 | if (errors := login_schema.validate(login_data)) : 43 | return validation_error(False, errors), 400 44 | 45 | return AuthService.login(login_data) 46 | 47 | 48 | @api.route("/register") 49 | class AuthRegister(Resource): 50 | """ User register endpoint 51 | User registers then receives the user's information and access_token 52 | """ 53 | 54 | auth_register = AuthDto.auth_register 55 | 56 | @api.doc( 57 | "Auth registration", 58 | responses={ 59 | 201: ("Successfully registered user.", auth_success), 60 | 400: "Malformed data or validations failed.", 61 | }, 62 | ) 63 | @api.expect(auth_register, validate=True) 64 | def post(self): 65 | """ User registration """ 66 | # Grab the json data 67 | register_data = request.get_json() 68 | 69 | # Validate data 70 | if (errors := register_schema.validate(register_data)) : 71 | return validation_error(False, errors), 400 72 | 73 | return AuthService.register(register_data) 74 | -------------------------------------------------------------------------------- /app/auth/dto.py: -------------------------------------------------------------------------------- 1 | from flask_restx import Namespace, fields 2 | 3 | 4 | class AuthDto: 5 | api = Namespace("auth", description="Authenticate and receive tokens.") 6 | 7 | user_obj = api.model( 8 | "User object", 9 | { 10 | "email": fields.String, 11 | "name": fields.String, 12 | "username": fields.String, 13 | "joined_date": fields.DateTime, 14 | "role_id": fields.Integer, 15 | }, 16 | ) 17 | 18 | auth_login = api.model( 19 | "Login data", 20 | { 21 | "email": fields.String(required=True), 22 | "password": fields.String(required=True), 23 | }, 24 | ) 25 | 26 | auth_register = api.model( 27 | "Registration data", 28 | { 29 | "email": fields.String(required=True), 30 | "username": fields.String(required=True), 31 | # Name is optional 32 | "name": fields.String, 33 | "password": fields.String(required=True), 34 | }, 35 | ) 36 | 37 | auth_success = api.model( 38 | "Auth success response", 39 | { 40 | "status": fields.Boolean, 41 | "message": fields.String, 42 | "access_token": fields.String, 43 | "user": fields.Nested(user_obj), 44 | }, 45 | ) 46 | -------------------------------------------------------------------------------- /app/auth/service.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from flask import current_app 3 | from flask_jwt_extended import create_access_token 4 | 5 | from app import db 6 | from app.utils import message, err_resp, internal_err_resp 7 | from app.models.user import User 8 | from app.models.schemas import UserSchema 9 | 10 | user_schema = UserSchema() 11 | 12 | 13 | class AuthService: 14 | @staticmethod 15 | def login(data): 16 | # Assign vars 17 | email = data["email"] 18 | password = data["password"] 19 | 20 | try: 21 | # Fetch user data 22 | if not (user := User.query.filter_by(email=email).first()): 23 | return err_resp( 24 | "The email you have entered does not match any account.", 25 | "email_404", 26 | 404, 27 | ) 28 | 29 | elif user and user.verify_password(password): 30 | user_info = user_schema.dump(user) 31 | 32 | access_token = create_access_token(identity=user.id) 33 | 34 | resp = message(True, "Successfully logged in.") 35 | resp["access_token"] = access_token 36 | resp["user"] = user_info 37 | 38 | return resp, 200 39 | 40 | return err_resp( 41 | "Failed to log in, password may be incorrect.", "password_invalid", 401 42 | ) 43 | 44 | except Exception as error: 45 | current_app.logger.error(error) 46 | return internal_err_resp() 47 | 48 | @staticmethod 49 | def register(data): 50 | # Assign vars 51 | 52 | ## Required values 53 | email = data["email"] 54 | username = data["username"] 55 | password = data["password"] 56 | 57 | ## Optional 58 | data_name = data.get("name") 59 | 60 | # Check if the email is taken 61 | if User.query.filter_by(email=email).first() is not None: 62 | return err_resp("Email is already being used.", "email_taken", 403) 63 | 64 | # Check if the username is taken 65 | if User.query.filter_by(username=username).first() is not None: 66 | return err_resp("Username is already taken.", "username_taken", 403) 67 | 68 | try: 69 | new_user = User( 70 | email=email, 71 | username=username, 72 | name=data_name, 73 | password=password, 74 | joined_date=datetime.utcnow(), 75 | ) 76 | 77 | db.session.add(new_user) 78 | db.session.flush() 79 | 80 | # Load the new user's info 81 | user_info = user_schema.dump(new_user) 82 | 83 | # Commit changes to DB 84 | db.session.commit() 85 | 86 | # Create an access token 87 | access_token = create_access_token(identity=new_user.id) 88 | 89 | resp = message(True, "User has been registered.") 90 | resp["access_token"] = access_token 91 | resp["user"] = user_info 92 | 93 | return resp, 201 94 | 95 | except Exception as error: 96 | current_app.logger.error(error) 97 | return internal_err_resp() 98 | -------------------------------------------------------------------------------- /app/auth/utils.py: -------------------------------------------------------------------------------- 1 | # Validations with Marshmallow 2 | from marshmallow import Schema, fields 3 | from marshmallow.validate import Regexp, Length 4 | 5 | 6 | class LoginSchema(Schema): 7 | """ /auth/login [POST] 8 | 9 | Parameters: 10 | - Email 11 | - Password (Str) 12 | """ 13 | 14 | email = fields.Email(required=True, validate=[Length(max=64)]) 15 | password = fields.Str(required=True, validate=[Length(min=8, max=128)]) 16 | 17 | 18 | class RegisterSchema(Schema): 19 | """ /auth/register [POST] 20 | 21 | Parameters: 22 | - Email 23 | - Username (Str) 24 | - Name (Str) 25 | - Password (Str) 26 | """ 27 | 28 | email = fields.Email(required=True, validate=[Length(max=64)]) 29 | username = fields.Str( 30 | required=True, 31 | validate=[ 32 | Length(min=4, max=15), 33 | Regexp( 34 | r"^([A-Za-z0-9_](?:(?:[A-Za-z0-9_]|(?:\.(?!\.))){0,28}(?:[A-Za-z0-9_]))?)$", 35 | error="Invalid username!", 36 | ), 37 | ], 38 | ) 39 | name = fields.Str( 40 | validate=[ 41 | Regexp( 42 | r"^[A-Za-z]+((\s)?((\'|\-|\.)?([A-Za-z])+))*$", error="Invalid name!", 43 | ) 44 | ] 45 | ) 46 | password = fields.Str(required=True, validate=[Length(min=8, max=128)]) 47 | -------------------------------------------------------------------------------- /app/extensions.py: -------------------------------------------------------------------------------- 1 | """ 2 | Extensions module 3 | 4 | Each extension is initialized when app is created. 5 | """ 6 | 7 | from flask_bcrypt import Bcrypt 8 | from flask_cors import CORS 9 | from flask_jwt_extended import JWTManager 10 | 11 | from flask_migrate import Migrate 12 | from flask_sqlalchemy import SQLAlchemy 13 | from flask_marshmallow import Marshmallow 14 | 15 | db = SQLAlchemy() 16 | 17 | bcrypt = Bcrypt() 18 | migrate = Migrate() 19 | cors = CORS() 20 | 21 | jwt = JWTManager() 22 | ma = Marshmallow() 23 | -------------------------------------------------------------------------------- /app/models/__init__.py: -------------------------------------------------------------------------------- 1 | # Entry point 2 | -------------------------------------------------------------------------------- /app/models/schemas.py: -------------------------------------------------------------------------------- 1 | # Model Schemas 2 | from app import ma 3 | 4 | from .user import User 5 | 6 | 7 | class UserSchema(ma.Schema): 8 | class Meta: 9 | # Fields to expose, add more if needed. 10 | fields = ("email", "name", "username", "joined_date", "role_id") 11 | -------------------------------------------------------------------------------- /app/models/user.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from app import db, bcrypt 3 | 4 | # Alias common DB names 5 | Column = db.Column 6 | Model = db.Model 7 | relationship = db.relationship 8 | 9 | 10 | class Permission: 11 | FOLLOW = 1 12 | COMMENT = 2 13 | WRITE = 4 14 | MODERATE = 8 15 | ADMIN = 16 16 | 17 | 18 | class Role(Model): 19 | __tablename__ = "roles" 20 | id = Column(db.Integer, primary_key=True) 21 | name = Column(db.String(64), unique=True) 22 | default = Column(db.Boolean, default=False, index=True) 23 | permissions = Column(db.Integer) 24 | description = Column(db.String(50)) 25 | 26 | users = db.relationship("User", backref="role", lazy="dynamic") 27 | 28 | def __init__(self, **kwargs): 29 | super(Role, self).__init__(**kwargs) 30 | if self.permissions is None: 31 | self.permissions = 0 32 | 33 | def __repr__(self): 34 | return f"<{self.name} - {self.id}>" 35 | 36 | @staticmethod 37 | def insert_roles(): 38 | roles = { 39 | "User": [Permission.FOLLOW, Permission.COMMENT, Permission.WRITE], 40 | "Moderator": [ 41 | Permission.FOLLOW, 42 | Permission.COMMENT, 43 | Permission.WRITE, 44 | Permission.MODERATE, 45 | ], 46 | "Admin": [ 47 | Permission.FOLLOW, 48 | Permission.COMMENT, 49 | Permission.WRITE, 50 | Permission.MODERATE, 51 | Permission.ADMIN, 52 | ], 53 | } 54 | 55 | default_role = "User" 56 | for r in roles: 57 | role = Role.query.filter_by(name=r).first() 58 | if role is None: 59 | role = Role(name=r) 60 | 61 | def has_permission(self, perm): 62 | return self.permissions & perm == perm 63 | 64 | def add_permission(self, perm): 65 | if not self.has_permission(perm): 66 | self.permissions += perm 67 | 68 | def remove_permission(self, perm): 69 | if self.has_permission(perm): 70 | self.permissions -= perm 71 | 72 | def reset_permission(self): 73 | self.permissions = 0 74 | 75 | 76 | class User(Model): 77 | """ User model for storing user related data """ 78 | 79 | id = Column(db.Integer, primary_key=True) 80 | email = Column(db.String(64), unique=True, index=True) 81 | username = Column(db.String(15), unique=True, index=True) 82 | name = Column(db.String(64)) 83 | password_hash = Column(db.String(128)) 84 | 85 | joined_date = Column(db.DateTime, default=datetime.utcnow) 86 | role_id = Column(db.Integer, db.ForeignKey("roles.id")) 87 | 88 | def __init__(self, **kwargs): 89 | super(User, self).__init__(**kwargs) 90 | 91 | @property 92 | def password(self): 93 | raise AttributeError("Password is not a readable attribute") 94 | 95 | @password.setter 96 | def password(self, password): 97 | self.password_hash = bcrypt.generate_password_hash(password).decode("utf-8") 98 | 99 | def verify_password(self, password): 100 | return bcrypt.check_password_hash(self.password_hash, password) 101 | 102 | def __repr__(self): 103 | return f"" 104 | -------------------------------------------------------------------------------- /app/utils.py: -------------------------------------------------------------------------------- 1 | def message(status, message): 2 | response_object = {"status": status, "message": message} 3 | return response_object 4 | 5 | 6 | def validation_error(status, errors): 7 | response_object = {"status": status, "errors": errors} 8 | 9 | return response_object 10 | 11 | 12 | def err_resp(msg, reason, code): 13 | err = message(False, msg) 14 | err["error_reason"] = reason 15 | return err, code 16 | 17 | 18 | def internal_err_resp(): 19 | err = message(False, "Something went wrong during the process!") 20 | err["error_reason"] = "server_error" 21 | return err, 500 22 | -------------------------------------------------------------------------------- /config.py: -------------------------------------------------------------------------------- 1 | import os 2 | from datetime import timedelta 3 | 4 | basedir = os.path.abspath(os.path.dirname(__file__)) 5 | 6 | 7 | class Config: 8 | # Change the secret key in production run. 9 | SECRET_KEY = os.environ.get("SECRET_KEY", os.urandom(24)) 10 | DEBUG = False 11 | 12 | # JWT Extended config 13 | JWT_SECRET_KEY = os.environ.get("JWT_SECRET_KEY", os.urandom(24)) 14 | ## Set the token to expire every week 15 | JWT_ACCESS_TOKEN_EXPIRES = timedelta(days=7) 16 | 17 | 18 | class DevelopmentConfig(Config): 19 | DEBUG = True 20 | SQLALCHEMY_DATABASE_URI = os.environ.get( 21 | "DATABASE_URL", "sqlite:///" + os.path.join(basedir, "data-dev.sqlite") 22 | ) 23 | SQLALCHEMY_TRACK_MODIFICATIONS = False 24 | 25 | # Add logger 26 | 27 | 28 | class TestingConfig(Config): 29 | DEBUG = True 30 | TESTING = True 31 | # In-memory SQLite for testing 32 | SQLALCHEMY_DATABASE_URI = "sqlite:///:memory:" 33 | PRESERVE_CONTEXT_ON_EXCEPTION = False 34 | SQLALCHEMY_TRACK_MODIFICATIONS = False 35 | 36 | 37 | class ProductionConfig(Config): 38 | DEBUG = False 39 | SQLALCHEMY_DATABASE_URI = os.environ.get( 40 | "DATABASE_URL", "sqlite:///" + os.path.join(basedir, "data.sqlite") 41 | ) 42 | SQLALCHEMY_TRACK_MODIFICATIONS = False 43 | 44 | 45 | config_by_name = dict( 46 | development=DevelopmentConfig, 47 | testing=TestingConfig, 48 | production=ProductionConfig, 49 | default=DevelopmentConfig, 50 | ) 51 | -------------------------------------------------------------------------------- /giya.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from dotenv import load_dotenv 4 | 5 | dotenv_path = os.path.join(os.path.dirname(__file__), ".env") 6 | if os.path.exists(dotenv_path): 7 | load_dotenv(dotenv_path) 8 | 9 | ## 10 | 11 | import click 12 | from flask_migrate import Migrate 13 | from app import create_app, db 14 | 15 | # Import models 16 | from app.models.user import User, Role, Permission 17 | 18 | 19 | app = create_app(os.getenv("FLASK_CONFIG") or "default") 20 | migrate = Migrate(app, db) 21 | 22 | 23 | @app.shell_context_processor 24 | def make_shell_context(): 25 | return dict(db=db, User=User, Role=Role, Permission=Permission) 26 | 27 | 28 | @app.cli.command() 29 | @click.argument("test_names", nargs=-1) 30 | def test(test_names): 31 | """ Run unit tests """ 32 | import unittest 33 | 34 | if test_names: 35 | """ Run specific unit tests. 36 | 37 | Example: 38 | $ flask test tests.test_auth_api tests.test_user_model ... 39 | """ 40 | tests = unittest.TestLoader().loadTestsFromNames(test_names) 41 | 42 | else: 43 | tests = unittest.TestLoader().discover("tests", pattern="test*.py") 44 | 45 | result = unittest.TextTestRunner(verbosity=2).run(tests) 46 | if result.wasSuccessful(): 47 | return 0 48 | 49 | # Return 1 if tests failed, won't reach here if succeeded. 50 | return 1 51 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # 2 | # These requirements were autogenerated by pipenv 3 | # To regenerate from the project's Pipfile, run: 4 | # 5 | # pipenv lock --requirements 6 | # 7 | 8 | -i https://pypi.org/simple 9 | alembic==1.7.3; python_version >= '3.6' 10 | aniso8601==9.0.1; python_version >= '3.5' 11 | attrs==21.2.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' 12 | bcrypt==3.2.0; python_version >= '3.6' 13 | cffi==1.14.6 14 | click==8.0.1; python_version >= '3.6' 15 | flask-bcrypt==0.7.1 16 | flask-cors==3.0.10 17 | flask-jwt-extended==4.3.0 18 | flask-marshmallow==0.14.0 19 | flask-migrate==3.1.0 20 | flask-restx==0.5.1 21 | flask-sqlalchemy==2.5.1 22 | flask==2.0.1; python_version >= '3.6' 23 | greenlet==1.1.2; python_version >= '3' and platform_machine == 'aarch64' or (platform_machine == 'ppc64le' or (platform_machine == 'x86_64' or (platform_machine == 'amd64' or (platform_machine == 'AMD64' or (platform_machine == 'win32' or platform_machine == 'WIN32'))))) 24 | itsdangerous==2.0.1; python_version >= '3.6' 25 | jinja2==3.0.1; python_version >= '3.6' 26 | jsonschema==4.0.1; python_version >= '3.7' 27 | mako==1.1.5; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3' 28 | markupsafe==2.0.1; python_version >= '3.6' 29 | marshmallow-sqlalchemy==0.26.1 30 | marshmallow==3.13.0; python_version >= '3.5' 31 | pycparser==2.20; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3' 32 | pyjwt==2.1.0; python_version >= '3.6' 33 | pyrsistent==0.18.0; python_version >= '3.6' 34 | python-dotenv==0.19.0 35 | pytz==2021.1 36 | six==1.16.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3' 37 | sqlalchemy==1.4.25; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5' 38 | werkzeug==2.0.1; python_version >= '3.6' 39 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | # Entry point 2 | -------------------------------------------------------------------------------- /tests/test_auth_api.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from tests.utils.base import BaseTestCase 4 | from tests.utils.common import register_user, login_user 5 | 6 | 7 | class TestAuthBlueprint(BaseTestCase): 8 | def test_register_and_login(self): 9 | """ Test Auth API registration and login """ 10 | # Test registration 11 | data = dict( 12 | email="test@user.com", 13 | username="test.User", 14 | name="Test User", 15 | password="test1234", 16 | ) 17 | 18 | register_resp = register_user(self, data) 19 | register_data = json.loads(register_resp.data.decode()) 20 | 21 | self.assertEquals(register_resp.status_code, 201) 22 | self.assertTrue(register_resp.status) 23 | self.assertEquals(register_data["user"]["username"], data["username"]) 24 | 25 | # Test account login 26 | login_resp = login_user(self, data["email"], data["password"]) 27 | login_data = json.loads(login_resp.data.decode()) 28 | 29 | self.assertEquals(login_resp.status_code, 200) 30 | self.assertTrue(login_resp.status) 31 | self.assertEquals(login_data["user"]["email"], data["email"]) 32 | -------------------------------------------------------------------------------- /tests/test_config.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import os 3 | 4 | from flask import current_app 5 | from app import create_app 6 | from config import basedir 7 | 8 | 9 | class TestDevelopmentConfig(unittest.TestCase): 10 | def test_app_is_development(self): 11 | """ Check if application is running in development mode """ 12 | app = create_app("development") 13 | 14 | self.assertFalse(app.config["SECRET_KEY"] == "GahNooSlasHLinUcks") 15 | self.assertTrue(app.config["DEBUG"]) 16 | self.assertFalse(current_app is None) 17 | self.assertTrue( 18 | app.config["SQLALCHEMY_DATABASE_URI"] 19 | == "sqlite:///" + os.path.join(basedir, "data-dev.sqlite") 20 | ) 21 | 22 | 23 | class TestProductionConfig(unittest.TestCase): 24 | def test_app_is_production(self): 25 | """ Check if application is running in production mode """ 26 | app = create_app("production") 27 | 28 | self.assertTrue(app.config["DEBUG"] is False) 29 | -------------------------------------------------------------------------------- /tests/test_user_api.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from flask_jwt_extended import create_access_token 4 | 5 | from app import db 6 | from app.models.user import User 7 | 8 | from tests.utils.base import BaseTestCase 9 | from tests.utils.common import register_user, login_user 10 | 11 | 12 | def get_user_data(self, access_token, username): 13 | return self.client.get( 14 | f"/api/user/{username}", 15 | headers={"Authorization": f"Bearer {access_token}"}, 16 | content_type="application/json", 17 | ) 18 | 19 | 20 | class TestUserBlueprint(BaseTestCase): 21 | def test_user_get(self): 22 | """ Test getting a user from DB """ 23 | 24 | # Create a mock user 25 | username = "test1234" 26 | user = User(username=username) 27 | 28 | db.session.add(user) 29 | db.session.commit() 30 | 31 | access_token = create_access_token(identity=user.id) 32 | 33 | user_resp = get_user_data(self, access_token, username) 34 | user_data = json.loads(user_resp.data.decode()) 35 | 36 | self.assertTrue(user_resp.status) 37 | self.assertEquals(user_resp.status_code, 200) 38 | self.assertEquals(user_data["user"]["username"], username) 39 | 40 | # Test a 404 request 41 | user_404_resp = get_user_data(self, access_token, "non.existent") 42 | self.assertEquals(user_404_resp.status_code, 404) 43 | -------------------------------------------------------------------------------- /tests/test_user_model.py: -------------------------------------------------------------------------------- 1 | from app import db 2 | from app.models.user import User 3 | from app.models.schemas import UserSchema 4 | 5 | from tests.utils.base import BaseTestCase 6 | 7 | 8 | class TestUserModel(BaseTestCase): 9 | def test_password_setter(self): 10 | u = User(password="cat") 11 | self.assertTrue(u.password_hash is not None) 12 | 13 | def test_no_password_getter(self): 14 | u = User(password="penguin") 15 | with self.assertRaises(AttributeError): 16 | u.password 17 | 18 | def test_password_salts_are_random(self): 19 | u = User(password="penguin") 20 | u2 = User(password="penguin") 21 | self.assertTrue(u.password_hash != u2.password_hash) 22 | 23 | def test_schema(self): 24 | u = User(username="gentoo", password="penguin") 25 | u_dump = UserSchema().dump(u) 26 | 27 | self.assertTrue(u_dump["username"] == "gentoo") 28 | 29 | with self.assertRaises(KeyError): 30 | u_dump["password_hash"] 31 | -------------------------------------------------------------------------------- /tests/utils/__init__.py: -------------------------------------------------------------------------------- 1 | # Entry point 2 | -------------------------------------------------------------------------------- /tests/utils/base.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from app import db, create_app 3 | 4 | 5 | class BaseTestCase(unittest.TestCase): 6 | def setUp(self): 7 | self.app = create_app("testing") 8 | self.app_context = self.app.app_context() 9 | self.app_context.push() 10 | self.client = self.app.test_client() 11 | 12 | db.create_all() 13 | 14 | def tearDown(self): 15 | db.session.remove() 16 | db.drop_all() 17 | self.app_context.pop() 18 | -------------------------------------------------------------------------------- /tests/utils/common.py: -------------------------------------------------------------------------------- 1 | # Commonly used test case functions. 2 | import json 3 | 4 | 5 | def register_user(self, data): 6 | return self.client.post( 7 | "/auth/register", data=json.dumps(data), content_type="application/json", 8 | ) 9 | 10 | 11 | def login_user(self, email, password): 12 | return self.client.post( 13 | "/auth/login", 14 | data=json.dumps(dict(email=email, password=password,)), 15 | content_type="application/json", 16 | ) 17 | --------------------------------------------------------------------------------