├── Dockerfile ├── Pipfile ├── Pipfile.lock ├── README.md ├── app ├── __init__.py ├── __init__.pyc ├── __pycache__ │ ├── __init__.cpython-37.pyc │ ├── api.cpython-37.pyc │ ├── login.cpython-37.pyc │ ├── serializer.cpython-37.pyc │ └── user.cpython-37.pyc ├── api.py ├── api_tests │ ├── __init__.py │ ├── __pycache__ │ │ └── __init__.cpython-37.pyc │ └── unittests │ │ ├── __init__.py │ │ ├── __pycache__ │ │ ├── __init__.cpython-37.pyc │ │ ├── api_flask_base_testcases.cpython-37.pyc │ │ ├── test_auth.cpython-37.pyc │ │ ├── test_flask_api.cpython-37.pyc │ │ ├── test_serpro.cpython-37.pyc │ │ └── test_user.cpython-37.pyc │ │ ├── api_flask_base_testcases.py │ │ ├── test_auth.py │ │ ├── test_flask_api.py │ │ └── test_user.py ├── login.py ├── models │ ├── __pycache__ │ │ └── tables.cpython-37.pyc │ └── tables.py ├── serializer.py ├── user.py └── utils │ ├── __init__.py │ ├── __pycache__ │ ├── __init__.cpython-37.pyc │ ├── decorators.cpython-37.pyc │ ├── exceptions.cpython-37.pyc │ └── serpro.cpython-37.pyc │ └── serpro.py ├── config.py ├── cpf_authentication.log ├── database.db ├── docker-compose.yml ├── htmlcov ├── app___init___py.html ├── app_api_py.html ├── app_api_tests___init___py.html ├── app_api_tests_unittests___init___py.html ├── app_api_tests_unittests_api_flask_base_testcases_py.html ├── app_api_tests_unittests_test_auth_py.html ├── app_api_tests_unittests_test_flask_api_py.html ├── app_api_tests_unittests_test_user_py.html ├── app_login_py.html ├── app_models_tables_py.html ├── app_serializer_py.html ├── app_user_py.html ├── app_utils___init___py.html ├── app_utils_serpro_py.html ├── coverage_html.js ├── index.html ├── jquery.ba-throttle-debounce.min.js ├── jquery.hotkeys.js ├── jquery.isonscreen.js ├── jquery.min.js ├── jquery.tablesorter.min.js ├── keybd_closed.png ├── keybd_open.png ├── status.json └── style.css ├── manager.py ├── manager.pyc ├── postman └── desafio-lendico.postman_collection.json └── requirements.txt /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:latest 2 | 3 | WORKDIR /usr/src/app 4 | 5 | COPY requirements.txt ./ 6 | RUN pip install --no-cache-dir -r requirements.txt 7 | 8 | COPY . . 9 | 10 | CMD ["flask", "run", "--host=0.0.0.0"] 11 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | name = "pypi" 3 | url = "https://pypi.org/simple" 4 | verify_ssl = true 5 | 6 | [dev-packages] 7 | 8 | [packages] 9 | click = "==7.0" 10 | coverage = "==4.5.3" 11 | flask-marshmallow = "==0.10.0" 12 | passlib = "==1.7.1" 13 | requests = "==2.21.0" 14 | marshmallow = "==2.19.0" 15 | marshmallow-sqlalchemy = "==0.16.3" 16 | Flask = "==1.0.2" 17 | Flask-Migrate = "==2.4.0" 18 | Flask-Script = "==2.0.6" 19 | Flask-JWT-Extended = "==3.18.2" 20 | Flask-SQLAlchemy = "==2.3.2" 21 | Markdown = "==3.1" 22 | PyJWT = "==1.7.1" 23 | SQLAlchemy = "==1.2.15" 24 | Flask-RESTful = "==0.3.7" 25 | 26 | [requires] 27 | python_version = "3.7" 28 | -------------------------------------------------------------------------------- /Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "b4c44741791474c1145ae2f7a992cda62b93ce20e9a9f66fcd8952bcc0f5c588" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3.7" 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:828dcaa922155a2b7166c4f36ec45268944e4055c86499bd14319b4c8c0094b7" 22 | ], 23 | "version": "==1.0.10" 24 | }, 25 | "aniso8601": { 26 | "hashes": [ 27 | "sha256:b8a6a9b24611fc50cf2d9b45d371bfdc4fd0581d1cc52254f5502130a776d4af", 28 | "sha256:bb167645c79f7a438f9dfab6161af9bed75508c645b1f07d1158240841d22673" 29 | ], 30 | "version": "==6.0.0" 31 | }, 32 | "certifi": { 33 | "hashes": [ 34 | "sha256:59b7658e26ca9c7339e00f8f4636cdfe59d34fa37b9b04f6f9e9926b3cece1a5", 35 | "sha256:b26104d6835d1f5e49452a26eb2ff87fe7090b89dfcaee5ea2212697e1e1d7ae" 36 | ], 37 | "version": "==2019.3.9" 38 | }, 39 | "chardet": { 40 | "hashes": [ 41 | "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", 42 | "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" 43 | ], 44 | "version": "==3.0.4" 45 | }, 46 | "click": { 47 | "hashes": [ 48 | "sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13", 49 | "sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7" 50 | ], 51 | "index": "pypi", 52 | "version": "==7.0" 53 | }, 54 | "coverage": { 55 | "hashes": [ 56 | "sha256:3684fabf6b87a369017756b551cef29e505cb155ddb892a7a29277b978da88b9", 57 | "sha256:39e088da9b284f1bd17c750ac672103779f7954ce6125fd4382134ac8d152d74", 58 | "sha256:3c205bc11cc4fcc57b761c2da73b9b72a59f8d5ca89979afb0c1c6f9e53c7390", 59 | "sha256:465ce53a8c0f3a7950dfb836438442f833cf6663d407f37d8c52fe7b6e56d7e8", 60 | "sha256:48020e343fc40f72a442c8a1334284620f81295256a6b6ca6d8aa1350c763bbe", 61 | "sha256:5296fc86ab612ec12394565c500b412a43b328b3907c0d14358950d06fd83baf", 62 | "sha256:5f61bed2f7d9b6a9ab935150a6b23d7f84b8055524e7be7715b6513f3328138e", 63 | "sha256:68a43a9f9f83693ce0414d17e019daee7ab3f7113a70c79a3dd4c2f704e4d741", 64 | "sha256:6b8033d47fe22506856fe450470ccb1d8ba1ffb8463494a15cfc96392a288c09", 65 | "sha256:7ad7536066b28863e5835e8cfeaa794b7fe352d99a8cded9f43d1161be8e9fbd", 66 | "sha256:7bacb89ccf4bedb30b277e96e4cc68cd1369ca6841bde7b005191b54d3dd1034", 67 | "sha256:839dc7c36501254e14331bcb98b27002aa415e4af7ea039d9009409b9d2d5420", 68 | "sha256:8f9a95b66969cdea53ec992ecea5406c5bd99c9221f539bca1e8406b200ae98c", 69 | "sha256:932c03d2d565f75961ba1d3cec41ddde00e162c5b46d03f7423edcb807734eab", 70 | "sha256:988529edadc49039d205e0aa6ce049c5ccda4acb2d6c3c5c550c17e8c02c05ba", 71 | "sha256:998d7e73548fe395eeb294495a04d38942edb66d1fa61eb70418871bc621227e", 72 | "sha256:9de60893fb447d1e797f6bf08fdf0dbcda0c1e34c1b06c92bd3a363c0ea8c609", 73 | "sha256:9e80d45d0c7fcee54e22771db7f1b0b126fb4a6c0a2e5afa72f66827207ff2f2", 74 | "sha256:a545a3dfe5082dc8e8c3eb7f8a2cf4f2870902ff1860bd99b6198cfd1f9d1f49", 75 | "sha256:a5d8f29e5ec661143621a8f4de51adfb300d7a476224156a39a392254f70687b", 76 | "sha256:aca06bfba4759bbdb09bf52ebb15ae20268ee1f6747417837926fae990ebc41d", 77 | "sha256:bb23b7a6fd666e551a3094ab896a57809e010059540ad20acbeec03a154224ce", 78 | "sha256:bfd1d0ae7e292105f29d7deaa9d8f2916ed8553ab9d5f39ec65bcf5deadff3f9", 79 | "sha256:c62ca0a38958f541a73cf86acdab020c2091631c137bd359c4f5bddde7b75fd4", 80 | "sha256:c709d8bda72cf4cd348ccec2a4881f2c5848fd72903c185f363d361b2737f773", 81 | "sha256:c968a6aa7e0b56ecbd28531ddf439c2ec103610d3e2bf3b75b813304f8cb7723", 82 | "sha256:df785d8cb80539d0b55fd47183264b7002077859028dfe3070cf6359bf8b2d9c", 83 | "sha256:f406628ca51e0ae90ae76ea8398677a921b36f0bd71aab2099dfed08abd0322f", 84 | "sha256:f46087bbd95ebae244a0eda01a618aff11ec7a069b15a3ef8f6b520db523dcf1", 85 | "sha256:f8019c5279eb32360ca03e9fac40a12667715546eed5c5eb59eb381f2f501260", 86 | "sha256:fc5f4d209733750afd2714e9109816a29500718b32dd9a5db01c0cb3a019b96a" 87 | ], 88 | "index": "pypi", 89 | "version": "==4.5.3" 90 | }, 91 | "flask": { 92 | "hashes": [ 93 | "sha256:2271c0070dbcb5275fad4a82e29f23ab92682dc45f9dfbc22c02ba9b9322ce48", 94 | "sha256:a080b744b7e345ccfcbc77954861cb05b3c63786e93f2b3875e0913d44b43f05" 95 | ], 96 | "index": "pypi", 97 | "version": "==1.0.2" 98 | }, 99 | "flask-jwt-extended": { 100 | "hashes": [ 101 | "sha256:69d683b51cc85f3a0beb187787e8456a1cfe60e3f8d9da00dc0d7c34e38b698a" 102 | ], 103 | "index": "pypi", 104 | "version": "==3.18.2" 105 | }, 106 | "flask-marshmallow": { 107 | "hashes": [ 108 | "sha256:b880246097f526e106395598f52f1b3d3bd0108ee8697bec8a1657499f27e123", 109 | "sha256:db6d1d2610a169e616e0aec3aeef67ed31525a85bcc567cabd832261121273f7" 110 | ], 111 | "index": "pypi", 112 | "version": "==0.10.0" 113 | }, 114 | "flask-migrate": { 115 | "hashes": [ 116 | "sha256:a361578cb829681f860e4de5ed2c48886264512f0c16144e404c36ddc95ab49c", 117 | "sha256:c24d105c5d6cc670de20f8cbfb909e04f4e04b8784d0df070005944de1f21549" 118 | ], 119 | "index": "pypi", 120 | "version": "==2.4.0" 121 | }, 122 | "flask-restful": { 123 | "hashes": [ 124 | "sha256:ecd620c5cc29f663627f99e04f17d1f16d095c83dc1d618426e2ad68b03092f8", 125 | "sha256:f8240ec12349afe8df1db168ea7c336c4e5b0271a36982bff7394f93275f2ca9" 126 | ], 127 | "index": "pypi", 128 | "version": "==0.3.7" 129 | }, 130 | "flask-script": { 131 | "hashes": [ 132 | "sha256:6425963d91054cfcc185807141c7314a9c5ad46325911bd24dcb489bd0161c65" 133 | ], 134 | "index": "pypi", 135 | "version": "==2.0.6" 136 | }, 137 | "flask-sqlalchemy": { 138 | "hashes": [ 139 | "sha256:3bc0fac969dd8c0ace01b32060f0c729565293302f0c4269beed154b46bec50b", 140 | "sha256:5971b9852b5888655f11db634e87725a9031e170f37c0ce7851cf83497f56e53" 141 | ], 142 | "index": "pypi", 143 | "version": "==2.3.2" 144 | }, 145 | "idna": { 146 | "hashes": [ 147 | "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", 148 | "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c" 149 | ], 150 | "version": "==2.8" 151 | }, 152 | "itsdangerous": { 153 | "hashes": [ 154 | "sha256:321b033d07f2a4136d3ec762eac9f16a10ccd60f53c0c91af90217ace7ba1f19", 155 | "sha256:b12271b2047cb23eeb98c8b5622e2e5c5e9abd9784a153e9d8ef9cb4dd09d749" 156 | ], 157 | "version": "==1.1.0" 158 | }, 159 | "jinja2": { 160 | "hashes": [ 161 | "sha256:065c4f02ebe7f7cf559e49ee5a95fb800a9e4528727aec6f24402a5374c65013", 162 | "sha256:14dd6caf1527abb21f08f86c784eac40853ba93edb79552aa1e4b8aef1b61c7b" 163 | ], 164 | "version": "==2.10.1" 165 | }, 166 | "mako": { 167 | "hashes": [ 168 | "sha256:7165919e78e1feb68b4dbe829871ea9941398178fa58e6beedb9ba14acf63965" 169 | ], 170 | "version": "==1.0.10" 171 | }, 172 | "markdown": { 173 | "hashes": [ 174 | "sha256:fc4a6f69a656b8d858d7503bda633f4dd63c2d70cf80abdc6eafa64c4ae8c250", 175 | "sha256:fe463ff51e679377e3624984c829022e2cfb3be5518726b06f608a07a3aad680" 176 | ], 177 | "index": "pypi", 178 | "version": "==3.1" 179 | }, 180 | "markupsafe": { 181 | "hashes": [ 182 | "sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473", 183 | "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161", 184 | "sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235", 185 | "sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5", 186 | "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff", 187 | "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b", 188 | "sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1", 189 | "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e", 190 | "sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183", 191 | "sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66", 192 | "sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1", 193 | "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1", 194 | "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e", 195 | "sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b", 196 | "sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905", 197 | "sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735", 198 | "sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d", 199 | "sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e", 200 | "sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d", 201 | "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c", 202 | "sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21", 203 | "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2", 204 | "sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5", 205 | "sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b", 206 | "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6", 207 | "sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f", 208 | "sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f", 209 | "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7" 210 | ], 211 | "version": "==1.1.1" 212 | }, 213 | "marshmallow": { 214 | "hashes": [ 215 | "sha256:f2668d41a817aaaf6106d9922479bd1a97cae240dfac0eb7e320e0c29148a084", 216 | "sha256:f8e51314623247b5c444e460b2bbb04aee102ca1ce7fb27bb16e3107cb81dfe9" 217 | ], 218 | "index": "pypi", 219 | "version": "==2.19.0" 220 | }, 221 | "marshmallow-sqlalchemy": { 222 | "hashes": [ 223 | "sha256:24d85d85262dad2efdfc26ec4064e5914e6958312310cb9018d3a19247a1f763", 224 | "sha256:30f27173a141dd8d532983d1f97978fbb7be6ef278b337b3bfd0401659e8bcb4" 225 | ], 226 | "index": "pypi", 227 | "version": "==0.16.3" 228 | }, 229 | "passlib": { 230 | "hashes": [ 231 | "sha256:3d948f64138c25633613f303bcc471126eae67c04d5e3f6b7b8ce6242f8653e0", 232 | "sha256:43526aea08fa32c6b6dbbbe9963c4c767285b78147b7437597f992812f69d280" 233 | ], 234 | "index": "pypi", 235 | "version": "==1.7.1" 236 | }, 237 | "pyjwt": { 238 | "hashes": [ 239 | "sha256:5c6eca3c2940464d106b99ba83b00c6add741c9becaec087fb7ccdefea71350e", 240 | "sha256:8d59a976fb773f3e6a39c85636357c4f0e242707394cadadd9814f5cbaa20e96" 241 | ], 242 | "index": "pypi", 243 | "version": "==1.7.1" 244 | }, 245 | "python-dateutil": { 246 | "hashes": [ 247 | "sha256:7e6584c74aeed623791615e26efd690f29817a27c73085b78e4bad02493df2fb", 248 | "sha256:c89805f6f4d64db21ed966fda138f8a5ed7a4fdbc1a8ee329ce1b74e3c74da9e" 249 | ], 250 | "version": "==2.8.0" 251 | }, 252 | "python-editor": { 253 | "hashes": [ 254 | "sha256:1bf6e860a8ad52a14c3ee1252d5dc25b2030618ed80c022598f00176adc8367d", 255 | "sha256:51fda6bcc5ddbbb7063b2af7509e43bd84bfc32a4ff71349ec7847713882327b", 256 | "sha256:5f98b069316ea1c2ed3f67e7f5df6c0d8f10b689964a4a811ff64f0106819ec8" 257 | ], 258 | "version": "==1.0.4" 259 | }, 260 | "pytz": { 261 | "hashes": [ 262 | "sha256:303879e36b721603cc54604edcac9d20401bdbe31e1e4fdee5b9f98d5d31dfda", 263 | "sha256:d747dd3d23d77ef44c6a3526e274af6efeb0a6f1afd5a69ba4d5be4098c8e141" 264 | ], 265 | "version": "==2019.1" 266 | }, 267 | "requests": { 268 | "hashes": [ 269 | "sha256:502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e", 270 | "sha256:7bf2a778576d825600030a110f3c0e3e8edc51dfaafe1c146e39a2027784957b" 271 | ], 272 | "index": "pypi", 273 | "version": "==2.21.0" 274 | }, 275 | "six": { 276 | "hashes": [ 277 | "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", 278 | "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" 279 | ], 280 | "version": "==1.12.0" 281 | }, 282 | "sqlalchemy": { 283 | "hashes": [ 284 | "sha256:809547455d012734b4252081db1e6b4fc731de2299f3755708c39863625e1c77" 285 | ], 286 | "index": "pypi", 287 | "version": "==1.2.15" 288 | }, 289 | "urllib3": { 290 | "hashes": [ 291 | "sha256:2393a695cd12afedd0dcb26fe5d50d0cf248e5a66f75dbd89a3d4eb333a61af4", 292 | "sha256:a637e5fae88995b256e3409dc4d52c2e2e0ba32c42a6365fee8bbd2238de3cfb" 293 | ], 294 | "version": "==1.24.3" 295 | }, 296 | "werkzeug": { 297 | "hashes": [ 298 | "sha256:865856ebb55c4dcd0630cdd8f3331a1847a819dda7e8c750d3db6f2aa6c0209c", 299 | "sha256:a0b915f0815982fb2a09161cb8f31708052d0951c3ba433ccc5e1aa276507ca6" 300 | ], 301 | "version": "==0.15.4" 302 | } 303 | }, 304 | "develop": {} 305 | } 306 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Coverage Status](https://coveralls.io/repos/github/Tilzen/cpf-status-api/badge.svg?branch=master)](https://coveralls.io/github/Tilzen/cpf-status-api?branch=master) 2 | # API Situação Cadastral CPF 3 | CPFs para testes: 4 | - 40442820135 5 | - 63017285995 6 | - 91708635203 7 | - 58136053391 8 | - 40532176871 9 | - 47123586964 10 | - 07691852312 11 | - 10975384600 12 | - 01648527949 13 | - 47893062592 14 | - 98302514705 15 | 16 | 17 | ## Instruções 18 | 19 | 20 | Status possíveis: 21 | 22 | - Regular: Quando o CPF não apresenta nenhuma irregularidade ou pendência. Devemos zelar para que nosso CPF esteja sempre assim. 23 | 24 | - Nula: Um CPF apresenta situação nula quando a Receita Federal, órgão responsável pela emissão do documento, detecta alguma fraude na inscrição, gerando, certamente, a anulação do documento. 25 | 26 | - Suspensa: Um CPF Suspenso. Nesse caso, o cadastramento ou inscrição do contribuinte não está completo ou não está correto, resultando nessa situação. 27 | 28 | - Cancelada: Já o cancelamento de um CPF pode ser causado pelo falecimento de seu titular. Ou ainda, através de uma decisão judicial ou administrativa. 29 | 30 | - Pendente de Regularização: Talvez a situação mais simples de ser resolvida. O CPF que esteja Pendente de Regularização está nessa situação por não ter entregue Declaração do Imposto de Renda da Pessoa Física (DIRPF). E, portanto, tinha obrigação, pelo menos uma vez, nos últimos cinco anos, como nos informa o site Brasil Consultas. 31 | 32 | 33 | 34 | ## API Endpoints 35 | `GET /` ou `GET /docs` ou `GET /documentation` 36 | - Apresenta a documentação da API 37 | 38 | 39 | `POST /register` 40 | #### Responses 41 | body: 42 | 43 | ```json 44 | "username": "", 45 | "password": "" 46 | ``` 47 | 48 | e retorna: 49 | ```json 50 | "username": "", 51 | "user_token": "" 52 | ``` 53 | 54 | `POST /login` 55 | #### Responses 56 | body: 57 | 58 | ```json 59 | "username": "", 60 | "password": "" 61 | ``` 62 | 63 | e retorna: 64 | ```json 65 | "access_token": "", 66 | "refresh_token": "", 67 | "message": "success" 68 | ``` 69 | 70 | `GET /consult/` 71 | #### Responses 72 | - `200 OK` em caso de requisição bem sucedida 73 | 74 | Respostas bem sucedidas terão o seguinte formato: 75 | 76 | ```json 77 | { 78 | "status": "" 79 | } 80 | ``` 81 | 82 | Exemplo: 83 | 84 | ```json 85 | { 86 | "status": "Regular" 87 | } 88 | ``` 89 | 90 | - `404 Not Found` se o CPF não for encontrado 91 | 92 | Respostas com erros terão o seguinte formato: 93 | 94 | ```json 95 | { 96 | "erro": { 97 | "reason": "error description" 98 | } 99 | } 100 | ``` 101 | 102 | ## Como Executar esse Projeto 103 | ```bash 104 | ~$ docker-compose build 105 | ~$ docker run -p 5000:5000 cpf-status-api 106 | ``` 107 | 108 | ## Gerar as Migrações 109 | ```bash 110 | ~$ flask db init 111 | ~$ flask db migrate 112 | ~$ flask db upgrade 113 | ``` 114 | 115 | ## Como Executar os Testes e Obter a Cobertura 116 | ```bash 117 | 118 | ~$ coverage run --source=app -m unittest discover -s . -v 119 | 120 | # mostra um resumo da cobertura no shell 121 | ~$ coverage report 122 | 123 | # gera o path '/htmlcov' com htmls estáticos da cobertura 124 | ~$ coverage html 125 | ``` 126 | -------------------------------------------------------------------------------- /app/__init__.py: -------------------------------------------------------------------------------- 1 | from flask import Flask 2 | from flask_migrate import Migrate 3 | from flask_jwt_extended import JWTManager 4 | from .serializer import configure_marshmallow 5 | from .models.tables import configure_database 6 | 7 | 8 | def create_app(): 9 | """ 10 | application factory. 11 | """ 12 | 13 | app = Flask(__name__) 14 | app.config.from_object('config') 15 | 16 | configure_database(app) 17 | configure_marshmallow(app) 18 | 19 | Migrate(app, app.db) 20 | JWTManager(app) 21 | 22 | from .api import bp_api 23 | app.register_blueprint(bp_api) 24 | 25 | from .user import bp_user 26 | app.register_blueprint(bp_user) 27 | 28 | from .login import bp_login 29 | app.register_blueprint(bp_login) 30 | 31 | return app 32 | -------------------------------------------------------------------------------- /app/__init__.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tilzen/cpf-status-api/c95a0d97a54700d37df5435a0242287ffbda9b34/app/__init__.pyc -------------------------------------------------------------------------------- /app/__pycache__/__init__.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tilzen/cpf-status-api/c95a0d97a54700d37df5435a0242287ffbda9b34/app/__pycache__/__init__.cpython-37.pyc -------------------------------------------------------------------------------- /app/__pycache__/api.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tilzen/cpf-status-api/c95a0d97a54700d37df5435a0242287ffbda9b34/app/__pycache__/api.cpython-37.pyc -------------------------------------------------------------------------------- /app/__pycache__/login.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tilzen/cpf-status-api/c95a0d97a54700d37df5435a0242287ffbda9b34/app/__pycache__/login.cpython-37.pyc -------------------------------------------------------------------------------- /app/__pycache__/serializer.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tilzen/cpf-status-api/c95a0d97a54700d37df5435a0242287ffbda9b34/app/__pycache__/serializer.cpython-37.pyc -------------------------------------------------------------------------------- /app/__pycache__/user.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tilzen/cpf-status-api/c95a0d97a54700d37df5435a0242287ffbda9b34/app/__pycache__/user.cpython-37.pyc -------------------------------------------------------------------------------- /app/api.py: -------------------------------------------------------------------------------- 1 | from os.path import dirname, join 2 | from logging import INFO, StreamHandler, FileHandler, getLogger, Formatter 3 | from flask import Blueprint, jsonify, request 4 | from flask_restful import Api, Resource 5 | from flask_jwt_extended import jwt_required 6 | from markdown import markdown 7 | from .utils.serpro import serpro_response 8 | 9 | 10 | bp_api = Blueprint('cpf_authentication', __name__) 11 | api = Api(bp_api) 12 | 13 | 14 | def generate_log(cpf: str, serpro_raw: str, status_code: int, file: bool): 15 | """function to generate logs for requests.""" 16 | log_name = 'cpf_authentication.log' 17 | formatter = Formatter((f'%(asctime)s - CPF: {cpf} ' 18 | f'- RAW: {serpro_raw} ' 19 | f'- STATUS: {status_code}')) 20 | logger = getLogger() 21 | 22 | def _file_handler(): 23 | """function to generate file handler""" 24 | file_handler = FileHandler(log_name) 25 | file_handler.setLevel(INFO) 26 | file_handler.setFormatter(formatter) 27 | return file_handler 28 | 29 | 30 | def _console_handler(): 31 | """function to generate console handler""" 32 | console_handler = StreamHandler() 33 | console_handler.setLevel(INFO) 34 | console_handler.setFormatter(formatter) 35 | return console_handler 36 | 37 | logger.addHandler(_file_handler()) if file else None 38 | logger.addHandler(_console_handler()) 39 | logger.info('') 40 | 41 | 42 | @bp_api.route("/") 43 | @bp_api.route("/docs") 44 | @bp_api.route("/documentation") 45 | def index(): 46 | """Present some documentation.""" 47 | 48 | readme_file = join(dirname(bp_api.root_path), 'README.md') 49 | 50 | with open(readme_file, 'r') as markdown_file: 51 | content = markdown_file.read() 52 | return markdown(content) 53 | 54 | 55 | class SituationCPF(Resource): 56 | @jwt_required 57 | def get(self, cpf: str): 58 | """get method to get the cadastral status of the cpf.""" 59 | response, status_code = serpro_response(cpf) 60 | 61 | if status_code == 200: 62 | status = response['situacao']['descricao'] 63 | result = {'status': status} 64 | return jsonify(result) 65 | 66 | generate_log(cpf, response, status_code, True) 67 | return jsonify({'error': {'reason': response}}) 68 | 69 | api.add_resource(SituationCPF, '/consult/') 70 | -------------------------------------------------------------------------------- /app/api_tests/__init__.py: -------------------------------------------------------------------------------- 1 | from .unittests import * 2 | -------------------------------------------------------------------------------- /app/api_tests/__pycache__/__init__.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tilzen/cpf-status-api/c95a0d97a54700d37df5435a0242287ffbda9b34/app/api_tests/__pycache__/__init__.cpython-37.pyc -------------------------------------------------------------------------------- /app/api_tests/unittests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tilzen/cpf-status-api/c95a0d97a54700d37df5435a0242287ffbda9b34/app/api_tests/unittests/__init__.py -------------------------------------------------------------------------------- /app/api_tests/unittests/__pycache__/__init__.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tilzen/cpf-status-api/c95a0d97a54700d37df5435a0242287ffbda9b34/app/api_tests/unittests/__pycache__/__init__.cpython-37.pyc -------------------------------------------------------------------------------- /app/api_tests/unittests/__pycache__/api_flask_base_testcases.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tilzen/cpf-status-api/c95a0d97a54700d37df5435a0242287ffbda9b34/app/api_tests/unittests/__pycache__/api_flask_base_testcases.cpython-37.pyc -------------------------------------------------------------------------------- /app/api_tests/unittests/__pycache__/test_auth.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tilzen/cpf-status-api/c95a0d97a54700d37df5435a0242287ffbda9b34/app/api_tests/unittests/__pycache__/test_auth.cpython-37.pyc -------------------------------------------------------------------------------- /app/api_tests/unittests/__pycache__/test_flask_api.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tilzen/cpf-status-api/c95a0d97a54700d37df5435a0242287ffbda9b34/app/api_tests/unittests/__pycache__/test_flask_api.cpython-37.pyc -------------------------------------------------------------------------------- /app/api_tests/unittests/__pycache__/test_serpro.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tilzen/cpf-status-api/c95a0d97a54700d37df5435a0242287ffbda9b34/app/api_tests/unittests/__pycache__/test_serpro.cpython-37.pyc -------------------------------------------------------------------------------- /app/api_tests/unittests/__pycache__/test_user.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tilzen/cpf-status-api/c95a0d97a54700d37df5435a0242287ffbda9b34/app/api_tests/unittests/__pycache__/test_user.cpython-37.pyc -------------------------------------------------------------------------------- /app/api_tests/unittests/api_flask_base_testcases.py: -------------------------------------------------------------------------------- 1 | from json import loads 2 | from unittest import TestCase 3 | from app import create_app 4 | from flask import url_for 5 | 6 | 7 | class TestAPIBase(TestCase): 8 | def setUp(self): 9 | self.app = create_app() 10 | self.app.testing = True 11 | self.app_context = self.app.test_request_context() 12 | self.app_context.push() 13 | self.client = self.app.test_client() 14 | self.user = { 15 | 'username': 'test', 16 | 'password': '1234' 17 | } 18 | 19 | def tearDown(self): 20 | self.app.db.drop_all() 21 | 22 | def create_user(self): 23 | self.client.post(url_for('user.register'), json=self.user) 24 | 25 | def create_token(self): 26 | login = self.client.post(url_for('login.login'), json=self.user) 27 | return { 28 | 'Authorization': 29 | 'Bearer ' + loads(login.data.decode())['access_token'] 30 | } 31 | -------------------------------------------------------------------------------- /app/api_tests/unittests/test_auth.py: -------------------------------------------------------------------------------- 1 | from flask import url_for 2 | from .api_flask_base_testcases import TestAPIBase 3 | 4 | 5 | class TestLogin(TestAPIBase): 6 | def test_should_generate_a_token(self): 7 | self.create_user() 8 | login = self.client.post(url_for('login.login'), json=self.user) 9 | expected = ['access_token', 'message', 'refresh_token'] 10 | response = list(login.json.keys()) 11 | self.assertEqual(response, expected) 12 | 13 | def test_api_dont_should_login_user_with_error(self): 14 | user = {"username": "test"} 15 | login = self.client.post(url_for('login.login'), json=user) 16 | expected = ['error'] 17 | response = list(login.json.keys()) 18 | self.assertEqual(response, expected) 19 | -------------------------------------------------------------------------------- /app/api_tests/unittests/test_flask_api.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | from flask import url_for 3 | from .api_flask_base_testcases import TestAPIBase 4 | 5 | 6 | class TestDocumentation(TestAPIBase): 7 | def test_documentation_should_return_200(self): 8 | response = self.client.get(url_for('cpf_authentication.index')) 9 | self.assertEqual(response.status_code, 200) 10 | 11 | 12 | class TestCPFSituation(TestAPIBase): 13 | def test_get_cpf_should_return_200(self): 14 | self.create_user() 15 | token = self.create_token() 16 | cpf_example = '40442820135' 17 | response = self.client.get(f'/consult/{cpf_example}', headers=token) 18 | self.assertEqual(response.status_code, 200) 19 | 20 | def test_get_cpf_should_return_error(self): 21 | self.create_user() 22 | token = self.create_token() 23 | cpf_example = '40442820137' 24 | response = self.client.get(f'/consult/{cpf_example}', headers=token) 25 | self.assertIn(b'error', response.data) 26 | 27 | def test_get_cpf_should_return_jwt_message(self): 28 | self.create_user() 29 | token = self.create_token() 30 | cpf_example = '40442820137' 31 | response = self.client.get(f'/consult/{cpf_example}') 32 | self.assertIn(b'msg', response.data) 33 | -------------------------------------------------------------------------------- /app/api_tests/unittests/test_user.py: -------------------------------------------------------------------------------- 1 | from flask import url_for 2 | from .api_flask_base_testcases import TestAPIBase 3 | 4 | 5 | class TestBlueprintUser(TestAPIBase): 6 | def test_api_should_register_the_user(self): 7 | response = self.client.post(url_for('user.register'), json=self.user) 8 | self.assertEqual(response.status_code, 201) 9 | 10 | def test_api_dont_should_register_the_user(self): 11 | user = {"username": "test"} 12 | response = self.client.post(url_for('user.register'), json=user) 13 | self.assertEqual(response.status_code, 401) 14 | -------------------------------------------------------------------------------- /app/login.py: -------------------------------------------------------------------------------- 1 | from datetime import timedelta 2 | from flask import Blueprint, request, jsonify 3 | from flask_jwt_extended import create_access_token, create_refresh_token 4 | from .serializer import UserSchema 5 | from .models.tables import User 6 | 7 | 8 | bp_login = Blueprint('login', __name__) 9 | 10 | 11 | @bp_login.route('/login', methods=['POST']) 12 | def login(): 13 | result = {} 14 | user, error = UserSchema().load(request.json) 15 | 16 | if error: 17 | result = {'error': {'reason': error}} 18 | return jsonify(result), 401 19 | 20 | user = User.query.filter_by(username=user.username).first() 21 | 22 | if user and user.verify_token(request.json['password']): 23 | access_token = create_access_token( 24 | identity=user.user_id, 25 | expires_delta=timedelta(seconds=60) 26 | ) 27 | refresh_token = create_refresh_token(identity=user.user_id) 28 | result = { 29 | 'access_token': access_token, 30 | 'refresh_token': refresh_token, 31 | 'message': 'success' 32 | } 33 | return jsonify(result), 200 34 | 35 | result = {'error': {'reason': 'credenciais inválidas'}} 36 | return jsonify(result), 401 37 | -------------------------------------------------------------------------------- /app/models/__pycache__/tables.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tilzen/cpf-status-api/c95a0d97a54700d37df5435a0242287ffbda9b34/app/models/__pycache__/tables.cpython-37.pyc -------------------------------------------------------------------------------- /app/models/tables.py: -------------------------------------------------------------------------------- 1 | """Definition of database table models.""" 2 | from flask_sqlalchemy import SQLAlchemy 3 | from passlib.handlers.sha2_crypt import sha512_crypt as hashcode 4 | 5 | 6 | db = SQLAlchemy() 7 | 8 | def configure_database(app): 9 | with app.app_context(): 10 | db.init_app(app) 11 | app.db = db 12 | db.create_all() 13 | 14 | 15 | class User(db.Model): 16 | """ 17 | Users model. 18 | """ 19 | __tablename__ = 'users' 20 | 21 | user_id = db.Column(db.Integer, primary_key=True, autoincrement=True) 22 | username = db.Column(db.String) 23 | password = db.Column(db.String) 24 | 25 | def __init__(self, username, password): 26 | self.username = username 27 | self.password = password 28 | 29 | def generate_hash(self): 30 | self.password = hashcode.hash(self.password) 31 | 32 | def verify_token(self, token): 33 | return hashcode.verify(token, self.password) 34 | 35 | def __repr__(self): 36 | return f'' 37 | -------------------------------------------------------------------------------- /app/serializer.py: -------------------------------------------------------------------------------- 1 | from app.models.tables import User 2 | from marshmallow import fields 3 | from flask_marshmallow import Marshmallow 4 | 5 | 6 | marshmallow = Marshmallow() 7 | 8 | def configure_marshmallow(app): 9 | marshmallow.init_app(app) 10 | 11 | 12 | class UserSchema(marshmallow.ModelSchema): 13 | class Meta: 14 | model = User 15 | fields = ('username', 'password') 16 | 17 | username = fields.Str(required=True) 18 | password = fields.Str(required=True) 19 | -------------------------------------------------------------------------------- /app/user.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint, request, jsonify, current_app 2 | from .serializer import UserSchema 3 | 4 | 5 | bp_user = Blueprint('user', __name__) 6 | 7 | @bp_user.route('/register', methods=['POST']) 8 | def register(): 9 | """function of the new user registration route.""" 10 | result = {} 11 | schema = UserSchema() 12 | 13 | user, error = schema.load(request.json) 14 | 15 | if not error: 16 | user.generate_hash() 17 | current_app.db.session.add(user) 18 | current_app.db.session.commit() 19 | return schema.jsonify(user), 201 20 | else: 21 | result = {"error": {"reason": error}} 22 | print(error) 23 | return jsonify(result), 401 24 | -------------------------------------------------------------------------------- /app/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tilzen/cpf-status-api/c95a0d97a54700d37df5435a0242287ffbda9b34/app/utils/__init__.py -------------------------------------------------------------------------------- /app/utils/__pycache__/__init__.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tilzen/cpf-status-api/c95a0d97a54700d37df5435a0242287ffbda9b34/app/utils/__pycache__/__init__.cpython-37.pyc -------------------------------------------------------------------------------- /app/utils/__pycache__/decorators.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tilzen/cpf-status-api/c95a0d97a54700d37df5435a0242287ffbda9b34/app/utils/__pycache__/decorators.cpython-37.pyc -------------------------------------------------------------------------------- /app/utils/__pycache__/exceptions.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tilzen/cpf-status-api/c95a0d97a54700d37df5435a0242287ffbda9b34/app/utils/__pycache__/exceptions.cpython-37.pyc -------------------------------------------------------------------------------- /app/utils/__pycache__/serpro.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tilzen/cpf-status-api/c95a0d97a54700d37df5435a0242287ffbda9b34/app/utils/__pycache__/serpro.cpython-37.pyc -------------------------------------------------------------------------------- /app/utils/serpro.py: -------------------------------------------------------------------------------- 1 | from requests import get 2 | 3 | 4 | def serpro_response(cpf: str, token: str='4e1a1858bdd584fdc077fb7d80f39283') -> [dict, str]: 5 | """function to make requests in the serpro API.""" 6 | headers = { 7 | 'Content-Type': "application/json", 8 | 'Authorization': f"Bearer {token}", 9 | 'Accept': "*/*", 10 | 'Cache-Control': "no-cache", 11 | 'Host': "apigateway.serpro.gov.br", 12 | 'accept-encoding': "gzip, deflate", 13 | 'Connection': "keep-alive", 14 | 'cache-control': "no-cache" 15 | } 16 | 17 | base_url = f'https://apigateway.serpro.gov.br/consulta-cpf-trial/v1/cpf/{cpf}' 18 | response = get(base_url, headers=headers) 19 | 20 | if response.status_code == 200: 21 | return response.json(), response.status_code 22 | return 'Nenhum registro encontrado.', response.status_code 23 | -------------------------------------------------------------------------------- /config.py: -------------------------------------------------------------------------------- 1 | from os.path import dirname, abspath, join 2 | 3 | basedir = abspath(dirname(__file__)) 4 | 5 | SQLALCHEMY_DATABASE_URI = 'sqlite:///' + join(basedir, 'database.db') 6 | SQLALCHEMY_TRACK_MODIFICATIONS = False 7 | DEBUG = True 8 | SECRET_KEY = 'secretkey' 9 | JWT_SECRET_KEY = 'jwtsecretkey' 10 | -------------------------------------------------------------------------------- /cpf_authentication.log: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tilzen/cpf-status-api/c95a0d97a54700d37df5435a0242287ffbda9b34/cpf_authentication.log -------------------------------------------------------------------------------- /database.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tilzen/cpf-status-api/c95a0d97a54700d37df5435a0242287ffbda9b34/database.db -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.4' 2 | 3 | services: 4 | desafio-lendico: 5 | build: . 6 | volumes: 7 | - .:/usr/src/app 8 | ports: 9 | - 5000:80 10 | environment: 11 | FLASK_APP: app 12 | FLASK_ENV: development 13 | -------------------------------------------------------------------------------- /htmlcov/app___init___py.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | Coverage for app/__init__.py: 100% 12 | 13 | 14 | 15 | 16 | 17 | 18 | 21 | 22 | 23 | 24 | 42 | 43 |
44 | Hide keyboard shortcuts 45 |

Hot-keys on this page

46 |
47 |

48 | r 49 | m 50 | x 51 | p   toggle line displays 52 |

53 |

54 | j 55 | k   next/prev highlighted chunk 56 |

57 |

58 | 0   (zero) top of page 59 |

60 |

61 | 1   (one) first highlighted chunk 62 |

63 |
64 |
65 | 66 |
67 | 68 | 69 | 103 | 137 | 138 |
70 |

1

71 |

2

72 |

3

73 |

4

74 |

5

75 |

6

76 |

7

77 |

8

78 |

9

79 |

10

80 |

11

81 |

12

82 |

13

83 |

14

84 |

15

85 |

16

86 |

17

87 |

18

88 |

19

89 |

20

90 |

21

91 |

22

92 |

23

93 |

24

94 |

25

95 |

26

96 |

27

97 |

28

98 |

29

99 |

30

100 |

31

101 | 102 |
104 |

from flask import Flask 

105 |

from flask_migrate import Migrate 

106 |

from flask_jwt_extended import JWTManager 

107 |

from .serializer import configure_marshmallow 

108 |

from .models.tables import configure_database 

109 |

 

110 |

 

111 |

def create_app(): 

112 |

""" 

113 |

application factory. 

114 |

""" 

115 |

 

116 |

app = Flask(__name__) 

117 |

app.config.from_object('config') 

118 |

 

119 |

configure_database(app) 

120 |

configure_marshmallow(app) 

121 |

 

122 |

Migrate(app, app.db) 

123 |

JWTManager(app) 

124 |

 

125 |

from .api import bp_api 

126 |

app.register_blueprint(bp_api) 

127 |

 

128 |

from .user import bp_user 

129 |

app.register_blueprint(bp_user) 

130 |

 

131 |

from .login import bp_login 

132 |

app.register_blueprint(bp_login) 

133 |

 

134 |

return app 

135 | 136 |
139 |
140 | 141 | 149 | 150 | 151 | 152 | -------------------------------------------------------------------------------- /htmlcov/app_api_tests___init___py.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | Coverage for app/api_tests/__init__.py: 100% 12 | 13 | 14 | 15 | 16 | 17 | 18 | 21 | 22 | 23 | 24 | 42 | 43 |
44 | Hide keyboard shortcuts 45 |

Hot-keys on this page

46 |
47 |

48 | r 49 | m 50 | x 51 | p   toggle line displays 52 |

53 |

54 | j 55 | k   next/prev highlighted chunk 56 |

57 |

58 | 0   (zero) top of page 59 |

60 |

61 | 1   (one) first highlighted chunk 62 |

63 |
64 |
65 | 66 |
67 | 68 | 69 | 73 | 77 | 78 |
70 |

1

71 | 72 |
74 |

from .unittests import * 

75 | 76 |
79 |
80 | 81 | 89 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /htmlcov/app_api_tests_unittests___init___py.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | Coverage for app/api_tests/unittests/__init__.py: 100% 12 | 13 | 14 | 15 | 16 | 17 | 18 | 21 | 22 | 23 | 24 | 42 | 43 |
44 | Hide keyboard shortcuts 45 |

Hot-keys on this page

46 |
47 |

48 | r 49 | m 50 | x 51 | p   toggle line displays 52 |

53 |

54 | j 55 | k   next/prev highlighted chunk 56 |

57 |

58 | 0   (zero) top of page 59 |

60 |

61 | 1   (one) first highlighted chunk 62 |

63 |
64 |
65 | 66 |
67 | 68 | 69 | 72 | 75 | 76 |
70 | 71 | 73 | 74 |
77 |
78 | 79 | 87 | 88 | 89 | 90 | -------------------------------------------------------------------------------- /htmlcov/app_api_tests_unittests_api_flask_base_testcases_py.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | Coverage for app/api_tests/unittests/api_flask_base_testcases.py: 100% 12 | 13 | 14 | 15 | 16 | 17 | 18 | 21 | 22 | 23 | 24 | 42 | 43 |
44 | Hide keyboard shortcuts 45 |

Hot-keys on this page

46 |
47 |

48 | r 49 | m 50 | x 51 | p   toggle line displays 52 |

53 |

54 | j 55 | k   next/prev highlighted chunk 56 |

57 |

58 | 0   (zero) top of page 59 |

60 |

61 | 1   (one) first highlighted chunk 62 |

63 |
64 |
65 | 66 |
67 | 68 | 69 | 102 | 135 | 136 |
70 |

1

71 |

2

72 |

3

73 |

4

74 |

5

75 |

6

76 |

7

77 |

8

78 |

9

79 |

10

80 |

11

81 |

12

82 |

13

83 |

14

84 |

15

85 |

16

86 |

17

87 |

18

88 |

19

89 |

20

90 |

21

91 |

22

92 |

23

93 |

24

94 |

25

95 |

26

96 |

27

97 |

28

98 |

29

99 |

30

100 | 101 |
103 |

from json import loads 

104 |

from unittest import TestCase 

105 |

from app import create_app 

106 |

from flask import url_for 

107 |

 

108 |

 

109 |

class TestAPIBase(TestCase): 

110 |

def setUp(self): 

111 |

self.app = create_app() 

112 |

self.app.testing = True 

113 |

self.app_context = self.app.test_request_context() 

114 |

self.app_context.push() 

115 |

self.client = self.app.test_client() 

116 |

self.user = { 

117 |

'username': 'test', 

118 |

'user_token': '1234' 

119 |

} 

120 |

 

121 |

def tearDown(self): 

122 |

self.app.db.drop_all() 

123 |

 

124 |

def create_user(self): 

125 |

self.client.post(url_for('user.register'), json=self.user) 

126 |

 

127 |

def create_token(self): 

128 |

login = self.client.post(url_for('login.login'), json=self.user) 

129 |

return { 

130 |

'Authorization': 

131 |

'Bearer ' + loads(login.data.decode())['access_token'] 

132 |

} 

133 | 134 |
137 |
138 | 139 | 147 | 148 | 149 | 150 | -------------------------------------------------------------------------------- /htmlcov/app_api_tests_unittests_test_auth_py.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | Coverage for app/api_tests/unittests/test_auth.py: 100% 12 | 13 | 14 | 15 | 16 | 17 | 18 | 21 | 22 | 23 | 24 | 42 | 43 |
44 | Hide keyboard shortcuts 45 |

Hot-keys on this page

46 |
47 |

48 | r 49 | m 50 | x 51 | p   toggle line displays 52 |

53 |

54 | j 55 | k   next/prev highlighted chunk 56 |

57 |

58 | 0   (zero) top of page 59 |

60 |

61 | 1   (one) first highlighted chunk 62 |

63 |
64 |
65 | 66 |
67 | 68 | 69 | 90 | 111 | 112 |
70 |

1

71 |

2

72 |

3

73 |

4

74 |

5

75 |

6

76 |

7

77 |

8

78 |

9

79 |

10

80 |

11

81 |

12

82 |

13

83 |

14

84 |

15

85 |

16

86 |

17

87 |

18

88 | 89 |
91 |

from flask import url_for 

92 |

from .api_flask_base_testcases import TestAPIBase 

93 |

 

94 |

 

95 |

class TestLogin(TestAPIBase): 

96 |

def test_should_generate_a_token(self): 

97 |

self.create_user() 

98 |

login = self.client.post(url_for('login.login'), json=self.user) 

99 |

expected = ['access_token', 'message', 'refresh_token'] 

100 |

response = list(login.json.keys()) 

101 |

self.assertEqual(response, expected) 

102 |

 

103 |

def test_api_dont_should_login_user(self): 

104 |

user = {"username": "test"} 

105 |

login = self.client.post(url_for('login.login'), json=user) 

106 |

expected = ['error'] 

107 |

response = list(login.json.keys()) 

108 |

self.assertEqual(response, expected) 

109 | 110 |
113 |
114 | 115 | 123 | 124 | 125 | 126 | -------------------------------------------------------------------------------- /htmlcov/app_api_tests_unittests_test_flask_api_py.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | Coverage for app/api_tests/unittests/test_flask_api.py: 100% 12 | 13 | 14 | 15 | 16 | 17 | 18 | 21 | 22 | 23 | 24 | 42 | 43 |
44 | Hide keyboard shortcuts 45 |

Hot-keys on this page

46 |
47 |

48 | r 49 | m 50 | x 51 | p   toggle line displays 52 |

53 |

54 | j 55 | k   next/prev highlighted chunk 56 |

57 |

58 | 0   (zero) top of page 59 |

60 |

61 | 1   (one) first highlighted chunk 62 |

63 |
64 |
65 | 66 |
67 | 68 | 69 | 104 | 139 | 140 |
70 |

1

71 |

2

72 |

3

73 |

4

74 |

5

75 |

6

76 |

7

77 |

8

78 |

9

79 |

10

80 |

11

81 |

12

82 |

13

83 |

14

84 |

15

85 |

16

86 |

17

87 |

18

88 |

19

89 |

20

90 |

21

91 |

22

92 |

23

93 |

24

94 |

25

95 |

26

96 |

27

97 |

28

98 |

29

99 |

30

100 |

31

101 |

32

102 | 103 |
105 |

from unittest import TestCase 

106 |

from flask import url_for 

107 |

from .api_flask_base_testcases import TestAPIBase 

108 |

 

109 |

 

110 |

class TestDocumentation(TestAPIBase): 

111 |

def test_documentation_should_return_200(self): 

112 |

response = self.client.get(url_for('cpf_authentication.index')) 

113 |

self.assertEqual(response.status_code, 200) 

114 |

 

115 |

 

116 |

class TestCPFSituation(TestAPIBase): 

117 |

def test_get_cpf_should_return_200(self): 

118 |

self.create_user() 

119 |

token = self.create_token() 

120 |

cpf_example = '40442820135' 

121 |

response = self.client.get(f'/consult/{cpf_example}', headers=token) 

122 |

self.assertEqual(response.status_code, 200) 

123 |

 

124 |

def test_get_cpf_should_return_error(self): 

125 |

self.create_user() 

126 |

token = self.create_token() 

127 |

cpf_example = '40442820137' 

128 |

response = self.client.get(f'/consult/{cpf_example}', headers=token) 

129 |

self.assertIn(b'error', response.data) 

130 |

 

131 |

def test_get_cpf_should_return_jwt_message(self): 

132 |

self.create_user() 

133 |

token = self.create_token() 

134 |

cpf_example = '40442820137' 

135 |

response = self.client.get(f'/consult/{cpf_example}') 

136 |

self.assertIn(b'msg', response.data) 

137 | 138 |
141 |
142 | 143 | 151 | 152 | 153 | 154 | -------------------------------------------------------------------------------- /htmlcov/app_api_tests_unittests_test_user_py.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | Coverage for app/api_tests/unittests/test_user.py: 100% 12 | 13 | 14 | 15 | 16 | 17 | 18 | 21 | 22 | 23 | 24 | 42 | 43 |
44 | Hide keyboard shortcuts 45 |

Hot-keys on this page

46 |
47 |

48 | r 49 | m 50 | x 51 | p   toggle line displays 52 |

53 |

54 | j 55 | k   next/prev highlighted chunk 56 |

57 |

58 | 0   (zero) top of page 59 |

60 |

61 | 1   (one) first highlighted chunk 62 |

63 |
64 |
65 | 66 |
67 | 68 | 69 | 85 | 101 | 102 |
70 |

1

71 |

2

72 |

3

73 |

4

74 |

5

75 |

6

76 |

7

77 |

8

78 |

9

79 |

10

80 |

11

81 |

12

82 |

13

83 | 84 |
86 |

from flask import url_for 

87 |

from .api_flask_base_testcases import TestAPIBase 

88 |

 

89 |

 

90 |

class TestBlueprintUser(TestAPIBase): 

91 |

def test_api_should_register_the_user(self): 

92 |

response = self.client.post(url_for('user.register'), json=self.user) 

93 |

self.assertEqual(response.status_code, 201) 

94 |

 

95 |

def test_api_dont_should_register_the_user(self): 

96 |

user = {"username": "test"} 

97 |

response = self.client.post(url_for('user.register'), json=user) 

98 |

self.assertEqual(response.status_code, 401) 

99 | 100 |
103 |
104 | 105 | 113 | 114 | 115 | 116 | -------------------------------------------------------------------------------- /htmlcov/app_login_py.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | Coverage for app/login.py: 90% 12 | 13 | 14 | 15 | 16 | 17 | 18 | 21 | 22 | 23 | 24 | 42 | 43 |
44 | Hide keyboard shortcuts 45 |

Hot-keys on this page

46 |
47 |

48 | r 49 | m 50 | x 51 | p   toggle line displays 52 |

53 |

54 | j 55 | k   next/prev highlighted chunk 56 |

57 |

58 | 0   (zero) top of page 59 |

60 |

61 | 1   (one) first highlighted chunk 62 |

63 |
64 |
65 | 66 |
67 | 68 | 69 | 108 | 147 | 148 |
70 |

1

71 |

2

72 |

3

73 |

4

74 |

5

75 |

6

76 |

7

77 |

8

78 |

9

79 |

10

80 |

11

81 |

12

82 |

13

83 |

14

84 |

15

85 |

16

86 |

17

87 |

18

88 |

19

89 |

20

90 |

21

91 |

22

92 |

23

93 |

24

94 |

25

95 |

26

96 |

27

97 |

28

98 |

29

99 |

30

100 |

31

101 |

32

102 |

33

103 |

34

104 |

35

105 |

36

106 | 107 |
109 |

from datetime import timedelta 

110 |

from flask import Blueprint, request, jsonify 

111 |

from flask_jwt_extended import create_access_token, create_refresh_token 

112 |

from .serializer import UserSchema 

113 |

from .models.tables import User 

114 |

 

115 |

 

116 |

bp_login = Blueprint('login', __name__) 

117 |

 

118 |

 

119 |

@bp_login.route('/login', methods=['POST']) 

120 |

def login(): 

121 |

result = {} 

122 |

user, error = UserSchema().load(request.json) 

123 |

 

124 |

if error: 

125 |

result = {'error': {'reason': error}} 

126 |

return jsonify(result), 401 

127 |

 

128 |

user = User.query.filter_by(username=user.username).first() 

129 |

 

130 |

if user and user.verify_token(request.json['user_token']): 

131 |

access_token = create_access_token( 

132 |

identity=user.user_id, 

133 |

expires_delta=timedelta(seconds=1) 

134 |

) 

135 |

refresh_token = create_refresh_token(identity=user.user_id) 

136 |

result = { 

137 |

'access_token': access_token, 

138 |

'refresh_token': refresh_token, 

139 |

'message': 'success' 

140 |

} 

141 |

return jsonify(result), 200 

142 |

 

143 |

result = {'error': {'reason': 'credenciais inválidas'}} 

144 |

return jsonify(result), 401 

145 | 146 |
149 |
150 | 151 | 159 | 160 | 161 | 162 | -------------------------------------------------------------------------------- /htmlcov/app_models_tables_py.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | Coverage for app/models/tables.py: 95% 12 | 13 | 14 | 15 | 16 | 17 | 18 | 21 | 22 | 23 | 24 | 42 | 43 |
44 | Hide keyboard shortcuts 45 |

Hot-keys on this page

46 |
47 |

48 | r 49 | m 50 | x 51 | p   toggle line displays 52 |

53 |

54 | j 55 | k   next/prev highlighted chunk 56 |

57 |

58 | 0   (zero) top of page 59 |

60 |

61 | 1   (one) first highlighted chunk 62 |

63 |
64 |
65 | 66 |
67 | 68 | 69 | 108 | 147 | 148 |
70 |

1

71 |

2

72 |

3

73 |

4

74 |

5

75 |

6

76 |

7

77 |

8

78 |

9

79 |

10

80 |

11

81 |

12

82 |

13

83 |

14

84 |

15

85 |

16

86 |

17

87 |

18

88 |

19

89 |

20

90 |

21

91 |

22

92 |

23

93 |

24

94 |

25

95 |

26

96 |

27

97 |

28

98 |

29

99 |

30

100 |

31

101 |

32

102 |

33

103 |

34

104 |

35

105 |

36

106 | 107 |
109 |

"""Definition of database table models.""" 

110 |

from flask_sqlalchemy import SQLAlchemy 

111 |

from passlib.handlers.sha2_crypt import sha512_crypt as hashcode 

112 |

 

113 |

 

114 |

db = SQLAlchemy() 

115 |

 

116 |

def configure_database(app): 

117 |

with app.app_context(): 

118 |

db.init_app(app) 

119 |

app.db = db 

120 |

db.create_all() 

121 |

 

122 |

 

123 |

class User(db.Model): 

124 |

""" 

125 |

Users model. 

126 |

""" 

127 |

__tablename__ = 'users' 

128 |

 

129 |

user_id = db.Column(db.Integer, primary_key=True, autoincrement=True) 

130 |

username = db.Column(db.String) 

131 |

user_token = db.Column(db.String) 

132 |

 

133 |

def __init__(self, username, user_token): 

134 |

self.username = username 

135 |

self.user_token = user_token 

136 |

 

137 |

def generate_hash(self): 

138 |

self.user_token = hashcode.hash(self.user_token) 

139 |

 

140 |

def verify_token(self, token): 

141 |

return hashcode.verify(token, self.user_token) 

142 |

 

143 |

def __repr__(self): 

144 |

return f'<User {self.username}>' 

145 | 146 |
149 |
150 | 151 | 159 | 160 | 161 | 162 | -------------------------------------------------------------------------------- /htmlcov/app_serializer_py.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | Coverage for app/serializer.py: 100% 12 | 13 | 14 | 15 | 16 | 17 | 18 | 21 | 22 | 23 | 24 | 42 | 43 |
44 | Hide keyboard shortcuts 45 |

Hot-keys on this page

46 |
47 |

48 | r 49 | m 50 | x 51 | p   toggle line displays 52 |

53 |

54 | j 55 | k   next/prev highlighted chunk 56 |

57 |

58 | 0   (zero) top of page 59 |

60 |

61 | 1   (one) first highlighted chunk 62 |

63 |
64 |
65 | 66 |
67 | 68 | 69 | 90 | 111 | 112 |
70 |

1

71 |

2

72 |

3

73 |

4

74 |

5

75 |

6

76 |

7

77 |

8

78 |

9

79 |

10

80 |

11

81 |

12

82 |

13

83 |

14

84 |

15

85 |

16

86 |

17

87 |

18

88 | 89 |
91 |

from app.models.tables import User 

92 |

from marshmallow import fields 

93 |

from flask_marshmallow import Marshmallow 

94 |

 

95 |

 

96 |

marshmallow = Marshmallow() 

97 |

 

98 |

def configure_marshmallow(app): 

99 |

marshmallow.init_app(app) 

100 |

 

101 |

 

102 |

class UserSchema(marshmallow.ModelSchema): 

103 |

class Meta: 

104 |

model = User 

105 |

fields = ('username', 'user_token') 

106 |

 

107 |

username = fields.Str(required=True) 

108 |

user_token = fields.Str(required=True) 

109 | 110 |
113 |
114 | 115 | 123 | 124 | 125 | 126 | -------------------------------------------------------------------------------- /htmlcov/app_user_py.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | Coverage for app/user.py: 100% 12 | 13 | 14 | 15 | 16 | 17 | 18 | 21 | 22 | 23 | 24 | 42 | 43 |
44 | Hide keyboard shortcuts 45 |

Hot-keys on this page

46 |
47 |

48 | r 49 | m 50 | x 51 | p   toggle line displays 52 |

53 |

54 | j 55 | k   next/prev highlighted chunk 56 |

57 |

58 | 0   (zero) top of page 59 |

60 |

61 | 1   (one) first highlighted chunk 62 |

63 |
64 |
65 | 66 |
67 | 68 | 69 | 94 | 119 | 120 |
70 |

1

71 |

2

72 |

3

73 |

4

74 |

5

75 |

6

76 |

7

77 |

8

78 |

9

79 |

10

80 |

11

81 |

12

82 |

13

83 |

14

84 |

15

85 |

16

86 |

17

87 |

18

88 |

19

89 |

20

90 |

21

91 |

22

92 | 93 |
95 |

from flask import Blueprint, request, jsonify, current_app 

96 |

from .serializer import UserSchema 

97 |

 

98 |

 

99 |

bp_user = Blueprint('user', __name__) 

100 |

 

101 |

@bp_user.route('/register', methods=['POST']) 

102 |

def register(): 

103 |

result = {} 

104 |

schema = UserSchema() 

105 |

 

106 |

user, error = schema.load(request.json) 

107 |

 

108 |

if not error: 

109 |

user.generate_hash() 

110 |

current_app.db.session.add(user) 

111 |

current_app.db.session.commit() 

112 |

return schema.jsonify(user), 201 

113 |

else: 

114 |

result = {"error": {"reason": error}} 

115 |

print(error) 

116 |

return jsonify(result), 401 

117 | 118 |
121 |
122 | 123 | 131 | 132 | 133 | 134 | -------------------------------------------------------------------------------- /htmlcov/app_utils___init___py.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | Coverage for app/utils/__init__.py: 100% 12 | 13 | 14 | 15 | 16 | 17 | 18 | 21 | 22 | 23 | 24 | 42 | 43 |
44 | Hide keyboard shortcuts 45 |

Hot-keys on this page

46 |
47 |

48 | r 49 | m 50 | x 51 | p   toggle line displays 52 |

53 |

54 | j 55 | k   next/prev highlighted chunk 56 |

57 |

58 | 0   (zero) top of page 59 |

60 |

61 | 1   (one) first highlighted chunk 62 |

63 |
64 |
65 | 66 |
67 | 68 | 69 | 72 | 75 | 76 |
70 | 71 | 73 | 74 |
77 |
78 | 79 | 87 | 88 | 89 | 90 | -------------------------------------------------------------------------------- /htmlcov/app_utils_serpro_py.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | Coverage for app/utils/serpro.py: 90% 12 | 13 | 14 | 15 | 16 | 17 | 18 | 21 | 22 | 23 | 24 | 42 | 43 |
44 | Hide keyboard shortcuts 45 |

Hot-keys on this page

46 |
47 |

48 | r 49 | m 50 | x 51 | p   toggle line displays 52 |

53 |

54 | j 55 | k   next/prev highlighted chunk 56 |

57 |

58 | 0   (zero) top of page 59 |

60 |

61 | 1   (one) first highlighted chunk 62 |

63 |
64 |
65 | 66 |
67 | 68 | 69 | 98 | 127 | 128 |
70 |

1

71 |

2

72 |

3

73 |

4

74 |

5

75 |

6

76 |

7

77 |

8

78 |

9

79 |

10

80 |

11

81 |

12

82 |

13

83 |

14

84 |

15

85 |

16

86 |

17

87 |

18

88 |

19

89 |

20

90 |

21

91 |

22

92 |

23

93 |

24

94 |

25

95 |

26

96 | 97 |
99 |

from requests import get 

100 |

 

101 |

 

102 |

def serpro_response(cpf: str, token: str='4e1a1858bdd584fdc077fb7d80f39283') -> [dict, str]: 

103 |

headers = { 

104 |

'Content-Type': "application/json", 

105 |

'Authorization': f"Bearer {token}", 

106 |

'Accept': "*/*", 

107 |

'Cache-Control': "no-cache", 

108 |

'Host': "apigateway.serpro.gov.br", 

109 |

'accept-encoding': "gzip, deflate", 

110 |

'Connection': "keep-alive", 

111 |

'cache-control': "no-cache" 

112 |

} 

113 |

 

114 |

base_url = f'https://apigateway.serpro.gov.br/consulta-cpf-trial/v1/cpf/{cpf}' 

115 |

response = get(base_url, headers=headers) 

116 |

 

117 |

if response.status_code == 200: 

118 |

return response.json(), response.status_code 

119 |

return 'Nenhum registro encontrado.', response.status_code 

120 |

 

121 |

 

122 |

if __name__ == '__main__': 

123 |

# 40442820135 

124 |

response = serpro_response('45757673877', '4e1a1858bdd584fdc077fb7d80f39283') 

125 | 126 |
129 |
130 | 131 | 139 | 140 | 141 | 142 | -------------------------------------------------------------------------------- /htmlcov/coverage_html.js: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 2 | // For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt 3 | 4 | // Coverage.py HTML report browser code. 5 | /*jslint browser: true, sloppy: true, vars: true, plusplus: true, maxerr: 50, indent: 4 */ 6 | /*global coverage: true, document, window, $ */ 7 | 8 | coverage = {}; 9 | 10 | // Find all the elements with shortkey_* class, and use them to assign a shortcut key. 11 | coverage.assign_shortkeys = function () { 12 | $("*[class*='shortkey_']").each(function (i, e) { 13 | $.each($(e).attr("class").split(" "), function (i, c) { 14 | if (/^shortkey_/.test(c)) { 15 | $(document).bind('keydown', c.substr(9), function () { 16 | $(e).click(); 17 | }); 18 | } 19 | }); 20 | }); 21 | }; 22 | 23 | // Create the events for the help panel. 24 | coverage.wire_up_help_panel = function () { 25 | $("#keyboard_icon").click(function () { 26 | // Show the help panel, and position it so the keyboard icon in the 27 | // panel is in the same place as the keyboard icon in the header. 28 | $(".help_panel").show(); 29 | var koff = $("#keyboard_icon").offset(); 30 | var poff = $("#panel_icon").position(); 31 | $(".help_panel").offset({ 32 | top: koff.top-poff.top, 33 | left: koff.left-poff.left 34 | }); 35 | }); 36 | $("#panel_icon").click(function () { 37 | $(".help_panel").hide(); 38 | }); 39 | }; 40 | 41 | // Create the events for the filter box. 42 | coverage.wire_up_filter = function () { 43 | // Cache elements. 44 | var table = $("table.index"); 45 | var table_rows = table.find("tbody tr"); 46 | var table_row_names = table_rows.find("td.name a"); 47 | var no_rows = $("#no_rows"); 48 | 49 | // Create a duplicate table footer that we can modify with dynamic summed values. 50 | var table_footer = $("table.index tfoot tr"); 51 | var table_dynamic_footer = table_footer.clone(); 52 | table_dynamic_footer.attr('class', 'total_dynamic hidden'); 53 | table_footer.after(table_dynamic_footer); 54 | 55 | // Observe filter keyevents. 56 | $("#filter").on("keyup change", $.debounce(150, function (event) { 57 | var filter_value = $(this).val(); 58 | 59 | if (filter_value === "") { 60 | // Filter box is empty, remove all filtering. 61 | table_rows.removeClass("hidden"); 62 | 63 | // Show standard footer, hide dynamic footer. 64 | table_footer.removeClass("hidden"); 65 | table_dynamic_footer.addClass("hidden"); 66 | 67 | // Hide placeholder, show table. 68 | if (no_rows.length > 0) { 69 | no_rows.hide(); 70 | } 71 | table.show(); 72 | 73 | } 74 | else { 75 | // Filter table items by value. 76 | var hidden = 0; 77 | var shown = 0; 78 | 79 | // Hide / show elements. 80 | $.each(table_row_names, function () { 81 | var element = $(this).parents("tr"); 82 | 83 | if ($(this).text().indexOf(filter_value) === -1) { 84 | // hide 85 | element.addClass("hidden"); 86 | hidden++; 87 | } 88 | else { 89 | // show 90 | element.removeClass("hidden"); 91 | shown++; 92 | } 93 | }); 94 | 95 | // Show placeholder if no rows will be displayed. 96 | if (no_rows.length > 0) { 97 | if (shown === 0) { 98 | // Show placeholder, hide table. 99 | no_rows.show(); 100 | table.hide(); 101 | } 102 | else { 103 | // Hide placeholder, show table. 104 | no_rows.hide(); 105 | table.show(); 106 | } 107 | } 108 | 109 | // Manage dynamic header: 110 | if (hidden > 0) { 111 | // Calculate new dynamic sum values based on visible rows. 112 | for (var column = 2; column < 20; column++) { 113 | // Calculate summed value. 114 | var cells = table_rows.find('td:nth-child(' + column + ')'); 115 | if (!cells.length) { 116 | // No more columns...! 117 | break; 118 | } 119 | 120 | var sum = 0, numer = 0, denom = 0; 121 | $.each(cells.filter(':visible'), function () { 122 | var ratio = $(this).data("ratio"); 123 | if (ratio) { 124 | var splitted = ratio.split(" "); 125 | numer += parseInt(splitted[0], 10); 126 | denom += parseInt(splitted[1], 10); 127 | } 128 | else { 129 | sum += parseInt(this.innerHTML, 10); 130 | } 131 | }); 132 | 133 | // Get footer cell element. 134 | var footer_cell = table_dynamic_footer.find('td:nth-child(' + column + ')'); 135 | 136 | // Set value into dynamic footer cell element. 137 | if (cells[0].innerHTML.indexOf('%') > -1) { 138 | // Percentage columns use the numerator and denominator, 139 | // and adapt to the number of decimal places. 140 | var match = /\.([0-9]+)/.exec(cells[0].innerHTML); 141 | var places = 0; 142 | if (match) { 143 | places = match[1].length; 144 | } 145 | var pct = numer * 100 / denom; 146 | footer_cell.text(pct.toFixed(places) + '%'); 147 | } 148 | else { 149 | footer_cell.text(sum); 150 | } 151 | } 152 | 153 | // Hide standard footer, show dynamic footer. 154 | table_footer.addClass("hidden"); 155 | table_dynamic_footer.removeClass("hidden"); 156 | } 157 | else { 158 | // Show standard footer, hide dynamic footer. 159 | table_footer.removeClass("hidden"); 160 | table_dynamic_footer.addClass("hidden"); 161 | } 162 | } 163 | })); 164 | 165 | // Trigger change event on setup, to force filter on page refresh 166 | // (filter value may still be present). 167 | $("#filter").trigger("change"); 168 | }; 169 | 170 | // Loaded on index.html 171 | coverage.index_ready = function ($) { 172 | // Look for a cookie containing previous sort settings: 173 | var sort_list = []; 174 | var cookie_name = "COVERAGE_INDEX_SORT"; 175 | var i; 176 | 177 | // This almost makes it worth installing the jQuery cookie plugin: 178 | if (document.cookie.indexOf(cookie_name) > -1) { 179 | var cookies = document.cookie.split(";"); 180 | for (i = 0; i < cookies.length; i++) { 181 | var parts = cookies[i].split("="); 182 | 183 | if ($.trim(parts[0]) === cookie_name && parts[1]) { 184 | sort_list = eval("[[" + parts[1] + "]]"); 185 | break; 186 | } 187 | } 188 | } 189 | 190 | // Create a new widget which exists only to save and restore 191 | // the sort order: 192 | $.tablesorter.addWidget({ 193 | id: "persistentSort", 194 | 195 | // Format is called by the widget before displaying: 196 | format: function (table) { 197 | if (table.config.sortList.length === 0 && sort_list.length > 0) { 198 | // This table hasn't been sorted before - we'll use 199 | // our stored settings: 200 | $(table).trigger('sorton', [sort_list]); 201 | } 202 | else { 203 | // This is not the first load - something has 204 | // already defined sorting so we'll just update 205 | // our stored value to match: 206 | sort_list = table.config.sortList; 207 | } 208 | } 209 | }); 210 | 211 | // Configure our tablesorter to handle the variable number of 212 | // columns produced depending on report options: 213 | var headers = []; 214 | var col_count = $("table.index > thead > tr > th").length; 215 | 216 | headers[0] = { sorter: 'text' }; 217 | for (i = 1; i < col_count-1; i++) { 218 | headers[i] = { sorter: 'digit' }; 219 | } 220 | headers[col_count-1] = { sorter: 'percent' }; 221 | 222 | // Enable the table sorter: 223 | $("table.index").tablesorter({ 224 | widgets: ['persistentSort'], 225 | headers: headers 226 | }); 227 | 228 | coverage.assign_shortkeys(); 229 | coverage.wire_up_help_panel(); 230 | coverage.wire_up_filter(); 231 | 232 | // Watch for page unload events so we can save the final sort settings: 233 | $(window).unload(function () { 234 | document.cookie = cookie_name + "=" + sort_list.toString() + "; path=/"; 235 | }); 236 | }; 237 | 238 | // -- pyfile stuff -- 239 | 240 | coverage.pyfile_ready = function ($) { 241 | // If we're directed to a particular line number, highlight the line. 242 | var frag = location.hash; 243 | if (frag.length > 2 && frag[1] === 'n') { 244 | $(frag).addClass('highlight'); 245 | coverage.set_sel(parseInt(frag.substr(2), 10)); 246 | } 247 | else { 248 | coverage.set_sel(0); 249 | } 250 | 251 | $(document) 252 | .bind('keydown', 'j', coverage.to_next_chunk_nicely) 253 | .bind('keydown', 'k', coverage.to_prev_chunk_nicely) 254 | .bind('keydown', '0', coverage.to_top) 255 | .bind('keydown', '1', coverage.to_first_chunk) 256 | ; 257 | 258 | $(".button_toggle_run").click(function (evt) {coverage.toggle_lines(evt.target, "run");}); 259 | $(".button_toggle_exc").click(function (evt) {coverage.toggle_lines(evt.target, "exc");}); 260 | $(".button_toggle_mis").click(function (evt) {coverage.toggle_lines(evt.target, "mis");}); 261 | $(".button_toggle_par").click(function (evt) {coverage.toggle_lines(evt.target, "par");}); 262 | 263 | coverage.assign_shortkeys(); 264 | coverage.wire_up_help_panel(); 265 | 266 | coverage.init_scroll_markers(); 267 | 268 | // Rebuild scroll markers after window high changing 269 | $(window).resize(coverage.resize_scroll_markers); 270 | }; 271 | 272 | coverage.toggle_lines = function (btn, cls) { 273 | btn = $(btn); 274 | var hide = "hide_"+cls; 275 | if (btn.hasClass(hide)) { 276 | $("#source ."+cls).removeClass(hide); 277 | btn.removeClass(hide); 278 | } 279 | else { 280 | $("#source ."+cls).addClass(hide); 281 | btn.addClass(hide); 282 | } 283 | }; 284 | 285 | // Return the nth line div. 286 | coverage.line_elt = function (n) { 287 | return $("#t" + n); 288 | }; 289 | 290 | // Return the nth line number div. 291 | coverage.num_elt = function (n) { 292 | return $("#n" + n); 293 | }; 294 | 295 | // Return the container of all the code. 296 | coverage.code_container = function () { 297 | return $(".linenos"); 298 | }; 299 | 300 | // Set the selection. b and e are line numbers. 301 | coverage.set_sel = function (b, e) { 302 | // The first line selected. 303 | coverage.sel_begin = b; 304 | // The next line not selected. 305 | coverage.sel_end = (e === undefined) ? b+1 : e; 306 | }; 307 | 308 | coverage.to_top = function () { 309 | coverage.set_sel(0, 1); 310 | coverage.scroll_window(0); 311 | }; 312 | 313 | coverage.to_first_chunk = function () { 314 | coverage.set_sel(0, 1); 315 | coverage.to_next_chunk(); 316 | }; 317 | 318 | coverage.is_transparent = function (color) { 319 | // Different browsers return different colors for "none". 320 | return color === "transparent" || color === "rgba(0, 0, 0, 0)"; 321 | }; 322 | 323 | coverage.to_next_chunk = function () { 324 | var c = coverage; 325 | 326 | // Find the start of the next colored chunk. 327 | var probe = c.sel_end; 328 | var color, probe_line; 329 | while (true) { 330 | probe_line = c.line_elt(probe); 331 | if (probe_line.length === 0) { 332 | return; 333 | } 334 | color = probe_line.css("background-color"); 335 | if (!c.is_transparent(color)) { 336 | break; 337 | } 338 | probe++; 339 | } 340 | 341 | // There's a next chunk, `probe` points to it. 342 | var begin = probe; 343 | 344 | // Find the end of this chunk. 345 | var next_color = color; 346 | while (next_color === color) { 347 | probe++; 348 | probe_line = c.line_elt(probe); 349 | next_color = probe_line.css("background-color"); 350 | } 351 | c.set_sel(begin, probe); 352 | c.show_selection(); 353 | }; 354 | 355 | coverage.to_prev_chunk = function () { 356 | var c = coverage; 357 | 358 | // Find the end of the prev colored chunk. 359 | var probe = c.sel_begin-1; 360 | var probe_line = c.line_elt(probe); 361 | if (probe_line.length === 0) { 362 | return; 363 | } 364 | var color = probe_line.css("background-color"); 365 | while (probe > 0 && c.is_transparent(color)) { 366 | probe--; 367 | probe_line = c.line_elt(probe); 368 | if (probe_line.length === 0) { 369 | return; 370 | } 371 | color = probe_line.css("background-color"); 372 | } 373 | 374 | // There's a prev chunk, `probe` points to its last line. 375 | var end = probe+1; 376 | 377 | // Find the beginning of this chunk. 378 | var prev_color = color; 379 | while (prev_color === color) { 380 | probe--; 381 | probe_line = c.line_elt(probe); 382 | prev_color = probe_line.css("background-color"); 383 | } 384 | c.set_sel(probe+1, end); 385 | c.show_selection(); 386 | }; 387 | 388 | // Return the line number of the line nearest pixel position pos 389 | coverage.line_at_pos = function (pos) { 390 | var l1 = coverage.line_elt(1), 391 | l2 = coverage.line_elt(2), 392 | result; 393 | if (l1.length && l2.length) { 394 | var l1_top = l1.offset().top, 395 | line_height = l2.offset().top - l1_top, 396 | nlines = (pos - l1_top) / line_height; 397 | if (nlines < 1) { 398 | result = 1; 399 | } 400 | else { 401 | result = Math.ceil(nlines); 402 | } 403 | } 404 | else { 405 | result = 1; 406 | } 407 | return result; 408 | }; 409 | 410 | // Returns 0, 1, or 2: how many of the two ends of the selection are on 411 | // the screen right now? 412 | coverage.selection_ends_on_screen = function () { 413 | if (coverage.sel_begin === 0) { 414 | return 0; 415 | } 416 | 417 | var top = coverage.line_elt(coverage.sel_begin); 418 | var next = coverage.line_elt(coverage.sel_end-1); 419 | 420 | return ( 421 | (top.isOnScreen() ? 1 : 0) + 422 | (next.isOnScreen() ? 1 : 0) 423 | ); 424 | }; 425 | 426 | coverage.to_next_chunk_nicely = function () { 427 | coverage.finish_scrolling(); 428 | if (coverage.selection_ends_on_screen() === 0) { 429 | // The selection is entirely off the screen: select the top line on 430 | // the screen. 431 | var win = $(window); 432 | coverage.select_line_or_chunk(coverage.line_at_pos(win.scrollTop())); 433 | } 434 | coverage.to_next_chunk(); 435 | }; 436 | 437 | coverage.to_prev_chunk_nicely = function () { 438 | coverage.finish_scrolling(); 439 | if (coverage.selection_ends_on_screen() === 0) { 440 | var win = $(window); 441 | coverage.select_line_or_chunk(coverage.line_at_pos(win.scrollTop() + win.height())); 442 | } 443 | coverage.to_prev_chunk(); 444 | }; 445 | 446 | // Select line number lineno, or if it is in a colored chunk, select the 447 | // entire chunk 448 | coverage.select_line_or_chunk = function (lineno) { 449 | var c = coverage; 450 | var probe_line = c.line_elt(lineno); 451 | if (probe_line.length === 0) { 452 | return; 453 | } 454 | var the_color = probe_line.css("background-color"); 455 | if (!c.is_transparent(the_color)) { 456 | // The line is in a highlighted chunk. 457 | // Search backward for the first line. 458 | var probe = lineno; 459 | var color = the_color; 460 | while (probe > 0 && color === the_color) { 461 | probe--; 462 | probe_line = c.line_elt(probe); 463 | if (probe_line.length === 0) { 464 | break; 465 | } 466 | color = probe_line.css("background-color"); 467 | } 468 | var begin = probe + 1; 469 | 470 | // Search forward for the last line. 471 | probe = lineno; 472 | color = the_color; 473 | while (color === the_color) { 474 | probe++; 475 | probe_line = c.line_elt(probe); 476 | color = probe_line.css("background-color"); 477 | } 478 | 479 | coverage.set_sel(begin, probe); 480 | } 481 | else { 482 | coverage.set_sel(lineno); 483 | } 484 | }; 485 | 486 | coverage.show_selection = function () { 487 | var c = coverage; 488 | 489 | // Highlight the lines in the chunk 490 | c.code_container().find(".highlight").removeClass("highlight"); 491 | for (var probe = c.sel_begin; probe > 0 && probe < c.sel_end; probe++) { 492 | c.num_elt(probe).addClass("highlight"); 493 | } 494 | 495 | c.scroll_to_selection(); 496 | }; 497 | 498 | coverage.scroll_to_selection = function () { 499 | // Scroll the page if the chunk isn't fully visible. 500 | if (coverage.selection_ends_on_screen() < 2) { 501 | // Need to move the page. The html,body trick makes it scroll in all 502 | // browsers, got it from http://stackoverflow.com/questions/3042651 503 | var top = coverage.line_elt(coverage.sel_begin); 504 | var top_pos = parseInt(top.offset().top, 10); 505 | coverage.scroll_window(top_pos - 30); 506 | } 507 | }; 508 | 509 | coverage.scroll_window = function (to_pos) { 510 | $("html,body").animate({scrollTop: to_pos}, 200); 511 | }; 512 | 513 | coverage.finish_scrolling = function () { 514 | $("html,body").stop(true, true); 515 | }; 516 | 517 | coverage.init_scroll_markers = function () { 518 | var c = coverage; 519 | // Init some variables 520 | c.lines_len = $('td.text p').length; 521 | c.body_h = $('body').height(); 522 | c.header_h = $('div#header').height(); 523 | c.missed_lines = $('td.text p.mis, td.text p.par'); 524 | 525 | // Build html 526 | c.resize_scroll_markers(); 527 | }; 528 | 529 | coverage.resize_scroll_markers = function () { 530 | var c = coverage, 531 | min_line_height = 3, 532 | max_line_height = 10, 533 | visible_window_h = $(window).height(); 534 | 535 | $('#scroll_marker').remove(); 536 | // Don't build markers if the window has no scroll bar. 537 | if (c.body_h <= visible_window_h) { 538 | return; 539 | } 540 | 541 | $("body").append("
 
"); 542 | var scroll_marker = $('#scroll_marker'), 543 | marker_scale = scroll_marker.height() / c.body_h, 544 | line_height = scroll_marker.height() / c.lines_len; 545 | 546 | // Line height must be between the extremes. 547 | if (line_height > min_line_height) { 548 | if (line_height > max_line_height) { 549 | line_height = max_line_height; 550 | } 551 | } 552 | else { 553 | line_height = min_line_height; 554 | } 555 | 556 | var previous_line = -99, 557 | last_mark, 558 | last_top; 559 | 560 | c.missed_lines.each(function () { 561 | var line_top = Math.round($(this).offset().top * marker_scale), 562 | id_name = $(this).attr('id'), 563 | line_number = parseInt(id_name.substring(1, id_name.length)); 564 | 565 | if (line_number === previous_line + 1) { 566 | // If this solid missed block just make previous mark higher. 567 | last_mark.css({ 568 | 'height': line_top + line_height - last_top 569 | }); 570 | } 571 | else { 572 | // Add colored line in scroll_marker block. 573 | scroll_marker.append('
'); 574 | last_mark = $('#m' + line_number); 575 | last_mark.css({ 576 | 'height': line_height, 577 | 'top': line_top 578 | }); 579 | last_top = line_top; 580 | } 581 | 582 | previous_line = line_number; 583 | }); 584 | }; 585 | -------------------------------------------------------------------------------- /htmlcov/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Coverage report 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 19 | 20 | 21 | 22 | 35 | 36 |
37 | Hide keyboard shortcuts 38 |

Hot-keys on this page

39 |
40 |

41 | n 42 | s 43 | m 44 | x 45 | 46 | c   change column sorting 47 |

48 |
49 |
50 | 51 |
52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 |
Modulestatementsmissingexcludedcoverage
Total2134098%
app/__init__.py1900100%
app/api.py4400100%
app/api_tests/__init__.py100100%
app/api_tests/unittests/__init__.py000100%
app/api_tests/unittests/api_flask_base_testcases.py1900100%
app/api_tests/unittests/test_auth.py1500100%
app/api_tests/unittests/test_flask_api.py2600100%
app/api_tests/unittests/test_user.py1000100%
app/login.py202090%
app/models/tables.py221095%
app/serializer.py1200100%
app/user.py1500100%
app/utils/__init__.py000100%
app/utils/serpro.py101090%
205 | 206 |

207 | No items found using the specified filter. 208 |

209 |
210 | 211 | 219 | 220 | 221 | 222 | -------------------------------------------------------------------------------- /htmlcov/jquery.ba-throttle-debounce.min.js: -------------------------------------------------------------------------------- 1 | /* 2 | * jQuery throttle / debounce - v1.1 - 3/7/2010 3 | * http://benalman.com/projects/jquery-throttle-debounce-plugin/ 4 | * 5 | * Copyright (c) 2010 "Cowboy" Ben Alman 6 | * Dual licensed under the MIT and GPL licenses. 7 | * http://benalman.com/about/license/ 8 | */ 9 | (function(b,c){var $=b.jQuery||b.Cowboy||(b.Cowboy={}),a;$.throttle=a=function(e,f,j,i){var h,d=0;if(typeof f!=="boolean"){i=j;j=f;f=c}function g(){var o=this,m=+new Date()-d,n=arguments;function l(){d=+new Date();j.apply(o,n)}function k(){h=c}if(i&&!h){l()}h&&clearTimeout(h);if(i===c&&m>e){l()}else{if(f!==true){h=setTimeout(i?k:l,i===c?e-m:e)}}}if($.guid){g.guid=j.guid=j.guid||$.guid++}return g};$.debounce=function(d,e,f){return f===c?a(d,e,false):a(d,f,e!==false)}})(this); 10 | -------------------------------------------------------------------------------- /htmlcov/jquery.hotkeys.js: -------------------------------------------------------------------------------- 1 | /* 2 | * jQuery Hotkeys Plugin 3 | * Copyright 2010, John Resig 4 | * Dual licensed under the MIT or GPL Version 2 licenses. 5 | * 6 | * Based upon the plugin by Tzury Bar Yochay: 7 | * http://github.com/tzuryby/hotkeys 8 | * 9 | * Original idea by: 10 | * Binny V A, http://www.openjs.com/scripts/events/keyboard_shortcuts/ 11 | */ 12 | 13 | (function(jQuery){ 14 | 15 | jQuery.hotkeys = { 16 | version: "0.8", 17 | 18 | specialKeys: { 19 | 8: "backspace", 9: "tab", 13: "return", 16: "shift", 17: "ctrl", 18: "alt", 19: "pause", 20 | 20: "capslock", 27: "esc", 32: "space", 33: "pageup", 34: "pagedown", 35: "end", 36: "home", 21 | 37: "left", 38: "up", 39: "right", 40: "down", 45: "insert", 46: "del", 22 | 96: "0", 97: "1", 98: "2", 99: "3", 100: "4", 101: "5", 102: "6", 103: "7", 23 | 104: "8", 105: "9", 106: "*", 107: "+", 109: "-", 110: ".", 111 : "/", 24 | 112: "f1", 113: "f2", 114: "f3", 115: "f4", 116: "f5", 117: "f6", 118: "f7", 119: "f8", 25 | 120: "f9", 121: "f10", 122: "f11", 123: "f12", 144: "numlock", 145: "scroll", 191: "/", 224: "meta" 26 | }, 27 | 28 | shiftNums: { 29 | "`": "~", "1": "!", "2": "@", "3": "#", "4": "$", "5": "%", "6": "^", "7": "&", 30 | "8": "*", "9": "(", "0": ")", "-": "_", "=": "+", ";": ": ", "'": "\"", ",": "<", 31 | ".": ">", "/": "?", "\\": "|" 32 | } 33 | }; 34 | 35 | function keyHandler( handleObj ) { 36 | // Only care when a possible input has been specified 37 | if ( typeof handleObj.data !== "string" ) { 38 | return; 39 | } 40 | 41 | var origHandler = handleObj.handler, 42 | keys = handleObj.data.toLowerCase().split(" "); 43 | 44 | handleObj.handler = function( event ) { 45 | // Don't fire in text-accepting inputs that we didn't directly bind to 46 | if ( this !== event.target && (/textarea|select/i.test( event.target.nodeName ) || 47 | event.target.type === "text") ) { 48 | return; 49 | } 50 | 51 | // Keypress represents characters, not special keys 52 | var special = event.type !== "keypress" && jQuery.hotkeys.specialKeys[ event.which ], 53 | character = String.fromCharCode( event.which ).toLowerCase(), 54 | key, modif = "", possible = {}; 55 | 56 | // check combinations (alt|ctrl|shift+anything) 57 | if ( event.altKey && special !== "alt" ) { 58 | modif += "alt+"; 59 | } 60 | 61 | if ( event.ctrlKey && special !== "ctrl" ) { 62 | modif += "ctrl+"; 63 | } 64 | 65 | // TODO: Need to make sure this works consistently across platforms 66 | if ( event.metaKey && !event.ctrlKey && special !== "meta" ) { 67 | modif += "meta+"; 68 | } 69 | 70 | if ( event.shiftKey && special !== "shift" ) { 71 | modif += "shift+"; 72 | } 73 | 74 | if ( special ) { 75 | possible[ modif + special ] = true; 76 | 77 | } else { 78 | possible[ modif + character ] = true; 79 | possible[ modif + jQuery.hotkeys.shiftNums[ character ] ] = true; 80 | 81 | // "$" can be triggered as "Shift+4" or "Shift+$" or just "$" 82 | if ( modif === "shift+" ) { 83 | possible[ jQuery.hotkeys.shiftNums[ character ] ] = true; 84 | } 85 | } 86 | 87 | for ( var i = 0, l = keys.length; i < l; i++ ) { 88 | if ( possible[ keys[i] ] ) { 89 | return origHandler.apply( this, arguments ); 90 | } 91 | } 92 | }; 93 | } 94 | 95 | jQuery.each([ "keydown", "keyup", "keypress" ], function() { 96 | jQuery.event.special[ this ] = { add: keyHandler }; 97 | }); 98 | 99 | })( jQuery ); 100 | -------------------------------------------------------------------------------- /htmlcov/jquery.isonscreen.js: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2010 2 | * @author Laurence Wheway 3 | * Dual licensed under the MIT (http://www.opensource.org/licenses/mit-license.php) 4 | * and GPL (http://www.opensource.org/licenses/gpl-license.php) licenses. 5 | * 6 | * @version 1.2.0 7 | */ 8 | (function($) { 9 | jQuery.extend({ 10 | isOnScreen: function(box, container) { 11 | //ensure numbers come in as intgers (not strings) and remove 'px' is it's there 12 | for(var i in box){box[i] = parseFloat(box[i])}; 13 | for(var i in container){container[i] = parseFloat(container[i])}; 14 | 15 | if(!container){ 16 | container = { 17 | left: $(window).scrollLeft(), 18 | top: $(window).scrollTop(), 19 | width: $(window).width(), 20 | height: $(window).height() 21 | } 22 | } 23 | 24 | if( box.left+box.width-container.left > 0 && 25 | box.left < container.width+container.left && 26 | box.top+box.height-container.top > 0 && 27 | box.top < container.height+container.top 28 | ) return true; 29 | return false; 30 | } 31 | }) 32 | 33 | 34 | jQuery.fn.isOnScreen = function (container) { 35 | for(var i in container){container[i] = parseFloat(container[i])}; 36 | 37 | if(!container){ 38 | container = { 39 | left: $(window).scrollLeft(), 40 | top: $(window).scrollTop(), 41 | width: $(window).width(), 42 | height: $(window).height() 43 | } 44 | } 45 | 46 | if( $(this).offset().left+$(this).width()-container.left > 0 && 47 | $(this).offset().left < container.width+container.left && 48 | $(this).offset().top+$(this).height()-container.top > 0 && 49 | $(this).offset().top < container.height+container.top 50 | ) return true; 51 | return false; 52 | } 53 | })(jQuery); 54 | -------------------------------------------------------------------------------- /htmlcov/jquery.tablesorter.min.js: -------------------------------------------------------------------------------- 1 | 2 | (function($){$.extend({tablesorter:new function(){var parsers=[],widgets=[];this.defaults={cssHeader:"header",cssAsc:"headerSortUp",cssDesc:"headerSortDown",sortInitialOrder:"asc",sortMultiSortKey:"shiftKey",sortForce:null,sortAppend:null,textExtraction:"simple",parsers:{},widgets:[],widgetZebra:{css:["even","odd"]},headers:{},widthFixed:false,cancelSelection:true,sortList:[],headerList:[],dateFormat:"us",decimal:'.',debug:false};function benchmark(s,d){log(s+","+(new Date().getTime()-d.getTime())+"ms");}this.benchmark=benchmark;function log(s){if(typeof console!="undefined"&&typeof console.debug!="undefined"){console.log(s);}else{alert(s);}}function buildParserCache(table,$headers){if(table.config.debug){var parsersDebug="";}var rows=table.tBodies[0].rows;if(table.tBodies[0].rows[0]){var list=[],cells=rows[0].cells,l=cells.length;for(var i=0;i1){arr=arr.concat(checkCellColSpan(table,headerArr,row++));}else{if(table.tHead.length==1||(cell.rowSpan>1||!r[row+1])){arr.push(cell);}}}return arr;};function checkHeaderMetadata(cell){if(($.metadata)&&($(cell).metadata().sorter===false)){return true;};return false;}function checkHeaderOptions(table,i){if((table.config.headers[i])&&(table.config.headers[i].sorter===false)){return true;};return false;}function applyWidget(table){var c=table.config.widgets;var l=c.length;for(var i=0;i');$("tr:first td",table.tBodies[0]).each(function(){colgroup.append($('').css('width',$(this).width()));});$(table).prepend(colgroup);};}function updateHeaderSortCount(table,sortList){var c=table.config,l=sortList.length;for(var i=0;ib)?1:0));};function sortTextDesc(a,b){return((ba)?1:0));};function sortNumeric(a,b){return a-b;};function sortNumericDesc(a,b){return b-a;};function getCachedSortType(parsers,i){return parsers[i].type;};this.construct=function(settings){return this.each(function(){if(!this.tHead||!this.tBodies)return;var $this,$document,$headers,cache,config,shiftDown=0,sortOrder;this.config={};config=$.extend(this.config,$.tablesorter.defaults,settings);$this=$(this);$headers=buildHeaders(this);this.config.parsers=buildParserCache(this,$headers);cache=buildCache(this);var sortCSS=[config.cssDesc,config.cssAsc];fixColumnWidth(this);$headers.click(function(e){$this.trigger("sortStart");var totalRows=($this[0].tBodies[0]&&$this[0].tBodies[0].rows.length)||0;if(!this.sortDisabled&&totalRows>0){var $cell=$(this);var i=this.column;this.order=this.count++%2;if(!e[config.sortMultiSortKey]){config.sortList=[];if(config.sortForce!=null){var a=config.sortForce;for(var j=0;j0){$this.trigger("sorton",[config.sortList]);}applyWidget(this);});};this.addParser=function(parser){var l=parsers.length,a=true;for(var i=0;ibody { 26 | font-size: 16px; 27 | } 28 | 29 | /* Set base font size to 12/16 */ 30 | p { 31 | font-size: .75em; /* 12/16 */ 32 | line-height: 1.33333333em; /* 16/12 */ 33 | } 34 | 35 | table { 36 | border-collapse: collapse; 37 | } 38 | td { 39 | vertical-align: top; 40 | } 41 | table tr.hidden { 42 | display: none !important; 43 | } 44 | 45 | p#no_rows { 46 | display: none; 47 | font-size: 1.2em; 48 | } 49 | 50 | a.nav { 51 | text-decoration: none; 52 | color: inherit; 53 | } 54 | a.nav:hover { 55 | text-decoration: underline; 56 | color: inherit; 57 | } 58 | 59 | /* Page structure */ 60 | #header { 61 | background: #f8f8f8; 62 | width: 100%; 63 | border-bottom: 1px solid #eee; 64 | } 65 | 66 | #source { 67 | padding: 1em; 68 | font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace; 69 | } 70 | 71 | .indexfile #footer { 72 | margin: 1em 3em; 73 | } 74 | 75 | .pyfile #footer { 76 | margin: 1em 1em; 77 | } 78 | 79 | #footer .content { 80 | padding: 0; 81 | font-size: 85%; 82 | font-family: verdana, sans-serif; 83 | color: #666666; 84 | font-style: italic; 85 | } 86 | 87 | #index { 88 | margin: 1em 0 0 3em; 89 | } 90 | 91 | /* Header styles */ 92 | #header .content { 93 | padding: 1em 3em; 94 | } 95 | 96 | h1 { 97 | font-size: 1.25em; 98 | display: inline-block; 99 | } 100 | 101 | #filter_container { 102 | display: inline-block; 103 | float: right; 104 | margin: 0 2em 0 0; 105 | } 106 | #filter_container input { 107 | width: 10em; 108 | } 109 | 110 | h2.stats { 111 | margin-top: .5em; 112 | font-size: 1em; 113 | } 114 | .stats span { 115 | border: 1px solid; 116 | padding: .1em .25em; 117 | margin: 0 .1em; 118 | cursor: pointer; 119 | border-color: #999 #ccc #ccc #999; 120 | } 121 | .stats span.hide_run, .stats span.hide_exc, 122 | .stats span.hide_mis, .stats span.hide_par, 123 | .stats span.par.hide_run.hide_par { 124 | border-color: #ccc #999 #999 #ccc; 125 | } 126 | .stats span.par.hide_run { 127 | border-color: #999 #ccc #ccc #999; 128 | } 129 | 130 | .stats span.run { 131 | background: #ddffdd; 132 | } 133 | .stats span.exc { 134 | background: #eeeeee; 135 | } 136 | .stats span.mis { 137 | background: #ffdddd; 138 | } 139 | .stats span.hide_run { 140 | background: #eeffee; 141 | } 142 | .stats span.hide_exc { 143 | background: #f5f5f5; 144 | } 145 | .stats span.hide_mis { 146 | background: #ffeeee; 147 | } 148 | .stats span.par { 149 | background: #ffffaa; 150 | } 151 | .stats span.hide_par { 152 | background: #ffffcc; 153 | } 154 | 155 | /* Help panel */ 156 | #keyboard_icon { 157 | float: right; 158 | margin: 5px; 159 | cursor: pointer; 160 | } 161 | 162 | .help_panel { 163 | position: absolute; 164 | background: #ffffcc; 165 | padding: .5em; 166 | border: 1px solid #883; 167 | display: none; 168 | } 169 | 170 | .indexfile .help_panel { 171 | width: 20em; height: 4em; 172 | } 173 | 174 | .pyfile .help_panel { 175 | width: 16em; height: 8em; 176 | } 177 | 178 | .help_panel .legend { 179 | font-style: italic; 180 | margin-bottom: 1em; 181 | } 182 | 183 | #panel_icon { 184 | float: right; 185 | cursor: pointer; 186 | } 187 | 188 | .keyhelp { 189 | margin: .75em; 190 | } 191 | 192 | .keyhelp .key { 193 | border: 1px solid black; 194 | border-color: #888 #333 #333 #888; 195 | padding: .1em .35em; 196 | font-family: monospace; 197 | font-weight: bold; 198 | background: #eee; 199 | } 200 | 201 | /* Source file styles */ 202 | .linenos p { 203 | text-align: right; 204 | margin: 0; 205 | padding: 0 .5em; 206 | color: #999999; 207 | font-family: verdana, sans-serif; 208 | font-size: .625em; /* 10/16 */ 209 | line-height: 1.6em; /* 16/10 */ 210 | } 211 | .linenos p.highlight { 212 | background: #ffdd00; 213 | } 214 | .linenos p a { 215 | text-decoration: none; 216 | color: #999999; 217 | } 218 | .linenos p a:hover { 219 | text-decoration: underline; 220 | color: #999999; 221 | } 222 | 223 | td.text { 224 | width: 100%; 225 | } 226 | .text p { 227 | margin: 0; 228 | padding: 0 0 0 .5em; 229 | border-left: 2px solid #ffffff; 230 | white-space: pre; 231 | position: relative; 232 | } 233 | 234 | .text p.mis { 235 | background: #ffdddd; 236 | border-left: 2px solid #ff0000; 237 | } 238 | .text p.run, .text p.run.hide_par { 239 | background: #ddffdd; 240 | border-left: 2px solid #00ff00; 241 | } 242 | .text p.exc { 243 | background: #eeeeee; 244 | border-left: 2px solid #808080; 245 | } 246 | .text p.par, .text p.par.hide_run { 247 | background: #ffffaa; 248 | border-left: 2px solid #eeee99; 249 | } 250 | .text p.hide_run, .text p.hide_exc, .text p.hide_mis, .text p.hide_par, 251 | .text p.hide_run.hide_par { 252 | background: inherit; 253 | } 254 | 255 | .text span.annotate { 256 | font-family: georgia; 257 | color: #666; 258 | float: right; 259 | padding-right: .5em; 260 | } 261 | .text p.hide_par span.annotate { 262 | display: none; 263 | } 264 | .text span.annotate.long { 265 | display: none; 266 | } 267 | .text p:hover span.annotate.long { 268 | display: block; 269 | max-width: 50%; 270 | white-space: normal; 271 | float: right; 272 | position: absolute; 273 | top: 1.75em; 274 | right: 1em; 275 | width: 30em; 276 | height: auto; 277 | color: #333; 278 | background: #ffffcc; 279 | border: 1px solid #888; 280 | padding: .25em .5em; 281 | z-index: 999; 282 | border-radius: .2em; 283 | box-shadow: #cccccc .2em .2em .2em; 284 | } 285 | 286 | /* Syntax coloring */ 287 | .text .com { 288 | color: green; 289 | font-style: italic; 290 | line-height: 1px; 291 | } 292 | .text .key { 293 | font-weight: bold; 294 | line-height: 1px; 295 | } 296 | .text .str { 297 | color: #000080; 298 | } 299 | 300 | /* index styles */ 301 | #index td, #index th { 302 | text-align: right; 303 | width: 5em; 304 | padding: .25em .5em; 305 | border-bottom: 1px solid #eee; 306 | } 307 | #index th { 308 | font-style: italic; 309 | color: #333; 310 | border-bottom: 1px solid #ccc; 311 | cursor: pointer; 312 | } 313 | #index th:hover { 314 | background: #eee; 315 | border-bottom: 1px solid #999; 316 | } 317 | #index td.left, #index th.left { 318 | padding-left: 0; 319 | } 320 | #index td.right, #index th.right { 321 | padding-right: 0; 322 | } 323 | #index th.headerSortDown, #index th.headerSortUp { 324 | border-bottom: 1px solid #000; 325 | white-space: nowrap; 326 | background: #eee; 327 | } 328 | #index th.headerSortDown:after { 329 | content: " ↓"; 330 | } 331 | #index th.headerSortUp:after { 332 | content: " ↑"; 333 | } 334 | #index td.name, #index th.name { 335 | text-align: left; 336 | width: auto; 337 | } 338 | #index td.name a { 339 | text-decoration: none; 340 | color: #000; 341 | } 342 | #index tr.total, 343 | #index tr.total_dynamic { 344 | } 345 | #index tr.total td, 346 | #index tr.total_dynamic td { 347 | font-weight: bold; 348 | border-top: 1px solid #ccc; 349 | border-bottom: none; 350 | } 351 | #index tr.file:hover { 352 | background: #eeeeee; 353 | } 354 | #index tr.file:hover td.name { 355 | text-decoration: underline; 356 | color: #000; 357 | } 358 | 359 | /* scroll marker styles */ 360 | #scroll_marker { 361 | position: fixed; 362 | right: 0; 363 | top: 0; 364 | width: 16px; 365 | height: 100%; 366 | background: white; 367 | border-left: 1px solid #eee; 368 | } 369 | 370 | #scroll_marker .marker { 371 | background: #eedddd; 372 | position: absolute; 373 | min-height: 3px; 374 | width: 100%; 375 | } 376 | -------------------------------------------------------------------------------- /manager.py: -------------------------------------------------------------------------------- 1 | from app import create_app 2 | 3 | 4 | if __name__ == '__main__': 5 | app = create_app() 6 | app.run(host='0.0.0.0', port=80, debug=True) 7 | -------------------------------------------------------------------------------- /manager.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tilzen/cpf-status-api/c95a0d97a54700d37df5435a0242287ffbda9b34/manager.pyc -------------------------------------------------------------------------------- /postman/desafio-lendico.postman_collection.json: -------------------------------------------------------------------------------- 1 | { 2 | "info": { 3 | "_postman_id": "eb0e8526-09a5-4ea3-9626-df28df121640", 4 | "name": "desafio-lendico", 5 | "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" 6 | }, 7 | "item": [ 8 | { 9 | "name": "register-success", 10 | "request": { 11 | "auth": { 12 | "type": "bearer", 13 | "bearer": [ 14 | { 15 | "key": "token", 16 | "value": "pTZMDRLs1kPL9$cWy8cwv.DkDmWor1xpQaZK7SJXWAPVjzNO2sKja1BWkhGqyzqZLUkyN31ixqihacb95xhBmCQA9.XB4zF1n8u1", 17 | "type": "string" 18 | } 19 | ] 20 | }, 21 | "method": "POST", 22 | "header": [ 23 | { 24 | "key": "Content-Type", 25 | "name": "Content-Type", 26 | "value": "application/json", 27 | "type": "text" 28 | } 29 | ], 30 | "body": { 31 | "mode": "raw", 32 | "raw": "{\n\t\"username\":\"test\",\n\t\"password\":\"1234\"\n}" 33 | }, 34 | "url": { 35 | "raw": "http://localhost:5000/register", 36 | "protocol": "http", 37 | "host": [ 38 | "localhost" 39 | ], 40 | "port": "5000", 41 | "path": [ 42 | "register" 43 | ] 44 | }, 45 | "description": "Exemplo de registro do usuário." 46 | }, 47 | "response": [] 48 | }, 49 | { 50 | "name": "login-success", 51 | "request": { 52 | "auth": { 53 | "type": "bearer", 54 | "bearer": [ 55 | { 56 | "key": "token", 57 | "value": "$6$rounds=656000$YZUHMmtA.3a9ZFCp$vYtkrqVFPsHwkGLiicd7sOOTRV7meRck7D3qXBTs6KvheJdp88GfxFFGq/4gSCg6yncxrdXsp/.YaXi6usKjj/", 58 | "type": "string" 59 | } 60 | ] 61 | }, 62 | "method": "POST", 63 | "header": [ 64 | { 65 | "key": "Content-Type", 66 | "name": "Content-Type", 67 | "value": "application/json", 68 | "type": "text" 69 | } 70 | ], 71 | "body": { 72 | "mode": "raw", 73 | "raw": "{\n\t\"username\": \"test\",\n\t\"password\": \"1234\"\n}" 74 | }, 75 | "url": { 76 | "raw": "http://localhost:5000/login", 77 | "protocol": "http", 78 | "host": [ 79 | "localhost" 80 | ], 81 | "port": "5000", 82 | "path": [ 83 | "login" 84 | ] 85 | }, 86 | "description": "Exemplo de login na API." 87 | }, 88 | "response": [] 89 | }, 90 | { 91 | "name": "consult-success", 92 | "protocolProfileBehavior": { 93 | "disableBodyPruning": true 94 | }, 95 | "request": { 96 | "auth": { 97 | "type": "noauth" 98 | }, 99 | "method": "GET", 100 | "header": [ 101 | { 102 | "key": "Authorization", 103 | "value": "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE1NTg2NDE5MTUsIm5iZiI6MTU1ODY0MTkxNSwianRpIjoiYTBlNGJmOWQtODMzYi00NGIyLTg0ODEtODBiZDJiZDcyN2FmIiwiZXhwIjoxNTU4NjQxOTc1LCJpZGVudGl0eSI6MSwiZnJlc2giOmZhbHNlLCJ0eXBlIjoiYWNjZXNzIn0.qAD02_L9pFEUTvmdrtsoZz1Zk-PTzR4FlaYVLqO94dg", 104 | "type": "text" 105 | } 106 | ], 107 | "body": { 108 | "mode": "raw", 109 | "raw": "" 110 | }, 111 | "url": { 112 | "raw": "localhost:5000/consult/40442820135", 113 | "host": [ 114 | "localhost" 115 | ], 116 | "port": "5000", 117 | "path": [ 118 | "consult", 119 | "40442820135" 120 | ] 121 | }, 122 | "description": "Exemplo de consulta na API." 123 | }, 124 | "response": [] 125 | }, 126 | { 127 | "name": "register-error", 128 | "request": { 129 | "method": "POST", 130 | "header": [ 131 | { 132 | "key": "Content-Type", 133 | "name": "Content-Type", 134 | "value": "application/json", 135 | "type": "text" 136 | } 137 | ], 138 | "body": { 139 | "mode": "raw", 140 | "raw": "{\n\t\"username\": \"test2\"\n}" 141 | }, 142 | "url": { 143 | "raw": "http://localhost:5000/register", 144 | "protocol": "http", 145 | "host": [ 146 | "localhost" 147 | ], 148 | "port": "5000", 149 | "path": [ 150 | "register" 151 | ] 152 | }, 153 | "description": "Exemplo de erro no registro." 154 | }, 155 | "response": [] 156 | }, 157 | { 158 | "name": "login-error", 159 | "request": { 160 | "method": "POST", 161 | "header": [ 162 | { 163 | "key": "Content-Type", 164 | "name": "Content-Type", 165 | "value": "application/json", 166 | "type": "text" 167 | } 168 | ], 169 | "body": { 170 | "mode": "raw", 171 | "raw": "{\n\t\"username\": \"teste\",\n\t\"password\": \"1234\"\n}" 172 | }, 173 | "url": { 174 | "raw": "http://localhost:5000/login", 175 | "protocol": "http", 176 | "host": [ 177 | "localhost" 178 | ], 179 | "port": "5000", 180 | "path": [ 181 | "login" 182 | ] 183 | }, 184 | "description": "Exemplo de erro no login." 185 | }, 186 | "response": [] 187 | }, 188 | { 189 | "name": "consult-error", 190 | "protocolProfileBehavior": { 191 | "disableBodyPruning": true 192 | }, 193 | "request": { 194 | "method": "GET", 195 | "header": [ 196 | { 197 | "key": "Authorization", 198 | "value": "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE1NTg2NDE3MzcsIm5iZiI6MTU1ODY0MTczNywianRpIjoiNDI1ODdlYWUtY2JiOC00YThhLWExNTMtYzEwOTJhZjNlZGRjIiwiZXhwIjoxNTU4NjQxNzk3LCJpZGVudGl0eSI6MSwiZnJlc2giOmZhbHNlLCJ0eXBlIjoiYWNjZXNzIn0.NaC0AZWqJqEfwCcYdlPHfMFzQRq56PAh9vddgw2C_S0", 199 | "type": "text" 200 | }, 201 | { 202 | "key": "Content-Type", 203 | "name": "Content-Type", 204 | "value": "application/json", 205 | "type": "text" 206 | } 207 | ], 208 | "body": { 209 | "mode": "raw", 210 | "raw": "{\n\t\"username\":\"test\",\n\t\"password\":\"1234\"\n}" 211 | }, 212 | "url": { 213 | "raw": "localhost:5000/consult/40442820137", 214 | "host": [ 215 | "localhost" 216 | ], 217 | "port": "5000", 218 | "path": [ 219 | "consult", 220 | "40442820137" 221 | ] 222 | }, 223 | "description": "Exemplo de erro na consulta." 224 | }, 225 | "response": [] 226 | } 227 | ] 228 | } -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | behave==1.2.5 2 | Click==7.0 3 | coverage==4.5.3 4 | Flask==1.0.2 5 | flask-marshmallow==0.10.0 6 | Flask-Migrate==2.4.0 7 | Flask-RESTful==0.3.7 8 | Flask-Script==2.0.6 9 | Flask-JWT-Extended==3.18.2 10 | Flask-SQLAlchemy==2.3.2 11 | flask-marshmallow==0.10.0 12 | Flask-Migrate==2.4.0 13 | Flask-Script==2.0.6 14 | Markdown==3.1 15 | PyJWT==1.7.1 16 | passlib==1.7.1 17 | requests==2.21.0 18 | SQLAlchemy==1.2.15 19 | marshmallow==2.19.0 20 | marshmallow-sqlalchemy==0.16.3 21 | --------------------------------------------------------------------------------