diff --git a/.travis.yml b/.travis.yml index e5f6857..6fbac08 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,7 +15,7 @@ env: global: - RANDOM_SEED=0 matrix: - - FLASK_VERSION=2.0.1 + - FLASK_VERSION=2.3.2 before_install: - pip install pipenv diff --git a/Pipfile b/Pipfile index 7ad31f6..69c54e4 100644 --- a/Pipfile +++ b/Pipfile @@ -21,7 +21,7 @@ nose = "*" coveragespace = "~=4.1" # Documentation -mkdocs = "~=0.17.2" +mkdocs = "~=1.2.3" docutils = "*" # Release diff --git a/Pipfile.lock b/Pipfile.lock index 65c5c10..3db0df7 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "4df95fb37b5d8a421e36e344557a2bf0ec143b4c19fbaa55f5503a0b0c4ad3ce" + "sha256": "662759c6b1e37a84f88b13fc4053a0d627d21897ff7f89cae656649393e94549" }, "pipfile-spec": 6, "requires": {}, @@ -14,37 +14,45 @@ ] }, "default": { + "blinker": { + "hashes": [ + "sha256:4afd3de66ef3a9f8067559fb7a1cbe555c17dcbe15971b05d1b625c3e7abe213", + "sha256:c3d739772abb7bc2860abf5f2ec284223d9ad5c76da018234f6f50d6f31ab1f0" + ], + "markers": "python_version >= '3.7'", + "version": "==1.6.2" + }, "click": { "hashes": [ - "sha256:8c04c11192119b1ef78ea049e0a6f0463e4c48ef00a30160c704337586f3ad7a", - "sha256:fba402a4a47334742d782209a7c79bc448911afe1149d07bdabdf480b3e2f4b6" + "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e", + "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48" ], - "markers": "python_version >= '3.6'", - "version": "==8.0.1" + "markers": "python_version >= '3.7'", + "version": "==8.1.3" }, "flask": { "hashes": [ - "sha256:1c4c257b1892aec1398784c63791cbaa43062f1f7aeb555c4da961b20ee68f55", - "sha256:a6209ca15eb63fc9385f38e452704113d679511d9574d09b2cf9183ae7d20dc9" + "sha256:77fd4e1249d8c9923de34907236b747ced06e5467ecac1a7bb7115ae0e9670b0", + "sha256:8c2f9abd47a9e8df7f0c3f091ce9497d011dc3b31effcf4c85a6e2b50f4114ef" ], "index": "pypi", - "version": "==2.0.1" + "version": "==2.3.2" }, "itsdangerous": { "hashes": [ - "sha256:5174094b9637652bdb841a3029700391451bd092ba3db90600dea710ba28e97c", - "sha256:9e724d68fc22902a1435351f84c3fb8623f303fffcc566a4cb952df8c572cff0" + "sha256:2c2349112351b88699d8d4b6b075022c0808887cb7ad10069318a8b0bc88db44", + "sha256:5dbbc68b317e5e42f327f9021763545dc3fc3bfe22e6deb96aaf1fc38874156a" ], - "markers": "python_version >= '3.6'", - "version": "==2.0.1" + "markers": "python_version >= '3.7'", + "version": "==2.1.2" }, "jinja2": { "hashes": [ - "sha256:1f06f2da51e7b56b8f238affdd6b4e2c61e39598a378cc49345bc1bd42a978a4", - "sha256:703f484b47a6af502e743c9122595cc812b0271f661722403114f71a79d0f5a4" + "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852", + "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61" ], - "markers": "python_version >= '3.6'", - "version": "==3.0.1" + "markers": "python_version >= '3.7'", + "version": "==3.1.2" }, "markdown": { "hashes": [ @@ -56,150 +64,317 @@ }, "markupsafe": { "hashes": [ - "sha256:01a9b8ea66f1658938f65b93a85ebe8bc016e6769611be228d797c9d998dd298", - "sha256:023cb26ec21ece8dc3907c0e8320058b2e0cb3c55cf9564da612bc325bed5e64", - "sha256:0446679737af14f45767963a1a9ef7620189912317d095f2d9ffa183a4d25d2b", - "sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567", - "sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff", - "sha256:10f82115e21dc0dfec9ab5c0223652f7197feb168c940f3ef61563fc2d6beb74", - "sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35", - "sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26", - "sha256:3c112550557578c26af18a1ccc9e090bfe03832ae994343cfdacd287db6a6ae7", - "sha256:47ab1e7b91c098ab893b828deafa1203de86d0bc6ab587b160f78fe6c4011f75", - "sha256:49e3ceeabbfb9d66c3aef5af3a60cc43b85c33df25ce03d0031a608b0a8b2e3f", - "sha256:4efca8f86c54b22348a5467704e3fec767b2db12fc39c6d963168ab1d3fc9135", - "sha256:53edb4da6925ad13c07b6d26c2a852bd81e364f95301c66e930ab2aef5b5ddd8", - "sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a", - "sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914", - "sha256:6557b31b5e2c9ddf0de32a691f2312a32f77cd7681d8af66c2692efdbef84c18", - "sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8", - "sha256:6a7fae0dd14cf60ad5ff42baa2e95727c3d81ded453457771d02b7d2b3f9c0c2", - "sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d", - "sha256:7d91275b0245b1da4d4cfa07e0faedd5b0812efc15b702576d103293e252af1b", - "sha256:905fec760bd2fa1388bb5b489ee8ee5f7291d692638ea5f67982d968366bef9f", - "sha256:97383d78eb34da7e1fa37dd273c20ad4320929af65d156e35a5e2d89566d9dfb", - "sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833", - "sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415", - "sha256:ab3ef638ace319fa26553db0624c4699e31a28bb2a835c5faca8f8acf6a5a902", - "sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9", - "sha256:b7f2d075102dc8c794cbde1947378051c4e5180d52d276987b8d28a3bd58c17d", - "sha256:be98f628055368795d818ebf93da628541e10b75b41c559fdf36d104c5787066", - "sha256:d7f9850398e85aba693bb640262d3611788b1f29a79f0c93c565694658f4071f", - "sha256:f5653a225f31e113b152e56f154ccbe59eeb1c7487b39b9d9f9cdb58e6c79dc5", - "sha256:f826e31d18b516f653fe296d967d700fddad5901ae07c622bb3705955e1faa94", - "sha256:f8ba0e8349a38d3001fae7eadded3f6606f0da5d748ee53cc1dab1d6527b9509", - "sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51", - "sha256:fa130dd50c57d53368c9d59395cb5526eda596d3ffe36666cd81a44d56e48872" - ], - "markers": "python_version >= '3.6'", - "version": "==2.0.1" + "sha256:0576fe974b40a400449768941d5d0858cc624e3249dfd1e0c33674e5c7ca7aed", + "sha256:085fd3201e7b12809f9e6e9bc1e5c96a368c8523fad5afb02afe3c051ae4afcc", + "sha256:090376d812fb6ac5f171e5938e82e7f2d7adc2b629101cec0db8b267815c85e2", + "sha256:0b462104ba25f1ac006fdab8b6a01ebbfbce9ed37fd37fd4acd70c67c973e460", + "sha256:137678c63c977754abe9086a3ec011e8fd985ab90631145dfb9294ad09c102a7", + "sha256:1bea30e9bf331f3fef67e0a3877b2288593c98a21ccb2cf29b74c581a4eb3af0", + "sha256:22152d00bf4a9c7c83960521fc558f55a1adbc0631fbb00a9471e097b19d72e1", + "sha256:22731d79ed2eb25059ae3df1dfc9cb1546691cc41f4e3130fe6bfbc3ecbbecfa", + "sha256:2298c859cfc5463f1b64bd55cb3e602528db6fa0f3cfd568d3605c50678f8f03", + "sha256:28057e985dace2f478e042eaa15606c7efccb700797660629da387eb289b9323", + "sha256:2e7821bffe00aa6bd07a23913b7f4e01328c3d5cc0b40b36c0bd81d362faeb65", + "sha256:2ec4f2d48ae59bbb9d1f9d7efb9236ab81429a764dedca114f5fdabbc3788013", + "sha256:340bea174e9761308703ae988e982005aedf427de816d1afe98147668cc03036", + "sha256:40627dcf047dadb22cd25ea7ecfe9cbf3bbbad0482ee5920b582f3809c97654f", + "sha256:40dfd3fefbef579ee058f139733ac336312663c6706d1163b82b3003fb1925c4", + "sha256:4cf06cdc1dda95223e9d2d3c58d3b178aa5dacb35ee7e3bbac10e4e1faacb419", + "sha256:50c42830a633fa0cf9e7d27664637532791bfc31c731a87b202d2d8ac40c3ea2", + "sha256:55f44b440d491028addb3b88f72207d71eeebfb7b5dbf0643f7c023ae1fba619", + "sha256:608e7073dfa9e38a85d38474c082d4281f4ce276ac0010224eaba11e929dd53a", + "sha256:63ba06c9941e46fa389d389644e2d8225e0e3e5ebcc4ff1ea8506dce646f8c8a", + "sha256:65608c35bfb8a76763f37036547f7adfd09270fbdbf96608be2bead319728fcd", + "sha256:665a36ae6f8f20a4676b53224e33d456a6f5a72657d9c83c2aa00765072f31f7", + "sha256:6d6607f98fcf17e534162f0709aaad3ab7a96032723d8ac8750ffe17ae5a0666", + "sha256:7313ce6a199651c4ed9d7e4cfb4aa56fe923b1adf9af3b420ee14e6d9a73df65", + "sha256:7668b52e102d0ed87cb082380a7e2e1e78737ddecdde129acadb0eccc5423859", + "sha256:7df70907e00c970c60b9ef2938d894a9381f38e6b9db73c5be35e59d92e06625", + "sha256:7e007132af78ea9df29495dbf7b5824cb71648d7133cf7848a2a5dd00d36f9ff", + "sha256:835fb5e38fd89328e9c81067fd642b3593c33e1e17e2fdbf77f5676abb14a156", + "sha256:8bca7e26c1dd751236cfb0c6c72d4ad61d986e9a41bbf76cb445f69488b2a2bd", + "sha256:8db032bf0ce9022a8e41a22598eefc802314e81b879ae093f36ce9ddf39ab1ba", + "sha256:99625a92da8229df6d44335e6fcc558a5037dd0a760e11d84be2260e6f37002f", + "sha256:9cad97ab29dfc3f0249b483412c85c8ef4766d96cdf9dcf5a1e3caa3f3661cf1", + "sha256:a4abaec6ca3ad8660690236d11bfe28dfd707778e2442b45addd2f086d6ef094", + "sha256:a6e40afa7f45939ca356f348c8e23048e02cb109ced1eb8420961b2f40fb373a", + "sha256:a6f2fcca746e8d5910e18782f976489939d54a91f9411c32051b4aab2bd7c513", + "sha256:a806db027852538d2ad7555b203300173dd1b77ba116de92da9afbc3a3be3eed", + "sha256:abcabc8c2b26036d62d4c746381a6f7cf60aafcc653198ad678306986b09450d", + "sha256:b8526c6d437855442cdd3d87eede9c425c4445ea011ca38d937db299382e6fa3", + "sha256:bb06feb762bade6bf3c8b844462274db0c76acc95c52abe8dbed28ae3d44a147", + "sha256:c0a33bc9f02c2b17c3ea382f91b4db0e6cde90b63b296422a939886a7a80de1c", + "sha256:c4a549890a45f57f1ebf99c067a4ad0cb423a05544accaf2b065246827ed9603", + "sha256:ca244fa73f50a800cf8c3ebf7fd93149ec37f5cb9596aa8873ae2c1d23498601", + "sha256:cf877ab4ed6e302ec1d04952ca358b381a882fbd9d1b07cccbfd61783561f98a", + "sha256:d9d971ec1e79906046aa3ca266de79eac42f1dbf3612a05dc9368125952bd1a1", + "sha256:da25303d91526aac3672ee6d49a2f3db2d9502a4a60b55519feb1a4c7714e07d", + "sha256:e55e40ff0cc8cc5c07996915ad367fa47da6b3fc091fdadca7f5403239c5fec3", + "sha256:f03a532d7dee1bed20bc4884194a16160a2de9ffc6354b3878ec9682bb623c54", + "sha256:f1cd098434e83e656abf198f103a8207a8187c0fc110306691a2e94a78d0abb2", + "sha256:f2bfb563d0211ce16b63c7cb9395d2c682a23187f54c3d79bfec33e6705473c6", + "sha256:f8ffb705ffcf5ddd0e80b65ddf7bed7ee4f5a441ea7d3419e861a12eaf41af58" + ], + "markers": "python_version >= '3.7'", + "version": "==2.1.2" }, "werkzeug": { "hashes": [ - "sha256:1de1db30d010ff1af14a009224ec49ab2329ad2cde454c8a708130642d579c42", - "sha256:6c1ec500dcdba0baa27600f6a22f6333d8b662d22027ff9f6202e3367413caa8" + "sha256:1d5a58e0377d1fe39d061a5de4469e414e78ccb1e1e59c0f5ad6fa1c36c52b76", + "sha256:48e5e61472fee0ddee27ebad085614ebedb7af41e88f687aaf881afb723a162f" ], - "markers": "python_version >= '3.6'", - "version": "==2.0.1" + "markers": "python_version >= '3.8'", + "version": "==2.3.4" } }, "develop": { "bleach": { "hashes": [ - "sha256:6123ddc1052673e52bab52cdc955bcb57a015264a1c57d37bea2f6b817af0125", - "sha256:98b3170739e5e83dd9dc19633f074727ad848cbedb6026708c8ac2d3b697a433" + "sha256:1a1a85c1595e07d8db14c5f09f09e6433502c51c595970edc090551f0db99414", + "sha256:33c16e3353dbd13028ab4799a0f89a83f113405c766e9c122df8a06f5b85b3f4" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==3.3.0" + "markers": "python_version >= '3.7'", + "version": "==6.0.0" }, "certifi": { "hashes": [ - "sha256:2bbf76fd432960138b3ef6dda3dde0544f27cbf8546c458e60baf371917ba9ee", - "sha256:50b1e4f8446b06f41be7dd6338db18e0990601dce795c2b1686458aa7e8fa7d8" + "sha256:0f0d56dc5a6ad56fd4ba36484d6cc34451e1c6548c61daad8c320169f91eddc7", + "sha256:c6c2e98f5c7869efca1f8916fed228dd91539f9f1b444c314c06eef02980c716" ], - "version": "==2021.5.30" - }, - "chardet": { - "hashes": [ - "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa", - "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==4.0.0" + "markers": "python_version >= '3.6'", + "version": "==2023.5.7" + }, + "cffi": { + "hashes": [ + "sha256:00a9ed42e88df81ffae7a8ab6d9356b371399b91dbdf0c3cb1e84c03a13aceb5", + "sha256:03425bdae262c76aad70202debd780501fabeaca237cdfddc008987c0e0f59ef", + "sha256:04ed324bda3cda42b9b695d51bb7d54b680b9719cfab04227cdd1e04e5de3104", + "sha256:0e2642fe3142e4cc4af0799748233ad6da94c62a8bec3a6648bf8ee68b1c7426", + "sha256:173379135477dc8cac4bc58f45db08ab45d228b3363adb7af79436135d028405", + "sha256:198caafb44239b60e252492445da556afafc7d1e3ab7a1fb3f0584ef6d742375", + "sha256:1e74c6b51a9ed6589199c787bf5f9875612ca4a8a0785fb2d4a84429badaf22a", + "sha256:2012c72d854c2d03e45d06ae57f40d78e5770d252f195b93f581acf3ba44496e", + "sha256:21157295583fe8943475029ed5abdcf71eb3911894724e360acff1d61c1d54bc", + "sha256:2470043b93ff09bf8fb1d46d1cb756ce6132c54826661a32d4e4d132e1977adf", + "sha256:285d29981935eb726a4399badae8f0ffdff4f5050eaa6d0cfc3f64b857b77185", + "sha256:30d78fbc8ebf9c92c9b7823ee18eb92f2e6ef79b45ac84db507f52fbe3ec4497", + "sha256:320dab6e7cb2eacdf0e658569d2575c4dad258c0fcc794f46215e1e39f90f2c3", + "sha256:33ab79603146aace82c2427da5ca6e58f2b3f2fb5da893ceac0c42218a40be35", + "sha256:3548db281cd7d2561c9ad9984681c95f7b0e38881201e157833a2342c30d5e8c", + "sha256:3799aecf2e17cf585d977b780ce79ff0dc9b78d799fc694221ce814c2c19db83", + "sha256:39d39875251ca8f612b6f33e6b1195af86d1b3e60086068be9cc053aa4376e21", + "sha256:3b926aa83d1edb5aa5b427b4053dc420ec295a08e40911296b9eb1b6170f6cca", + "sha256:3bcde07039e586f91b45c88f8583ea7cf7a0770df3a1649627bf598332cb6984", + "sha256:3d08afd128ddaa624a48cf2b859afef385b720bb4b43df214f85616922e6a5ac", + "sha256:3eb6971dcff08619f8d91607cfc726518b6fa2a9eba42856be181c6d0d9515fd", + "sha256:40f4774f5a9d4f5e344f31a32b5096977b5d48560c5592e2f3d2c4374bd543ee", + "sha256:4289fc34b2f5316fbb762d75362931e351941fa95fa18789191b33fc4cf9504a", + "sha256:470c103ae716238bbe698d67ad020e1db9d9dba34fa5a899b5e21577e6d52ed2", + "sha256:4f2c9f67e9821cad2e5f480bc8d83b8742896f1242dba247911072d4fa94c192", + "sha256:50a74364d85fd319352182ef59c5c790484a336f6db772c1a9231f1c3ed0cbd7", + "sha256:54a2db7b78338edd780e7ef7f9f6c442500fb0d41a5a4ea24fff1c929d5af585", + "sha256:5635bd9cb9731e6d4a1132a498dd34f764034a8ce60cef4f5319c0541159392f", + "sha256:59c0b02d0a6c384d453fece7566d1c7e6b7bae4fc5874ef2ef46d56776d61c9e", + "sha256:5d598b938678ebf3c67377cdd45e09d431369c3b1a5b331058c338e201f12b27", + "sha256:5df2768244d19ab7f60546d0c7c63ce1581f7af8b5de3eb3004b9b6fc8a9f84b", + "sha256:5ef34d190326c3b1f822a5b7a45f6c4535e2f47ed06fec77d3d799c450b2651e", + "sha256:6975a3fac6bc83c4a65c9f9fcab9e47019a11d3d2cf7f3c0d03431bf145a941e", + "sha256:6c9a799e985904922a4d207a94eae35c78ebae90e128f0c4e521ce339396be9d", + "sha256:70df4e3b545a17496c9b3f41f5115e69a4f2e77e94e1d2a8e1070bc0c38c8a3c", + "sha256:7473e861101c9e72452f9bf8acb984947aa1661a7704553a9f6e4baa5ba64415", + "sha256:8102eaf27e1e448db915d08afa8b41d6c7ca7a04b7d73af6514df10a3e74bd82", + "sha256:87c450779d0914f2861b8526e035c5e6da0a3199d8f1add1a665e1cbc6fc6d02", + "sha256:8b7ee99e510d7b66cdb6c593f21c043c248537a32e0bedf02e01e9553a172314", + "sha256:91fc98adde3d7881af9b59ed0294046f3806221863722ba7d8d120c575314325", + "sha256:94411f22c3985acaec6f83c6df553f2dbe17b698cc7f8ae751ff2237d96b9e3c", + "sha256:98d85c6a2bef81588d9227dde12db8a7f47f639f4a17c9ae08e773aa9c697bf3", + "sha256:9ad5db27f9cabae298d151c85cf2bad1d359a1b9c686a275df03385758e2f914", + "sha256:a0b71b1b8fbf2b96e41c4d990244165e2c9be83d54962a9a1d118fd8657d2045", + "sha256:a0f100c8912c114ff53e1202d0078b425bee3649ae34d7b070e9697f93c5d52d", + "sha256:a591fe9e525846e4d154205572a029f653ada1a78b93697f3b5a8f1f2bc055b9", + "sha256:a5c84c68147988265e60416b57fc83425a78058853509c1b0629c180094904a5", + "sha256:a66d3508133af6e8548451b25058d5812812ec3798c886bf38ed24a98216fab2", + "sha256:a8c4917bd7ad33e8eb21e9a5bbba979b49d9a97acb3a803092cbc1133e20343c", + "sha256:b3bbeb01c2b273cca1e1e0c5df57f12dce9a4dd331b4fa1635b8bec26350bde3", + "sha256:cba9d6b9a7d64d4bd46167096fc9d2f835e25d7e4c121fb2ddfc6528fb0413b2", + "sha256:cc4d65aeeaa04136a12677d3dd0b1c0c94dc43abac5860ab33cceb42b801c1e8", + "sha256:ce4bcc037df4fc5e3d184794f27bdaab018943698f4ca31630bc7f84a7b69c6d", + "sha256:cec7d9412a9102bdc577382c3929b337320c4c4c4849f2c5cdd14d7368c5562d", + "sha256:d400bfb9a37b1351253cb402671cea7e89bdecc294e8016a707f6d1d8ac934f9", + "sha256:d61f4695e6c866a23a21acab0509af1cdfd2c013cf256bbf5b6b5e2695827162", + "sha256:db0fbb9c62743ce59a9ff687eb5f4afbe77e5e8403d6697f7446e5f609976f76", + "sha256:dd86c085fae2efd48ac91dd7ccffcfc0571387fe1193d33b6394db7ef31fe2a4", + "sha256:e00b098126fd45523dd056d2efba6c5a63b71ffe9f2bbe1a4fe1716e1d0c331e", + "sha256:e229a521186c75c8ad9490854fd8bbdd9a0c9aa3a524326b55be83b54d4e0ad9", + "sha256:e263d77ee3dd201c3a142934a086a4450861778baaeeb45db4591ef65550b0a6", + "sha256:ed9cb427ba5504c1dc15ede7d516b84757c3e3d7868ccc85121d9310d27eed0b", + "sha256:fa6693661a4c91757f4412306191b6dc88c1703f780c8234035eac011922bc01", + "sha256:fcd131dd944808b5bdb38e6f5b53013c5aa4f334c5cad0c72742f6eba4b73db0" + ], + "version": "==1.15.1" + }, + "charset-normalizer": { + "hashes": [ + "sha256:04afa6387e2b282cf78ff3dbce20f0cc071c12dc8f685bd40960cc68644cfea6", + "sha256:04eefcee095f58eaabe6dc3cc2262f3bcd776d2c67005880894f447b3f2cb9c1", + "sha256:0be65ccf618c1e7ac9b849c315cc2e8a8751d9cfdaa43027d4f6624bd587ab7e", + "sha256:0c95f12b74681e9ae127728f7e5409cbbef9cd914d5896ef238cc779b8152373", + "sha256:0ca564606d2caafb0abe6d1b5311c2649e8071eb241b2d64e75a0d0065107e62", + "sha256:10c93628d7497c81686e8e5e557aafa78f230cd9e77dd0c40032ef90c18f2230", + "sha256:11d117e6c63e8f495412d37e7dc2e2fff09c34b2d09dbe2bee3c6229577818be", + "sha256:11d3bcb7be35e7b1bba2c23beedac81ee893ac9871d0ba79effc7fc01167db6c", + "sha256:12a2b561af122e3d94cdb97fe6fb2bb2b82cef0cdca131646fdb940a1eda04f0", + "sha256:12d1a39aa6b8c6f6248bb54550efcc1c38ce0d8096a146638fd4738e42284448", + "sha256:1435ae15108b1cb6fffbcea2af3d468683b7afed0169ad718451f8db5d1aff6f", + "sha256:1c60b9c202d00052183c9be85e5eaf18a4ada0a47d188a83c8f5c5b23252f649", + "sha256:1e8fcdd8f672a1c4fc8d0bd3a2b576b152d2a349782d1eb0f6b8e52e9954731d", + "sha256:20064ead0717cf9a73a6d1e779b23d149b53daf971169289ed2ed43a71e8d3b0", + "sha256:21fa558996782fc226b529fdd2ed7866c2c6ec91cee82735c98a197fae39f706", + "sha256:22908891a380d50738e1f978667536f6c6b526a2064156203d418f4856d6e86a", + "sha256:3160a0fd9754aab7d47f95a6b63ab355388d890163eb03b2d2b87ab0a30cfa59", + "sha256:322102cdf1ab682ecc7d9b1c5eed4ec59657a65e1c146a0da342b78f4112db23", + "sha256:34e0a2f9c370eb95597aae63bf85eb5e96826d81e3dcf88b8886012906f509b5", + "sha256:3573d376454d956553c356df45bb824262c397c6e26ce43e8203c4c540ee0acb", + "sha256:3747443b6a904001473370d7810aa19c3a180ccd52a7157aacc264a5ac79265e", + "sha256:38e812a197bf8e71a59fe55b757a84c1f946d0ac114acafaafaf21667a7e169e", + "sha256:3a06f32c9634a8705f4ca9946d667609f52cf130d5548881401f1eb2c39b1e2c", + "sha256:3a5fc78f9e3f501a1614a98f7c54d3969f3ad9bba8ba3d9b438c3bc5d047dd28", + "sha256:3d9098b479e78c85080c98e1e35ff40b4a31d8953102bb0fd7d1b6f8a2111a3d", + "sha256:3dc5b6a8ecfdc5748a7e429782598e4f17ef378e3e272eeb1340ea57c9109f41", + "sha256:4155b51ae05ed47199dc5b2a4e62abccb274cee6b01da5b895099b61b1982974", + "sha256:49919f8400b5e49e961f320c735388ee686a62327e773fa5b3ce6721f7e785ce", + "sha256:53d0a3fa5f8af98a1e261de6a3943ca631c526635eb5817a87a59d9a57ebf48f", + "sha256:5f008525e02908b20e04707a4f704cd286d94718f48bb33edddc7d7b584dddc1", + "sha256:628c985afb2c7d27a4800bfb609e03985aaecb42f955049957814e0491d4006d", + "sha256:65ed923f84a6844de5fd29726b888e58c62820e0769b76565480e1fdc3d062f8", + "sha256:6734e606355834f13445b6adc38b53c0fd45f1a56a9ba06c2058f86893ae8017", + "sha256:6baf0baf0d5d265fa7944feb9f7451cc316bfe30e8df1a61b1bb08577c554f31", + "sha256:6f4f4668e1831850ebcc2fd0b1cd11721947b6dc7c00bf1c6bd3c929ae14f2c7", + "sha256:6f5c2e7bc8a4bf7c426599765b1bd33217ec84023033672c1e9a8b35eaeaaaf8", + "sha256:6f6c7a8a57e9405cad7485f4c9d3172ae486cfef1344b5ddd8e5239582d7355e", + "sha256:7381c66e0561c5757ffe616af869b916c8b4e42b367ab29fedc98481d1e74e14", + "sha256:73dc03a6a7e30b7edc5b01b601e53e7fc924b04e1835e8e407c12c037e81adbd", + "sha256:74db0052d985cf37fa111828d0dd230776ac99c740e1a758ad99094be4f1803d", + "sha256:75f2568b4189dda1c567339b48cba4ac7384accb9c2a7ed655cd86b04055c795", + "sha256:78cacd03e79d009d95635e7d6ff12c21eb89b894c354bd2b2ed0b4763373693b", + "sha256:80d1543d58bd3d6c271b66abf454d437a438dff01c3e62fdbcd68f2a11310d4b", + "sha256:830d2948a5ec37c386d3170c483063798d7879037492540f10a475e3fd6f244b", + "sha256:891cf9b48776b5c61c700b55a598621fdb7b1e301a550365571e9624f270c203", + "sha256:8f25e17ab3039b05f762b0a55ae0b3632b2e073d9c8fc88e89aca31a6198e88f", + "sha256:9a3267620866c9d17b959a84dd0bd2d45719b817245e49371ead79ed4f710d19", + "sha256:a04f86f41a8916fe45ac5024ec477f41f886b3c435da2d4e3d2709b22ab02af1", + "sha256:aaf53a6cebad0eae578f062c7d462155eada9c172bd8c4d250b8c1d8eb7f916a", + "sha256:abc1185d79f47c0a7aaf7e2412a0eb2c03b724581139193d2d82b3ad8cbb00ac", + "sha256:ac0aa6cd53ab9a31d397f8303f92c42f534693528fafbdb997c82bae6e477ad9", + "sha256:ac3775e3311661d4adace3697a52ac0bab17edd166087d493b52d4f4f553f9f0", + "sha256:b06f0d3bf045158d2fb8837c5785fe9ff9b8c93358be64461a1089f5da983137", + "sha256:b116502087ce8a6b7a5f1814568ccbd0e9f6cfd99948aa59b0e241dc57cf739f", + "sha256:b82fab78e0b1329e183a65260581de4375f619167478dddab510c6c6fb04d9b6", + "sha256:bd7163182133c0c7701b25e604cf1611c0d87712e56e88e7ee5d72deab3e76b5", + "sha256:c36bcbc0d5174a80d6cccf43a0ecaca44e81d25be4b7f90f0ed7bcfbb5a00909", + "sha256:c3af8e0f07399d3176b179f2e2634c3ce9c1301379a6b8c9c9aeecd481da494f", + "sha256:c84132a54c750fda57729d1e2599bb598f5fa0344085dbde5003ba429a4798c0", + "sha256:cb7b2ab0188829593b9de646545175547a70d9a6e2b63bf2cd87a0a391599324", + "sha256:cca4def576f47a09a943666b8f829606bcb17e2bc2d5911a46c8f8da45f56755", + "sha256:cf6511efa4801b9b38dc5546d7547d5b5c6ef4b081c60b23e4d941d0eba9cbeb", + "sha256:d16fd5252f883eb074ca55cb622bc0bee49b979ae4e8639fff6ca3ff44f9f854", + "sha256:d2686f91611f9e17f4548dbf050e75b079bbc2a82be565832bc8ea9047b61c8c", + "sha256:d7fc3fca01da18fbabe4625d64bb612b533533ed10045a2ac3dd194bfa656b60", + "sha256:dd5653e67b149503c68c4018bf07e42eeed6b4e956b24c00ccdf93ac79cdff84", + "sha256:de5695a6f1d8340b12a5d6d4484290ee74d61e467c39ff03b39e30df62cf83a0", + "sha256:e0ac8959c929593fee38da1c2b64ee9778733cdf03c482c9ff1d508b6b593b2b", + "sha256:e1b25e3ad6c909f398df8921780d6a3d120d8c09466720226fc621605b6f92b1", + "sha256:e633940f28c1e913615fd624fcdd72fdba807bf53ea6925d6a588e84e1151531", + "sha256:e89df2958e5159b811af9ff0f92614dabf4ff617c03a4c1c6ff53bf1c399e0e1", + "sha256:ea9f9c6034ea2d93d9147818f17c2a0860d41b71c38b9ce4d55f21b6f9165a11", + "sha256:f645caaf0008bacf349875a974220f1f1da349c5dbe7c4ec93048cdc785a3326", + "sha256:f8303414c7b03f794347ad062c0516cee0e15f7a612abd0ce1e25caf6ceb47df", + "sha256:fca62a8301b605b954ad2e9c3666f9d97f63872aa4efcae5492baca2056b74ab" + ], + "markers": "python_version >= '3.7'", + "version": "==3.1.0" }, "click": { "hashes": [ - "sha256:8c04c11192119b1ef78ea049e0a6f0463e4c48ef00a30160c704337586f3ad7a", - "sha256:fba402a4a47334742d782209a7c79bc448911afe1149d07bdabdf480b3e2f4b6" + "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e", + "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48" ], - "markers": "python_version >= '3.6'", - "version": "==8.0.1" + "markers": "python_version >= '3.7'", + "version": "==8.1.3" }, "colorama": { "hashes": [ - "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b", - "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2" + "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", + "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==0.4.4" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5, 3.6'", + "version": "==0.4.6" }, "coverage": { "hashes": [ - "sha256:004d1880bed2d97151facef49f08e255a20ceb6f9432df75f4eef018fdd5a78c", - "sha256:01d84219b5cdbfc8122223b39a954820929497a1cb1422824bb86b07b74594b6", - "sha256:040af6c32813fa3eae5305d53f18875bedd079960822ef8ec067a66dd8afcd45", - "sha256:06191eb60f8d8a5bc046f3799f8a07a2d7aefb9504b0209aff0b47298333302a", - "sha256:13034c4409db851670bc9acd836243aeee299949bd5673e11844befcb0149f03", - "sha256:13c4ee887eca0f4c5a247b75398d4114c37882658300e153113dafb1d76de529", - "sha256:184a47bbe0aa6400ed2d41d8e9ed868b8205046518c52464fde713ea06e3a74a", - "sha256:18ba8bbede96a2c3dde7b868de9dcbd55670690af0988713f0603f037848418a", - "sha256:1aa846f56c3d49205c952d8318e76ccc2ae23303351d9270ab220004c580cfe2", - "sha256:217658ec7187497e3f3ebd901afdca1af062b42cfe3e0dafea4cced3983739f6", - "sha256:24d4a7de75446be83244eabbff746d66b9240ae020ced65d060815fac3423759", - "sha256:2910f4d36a6a9b4214bb7038d537f015346f413a975d57ca6b43bf23d6563b53", - "sha256:2949cad1c5208b8298d5686d5a85b66aae46d73eec2c3e08c817dd3513e5848a", - "sha256:2a3859cb82dcbda1cfd3e6f71c27081d18aa251d20a17d87d26d4cd216fb0af4", - "sha256:2cafbbb3af0733db200c9b5f798d18953b1a304d3f86a938367de1567f4b5bff", - "sha256:2e0d881ad471768bf6e6c2bf905d183543f10098e3b3640fc029509530091502", - "sha256:30c77c1dc9f253283e34c27935fded5015f7d1abe83bc7821680ac444eaf7793", - "sha256:3487286bc29a5aa4b93a072e9592f22254291ce96a9fbc5251f566b6b7343cdb", - "sha256:372da284cfd642d8e08ef606917846fa2ee350f64994bebfbd3afb0040436905", - "sha256:41179b8a845742d1eb60449bdb2992196e211341818565abded11cfa90efb821", - "sha256:44d654437b8ddd9eee7d1eaee28b7219bec228520ff809af170488fd2fed3e2b", - "sha256:4a7697d8cb0f27399b0e393c0b90f0f1e40c82023ea4d45d22bce7032a5d7b81", - "sha256:51cb9476a3987c8967ebab3f0fe144819781fca264f57f89760037a2ea191cb0", - "sha256:52596d3d0e8bdf3af43db3e9ba8dcdaac724ba7b5ca3f6358529d56f7a166f8b", - "sha256:53194af30d5bad77fcba80e23a1441c71abfb3e01192034f8246e0d8f99528f3", - "sha256:5fec2d43a2cc6965edc0bb9e83e1e4b557f76f843a77a2496cbe719583ce8184", - "sha256:6c90e11318f0d3c436a42409f2749ee1a115cd8b067d7f14c148f1ce5574d701", - "sha256:74d881fc777ebb11c63736622b60cb9e4aee5cace591ce274fb69e582a12a61a", - "sha256:7501140f755b725495941b43347ba8a2777407fc7f250d4f5a7d2a1050ba8e82", - "sha256:796c9c3c79747146ebd278dbe1e5c5c05dd6b10cc3bcb8389dfdf844f3ead638", - "sha256:869a64f53488f40fa5b5b9dcb9e9b2962a66a87dab37790f3fcfb5144b996ef5", - "sha256:8963a499849a1fc54b35b1c9f162f4108017b2e6db2c46c1bed93a72262ed083", - "sha256:8d0a0725ad7c1a0bcd8d1b437e191107d457e2ec1084b9f190630a4fb1af78e6", - "sha256:900fbf7759501bc7807fd6638c947d7a831fc9fdf742dc10f02956ff7220fa90", - "sha256:92b017ce34b68a7d67bd6d117e6d443a9bf63a2ecf8567bb3d8c6c7bc5014465", - "sha256:970284a88b99673ccb2e4e334cfb38a10aab7cd44f7457564d11898a74b62d0a", - "sha256:972c85d205b51e30e59525694670de6a8a89691186012535f9d7dbaa230e42c3", - "sha256:9a1ef3b66e38ef8618ce5fdc7bea3d9f45f3624e2a66295eea5e57966c85909e", - "sha256:af0e781009aaf59e25c5a678122391cb0f345ac0ec272c7961dc5455e1c40066", - "sha256:b6d534e4b2ab35c9f93f46229363e17f63c53ad01330df9f2d6bd1187e5eaacf", - "sha256:b7895207b4c843c76a25ab8c1e866261bcfe27bfaa20c192de5190121770672b", - "sha256:c0891a6a97b09c1f3e073a890514d5012eb256845c451bd48f7968ef939bf4ae", - "sha256:c2723d347ab06e7ddad1a58b2a821218239249a9e4365eaff6649d31180c1669", - "sha256:d1f8bf7b90ba55699b3a5e44930e93ff0189aa27186e96071fac7dd0d06a1873", - "sha256:d1f9ce122f83b2305592c11d64f181b87153fc2c2bbd3bb4a3dde8303cfb1a6b", - "sha256:d314ed732c25d29775e84a960c3c60808b682c08d86602ec2c3008e1202e3bb6", - "sha256:d636598c8305e1f90b439dbf4f66437de4a5e3c31fdf47ad29542478c8508bbb", - "sha256:deee1077aae10d8fa88cb02c845cfba9b62c55e1183f52f6ae6a2df6a2187160", - "sha256:ebe78fe9a0e874362175b02371bdfbee64d8edc42a044253ddf4ee7d3c15212c", - "sha256:f030f8873312a16414c0d8e1a1ddff2d3235655a2174e3648b4fa66b3f2f1079", - "sha256:f0b278ce10936db1a37e6954e15a3730bea96a0997c26d7fee88e6c396c2086d", - "sha256:f11642dddbb0253cc8853254301b51390ba0081750a8ac03f20ea8103f0c56b6" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4.0'", - "version": "==5.5" + "sha256:06a9a2be0b5b576c3f18f1a241f0473575c4a26021b52b2a85263a00f034d51f", + "sha256:06fb182e69f33f6cd1d39a6c597294cff3143554b64b9825d1dc69d18cc2fff2", + "sha256:0a5f9e1dbd7fbe30196578ca36f3fba75376fb99888c395c5880b355e2875f8a", + "sha256:0e1f928eaf5469c11e886fe0885ad2bf1ec606434e79842a879277895a50942a", + "sha256:171717c7cb6b453aebac9a2ef603699da237f341b38eebfee9be75d27dc38e01", + "sha256:1e9d683426464e4a252bf70c3498756055016f99ddaec3774bf368e76bbe02b6", + "sha256:201e7389591af40950a6480bd9edfa8ed04346ff80002cec1a66cac4549c1ad7", + "sha256:245167dd26180ab4c91d5e1496a30be4cd721a5cf2abf52974f965f10f11419f", + "sha256:2aee274c46590717f38ae5e4650988d1af340fe06167546cc32fe2f58ed05b02", + "sha256:2e07b54284e381531c87f785f613b833569c14ecacdcb85d56b25c4622c16c3c", + "sha256:31563e97dae5598556600466ad9beea39fb04e0229e61c12eaa206e0aa202063", + "sha256:33d6d3ea29d5b3a1a632b3c4e4f4ecae24ef170b0b9ee493883f2df10039959a", + "sha256:3d376df58cc111dc8e21e3b6e24606b5bb5dee6024f46a5abca99124b2229ef5", + "sha256:419bfd2caae268623dd469eff96d510a920c90928b60f2073d79f8fe2bbc5959", + "sha256:48c19d2159d433ccc99e729ceae7d5293fbffa0bdb94952d3579983d1c8c9d97", + "sha256:49969a9f7ffa086d973d91cec8d2e31080436ef0fb4a359cae927e742abfaaa6", + "sha256:52edc1a60c0d34afa421c9c37078817b2e67a392cab17d97283b64c5833f427f", + "sha256:537891ae8ce59ef63d0123f7ac9e2ae0fc8b72c7ccbe5296fec45fd68967b6c9", + "sha256:54b896376ab563bd38453cecb813c295cf347cf5906e8b41d340b0321a5433e5", + "sha256:58c2ccc2f00ecb51253cbe5d8d7122a34590fac9646a960d1430d5b15321d95f", + "sha256:5b7540161790b2f28143191f5f8ec02fb132660ff175b7747b95dcb77ac26562", + "sha256:5baa06420f837184130752b7c5ea0808762083bf3487b5038d68b012e5937dbe", + "sha256:5e330fc79bd7207e46c7d7fd2bb4af2963f5f635703925543a70b99574b0fea9", + "sha256:61b9a528fb348373c433e8966535074b802c7a5d7f23c4f421e6c6e2f1697a6f", + "sha256:63426706118b7f5cf6bb6c895dc215d8a418d5952544042c8a2d9fe87fcf09cb", + "sha256:6d040ef7c9859bb11dfeb056ff5b3872436e3b5e401817d87a31e1750b9ae2fb", + "sha256:6f48351d66575f535669306aa7d6d6f71bc43372473b54a832222803eb956fd1", + "sha256:7ee7d9d4822c8acc74a5e26c50604dff824710bc8de424904c0982e25c39c6cb", + "sha256:81c13a1fc7468c40f13420732805a4c38a105d89848b7c10af65a90beff25250", + "sha256:8d13c64ee2d33eccf7437961b6ea7ad8673e2be040b4f7fd4fd4d4d28d9ccb1e", + "sha256:8de8bb0e5ad103888d65abef8bca41ab93721647590a3f740100cd65c3b00511", + "sha256:8fa03bce9bfbeeef9f3b160a8bed39a221d82308b4152b27d82d8daa7041fee5", + "sha256:924d94291ca674905fe9481f12294eb11f2d3d3fd1adb20314ba89e94f44ed59", + "sha256:975d70ab7e3c80a3fe86001d8751f6778905ec723f5b110aed1e450da9d4b7f2", + "sha256:976b9c42fb2a43ebf304fa7d4a310e5f16cc99992f33eced91ef6f908bd8f33d", + "sha256:9e31cb64d7de6b6f09702bb27c02d1904b3aebfca610c12772452c4e6c21a0d3", + "sha256:a342242fe22407f3c17f4b499276a02b01e80f861f1682ad1d95b04018e0c0d4", + "sha256:a3d33a6b3eae87ceaefa91ffdc130b5e8536182cd6dfdbfc1aa56b46ff8c86de", + "sha256:a895fcc7b15c3fc72beb43cdcbdf0ddb7d2ebc959edac9cef390b0d14f39f8a9", + "sha256:afb17f84d56068a7c29f5fa37bfd38d5aba69e3304af08ee94da8ed5b0865833", + "sha256:b1c546aca0ca4d028901d825015dc8e4d56aac4b541877690eb76490f1dc8ed0", + "sha256:b29019c76039dc3c0fd815c41392a044ce555d9bcdd38b0fb60fb4cd8e475ba9", + "sha256:b46517c02ccd08092f4fa99f24c3b83d8f92f739b4657b0f146246a0ca6a831d", + "sha256:b7aa5f8a41217360e600da646004f878250a0d6738bcdc11a0a39928d7dc2050", + "sha256:b7b4c971f05e6ae490fef852c218b0e79d4e52f79ef0c8475566584a8fb3e01d", + "sha256:ba90a9563ba44a72fda2e85302c3abc71c5589cea608ca16c22b9804262aaeb6", + "sha256:cb017fd1b2603ef59e374ba2063f593abe0fc45f2ad9abdde5b4d83bd922a353", + "sha256:d22656368f0e6189e24722214ed8d66b8022db19d182927b9a248a2a8a2f67eb", + "sha256:d2c2db7fd82e9b72937969bceac4d6ca89660db0a0967614ce2481e81a0b771e", + "sha256:d39b5b4f2a66ccae8b7263ac3c8170994b65266797fb96cbbfd3fb5b23921db8", + "sha256:d62a5c7dad11015c66fbb9d881bc4caa5b12f16292f857842d9d1871595f4495", + "sha256:e7d9405291c6928619403db1d10bd07888888ec1abcbd9748fdaa971d7d661b2", + "sha256:e84606b74eb7de6ff581a7915e2dab7a28a0517fbe1c9239eb227e1354064dcd", + "sha256:eb393e5ebc85245347950143969b241d08b52b88a3dc39479822e073a1a8eb27", + "sha256:ebba1cd308ef115925421d3e6a586e655ca5a77b5bf41e02eb0e4562a111f2d1", + "sha256:ee57190f24fba796e36bb6d3aa8a8783c643d8fa9760c89f7a98ab5455fbf818", + "sha256:f2f67fe12b22cd130d34d0ef79206061bfb5eda52feb6ce0dba0644e20a03cf4", + "sha256:f6951407391b639504e3b3be51b7ba5f3528adbf1a8ac3302b687ecababf929e", + "sha256:f75f7168ab25dd93110c8a8117a22450c19976afbc44234cbf71481094c1b850", + "sha256:fdec9e8cbf13a5bf63290fc6013d216a4c7232efb51548594ca3631a7f13c3a3" + ], + "markers": "python_version >= '3.7'", + "version": "==7.2.7" }, "coveragespace": { "hashes": [ @@ -209,6 +384,31 @@ "index": "pypi", "version": "==4.1" }, + "cryptography": { + "hashes": [ + "sha256:0ddaee209d1cf1f180f1efa338a68c4621154de0afaef92b89486f5f96047c55", + "sha256:14754bcdae909d66ff24b7b5f166d69340ccc6cb15731670435efd5719294895", + "sha256:344c6de9f8bda3c425b3a41b319522ba3208551b70c2ae00099c205f0d9fd3be", + "sha256:34d405ea69a8b34566ba3dfb0521379b210ea5d560fafedf9f800a9a94a41928", + "sha256:3680248309d340fda9611498a5319b0193a8dbdb73586a1acf8109d06f25b92d", + "sha256:3c5ef25d060c80d6d9f7f9892e1d41bb1c79b78ce74805b8cb4aa373cb7d5ec8", + "sha256:4ab14d567f7bbe7f1cdff1c53d5324ed4d3fc8bd17c481b395db224fb405c237", + "sha256:5c1f7293c31ebc72163a9a0df246f890d65f66b4a40d9ec80081969ba8c78cc9", + "sha256:6b71f64beeea341c9b4f963b48ee3b62d62d57ba93eb120e1196b31dc1025e78", + "sha256:7d92f0248d38faa411d17f4107fc0bce0c42cae0b0ba5415505df72d751bf62d", + "sha256:8362565b3835ceacf4dc8f3b56471a2289cf51ac80946f9087e66dc283a810e0", + "sha256:84a165379cb9d411d58ed739e4af3396e544eac190805a54ba2e0322feb55c46", + "sha256:88ff107f211ea696455ea8d911389f6d2b276aabf3231bf72c8853d22db755c5", + "sha256:9f65e842cb02550fac96536edb1d17f24c0a338fd84eaf582be25926e993dde4", + "sha256:a4fc68d1c5b951cfb72dfd54702afdbbf0fb7acdc9b7dc4301bbf2225a27714d", + "sha256:b7f2f5c525a642cecad24ee8670443ba27ac1fab81bba4cc24c7b6b41f2d0c75", + "sha256:b846d59a8d5a9ba87e2c3d757ca019fa576793e8758174d3868aecb88d6fc8eb", + "sha256:bf8fc66012ca857d62f6a347007e166ed59c0bc150cefa49f28376ebe7d992a2", + "sha256:f5d0bf9b252f30a31664b6f64432b4730bb7038339bd18b1fafe129cfc2be9be" + ], + "index": "pypi", + "version": "==41.0.0" + }, "docopt": { "hashes": [ "sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491" @@ -239,49 +439,65 @@ "index": "pypi", "version": "==3.7.9" }, + "ghp-import": { + "hashes": [ + "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619", + "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343" + ], + "version": "==2.1.0" + }, "idna": { "hashes": [ - "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6", - "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0" + "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4", + "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==2.10" + "markers": "python_version >= '3.5'", + "version": "==3.4" }, "importlib-metadata": { "hashes": [ - "sha256:960d52ba7c21377c990412aca380bf3642d734c2eaab78a2c39319f67c6a5786", - "sha256:e592faad8de1bda9fe920cf41e15261e7131bcf266c30306eec00e8e225c1dd5" + "sha256:43dd286a2cd8995d5eaef7fee2066340423b818ed3fd70adf0bad5f1fac53fed", + "sha256:92501cdf9cc66ebd3e612f1b4f0c0765dfa42f0fa38ffb319b6bd84dd675d705" ], - "markers": "python_version >= '3.6'", - "version": "==4.4.0" + "markers": "python_version >= '3.7'", + "version": "==6.6.0" }, - "jinja2": { + "jaraco.classes": { "hashes": [ - "sha256:1f06f2da51e7b56b8f238affdd6b4e2c61e39598a378cc49345bc1bd42a978a4", - "sha256:703f484b47a6af502e743c9122595cc812b0271f661722403114f71a79d0f5a4" + "sha256:2353de3288bc6b82120752201c6b1c1a14b058267fa424ed5ce5984e3b922158", + "sha256:89559fa5c1d3c34eff6f631ad80bb21f378dbcbb35dd161fd2c6b93f5be2f98a" ], - "markers": "python_version >= '3.6'", - "version": "==3.0.1" + "markers": "python_version >= '3.7'", + "version": "==3.2.3" }, - "keyring": { + "jeepney": { "hashes": [ - "sha256:045703609dd3fccfcdb27da201684278823b72af515aedec1a8515719a038cb8", - "sha256:8f607d7d1cc502c43a932a275a56fe47db50271904513a379d39df1af277ac48" + "sha256:5efe48d255973902f6badc3ce55e2aa6c5c3b3bc642059ef3a91247bcfcc5806", + "sha256:c0a454ad016ca575060802ee4d590dd912e35c122fa04e70306de3d076cce755" ], - "markers": "python_version >= '3.6'", - "version": "==23.0.1" + "markers": "sys_platform == 'linux'", + "version": "==0.8.0" }, - "livereload": { + "jinja2": { + "hashes": [ + "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852", + "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61" + ], + "markers": "python_version >= '3.7'", + "version": "==3.1.2" + }, + "keyring": { "hashes": [ - "sha256:776f2f865e59fde56490a56bcc6773b6917366bce0c267c60ee8aaf1a0959869" + "sha256:771ed2a91909389ed6148631de678f82ddc73737d85a927f382a8a1b157898cd", + "sha256:ba2e15a9b35e21908d0aaf4e0a47acc52d6ae33444df0da2b49d41a46ef6d678" ], - "version": "==2.6.3" + "markers": "python_version >= '3.7'", + "version": "==23.13.1" }, "macfsevents": { "hashes": [ "sha256:1324b66b356051de662ba87d84f73ada062acd42b047ed1246e60a449f833e10" ], - "index": "pypi", "markers": "sys_platform == 'darwin'", "version": "==0.8.1" }, @@ -295,43 +511,59 @@ }, "markupsafe": { "hashes": [ - "sha256:01a9b8ea66f1658938f65b93a85ebe8bc016e6769611be228d797c9d998dd298", - "sha256:023cb26ec21ece8dc3907c0e8320058b2e0cb3c55cf9564da612bc325bed5e64", - "sha256:0446679737af14f45767963a1a9ef7620189912317d095f2d9ffa183a4d25d2b", - "sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567", - "sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff", - "sha256:10f82115e21dc0dfec9ab5c0223652f7197feb168c940f3ef61563fc2d6beb74", - "sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35", - "sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26", - "sha256:3c112550557578c26af18a1ccc9e090bfe03832ae994343cfdacd287db6a6ae7", - "sha256:47ab1e7b91c098ab893b828deafa1203de86d0bc6ab587b160f78fe6c4011f75", - "sha256:49e3ceeabbfb9d66c3aef5af3a60cc43b85c33df25ce03d0031a608b0a8b2e3f", - "sha256:4efca8f86c54b22348a5467704e3fec767b2db12fc39c6d963168ab1d3fc9135", - "sha256:53edb4da6925ad13c07b6d26c2a852bd81e364f95301c66e930ab2aef5b5ddd8", - "sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a", - "sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914", - "sha256:6557b31b5e2c9ddf0de32a691f2312a32f77cd7681d8af66c2692efdbef84c18", - "sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8", - "sha256:6a7fae0dd14cf60ad5ff42baa2e95727c3d81ded453457771d02b7d2b3f9c0c2", - "sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d", - "sha256:7d91275b0245b1da4d4cfa07e0faedd5b0812efc15b702576d103293e252af1b", - "sha256:905fec760bd2fa1388bb5b489ee8ee5f7291d692638ea5f67982d968366bef9f", - "sha256:97383d78eb34da7e1fa37dd273c20ad4320929af65d156e35a5e2d89566d9dfb", - "sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833", - "sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415", - "sha256:ab3ef638ace319fa26553db0624c4699e31a28bb2a835c5faca8f8acf6a5a902", - "sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9", - "sha256:b7f2d075102dc8c794cbde1947378051c4e5180d52d276987b8d28a3bd58c17d", - "sha256:be98f628055368795d818ebf93da628541e10b75b41c559fdf36d104c5787066", - "sha256:d7f9850398e85aba693bb640262d3611788b1f29a79f0c93c565694658f4071f", - "sha256:f5653a225f31e113b152e56f154ccbe59eeb1c7487b39b9d9f9cdb58e6c79dc5", - "sha256:f826e31d18b516f653fe296d967d700fddad5901ae07c622bb3705955e1faa94", - "sha256:f8ba0e8349a38d3001fae7eadded3f6606f0da5d748ee53cc1dab1d6527b9509", - "sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51", - "sha256:fa130dd50c57d53368c9d59395cb5526eda596d3ffe36666cd81a44d56e48872" - ], - "markers": "python_version >= '3.6'", - "version": "==2.0.1" + "sha256:0576fe974b40a400449768941d5d0858cc624e3249dfd1e0c33674e5c7ca7aed", + "sha256:085fd3201e7b12809f9e6e9bc1e5c96a368c8523fad5afb02afe3c051ae4afcc", + "sha256:090376d812fb6ac5f171e5938e82e7f2d7adc2b629101cec0db8b267815c85e2", + "sha256:0b462104ba25f1ac006fdab8b6a01ebbfbce9ed37fd37fd4acd70c67c973e460", + "sha256:137678c63c977754abe9086a3ec011e8fd985ab90631145dfb9294ad09c102a7", + "sha256:1bea30e9bf331f3fef67e0a3877b2288593c98a21ccb2cf29b74c581a4eb3af0", + "sha256:22152d00bf4a9c7c83960521fc558f55a1adbc0631fbb00a9471e097b19d72e1", + "sha256:22731d79ed2eb25059ae3df1dfc9cb1546691cc41f4e3130fe6bfbc3ecbbecfa", + "sha256:2298c859cfc5463f1b64bd55cb3e602528db6fa0f3cfd568d3605c50678f8f03", + "sha256:28057e985dace2f478e042eaa15606c7efccb700797660629da387eb289b9323", + "sha256:2e7821bffe00aa6bd07a23913b7f4e01328c3d5cc0b40b36c0bd81d362faeb65", + "sha256:2ec4f2d48ae59bbb9d1f9d7efb9236ab81429a764dedca114f5fdabbc3788013", + "sha256:340bea174e9761308703ae988e982005aedf427de816d1afe98147668cc03036", + "sha256:40627dcf047dadb22cd25ea7ecfe9cbf3bbbad0482ee5920b582f3809c97654f", + "sha256:40dfd3fefbef579ee058f139733ac336312663c6706d1163b82b3003fb1925c4", + "sha256:4cf06cdc1dda95223e9d2d3c58d3b178aa5dacb35ee7e3bbac10e4e1faacb419", + "sha256:50c42830a633fa0cf9e7d27664637532791bfc31c731a87b202d2d8ac40c3ea2", + "sha256:55f44b440d491028addb3b88f72207d71eeebfb7b5dbf0643f7c023ae1fba619", + "sha256:608e7073dfa9e38a85d38474c082d4281f4ce276ac0010224eaba11e929dd53a", + "sha256:63ba06c9941e46fa389d389644e2d8225e0e3e5ebcc4ff1ea8506dce646f8c8a", + "sha256:65608c35bfb8a76763f37036547f7adfd09270fbdbf96608be2bead319728fcd", + "sha256:665a36ae6f8f20a4676b53224e33d456a6f5a72657d9c83c2aa00765072f31f7", + "sha256:6d6607f98fcf17e534162f0709aaad3ab7a96032723d8ac8750ffe17ae5a0666", + "sha256:7313ce6a199651c4ed9d7e4cfb4aa56fe923b1adf9af3b420ee14e6d9a73df65", + "sha256:7668b52e102d0ed87cb082380a7e2e1e78737ddecdde129acadb0eccc5423859", + "sha256:7df70907e00c970c60b9ef2938d894a9381f38e6b9db73c5be35e59d92e06625", + "sha256:7e007132af78ea9df29495dbf7b5824cb71648d7133cf7848a2a5dd00d36f9ff", + "sha256:835fb5e38fd89328e9c81067fd642b3593c33e1e17e2fdbf77f5676abb14a156", + "sha256:8bca7e26c1dd751236cfb0c6c72d4ad61d986e9a41bbf76cb445f69488b2a2bd", + "sha256:8db032bf0ce9022a8e41a22598eefc802314e81b879ae093f36ce9ddf39ab1ba", + "sha256:99625a92da8229df6d44335e6fcc558a5037dd0a760e11d84be2260e6f37002f", + "sha256:9cad97ab29dfc3f0249b483412c85c8ef4766d96cdf9dcf5a1e3caa3f3661cf1", + "sha256:a4abaec6ca3ad8660690236d11bfe28dfd707778e2442b45addd2f086d6ef094", + "sha256:a6e40afa7f45939ca356f348c8e23048e02cb109ced1eb8420961b2f40fb373a", + "sha256:a6f2fcca746e8d5910e18782f976489939d54a91f9411c32051b4aab2bd7c513", + "sha256:a806db027852538d2ad7555b203300173dd1b77ba116de92da9afbc3a3be3eed", + "sha256:abcabc8c2b26036d62d4c746381a6f7cf60aafcc653198ad678306986b09450d", + "sha256:b8526c6d437855442cdd3d87eede9c425c4445ea011ca38d937db299382e6fa3", + "sha256:bb06feb762bade6bf3c8b844462274db0c76acc95c52abe8dbed28ae3d44a147", + "sha256:c0a33bc9f02c2b17c3ea382f91b4db0e6cde90b63b296422a939886a7a80de1c", + "sha256:c4a549890a45f57f1ebf99c067a4ad0cb423a05544accaf2b065246827ed9603", + "sha256:ca244fa73f50a800cf8c3ebf7fd93149ec37f5cb9596aa8873ae2c1d23498601", + "sha256:cf877ab4ed6e302ec1d04952ca358b381a882fbd9d1b07cccbfd61783561f98a", + "sha256:d9d971ec1e79906046aa3ca266de79eac42f1dbf3612a05dc9368125952bd1a1", + "sha256:da25303d91526aac3672ee6d49a2f3db2d9502a4a60b55519feb1a4c7714e07d", + "sha256:e55e40ff0cc8cc5c07996915ad367fa47da6b3fc091fdadca7f5403239c5fec3", + "sha256:f03a532d7dee1bed20bc4884194a16160a2de9ffc6354b3878ec9682bb623c54", + "sha256:f1cd098434e83e656abf198f103a8207a8187c0fc110306691a2e94a78d0abb2", + "sha256:f2bfb563d0211ce16b63c7cb9395d2c682a23187f54c3d79bfec33e6705473c6", + "sha256:f8ffb705ffcf5ddd0e80b65ddf7bed7ee4f5a441ea7d3419e861a12eaf41af58" + ], + "markers": "python_version >= '3.7'", + "version": "==2.1.2" }, "mccabe": { "hashes": [ @@ -340,21 +572,37 @@ ], "version": "==0.6.1" }, + "mergedeep": { + "hashes": [ + "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8", + "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307" + ], + "markers": "python_version >= '3.6'", + "version": "==1.3.4" + }, "minilog": { "hashes": [ - "sha256:58499302bca86cf507eb3c3dfa3853de9389bfde275ba5155bdc3b4551175918", - "sha256:891ad346bdd63aee4c210faa688497f7ba412cf52a54fb6cba6bf511a34a5138" + "sha256:0c48879cc9e72f0127aa2c36b522dc6fa10fa8532956197436b491d31617d5d5", + "sha256:2048a8d381b36ef5f146fb9a657e627729411f8e2ed0047e2c1286cf8e3e58d7" ], - "markers": "python_version >= '3.6' and python_version < '4.0'", - "version": "==2.0" + "markers": "python_version >= '3.7' and python_version < '4.0'", + "version": "==2.1" }, "mkdocs": { "hashes": [ - "sha256:1b4d46cd1cb517cd743358da96a3efc588fd86f81512fb9c28214597b6dc731f", - "sha256:cd7264ea42d76f5bc1a0bd8b0a2c6c6e6be3a8742f5e78f47104a452dbe93600" + "sha256:89f5a094764381cda656af4298727c9f53dc3e602983087e1fe96ea1df24f4c1", + "sha256:a1fa8c2d0c1305d7fc2b9d9f607c71778572a8b110fb26642aa00296c9e6d072" ], "index": "pypi", - "version": "==0.17.5" + "version": "==1.2.3" + }, + "more-itertools": { + "hashes": [ + "sha256:cabaa341ad0389ea83c17a94566a53ae4c9d07349861ecb14dc6d0345cf9ac5d", + "sha256:d2bc7f02446e86a68911e58ded76d6561eea00cddfb2a91e7019bbb586c799f3" + ], + "markers": "python_version >= '3.7'", + "version": "==9.1.0" }, "nose": { "hashes": [ @@ -367,18 +615,19 @@ }, "packaging": { "hashes": [ - "sha256:5b327ac1320dc863dca72f4514ecc086f31186744b84a230374cc1fd776feae5", - "sha256:67714da7f7bc052e064859c05c595155bd1ee9f69f76557e21f051443c20947a" + "sha256:994793af429502c4ea2ebf6bf664629d07c1a9fe974af92966e4b8d2df7edc61", + "sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==20.9" + "markers": "python_version >= '3.7'", + "version": "==23.1" }, "pkginfo": { "hashes": [ - "sha256:029a70cb45c6171c329dfc890cde0879f8c52d6f3922794796e06f577bb03db4", - "sha256:9fdbea6495622e022cc72c2e5e1b735218e4ffb2a2a69cde2694a6c1f16afb75" + "sha256:4b7a555a6d5a22169fcc9cf7bfd78d296b0361adad412a346c1226849af5e546", + "sha256:8fd5896e8718a4372f0ea9cc9d96f6417c9b986e23a4d116dda26b62cc29d046" ], - "version": "==1.7.0" + "markers": "python_version >= '3.6'", + "version": "==1.9.6" }, "pycodestyle": { "hashes": [ @@ -388,6 +637,13 @@ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.5.0" }, + "pycparser": { + "hashes": [ + "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9", + "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206" + ], + "version": "==2.21" + }, "pyflakes": { "hashes": [ "sha256:17dbeb2e3f4d772725c777fabc446d5634d1038f234e77343108ce445ea69ce0", @@ -398,35 +654,26 @@ }, "pygments": { "hashes": [ - "sha256:a18f47b506a429f6f4b9df81bb02beab9ca21d0a5fee38ed15aef65f0545519f", - "sha256:d66e804411278594d764fc69ec36ec13d9ae9147193a1740cd34d272ca383b8e" + "sha256:8ace4d3c1dd481894b2005f560ead0f9f19ee64fe983366be1a21e171d12775c", + "sha256:db2db3deb4b4179f399a09054b023b6a586b76499d36965813c71aa8ed7b5fd1" ], - "markers": "python_version >= '3.5'", - "version": "==2.9.0" + "markers": "python_version >= '3.7'", + "version": "==2.15.1" }, "pync": { "hashes": [ "sha256:85737aab9fc69cf59dc9fe831adbe94ac224944c05e297c98de3c2413f253530" ], - "index": "pypi", "markers": "sys_platform == 'darwin'", "version": "==1.6.1" }, - "pyparsing": { - "hashes": [ - "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1", - "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b" - ], - "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==2.4.7" - }, "python-dateutil": { "hashes": [ - "sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c", - "sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a" + "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86", + "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==2.8.1" + "version": "==2.8.2" }, "python-termstyle": { "hashes": [ @@ -437,67 +684,105 @@ }, "pyyaml": { "hashes": [ - "sha256:08682f6b72c722394747bddaf0aa62277e02557c0fd1c42cb853016a38f8dedf", - "sha256:0f5f5786c0e09baddcd8b4b45f20a7b5d61a7e7e99846e3c799b05c7c53fa696", - "sha256:129def1b7c1bf22faffd67b8f3724645203b79d8f4cc81f674654d9902cb4393", - "sha256:294db365efa064d00b8d1ef65d8ea2c3426ac366c0c4368d930bf1c5fb497f77", - "sha256:3b2b1824fe7112845700f815ff6a489360226a5609b96ec2190a45e62a9fc922", - "sha256:3bd0e463264cf257d1ffd2e40223b197271046d09dadf73a0fe82b9c1fc385a5", - "sha256:4465124ef1b18d9ace298060f4eccc64b0850899ac4ac53294547536533800c8", - "sha256:49d4cdd9065b9b6e206d0595fee27a96b5dd22618e7520c33204a4a3239d5b10", - "sha256:4e0583d24c881e14342eaf4ec5fbc97f934b999a6828693a99157fde912540cc", - "sha256:5accb17103e43963b80e6f837831f38d314a0495500067cb25afab2e8d7a4018", - "sha256:607774cbba28732bfa802b54baa7484215f530991055bb562efbed5b2f20a45e", - "sha256:6c78645d400265a062508ae399b60b8c167bf003db364ecb26dcab2bda048253", - "sha256:72a01f726a9c7851ca9bfad6fd09ca4e090a023c00945ea05ba1638c09dc3347", - "sha256:74c1485f7707cf707a7aef42ef6322b8f97921bd89be2ab6317fd782c2d53183", - "sha256:895f61ef02e8fed38159bb70f7e100e00f471eae2bc838cd0f4ebb21e28f8541", - "sha256:8c1be557ee92a20f184922c7b6424e8ab6691788e6d86137c5d93c1a6ec1b8fb", - "sha256:bb4191dfc9306777bc594117aee052446b3fa88737cd13b7188d0e7aa8162185", - "sha256:bfb51918d4ff3d77c1c856a9699f8492c612cde32fd3bcd344af9be34999bfdc", - "sha256:c20cfa2d49991c8b4147af39859b167664f2ad4561704ee74c1de03318e898db", - "sha256:cb333c16912324fd5f769fff6bc5de372e9e7a202247b48870bc251ed40239aa", - "sha256:d2d9808ea7b4af864f35ea216be506ecec180628aced0704e34aca0b040ffe46", - "sha256:d483ad4e639292c90170eb6f7783ad19490e7a8defb3e46f97dfe4bacae89122", - "sha256:dd5de0646207f053eb0d6c74ae45ba98c3395a571a2891858e87df7c9b9bd51b", - "sha256:e1d4970ea66be07ae37a3c2e48b5ec63f7ba6804bdddfdbd3cfd954d25a82e63", - "sha256:e4fac90784481d221a8e4b1162afa7c47ed953be40d31ab4629ae917510051df", - "sha256:fa5ae20527d8e831e8230cbffd9f8fe952815b2b7dae6ffec25318803a7528fc", - "sha256:fd7f6999a8070df521b6384004ef42833b9bd62cfee11a09bda1079b4b704247", - "sha256:fdc842473cd33f45ff6bce46aea678a54e3d21f1b61a7750ce3c498eedfe25d6", - "sha256:fe69978f3f768926cfa37b867e3843918e012cf83f680806599ddce33c2c68b0" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", - "version": "==5.4.1" + "sha256:01b45c0191e6d66c470b6cf1b9531a771a83c1c4208272ead47a3ae4f2f603bf", + "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293", + "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b", + "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57", + "sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b", + "sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4", + "sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07", + "sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba", + "sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9", + "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287", + "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513", + "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0", + "sha256:432557aa2c09802be39460360ddffd48156e30721f5e8d917f01d31694216782", + "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0", + "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92", + "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f", + "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2", + "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc", + "sha256:81957921f441d50af23654aa6c5e5eaf9b06aba7f0a19c18a538dc7ef291c5a1", + "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c", + "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86", + "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4", + "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c", + "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34", + "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b", + "sha256:afa17f5bc4d1b10afd4466fd3a44dc0e245382deca5b3c353d8b757f9e3ecb8d", + "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c", + "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb", + "sha256:bfaef573a63ba8923503d27530362590ff4f576c626d86a9fed95822a8255fd7", + "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737", + "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3", + "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d", + "sha256:d4b0ba9512519522b118090257be113b9468d804b19d63c71dbcf4a48fa32358", + "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53", + "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78", + "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803", + "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a", + "sha256:dbad0e9d368bb989f4515da330b88a057617d16b6a8245084f1b05400f24609f", + "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174", + "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5" + ], + "markers": "python_version >= '3.6'", + "version": "==6.0" + }, + "pyyaml-env-tag": { + "hashes": [ + "sha256:70092675bda14fdec33b31ba77e7543de9ddc88f2e5b99160396572d11525bdb", + "sha256:af31106dec8a4d68c60207c1886031cbf839b68aa7abccdb19868200532c2069" + ], + "markers": "python_version >= '3.6'", + "version": "==0.1" }, "readme-renderer": { "hashes": [ - "sha256:63b4075c6698fcfa78e584930f07f39e05d46f3ec97f65006e430b595ca6348c", - "sha256:92fd5ac2bf8677f310f3303aa4bce5b9d5f9f2094ab98c29f13791d7b805a3db" + "sha256:cd653186dfc73055656f090f227f5cb22a046d7f71a841dfa305f55c9a513273", + "sha256:f67a16caedfa71eef48a31b39708637a6f4664c4394801a7b0d6432d13907343" ], - "version": "==29.0" + "markers": "python_version >= '3.7'", + "version": "==37.3" }, "requests": { "hashes": [ - "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804", - "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e" + "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f", + "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==2.25.1" + "markers": "python_version >= '3.7'", + "version": "==2.31.0" }, "requests-toolbelt": { "hashes": [ - "sha256:380606e1d10dc85c3bd47bf5a6095f815ec007be7a8b69c878507068df059e6f", - "sha256:968089d4584ad4ad7c171454f0a5c6dac23971e9472521ea3b6d49d610aa6fc0" + "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6", + "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06" ], - "version": "==0.9.1" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==1.0.0" }, "rfc3986": { "hashes": [ - "sha256:270aaf10d87d0d4e095063c65bf3ddbc6ee3d0b226328ce21e036f946e421835", - "sha256:a86d6e1f5b1dc238b218b012df0aa79409667bb209e58da56d0b94704e712a97" + "sha256:50b1502b60e289cb37883f3dfd34532b8873c7de9f49bb546641ce9cbd256ebd", + "sha256:97aacf9dbd4bfd829baad6e6309fa6573aaf1be3f6fa735c8ab05e46cecb261c" + ], + "markers": "python_version >= '3.7'", + "version": "==2.0.0" + }, + "secretstorage": { + "hashes": [ + "sha256:2403533ef369eca6d2ba81718576c5e0f564d5cca1b58f73a8b23e7d4eeebd77", + "sha256:f356e6628222568e3af06f2eba8df495efa13b3b63081dafd4f7d9a7b7bc9f99" + ], + "markers": "sys_platform == 'linux'", + "version": "==3.3.3" + }, + "setuptools": { + "hashes": [ + "sha256:5df61bf30bb10c6f756eb19e7c9f3b473051f48db77fddbe06ff2ca307df9a6f", + "sha256:62642358adc77ffa87233bc4d2354c4b2682d214048f500964dbe760ccedf102" ], - "version": "==1.5.0" + "markers": "python_version >= '3.7'", + "version": "==67.8.0" }, "six": { "hashes": [ @@ -515,23 +800,13 @@ "index": "pypi", "version": "==0.4.1" }, - "tornado": { - "hashes": [ - "sha256:5ef073ac6180038ccf99411fe05ae9aafb675952a2c8db60592d5daf8401f803", - "sha256:6d14e47eab0e15799cf3cdcc86b0b98279da68522caace2bd7ce644287685f0a", - "sha256:92b7ca81e18ba9ec3031a7ee73d4577ac21d41a0c9b775a9182f43301c3b5f8e", - "sha256:ab587996fe6fb9ce65abfda440f9b61e4f9f2cf921967723540679176915e4c3", - "sha256:b36298e9f63f18cad97378db2222c0e0ca6a55f6304e605515e05a25483ed51a" - ], - "version": "==4.5.3" - }, "tqdm": { "hashes": [ - "sha256:736524215c690621b06fc89d0310a49822d75e599fcd0feb7cc742b98d692493", - "sha256:cd5791b5d7c3f2f1819efc81d36eb719a38e0906a7380365c556779f585ea042" + "sha256:1871fb68a86b8fb3b59ca4cdd3dcccbc7e6d613eeed31f4c332531977b89beb5", + "sha256:c4f53a17fe37e132815abceec022631be8ffe1b9381c2e6e30aa70edc99e9671" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==4.61.0" + "markers": "python_version >= '3.7'", + "version": "==4.65.0" }, "twine": { "hashes": [ @@ -543,11 +818,44 @@ }, "urllib3": { "hashes": [ - "sha256:753a0374df26658f99d826cfe40394a686d05985786d946fbe4165b5148f5a7c", - "sha256:a7acd0977125325f516bda9735fa7142b909a8d01e8b2e4c8108d0984e6e0098" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4.0'", - "version": "==1.26.5" + "sha256:61717a1095d7e155cdb737ac7bb2f4324a858a1e2e6466f6d03ff630ca68d3cc", + "sha256:d055c2f9d38dc53c808f6fdc8eab7360b6fdbbde02340ed25cfbcd817c62469e" + ], + "markers": "python_version >= '3.7'", + "version": "==2.0.2" + }, + "watchdog": { + "hashes": [ + "sha256:0e06ab8858a76e1219e68c7573dfeba9dd1c0219476c5a44d5333b01d7e1743a", + "sha256:13bbbb462ee42ec3c5723e1205be8ced776f05b100e4737518c67c8325cf6100", + "sha256:233b5817932685d39a7896b1090353fc8efc1ef99c9c054e46c8002561252fb8", + "sha256:25f70b4aa53bd743729c7475d7ec41093a580528b100e9a8c5b5efe8899592fc", + "sha256:2b57a1e730af3156d13b7fdddfc23dea6487fceca29fc75c5a868beed29177ae", + "sha256:336adfc6f5cc4e037d52db31194f7581ff744b67382eb6021c868322e32eef41", + "sha256:3aa7f6a12e831ddfe78cdd4f8996af9cf334fd6346531b16cec61c3b3c0d8da0", + "sha256:3ed7c71a9dccfe838c2f0b6314ed0d9b22e77d268c67e015450a29036a81f60f", + "sha256:4c9956d27be0bb08fc5f30d9d0179a855436e655f046d288e2bcc11adfae893c", + "sha256:4d98a320595da7a7c5a18fc48cb633c2e73cda78f93cac2ef42d42bf609a33f9", + "sha256:4f94069eb16657d2c6faada4624c39464f65c05606af50bb7902e036e3219be3", + "sha256:5113334cf8cf0ac8cd45e1f8309a603291b614191c9add34d33075727a967709", + "sha256:51f90f73b4697bac9c9a78394c3acbbd331ccd3655c11be1a15ae6fe289a8c83", + "sha256:5d9f3a10e02d7371cd929b5d8f11e87d4bad890212ed3901f9b4d68767bee759", + "sha256:7ade88d0d778b1b222adebcc0927428f883db07017618a5e684fd03b83342bd9", + "sha256:7c5f84b5194c24dd573fa6472685b2a27cc5a17fe5f7b6fd40345378ca6812e3", + "sha256:7e447d172af52ad204d19982739aa2346245cc5ba6f579d16dac4bfec226d2e7", + "sha256:8ae9cda41fa114e28faf86cb137d751a17ffd0316d1c34ccf2235e8a84365c7f", + "sha256:8f3ceecd20d71067c7fd4c9e832d4e22584318983cabc013dbf3f70ea95de346", + "sha256:9fac43a7466eb73e64a9940ac9ed6369baa39b3bf221ae23493a9ec4d0022674", + "sha256:a70a8dcde91be523c35b2bf96196edc5730edb347e374c7de7cd20c43ed95397", + "sha256:adfdeab2da79ea2f76f87eb42a3ab1966a5313e5a69a0213a3cc06ef692b0e96", + "sha256:ba07e92756c97e3aca0912b5cbc4e5ad802f4557212788e72a72a47ff376950d", + "sha256:c07253088265c363d1ddf4b3cdb808d59a0468ecd017770ed716991620b8f77a", + "sha256:c9d8c8ec7efb887333cf71e328e39cffbf771d8f8f95d308ea4125bf5f90ba64", + "sha256:d00e6be486affb5781468457b21a6cbe848c33ef43f9ea4a73b4882e5f188a44", + "sha256:d429c2430c93b7903914e4db9a966c7f2b068dd2ebdd2fa9b9ce094c7d459f33" + ], + "markers": "python_version >= '3.7'", + "version": "==3.0.0" }, "webencodings": { "hashes": [ @@ -558,11 +866,11 @@ }, "zipp": { "hashes": [ - "sha256:3607921face881ba3e026887d8150cca609d517579abe052ac81fc5aeffdbd76", - "sha256:51cb66cc54621609dd593d1787f286ee42a5c0adbb4b29abea5a63edc3e03098" + "sha256:112929ad649da941c23de50f356a2b5570c954b65150642bccdd66bf194d224b", + "sha256:48904fc76a60e542af151aded95726c1a5c34ed43ab4134b597665c86d7ad556" ], - "markers": "python_version >= '3.6'", - "version": "==3.4.1" + "markers": "python_version >= '3.7'", + "version": "==3.15.0" } } } diff --git a/docs/about/release-notes.md b/docs/about/release-notes.md index 9252703..084d595 100644 --- a/docs/about/release-notes.md +++ b/docs/about/release-notes.md @@ -1,5 +1,9 @@ # Release Notes +## Version 3.1 + +- Fixed support for Flask `2.3`. + ## Version 3.0 * Dropped support for Flask `<2.0`. diff --git a/example.py b/example.py index 0680147..4b5511c 100644 --- a/example.py +++ b/example.py @@ -1,29 +1,30 @@ from flask import request, url_for -from flask.ext.api import FlaskAPI, status, exceptions +from flask.ext.api import FlaskAPI, exceptions, status app = FlaskAPI(__name__) notes = { - 0: 'do the shopping', - 1: 'build the codez', - 2: 'paint the door', + 0: "do the shopping", + 1: "build the codez", + 2: "paint the door", } + def note_repr(key): return { - 'url': request.host_url.rstrip('/') + url_for('notes_detail', key=key), - 'text': notes[key] + "url": request.host_url.rstrip("/") + url_for("notes_detail", key=key), + "text": notes[key], } -@app.route("/", methods=['GET', 'POST']) +@app.route("/", methods=["GET", "POST"]) def notes_list(): """ List or create notes. """ - if request.method == 'POST': - note = str(request.data.get('text', '')) + if request.method == "POST": + note = str(request.data.get("text", "")) idx = max(notes.keys()) + 1 notes[idx] = note return note_repr(idx), status.HTTP_201_CREATED @@ -32,19 +33,19 @@ def notes_list(): return [note_repr(idx) for idx in sorted(notes.keys())] -@app.route("//", methods=['GET', 'PUT', 'DELETE']) +@app.route("//", methods=["GET", "PUT", "DELETE"]) def notes_detail(key): """ Retrieve, update or delete note instances. """ - if request.method == 'PUT': - note = str(request.data.get('text', '')) + if request.method == "PUT": + note = str(request.data.get("text", "")) notes[key] = note return note_repr(key) - elif request.method == 'DELETE': + elif request.method == "DELETE": notes.pop(key, None) - return '', status.HTTP_204_NO_CONTENT + return "", status.HTTP_204_NO_CONTENT # request.method == 'GET' if key not in notes: diff --git a/flask_api/__init__.py b/flask_api/__init__.py index 0807d17..7d4d782 100644 --- a/flask_api/__init__.py +++ b/flask_api/__init__.py @@ -1,3 +1,3 @@ from flask_api.app import FlaskAPI -__version__ = '3.0.post1' +__version__ = "3.1" diff --git a/flask_api/app.py b/flask_api/app.py index 25f75a6..5556445 100644 --- a/flask_api/app.py +++ b/flask_api/app.py @@ -1,22 +1,23 @@ -# coding: utf8 -from __future__ import unicode_literals -from flask import request, Flask, Blueprint +import re +import sys +from itertools import chain + +from flask import Blueprint, Flask, request +from werkzeug.exceptions import HTTPException + +from flask_api.compat import is_flask_legacy from flask_api.exceptions import APIException from flask_api.request import APIRequest from flask_api.response import APIResponse from flask_api.settings import APISettings from flask_api.status import HTTP_204_NO_CONTENT -from itertools import chain -from werkzeug.exceptions import HTTPException -import re -import sys -from flask_api.compat import is_flask_legacy - api_resources = Blueprint( - 'flask-api', __name__, - url_prefix='/flask-api', - template_folder='templates', static_folder='static' + "flask-api", + __name__, + url_prefix="/flask-api", + template_folder="templates", + static_folder="static", ) @@ -32,7 +33,7 @@ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.api_settings = APISettings(self.config) self.register_blueprint(api_resources) - self.jinja_env.filters['urlize_quoted_links'] = urlize_quoted_links + self.jinja_env.filters["urlize_quoted_links"] = urlize_quoted_links def preprocess_request(self): request.parser_classes = self.api_settings.DEFAULT_PARSERS @@ -49,10 +50,10 @@ def make_response(self, rv): rv, status_or_headers, headers = rv + (None,) * (3 - len(rv)) if rv is None and status_or_headers == HTTP_204_NO_CONTENT: - rv = '' + rv = "" if rv is None and status_or_headers: - raise ValueError('View function did not return a response') + raise ValueError("View function did not return a response") if isinstance(status_or_headers, (dict, list)): headers, status_or_headers = status_or_headers, None @@ -98,15 +99,16 @@ def handle_user_exception(self, e): if isinstance(e, typecheck): return handler(e) else: - for typecheck, handler in chain(dict(blueprint_handlers).items(), - dict(app_handlers).items()): + for typecheck, handler in chain( + dict(blueprint_handlers).items(), dict(app_handlers).items() + ): if isinstance(e, typecheck): return handler(e) raise e def handle_api_exception(self, exc): - content = {'message': exc.detail} + content = {"message": exc.detail} status = exc.status_code return self.response_class(content, status=status) @@ -119,13 +121,15 @@ def create_url_adapter(self, request): """ if request is not None: environ = request.environ.copy() - environ['REQUEST_METHOD'] = request.method - return self.url_map.bind_to_environ(environ, - server_name=self.config['SERVER_NAME']) + environ["REQUEST_METHOD"] = request.method + return self.url_map.bind_to_environ( + environ, server_name=self.config["SERVER_NAME"] + ) # We need at the very least the server name to be set for this # to work. - if self.config['SERVER_NAME'] is not None: + if self.config["SERVER_NAME"] is not None: return self.url_map.bind( - self.config['SERVER_NAME'], - script_name=self.config['APPLICATION_ROOT'] or '/', - url_scheme=self.config['PREFERRED_URL_SCHEME']) + self.config["SERVER_NAME"], + script_name=self.config["APPLICATION_ROOT"] or "/", + url_scheme=self.config["PREFERRED_URL_SCHEME"], + ) diff --git a/flask_api/compat.py b/flask_api/compat.py index c59b984..8760df5 100644 --- a/flask_api/compat.py +++ b/flask_api/compat.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals, absolute_import from flask import __version__ as flask_version # Markdown is optional @@ -17,7 +15,6 @@ def apply_markdown(text): md = markdown.Markdown(extensions=extensions) return md.convert(text) - except ImportError: # pragma: no cover - markdown installed for tests apply_markdown = None diff --git a/flask_api/decorators.py b/flask_api/decorators.py index d3a09b2..6b9f5b8 100644 --- a/flask_api/decorators.py +++ b/flask_api/decorators.py @@ -1,4 +1,5 @@ from functools import wraps + from flask import request @@ -11,7 +12,9 @@ def decorated_function(*args, **kwargs): else: request.parser_classes = parsers return func(*args, **kwargs) + return decorated_function + return decorator @@ -24,5 +27,7 @@ def decorated_function(*args, **kwargs): else: request.renderer_classes = renderers return func(*args, **kwargs) + return decorated_function + return decorator diff --git a/flask_api/exceptions.py b/flask_api/exceptions.py index 27b261d..0ac0df1 100644 --- a/flask_api/exceptions.py +++ b/flask_api/exceptions.py @@ -1,10 +1,9 @@ -from __future__ import unicode_literals from flask_api import status class APIException(Exception): status_code = status.HTTP_500_INTERNAL_SERVER_ERROR - detail = '' + detail = "" def __init__(self, detail=None): if detail is not None: @@ -16,27 +15,28 @@ def __str__(self): class ParseError(APIException): status_code = status.HTTP_400_BAD_REQUEST - detail = 'Malformed request.' + detail = "Malformed request." class AuthenticationFailed(APIException): status_code = status.HTTP_401_UNAUTHORIZED - detail = 'Incorrect authentication credentials.' + detail = "Incorrect authentication credentials." class NotAuthenticated(APIException): status_code = status.HTTP_401_UNAUTHORIZED - detail = 'Authentication credentials were not provided.' + detail = "Authentication credentials were not provided." class PermissionDenied(APIException): status_code = status.HTTP_403_FORBIDDEN - detail = 'You do not have permission to perform this action.' + detail = "You do not have permission to perform this action." class NotFound(APIException): status_code = status.HTTP_404_NOT_FOUND - detail = 'This resource does not exist.' + detail = "This resource does not exist." + # class MethodNotAllowed(APIException): # status_code = status.HTTP_405_METHOD_NOT_ALLOWED @@ -48,17 +48,18 @@ class NotFound(APIException): class NotAcceptable(APIException): status_code = status.HTTP_406_NOT_ACCEPTABLE - detail = 'Could not satisfy the request Accept header.' + detail = "Could not satisfy the request Accept header." class UnsupportedMediaType(APIException): status_code = status.HTTP_415_UNSUPPORTED_MEDIA_TYPE - detail = 'Unsupported media type in the request Content-Type header.' + detail = "Unsupported media type in the request Content-Type header." class Throttled(APIException): status_code = status.HTTP_429_TOO_MANY_REQUESTS - detail = 'Request was throttled.' + detail = "Request was throttled." + # def __init__(self, wait=None, detail=None): # if wait is None: diff --git a/flask_api/mediatypes.py b/flask_api/mediatypes.py index 7b9bcb9..e159b37 100644 --- a/flask_api/mediatypes.py +++ b/flask_api/mediatypes.py @@ -1,14 +1,10 @@ -# coding: utf8 -from __future__ import unicode_literals - - -class MediaType(object): +class MediaType: def __init__(self, media_type): self.main_type, self.sub_type, self.params = self._parse(media_type) @property def full_type(self): - return self.main_type + '/' + self.sub_type + return self.main_type + "/" + self.sub_type @property def precedence(self): @@ -20,11 +16,11 @@ def precedence(self): 1. 'type/*' 0. '*/*' """ - if self.main_type == '*': + if self.main_type == "*": return 0 - elif self.sub_type == '*': + elif self.sub_type == "*": return 1 - elif not self.params or list(self.params.keys()) == ['q']: + elif not self.params or list(self.params.keys()) == ["q"]: return 2 return 3 @@ -39,13 +35,21 @@ def satisfies(self, other): '*/*' >= 'text/plain' """ for key in self.params.keys(): - if key != 'q' and other.params.get(key, None) != self.params.get(key, None): + if key != "q" and other.params.get(key, None) != self.params.get(key, None): return False - if self.sub_type != '*' and other.sub_type != '*' and other.sub_type != self.sub_type: + if ( + self.sub_type != "*" + and other.sub_type != "*" + and other.sub_type != self.sub_type + ): return False - if self.main_type != '*' and other.main_type != '*' and other.main_type != self.main_type: + if ( + self.main_type != "*" + and other.main_type != "*" + and other.main_type != self.main_type + ): return False return True @@ -55,15 +59,15 @@ def _parse(self, media_type): Parse a media type string, like "application/json; indent=4" into a three-tuple, like: ('application', 'json', {'indent': 4}) """ - full_type, sep, param_string = media_type.partition(';') + full_type, sep, param_string = media_type.partition(";") params = {} - for token in param_string.strip().split(','): - key, sep, value = [s.strip() for s in token.partition('=')] + for token in param_string.strip().split(","): + key, sep, value = [s.strip() for s in token.partition("=")] if value.startswith('"') and value.endswith('"'): value = value[1:-1] if key: params[key] = value - main_type, sep, sub_type = [s.strip() for s in full_type.partition('/')] + main_type, sep, sub_type = [s.strip() for s in full_type.partition("/")] return (main_type, sub_type, params) def __repr__(self): @@ -75,11 +79,10 @@ def __str__(self): Note that this ensures the params are sorted. """ if self.params: - params_str = ', '.join([ - '%s="%s"' % (key, val) - for key, val in sorted(self.params.items()) - ]) - return self.full_type + '; ' + params_str + params_str = ", ".join( + ['%s="%s"' % (key, val) for key, val in sorted(self.params.items())] + ) + return self.full_type + "; " + params_str return self.full_type def __hash__(self): @@ -87,9 +90,7 @@ def __hash__(self): def __eq__(self, other): # Compare two MediaType instances, ignoring parameter ordering. - return ( - self.full_type == other.full_type and self.params == other.params - ) + return self.full_type == other.full_type and self.params == other.params def parse_accept_header(accept): @@ -105,7 +106,7 @@ def parse_accept_header(accept): ] """ ret = [set(), set(), set(), set()] - for token in accept.split(','): + for token in accept.split(","): media_type = MediaType(token.strip()) ret[3 - media_type.precedence].add(media_type) return [media_types for media_types in ret if media_types] diff --git a/flask_api/negotiation.py b/flask_api/negotiation.py index 9ce0549..299d3a2 100644 --- a/flask_api/negotiation.py +++ b/flask_api/negotiation.py @@ -1,11 +1,10 @@ -# coding: utf8 -from __future__ import unicode_literals from flask import request + from flask_api import exceptions from flask_api.mediatypes import MediaType, parse_accept_header -class BaseNegotiation(object): +class BaseNegotiation: def select_parser(self, parsers): msg = '`select_parser()` method must be implemented for class "%s"' raise NotImplementedError(msg % self.__class__.__name__) @@ -36,7 +35,7 @@ def select_renderer(self, renderers): Determine which renderer to use for rendering the response body. Returns a two-tuple of (renderer, content type). """ - accept_header = request.headers.get('Accept', '*/*') + accept_header = request.headers.get("Accept", "*/*") for client_media_types in parse_accept_header(accept_header): for renderer in renderers: diff --git a/flask_api/parsers.py b/flask_api/parsers.py index 4229efa..56f45f6 100644 --- a/flask_api/parsers.py +++ b/flask_api/parsers.py @@ -1,16 +1,16 @@ -# coding: utf8 -from __future__ import unicode_literals -from flask_api import exceptions +import json + from werkzeug.formparser import MultiPartParser as WerkzeugMultiPartParser from werkzeug.formparser import default_stream_factory from werkzeug.urls import url_decode_stream -import json + +from flask_api import exceptions -class BaseParser(object): +class BaseParser: media_type = None handles_file_uploads = False # If set then 'request.files' will be populated. - handles_form_data = False # If set then 'request.form' will be populated. + handles_form_data = False # If set then 'request.form' will be populated. def parse(self, stream, media_type, **options): msg = '`parse()` method must be implemented for class "%s"' @@ -18,46 +18,50 @@ def parse(self, stream, media_type, **options): class JSONParser(BaseParser): - media_type = 'application/json' + media_type = "application/json" def parse(self, stream, media_type, **options): - data = stream.read().decode('utf-8') + data = stream.read().decode("utf-8") try: return json.loads(data) except ValueError as exc: - msg = 'JSON parse error - %s' % str(exc) + msg = "JSON parse error - %s" % str(exc) raise exceptions.ParseError(msg) class MultiPartParser(BaseParser): - media_type = 'multipart/form-data' + media_type = "multipart/form-data" handles_file_uploads = True handles_form_data = True def parse(self, stream, media_type, **options): - boundary = media_type.params.get('boundary') + boundary = media_type.params.get("boundary") if boundary is None: - msg = 'Multipart message missing boundary in Content-Type header' + msg = "Multipart message missing boundary in Content-Type header" raise exceptions.ParseError(msg) - boundary = boundary.encode('ascii') + boundary = boundary.encode("ascii") - content_length = options.get('content_length') - assert content_length is not None, 'MultiPartParser.parse() requires `content_length` argument' + content_length = options.get("content_length") + assert ( + content_length is not None + ), "MultiPartParser.parse() requires `content_length` argument" buffer_size = content_length while buffer_size % 4 or buffer_size < 1024: buffer_size += 1 - multipart_parser = WerkzeugMultiPartParser(default_stream_factory, buffer_size=buffer_size) + multipart_parser = WerkzeugMultiPartParser( + default_stream_factory, buffer_size=buffer_size + ) try: return multipart_parser.parse(stream, boundary, content_length) except ValueError as exc: - msg = 'Multipart parse error - %s' % str(exc) + msg = "Multipart parse error - %s" % str(exc) raise exceptions.ParseError(msg) class URLEncodedParser(BaseParser): - media_type = 'application/x-www-form-urlencoded' + media_type = "application/x-www-form-urlencoded" handles_form_data = True def parse(self, stream, media_type, **options): diff --git a/flask_api/renderers.py b/flask_api/renderers.py index f84b366..a9aed23 100644 --- a/flask_api/renderers.py +++ b/flask_api/renderers.py @@ -1,13 +1,12 @@ -# coding: utf8 -from __future__ import unicode_literals -from flask import request, render_template, current_app -from flask.globals import _request_ctx_stack -from flask_api.mediatypes import MediaType -from flask_api.compat import apply_markdown -import json import pydoc import re +from flask import current_app, render_template, request +from flask.globals import _request_ctx_stack + +from flask_api.compat import apply_markdown +from flask_api.mediatypes import MediaType + def dedent(content): """ @@ -18,26 +17,29 @@ def dedent(content): as it fails to dedent multiline docstrings that include unindented text on the initial line. """ - whitespace_counts = [len(line) - len(line.lstrip(' ')) - for line in content.splitlines()[1:] if line.lstrip()] + whitespace_counts = [ + len(line) - len(line.lstrip(" ")) + for line in content.splitlines()[1:] + if line.lstrip() + ] # unindent the content if needed if whitespace_counts: - whitespace_pattern = '^' + (' ' * min(whitespace_counts)) - content = re.sub(re.compile(whitespace_pattern, re.MULTILINE), '', content) + whitespace_pattern = "^" + (" " * min(whitespace_counts)) + content = re.sub(re.compile(whitespace_pattern, re.MULTILINE), "", content) return content.strip() def convert_to_title(name): - for char in ['-', '_', '.']: - name = name.replace(char, ' ') + for char in ["-", "_", "."]: + name = name.replace(char, " ") return name.capitalize() -class BaseRenderer(object): +class BaseRenderer: media_type = None - charset = 'utf-8' + charset = "utf-8" handles_empty_responses = False def render(self, data, media_type, **options): @@ -46,44 +48,47 @@ def render(self, data, media_type, **options): class JSONRenderer(BaseRenderer): - media_type = 'application/json' + media_type = "application/json" charset = None def render(self, data, media_type, **options): # Requested indentation may be set in the Accept header. try: - indent = max(min(int(media_type.params['indent']), 8), 0) + indent = max(min(int(media_type.params["indent"]), 8), 0) except (KeyError, ValueError, TypeError): indent = None # Indent may be set explicitly, eg when rendered by the browsable API. - indent = options.get('indent', indent) - return json.dumps(data, cls=current_app.json_encoder, ensure_ascii=False, indent=indent) + indent = options.get("indent", indent) + return current_app.json.dumps( + data, ensure_ascii=False, indent=indent + ) -class HTMLRenderer(object): - media_type = 'text/html' - charset = 'utf-8' +class HTMLRenderer: + media_type = "text/html" + charset = "utf-8" def render(self, data, media_type, **options): return data.encode(self.charset) class BrowsableAPIRenderer(BaseRenderer): - media_type = 'text/html' + media_type = "text/html" handles_empty_responses = True - template = 'base.html' + template = "base.html" def render(self, data, media_type, **options): # Render the content as it would have been if the client # had requested 'Accept: */*'. available_renderers = [ - renderer for renderer in request.renderer_classes + renderer + for renderer in request.renderer_classes if not issubclass(renderer, BrowsableAPIRenderer) ] - assert available_renderers, 'BrowsableAPIRenderer cannot be the only renderer' + assert available_renderers, "BrowsableAPIRenderer cannot be the only renderer" mock_renderer = available_renderers[0]() mock_media_type = MediaType(mock_renderer.media_type) - if data == '' and not mock_renderer.handles_empty_responses: + if data == "" and not mock_renderer.handles_empty_responses: mock_content = None else: text = mock_renderer.render(data, mock_media_type, indent=4) @@ -104,30 +109,26 @@ def render(self, data, media_type, **options): view_description = dedent(view_description) view_description = pydoc.html.preformat(view_description) - status = options['status'] - headers = options['headers'] - headers['Content-Type'] = str(mock_media_type) + status = options["status"] + headers = options["headers"] + headers["Content-Type"] = str(mock_media_type) from flask_api import __version__ context = { - 'status': status, - 'headers': headers, - 'content': mock_content, - 'allowed_methods': allowed_methods, - 'view_name': convert_to_title(view_name), - 'view_description': view_description, - 'version': __version__ + "status": status, + "headers": headers, + "content": mock_content, + "allowed_methods": allowed_methods, + "view_name": convert_to_title(view_name), + "view_description": view_description, + "version": __version__, } return render_template(self.template, **context) @staticmethod def _html_escape(text): - escape_table = [ - ("&", "&"), - ("<", "<"), - (">", ">") - ] + escape_table = [("&", "&"), ("<", "<"), (">", ">")] for char, replacement in escape_table: text = text.replace(char, replacement) diff --git a/flask_api/request.py b/flask_api/request.py index e75d879..33c3eaa 100644 --- a/flask_api/request.py +++ b/flask_api/request.py @@ -1,12 +1,12 @@ -# coding: utf8 -from __future__ import unicode_literals +import io + from flask import Request -from flask_api.negotiation import DefaultNegotiation -from flask_api.settings import default_settings from werkzeug.datastructures import MultiDict from werkzeug.urls import url_decode_stream from werkzeug.wsgi import get_content_length -import io + +from flask_api.negotiation import DefaultNegotiation +from flask_api.settings import default_settings class APIRequest(Request): @@ -19,19 +19,19 @@ class APIRequest(Request): @property def data(self): - if not hasattr(self, '_data'): + if not hasattr(self, "_data"): self._parse() return self._data @property def form(self): - if not hasattr(self, '_form'): + if not hasattr(self, "_form"): self._parse() return self._form @property def files(self): - if not hasattr(self, '_files'): + if not hasattr(self, "_files"): self._parse() return self._files @@ -57,7 +57,9 @@ def _parse(self): raise e from None if parser.handles_file_uploads: - assert isinstance(ret, tuple) and len(ret) == 2, 'Expected a two-tuple of (data, files)' + assert ( + isinstance(ret, tuple) and len(ret) == 2 + ), "Expected a two-tuple of (data, files)" self._data, self._files = ret else: self._data = ret @@ -69,7 +71,7 @@ def _get_parser_options(self): """ Any additional information to pass to the parser. """ - return {'content_length': self.content_length} + return {"content_length": self.content_length} def _set_empty_data(self): """ @@ -83,13 +85,13 @@ def _set_empty_data(self): @property def accepted_renderer(self): - if not hasattr(self, '_accepted_renderer'): + if not hasattr(self, "_accepted_renderer"): self._perform_content_negotiation() return self._accepted_renderer @property def accepted_media_type(self): - if not hasattr(self, '_accepted_media_type'): + if not hasattr(self, "_accepted_media_type"): self._perform_content_negotiation() return self._accepted_media_type @@ -100,13 +102,15 @@ def _perform_content_negotiation(self): """ negotiator = self.negotiator_class() renderers = [renderer() for renderer in self.renderer_classes] - self._accepted_renderer, self._accepted_media_type = negotiator.select_renderer(renderers) + self._accepted_renderer, self._accepted_media_type = negotiator.select_renderer( + renderers + ) # Method and content type overloading. @property def method(self): - if not hasattr(self, '_method'): + if not hasattr(self, "_method"): self._perform_method_overloading() return self._method @@ -116,19 +120,19 @@ def method(self, value): @property def content_type(self): - if not hasattr(self, '_content_type'): + if not hasattr(self, "_content_type"): self._perform_method_overloading() return self._content_type @property def content_length(self): - if not hasattr(self, '_content_length'): + if not hasattr(self, "_content_length"): self._perform_method_overloading() return self._content_length @property def stream(self): - if not hasattr(self, '_stream'): + if not hasattr(self, "_stream"): self._perform_method_overloading() return self._stream @@ -137,30 +141,33 @@ def _perform_method_overloading(self): Perform method and content type overloading. Provides support for browser PUT, PATCH, DELETE & other requests, - by specifing a '_method' form field. + by specifying a '_method' form field. Also provides support for browser non-form requests (eg JSON), - by specifing '_content' and '_content_type' form fields. + by specifying '_content' and '_content_type' form fields. """ - if not hasattr(self, '_method'): + if not hasattr(self, "_method"): self.method = super().method self._stream = super().stream - self._content_type = self.headers.get('Content-Type') + self._content_type = self.headers.get("Content-Type") self._content_length = get_content_length(self.environ) - if (self._method == 'POST' and self._content_type == 'application/x-www-form-urlencoded'): + if ( + self._method == "POST" + and self._content_type == "application/x-www-form-urlencoded" + ): # Read the request data, then push it back onto the stream again. body = self.get_data() data = url_decode_stream(io.BytesIO(body)) self._stream = io.BytesIO(body) - if '_method' in data: + if "_method" in data: # Support browser forms with PUT, PATCH, DELETE & other methods. - self._method = data['_method'] - if '_content' in data and '_content_type' in data: + self._method = data["_method"] + if "_content" in data and "_content_type" in data: # Support browser forms with non-form data, such as JSON. - body = data['_content'].encode('utf8') + body = data["_content"].encode("utf8") self._stream = io.BytesIO(body) - self._content_type = data['_content_type'] + self._content_type = data["_content_type"] self._content_length = len(body) # Misc... @@ -173,7 +180,7 @@ def full_path(self): """ if not self.query_string: return self.path - return self.path + '?' + self.query_string.decode() + return self.path + "?" + self.query_string.decode() # @property # def auth(self): diff --git a/flask_api/response.py b/flask_api/response.py index c545d44..701c299 100644 --- a/flask_api/response.py +++ b/flask_api/response.py @@ -1,6 +1,4 @@ -# coding: utf8 -from __future__ import unicode_literals -from flask import request, Response +from flask import Response, request class APIResponse(Response): @@ -11,9 +9,9 @@ def __init__(self, content=None, *args, **kwargs): super().__init__(None, *args, **kwargs) media_type = None - if isinstance(content, self.api_return_types) or content == '': + if isinstance(content, self.api_return_types) or content == "": renderer = request.accepted_renderer - if content != '' or renderer.handles_empty_responses: + if content != "" or renderer.handles_empty_responses: media_type = request.accepted_media_type options = self.get_renderer_options() content = renderer.render(content, media_type, **options) @@ -29,11 +27,11 @@ def __init__(self, content=None, *args, **kwargs): self.response = content if media_type is not None: - self.headers['Content-Type'] = str(media_type) + self.headers["Content-Type"] = str(media_type) def get_renderer_options(self): return { - 'status': self.status, - 'status_code': self.status_code, - 'headers': self.headers + "status": self.status, + "status_code": self.status_code, + "headers": self.headers, } diff --git a/flask_api/settings.py b/flask_api/settings.py index 85492e1..58f02d4 100644 --- a/flask_api/settings.py +++ b/flask_api/settings.py @@ -19,8 +19,8 @@ def import_from_string(val, setting_name): """ try: # Nod to tastypie's use of importlib. - parts = val.split('.') - module_path, class_name = '.'.join(parts[:-1]), parts[-1] + parts = val.split(".") + module_path, class_name = ".".join(parts[:-1]), parts[-1] module = importlib.import_module(module_path) return getattr(module, class_name) except ImportError as exc: @@ -29,28 +29,28 @@ def import_from_string(val, setting_name): raise ImportError(msg) -class APISettings(object): +class APISettings: def __init__(self, user_config=None): self.user_config = user_config or {} @property def DEFAULT_PARSERS(self): default = [ - 'flask_api.parsers.JSONParser', - 'flask_api.parsers.URLEncodedParser', - 'flask_api.parsers.MultiPartParser' + "flask_api.parsers.JSONParser", + "flask_api.parsers.URLEncodedParser", + "flask_api.parsers.MultiPartParser", ] - val = self.user_config.get('DEFAULT_PARSERS', default) - return perform_imports(val, 'DEFAULT_PARSERS') + val = self.user_config.get("DEFAULT_PARSERS", default) + return perform_imports(val, "DEFAULT_PARSERS") @property def DEFAULT_RENDERERS(self): default = [ - 'flask_api.renderers.JSONRenderer', - 'flask_api.renderers.BrowsableAPIRenderer' + "flask_api.renderers.JSONRenderer", + "flask_api.renderers.BrowsableAPIRenderer", ] - val = self.user_config.get('DEFAULT_RENDERERS', default) - return perform_imports(val, 'DEFAULT_RENDERERS') + val = self.user_config.get("DEFAULT_RENDERERS", default) + return perform_imports(val, "DEFAULT_RENDERERS") default_settings = APISettings() diff --git a/flask_api/status.py b/flask_api/status.py index 422c0ef..ffb78a0 100644 --- a/flask_api/status.py +++ b/flask_api/status.py @@ -1,4 +1,3 @@ -# coding: utf8 """ Descriptive HTTP status codes, for code readability. @@ -7,7 +6,6 @@ RFC 2616: http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html RFC 6585: http://tools.ietf.org/html/rfc6585 """ -from __future__ import unicode_literals def is_informational(code): diff --git a/flask_api/tests/__init__.py b/flask_api/tests/__init__.py index 16a018b..ec9ff18 100644 --- a/flask_api/tests/__init__.py +++ b/flask_api/tests/__init__.py @@ -1,12 +1,14 @@ # This is a fudge that allows us to easily specify test modules. # For example: # ./runtests test_parsers -# ./runtests test_rendereres.RendererTests.test_render_json +# ./runtests test_renderers.RendererTests.test_render_json import os -modules = [filename.rsplit('.', 1)[0] - for filename in os.listdir(os.path.dirname(__file__)) - if filename.endswith('.py') and filename.startswith('test_')] +modules = [ + filename.rsplit(".", 1)[0] + for filename in os.listdir(os.path.dirname(__file__)) + if filename.endswith(".py") and filename.startswith("test_") +] for module in modules: exec("from flask_api.tests.%s import *" % module) diff --git a/flask_api/tests/test_app.py b/flask_api/tests/test_app.py index 5c4bebd..47e167c 100644 --- a/flask_api/tests/test_app.py +++ b/flask_api/tests/test_app.py @@ -1,14 +1,13 @@ -# coding: utf8 -from __future__ import unicode_literals -from flask import abort, make_response, request, jsonify -from flask_api.decorators import set_renderers -from flask_api import exceptions, renderers, status, FlaskAPI import json import unittest +from flask import abort, jsonify, make_response, request + +from flask_api import FlaskAPI, exceptions, renderers, status +from flask_api.decorators import set_renderers app = FlaskAPI(__name__) -app.config['TESTING'] = True +app.config["TESTING"] = True class JSONVersion1(renderers.JSONRenderer): @@ -32,7 +31,7 @@ def __init__(self, message, status_code=None, payload=None): def to_dict(self): rv = dict(self.payload or ()) - rv['message'] = self.message + rv["message"] = self.message return rv @@ -43,149 +42,151 @@ def handle_invalid_usage(error): return response -@app.route('/set_status_and_headers/') +@app.route("/set_status_and_headers/") def set_status_and_headers(): - headers = {'Location': 'http://example.com/456'} - return {'example': 'content'}, status.HTTP_201_CREATED, headers + headers = {"Location": "http://example.com/456"} + return {"example": "content"}, status.HTTP_201_CREATED, headers -@app.route('/set_headers/') +@app.route("/set_headers/") def set_headers(): - headers = {'Location': 'http://example.com/456'} - return {'example': 'content'}, headers + headers = {"Location": "http://example.com/456"} + return {"example": "content"}, headers -@app.route('/make_response_view/') +@app.route("/make_response_view/") def make_response_view(): - response = make_response({'example': 'content'}) - response.headers['Location'] = 'http://example.com/456' + response = make_response({"example": "content"}) + response.headers["Location"] = "http://example.com/456" return response -@app.route('/none_204_response/') +@app.route("/none_204_response/") def none_204_response(): return None, status.HTTP_204_NO_CONTENT -@app.route('/none_200_response/') +@app.route("/none_200_response/") def none_200_response(): return None, status.HTTP_200_OK -@app.route('/api_exception/') +@app.route("/api_exception/") def api_exception(): raise exceptions.PermissionDenied() -@app.route('/custom_exception/') +@app.route("/custom_exception/") def custom_exception(): - raise InvalidUsage('Invalid usage test.', status_code=410) + raise InvalidUsage("Invalid usage test.", status_code=410) -@app.route('/custom_exception_no_code/') +@app.route("/custom_exception_no_code/") def custom_exception_no_status_code(): - raise InvalidUsage('Invalid usage test.') + raise InvalidUsage("Invalid usage test.") -@app.route('/abort_view/') +@app.route("/abort_view/") def abort_view(): abort(status.HTTP_403_FORBIDDEN) -@app.route('/options/') +@app.route("/options/") def options_view(): return {} -@app.route('/accepted_media_type/') +@app.route("/accepted_media_type/") @set_renderers([JSONVersion2, JSONVersion1]) def accepted_media_type(): - return {'accepted_media_type': str(request.accepted_media_type)} + return {"accepted_media_type": str(request.accepted_media_type)} class AppTests(unittest.TestCase): def test_set_status_and_headers(self): with app.test_client() as client: - response = client.get('/set_status_and_headers/') + response = client.get("/set_status_and_headers/") self.assertEqual(response.status_code, status.HTTP_201_CREATED) - self.assertEqual(response.headers['Location'], 'http://example.com/456') - self.assertEqual(response.content_type, 'application/json') + self.assertEqual(response.headers["Location"], "http://example.com/456") + self.assertEqual(response.content_type, "application/json") expected = '{"example": "content"}' - self.assertEqual(response.get_data().decode('utf8'), expected) + self.assertEqual(response.get_data().decode("utf8"), expected) def test_set_headers(self): with app.test_client() as client: - response = client.get('/set_headers/') + response = client.get("/set_headers/") self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.headers['Location'], 'http://example.com/456') - self.assertEqual(response.content_type, 'application/json') + self.assertEqual(response.headers["Location"], "http://example.com/456") + self.assertEqual(response.content_type, "application/json") expected = '{"example": "content"}' - self.assertEqual(response.get_data().decode('utf8'), expected) + self.assertEqual(response.get_data().decode("utf8"), expected) def test_make_response(self): with app.test_client() as client: - response = client.get('/make_response_view/') - self.assertEqual(response.content_type, 'application/json') - self.assertEqual(response.headers['Location'], 'http://example.com/456') - self.assertEqual(response.content_type, 'application/json') + response = client.get("/make_response_view/") + self.assertEqual(response.content_type, "application/json") + self.assertEqual(response.headers["Location"], "http://example.com/456") + self.assertEqual(response.content_type, "application/json") expected = '{"example": "content"}' - self.assertEqual(response.get_data().decode('utf8'), expected) + self.assertEqual(response.get_data().decode("utf8"), expected) def test_none_204_response(self): with app.test_client() as client: - response = client.get('/none_204_response/') + response = client.get("/none_204_response/") self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) - expected = '' - self.assertEqual(response.get_data().decode('utf8'), expected) + expected = "" + self.assertEqual(response.get_data().decode("utf8"), expected) def test_none_200_response(self): with app.test_client() as client: with self.assertRaises(ValueError): - client.get('/none_200_response/') + client.get("/none_200_response/") def test_api_exception(self): with app.test_client() as client: - response = client.get('/api_exception/') + response = client.get("/api_exception/") self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - self.assertEqual(response.content_type, 'application/json') - expected = '{"message": "You do not have permission to perform this action."}' - self.assertEqual(response.get_data().decode('utf8'), expected) + self.assertEqual(response.content_type, "application/json") + expected = ( + '{"message": "You do not have permission to perform this action."}' + ) + self.assertEqual(response.get_data().decode("utf8"), expected) def test_custom_exception(self): with app.test_client() as client: - response = client.get('/custom_exception/') + response = client.get("/custom_exception/") self.assertEqual(response.status_code, status.HTTP_410_GONE) - self.assertEqual(response.content_type, 'application/json') + self.assertEqual(response.content_type, "application/json") def test_custom_exception_default_code(self): with app.test_client() as client: - response = client.get('/custom_exception_no_code/') + response = client.get("/custom_exception_no_code/") self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual(response.content_type, 'application/json') + self.assertEqual(response.content_type, "application/json") def test_abort_view(self): with app.test_client() as client: - response = client.get('/abort_view/') + response = client.get("/abort_view/") self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) def test_options_view(self): with app.test_client() as client: - response = client.options('/options/') + response = client.options("/options/") # Errors if `response.response` is `None` response.get_data() def test_accepted_media_type_property(self): with app.test_client() as client: # Explicitly request the "api-version 1.0" renderer. - headers = {'Accept': 'application/json; api-version="1.0"'} - response = client.get('/accepted_media_type/', headers=headers) - data = json.loads(response.get_data().decode('utf8')) - expected = {'accepted_media_type': 'application/json; api-version="1.0"'} + headers = {"Accept": 'application/json; api-version="1.0"'} + response = client.get("/accepted_media_type/", headers=headers) + data = json.loads(response.get_data().decode("utf8")) + expected = {"accepted_media_type": 'application/json; api-version="1.0"'} self.assertEqual(data, expected) # Request the default renderer, which is "api-version 2.0". - headers = {'Accept': '*/*'} - response = client.get('/accepted_media_type/', headers=headers) - data = json.loads(response.get_data().decode('utf8')) - expected = {'accepted_media_type': 'application/json; api-version="2.0"'} + headers = {"Accept": "*/*"} + response = client.get("/accepted_media_type/", headers=headers) + data = json.loads(response.get_data().decode("utf8")) + expected = {"accepted_media_type": 'application/json; api-version="2.0"'} self.assertEqual(data, expected) diff --git a/flask_api/tests/test_exceptions.py b/flask_api/tests/test_exceptions.py index d334718..ad04c5c 100644 --- a/flask_api/tests/test_exceptions.py +++ b/flask_api/tests/test_exceptions.py @@ -1,13 +1,11 @@ -# coding: utf8 -from __future__ import unicode_literals -from flask_api import exceptions -from flask_api import status import unittest +from flask_api import exceptions, status + class Conflict(exceptions.APIException): status_code = status.HTTP_409_CONFLICT - detail = 'Could not update the resource' + detail = "Could not update the resource" class TestExceptions(unittest.TestCase): @@ -15,12 +13,12 @@ def test_custom_exception(self): try: raise Conflict() except Conflict as exc: - self.assertEqual(str(exc), 'Could not update the resource') + self.assertEqual(str(exc), "Could not update the resource") self.assertEqual(exc.status_code, 409) def test_override_exception_detail(self): try: - raise Conflict('A widget with this id already exists') + raise Conflict("A widget with this id already exists") except Conflict as exc: - self.assertEqual(str(exc), 'A widget with this id already exists') + self.assertEqual(str(exc), "A widget with this id already exists") self.assertEqual(exc.status_code, 409) diff --git a/flask_api/tests/test_mediatypes.py b/flask_api/tests/test_mediatypes.py index c66fafb..f40c79a 100644 --- a/flask_api/tests/test_mediatypes.py +++ b/flask_api/tests/test_mediatypes.py @@ -1,106 +1,106 @@ -# coding: utf8 -from __future__ import unicode_literals -from flask_api.mediatypes import MediaType, parse_accept_header import unittest +from flask_api.mediatypes import MediaType, parse_accept_header + class MediaTypeParsingTests(unittest.TestCase): def test_media_type_with_params(self): - media = MediaType('application/xml; schema=foobar, q=0.5') + media = MediaType("application/xml; schema=foobar, q=0.5") self.assertEqual(str(media), 'application/xml; q="0.5", schema="foobar"') - self.assertEqual(media.main_type, 'application') - self.assertEqual(media.sub_type, 'xml') - self.assertEqual(media.full_type, 'application/xml') - self.assertEqual(media.params, {'schema': 'foobar', 'q': '0.5'}) + self.assertEqual(media.main_type, "application") + self.assertEqual(media.sub_type, "xml") + self.assertEqual(media.full_type, "application/xml") + self.assertEqual(media.params, {"schema": "foobar", "q": "0.5"}) self.assertEqual(media.precedence, 3) - self.assertEqual(repr(media), '') + self.assertEqual( + repr(media), '' + ) def test_media_type_with_q_params(self): - media = MediaType('application/xml; q=0.5') + media = MediaType("application/xml; q=0.5") self.assertEqual(str(media), 'application/xml; q="0.5"') - self.assertEqual(media.main_type, 'application') - self.assertEqual(media.sub_type, 'xml') - self.assertEqual(media.full_type, 'application/xml') - self.assertEqual(media.params, {'q': '0.5'}) + self.assertEqual(media.main_type, "application") + self.assertEqual(media.sub_type, "xml") + self.assertEqual(media.full_type, "application/xml") + self.assertEqual(media.params, {"q": "0.5"}) self.assertEqual(media.precedence, 2) def test_media_type_without_params(self): - media = MediaType('application/xml') - self.assertEqual(str(media), 'application/xml') - self.assertEqual(media.main_type, 'application') - self.assertEqual(media.sub_type, 'xml') - self.assertEqual(media.full_type, 'application/xml') + media = MediaType("application/xml") + self.assertEqual(str(media), "application/xml") + self.assertEqual(media.main_type, "application") + self.assertEqual(media.sub_type, "xml") + self.assertEqual(media.full_type, "application/xml") self.assertEqual(media.params, {}) self.assertEqual(media.precedence, 2) def test_media_type_with_wildcard_sub_type(self): - media = MediaType('application/*') - self.assertEqual(str(media), 'application/*') - self.assertEqual(media.main_type, 'application') - self.assertEqual(media.sub_type, '*') - self.assertEqual(media.full_type, 'application/*') + media = MediaType("application/*") + self.assertEqual(str(media), "application/*") + self.assertEqual(media.main_type, "application") + self.assertEqual(media.sub_type, "*") + self.assertEqual(media.full_type, "application/*") self.assertEqual(media.params, {}) self.assertEqual(media.precedence, 1) def test_media_type_with_wildcard_main_type(self): - media = MediaType('*/*') - self.assertEqual(str(media), '*/*') - self.assertEqual(media.main_type, '*') - self.assertEqual(media.sub_type, '*') - self.assertEqual(media.full_type, '*/*') + media = MediaType("*/*") + self.assertEqual(str(media), "*/*") + self.assertEqual(media.main_type, "*") + self.assertEqual(media.sub_type, "*") + self.assertEqual(media.full_type, "*/*") self.assertEqual(media.params, {}) self.assertEqual(media.precedence, 0) class MediaTypeMatchingTests(unittest.TestCase): def test_media_type_includes_params(self): - media_type = MediaType('application/json') - other = MediaType('application/json; version=1.0') + media_type = MediaType("application/json") + other = MediaType("application/json; version=1.0") self.assertTrue(media_type.satisfies(other)) def test_media_type_missing_params(self): - media_type = MediaType('application/json; version=1.0') - other = MediaType('application/json') + media_type = MediaType("application/json; version=1.0") + other = MediaType("application/json") self.assertFalse(media_type.satisfies(other)) def test_media_type_matching_params(self): - media_type = MediaType('application/json; version=1.0') - other = MediaType('application/json; version=1.0') + media_type = MediaType("application/json; version=1.0") + other = MediaType("application/json; version=1.0") self.assertTrue(media_type.satisfies(other)) def test_media_type_non_matching_params(self): - media_type = MediaType('application/json; version=1.0') - other = MediaType('application/json; version=2.0') + media_type = MediaType("application/json; version=1.0") + other = MediaType("application/json; version=2.0") self.assertFalse(media_type.satisfies(other)) def test_media_type_main_type_match(self): - media_type = MediaType('image/*') - other = MediaType('image/png') + media_type = MediaType("image/*") + other = MediaType("image/png") self.assertTrue(media_type.satisfies(other)) def test_media_type_sub_type_mismatch(self): - media_type = MediaType('image/jpeg') - other = MediaType('image/png') + media_type = MediaType("image/jpeg") + other = MediaType("image/png") self.assertFalse(media_type.satisfies(other)) def test_media_type_wildcard_match(self): - media_type = MediaType('*/*') - other = MediaType('image/png') + media_type = MediaType("*/*") + other = MediaType("image/png") self.assertTrue(media_type.satisfies(other)) def test_media_type_wildcard_mismatch(self): - media_type = MediaType('image/*') - other = MediaType('audio/*') + media_type = MediaType("image/*") + other = MediaType("audio/*") self.assertFalse(media_type.satisfies(other)) class AcceptHeaderTests(unittest.TestCase): def test_parse_simple_accept_header(self): - parsed = parse_accept_header('*/*, application/json') - self.assertEqual(parsed, [ - set([MediaType('application/json')]), - set([MediaType('*/*')]) - ]) + parsed = parse_accept_header("*/*, application/json") + self.assertEqual( + parsed, [set([MediaType("application/json")]), set([MediaType("*/*")])] + ) def test_parse_complex_accept_header(self): """ @@ -110,10 +110,17 @@ def test_parse_complex_accept_header(self): Note that we disregard 'q' values when determining precedence, and instead differentiate equal values by using the server preference. """ - header = 'application/xml; schema=foo, application/json; q=0.9, application/xml, */*' + header = ( + "application/xml; schema=foo, application/json; q=0.9, application/xml, */*" + ) parsed = parse_accept_header(header) - self.assertEqual(parsed, [ - set([MediaType('application/xml; schema=foo')]), - set([MediaType('application/json; q=0.9'), MediaType('application/xml')]), - set([MediaType('*/*')]), - ]) + self.assertEqual( + parsed, + [ + set([MediaType("application/xml; schema=foo")]), + set( + [MediaType("application/json; q=0.9"), MediaType("application/xml")] + ), + set([MediaType("*/*")]), + ], + ) diff --git a/flask_api/tests/test_negotiation.py b/flask_api/tests/test_negotiation.py index aec99a6..832fc87 100644 --- a/flask_api/tests/test_negotiation.py +++ b/flask_api/tests/test_negotiation.py @@ -1,35 +1,33 @@ -# coding: utf8 -from __future__ import unicode_literals import unittest + import flask_api from flask_api import exceptions from flask_api.negotiation import BaseNegotiation, DefaultNegotiation - app = flask_api.FlaskAPI(__name__) -class JSON(object): - media_type = 'application/json' +class JSON: + media_type = "application/json" -class HTML(object): - media_type = 'application/html' +class HTML: + media_type = "application/html" -class URLEncodedForm(object): - media_type = 'application/x-www-form-urlencoded' +class URLEncodedForm: + media_type = "application/x-www-form-urlencoded" class TestRendererNegotiation(unittest.TestCase): def test_select_renderer_client_preference(self): negotiation = DefaultNegotiation() renderers = [JSON, HTML] - headers = {'Accept': 'application/html'} + headers = {"Accept": "application/html"} with app.test_request_context(headers=headers): renderer, media_type = negotiation.select_renderer(renderers) self.assertEqual(renderer, HTML) - self.assertEqual(str(media_type), 'application/html') + self.assertEqual(str(media_type), "application/html") def test_select_renderer_no_accept_header(self): negotiation = DefaultNegotiation() @@ -37,21 +35,21 @@ def test_select_renderer_no_accept_header(self): with app.test_request_context(): renderer, media_type = negotiation.select_renderer(renderers) self.assertEqual(renderer, JSON) - self.assertEqual(str(media_type), 'application/json') + self.assertEqual(str(media_type), "application/json") def test_select_renderer_server_preference(self): negotiation = DefaultNegotiation() renderers = [JSON, HTML] - headers = {'Accept': '*/*'} + headers = {"Accept": "*/*"} with app.test_request_context(headers=headers): renderer, media_type = negotiation.select_renderer(renderers) self.assertEqual(renderer, JSON) - self.assertEqual(str(media_type), 'application/json') + self.assertEqual(str(media_type), "application/json") def test_select_renderer_failed(self): negotiation = DefaultNegotiation() renderers = [JSON, HTML] - headers = {'Accept': 'application/xml'} + headers = {"Accept": "application/xml"} with app.test_request_context(headers=headers): with self.assertRaises(exceptions.NotAcceptable): renderer, media_type = negotiation.select_renderer(renderers) @@ -61,7 +59,9 @@ def test_renderer_negotiation_not_implemented(self): with self.assertRaises(NotImplementedError) as context: negotiation.select_renderer([]) msg = str(context.exception) - expected = '`select_renderer()` method must be implemented for class "BaseNegotiation"' + expected = ( + '`select_renderer()` method must be implemented for class "BaseNegotiation"' + ) self.assertEqual(msg, expected) @@ -69,16 +69,16 @@ class TestParserNegotiation(unittest.TestCase): def test_select_parser(self): negotiation = DefaultNegotiation() parsers = [JSON, URLEncodedForm] - headers = {'Content-Type': 'application/x-www-form-urlencoded'} + headers = {"Content-Type": "application/x-www-form-urlencoded"} with app.test_request_context(headers=headers): renderer, media_type = negotiation.select_parser(parsers) self.assertEqual(renderer, URLEncodedForm) - self.assertEqual(str(media_type), 'application/x-www-form-urlencoded') + self.assertEqual(str(media_type), "application/x-www-form-urlencoded") def test_select_parser_failed(self): negotiation = DefaultNegotiation() parsers = [JSON, URLEncodedForm] - headers = {'Content-Type': 'application/xml'} + headers = {"Content-Type": "application/xml"} with app.test_request_context(headers=headers): with self.assertRaises(exceptions.UnsupportedMediaType): renderer, media_type = negotiation.select_parser(parsers) @@ -88,5 +88,7 @@ def test_parser_negotiation_not_implemented(self): with self.assertRaises(NotImplementedError) as context: negotiation.select_parser([]) msg = str(context.exception) - expected = '`select_parser()` method must be implemented for class "BaseNegotiation"' + expected = ( + '`select_parser()` method must be implemented for class "BaseNegotiation"' + ) self.assertEqual(msg, expected) diff --git a/flask_api/tests/test_parsers.py b/flask_api/tests/test_parsers.py index fd15342..85307aa 100644 --- a/flask_api/tests/test_parsers.py +++ b/flask_api/tests/test_parsers.py @@ -1,25 +1,26 @@ -# coding: utf8 -from __future__ import unicode_literals -from flask import request -from flask_api import exceptions, parsers, status, mediatypes, FlaskAPI -from flask_api.decorators import set_parsers import io import json import unittest +from flask import request + +from flask_api import FlaskAPI, exceptions, mediatypes, parsers, status +from flask_api.decorators import set_parsers app = FlaskAPI(__name__) -@app.route('/', methods=['POST']) +@app.route("/", methods=["POST"]) def data(): return { - 'data': request.data, - 'form': request.form, - 'files': dict([ - (key, {'name': val.filename, 'contents': val.read().decode('utf8')}) - for key, val in request.files.items() - ]) + "data": request.data, + "form": request.form, + "files": dict( + [ + (key, {"name": val.filename, "contents": val.read().decode("utf8")}) + for key, val in request.files.items() + ] + ), } @@ -27,34 +28,34 @@ class ParserTests(unittest.TestCase): def test_valid_json(self): parser = parsers.JSONParser() stream = io.BytesIO(b'{"key": 1, "other": "two"}') - data = parser.parse(stream, 'application/json') + data = parser.parse(stream, "application/json") self.assertEqual(data, {"key": 1, "other": "two"}) def test_invalid_json(self): parser = parsers.JSONParser() stream = io.BytesIO(b'{key: 1, "other": "two"}') with self.assertRaises(exceptions.ParseError) as context: - parser.parse(stream, mediatypes.MediaType('application/json')) + parser.parse(stream, mediatypes.MediaType("application/json")) detail = str(context.exception) - expected_pypy = 'JSON parse error - Key name must be string at char: line 1 column 2 (char 1)' - expected_py3 = 'JSON parse error - Expecting property name enclosed in double quotes: line 1 column 2 (char 1)' + expected_pypy = "JSON parse error - Key name must be string at char: line 1 column 2 (char 1)" + expected_py3 = "JSON parse error - Expecting property name enclosed in double quotes: line 1 column 2 (char 1)" self.assertIn(detail, (expected_pypy, expected_py3)) def test_invalid_multipart(self): parser = parsers.MultiPartParser() - stream = io.BytesIO(b'invalid') + stream = io.BytesIO(b"invalid") media_type = mediatypes.MediaType('multipart/form-data; boundary="foo"') with self.assertRaises(exceptions.ParseError) as context: - parser.parse(stream, media_type, content_length=len('invalid')) - self.assertIn('Multipart parse error', str(context.exception)) + parser.parse(stream, media_type, content_length=len("invalid")) + self.assertIn("Multipart parse error", str(context.exception)) def test_invalid_multipart_no_boundary(self): parser = parsers.MultiPartParser() - stream = io.BytesIO(b'invalid') + stream = io.BytesIO(b"invalid") with self.assertRaises(exceptions.ParseError) as context: - parser.parse(stream, mediatypes.MediaType('multipart/form-data')) + parser.parse(stream, mediatypes.MediaType("multipart/form-data")) detail = str(context.exception) - expected = 'Multipart message missing boundary in Content-Type header' + expected = "Multipart message missing boundary in Content-Type header" self.assertEqual(detail, expected) def test_renderer_negotiation_not_implemented(self): @@ -67,43 +68,42 @@ def test_renderer_negotiation_not_implemented(self): def test_accessing_json(self): with app.test_client() as client: - data = json.dumps({'example': 'example'}) - response = client.post('/', data=data, content_type='application/json') + data = json.dumps({"example": "example"}) + response = client.post("/", data=data, content_type="application/json") self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.headers['Content-Type'], 'application/json') - data = json.loads(response.get_data().decode('utf8')) - expected = { - "data": {"example": "example"}, - "form": {}, - "files": {} - } + self.assertEqual(response.headers["Content-Type"], "application/json") + data = json.loads(response.get_data().decode("utf8")) + expected = {"data": {"example": "example"}, "form": {}, "files": {}} self.assertEqual(data, expected) def test_accessing_url_encoded(self): with app.test_client() as client: - data = {'example': 'example'} - response = client.post('/', data=data) + data = {"example": "example"} + response = client.post("/", data=data) self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.headers['Content-Type'], 'application/json') - data = json.loads(response.get_data().decode('utf8')) + self.assertEqual(response.headers["Content-Type"], "application/json") + data = json.loads(response.get_data().decode("utf8")) expected = { "data": {"example": "example"}, "form": {"example": "example"}, - "files": {} + "files": {}, } self.assertEqual(data, expected) def test_accessing_multipart(self): with app.test_client() as client: - data = {'example': 'example', 'upload': (io.BytesIO(b'file contents'), 'name.txt')} - response = client.post('/', data=data) + data = { + "example": "example", + "upload": (io.BytesIO(b"file contents"), "name.txt"), + } + response = client.post("/", data=data) self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.headers['Content-Type'], 'application/json') - data = json.loads(response.get_data().decode('utf8')) + self.assertEqual(response.headers["Content-Type"], "application/json") + data = json.loads(response.get_data().decode("utf8")) expected = { "data": {"example": "example"}, "form": {"example": "example"}, - "files": {"upload": {"name": "name.txt", "contents": "file contents"}} + "files": {"upload": {"name": "name.txt", "contents": "file contents"}}, } self.assertEqual(data, expected) @@ -111,43 +111,43 @@ def test_accessing_multipart(self): class OverrideParserSettings(unittest.TestCase): def setUp(self): class CustomParser1(parsers.BaseParser): - media_type = '*/*' + media_type = "*/*" def parse(self, stream, media_type, content_length=None): - return 'custom parser 1' + return "custom parser 1" class CustomParser2(parsers.BaseParser): - media_type = '*/*' + media_type = "*/*" def parse(self, stream, media_type, content_length=None): - return 'custom parser 2' + return "custom parser 2" app = FlaskAPI(__name__) - app.config['DEFAULT_PARSERS'] = [CustomParser1] + app.config["DEFAULT_PARSERS"] = [CustomParser1] - @app.route('/custom_parser_1/', methods=['POST']) + @app.route("/custom_parser_1/", methods=["POST"]) def custom_parser_1(): - return {'data': request.data} + return {"data": request.data} - @app.route('/custom_parser_2/', methods=['POST']) + @app.route("/custom_parser_2/", methods=["POST"]) @set_parsers([CustomParser2]) def custom_parser_2(): - return {'data': request.data} + return {"data": request.data} - @app.route('/custom_parser_2_as_args/', methods=['POST']) + @app.route("/custom_parser_2_as_args/", methods=["POST"]) @set_parsers(CustomParser2, CustomParser1) def custom_parser_2_as_args(): - return {'data': request.data} + return {"data": request.data} self.app = app def test_overridden_parsers_with_settings(self): with self.app.test_client() as client: - data = {'example': 'example'} - response = client.post('/custom_parser_1/', data=data) + data = {"example": "example"} + response = client.post("/custom_parser_1/", data=data) self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.headers['Content-Type'], 'application/json') - data = json.loads(response.get_data().decode('utf8')) + self.assertEqual(response.headers["Content-Type"], "application/json") + data = json.loads(response.get_data().decode("utf8")) expected = { "data": "custom parser 1", } @@ -155,11 +155,11 @@ def test_overridden_parsers_with_settings(self): def test_overridden_parsers_with_decorator(self): with self.app.test_client() as client: - data = {'example': 'example'} - response = client.post('/custom_parser_2/', data=data) + data = {"example": "example"} + response = client.post("/custom_parser_2/", data=data) self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.headers['Content-Type'], 'application/json') - data = json.loads(response.get_data().decode('utf8')) + self.assertEqual(response.headers["Content-Type"], "application/json") + data = json.loads(response.get_data().decode("utf8")) expected = { "data": "custom parser 2", } @@ -167,11 +167,11 @@ def test_overridden_parsers_with_decorator(self): def test_overridden_parsers_with_decorator_as_args(self): with self.app.test_client() as client: - data = {'example': 'example'} - response = client.post('/custom_parser_2_as_args/', data=data) + data = {"example": "example"} + response = client.post("/custom_parser_2_as_args/", data=data) self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.headers['Content-Type'], 'application/json') - data = json.loads(response.get_data().decode('utf8')) + self.assertEqual(response.headers["Content-Type"], "application/json") + data = json.loads(response.get_data().decode("utf8")) expected = { "data": "custom parser 2", } diff --git a/flask_api/tests/test_renderers.py b/flask_api/tests/test_renderers.py index ca9a7e0..549a3de 100644 --- a/flask_api/tests/test_renderers.py +++ b/flask_api/tests/test_renderers.py @@ -1,18 +1,18 @@ -# coding: utf8 -from __future__ import unicode_literals +import unittest from datetime import datetime -from flask.json import JSONEncoder -from flask_api import renderers, status, FlaskAPI + +from flask.json.provider import DefaultJSONProvider + +from flask_api import FlaskAPI, renderers, status from flask_api.decorators import set_renderers from flask_api.mediatypes import MediaType -import unittest class RendererTests(unittest.TestCase): def _make_app(self): app = FlaskAPI(__name__) - @app.route('/_love', methods=['GET']) + @app.route("/_love", methods=["GET"]) def love(): return {"test": "I <3 Python"} @@ -22,7 +22,9 @@ def test_render_json(self): app = self._make_app() renderer = renderers.JSONRenderer() with app.app_context(): - content = renderer.render({'example': 'example'}, MediaType('application/json')) + content = renderer.render( + {"example": "example"}, MediaType("application/json") + ) expected = '{"example": "example"}' self.assertEqual(content, expected) @@ -30,73 +32,75 @@ def test_render_json_with_indent(self): app = self._make_app() renderer = renderers.JSONRenderer() with app.app_context(): - content = renderer.render({'example': 'example'}, MediaType('application/json; indent=4')) + content = renderer.render( + {"example": "example"}, MediaType("application/json; indent=4") + ) expected = '{\n "example": "example"\n}' self.assertEqual(content, expected) def test_render_json_with_custom_encoder(self): - class CustomJsonEncoder(JSONEncoder): + class CustomJsonProvider(DefaultJSONProvider): def default(self, o): if isinstance(o, datetime): return o.isoformat() return super().default(o) app = self._make_app() - app.json_encoder = CustomJsonEncoder + app.json = CustomJsonProvider(app) renderer = renderers.JSONRenderer() date = datetime(2017, 10, 5, 15, 22) with app.app_context(): - content = renderer.render(date, MediaType('application/json')) + content = renderer.render(date, MediaType("application/json")) self.assertEqual(content, '"{}"'.format(date.isoformat())) def test_render_browsable_encoding(self): app = FlaskAPI(__name__) - @app.route('/_love', methods=['GET']) + @app.route("/_love", methods=["GET"]) def love(): return {"test": "I <3 Python"} with app.test_client() as client: - response = client.get('/_love', headers={"Accept": "text/html"}) + response = client.get("/_love", headers={"Accept": "text/html"}) html = str(response.get_data()) - self.assertTrue('I <3 Python' in html) - self.assertTrue('

