diff --git a/.gitignore b/.gitignore index 62c63c8..edcf3b4 100644 --- a/.gitignore +++ b/.gitignore @@ -30,7 +30,6 @@ Icon* *.gdraw # Testing and coverage results -/.pytest/ /.coverage /.coverage.* /htmlcov/ diff --git a/.scrutinizer.yml b/.scrutinizer.yml index 7796be8..77822ca 100644 --- a/.scrutinizer.yml +++ b/.scrutinizer.yml @@ -1,3 +1,10 @@ +build: + nodes: + analysis: + tests: + override: + - pylint-run --rcfile=.pylint.ini + - py-scrutinizer-run checks: python: code_rating: true @@ -5,4 +12,4 @@ checks: filter: excluded_paths: - "*/tests/*" - - "*.min.js" + - "*/static/js/*min.js" diff --git a/.travis.yml b/.travis.yml index c8ef0ec..6fbac08 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,27 +1,31 @@ -language: python +dist: xenial +language: python python: - - 2.7 - - 3.3 - - 3.4 - - 3.5 - 3.6 + - 3.7 + - 3.8 + +cache: + pip: true + directories: + - .venv env: global: - - RANDOM_SEED=0 - - PIPENV_NOSPIN=true + - RANDOM_SEED=0 matrix: - - FLASK_VERSION=0.10.2 - - FLASK_VERSION=0.11.2 - - FLASK_VERSION=0.12.2 + - FLASK_VERSION=2.3.2 before_install: - - pip install pipenv~=5.0 + - pip install pipenv install: - make install +before_script: + - pipenv run pip install flask==${FLASK_VERSION} + script: - make check - make test diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9086992..d66699e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -41,7 +41,7 @@ $ make watch Build the documentation: ```sh -$ make doc +$ make docs ``` ### Static Analysis diff --git a/Makefile b/Makefile index 80411ad..d89b3e6 100644 --- a/Makefile +++ b/Makefile @@ -8,51 +8,12 @@ PACKAGES := $(PACKAGE) tests CONFIG := $(wildcard *.py) MODULES := $(wildcard $(PACKAGE)/*.py) -# Python settings -PYTHON_MAJOR ?= 2 -PYTHON_MINOR ?= 7 - -# System paths -PLATFORM := $(shell python -c 'import sys; print(sys.platform)') -ifneq ($(findstring win32, $(PLATFORM)), ) - WINDOWS := true - SYS_PYTHON_DIR := C:\\Python$(PYTHON_MAJOR)$(PYTHON_MINOR) - SYS_PYTHON := $(SYS_PYTHON_DIR)\\python.exe - # https://bugs.launchpad.net/virtualenv/+bug/449537 - export TCL_LIBRARY=$(SYS_PYTHON_DIR)\\tcl\\tcl8.5 -else - ifneq ($(findstring darwin, $(PLATFORM)), ) - MAC := true - else - LINUX := true - endif - SYS_PYTHON := python$(PYTHON_MAJOR) - ifdef PYTHON_MINOR - SYS_PYTHON := $(SYS_PYTHON).$(PYTHON_MINOR) - endif -endif - # Virtual environment paths -ENV := .venv -ifneq ($(findstring win32, $(PLATFORM)), ) - BIN := $(ENV)/Scripts - ACTIVATE := $(BIN)/activate.bat - OPEN := cmd /c start - PYTHON := $(BIN)/python.exe - PIP := $(BIN)/pip.exe -else - BIN := $(ENV)/bin - ACTIVATE := . $(BIN)/activate - ifneq ($(findstring cygwin, $(PLATFORM)), ) - OPEN := cygstart - else - OPEN := open - endif - PYTHON := $(BIN)/python - PIP := $(BIN)/pip -endif +export PIPENV_VENV_IN_PROJECT=true +export PIPENV_IGNORE_VIRTUALENVS=true +VENV := .venv -# MAIN TASKS ################################################################### +# MAIN TASKS ################################################################## SNIFFER := pipenv run sniffer @@ -68,40 +29,21 @@ watch: install .clean-test ## Continuously run all CI tasks when files chanage .PHONY: run ## Start the program run: install - $(PYTHON) $(PACKAGE)/__main__.py + pipenv run python $(PACKAGE)/__main__.py -# PROJECT DEPENDENCIES ######################################################### +# PROJECT DEPENDENCIES ######################################################## -export PIPENV_SHELL_COMPAT=true -export PIPENV_VENV_IN_PROJECT=true -export PIPENV_IGNORE_VIRTUALENVS=true - -DEPENDENCIES := $(ENV)/.installed -METADATA := *.egg-info +DEPENDENCIES := $(VENV)/.installed .PHONY: install -install: $(DEPENDENCIES) $(METADATA) +install: $(DEPENDENCIES) -$(DEPENDENCIES): $(PIP) Pipfile* +$(DEPENDENCIES): Pipfile* setup.py + pipenv run python setup.py develop pipenv install --dev -ifdef WINDOWS - @ echo "Manually install pywin32: https://sourceforge.net/projects/pywin32/files/pywin32" -else ifdef MAC - $(PIP) install pync MacFSEvents -else ifdef LINUX - $(PIP) install pyinotify -endif - @ touch $@ - -$(METADATA): $(PYTHON) setup.py - $(PYTHON) setup.py develop @ touch $@ -$(PYTHON) $(PIP): - pipenv --python=$(SYS_PYTHON) - pipenv run pip --version - -# CHECKS ####################################################################### +# CHECKS ###################################################################### FLAKE8 := pipenv run flake8 @@ -112,11 +54,11 @@ check: flake8 ## Run linters and static analysis flake8: install $(FLAKE8) flask_api --ignore=E128,E501 --exclude=__init__.py -# TESTS ######################################################################## +# TESTS ####################################################################### NOSE := pipenv run nosetests COVERAGE := pipenv run coverage -COVERAGE_SPACE := pipenv run coverage.space +COVERAGESPACE := pipenv run coveragespace RANDOM_SEED ?= $(shell date +%s) @@ -128,20 +70,20 @@ endif .PHONY: test test: install ## Run unit and integration tests $(NOSE) $(PACKAGE) $(NOSE_OPTIONS) - $(COVERAGE_SPACE) $(REPOSITORY) overall + $(COVERAGESPACE) update overall .PHONY: read-coverage read-coverage: - $(OPEN) coverage/index.html + open htmlcov/index.html -# DOCUMENTATION ################################################################ +# DOCUMENTATION ############################################################### MKDOCS := pipenv run mkdocs MKDOCS_INDEX := site/index.html -.PHONY: doc -doc: mkdocs ## Generate documentation +.PHONY: docs +docs: mkdocs ## Generate documentation .PHONY: mkdocs mkdocs: install $(MKDOCS_INDEX) @@ -153,7 +95,7 @@ mkdocs-live: mkdocs eval "sleep 3; open http://127.0.0.1:8000" & $(MKDOCS) serve -# BUILD ######################################################################## +# BUILD ####################################################################### DIST_FILES := dist/*.tar.gz dist/*.whl @@ -161,75 +103,51 @@ DIST_FILES := dist/*.tar.gz dist/*.whl dist: install $(DIST_FILES) $(DIST_FILES): $(MODULES) rm -f $(DIST_FILES) - $(PYTHON) setup.py check --restructuredtext --strict --metadata - $(PYTHON) setup.py sdist - $(PYTHON) setup.py bdist_wheel + pipenv run python setup.py check --restructuredtext --strict --metadata + pipenv run python setup.py sdist + pipenv run python setup.py bdist_wheel # RELEASE ###################################################################### TWINE := pipenv run twine -.PHONY: register -register: dist ## Register the project on PyPI - @ echo NOTE: your project must be registered manually - @ echo https://github.com/pypa/python-packaging-user-guide/issues/263 - # TODO: switch to twine when the above issue is resolved - # $(TWINE) register dist/*.whl - .PHONY: upload -upload: .git-no-changes register ## Upload the current version to PyPI +upload: dist ## Upload the current version to PyPI + git diff --name-only --exit-code $(TWINE) upload dist/*.* - $(OPEN) https://pypi.python.org/pypi/$(PROJECT) - -.PHONY: .git-no-changes -.git-no-changes: - @ if git diff --name-only --exit-code; \ - then \ - echo Git working copy is clean...; \ - else \ - echo ERROR: Git working copy is dirty!; \ - echo Commit your changes and try again.; \ - exit -1; \ - fi; + open https://pypi.org/project/$(PROJECT) -# CLEANUP ###################################################################### +# CLEANUP ##################################################################### .PHONY: clean -clean: .clean-dist .clean-test .clean-doc .clean-build ## Delete all generated and temporary files +clean: .clean-build .clean-docs .clean-test .clean-install ## Delete all generated and temporary files .PHONY: clean-all -clean-all: clean .clean-env .clean-workspace +clean-all: clean + rm -rf $(VENV) -.PHONY: .clean-build -.clean-build: +.PHONY: .clean-install +.clean-install: find $(PACKAGES) -name '*.pyc' -delete find $(PACKAGES) -name '__pycache__' -delete rm -rf *.egg-info -.PHONY: .clean-doc -.clean-doc: - rm -rf README.rst docs/apidocs *.html docs/*.png site - .PHONY: .clean-test .clean-test: rm -rf .cache .pytest .coverage htmlcov xmlreport -.PHONY: .clean-dist -.clean-dist: - rm -rf *.spec dist build - -.PHONY: .clean-env -.clean-env: clean - rm -rf $(ENV) +.PHONY: .clean-docs +.clean-docs: + rm -rf *.rst docs/apidocs *.html docs/*.png site -.PHONY: .clean-workspace -.clean-workspace: - rm -rf *.sublime-workspace +.PHONY: .clean-build +.clean-build: + rm -rf *.spec dist build -# HELP ######################################################################### +# HELP ######################################################################## .PHONY: help help: all - @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' + @ grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' .DEFAULT_GOAL := help diff --git a/Pipfile b/Pipfile index abb79ad..69c54e4 100644 --- a/Pipfile +++ b/Pipfile @@ -1,17 +1,33 @@ [[source]] -url = "https://pypi.python.org/simple" + +url = "https://pypi.org/simple" verify_ssl = true +name = "pypi" [packages] -Flask = "~=0.10.1" -Markdown = "*" + +Flask = "*" +Markdown = "<3" [dev-packages] + +# Linters +flake8 = "~=3.7.9" + +# Testing nose = "*" -coverage = "~=3.7.1" -"coverage.space" = "*" -flake8 = "~=2.1" -mkdocs = "==0.12" + +# Reports +coveragespace = "~=4.1" + +# Documentation +mkdocs = "~=1.2.3" docutils = "*" -sniffer = "*" + +# Release twine = "*" + +# Tooling +sniffer = "*" +pync = { version = "<2.0", sys_platform = "== 'darwin'" } +MacFSEvents = { version = "*", sys_platform = "== 'darwin'" } diff --git a/Pipfile.lock b/Pipfile.lock index d5151f7..3db0df7 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,136 +1,876 @@ { "_meta": { "hash": { - "sha256": "77d9453222dda519832b3e9753aa60167a725fad64a3e69f11373e375ef71c23" + "sha256": "662759c6b1e37a84f88b13fc4053a0d627d21897ff7f89cae656649393e94549" }, + "pipfile-spec": 6, "requires": {}, "sources": [ { - "url": "https://pypi.python.org/simple", + "name": "pypi", + "url": "https://pypi.org/simple", "verify_ssl": true } ] }, "default": { + "blinker": { + "hashes": [ + "sha256:4afd3de66ef3a9f8067559fb7a1cbe555c17dcbe15971b05d1b625c3e7abe213", + "sha256:c3d739772abb7bc2860abf5f2ec284223d9ad5c76da018234f6f50d6f31ab1f0" + ], + "markers": "python_version >= '3.7'", + "version": "==1.6.2" + }, + "click": { + "hashes": [ + "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e", + "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48" + ], + "markers": "python_version >= '3.7'", + "version": "==8.1.3" + }, "flask": { - "version": "==0.10.1" + "hashes": [ + "sha256:77fd4e1249d8c9923de34907236b747ced06e5467ecac1a7bb7115ae0e9670b0", + "sha256:8c2f9abd47a9e8df7f0c3f091ce9497d011dc3b31effcf4c85a6e2b50f4114ef" + ], + "index": "pypi", + "version": "==2.3.2" }, "itsdangerous": { - "version": "==0.24" + "hashes": [ + "sha256:2c2349112351b88699d8d4b6b075022c0808887cb7ad10069318a8b0bc88db44", + "sha256:5dbbc68b317e5e42f327f9021763545dc3fc3bfe22e6deb96aaf1fc38874156a" + ], + "markers": "python_version >= '3.7'", + "version": "==2.1.2" }, "jinja2": { - "version": "==2.9.6" + "hashes": [ + "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852", + "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61" + ], + "markers": "python_version >= '3.7'", + "version": "==3.1.2" }, "markdown": { - "version": "==2.6.9" + "hashes": [ + "sha256:9ba587db9daee7ec761cfc656272be6aabe2ed300fece21208e4aab2e457bc8f", + "sha256:a856869c7ff079ad84a3e19cd87a64998350c2b94e9e08e44270faef33400f81" + ], + "index": "pypi", + "version": "==2.6.11" }, "markupsafe": { - "version": "==1.0" + "hashes": [ + "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": { - "version": "==0.12.2" + "hashes": [ + "sha256:1d5a58e0377d1fe39d061a5de4469e414e78ccb1e1e59c0f5ad6fa1c36c52b76", + "sha256:48e5e61472fee0ddee27ebad085614ebedb7af41e88f687aaf881afb723a162f" + ], + "markers": "python_version >= '3.8'", + "version": "==2.3.4" } }, "develop": { - "backports-abc": { - "version": "==0.5" - }, - "backports.shutil-get-terminal-size": { - "version": "==1.0.0" + "bleach": { + "hashes": [ + "sha256:1a1a85c1595e07d8db14c5f09f09e6433502c51c595970edc090551f0db99414", + "sha256:33c16e3353dbd13028ab4799a0f89a83f113405c766e9c122df8a06f5b85b3f4" + ], + "markers": "python_version >= '3.7'", + "version": "==6.0.0" }, "certifi": { - "version": "==2017.7.27.1" - }, - "chardet": { - "version": "==3.0.4" + "hashes": [ + "sha256:0f0d56dc5a6ad56fd4ba36484d6cc34451e1c6548c61daad8c320169f91eddc7", + "sha256:c6c2e98f5c7869efca1f8916fed228dd91539f9f1b444c314c06eef02980c716" + ], + "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:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e", + "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48" + ], + "markers": "python_version >= '3.7'", + "version": "==8.1.3" }, "colorama": { - "version": "==0.3.9" + "hashes": [ + "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", + "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6" + ], + "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": { - "version": "==4.4.1" - }, - "coverage.space": { - "version": "==0.8" + "hashes": [ + "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": [ + "sha256:6dcdee802be5cdaa9820538203ad0e182a1ea56c679cdf65b36d4b85937d2e38", + "sha256:a59fa4227166406f74c0fd89ad871b6d699d35de5468eeca96bf556838af0f0c" + ], + "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" + ], "version": "==0.6.2" }, "docutils": { - "version": "==0.14" + "hashes": [ + "sha256:686577d2e4c32380bb50cbb22f575ed742d58168cee37e99117a854bcd88f125", + "sha256:cf316c8370a737a022b72b56874f6602acf974a37a9fba42ec2876387549fc61" + ], + "index": "pypi", + "version": "==0.17.1" + }, + "entrypoints": { + "hashes": [ + "sha256:589f874b313739ad35be6e0cd7efde2a4e9b6fea91edcc34e58ecbb8dbe56d19", + "sha256:c70dd71abe5a8c85e55e12c19bd91ccfeec11a6e99044204511f9ed547d48451" + ], + "markers": "python_version >= '2.7'", + "version": "==0.3" }, "flake8": { - "version": "==2.6.2" + "hashes": [ + "sha256:45681a117ecc81e870cbf1262835ae4af5e7a8b08e40b944a8a6e6b895914cfb", + "sha256:49356e766643ad15072a789a20915d3c91dc89fd313ccd71802303fd67e4deca" + ], + "index": "pypi", + "version": "==3.7.9" }, "ghp-import": { - "version": "==0.5.5" + "hashes": [ + "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619", + "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343" + ], + "version": "==2.1.0" }, "idna": { - "version": "==2.6" + "hashes": [ + "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4", + "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2" + ], + "markers": "python_version >= '3.5'", + "version": "==3.4" + }, + "importlib-metadata": { + "hashes": [ + "sha256:43dd286a2cd8995d5eaef7fee2066340423b818ed3fd70adf0bad5f1fac53fed", + "sha256:92501cdf9cc66ebd3e612f1b4f0c0765dfa42f0fa38ffb319b6bd84dd675d705" + ], + "markers": "python_version >= '3.7'", + "version": "==6.6.0" + }, + "jaraco.classes": { + "hashes": [ + "sha256:2353de3288bc6b82120752201c6b1c1a14b058267fa424ed5ce5984e3b922158", + "sha256:89559fa5c1d3c34eff6f631ad80bb21f378dbcbb35dd161fd2c6b93f5be2f98a" + ], + "markers": "python_version >= '3.7'", + "version": "==3.2.3" + }, + "jeepney": { + "hashes": [ + "sha256:5efe48d255973902f6badc3ce55e2aa6c5c3b3bc642059ef3a91247bcfcc5806", + "sha256:c0a454ad016ca575060802ee4d590dd912e35c122fa04e70306de3d076cce755" + ], + "markers": "sys_platform == 'linux'", + "version": "==0.8.0" }, "jinja2": { - "version": "==2.9.6" - }, - "livereload": { - "version": "==2.5.1" + "hashes": [ + "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852", + "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61" + ], + "markers": "python_version >= '3.7'", + "version": "==3.1.2" + }, + "keyring": { + "hashes": [ + "sha256:771ed2a91909389ed6148631de678f82ddc73737d85a927f382a8a1b157898cd", + "sha256:ba2e15a9b35e21908d0aaf4e0a47acc52d6ae33444df0da2b49d41a46ef6d678" + ], + "markers": "python_version >= '3.7'", + "version": "==23.13.1" + }, + "macfsevents": { + "hashes": [ + "sha256:1324b66b356051de662ba87d84f73ada062acd42b047ed1246e60a449f833e10" + ], + "markers": "sys_platform == 'darwin'", + "version": "==0.8.1" }, "markdown": { - "version": "==2.6.9" + "hashes": [ + "sha256:9ba587db9daee7ec761cfc656272be6aabe2ed300fece21208e4aab2e457bc8f", + "sha256:a856869c7ff079ad84a3e19cd87a64998350c2b94e9e08e44270faef33400f81" + ], + "index": "pypi", + "version": "==2.6.11" }, "markupsafe": { - "version": "==1.0" + "hashes": [ + "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": { - "version": "==0.5.3" + "hashes": [ + "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", + "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" + ], + "version": "==0.6.1" + }, + "mergedeep": { + "hashes": [ + "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8", + "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307" + ], + "markers": "python_version >= '3.6'", + "version": "==1.3.4" + }, + "minilog": { + "hashes": [ + "sha256:0c48879cc9e72f0127aa2c36b522dc6fa10fa8532956197436b491d31617d5d5", + "sha256:2048a8d381b36ef5f146fb9a657e627729411f8e2ed0047e2c1286cf8e3e58d7" + ], + "markers": "python_version >= '3.7' and python_version < '4.0'", + "version": "==2.1" + }, + "mkdocs": { + "hashes": [ + "sha256:89f5a094764381cda656af4298727c9f53dc3e602983087e1fe96ea1df24f4c1", + "sha256:a1fa8c2d0c1305d7fc2b9d9f607c71778572a8b110fb26642aa00296c9e6d072" + ], + "index": "pypi", + "version": "==1.2.3" + }, + "more-itertools": { + "hashes": [ + "sha256:cabaa341ad0389ea83c17a94566a53ae4c9d07349861ecb14dc6d0345cf9ac5d", + "sha256:d2bc7f02446e86a68911e58ded76d6561eea00cddfb2a91e7019bbb586c799f3" + ], + "markers": "python_version >= '3.7'", + "version": "==9.1.0" }, - "mkdocs": "==0.12", "nose": { + "hashes": [ + "sha256:9ff7c6cc443f8c51994b34a667bbcf45afd6d945be7477b52e97516fd17c53ac", + "sha256:dadcddc0aefbf99eea214e0f1232b94f2fa9bd98fa8353711dacb112bfcbbb2a", + "sha256:f1bffef9cbc82628f6e7d7b40d7e255aefaa1adb6a1b1d26c69a8b79e6208a98" + ], + "index": "pypi", "version": "==1.3.7" }, + "packaging": { + "hashes": [ + "sha256:994793af429502c4ea2ebf6bf664629d07c1a9fe974af92966e4b8d2df7edc61", + "sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f" + ], + "markers": "python_version >= '3.7'", + "version": "==23.1" + }, "pkginfo": { - "version": "==1.4.1" + "hashes": [ + "sha256:4b7a555a6d5a22169fcc9cf7bfd78d296b0361adad412a346c1226849af5e546", + "sha256:8fd5896e8718a4372f0ea9cc9d96f6417c9b986e23a4d116dda26b62cc29d046" + ], + "markers": "python_version >= '3.6'", + "version": "==1.9.6" }, "pycodestyle": { - "version": "==2.0.0" + "hashes": [ + "sha256:95a2219d12372f05704562a14ec30bc76b05a5b297b21a5dfe3f6fac3491ae56", + "sha256:e40a936c9a450ad81df37f549d676d127b1b66000a6c500caa2b085bc0ca976c" + ], + "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": { - "version": "==1.2.3" + "hashes": [ + "sha256:17dbeb2e3f4d772725c777fabc446d5634d1038f234e77343108ce445ea69ce0", + "sha256:d976835886f8c5b31d47970ed689944a0262b5f3afa00a5a7b4dc81e5449f8a2" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==2.1.1" + }, + "pygments": { + "hashes": [ + "sha256:8ace4d3c1dd481894b2005f560ead0f9f19ee64fe983366be1a21e171d12775c", + "sha256:db2db3deb4b4179f399a09054b023b6a586b76499d36965813c71aa8ed7b5fd1" + ], + "markers": "python_version >= '3.7'", + "version": "==2.15.1" + }, + "pync": { + "hashes": [ + "sha256:85737aab9fc69cf59dc9fe831adbe94ac224944c05e297c98de3c2413f253530" + ], + "markers": "sys_platform == 'darwin'", + "version": "==1.6.1" + }, + "python-dateutil": { + "hashes": [ + "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.2" }, "python-termstyle": { + "hashes": [ + "sha256:6faf42ba42f2826c38cf70dacb3ac51f248a418e48afc0e36593df11cf3ab1d2", + "sha256:f42a6bb16fbfc5e2c66d553e7ad46524ea833872f75ee5d827c15115fafc94e2" + ], "version": "==0.1.10" }, "pyyaml": { - "version": "==3.12" + "hashes": [ + "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:cd653186dfc73055656f090f227f5cb22a046d7f71a841dfa305f55c9a513273", + "sha256:f67a16caedfa71eef48a31b39708637a6f4664c4394801a7b0d6432d13907343" + ], + "markers": "python_version >= '3.7'", + "version": "==37.3" }, "requests": { - "version": "==2.18.4" + "hashes": [ + "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f", + "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1" + ], + "markers": "python_version >= '3.7'", + "version": "==2.31.0" }, "requests-toolbelt": { - "version": "==0.8.0" + "hashes": [ + "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6", + "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==1.0.0" }, - "setuptools": { - "version": "==36.3.0" + "rfc3986": { + "hashes": [ + "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" }, - "singledispatch": { - "version": "==3.4.0.3" + "setuptools": { + "hashes": [ + "sha256:5df61bf30bb10c6f756eb19e7c9f3b473051f48db77fddbe06ff2ca307df9a6f", + "sha256:62642358adc77ffa87233bc4d2354c4b2682d214048f500964dbe760ccedf102" + ], + "markers": "python_version >= '3.7'", + "version": "==67.8.0" }, "six": { - "version": "==1.10.0" + "hashes": [ + "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", + "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==1.16.0" }, "sniffer": { - "version": "==0.4.0" - }, - "tornado": { - "version": "==4.5.2" + "hashes": [ + "sha256:b37665053fb83d7790bf9e51d616c11970863d14b5ea5a51155a4e95759d1529", + "sha256:f120843fe152d0e380402fc11313b151e2044c47fdd36895de2efedc8624dbb8" + ], + "index": "pypi", + "version": "==0.4.1" }, "tqdm": { - "version": "==4.15.0" + "hashes": [ + "sha256:1871fb68a86b8fb3b59ca4cdd3dcccbc7e6d613eeed31f4c332531977b89beb5", + "sha256:c4f53a17fe37e132815abceec022631be8ffe1b9381c2e6e30aa70edc99e9671" + ], + "markers": "python_version >= '3.7'", + "version": "==4.65.0" }, "twine": { - "version": "==1.9.1" + "hashes": [ + "sha256:16f706f2f1687d7ce30e7effceee40ed0a09b7c33b9abb5ef6434e5551565d83", + "sha256:a56c985264b991dc8a8f4234eb80c5af87fa8080d0c224ad8f2cd05a2c22e83b" + ], + "index": "pypi", + "version": "==3.4.1" }, "urllib3": { - "version": "==1.22" + "hashes": [ + "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": [ + "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78", + "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923" + ], + "version": "==0.5.1" + }, + "zipp": { + "hashes": [ + "sha256:112929ad649da941c23de50f356a2b5570c954b65150642bccdd66bf194d224b", + "sha256:48904fc76a60e542af151aded95726c1a5c34ed43ab4134b597665c86d7ad556" + ], + "markers": "python_version >= '3.7'", + "version": "==3.15.0" } } } diff --git a/README.md b/README.md index ddc8f7c..29166a3 100644 --- a/README.md +++ b/README.md @@ -2,16 +2,16 @@ Browsable web APIs for Flask. -[![Unix Build Status](https://img.shields.io/travis/flask-api/flask-api.svg)](https://travis-ci.org/flask-api/flask-api) +[![Unix Build Status](https://img.shields.io/travis/com/flask-api/flask-api.svg)](https://travis-ci.com/flask-api/flask-api) [![Coverage Status](https://img.shields.io/coveralls/flask-api/flask-api.svg)](https://coveralls.io/r/flask-api/flask-api) [![Scrutinizer Code Quality](https://img.shields.io/scrutinizer/g/flask-api/flask-api.svg)](https://scrutinizer-ci.com/g/flask-api/flask-api/) [![PyPI Version](https://img.shields.io/pypi/v/Flask-API.svg)](https://pypi.org/project/Flask-API/) -**Status**: This project is in maintenance mode. The original author ([Tom Christie](https://twitter.com/_tomchristie)) has shifted their focus to other API projects (http://www.coreapi.org and http://www.api-star.org). Passing PRs will still be considered for releases by the maintainers ([Jace Browning](https://twitter.com/jacebrowning)). +**Status**: This project is in maintenance mode. The original author ([Tom Christie](https://twitter.com/_tomchristie)) has shifted his focus to [API Star](https://github.com/encode/apistar). Passing PRs will still be considered for releases by the maintainer ([Jace Browning](https://twitter.com/jacebrowning)). ## Overview -Flask API is a drop-in replacement for Flask that provides an implementation of browsable APIs similar to what [Django REST framework](http://www.django-rest-framework.org) provides. It gives you properly content negotiated-responses and smart request parsing: +Flask API is a drop-in replacement for Flask that provides an implementation of browsable APIs similar to what [Django REST framework](http://www.django-rest-framework.org) offers. It gives you properly content-negotiated responses and smart request parsing: ![Screenshot](docs/screenshot.png) @@ -19,111 +19,129 @@ Flask API is a drop-in replacement for Flask that provides an implementation of Requirements: -* Python 2.7+ or 3.3+ -* Flask 0.10+ +* Python 3.6+ +* Flask 1.1.+ Install using `pip`: - pip install Flask-API +```shell +$ pip install Flask-API +``` Import and initialize your application: - from flask_api import FlaskAPI +```python +from flask_api import FlaskAPI - app = FlaskAPI(__name__) +app = FlaskAPI(__name__) +``` ## Responses Return any valid response object as normal, or return a `list` or `dict`. - @app.route('/example/') - def example(): - return {'hello': 'world'} +```python +@app.route('/example/') +def example(): + return {'hello': 'world'} +``` -A renderer for the response data will be selected using content negotiation based on the client 'Accept' header. If you're making the API request from a regular client, this will default to a JSON response. If you're viewing the API in a browser it'll default to the browsable API HTML. +A renderer for the response data will be selected using content negotiation based on the client 'Accept' header. If you're making the API request from a regular client, this will default to a JSON response. If you're viewing the API in a browser, it'll default to the browsable API HTML. ## Requests Access the parsed request data using `request.data`. This will handle JSON or form data by default. - @app.route('/example/') - def example(): - return {'request data': request.data} +```python +@app.route('/example/') +def example(): + return {'request data': request.data} +``` ## Example The following example demonstrates a simple API for creating, listing, updating and deleting notes. - from flask import request, url_for - from flask_api import FlaskAPI, status, exceptions - - app = FlaskAPI(__name__) - - - notes = { - 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] - } - - - @app.route("/", methods=['GET', 'POST']) - def notes_list(): - """ - List or create notes. - """ - 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 - - # request.method == 'GET' - return [note_repr(idx) for idx in sorted(notes.keys())] - - - @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', '')) - notes[key] = note - return note_repr(key) - - elif request.method == 'DELETE': - notes.pop(key, None) - return '', status.HTTP_204_NO_CONTENT - - # request.method == 'GET' - if key not in notes: - raise exceptions.NotFound() - return note_repr(key) - - - if __name__ == "__main__": - app.run(debug=True) +```python +from flask import request, url_for +from flask_api import FlaskAPI, status, exceptions + +app = FlaskAPI(__name__) + + +notes = { + 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] + } + + +@app.route("/", methods=['GET', 'POST']) +def notes_list(): + """ + List or create notes. + """ + 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 + + # request.method == 'GET' + return [note_repr(idx) for idx in sorted(notes.keys())] + + +@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', '')) + notes[key] = note + return note_repr(key) + + elif request.method == 'DELETE': + notes.pop(key, None) + return '', status.HTTP_204_NO_CONTENT + + # request.method == 'GET' + if key not in notes: + raise exceptions.NotFound() + return note_repr(key) + + +if __name__ == "__main__": + app.run(debug=True) +``` Now run the webapp: - $ python ./example.py - * Running on http://127.0.0.1:5000/ - * Restarting with reloader +```shell +$ python ./example.py + * Running on http://127.0.0.1:5000/ + * Restarting with reloader +``` You can now open a new tab and interact with the API from the command line: - $ curl -X GET http://127.0.0.1:5000/ - [{"url": "http://127.0.0.1:5000/0/", "text": "do the shopping"}, {"url": "http://127.0.0.1:5000/1/", "text": "build the codez"}, {"url": "http://127.0.0.1:5000/2/", "text": "paint the door"}] - $ curl -X GET http://127.0.0.1:5000/1/ - {"url": "http://127.0.0.1:5000/1/", "text": "build the codez"} - $ curl -X PUT http://127.0.0.1:5000/1/ -d text="flask api is teh awesomez" - {"url": "http://127.0.0.1:5000/1/", "text": "flask api is teh awesomez"} +```shell +$ curl -X GET http://127.0.0.1:5000/ +[{"url": "http://127.0.0.1:5000/0/", "text": "do the shopping"}, + {"url": "http://127.0.0.1:5000/1/", "text": "build the codez"}, + {"url": "http://127.0.0.1:5000/2/", "text": "paint the door"}] + +$ curl -X GET http://127.0.0.1:5000/1/ +{"url": "http://127.0.0.1:5000/1/", "text": "build the codez"} + +$ curl -X PUT http://127.0.0.1:5000/1/ -d text="flask api is teh awesomez" +{"url": "http://127.0.0.1:5000/1/", "text": "flask api is teh awesomez"} +``` You can also work on the API directly in your browser, by opening . You can then navigate between notes, and make `GET`, `PUT`, `POST` and `DELETE` API requests. diff --git a/docs/CNAME b/docs/CNAME deleted file mode 100644 index 08b39d2..0000000 --- a/docs/CNAME +++ /dev/null @@ -1 +0,0 @@ -www.flaskapi.org diff --git a/docs/about/release-notes.md b/docs/about/release-notes.md index 48523cd..084d595 100644 --- a/docs/about/release-notes.md +++ b/docs/about/release-notes.md @@ -1,5 +1,25 @@ # Release Notes +## Version 3.1 + +- Fixed support for Flask `2.3`. + +## Version 3.0 + +* Dropped support for Flask `<2.0`. + +## Version 2.0 + +* Dropped support for Python `<3.6`. +* Dropped support for Flask `<1.1`. + +## Version 1.1 + +* Added support for custom JSON encoders. +* Added `None` as a valid return value for empty responses (204). +* Dropped support for Flask `<0.12.3` due to [CVE-2018-1000656](https://nvd.nist.gov/vuln/detail/CVE-2018-1000656). +* Added support for `python-markdown` 3+. + ## Version 1.0 * Stable release to enter maintenance mode. diff --git a/docs/api-guide/status-codes.md b/docs/api-guide/status-codes.md index f72c800..837a774 100644 --- a/docs/api-guide/status-codes.md +++ b/docs/api-guide/status-codes.md @@ -2,7 +2,7 @@ Flask API includes a set of named constants that you can use to make more code more obvious and readable. - from flask.ext.api import status + from flask_api import status ... @@ -15,7 +15,7 @@ The full set of HTTP status codes included in the `status` module is listed belo The module also includes a set of helper functions for testing if a status code is in a given range. - from flask.ext.api import status + from flask_api import status import unittest ... diff --git a/docs/api-guide/templates.md b/docs/api-guide/templates.md new file mode 100644 index 0000000..a55f621 --- /dev/null +++ b/docs/api-guide/templates.md @@ -0,0 +1,23 @@ +# Templates + +Flask API utilizes [blueprints](http://flask.pocoo.org/docs/latest/blueprints/) for managing browsable pages' template path. + +Off the box, it includes a default template. But you would want to customise this for your needs. + +To do that, simply copy `static` and `templates` to your project. + +Then override the previous blueprint with following: + + from flask import Blueprint + from flask_api import FlaskAPI + + theme = Blueprint( + 'flask-api', __name__, + url_prefix='/flask-api', + template_folder='templates', static_folder='static' + ) + + app = FlaskAPI(__name__) + app.blueprints['flask-api'] = theme + +Use `templates/base.html` as your base custom template. Note that this cannot be renamed. \ No newline at end of file diff --git a/docs/img/android-chrome-144x144.png b/docs/img/android-chrome-144x144.png new file mode 100644 index 0000000..bb754b4 Binary files /dev/null and b/docs/img/android-chrome-144x144.png differ diff --git a/docs/img/android-chrome-192x192.png b/docs/img/android-chrome-192x192.png new file mode 100644 index 0000000..8290d95 Binary files /dev/null and b/docs/img/android-chrome-192x192.png differ diff --git a/docs/img/android-chrome-256x256.png b/docs/img/android-chrome-256x256.png new file mode 100644 index 0000000..0fb7aa1 Binary files /dev/null and b/docs/img/android-chrome-256x256.png differ diff --git a/docs/img/android-chrome-36x36.png b/docs/img/android-chrome-36x36.png new file mode 100644 index 0000000..1c8b8c3 Binary files /dev/null and b/docs/img/android-chrome-36x36.png differ diff --git a/docs/img/android-chrome-384x384.png b/docs/img/android-chrome-384x384.png new file mode 100644 index 0000000..14e8162 Binary files /dev/null and b/docs/img/android-chrome-384x384.png differ diff --git a/docs/img/android-chrome-48x48.png b/docs/img/android-chrome-48x48.png new file mode 100644 index 0000000..33ce393 Binary files /dev/null and b/docs/img/android-chrome-48x48.png differ diff --git a/docs/img/android-chrome-512x512.png b/docs/img/android-chrome-512x512.png new file mode 100644 index 0000000..efcfa60 Binary files /dev/null and b/docs/img/android-chrome-512x512.png differ diff --git a/docs/img/android-chrome-72x72.png b/docs/img/android-chrome-72x72.png new file mode 100644 index 0000000..203309f Binary files /dev/null and b/docs/img/android-chrome-72x72.png differ diff --git a/docs/img/android-chrome-96x96.png b/docs/img/android-chrome-96x96.png new file mode 100644 index 0000000..6d0977a Binary files /dev/null and b/docs/img/android-chrome-96x96.png differ diff --git a/docs/img/apple-touch-icon-114x114.png b/docs/img/apple-touch-icon-114x114.png new file mode 100644 index 0000000..3092a66 Binary files /dev/null and b/docs/img/apple-touch-icon-114x114.png differ diff --git a/docs/img/apple-touch-icon-120x120.png b/docs/img/apple-touch-icon-120x120.png new file mode 100644 index 0000000..cabcf5e Binary files /dev/null and b/docs/img/apple-touch-icon-120x120.png differ diff --git a/docs/img/apple-touch-icon-144x144.png b/docs/img/apple-touch-icon-144x144.png new file mode 100644 index 0000000..6000d3e Binary files /dev/null and b/docs/img/apple-touch-icon-144x144.png differ diff --git a/docs/img/apple-touch-icon-152x152.png b/docs/img/apple-touch-icon-152x152.png new file mode 100644 index 0000000..26397cf Binary files /dev/null and b/docs/img/apple-touch-icon-152x152.png differ diff --git a/docs/img/apple-touch-icon-180x180.png b/docs/img/apple-touch-icon-180x180.png new file mode 100644 index 0000000..bacb214 Binary files /dev/null and b/docs/img/apple-touch-icon-180x180.png differ diff --git a/docs/img/apple-touch-icon-57x57.png b/docs/img/apple-touch-icon-57x57.png new file mode 100644 index 0000000..8005a77 Binary files /dev/null and b/docs/img/apple-touch-icon-57x57.png differ diff --git a/docs/img/apple-touch-icon-60x60.png b/docs/img/apple-touch-icon-60x60.png new file mode 100644 index 0000000..5814044 Binary files /dev/null and b/docs/img/apple-touch-icon-60x60.png differ diff --git a/docs/img/apple-touch-icon-72x72.png b/docs/img/apple-touch-icon-72x72.png new file mode 100644 index 0000000..59c9522 Binary files /dev/null and b/docs/img/apple-touch-icon-72x72.png differ diff --git a/docs/img/apple-touch-icon-76x76.png b/docs/img/apple-touch-icon-76x76.png new file mode 100644 index 0000000..9f81006 Binary files /dev/null and b/docs/img/apple-touch-icon-76x76.png differ diff --git a/docs/img/apple-touch-icon.png b/docs/img/apple-touch-icon.png new file mode 100644 index 0000000..7521822 Binary files /dev/null and b/docs/img/apple-touch-icon.png differ diff --git a/docs/img/browserconfig.xml b/docs/img/browserconfig.xml new file mode 100644 index 0000000..7ebe71a --- /dev/null +++ b/docs/img/browserconfig.xml @@ -0,0 +1,12 @@ + + + + + + + + + #e1e1e8 + + + diff --git a/docs/img/favicon-16x16.png b/docs/img/favicon-16x16.png new file mode 100644 index 0000000..36a756b Binary files /dev/null and b/docs/img/favicon-16x16.png differ diff --git a/docs/img/favicon-194x194.png b/docs/img/favicon-194x194.png new file mode 100644 index 0000000..ff2186e Binary files /dev/null and b/docs/img/favicon-194x194.png differ diff --git a/docs/img/favicon-32x32.png b/docs/img/favicon-32x32.png new file mode 100644 index 0000000..1479c3d Binary files /dev/null and b/docs/img/favicon-32x32.png differ diff --git a/docs/img/favicon.ico b/docs/img/favicon.ico new file mode 100644 index 0000000..b8435c6 Binary files /dev/null and b/docs/img/favicon.ico differ diff --git a/docs/img/manifest.json b/docs/img/manifest.json new file mode 100644 index 0000000..e85909a --- /dev/null +++ b/docs/img/manifest.json @@ -0,0 +1,53 @@ +{ + "name": "Flask API", + "icons": [ + { + "src": "./img/android-chrome-36x36.png", + "sizes": "36x36", + "type": "image/png" + }, + { + "src": "./img/android-chrome-48x48.png", + "sizes": "48x48", + "type": "image/png" + }, + { + "src": "./img/android-chrome-72x72.png", + "sizes": "72x72", + "type": "image/png" + }, + { + "src": "./img/android-chrome-96x96.png", + "sizes": "96x96", + "type": "image/png" + }, + { + "src": "./img/android-chrome-144x144.png", + "sizes": "144x144", + "type": "image/png" + }, + { + "src": "./img/android-chrome-192x192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "./img/android-chrome-256x256.png", + "sizes": "256x256", + "type": "image/png" + }, + { + "src": "./img/android-chrome-384x384.png", + "sizes": "384x384", + "type": "image/png" + }, + { + "src": "./img/android-chrome-512x512.png", + "sizes": "512x512", + "type": "image/png" + } + ], + "theme_color": "#e1e1e8", + "background_color": "#e1e1e8", + "display": "standalone" +} \ No newline at end of file diff --git a/docs/img/mstile-144x144.png b/docs/img/mstile-144x144.png new file mode 100644 index 0000000..d7cae80 Binary files /dev/null and b/docs/img/mstile-144x144.png differ diff --git a/docs/img/mstile-150x150.png b/docs/img/mstile-150x150.png new file mode 100644 index 0000000..12115a4 Binary files /dev/null and b/docs/img/mstile-150x150.png differ diff --git a/docs/img/mstile-310x150.png b/docs/img/mstile-310x150.png new file mode 100644 index 0000000..d2d64cf Binary files /dev/null and b/docs/img/mstile-310x150.png differ diff --git a/docs/img/mstile-310x310.png b/docs/img/mstile-310x310.png new file mode 100644 index 0000000..6fe3189 Binary files /dev/null and b/docs/img/mstile-310x310.png differ diff --git a/docs/img/mstile-70x70.png b/docs/img/mstile-70x70.png new file mode 100644 index 0000000..7833b6f Binary files /dev/null and b/docs/img/mstile-70x70.png differ diff --git a/docs/img/safari-pinned-tab.svg b/docs/img/safari-pinned-tab.svg new file mode 100644 index 0000000..5ce08d0 --- /dev/null +++ b/docs/img/safari-pinned-tab.svg @@ -0,0 +1,44 @@ + + + + +Created by potrace 1.11, written by Peter Selinger 2001-2013 + + + + + + + + + + + + diff --git a/docs/index.md b/docs/index.md index 4bdbcca..43831b7 100644 --- a/docs/index.md +++ b/docs/index.md @@ -14,12 +14,12 @@ Flask API is a drop-in replacement for Flask that provides an implementation of Requirements: -* Python 2.7+ or 3.3+ -* Flask 0.10+ +* Python 3.6+ +* Flask 1.1+ The following packages are optional: -* Markdown (2.1.0+) - Markdown support for the browsable API. +* Markdown (`2.6+`): Markdown support for the browsable API Install using `pip`: @@ -39,7 +39,7 @@ Return any valid response object as normal, or return a `list` or `dict`. def example(): return {'hello': 'world'} -A renderer for the response data will be selected using content negotiation based on the client 'Accept' header. If you're making the API request from a regular client, this will default to a JSON response. If you're viewing the API in a browser it'll default to the browsable API HTML. +A renderer for the response data will be selected using content negotiation based on the client 'Accept' header. If you're making the API request from a regular client, this will default to a JSON response. If you're viewing the API in a browser it'll default to the browsable API HTML. ## Requests @@ -55,23 +55,23 @@ The following example demonstrates a simple API for creating, listing, updating from flask import request, url_for from flask_api import FlaskAPI, status, exceptions - + app = FlaskAPI(__name__) - - + + notes = { 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] } - - + + @app.route("/", methods=['GET', 'POST']) def notes_list(): """ @@ -82,11 +82,11 @@ The following example demonstrates a simple API for creating, listing, updating idx = max(notes.keys()) + 1 notes[idx] = note return note_repr(idx), status.HTTP_201_CREATED - + # request.method == 'GET' return [note_repr(idx) for idx in sorted(notes.keys())] - - + + @app.route("//", methods=['GET', 'PUT', 'DELETE']) def notes_detail(key): """ @@ -96,17 +96,17 @@ The following example demonstrates a simple API for creating, listing, updating note = str(request.data.get('text', '')) notes[key] = note return note_repr(key) - + elif request.method == 'DELETE': notes.pop(key, None) return '', status.HTTP_204_NO_CONTENT - + # request.method == 'GET' if key not in notes: raise exceptions.NotFound() return note_repr(key) - - + + if __name__ == "__main__": app.run(debug=True) diff --git a/docs/logo.draw.io.xml b/docs/logo.draw.io.xml new file mode 100644 index 0000000..2bb0b6b --- /dev/null +++ b/docs/logo.draw.io.xml @@ -0,0 +1 @@ +7ZdNk6MgEIZ/jXcQNeY4cTO7lznlsGeiHaUGJUXImOyvH1T8iqY2M6VOTe3CQXy7W+FpELRIkF5+SnpMXkQE3LJRdLHID8u2se96+lIoV6Mgb1UpsWSR0Vphx/5A7WjUM4vg1HNUQnDFjn0xFFkGoeppVEqR990OgvffeqQxDIRdSPlQ/c0ilVSq56BW/wUsTtSNYU/D11iKc2ZeZ9nkUJbKnNL6Ucb/lNBI5B2JbC0SSCFU1UovAfCCbU2tinu+Y226LSFTDwWYiDfKz1B3ueyYutYsyuFAEYAssskTpmB3pGFhzXX6tZaolOs7rJsnJcUrBIILWUaTLdbVbyw1TT3SzYFx3vF8LovWY0kjpgdQ2zKRQeEuWkm7o7JonXIWZ1qTVT42ZkQgFVzuYsENbD2JQaSg5FW7mPlapzMfyX3SzTsyIjXzLW4e1ULXDcN9PAf23CloSPVTgIcpsMOiLsT6b3CdCdiSj7DFn2Ab7kfZjkxvWHvIXS3E1gSQ+jtj5rU/nNfYngm9Mzd6hFyyf+jLgpBPgqdl0Tv+16F350YPWNeH0B9Wuq4XRt8nj70hetebCb33H30H/dhGOorenwD96hujv3PkmT4j5NHFMMlq8Oc+2nzidLngasA326+DhvAbberlsP63999b9q49ZO9Pswno2/aPrbR1fovJ9h0= \ No newline at end of file diff --git a/docs/screenshot.png b/docs/screenshot.png index 9dba138..5d01222 100644 Binary files a/docs/screenshot.png and b/docs/screenshot.png differ 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 143cce5..7d4d782 100644 --- a/flask_api/__init__.py +++ b/flask_api/__init__.py @@ -1,3 +1,3 @@ from flask_api.app import FlaskAPI -__version__ = '1.0' +__version__ = "3.1" diff --git a/flask_api/app.py b/flask_api/app.py index fbffd20..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 -from flask._compat import reraise, string_types, text_type +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 itertools import chain -from werkzeug.exceptions import HTTPException -import re -import sys -from flask_api.compat import is_flask_legacy - +from flask_api.status import HTTP_204_NO_CONTENT 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", ) @@ -29,15 +30,15 @@ class FlaskAPI(Flask): response_class = APIResponse def __init__(self, *args, **kwargs): - super(FlaskAPI, self).__init__(*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 request.renderer_classes = self.api_settings.DEFAULT_RENDERERS - return super(FlaskAPI, self).preprocess_request() + return super().preprocess_request() def make_response(self, rv): """ @@ -48,14 +49,17 @@ def make_response(self, rv): if isinstance(rv, tuple): rv, status_or_headers, headers = rv + (None,) * (3 - len(rv)) + if rv is None and status_or_headers == HTTP_204_NO_CONTENT: + 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 if not isinstance(rv, self.response_class): - if isinstance(rv, (text_type, bytes, bytearray, list, dict)): + if isinstance(rv, (str, bytes, bytearray, list, dict)): status = status_or_headers rv = self.response_class(rv, headers=headers, status=status) headers = status_or_headers = None @@ -63,7 +67,7 @@ def make_response(self, rv): rv = self.response_class.force_type(rv, request.environ) if status_or_headers is not None: - if isinstance(status_or_headers, string_types): + if isinstance(status_or_headers, str): rv.status = status_or_headers else: rv.status_code = status_or_headers @@ -95,15 +99,18 @@ 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) - reraise(exc_type, exc_value, tb) + raise e def handle_api_exception(self, exc): - return APIResponse({'message': exc.detail}, status=exc.status_code) + content = {"message": exc.detail} + status = exc.status_code + return self.response_class(content, status=status) def create_url_adapter(self, request): """ @@ -114,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 65d36a2..8760df5 100644 --- a/flask_api/compat.py +++ b/flask_api/compat.py @@ -1,10 +1,9 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals, absolute_import from flask import __version__ as flask_version # Markdown is optional try: import markdown + from markdown.extensions.toc import TocExtension def apply_markdown(text): """ @@ -12,11 +11,10 @@ def apply_markdown(text): of '#' style headers to

