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/.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/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/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/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" } } } 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. | diff --git a/app/__init__.py b/app/__init__.py index ade83a3..7a71b60 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,23 @@ 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): diff --git a/app/helpers/check_functions.py b/app/helpers/check_functions.py index 897524d..14b6553 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): @@ -53,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 @@ -72,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/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/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 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/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/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 f692fc6..dd8fad0 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,10 +213,26 @@ 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 ) ) + 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): """ @@ -238,10 +254,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 +269,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 +304,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 @@ -316,7 +329,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..b75c1c4 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( @@ -19,11 +38,30 @@ 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 + } + ) + + 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 } ) @@ -31,14 +69,14 @@ def test_colorized_icon_with_wrong_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 } ) @@ -58,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, @@ -79,10 +117,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..2bb6396 100644 --- a/wsgi.py +++ b/wsgi.py @@ -33,12 +33,11 @@ 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': - { - 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()