Love

' in html) - self.assertTrue('/_love' in html) + self.assertTrue("I <3 Python" in html) + self.assertTrue("

Love

" in html) + self.assertTrue("/_love" in html) def test_render_browsable_encoding_with_markdown(self): app = FlaskAPI(__name__) - @app.route('/_foo', methods=['GET']) + @app.route("/_foo", methods=["GET"]) def foo(): """Bar: - - `qux` + - `qux` """ return {"test": "I <3 Python"} with app.test_client() as client: - response = client.get('/_foo', headers={"Accept": "text/html"}) + response = client.get("/_foo", headers={"Accept": "text/html"}) html = str(response.get_data()) print(html) - self.assertTrue('

Foo

' in html) - self.assertTrue('

Bar:' in html) - self.assertTrue('qux' in html) + self.assertTrue("

Foo

" in html) + self.assertTrue("

Bar:" in html) + self.assertTrue("qux" in html) def test_render_browsable_linking(self): app = FlaskAPI(__name__) - @app.route('/_happiness', methods=['GET']) + @app.route("/_happiness", methods=["GET"]) def happiness(): - return {"url": "http://example.org", - "a tag": "
"} + return {"url": "http://example.org", "a tag": "
"} with app.test_client() as client: - response = client.get('/_happiness', - headers={"Accept": "text/html"}) + response = client.get("/_happiness", headers={"Accept": "text/html"}) html = str(response.get_data()) - self.assertTrue('http://example.org' in html) - self.assertTrue('<br />'in html) - self.assertTrue('

Happiness

' in html) - self.assertTrue('/_happiness' in html) + self.assertTrue( + 'http://example.org' in html + ) + self.assertTrue("<br />" in html) + self.assertTrue("

Happiness

" in html) + self.assertTrue("/_happiness" in html) def test_renderer_negotiation_not_implemented(self): renderer = renderers.BaseRenderer() @@ -110,59 +114,59 @@ def test_renderer_negotiation_not_implemented(self): class OverrideParserSettings(unittest.TestCase): def setUp(self): class CustomRenderer1(renderers.BaseRenderer): - media_type = 'application/example1' + media_type = "application/example1" def render(self, data, media_type, **options): - return 'custom renderer 1' + return "custom renderer 1" class CustomRenderer2(renderers.BaseRenderer): - media_type = 'application/example2' + media_type = "application/example2" def render(self, data, media_type, **options): - return 'custom renderer 2' + return "custom renderer 2" app = FlaskAPI(__name__) - app.config['DEFAULT_RENDERERS'] = [CustomRenderer1] - app.config['PROPAGATE_EXCEPTIONS'] = True + app.config["DEFAULT_RENDERERS"] = [CustomRenderer1] + app.config["PROPAGATE_EXCEPTIONS"] = True - @app.route('/custom_renderer_1/', methods=['GET']) + @app.route("/custom_renderer_1/", methods=["GET"]) def custom_renderer_1(): - return {'data': 'example'} + return {"data": "example"} - @app.route('/custom_renderer_2/', methods=['GET']) + @app.route("/custom_renderer_2/", methods=["GET"]) @set_renderers([CustomRenderer2]) def custom_renderer_2(): - return {'data': 'example'} + return {"data": "example"} - @app.route('/custom_renderer_2_as_args/', methods=['GET']) + @app.route("/custom_renderer_2_as_args/", methods=["GET"]) @set_renderers(CustomRenderer2) def custom_renderer_2_as_args(): - return {'data': 'example'} + return {"data": "example"} self.app = app def test_overridden_parsers_with_settings(self): with self.app.test_client() as client: - response = client.get('/custom_renderer_1/') + response = client.get("/custom_renderer_1/") self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.headers['Content-Type'], 'application/example1') - data = response.get_data().decode('utf8') + self.assertEqual(response.headers["Content-Type"], "application/example1") + data = response.get_data().decode("utf8") self.assertEqual(data, "custom renderer 1") def test_overridden_parsers_with_decorator(self): with self.app.test_client() as client: - data = {'example': 'example'} - response = client.get('/custom_renderer_2/', data=data) + data = {"example": "example"} + response = client.get("/custom_renderer_2/", data=data) self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.headers['Content-Type'], 'application/example2') - data = response.get_data().decode('utf8') + self.assertEqual(response.headers["Content-Type"], "application/example2") + data = response.get_data().decode("utf8") self.assertEqual(data, "custom renderer 2") def test_overridden_parsers_with_decorator_as_args(self): with self.app.test_client() as client: - data = {'example': 'example'} - response = client.get('/custom_renderer_2_as_args/', data=data) + data = {"example": "example"} + response = client.get("/custom_renderer_2_as_args/", data=data) self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.headers['Content-Type'], 'application/example2') - data = response.get_data().decode('utf8') + self.assertEqual(response.headers["Content-Type"], "application/example2") + data = response.get_data().decode("utf8") self.assertEqual(data, "custom renderer 2") diff --git a/flask_api/tests/test_request.py b/flask_api/tests/test_request.py index f033bb4..900fa9d 100644 --- a/flask_api/tests/test_request.py +++ b/flask_api/tests/test_request.py @@ -1,29 +1,29 @@ -# coding: utf8 -from __future__ import unicode_literals -from flask import request -from flask_api import exceptions -import flask_api import io import unittest +from flask import request + +import flask_api +from flask_api import exceptions + app = flask_api.FlaskAPI(__name__) class MediaTypeParsingTests(unittest.TestCase): def test_json_request(self): kwargs = { - 'method': 'PUT', - 'input_stream': io.BytesIO(b'{"key": 1, "other": "two"}'), - 'content_type': 'application/json' + "method": "PUT", + "input_stream": io.BytesIO(b'{"key": 1, "other": "two"}'), + "content_type": "application/json", } with app.test_request_context(**kwargs): self.assertEqual(request.data, {"key": 1, "other": "two"}) def test_invalid_content_type_request(self): kwargs = { - 'method': 'PUT', - 'input_stream': io.BytesIO(b'Cannot parse this content type.'), - 'content_type': 'text/plain' + "method": "PUT", + "input_stream": io.BytesIO(b"Cannot parse this content type."), + "content_type": "text/plain", } with app.test_request_context(**kwargs): with self.assertRaises(exceptions.UnsupportedMediaType): @@ -34,18 +34,18 @@ def test_no_content_request(self): Ensure that requests with no data do not populate the `.data`, `.form` or `.files` attributes. """ - with app.test_request_context(method='PUT'): + with app.test_request_context(method="PUT"): self.assertFalse(request.data) - with app.test_request_context(method='PUT'): + with app.test_request_context(method="PUT"): self.assertFalse(request.form) - with app.test_request_context(method='PUT'): + with app.test_request_context(method="PUT"): self.assertFalse(request.files) def test_encode_request(self): """ Ensure that `.full_path` is correctly decoded in python 3 """ - with app.test_request_context(method='GET', path='/?a=b'): - self.assertEqual(request.full_path, '/?a=b') + with app.test_request_context(method="GET", path="/?a=b"): + self.assertEqual(request.full_path, "/?a=b") diff --git a/flask_api/tests/test_settings.py b/flask_api/tests/test_settings.py index c5cf824..94b8184 100644 --- a/flask_api/tests/test_settings.py +++ b/flask_api/tests/test_settings.py @@ -1,12 +1,11 @@ -# coding: utf8 -from __future__ import unicode_literals -from flask_api.settings import APISettings import unittest +from flask_api.settings import APISettings + class SettingsTests(unittest.TestCase): def test_bad_import(self): - settings = APISettings({'DEFAULT_PARSERS': 'foobarz.FailedImport'}) + settings = APISettings({"DEFAULT_PARSERS": "foobarz.FailedImport"}) with self.assertRaises(ImportError) as context: settings.DEFAULT_PARSERS msg = str(context.exception) diff --git a/flask_api/tests/test_status.py b/flask_api/tests/test_status.py index f6a9c24..7e3667b 100644 --- a/flask_api/tests/test_status.py +++ b/flask_api/tests/test_status.py @@ -1,8 +1,7 @@ -# coding: utf8 -from __future__ import unicode_literals -from flask_api import status import unittest +from flask_api import status + class TestStatus(unittest.TestCase): def test_status_categories(self): diff --git a/scent.py b/scent.py index 31b9083..fb3aba1 100644 --- a/scent.py +++ b/scent.py @@ -1,11 +1,11 @@ -# -*- coding: utf-8 -*- """Configuration file for sniffer.""" # pylint: disable=superfluous-parens,bad-continuation -import time import subprocess +import time + +from sniffer.api import file_validator, runnable, select_runnable -from sniffer.api import select_runnable, file_validator, runnable try: from pync import Notifier except ImportError: @@ -17,34 +17,34 @@ watch_paths = ["flask_api"] -class Options(object): +class Options: group = int(time.time()) # unique per run show_coverage = False rerun_args = None targets = [ - (('make', 'test'), "Run Tests", True), - (('make', 'check'), "Static Analysis", True), - (('make', 'docs'), None, True), + (("make", "test"), "Run Tests", True), + (("make", "check"), "Static Analysis", True), + (("make", "docs"), None, True), ] -@select_runnable('run_targets') +@select_runnable("run_targets") @file_validator def python_files(filename): - return filename.endswith('.py') + return filename.endswith(".py") -@select_runnable('run_targets') +@select_runnable("run_targets") @file_validator def html_files(filename): - return filename.split('.')[-1] in ['html', 'css', 'js'] + return filename.split(".")[-1] in ["html", "css", "js"] @runnable def run_targets(*args): """Run targets for Python.""" - Options.show_coverage = 'coverage' in args + Options.show_coverage = "coverage" in args count = 0 for count, (command, title, retry) in enumerate(Options.targets, start=1): @@ -74,7 +74,7 @@ def call(command, title, retry): return False print("") - print("$ %s" % ' '.join(command)) + print("$ %s" % " ".join(command)) failure = subprocess.call(command) if failure and retry: @@ -92,6 +92,6 @@ def show_notification(message, title): def show_coverage(): """Launch the coverage report.""" if Options.show_coverage: - subprocess.call(['make', 'read-coverage']) + subprocess.call(["make", "read-coverage"]) Options.show_coverage = False diff --git a/setup.py b/setup.py index a7e54d7..6039298 100755 --- a/setup.py +++ b/setup.py @@ -1,21 +1,19 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- +#!/usr/bin/env python3 -from __future__ import print_function -from setuptools import setup -import re import os +import re import sys +from setuptools import setup -name = 'Flask-API' -package = 'flask_api' -description = 'Browsable web APIs for Flask.' -url = 'https://flask-api.github.io/flask-api/' -author = 'Tom Christie' -author_email = 'tom@tomchristie.com' -license = 'BSD' -install_requires = ['Flask >= 2.0'] +name = "Flask-API" +package = "flask_api" +description = "Browsable web APIs for Flask." +url = "https://flask-api.github.io/flask-api/" +author = "Tom Christie" +author_email = "tom@tomchristie.com" +license = "BSD" +install_requires = ["Flask >= 2.0.0"] long_description = """Browsable web APIs for Flask.""" @@ -24,17 +22,21 @@ def get_version(package): """ Return package version as listed in `__version__` in `init.py`. """ - init_py = open(os.path.join(package, '__init__.py')).read() - return re.search("^__version__ = ['\"]([^'\"]+)['\"]", init_py, re.MULTILINE).group(1) + init_py = open(os.path.join(package, "__init__.py")).read() + return re.search("^__version__ = ['\"]([^'\"]+)['\"]", init_py, re.MULTILINE).group( + 1 + ) def get_packages(package): """ Return root package and all sub-packages. """ - return [dirpath - for dirpath, dirnames, filenames in os.walk(package) - if os.path.exists(os.path.join(dirpath, '__init__.py'))] + return [ + dirpath + for dirpath, dirnames, filenames in os.walk(package) + if os.path.exists(os.path.join(dirpath, "__init__.py")) + ] def get_package_data(package): @@ -42,16 +44,18 @@ def get_package_data(package): Return all files under the root package, that are not in a package themselves. """ - walk = [(dirpath.replace(package + os.sep, '', 1), filenames) - for dirpath, dirnames, filenames in os.walk(package) - if not os.path.exists(os.path.join(dirpath, '__init__.py'))] + walk = [ + (dirpath.replace(package + os.sep, "", 1), filenames) + for dirpath, dirnames, filenames in os.walk(package) + if not os.path.exists(os.path.join(dirpath, "__init__.py")) + ] filepaths = [] for base, filenames in walk: - filepaths.extend([os.path.join(base, filename) - for filename in filenames]) + filepaths.extend([os.path.join(base, filename) for filename in filenames]) return {package: filepaths} + setup( name=name, version=get_version(package), @@ -65,19 +69,19 @@ def get_package_data(package): package_data=get_package_data(package), install_requires=install_requires, classifiers=[ - 'Development Status :: 5 - Production/Stable', - 'Environment :: Web Environment', - 'Framework :: Flask', - 'Intended Audience :: Developers', - 'License :: OSI Approved :: BSD License', - 'Natural Language :: English', - 'Operating System :: OS Independent', - 'Programming Language :: Python', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.6', - 'Programming Language :: Python :: 3.7', - 'Programming Language :: Python :: 3.8', - 'Programming Language :: Python :: 3.9', - 'Topic :: Internet :: WWW/HTTP', - ] + "Development Status :: 5 - Production/Stable", + "Environment :: Web Environment", + "Framework :: Flask", + "Intended Audience :: Developers", + "License :: OSI Approved :: BSD License", + "Natural Language :: English", + "Operating System :: OS Independent", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Topic :: Internet :: WWW/HTTP", + ], )