From ef8bc0c1279f45abfa3eff22c766a31c2b09dccc Mon Sep 17 00:00:00 2001 From: Brice Schaffner Date: Tue, 25 Jan 2022 16:17:46 +0100 Subject: [PATCH 01/13] Improved route logging --- app/__init__.py | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/app/__init__.py b/app/__init__.py index ade83a3..10cc6b6 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -1,10 +1,12 @@ import logging import re +import time from werkzeug.exceptions import HTTPException from flask import Flask from flask import abort +from flask import g from flask import request from app.helpers import make_error_msg @@ -27,7 +29,8 @@ # the route might not be logged if another method reject the request. @app.before_request def log_route(): - route_logger.info('%s %s', request.method, request.path) + g.setdefault('started', time.time()) + route_logger.debug('%s %s', request.method, request.path) # Add CORS Headers to all request @@ -68,6 +71,24 @@ def validate_origin(): abort(403, 'Not allowed') +@app.after_request +def log_response(response): + logger.info( + "%s %s - %s", + request.method, + request.path, + response.status, + extra={ + 'response': + { + "status_code": response.status_code, "headers": dict(response.headers.items()) + }, + "duration": time.time() - g.get('started', time.time()) + } + ) + return response + + # Register error handler to make sure that every error returns a json answer @app.errorhandler(Exception) def handle_exception(err): From 9f9fd6a8f63d5841d0a7c643eebe1f946ca9def6 Mon Sep 17 00:00:00 2001 From: Brice Schaffner Date: Wed, 26 Jan 2022 06:55:02 +0100 Subject: [PATCH 02/13] Updated docu and added license --- .env.local | 2 +- .gitignore | 4 ++- LICENSE | 29 +++++++++++++++++ README.md | 93 +++++++++++++++--------------------------------------- 4 files changed, 58 insertions(+), 70 deletions(-) create mode 100644 LICENSE diff --git a/.env.local b/.env.local index 34ccbd4..2bf4846 100644 --- a/.env.local +++ b/.env.local @@ -1 +1 @@ -ALLOWED_DOMAINS=http[s]?://localhost \ No newline at end of file +ALLOWED_DOMAINS=.* \ No newline at end of file diff --git a/.gitignore b/.gitignore index a58f951..a2bc075 100644 --- a/.gitignore +++ b/.gitignore @@ -58,4 +58,6 @@ nose2-junit.xml *.orig # visual studio code config -.vscode \ No newline at end of file +.vscode + +out.png \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..e11be44 --- /dev/null +++ b/LICENSE @@ -0,0 +1,29 @@ +BSD 3-Clause License + +Copyright (c) 2020, swisstopo +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/README.md b/README.md index 05c10a8..d01a798 100644 --- a/README.md +++ b/README.md @@ -11,9 +11,6 @@ - [Description](#description) - [Dependencies](#dependencies) - [Service API](#service-api) - - [Staging Environments](#staging-environments) - - [checker GET](#checker-get) - - [color GET](#color-get) - [Versioning](#versioning) - [Local Development](#local-development) - [Make Dependencies](#make-dependencies) @@ -22,8 +19,6 @@ - [Test your work](#test-your-work) - [Docker](#docker) - [Deployment](#deployment) - - [Dev](#dev) - - [Int](#int) - [Deployment configuration](#deployment-configuration) ## Description @@ -36,37 +31,19 @@ This service doesn't have any external dependencies ## Service API -This service has two endpoints: +The service has the following endpoints: -- [checker GET](#checker-get) -- [color GET](#color-get) +- `GET /checker` +- `GET /sets` +- `GET /sets/` +- `GET /sets//icons` +- `GET /sets//icons/` +- `GET /sets//icons/.png` +- `GET /sets//icons/-,,.png` +- `GET /sets//icons/@.png` +- `GET /sets//icons/@-,,.png` -A detailed descriptions of the endpoints can be found in the [OpenAPI Spec](openapi.yaml). - -### Staging Environments - -| Environments | URL | -| ------------ | --------------------------------------------------------------------------------------------------------------------- | -| DEV | [https://service-icons.bgdi-dev.swisstopo.cloud/v4/icons/](https://service-icons.bgdi-dev.swisstopo.cloud/v4/icons/) | -| INT | [https://service-icons.bgdi-int.swisstopo.cloud/v4/icons/](https://service-icons.bgdi-int.swisstopo.cloud/v4/icons/) | -| PROD | [https://service-icons.bgdi-prod.swisstopo.cloud/v4/icons/](https://service-icons.bgdi-int.swisstopo.cloud/v4/icons/) | - -### checker GET - -This is a simple route meant to test if the server is up. - -| Path | Method | Argument | Response Type | -| ----------------- | ------ | -------- | ---------------- | -| /v4/icons/checker | GET | - | application/json | - -### color GET - -This route takes a color (defined by r, g and b values) and the name of a file containing a symbol to be colorized -and returns the colorized symbol. - -| Path | Method | Argument | Response Type | -| --------- | ------ | ----------------- | ------------- | -| /v4/icons | GET | r, g, b, filename | image/png | +A detailed descriptions of the endpoints can be found in the [OpenAPI Spec](https://github.com/geoadmin/doc-api-specs) repository. ## Versioning @@ -78,7 +55,7 @@ See also [Git Flow - Versioning](https://github.com/geoadmin/doc-guidelines/blob ### Make Dependencies -The **Make** targets assume you have **python3.7**, **pipenv**, **bash**, **curl**, **tar**, **docker** and **docker-compose** installed. +The **Make** targets assume you have **python3.9**, **pipenv**, **bash**, **curl**, **tar**, **docker** and **docker-compose** installed. ### Setting up to work @@ -91,7 +68,7 @@ git clone git@github.com:geoadmin/service-icons Then, you can run the setup target to ensure you have everything needed to develop, test and serve locally ```bash -make setup +make dev ``` That's it, you're ready to work. @@ -137,9 +114,14 @@ make dockerrun This will serve the application with the wsgi server, inside a container. - curl -H "Origin: https://map.geo.admin.ch/" http://localhost:5000/v4/icons/255,133,133/001-marker-24@2x.png --output out.dat +Here below are simple examples of how to test the service after serving on localhost:5000: + +```bash +curl -H "Origin: www.example.com" http://localhost:5000/sets/default/icons +curl -H "Origin: www.example.com" http://localhost:5000/sets/default/icons/001-marker@2x-255,133,133.png --output out.png +``` -This is a simple example of how to test the service after serving on localhost:5000 (`out.dat` will either contain a PNG image or contain an error message.) +*NOTE: if you serve using gunicorn or docker, you need to add the route prefix `/api/icons`* ## Docker @@ -176,35 +158,6 @@ docker ps --format="table {{.ID}}\t{{.Image}}\t{{.Labels}}" ## Deployment -This service is to be deployed to the Kubernetes cluster once it is merged. - -**Check on which k8s you are before doing anything** - -by running : - -```bash -kubectl config get-contexts -``` -or (if you already have it installed) -```bash -kubectx -``` - -Make sure you are on the right context (staging) for what you want to achieve - -### Dev - -To deploy (or refresh) on dev, we have to kill all pods. The K8S configuration is made so that it will revive any killed pod while always retrieving the latest docker image. -So as soon as your merge on `develop` as been successfully built by the CI (and so the CI has pushed the new image to our AWS ECR registry) you can kill all dev pods by running - -```bash -kubectl delete --all pods --namespace=service-icons -``` - -### Int - -TO DO: give instructions to deploy to kubernetes. - ### Deployment configuration The service is configured by Environment Variable: @@ -213,5 +166,9 @@ The service is configured by Environment Variable: | ----------- | --------------------- | -------------------------- | | LOGGING_CFG | `logging-cfg-local.yml` | Logging configuration file | | ALLOWED_DOMAINS | `.*` | Comma separated list of regex that are allowed as domain in Origin header | -| CACHE_CONTROL | `public, max-age=86400` | Cache Control header value of the `GET /v4/icons/*` endpoints | +| CACHE_CONTROL | `public, max-age=86400` | Cache Control header value of the `GET /*` endpoints | | CACHE_CONTROL_4XX | `public, max-age=3600` | Cache Control header for 4XX responses | +| FORWARED_ALLOW_IPS | `*` | Sets the gunicorn `forwarded_allow_ips`. See [Gunicorn Doc](https://docs.gunicorn.org/en/stable/settings.html#forwarded-allow-ips). This setting is required in order to `secure_scheme_headers` to work. | +| FORWARDED_PROTO_HEADER_NAME | `X-Forwarded-Proto` | Sets gunicorn `secure_scheme_headers` parameter to `{${FORWARDED_PROTO_HEADER_NAME}: 'https'}`. This settings is required in order to generate correct URLs in the service responses. See [Gunicorn Doc](https://docs.gunicorn.org/en/stable/settings.html#secure-scheme-headers). | +| SCRIPT_NAME | `''` | If the service is behind a reverse proxy and not served at the root, the route prefix must be set in `SCRIPT_NAME`. | +| WSGI_TIMEOUT | `5` | WSGI timeout. | From 8e16085e3d7e31ec6fe18a781bd273e8c79f708d Mon Sep 17 00:00:00 2001 From: Brice Schaffner Date: Wed, 26 Jan 2022 07:01:59 +0100 Subject: [PATCH 03/13] Updating yapf formatting --- .style.yapf | 13 ++++++++++++- app/__init__.py | 7 +++---- app/version.py | 11 +++++------ tests/unit_tests/test_all_icons.py | 2 +- tests/unit_tests/test_icons.py | 16 +++++++--------- wsgi.py | 7 +++---- 6 files changed, 31 insertions(+), 25 deletions(-) diff --git a/.style.yapf b/.style.yapf index 1d02d73..1930af0 100644 --- a/.style.yapf +++ b/.style.yapf @@ -17,9 +17,20 @@ based_on_style=google # end_ts=now(), # ) # <--- this bracket is dedented and on a separate line dedent_closing_brackets=True +coalesce_brackets=True -# Split before arguments, but do not split all subexpressions recursively +arithmetic_precedence_indication=True +no_spaces_around_selected_binary_operators=True + +# This avoid issues with complex dictionary +# see https://github.com/google/yapf/issues/392#issuecomment-407958737 +indent_dictionary_value=True +allow_split_before_dict_value=False + +# Split before arguments, but do not split all sub expressions recursively # (unless needed). split_all_top_level_comma_separated_values=True +# Split lines longer than 100 characters (this only applies to code not to +# comment and docstring) column_limit=100 \ No newline at end of file diff --git a/app/__init__.py b/app/__init__.py index 10cc6b6..7a71b60 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -79,10 +79,9 @@ def log_response(response): request.path, response.status, extra={ - 'response': - { - "status_code": response.status_code, "headers": dict(response.headers.items()) - }, + 'response': { + "status_code": response.status_code, "headers": dict(response.headers.items()) + }, "duration": time.time() - g.get('started', time.time()) } ) diff --git a/app/version.py b/app/version.py index f81a824..f87994d 100644 --- a/app/version.py +++ b/app/version.py @@ -10,17 +10,16 @@ # the tag is directly related to the commit or has an additional # suffix 'v[0-9]+\.[0-9]+\.[0-9]+-beta.[0-9]-[0-9]+-gHASH' denoting # the 'distance' to the latest tag -with subprocess.Popen( - ["git", "describe", "--tags"], stdout=subprocess.PIPE, stderr=subprocess.PIPE -) as proc: +with subprocess.Popen(["git", "describe", "--tags"], stdout=subprocess.PIPE, + stderr=subprocess.PIPE) as proc: stdout, stderr = proc.communicate() GIT_VERSION = stdout.decode('utf-8').strip() if GIT_VERSION == '': # If theres no git tag found in the history we simply use the short # version of the latest git commit hash - with subprocess.Popen( - ["git", "rev-parse", "--short", "HEAD"], stdout=subprocess.PIPE, stderr=subprocess.PIPE - ) as proc: + with subprocess.Popen(["git", "rev-parse", "--short", "HEAD"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) as proc: stdout, stderr = proc.communicate() APP_VERSION = f"v_{stdout.decode('utf-8').strip()}" else: diff --git a/tests/unit_tests/test_all_icons.py b/tests/unit_tests/test_all_icons.py index f692fc6..8372e58 100644 --- a/tests/unit_tests/test_all_icons.py +++ b/tests/unit_tests/test_all_icons.py @@ -316,7 +316,7 @@ def test_all_icons_colorized_and_half_size(self): with self.subTest(icon_set_name=icon_set_name, icon_name=icon_name): icon_set = get_icon_set(icon_set_name) params = { - "icon_set_name": icon_set_name, "icon_name": icon_name, "scale": ".5x" + "icon_set_name": icon_set_name, "icon_name": icon_name, "scale": '0.5x' } if icon_set.colorable: params["red"] = 0 diff --git a/tests/unit_tests/test_icons.py b/tests/unit_tests/test_icons.py index 66a0a63..9f536f2 100644 --- a/tests/unit_tests/test_icons.py +++ b/tests/unit_tests/test_icons.py @@ -19,11 +19,10 @@ def test_colorized_icon_with_wrong_rgb_value(self): self.assertEqual( response.json, { - "error": - { - "code": 400, - "message": "Color channel values must be integers in the range of 0 to 255." - }, + "error": { + "code": 400, + "message": "Color channel values must be integers in the range of 0 to 255." + }, "success": False } ) @@ -79,10 +78,9 @@ def test_colorized_icon_no_http_post_method_allowed_on_endpoint(self): self.assertEqual( response.json, { - "error": - { - "code": 405, "message": "The method is not allowed for the requested URL." - }, + "error": { + "code": 405, "message": "The method is not allowed for the requested URL." + }, "success": False } ) diff --git a/wsgi.py b/wsgi.py index 3d8be6a..473c4c4 100644 --- a/wsgi.py +++ b/wsgi.py @@ -36,9 +36,8 @@ def load(self): 'timeout': 60, 'logconfig_dict': get_logging_cfg(), 'forwarded_allow_ips': os.getenv('FORWARED_ALLOW_IPS', '*'), - 'secure_scheme_headers': - { - os.getenv('FORWARDED_PROTO_HEADER_NAME', 'X-Forwarded-Proto').upper(): 'https' - } + 'secure_scheme_headers': { + os.getenv('FORWARDED_PROTO_HEADER_NAME', 'X-Forwarded-Proto').upper(): 'https' + } } StandaloneApplication(application, options).run() From 3f10f045a0c4ec661a768972a96a70a26a298a64 Mon Sep 17 00:00:00 2001 From: Brice Schaffner Date: Wed, 26 Jan 2022 07:02:33 +0100 Subject: [PATCH 04/13] Reduced the WSGI default timeout and made it configurable This service is quite fast and doesn't make sense to have a 60s timeout, every request should be done without a few hundred milliseconds. --- wsgi.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wsgi.py b/wsgi.py index 473c4c4..2bb6396 100644 --- a/wsgi.py +++ b/wsgi.py @@ -33,7 +33,7 @@ def load(self): 'bind': f"0.0.0.0:{HTTP_PORT}", 'worker_class': 'gevent', 'workers': 2, # scaling horizontally is left to Kubernetes - 'timeout': 60, + 'timeout': int(os.getenv('WSGI_TIMEOUT', '5')), 'logconfig_dict': get_logging_cfg(), 'forwarded_allow_ips': os.getenv('FORWARED_ALLOW_IPS', '*'), 'secure_scheme_headers': { From d02500e80fca7a064c80022c866f488a1fc76f47 Mon Sep 17 00:00:00 2001 From: Brice Schaffner Date: Wed, 26 Jan 2022 07:04:04 +0100 Subject: [PATCH 05/13] Upgraded to python 3.9 --- Dockerfile | 4 +- Pipfile | 2 +- Pipfile.lock | 278 ++++++++++++++++++++------------------------------- 3 files changed, 111 insertions(+), 173 deletions(-) diff --git a/Dockerfile b/Dockerfile index 9e1cb52..5239e66 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ -# Buster slim python 3.7 base image. -FROM python:3.7-slim-buster +# Buster slim python 3.9 base image. +FROM python:3.9-slim-buster ENV HTTP_PORT 8080 RUN groupadd -r geoadmin && useradd -r -s /bin/false -g geoadmin geoadmin diff --git a/Pipfile b/Pipfile index 354d42a..1fc5170 100644 --- a/Pipfile +++ b/Pipfile @@ -19,4 +19,4 @@ pylint = "*" pylint-flask = "*" [requires] -python_version = "3.7" \ No newline at end of file +python_version = "3.9" \ No newline at end of file diff --git a/Pipfile.lock b/Pipfile.lock index 49782cf..e345c25 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,11 +1,11 @@ { "_meta": { "hash": { - "sha256": "95fb9e236d7106c3d45ab9ecb1c2248096e1ea34f9d909f1d19d0c4c4e98cfa3" + "sha256": "8a662da75809c934f7d8b89ef809ecead11c571a705c5a85d273b5e6d84b6d3a" }, "pipfile-spec": 6, "requires": { - "python_version": "3.7" + "python_version": "3.9" }, "sources": [ { @@ -21,7 +21,6 @@ "sha256:353f466495adaeb40b6b5f592f9f91cb22372351c84caeb068132442a4518ef3", "sha256:410e932b050f5eed773c4cda94de75971c89cdb3155a72a0831139a79e5ecb5b" ], - "markers": "python_version >= '3.6'", "version": "==8.0.3" }, "flask": { @@ -68,6 +67,7 @@ }, "greenlet": { "hashes": [ + "sha256:0051c6f1f27cb756ffc0ffbac7d2cd48cb0362ac1736871399a739b2885134d3", "sha256:00e44c8afdbe5467e4f7b5851be223be68adb4272f44696ee71fe46b7036a711", "sha256:013d61294b6cd8fe3242932c1c5e36e5d1db2c8afb58606c5a67efce62c1f5fd", "sha256:049fe7579230e44daef03a259faa24511d10ebfa44f69411d99e6a184fe68073", @@ -77,6 +77,7 @@ "sha256:1e12bdc622676ce47ae9abbf455c189e442afdde8818d9da983085df6312e7a1", "sha256:21915eb821a6b3d9d8eefdaf57d6c345b970ad722f856cd71739493ce003ad08", "sha256:288c6a76705dc54fba69fbcb59904ae4ad768b4c768839b8ca5fdadec6dd8cfd", + "sha256:2bde6792f313f4e918caabc46532aa64aa27a0db05d75b20edfc5c6f46479de2", "sha256:32ca72bbc673adbcfecb935bb3fb1b74e663d10a4b241aaa2f5a75fe1d1f90aa", "sha256:356b3576ad078c89a6107caa9c50cc14e98e3a6c4874a37c3e0273e4baf33de8", "sha256:40b951f601af999a8bf2ce8c71e8aaa4e8c6f78ff8afae7b808aae2dc50d4c40", @@ -89,6 +90,7 @@ "sha256:7ff61ff178250f9bb3cd89752df0f1dd0e27316a8bd1465351652b1b4a4cdfd3", "sha256:833e1551925ed51e6b44c800e71e77dacd7e49181fdc9ac9a0bf3714d515785d", "sha256:8639cadfda96737427330a094476d4c7a56ac03de7265622fcf4cfe57c8ae18d", + "sha256:8c5d5b35f789a030ebb95bff352f1d27a93d81069f2adb3182d99882e095cefe", "sha256:8c790abda465726cfb8bb08bd4ca9a5d0a7bd77c7ac1ca1b839ad823b948ea28", "sha256:8d2f1fb53a421b410751887eb4ff21386d119ef9cde3797bf5e7ed49fb51a3b3", "sha256:903bbd302a2378f984aef528f76d4c9b1748f318fe1294961c072bdc7f2ffa3e", @@ -102,6 +104,8 @@ "sha256:aec52725173bd3a7b56fe91bc56eccb26fbdff1386ef123abb63c84c5b43b63a", "sha256:b11548073a2213d950c3f671aa88e6f83cda6e2fb97a8b6317b1b5b33d850e06", "sha256:b1692f7d6bc45e3200844be0dba153612103db241691088626a33ff1f24a0d88", + "sha256:b336501a05e13b616ef81ce329c0e09ac5ed8c732d9ba7e3e983fcc1a9e86965", + "sha256:b8c008de9d0daba7b6666aa5bbfdc23dcd78cafc33997c9b7741ff6353bafb7f", "sha256:b92e29e58bef6d9cfd340c72b04d74c4b4e9f70c9fa7c78b674d1fec18896dc4", "sha256:be5f425ff1f5f4b3c1e33ad64ab994eed12fc284a6ea71c5243fd564502ecbe5", "sha256:dd0b1e9e891f69e7675ba5c92e28b90eaa045f6ab134ffe70b52e948aa175b3c", @@ -130,20 +134,11 @@ "index": "pypi", "version": "==20.1.0" }, - "importlib-metadata": { - "hashes": [ - "sha256:53ccfd5c134223e497627b9815d5030edf77d2ed573922f7a0b8f8bb81a1c100", - "sha256:75bdec14c397f528724c1bfd9709d660b33a4d2e77387a3358f20b848bb5e5fb" - ], - "markers": "python_version < '3.8'", - "version": "==4.8.2" - }, "itsdangerous": { "hashes": [ "sha256:5174094b9637652bdb841a3029700391451bd092ba3db90600dea710ba28e97c", "sha256:9e724d68fc22902a1435351f84c3fb8623f303fffcc566a4cb952df8c572cff0" ], - "markers": "python_version >= '3.6'", "version": "==2.0.1" }, "jinja2": { @@ -151,7 +146,6 @@ "sha256:077ce6014f7b40d03b47d1f1ca4b0fc8328a692bd284016f806ed0eaca390ad8", "sha256:611bb273cd68f3b993fabdc4064fc858c5b47a973cb5aa7999ec1ba405c87cd7" ], - "markers": "python_version >= '3.6'", "version": "==3.0.3" }, "logging-utilities": { @@ -234,7 +228,6 @@ "sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51", "sha256:fa130dd50c57d53368c9d59395cb5526eda596d3ffe36666cd81a44d56e48872" ], - "markers": "python_version >= '3.6'", "version": "==2.0.1" }, "pillow": { @@ -339,38 +332,13 @@ "index": "pypi", "version": "==5.4.1" }, - "setuptools": { - "hashes": [ - "sha256:b4c634615a0cf5b02cf83c7bedffc8da0ca439f00e79452699454da6fbd4153d", - "sha256:feb5ff19b354cde9efd2344ef6d5e79880ce4be643037641b49508bbb850d060" - ], - "markers": "python_version >= '3.6'", - "version": "==59.4.0" - }, - "typing-extensions": { - "hashes": [ - "sha256:4ca091dea149f945ec56afb48dae714f21e8692ef22a395223bcd328961b6a0e", - "sha256:7f001e5ac290a0c0401508864c7ec868be4e701886d5b573a9528ed3973d9d3b" - ], - "markers": "python_version < '3.8'", - "version": "==4.0.1" - }, "werkzeug": { "hashes": [ "sha256:63d3dc1cf60e7b7e35e97fa9861f7397283b75d765afcaefd993d6046899de8f", "sha256:aa2bb6fc8dee8d6c504c0ac1e7f5f7dc5810a9903e793b6f715a9f015bdadb9a" ], - "markers": "python_version >= '3.6'", "version": "==2.0.2" }, - "zipp": { - "hashes": [ - "sha256:71c644c5369f4a6e07636f0aa966270449561fcea2e3d6747b8d23efaa9d7832", - "sha256:9fe5ea21568a0a70e50f273397638d39b03353731e6cbbb3fd8502a33fec40bc" - ], - "markers": "python_version >= '3.6'", - "version": "==3.6.0" - }, "zope.event": { "hashes": [ "sha256:2666401939cdaa5f4e0c08cf7f20c9b21423b95e88f4675b1443973bdb080c42", @@ -432,107 +400,114 @@ "sha256:f44e517131a98f7a76696a7b21b164bcb85291cee106a23beccce454e1f433a4", "sha256:f7ee479e96f7ee350db1cf24afa5685a5899e2b34992fb99e1f7c1b0b758d263" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", "version": "==5.4.0" } }, "develop": { "astroid": { "hashes": [ - "sha256:5939cf55de24b92bda00345d4d0659d01b3c7dafb5055165c330bc7c568ba273", - "sha256:776ca0b748b4ad69c00bfe0fff38fa2d21c338e12c84aa9715ee0d473c422778" + "sha256:1efdf4e867d4d8ba4a9f6cf9ce07cd182c4c41de77f23814feb27ca93ca9d877", + "sha256:506daabe5edffb7e696ad82483ad0228245a9742ed7d2d8c9cdb31537decf9f6" ], - "markers": "python_version ~= '3.6'", - "version": "==2.9.0" + "version": "==2.9.3" }, "coverage": { "hashes": [ - "sha256:01774a2c2c729619760320270e42cd9e797427ecfddd32c2a7b639cdc481f3c0", - "sha256:03b20e52b7d31be571c9c06b74746746d4eb82fc260e594dc662ed48145e9efd", - "sha256:0a7726f74ff63f41e95ed3a89fef002916c828bb5fcae83b505b49d81a066884", - "sha256:1219d760ccfafc03c0822ae2e06e3b1248a8e6d1a70928966bafc6838d3c9e48", - "sha256:13362889b2d46e8d9f97c421539c97c963e34031ab0cb89e8ca83a10cc71ac76", - "sha256:174cf9b4bef0db2e8244f82059a5a72bd47e1d40e71c68ab055425172b16b7d0", - "sha256:17e6c11038d4ed6e8af1407d9e89a2904d573be29d51515f14262d7f10ef0a64", - "sha256:215f8afcc02a24c2d9a10d3790b21054b58d71f4b3c6f055d4bb1b15cecce685", - "sha256:22e60a3ca5acba37d1d4a2ee66e051f5b0e1b9ac950b5b0cf4aa5366eda41d47", - "sha256:2641f803ee9f95b1f387f3e8f3bf28d83d9b69a39e9911e5bfee832bea75240d", - "sha256:276651978c94a8c5672ea60a2656e95a3cce2a3f31e9fb2d5ebd4c215d095840", - "sha256:3f7c17209eef285c86f819ff04a6d4cbee9b33ef05cbcaae4c0b4e8e06b3ec8f", - "sha256:3feac4084291642165c3a0d9eaebedf19ffa505016c4d3db15bfe235718d4971", - "sha256:49dbff64961bc9bdd2289a2bda6a3a5a331964ba5497f694e2cbd540d656dc1c", - "sha256:4e547122ca2d244f7c090fe3f4b5a5861255ff66b7ab6d98f44a0222aaf8671a", - "sha256:5829192582c0ec8ca4a2532407bc14c2f338d9878a10442f5d03804a95fac9de", - "sha256:5d6b09c972ce9200264c35a1d53d43ca55ef61836d9ec60f0d44273a31aa9f17", - "sha256:600617008aa82032ddeace2535626d1bc212dfff32b43989539deda63b3f36e4", - "sha256:619346d57c7126ae49ac95b11b0dc8e36c1dd49d148477461bb66c8cf13bb521", - "sha256:63c424e6f5b4ab1cf1e23a43b12f542b0ec2e54f99ec9f11b75382152981df57", - "sha256:6dbc1536e105adda7a6312c778f15aaabe583b0e9a0b0a324990334fd458c94b", - "sha256:6e1394d24d5938e561fbeaa0cd3d356207579c28bd1792f25a068743f2d5b282", - "sha256:86f2e78b1eff847609b1ca8050c9e1fa3bd44ce755b2ec30e70f2d3ba3844644", - "sha256:8bdfe9ff3a4ea37d17f172ac0dff1e1c383aec17a636b9b35906babc9f0f5475", - "sha256:8e2c35a4c1f269704e90888e56f794e2d9c0262fb0c1b1c8c4ee44d9b9e77b5d", - "sha256:92b8c845527eae547a2a6617d336adc56394050c3ed8a6918683646328fbb6da", - "sha256:9365ed5cce5d0cf2c10afc6add145c5037d3148585b8ae0e77cc1efdd6aa2953", - "sha256:9a29311bd6429be317c1f3fe4bc06c4c5ee45e2fa61b2a19d4d1d6111cb94af2", - "sha256:9a2b5b52be0a8626fcbffd7e689781bf8c2ac01613e77feda93d96184949a98e", - "sha256:a4bdeb0a52d1d04123b41d90a4390b096f3ef38eee35e11f0b22c2d031222c6c", - "sha256:a9c8c4283e17690ff1a7427123ffb428ad6a52ed720d550e299e8291e33184dc", - "sha256:b637c57fdb8be84e91fac60d9325a66a5981f8086c954ea2772efe28425eaf64", - "sha256:bf154ba7ee2fd613eb541c2bc03d3d9ac667080a737449d1a3fb342740eb1a74", - "sha256:c254b03032d5a06de049ce8bca8338a5185f07fb76600afff3c161e053d88617", - "sha256:c332d8f8d448ded473b97fefe4a0983265af21917d8b0cdcb8bb06b2afe632c3", - "sha256:c7912d1526299cb04c88288e148c6c87c0df600eca76efd99d84396cfe00ef1d", - "sha256:cfd9386c1d6f13b37e05a91a8583e802f8059bebfccde61a418c5808dea6bbfa", - "sha256:d5d2033d5db1d58ae2d62f095e1aefb6988af65b4b12cb8987af409587cc0739", - "sha256:dca38a21e4423f3edb821292e97cec7ad38086f84313462098568baedf4331f8", - "sha256:e2cad8093172b7d1595b4ad66f24270808658e11acf43a8f95b41276162eb5b8", - "sha256:e3db840a4dee542e37e09f30859f1612da90e1c5239a6a2498c473183a50e781", - "sha256:edcada2e24ed68f019175c2b2af2a8b481d3d084798b8c20d15d34f5c733fa58", - "sha256:f467bbb837691ab5a8ca359199d3429a11a01e6dfb3d9dcc676dc035ca93c0a9", - "sha256:f506af4f27def639ba45789fa6fde45f9a217da0be05f8910458e4557eed020c", - "sha256:f614fc9956d76d8a88a88bb41ddc12709caa755666f580af3a688899721efecd", - "sha256:f9afb5b746781fc2abce26193d1c817b7eb0e11459510fba65d2bd77fe161d9e", - "sha256:fb8b8ee99b3fffe4fd86f4c81b35a6bf7e4462cba019997af2fe679365db0c49" - ], - "markers": "python_version >= '3.6'", - "version": "==6.2" + "sha256:012157499ec4f135fc36cd2177e3d1a1840af9b236cbe80e9a5ccfc83d912a69", + "sha256:0a34d313105cdd0d3644c56df2d743fe467270d6ab93b5d4a347eb9fec8924d6", + "sha256:11e61c5548ecf74ea1f8b059730b049871f0e32b74f88bd0d670c20c819ad749", + "sha256:152cc2624381df4e4e604e21bd8e95eb8059535f7b768c1fb8b8ae0b26f47ab0", + "sha256:1b4285fde5286b946835a1a53bba3ad41ef74285ba9e8013e14b5ea93deaeafc", + "sha256:27a94db5dc098c25048b0aca155f5fac674f2cf1b1736c5272ba28ead2fc267e", + "sha256:27ac7cb84538e278e07569ceaaa6f807a029dc194b1c819a9820b9bb5dbf63ab", + "sha256:2a491e159294d756e7fc8462f98175e2d2225e4dbe062cca7d3e0d5a75ba6260", + "sha256:2bc85664b06ba42d14bb74d6ddf19d8bfc520cb660561d2d9ce5786ae72f71b5", + "sha256:32168001f33025fd756884d56d01adebb34e6c8c0b3395ca8584cdcee9c7c9d2", + "sha256:3c4ce3b647bd1792d4394f5690d9df6dc035b00bcdbc5595099c01282a59ae01", + "sha256:433b99f7b0613bdcdc0b00cc3d39ed6d756797e3b078d2c43f8a38288520aec6", + "sha256:4578728c36de2801c1deb1c6b760d31883e62e33f33c7ba8f982e609dc95167d", + "sha256:509c68c3e2015022aeda03b003dd68fa19987cdcf64e9d4edc98db41cfc45d30", + "sha256:51372e24b1f7143ee2df6b45cff6a721f3abe93b1e506196f3ffa4155c2497f7", + "sha256:5d008e0f67ac800b0ca04d7914b8501312c8c6c00ad8c7ba17754609fae1231a", + "sha256:649df3641eb351cdfd0d5533c92fc9df507b6b2bf48a7ef8c71ab63cbc7b5c3c", + "sha256:6e78b1e25e5c5695dea012be473e442f7094d066925604be20b30713dbd47f89", + "sha256:72d9d186508325a456475dd05b1756f9a204c7086b07fffb227ef8cee03b1dc2", + "sha256:7d82c610a2e10372e128023c5baf9ce3d270f3029fe7274ff5bc2897c68f1318", + "sha256:7ee317486593193e066fc5e98ac0ce712178c21529a85c07b7cb978171f25d53", + "sha256:7eed8459a2b81848cafb3280b39d7d49950d5f98e403677941c752e7e7ee47cb", + "sha256:823f9325283dc9565ba0aa2d240471a93ca8999861779b2b6c7aded45b58ee0f", + "sha256:85c5fc9029043cf8b07f73fbb0a7ab6d3b717510c3b5642b77058ea55d7cacde", + "sha256:86c91c511853dfda81c2cf2360502cb72783f4b7cebabef27869f00cbe1db07d", + "sha256:8e0c3525b1a182c8ffc9bca7e56b521e0c2b8b3e82f033c8e16d6d721f1b54d6", + "sha256:987a84ff98a309994ca77ed3cc4b92424f824278e48e4bf7d1bb79a63cfe2099", + "sha256:9ed3244b415725f08ca3bdf02ed681089fd95e9465099a21c8e2d9c5d6ca2606", + "sha256:a189036c50dcd56100746139a459f0d27540fef95b09aba03e786540b8feaa5f", + "sha256:a4748349734110fd32d46ff8897b561e6300d8989a494ad5a0a2e4f0ca974fc7", + "sha256:a5d79c9af3f410a2b5acad91258b4ae179ee9c83897eb9de69151b179b0227f5", + "sha256:a7596aa2f2b8fa5604129cfc9a27ad9beec0a96f18078cb424d029fdd707468d", + "sha256:ab4fc4b866b279740e0d917402f0e9a08683e002f43fa408e9655818ed392196", + "sha256:bde4aeabc0d1b2e52c4036c54440b1ad05beeca8113f47aceb4998bb7471e2c2", + "sha256:c72bb4679283c6737f452eeb9b2a0e570acaef2197ad255fb20162adc80bea76", + "sha256:c8582e9280f8d0f38114fe95a92ae8d0790b56b099d728cc4f8a2e14b1c4a18c", + "sha256:ca29c352389ea27a24c79acd117abdd8a865c6eb01576b6f0990cd9a4e9c9f48", + "sha256:ce443a3e6df90d692c38762f108fc4c88314bf477689f04de76b3f252e7a351c", + "sha256:d1675db48490e5fa0b300f6329ecb8a9a37c29b9ab64fa9c964d34111788ca2d", + "sha256:da1a428bdbe71f9a8c270c7baab29e9552ac9d0e0cba5e7e9a4c9ee6465d258d", + "sha256:e4ff163602c5c77e7bb4ea81ba5d3b793b4419f8acd296aae149370902cf4e92", + "sha256:e67ccd53da5958ea1ec833a160b96357f90859c220a00150de011b787c27b98d", + "sha256:e8071e7d9ba9f457fc674afc3de054450be2c9b195c470147fbbc082468d8ff7", + "sha256:fff16a30fdf57b214778eff86391301c4509e327a65b877862f7c929f10a4253" + ], + "version": "==6.3" }, "isort": { "hashes": [ "sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7", "sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951" ], - "markers": "python_version < '4.0' and python_full_version >= '3.6.1'", "version": "==5.10.1" }, "lazy-object-proxy": { "hashes": [ - "sha256:17e0967ba374fc24141738c69736da90e94419338fd4c7c7bef01ee26b339653", - "sha256:1fee665d2638491f4d6e55bd483e15ef21f6c8c2095f235fef72601021e64f61", - "sha256:22ddd618cefe54305df49e4c069fa65715be4ad0e78e8d252a33debf00f6ede2", - "sha256:24a5045889cc2729033b3e604d496c2b6f588c754f7a62027ad4437a7ecc4837", - "sha256:410283732af311b51b837894fa2f24f2c0039aa7f220135192b38fcc42bd43d3", - "sha256:4732c765372bd78a2d6b2150a6e99d00a78ec963375f236979c0626b97ed8e43", - "sha256:489000d368377571c6f982fba6497f2aa13c6d1facc40660963da62f5c379726", - "sha256:4f60460e9f1eb632584c9685bccea152f4ac2130e299784dbaf9fae9f49891b3", - "sha256:5743a5ab42ae40caa8421b320ebf3a998f89c85cdc8376d6b2e00bd12bd1b587", - "sha256:85fb7608121fd5621cc4377a8961d0b32ccf84a7285b4f1d21988b2eae2868e8", - "sha256:9698110e36e2df951c7c36b6729e96429c9c32b3331989ef19976592c5f3c77a", - "sha256:9d397bf41caad3f489e10774667310d73cb9c4258e9aed94b9ec734b34b495fd", - "sha256:b579f8acbf2bdd9ea200b1d5dea36abd93cabf56cf626ab9c744a432e15c815f", - "sha256:b865b01a2e7f96db0c5d12cfea590f98d8c5ba64ad222300d93ce6ff9138bcad", - "sha256:bf34e368e8dd976423396555078def5cfc3039ebc6fc06d1ae2c5a65eebbcde4", - "sha256:c6938967f8528b3668622a9ed3b31d145fab161a32f5891ea7b84f6b790be05b", - "sha256:d1c2676e3d840852a2de7c7d5d76407c772927addff8d742b9808fe0afccebdf", - "sha256:d7124f52f3bd259f510651450e18e0fd081ed82f3c08541dffc7b94b883aa981", - "sha256:d900d949b707778696fdf01036f58c9876a0d8bfe116e8d220cfd4b15f14e741", - "sha256:ebfd274dcd5133e0afae738e6d9da4323c3eb021b3e13052d8cbd0e457b1256e", - "sha256:ed361bb83436f117f9917d282a456f9e5009ea12fd6de8742d1a4752c3017e93", - "sha256:f5144c75445ae3ca2057faac03fda5a902eff196702b0a24daf1d6ce0650514b" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", - "version": "==1.6.0" + "sha256:043651b6cb706eee4f91854da4a089816a6606c1428fd391573ef8cb642ae4f7", + "sha256:07fa44286cda977bd4803b656ffc1c9b7e3bc7dff7d34263446aec8f8c96f88a", + "sha256:12f3bb77efe1367b2515f8cb4790a11cffae889148ad33adad07b9b55e0ab22c", + "sha256:2052837718516a94940867e16b1bb10edb069ab475c3ad84fd1e1a6dd2c0fcfc", + "sha256:2130db8ed69a48a3440103d4a520b89d8a9405f1b06e2cc81640509e8bf6548f", + "sha256:39b0e26725c5023757fc1ab2a89ef9d7ab23b84f9251e28f9cc114d5b59c1b09", + "sha256:46ff647e76f106bb444b4533bb4153c7370cdf52efc62ccfc1a28bdb3cc95442", + "sha256:4dca6244e4121c74cc20542c2ca39e5c4a5027c81d112bfb893cf0790f96f57e", + "sha256:553b0f0d8dbf21890dd66edd771f9b1b5f51bd912fa5f26de4449bfc5af5e029", + "sha256:677ea950bef409b47e51e733283544ac3d660b709cfce7b187f5ace137960d61", + "sha256:6a24357267aa976abab660b1d47a34aaf07259a0c3859a34e536f1ee6e76b5bb", + "sha256:6a6e94c7b02641d1311228a102607ecd576f70734dc3d5e22610111aeacba8a0", + "sha256:6aff3fe5de0831867092e017cf67e2750c6a1c7d88d84d2481bd84a2e019ec35", + "sha256:6ecbb350991d6434e1388bee761ece3260e5228952b1f0c46ffc800eb313ff42", + "sha256:7096a5e0c1115ec82641afbdd70451a144558ea5cf564a896294e346eb611be1", + "sha256:70ed0c2b380eb6248abdef3cd425fc52f0abd92d2b07ce26359fcbc399f636ad", + "sha256:8561da8b3dd22d696244d6d0d5330618c993a215070f473b699e00cf1f3f6443", + "sha256:85b232e791f2229a4f55840ed54706110c80c0a210d076eee093f2b2e33e1bfd", + "sha256:898322f8d078f2654d275124a8dd19b079080ae977033b713f677afcfc88e2b9", + "sha256:8f3953eb575b45480db6568306893f0bd9d8dfeeebd46812aa09ca9579595148", + "sha256:91ba172fc5b03978764d1df5144b4ba4ab13290d7bab7a50f12d8117f8630c38", + "sha256:9d166602b525bf54ac994cf833c385bfcc341b364e3ee71e3bf5a1336e677b55", + "sha256:a57d51ed2997e97f3b8e3500c984db50a554bb5db56c50b5dab1b41339b37e36", + "sha256:b9e89b87c707dd769c4ea91f7a31538888aad05c116a59820f28d59b3ebfe25a", + "sha256:bb8c5fd1684d60a9902c60ebe276da1f2281a318ca16c1d0a96db28f62e9166b", + "sha256:c19814163728941bb871240d45c4c30d33b8a2e85972c44d4e63dd7107faba44", + "sha256:c4ce15276a1a14549d7e81c243b887293904ad2d94ad767f42df91e75fd7b5b6", + "sha256:c7a683c37a8a24f6428c28c561c80d5f4fd316ddcf0c7cab999b15ab3f5c5c69", + "sha256:d609c75b986def706743cdebe5e47553f4a5a1da9c5ff66d76013ef396b5a8a4", + "sha256:d66906d5785da8e0be7360912e99c9188b70f52c422f9fc18223347235691a84", + "sha256:dd7ed7429dbb6c494aa9bc4e09d94b778a3579be699f9d67da7e6804c422d3de", + "sha256:df2631f9d67259dc9620d831384ed7732a198eb434eadf69aea95ad18c587a28", + "sha256:e368b7f7eac182a59ff1f81d5f3802161932a41dc1b1cc45c1f757dc876b5d2c", + "sha256:e40f2013d96d30217a51eeb1db28c9ac41e9d0ee915ef9d00da639c5b63f01a1", + "sha256:f769457a639403073968d118bc70110e7dce294688009f5c24ab78800ae56dc8", + "sha256:fccdf7c2c5821a8cbd0a9440a456f5050492f2270bd54e94360cac663398739b", + "sha256:fd45683c3caddf83abbb1249b653a266e7069a09f486daa8863fb0e7496a9fdb" + ], + "version": "==1.7.1" }, "mccabe": { "hashes": [ @@ -551,19 +526,18 @@ }, "platformdirs": { "hashes": [ - "sha256:367a5e80b3d04d2428ffa76d33f124cf11e8fff2acdaa9b43d545f5c7d661ef2", - "sha256:8868bbe3c3c80d42f20156f22e7131d2fb321f5bc86a2a345375c6481a67021d" + "sha256:1d7385c7db91728b83efd0ca99a5afb296cab9d0ed8313a45ed8ba17967ecfca", + "sha256:440633ddfebcc36264232365d7840a970e75e1018d15b4327d11f91909045fda" ], - "markers": "python_version >= '3.6'", - "version": "==2.4.0" + "version": "==2.4.1" }, "pylint": { "hashes": [ - "sha256:4f4a52b132c05b49094b28e109febcec6bfb7bc6961c7485a5ad0a0f961df289", - "sha256:b4b5a7b6d04e914a11c198c816042af1fb2d3cda29bb0c98a9c637010da2a5c5" + "sha256:9d945a73640e1fec07ee34b42f5669b770c759acd536ec7b16d7e4b87a9c9ff9", + "sha256:daabda3f7ed9d1c60f52d563b1b854632fd90035bcf01443e234d3dc794e3b74" ], "index": "pypi", - "version": "==2.12.1" + "version": "==2.12.2" }, "pylint-flask": { "hashes": [ @@ -574,25 +548,16 @@ }, "pylint-plugin-utils": { "hashes": [ - "sha256:2f30510e1c46edf268d3a195b2849bd98a1b9433229bb2ba63b8d776e1fc4d0a", - "sha256:57625dcca20140f43731311cd8fd879318bf45a8b0fd17020717a8781714a25a" - ], - "version": "==0.6" - }, - "setuptools": { - "hashes": [ - "sha256:b4c634615a0cf5b02cf83c7bedffc8da0ca439f00e79452699454da6fbd4153d", - "sha256:feb5ff19b354cde9efd2344ef6d5e79880ce4be643037641b49508bbb850d060" + "sha256:b3d43e85ab74c4f48bb46ae4ce771e39c3a20f8b3d56982ab17aa73b4f98d535", + "sha256:ce48bc0516ae9415dd5c752c940dfe601b18fe0f48aa249f2386adfa95a004dd" ], - "markers": "python_version >= '3.6'", - "version": "==59.4.0" + "version": "==0.7" }, "six": { "hashes": [ "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.16.0" }, "toml": { @@ -600,40 +565,14 @@ "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f" ], - "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==0.10.2" }, - "typed-ast": { - "hashes": [ - "sha256:14fed8820114a389a2b7e91624db5f85f3f6682fda09fe0268a59aabd28fe5f5", - "sha256:155b74b078be842d2eb630dd30a280025eca0a5383c7d45853c27afee65f278f", - "sha256:224afecb8b39739f5c9562794a7c98325cb9d972712e1a98b6989a4720219541", - "sha256:361b9e5d27bd8e3ccb6ea6ad6c4f3c0be322a1a0f8177db6d56264fa0ae40410", - "sha256:37ba2ab65a0028b1a4f2b61a8fe77f12d242731977d274a03d68ebb751271508", - "sha256:49af5b8f6f03ed1eb89ee06c1d7c2e7c8e743d720c3746a5857609a1abc94c94", - "sha256:51040bf45aacefa44fa67fb9ebcd1f2bec73182b99a532c2394eea7dabd18e24", - "sha256:52ca2b2b524d770bed7a393371a38e91943f9160a190141e0df911586066ecda", - "sha256:618912cbc7e17b4aeba86ffe071698c6e2d292acbd6d1d5ec1ee724b8c4ae450", - "sha256:65c81abbabda7d760df7304d843cc9dbe7ef5d485504ca59a46ae2d1731d2428", - "sha256:7b310a207ee9fde3f46ba327989e6cba4195bc0c8c70a158456e7b10233e6bed", - "sha256:7e6731044f748340ef68dcadb5172a4b1f40847a2983fe3983b2a66445fbc8e6", - "sha256:806e0c7346b9b4af8c62d9a29053f484599921a4448c37fbbcbbf15c25138570", - "sha256:a67fd5914603e2165e075f1b12f5a8356bfb9557e8bfb74511108cfbab0f51ed", - "sha256:e4374a76e61399a173137e7984a1d7e356038cf844f24fd8aea46c8029a2f712", - "sha256:e8a9b9c87801cecaad3b4c2b8876387115d1a14caa602c1618cedbb0cb2a14e6", - "sha256:ea517c2bb11c5e4ba7a83a91482a2837041181d57d3ed0749a6c382a2b6b7086", - "sha256:ec184dfb5d3d11e82841dbb973e7092b75f306b625fad7b2e665b64c5d60ab3f", - "sha256:ff4ad88271aa7a55f19b6a161ed44e088c393846d954729549e3cde8257747bb" - ], - "markers": "python_version < '3.8' and implementation_name == 'cpython'", - "version": "==1.5.0" - }, "typing-extensions": { "hashes": [ "sha256:4ca091dea149f945ec56afb48dae714f21e8692ef22a395223bcd328961b6a0e", "sha256:7f001e5ac290a0c0401508864c7ec868be4e701886d5b573a9528ed3973d9d3b" ], - "markers": "python_version < '3.8'", + "markers": "python_version < '3.10'", "version": "==4.0.1" }, "wrapt": { @@ -690,16 +629,15 @@ "sha256:f9c51d9af9abb899bd34ace878fbec8bf357b3194a10c4e8e0a25512826ef056", "sha256:fd76c47f20984b43d93de9a82011bb6e5f8325df6c9ed4d8310029a55fa361ea" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", "version": "==1.13.3" }, "yapf": { "hashes": [ - "sha256:408fb9a2b254c302f49db83c59f9aa0b4b0fd0ec25be3a5c51181327922ff63d", - "sha256:e3a234ba8455fe201eaa649cdac872d590089a18b661e39bbac7020978dd9c2e" + "sha256:8fea849025584e486fd06d6ba2bed717f396080fd3cc236ba10cb97c4c51cf32", + "sha256:a3f5085d37ef7e3e004c4ba9f9b3e40c54ff1901cd111f05145ae313a7c67d1b" ], "index": "pypi", - "version": "==0.31.0" + "version": "==0.32.0" } } } From f6b84f097a2e794cd71c2593ddea5f4dc4b822f9 Mon Sep 17 00:00:00 2001 From: Brice Schaffner Date: Wed, 26 Jan 2022 07:05:45 +0100 Subject: [PATCH 06/13] Allow any valid @x and improved route parsing When using a valid scale not part of 1x, 0.5x or 2x, the service silently used scale 1x. Also when entering an invalid color value type, e.g. `@1x-0,0,a.pgn`, the service returned 404 instead of 400. Now we can use any valid scale and not only 1x, 0.5x and 2x. Also the parsing of the route don't uses converter anymore which improved the parsing and allow to use correct error message in case of invalid scale or invalid color. --- app/helpers/check_functions.py | 20 ++++++++++++--- app/routes.py | 22 +++++++--------- app/settings.py | 2 +- tests/unit_tests/base_test.py | 10 ++++---- tests/unit_tests/test_all_icons.py | 13 ++++------ tests/unit_tests/test_icons.py | 41 +++++++++++++++++++++++++++++- 6 files changed, 77 insertions(+), 31 deletions(-) diff --git a/app/helpers/check_functions.py b/app/helpers/check_functions.py index 897524d..6d5995a 100644 --- a/app/helpers/check_functions.py +++ b/app/helpers/check_functions.py @@ -9,7 +9,10 @@ def __check_color(color): - return 0 <= color <= 255 + try: + return 0 <= int(color) <= 255 + except ValueError: + return False def check_color_channels(red, green, blue): @@ -29,13 +32,24 @@ def check_color_channels(red, green, blue): if not (__check_color(red) and __check_color(green) and __check_color(blue)): logger.error( "Color channel values must be integers in the range of 0 to 255. " - "(given: %d, %d, %d)", + "(given: %s, %s, %s)", red, green, blue ) abort(400, "Color channel values must be integers in the range of 0 to 255.") - return red, green, blue + return int(red), int(green), int(blue) + + +def check_scale(scale): + try: + _scale = float(scale.rstrip('x')) + except ValueError: + _scale = 0 + if _scale <= 0: + logger.error('Invalid Scale %s: must be a number > 0', scale) + abort(400, "Invalid scale must be a positive number") + return _scale def get_and_check_icon_set(icon_set_name): diff --git a/app/routes.py b/app/routes.py index c34bd43..d42194e 100644 --- a/app/routes.py +++ b/app/routes.py @@ -9,6 +9,7 @@ from app import app from app.helpers.check_functions import check_color_channels +from app.helpers.check_functions import check_scale from app.helpers.check_functions import get_and_check_icon from app.helpers.check_functions import get_and_check_icon_set from app.icon import Icon @@ -64,18 +65,17 @@ def icon_metadata(icon_set_name, icon_name): return make_api_compliant_response(icon) -@app.route('/sets//icons/.png', methods=['GET']) @app.route( - '/sets//icons/-,,.png', - methods=['GET'] + '/sets//icons/.png', ) @app.route( - '/sets//icons/@.png', methods=['GET'] + '/sets//icons/-,,.png', ) @app.route( - '/sets//icons/@' - '-,,.png', - methods=['GET'] + '/sets//icons/@.png', +) +@app.route( + '/sets//icons/@-,,.png', ) def colorized_icon( icon_set_name, @@ -88,17 +88,13 @@ def colorized_icon( red, green, blue = check_color_channels(red, green, blue) icon_set = get_and_check_icon_set(icon_set_name) icon = get_and_check_icon(icon_set, icon_name) - scale_factor = 1 - if scale == '2x': - scale_factor = 2 - elif scale in ('0.5x', '.5x'): - scale_factor = 0.5 + scale = check_scale(scale) with open(icon.get_icon_filepath(), 'rb') as fd: image = Image.open(fd) if image.mode == 'P': image = image.convert('RGBA') - new_size = int(48 * scale_factor) + new_size = int(48 * scale) if new_size != icon_set.get_default_pixel_size(): image = image.resize((new_size, new_size)) if icon_set.colorable: diff --git a/app/settings.py b/app/settings.py index 8f49448..de6f149 100644 --- a/app/settings.py +++ b/app/settings.py @@ -12,7 +12,7 @@ IMAGE_FOLDER = os.path.abspath(os.path.join(os.path.dirname(__file__), '../static/images/')) COLORABLE_ICON_SETS = ['default'] -DEFAULT_COLOR = {"r": 255, "g": 0, "b": 0} +DEFAULT_COLOR = {"r": '255', "g": '0', "b": '0'} TRAP_HTTP_EXCEPTIONS = True LOGS_DIR = os.getenv('LOGS_DIR', str(BASE_DIR / 'logs')) os.environ['LOGS_DIR'] = LOGS_DIR # Set default if not set diff --git a/tests/unit_tests/base_test.py b/tests/unit_tests/base_test.py index 2bcb2f5..7224a47 100644 --- a/tests/unit_tests/base_test.py +++ b/tests/unit_tests/base_test.py @@ -47,11 +47,11 @@ def assertCors(self, response, check_origin=True): # pylint: disable=invalid-na def request_colorized_icon( self, - icon_name="marker", + icon_name="001-marker", scale='1x', - red=255, - green=0, - blue=0, + red='255', + green='0', + blue='0', icon_category="default", # see .env.test origin=ORIGIN_FOR_TESTING @@ -63,7 +63,7 @@ def request_colorized_icon( icon_name=icon_name, scale=scale, red=red, - green=red, + green=green, blue=blue ), headers={"Origin": origin} diff --git a/tests/unit_tests/test_all_icons.py b/tests/unit_tests/test_all_icons.py index 8372e58..200b98f 100644 --- a/tests/unit_tests/test_all_icons.py +++ b/tests/unit_tests/test_all_icons.py @@ -202,7 +202,7 @@ def test_all_icon_metadata_endpoint(self): red="255", green="0", blue="0", - scale="1x", + scale='1x', _external=True ) ) @@ -213,7 +213,7 @@ def test_all_icon_metadata_endpoint(self): 'colorized_icon', icon_set_name=icon_set_name, icon_name=icon_name, - scale="1x", + scale='1x', _external=True ) ) @@ -238,10 +238,7 @@ def test_all_icon_double_size(self): for icon_name in icon_set: with self.subTest(icon_set_name=icon_set_name, icon_name=icon_name): double_size_icon_url = url_for( - 'colorized_icon', - icon_set_name=icon_set_name, - icon_name=icon_name, - scale="2x" + 'colorized_icon', icon_set_name=icon_set_name, icon_name=icon_name, scale=2 ) self.check_image(icon_name, double_size_icon_url, expected_size=96) @@ -256,7 +253,7 @@ def test_all_icon_half_size(self): 'colorized_icon', icon_set_name=icon_set_name, icon_name=icon_name, - scale=".5x" + scale='0.5x' ) self.check_image(icon_name, half_size_icon_url, expected_size=24) @@ -291,7 +288,7 @@ def test_all_icons_colorized_and_double_size(self): for icon_name in icon_set: with self.subTest(icon_set_name=icon_set_name, icon_name=icon_name): icon_set = get_icon_set(icon_set_name) - params = {"icon_set_name": icon_set_name, "icon_name": icon_name, "scale": "2x"} + params = {"icon_set_name": icon_set_name, "icon_name": icon_name, "scale": '2x'} if icon_set.colorable: params["red"] = 0 params["green"] = 0 diff --git a/tests/unit_tests/test_icons.py b/tests/unit_tests/test_icons.py index 9f536f2..93fd960 100644 --- a/tests/unit_tests/test_icons.py +++ b/tests/unit_tests/test_icons.py @@ -7,6 +7,25 @@ class IconsTests(ServiceIconsUnitTests): + def test_colorized_icon_with_invalid_scale_value(self): + response = self.request_colorized_icon(scale='0x') + self.assertEqual( + response.status_code, 400, "Should return a HTTP 400 when a invalid scale value" + ) + self.assertCors(response) + self.assertIn('Cache-Control', response.headers) + self.assertIn('max-age=3600', response.headers['Cache-Control']) + self.assertEqual(response.content_type, "application/json") + self.assertEqual( + response.json, + { + "error": { + "code": 400, "message": "Invalid scale must be a positive number" + }, + "success": False + } + ) + def test_colorized_icon_with_wrong_rgb_value(self): response = self.request_colorized_icon(red=2155) self.assertEqual( @@ -27,6 +46,26 @@ def test_colorized_icon_with_wrong_rgb_value(self): } ) + def test_colorized_icon_with_invalid_rgb_value(self): + response = self.request_colorized_icon(red='invalid') + self.assertEqual( + response.status_code, 400, "Should return a HTTP 400 when a RGB value is out of range" + ) + self.assertCors(response) + self.assertIn('Cache-Control', response.headers) + self.assertIn('max-age=3600', response.headers['Cache-Control']) + self.assertEqual(response.content_type, "application/json") + self.assertEqual( + response.json, + { + "error": { + "code": 400, + "message": "Color channel values must be integers in the range of 0 to 255." + }, + "success": False + } + ) + def test_colorized_icon_non_existent_icon_name(self): response = self.request_colorized_icon(icon_name="non_existent_dummy_icon") self.assertEqual( @@ -57,7 +96,7 @@ def test_colorized_icon_no_http_post_method_allowed_on_endpoint(self): url_for( 'colorized_icon', icon_set_name="default", - icon_name="marker", + icon_name="001-marker", scale='1x', red=255, green=0, From 5d122686c6e7cb0bc28b5383a072b9dc3b41e992 Mon Sep 17 00:00:00 2001 From: Brice Schaffner Date: Wed, 26 Jan 2022 07:10:03 +0100 Subject: [PATCH 07/13] Sorts the icons in the output This is then easier for human to read the output and debug it. --- app/icon_set.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/icon_set.py b/app/icon_set.py index 212643e..5205f0e 100644 --- a/app/icon_set.py +++ b/app/icon_set.py @@ -107,7 +107,7 @@ def get_all_icons(self): return None icons = [] for root, dirs, files in os.walk(os.path.join(IMAGE_FOLDER, self.name)): - for icon_filename in files: + for icon_filename in sorted(files): name_without_extension = os.path.splitext(icon_filename)[0] icons.append(self.get_icon(name_without_extension)) return icons From 51e082a55e107a1cbe8500b72e0ddab5d07ca37f Mon Sep 17 00:00:00 2001 From: Brice Schaffner Date: Wed, 26 Jan 2022 07:29:03 +0100 Subject: [PATCH 08/13] Returns 404 when icon not found instead of 400 --- app/helpers/check_functions.py | 4 ++-- tests/unit_tests/test_icons.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/helpers/check_functions.py b/app/helpers/check_functions.py index 6d5995a..14b6553 100644 --- a/app/helpers/check_functions.py +++ b/app/helpers/check_functions.py @@ -67,7 +67,7 @@ def get_and_check_icon_set(icon_set_name): icon_set = get_icon_set(icon_set_name) if not icon_set: logger.error("Icon set not found: %s", icon_set_name) - abort(400, "Icon set not found") + abort(404, "Icon set not found") return icon_set @@ -86,5 +86,5 @@ def get_and_check_icon(icon_set, icon_name): path = icon.get_icon_filepath() if not os.path.isfile(path): logger.error("The icon doesn't exist: %s", path) - abort(400, "Icon not found in icon set") + abort(404, "Icon not found in icon set") return icon diff --git a/tests/unit_tests/test_icons.py b/tests/unit_tests/test_icons.py index 93fd960..b75c1c4 100644 --- a/tests/unit_tests/test_icons.py +++ b/tests/unit_tests/test_icons.py @@ -69,14 +69,14 @@ def test_colorized_icon_with_invalid_rgb_value(self): def test_colorized_icon_non_existent_icon_name(self): response = self.request_colorized_icon(icon_name="non_existent_dummy_icon") self.assertEqual( - response.status_code, 400, msg="Should return a HTTP 400 when file not found" + response.status_code, 404, msg="Should return a HTTP 404 when file not found" ) self.assertIn('max-age=3600', response.headers['Cache-Control']) self.assertEqual(response.content_type, "application/json") self.assertEqual( response.json, { "error": { - "code": 400, "message": "Icon not found in icon set" + "code": 404, "message": "Icon not found in icon set" }, "success": False } ) From 1bc7059c0838776d7d60c0b9cc2841fd3bf4a3f3 Mon Sep 17 00:00:00 2001 From: Brice Schaffner Date: Wed, 26 Jan 2022 07:39:02 +0100 Subject: [PATCH 09/13] BGDIINF_SB-2136: Added icon anchor offset fraction The marker icons have different anchor offset as the others, therefor the service is now returning for each icon which anchor it needs to use (default anchor is the center of the icon) --- app/icon.py | 15 +++++++++++++++ tests/unit_tests/test_all_icons.py | 16 ++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/app/icon.py b/app/icon.py index d40f006..c06f08f 100644 --- a/app/icon.py +++ b/app/icon.py @@ -7,6 +7,19 @@ from app.settings import DEFAULT_COLOR from app.settings import IMAGE_FOLDER +# Here we disable yapf to avoid putting spaces between fractional parts +# (`24/48` instead of `24 / 48`) +# yapf: disable + +# Icon anchor is defined as [x, y] and as fractional. Here below we used the x and y in pixels +# to define the fraction with width=48px and height=48px +DEFAULT_ICON_ANCHOR = [24/48, 24/48] +ICON_ANCHORS = { + '001-marker': [24/48, 42/48], + '007-marker-stroked': [24/48, 42/48], +} +# yapf: enable + class Icon: """ @@ -21,6 +34,7 @@ def __init__(self, name, icon_set): """ self.name = name self.icon_set = icon_set + self.anchor = ICON_ANCHORS.get(name, DEFAULT_ICON_ANCHOR) def get_icon_url( self, red=DEFAULT_COLOR['r'], green=DEFAULT_COLOR['g'], blue=DEFAULT_COLOR['b'] @@ -80,6 +94,7 @@ def serialize(self): """ return { "name": self.name, + "anchor": self.anchor, "icon_set": self.icon_set.name, "url": self.get_icon_url(), "template_url": get_icon_template_url(get_base_url()) diff --git a/tests/unit_tests/test_all_icons.py b/tests/unit_tests/test_all_icons.py index 200b98f..dd8fad0 100644 --- a/tests/unit_tests/test_all_icons.py +++ b/tests/unit_tests/test_all_icons.py @@ -217,6 +217,22 @@ def test_all_icon_metadata_endpoint(self): _external=True ) ) + self.assertIn('anchor', json_response) + self.assertIsInstance( + json_response['anchor'], + list, + msg='"anchor" should be a list with x and y fraction' + ) + self.assertEqual( + len(json_response['anchor']), + 2, + msg='"anchor" should have two items; x and y fraction' + ) + for fraction in json_response['anchor']: + self.assertIsInstance( + fraction, (int, float), msg='"anchor" fraction should be int or float' + ) + self.assertTrue(fraction > 0, msg='"anchor" fraction should be > 0') def test_all_icon_basic_image(self): """ From 04e6899ac147b38063585e2e5a5bc0c2e8799cb3 Mon Sep 17 00:00:00 2001 From: Brice Schaffner Date: Mon, 31 Jan 2022 10:16:34 +0100 Subject: [PATCH 10/13] Fixed CI github branch resolution Depending on the context the github branch resolution was wrong, for example the github branch was resolved to another dependabot PR branch due to the `sed -n 2p` which takes the second line of the `git branch -a` command. Newly Codebuild provide a variable to know the base branch of a PR. --- buildspec.yml | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/buildspec.yml b/buildspec.yml index 6150245..1eb7c57 100644 --- a/buildspec.yml +++ b/buildspec.yml @@ -20,14 +20,8 @@ phases: pre_build: commands: - echo "Export of the image tag for build and push purposes" - # Reading git branch (the utility in the deploy script is unable to read it automatically on CodeBuild) - # see https://stackoverflow.com/questions/47657423/get-github-git-branch-for-aws-codebuild - - export GITHUB_BRANCH="$(git symbolic-ref HEAD --short 2>/dev/null)" - - | - if [ "${GITHUB_BRANCH}" = "" ] ; then - GITHUB_BRANCH="$(git branch -a --contains HEAD | sed -n 2p | awk '{ printf $1 }')"; - export GITHUB_BRANCH=${GITHUB_BRANCH#remotes/origin/}; - fi + - echo "CODEBUILD_WEBHOOK_HEAD_REF=${CODEBUILD_WEBHOOK_HEAD_REF} CODEBUILD_WEBHOOK_BASE_REF=${CODEBUILD_WEBHOOK_BASE_REF}" + - export GITHUB_BRANCH="${CODEBUILD_WEBHOOK_HEAD_REF#refs/heads/}" - export GITHUB_COMMIT=$(echo $CODEBUILD_RESOLVED_SOURCE_VERSION | cut -c 1-7) - export GITHUB_TAG="$(git describe --tags 2>/dev/null)" - echo "GITHUB_BRANCH=${GITHUB_BRANCH} GITHUB_COMMIT=${GITHUB_COMMIT} GITHUB_TAG=${GITHUB_TAG} DOCKER_IMG_TAG=${DOCKER_IMG_TAG}" From 2495702cb1dc6e575e468dd2c8a563ffb3a089dc Mon Sep 17 00:00:00 2001 From: Brice Schaffner Date: Mon, 31 Jan 2022 13:15:03 +0100 Subject: [PATCH 11/13] BGDIINF_SB-2163: Fixing CI for dependabot Github Dependabot uses branch names with `/` that are not allowed as docker tag name, therefore replace `/` by `_` for docker tag names. --- buildspec.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/buildspec.yml b/buildspec.yml index 1eb7c57..9c5a213 100644 --- a/buildspec.yml +++ b/buildspec.yml @@ -35,10 +35,10 @@ phases: commands: - echo Build started on $(date) - export DOCKER_IMG_TAG=${REGISTRY}/${IMAGE_BASE_NAME}:${GITHUB_TAG} - - export DOCKER_IMG_TAG_LATEST=${REGISTRY}/${IMAGE_BASE_NAME}:${GITHUB_BRANCH}.latest + - export DOCKER_IMG_TAG_LATEST=${REGISTRY}/${IMAGE_BASE_NAME}:${GITHUB_BRANCH//\//_}.latest - |- if [ "${GITHUB_TAG}" = "" ] ; then - export DOCKER_IMG_TAG=${REGISTRY}/${IMAGE_BASE_NAME}:${GITHUB_BRANCH}.${GITHUB_COMMIT} + export DOCKER_IMG_TAG=${REGISTRY}/${IMAGE_BASE_NAME}:${GITHUB_BRANCH//\//_}.${GITHUB_COMMIT} export GITHUB_TAG=${GITHUB_COMMIT} fi - echo "Building docker image with tags ${DOCKER_IMG_TAG} and ${DOCKER_IMG_TAG_LATEST}" From 3a1ff55679fc11fdb645b4dda179b0ef85a5159b Mon Sep 17 00:00:00 2001 From: Brice Schaffner Date: Mon, 31 Jan 2022 13:38:24 +0100 Subject: [PATCH 12/13] Use bash instead of hash in codebuild Regex expension is a bash feature that is not available in hash. --- buildspec.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/buildspec.yml b/buildspec.yml index 9c5a213..70b6b11 100644 --- a/buildspec.yml +++ b/buildspec.yml @@ -1,6 +1,7 @@ version: 0.2 env: + shell: bash variables: IMAGE_BASE_NAME: "service-icons" REGISTRY: "974517877189.dkr.ecr.eu-central-1.amazonaws.com" From 6ffafd77f7eab7f9faf948d7f257e61c9f6269a4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 31 Jan 2022 13:03:52 +0000 Subject: [PATCH 13/13] Bump pillow from 8.3.2 to 9.0.0 Bumps [pillow](https://github.com/python-pillow/Pillow) from 8.3.2 to 9.0.0. - [Release notes](https://github.com/python-pillow/Pillow/releases) - [Changelog](https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst) - [Commits](https://github.com/python-pillow/Pillow/compare/8.3.2...9.0.0) --- updated-dependencies: - dependency-name: pillow dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- Pipfile | 2 +- Pipfile.lock | 120 ++++++++++++++++++++++++++++----------------------- 2 files changed, 66 insertions(+), 56 deletions(-) diff --git a/Pipfile b/Pipfile index 1fc5170..77a0dbd 100644 --- a/Pipfile +++ b/Pipfile @@ -8,7 +8,7 @@ PyYAML = "~=5.4.1" gevent = "~=21.1.2" gunicorn = "~=20.1.0" Flask = "~=2.0.1" -Pillow = "~=8.3.2" +Pillow = "~=9.0.0" python-dotenv = "~=0.17.0" logging-utilities = "~=1.2.3" diff --git a/Pipfile.lock b/Pipfile.lock index e345c25..5e9df22 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "8a662da75809c934f7d8b89ef809ecead11c571a705c5a85d273b5e6d84b6d3a" + "sha256": "41765dc5e72057b0147e93483fcab0e59f4fe58afe6c20a4262c0f5ed90b7851" }, "pipfile-spec": 6, "requires": { @@ -21,6 +21,7 @@ "sha256:353f466495adaeb40b6b5f592f9f91cb22372351c84caeb068132442a4518ef3", "sha256:410e932b050f5eed773c4cda94de75971c89cdb3155a72a0831139a79e5ecb5b" ], + "markers": "python_version >= '3.6'", "version": "==8.0.3" }, "flask": { @@ -139,6 +140,7 @@ "sha256:5174094b9637652bdb841a3029700391451bd092ba3db90600dea710ba28e97c", "sha256:9e724d68fc22902a1435351f84c3fb8623f303fffcc566a4cb952df8c572cff0" ], + "markers": "python_version >= '3.6'", "version": "==2.0.1" }, "jinja2": { @@ -146,6 +148,7 @@ "sha256:077ce6014f7b40d03b47d1f1ca4b0fc8328a692bd284016f806ed0eaca390ad8", "sha256:611bb273cd68f3b993fabdc4064fc858c5b47a973cb5aa7999ec1ba405c87cd7" ], + "markers": "python_version >= '3.6'", "version": "==3.0.3" }, "logging-utilities": { @@ -228,66 +231,46 @@ "sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51", "sha256:fa130dd50c57d53368c9d59395cb5526eda596d3ffe36666cd81a44d56e48872" ], + "markers": "python_version >= '3.6'", "version": "==2.0.1" }, "pillow": { "hashes": [ - "sha256:0412516dcc9de9b0a1e0ae25a280015809de8270f134cc2c1e32c4eeb397cf30", - "sha256:04835e68ef12904bc3e1fd002b33eea0779320d4346082bd5b24bec12ad9c3e9", - "sha256:06d1adaa284696785375fa80a6a8eb309be722cf4ef8949518beb34487a3df71", - "sha256:085a90a99404b859a4b6c3daa42afde17cb3ad3115e44a75f0d7b4a32f06a6c9", - "sha256:0b9911ec70731711c3b6ebcde26caea620cbdd9dcb73c67b0730c8817f24711b", - "sha256:10e00f7336780ca7d3653cf3ac26f068fa11b5a96894ea29a64d3dc4b810d630", - "sha256:11c27e74bab423eb3c9232d97553111cc0be81b74b47165f07ebfdd29d825875", - "sha256:11eb7f98165d56042545c9e6db3ce394ed8b45089a67124298f0473b29cb60b2", - "sha256:13654b521fb98abdecec105ea3fb5ba863d1548c9b58831dd5105bb3873569f1", - "sha256:15ccb81a6ffc57ea0137f9f3ac2737ffa1d11f786244d719639df17476d399a7", - "sha256:18a07a683805d32826c09acfce44a90bf474e6a66ce482b1c7fcd3757d588df3", - "sha256:19ec4cfe4b961edc249b0e04b5618666c23a83bc35842dea2bfd5dfa0157f81b", - "sha256:1c3ff00110835bdda2b1e2b07f4a2548a39744bb7de5946dc8e95517c4fb2ca6", - "sha256:27a330bf7014ee034046db43ccbb05c766aa9e70b8d6c5260bfc38d73103b0ba", - "sha256:2b11c9d310a3522b0fd3c35667914271f570576a0e387701f370eb39d45f08a4", - "sha256:2c661542c6f71dfd9dc82d9d29a8386287e82813b0375b3a02983feac69ef864", - "sha256:2cde7a4d3687f21cffdf5bb171172070bb95e02af448c4c8b2f223d783214056", - "sha256:2d5e9dc0bf1b5d9048a94c48d0813b6c96fccfa4ccf276d9c36308840f40c228", - "sha256:2f23b2d3079522fdf3c09de6517f625f7a964f916c956527bed805ac043799b8", - "sha256:35d27687f027ad25a8d0ef45dd5208ef044c588003cdcedf05afb00dbc5c2deb", - "sha256:35d409030bf3bd05fa66fb5fdedc39c521b397f61ad04309c90444e893d05f7d", - "sha256:4326ea1e2722f3dc00ed77c36d3b5354b8fb7399fb59230249ea6d59cbed90da", - "sha256:4abc247b31a98f29e5224f2d31ef15f86a71f79c7f4d2ac345a5d551d6393073", - "sha256:4d89a2e9219a526401015153c0e9dd48319ea6ab9fe3b066a20aa9aee23d9fd3", - "sha256:4e59e99fd680e2b8b11bbd463f3c9450ab799305d5f2bafb74fefba6ac058616", - "sha256:548794f99ff52a73a156771a0402f5e1c35285bd981046a502d7e4793e8facaa", - "sha256:56fd98c8294f57636084f4b076b75f86c57b2a63a8410c0cd172bc93695ee979", - "sha256:59697568a0455764a094585b2551fd76bfd6b959c9f92d4bdec9d0e14616303a", - "sha256:6bff50ba9891be0a004ef48828e012babaaf7da204d81ab9be37480b9020a82b", - "sha256:6cb3dd7f23b044b0737317f892d399f9e2f0b3a02b22b2c692851fb8120d82c6", - "sha256:7dbfbc0020aa1d9bc1b0b8bcf255a7d73f4ad0336f8fd2533fcc54a4ccfb9441", - "sha256:838eb85de6d9307c19c655c726f8d13b8b646f144ca6b3771fa62b711ebf7624", - "sha256:8b68f565a4175e12e68ca900af8910e8fe48aaa48fd3ca853494f384e11c8bcd", - "sha256:8f284dc1695caf71a74f24993b7c7473d77bc760be45f776a2c2f4e04c170550", - "sha256:963ebdc5365d748185fdb06daf2ac758116deecb2277ec5ae98139f93844bc09", - "sha256:a048dad5ed6ad1fad338c02c609b862dfaa921fcd065d747194a6805f91f2196", - "sha256:a1bd983c565f92779be456ece2479840ec39d386007cd4ae83382646293d681b", - "sha256:a66566f8a22561fc1a88dc87606c69b84fa9ce724f99522cf922c801ec68f5c1", - "sha256:bcb04ff12e79b28be6c9988f275e7ab69f01cc2ba319fb3114f87817bb7c74b6", - "sha256:bd24054aaf21e70a51e2a2a5ed1183560d3a69e6f9594a4bfe360a46f94eba83", - "sha256:be25cb93442c6d2f8702c599b51184bd3ccd83adebd08886b682173e09ef0c3f", - "sha256:c691b26283c3a31594683217d746f1dad59a7ae1d4cfc24626d7a064a11197d4", - "sha256:cc9d0dec711c914ed500f1d0d3822868760954dce98dfb0b7382a854aee55d19", - "sha256:ce2e5e04bb86da6187f96d7bab3f93a7877830981b37f0287dd6479e27a10341", - "sha256:ce651ca46d0202c302a535d3047c55a0131a720cf554a578fc1b8a2aff0e7d96", - "sha256:d0c8ebbfd439c37624db98f3877d9ed12c137cadd99dde2d2eae0dab0bbfc355", - "sha256:d675a876b295afa114ca8bf42d7f86b5fb1298e1b6bb9a24405a3f6c8338811c", - "sha256:dde3f3ed8d00c72631bc19cbfff8ad3b6215062a5eed402381ad365f82f0c18c", - "sha256:e5a31c07cea5edbaeb4bdba6f2b87db7d3dc0f446f379d907e51cc70ea375629", - "sha256:f514c2717012859ccb349c97862568fdc0479aad85b0270d6b5a6509dbc142e2", - "sha256:fc0db32f7223b094964e71729c0361f93db43664dd1ec86d3df217853cedda87", - "sha256:fd4fd83aa912d7b89b4b4a1580d30e2a4242f3936882a3f433586e5ab97ed0d5", - "sha256:feb5db446e96bfecfec078b943cc07744cc759893cef045aa8b8b6d6aaa8274e" + "sha256:03b27b197deb4ee400ed57d8d4e572d2d8d80f825b6634daf6e2c18c3c6ccfa6", + "sha256:0b281fcadbb688607ea6ece7649c5d59d4bbd574e90db6cd030e9e85bde9fecc", + "sha256:0ebd8b9137630a7bbbff8c4b31e774ff05bbb90f7911d93ea2c9371e41039b52", + "sha256:113723312215b25c22df1fdf0e2da7a3b9c357a7d24a93ebbe80bfda4f37a8d4", + "sha256:2d16b6196fb7a54aff6b5e3ecd00f7c0bab1b56eee39214b2b223a9d938c50af", + "sha256:2fd8053e1f8ff1844419842fd474fc359676b2e2a2b66b11cc59f4fa0a301315", + "sha256:31b265496e603985fad54d52d11970383e317d11e18e856971bdbb86af7242a4", + "sha256:3586e12d874ce2f1bc875a3ffba98732ebb12e18fb6d97be482bd62b56803281", + "sha256:47f5cf60bcb9fbc46011f75c9b45a8b5ad077ca352a78185bd3e7f1d294b98bb", + "sha256:490e52e99224858f154975db61c060686df8a6b3f0212a678e5d2e2ce24675c9", + "sha256:500d397ddf4bbf2ca42e198399ac13e7841956c72645513e8ddf243b31ad2128", + "sha256:52abae4c96b5da630a8b4247de5428f593465291e5b239f3f843a911a3cf0105", + "sha256:6579f9ba84a3d4f1807c4aab4be06f373017fc65fff43498885ac50a9b47a553", + "sha256:68e06f8b2248f6dc8b899c3e7ecf02c9f413aab622f4d6190df53a78b93d97a5", + "sha256:6c5439bfb35a89cac50e81c751317faea647b9a3ec11c039900cd6915831064d", + "sha256:72c3110228944019e5f27232296c5923398496b28be42535e3b2dc7297b6e8b6", + "sha256:72f649d93d4cc4d8cf79c91ebc25137c358718ad75f99e99e043325ea7d56100", + "sha256:7aaf07085c756f6cb1c692ee0d5a86c531703b6e8c9cae581b31b562c16b98ce", + "sha256:80fe92813d208ce8aa7d76da878bdc84b90809f79ccbad2a288e9bcbeac1d9bd", + "sha256:95545137fc56ce8c10de646074d242001a112a92de169986abd8c88c27566a05", + "sha256:97b6d21771da41497b81652d44191489296555b761684f82b7b544c49989110f", + "sha256:98cb63ca63cb61f594511c06218ab4394bf80388b3d66cd61d0b1f63ee0ea69f", + "sha256:9f3b4522148586d35e78313db4db0df4b759ddd7649ef70002b6c3767d0fdeb7", + "sha256:a09a9d4ec2b7887f7a088bbaacfd5c07160e746e3d47ec5e8050ae3b2a229e9f", + "sha256:b5050d681bcf5c9f2570b93bee5d3ec8ae4cf23158812f91ed57f7126df91762", + "sha256:bb47a548cea95b86494a26c89d153fd31122ed65255db5dcbc421a2d28eb3379", + "sha256:bc462d24500ba707e9cbdef436c16e5c8cbf29908278af053008d9f689f56dee", + "sha256:c2067b3bb0781f14059b112c9da5a91c80a600a97915b4f48b37f197895dd925", + "sha256:d154ed971a4cc04b93a6d5b47f37948d1f621f25de3e8fa0c26b2d44f24e3e8f", + "sha256:d5dcea1387331c905405b09cdbfb34611050cc52c865d71f2362f354faee1e9f", + "sha256:ee6e2963e92762923956fe5d3479b1fdc3b76c83f290aad131a2f98c3df0593e", + "sha256:fd0e5062f11cb3e730450a7d9f323f4051b532781026395c4323b8ad055523c4" ], "index": "pypi", - "version": "==8.3.2" + "version": "==9.0.0" }, "python-dotenv": { "hashes": [ @@ -332,11 +315,20 @@ "index": "pypi", "version": "==5.4.1" }, + "setuptools": { + "hashes": [ + "sha256:c99207037c38984eae838c2fd986f39a9ddf4fabfe0fddd957e622d1d1dcdd05", + "sha256:eb83b1012ae6bf436901c2a2cee35d45b7260f31fd4b65fd1e50a9f99c11d7f8" + ], + "markers": "python_version >= '3.7'", + "version": "==60.6.0" + }, "werkzeug": { "hashes": [ "sha256:63d3dc1cf60e7b7e35e97fa9861f7397283b75d765afcaefd993d6046899de8f", "sha256:aa2bb6fc8dee8d6c504c0ac1e7f5f7dc5810a9903e793b6f715a9f015bdadb9a" ], + "markers": "python_version >= '3.6'", "version": "==2.0.2" }, "zope.event": { @@ -400,6 +392,7 @@ "sha256:f44e517131a98f7a76696a7b21b164bcb85291cee106a23beccce454e1f433a4", "sha256:f7ee479e96f7ee350db1cf24afa5685a5899e2b34992fb99e1f7c1b0b758d263" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", "version": "==5.4.0" } }, @@ -409,6 +402,7 @@ "sha256:1efdf4e867d4d8ba4a9f6cf9ce07cd182c4c41de77f23814feb27ca93ca9d877", "sha256:506daabe5edffb7e696ad82483ad0228245a9742ed7d2d8c9cdb31537decf9f6" ], + "markers": "python_full_version >= '3.6.2'", "version": "==2.9.3" }, "coverage": { @@ -458,6 +452,7 @@ "sha256:e8071e7d9ba9f457fc674afc3de054450be2c9b195c470147fbbc082468d8ff7", "sha256:fff16a30fdf57b214778eff86391301c4509e327a65b877862f7c929f10a4253" ], + "markers": "python_version >= '3.7'", "version": "==6.3" }, "isort": { @@ -465,6 +460,7 @@ "sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7", "sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951" ], + "markers": "python_version < '4.0' and python_full_version >= '3.6.1'", "version": "==5.10.1" }, "lazy-object-proxy": { @@ -507,6 +503,7 @@ "sha256:fccdf7c2c5821a8cbd0a9440a456f5050492f2270bd54e94360cac663398739b", "sha256:fd45683c3caddf83abbb1249b653a266e7069a09f486daa8863fb0e7496a9fdb" ], + "markers": "python_version >= '3.6'", "version": "==1.7.1" }, "mccabe": { @@ -529,6 +526,7 @@ "sha256:1d7385c7db91728b83efd0ca99a5afb296cab9d0ed8313a45ed8ba17967ecfca", "sha256:440633ddfebcc36264232365d7840a970e75e1018d15b4327d11f91909045fda" ], + "markers": "python_version >= '3.7'", "version": "==2.4.1" }, "pylint": { @@ -551,13 +549,23 @@ "sha256:b3d43e85ab74c4f48bb46ae4ce771e39c3a20f8b3d56982ab17aa73b4f98d535", "sha256:ce48bc0516ae9415dd5c752c940dfe601b18fe0f48aa249f2386adfa95a004dd" ], + "markers": "python_full_version >= '3.6.2'", "version": "==0.7" }, + "setuptools": { + "hashes": [ + "sha256:c99207037c38984eae838c2fd986f39a9ddf4fabfe0fddd957e622d1d1dcdd05", + "sha256:eb83b1012ae6bf436901c2a2cee35d45b7260f31fd4b65fd1e50a9f99c11d7f8" + ], + "markers": "python_version >= '3.7'", + "version": "==60.6.0" + }, "six": { "hashes": [ "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.16.0" }, "toml": { @@ -565,6 +573,7 @@ "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f" ], + "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==0.10.2" }, "typing-extensions": { @@ -629,6 +638,7 @@ "sha256:f9c51d9af9abb899bd34ace878fbec8bf357b3194a10c4e8e0a25512826ef056", "sha256:fd76c47f20984b43d93de9a82011bb6e5f8325df6c9ed4d8310029a55fa361ea" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", "version": "==1.13.3" }, "yapf": {