diff --git a/.dockerignore b/.dockerignore index 7510706..97fc2ee 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,4 +1,4 @@ * -!Pipfile* -!pip* +!pyproject.toml +!poetry.lock !kube_ops_view diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..7f05220 --- /dev/null +++ b/.flake8 @@ -0,0 +1,3 @@ +[flake8] +max-line-length=240 +ignore=E722,W503,E402,E203 diff --git a/.gitignore b/.gitignore index 6e42876..f24d8f4 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,4 @@ scm-source.json .cache/ .coverage .pytest_cache/ +.mypy_cache diff --git a/.travis.yml b/.travis.yml index eb68c1c..728b6a9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,4 +1,4 @@ -dist: xenial +dist: bionic sudo: yes language: python python: @@ -6,9 +6,7 @@ python: services: - docker install: - - pip install tox tox-travis coveralls - - pip install pipenv - - pipenv install --dev + - pip install poetry - nvm install 7.4 - npm install -g eslint script: diff --git a/Dockerfile b/Dockerfile index bc8d3f4..1bd918a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,27 +1,30 @@ -FROM python:3.7-alpine3.10 +FROM python:3.8-slim WORKDIR / -RUN apk add --no-cache python3 python3-dev gcc musl-dev zlib-dev libffi-dev openssl-dev ca-certificates +RUN apt-get update && apt-get install --yes gcc -COPY Pipfile.lock / -COPY pipenv-install.py / +RUN pip3 install poetry -RUN /pipenv-install.py && \ - rm -fr /usr/local/lib/python3.7/site-packages/pip && \ - rm -fr /usr/local/lib/python3.7/site-packages/setuptools && \ - apk del python3-dev gcc musl-dev zlib-dev libffi-dev openssl-dev && \ - rm -rf /var/cache/apk/* /root/.cache /tmp/* +COPY poetry.lock / +COPY pyproject.toml / -FROM python:3.7-alpine3.10 +RUN poetry config virtualenvs.create false && \ + poetry install --no-interaction --no-dev --no-ansi + + +FROM python:3.8-slim WORKDIR / -COPY --from=0 /usr/local/lib/python3.7/site-packages /usr/local/lib/python3.7/site-packages +# copy pre-built packages to this image +COPY --from=0 /usr/local/lib/python3.8/site-packages /usr/local/lib/python3.8/site-packages +# now copy the actual code we will execute (poetry install above was just for dependencies) COPY kube_ops_view /kube_ops_view ARG VERSION=dev + RUN sed -i "s/__version__ = .*/__version__ = '${VERSION}'/" /kube_ops_view/__init__.py -ENTRYPOINT ["/usr/local/bin/python", "-m", "kube_ops_view"] +ENTRYPOINT ["python3", "-m", "kube_ops_view"] diff --git a/Makefile b/Makefile index f6a9bc7..fc6378d 100644 --- a/Makefile +++ b/Makefile @@ -7,13 +7,19 @@ TTYFLAGS = $(shell test -t 0 && echo "-it") default: docker +.PHONY: install +install: + poetry install + clean: rm -fr kube_ops_view/static/build -test: - pipenv run flake8 - pipenv run coverage run --source=kube_ops_view -m py.test - pipenv run coverage report +test: install + poetry run flake8 + poetry run black --check kube_ops_view + # poetry run mypy --ignore-missing-imports kube_ops_view + poetry run coverage run --source=kube_ops_view -m py.test -v + poetry run coverage report appjs: docker run $(TTYFLAGS) -u $$(id -u) -v $$(pwd):/workdir -w /workdir/app -e NPM_CONFIG_CACHE=/tmp node:11.10-alpine npm install diff --git a/Pipfile b/Pipfile deleted file mode 100644 index 8776957..0000000 --- a/Pipfile +++ /dev/null @@ -1,25 +0,0 @@ -[[source]] -url = "https://pypi.org/simple" -verify_ssl = true -name = "pypi" - -[packages] -click = "*" -gevent = "*" -requests = "*" -stups-tokens = ">=1.1.19" -redlock-py = "*" -json-delta = ">=2.0" -flask = "*" -pykube-ng = "*" -flask-dance = "*" - -[dev-packages] -"flake8" = "*" -pytest = "*" -pipenv = "*" -pytest-cov = "*" -coveralls = "*" - -[requires] -python_version = "3.7" diff --git a/Pipfile.lock b/Pipfile.lock deleted file mode 100644 index ebd7c7b..0000000 --- a/Pipfile.lock +++ /dev/null @@ -1,513 +0,0 @@ -{ - "_meta": { - "hash": { - "sha256": "b08b64b6ea15f966d4c4cdb6f96e61a7b5239bdcc5ea27953c16bd76d9572d34" - }, - "pipfile-spec": 6, - "requires": { - "python_version": "3.7" - }, - "sources": [ - { - "name": "pypi", - "url": "https://pypi.org/simple", - "verify_ssl": true - } - ] - }, - "default": { - "certifi": { - "hashes": [ - "sha256:e4f3620cfea4f83eedc95b24abd9cd56f3c4b146dd0177e83a21b4eb49e21e50", - "sha256:fd7c7c74727ddcf00e9acd26bba8da604ffec95bf1c2144e67aff7a8b50e6cef" - ], - "version": "==2019.9.11" - }, - "chardet": { - "hashes": [ - "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", - "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" - ], - "version": "==3.0.4" - }, - "click": { - "hashes": [ - "sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13", - "sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7" - ], - "index": "pypi", - "version": "==7.0" - }, - "flask": { - "hashes": [ - "sha256:13f9f196f330c7c2c5d7a5cf91af894110ca0215ac051b5844701f2bfd934d52", - "sha256:45eb5a6fd193d6cf7e0cf5d8a5b31f83d5faae0293695626f539a823e93b13f6" - ], - "index": "pypi", - "version": "==1.1.1" - }, - "flask-dance": { - "hashes": [ - "sha256:83d6f8d684150ac8fe7d4f2ad8d71170c3233831a09601eb0e5b40d0c28e337d", - "sha256:c3fd1da1c93ada28092e83a5eb843def82701d6d56bd03a512a2e008a25ec106" - ], - "index": "pypi", - "version": "==2.2.0" - }, - "gevent": { - "hashes": [ - "sha256:0774babec518a24d9a7231d4e689931f31b332c4517a771e532002614e270a64", - "sha256:0e1e5b73a445fe82d40907322e1e0eec6a6745ca3cea19291c6f9f50117bb7ea", - "sha256:0ff2b70e8e338cf13bedf146b8c29d475e2a544b5d1fe14045aee827c073842c", - "sha256:107f4232db2172f7e8429ed7779c10f2ed16616d75ffbe77e0e0c3fcdeb51a51", - "sha256:14b4d06d19d39a440e72253f77067d27209c67e7611e352f79fe69e0f618f76e", - "sha256:1b7d3a285978b27b469c0ff5fb5a72bcd69f4306dbbf22d7997d83209a8ba917", - "sha256:1eb7fa3b9bd9174dfe9c3b59b7a09b768ecd496debfc4976a9530a3e15c990d1", - "sha256:2711e69788ddb34c059a30186e05c55a6b611cb9e34ac343e69cf3264d42fe1c", - "sha256:28a0c5417b464562ab9842dd1fb0cc1524e60494641d973206ec24d6ec5f6909", - "sha256:3249011d13d0c63bea72d91cec23a9cf18c25f91d1f115121e5c9113d753fa12", - "sha256:44089ed06a962a3a70e96353c981d628b2d4a2f2a75ea5d90f916a62d22af2e8", - "sha256:4bfa291e3c931ff3c99a349d8857605dca029de61d74c6bb82bd46373959c942", - "sha256:50024a1ee2cf04645535c5ebaeaa0a60c5ef32e262da981f4be0546b26791950", - "sha256:53b72385857e04e7faca13c613c07cab411480822ac658d97fd8a4ddbaf715c8", - "sha256:74b7528f901f39c39cdbb50cdf08f1a2351725d9aebaef212a29abfbb06895ee", - "sha256:7d0809e2991c9784eceeadef01c27ee6a33ca09ebba6154317a257353e3af922", - "sha256:896b2b80931d6b13b5d9feba3d4eebc67d5e6ec54f0cf3339d08487d55d93b0e", - "sha256:8d9ec51cc06580f8c21b41fd3f2b3465197ba5b23c00eb7d422b7ae0380510b0", - "sha256:9f7a1e96fec45f70ad364e46de32ccacab4d80de238bd3c2edd036867ccd48ad", - "sha256:ab4dc33ef0e26dc627559786a4fba0c2227f125db85d970abbf85b77506b3f51", - "sha256:d1e6d1f156e999edab069d79d890859806b555ce4e4da5b6418616322f0a3df1", - "sha256:d752bcf1b98174780e2317ada12013d612f05116456133a6acf3e17d43b71f05", - "sha256:e5bcc4270671936349249d26140c267397b7b4b1381f5ec8b13c53c5b53ab6e1" - ], - "index": "pypi", - "version": "==1.4.0" - }, - "greenlet": { - "hashes": [ - "sha256:000546ad01e6389e98626c1367be58efa613fa82a1be98b0c6fc24b563acc6d0", - "sha256:0d48200bc50cbf498716712129eef819b1729339e34c3ae71656964dac907c28", - "sha256:23d12eacffa9d0f290c0fe0c4e81ba6d5f3a5b7ac3c30a5eaf0126bf4deda5c8", - "sha256:37c9ba82bd82eb6a23c2e5acc03055c0e45697253b2393c9a50cef76a3985304", - "sha256:51503524dd6f152ab4ad1fbd168fc6c30b5795e8c70be4410a64940b3abb55c0", - "sha256:8041e2de00e745c0e05a502d6e6db310db7faa7c979b3a5877123548a4c0b214", - "sha256:81fcd96a275209ef117e9ec91f75c731fa18dcfd9ffaa1c0adbdaa3616a86043", - "sha256:853da4f9563d982e4121fed8c92eea1a4594a2299037b3034c3c898cb8e933d6", - "sha256:8b4572c334593d449113f9dc8d19b93b7b271bdbe90ba7509eb178923327b625", - "sha256:9416443e219356e3c31f1f918a91badf2e37acf297e2fa13d24d1cc2380f8fbc", - "sha256:9854f612e1b59ec66804931df5add3b2d5ef0067748ea29dc60f0efdcda9a638", - "sha256:99a26afdb82ea83a265137a398f570402aa1f2b5dfb4ac3300c026931817b163", - "sha256:a19bf883b3384957e4a4a13e6bd1ae3d85ae87f4beb5957e35b0be287f12f4e4", - "sha256:a9f145660588187ff835c55a7d2ddf6abfc570c2651c276d3d4be8a2766db490", - "sha256:ac57fcdcfb0b73bb3203b58a14501abb7e5ff9ea5e2edfa06bb03035f0cff248", - "sha256:bcb530089ff24f6458a81ac3fa699e8c00194208a724b644ecc68422e1111939", - "sha256:beeabe25c3b704f7d56b573f7d2ff88fc99f0138e43480cecdfcaa3b87fe4f87", - "sha256:d634a7ea1fc3380ff96f9e44d8d22f38418c1c381d5fac680b272d7d90883720", - "sha256:d97b0661e1aead761f0ded3b769044bb00ed5d33e1ec865e891a8b128bf7c656" - ], - "markers": "platform_python_implementation == 'CPython'", - "version": "==0.4.15" - }, - "idna": { - "hashes": [ - "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", - "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c" - ], - "version": "==2.8" - }, - "itsdangerous": { - "hashes": [ - "sha256:321b033d07f2a4136d3ec762eac9f16a10ccd60f53c0c91af90217ace7ba1f19", - "sha256:b12271b2047cb23eeb98c8b5622e2e5c5e9abd9784a153e9d8ef9cb4dd09d749" - ], - "version": "==1.1.0" - }, - "jinja2": { - "hashes": [ - "sha256:065c4f02ebe7f7cf559e49ee5a95fb800a9e4528727aec6f24402a5374c65013", - "sha256:14dd6caf1527abb21f08f86c784eac40853ba93edb79552aa1e4b8aef1b61c7b" - ], - "version": "==2.10.1" - }, - "json-delta": { - "hashes": [ - "sha256:462a73672f7527517d863930bb442ed1986c35dfb6960e0fb1cb84393deea652", - "sha256:cdbb8d38b69b5bef9ac54863aff2df8ae95ab897d165448f8d72cc3501fd32f6" - ], - "index": "pypi", - "version": "==2.0" - }, - "markupsafe": { - "hashes": [ - "sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473", - "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161", - "sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235", - "sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5", - "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff", - "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b", - "sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1", - "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e", - "sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183", - "sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66", - "sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1", - "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1", - "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e", - "sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b", - "sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905", - "sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735", - "sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d", - "sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e", - "sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d", - "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c", - "sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21", - "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2", - "sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5", - "sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b", - "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6", - "sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f", - "sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f", - "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7" - ], - "version": "==1.1.1" - }, - "oauthlib": { - "hashes": [ - "sha256:bee41cc35fcca6e988463cacc3bcb8a96224f470ca547e697b604cc697b2f889", - "sha256:df884cd6cbe20e32633f1db1072e9356f53638e4361bef4e8b03c9127c9328ea" - ], - "version": "==3.1.0" - }, - "pykube-ng": { - "hashes": [ - "sha256:22e595aede4caf8eeb4fe5289e22785a986b4ff641b454d18c88347e71b8fe9d", - "sha256:eeac0cb0104d7167ad0c8532cf30de86b2691d1368638e624b86ed36d6f77638" - ], - "index": "pypi", - "version": "==19.9.2" - }, - "pyyaml": { - "hashes": [ - "sha256:0113bc0ec2ad727182326b61326afa3d1d8280ae1122493553fd6f4397f33df9", - "sha256:01adf0b6c6f61bd11af6e10ca52b7d4057dd0be0343eb9283c878cf3af56aee4", - "sha256:5124373960b0b3f4aa7df1707e63e9f109b5263eca5976c66e08b1c552d4eaf8", - "sha256:5ca4f10adbddae56d824b2c09668e91219bb178a1eee1faa56af6f99f11bf696", - "sha256:7907be34ffa3c5a32b60b95f4d95ea25361c951383a894fec31be7252b2b6f34", - "sha256:7ec9b2a4ed5cad025c2278a1e6a19c011c80a3caaac804fd2d329e9cc2c287c9", - "sha256:87ae4c829bb25b9fe99cf71fbb2140c448f534e24c998cc60f39ae4f94396a73", - "sha256:9de9919becc9cc2ff03637872a440195ac4241c80536632fffeb6a1e25a74299", - "sha256:a5a85b10e450c66b49f98846937e8cfca1db3127a9d5d1e31ca45c3d0bef4c5b", - "sha256:b0997827b4f6a7c286c01c5f60384d218dca4ed7d9efa945c3e1aa623d5709ae", - "sha256:b631ef96d3222e62861443cc89d6563ba3eeb816eeb96b2629345ab795e53681", - "sha256:bf47c0607522fdbca6c9e817a6e81b08491de50f3766a7a0e6a5be7905961b41", - "sha256:f81025eddd0327c7d4cfe9b62cf33190e1e736cc6e97502b3ec425f574b3e7a8" - ], - "version": "==5.1.2" - }, - "redis": { - "hashes": [ - "sha256:98a22fb750c9b9bb46e75e945dc3f61d0ab30d06117cbb21ff9cd1d315fedd3b", - "sha256:c504251769031b0dd7dd5cf786050a6050197c6de0d37778c80c08cb04ae8275" - ], - "version": "==3.3.8" - }, - "redlock-py": { - "hashes": [ - "sha256:0b8722c4843ddeabc2fc1dd37c05859e0da29fbce3bd1f6ecc73c98396f139ac" - ], - "index": "pypi", - "version": "==1.0.8" - }, - "requests": { - "hashes": [ - "sha256:11e007a8a2aa0323f5a921e9e6a2d7e4e67d9877e85773fba9ba6419025cbeb4", - "sha256:9cf5292fcd0f598c671cfc1e0d7d1a7f13bb8085e9a590f48c010551dc6c4b31" - ], - "index": "pypi", - "version": "==2.22.0" - }, - "requests-oauthlib": { - "hashes": [ - "sha256:bd6533330e8748e94bf0b214775fed487d309b8b8fe823dc45641ebcd9a32f57", - "sha256:d3ed0c8f2e3bbc6b344fa63d6f933745ab394469da38db16bdddb461c7e25140" - ], - "version": "==1.2.0" - }, - "six": { - "hashes": [ - "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", - "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" - ], - "version": "==1.12.0" - }, - "stups-tokens": { - "hashes": [ - "sha256:317f4386763bac9dd5c0a4c8b0f9f0238dc3fa81de3c6fd1971b6b01662b5750", - "sha256:7830ad83ccbfd52a9734608ffcefcca917137ce9480cc91a4fbd321a4aca3160" - ], - "index": "pypi", - "version": "==1.1.19" - }, - "urllib3": { - "hashes": [ - "sha256:b246607a25ac80bedac05c6f282e3cdaf3afb65420fd024ac94435cabe6e18d1", - "sha256:dbe59173209418ae49d485b87d1681aefa36252ee85884c31346debd19463232" - ], - "version": "==1.25.3" - }, - "urlobject": { - "hashes": [ - "sha256:47b2e20e6ab9c8366b2f4a3566b6ff4053025dad311c4bb71279bbcfa2430caa" - ], - "version": "==2.4.3" - }, - "werkzeug": { - "hashes": [ - "sha256:00d32beac38fcd48d329566f80d39f10ec2ed994efbecfb8dd4b320062d05902", - "sha256:0a24d43be6a7dce81bae05292356176d6c46d63e42a0dd3f9504b210a9cfaa43" - ], - "version": "==0.15.6" - } - }, - "develop": { - "atomicwrites": { - "hashes": [ - "sha256:03472c30eb2c5d1ba9227e4c2ca66ab8287fbfbbda3888aa93dc2e28fc6811b4", - "sha256:75a9445bac02d8d058d5e1fe689654ba5a6556a1dfd8ce6ec55a0ed79866cfa6" - ], - "version": "==1.3.0" - }, - "attrs": { - "hashes": [ - "sha256:69c0dbf2ed392de1cb5ec704444b08a5ef81680a61cb899dc08127123af36a79", - "sha256:f0b870f674851ecbfbbbd364d6b5cbdff9dcedbc7f3f5e18a6891057f21fe399" - ], - "version": "==19.1.0" - }, - "certifi": { - "hashes": [ - "sha256:e4f3620cfea4f83eedc95b24abd9cd56f3c4b146dd0177e83a21b4eb49e21e50", - "sha256:fd7c7c74727ddcf00e9acd26bba8da604ffec95bf1c2144e67aff7a8b50e6cef" - ], - "version": "==2019.9.11" - }, - "chardet": { - "hashes": [ - "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", - "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" - ], - "version": "==3.0.4" - }, - "coverage": { - "hashes": [ - "sha256:08907593569fe59baca0bf152c43f3863201efb6113ecb38ce7e97ce339805a6", - "sha256:0be0f1ed45fc0c185cfd4ecc19a1d6532d72f86a2bac9de7e24541febad72650", - "sha256:141f08ed3c4b1847015e2cd62ec06d35e67a3ac185c26f7635f4406b90afa9c5", - "sha256:19e4df788a0581238e9390c85a7a09af39c7b539b29f25c89209e6c3e371270d", - "sha256:23cc09ed395b03424d1ae30dcc292615c1372bfba7141eb85e11e50efaa6b351", - "sha256:245388cda02af78276b479f299bbf3783ef0a6a6273037d7c60dc73b8d8d7755", - "sha256:331cb5115673a20fb131dadd22f5bcaf7677ef758741312bee4937d71a14b2ef", - "sha256:386e2e4090f0bc5df274e720105c342263423e77ee8826002dcffe0c9533dbca", - "sha256:3a794ce50daee01c74a494919d5ebdc23d58873747fa0e288318728533a3e1ca", - "sha256:60851187677b24c6085248f0a0b9b98d49cba7ecc7ec60ba6b9d2e5574ac1ee9", - "sha256:63a9a5fc43b58735f65ed63d2cf43508f462dc49857da70b8980ad78d41d52fc", - "sha256:6b62544bb68106e3f00b21c8930e83e584fdca005d4fffd29bb39fb3ffa03cb5", - "sha256:6ba744056423ef8d450cf627289166da65903885272055fb4b5e113137cfa14f", - "sha256:7494b0b0274c5072bddbfd5b4a6c6f18fbbe1ab1d22a41e99cd2d00c8f96ecfe", - "sha256:826f32b9547c8091679ff292a82aca9c7b9650f9fda3e2ca6bf2ac905b7ce888", - "sha256:93715dffbcd0678057f947f496484e906bf9509f5c1c38fc9ba3922893cda5f5", - "sha256:9a334d6c83dfeadae576b4d633a71620d40d1c379129d587faa42ee3e2a85cce", - "sha256:af7ed8a8aa6957aac47b4268631fa1df984643f07ef00acd374e456364b373f5", - "sha256:bf0a7aed7f5521c7ca67febd57db473af4762b9622254291fbcbb8cd0ba5e33e", - "sha256:bf1ef9eb901113a9805287e090452c05547578eaab1b62e4ad456fcc049a9b7e", - "sha256:c0afd27bc0e307a1ffc04ca5ec010a290e49e3afbe841c5cafc5c5a80ecd81c9", - "sha256:dd579709a87092c6dbee09d1b7cfa81831040705ffa12a1b248935274aee0437", - "sha256:df6712284b2e44a065097846488f66840445eb987eb81b3cc6e4149e7b6982e1", - "sha256:e07d9f1a23e9e93ab5c62902833bf3e4b1f65502927379148b6622686223125c", - "sha256:e2ede7c1d45e65e209d6093b762e98e8318ddeff95317d07a27a2140b80cfd24", - "sha256:e4ef9c164eb55123c62411f5936b5c2e521b12356037b6e1c2617cef45523d47", - "sha256:eca2b7343524e7ba246cab8ff00cab47a2d6d54ada3b02772e908a45675722e2", - "sha256:eee64c616adeff7db37cc37da4180a3a5b6177f5c46b187894e633f088fb5b28", - "sha256:ef824cad1f980d27f26166f86856efe11eff9912c4fed97d3804820d43fa550c", - "sha256:efc89291bd5a08855829a3c522df16d856455297cf35ae827a37edac45f466a7", - "sha256:fa964bae817babece5aa2e8c1af841bebb6d0b9add8e637548809d040443fee0", - "sha256:ff37757e068ae606659c28c3bd0d923f9d29a85de79bf25b2b34b148473b5025" - ], - "version": "==4.5.4" - }, - "coveralls": { - "hashes": [ - "sha256:9bc5a1f92682eef59f688a8f280207190d9a6afb84cef8f567fa47631a784060", - "sha256:fb51cddef4bc458de347274116df15d641a735d3f0a580a9472174e2e62f408c" - ], - "index": "pypi", - "version": "==1.8.2" - }, - "docopt": { - "hashes": [ - "sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491" - ], - "version": "==0.6.2" - }, - "entrypoints": { - "hashes": [ - "sha256:589f874b313739ad35be6e0cd7efde2a4e9b6fea91edcc34e58ecbb8dbe56d19", - "sha256:c70dd71abe5a8c85e55e12c19bd91ccfeec11a6e99044204511f9ed547d48451" - ], - "version": "==0.3" - }, - "flake8": { - "hashes": [ - "sha256:19241c1cbc971b9962473e4438a2ca19749a7dd002dd1a946eaba171b4114548", - "sha256:8e9dfa3cecb2400b3738a42c54c3043e821682b9c840b0448c0503f781130696" - ], - "index": "pypi", - "version": "==3.7.8" - }, - "idna": { - "hashes": [ - "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", - "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c" - ], - "version": "==2.8" - }, - "importlib-metadata": { - "hashes": [ - "sha256:652234b6ab8f2506ae58e528b6fbcc668831d3cc758e1bc01ef438d328b68cdb", - "sha256:6f264986fb88042bc1f0535fa9a557e6a376cfe5679dc77caac7fe8b5d43d05f" - ], - "markers": "python_version < '3.8'", - "version": "==0.22" - }, - "mccabe": { - "hashes": [ - "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", - "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" - ], - "version": "==0.6.1" - }, - "more-itertools": { - "hashes": [ - "sha256:409cd48d4db7052af495b09dec721011634af3753ae1ef92d2b32f73a745f832", - "sha256:92b8c4b06dac4f0611c0729b2f2ede52b2e1bac1ab48f089c7ddc12e26bb60c4" - ], - "version": "==7.2.0" - }, - "packaging": { - "hashes": [ - "sha256:a7ac867b97fdc07ee80a8058fe4435ccd274ecc3b0ed61d852d7d53055528cf9", - "sha256:c491ca87294da7cc01902edbe30a5bc6c4c28172b5138ab4e4aa1b9d7bfaeafe" - ], - "version": "==19.1" - }, - "pipenv": { - "hashes": [ - "sha256:56ad5f5cb48f1e58878e14525a6e3129d4306049cb76d2f6a3e95df0d5fc6330", - "sha256:7df8e33a2387de6f537836f48ac6fcd94eda6ed9ba3d5e3fd52e35b5bc7ff49e", - "sha256:a673e606e8452185e9817a987572b55360f4d28b50831ef3b42ac3cab3fee846" - ], - "index": "pypi", - "version": "==2018.11.26" - }, - "pluggy": { - "hashes": [ - "sha256:0db4b7601aae1d35b4a033282da476845aa19185c1e6964b25cf324b5e4ec3e6", - "sha256:fa5fa1622fa6dd5c030e9cad086fa19ef6a0cf6d7a2d12318e10cb49d6d68f34" - ], - "version": "==0.13.0" - }, - "py": { - "hashes": [ - "sha256:64f65755aee5b381cea27766a3a147c3f15b9b6b9ac88676de66ba2ae36793fa", - "sha256:dc639b046a6e2cff5bbe40194ad65936d6ba360b52b3c3fe1d08a82dd50b5e53" - ], - "version": "==1.8.0" - }, - "pycodestyle": { - "hashes": [ - "sha256:95a2219d12372f05704562a14ec30bc76b05a5b297b21a5dfe3f6fac3491ae56", - "sha256:e40a936c9a450ad81df37f549d676d127b1b66000a6c500caa2b085bc0ca976c" - ], - "version": "==2.5.0" - }, - "pyflakes": { - "hashes": [ - "sha256:17dbeb2e3f4d772725c777fabc446d5634d1038f234e77343108ce445ea69ce0", - "sha256:d976835886f8c5b31d47970ed689944a0262b5f3afa00a5a7b4dc81e5449f8a2" - ], - "version": "==2.1.1" - }, - "pyparsing": { - "hashes": [ - "sha256:6f98a7b9397e206d78cc01df10131398f1c8b8510a2f4d97d9abd82e1aacdd80", - "sha256:d9338df12903bbf5d65a0e4e87c2161968b10d2e489652bb47001d82a9b028b4" - ], - "version": "==2.4.2" - }, - "pytest": { - "hashes": [ - "sha256:95d13143cc14174ca1a01ec68e84d76ba5d9d493ac02716fd9706c949a505210", - "sha256:b78fe2881323bd44fd9bd76e5317173d4316577e7b1cddebae9136a4495ec865" - ], - "index": "pypi", - "version": "==5.1.2" - }, - "pytest-cov": { - "hashes": [ - "sha256:2b097cde81a302e1047331b48cadacf23577e431b61e9c6f49a1170bbe3d3da6", - "sha256:e00ea4fdde970725482f1f35630d12f074e121a23801aabf2ae154ec6bdd343a" - ], - "index": "pypi", - "version": "==2.7.1" - }, - "requests": { - "hashes": [ - "sha256:11e007a8a2aa0323f5a921e9e6a2d7e4e67d9877e85773fba9ba6419025cbeb4", - "sha256:9cf5292fcd0f598c671cfc1e0d7d1a7f13bb8085e9a590f48c010551dc6c4b31" - ], - "index": "pypi", - "version": "==2.22.0" - }, - "six": { - "hashes": [ - "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", - "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" - ], - "version": "==1.12.0" - }, - "urllib3": { - "hashes": [ - "sha256:b246607a25ac80bedac05c6f282e3cdaf3afb65420fd024ac94435cabe6e18d1", - "sha256:dbe59173209418ae49d485b87d1681aefa36252ee85884c31346debd19463232" - ], - "version": "==1.25.3" - }, - "virtualenv": { - "hashes": [ - "sha256:680af46846662bb38c5504b78bad9ed9e4f3ba2d54f54ba42494fdf94337fe30", - "sha256:f78d81b62d3147396ac33fc9d77579ddc42cc2a98dd9ea38886f616b33bc7fb2" - ], - "version": "==16.7.5" - }, - "virtualenv-clone": { - "hashes": [ - "sha256:532f789a5c88adf339506e3ca03326f20ee82fd08ee5586b44dc859b5b4468c5", - "sha256:c88ae171a11b087ea2513f260cdac9232461d8e9369bcd1dc143fc399d220557" - ], - "version": "==0.5.3" - }, - "wcwidth": { - "hashes": [ - "sha256:3df37372226d6e63e1b1e1eda15c594bca98a22d33a23832a90998faa96bc65e", - "sha256:f4ebe71925af7b40a864553f761ed559b43544f8f71746c2d756c7fe788ade7c" - ], - "version": "==0.1.7" - }, - "zipp": { - "hashes": [ - "sha256:3718b1cbcd963c7d4c5511a8240812904164b7f381b647143a89d3b98f9bcd8e", - "sha256:f06903e9f1f43b12d371004b4ac7b06ab39a44adc747266928ae6debfa7b3335" - ], - "version": "==0.6.0" - } - } -} diff --git a/kube_ops_view/__init__.py b/kube_ops_view/__init__.py index d9f0a38..3b7d2de 100644 --- a/kube_ops_view/__init__.py +++ b/kube_ops_view/__init__.py @@ -1,2 +1,2 @@ # This version is replaced during release process. -__version__ = '2017.0.dev1' +__version__ = "2017.0.dev1" diff --git a/kube_ops_view/cluster_discovery.py b/kube_ops_view/cluster_discovery.py index 7392066..cf6d423 100644 --- a/kube_ops_view/cluster_discovery.py +++ b/kube_ops_view/cluster_discovery.py @@ -9,9 +9,10 @@ import tokens from requests.auth import AuthBase from pykube import HTTPClient, KubeConfig + # default URL points to kubectl proxy -DEFAULT_CLUSTERS = 'http://localhost:8001/' -CLUSTER_ID_INVALID_CHARS = re.compile('[^a-z0-9:-]') +DEFAULT_CLUSTERS = "http://localhost:8001/" +CLUSTER_ID_INVALID_CHARS = re.compile("[^a-z0-9:-]") logger = logging.getLogger(__name__) @@ -19,27 +20,27 @@ tokens.configure(from_file_only=True) def generate_cluster_id(url: str): - '''Generate some "cluster ID" from given API server URL''' - for prefix in ('https://', 'http://'): + """Generate some "cluster ID" from given API server URL""" + for prefix in ("https://", "http://"): if url.startswith(prefix): - url = url[len(prefix):] - return CLUSTER_ID_INVALID_CHARS.sub('-', url.lower()).strip('-') + url = url[len(prefix) :] + return CLUSTER_ID_INVALID_CHARS.sub("-", url.lower()).strip("-") class StaticAuthorizationHeaderAuth(AuthBase): - '''Static authentication with given "Authorization" header''' + """Static authentication with given "Authorization" header""" def __init__(self, authorization): self.authorization = authorization def __call__(self, request): - request.headers['Authorization'] = self.authorization + request.headers["Authorization"] = self.authorization return request class OAuthTokenAuth(AuthBase): - '''Dynamic authentication using the "tokens" library to load OAuth tokens from file - (potentially mounted from a Kubernetes secret)''' + """Dynamic authentication using the "tokens" library to load OAuth tokens from file + (potentially mounted from a Kubernetes secret)""" def __init__(self, token_name): self.token_name = token_name @@ -47,18 +48,12 @@ class OAuthTokenAuth(AuthBase): def __call__(self, request): token = tokens.get(self.token_name) - request.headers['Authorization'] = f'Bearer {token}' + request.headers["Authorization"] = f"Bearer {token}" return request class Cluster: - def __init__( - self, - id: str, - name: str, - api_server_url: str, - client: HTTPClient - ): + def __init__(self, id: str, name: str, api_server_url: str, client: HTTPClient): self.id = id self.name = name self.api_server_url = api_server_url @@ -66,7 +61,6 @@ class Cluster: class StaticClusterDiscoverer: - def __init__(self, api_server_urls: list): self._clusters = [] @@ -79,15 +73,18 @@ class StaticClusterDiscoverer: config = KubeConfig.from_url(DEFAULT_CLUSTERS) client = HTTPClient(config) cluster = Cluster( - generate_cluster_id(DEFAULT_CLUSTERS), "cluster", DEFAULT_CLUSTERS, client + generate_cluster_id(DEFAULT_CLUSTERS), + "cluster", + DEFAULT_CLUSTERS, + client, ) else: client = HTTPClient(config) cluster = Cluster( - generate_cluster_id(config.cluster['server']), + generate_cluster_id(config.cluster["server"]), "cluster", - config.cluster['server'], - client + config.cluster["server"], + client, ) self._clusters.append(cluster) else: @@ -95,47 +92,43 @@ class StaticClusterDiscoverer: config = KubeConfig.from_url(api_server_url) client = HTTPClient(config) generated_id = generate_cluster_id(api_server_url) - self._clusters.append(Cluster(generated_id, generated_id, api_server_url, client)) + self._clusters.append( + Cluster(generated_id, generated_id, api_server_url, client) + ) def get_clusters(self): return self._clusters class ClusterRegistryDiscoverer: - def __init__(self, cluster_registry_url: str, cache_lifetime=60): self._url = cluster_registry_url self._cache_lifetime = cache_lifetime self._last_cache_refresh = 0 self._clusters = [] self._session = requests.Session() - self._session.auth = OAuthTokenAuth('read-only') + self._session.auth = OAuthTokenAuth("read-only") def refresh(self): try: - response = self._session.get(urljoin(self._url, '/kubernetes-clusters'), timeout=10) + response = self._session.get( + urljoin(self._url, "/kubernetes-clusters"), timeout=10 + ) response.raise_for_status() clusters = [] - for row in response.json()['items']: + for row in response.json()["items"]: # only consider "ready" clusters if row.get("lifecycle_status", "ready") == "ready": - config = KubeConfig.from_url(row['api_server_url']) + config = KubeConfig.from_url(row["api_server_url"]) client = HTTPClient(config) client.session.auth = OAuthTokenAuth("read-only") clusters.append( - Cluster( - row["id"], - row["alias"], - row["api_server_url"], - client - ) + Cluster(row["id"], row["alias"], row["api_server_url"], client) ) self._clusters = clusters self._last_cache_refresh = time.time() except: - logger.exception( - f"Failed to refresh from cluster registry {self._url}" - ) + logger.exception(f"Failed to refresh from cluster registry {self._url}") def get_clusters(self): now = time.time() @@ -145,7 +138,6 @@ class ClusterRegistryDiscoverer: class KubeconfigDiscoverer: - def __init__(self, kubeconfig_path: Path, contexts: set): self._path = kubeconfig_path self._contexts = contexts @@ -162,21 +154,17 @@ class KubeconfigDiscoverer: context_config = KubeConfig(config.doc, context) client = HTTPClient(context_config) cluster = Cluster( - context, - context, - context_config.cluster['server'], - client + context, context, context_config.cluster["server"], client ) yield cluster class MockDiscoverer: - def get_clusters(self): for i in range(3): yield Cluster( f"mock-cluster-{i}", f"mock-cluster-{i}", api_server_url=f"https://kube-{i}.example.org", - client=None + client=None, ) diff --git a/kube_ops_view/kubernetes.py b/kube_ops_view/kubernetes.py index ec6f2f0..d85a8f0 100644 --- a/kube_ops_view/kubernetes.py +++ b/kube_ops_view/kubernetes.py @@ -19,57 +19,67 @@ session = requests.Session() # https://github.com/kubernetes/community/blob/master/contributors/design-proposals/instrumentation/resource-metrics-api.md class NodeMetrics(APIObject): - version = 'metrics.k8s.io/v1beta1' - endpoint = 'nodes' - kind = 'NodeMetrics' + version = "metrics.k8s.io/v1beta1" + endpoint = "nodes" + kind = "NodeMetrics" # https://github.com/kubernetes/community/blob/master/contributors/design-proposals/instrumentation/resource-metrics-api.md class PodMetrics(NamespacedAPIObject): - version = 'metrics.k8s.io/v1beta1' - endpoint = 'pods' - kind = 'PodMetrics' + version = "metrics.k8s.io/v1beta1" + endpoint = "pods" + kind = "PodMetrics" def map_node_status(status: dict): return { - 'addresses': status.get('addresses'), - 'capacity': status.get('capacity'), - 'allocatable': status.get('allocatable') + "addresses": status.get("addresses"), + "capacity": status.get("capacity"), + "allocatable": status.get("allocatable"), } def map_node(node: dict): return { - 'name': node['metadata']['name'], - 'labels': node['metadata']['labels'], - 'status': map_node_status(node['status']), - 'pods': {} + "name": node["metadata"]["name"], + "labels": node["metadata"]["labels"], + "status": map_node_status(node["status"]), + "pods": {}, } def map_pod(pod: dict): return { - 'name': pod['metadata']['name'], - 'namespace': pod['metadata']['namespace'], - 'labels': pod['metadata'].get('labels', {}), - 'phase': pod['status'].get('phase'), - 'startTime': pod['status']['startTime'] if 'startTime' in pod['status'] else '', - 'containers': [] + "name": pod["metadata"]["name"], + "namespace": pod["metadata"]["namespace"], + "labels": pod["metadata"].get("labels", {}), + "phase": pod["status"].get("phase"), + "startTime": pod["status"]["startTime"] if "startTime" in pod["status"] else "", + "containers": [], } def map_container(cont: dict, pod: dict): - obj = {'name': cont['name'], 'image': cont['image'], 'resources': cont['resources']} - status = list([s for s in pod.get('status', {}).get('containerStatuses', []) if s['name'] == cont['name']]) + obj = {"name": cont["name"], "image": cont["image"], "resources": cont["resources"]} + status = list( + [ + s + for s in pod.get("status", {}).get("containerStatuses", []) + if s["name"] == cont["name"] + ] + ) if status: obj.update(**status[0]) return obj def parse_time(s: str): - return datetime.datetime.strptime(s, '%Y-%m-%dT%H:%M:%SZ').replace(tzinfo=datetime.timezone.utc).timestamp() + return ( + datetime.datetime.strptime(s, "%Y-%m-%dT%H:%M:%SZ") + .replace(tzinfo=datetime.timezone.utc) + .timestamp() + ) def query_kubernetes_cluster(cluster): @@ -80,54 +90,66 @@ def query_kubernetes_cluster(cluster): unassigned_pods = {} for node in Node.objects(cluster.client): obj = map_node(node.obj) - nodes[obj['name']] = obj + nodes[obj["name"]] = obj now = time.time() for pod in Pod.objects(cluster.client, namespace=pykube.all): obj = map_pod(pod.obj) - if 'deletionTimestamp' in pod.metadata: - obj['deleted'] = parse_time(pod.metadata['deletionTimestamp']) - for cont in pod.obj['spec']['containers']: - obj['containers'].append(map_container(cont, pod.obj)) - if obj['phase'] in ('Succeeded', 'Failed'): + if "deletionTimestamp" in pod.metadata: + obj["deleted"] = parse_time(pod.metadata["deletionTimestamp"]) + for cont in pod.obj["spec"]["containers"]: + obj["containers"].append(map_container(cont, pod.obj)) + if obj["phase"] in ("Succeeded", "Failed"): last_termination_time = 0 - for container in obj['containers']: - termination_time = container.get('state', {}).get('terminated', {}).get('finishedAt') + for container in obj["containers"]: + termination_time = ( + container.get("state", {}).get("terminated", {}).get("finishedAt") + ) if termination_time: termination_time = parse_time(termination_time) if termination_time > last_termination_time: last_termination_time = termination_time - if (last_termination_time and last_termination_time < now - 3600) or (obj.get('reason') == 'Evicted'): + if (last_termination_time and last_termination_time < now - 3600) or ( + obj.get("reason") == "Evicted" + ): # the job/pod finished more than an hour ago or if it is evicted by cgroup limits # => filter out continue pods_by_namespace_name[(pod.namespace, pod.name)] = obj - pod_key = f'{pod.namespace}/{pod.name}' - node_name = pod.obj['spec'].get('nodeName') + pod_key = f"{pod.namespace}/{pod.name}" + node_name = pod.obj["spec"].get("nodeName") if node_name in nodes: - nodes[node_name]['pods'][pod_key] = obj + nodes[node_name]["pods"][pod_key] = obj else: unassigned_pods[pod_key] = obj try: for node_metrics in NodeMetrics.objects(cluster.client): key = node_metrics.name - nodes[key]['usage'] = node_metrics.obj.get('usage', {}) + nodes[key]["usage"] = node_metrics.obj.get("usage", {}) except Exception as e: - logger.warning('Failed to query node metrics {}: {}'.format(cluster.id, get_short_error_message(e))) + logger.warning( + "Failed to query node metrics {}: {}".format( + cluster.id, get_short_error_message(e) + ) + ) try: for pod_metrics in PodMetrics.objects(cluster.client, namespace=pykube.all): key = (pod_metrics.namespace, pod_metrics.name) pod = pods_by_namespace_name.get(key) if pod: - for container in pod['containers']: - for container_metrics in pod_metrics.obj.get('containers', []): - if container['name'] == container_metrics['name']: - container['resources']['usage'] = container_metrics['usage'] + for container in pod["containers"]: + for container_metrics in pod_metrics.obj.get("containers", []): + if container["name"] == container_metrics["name"]: + container["resources"]["usage"] = container_metrics["usage"] except Exception as e: - logger.warning('Failed to query pod metrics for cluster {}: {}'.format(cluster.id, get_short_error_message(e))) + logger.warning( + "Failed to query pod metrics for cluster {}: {}".format( + cluster.id, get_short_error_message(e) + ) + ) return { - 'id': cluster_id, - 'api_server_url': api_server_url, - 'nodes': nodes, - 'unassigned_pods': unassigned_pods + "id": cluster_id, + "api_server_url": api_server_url, + "nodes": nodes, + "unassigned_pods": unassigned_pods, } diff --git a/kube_ops_view/main.py b/kube_ops_view/main.py index 78dbde8..e9cd2ff 100644 --- a/kube_ops_view/main.py +++ b/kube_ops_view/main.py @@ -24,25 +24,32 @@ from urllib.parse import urljoin from .mock import query_mock_cluster from .kubernetes import query_kubernetes_cluster from .stores import MemoryStore, RedisStore -from .cluster_discovery import DEFAULT_CLUSTERS, StaticClusterDiscoverer, ClusterRegistryDiscoverer, KubeconfigDiscoverer, MockDiscoverer +from .cluster_discovery import ( + DEFAULT_CLUSTERS, + StaticClusterDiscoverer, + ClusterRegistryDiscoverer, + KubeconfigDiscoverer, + MockDiscoverer, +) from .update import update_clusters logger = logging.getLogger(__name__) -SERVER_STATUS = {'shutdown': False} -AUTHORIZE_URL = os.getenv('AUTHORIZE_URL') -ACCESS_TOKEN_URL = os.getenv('ACCESS_TOKEN_URL') -APP_URL = os.getenv('APP_URL') -SCOPE = os.getenv('SCOPE') +SERVER_STATUS = {"shutdown": False} +AUTHORIZE_URL = os.getenv("AUTHORIZE_URL") +ACCESS_TOKEN_URL = os.getenv("ACCESS_TOKEN_URL") +APP_URL = os.getenv("APP_URL") +SCOPE = os.getenv("SCOPE") app = Flask(__name__) oauth_blueprint = OAuth2ConsumerBlueprintWithClientRefresh( - "oauth", __name__, + "oauth", + __name__, authorization_url=AUTHORIZE_URL, token_url=ACCESS_TOKEN_URL, - scope=SCOPE + scope=SCOPE, ) app.register_blueprint(oauth_blueprint, url_prefix="/login") @@ -50,35 +57,48 @@ app.register_blueprint(oauth_blueprint, url_prefix="/login") def authorize(f): @functools.wraps(f) def wrapper(*args, **kwargs): - if AUTHORIZE_URL and 'auth_token' not in flask.session and not oauth_blueprint.session.authorized: - return redirect(url_for('oauth.login')) + if ( + AUTHORIZE_URL + and "auth_token" not in flask.session + and not oauth_blueprint.session.authorized + ): + return redirect(url_for("oauth.login")) return f(*args, **kwargs) return wrapper -@app.route('/health') +@app.route("/health") def health(): - if SERVER_STATUS['shutdown']: + if SERVER_STATUS["shutdown"]: flask.abort(503) else: - return 'OK' + return "OK" -@app.route('/') +@app.route("/") @authorize def index(): - static_build_path = Path(__file__).parent / 'static' / 'build' - candidates = sorted(static_build_path.glob('app*.js')) + static_build_path = Path(__file__).parent / "static" / "build" + candidates = sorted(static_build_path.glob("app*.js")) if candidates: app_js = candidates[0].name if app.debug: # cache busting for local development - app_js += '?_={}'.format(time.time()) + app_js += "?_={}".format(time.time()) else: - logger.error('Could not find JavaScript application bundle app*.js in {}'.format(static_build_path)) - flask.abort(503, 'JavaScript application bundle not found (missing build)') - return flask.render_template('index.html', app_js=app_js, version=kube_ops_view.__version__, app_config_json=json.dumps(app.app_config)) + logger.error( + "Could not find JavaScript application bundle app*.js in {}".format( + static_build_path + ) + ) + flask.abort(503, "JavaScript application bundle not found (missing build)") + return flask.render_template( + "index.html", + app_js=app_js, + version=kube_ops_view.__version__, + app_config_json=json.dumps(app.app_config), + ) def event(cluster_ids: set): @@ -89,55 +109,72 @@ def event(cluster_ids: set): if status: # send the cluster status including last_query_time BEFORE the cluster data # so the UI knows how to render correctly from the start - yield 'event: clusterstatus\ndata: ' + json.dumps({'cluster_id': cluster_id, 'status': status}, separators=(',', ':')) + '\n\n' + yield "event: clusterstatus\ndata: " + json.dumps( + {"cluster_id": cluster_id, "status": status}, separators=(",", ":") + ) + "\n\n" cluster = app.store.get_cluster_data(cluster_id) if cluster: - yield 'event: clusterupdate\ndata: ' + json.dumps(cluster, separators=(',', ':')) + '\n\n' - yield 'event: bootstrapend\ndata: \n\n' + yield "event: clusterupdate\ndata: " + json.dumps( + cluster, separators=(",", ":") + ) + "\n\n" + yield "event: bootstrapend\ndata: \n\n" while True: for event_type, event_data in app.store.listen(): # hacky, event_data can be delta or full cluster object - if not cluster_ids or event_data.get('cluster_id', event_data.get('id')) in cluster_ids: - yield 'event: ' + event_type + '\ndata: ' + json.dumps(event_data, separators=(',', ':')) + '\n\n' + if ( + not cluster_ids + or event_data.get("cluster_id", event_data.get("id")) in cluster_ids + ): + yield "event: " + event_type + "\ndata: " + json.dumps( + event_data, separators=(",", ":") + ) + "\n\n" -@app.route('/events') +@app.route("/events") @authorize def get_events(): - '''SSE (Server Side Events), for an EventSource''' + """SSE (Server Side Events), for an EventSource""" cluster_ids = set() - for _id in flask.request.args.get('cluster_ids', '').split(): + for _id in flask.request.args.get("cluster_ids", "").split(): if _id: cluster_ids.add(_id) - return flask.Response(event(cluster_ids), mimetype='text/event-stream', headers={'Cache-Control': 'no-cache', 'X-Accel-Buffering': 'no'}) + return flask.Response( + event(cluster_ids), + mimetype="text/event-stream", + headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"}, + ) -@app.route('/screen-tokens', methods=['GET', 'POST']) +@app.route("/screen-tokens", methods=["GET", "POST"]) @authorize def screen_tokens(): new_token = None - if flask.request.method == 'POST': + if flask.request.method == "POST": new_token = app.store.create_screen_token() - return flask.render_template('screen-tokens.html', new_token=new_token) + return flask.render_template("screen-tokens.html", new_token=new_token) -@app.route('/screen/') +@app.route("/screen/") def redeem_screen_token(token: str): - remote_addr = flask.request.headers.get('X-Forwarded-For') or flask.request.remote_addr - logger.info('Trying to redeem screen token "{}" for IP {}..'.format(token, remote_addr)) + remote_addr = ( + flask.request.headers.get("X-Forwarded-For") or flask.request.remote_addr + ) + logger.info( + 'Trying to redeem screen token "{}" for IP {}..'.format(token, remote_addr) + ) try: app.store.redeem_screen_token(token, remote_addr) except: flask.abort(401) - flask.session['auth_token'] = (token, '') - return redirect(urljoin(APP_URL, '/')) + flask.session["auth_token"] = (token, "") + return redirect(urljoin(APP_URL, "/")) -@app.route('/logout') +@app.route("/logout") def logout(): - flask.session.pop('auth_token', None) - return redirect(urljoin(APP_URL, '/')) + flask.session.pop("auth_token", None) + return redirect(urljoin(APP_URL, "/")) def shutdown(): @@ -150,48 +187,120 @@ def shutdown(): def exit_gracefully(signum, frame): - logger.info('Received TERM signal, shutting down..') - SERVER_STATUS['shutdown'] = True + logger.info("Received TERM signal, shutting down..") + SERVER_STATUS["shutdown"] = True gevent.spawn(shutdown) def print_version(ctx, param, value): if not value or ctx.resilient_parsing: return - click.echo('Kubernetes Operational View {}'.format(kube_ops_view.__version__)) + click.echo("Kubernetes Operational View {}".format(kube_ops_view.__version__)) ctx.exit() class CommaSeparatedValues(click.ParamType): - name = 'comma_separated_values' + name = "comma_separated_values" def convert(self, value, param, ctx): if isinstance(value, str): - values = filter(None, value.split(',')) + values = filter(None, value.split(",")) else: values = value return values -@click.command(context_settings={'help_option_names': ['-h', '--help']}) -@click.option('-V', '--version', is_flag=True, callback=print_version, expose_value=False, is_eager=True, - help='Print the current version number and exit.') -@click.option('-p', '--port', type=int, help='HTTP port to listen on (default: 8080)', envvar='SERVER_PORT', default=8080) -@click.option('-d', '--debug', is_flag=True, help='Run in debugging mode', envvar='DEBUG') -@click.option('-m', '--mock', is_flag=True, help='Mock Kubernetes clusters', envvar='MOCK') -@click.option('--secret-key', help='Secret key for session cookies', envvar='SECRET_KEY', default='development') -@click.option('--redis-url', help='Redis URL to use for pub/sub and job locking', envvar='REDIS_URL') -@click.option('--clusters', type=CommaSeparatedValues(), - help='Comma separated list of Kubernetes API server URLs (default: {})'.format(DEFAULT_CLUSTERS), envvar='CLUSTERS') -@click.option('--cluster-registry-url', help='URL to cluster registry', envvar='CLUSTER_REGISTRY_URL') -@click.option('--kubeconfig-path', type=click.Path(exists=True), help='Path to kubeconfig file', envvar='KUBECONFIG_PATH') -@click.option('--kubeconfig-contexts', type=CommaSeparatedValues(), - help='List of kubeconfig contexts to use (default: use all defined contexts)', envvar='KUBECONFIG_CONTEXTS') -@click.option('--query-interval', type=float, help='Interval in seconds for querying clusters (default: 5)', envvar='QUERY_INTERVAL', default=5) -@click.option('--node-link-url-template', help='Template for target URL when clicking on a Node', envvar='NODE_LINK_URL_TEMPLATE') -@click.option('--pod-link-url-template', help='Template for target URL when clicking on a Pod', envvar='POD_LINK_URL_TEMPLATE') -def main(port, debug, mock, secret_key, redis_url, clusters: list, cluster_registry_url, kubeconfig_path, kubeconfig_contexts: list, query_interval, - node_link_url_template: str, pod_link_url_template: str): +@click.command(context_settings={"help_option_names": ["-h", "--help"]}) +@click.option( + "-V", + "--version", + is_flag=True, + callback=print_version, + expose_value=False, + is_eager=True, + help="Print the current version number and exit.", +) +@click.option( + "-p", + "--port", + type=int, + help="HTTP port to listen on (default: 8080)", + envvar="SERVER_PORT", + default=8080, +) +@click.option( + "-d", "--debug", is_flag=True, help="Run in debugging mode", envvar="DEBUG" +) +@click.option( + "-m", "--mock", is_flag=True, help="Mock Kubernetes clusters", envvar="MOCK" +) +@click.option( + "--secret-key", + help="Secret key for session cookies", + envvar="SECRET_KEY", + default="development", +) +@click.option( + "--redis-url", + help="Redis URL to use for pub/sub and job locking", + envvar="REDIS_URL", +) +@click.option( + "--clusters", + type=CommaSeparatedValues(), + help="Comma separated list of Kubernetes API server URLs (default: {})".format( + DEFAULT_CLUSTERS + ), + envvar="CLUSTERS", +) +@click.option( + "--cluster-registry-url", + help="URL to cluster registry", + envvar="CLUSTER_REGISTRY_URL", +) +@click.option( + "--kubeconfig-path", + type=click.Path(exists=True), + help="Path to kubeconfig file", + envvar="KUBECONFIG_PATH", +) +@click.option( + "--kubeconfig-contexts", + type=CommaSeparatedValues(), + help="List of kubeconfig contexts to use (default: use all defined contexts)", + envvar="KUBECONFIG_CONTEXTS", +) +@click.option( + "--query-interval", + type=float, + help="Interval in seconds for querying clusters (default: 5)", + envvar="QUERY_INTERVAL", + default=5, +) +@click.option( + "--node-link-url-template", + help="Template for target URL when clicking on a Node", + envvar="NODE_LINK_URL_TEMPLATE", +) +@click.option( + "--pod-link-url-template", + help="Template for target URL when clicking on a Pod", + envvar="POD_LINK_URL_TEMPLATE", +) +def main( + port, + debug, + mock, + secret_key, + redis_url, + clusters: list, + cluster_registry_url, + kubeconfig_path, + kubeconfig_contexts: list, + query_interval, + node_link_url_template: str, + pod_link_url_template: str, +): logging.basicConfig(level=logging.DEBUG if debug else logging.INFO) store = RedisStore(redis_url) if redis_url else MemoryStore() @@ -199,7 +308,10 @@ def main(port, debug, mock, secret_key, redis_url, clusters: list, cluster_regis app.debug = debug app.secret_key = secret_key app.store = store - app.app_config = {'node_link_url_template': node_link_url_template, 'pod_link_url_template': pod_link_url_template} + app.app_config = { + "node_link_url_template": node_link_url_template, + "pod_link_url_template": pod_link_url_template, + } if mock: cluster_query = query_mock_cluster @@ -209,14 +321,23 @@ def main(port, debug, mock, secret_key, redis_url, clusters: list, cluster_regis if cluster_registry_url: discoverer = ClusterRegistryDiscoverer(cluster_registry_url) elif kubeconfig_path: - discoverer = KubeconfigDiscoverer(Path(kubeconfig_path), set(kubeconfig_contexts or [])) + discoverer = KubeconfigDiscoverer( + Path(kubeconfig_path), set(kubeconfig_contexts or []) + ) else: api_server_urls = clusters or [] discoverer = StaticClusterDiscoverer(api_server_urls) - gevent.spawn(update_clusters, cluster_discoverer=discoverer, query_cluster=cluster_query, store=store, query_interval=query_interval, debug=debug) + gevent.spawn( + update_clusters, + cluster_discoverer=discoverer, + query_cluster=cluster_query, + store=store, + query_interval=query_interval, + debug=debug, + ) signal.signal(signal.SIGTERM, exit_gracefully) - http_server = gevent.pywsgi.WSGIServer(('0.0.0.0', port), app) - logger.info('Listening on :{}..'.format(port)) + http_server = gevent.pywsgi.WSGIServer(("0.0.0.0", port), app) + logger.info("Listening on :{}..".format(port)) http_server.serve_forever() diff --git a/kube_ops_view/mock.py b/kube_ops_view/mock.py index 950262a..e749d38 100644 --- a/kube_ops_view/mock.py +++ b/kube_ops_view/mock.py @@ -4,34 +4,33 @@ import string def hash_int(x: int): - x = ((x >> 16) ^ x) * 0x45d9f3b - x = ((x >> 16) ^ x) * 0x45d9f3b + x = ((x >> 16) ^ x) * 0x45D9F3B + x = ((x >> 16) ^ x) * 0x45D9F3B x = (x >> 16) ^ x return x def generate_mock_pod(index: int, i: int, j: int): names = [ - 'agent-cooper', - 'black-lodge', - 'bob', - 'bobby-briggs', - 'laura-palmer', - 'leland-palmer', - 'log-lady', - 'sheriff-truman', + "agent-cooper", + "black-lodge", + "bob", + "bobby-briggs", + "laura-palmer", + "leland-palmer", + "log-lady", + "sheriff-truman", ] - labels = { - 'env': ['prod', 'dev'], - 'owner': ['x-wing', 'iris'] - } - pod_phases = ['Pending', 'Running', 'Running', 'Failed'] + labels = {"env": ["prod", "dev"], "owner": ["x-wing", "iris"]} + pod_phases = ["Pending", "Running", "Running", "Failed"] pod_labels = {} for li, k in enumerate(labels): v = labels[k] - label_choice = hash_int((index + 1) * (i + 1) * (j + 1) * (li + 1)) % (len(v) + 1) - if(label_choice != 0): + label_choice = hash_int((index + 1) * (i + 1) * (j + 1) * (li + 1)) % ( + len(v) + 1 + ) + if label_choice != 0: pod_labels[k] = v[label_choice - 1] phase = pod_phases[hash_int((index + 1) * (i + 1) * (j + 1)) % len(pod_phases)] @@ -44,41 +43,53 @@ def generate_mock_pod(index: int, i: int, j: int): usage_cpu = max(requests_cpu + random.randint(-30, 30), 1) usage_memory = max(requests_memory + random.randint(-64, 128), 1) container = { - 'name': 'myapp', - 'image': 'foo/bar/{}'.format(j), - 'resources': { - 'requests': {'cpu': f'{requests_cpu}m', 'memory': f'{requests_memory}Mi'}, - 'limits': {}, - 'usage': {'cpu': f'{usage_cpu}m', 'memory': f'{usage_memory}Mi'}, + "name": "myapp", + "image": "foo/bar/{}".format(j), + "resources": { + "requests": { + "cpu": f"{requests_cpu}m", + "memory": f"{requests_memory}Mi", + }, + "limits": {}, + "usage": {"cpu": f"{usage_cpu}m", "memory": f"{usage_memory}Mi"}, }, - 'ready': True, - 'state': {'running': {}} + "ready": True, + "state": {"running": {}}, } - if phase == 'Running': + if phase == "Running": if j % 13 == 0: - container.update(**{'ready': False, 'state': {'waiting': {'reason': 'CrashLoopBackOff'}}}) + container.update( + **{ + "ready": False, + "state": {"waiting": {"reason": "CrashLoopBackOff"}}, + } + ) elif j % 7 == 0: - container.update(**{'ready': False, 'state': {'running': {}}, 'restartCount': 3}) - elif phase == 'Failed': - del container['state'] - del container['ready'] + container.update( + **{"ready": False, "state": {"running": {}}, "restartCount": 3} + ) + elif phase == "Failed": + del container["state"] + del container["ready"] containers.append(container) pod = { - 'name': '{}-{}-{}'.format(names[hash_int((i + 1) * (j + 1)) % len(names)], i, j), - 'namespace': 'kube-system' if j < 3 else 'default', - 'labels': pod_labels, - 'phase': phase, - 'containers': containers + "name": "{}-{}-{}".format( + names[hash_int((i + 1) * (j + 1)) % len(names)], i, j + ), + "namespace": "kube-system" if j < 3 else "default", + "labels": pod_labels, + "phase": phase, + "containers": containers, } - if phase == 'Running' and j % 17 == 0: - pod['deleted'] = 123 + if phase == "Running" and j % 17 == 0: + pod["deleted"] = 123 return pod def query_mock_cluster(cluster): - '''Generate deterministic (no randomness!) mock data''' - index = int(cluster.id.split('-')[-1]) + """Generate deterministic (no randomness!) mock data""" + index = int(cluster.id.split("-")[-1]) nodes = {} for i in range(10): # add/remove the second to last node every 13 seconds @@ -88,11 +99,11 @@ def query_mock_cluster(cluster): # only the first two clusters have master nodes if i < 2 and index < 2: if index == 0: - labels['kubernetes.io/role'] = 'master' + labels["kubernetes.io/role"] = "master" elif index == 1: - labels['node-role.kubernetes.io/master'] = '' + labels["node-role.kubernetes.io/master"] = "" else: - labels['master'] = 'true' + labels["master"] = "true" pods = {} for j in range(hash_int((index + 1) * (i + 1)) % 32): # add/remove some pods every 7 seconds @@ -100,7 +111,7 @@ def query_mock_cluster(cluster): pass else: pod = generate_mock_pod(index, i, j) - pods['{}/{}'.format(pod['namespace'], pod['name'])] = pod + pods["{}/{}".format(pod["namespace"], pod["name"])] = pod # use data from containers (usage) usage_cpu = 0 @@ -111,27 +122,27 @@ def query_mock_cluster(cluster): usage_memory += int(c["resources"]["usage"]["memory"].split("Mi")[0]) # generate longer name for a node - suffix = ''.join( + suffix = "".join( [random.choice(string.ascii_letters) for n in range(random.randint(1, 20))] ) node = { - 'name': f'node-{i}-{suffix}', - 'labels': labels, - 'status': { - 'capacity': {'cpu': '8', 'memory': '64Gi', 'pods': '110'}, - 'allocatable': {'cpu': '7800m', 'memory': '62Gi'} + "name": f"node-{i}-{suffix}", + "labels": labels, + "status": { + "capacity": {"cpu": "8", "memory": "64Gi", "pods": "110"}, + "allocatable": {"cpu": "7800m", "memory": "62Gi"}, }, - 'pods': pods, + "pods": pods, # get data from containers (usage) - 'usage': {'cpu': f'{usage_cpu}m', 'memory': f'{usage_memory}Mi'} + "usage": {"cpu": f"{usage_cpu}m", "memory": f"{usage_memory}Mi"}, } - nodes[node['name']] = node + nodes[node["name"]] = node pod = generate_mock_pod(index, 11, index) - unassigned_pods = {'{}/{}'.format(pod['namespace'], pod['name']): pod} + unassigned_pods = {"{}/{}".format(pod["namespace"], pod["name"]): pod} return { - 'id': 'mock-cluster-{}'.format(index), - 'api_server_url': 'https://kube-{}.example.org'.format(index), - 'nodes': nodes, - 'unassigned_pods': unassigned_pods + "id": "mock-cluster-{}".format(index), + "api_server_url": "https://kube-{}.example.org".format(index), + "nodes": nodes, + "unassigned_pods": unassigned_pods, } diff --git a/kube_ops_view/oauth.py b/kube_ops_view/oauth.py index 48380e3..5fb2579 100644 --- a/kube_ops_view/oauth.py +++ b/kube_ops_view/oauth.py @@ -3,17 +3,17 @@ import os from flask_dance.consumer import OAuth2ConsumerBlueprint -CREDENTIALS_DIR = os.getenv('CREDENTIALS_DIR', '') +CREDENTIALS_DIR = os.getenv("CREDENTIALS_DIR", "") class OAuth2ConsumerBlueprintWithClientRefresh(OAuth2ConsumerBlueprint): - '''Same as flask_dance.consumer.OAuth2ConsumerBlueprint, but loads client credentials from file''' + """Same as flask_dance.consumer.OAuth2ConsumerBlueprint, but loads client credentials from file""" def refresh_credentials(self): - with open(os.path.join(CREDENTIALS_DIR, 'authcode-client-id')) as fd: + with open(os.path.join(CREDENTIALS_DIR, "authcode-client-id")) as fd: # note that we need to set two attributes because of how OAuth2ConsumerBlueprint works :-/ self._client_id = self.client_id = fd.read().strip() - with open(os.path.join(CREDENTIALS_DIR, 'authcode-client-secret')) as fd: + with open(os.path.join(CREDENTIALS_DIR, "authcode-client-secret")) as fd: self.client_secret = fd.read().strip() def login(self): diff --git a/kube_ops_view/stores.py b/kube_ops_view/stores.py index 0ed3b4f..e5028dc 100644 --- a/kube_ops_view/stores.py +++ b/kube_ops_view/stores.py @@ -14,52 +14,55 @@ ONE_YEAR = 3600 * 24 * 365 def generate_token(n: int): - '''Generate a random ASCII token of length n''' + """Generate a random ASCII token of length n""" # uses os.urandom() rng = random.SystemRandom() - return ''.join([rng.choice(string.ascii_letters + string.digits) for i in range(n)]) + return "".join([rng.choice(string.ascii_letters + string.digits) for i in range(n)]) def generate_token_data(): - '''Generate screen token data for storing''' + """Generate screen token data for storing""" token = generate_token(10) now = time.time() - return {'token': token, 'created': now, 'expires': now + ONE_YEAR} + return {"token": token, "created": now, "expires": now + ONE_YEAR} def check_token(token: str, remote_addr: str, data: dict): - '''Check whether the given screen token is valid, raises exception if not''' + """Check whether the given screen token is valid, raises exception if not""" now = time.time() - if data and now < data['expires'] and data.get('remote_addr', remote_addr) == remote_addr: - data['remote_addr'] = remote_addr + if ( + data + and now < data["expires"] + and data.get("remote_addr", remote_addr) == remote_addr + ): + data["remote_addr"] = remote_addr return data else: - raise ValueError('Invalid token') + raise ValueError("Invalid token") class AbstractStore: - def get_cluster_ids(self): - return self.get('cluster-ids') or [] + return self.get("cluster-ids") or [] def set_cluster_ids(self, cluster_ids: set): - self.set('cluster-ids', list(sorted(cluster_ids))) + self.set("cluster-ids", list(sorted(cluster_ids))) def get_cluster_status(self, cluster_id: str) -> dict: - return self.get('clusters:{}:status'.format(cluster_id)) or {} + return self.get("clusters:{}:status".format(cluster_id)) or {} def set_cluster_status(self, cluster_id: str, status: dict): - self.set('clusters:{}:status'.format(cluster_id), status) + self.set("clusters:{}:status".format(cluster_id), status) def get_cluster_data(self, cluster_id: str) -> dict: - return self.get('clusters:{}:data'.format(cluster_id)) or {} + return self.get("clusters:{}:data".format(cluster_id)) or {} def set_cluster_data(self, cluster_id: str, data: dict): - self.set('clusters:{}:data'.format(cluster_id), data) + self.set("clusters:{}:data".format(cluster_id), data) class MemoryStore(AbstractStore): - '''Memory-only backend, mostly useful for local debugging''' + """Memory-only backend, mostly useful for local debugging""" def __init__(self): self._data = {} @@ -74,7 +77,7 @@ class MemoryStore(AbstractStore): def acquire_lock(self): # no-op for memory store - return 'fake-lock' + return "fake-lock" def release_lock(self, lock): # no op for memory store @@ -96,7 +99,7 @@ class MemoryStore(AbstractStore): def create_screen_token(self): data = generate_token_data() - token = data['token'] + token = data["token"] self._screen_tokens[token] = data return token @@ -107,51 +110,54 @@ class MemoryStore(AbstractStore): class RedisStore(AbstractStore): - '''Redis-based backend for deployments with replicas > 1''' + """Redis-based backend for deployments with replicas > 1""" def __init__(self, url: str): - logger.info('Connecting to Redis on {}..'.format(url)) + logger.info("Connecting to Redis on {}..".format(url)) self._redis = redis.StrictRedis.from_url(url) self._redlock = Redlock([url]) def set(self, key, value): - self._redis.set(key, json.dumps(value, separators=(',', ':'))) + self._redis.set(key, json.dumps(value, separators=(",", ":"))) def get(self, key): value = self._redis.get(key) if value: - return json.loads(value.decode('utf-8')) + return json.loads(value.decode("utf-8")) def acquire_lock(self): - return self._redlock.lock('update', 10000) + return self._redlock.lock("update", 10000) def release_lock(self, lock): self._redlock.unlock(lock) def publish(self, event_type, event_data): - self._redis.publish('default', '{}:{}'.format(event_type, json.dumps(event_data, separators=(',', ':')))) + self._redis.publish( + "default", + "{}:{}".format(event_type, json.dumps(event_data, separators=(",", ":"))), + ) def listen(self): p = self._redis.pubsub() - p.subscribe('default') + p.subscribe("default") for message in p.listen(): - if message['type'] == 'message': - event_type, data = message['data'].decode('utf-8').split(':', 1) + if message["type"] == "message": + event_type, data = message["data"].decode("utf-8").split(":", 1) yield (event_type, json.loads(data)) def create_screen_token(self): - '''Generate a new screen token and store it in Redis''' + """Generate a new screen token and store it in Redis""" data = generate_token_data() - token = data['token'] - self._redis.set('screen-tokens:{}'.format(token), json.dumps(data)) + token = data["token"] + self._redis.set("screen-tokens:{}".format(token), json.dumps(data)) return token def redeem_screen_token(self, token: str, remote_addr: str): - '''Validate the given token and bind it to the IP''' - redis_key = 'screen-tokens:{}'.format(token) + """Validate the given token and bind it to the IP""" + redis_key = "screen-tokens:{}".format(token) data = self._redis.get(redis_key) if not data: - raise ValueError('Invalid token') - data = json.loads(data.decode('utf-8')) + raise ValueError("Invalid token") + data = json.loads(data.decode("utf-8")) data = check_token(token, remote_addr, data) self._redis.set(redis_key, json.dumps(data)) diff --git a/kube_ops_view/update.py b/kube_ops_view/update.py index a1ec92f..eee01a5 100644 --- a/kube_ops_view/update.py +++ b/kube_ops_view/update.py @@ -18,21 +18,30 @@ def calculate_backoff(tries: int): def handle_query_failure(e: Exception, cluster, backoff: dict): if not backoff: backoff = {} - tries = backoff.get('tries', 0) + 1 - backoff['tries'] = tries + tries = backoff.get("tries", 0) + 1 + backoff["tries"] = tries wait_seconds = calculate_backoff(tries) - backoff['next_try'] = time.time() + wait_seconds + backoff["next_try"] = time.time() + wait_seconds message = get_short_error_message(e) if isinstance(e, requests.exceptions.RequestException): log = logger.error else: log = logger.exception - log('Failed to query cluster {} ({}): {} (try {}, wait {} seconds)'.format( - cluster.id, cluster.api_server_url, message, tries, round(wait_seconds))) + log( + "Failed to query cluster {} ({}): {} (try {}, wait {} seconds)".format( + cluster.id, cluster.api_server_url, message, tries, round(wait_seconds) + ) + ) return backoff -def update_clusters(cluster_discoverer, query_cluster: callable, store, query_interval: float=5, debug: bool=False): +def update_clusters( + cluster_discoverer, + query_cluster: callable, + store, + query_interval: float = 5, + debug: bool = False, +): while True: lock = store.acquire_lock() if lock: @@ -43,42 +52,65 @@ def update_clusters(cluster_discoverer, query_cluster: callable, store, query_in cluster_ids.add(cluster.id) status = store.get_cluster_status(cluster.id) now = time.time() - if now < status.get('last_query_time', 0) + query_interval: + if now < status.get("last_query_time", 0) + query_interval: continue - backoff = status.get('backoff') - if backoff and now < backoff['next_try']: + backoff = status.get("backoff") + if backoff and now < backoff["next_try"]: # cluster is still in backoff, skip continue try: - logger.debug('Querying cluster {} ({})..'.format(cluster.id, cluster.api_server_url)) + logger.debug( + "Querying cluster {} ({})..".format( + cluster.id, cluster.api_server_url + ) + ) data = query_cluster(cluster) except Exception as e: backoff = handle_query_failure(e, cluster, backoff) - status['backoff'] = backoff - store.publish('clusterstatus', {'cluster_id': cluster.id, 'status': status}) + status["backoff"] = backoff + store.publish( + "clusterstatus", + {"cluster_id": cluster.id, "status": status}, + ) else: - status['last_query_time'] = now + status["last_query_time"] = now if backoff: - logger.info('Cluster {} ({}) recovered after {} tries.'.format(cluster.id, cluster.api_server_url, backoff['tries'])) - del status['backoff'] - old_data = store.get_cluster_data(data['id']) + logger.info( + "Cluster {} ({}) recovered after {} tries.".format( + cluster.id, cluster.api_server_url, backoff["tries"] + ) + ) + del status["backoff"] + old_data = store.get_cluster_data(data["id"]) if old_data: # https://pikacode.com/phijaro/json_delta/ticket/11/ # diff is extremely slow without array_align=False - delta = json_delta.diff(old_data, data, verbose=debug, array_align=False) - store.publish('clusterdelta', {'cluster_id': cluster.id, 'delta': delta}) + delta = json_delta.diff( + old_data, data, verbose=debug, array_align=False + ) + store.publish( + "clusterdelta", + {"cluster_id": cluster.id, "delta": delta}, + ) if delta: store.set_cluster_data(cluster.id, data) else: - logger.info('Discovered new cluster {} ({}).'.format(cluster.id, cluster.api_server_url)) + logger.info( + "Discovered new cluster {} ({}).".format( + cluster.id, cluster.api_server_url + ) + ) # first send status with last_query_time! - store.publish('clusterstatus', {'cluster_id': cluster.id, 'status': status}) - store.publish('clusterupdate', data) + store.publish( + "clusterstatus", + {"cluster_id": cluster.id, "status": status}, + ) + store.publish("clusterupdate", data) store.set_cluster_data(cluster.id, data) store.set_cluster_status(cluster.id, status) store.set_cluster_ids(cluster_ids) except: - logger.exception('Failed to update') + logger.exception("Failed to update") finally: store.release_lock(lock) # sleep 1-2 seconds diff --git a/kube_ops_view/utils.py b/kube_ops_view/utils.py index f2a9f93..3d3cc13 100644 --- a/kube_ops_view/utils.py +++ b/kube_ops_view/utils.py @@ -2,11 +2,11 @@ import requests.exceptions def get_short_error_message(e: Exception): - '''Generate a reasonable short message why the HTTP request failed''' + """Generate a reasonable short message why the HTTP request failed""" if isinstance(e, requests.exceptions.RequestException) and e.response is not None: # e.g. "401 Unauthorized" - return '{} {}'.format(e.response.status_code, e.response.reason) + return "{} {}".format(e.response.status_code, e.response.reason) elif isinstance(e, requests.exceptions.ConnectionError): # e.g. "ConnectionError" or "ConnectTimeout" return e.__class__.__name__ diff --git a/pipenv-install.py b/pipenv-install.py deleted file mode 100755 index d790d61..0000000 --- a/pipenv-install.py +++ /dev/null @@ -1,15 +0,0 @@ -#!/usr/bin/env python3 -""" -Helper script for Docker build to install packages from Pipfile.lock without installing Pipenv -""" -import json -import subprocess - -with open("Pipfile.lock") as fd: - data = json.load(fd) - -packages = [] -for k, v in data["default"].items(): - packages.append(k + v["version"]) - -subprocess.run(["pip3", "install"] + packages, check=True) diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..4feb3cf --- /dev/null +++ b/poetry.lock @@ -0,0 +1,1056 @@ +[[package]] +category = "dev" +description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +name = "appdirs" +optional = false +python-versions = "*" +version = "1.4.3" + +[[package]] +category = "dev" +description = "Atomic file writes." +marker = "sys_platform == \"win32\"" +name = "atomicwrites" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "1.3.0" + +[[package]] +category = "dev" +description = "Classes Without Boilerplate" +name = "attrs" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "19.3.0" + +[package.extras] +azure-pipelines = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "pytest-azurepipelines"] +dev = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "sphinx", "pre-commit"] +docs = ["sphinx", "zope.interface"] +tests = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface"] + +[[package]] +category = "dev" +description = "The uncompromising code formatter." +name = "black" +optional = false +python-versions = ">=3.6" +version = "19.10b0" + +[package.dependencies] +appdirs = "*" +attrs = ">=18.1.0" +click = ">=6.5" +pathspec = ">=0.6,<1" +regex = "*" +toml = ">=0.9.4" +typed-ast = ">=1.4.0" + +[package.extras] +d = ["aiohttp (>=3.3.2)", "aiohttp-cors"] + +[[package]] +category = "main" +description = "Python package for providing Mozilla's CA Bundle." +name = "certifi" +optional = false +python-versions = "*" +version = "2019.11.28" + +[[package]] +category = "main" +description = "Foreign Function Interface for Python calling C code." +marker = "sys_platform == \"win32\" and platform_python_implementation == \"CPython\"" +name = "cffi" +optional = false +python-versions = "*" +version = "1.13.2" + +[package.dependencies] +pycparser = "*" + +[[package]] +category = "main" +description = "Universal encoding detector for Python 2 and 3" +name = "chardet" +optional = false +python-versions = "*" +version = "3.0.4" + +[[package]] +category = "main" +description = "Composable command line interface toolkit" +name = "click" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "7.0" + +[[package]] +category = "dev" +description = "Cross-platform colored terminal text." +marker = "sys_platform == \"win32\"" +name = "colorama" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +version = "0.4.3" + +[[package]] +category = "dev" +description = "Code coverage measurement for Python" +name = "coverage" +optional = false +python-versions = "*" +version = "4.4.2" + +[[package]] +category = "dev" +description = "Show coverage stats online via coveralls.io" +name = "coveralls" +optional = false +python-versions = "*" +version = "1.9.2" + +[package.dependencies] +coverage = ">=3.6,<5.0" +docopt = ">=0.6.1" +requests = ">=1.0.0" + +[package.extras] +yaml = ["PyYAML (>=3.10)"] + +[[package]] +category = "dev" +description = "Pythonic argument parser, that will make you smile" +name = "docopt" +optional = false +python-versions = "*" +version = "0.6.2" + +[[package]] +category = "dev" +description = "Discover and load entry points from installed packages." +name = "entrypoints" +optional = false +python-versions = ">=2.7" +version = "0.3" + +[[package]] +category = "dev" +description = "the modular source code checker: pep8, pyflakes and co" +name = "flake8" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "3.7.9" + +[package.dependencies] +entrypoints = ">=0.3.0,<0.4.0" +mccabe = ">=0.6.0,<0.7.0" +pycodestyle = ">=2.5.0,<2.6.0" +pyflakes = ">=2.1.0,<2.2.0" + +[[package]] +category = "main" +description = "A simple framework for building complex web applications." +name = "flask" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +version = "1.1.1" + +[package.dependencies] +Jinja2 = ">=2.10.1" +Werkzeug = ">=0.15" +click = ">=5.1" +itsdangerous = ">=0.24" + +[package.extras] +dev = ["pytest", "coverage", "tox", "sphinx", "pallets-sphinx-themes", "sphinxcontrib-log-cabinet", "sphinx-issues"] +docs = ["sphinx", "pallets-sphinx-themes", "sphinxcontrib-log-cabinet", "sphinx-issues"] +dotenv = ["python-dotenv"] + +[[package]] +category = "main" +description = "Doing the OAuth dance with style using Flask, requests, and oauthlib" +name = "flask-dance" +optional = false +python-versions = "*" +version = "3.0.0" + +[package.dependencies] +Flask = ">=0.7" +oauthlib = "*" +requests = ">=2.0" +requests-oauthlib = ">=1.0.0" +six = "*" +urlobject = "*" + +[package.extras] +signals = ["blinker"] +sqla = ["sqlalchemy", "sqlalchemy-utils"] + +[[package]] +category = "main" +description = "Coroutine-based network library" +name = "gevent" +optional = false +python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*" +version = "1.4.0" + +[package.dependencies] +cffi = ">=1.11.5" +greenlet = ">=0.4.14" + +[package.extras] +dnspython = ["dnspython", "idna"] +doc = ["repoze.sphinx.autointerface"] +events = ["zope.event", "zope.interface"] +test = ["zope.interface", "zope.event", "requests", "objgraph", "psutil", "futures", "mock", "coverage (>=5.0a3)", "coveralls (>=1.0)"] + +[[package]] +category = "main" +description = "Lightweight in-process concurrent programming" +marker = "platform_python_implementation == \"CPython\"" +name = "greenlet" +optional = false +python-versions = "*" +version = "0.4.15" + +[[package]] +category = "main" +description = "Internationalized Domain Names in Applications (IDNA)" +name = "idna" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "2.8" + +[[package]] +category = "dev" +description = "Read metadata from Python packages" +marker = "python_version < \"3.8\"" +name = "importlib-metadata" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" +version = "1.3.0" + +[package.dependencies] +zipp = ">=0.5" + +[package.extras] +docs = ["sphinx", "rst.linker"] +testing = ["packaging", "importlib-resources"] + +[[package]] +category = "main" +description = "Various helpers to pass data to untrusted environments and back." +name = "itsdangerous" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "1.1.0" + +[[package]] +category = "main" +description = "A very fast and expressive template engine." +name = "jinja2" +optional = false +python-versions = "*" +version = "2.10.3" + +[package.dependencies] +MarkupSafe = ">=0.23" + +[package.extras] +i18n = ["Babel (>=0.8)"] + +[[package]] +category = "main" +description = "A diff/patch pair for JSON-serialized data structures." +name = "json-delta" +optional = false +python-versions = "*" +version = "2.0" + +[[package]] +category = "main" +description = "Safely add untrusted strings to HTML/XML markup." +name = "markupsafe" +optional = false +python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*" +version = "1.1.1" + +[[package]] +category = "dev" +description = "McCabe checker, plugin for flake8" +name = "mccabe" +optional = false +python-versions = "*" +version = "0.6.1" + +[[package]] +category = "dev" +description = "More routines for operating on iterables, beyond itertools" +name = "more-itertools" +optional = false +python-versions = ">=3.5" +version = "8.0.2" + +[[package]] +category = "dev" +description = "Optional static typing for Python" +name = "mypy" +optional = false +python-versions = ">=3.5" +version = "0.761" + +[package.dependencies] +mypy-extensions = ">=0.4.3,<0.5.0" +typed-ast = ">=1.4.0,<1.5.0" +typing-extensions = ">=3.7.4" + +[package.extras] +dmypy = ["psutil (>=4.0)"] + +[[package]] +category = "dev" +description = "Experimental type system extensions for programs checked with the mypy typechecker." +name = "mypy-extensions" +optional = false +python-versions = "*" +version = "0.4.3" + +[[package]] +category = "main" +description = "A generic, spec-compliant, thorough implementation of the OAuth request-signing logic" +name = "oauthlib" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "3.1.0" + +[package.extras] +rsa = ["cryptography"] +signals = ["blinker"] +signedtoken = ["cryptography", "pyjwt (>=1.0.0)"] + +[[package]] +category = "dev" +description = "Core utilities for Python packages" +name = "packaging" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "19.2" + +[package.dependencies] +pyparsing = ">=2.0.2" +six = "*" + +[[package]] +category = "dev" +description = "Utility library for gitignore style pattern matching of file paths." +name = "pathspec" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "0.6.0" + +[[package]] +category = "dev" +description = "plugin and hook calling mechanisms for python" +name = "pluggy" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "0.13.1" + +[package.dependencies] +[package.dependencies.importlib-metadata] +python = "<3.8" +version = ">=0.12" + +[package.extras] +dev = ["pre-commit", "tox"] + +[[package]] +category = "dev" +description = "library with cross-python path, ini-parsing, io, code, log facilities" +name = "py" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "1.8.0" + +[[package]] +category = "dev" +description = "Python style guide checker" +name = "pycodestyle" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "2.5.0" + +[[package]] +category = "main" +description = "C parser in Python" +marker = "sys_platform == \"win32\" and platform_python_implementation == \"CPython\"" +name = "pycparser" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "2.19" + +[[package]] +category = "dev" +description = "passive checker of Python programs" +name = "pyflakes" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "2.1.1" + +[[package]] +category = "main" +description = "Python client library for Kubernetes" +name = "pykube-ng" +optional = false +python-versions = ">=3.6" +version = "19.12.1" + +[package.dependencies] +pyyaml = "*" +requests = ">=2.12" + +[package.extras] +gcp = ["google-auth", "jsonpath-ng"] + +[[package]] +category = "dev" +description = "Python parsing module" +name = "pyparsing" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" +version = "2.4.5" + +[[package]] +category = "dev" +description = "pytest: simple powerful testing with Python" +name = "pytest" +optional = false +python-versions = ">=3.5" +version = "5.3.2" + +[package.dependencies] +atomicwrites = ">=1.0" +attrs = ">=17.4.0" +colorama = "*" +more-itertools = ">=4.0.0" +packaging = "*" +pluggy = ">=0.12,<1.0" +py = ">=1.5.0" +wcwidth = "*" + +[package.dependencies.importlib-metadata] +python = "<3.8" +version = ">=0.12" + +[package.extras] +testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] + +[[package]] +category = "dev" +description = "Pytest plugin for measuring coverage." +name = "pytest-cov" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "2.8.1" + +[package.dependencies] +coverage = ">=4.4" +pytest = ">=3.6" + +[package.extras] +testing = ["fields", "hunter", "process-tests (2.0.2)", "six", "virtualenv"] + +[[package]] +category = "main" +description = "YAML parser and emitter for Python" +name = "pyyaml" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "5.2" + +[[package]] +category = "main" +description = "Python client for Redis key-value store" +name = "redis" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "3.3.11" + +[package.extras] +hiredis = ["hiredis (>=0.1.3)"] + +[[package]] +category = "main" +description = "Redis locking mechanism" +name = "redlock-py" +optional = false +python-versions = "*" +version = "1.0.8" + +[package.dependencies] +redis = "*" + +[[package]] +category = "dev" +description = "Alternative regular expression module, to replace re." +name = "regex" +optional = false +python-versions = "*" +version = "2019.12.20" + +[[package]] +category = "main" +description = "Python HTTP for Humans." +name = "requests" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +version = "2.22.0" + +[package.dependencies] +certifi = ">=2017.4.17" +chardet = ">=3.0.2,<3.1.0" +idna = ">=2.5,<2.9" +urllib3 = ">=1.21.1,<1.25.0 || >1.25.0,<1.25.1 || >1.25.1,<1.26" + +[package.extras] +security = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)"] +socks = ["PySocks (>=1.5.6,<1.5.7 || >1.5.7)", "win-inet-pton"] + +[[package]] +category = "main" +description = "OAuthlib authentication support for Requests." +name = "requests-oauthlib" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "1.3.0" + +[package.dependencies] +oauthlib = ">=3.0.0" +requests = ">=2.0.0" + +[package.extras] +rsa = ["oauthlib (>=3.0.0)"] + +[[package]] +category = "main" +description = "Python 2 and 3 compatibility utilities" +name = "six" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*" +version = "1.13.0" + +[[package]] +category = "main" +description = "Python library to manage OAuth access tokens" +name = "stups-tokens" +optional = false +python-versions = "*" +version = "1.1.19" + +[package.dependencies] +requests = "*" + +[package.extras] +tests = ["flake8"] + +[[package]] +category = "dev" +description = "Python Library for Tom's Obvious, Minimal Language" +name = "toml" +optional = false +python-versions = "*" +version = "0.10.0" + +[[package]] +category = "dev" +description = "a fork of Python 2 and 3 ast modules with type comment support" +name = "typed-ast" +optional = false +python-versions = "*" +version = "1.4.0" + +[[package]] +category = "dev" +description = "Backported and Experimental Type Hints for Python 3.5+" +name = "typing-extensions" +optional = false +python-versions = "*" +version = "3.7.4.1" + +[[package]] +category = "main" +description = "HTTP library with thread-safe connection pooling, file post, and more." +name = "urllib3" +optional = false +python-versions = "*" +version = "1.22" + +[package.extras] +secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"] +socks = ["PySocks (>=1.5.6,<1.5.7 || >1.5.7,<2.0)"] + +[[package]] +category = "main" +description = "A utility class for manipulating URLs." +name = "urlobject" +optional = false +python-versions = "*" +version = "2.4.3" + +[[package]] +category = "dev" +description = "Measures number of Terminal column cells of wide-character codes" +name = "wcwidth" +optional = false +python-versions = "*" +version = "0.1.7" + +[[package]] +category = "main" +description = "The comprehensive WSGI web application library." +name = "werkzeug" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "0.16.0" + +[package.extras] +dev = ["pytest", "coverage", "tox", "sphinx", "pallets-sphinx-themes", "sphinx-issues"] +termcolor = ["termcolor"] +watchdog = ["watchdog"] + +[[package]] +category = "dev" +description = "Backport of pathlib-compatible object wrapper for zip files" +marker = "python_version < \"3.8\"" +name = "zipp" +optional = false +python-versions = ">=2.7" +version = "0.6.0" + +[package.dependencies] +more-itertools = "*" + +[package.extras] +docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"] +testing = ["pathlib2", "contextlib2", "unittest2"] + +[metadata] +content-hash = "b4f38cdf21a453cc828f14cfd209e4968ce7db806578ba07c9ea7f96948b09b4" +python-versions = ">=3.7" + +[metadata.files] +appdirs = [ + {file = "appdirs-1.4.3-py2.py3-none-any.whl", hash = "sha256:d8b24664561d0d34ddfaec54636d502d7cea6e29c3eaf68f3df6180863e2166e"}, + {file = "appdirs-1.4.3.tar.gz", hash = "sha256:9e5896d1372858f8dd3344faf4e5014d21849c756c8d5701f78f8a103b372d92"}, +] +atomicwrites = [ + {file = "atomicwrites-1.3.0-py2.py3-none-any.whl", hash = "sha256:03472c30eb2c5d1ba9227e4c2ca66ab8287fbfbbda3888aa93dc2e28fc6811b4"}, + {file = "atomicwrites-1.3.0.tar.gz", hash = "sha256:75a9445bac02d8d058d5e1fe689654ba5a6556a1dfd8ce6ec55a0ed79866cfa6"}, +] +attrs = [ + {file = "attrs-19.3.0-py2.py3-none-any.whl", hash = "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c"}, + {file = "attrs-19.3.0.tar.gz", hash = "sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72"}, +] +black = [ + {file = "black-19.10b0-py36-none-any.whl", hash = "sha256:1b30e59be925fafc1ee4565e5e08abef6b03fe455102883820fe5ee2e4734e0b"}, + {file = "black-19.10b0.tar.gz", hash = "sha256:c2edb73a08e9e0e6f65a0e6af18b059b8b1cdd5bef997d7a0b181df93dc81539"}, +] +certifi = [ + {file = "certifi-2019.11.28-py2.py3-none-any.whl", hash = "sha256:017c25db2a153ce562900032d5bc68e9f191e44e9a0f762f373977de9df1fbb3"}, + {file = "certifi-2019.11.28.tar.gz", hash = "sha256:25b64c7da4cd7479594d035c08c2d809eb4aab3a26e5a990ea98cc450c320f1f"}, +] +cffi = [ + {file = "cffi-1.13.2-cp27-cp27m-macosx_10_6_intel.whl", hash = "sha256:3c9fff570f13480b201e9ab69453108f6d98244a7f495e91b6c654a47486ba43"}, + {file = "cffi-1.13.2-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:2c5e309ec482556397cb21ede0350c5e82f0eb2621de04b2633588d118da4396"}, + {file = "cffi-1.13.2-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:19db0cdd6e516f13329cba4903368bff9bb5a9331d3410b1b448daaadc495e54"}, + {file = "cffi-1.13.2-cp27-cp27m-win32.whl", hash = "sha256:5c4fae4e9cdd18c82ba3a134be256e98dc0596af1e7285a3d2602c97dcfa5159"}, + {file = "cffi-1.13.2-cp27-cp27m-win_amd64.whl", hash = "sha256:32a262e2b90ffcfdd97c7a5e24a6012a43c61f1f5a57789ad80af1d26c6acd97"}, + {file = "cffi-1.13.2-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:4a43c91840bda5f55249413037b7a9b79c90b1184ed504883b72c4df70778579"}, + {file = "cffi-1.13.2-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:8169cf44dd8f9071b2b9248c35fc35e8677451c52f795daa2bb4643f32a540bc"}, + {file = "cffi-1.13.2-cp34-cp34m-macosx_10_6_intel.whl", hash = "sha256:71a608532ab3bd26223c8d841dde43f3516aa5d2bf37b50ac410bb5e99053e8f"}, + {file = "cffi-1.13.2-cp34-cp34m-manylinux1_i686.whl", hash = "sha256:7f627141a26b551bdebbc4855c1157feeef18241b4b8366ed22a5c7d672ef858"}, + {file = "cffi-1.13.2-cp34-cp34m-manylinux1_x86_64.whl", hash = "sha256:0b49274afc941c626b605fb59b59c3485c17dc776dc3cc7cc14aca74cc19cc42"}, + {file = "cffi-1.13.2-cp34-cp34m-win32.whl", hash = "sha256:4424e42199e86b21fc4db83bd76909a6fc2a2aefb352cb5414833c030f6ed71b"}, + {file = "cffi-1.13.2-cp34-cp34m-win_amd64.whl", hash = "sha256:7d4751da932caaec419d514eaa4215eaf14b612cff66398dd51129ac22680b20"}, + {file = "cffi-1.13.2-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:ccb032fda0873254380aa2bfad2582aedc2959186cce61e3a17abc1a55ff89c3"}, + {file = "cffi-1.13.2-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:dcd65317dd15bc0451f3e01c80da2216a31916bdcffd6221ca1202d96584aa25"}, + {file = "cffi-1.13.2-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:135f69aecbf4517d5b3d6429207b2dff49c876be724ac0c8bf8e1ea99df3d7e5"}, + {file = "cffi-1.13.2-cp35-cp35m-win32.whl", hash = "sha256:7b93a885bb13073afb0aa73ad82059a4c41f4b7d8eb8368980448b52d4c7dc2c"}, + {file = "cffi-1.13.2-cp35-cp35m-win_amd64.whl", hash = "sha256:e570d3ab32e2c2861c4ebe6ffcad6a8abf9347432a37608fe1fbd157b3f0036b"}, + {file = "cffi-1.13.2-cp36-cp36m-macosx_10_6_intel.whl", hash = "sha256:0e3ea92942cb1168e38c05c1d56b0527ce31f1a370f6117f1d490b8dcd6b3a04"}, + {file = "cffi-1.13.2-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:5ecfa867dea6fabe2a58f03ac9186ea64da1386af2159196da51c4904e11d652"}, + {file = "cffi-1.13.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:291f7c42e21d72144bb1c1b2e825ec60f46d0a7468f5346841860454c7aa8f57"}, + {file = "cffi-1.13.2-cp36-cp36m-win32.whl", hash = "sha256:62f2578358d3a92e4ab2d830cd1c2049c9c0d0e6d3c58322993cc341bdeac22e"}, + {file = "cffi-1.13.2-cp36-cp36m-win_amd64.whl", hash = "sha256:fd43a88e045cf992ed09fa724b5315b790525f2676883a6ea64e3263bae6549d"}, + {file = "cffi-1.13.2-cp37-cp37m-macosx_10_6_intel.whl", hash = "sha256:d75c461e20e29afc0aee7172a0950157c704ff0dd51613506bd7d82b718e7410"}, + {file = "cffi-1.13.2-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:aa00d66c0fab27373ae44ae26a66a9e43ff2a678bf63a9c7c1a9a4d61172827a"}, + {file = "cffi-1.13.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:2e9c80a8c3344a92cb04661115898a9129c074f7ab82011ef4b612f645939f12"}, + {file = "cffi-1.13.2-cp37-cp37m-win32.whl", hash = "sha256:d754f39e0d1603b5b24a7f8484b22d2904fa551fe865fd0d4c3332f078d20d4e"}, + {file = "cffi-1.13.2-cp37-cp37m-win_amd64.whl", hash = "sha256:6471a82d5abea994e38d2c2abc77164b4f7fbaaf80261cb98394d5793f11b12a"}, + {file = "cffi-1.13.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:74a1d8c85fb6ff0b30fbfa8ad0ac23cd601a138f7509dc617ebc65ef305bb98d"}, + {file = "cffi-1.13.2-cp38-cp38-manylinux1_i686.whl", hash = "sha256:42194f54c11abc8583417a7cf4eaff544ce0de8187abaf5d29029c91b1725ad3"}, + {file = "cffi-1.13.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:415bdc7ca8c1c634a6d7163d43fb0ea885a07e9618a64bda407e04b04333b7db"}, + {file = "cffi-1.13.2-cp38-cp38-win32.whl", hash = "sha256:6d4f18483d040e18546108eb13b1dfa1000a089bcf8529e30346116ea6240506"}, + {file = "cffi-1.13.2-cp38-cp38-win_amd64.whl", hash = "sha256:2781e9ad0e9d47173c0093321bb5435a9dfae0ed6a762aabafa13108f5f7b2ba"}, + {file = "cffi-1.13.2.tar.gz", hash = "sha256:599a1e8ff057ac530c9ad1778293c665cb81a791421f46922d80a86473c13346"}, +] +chardet = [ + {file = "chardet-3.0.4-py2.py3-none-any.whl", hash = "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"}, + {file = "chardet-3.0.4.tar.gz", hash = "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae"}, +] +click = [ + {file = "Click-7.0-py2.py3-none-any.whl", hash = "sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13"}, + {file = "Click-7.0.tar.gz", hash = "sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7"}, +] +colorama = [ + {file = "colorama-0.4.3-py2.py3-none-any.whl", hash = "sha256:7d73d2a99753107a36ac6b455ee49046802e59d9d076ef8e47b61499fa29afff"}, + {file = "colorama-0.4.3.tar.gz", hash = "sha256:e96da0d330793e2cb9485e9ddfd918d456036c7149416295932478192f4436a1"}, +] +coverage = [ + {file = "coverage-4.4.2-cp26-cp26m-macosx_10_10_x86_64.whl", hash = "sha256:d1ee76f560c3c3e8faada866a07a32485445e16ed2206ac8378bd90dadffb9f0"}, + {file = "coverage-4.4.2-cp26-cp26m-manylinux1_i686.whl", hash = "sha256:007eeef7e23f9473622f7d94a3e029a45d55a92a1f083f0f3512f5ab9a669b05"}, + {file = "coverage-4.4.2-cp26-cp26m-manylinux1_x86_64.whl", hash = "sha256:17307429935f96c986a1b1674f78079528833410750321d22b5fb35d1883828e"}, + {file = "coverage-4.4.2-cp26-cp26mu-manylinux1_i686.whl", hash = "sha256:845fddf89dca1e94abe168760a38271abfc2e31863fbb4ada7f9a99337d7c3dc"}, + {file = "coverage-4.4.2-cp26-cp26mu-manylinux1_x86_64.whl", hash = "sha256:3f4d0b3403d3e110d2588c275540649b1841725f5a11a7162620224155d00ba2"}, + {file = "coverage-4.4.2-cp27-cp27m-macosx_10_12_intel.whl", hash = "sha256:4c4f368ffe1c2e7602359c2c50233269f3abe1c48ca6b288dcd0fb1d1c679733"}, + {file = "coverage-4.4.2-cp27-cp27m-macosx_10_12_x86_64.whl", hash = "sha256:f8c55dd0f56d3d618dfacf129e010cbe5d5f94b6951c1b2f13ab1a2f79c284da"}, + {file = "coverage-4.4.2-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:cdd92dd9471e624cd1d8c1a2703d25f114b59b736b0f1f659a98414e535ffb3d"}, + {file = "coverage-4.4.2-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:2ad357d12971e77360034c1596011a03f50c0f9e1ecd12e081342b8d1aee2236"}, + {file = "coverage-4.4.2-cp27-cp27m-win32.whl", hash = "sha256:700d7579995044dc724847560b78ac786f0ca292867447afda7727a6fbaa082e"}, + {file = "coverage-4.4.2-cp27-cp27m-win_amd64.whl", hash = "sha256:66f393e10dd866be267deb3feca39babba08ae13763e0fc7a1063cbe1f8e49f6"}, + {file = "coverage-4.4.2-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:e9a0e1caed2a52f15c96507ab78a48f346c05681a49c5b003172f8073da6aa6b"}, + {file = "coverage-4.4.2-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:eea9135432428d3ca7ee9be86af27cb8e56243f73764a9b6c3e0bda1394916be"}, + {file = "coverage-4.4.2-cp33-cp33m-macosx_10_10_x86_64.whl", hash = "sha256:5ff16548492e8a12e65ff3d55857ccd818584ed587a6c2898a9ebbe09a880674"}, + {file = "coverage-4.4.2-cp33-cp33m-manylinux1_i686.whl", hash = "sha256:d00e29b78ff610d300b2c37049a41234d48ea4f2d2581759ebcf67caaf731c31"}, + {file = "coverage-4.4.2-cp33-cp33m-manylinux1_x86_64.whl", hash = "sha256:87d942863fe74b1c3be83a045996addf1639218c2cb89c5da18c06c0fe3917ea"}, + {file = "coverage-4.4.2-cp34-cp34m-macosx_10_10_x86_64.whl", hash = "sha256:358d635b1fc22a425444d52f26287ae5aea9e96e254ff3c59c407426f44574f4"}, + {file = "coverage-4.4.2-cp34-cp34m-manylinux1_i686.whl", hash = "sha256:81912cfe276e0069dca99e1e4e6be7b06b5fc8342641c6b472cb2fed7de7ae18"}, + {file = "coverage-4.4.2-cp34-cp34m-manylinux1_x86_64.whl", hash = "sha256:079248312838c4c8f3494934ab7382a42d42d5f365f0cf7516f938dbb3f53f3f"}, + {file = "coverage-4.4.2-cp34-cp34m-win32.whl", hash = "sha256:b0059630ca5c6b297690a6bf57bf2fdac1395c24b7935fd73ee64190276b743b"}, + {file = "coverage-4.4.2-cp34-cp34m-win_amd64.whl", hash = "sha256:493082f104b5ca920e97a485913de254cbe351900deed72d4264571c73464cd0"}, + {file = "coverage-4.4.2-cp35-cp35m-macosx_10_10_x86_64.whl", hash = "sha256:e3ba9b14607c23623cf38f90b23f5bed4a3be87cbfa96e2e9f4eabb975d1e98b"}, + {file = "coverage-4.4.2-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:82cbd3317320aa63c65555aa4894bf33a13fb3a77f079059eb5935eea415938d"}, + {file = "coverage-4.4.2-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:9721f1b7275d3112dc7ccf63f0553c769f09b5c25a26ee45872c7f5c09edf6c1"}, + {file = "coverage-4.4.2-cp35-cp35m-win32.whl", hash = "sha256:bd4800e32b4c8d99c3a2c943f1ac430cbf80658d884123d19639bcde90dad44a"}, + {file = "coverage-4.4.2-cp35-cp35m-win_amd64.whl", hash = "sha256:f29841e865590af72c4b90d7b5b8e93fd560f5dea436c1d5ee8053788f9285de"}, + {file = "coverage-4.4.2-cp36-cp36m-macosx_10_12_x86_64.whl", hash = "sha256:f3a5c6d054c531536a83521c00e5d4004f1e126e2e2556ce399bef4180fbe540"}, + {file = "coverage-4.4.2-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:dd707a21332615108b736ef0b8513d3edaf12d2a7d5fc26cd04a169a8ae9b526"}, + {file = "coverage-4.4.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:2e1a5c6adebb93c3b175103c2f855eda957283c10cf937d791d81bef8872d6ca"}, + {file = "coverage-4.4.2-cp36-cp36m-win32.whl", hash = "sha256:f87f522bde5540d8a4b11df80058281ac38c44b13ce29ced1e294963dd51a8f8"}, + {file = "coverage-4.4.2-cp36-cp36m-win_amd64.whl", hash = "sha256:a7cfaebd8f24c2b537fa6a271229b051cdac9c1734bb6f939ccfc7c055689baa"}, + {file = "coverage-4.4.2.tar.gz", hash = "sha256:309d91bd7a35063ec7a0e4d75645488bfab3f0b66373e7722f23da7f5b0f34cc"}, + {file = "coverage-4.4.2.win-amd64-py2.7.exe", hash = "sha256:b6cebae1502ce5b87d7c6f532fa90ab345cfbda62b95aeea4e431e164d498a3d"}, + {file = "coverage-4.4.2.win-amd64-py3.4.exe", hash = "sha256:a4497faa4f1c0fc365ba05eaecfb6b5d24e3c8c72e95938f9524e29dadb15e76"}, + {file = "coverage-4.4.2.win-amd64-py3.5.exe", hash = "sha256:2b4d7f03a8a6632598cbc5df15bbca9f778c43db7cf1a838f4fa2c8599a8691a"}, + {file = "coverage-4.4.2.win-amd64-py3.6.exe", hash = "sha256:1afccd7e27cac1b9617be8c769f6d8a6d363699c9b86820f40c74cfb3328921c"}, + {file = "coverage-4.4.2.win32-py2.7.exe", hash = "sha256:0388c12539372bb92d6dde68b4627f0300d948965bbb7fc104924d715fdc0965"}, + {file = "coverage-4.4.2.win32-py3.4.exe", hash = "sha256:ab3508df9a92c1d3362343d235420d08e2662969b83134f8a97dc1451cbe5e84"}, + {file = "coverage-4.4.2.win32-py3.5.exe", hash = "sha256:43a155eb76025c61fc20c3d03b89ca28efa6f5be572ab6110b2fb68eda96bfea"}, + {file = "coverage-4.4.2.win32-py3.6.exe", hash = "sha256:f98b461cb59f117887aa634a66022c0bd394278245ed51189f63a036516e32de"}, +] +coveralls = [ + {file = "coveralls-1.9.2-py2.py3-none-any.whl", hash = "sha256:25522a50cdf720d956601ca6ef480786e655ae2f0c94270c77e1a23d742de558"}, + {file = "coveralls-1.9.2.tar.gz", hash = "sha256:8e3315e8620bb6b3c6f3179a75f498e7179c93b3ddc440352404f941b1f70524"}, +] +docopt = [ + {file = "docopt-0.6.2.tar.gz", hash = "sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491"}, +] +entrypoints = [ + {file = "entrypoints-0.3-py2.py3-none-any.whl", hash = "sha256:589f874b313739ad35be6e0cd7efde2a4e9b6fea91edcc34e58ecbb8dbe56d19"}, + {file = "entrypoints-0.3.tar.gz", hash = "sha256:c70dd71abe5a8c85e55e12c19bd91ccfeec11a6e99044204511f9ed547d48451"}, +] +flake8 = [ + {file = "flake8-3.7.9-py2.py3-none-any.whl", hash = "sha256:49356e766643ad15072a789a20915d3c91dc89fd313ccd71802303fd67e4deca"}, + {file = "flake8-3.7.9.tar.gz", hash = "sha256:45681a117ecc81e870cbf1262835ae4af5e7a8b08e40b944a8a6e6b895914cfb"}, +] +flask = [ + {file = "Flask-1.1.1-py2.py3-none-any.whl", hash = "sha256:45eb5a6fd193d6cf7e0cf5d8a5b31f83d5faae0293695626f539a823e93b13f6"}, + {file = "Flask-1.1.1.tar.gz", hash = "sha256:13f9f196f330c7c2c5d7a5cf91af894110ca0215ac051b5844701f2bfd934d52"}, +] +flask-dance = [ + {file = "Flask-Dance-3.0.0.tar.gz", hash = "sha256:5ec8fae543ef93aab96d6a4c6d34ed835c793e2f6a211212a89a4e9c31c42bed"}, + {file = "Flask_Dance-3.0.0-py2.py3-none-any.whl", hash = "sha256:1b30ea6765ca2a9c5c15eb00a18e18f8c347a4bf2f9947b68ff0024c4c7d1dfa"}, + {file = "Flask_Dance-3.0.0-py3.7.egg", hash = "sha256:8f567087e8308bdeb225638d581b93e3b1d671871f8de394beb6c45923497e89"}, +] +gevent = [ + {file = "gevent-1.4.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:1b7d3a285978b27b469c0ff5fb5a72bcd69f4306dbbf22d7997d83209a8ba917"}, + {file = "gevent-1.4.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:44089ed06a962a3a70e96353c981d628b2d4a2f2a75ea5d90f916a62d22af2e8"}, + {file = "gevent-1.4.0-cp27-cp27m-win32.whl", hash = "sha256:0e1e5b73a445fe82d40907322e1e0eec6a6745ca3cea19291c6f9f50117bb7ea"}, + {file = "gevent-1.4.0-cp27-cp27m-win_amd64.whl", hash = "sha256:74b7528f901f39c39cdbb50cdf08f1a2351725d9aebaef212a29abfbb06895ee"}, + {file = "gevent-1.4.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:0ff2b70e8e338cf13bedf146b8c29d475e2a544b5d1fe14045aee827c073842c"}, + {file = "gevent-1.4.0-cp34-cp34m-macosx_10_14_x86_64.whl", hash = "sha256:0774babec518a24d9a7231d4e689931f31b332c4517a771e532002614e270a64"}, + {file = "gevent-1.4.0-cp34-cp34m-manylinux1_x86_64.whl", hash = "sha256:d752bcf1b98174780e2317ada12013d612f05116456133a6acf3e17d43b71f05"}, + {file = "gevent-1.4.0-cp34-cp34m-win32.whl", hash = "sha256:3249011d13d0c63bea72d91cec23a9cf18c25f91d1f115121e5c9113d753fa12"}, + {file = "gevent-1.4.0-cp34-cp34m-win_amd64.whl", hash = "sha256:d1e6d1f156e999edab069d79d890859806b555ce4e4da5b6418616322f0a3df1"}, + {file = "gevent-1.4.0-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:7d0809e2991c9784eceeadef01c27ee6a33ca09ebba6154317a257353e3af922"}, + {file = "gevent-1.4.0-cp35-cp35m-win32.whl", hash = "sha256:14b4d06d19d39a440e72253f77067d27209c67e7611e352f79fe69e0f618f76e"}, + {file = "gevent-1.4.0-cp35-cp35m-win_amd64.whl", hash = "sha256:53b72385857e04e7faca13c613c07cab411480822ac658d97fd8a4ddbaf715c8"}, + {file = "gevent-1.4.0-cp36-cp36m-macosx_10_6_intel.whl", hash = "sha256:8d9ec51cc06580f8c21b41fd3f2b3465197ba5b23c00eb7d422b7ae0380510b0"}, + {file = "gevent-1.4.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:2711e69788ddb34c059a30186e05c55a6b611cb9e34ac343e69cf3264d42fe1c"}, + {file = "gevent-1.4.0-cp36-cp36m-win32.whl", hash = "sha256:e5bcc4270671936349249d26140c267397b7b4b1381f5ec8b13c53c5b53ab6e1"}, + {file = "gevent-1.4.0-cp36-cp36m-win_amd64.whl", hash = "sha256:9f7a1e96fec45f70ad364e46de32ccacab4d80de238bd3c2edd036867ccd48ad"}, + {file = "gevent-1.4.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:50024a1ee2cf04645535c5ebaeaa0a60c5ef32e262da981f4be0546b26791950"}, + {file = "gevent-1.4.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:4bfa291e3c931ff3c99a349d8857605dca029de61d74c6bb82bd46373959c942"}, + {file = "gevent-1.4.0-cp37-cp37m-win32.whl", hash = "sha256:ab4dc33ef0e26dc627559786a4fba0c2227f125db85d970abbf85b77506b3f51"}, + {file = "gevent-1.4.0-cp37-cp37m-win_amd64.whl", hash = "sha256:896b2b80931d6b13b5d9feba3d4eebc67d5e6ec54f0cf3339d08487d55d93b0e"}, + {file = "gevent-1.4.0-pp260-pypy_41-macosx_10_14_x86_64.whl", hash = "sha256:107f4232db2172f7e8429ed7779c10f2ed16616d75ffbe77e0e0c3fcdeb51a51"}, + {file = "gevent-1.4.0-pp260-pypy_41-win32.whl", hash = "sha256:28a0c5417b464562ab9842dd1fb0cc1524e60494641d973206ec24d6ec5f6909"}, + {file = "gevent-1.4.0.tar.gz", hash = "sha256:1eb7fa3b9bd9174dfe9c3b59b7a09b768ecd496debfc4976a9530a3e15c990d1"}, +] +greenlet = [ + {file = "greenlet-0.4.15-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:99a26afdb82ea83a265137a398f570402aa1f2b5dfb4ac3300c026931817b163"}, + {file = "greenlet-0.4.15-cp27-cp27m-win32.whl", hash = "sha256:beeabe25c3b704f7d56b573f7d2ff88fc99f0138e43480cecdfcaa3b87fe4f87"}, + {file = "greenlet-0.4.15-cp27-cp27m-win_amd64.whl", hash = "sha256:9854f612e1b59ec66804931df5add3b2d5ef0067748ea29dc60f0efdcda9a638"}, + {file = "greenlet-0.4.15-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:ac57fcdcfb0b73bb3203b58a14501abb7e5ff9ea5e2edfa06bb03035f0cff248"}, + {file = "greenlet-0.4.15-cp33-cp33m-win32.whl", hash = "sha256:d634a7ea1fc3380ff96f9e44d8d22f38418c1c381d5fac680b272d7d90883720"}, + {file = "greenlet-0.4.15-cp33-cp33m-win_amd64.whl", hash = "sha256:0d48200bc50cbf498716712129eef819b1729339e34c3ae71656964dac907c28"}, + {file = "greenlet-0.4.15-cp34-cp34m-manylinux1_x86_64.whl", hash = "sha256:bcb530089ff24f6458a81ac3fa699e8c00194208a724b644ecc68422e1111939"}, + {file = "greenlet-0.4.15-cp34-cp34m-win32.whl", hash = "sha256:8b4572c334593d449113f9dc8d19b93b7b271bdbe90ba7509eb178923327b625"}, + {file = "greenlet-0.4.15-cp34-cp34m-win_amd64.whl", hash = "sha256:a9f145660588187ff835c55a7d2ddf6abfc570c2651c276d3d4be8a2766db490"}, + {file = "greenlet-0.4.15-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:51503524dd6f152ab4ad1fbd168fc6c30b5795e8c70be4410a64940b3abb55c0"}, + {file = "greenlet-0.4.15-cp35-cp35m-win32.whl", hash = "sha256:a19bf883b3384957e4a4a13e6bd1ae3d85ae87f4beb5957e35b0be287f12f4e4"}, + {file = "greenlet-0.4.15-cp35-cp35m-win_amd64.whl", hash = "sha256:853da4f9563d982e4121fed8c92eea1a4594a2299037b3034c3c898cb8e933d6"}, + {file = "greenlet-0.4.15-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:23d12eacffa9d0f290c0fe0c4e81ba6d5f3a5b7ac3c30a5eaf0126bf4deda5c8"}, + {file = "greenlet-0.4.15-cp36-cp36m-win32.whl", hash = "sha256:000546ad01e6389e98626c1367be58efa613fa82a1be98b0c6fc24b563acc6d0"}, + {file = "greenlet-0.4.15-cp36-cp36m-win_amd64.whl", hash = "sha256:d97b0661e1aead761f0ded3b769044bb00ed5d33e1ec865e891a8b128bf7c656"}, + {file = "greenlet-0.4.15-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:8041e2de00e745c0e05a502d6e6db310db7faa7c979b3a5877123548a4c0b214"}, + {file = "greenlet-0.4.15-cp37-cp37m-win32.whl", hash = "sha256:81fcd96a275209ef117e9ec91f75c731fa18dcfd9ffaa1c0adbdaa3616a86043"}, + {file = "greenlet-0.4.15-cp37-cp37m-win_amd64.whl", hash = "sha256:37c9ba82bd82eb6a23c2e5acc03055c0e45697253b2393c9a50cef76a3985304"}, + {file = "greenlet-0.4.15.tar.gz", hash = "sha256:9416443e219356e3c31f1f918a91badf2e37acf297e2fa13d24d1cc2380f8fbc"}, +] +idna = [ + {file = "idna-2.8-py2.py3-none-any.whl", hash = "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c"}, + {file = "idna-2.8.tar.gz", hash = "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407"}, +] +importlib-metadata = [ + {file = "importlib_metadata-1.3.0-py2.py3-none-any.whl", hash = "sha256:d95141fbfa7ef2ec65cfd945e2af7e5a6ddbd7c8d9a25e66ff3be8e3daf9f60f"}, + {file = "importlib_metadata-1.3.0.tar.gz", hash = "sha256:073a852570f92da5f744a3472af1b61e28e9f78ccf0c9117658dc32b15de7b45"}, +] +itsdangerous = [ + {file = "itsdangerous-1.1.0-py2.py3-none-any.whl", hash = "sha256:b12271b2047cb23eeb98c8b5622e2e5c5e9abd9784a153e9d8ef9cb4dd09d749"}, + {file = "itsdangerous-1.1.0.tar.gz", hash = "sha256:321b033d07f2a4136d3ec762eac9f16a10ccd60f53c0c91af90217ace7ba1f19"}, +] +jinja2 = [ + {file = "Jinja2-2.10.3-py2.py3-none-any.whl", hash = "sha256:74320bb91f31270f9551d46522e33af46a80c3d619f4a4bf42b3164d30b5911f"}, + {file = "Jinja2-2.10.3.tar.gz", hash = "sha256:9fe95f19286cfefaa917656583d020be14e7859c6b0252588391e47db34527de"}, +] +json-delta = [ + {file = "json_delta-2.0-py2.py3-none-any.whl", hash = "sha256:cdbb8d38b69b5bef9ac54863aff2df8ae95ab897d165448f8d72cc3501fd32f6"}, + {file = "json_delta-2.0.tar.gz", hash = "sha256:462a73672f7527517d863930bb442ed1986c35dfb6960e0fb1cb84393deea652"}, +] +markupsafe = [ + {file = "MarkupSafe-1.1.1-cp27-cp27m-macosx_10_6_intel.whl", hash = "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161"}, + {file = "MarkupSafe-1.1.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7"}, + {file = "MarkupSafe-1.1.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183"}, + {file = "MarkupSafe-1.1.1-cp27-cp27m-win32.whl", hash = "sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b"}, + {file = "MarkupSafe-1.1.1-cp27-cp27m-win_amd64.whl", hash = "sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e"}, + {file = "MarkupSafe-1.1.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f"}, + {file = "MarkupSafe-1.1.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1"}, + {file = "MarkupSafe-1.1.1-cp34-cp34m-macosx_10_6_intel.whl", hash = "sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5"}, + {file = "MarkupSafe-1.1.1-cp34-cp34m-manylinux1_i686.whl", hash = "sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1"}, + {file = "MarkupSafe-1.1.1-cp34-cp34m-manylinux1_x86_64.whl", hash = "sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735"}, + {file = "MarkupSafe-1.1.1-cp34-cp34m-win32.whl", hash = "sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21"}, + {file = "MarkupSafe-1.1.1-cp34-cp34m-win_amd64.whl", hash = "sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235"}, + {file = "MarkupSafe-1.1.1-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b"}, + {file = "MarkupSafe-1.1.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f"}, + {file = "MarkupSafe-1.1.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905"}, + {file = "MarkupSafe-1.1.1-cp35-cp35m-win32.whl", hash = "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1"}, + {file = "MarkupSafe-1.1.1-cp35-cp35m-win_amd64.whl", hash = "sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d"}, + {file = "MarkupSafe-1.1.1-cp36-cp36m-macosx_10_6_intel.whl", hash = "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff"}, + {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473"}, + {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e"}, + {file = "MarkupSafe-1.1.1-cp36-cp36m-win32.whl", hash = "sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66"}, + {file = "MarkupSafe-1.1.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5"}, + {file = "MarkupSafe-1.1.1-cp37-cp37m-macosx_10_6_intel.whl", hash = "sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d"}, + {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e"}, + {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6"}, + {file = "MarkupSafe-1.1.1-cp37-cp37m-win32.whl", hash = "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2"}, + {file = "MarkupSafe-1.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c"}, + {file = "MarkupSafe-1.1.1.tar.gz", hash = "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b"}, +] +mccabe = [ + {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, + {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, +] +more-itertools = [ + {file = "more-itertools-8.0.2.tar.gz", hash = "sha256:b84b238cce0d9adad5ed87e745778d20a3f8487d0f0cb8b8a586816c7496458d"}, + {file = "more_itertools-8.0.2-py3-none-any.whl", hash = "sha256:c833ef592a0324bcc6a60e48440da07645063c453880c9477ceb22490aec1564"}, +] +mypy = [ + {file = "mypy-0.761-cp35-cp35m-macosx_10_6_x86_64.whl", hash = "sha256:7f672d02fffcbace4db2b05369142e0506cdcde20cea0e07c7c2171c4fd11dd6"}, + {file = "mypy-0.761-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:87c556fb85d709dacd4b4cb6167eecc5bbb4f0a9864b69136a0d4640fdc76a36"}, + {file = "mypy-0.761-cp35-cp35m-win_amd64.whl", hash = "sha256:c6d27bd20c3ba60d5b02f20bd28e20091d6286a699174dfad515636cb09b5a72"}, + {file = "mypy-0.761-cp36-cp36m-macosx_10_6_x86_64.whl", hash = "sha256:4b9365ade157794cef9685791032521233729cb00ce76b0ddc78749abea463d2"}, + {file = "mypy-0.761-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:634aef60b4ff0f650d3e59d4374626ca6153fcaff96ec075b215b568e6ee3cb0"}, + {file = "mypy-0.761-cp36-cp36m-win_amd64.whl", hash = "sha256:53ea810ae3f83f9c9b452582261ea859828a9ed666f2e1ca840300b69322c474"}, + {file = "mypy-0.761-cp37-cp37m-macosx_10_6_x86_64.whl", hash = "sha256:0a9a45157e532da06fe56adcfef8a74629566b607fa2c1ac0122d1ff995c748a"}, + {file = "mypy-0.761-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:7eadc91af8270455e0d73565b8964da1642fe226665dd5c9560067cd64d56749"}, + {file = "mypy-0.761-cp37-cp37m-win_amd64.whl", hash = "sha256:e2bb577d10d09a2d8822a042a23b8d62bc3b269667c9eb8e60a6edfa000211b1"}, + {file = "mypy-0.761-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2c35cae79ceb20d47facfad51f952df16c2ae9f45db6cb38405a3da1cf8fc0a7"}, + {file = "mypy-0.761-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:f97a605d7c8bc2c6d1172c2f0d5a65b24142e11a58de689046e62c2d632ca8c1"}, + {file = "mypy-0.761-cp38-cp38-win_amd64.whl", hash = "sha256:a6bd44efee4dc8c3324c13785a9dc3519b3ee3a92cada42d2b57762b7053b49b"}, + {file = "mypy-0.761-py3-none-any.whl", hash = "sha256:7e396ce53cacd5596ff6d191b47ab0ea18f8e0ec04e15d69728d530e86d4c217"}, + {file = "mypy-0.761.tar.gz", hash = "sha256:85baab8d74ec601e86134afe2bcccd87820f79d2f8d5798c889507d1088287bf"}, +] +mypy-extensions = [ + {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, + {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, +] +oauthlib = [ + {file = "oauthlib-3.1.0-py2.py3-none-any.whl", hash = "sha256:df884cd6cbe20e32633f1db1072e9356f53638e4361bef4e8b03c9127c9328ea"}, + {file = "oauthlib-3.1.0.tar.gz", hash = "sha256:bee41cc35fcca6e988463cacc3bcb8a96224f470ca547e697b604cc697b2f889"}, +] +packaging = [ + {file = "packaging-19.2-py2.py3-none-any.whl", hash = "sha256:d9551545c6d761f3def1677baf08ab2a3ca17c56879e70fecba2fc4dde4ed108"}, + {file = "packaging-19.2.tar.gz", hash = "sha256:28b924174df7a2fa32c1953825ff29c61e2f5e082343165438812f00d3a7fc47"}, +] +pathspec = [ + {file = "pathspec-0.6.0.tar.gz", hash = "sha256:e285ccc8b0785beadd4c18e5708b12bb8fcf529a1e61215b3feff1d1e559ea5c"}, +] +pluggy = [ + {file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"}, + {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"}, +] +py = [ + {file = "py-1.8.0-py2.py3-none-any.whl", hash = "sha256:64f65755aee5b381cea27766a3a147c3f15b9b6b9ac88676de66ba2ae36793fa"}, + {file = "py-1.8.0.tar.gz", hash = "sha256:dc639b046a6e2cff5bbe40194ad65936d6ba360b52b3c3fe1d08a82dd50b5e53"}, +] +pycodestyle = [ + {file = "pycodestyle-2.5.0-py2.py3-none-any.whl", hash = "sha256:95a2219d12372f05704562a14ec30bc76b05a5b297b21a5dfe3f6fac3491ae56"}, + {file = "pycodestyle-2.5.0.tar.gz", hash = "sha256:e40a936c9a450ad81df37f549d676d127b1b66000a6c500caa2b085bc0ca976c"}, +] +pycparser = [ + {file = "pycparser-2.19.tar.gz", hash = "sha256:a988718abfad80b6b157acce7bf130a30876d27603738ac39f140993246b25b3"}, +] +pyflakes = [ + {file = "pyflakes-2.1.1-py2.py3-none-any.whl", hash = "sha256:17dbeb2e3f4d772725c777fabc446d5634d1038f234e77343108ce445ea69ce0"}, + {file = "pyflakes-2.1.1.tar.gz", hash = "sha256:d976835886f8c5b31d47970ed689944a0262b5f3afa00a5a7b4dc81e5449f8a2"}, +] +pykube-ng = [ + {file = "pykube-ng-19.12.1.tar.gz", hash = "sha256:d8d184a4834e577236508198f0db8d6d46508698512ac742b6861d47de79265c"}, + {file = "pykube_ng-19.12.1-py3-none-any.whl", hash = "sha256:46974ff355d7d18ca9c3722f957e1182fb27a0401c4c06942bdfcd7e11ecffd3"}, +] +pyparsing = [ + {file = "pyparsing-2.4.5-py2.py3-none-any.whl", hash = "sha256:20f995ecd72f2a1f4bf6b072b63b22e2eb457836601e76d6e5dfcd75436acc1f"}, + {file = "pyparsing-2.4.5.tar.gz", hash = "sha256:4ca62001be367f01bd3e92ecbb79070272a9d4964dce6a48a82ff0b8bc7e683a"}, +] +pytest = [ + {file = "pytest-5.3.2-py3-none-any.whl", hash = "sha256:e41d489ff43948babd0fad7ad5e49b8735d5d55e26628a58673c39ff61d95de4"}, + {file = "pytest-5.3.2.tar.gz", hash = "sha256:6b571215b5a790f9b41f19f3531c53a45cf6bb8ef2988bc1ff9afb38270b25fa"}, +] +pytest-cov = [ + {file = "pytest-cov-2.8.1.tar.gz", hash = "sha256:cc6742d8bac45070217169f5f72ceee1e0e55b0221f54bcf24845972d3a47f2b"}, + {file = "pytest_cov-2.8.1-py2.py3-none-any.whl", hash = "sha256:cdbdef4f870408ebdbfeb44e63e07eb18bb4619fae852f6e760645fa36172626"}, +] +pyyaml = [ + {file = "PyYAML-5.2-cp27-cp27m-win32.whl", hash = "sha256:35ace9b4147848cafac3db142795ee42deebe9d0dad885ce643928e88daebdcc"}, + {file = "PyYAML-5.2-cp27-cp27m-win_amd64.whl", hash = "sha256:ebc4ed52dcc93eeebeae5cf5deb2ae4347b3a81c3fa12b0b8c976544829396a4"}, + {file = "PyYAML-5.2-cp35-cp35m-win32.whl", hash = "sha256:38a4f0d114101c58c0f3a88aeaa44d63efd588845c5a2df5290b73db8f246d15"}, + {file = "PyYAML-5.2-cp35-cp35m-win_amd64.whl", hash = "sha256:483eb6a33b671408c8529106df3707270bfacb2447bf8ad856a4b4f57f6e3075"}, + {file = "PyYAML-5.2-cp36-cp36m-win32.whl", hash = "sha256:7f38e35c00e160db592091751d385cd7b3046d6d51f578b29943225178257b31"}, + {file = "PyYAML-5.2-cp36-cp36m-win_amd64.whl", hash = "sha256:0e7f69397d53155e55d10ff68fdfb2cf630a35e6daf65cf0bdeaf04f127c09dc"}, + {file = "PyYAML-5.2-cp37-cp37m-win32.whl", hash = "sha256:e4c015484ff0ff197564917b4b4246ca03f411b9bd7f16e02a2f586eb48b6d04"}, + {file = "PyYAML-5.2-cp37-cp37m-win_amd64.whl", hash = "sha256:4b6be5edb9f6bb73680f5bf4ee08ff25416d1400fbd4535fe0069b2994da07cd"}, + {file = "PyYAML-5.2-cp38-cp38-win32.whl", hash = "sha256:8100c896ecb361794d8bfdb9c11fce618c7cf83d624d73d5ab38aef3bc82d43f"}, + {file = "PyYAML-5.2-cp38-cp38-win_amd64.whl", hash = "sha256:2e9f0b7c5914367b0916c3c104a024bb68f269a486b9d04a2e8ac6f6597b7803"}, + {file = "PyYAML-5.2.tar.gz", hash = "sha256:c0ee8eca2c582d29c3c2ec6e2c4f703d1b7f1fb10bc72317355a746057e7346c"}, +] +redis = [ + {file = "redis-3.3.11-py2.py3-none-any.whl", hash = "sha256:3613daad9ce5951e426f460deddd5caf469e08a3af633e9578fc77d362becf62"}, + {file = "redis-3.3.11.tar.gz", hash = "sha256:8d0fc278d3f5e1249967cba2eb4a5632d19e45ce5c09442b8422d15ee2c22cc2"}, +] +redlock-py = [ + {file = "redlock-py-1.0.8.tar.gz", hash = "sha256:0b8722c4843ddeabc2fc1dd37c05859e0da29fbce3bd1f6ecc73c98396f139ac"}, +] +regex = [ + {file = "regex-2019.12.20-cp27-cp27m-win32.whl", hash = "sha256:7bbbdbada3078dc360d4692a9b28479f569db7fc7f304b668787afc9feb38ec8"}, + {file = "regex-2019.12.20-cp27-cp27m-win_amd64.whl", hash = "sha256:a83049eb717ae828ced9cf607845929efcb086a001fc8af93ff15c50012a5716"}, + {file = "regex-2019.12.20-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:27d1bd20d334f50b7ef078eba0f0756a640fd25f5f1708d3b5bed18a5d6bced9"}, + {file = "regex-2019.12.20-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:1768cf42a78a11dae63152685e7a1d90af7a8d71d2d4f6d2387edea53a9e0588"}, + {file = "regex-2019.12.20-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:4850c78b53acf664a6578bba0e9ebeaf2807bb476c14ec7e0f936f2015133cae"}, + {file = "regex-2019.12.20-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:78b3712ec529b2a71731fbb10b907b54d9c53a17ca589b42a578bc1e9a2c82ea"}, + {file = "regex-2019.12.20-cp36-cp36m-win32.whl", hash = "sha256:8d9ef7f6c403e35e73b7fc3cde9f6decdc43b1cb2ff8d058c53b9084bfcb553e"}, + {file = "regex-2019.12.20-cp36-cp36m-win_amd64.whl", hash = "sha256:faad39fdbe2c2ccda9846cd21581063086330efafa47d87afea4073a08128656"}, + {file = "regex-2019.12.20-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:adc35d38952e688535980ae2109cad3a109520033642e759f987cf47fe278aa1"}, + {file = "regex-2019.12.20-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:ef0b828a7e22e58e06a1cceddba7b4665c6af8afeb22a0d8083001330572c147"}, + {file = "regex-2019.12.20-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:0e6cf1e747f383f52a0964452658c04300a9a01e8a89c55ea22813931b580aa8"}, + {file = "regex-2019.12.20-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:032fdcc03406e1a6485ec09b826eac78732943840c4b29e503b789716f051d8d"}, + {file = "regex-2019.12.20-cp37-cp37m-win32.whl", hash = "sha256:77ae8d926f38700432807ba293d768ba9e7652df0cbe76df2843b12f80f68885"}, + {file = "regex-2019.12.20-cp37-cp37m-win_amd64.whl", hash = "sha256:c29a77ad4463f71a506515d9ec3a899ed026b4b015bf43245c919ff36275444b"}, + {file = "regex-2019.12.20-cp38-cp38-manylinux1_i686.whl", hash = "sha256:57eacd38a5ec40ed7b19a968a9d01c0d977bda55664210be713e750dd7b33540"}, + {file = "regex-2019.12.20-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:724eb24b92fc5fdc1501a1b4df44a68b9c1dda171c8ef8736799e903fb100f63"}, + {file = "regex-2019.12.20-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:d508875793efdf6bab3d47850df8f40d4040ae9928d9d80864c1768d6aeaf8e3"}, + {file = "regex-2019.12.20-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:cfd31b3300fefa5eecb2fe596c6dee1b91b3a05ece9d5cfd2631afebf6c6fadd"}, + {file = "regex-2019.12.20-cp38-cp38-win32.whl", hash = "sha256:29b20f66f2e044aafba86ecf10a84e611b4667643c42baa004247f5dfef4f90b"}, + {file = "regex-2019.12.20-cp38-cp38-win_amd64.whl", hash = "sha256:d3ee0b035816e0520fac928de31b6572106f0d75597f6fa3206969a02baba06f"}, + {file = "regex-2019.12.20.tar.gz", hash = "sha256:106e25a841921d8259dcef2a42786caae35bc750fb996f830065b3dfaa67b77e"}, +] +requests = [ + {file = "requests-2.22.0-py2.py3-none-any.whl", hash = "sha256:9cf5292fcd0f598c671cfc1e0d7d1a7f13bb8085e9a590f48c010551dc6c4b31"}, + {file = "requests-2.22.0.tar.gz", hash = "sha256:11e007a8a2aa0323f5a921e9e6a2d7e4e67d9877e85773fba9ba6419025cbeb4"}, +] +requests-oauthlib = [ + {file = "requests-oauthlib-1.3.0.tar.gz", hash = "sha256:b4261601a71fd721a8bd6d7aa1cc1d6a8a93b4a9f5e96626f8e4d91e8beeaa6a"}, + {file = "requests_oauthlib-1.3.0-py2.py3-none-any.whl", hash = "sha256:7f71572defaecd16372f9006f33c2ec8c077c3cfa6f5911a9a90202beb513f3d"}, + {file = "requests_oauthlib-1.3.0-py3.7.egg", hash = "sha256:fa6c47b933f01060936d87ae9327fead68768b69c6c9ea2109c48be30f2d4dbc"}, +] +six = [ + {file = "six-1.13.0-py2.py3-none-any.whl", hash = "sha256:1f1b7d42e254082a9db6279deae68afb421ceba6158efa6131de7b3003ee93fd"}, + {file = "six-1.13.0.tar.gz", hash = "sha256:30f610279e8b2578cab6db20741130331735c781b56053c59c4076da27f06b66"}, +] +stups-tokens = [ + {file = "stups-tokens-1.1.19.tar.gz", hash = "sha256:7830ad83ccbfd52a9734608ffcefcca917137ce9480cc91a4fbd321a4aca3160"}, + {file = "stups_tokens-1.1.19-py3-none-any.whl", hash = "sha256:317f4386763bac9dd5c0a4c8b0f9f0238dc3fa81de3c6fd1971b6b01662b5750"}, +] +toml = [ + {file = "toml-0.10.0-py2.7.egg", hash = "sha256:f1db651f9657708513243e61e6cc67d101a39bad662eaa9b5546f789338e07a3"}, + {file = "toml-0.10.0-py2.py3-none-any.whl", hash = "sha256:235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e"}, + {file = "toml-0.10.0.tar.gz", hash = "sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c"}, +] +typed-ast = [ + {file = "typed_ast-1.4.0-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:262c247a82d005e43b5b7f69aff746370538e176131c32dda9cb0f324d27141e"}, + {file = "typed_ast-1.4.0-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:71211d26ffd12d63a83e079ff258ac9d56a1376a25bc80b1cdcdf601b855b90b"}, + {file = "typed_ast-1.4.0-cp35-cp35m-win32.whl", hash = "sha256:630968c5cdee51a11c05a30453f8cd65e0cc1d2ad0d9192819df9978984529f4"}, + {file = "typed_ast-1.4.0-cp35-cp35m-win_amd64.whl", hash = "sha256:ffde2fbfad571af120fcbfbbc61c72469e72f550d676c3342492a9dfdefb8f12"}, + {file = "typed_ast-1.4.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:4e0b70c6fc4d010f8107726af5fd37921b666f5b31d9331f0bd24ad9a088e631"}, + {file = "typed_ast-1.4.0-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:bc6c7d3fa1325a0c6613512a093bc2a2a15aeec350451cbdf9e1d4bffe3e3233"}, + {file = "typed_ast-1.4.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:cc34a6f5b426748a507dd5d1de4c1978f2eb5626d51326e43280941206c209e1"}, + {file = "typed_ast-1.4.0-cp36-cp36m-win32.whl", hash = "sha256:d896919306dd0aa22d0132f62a1b78d11aaf4c9fc5b3410d3c666b818191630a"}, + {file = "typed_ast-1.4.0-cp36-cp36m-win_amd64.whl", hash = "sha256:354c16e5babd09f5cb0ee000d54cfa38401d8b8891eefa878ac772f827181a3c"}, + {file = "typed_ast-1.4.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:95bd11af7eafc16e829af2d3df510cecfd4387f6453355188342c3e79a2ec87a"}, + {file = "typed_ast-1.4.0-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:18511a0b3e7922276346bcb47e2ef9f38fb90fd31cb9223eed42c85d1312344e"}, + {file = "typed_ast-1.4.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:d7c45933b1bdfaf9f36c579671fec15d25b06c8398f113dab64c18ed1adda01d"}, + {file = "typed_ast-1.4.0-cp37-cp37m-win32.whl", hash = "sha256:d755f03c1e4a51e9b24d899561fec4ccaf51f210d52abdf8c07ee2849b212a36"}, + {file = "typed_ast-1.4.0-cp37-cp37m-win_amd64.whl", hash = "sha256:2b907eb046d049bcd9892e3076c7a6456c93a25bebfe554e931620c90e6a25b0"}, + {file = "typed_ast-1.4.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:fdc1c9bbf79510b76408840e009ed65958feba92a88833cdceecff93ae8fff66"}, + {file = "typed_ast-1.4.0-cp38-cp38-manylinux1_i686.whl", hash = "sha256:7954560051331d003b4e2b3eb822d9dd2e376fa4f6d98fee32f452f52dd6ebb2"}, + {file = "typed_ast-1.4.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:48e5b1e71f25cfdef98b013263a88d7145879fbb2d5185f2a0c79fa7ebbeae47"}, + {file = "typed_ast-1.4.0-cp38-cp38-win32.whl", hash = "sha256:1170afa46a3799e18b4c977777ce137bb53c7485379d9706af8a59f2ea1aa161"}, + {file = "typed_ast-1.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:838997f4310012cf2e1ad3803bce2f3402e9ffb71ded61b5ee22617b3a7f6b6e"}, + {file = "typed_ast-1.4.0.tar.gz", hash = "sha256:66480f95b8167c9c5c5c87f32cf437d585937970f3fc24386f313a4c97b44e34"}, +] +typing-extensions = [ + {file = "typing_extensions-3.7.4.1-py2-none-any.whl", hash = "sha256:910f4656f54de5993ad9304959ce9bb903f90aadc7c67a0bef07e678014e892d"}, + {file = "typing_extensions-3.7.4.1-py3-none-any.whl", hash = "sha256:cf8b63fedea4d89bab840ecbb93e75578af28f76f66c35889bd7065f5af88575"}, + {file = "typing_extensions-3.7.4.1.tar.gz", hash = "sha256:091ecc894d5e908ac75209f10d5b4f118fbdb2eb1ede6a63544054bb1edb41f2"}, +] +urllib3 = [ + {file = "urllib3-1.22-py2.py3-none-any.whl", hash = "sha256:06330f386d6e4b195fbfc736b297f58c5a892e4440e54d294d7004e3a9bbea1b"}, + {file = "urllib3-1.22.tar.gz", hash = "sha256:cc44da8e1145637334317feebd728bd869a35285b93cbb4cca2577da7e62db4f"}, +] +urlobject = [ + {file = "URLObject-2.4.3.tar.gz", hash = "sha256:47b2e20e6ab9c8366b2f4a3566b6ff4053025dad311c4bb71279bbcfa2430caa"}, +] +wcwidth = [ + {file = "wcwidth-0.1.7-py2.py3-none-any.whl", hash = "sha256:f4ebe71925af7b40a864553f761ed559b43544f8f71746c2d756c7fe788ade7c"}, + {file = "wcwidth-0.1.7.tar.gz", hash = "sha256:3df37372226d6e63e1b1e1eda15c594bca98a22d33a23832a90998faa96bc65e"}, +] +werkzeug = [ + {file = "Werkzeug-0.16.0-py2.py3-none-any.whl", hash = "sha256:e5f4a1f98b52b18a93da705a7458e55afb26f32bff83ff5d19189f92462d65c4"}, + {file = "Werkzeug-0.16.0.tar.gz", hash = "sha256:7280924747b5733b246fe23972186c6b348f9ae29724135a6dfc1e53cea433e7"}, +] +zipp = [ + {file = "zipp-0.6.0-py2.py3-none-any.whl", hash = "sha256:f06903e9f1f43b12d371004b4ac7b06ab39a44adc747266928ae6debfa7b3335"}, + {file = "zipp-0.6.0.tar.gz", hash = "sha256:3718b1cbcd963c7d4c5511a8240812904164b7f381b647143a89d3b98f9bcd8e"}, +] diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..dfc5d7e --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,26 @@ +[tool] +[tool.poetry] +name = "kube-ops-view" +version = "2020.0.dev1" +description = "Kubernetes Operational View - read-only system dashboard for multiple K8s clusters" +authors = ["Henning Jacobs "] + +[tool.poetry.dependencies] +python = ">=3.7" +click = "*" +flask = "*" +flask-dance = "*" +gevent = "*" +json-delta = ">=2.0" +pykube-ng = "*" +redlock-py = "*" +requests = "*" +stups-tokens = ">=1.1.19" + +[tool.poetry.dev-dependencies] +coveralls = "*" +flake8 = "*" +pytest = "*" +pytest-cov = "*" +black = "^19.10b0" +mypy = "^0.761" diff --git a/setup.py b/setup.py deleted file mode 100644 index 122bc95..0000000 --- a/setup.py +++ /dev/null @@ -1,78 +0,0 @@ -import sys - -from setuptools import find_packages, setup -from setuptools.command.test import test as TestCommand - -from pathlib import Path - - -def read_version(package): - with (Path(package) / '__init__.py').open() as fd: - for line in fd: - if line.startswith('__version__ = '): - return line.split()[-1].strip().strip("'") - - -version = read_version('kube_ops_view') - - -class PyTest(TestCommand): - - user_options = [('cov-html=', None, 'Generate junit html report')] - - def initialize_options(self): - TestCommand.initialize_options(self) - self.cov = None - self.pytest_args = ['--cov', 'kube_ops_view', '--cov-report', 'term-missing', '-v'] - self.cov_html = False - - def finalize_options(self): - TestCommand.finalize_options(self) - if self.cov_html: - self.pytest_args.extend(['--cov-report', 'html']) - self.pytest_args.extend(['tests']) - - def run_tests(self): - import pytest - - errno = pytest.main(self.pytest_args) - sys.exit(errno) - - -def readme(): - return open('README.rst', encoding='utf-8').read() - - -tests_require = [ - 'pytest', - 'pytest-cov' -] - -setup( - name='kube-ops-view', - packages=find_packages(), - version=version, - description='Kubernetes Operational View - read-only system dashboard for multiple K8s clusters', - long_description=readme(), - author='Henning Jacobs', - url='https://github.com/hjacobs/kube-ops-view', - keywords='kubernetes operations dashboard view k8s', - license='GNU General Public License v3 (GPLv3)', - tests_require=tests_require, - extras_require={'tests': tests_require}, - cmdclass={'test': PyTest}, - test_suite='tests', - classifiers=[ - 'Development Status :: 3 - Alpha', - 'Intended Audience :: Developers', - 'Intended Audience :: System Administrators', - 'License :: OSI Approved :: GNU General Public License v3 (GPLv3)', - 'Operating System :: OS Independent', - 'Programming Language :: Python', - 'Programming Language :: Python :: 3.5', - 'Topic :: System :: Clustering', - 'Topic :: System :: Monitoring', - ], - include_package_data=True, # needed to include JavaScript (see MANIFEST.in) - entry_points={'console_scripts': ['kube-ops-view = kube_ops_view.main:main']} -) diff --git a/tox.ini b/tox.ini deleted file mode 100644 index 67db935..0000000 --- a/tox.ini +++ /dev/null @@ -1,26 +0,0 @@ -[tox] -envlist=py36,flake8,eslint - -[tox:travis] -3.6=py36,flake8,eslint - -[testenv] -deps=pipenv -commands= - pipenv install --dev - pipenv run python setup.py test - -[testenv:flake8] -deps=pipenv -commands= - pipenv install --dev - pipenv run flake8 - -[testenv:eslint] -whitelist_externals=eslint -changedir=app -commands=eslint src - -[flake8] -max-line-length=160 -ignore=E402,E722,E252