. """ - extensions = ['headerid(level=2)'] + extensions = [TocExtension(baselevel=2)] 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 eac96e8..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,10 +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): @@ -106,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 de08c02..56f45f6 100644 --- a/flask_api/parsers.py +++ b/flask_api/parsers.py @@ -1,17 +1,16 @@ -# coding: utf8 -from __future__ import unicode_literals -from flask._compat import text_type -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"' @@ -19,43 +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' % text_type(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): - multipart_parser = WerkzeugMultiPartParser(default_stream_factory) - - 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 + ) try: return multipart_parser.parse(stream, boundary, content_length) except ValueError as exc: - msg = 'Multipart parse error - %s' % text_type(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 58582aa..a9aed23 100644 --- a/flask_api/renderers.py +++ b/flask_api/renderers.py @@ -1,14 +1,12 @@ -# coding: utf8 -from __future__ import unicode_literals -from flask import request, render_template, current_app -from flask.json import JSONEncoder -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): """ @@ -19,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): @@ -47,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=JSONEncoder, 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) @@ -105,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 aee96ce..33c3eaa 100644 --- a/flask_api/request.py +++ b/flask_api/request.py @@ -1,13 +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 -from werkzeug._compat import to_unicode -import io + +from flask_api.negotiation import DefaultNegotiation +from flask_api.settings import default_settings class APIRequest(Request): @@ -20,25 +19,25 @@ 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 def _parse(self): """ - Parse the body of the request, using whichever parser satifies the + Parse the body of the request, using whichever parser satisfies the client 'Content-Type' header. """ if not self.content_type or not self.content_length: @@ -51,14 +50,16 @@ def _parse(self): try: parser, media_type = negotiator.select_parser(parsers) ret = parser.parse(self.stream, media_type, **options) - except: + except Exception as e: # Ensure that accessing `request.data` again does not reraise # the exception, so that eg exceptions can handle properly. self._set_empty_data() - raise + 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 @@ -70,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): """ @@ -84,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 @@ -101,31 +102,37 @@ 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 + @method.setter + def method(self, value): + self._method = 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 @@ -134,29 +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. """ - self._method = super(APIRequest, self).method - self._stream = super(APIRequest, self).stream - self._content_type = self.headers.get('Content-Type') + if not hasattr(self, "_method"): + self.method = super().method + self._stream = super().stream + 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... @@ -169,7 +180,7 @@ def full_path(self): """ if not self.query_string: return self.path - return self.path + u'?' + to_unicode(self.query_string, self.url_charset) + return self.path + "?" + self.query_string.decode() # @property # def auth(self): diff --git a/flask_api/response.py b/flask_api/response.py index 3551158..701c299 100644 --- a/flask_api/response.py +++ b/flask_api/response.py @@ -1,7 +1,4 @@ -# coding: utf8 -from __future__ import unicode_literals -from flask import request, Response -from flask._compat import text_type +from flask import Response, request class APIResponse(Response): @@ -9,12 +6,12 @@ class APIResponse(Response): api_return_types = (list, dict) def __init__(self, content=None, *args, **kwargs): - super(APIResponse, self).__init__(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) @@ -24,17 +21,17 @@ def __init__(self, content=None, *args, **kwargs): # From `werkzeug.wrappers.BaseResponse` if content is None: content = [] - if isinstance(content, (text_type, bytes, bytearray)): + if isinstance(content, (str, bytes, bytearray)): self.set_data(content) else: 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 b48044e..58f02d4 100644 --- a/flask_api/settings.py +++ b/flask_api/settings.py @@ -1,4 +1,3 @@ -from flask._compat import string_types import importlib @@ -7,7 +6,7 @@ def perform_imports(val, setting_name): If the given setting is a string import notation, then perform the necessary import or imports. """ - if isinstance(val, string_types): + if isinstance(val, str): return import_from_string(val, setting_name) elif isinstance(val, (list, tuple)): return [perform_imports(item, setting_name) for item in val] @@ -20,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: @@ -30,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 4163c7b..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): @@ -67,6 +65,7 @@ def is_server_error(code): HTTP_415_UNSUPPORTED_MEDIA_TYPE = 415 HTTP_416_REQUESTED_RANGE_NOT_SATISFIABLE = 416 HTTP_417_EXPECTATION_FAILED = 417 +HTTP_418_IM_A_TEAPOT = 418 HTTP_428_PRECONDITION_REQUIRED = 428 HTTP_429_TOO_MANY_REQUESTS = 429 HTTP_431_REQUEST_HEADER_FIELDS_TOO_LARGE = 431 diff --git a/flask_api/templates/base.html b/flask_api/templates/base.html index 0a60422..0603943 100644 --- a/flask_api/templates/base.html +++ b/flask_api/templates/base.html @@ -31,7 +31,7 @@