diff --git a/.readthedocs.yml b/.readthedocs.yaml similarity index 95% rename from .readthedocs.yml rename to .readthedocs.yaml index 55059ff6..690fca6f 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yaml @@ -1,4 +1,4 @@ -# .readthedocs.yml +# .readthedocs.yaml # Read the Docs configuration file # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 511a0559..b7634638 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -9,6 +9,7 @@ Code * [alechewitt](https://github.com/alechewitt) * [camponez](https://github.com/camponez) * [Darumin](https://github.com/Darumin) + * [davidpirogov](https://github.com/davidpirogov) * [dev-iks](https://github.com/dev-iks) * [dphildebrandt](https://github.com/dphildebrandt) * [dstmar](https://github.com/dstmar) @@ -45,6 +46,7 @@ Testing Packaging and Distribution -------------------------- + * [Crozzers](https://github.com/Crozzers) * [Diapente](https://github.com/Diapente) * [onkelbeh](https://github.com/onkelbeh) * [Simone-Zabberoni](https://github.com/Simone-Zabberoni) diff --git a/Pipfile b/Pipfile index 78cf9190..265d26ce 100644 --- a/Pipfile +++ b/Pipfile @@ -4,6 +4,7 @@ verify_ssl = true name = "pypi" [dev-packages] +Babel = ">=2.9.1" coverage = "*" coveralls = "*" Jinja2 = "*" diff --git a/Pipfile.lock b/Pipfile.lock index 1f90bb6b..03f5d728 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "e44b6cdd7a5d41b1fd2bd054d76da4d807fb1c2a93ecea2aae4f84cb73928393" + "sha256": "1ca0af00f60358450cf35ac2a7bd3a1dd908579afcff210683aa3c96ca7d594e" }, "pipfile-spec": 6, "requires": {}, @@ -16,18 +16,92 @@ "default": { "certifi": { "hashes": [ - "sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c", - "sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830" + "sha256:0f0d56dc5a6ad56fd4ba36484d6cc34451e1c6548c61daad8c320169f91eddc7", + "sha256:c6c2e98f5c7869efca1f8916fed228dd91539f9f1b444c314c06eef02980c716" ], - "version": "==2020.12.5" - }, - "chardet": { - "hashes": [ - "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa", - "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==4.0.0" + "markers": "python_version >= '3.6'", + "version": "==2023.5.7" + }, + "charset-normalizer": { + "hashes": [ + "sha256:04afa6387e2b282cf78ff3dbce20f0cc071c12dc8f685bd40960cc68644cfea6", + "sha256:04eefcee095f58eaabe6dc3cc2262f3bcd776d2c67005880894f447b3f2cb9c1", + "sha256:0be65ccf618c1e7ac9b849c315cc2e8a8751d9cfdaa43027d4f6624bd587ab7e", + "sha256:0c95f12b74681e9ae127728f7e5409cbbef9cd914d5896ef238cc779b8152373", + "sha256:0ca564606d2caafb0abe6d1b5311c2649e8071eb241b2d64e75a0d0065107e62", + "sha256:10c93628d7497c81686e8e5e557aafa78f230cd9e77dd0c40032ef90c18f2230", + "sha256:11d117e6c63e8f495412d37e7dc2e2fff09c34b2d09dbe2bee3c6229577818be", + "sha256:11d3bcb7be35e7b1bba2c23beedac81ee893ac9871d0ba79effc7fc01167db6c", + "sha256:12a2b561af122e3d94cdb97fe6fb2bb2b82cef0cdca131646fdb940a1eda04f0", + "sha256:12d1a39aa6b8c6f6248bb54550efcc1c38ce0d8096a146638fd4738e42284448", + "sha256:1435ae15108b1cb6fffbcea2af3d468683b7afed0169ad718451f8db5d1aff6f", + "sha256:1c60b9c202d00052183c9be85e5eaf18a4ada0a47d188a83c8f5c5b23252f649", + "sha256:1e8fcdd8f672a1c4fc8d0bd3a2b576b152d2a349782d1eb0f6b8e52e9954731d", + "sha256:20064ead0717cf9a73a6d1e779b23d149b53daf971169289ed2ed43a71e8d3b0", + "sha256:21fa558996782fc226b529fdd2ed7866c2c6ec91cee82735c98a197fae39f706", + "sha256:22908891a380d50738e1f978667536f6c6b526a2064156203d418f4856d6e86a", + "sha256:3160a0fd9754aab7d47f95a6b63ab355388d890163eb03b2d2b87ab0a30cfa59", + "sha256:322102cdf1ab682ecc7d9b1c5eed4ec59657a65e1c146a0da342b78f4112db23", + "sha256:34e0a2f9c370eb95597aae63bf85eb5e96826d81e3dcf88b8886012906f509b5", + "sha256:3573d376454d956553c356df45bb824262c397c6e26ce43e8203c4c540ee0acb", + "sha256:3747443b6a904001473370d7810aa19c3a180ccd52a7157aacc264a5ac79265e", + "sha256:38e812a197bf8e71a59fe55b757a84c1f946d0ac114acafaafaf21667a7e169e", + "sha256:3a06f32c9634a8705f4ca9946d667609f52cf130d5548881401f1eb2c39b1e2c", + "sha256:3a5fc78f9e3f501a1614a98f7c54d3969f3ad9bba8ba3d9b438c3bc5d047dd28", + "sha256:3d9098b479e78c85080c98e1e35ff40b4a31d8953102bb0fd7d1b6f8a2111a3d", + "sha256:3dc5b6a8ecfdc5748a7e429782598e4f17ef378e3e272eeb1340ea57c9109f41", + "sha256:4155b51ae05ed47199dc5b2a4e62abccb274cee6b01da5b895099b61b1982974", + "sha256:49919f8400b5e49e961f320c735388ee686a62327e773fa5b3ce6721f7e785ce", + "sha256:53d0a3fa5f8af98a1e261de6a3943ca631c526635eb5817a87a59d9a57ebf48f", + "sha256:5f008525e02908b20e04707a4f704cd286d94718f48bb33edddc7d7b584dddc1", + "sha256:628c985afb2c7d27a4800bfb609e03985aaecb42f955049957814e0491d4006d", + "sha256:65ed923f84a6844de5fd29726b888e58c62820e0769b76565480e1fdc3d062f8", + "sha256:6734e606355834f13445b6adc38b53c0fd45f1a56a9ba06c2058f86893ae8017", + "sha256:6baf0baf0d5d265fa7944feb9f7451cc316bfe30e8df1a61b1bb08577c554f31", + "sha256:6f4f4668e1831850ebcc2fd0b1cd11721947b6dc7c00bf1c6bd3c929ae14f2c7", + "sha256:6f5c2e7bc8a4bf7c426599765b1bd33217ec84023033672c1e9a8b35eaeaaaf8", + "sha256:6f6c7a8a57e9405cad7485f4c9d3172ae486cfef1344b5ddd8e5239582d7355e", + "sha256:7381c66e0561c5757ffe616af869b916c8b4e42b367ab29fedc98481d1e74e14", + "sha256:73dc03a6a7e30b7edc5b01b601e53e7fc924b04e1835e8e407c12c037e81adbd", + "sha256:74db0052d985cf37fa111828d0dd230776ac99c740e1a758ad99094be4f1803d", + "sha256:75f2568b4189dda1c567339b48cba4ac7384accb9c2a7ed655cd86b04055c795", + "sha256:78cacd03e79d009d95635e7d6ff12c21eb89b894c354bd2b2ed0b4763373693b", + "sha256:80d1543d58bd3d6c271b66abf454d437a438dff01c3e62fdbcd68f2a11310d4b", + "sha256:830d2948a5ec37c386d3170c483063798d7879037492540f10a475e3fd6f244b", + "sha256:891cf9b48776b5c61c700b55a598621fdb7b1e301a550365571e9624f270c203", + "sha256:8f25e17ab3039b05f762b0a55ae0b3632b2e073d9c8fc88e89aca31a6198e88f", + "sha256:9a3267620866c9d17b959a84dd0bd2d45719b817245e49371ead79ed4f710d19", + "sha256:a04f86f41a8916fe45ac5024ec477f41f886b3c435da2d4e3d2709b22ab02af1", + "sha256:aaf53a6cebad0eae578f062c7d462155eada9c172bd8c4d250b8c1d8eb7f916a", + "sha256:abc1185d79f47c0a7aaf7e2412a0eb2c03b724581139193d2d82b3ad8cbb00ac", + "sha256:ac0aa6cd53ab9a31d397f8303f92c42f534693528fafbdb997c82bae6e477ad9", + "sha256:ac3775e3311661d4adace3697a52ac0bab17edd166087d493b52d4f4f553f9f0", + "sha256:b06f0d3bf045158d2fb8837c5785fe9ff9b8c93358be64461a1089f5da983137", + "sha256:b116502087ce8a6b7a5f1814568ccbd0e9f6cfd99948aa59b0e241dc57cf739f", + "sha256:b82fab78e0b1329e183a65260581de4375f619167478dddab510c6c6fb04d9b6", + "sha256:bd7163182133c0c7701b25e604cf1611c0d87712e56e88e7ee5d72deab3e76b5", + "sha256:c36bcbc0d5174a80d6cccf43a0ecaca44e81d25be4b7f90f0ed7bcfbb5a00909", + "sha256:c3af8e0f07399d3176b179f2e2634c3ce9c1301379a6b8c9c9aeecd481da494f", + "sha256:c84132a54c750fda57729d1e2599bb598f5fa0344085dbde5003ba429a4798c0", + "sha256:cb7b2ab0188829593b9de646545175547a70d9a6e2b63bf2cd87a0a391599324", + "sha256:cca4def576f47a09a943666b8f829606bcb17e2bc2d5911a46c8f8da45f56755", + "sha256:cf6511efa4801b9b38dc5546d7547d5b5c6ef4b081c60b23e4d941d0eba9cbeb", + "sha256:d16fd5252f883eb074ca55cb622bc0bee49b979ae4e8639fff6ca3ff44f9f854", + "sha256:d2686f91611f9e17f4548dbf050e75b079bbc2a82be565832bc8ea9047b61c8c", + "sha256:d7fc3fca01da18fbabe4625d64bb612b533533ed10045a2ac3dd194bfa656b60", + "sha256:dd5653e67b149503c68c4018bf07e42eeed6b4e956b24c00ccdf93ac79cdff84", + "sha256:de5695a6f1d8340b12a5d6d4484290ee74d61e467c39ff03b39e30df62cf83a0", + "sha256:e0ac8959c929593fee38da1c2b64ee9778733cdf03c482c9ff1d508b6b593b2b", + "sha256:e1b25e3ad6c909f398df8921780d6a3d120d8c09466720226fc621605b6f92b1", + "sha256:e633940f28c1e913615fd624fcdd72fdba807bf53ea6925d6a588e84e1151531", + "sha256:e89df2958e5159b811af9ff0f92614dabf4ff617c03a4c1c6ff53bf1c399e0e1", + "sha256:ea9f9c6034ea2d93d9147818f17c2a0860d41b71c38b9ce4d55f21b6f9165a11", + "sha256:f645caaf0008bacf349875a974220f1f1da349c5dbe7c4ec93048cdc785a3326", + "sha256:f8303414c7b03f794347ad062c0516cee0e15f7a612abd0ce1e25caf6ceb47df", + "sha256:fca62a8301b605b954ad2e9c3666f9d97f63872aa4efcae5492baca2056b74ab" + ], + "markers": "python_version >= '3.7'", + "version": "==3.1.0" }, "geojson": { "hashes": [ @@ -39,10 +113,11 @@ }, "idna": { "hashes": [ - "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6", - "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0" + "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4", + "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2" ], - "version": "==2.10" + "markers": "python_version >= '3.5'", + "version": "==3.4" }, "pysocks": { "hashes": [ @@ -54,130 +129,223 @@ "version": "==1.7.1" }, "requests": { + "extras": [ + "socks" + ], "hashes": [ - "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804", - "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e" + "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f", + "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1" ], "index": "pypi", - "version": "==2.25.1" + "version": "==2.31.0" }, "urllib3": { "hashes": [ - "sha256:1b465e494e3e0d8939b50680403e3aedaa2bc434b7d5af64dfd3c958d7f5ae80", - "sha256:de3eedaad74a2683334e282005cd8d7f22f4d55fa690a2a1020a416cb0a47e73" + "sha256:61717a1095d7e155cdb737ac7bb2f4324a858a1e2e6466f6d03ff630ca68d3cc", + "sha256:d055c2f9d38dc53c808f6fdc8eab7360b6fdbbde02340ed25cfbcd817c62469e" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'", - "version": "==1.26.3" + "markers": "python_version >= '3.7'", + "version": "==2.0.2" } }, "develop": { "alabaster": { "hashes": [ - "sha256:446438bdcca0e05bd45ea2de1668c1d9b032e1a9154c2c259092d77031ddd359", - "sha256:a661d72d58e6ea8a57f7a86e37d86716863ee5e92788398526d58b26a4e4dc02" + "sha256:1ee19aca801bbabb5ba3f5f258e4422dfa86f82f3e9cefb0859b283cdd7f62a3", + "sha256:a27a4a084d5e690e16e01e03ad2b2e552c61a65469419b907243193de1a84ae2" ], - "version": "==0.7.12" - }, - "appdirs": { - "hashes": [ - "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41", - "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128" - ], - "version": "==1.4.4" + "markers": "python_version >= '3.6'", + "version": "==0.7.13" }, "attrs": { "hashes": [ - "sha256:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6", - "sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700" + "sha256:1f28b4522cdc2fb4256ac1a020c78acf9cba2c6b461ccd2c126f3aa8e8335d04", + "sha256:6279836d581513a26f1bf235f9acd333bc9115683f14f7e8fae46c98fc50e015" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==20.3.0" + "markers": "python_version >= '3.7'", + "version": "==23.1.0" }, "babel": { "hashes": [ - "sha256:9d35c22fcc79893c3ecc85ac4a56cde1ecf3f19c540bba0922308a6c06ca6fa5", - "sha256:da031ab54472314f210b0adcff1588ee5d1d1d0ba4dbd07b94dba82bde791e05" + "sha256:ab49e12b91d937cd11f0b67cb259a57ab4ad2b59ac7a3b41d6c06c0ac5b0def9", + "sha256:bc0c176f9f6a994582230df350aa6e05ba2ebe4b3ac317eab29d9be5d2768da0" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==2.9.0" + "index": "pypi", + "version": "==2.9.1" }, "bleach": { "hashes": [ - "sha256:6123ddc1052673e52bab52cdc955bcb57a015264a1c57d37bea2f6b817af0125", - "sha256:98b3170739e5e83dd9dc19633f074727ad848cbedb6026708c8ac2d3b697a433" - ], - "sha256:52b5919b81842b1854196eaae5ca29679a2f2e378905c346d3ca8227c2c66080", - "sha256:6123ddc1052673e52bab52cdc955bcb57a015264a1c57d37bea2f6b817af0125", - "sha256:98b3170739e5e83dd9dc19633f074727ad848cbedb6026708c8ac2d3b697a433", - "sha256:9f8ccbeb6183c6e6cddea37592dfb0167485c1e3b13b3363bc325aa8bda3adbd" + "sha256:1a1a85c1595e07d8db14c5f09f09e6433502c51c595970edc090551f0db99414", + "sha256:33c16e3353dbd13028ab4799a0f89a83f113405c766e9c122df8a06f5b85b3f4" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==3.3.0" + "markers": "python_version >= '3.7'", + "version": "==6.0.0" }, "certifi": { "hashes": [ - "sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c", - "sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830" + "sha256:0f0d56dc5a6ad56fd4ba36484d6cc34451e1c6548c61daad8c320169f91eddc7", + "sha256:c6c2e98f5c7869efca1f8916fed228dd91539f9f1b444c314c06eef02980c716" ], - "version": "==2020.12.5" + "markers": "python_version >= '3.6'", + "version": "==2023.5.7" }, "cffi": { "hashes": [ - "sha256:005a36f41773e148deac64b08f233873a4d0c18b053d37da83f6af4d9087b813", - "sha256:0857f0ae312d855239a55c81ef453ee8fd24136eaba8e87a2eceba644c0d4c06", - "sha256:1071534bbbf8cbb31b498d5d9db0f274f2f7a865adca4ae429e147ba40f73dea", - "sha256:158d0d15119b4b7ff6b926536763dc0714313aa59e320ddf787502c70c4d4bee", - "sha256:1f436816fc868b098b0d63b8920de7d208c90a67212546d02f84fe78a9c26396", - "sha256:2894f2df484ff56d717bead0a5c2abb6b9d2bf26d6960c4604d5c48bbc30ee73", - "sha256:29314480e958fd8aab22e4a58b355b629c59bf5f2ac2492b61e3dc06d8c7a315", - "sha256:34eff4b97f3d982fb93e2831e6750127d1355a923ebaeeb565407b3d2f8d41a1", - "sha256:35f27e6eb43380fa080dccf676dece30bef72e4a67617ffda586641cd4508d49", - "sha256:3d3dd4c9e559eb172ecf00a2a7517e97d1e96de2a5e610bd9b68cea3925b4892", - "sha256:43e0b9d9e2c9e5d152946b9c5fe062c151614b262fda2e7b201204de0b99e482", - "sha256:48e1c69bbacfc3d932221851b39d49e81567a4d4aac3b21258d9c24578280058", - "sha256:51182f8927c5af975fece87b1b369f722c570fe169f9880764b1ee3bca8347b5", - "sha256:58e3f59d583d413809d60779492342801d6e82fefb89c86a38e040c16883be53", - "sha256:5de7970188bb46b7bf9858eb6890aad302577a5f6f75091fd7cdd3ef13ef3045", - "sha256:65fa59693c62cf06e45ddbb822165394a288edce9e276647f0046e1ec26920f3", - "sha256:69e395c24fc60aad6bb4fa7e583698ea6cc684648e1ffb7fe85e3c1ca131a7d5", - "sha256:6c97d7350133666fbb5cf4abdc1178c812cb205dc6f41d174a7b0f18fb93337e", - "sha256:6e4714cc64f474e4d6e37cfff31a814b509a35cb17de4fb1999907575684479c", - "sha256:72d8d3ef52c208ee1c7b2e341f7d71c6fd3157138abf1a95166e6165dd5d4369", - "sha256:8ae6299f6c68de06f136f1f9e69458eae58f1dacf10af5c17353eae03aa0d827", - "sha256:8b198cec6c72df5289c05b05b8b0969819783f9418e0409865dac47288d2a053", - "sha256:99cd03ae7988a93dd00bcd9d0b75e1f6c426063d6f03d2f90b89e29b25b82dfa", - "sha256:9cf8022fb8d07a97c178b02327b284521c7708d7c71a9c9c355c178ac4bbd3d4", - "sha256:9de2e279153a443c656f2defd67769e6d1e4163952b3c622dcea5b08a6405322", - "sha256:9e93e79c2551ff263400e1e4be085a1210e12073a31c2011dbbda14bda0c6132", - "sha256:9ff227395193126d82e60319a673a037d5de84633f11279e336f9c0f189ecc62", - "sha256:a465da611f6fa124963b91bf432d960a555563efe4ed1cc403ba5077b15370aa", - "sha256:ad17025d226ee5beec591b52800c11680fca3df50b8b29fe51d882576e039ee0", - "sha256:afb29c1ba2e5a3736f1c301d9d0abe3ec8b86957d04ddfa9d7a6a42b9367e396", - "sha256:b85eb46a81787c50650f2392b9b4ef23e1f126313b9e0e9013b35c15e4288e2e", - "sha256:bb89f306e5da99f4d922728ddcd6f7fcebb3241fc40edebcb7284d7514741991", - "sha256:cbde590d4faaa07c72bf979734738f328d239913ba3e043b1e98fe9a39f8b2b6", - "sha256:cd2868886d547469123fadc46eac7ea5253ea7fcb139f12e1dfc2bbd406427d1", - "sha256:d42b11d692e11b6634f7613ad8df5d6d5f8875f5d48939520d351007b3c13406", - "sha256:f2d45f97ab6bb54753eab54fffe75aaf3de4ff2341c9daee1987ee1837636f1d", - "sha256:fd78e5fee591709f32ef6edb9a015b4aa1a5022598e36227500c8f4e02328d9c" - ], - "version": "==1.14.5" - }, - "chardet": { - "hashes": [ - "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa", - "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==4.0.0" + "sha256:00a9ed42e88df81ffae7a8ab6d9356b371399b91dbdf0c3cb1e84c03a13aceb5", + "sha256:03425bdae262c76aad70202debd780501fabeaca237cdfddc008987c0e0f59ef", + "sha256:04ed324bda3cda42b9b695d51bb7d54b680b9719cfab04227cdd1e04e5de3104", + "sha256:0e2642fe3142e4cc4af0799748233ad6da94c62a8bec3a6648bf8ee68b1c7426", + "sha256:173379135477dc8cac4bc58f45db08ab45d228b3363adb7af79436135d028405", + "sha256:198caafb44239b60e252492445da556afafc7d1e3ab7a1fb3f0584ef6d742375", + "sha256:1e74c6b51a9ed6589199c787bf5f9875612ca4a8a0785fb2d4a84429badaf22a", + "sha256:2012c72d854c2d03e45d06ae57f40d78e5770d252f195b93f581acf3ba44496e", + "sha256:21157295583fe8943475029ed5abdcf71eb3911894724e360acff1d61c1d54bc", + "sha256:2470043b93ff09bf8fb1d46d1cb756ce6132c54826661a32d4e4d132e1977adf", + "sha256:285d29981935eb726a4399badae8f0ffdff4f5050eaa6d0cfc3f64b857b77185", + "sha256:30d78fbc8ebf9c92c9b7823ee18eb92f2e6ef79b45ac84db507f52fbe3ec4497", + "sha256:320dab6e7cb2eacdf0e658569d2575c4dad258c0fcc794f46215e1e39f90f2c3", + "sha256:33ab79603146aace82c2427da5ca6e58f2b3f2fb5da893ceac0c42218a40be35", + "sha256:3548db281cd7d2561c9ad9984681c95f7b0e38881201e157833a2342c30d5e8c", + "sha256:3799aecf2e17cf585d977b780ce79ff0dc9b78d799fc694221ce814c2c19db83", + "sha256:39d39875251ca8f612b6f33e6b1195af86d1b3e60086068be9cc053aa4376e21", + "sha256:3b926aa83d1edb5aa5b427b4053dc420ec295a08e40911296b9eb1b6170f6cca", + "sha256:3bcde07039e586f91b45c88f8583ea7cf7a0770df3a1649627bf598332cb6984", + "sha256:3d08afd128ddaa624a48cf2b859afef385b720bb4b43df214f85616922e6a5ac", + "sha256:3eb6971dcff08619f8d91607cfc726518b6fa2a9eba42856be181c6d0d9515fd", + "sha256:40f4774f5a9d4f5e344f31a32b5096977b5d48560c5592e2f3d2c4374bd543ee", + "sha256:4289fc34b2f5316fbb762d75362931e351941fa95fa18789191b33fc4cf9504a", + "sha256:470c103ae716238bbe698d67ad020e1db9d9dba34fa5a899b5e21577e6d52ed2", + "sha256:4f2c9f67e9821cad2e5f480bc8d83b8742896f1242dba247911072d4fa94c192", + "sha256:50a74364d85fd319352182ef59c5c790484a336f6db772c1a9231f1c3ed0cbd7", + "sha256:54a2db7b78338edd780e7ef7f9f6c442500fb0d41a5a4ea24fff1c929d5af585", + "sha256:5635bd9cb9731e6d4a1132a498dd34f764034a8ce60cef4f5319c0541159392f", + "sha256:59c0b02d0a6c384d453fece7566d1c7e6b7bae4fc5874ef2ef46d56776d61c9e", + "sha256:5d598b938678ebf3c67377cdd45e09d431369c3b1a5b331058c338e201f12b27", + "sha256:5df2768244d19ab7f60546d0c7c63ce1581f7af8b5de3eb3004b9b6fc8a9f84b", + "sha256:5ef34d190326c3b1f822a5b7a45f6c4535e2f47ed06fec77d3d799c450b2651e", + "sha256:6975a3fac6bc83c4a65c9f9fcab9e47019a11d3d2cf7f3c0d03431bf145a941e", + "sha256:6c9a799e985904922a4d207a94eae35c78ebae90e128f0c4e521ce339396be9d", + "sha256:70df4e3b545a17496c9b3f41f5115e69a4f2e77e94e1d2a8e1070bc0c38c8a3c", + "sha256:7473e861101c9e72452f9bf8acb984947aa1661a7704553a9f6e4baa5ba64415", + "sha256:8102eaf27e1e448db915d08afa8b41d6c7ca7a04b7d73af6514df10a3e74bd82", + "sha256:87c450779d0914f2861b8526e035c5e6da0a3199d8f1add1a665e1cbc6fc6d02", + "sha256:8b7ee99e510d7b66cdb6c593f21c043c248537a32e0bedf02e01e9553a172314", + "sha256:91fc98adde3d7881af9b59ed0294046f3806221863722ba7d8d120c575314325", + "sha256:94411f22c3985acaec6f83c6df553f2dbe17b698cc7f8ae751ff2237d96b9e3c", + "sha256:98d85c6a2bef81588d9227dde12db8a7f47f639f4a17c9ae08e773aa9c697bf3", + "sha256:9ad5db27f9cabae298d151c85cf2bad1d359a1b9c686a275df03385758e2f914", + "sha256:a0b71b1b8fbf2b96e41c4d990244165e2c9be83d54962a9a1d118fd8657d2045", + "sha256:a0f100c8912c114ff53e1202d0078b425bee3649ae34d7b070e9697f93c5d52d", + "sha256:a591fe9e525846e4d154205572a029f653ada1a78b93697f3b5a8f1f2bc055b9", + "sha256:a5c84c68147988265e60416b57fc83425a78058853509c1b0629c180094904a5", + "sha256:a66d3508133af6e8548451b25058d5812812ec3798c886bf38ed24a98216fab2", + "sha256:a8c4917bd7ad33e8eb21e9a5bbba979b49d9a97acb3a803092cbc1133e20343c", + "sha256:b3bbeb01c2b273cca1e1e0c5df57f12dce9a4dd331b4fa1635b8bec26350bde3", + "sha256:cba9d6b9a7d64d4bd46167096fc9d2f835e25d7e4c121fb2ddfc6528fb0413b2", + "sha256:cc4d65aeeaa04136a12677d3dd0b1c0c94dc43abac5860ab33cceb42b801c1e8", + "sha256:ce4bcc037df4fc5e3d184794f27bdaab018943698f4ca31630bc7f84a7b69c6d", + "sha256:cec7d9412a9102bdc577382c3929b337320c4c4c4849f2c5cdd14d7368c5562d", + "sha256:d400bfb9a37b1351253cb402671cea7e89bdecc294e8016a707f6d1d8ac934f9", + "sha256:d61f4695e6c866a23a21acab0509af1cdfd2c013cf256bbf5b6b5e2695827162", + "sha256:db0fbb9c62743ce59a9ff687eb5f4afbe77e5e8403d6697f7446e5f609976f76", + "sha256:dd86c085fae2efd48ac91dd7ccffcfc0571387fe1193d33b6394db7ef31fe2a4", + "sha256:e00b098126fd45523dd056d2efba6c5a63b71ffe9f2bbe1a4fe1716e1d0c331e", + "sha256:e229a521186c75c8ad9490854fd8bbdd9a0c9aa3a524326b55be83b54d4e0ad9", + "sha256:e263d77ee3dd201c3a142934a086a4450861778baaeeb45db4591ef65550b0a6", + "sha256:ed9cb427ba5504c1dc15ede7d516b84757c3e3d7868ccc85121d9310d27eed0b", + "sha256:fa6693661a4c91757f4412306191b6dc88c1703f780c8234035eac011922bc01", + "sha256:fcd131dd944808b5bdb38e6f5b53013c5aa4f334c5cad0c72742f6eba4b73db0" + ], + "version": "==1.15.1" + }, + "charset-normalizer": { + "hashes": [ + "sha256:04afa6387e2b282cf78ff3dbce20f0cc071c12dc8f685bd40960cc68644cfea6", + "sha256:04eefcee095f58eaabe6dc3cc2262f3bcd776d2c67005880894f447b3f2cb9c1", + "sha256:0be65ccf618c1e7ac9b849c315cc2e8a8751d9cfdaa43027d4f6624bd587ab7e", + "sha256:0c95f12b74681e9ae127728f7e5409cbbef9cd914d5896ef238cc779b8152373", + "sha256:0ca564606d2caafb0abe6d1b5311c2649e8071eb241b2d64e75a0d0065107e62", + "sha256:10c93628d7497c81686e8e5e557aafa78f230cd9e77dd0c40032ef90c18f2230", + "sha256:11d117e6c63e8f495412d37e7dc2e2fff09c34b2d09dbe2bee3c6229577818be", + "sha256:11d3bcb7be35e7b1bba2c23beedac81ee893ac9871d0ba79effc7fc01167db6c", + "sha256:12a2b561af122e3d94cdb97fe6fb2bb2b82cef0cdca131646fdb940a1eda04f0", + "sha256:12d1a39aa6b8c6f6248bb54550efcc1c38ce0d8096a146638fd4738e42284448", + "sha256:1435ae15108b1cb6fffbcea2af3d468683b7afed0169ad718451f8db5d1aff6f", + "sha256:1c60b9c202d00052183c9be85e5eaf18a4ada0a47d188a83c8f5c5b23252f649", + "sha256:1e8fcdd8f672a1c4fc8d0bd3a2b576b152d2a349782d1eb0f6b8e52e9954731d", + "sha256:20064ead0717cf9a73a6d1e779b23d149b53daf971169289ed2ed43a71e8d3b0", + "sha256:21fa558996782fc226b529fdd2ed7866c2c6ec91cee82735c98a197fae39f706", + "sha256:22908891a380d50738e1f978667536f6c6b526a2064156203d418f4856d6e86a", + "sha256:3160a0fd9754aab7d47f95a6b63ab355388d890163eb03b2d2b87ab0a30cfa59", + "sha256:322102cdf1ab682ecc7d9b1c5eed4ec59657a65e1c146a0da342b78f4112db23", + "sha256:34e0a2f9c370eb95597aae63bf85eb5e96826d81e3dcf88b8886012906f509b5", + "sha256:3573d376454d956553c356df45bb824262c397c6e26ce43e8203c4c540ee0acb", + "sha256:3747443b6a904001473370d7810aa19c3a180ccd52a7157aacc264a5ac79265e", + "sha256:38e812a197bf8e71a59fe55b757a84c1f946d0ac114acafaafaf21667a7e169e", + "sha256:3a06f32c9634a8705f4ca9946d667609f52cf130d5548881401f1eb2c39b1e2c", + "sha256:3a5fc78f9e3f501a1614a98f7c54d3969f3ad9bba8ba3d9b438c3bc5d047dd28", + "sha256:3d9098b479e78c85080c98e1e35ff40b4a31d8953102bb0fd7d1b6f8a2111a3d", + "sha256:3dc5b6a8ecfdc5748a7e429782598e4f17ef378e3e272eeb1340ea57c9109f41", + "sha256:4155b51ae05ed47199dc5b2a4e62abccb274cee6b01da5b895099b61b1982974", + "sha256:49919f8400b5e49e961f320c735388ee686a62327e773fa5b3ce6721f7e785ce", + "sha256:53d0a3fa5f8af98a1e261de6a3943ca631c526635eb5817a87a59d9a57ebf48f", + "sha256:5f008525e02908b20e04707a4f704cd286d94718f48bb33edddc7d7b584dddc1", + "sha256:628c985afb2c7d27a4800bfb609e03985aaecb42f955049957814e0491d4006d", + "sha256:65ed923f84a6844de5fd29726b888e58c62820e0769b76565480e1fdc3d062f8", + "sha256:6734e606355834f13445b6adc38b53c0fd45f1a56a9ba06c2058f86893ae8017", + "sha256:6baf0baf0d5d265fa7944feb9f7451cc316bfe30e8df1a61b1bb08577c554f31", + "sha256:6f4f4668e1831850ebcc2fd0b1cd11721947b6dc7c00bf1c6bd3c929ae14f2c7", + "sha256:6f5c2e7bc8a4bf7c426599765b1bd33217ec84023033672c1e9a8b35eaeaaaf8", + "sha256:6f6c7a8a57e9405cad7485f4c9d3172ae486cfef1344b5ddd8e5239582d7355e", + "sha256:7381c66e0561c5757ffe616af869b916c8b4e42b367ab29fedc98481d1e74e14", + "sha256:73dc03a6a7e30b7edc5b01b601e53e7fc924b04e1835e8e407c12c037e81adbd", + "sha256:74db0052d985cf37fa111828d0dd230776ac99c740e1a758ad99094be4f1803d", + "sha256:75f2568b4189dda1c567339b48cba4ac7384accb9c2a7ed655cd86b04055c795", + "sha256:78cacd03e79d009d95635e7d6ff12c21eb89b894c354bd2b2ed0b4763373693b", + "sha256:80d1543d58bd3d6c271b66abf454d437a438dff01c3e62fdbcd68f2a11310d4b", + "sha256:830d2948a5ec37c386d3170c483063798d7879037492540f10a475e3fd6f244b", + "sha256:891cf9b48776b5c61c700b55a598621fdb7b1e301a550365571e9624f270c203", + "sha256:8f25e17ab3039b05f762b0a55ae0b3632b2e073d9c8fc88e89aca31a6198e88f", + "sha256:9a3267620866c9d17b959a84dd0bd2d45719b817245e49371ead79ed4f710d19", + "sha256:a04f86f41a8916fe45ac5024ec477f41f886b3c435da2d4e3d2709b22ab02af1", + "sha256:aaf53a6cebad0eae578f062c7d462155eada9c172bd8c4d250b8c1d8eb7f916a", + "sha256:abc1185d79f47c0a7aaf7e2412a0eb2c03b724581139193d2d82b3ad8cbb00ac", + "sha256:ac0aa6cd53ab9a31d397f8303f92c42f534693528fafbdb997c82bae6e477ad9", + "sha256:ac3775e3311661d4adace3697a52ac0bab17edd166087d493b52d4f4f553f9f0", + "sha256:b06f0d3bf045158d2fb8837c5785fe9ff9b8c93358be64461a1089f5da983137", + "sha256:b116502087ce8a6b7a5f1814568ccbd0e9f6cfd99948aa59b0e241dc57cf739f", + "sha256:b82fab78e0b1329e183a65260581de4375f619167478dddab510c6c6fb04d9b6", + "sha256:bd7163182133c0c7701b25e604cf1611c0d87712e56e88e7ee5d72deab3e76b5", + "sha256:c36bcbc0d5174a80d6cccf43a0ecaca44e81d25be4b7f90f0ed7bcfbb5a00909", + "sha256:c3af8e0f07399d3176b179f2e2634c3ce9c1301379a6b8c9c9aeecd481da494f", + "sha256:c84132a54c750fda57729d1e2599bb598f5fa0344085dbde5003ba429a4798c0", + "sha256:cb7b2ab0188829593b9de646545175547a70d9a6e2b63bf2cd87a0a391599324", + "sha256:cca4def576f47a09a943666b8f829606bcb17e2bc2d5911a46c8f8da45f56755", + "sha256:cf6511efa4801b9b38dc5546d7547d5b5c6ef4b081c60b23e4d941d0eba9cbeb", + "sha256:d16fd5252f883eb074ca55cb622bc0bee49b979ae4e8639fff6ca3ff44f9f854", + "sha256:d2686f91611f9e17f4548dbf050e75b079bbc2a82be565832bc8ea9047b61c8c", + "sha256:d7fc3fca01da18fbabe4625d64bb612b533533ed10045a2ac3dd194bfa656b60", + "sha256:dd5653e67b149503c68c4018bf07e42eeed6b4e956b24c00ccdf93ac79cdff84", + "sha256:de5695a6f1d8340b12a5d6d4484290ee74d61e467c39ff03b39e30df62cf83a0", + "sha256:e0ac8959c929593fee38da1c2b64ee9778733cdf03c482c9ff1d508b6b593b2b", + "sha256:e1b25e3ad6c909f398df8921780d6a3d120d8c09466720226fc621605b6f92b1", + "sha256:e633940f28c1e913615fd624fcdd72fdba807bf53ea6925d6a588e84e1151531", + "sha256:e89df2958e5159b811af9ff0f92614dabf4ff617c03a4c1c6ff53bf1c399e0e1", + "sha256:ea9f9c6034ea2d93d9147818f17c2a0860d41b71c38b9ce4d55f21b6f9165a11", + "sha256:f645caaf0008bacf349875a974220f1f1da349c5dbe7c4ec93048cdc785a3326", + "sha256:f8303414c7b03f794347ad062c0516cee0e15f7a612abd0ce1e25caf6ceb47df", + "sha256:fca62a8301b605b954ad2e9c3666f9d97f63872aa4efcae5492baca2056b74ab" + ], + "markers": "python_version >= '3.7'", + "version": "==3.1.0" }, "colorama": { "hashes": [ - "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b", - "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2" + "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", + "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==0.4.4" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5, 3.6'", + "version": "==0.4.6" }, "commonmark": { "hashes": [ @@ -188,91 +356,90 @@ }, "coverage": { "hashes": [ - "sha256:03ed2a641e412e42cc35c244508cf186015c217f0e4d496bf6d7078ebe837ae7", - "sha256:04b14e45d6a8e159c9767ae57ecb34563ad93440fc1b26516a89ceb5b33c1ad5", - "sha256:0cdde51bfcf6b6bd862ee9be324521ec619b20590787d1655d005c3fb175005f", - "sha256:0f48fc7dc82ee14aeaedb986e175a429d24129b7eada1b7e94a864e4f0644dde", - "sha256:107d327071061fd4f4a2587d14c389a27e4e5c93c7cba5f1f59987181903902f", - "sha256:1375bb8b88cb050a2d4e0da901001347a44302aeadb8ceb4b6e5aa373b8ea68f", - "sha256:14a9f1887591684fb59fdba8feef7123a0da2424b0652e1b58dd5b9a7bb1188c", - "sha256:16baa799ec09cc0dcb43a10680573269d407c159325972dd7114ee7649e56c66", - "sha256:1b811662ecf72eb2d08872731636aee6559cae21862c36f74703be727b45df90", - "sha256:1ccae21a076d3d5f471700f6d30eb486da1626c380b23c70ae32ab823e453337", - "sha256:2f2cf7a42d4b7654c9a67b9d091ec24374f7c58794858bff632a2039cb15984d", - "sha256:322549b880b2d746a7672bf6ff9ed3f895e9c9f108b714e7360292aa5c5d7cf4", - "sha256:32ab83016c24c5cf3db2943286b85b0a172dae08c58d0f53875235219b676409", - "sha256:3fe50f1cac369b02d34ad904dfe0771acc483f82a1b54c5e93632916ba847b37", - "sha256:4a780807e80479f281d47ee4af2eb2df3e4ccf4723484f77da0bb49d027e40a1", - "sha256:4a8eb7785bd23565b542b01fb39115a975fefb4a82f23d407503eee2c0106247", - "sha256:5bee3970617b3d74759b2d2df2f6a327d372f9732f9ccbf03fa591b5f7581e39", - "sha256:60a3307a84ec60578accd35d7f0c71a3a971430ed7eca6567399d2b50ef37b8c", - "sha256:6625e52b6f346a283c3d563d1fd8bae8956daafc64bb5bbd2b8f8a07608e3994", - "sha256:66a5aae8233d766a877c5ef293ec5ab9520929c2578fd2069308a98b7374ea8c", - "sha256:68fb816a5dd901c6aff352ce49e2a0ffadacdf9b6fae282a69e7a16a02dad5fb", - "sha256:6b588b5cf51dc0fd1c9e19f622457cc74b7d26fe295432e434525f1c0fae02bc", - "sha256:6c4d7165a4e8f41eca6b990c12ee7f44fef3932fac48ca32cecb3a1b2223c21f", - "sha256:6d2e262e5e8da6fa56e774fb8e2643417351427604c2b177f8e8c5f75fc928ca", - "sha256:6d9c88b787638a451f41f97446a1c9fd416e669b4d9717ae4615bd29de1ac135", - "sha256:755c56beeacac6a24c8e1074f89f34f4373abce8b662470d3aa719ae304931f3", - "sha256:7e40d3f8eb472c1509b12ac2a7e24158ec352fc8567b77ab02c0db053927e339", - "sha256:812eaf4939ef2284d29653bcfee9665f11f013724f07258928f849a2306ea9f9", - "sha256:84df004223fd0550d0ea7a37882e5c889f3c6d45535c639ce9802293b39cd5c9", - "sha256:859f0add98707b182b4867359e12bde806b82483fb12a9ae868a77880fc3b7af", - "sha256:87c4b38288f71acd2106f5d94f575bc2136ea2887fdb5dfe18003c881fa6b370", - "sha256:89fc12c6371bf963809abc46cced4a01ca4f99cba17be5e7d416ed7ef1245d19", - "sha256:9564ac7eb1652c3701ac691ca72934dd3009997c81266807aef924012df2f4b3", - "sha256:9754a5c265f991317de2bac0c70a746efc2b695cf4d49f5d2cddeac36544fb44", - "sha256:a565f48c4aae72d1d3d3f8e8fb7218f5609c964e9c6f68604608e5958b9c60c3", - "sha256:a636160680c6e526b84f85d304e2f0bb4e94f8284dd765a1911de9a40450b10a", - "sha256:a839e25f07e428a87d17d857d9935dd743130e77ff46524abb992b962eb2076c", - "sha256:b62046592b44263fa7570f1117d372ae3f310222af1fc1407416f037fb3af21b", - "sha256:b7f7421841f8db443855d2854e25914a79a1ff48ae92f70d0a5c2f8907ab98c9", - "sha256:ba7ca81b6d60a9f7a0b4b4e175dcc38e8fef4992673d9d6e6879fd6de00dd9b8", - "sha256:bb32ca14b4d04e172c541c69eec5f385f9a075b38fb22d765d8b0ce3af3a0c22", - "sha256:c0ff1c1b4d13e2240821ef23c1efb1f009207cb3f56e16986f713c2b0e7cd37f", - "sha256:c669b440ce46ae3abe9b2d44a913b5fd86bb19eb14a8701e88e3918902ecd345", - "sha256:c67734cff78383a1f23ceba3b3239c7deefc62ac2b05fa6a47bcd565771e5880", - "sha256:c6809ebcbf6c1049002b9ac09c127ae43929042ec1f1dbd8bb1615f7cd9f70a0", - "sha256:cd601187476c6bed26a0398353212684c427e10a903aeafa6da40c63309d438b", - "sha256:ebfa374067af240d079ef97b8064478f3bf71038b78b017eb6ec93ede1b6bcec", - "sha256:fbb17c0d0822684b7d6c09915677a32319f16ff1115df5ec05bdcaaee40b35f3", - "sha256:fff1f3a586246110f34dc762098b5afd2de88de507559e63553d7da643053786" + "sha256:1245ab82e8554fa88c4b2ab1e098ae051faac5af829efdcf2ce6b34dccd5567c", + "sha256:1bc6d709939ff262fd1432f03f080c5042dc6508b6e0d3d20e61dd045456a1a0", + "sha256:25e73d4c81efa8ea3785274a2f7f3bfbbeccb6fcba2a0bdd3be9223371c37554", + "sha256:276b13cc085474e482566c477c25ed66a097b44c6e77132f3304ac0b039f83eb", + "sha256:2aed4761809640f02e44e16b8b32c1a5dee5e80ea30a0ff0912158bde9c501f2", + "sha256:2dd70a167843b4b4b2630c0c56f1b586fe965b4f8ac5da05b6690344fd065c6b", + "sha256:352c68e233409c31048a3725c446a9e48bbff36e39db92774d4f2380d630d8f8", + "sha256:3f2b05757c92ad96b33dbf8e8ec8d4ccb9af6ae3c9e9bd141c7cc44d20c6bcba", + "sha256:448d7bde7ceb6c69e08474c2ddbc5b4cd13c9e4aa4a717467f716b5fc938a734", + "sha256:463e52616ea687fd323888e86bf25e864a3cc6335a043fad6bbb037dbf49bbe2", + "sha256:482fb42eea6164894ff82abbcf33d526362de5d1a7ed25af7ecbdddd28fc124f", + "sha256:56c4a409381ddd7bbff134e9756077860d4e8a583d310a6f38a2315b9ce301d0", + "sha256:56d296cbc8254a7dffdd7bcc2eb70be5a233aae7c01856d2d936f5ac4e8ac1f1", + "sha256:5e15d424b8153756b7c903bde6d4610be0c3daca3986173c18dd5c1a1625e4cd", + "sha256:618eeba986cea7f621d8607ee378ecc8c2504b98b3fdc4952b30fe3578304687", + "sha256:61d47a897c1e91f33f177c21de897267b38fbb45f2cd8e22a710bcef1df09ac1", + "sha256:621f6ea7260ea2ffdaec64fe5cb521669984f567b66f62f81445221d4754df4c", + "sha256:6a5cdc3adb4f8bb8d8f5e64c2e9e282bc12980ef055ec6da59db562ee9bdfefa", + "sha256:6c3f6158b02ac403868eea390930ae64e9a9a2a5bbfafefbb920d29258d9f2f8", + "sha256:704f89b87c4f4737da2860695a18c852b78ec7279b24eedacab10b29067d3a38", + "sha256:72128176fea72012063200b7b395ed8a57849282b207321124d7ff14e26988e8", + "sha256:78fbb2be068a13a5d99dce9e1e7d168db880870f7bc73f876152130575bd6167", + "sha256:7bff3a98f63b47464480de1b5bdd80c8fade0ba2832c9381253c9b74c4153c27", + "sha256:84f2436d6742c01136dd940ee158bfc7cf5ced3da7e4c949662b8703b5cd8145", + "sha256:9976fb0a5709988778ac9bc44f3d50fccd989987876dfd7716dee28beed0a9fa", + "sha256:9ad0a117b8dc2061ce9461ea4c1b4799e55edceb236522c5b8f958ce9ed8fa9a", + "sha256:9e3dd806f34de38d4c01416344e98eab2437ac450b3ae39c62a0ede2f8b5e4ed", + "sha256:9eb494070aa060ceba6e4bbf44c1bc5fa97bfb883a0d9b0c9049415f9e944793", + "sha256:9fde6b90889522c220dd56a670102ceef24955d994ff7af2cb786b4ba8fe11e4", + "sha256:9fff3ff052922cb99f9e52f63f985d4f7a54f6b94287463bc66b7cdf3eb41217", + "sha256:a06c358f4aed05fa1099c39decc8022261bb07dfadc127c08cfbd1391b09689e", + "sha256:a4f923b9ab265136e57cc14794a15b9dcea07a9c578609cd5dbbfff28a0d15e6", + "sha256:c5b81fb37db76ebea79aa963b76d96ff854e7662921ce742293463635a87a78d", + "sha256:d5ed164af5c9078596cfc40b078c3b337911190d3faeac830c3f1274f26b8320", + "sha256:d651fde74a4d3122e5562705824507e2f5b2d3d57557f1916c4b27635f8fbe3f", + "sha256:de73fca6fb403dd72d4da517cfc49fcf791f74eee697d3219f6be29adf5af6ce", + "sha256:e647a0be741edbb529a72644e999acb09f2ad60465f80757da183528941ff975", + "sha256:e92c7a5f7d62edff50f60a045dc9542bf939758c95b2fcd686175dd10ce0ed10", + "sha256:eeffd96882d8c06d31b65dddcf51db7c612547babc1c4c5db6a011abe9798525", + "sha256:f5a4551dfd09c3bd12fca8144d47fe7745275adf3229b7223c2f9e29a975ebda", + "sha256:fac0bcc5b7e8169bffa87f0dcc24435446d329cbc2b5486d155c2e0f3b493ae1" ], "index": "pypi", - "version": "==5.4" + "version": "==6.3.1" }, "coveralls": { "hashes": [ - "sha256:5399c0565ab822a70a477f7031f6c88a9dd196b3de2877b3facb43b51bd13434", - "sha256:f8384968c57dee4b7133ae701ecdad88e85e30597d496dcba0d7fbb470dca41f" + "sha256:b32a8bb5d2df585207c119d6c01567b81fba690c9c10a753bfe27a335bfc43ea", + "sha256:f42015f31d386b351d4226389b387ae173207058832fbf5c8ec4b40e27b16026" ], "index": "pypi", - "version": "==3.0.0" + "version": "==3.3.1" }, "cryptography": { "hashes": [ - "sha256:066bc53f052dfeda2f2d7c195cf16fb3e5ff13e1b6b7415b468514b40b381a5b", - "sha256:0923ba600d00718d63a3976f23cab19aef10c1765038945628cd9be047ad0336", - "sha256:2d32223e5b0ee02943f32b19245b61a62db83a882f0e76cc564e1cec60d48f87", - "sha256:4169a27b818de4a1860720108b55a2801f32b6ae79e7f99c00d79f2a2822eeb7", - "sha256:57ad77d32917bc55299b16d3b996ffa42a1c73c6cfa829b14043c561288d2799", - "sha256:5ecf2bcb34d17415e89b546dbb44e73080f747e504273e4d4987630493cded1b", - "sha256:600cf9bfe75e96d965509a4c0b2b183f74a4fa6f5331dcb40fb7b77b7c2484df", - "sha256:66b57a9ca4b3221d51b237094b0303843b914b7d5afd4349970bb26518e350b0", - "sha256:93cfe5b7ff006de13e1e89830810ecbd014791b042cbe5eec253be11ac2b28f3", - "sha256:9e98b452132963678e3ac6c73f7010fe53adf72209a32854d55690acac3f6724", - "sha256:df186fcbf86dc1ce56305becb8434e4b6b7504bc724b71ad7a3239e0c9d14ef2", - "sha256:fec7fb46b10da10d9e1d078d1ff8ed9e05ae14f431fdbd11145edd0550b9a964" + "sha256:05dc219433b14046c476f6f09d7636b92a1c3e5808b9a6536adf4932b3b2c440", + "sha256:0dcca15d3a19a66e63662dc8d30f8036b07be851a8680eda92d079868f106288", + "sha256:142bae539ef28a1c76794cca7f49729e7c54423f615cfd9b0b1fa90ebe53244b", + "sha256:3daf9b114213f8ba460b829a02896789751626a2a4e7a43a28ee77c04b5e4958", + "sha256:48f388d0d153350f378c7f7b41497a54ff1513c816bcbbcafe5b829e59b9ce5b", + "sha256:4df2af28d7bedc84fe45bd49bc35d710aede676e2a4cb7fc6d103a2adc8afe4d", + "sha256:4f01c9863da784558165f5d4d916093737a75203a5c5286fde60e503e4276c7a", + "sha256:7a38250f433cd41df7fcb763caa3ee9362777fdb4dc642b9a349721d2bf47404", + "sha256:8f79b5ff5ad9d3218afb1e7e20ea74da5f76943ee5edb7f76e56ec5161ec782b", + "sha256:956ba8701b4ffe91ba59665ed170a2ebbdc6fc0e40de5f6059195d9f2b33ca0e", + "sha256:a04386fb7bc85fab9cd51b6308633a3c271e3d0d3eae917eebab2fac6219b6d2", + "sha256:a95f4802d49faa6a674242e25bfeea6fc2acd915b5e5e29ac90a32b1139cae1c", + "sha256:adc0d980fd2760c9e5de537c28935cc32b9353baaf28e0814df417619c6c8c3b", + "sha256:aecbb1592b0188e030cb01f82d12556cf72e218280f621deed7d806afd2113f9", + "sha256:b12794f01d4cacfbd3177b9042198f3af1c856eedd0a98f10f141385c809a14b", + "sha256:c0764e72b36a3dc065c155e5b22f93df465da9c39af65516fe04ed3c68c92636", + "sha256:c33c0d32b8594fa647d2e01dbccc303478e16fdd7cf98652d5b3ed11aa5e5c99", + "sha256:cbaba590180cba88cb99a5f76f90808a624f18b169b90a4abb40c1fd8c19420e", + "sha256:d5a1bd0e9e2031465761dfa920c16b0065ad77321d8a8c1f5ee331021fda65e9" ], "markers": "python_version >= '3.6'", - "version": "==3.4.6" + "version": "==40.0.2" }, "distlib": { "hashes": [ - "sha256:8c09de2c67b3e7deef7184574fc060ab8a793e7adbb183d942c389c8b13c52fb", - "sha256:edf6116872c863e1aa9d5bb7cb5e05a022c519a4594dc703843343a9ddd9bff1" + "sha256:14bad2d9b04d3a36127ac97f30b12a19268f211063d8f8ee4f47108896e11b46", + "sha256:f35c4b692542ca110de7ef0bea44d73981caeb34ca0b9b6b2e6d7790dda8f80e" ], - "version": "==0.3.1" + "version": "==0.3.6" }, "docopt": { "hashes": [ @@ -282,201 +449,242 @@ }, "docutils": { "hashes": [ - "sha256:0c5b78adfbf7762415433f5515cd5c9e762339e23369dbe8000d84a4bf4ab3af", - "sha256:c2de3a60e9e7d07be26b7f2b00ca0309c207e06c100f9cc2a94931fc75a478fc" + "sha256:686577d2e4c32380bb50cbb22f575ed742d58168cee37e99117a854bcd88f125", + "sha256:cf316c8370a737a022b72b56874f6602acf974a37a9fba42ec2876387549fc61" ], - "version": "==0.16" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", + "version": "==0.17.1" }, "filelock": { "hashes": [ - "sha256:18d82244ee114f543149c66a6e0c14e9c4f8a1044b5cdaadd0f82159d6a6ff59", - "sha256:929b7d63ec5b7d6b71b0fa5ac14e030b3f70b75747cef1b10da9b879fef15836" + "sha256:ad98852315c2ab702aeb628412cbf7e95b7ce8c3bf9565670b4eaecf1db370a9", + "sha256:fc03ae43288c013d2ea83c8597001b1129db351aad9c57fe2409327916b8e718" ], - "version": "==3.0.12" + "markers": "python_version >= '3.7'", + "version": "==3.12.0" }, "idna": { "hashes": [ - "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6", - "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0" + "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4", + "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2" ], - "version": "==2.10" + "markers": "python_version >= '3.5'", + "version": "==3.4" }, "imagesize": { "hashes": [ - "sha256:6965f19a6a2039c7d48bca7dba2473069ff854c36ae6f19d2cde309d998228a1", - "sha256:b1f6b5a4eab1f73479a50fb79fcf729514a900c341d8503d62a62dbc4127a2b1" + "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b", + "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==1.4.1" + }, + "importlib-metadata": { + "hashes": [ + "sha256:43dd286a2cd8995d5eaef7fee2066340423b818ed3fd70adf0bad5f1fac53fed", + "sha256:92501cdf9cc66ebd3e612f1b4f0c0765dfa42f0fa38ffb319b6bd84dd675d705" ], - "version": "==1.2.0" + "markers": "python_version >= '3.7'", + "version": "==6.6.0" }, "iniconfig": { "hashes": [ - "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3", - "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32" + "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", + "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374" + ], + "markers": "python_version >= '3.7'", + "version": "==2.0.0" + }, + "jaraco.classes": { + "hashes": [ + "sha256:2353de3288bc6b82120752201c6b1c1a14b058267fa424ed5ce5984e3b922158", + "sha256:89559fa5c1d3c34eff6f631ad80bb21f378dbcbb35dd161fd2c6b93f5be2f98a" ], - "version": "==1.1.1" + "markers": "python_version >= '3.7'", + "version": "==3.2.3" }, "jeepney": { "hashes": [ - "sha256:7d59b6622675ca9e993a6bd38de845051d315f8b0c72cca3aef733a20b648657", - "sha256:aec56c0eb1691a841795111e184e13cad504f7703b9a64f63020816afa79a8ae" + "sha256:5efe48d255973902f6badc3ce55e2aa6c5c3b3bc642059ef3a91247bcfcc5806", + "sha256:c0a454ad016ca575060802ee4d590dd912e35c122fa04e70306de3d076cce755" ], "markers": "sys_platform == 'linux'", - "version": "==0.6.0" + "version": "==0.8.0" }, "jinja2": { "hashes": [ - "sha256:03e47ad063331dd6a3f04a43eddca8a966a26ba0c5b7207a9a9e4e08f1b29419", - "sha256:a6d58433de0ae800347cab1fa3043cebbabe8baa9d29e668f1c768cb87a333c6" + "sha256:077ce6014f7b40d03b47d1f1ca4b0fc8328a692bd284016f806ed0eaca390ad8", + "sha256:611bb273cd68f3b993fabdc4064fc858c5b47a973cb5aa7999ec1ba405c87cd7" ], "index": "pypi", - "version": "==2.11.3" + "version": "==3.0.3" }, "keyring": { "hashes": [ - "sha256:9acb3e1452edbb7544822b12fd25459078769e560fa51f418b6d00afaa6178df", - "sha256:9f44660a5d4931bdc14c08a1d01ef30b18a7a8147380710d8c9f9531e1f6c3c0" + "sha256:771ed2a91909389ed6148631de678f82ddc73737d85a927f382a8a1b157898cd", + "sha256:ba2e15a9b35e21908d0aaf4e0a47acc52d6ae33444df0da2b49d41a46ef6d678" ], - "markers": "python_version >= '3.6'", - "version": "==22.0.1" + "markers": "python_version >= '3.7'", + "version": "==23.13.1" }, "markupsafe": { "hashes": [ - "sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473", - "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161", - "sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235", - "sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5", - "sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42", - "sha256:195d7d2c4fbb0ee8139a6cf67194f3973a6b3042d742ebe0a9ed36d8b6f0c07f", - "sha256:22c178a091fc6630d0d045bdb5992d2dfe14e3259760e713c490da5323866c39", - "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff", - "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b", - "sha256:2beec1e0de6924ea551859edb9e7679da6e4870d32cb766240ce17e0a0ba2014", - "sha256:3b8a6499709d29c2e2399569d96719a1b21dcd94410a586a18526b143ec8470f", - "sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1", - "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e", - "sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183", - "sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66", - "sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b", - "sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1", - "sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15", - "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1", - "sha256:6f1e273a344928347c1290119b493a1f0303c52f5a5eae5f16d74f48c15d4a85", - "sha256:6fffc775d90dcc9aed1b89219549b329a9250d918fd0b8fa8d93d154918422e1", - "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e", - "sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b", - "sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905", - "sha256:7fed13866cf14bba33e7176717346713881f56d9d2bcebab207f7a036f41b850", - "sha256:84dee80c15f1b560d55bcfe6d47b27d070b4681c699c572af2e3c7cc90a3b8e0", - "sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735", - "sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d", - "sha256:98bae9582248d6cf62321dcb52aaf5d9adf0bad3b40582925ef7c7f0ed85fceb", - "sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e", - "sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d", - "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c", - "sha256:a6a744282b7718a2a62d2ed9d993cad6f5f585605ad352c11de459f4108df0a1", - "sha256:acf08ac40292838b3cbbb06cfe9b2cb9ec78fce8baca31ddb87aaac2e2dc3bc2", - "sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21", - "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2", - "sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5", - "sha256:b1dba4527182c95a0db8b6060cc98ac49b9e2f5e64320e2b56e47cb2831978c7", - "sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b", - "sha256:b7d644ddb4dbd407d31ffb699f1d140bc35478da613b441c582aeb7c43838dd8", - "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6", - "sha256:bf5aa3cbcfdf57fa2ee9cd1822c862ef23037f5c832ad09cfea57fa846dec193", - "sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f", - "sha256:caabedc8323f1e93231b52fc32bdcde6db817623d33e100708d9a68e1f53b26b", - "sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f", - "sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2", - "sha256:d53bc011414228441014aa71dbec320c66468c1030aae3a6e29778a3382d96e5", - "sha256:d73a845f227b0bfe8a7455ee623525ee656a9e2e749e4742706d80a6065d5e2c", - "sha256:d9be0ba6c527163cbed5e0857c451fcd092ce83947944d6c14bc95441203f032", - "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7", - "sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be", - "sha256:feb7b34d6325451ef96bc0e36e1a6c0c1c64bc1fbec4b854f4529e51887b1621" - ], - "version": "==1.1.1" + "sha256:0576fe974b40a400449768941d5d0858cc624e3249dfd1e0c33674e5c7ca7aed", + "sha256:085fd3201e7b12809f9e6e9bc1e5c96a368c8523fad5afb02afe3c051ae4afcc", + "sha256:090376d812fb6ac5f171e5938e82e7f2d7adc2b629101cec0db8b267815c85e2", + "sha256:0b462104ba25f1ac006fdab8b6a01ebbfbce9ed37fd37fd4acd70c67c973e460", + "sha256:137678c63c977754abe9086a3ec011e8fd985ab90631145dfb9294ad09c102a7", + "sha256:1bea30e9bf331f3fef67e0a3877b2288593c98a21ccb2cf29b74c581a4eb3af0", + "sha256:22152d00bf4a9c7c83960521fc558f55a1adbc0631fbb00a9471e097b19d72e1", + "sha256:22731d79ed2eb25059ae3df1dfc9cb1546691cc41f4e3130fe6bfbc3ecbbecfa", + "sha256:2298c859cfc5463f1b64bd55cb3e602528db6fa0f3cfd568d3605c50678f8f03", + "sha256:28057e985dace2f478e042eaa15606c7efccb700797660629da387eb289b9323", + "sha256:2e7821bffe00aa6bd07a23913b7f4e01328c3d5cc0b40b36c0bd81d362faeb65", + "sha256:2ec4f2d48ae59bbb9d1f9d7efb9236ab81429a764dedca114f5fdabbc3788013", + "sha256:340bea174e9761308703ae988e982005aedf427de816d1afe98147668cc03036", + "sha256:40627dcf047dadb22cd25ea7ecfe9cbf3bbbad0482ee5920b582f3809c97654f", + "sha256:40dfd3fefbef579ee058f139733ac336312663c6706d1163b82b3003fb1925c4", + "sha256:4cf06cdc1dda95223e9d2d3c58d3b178aa5dacb35ee7e3bbac10e4e1faacb419", + "sha256:50c42830a633fa0cf9e7d27664637532791bfc31c731a87b202d2d8ac40c3ea2", + "sha256:55f44b440d491028addb3b88f72207d71eeebfb7b5dbf0643f7c023ae1fba619", + "sha256:608e7073dfa9e38a85d38474c082d4281f4ce276ac0010224eaba11e929dd53a", + "sha256:63ba06c9941e46fa389d389644e2d8225e0e3e5ebcc4ff1ea8506dce646f8c8a", + "sha256:65608c35bfb8a76763f37036547f7adfd09270fbdbf96608be2bead319728fcd", + "sha256:665a36ae6f8f20a4676b53224e33d456a6f5a72657d9c83c2aa00765072f31f7", + "sha256:6d6607f98fcf17e534162f0709aaad3ab7a96032723d8ac8750ffe17ae5a0666", + "sha256:7313ce6a199651c4ed9d7e4cfb4aa56fe923b1adf9af3b420ee14e6d9a73df65", + "sha256:7668b52e102d0ed87cb082380a7e2e1e78737ddecdde129acadb0eccc5423859", + "sha256:7df70907e00c970c60b9ef2938d894a9381f38e6b9db73c5be35e59d92e06625", + "sha256:7e007132af78ea9df29495dbf7b5824cb71648d7133cf7848a2a5dd00d36f9ff", + "sha256:835fb5e38fd89328e9c81067fd642b3593c33e1e17e2fdbf77f5676abb14a156", + "sha256:8bca7e26c1dd751236cfb0c6c72d4ad61d986e9a41bbf76cb445f69488b2a2bd", + "sha256:8db032bf0ce9022a8e41a22598eefc802314e81b879ae093f36ce9ddf39ab1ba", + "sha256:99625a92da8229df6d44335e6fcc558a5037dd0a760e11d84be2260e6f37002f", + "sha256:9cad97ab29dfc3f0249b483412c85c8ef4766d96cdf9dcf5a1e3caa3f3661cf1", + "sha256:a4abaec6ca3ad8660690236d11bfe28dfd707778e2442b45addd2f086d6ef094", + "sha256:a6e40afa7f45939ca356f348c8e23048e02cb109ced1eb8420961b2f40fb373a", + "sha256:a6f2fcca746e8d5910e18782f976489939d54a91f9411c32051b4aab2bd7c513", + "sha256:a806db027852538d2ad7555b203300173dd1b77ba116de92da9afbc3a3be3eed", + "sha256:abcabc8c2b26036d62d4c746381a6f7cf60aafcc653198ad678306986b09450d", + "sha256:b8526c6d437855442cdd3d87eede9c425c4445ea011ca38d937db299382e6fa3", + "sha256:bb06feb762bade6bf3c8b844462274db0c76acc95c52abe8dbed28ae3d44a147", + "sha256:c0a33bc9f02c2b17c3ea382f91b4db0e6cde90b63b296422a939886a7a80de1c", + "sha256:c4a549890a45f57f1ebf99c067a4ad0cb423a05544accaf2b065246827ed9603", + "sha256:ca244fa73f50a800cf8c3ebf7fd93149ec37f5cb9596aa8873ae2c1d23498601", + "sha256:cf877ab4ed6e302ec1d04952ca358b381a882fbd9d1b07cccbfd61783561f98a", + "sha256:d9d971ec1e79906046aa3ca266de79eac42f1dbf3612a05dc9368125952bd1a1", + "sha256:da25303d91526aac3672ee6d49a2f3db2d9502a4a60b55519feb1a4c7714e07d", + "sha256:e55e40ff0cc8cc5c07996915ad367fa47da6b3fc091fdadca7f5403239c5fec3", + "sha256:f03a532d7dee1bed20bc4884194a16160a2de9ffc6354b3878ec9682bb623c54", + "sha256:f1cd098434e83e656abf198f103a8207a8187c0fc110306691a2e94a78d0abb2", + "sha256:f2bfb563d0211ce16b63c7cb9395d2c682a23187f54c3d79bfec33e6705473c6", + "sha256:f8ffb705ffcf5ddd0e80b65ddf7bed7ee4f5a441ea7d3419e861a12eaf41af58" + ], + "markers": "python_version >= '3.7'", + "version": "==2.1.2" + }, + "more-itertools": { + "hashes": [ + "sha256:cabaa341ad0389ea83c17a94566a53ae4c9d07349861ecb14dc6d0345cf9ac5d", + "sha256:d2bc7f02446e86a68911e58ded76d6561eea00cddfb2a91e7019bbb586c799f3" + ], + "markers": "python_version >= '3.7'", + "version": "==9.1.0" }, "packaging": { "hashes": [ - "sha256:5b327ac1320dc863dca72f4514ecc086f31186744b84a230374cc1fd776feae5", - "sha256:67714da7f7bc052e064859c05c595155bd1ee9f69f76557e21f051443c20947a" + "sha256:994793af429502c4ea2ebf6bf664629d07c1a9fe974af92966e4b8d2df7edc61", + "sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==20.9" + "markers": "python_version >= '3.7'", + "version": "==23.1" + }, + "pip": { + "hashes": [ + "sha256:c146f331f0805c77017c6bb9740cec4a49a0d4582d0c3cc8244b057f83eca359", + "sha256:f29d589df8c8ab99c060e68ad294c4a9ed896624f6368c5349d70aa581b333d0" + ], + "index": "pypi", + "version": "==22.0.3" }, "pkginfo": { "hashes": [ - "sha256:029a70cb45c6171c329dfc890cde0879f8c52d6f3922794796e06f577bb03db4", - "sha256:9fdbea6495622e022cc72c2e5e1b735218e4ffb2a2a69cde2694a6c1f16afb75" + "sha256:4b7a555a6d5a22169fcc9cf7bfd78d296b0361adad412a346c1226849af5e546", + "sha256:8fd5896e8718a4372f0ea9cc9d96f6417c9b986e23a4d116dda26b62cc29d046" ], - "version": "==1.7.0" + "markers": "python_version >= '3.6'", + "version": "==1.9.6" + }, + "platformdirs": { + "hashes": [ + "sha256:83c8f6d04389165de7c9b6f0c682439697887bca0aa2f1c87ef1826be3584490", + "sha256:e1fea1fe471b9ff8332e229df3cb7de4f53eeea4998d3b6bfff542115e998bd2" + ], + "markers": "python_version >= '3.7'", + "version": "==2.6.2" }, "pluggy": { "hashes": [ - "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0", - "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d" + "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159", + "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3" ], - "version": "==0.13.1" + "markers": "python_version >= '3.6'", + "version": "==1.0.0" }, "pproxy": { "hashes": [ - "sha256:e013818b1325603a08769a1c1e36e1ce79482b35069cf64fcdc1c11d34f52fdb", - "sha256:f59e18328f18c8edf601a2b09f857be81f525d043c935e3d5dfa3b31d7f48f1b" + "sha256:9f300bae5288c7c7f56be70d6275571efd2b4862f306d25bdace3c3537fb53a7", + "sha256:fab73cc13b2bb10c9fc4d9c1a8ec8011a354c9bcbffa446d91229e13c5d996c8" ], "index": "pypi", - "version": "==2.7.4" + "version": "==2.7.8" }, "py": { "hashes": [ - "sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3", - "sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a" + "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719", + "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==1.10.0" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", + "version": "==1.11.0" }, "pycparser": { "hashes": [ - "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0", - "sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705" + "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9", + "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206" ], - "version": "==2.20" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==2.21" }, "pygments": { "hashes": [ - "sha256:37a13ba168a02ac54cc5891a42b1caec333e59b66addb7fa633ea8a6d73445c0", - "sha256:b21b072d0ccdf29297a82a2363359d99623597b8a265b8081760e4d0f7153c88" - ], - "markers": "python_version >= '3.5'", - "version": "==2.8.0" - }, - "pyparsing": { - "hashes": [ - "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1", - "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b" + "sha256:8ace4d3c1dd481894b2005f560ead0f9f19ee64fe983366be1a21e171d12775c", + "sha256:db2db3deb4b4179f399a09054b023b6a586b76499d36965813c71aa8ed7b5fd1" ], - "version": "==2.4.7" + "markers": "python_version >= '3.7'", + "version": "==2.15.1" }, "pytest": { "hashes": [ - "sha256:9d1edf9e7d0b84d72ea3dbcdfd22b35fb543a5e8f2a60092dd578936bf63d7f9", - "sha256:b574b57423e818210672e07ca1fa90aaf194a4f63f3ab909a2c67ebb22913839" + "sha256:9ce3ff477af913ecf6321fe337b93a2c0dcf2a0a1439c43f5452112c1e4280db", + "sha256:e30905a0c131d3d94b89624a1cc5afec3e0ba2fbdb151867d8e0ebd49850f171" ], "index": "pypi", - "version": "==6.2.2" + "version": "==7.0.1" }, "pytz": { "hashes": [ - "sha256:83a4a90894bf38e243cf052c8b58f381bfe9a7a483f6a9cab140bc7f702ac4da", - "sha256:eb10ce3e7736052ed3623d49975ce333bcd712c7bb19a58b9e2089d4057d0798" + "sha256:1d8ce29db189191fb55338ee6d0387d82ab59f3d00eac103412d64e0ebd0c588", + "sha256:a151b3abb88eda1d4e34a9814df37de2a80e301e68ba0fd856fb9b46bfbbbffb" ], - "version": "==2021.1" + "version": "==2023.3" }, "readme-renderer": { "hashes": [ - "sha256:63b4075c6698fcfa78e584930f07f39e05d46f3ec97f65006e430b595ca6348c", - "sha256:92fd5ac2bf8677f310f3303aa4bce5b9d5f9f2094ab98c29f13791d7b805a3db" + "sha256:cd653186dfc73055656f090f227f5cb22a046d7f71a841dfa305f55c9a513273", + "sha256:f67a16caedfa71eef48a31b39708637a6f4664c4394801a7b0d6432d13907343" ], - "version": "==29.0" + "markers": "python_version >= '3.7'", + "version": "==37.3" }, "recommonmark": { "hashes": [ @@ -487,56 +695,62 @@ "version": "==0.7.1" }, "requests": { + "extras": [ + "socks" + ], "hashes": [ - "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804", - "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e" + "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f", + "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1" ], "index": "pypi", - "version": "==2.25.1" + "version": "==2.31.0" }, "requests-toolbelt": { "hashes": [ - "sha256:380606e1d10dc85c3bd47bf5a6095f815ec007be7a8b69c878507068df059e6f", - "sha256:968089d4584ad4ad7c171454f0a5c6dac23971e9472521ea3b6d49d610aa6fc0" + "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6", + "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06" ], - "version": "==0.9.1" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==1.0.0" }, "rfc3986": { "hashes": [ - "sha256:112398da31a3344dc25dbf477d8df6cb34f9278a94fee2625d89e4514be8bb9d", - "sha256:af9147e9aceda37c91a05f4deb128d4b4b49d6b199775fd2d2927768abdc8f50" + "sha256:50b1502b60e289cb37883f3dfd34532b8873c7de9f49bb546641ce9cbd256ebd", + "sha256:97aacf9dbd4bfd829baad6e6309fa6573aaf1be3f6fa735c8ab05e46cecb261c" ], - "version": "==1.4.0" + "markers": "python_version >= '3.7'", + "version": "==2.0.0" }, "secretstorage": { "hashes": [ - "sha256:422d82c36172d88d6a0ed5afdec956514b189ddbfb72fefab0c8a1cee4eaf71f", - "sha256:fd666c51a6bf200643495a04abb261f83229dcb6fd8472ec393df7ffc8b6f195" + "sha256:2403533ef369eca6d2ba81718576c5e0f564d5cca1b58f73a8b23e7d4eeebd77", + "sha256:f356e6628222568e3af06f2eba8df495efa13b3b63081dafd4f7d9a7b7bc9f99" ], "markers": "sys_platform == 'linux'", - "version": "==3.3.1" + "version": "==3.3.3" }, "six": { "hashes": [ - "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", - "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced" + "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", + "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" ], - "version": "==1.15.0" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==1.16.0" }, "snowballstemmer": { "hashes": [ - "sha256:b51b447bea85f9968c13b650126a888aabd4cb4463fca868ec596826325dedc2", - "sha256:e997baa4f2e9139951b6f4c631bad912dfd3c792467e2f03d7239464af90e914" + "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1", + "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a" ], - "version": "==2.1.0" + "version": "==2.2.0" }, "sphinx": { "hashes": [ - "sha256:11d521e787d9372c289472513d807277caafb1684b33eb4f08f7574c405893a9", - "sha256:e90161222e4d80ce5fc811ace7c6787a226b4f5951545f7f42acf97277bfc35c" + "sha256:5da895959511473857b6d0200f56865ed62c31e8f82dd338063b84ec022701fe", + "sha256:6caad9786055cb1fa22b4a365c1775816b876f91966481765d7d50e9f0dd35cc" ], "index": "pypi", - "version": "==3.5.1" + "version": "==4.4.0" }, "sphinx-readable-theme": { "hashes": [ @@ -547,30 +761,34 @@ }, "sphinxcontrib-applehelp": { "hashes": [ - "sha256:806111e5e962be97c29ec4c1e7fe277bfd19e9652fb1a4392105b43e01af885a", - "sha256:a072735ec80e7675e3f432fcae8610ecf509c5f1869d17e2eecff44389cdbc58" + "sha256:29d341f67fb0f6f586b23ad80e072c8e6ad0b48417db2bde114a4c9746feb228", + "sha256:828f867945bbe39817c210a1abfd1bc4895c8b73fcaade56d45357a348a07d7e" ], - "version": "==1.0.2" + "markers": "python_version >= '3.8'", + "version": "==1.0.4" }, "sphinxcontrib-devhelp": { "hashes": [ "sha256:8165223f9a335cc1af7ffe1ed31d2871f325254c0423bc0c4c7cd1c1e4734a2e", "sha256:ff7f1afa7b9642e7060379360a67e9c41e8f3121f2ce9164266f61b9f4b338e4" ], + "markers": "python_version >= '3.5'", "version": "==1.0.2" }, "sphinxcontrib-htmlhelp": { "hashes": [ - "sha256:3c0bc24a2c41e340ac37c85ced6dafc879ab485c095b1d65d2461ac2f7cca86f", - "sha256:e8f5bb7e31b2dbb25b9cc435c8ab7a79787ebf7f906155729338f3156d93659b" + "sha256:0cbdd302815330058422b98a113195c9249825d681e18f11e8b1f78a2f11efff", + "sha256:c38cb46dccf316c79de6e5515e1770414b797162b23cd3d06e67020e1d2a6903" ], - "version": "==1.0.3" + "markers": "python_version >= '3.8'", + "version": "==2.0.1" }, "sphinxcontrib-jsmath": { "hashes": [ "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178", "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8" ], + "markers": "python_version >= '3.5'", "version": "==1.0.1" }, "sphinxcontrib-qthelp": { @@ -578,14 +796,16 @@ "sha256:4c33767ee058b70dba89a6fc5c1892c0d57a54be67ddd3e7875a18d14cba5a72", "sha256:bd9fc24bcb748a8d51fd4ecaade681350aa63009a347a8c14e637895444dfab6" ], + "markers": "python_version >= '3.5'", "version": "==1.0.3" }, "sphinxcontrib-serializinghtml": { "hashes": [ - "sha256:eaa0eccc86e982a9b939b2b82d12cc5d013385ba5eadcc7e4fed23f4405f77bc", - "sha256:f242a81d423f59617a8e5cf16f5d4d74e28ee9a66f9e5b637a18082991db5a9a" + "sha256:352a9a00ae864471d3a7ead8d7d79f5fc0b57e8b3f95e9867eb9eb28999b92fd", + "sha256:aa5f6de5dfdf809ef505c4895e51ef5c9eac17d0f287933eb49ec495280b6952" ], - "version": "==1.1.4" + "markers": "python_version >= '3.5'", + "version": "==1.1.5" }, "toml": { "hashes": [ @@ -595,13 +815,21 @@ "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==0.10.2" }, + "tomli": { + "hashes": [ + "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc", + "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f" + ], + "markers": "python_version >= '3.7'", + "version": "==2.0.1" + }, "tox": { "hashes": [ - "sha256:89afa9c59c04beb55eda789c7a65feb1a70fde117f85f1bd1c27c66758456e60", - "sha256:ed1e650cf6368bcbc4a071eeeba363c480920e0ed8a9ad1793c7caaa5ad33d49" + "sha256:67e0e32c90e278251fea45b696d0fef3879089ccbe979b0c556d35d5a70e2993", + "sha256:be3362472a33094bce26727f5f771ca0facf6dafa217f65875314e9a6600c95c" ], "index": "pypi", - "version": "==3.22.0" + "version": "==3.24.5" }, "tox-travis": { "hashes": [ @@ -613,35 +841,35 @@ }, "tqdm": { "hashes": [ - "sha256:65185676e9fdf20d154cffd1c5de8e39ef9696ff7e59fe0156b1b08e468736af", - "sha256:70657337ec104eb4f3fb229285358f23f045433f6aea26846cdd55f0fd68945c" + "sha256:1871fb68a86b8fb3b59ca4cdd3dcccbc7e6d613eeed31f4c332531977b89beb5", + "sha256:c4f53a17fe37e132815abceec022631be8ffe1b9381c2e6e30aa70edc99e9671" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==4.57.0" + "markers": "python_version >= '3.7'", + "version": "==4.65.0" }, "twine": { "hashes": [ - "sha256:2f6942ec2a17417e19d2dd372fc4faa424c87ee9ce49b4e20c427eb00a0f3f41", - "sha256:fcffa8fc37e8083a5be0728371f299598870ee1eccc94e9a25cef7b1dcfa8297" + "sha256:8efa52658e0ae770686a13b675569328f1fba9837e5de1867bfe5f46a9aefe19", + "sha256:d0550fca9dc19f3d5e8eadfce0c227294df0a2a951251a4385797c8a6198b7c8" ], "index": "pypi", - "version": "==3.3.0" + "version": "==3.8.0" }, "urllib3": { "hashes": [ - "sha256:1b465e494e3e0d8939b50680403e3aedaa2bc434b7d5af64dfd3c958d7f5ae80", - "sha256:de3eedaad74a2683334e282005cd8d7f22f4d55fa690a2a1020a416cb0a47e73" + "sha256:61717a1095d7e155cdb737ac7bb2f4324a858a1e2e6466f6d03ff630ca68d3cc", + "sha256:d055c2f9d38dc53c808f6fdc8eab7360b6fdbbde02340ed25cfbcd817c62469e" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'", - "version": "==1.26.3" + "markers": "python_version >= '3.7'", + "version": "==2.0.2" }, "virtualenv": { "hashes": [ - "sha256:147b43894e51dd6bba882cf9c282447f780e2251cd35172403745fc381a0a80d", - "sha256:2be72df684b74df0ea47679a7df93fd0e04e72520022c57b479d8f881485dbe3" + "sha256:45e1d053cad4cd453181ae877c4ffc053546ae99e7dd049b9ff1d9be7491abf7", + "sha256:e0621bcbf4160e4e1030f05065c8834b4e93f4fcc223255db2a823440aca9c14" ], "index": "pypi", - "version": "==20.4.2" + "version": "==20.13.1" }, "webencodings": { "hashes": [ @@ -649,6 +877,14 @@ "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923" ], "version": "==0.5.1" + }, + "zipp": { + "hashes": [ + "sha256:112929ad649da941c23de50f356a2b5570c954b65150642bccdd66bf194d224b", + "sha256:48904fc76a60e542af151aded95726c1a5c34ed43ab4134b597665c86d7ad556" + ], + "markers": "python_version >= '3.7'", + "version": "==3.15.0" } } } diff --git a/dev-requirements.txt b/dev-requirements.txt index 1ecd4305..ec4f9b40 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -11,3 +11,4 @@ tox tox-travis virtualenv twine +urllib3>=1.26.5 diff --git a/pyowm/agroapi10/agro_manager.py b/pyowm/agroapi10/agro_manager.py index 9b1e350a..13b527f8 100644 --- a/pyowm/agroapi10/agro_manager.py +++ b/pyowm/agroapi10/agro_manager.py @@ -6,7 +6,8 @@ from pyowm.agroapi10.polygon import Polygon, GeoPolygon from pyowm.agroapi10.search import SatelliteImagerySearchResultSet from pyowm.agroapi10.soil import Soil -from pyowm.agroapi10.uris import ROOT_AGRO_API, POLYGONS_URI, NAMED_POLYGON_URI, SOIL_URI, SATELLITE_IMAGERY_SEARCH_URI +from pyowm.agroapi10.uris import ROOT_AGRO_API, ROOT_DOWNLOAD_PNG_API, ROOT_DOWNLOAD_GEOTIFF_API, POLYGONS_URI, \ + NAMED_POLYGON_URI, SOIL_URI, SATELLITE_IMAGERY_SEARCH_URI from pyowm.commons.http_client import HttpClient from pyowm.commons.image import Image from pyowm.commons.tile import Tile @@ -33,6 +34,8 @@ def __init__(self, API_key, config): self.API_key = API_key assert isinstance(config, dict) self.http_client = HttpClient(API_key, config, ROOT_AGRO_API) + self.geotiff_downloader_http_client = HttpClient(self.API_key, config, ROOT_DOWNLOAD_GEOTIFF_API) + self.png_downloader_http_client = HttpClient(self.API_key, config, ROOT_DOWNLOAD_PNG_API) def agro_api_version(self): return AGRO_API_VERSION @@ -279,14 +282,14 @@ def download_satellite_image(self, metaimage, x=None, y=None, zoom=None, palette # polygon PNG if isinstance(metaimage, MetaPNGImage): prepared_url = metaimage.url - status, data = self.http_client.get_png( + status, data = self.png_downloader_http_client.get_png( prepared_url, params=params) img = Image(data, metaimage.image_type) return SatelliteImage(metaimage, img, downloaded_on=timestamps.now(timeformat='unix'), palette=palette) # GeoTIF elif isinstance(metaimage, MetaGeoTiffImage): prepared_url = metaimage.url - status, data = self.http_client.get_geotiff( + status, data = self.geotiff_downloader_http_client.get_geotiff( prepared_url, params=params) img = Image(data, metaimage.image_type) return SatelliteImage(metaimage, img, downloaded_on=timestamps.now(timeformat='unix'), palette=palette) diff --git a/pyowm/agroapi10/uris.py b/pyowm/agroapi10/uris.py index fdce3b78..fec2650e 100644 --- a/pyowm/agroapi10/uris.py +++ b/pyowm/agroapi10/uris.py @@ -2,6 +2,8 @@ # -*- coding: utf-8 -*- ROOT_AGRO_API = 'agromonitoring.com/agro/1.0' +ROOT_DOWNLOAD_PNG_API = 'agromonitoring.com/image/1.0' +ROOT_DOWNLOAD_GEOTIFF_API = 'agromonitoring.com/data/1.0' # Polygons API subset POLYGONS_URI = 'polygons' diff --git a/pyowm/commons/cityidregistry.py b/pyowm/commons/cityidregistry.py index 8ecd5ac1..86cdf0fb 100644 --- a/pyowm/commons/cityidregistry.py +++ b/pyowm/commons/cityidregistry.py @@ -2,34 +2,23 @@ # -*- coding: utf-8 -*- import bz2 +import sqlite3 +import tempfile from pkg_resources import resource_filename from pyowm.weatherapi25.location import Location - -CITY_ID_FILES_PATH = 'cityids/%03d-%03d.txt.bz2' +CITY_ID_DB_PATH = 'cityids/cities.db.bz2' class CityIDRegistry: MATCHINGS = { - 'exact': lambda city_name, toponym: city_name == toponym, - 'nocase': lambda city_name, toponym: city_name.lower() == toponym.lower(), - 'like': lambda city_name, toponym: city_name.lower() in toponym.lower(), - 'startswith': lambda city_name, toponym: toponym.lower().startswith(city_name.lower()) + 'exact': "SELECT city_id, name, country, state, lat, lon FROM city WHERE name=?", + 'like': r"SELECT city_id, name, country, state, lat, lon FROM city WHERE name LIKE ?" } - def __init__(self, filepath_regex): - """ - Initialise a registry that can be used to lookup info about cities. - - :param filepath_regex: Python format string that gives the path of the files - that store the city IDs information. - Eg: ``folder1/folder2/%02d-%02d.txt`` - :type filepath_regex: str - :returns: a *CityIDRegistry* instance - - """ - self._filepath_regex = filepath_regex + def __init__(self, sqlite_db_path: str): + self.connection = self.__decompress_db_to_memory(sqlite_db_path) @classmethod def get_instance(cls): @@ -37,27 +26,63 @@ def get_instance(cls): Factory method returning the default city ID registry :return: a `CityIDRegistry` instance """ - return CityIDRegistry(CITY_ID_FILES_PATH) + return CityIDRegistry(CITY_ID_DB_PATH) - def ids_for(self, city_name, country=None, matching='nocase'): + def __decompress_db_to_memory(self, sqlite_db_path: str): """ - Returns a list of tuples in the form (long, str, str) corresponding to - the int IDs and relative toponyms and 2-chars country of the cities - matching the provided city name. - The rule for identifying matchings is according to the provided - `matching` parameter value. + Decompresses to memory the SQLite database at the provided path + :param sqlite_db_path: str + :return: None + """ + # https://stackoverflow.com/questions/3850022/how-to-load-existing-db-file-to-memory-in-python-sqlite3 + # https://stackoverflow.com/questions/32681761/how-can-i-attach-an-in-memory-sqlite-database-in-python + # https://pymotw.com/2/bz2/ + + # read and uncompress data from compressed DB + res_name = resource_filename(__name__, sqlite_db_path) + bz2_db = bz2.BZ2File(res_name) + decompressed_data = bz2_db.read() + + # dump decompressed data to a temp DB + with tempfile.NamedTemporaryFile(mode='wb') as tmpf: + tmpf.write(decompressed_data) + tmpf_name = tmpf.name + + # read temp DB to memory and return handle + src_conn = sqlite3.connect(tmpf_name) + dest_conn = sqlite3.connect(':memory:') + src_conn.backup(dest_conn) + src_conn.close() + return dest_conn + + def __query(self, sql_query: str, *args): + """ + Queries the DB with the specified SQL query + :param sql_query: str + :return: list of tuples + """ + cursor = self.connection.cursor() + try: + return cursor.execute(sql_query, args).fetchall() + finally: + cursor.close() + + def ids_for(self, city_name, country=None, state=None, matching='like'): + """ + Returns a list of tuples in the form (city_id, name, country, state, lat, lon ) + The rule for querying follows the provided `matching` parameter value. If `country` is provided, the search is restricted to the cities of - the specified country. + the specified country, and an even stricter search when `state` is provided as well + :param city_name: the string toponym of the city to search :param country: two character str representing the country where to search for the city. Defaults to `None`, which means: search in all countries. - :param matching: str. Default is `nocase`. Possible values: - `exact` - literal, case-sensitive matching, - `nocase` - literal, case-insensitive matching, + :param state: two character str representing the state where to + search for the city. Defaults to `None`. When not `None` also `state` must be specified + :param matching: str. Default is `like`. Possible values: + `exact` - literal, case-sensitive matching `like` - matches cities whose name contains, as a substring, the string fed to the function, case-insensitive, - `startswith` - matches cities whose names start with the string fed - to the function, case-insensitive. :raises ValueError if the value for `matching` is unknown :return: list of tuples """ @@ -68,43 +93,49 @@ def ids_for(self, city_name, country=None, matching='nocase'): "allowed values are %s" % ", ".join(self.MATCHINGS)) if country is not None and len(country) != 2: raise ValueError("Country must be a 2-char string") - splits = self._filter_matching_lines(city_name, country, matching) - return [(int(item[1]), item[0], item[4]) for item in splits] + if state is not None and country is None: + raise ValueError("A country must be specified whenever a state is specified too") + + q = self.MATCHINGS[matching] + if matching == 'exact': + params = [city_name] + else: + params = ['%' + city_name + '%'] + + if country is not None: + q = q + ' AND country=?' + params.append(country) + + if state is not None: + q = q + ' AND state=?' + params.append(state) + + rows = self.__query(q, *params) + return rows - def locations_for(self, city_name, country=None, matching='nocase'): + def locations_for(self, city_name, country=None, state=None, matching='like'): """ - Returns a list of Location objects corresponding to - the int IDs and relative toponyms and 2-chars country of the cities - matching the provided city name. - The rule for identifying matchings is according to the provided - `matching` parameter value. + Returns a list of `Location` objects + The rule for querying follows the provided `matching` parameter value. If `country` is provided, the search is restricted to the cities of - the specified country. + the specified country, and an even stricter search when `state` is provided as well + :param city_name: the string toponym of the city to search :param country: two character str representing the country where to search for the city. Defaults to `None`, which means: search in all countries. - :param matching: str. Default is `nocase`. Possible values: - `exact` - literal, case-sensitive matching, - `nocase` - literal, case-insensitive matching, + :param state: two character str representing the state where to + search for the city. Defaults to `None`. When not `None` also `state` must be specified + :param matching: str. Default is `like`. Possible values: + `exact` - literal, case-sensitive matching `like` - matches cities whose name contains, as a substring, the string fed to the function, case-insensitive, - `startswith` - matches cities whose names start with the string fed - to the function, case-insensitive. :raises ValueError if the value for `matching` is unknown - :return: list of `weatherapi25.location.Location` objects + :return: list of `Location` objects """ - if not city_name: - return [] - if matching not in self.MATCHINGS: - raise ValueError("Unknown type of matching: " - "allowed values are %s" % ", ".join(self.MATCHINGS)) - if country is not None and len(country) != 2: - raise ValueError("Country must be a 2-char string") - splits = self._filter_matching_lines(city_name, country, matching) - return [Location(item[0], float(item[3]), float(item[2]), - int(item[1]), item[4]) for item in splits] + items = self.ids_for(city_name, country=country, state=state, matching=matching) + return [Location(item[1], item[5], item[4], item[0], country=item[2]) for item in items] - def geopoints_for(self, city_name, country=None, matching='nocase'): + def geopoints_for(self, city_name, country=None, state=None, matching='like'): """ Returns a list of ``pyowm.utils.geo.Point`` objects corresponding to the int IDs and relative toponyms and 2-chars country of the cities @@ -113,114 +144,18 @@ def geopoints_for(self, city_name, country=None, matching='nocase'): `matching` parameter value. If `country` is provided, the search is restricted to the cities of the specified country. + :param city_name: the string toponym of the city to search :param country: two character str representing the country where to search for the city. Defaults to `None`, which means: search in all countries. + :param state: two character str representing the state where to + search for the city. Defaults to `None`. When not `None` also `state` must be specified :param matching: str. Default is `nocase`. Possible values: - `exact` - literal, case-sensitive matching, - `nocase` - literal, case-insensitive matching, + `exact` - literal, case-sensitive matching `like` - matches cities whose name contains, as a substring, the string fed to the function, case-insensitive, - `startswith` - matches cities whose names start with the string fed - to the function, case-insensitive. :raises ValueError if the value for `matching` is unknown :return: list of `pyowm.utils.geo.Point` objects """ - locations = self.locations_for(city_name, country, matching=matching) + locations = self.locations_for(city_name, country=country, state=state, matching=matching) return [loc.to_geopoint() for loc in locations] - - # helper functions - - def _filter_matching_lines(self, city_name, country, matching): - """ - Returns an iterable whose items are the lists of split tokens of every - text line matched against the city ID files according to the provided - combination of city_name, country and matching style - :param city_name: str - :param country: str or `None` - :param matching: str - :return: list of lists - """ - result = [] - - # find the right file to scan and extract its lines. Upon "like" - # matchings, just read all files - if matching == 'like': - lines = [l.strip() for l in self._get_all_lines()] - else: - filename = self._assess_subfile_from(city_name) - lines = [l.strip() for l in self._get_lines(filename)] - - # look for toponyms matching the specified city_name and according to - # the specified matching style - for line in lines: - tokens = line.split(",") - # sometimes city names have one or more inner commas - if len(tokens) > 5: - tokens = [','.join(tokens[:-4]), *tokens[-4:]] - # check country - if country is not None and tokens[4] != country: - continue - - # check city_name - if self._city_name_matches(city_name, tokens[0], matching): - result.append(tokens) - - return result - - def _city_name_matches(self, city_name, toponym, matching): - comparison_function = self.MATCHINGS[matching] - return comparison_function(city_name, toponym) - - def _lookup_line_by_city_name(self, city_name): - filename = self._assess_subfile_from(city_name) - lines = self._get_lines(filename) - return self._match_line(city_name, lines) - - def _assess_subfile_from(self, city_name): - c = ord(city_name.lower()[0]) - if c < 97: # not a letter - raise ValueError('Error: city name must start with a letter') - elif c in range(97, 103): # from a to f - return self._filepath_regex % (97, 102) - elif c in range(103, 109): # from g to l - return self._filepath_regex % (103, 108) - elif c in range(109, 115): # from m to r - return self._filepath_regex % (109, 114) - elif c in range(115, 123): # from s to z - return self._filepath_regex % (115, 122) - else: - raise ValueError('Error: city name must start with a letter') - - def _get_lines(self, filename): - res_name = resource_filename(__name__, filename) - with bz2.open(res_name, mode='rb') as fh: - lines = fh.readlines() - if type(lines[0]) is bytes: - lines = map(lambda l: l.decode("utf-8"), lines) - return lines - - def _get_all_lines(self): - all_lines = [] - for city_name in ['a', 'g', 'm', 's']: # all available city ID files - filename = self._assess_subfile_from(city_name) - all_lines.extend(self._get_lines(filename)) - return all_lines - - def _match_line(self, city_name, lines): - """ - The lookup is case insensitive and returns the first matching line, - stripped. - :param city_name: str - :param lines: list of str - :return: str - """ - for line in lines: - toponym = line.split(',')[0] - if toponym.lower() == city_name.lower(): - return line.strip() - return None - - def __repr__(self): - return "<%s.%s - filepath_regex=%s>" % (__name__, \ - self.__class__.__name__, self._filepath_regex) diff --git a/pyowm/commons/cityids/097-102.txt.bz2 b/pyowm/commons/cityids/097-102.txt.bz2 deleted file mode 100644 index 49d54391..00000000 Binary files a/pyowm/commons/cityids/097-102.txt.bz2 and /dev/null differ diff --git a/pyowm/commons/cityids/103-108.txt.bz2 b/pyowm/commons/cityids/103-108.txt.bz2 deleted file mode 100644 index 634827bb..00000000 Binary files a/pyowm/commons/cityids/103-108.txt.bz2 and /dev/null differ diff --git a/pyowm/commons/cityids/109-114.txt.bz2 b/pyowm/commons/cityids/109-114.txt.bz2 deleted file mode 100644 index 5c7e3f22..00000000 Binary files a/pyowm/commons/cityids/109-114.txt.bz2 and /dev/null differ diff --git a/pyowm/commons/cityids/115-122.txt.bz2 b/pyowm/commons/cityids/115-122.txt.bz2 deleted file mode 100644 index 1412cd84..00000000 Binary files a/pyowm/commons/cityids/115-122.txt.bz2 and /dev/null differ diff --git a/pyowm/commons/cityids/cities.db.bz2 b/pyowm/commons/cityids/cities.db.bz2 new file mode 100644 index 00000000..b8e053a8 Binary files /dev/null and b/pyowm/commons/cityids/cities.db.bz2 differ diff --git a/pyowm/commons/http_client.py b/pyowm/commons/http_client.py index 7ba435b7..d3eee06d 100644 --- a/pyowm/commons/http_client.py +++ b/pyowm/commons/http_client.py @@ -3,6 +3,8 @@ import json import requests +from requests.adapters import HTTPAdapter +from requests.packages.urllib3.util.retry import Retry from pyowm.commons import exceptions from pyowm.commons.enums import ImageTypeEnum @@ -119,6 +121,22 @@ def __init__(self, api_key, config, root_uri, admits_subdomains=True): assert isinstance(admits_subdomains, bool) self.admits_subdomains = admits_subdomains + if self.config['connection']['max_retries'] is not None: + # this adapter tells how to perform retries + self.session_adapter = HTTPAdapter( + max_retries=Retry( + total=self.config['connection']['max_retries'], + status_forcelist=[429, 500, 502, 503, 504], + method_whitelist=["HEAD", "GET", "PUT", "DELETE", "OPTIONS", "TRACE"] + ) + ) + # this is the adapted requests client + self.http = requests.Session() + self.http.mount("https://", self.session_adapter) + self.http.mount("http://", self.session_adapter) + else: + self.http = requests + def get_json(self, path, params=None, headers=None): builder = HttpRequestBuilder(self.root_uri, self.api_key, self.config, has_subdomains=self.admits_subdomains)\ .with_path(path)\ @@ -128,7 +146,7 @@ def get_json(self, path, params=None, headers=None): .with_headers(headers if headers is not None else dict()) url, params, headers, proxies = builder.build() try: - resp = requests.get(url, params=params, headers=headers, proxies=proxies, + resp = self.http.get(url, params=params, headers=headers, proxies=proxies, timeout=self.config['connection']['timeout_secs'], verify=self.config['connection']['verify_ssl_certs']) except requests.exceptions.SSLError as e: @@ -144,8 +162,14 @@ def get_json(self, path, params=None, headers=None): raise exceptions.ParseAPIResponseError('Impossible to parse API response data') def get_png(self, path, params=None, headers=None): + # check URL fromt the metaimage: if it looks like a complete URL, use that one (I know, it's a hack...) + try: + partial_path = path.split(self.root_uri)[1].lstrip('/') + except: + partial_path = '' # fallback so that a 404 is issued + builder = HttpRequestBuilder(self.root_uri, self.api_key, self.config, has_subdomains=self.admits_subdomains)\ - .with_path(path)\ + .with_path(partial_path)\ .with_api_key()\ .with_language()\ .with_query_params(params if params is not None else dict())\ @@ -153,7 +177,7 @@ def get_png(self, path, params=None, headers=None): .with_header('Accept', ImageTypeEnum.PNG.mime_type) url, params, headers, proxies = builder.build() try: - resp = requests.get(url, stream=True, params=params, headers=headers, proxies=proxies, + resp = self.http.get(url, stream=True, params=params, headers=headers, proxies=proxies, timeout=self.config['connection']['timeout_secs'], verify=self.config['connection']['verify_ssl_certs']) except requests.exceptions.SSLError as e: @@ -170,8 +194,14 @@ def get_png(self, path, params=None, headers=None): 'API response data') def get_geotiff(self, path, params=None, headers=None): + # check URL fromt the metaimage: if it looks like a complete URL, use that one (I know, it's a hack...) + try: + partial_path = path.split(self.root_uri)[1].lstrip('/') + except: + partial_path = '' # fallback so that a 404 is issued + builder = HttpRequestBuilder(self.root_uri, self.api_key, self.config, has_subdomains=self.admits_subdomains)\ - .with_path(path)\ + .with_path(partial_path)\ .with_api_key()\ .with_language()\ .with_query_params(params if params is not None else dict())\ @@ -179,7 +209,7 @@ def get_geotiff(self, path, params=None, headers=None): .with_header('Accept', ImageTypeEnum.GEOTIFF.mime_type) url, params, headers, proxies = builder.build() try: - resp = requests.get(url, stream=True, params=params, headers=headers, proxies=proxies, + resp = self.http.get(url, stream=True, params=params, headers=headers, proxies=proxies, timeout=self.config['connection']['timeout_secs'], verify=self.config['connection']['verify_ssl_certs']) except requests.exceptions.SSLError as e: @@ -204,7 +234,7 @@ def post(self, path, params=None, data=None, headers=None): .with_headers(headers if headers is not None else dict()) url, params, headers, proxies = builder.build() try: - resp = requests.post(url, params=params, json=data, headers=headers, proxies=proxies, + resp = self.http.post(url, params=params, json=data, headers=headers, proxies=proxies, timeout=self.config['connection']['timeout_secs'], verify=self.config['connection']['verify_ssl_certs']) except requests.exceptions.SSLError as e: @@ -230,7 +260,7 @@ def put(self, path, params=None, data=None, headers=None): .with_headers(headers if headers is not None else dict()) url, params, headers, proxies = builder.build() try: - resp = requests.put(url, params=params, json=data, headers=headers, proxies=proxies, + resp = self.http.put(url, params=params, json=data, headers=headers, proxies=proxies, timeout=self.config['connection']['timeout_secs'], verify=self.config['connection']['verify_ssl_certs']) except requests.exceptions.SSLError as e: @@ -256,7 +286,7 @@ def delete(self, path, params=None, data=None, headers=None): .with_headers(headers if headers is not None else dict()) url, params, headers, proxies = builder.build() try: - resp = requests.delete(url, params=params, json=data, headers=headers, proxies=proxies, + resp = self.http.delete(url, params=params, json=data, headers=headers, proxies=proxies, timeout=self.config['connection']['timeout_secs'], verify=self.config['connection']['verify_ssl_certs']) except requests.exceptions.SSLError as e: diff --git a/pyowm/config.py b/pyowm/config.py index 7e5ea835..714fbf11 100644 --- a/pyowm/config.py +++ b/pyowm/config.py @@ -10,7 +10,8 @@ 'use_ssl': True, 'verify_ssl_certs': True, 'use_proxy': False, - 'timeout_secs': 5 + 'timeout_secs': 5, + 'max_retries': None }, 'proxies': { 'http': 'http://user:pass@host:port', diff --git a/pyowm/constants.py b/pyowm/constants.py index 21deef27..d0de7b7b 100644 --- a/pyowm/constants.py +++ b/pyowm/constants.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -PYOWM_VERSION = (3, 2, 0) +PYOWM_VERSION = (3, 3, 0) AGRO_API_VERSION = (1, 0, 0) AIRPOLLUTION_API_VERSION = (3, 0, 0) ALERT_API_VERSION = (3, 0, 0) diff --git a/pyowm/utils/config.py b/pyowm/utils/config.py index af66fa1d..6dac2ba7 100644 --- a/pyowm/utils/config.py +++ b/pyowm/utils/config.py @@ -10,14 +10,14 @@ def get_config_from(path_to_file): - """Loads configuration data from the supplied file and returns it. + """ + Loads configuration data from the supplied file and returns it. :param path_to_file: path to the configuration file :type path_to_file: str :returns: the configuration `dict` :raises: `ConfigurationNotFoundError` when the supplied filepath is not a regular file; `ConfigurationParseError` when the supplied file cannot be parsed - """ assert path_to_file is not None if not os.path.isfile(path_to_file): @@ -33,21 +33,21 @@ def get_config_from(path_to_file): def get_default_config(): - """Returns the default PyOWM configuration. + """ + Returns the default PyOWM configuration. :returns: the configuration `dict` - """ return DEFAULT_CONFIG def get_default_config_for_subscription_type(name): - """Returns the PyOWM configuration for a specific OWM API Plan subscription type + """ + Returns the PyOWM configuration for a specific OWM API Plan subscription type :param name: name of the subscription type :type name: str :returns: the configuration `dict` - """ assert isinstance(name, str) config = get_default_config() @@ -56,14 +56,14 @@ def get_default_config_for_subscription_type(name): def get_default_config_for_proxy(http_url, https_url): - """Returns the PyOWM configuration to be used behind a proxy server + """ + Returns the PyOWM configuration to be used behind a proxy server :param http_url: URL connection string for HTTP protocol :type http_url: str :param https_url: URL connection string for HTTPS protocol :type https_url: str :returns: the configuration `dict` - """ assert isinstance(http_url, str) assert isinstance(https_url, str) diff --git a/pyowm/weatherapi25/national_weather_alert.py b/pyowm/weatherapi25/national_weather_alert.py new file mode 100644 index 00000000..e4a06e34 --- /dev/null +++ b/pyowm/weatherapi25/national_weather_alert.py @@ -0,0 +1,123 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +from pyowm.utils import formatting +from pyowm.commons import exceptions + + +class NationalWeatherAlert: + """ + A class representing a national weather alert . Alerts are provided by major national + weather warning systems. + + This is the list of such major warning systems: + https://openweathermap.org/api/one-call-api#listsource + + :param sender: the warning systems's name + :type sender: str + :param title: alert event name + :type title: str + :param description: description of the alert + :type description: str + :param start_time: UTC UNIXtime telling when the event is to start + :type start_time: int + :param end_time: UTC UNIXtime telling when the event is to end + :type end_time: int + :param tags: list of labels categorizing the event + :type tags: list of str + + :returns: a *NationalWeatherAlert* instance + :raises: *ValueError* if any parameter has wrong type + """ + def __init__(self, sender, title, description, start_time, end_time, tags=None): + assert sender is not None, 'Event sender name must be specified' + assert title, 'Event title must be specified' + assert description, 'Event description must be specified' + assert start_time, 'Event start time must be specified' + assert end_time, 'Event end time must be specified' + + self.sender = sender + self.title = title + self.description = description + self.start = start_time + self.end = end_time + if tags is None: + self.tags = [] + else: + if not isinstance(tags, list): + raise ValueError('If provided, event tags must be a list of strings') + self.tags = tags + + def start_time(self, timeformat='unix'): + """ + Returns the UTC start time of the weather alert event + + :param timeformat: the format for the time value. May be: + '*unix*' (default) for UNIX time + '*iso*' for ISO8601-formatted string in the format ``YYYY-MM-DD HH:MM:SS+00`` + '*date* for ``datetime.datetime`` object instance + :type timeformat: str + :returns: an int or a str + :raises: ValueError when negative values are provided + + """ + return formatting.timeformat(self.start, timeformat) + + def end_time(self, timeformat='unix'): + """ + Returns the UTC end time of the weather alert event + + :param timeformat: the format for the time value. May be: + '*unix*' (default) for UNIX time + '*iso*' for ISO8601-formatted string in the format ``YYYY-MM-DD HH:MM:SS+00`` + '*date* for ``datetime.datetime`` object instance + :type timeformat: str + :returns: an int or a str + :raises: ValueError when negative values are provided + + """ + return formatting.timeformat(self.end, timeformat) + + @classmethod + def from_dict(cls, the_dict): + """ + Parses a *NationalWeatherAlert* instance out of a data dictionary. Only certain properties of the data dictionary + are used: if these properties are not found or cannot be parsed, an exception is issued. + + :param the_dict: the input dictionary + :type the_dict: `dict` + :returns: a *NationalWeatherAlert* instance or ``None`` if no data is available + :raises: *ParseAPIResponseError* if it is impossible to find or parse the data needed to build the result + + """ + if the_dict is None: + raise exceptions.ParseAPIResponseError('Data is None') + + try: + sender = the_dict['sender_name'] + title = the_dict['event'] + description = the_dict['description'] + start_time = the_dict['start'] + end_time = the_dict['end'] + except KeyError: + raise exceptions.ParseAPIResponseError('Invalid data payload') + tags = the_dict.get('tags', []) + return NationalWeatherAlert(sender, title, description, start_time, end_time, tags) + + def to_dict(self): + """Dumps object to a dictionary + + :returns: a `dict` + """ + return {'sender_name': self.sender, + 'event': self.title, + 'start': self.start, + 'end': self.end, + 'description': self.description, + 'tags': self.tags } + + def __repr__(self): + return "<%s.%s - sender=%s, title=%s, start=%s, end=%s>" % ( + __name__, self.__class__.__name__, + self.sender, self.title, self.start_time('iso'), self.end_time('iso') + ) diff --git a/pyowm/weatherapi25/one_call.py b/pyowm/weatherapi25/one_call.py index 50ab7646..e00b74cd 100644 --- a/pyowm/weatherapi25/one_call.py +++ b/pyowm/weatherapi25/one_call.py @@ -3,6 +3,7 @@ from pyowm.commons import exceptions from pyowm.utils import geo from pyowm.weatherapi25.weather import Weather +from pyowm.weatherapi25.national_weather_alert import NationalWeatherAlert class OneCall: @@ -11,10 +12,12 @@ def __init__(self, lat: Union[int, float], lon: Union[int, float], timezone: str, + timezone_offset: int, current: Weather, - forecast_minutely:Optional[Weather] = None, - forecast_hourly:Optional[Weather] = None, - forecast_daily: Optional[Weather] = None + forecast_minutely: Optional[Weather] = None, + forecast_hourly: Optional[Weather] = None, + forecast_daily: Optional[Weather] = None, + national_weather_alerts: Optional[list] = None ) -> None: geo.assert_is_lat(lat) self.lat = lat @@ -23,6 +26,7 @@ def __init__(self, self.lon = lon self.timezone = timezone + self.timezone_offset = timezone_offset if current is None: raise ValueError("'current' must be set") @@ -30,6 +34,7 @@ def __init__(self, self.forecast_minutely = forecast_minutely self.forecast_hourly = forecast_hourly self.forecast_daily = forecast_daily + self.national_weather_alerts = national_weather_alerts def __repr__(self): return "<%s.%s - lat=%s, lon=%s, retrieval_time=%s>" % ( @@ -92,12 +97,18 @@ def from_dict(cls, the_dict: dict): except KeyError: raise exceptions.ParseAPIResponseError(f"{__name__}: impossible to read weather info from input data") + alerts = None + if 'alerts' in the_dict: + alerts = [NationalWeatherAlert.from_dict(item) for item in the_dict['alerts']] + return OneCall( lat=the_dict.get("lat", None), lon=the_dict.get("lon", None), timezone=the_dict.get("timezone", None), + timezone_offset=the_dict.get("timezone_offset", None) current=current, forecast_minutely=minutely, forecast_hourly=hourly, - forecast_daily=daily + forecast_daily=daily, + national_weather_alerts=alerts ) diff --git a/scripts/generate_city_id_files.py b/scripts/generate_city_id_files.py index 3fa2fd9a..f522d0da 100644 --- a/scripts/generate_city_id_files.py +++ b/scripts/generate_city_id_files.py @@ -1,6 +1,6 @@ #!/usr/bin/env python -import requests, sys, os, codecs, json, gzip, bz2, collections, csv +import requests, sys, os, codecs, json, gzip, bz2, collections, csv, sqlite3, re city_list_url = 'http://bulk.openweathermap.org/sample/city.list.json.gz' @@ -52,6 +52,43 @@ def read_all_cities_into_dict(): return all_cities +URL_REGEX = re.compile("""(http|ftp|https):\/\/([\w_-]+(?:(?:\.[\w_-]+)+))([\w.,@?^=%&:\/~+#-]*[\w@?^=%&\/~+#-])""") + + +def read_all_cities_into_lists(): + print('Reading city data from files ...') + all_cities = [] + with gzip.open(city_list_gz, "rb", "utf-8") as i: + cities = json.loads(i.read()) + for city_dict in cities: + # check for URLs in city details (see https://github.com/csparpa/pyowm/pull/389) + for item in ('name', 'country', 'state'): + item_no_url = URL_REGEX.sub('', city_dict[item]) + if city_dict[item] != item_no_url: + # item contains URL so prompt user for a correction + print( + f'URL detected in entry [ID {city_dict["id"]!r}]' + + f'\n\tKey: {item!r}' + + f'\n\tValue: {city_dict[item]!r}' + + f'\n\tSuggested correction: {item_no_url!r}' + ) + prompt = input("Use suggested correction? (Yes, No, Edit) ").lower() + if prompt.startswith('y'): + city_dict[item] = item_no_url + elif prompt.startswith('e'): + city_dict[item] = input('Enter a correction: ') + + if city_dict['state'] != '': + state = city_dict['state'] + else: + state = None + + t = [city_dict['id'], city_dict['name'], city_dict['country'], state, city_dict['coord']['lat'], city_dict['coord']['lon']] + all_cities.append(t) + print('... done') + return all_cities + + def order_dict_by_city_id(all_cities): print('Ordering city dict by city ID ...') all_cities_ordered = collections.OrderedDict(sorted(all_cities.items())) @@ -63,6 +100,7 @@ def city_to_string(city_id, city_dict): return ','.join([city_dict['name'], str(city_id), str(city_dict['lat']), str(city_dict['lon']), city_dict['country']]) + def split_keyset(cities_dict): print('Splitting keyset of %d city names into 4 subsets based on the initial letter:' % (len(cities_dict),)) print('-> from "a" = ASCII 97 to "f" = ASCII 102') @@ -133,8 +171,8 @@ def bz2_all(outdir): '%s%s115-122.txt.bz2' % (outdir, os.sep)) -if __name__ == '__main__': - target_folder = os.path.abspath(sys.argv[1]) +def generate_city_id_gz_files(target_path='.'): + target_folder = os.path.abspath(target_path) print('Will save output files to folder: %s' % (target_folder,)) print('Job started') download_the_files() @@ -145,3 +183,59 @@ def bz2_all(outdir): bz2_all(target_folder) print('Job finished') + +# SQLite + +def create_db_sqlite(db_path): + with open(db_path, 'w') as _: + pass + sql_schema_statement = ''' + CREATE TABLE IF NOT EXISTS city ( + id integer NOT NULL PRIMARY KEY, + city_id integer NOT NULL, + name text NOT NULL, + country text NOT NULL, + state text, + lat real NOT NULL, + lon real NOT NULL + );''' + + conn = sqlite3.connect(db_path) + cur = conn.cursor() + cur.execute(sql_schema_statement) + conn.commit() + conn.close() + print('Created SQLite empty database') + + +def populate_db_sqlite(db_path, cities_list): + conn = sqlite3.connect(db_path) + cur = conn.cursor() + cur.executemany('INSERT INTO city (city_id, name, country, state, lat, lon) VALUES (?, ?, ?, ?, ?, ?)', cities_list) + conn.commit() + conn.close() + print('Populated SQLite database') + + +def generate_sqlite_db(target_path='.'): + DB_NAME = 'cities.db' + target_folder = os.path.abspath(target_path) + db_path = target_folder + os.path.sep + DB_NAME + print('Will save output SQLite DB to folder: %s' % (target_folder,)) + print('Job started') + download_the_files() + cities = read_all_cities_into_lists() + create_db_sqlite(db_path) + populate_db_sqlite(db_path, cities) + print('Job finished') + print("******** DON'T FORGET TO MANUALLY BZ2 COMPRESS THE DB !!! ******** ") + + +if __name__ == '__main__': + if len(sys.argv) == 2: + target_path = sys.argv[1] + else: + target_path = '.' + #generate_city_id_gz_files(target_path) + generate_sqlite_db() + diff --git a/sphinx/pyowm.agroapi10.rst b/sphinx/pyowm.agroapi10.rst index 2aa27983..0e954379 100644 --- a/sphinx/pyowm.agroapi10.rst +++ b/sphinx/pyowm.agroapi10.rst @@ -64,7 +64,7 @@ pyowm.agroapi10.uris module Module contents --------------- -.. automodule:: pyowm.alertapi30 +.. automodule:: pyowm.agroapi10 :members: :undoc-members: :show-inheritance: diff --git a/sphinx/pyowm.geocodingapi10.rst b/sphinx/pyowm.geocodingapi10.rst index b3810ae2..002d9249 100644 --- a/sphinx/pyowm.geocodingapi10.rst +++ b/sphinx/pyowm.geocodingapi10.rst @@ -11,3 +11,11 @@ pyowm.geocodingapi10.geocoding_manager module :members: :undoc-members: :show-inheritance: + +Module contents +--------------- + +.. automodule:: pyowm.geocodingapi10 + :members: + :undoc-members: + :show-inheritance: \ No newline at end of file diff --git a/sphinx/pyowm.utils.rst b/sphinx/pyowm.utils.rst index 34543094..7343a2a1 100644 --- a/sphinx/pyowm.utils.rst +++ b/sphinx/pyowm.utils.rst @@ -15,7 +15,7 @@ pyowm.utils.config module pyowm.utils.decorators module ----------------------------- -.. automodule:: pyowm.utils.config +.. automodule:: pyowm.utils.decorators :members: :undoc-members: :show-inheritance: diff --git a/sphinx/pyowm.weatherapi25.rst b/sphinx/pyowm.weatherapi25.rst index d53c2c6a..4fbba029 100644 --- a/sphinx/pyowm.weatherapi25.rst +++ b/sphinx/pyowm.weatherapi25.rst @@ -42,6 +42,14 @@ pyowm.weatherapi25.location module :undoc-members: :show-inheritance: +pyowm.weatherapi25.national_weather_alert module +------------------------------------------------ + +.. automodule:: pyowm.weatherapi25.national_weather_alert + :members: + :undoc-members: + :show-inheritance: + pyowm.weatherapi25.observation module ------------------------------------- @@ -50,6 +58,14 @@ pyowm.weatherapi25.observation module :undoc-members: :show-inheritance: +pyowm.weatherapi25.one_call module +---------------------------------- + +.. automodule:: pyowm.weatherapi25.one_call + :members: + :undoc-members: + :show-inheritance: + pyowm.weatherapi25.stationhistory module ---------------------------------------- @@ -58,6 +74,15 @@ pyowm.weatherapi25.stationhistory module :undoc-members: :show-inheritance: +pyowm.weatherapi25.uris module +------------------------------ + +.. automodule:: pyowm.weatherapi25.uris + :members: + :undoc-members: + :show-inheritance: + + pyowm.weatherapi25.weather module --------------------------------- diff --git a/sphinx/v3/code-recipes.md b/sphinx/v3/code-recipes.md index 0a4dec25..fdd7bc14 100644 --- a/sphinx/v3/code-recipes.md +++ b/sphinx/v3/code-recipes.md @@ -89,8 +89,8 @@ owm = OWM('your-api-key', config_dict) ### Language setting The OWM API can be asked to return localized *detailed statuses* for weather data -In PyOWM this means that you can specify a language and you'll retrieve `Weather` objects having the `detailed_status` -field localized in that language. Localization is not provided for `status` field instead, so pay attention to that. +**In PyOWM this means that you can specify a language and you'll retrieve `Weather` objects having the `detailed_status` +field localized in that language. Localization is not provided for `status` field instead, so pay attention to that.** The list of supported languages is given by: ```python @@ -140,67 +140,98 @@ Also you can leverage direct/reverse geocoding The following calls will not result in any OWM API call in the background, so they will only happen locally to your machine. #### Obtain the city ID registry -Use the city ID registry to lookup the ID of a city given its name +As easy as: + ```python from pyowm.owm import OWM owm = OWM('your-api-key') -city_id_registry = owm.city_id_registry() +reg = owm.city_id_registry() ``` #### Get the ID of a city given its name -Don't forget that there is a high probability that your city is not unique in the world, and multiple cities with the same name exist in other countries -Therefore specify toponyms and country 2-letter names separated by comma. Eg: if you search for the British `London` you'll likely multiple results: -you then should also specify the country (`GB`) to narrow the search only to Great Britain. - -Let's search for it: +Once you've got it, use the city ID registry to lookup the ID of a city given its name: ```python -from pyowm.owm import OWM -owm = OWM('your-api-key') -reg = owm.city_id_registry() -list_of_tuples = london = reg.ids_for('London') # lots of results -list_of_tuples = london = reg.ids_for('London', country='GB') # only one: [ (2643743,, 'London, GB') ] -id_of_london_city = list_of_tuples[0][0] +list_of_tuples = london = reg.ids_for('London', matching='like') +print(list_of_tuples) # This will give something like: + # [ (2643743, 'London', 'GB', None, 51.50853, -0.12574), + # (1006984, 'East London', 'ZA', None, -33.015289, 27.911619), + # (1644003, 'Glondong', 'ID', None, -6.7924, 111.891602), + # ... ] ``` -The search here was by default case insensitive: you could have tried +**This call searches for all the places that contain the string `'London'` in their names, in any part of the world**. +This is because the search matching criterion we've used is `like` (this is the default one, if you don't specify it) + +The other available matching criterion is `exact`, which retrieves all places having exactly `'London'` as their name, +in any part of the world (be careful that this operation is case-sensitive !) + +Let's try to search for the same city with an exact match: ```python -list_of_tuples = london = reg.ids_for('london', country='GB') # notice the lowercase -list_of_tuples = london = reg.ids_for('LoNdoN', country='GB') # notice the camelcase +list_of_tuples = london = reg.ids_for('London', matching='exact') +print(list_of_tuples) # This will give something like: + # [ (2643743, 'London', 'GB', None, 51.50853, -0.12574), + # (4119617, 'London', 'US', 'AR', 35.328972, -93.25296), + # (4298960, 'London', 'US', 'KY', 37.128979, -84.08326) + # ... ] ``` -and would get the very same results as above. +As you can see, all results are exactly named `'London'`. + +All the above searches give you back a list of tuples: each tuple is in the format `(city_id, name, country, state, lat, lon)` (fields as +self-explanatory). + + +### City disambiguation +As you might have guessed, there is a high probability that your city is not unique in the world, and multiple cities with the same name exist in other countries +Therefore: whenever you search for a specific city in a specific country then also pass in the 2-letter country name and - even further - also specify a 2-letter state name if you're searching for places in the United States. + +Eg: if you search for the British `London` you'll get multiple results. You then should also specify the country (`GB`) in order to narrow the search only to Great Britain. + +Let's search for it: -#### Get the IDs of cities whose name contain a specific string -In order to find all cities with names having your string as a substring you need to use the optional parameter `matching='like'` +```python +from pyowm.owm import OWM +owm = OWM('your-api-key') +reg = owm.city_id_registry() +list_of_tuples = reg.ids_for('London', matching='exact') # lots of results, spread all over the world +list_of_tuples = reg.ids_for('London', country='GB', matching='exact') # only one: [(2643743, 'London', 'GB', None, 51.50853, -0.12574)] +london_gb = list_of_tuples[0] +id_of_lonfon_gb = london_gb[0] # ID of London, GB +``` -In example, let's find IDs for all British cities having the string `london` in their names: +Whenever searching cities in the US, you'd better also specify the relevant US-state. +For instance, `'Ontario'` is a city in Canada and multiple aliases exist in different US-states: ```python from pyowm.owm import OWM owm = OWM('your-api-key') reg = owm.city_id_registry() -list_of_tuples = reg.ids_for('london', country='GB', matching='like') # We'll get [(2643741, 'City of London', 'GB'), - # (2648110, 'Greater London', 'GB'), - # (7535661, 'London Borough of Harrow', 'GB'), - # (2643743, 'London', 'GB'), - # (2643734, 'Londonderry County Borough', 'GB')] + +# All Ontario cities in the uS +ontarios_in_us = reg.ids_for('Ontario', country='US', matching='exact') # five results + +# Ontario in Canade +ontario_in_canada = reg.ids_for('Ontario', country='CA', matching='exact') # one result: [(6093943, 'Ontario', 'CA', None, 49.250141, -84.499832)] + +# Ontario in the state of New York +ontario_in_ny = reg.ids_for('Ontario', country='US', state='NY', matching='exact') # one result: [(5129887, 'Ontario', 'US', 'NY', 43.220901, -77.283043)] ``` #### Get geographic coordinates of a city given its name Just use call `locations_for` on the registry: this will give you a `Location` object containing lat & lon -Let's find geocoords for Moscow (Russia): +Let's find geocoords for Tokyo (Japan): ```python from pyowm.owm import OWM owm = OWM('your-api-key') reg = owm.city_id_registry() -list_of_locations = reg.locations_for('moscow', country='RU') -moscow = list_of_locations[0] -lat = moscow.lat # 55.75222 -lon = moscow.lon # 37.615555 +list_of_locations = reg.locations_for('Tokyo', country='JP', matching='exact') +tokyo = list_of_locations[0] +lat = tokyo.lat # 35.689499 +lon = tokyo.lon # 139.691711 ``` #### Get GeoJSON geometry (point) for a city given its name @@ -212,7 +243,7 @@ This means, for example, that you can get a `Point` geometry using the registry. from pyowm.owm import OWM owm = OWM('your-api-key') reg = owm.city_id_registry() -list_of_geopoints = reg.geopoints_for('rome') +list_of_geopoints = reg.geopoints_for('Rome', matching='exact') ``` ### Direct/reverse geocoding @@ -281,7 +312,7 @@ list_of_locations = mgr.reverse_geocode(lat, lon) # list contains: City of Lond With the OneCall Api you can get the current weather, hourly forecast for the next 48 hours and the daily forecast for the next seven days in one call. -One Call objects can be thought of as datasets that "photograph" of observed and forecasted weather data for a location: such photos are given for a specific timestamp. +One Call objects can be thought of as datasets that "photograph" observed and forecasted weather for a location: such photos are given for a specific timestamp. It is possible to get: - current OneCall data: the "photo" given for today) @@ -353,6 +384,25 @@ one_call.current.temperature() # Eg.: 74.07 (deg F) one_call.forecast_hourly # empty because it was excluded from the request ``` +#### Checking available National Weather Alerts for a location +Many countries have early warning systems in place to notify about upcoming severe weather events/conditions. +Each alert has a title, a description, start/end timestamps and is tagged with labels. +You can check if any national alert has been issued for a specific location this way: + +```python +from pyowm.owm import OWM +owm = OWM('your-api-key') +mgr = owm.weather_manager() +one_call = mgr.one_call(lat=52.5244, lon=13.4105) +national_weather_alerts = one_call. national_weather_alerts + +for alert in national_weather_alerts: + alert.sender # issuing national authority + alert.title # brief description + alert.description # long description + alert.start_time() # start time in UNIX epoch + alert.end_time(timeformat='ISO') # end time in ISO format +``` ### Historical OneCall data diff --git a/sphinx/v3/pyowm-configuration-description.md b/sphinx/v3/pyowm-configuration-description.md index 58d92335..becc91ab 100644 --- a/sphinx/v3/pyowm-configuration-description.md +++ b/sphinx/v3/pyowm-configuration-description.md @@ -16,9 +16,10 @@ The config dict is formatted as follows: "language": , "connection": { "use_ssl": - "verify_ssl_certs": >, + "verify_ssl_certs": , "use_proxy": , - "timeout_secs": + "timeout_secs": , + "max_retries": | }, "proxies": { "http": , @@ -36,6 +37,7 @@ Here are the keys: * `verify_ssl_certs`: speaks by itself.. * `use_proxy`: whether to use a proxy server or not (useful if you're eg. in a corporate network). HTTP and SOCKS5 proxies are allowed * `timeout_secs`: after how many seconds the API calls should be timeouted + * `max_retries`: how many times PyOWM should retry to call the API if it responds with an error or timeouts. Defaults to `None`, which means: call forever. * `proxies` (this sub-dict is ignored if `use_proxy == False`) * `http`: the HTTP URL of the proxy server * `https`: the HTTPS/SOCKS5 URL of the proxy server diff --git a/tests/integration/agroapi10/test_integration_satellite_imagery_search.py b/tests/integration/agroapi10/test_integration_satellite_imagery_search.py index 52786800..e48a6435 100644 --- a/tests/integration/agroapi10/test_integration_satellite_imagery_search.py +++ b/tests/integration/agroapi10/test_integration_satellite_imagery_search.py @@ -97,7 +97,6 @@ def test_detailed_search(self): PresetEnum.TRUE_COLOR, None, None, SatelliteEnum.SENTINEL_2.symbol, None, 5, 90, None) self.assertIsInstance(result_set, list) - self.assertEqual(len(result_set), 30) self.assertTrue(all([isinstance(i, MetaImage) and i.preset == PresetEnum.TRUE_COLOR and i.image_type == ImageTypeEnum.PNG and diff --git a/tests/integration/commons/test_cityidregistry.py b/tests/integration/commons/test_cityidregistry.py new file mode 100644 index 00000000..35e1dfcf --- /dev/null +++ b/tests/integration/commons/test_cityidregistry.py @@ -0,0 +1,136 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +import unittest +from pyowm.commons.cityidregistry import CityIDRegistry +from pyowm.utils.geo import Point +from pyowm.weatherapi25.location import Location + + +class TestCityIDRegistryReadsFS(unittest.TestCase): + + _instance = CityIDRegistry.get_instance() + + def _assertLocationsEqual(self, loc1, loc2): + self.assertEqual(loc1.id, loc2.id) + self.assertEqual(loc1.name, loc2.name) + self.assertEqual(loc1.lat, loc2.lat) + self.assertEqual(loc1.lon, loc2.lon) + self.assertEqual(loc1.country, loc2.country) + + def _assertGeopointsEqual(self, point1, point2): + self.assertIsInstance(point1, Point) + self.assertIsInstance(point2, Point) + self.assertEqual(point1.to_dict(), point2.to_dict()) + + def test_ids_for(self): + # No matches + self.assertEqual([], self._instance.ids_for('aaaaaaaaaa', matching="exact")) + + # One match + result = self._instance.ids_for("Milano", country="IT", matching="exact") + self.assertEqual(1, len(result)) + + # Multiple matches + result = self._instance.ids_for("Bologna", country="IT", matching="exact") + self.assertEqual(3, len(result)) + + def test_ids_for_fails_with_malformed_inputs(self): + self.assertEqual([], self._instance.ids_for(None)) + self.assertRaises(ValueError, CityIDRegistry.ids_for, self._instance, 'name', 'country', None, '123abc') + self.assertRaises(ValueError, CityIDRegistry.ids_for, self._instance, 'name', 'US', 'state', '123abc') + self.assertRaises(ValueError, CityIDRegistry.ids_for, self._instance, 'name', None, 'CA', '123abc') + self.assertRaises(ValueError, CityIDRegistry.ids_for, self._instance, 'name', 'US', 'CA', '123abc') + + def test_ids_for_matching_criteria(self): + # look for an empty name + result = self._instance.ids_for("") + self.assertEqual(0, len(result)) + + # exact + result = self._instance.ids_for("gollar", matching='exact') + self.assertEqual(0, len(result)) + + result = self._instance.ids_for("Gollar", matching='exact') + self.assertEqual(1, len(result)) + self.assertEqual((18007, 'Gollar', 'IR', None, 37.383331, 46.25), result[0]) + + # like + result = self._instance.ids_for("abbans", matching='like') + self.assertEqual(2, len(result)) + self.assertEqual((3038800, 'Abbans-Dessus', 'FR', None, 47.120548, 5.88188), result[0]) + self.assertEqual((6452202, 'Abbans-Dessus', 'FR', None, 47.116669, 5.88333), result[1]) + + result = self._instance.ids_for("Abbans", matching='like') + self.assertEqual(2, len(result)) + + def test_ids_for_same_name_different_countries(self): + result = self._instance.ids_for("Ontario", matching='exact') + self.assertEqual(6, len(result)) + + result = self._instance.ids_for("Ontario", country="US", matching='exact') + self.assertEqual(5, len(result)) + + result = self._instance.ids_for("Ontario", country="CA", matching='exact') + self.assertEqual(1, len(result)) + + result = self._instance.ids_for("Ontario", country="US", state="NY", matching='exact') + self.assertEqual(1, len(result)) + + def test_ids_for_with_commas_in_city_names(self): + result = self._instance.ids_for("Pitcairn, Henderson, Ducie and Oeno Islands", matching='exact') + self.assertEqual(1, len(result)) + self.assertEqual((4030699, 'Pitcairn, Henderson, Ducie and Oeno Islands', 'PN', None, -25.066669, -130.100006), result[0]) + + def test_locations_for(self): + # No matches + self.assertEqual([], self._instance.locations_for('aaaaaaaaaa')) + + # One match + expected = Location('Milano', 9.19199, 45.464161, 6542283, country='IT') + result = self._instance.locations_for("Milano", country="IT", matching="exact") + self.assertEqual(1, len(result)) + self._assertLocationsEqual(expected, result[0]) + + # Multiple matches + expected1 = Location('Bologna', 11.43333, 44.466671, 3181927, country='IT') + expected2 = Location('Bologna', 11.33875, 44.493809, 3181928, country='IT') + expected3 = Location('Bologna', 11.35041, 44.506569, 6541998, country='IT') + result = self._instance.locations_for("Bologna", country="IT", matching="exact") + self.assertEqual(3, len(result)) + self._assertLocationsEqual(expected1, result[0]) + self._assertLocationsEqual(expected2, result[1]) + self._assertLocationsEqual(expected3, result[2]) + + def test_locations_for_fails_with_malformed_inputs(self): + self.assertEqual([], self._instance.locations_for(None)) + self.assertRaises(ValueError, CityIDRegistry.locations_for, self._instance, 'name', 'country', None, '123abc') + self.assertRaises(ValueError, CityIDRegistry.locations_for, self._instance, 'name', 'US', 'state', '123abc') + self.assertRaises(ValueError, CityIDRegistry.locations_for, self._instance, 'name', None, 'CA', '123abc') + self.assertRaises(ValueError, CityIDRegistry.locations_for, self._instance, 'name', 'US', 'CA', '123abc') + + def test_geopoints_for(self): + # No matches + self.assertEqual([], self._instance.geopoints_for('aaaaaaaaaa')) + + # One match + expected = Location('Milano', 9.19199, 45.464161, 6542283, country='IT').to_geopoint() + result = self._instance.geopoints_for("Milano", country="IT", matching="exact") + self.assertEqual(1, len(result)) + self._assertGeopointsEqual(expected, result[0]) + + # Multiple matches + expected1 = Location('Bologna', 11.43333, 44.466671, 3181927, country='IT').to_geopoint() + expected2 = Location('Bologna', 11.33875, 44.493809, 3181928, country='IT').to_geopoint() + expected3 = Location('Bologna', 11.35041, 44.506569, 6541998, country='IT').to_geopoint() + result = self._instance.geopoints_for("Bologna", country="IT", matching="exact") + self.assertEqual(3, len(result)) + self._assertGeopointsEqual(expected1, result[0]) + self._assertGeopointsEqual(expected2, result[1]) + self._assertGeopointsEqual(expected3, result[2]) + + def test_repr(self): + print(self._instance) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/integration/utils/test_config.json b/tests/integration/utils/test_config.json index 8a48a70d..bfc8b569 100644 --- a/tests/integration/utils/test_config.json +++ b/tests/integration/utils/test_config.json @@ -5,6 +5,7 @@ "connection": { "use_ssl": true, "verify_ssl_certs": true, - "timeout_secs": 1 + "timeout_secs": 1, + "max_retries": null } } diff --git a/tests/integration/utils/test_default_config.py b/tests/integration/utils/test_default_config.py index b6d5e383..cdfb5e5f 100644 --- a/tests/integration/utils/test_default_config.py +++ b/tests/integration/utils/test_default_config.py @@ -35,6 +35,9 @@ def test_default_config_is_complete(self): self.assertTrue('timeout_secs' in connection) self.assertEqual(5, connection['timeout_secs']) + self.assertTrue('max_retries' in connection) + self.assertEqual(None, connection['max_retries']) + # proxies is a sub-dict, check its keys self.assertTrue('proxies' in DEFAULT_CONFIG) proxies = DEFAULT_CONFIG['proxies'] diff --git a/tests/integration/weatherapi25/097-102.txt b/tests/integration/weatherapi25/097-102.txt deleted file mode 100644 index caf21d46..00000000 --- a/tests/integration/weatherapi25/097-102.txt +++ /dev/null @@ -1,6 +0,0 @@ -dongditou,1812600,39.261391,117.368332,CN -dongdu,1812597,35.849998,117.699997,CN -dongel,747912,40.693600,29.941540,TR -dongen,2756723,51.626671,4.938890,NL -dongerying,1812594,39.957500,117.279167,CN -donges,3021093,47.318241,-2.075380,FR \ No newline at end of file diff --git a/tests/integration/weatherapi25/test_cityidregistry_reads_fs.py b/tests/integration/weatherapi25/test_cityidregistry_reads_fs.py deleted file mode 100644 index 1dbe3793..00000000 --- a/tests/integration/weatherapi25/test_cityidregistry_reads_fs.py +++ /dev/null @@ -1,81 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -import unittest -from os import sep -from pyowm.commons.cityidregistry import CityIDRegistry -from pyowm.weatherapi25.location import Location - - -class TestCityIDRegistryReadsFS(unittest.TestCase): - - _prefix = 'cityids'+sep - _instance = CityIDRegistry(_prefix+'%03d-%03d.txt.bz2') - - def test_assess_subfile_from(self): - self.assertEqual(self._instance._assess_subfile_from('b-city'), - self._prefix+'097-102.txt.bz2') - self.assertEqual(self._instance._assess_subfile_from('h-city'), - self._prefix+'103-108.txt.bz2') - self.assertEqual(self._instance._assess_subfile_from('n-city'), - self._prefix+'109-114.txt.bz2') - self.assertEqual(self._instance._assess_subfile_from('t-city'), - self._prefix+'115-122.txt.bz2') - self.assertRaises(ValueError, CityIDRegistry._assess_subfile_from, - self._instance, '123abc') - self.assertRaises(ValueError, CityIDRegistry._assess_subfile_from, - self._instance, '{abc') - - def test_lookup_line_by_city_name(self): - expected = u'Dongen,2756723,51.626671,4.93889,NL' - self.assertEqual(expected, - self._instance._lookup_line_by_city_name('dongen')) - self.assertTrue(self._instance. \ - _lookup_line_by_city_name('aaaaaaaa') is None) - - def test_ids_for(self): - self.assertEqual([(2756723, 'Dongen', 'NL')], self._instance.ids_for('dongen')) - self.assertEqual([], self._instance.ids_for('aaaaaaaaaa')) - - def test_ids_for_fails_with_malformed_inputs(self): - self.assertRaises(ValueError, CityIDRegistry.ids_for, self._instance, '123abc') - - def test_locations_for(self): - expected = Location('Dongen', 4.938890, 51.626671, 2756723, 'NL') - result_list = self._instance.locations_for('dongen') - self.assertEqual(1, len(result_list)) - result = result_list[0] - self.assertEqual(result.name, expected.name) - self.assertEqual(result.country, expected.country) - self.assertEqual(result.id, expected.id) - self.assertEqual(result.lat, expected.lat) - self.assertEqual(result.lon, expected.lon) - self.assertEqual([], self._instance.locations_for('aaaaaaaaaa')) - - def test_locations_for_fails_with_malformed_inputs(self): - self.assertRaises(ValueError, CityIDRegistry.locations_for, self._instance, '123abc') - - def test_ids_for_more_cases(self): - result = self._instance.ids_for("bologna", matching='exact') - self.assertEqual(0, len(result)) - - result = self._instance.ids_for("Abbans-Dessus") - self.assertEqual(2, len(result)) - self.assertTrue((3038800, 'Abbans-Dessus', 'FR') in result) - self.assertTrue((6452202, 'Abbans-Dessus', 'FR') in result) - - result = self._instance.ids_for("Dessus", matching='like') - self.assertEqual(6, len(result)) - - def test_locations_for_more_cases(self): - expected1 = Location('Abbans-Dessus', 5.88188, 47.120548, 3038800, 'FR') - expected2 = Location('Abbans-Dessus', 5.88333, 47.116669, 6452202, 'FR') - result = self._instance.locations_for("Abbans-Dessus") - self.assertEqual(2, len(result)) - for l in result: - self.assertTrue(isinstance(l, Location)) - self.assertTrue(l.id in [expected1.id, expected2.id]) - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/integration/weatherapi25/test_integration.py b/tests/integration/weatherapi25/test_integration.py index 94923918..f239adab 100644 --- a/tests/integration/weatherapi25/test_integration.py +++ b/tests/integration/weatherapi25/test_integration.py @@ -8,6 +8,7 @@ from pyowm import owm from pyowm.weatherapi25.one_call import OneCall from pyowm.weatherapi25.weather import Weather +from pyowm.weatherapi25.national_weather_alert import NationalWeatherAlert class IntegrationTestsWebAPI25(unittest.TestCase): @@ -436,12 +437,16 @@ def test_one_call(self): self.assertEqual(8, len(result.forecast_daily)) for i, weather in enumerate(result.forecast_daily): self.assertTrue(isinstance(weather, Weather), f"entry {i} of forecast_hourly is invalid") + if result.national_weather_alerts is not None: + self.assertTrue(isinstance(result.national_weather_alerts, list)) + for alert in result.national_weather_alerts: + self.assertTrue(isinstance(alert, NationalWeatherAlert)) def test_one_call_historical(self): result = self.__owm.one_call_history(lat=48.8576, lon=2.3377) self.assertTrue(isinstance(result, OneCall)) - self.assertEqual(48.86, result.lat) - self.assertEqual(2.34, result.lon) + self.assertEqual(48.8576, result.lat) + self.assertEqual(2.3377, result.lon) self.assertEqual("Europe/Paris", result.timezone) self.assertTrue(isinstance(result.current, Weather)) if result.forecast_hourly is not None: diff --git a/tests/unit/agroapi10/test_agro_manager.py b/tests/unit/agroapi10/test_agro_manager.py index f7d36e9d..b714cb5d 100644 --- a/tests/unit/agroapi10/test_agro_manager.py +++ b/tests/unit/agroapi10/test_agro_manager.py @@ -156,6 +156,8 @@ class TestAgroManager(unittest.TestCase): def factory(self, _kls): sm = AgroManager('APIKey', DEFAULT_CONFIG) sm.http_client = _kls('APIKey', DEFAULT_CONFIG, 'fake-root.com') + sm.geotiff_downloader_http_client = _kls('APIKey', DEFAULT_CONFIG, 'fake-data-root.com') + sm.png_downloader_http_client = _kls('APIKey', DEFAULT_CONFIG, 'fake-image-root.com') return sm def test_instantiation_with_wrong_params(self): diff --git a/tests/unit/commons/test_cityidregistry.py b/tests/unit/commons/test_cityidregistry.py deleted file mode 100644 index b033fde9..00000000 --- a/tests/unit/commons/test_cityidregistry.py +++ /dev/null @@ -1,541 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -import unittest -from io import StringIO -from itertools import chain -from pyowm.commons.cityidregistry import CityIDRegistry -from pyowm.weatherapi25.location import Location -from pyowm.utils.geo import Point - - -class TestCityIDRegistry(unittest.TestCase): - - _instance = CityIDRegistry('%03d-%03d.txt') - _test_file_contents = """dongditou,1812600,39.261391,117.368332,CN -dongdu,1812597,35.849998,117.699997,CN -dongel,747912,40.693600,29.941540,TR -dongen,2756723,51.626671,4.938890,NL -dongerying,1812594,39.957500,117.279167,CN -donges,3021093,47.318241,-2.075380,FR""" - _test_file_contents_with_homonymies = """Abasolo,3533505,24.066669,-98.366669,MX -Abasolo,4019867,25.950001,-100.400002,MX -Abasolo,4019869,20.450001,-101.51667,MX -Abbans-Dessus,3038800,47.120548,5.88188,FR -Abbans-Dessus,6452202,47.116669,5.88333,FR -Abbeville,3038789,50.099998,1.83333,FR -Abbeville,4178992,31.992121,-83.306824,US -Abbeville,4314295,29.974649,-92.134293,US -Abbeville,4568985,34.178169,-82.379013,US -Abbeville,4829449,31.57184,-85.250488,US -Bologna,2829449,30.57184,-83.250488,IT""" - _test_file_contents_with_commas_in_names = """Thalassery,1254780,11.75,75.533333,IN -Thale, Stadt,6550950,51.7528,11.058,DE -Pitcairn,5206361,40.403118,-79.778099,PA -Pitcairn, Henderson, Ducie and Oeno Islands,4030699,-25.066669,-130.100006,PN""" - - test_filelines = [ - 'Londinieres,2997784,49.831871,1.40232,FR\n', - 'Londoko,2020707,49.033329,131.983337,RU\n', - 'London Borough of Harrow,7535661,51.566669,-0.33333,GB\n', - 'London Village,4030939,1.98487,-157.475021,KI\n', - 'London,2643743,51.50853,-0.12574,GB\n', - 'London,4119617,35.328972,-93.25296,US\n'] - - # mocked functions and helpers - - def _mock_get_lines(self, filename): - return StringIO(self._test_file_contents).readlines() - - def _mock_get_lines_with_homonymies(self, filename): - return StringIO(self._test_file_contents_with_homonymies).readlines() - - def _mock_get_all_lines(self): - return StringIO(self._test_file_contents_with_homonymies).readlines() - - def _mock_test_file_contents_with_commas_in_names(self, filename): - return StringIO(self._test_file_contents_with_commas_in_names).readlines() - - def _assertLocationsEqual(self, loc1, loc2): - self.assertEqual(loc1.id, loc2.id) - self.assertEqual(loc1.name, loc2.name) - self.assertEqual(loc1.lat, loc2.lat) - self.assertEqual(loc1.lon, loc2.lon) - self.assertEqual(loc1.country, loc2.country) - - def _assertGeopointsEqual(self, point1, point2): - self.assertIsInstance(point1, Point) - self.assertIsInstance(point2, Point) - self.assertEqual(point1.to_dict(), point2.to_dict()) - - def test_get_instance(self): - result = CityIDRegistry.get_instance() - self.assertTrue(isinstance(result, CityIDRegistry)) - - # tests for helper functions - - def test_assess_subfile_from(self): - self.assertEqual(self._instance._assess_subfile_from('b-city'), - '097-102.txt') - self.assertEqual(self._instance._assess_subfile_from('h-city'), - '103-108.txt') - self.assertEqual(self._instance._assess_subfile_from('n-city'), - '109-114.txt') - self.assertEqual(self._instance._assess_subfile_from('t-city'), - '115-122.txt') - self.assertRaises(ValueError, CityIDRegistry._assess_subfile_from, - self._instance, '123abc') - self.assertRaises(ValueError, CityIDRegistry._assess_subfile_from, - self._instance, '{abc') - - def test_lookup_line_by_city_name(self): - ref_to_original = CityIDRegistry._get_lines - CityIDRegistry._get_lines = self._mock_get_lines - expected = 'dongen,2756723,51.626671,4.938890,NL' - result_1 = self._instance._lookup_line_by_city_name('dongen') - result_2 = self._instance._lookup_line_by_city_name('aaaaaaaa') - CityIDRegistry._get_lines = ref_to_original - self.assertEqual(expected, result_1) - self.assertTrue(result_2 is None) - - def test_city_name_matches(self): - self.assertTrue(self._instance._city_name_matches( - 'test', 'test', 'exact')) - self.assertFalse(self._instance._city_name_matches( - 'Test', 'test', 'exact')) - self.assertFalse(self._instance._city_name_matches( - 'foo', 'bar', 'exact')) - self.assertTrue(self._instance._city_name_matches( - 'tEsT mE', 'teST ME', 'nocase')) - self.assertFalse(self._instance._city_name_matches( - 'foo', 'bar', 'nocase')) - self.assertTrue(self._instance._city_name_matches( - 'test', 'test me', 'like')) - self.assertFalse(self._instance._city_name_matches( - 'foo', 'bar', 'like')) - self.assertTrue(self._instance._city_name_matches( - 'Me', 'test me', 'like')) - self.assertTrue(self._instance._city_name_matches( - 'Test', 'test me', 'startswith')) - self.assertFalse(self._instance._city_name_matches( - 'me', 'test me', 'startswith')) - self.assertFalse(self._instance._city_name_matches( - 'foo', 'bar', 'startswith')) - - # tests for IDs retrieval - - def test_get_lines(self): - instance = CityIDRegistry.get_instance() - result = instance._get_lines('cityids/097-102.txt.bz2') - self.assertIsInstance(result, map) - self.assertTrue(all(map(lambda s: isinstance(s, str), result))) - - def test_get_all_lines(self): - instance = CityIDRegistry.get_instance() - result = instance._get_all_lines() - self.assertIsInstance(result, list) - - def test_match_line(self): - instance = CityIDRegistry('test') - - # no matches - result = instance._match_line('blabla', self.test_filelines) - self.assertIsNone(result) - - # single exact matches - result = instance._match_line('Londoko', self.test_filelines) - self.assertEqual('Londoko,2020707,49.033329,131.983337,RU', result) - result = instance._match_line('London Borough of Harrow', self.test_filelines) - self.assertEqual('London Borough of Harrow,7535661,51.566669,-0.33333,GB', - result) - - # single match with different casing - result1 = instance._match_line('LONDOKO', self.test_filelines) - result2 = instance._match_line('londoko', self.test_filelines) - self.assertEqual(result1, result2) - self.assertEqual('Londoko,2020707,49.033329,131.983337,RU', result1) - result3 = instance._match_line('London borough of harrow', self.test_filelines) - result4 = instance._match_line('london BOROUGH of Harrow', self.test_filelines) - self.assertEqual(result3, result4) - self.assertEqual('London Borough of Harrow,7535661,51.566669,-0.33333,GB', - result3) - # homonymies - result = instance._match_line('London', self.test_filelines) - self.assertEqual('London,2643743,51.50853,-0.12574,GB', result) - - def test_ids_for(self): - ref_to_original = CityIDRegistry._get_lines - CityIDRegistry._get_lines = self._mock_get_lines_with_homonymies - - # No matches - result = self._instance.ids_for('aaaaaaaaaa') - self.assertEqual(0, len(result)) - - # One match - result = self._instance.ids_for("Bologna") - self.assertEqual(1, len(result)) - self.assertEqual((2829449, 'Bologna', 'IT'), result[0]) - - # Multiple matches - result = self._instance.ids_for("Abbans-Dessus") - self.assertEqual(2, len(result)) - self.assertTrue((3038800, 'Abbans-Dessus', 'FR') in result) - self.assertTrue((6452202, 'Abbans-Dessus', 'FR') in result) - - CityIDRegistry._get_lines = ref_to_original - - def test_ids_for_with_wrong_input_values(self): - self.assertRaises(ValueError, - CityIDRegistry.ids_for, self._instance, - "bologna", matching='xyz') - self.assertRaises(ValueError, - CityIDRegistry.ids_for, self._instance, - "bologna", country='superlongcountry') - - def test_ids_for_matching_criteria(self): - original_get_lines = CityIDRegistry._get_lines - original_get_all_lines = CityIDRegistry._get_all_lines - CityIDRegistry._get_lines = self._mock_get_lines_with_homonymies - CityIDRegistry._get_all_lines = self._mock_get_all_lines - - # look for an empty name - result = self._instance.ids_for("") - self.assertEqual(0, len(result)) - - # case sensitive - result = self._instance.ids_for("bologna", matching='exact') - self.assertEqual(0, len(result)) - - result = self._instance.ids_for("Bologna", matching='exact') - self.assertEqual(1, len(result)) - self.assertEqual((2829449, 'Bologna', 'IT'), result[0]) - - # case insensitive - result = self._instance.ids_for("bologna", matching='nocase') - self.assertEqual(1, len(result)) - self.assertEqual((2829449, 'Bologna', 'IT'), result[0]) - - result = self._instance.ids_for("Bologna", matching='nocase') - self.assertEqual(1, len(result)) - self.assertEqual((2829449, 'Bologna', 'IT'), result[0]) - - # like - result = self._instance.ids_for("abbans", matching='like') - self.assertEqual(2, len(result)) - self.assertTrue((3038800, 'Abbans-Dessus', 'FR') in result) - self.assertTrue((6452202, 'Abbans-Dessus', 'FR') in result) - - result = self._instance.ids_for("Dessus", matching='like') - self.assertEqual(2, len(result)) - self.assertTrue((3038800, 'Abbans-Dessus', 'FR') in result) - self.assertTrue((6452202, 'Abbans-Dessus', 'FR') in result) - - # startswith - result = self._instance.ids_for("abban", matching='startswith') - self.assertEqual(2, len(result)) - self.assertTrue((3038800, 'Abbans-Dessus', 'FR') in result) - self.assertTrue((6452202, 'Abbans-Dessus', 'FR') in result) - - result = self._instance.ids_for("dessus", matching='startswith') - self.assertEqual(0, len(result)) - - result = self._instance.ids_for("abbe", matching='startswith') - self.assertEqual(5, len(result)) - self.assertTrue((3038789, 'Abbeville', 'FR') in result) - self.assertTrue((4568985, 'Abbeville', 'US') in result) - - result = self._instance.ids_for("ville", matching='startswith') - self.assertEqual(0, len(result)) - - CityIDRegistry._get_lines = original_get_lines - CityIDRegistry._get_all_lines = original_get_all_lines - - def test_ids_for_restricted_to_country(self): - ref_to_original = CityIDRegistry._get_lines - CityIDRegistry._get_lines = self._mock_get_lines_with_homonymies - - result = self._instance.ids_for("Abbeville", country='JP') - self.assertEqual(0, len(result)) - - result = self._instance.ids_for("Abbeville", country='US') - self.assertEqual(4, len(result)) - self.assertTrue((4178992, 'Abbeville', 'US') in result) - self.assertTrue((4314295, 'Abbeville', 'US') in result) - self.assertTrue((4568985, 'Abbeville', 'US') in result) - self.assertTrue((4829449, 'Abbeville', 'US') in result) - - result = self._instance.ids_for("Abbeville", country='FR') - self.assertEqual(1, len(result)) - self.assertTrue((3038789, 'Abbeville', 'FR') in result) - - CityIDRegistry._get_lines = ref_to_original - - def test_ids_for_with_commas_in_city_names(self): - ref_to_original = CityIDRegistry._get_lines - CityIDRegistry._get_lines = self._mock_test_file_contents_with_commas_in_names - - result = self._instance.ids_for("Thale, Stadt") - self.assertEqual(1, len(result)) - self.assertTrue((6550950, 'Thale, Stadt', 'DE') in result) - - result = self._instance.ids_for("Pitcairn, Henderson, Ducie and Oeno Islands") - self.assertEqual(1, len(result)) - self.assertTrue((4030699, 'Pitcairn, Henderson, Ducie and Oeno Islands', 'PN') in result) - - CityIDRegistry._get_lines = ref_to_original - - def test_ids_for_with_commas_in_city_names_like(self): - ref_to_original = CityIDRegistry._get_lines - CityIDRegistry._get_lines = self._mock_test_file_contents_with_commas_in_names - - result = self._instance.ids_for("Pitca", matching='like') - self.assertTrue((5206361, 'Pitcairn', 'PA') in result) - self.assertTrue((4030699, 'Pitcairn, Henderson, Ducie and Oeno Islands', 'PN') in result) - - CityIDRegistry._get_lines = ref_to_original - - def test_ids_for_with_commas_in_city_names_like_country(self): - ref_to_original = CityIDRegistry._get_lines - CityIDRegistry._get_lines = self._mock_test_file_contents_with_commas_in_names - - result = self._instance.ids_for("Pitc", country='PA',matching='like') - self.assertTrue((5206361, 'Pitcairn', 'PA') in result) - - result = self._instance.ids_for("Ducie and Oeno", country='PN',matching='like') - self.assertTrue((4030699, 'Pitcairn, Henderson, Ducie and Oeno Islands', 'PN') in result) - - CityIDRegistry._get_lines = ref_to_original - - def test_ids_for_with_commas_in_city_names_startswith(self): - ref_to_original = CityIDRegistry._get_lines - CityIDRegistry._get_lines = self._mock_test_file_contents_with_commas_in_names - - result = self._instance.ids_for("Pitc", matching='startswith') - self.assertEqual(2, len(result)) - self.assertTrue((5206361, 'Pitcairn', 'PA') in result) - self.assertTrue((4030699, 'Pitcairn, Henderson, Ducie and Oeno Islands', 'PN') in result) - - CityIDRegistry._get_lines = ref_to_original - - def test_ids_for_with_commas_in_city_names_startswith_country(self): - ref_to_original = CityIDRegistry._get_lines - CityIDRegistry._get_lines = self._mock_test_file_contents_with_commas_in_names - - result = self._instance.ids_for("Pitc", country="PA", matching='startswith') - self.assertEqual(1, len(result)) - self.assertTrue((5206361, 'Pitcairn', 'PA') in result) - self.assertFalse((4030699, 'Pitcairn, Henderson, Ducie and Oeno Islands', 'PN') in result) - - CityIDRegistry._get_lines = ref_to_original - - # tests for locations retrieval - - def test_locations_for(self): - ref_to_original = CityIDRegistry._get_lines - CityIDRegistry._get_lines = self._mock_get_lines_with_homonymies - - # No matches - result = self._instance.locations_for('aaaaaaaaaa') - self.assertEqual(0, len(result)) - - # One match - expected = Location('Bologna', -83.250488, 30.57184, 2829449, 'IT') - result = self._instance.locations_for("Bologna") - self.assertEqual(1, len(result)) - self._assertLocationsEqual(expected, result[0]) - - # Multiple matches - expected1 = Location('Abbans-Dessus', 5.88188, 47.120548, 3038800, 'FR') - expected2 = Location('Abbans-Dessus', 5.88333, 47.116669, 6452202, 'FR') - result = self._instance.locations_for("Abbans-Dessus") - self.assertEqual(2, len(result)) - self._assertLocationsEqual(expected1, result[0]) - self._assertLocationsEqual(expected2, result[1]) - - CityIDRegistry._get_lines = ref_to_original - - def test_locations_for_matching_criteria(self): - original_get_lines = CityIDRegistry._get_lines - original_get_all_lines = CityIDRegistry._get_all_lines - CityIDRegistry._get_lines = self._mock_get_lines_with_homonymies - CityIDRegistry._get_all_lines = self._mock_get_all_lines - - # look for an empty name - result = self._instance.locations_for("") - self.assertEqual(0, len(result)) - - # country string is too long - with self.assertRaises(ValueError): - self._instance.locations_for("place", country="a_country", matching="nocase") - - # unknown matching type - with self.assertRaises(ValueError): - self._instance.locations_for("place", country="IT", matching="impossible") - - expected = Location('Bologna', -83.250488, 30.57184, 2829449, 'IT') - - # case sensitive - result = self._instance.locations_for("bologna", matching='exact') - self.assertEqual(0, len(result)) - - result = self._instance.locations_for("Bologna", matching='exact') - self.assertEqual(1, len(result)) - self._assertLocationsEqual(expected, result[0]) - - # case insensitive - result = self._instance.locations_for("bologna", matching='nocase') - self.assertEqual(1, len(result)) - self._assertLocationsEqual(expected, result[0]) - - result = self._instance.locations_for("Bologna", matching='nocase') - self.assertEqual(1, len(result)) - self._assertLocationsEqual(expected, result[0]) - - # like - expected1 = Location('Abbans-Dessus', 5.88188, 47.120548, 3038800, 'FR') - expected2 = Location('Abbans-Dessus', 5.88333, 47.116669, 6452202, 'FR') - - result = self._instance.locations_for("abbans", matching='like') - self.assertEqual(2, len(result)) - self._assertLocationsEqual(expected1, result[0]) - self._assertLocationsEqual(expected2, result[1]) - - result = self._instance.locations_for("Dessus", matching='like') - self.assertEqual(2, len(result)) - self._assertLocationsEqual(expected1, result[0]) - self._assertLocationsEqual(expected2, result[1]) - - # startswith - result = self._instance.locations_for("abba", matching='startswith') - self.assertEqual(2, len(result)) - self._assertLocationsEqual(expected1, result[0]) - self._assertLocationsEqual(expected2, result[1]) - - result = self._instance.locations_for("bbans", matching='startswith') - self.assertEqual(0, len(result)) - - CityIDRegistry._get_lines = original_get_lines - CityIDRegistry._get_all_lines = original_get_all_lines - - def test_locations_for_restricted_to_country(self): - ref_to_original = CityIDRegistry._get_lines - CityIDRegistry._get_lines = self._mock_get_lines_with_homonymies - - result = self._instance.locations_for("Abbeville", country='JP') - self.assertEqual(0, len(result)) - - result = self._instance.locations_for("Abbeville", country='US') - self.assertEqual(4, len(result)) - self._assertLocationsEqual( - Location('Abbeville', -83.306824, 31.992121, 4178992, 'US'), - result[0]) - self._assertLocationsEqual( - Location('Abbeville', -92.134293, 29.974649, 4314295, 'US'), - result[1]) - self._assertLocationsEqual( - Location('Abbeville', -82.379013, 34.178169, 4568985, 'US'), - result[2]) - self._assertLocationsEqual( - Location('Abbeville', -85.250488, 31.57184, 4829449, 'US'), - result[3]) - - result = self._instance.locations_for("Abbeville", country='FR') - self.assertEqual(1, len(result)) - self._assertLocationsEqual( - Location("Abbeville", 1.83333, 50.099998, 3038789, 'FR'), - result[0]) - - CityIDRegistry._get_lines = ref_to_original - - def test_locations_for_with_commas_in_city_names(self): - ref_to_original = CityIDRegistry._get_lines - CityIDRegistry._get_lines = self._mock_test_file_contents_with_commas_in_names - - result = self._instance.locations_for("Thale, Stadt") - self.assertEqual(1, len(result)) - self._assertLocationsEqual( - Location('Thale, Stadt', 11.058, 51.7528, 6550950, 'DE'), - result[0]) - - result = self._instance.locations_for("Pitcairn, Henderson, Ducie and Oeno Islands") - self.assertEqual(1, len(result)) - self._assertLocationsEqual( - Location('Pitcairn, Henderson, Ducie and Oeno Islands', -130.100006, -25.066669, 4030699, 'PN'), - result[0]) - - CityIDRegistry._get_lines = ref_to_original - - def test_locations_for_with_commas_in_city_names_like(self): - ref_to_original = CityIDRegistry._get_lines - CityIDRegistry._get_lines = self._mock_test_file_contents_with_commas_in_names - - result = self._instance.locations_for("Pitcai", matching="like") - self._assertLocationsEqual(result[0], Location('Pitcairn', -79.778099, 40.403118, 5206361, 'PA')) - self._assertLocationsEqual(result[1], Location('Pitcairn, Henderson, Ducie and Oeno Islands', -130.100006, -25.066669, 4030699, 'PN')) - - CityIDRegistry._get_lines = ref_to_original - - def test_locations_for_with_commas_in_city_names_like_country(self): - ref_to_original = CityIDRegistry._get_lines - CityIDRegistry._get_lines = self._mock_test_file_contents_with_commas_in_names - - result = self._instance.locations_for("Pitca", country='PA', matching="like") - self._assertLocationsEqual(result[0], Location('Pitcairn', -79.778099, 40.403118, 5206361, 'PA')) - - result = self._instance.locations_for("Ducie", country='PN', matching="like") - self._assertLocationsEqual(result[1], Location('Pitcairn, Henderson, Ducie and Oeno Islands', -130.100006, -25.066669, 4030699, 'PN')) - - CityIDRegistry._get_lines = ref_to_original - - def test_locations_for_with_commas_in_city_names_startswith(self): - ref_to_original = CityIDRegistry._get_lines - CityIDRegistry._get_lines = self._mock_test_file_contents_with_commas_in_names - - result = self._instance.locations_for("Pitcai", matching="startswith") - self.assertEqual(2, len(result)) - self._assertLocationsEqual(result[0], Location('Pitcairn', -79.778099, 40.403118, 5206361, 'PA')) - self._assertLocationsEqual(result[1], Location('Pitcairn, Henderson, Ducie and Oeno Islands', -130.100006, -25.066669, 4030699, 'PN')) - - result = self._instance.locations_for("vil", matching="startswith") - self.assertEqual(0, len(result)) - - CityIDRegistry._get_lines = ref_to_original - - def test_locations_for_with_commas_in_city_names_startswith_country(self): - ref_to_original = CityIDRegistry._get_lines - CityIDRegistry._get_lines = self._mock_test_file_contents_with_commas_in_names - - result = self._instance.locations_for("Pit", country='PA', matching="startswith") - self._assertLocationsEqual(result[0], Location('Pitcairn', -79.778099, 40.403118, 5206361, 'PA')) - - result = self._instance.locations_for("Ducie", country='PN', matching="startswith") - self.assertEqual(0, len(result)) - - CityIDRegistry._get_lines = ref_to_original - - def test_geopoints_for(self): - ref_to_original = CityIDRegistry._get_lines - CityIDRegistry._get_lines = self._mock_get_lines_with_homonymies - - # No matches - result = self._instance.geopoints_for('aaaaaaaaaa') - self.assertEqual(0, len(result)) - - # One match - expected = Location('Bologna', -83.250488, 30.57184, 2829449, 'IT').to_geopoint() - result = self._instance.geopoints_for("Bologna") - self.assertEqual(1, len(result)) - self._assertGeopointsEqual(expected, result[0]) - - # Multiple matches - expected1 = Location('Abbans-Dessus', 5.88188, 47.120548, 3038800, 'FR').to_geopoint() - expected2 = Location('Abbans-Dessus', 5.88333, 47.116669, 6452202, 'FR').to_geopoint() - result = self._instance.geopoints_for("Abbans-Dessus") - self.assertEqual(2, len(result)) - self._assertGeopointsEqual(expected1, result[0]) - self._assertGeopointsEqual(expected2, result[1]) - - CityIDRegistry._get_lines = ref_to_original - - def test_repr(self): - print(self._instance) diff --git a/tests/unit/commons/test_http_client.py b/tests/unit/commons/test_http_client.py index a32b9055..fd5e50c8 100644 --- a/tests/unit/commons/test_http_client.py +++ b/tests/unit/commons/test_http_client.py @@ -132,6 +132,7 @@ def monkey_patched_get_timeouting(uri, params=None, headers=None, proxies=None, requests.get = monkey_patched_get_timeouting config = DEFAULT_CONFIG.copy() config['connection']['timeout_secs'] = timeout + config['connection']['max_retries'] = None try: status, data = HttpClient('apikey', config, 'anyurl.com').get_json('/resource') self.fail() diff --git a/tests/unit/weatherapi25/test_national_weather_alert.py b/tests/unit/weatherapi25/test_national_weather_alert.py new file mode 100644 index 00000000..a5ddc175 --- /dev/null +++ b/tests/unit/weatherapi25/test_national_weather_alert.py @@ -0,0 +1,65 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +import unittest +import json +from pyowm.weatherapi25.national_weather_alert import NationalWeatherAlert +from pyowm.commons.exceptions import APIResponseError, ParseAPIResponseError + + +class TestNationalWeatherAlert(unittest.TestCase): + + __test_time_start= 1629648000 + __test_time_end = 1629723600 + __test_sender = "Deutscher Wetterdienst" + __test_title = "very heavy / persistent rain" + __test_description = "There is a high potential for the development of very heavy / heavy persistent rain." + __test_tags = ['Rain'] + __test_instance = NationalWeatherAlert(__test_sender, __test_title, __test_description, __test_time_start, + __test_time_end, __test_tags) + + __bad_json = '{"a": "test", "b": 1.234, "c": [ "hello", "world"] }' + __bad_json_2 = '{"message": "test", "cod": "500"}' + __no_items_json = '{"cod": "200", "count": "0" }' + __404_json = '{"cod": "404" }' + + NATIONAL_WEATHER_ALERT_JSON_DUMP = '{"sender_name": "Deutscher Wetterdienst", "event": "very heavy / persistent rain", ' \ + '"start": 1629648000, "end": 1629723600, "description": "There is a high potential ' \ + 'for the development of very heavy / heavy persistent rain.", "tags": ["Rain"]}' + + def test_init_failures(self): + self.assertRaises(AssertionError, NationalWeatherAlert, None, self.__test_title, self.__test_description, + self.__test_time_start, self.__test_time_end) + self.assertRaises(AssertionError, NationalWeatherAlert, self.__test_sender, None, self.__test_description, + self.__test_time_start, self.__test_time_end) + self.assertRaises(AssertionError, NationalWeatherAlert, self.__test_sender, self.__test_title, None, + self.__test_time_start, self.__test_time_end) + self.assertRaises(AssertionError, NationalWeatherAlert, self.__test_sender, self.__test_title, self.__test_description, + None, self.__test_time_end) + self.assertRaises(AssertionError, NationalWeatherAlert, self.__test_sender, self.__test_title, self.__test_description, + self.__test_time_start, None) + self.assertRaises(ValueError, NationalWeatherAlert, self.__test_sender, self.__test_title, self.__test_description, + self.__test_time_start, self.__test_time_end, 'testtesttest') + + def test_from_dict(self): + d = json.loads(self.NATIONAL_WEATHER_ALERT_JSON_DUMP) + result = NationalWeatherAlert.from_dict(d) + self.assertTrue(result is not None) + self.assertFalse(result.sender is None) + self.assertFalse(result.title is None) + self.assertFalse(result.description is None) + self.assertFalse(result.start_time() is None) + self.assertFalse(result.end_time() is None) + self.assertTrue(isinstance(result.tags, list)) + + def test_from_dict_fails_when_JSON_data_is_None(self): + with self.assertRaises(ParseAPIResponseError): + NationalWeatherAlert.from_dict(None) + + def test_to_dict(self): + expected = json.loads(self.NATIONAL_WEATHER_ALERT_JSON_DUMP) + result = self.__test_instance.to_dict() + self.assertEqual(expected, result) + + def test__repr(self): + print(self.__test_instance) \ No newline at end of file diff --git a/tests/unit/weatherapi25/test_one_call.py b/tests/unit/weatherapi25/test_one_call.py index 5ebf5326..ff2b62d8 100644 --- a/tests/unit/weatherapi25/test_one_call.py +++ b/tests/unit/weatherapi25/test_one_call.py @@ -8,9 +8,10 @@ from pyowm.utils import geo from pyowm.weatherapi25.one_call import OneCall from pyowm.weatherapi25.weather import Weather +from pyowm.weatherapi25.national_weather_alert import NationalWeatherAlert -class TestWeather(unittest.TestCase): +class TestOneCall(unittest.TestCase): def test_one_call_from_dict(self): result = OneCall.from_dict(self.__test_data_bozen) @@ -34,6 +35,8 @@ def test_one_call_from_dict(self): self.assertTrue(isinstance(weather, Weather), f"entry {i} of forecast_hourly is invalid") self.assertEqual(dt_daily, weather.reference_time()) dt_daily += 86400 + self.assertEqual(1, len(result.national_weather_alerts)) + self.assertTrue(isinstance(result.national_weather_alerts[0], NationalWeatherAlert)) def test_one_call_historical_from_dict(self): result = OneCall.from_dict(self.__test_data_hostorical_bozen) @@ -1441,6 +1444,18 @@ def test_to_geopoint(self): "rain": 1.92, "uvi": 6.93 } + ], + "alerts": [ + { + "sender_name": "Italian Air Force National Meteorological Service", + "event": "Yellow Thunderstorm Warning", + "start": 1629626400, + "end": 1629658740, + "description": 'No Special Awareness Required (DISCLAIMER: "Information provided on METEOALARM for Italy regard only the intensity and recurrence of the phenomena, further details can be found at www.meteoam.it. METEOALARM information do not provide the assessment of impact on the territory and they do not represent the Official Alerts messages that are issued by the National Civil Protection Service www.protezionecivile.it', + "tags": [ + "Thunderstorm" + ] + } ] }