diff --git a/.gitignore b/.gitignore index e3560beb..00e11d24 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,6 @@ sphinx/_build/* .coverage tests/integration/.tox/* .cache +.eggs/ +htmlcov/* + diff --git a/.travis.yml b/.travis.yml index 9fb49589..c85a873f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,21 +1,66 @@ language: python python: - - "2.7" - "3.4" - "3.5" - "3.6" - "3.7-dev" -install: - - pip install --upgrade pip - - pip install coverage tox-travis virtualenv coveralls -script: - - tox - - coverage run --rcfile=.coveragerc setup.py test -s tests.unit - - bash tests/local_installation_test.sh +script: tox after_success: - coveralls + - coveralls + notifications: email: on_failure: change -dist: xenial -sudo: false +env: + global: + - TEST_PYPI_URL=https://test.pypi.org/legacy/ + - PYPI_URL=https://upload.pypi.org/legacy/ +install: + - pip install --upgrade pip && pip install -r dev-requirements.txt && pip install . +cache: pip + +stages: + - test + - "Local Installation Test" + - "Build docs" + - "Deploy to PyPI" + - "PyPI Installation Test" + +jobs: + include: + - &local_installation_test + stage: "Local Installation Test" + python: 3.4 + script: bash tests/local_installation_test.sh + - <<: *local_installation_test + python: 3.5 + - <<: *local_installation_test + python: 3.6 + - <<: *local_installation_test + python: "3.7-dev" + + - stage: "Build docs" + script: cd sphinx && make clean && make html + + - &deploy_to_pypi + stage: "Deploy to PyPI" + python: 3.4 + script: bash deploy/deploy_to_pypi.sh + - <<: *deploy_to_pypi + python: 3.5 + - <<: *deploy_to_pypi + python: 3.6 + - <<: *deploy_to_pypi + python: "3.7-dev" + + - &pypi_installation_test + stage: "PyPI Installation Test" + python: 3.4 + if: branch = master + script: bash tests/pypi_installation_test.sh + - <<: *pypi_installation_test + python: 3.5 + - <<: *pypi_installation_test + python: 3.6 + - <<: *pypi_installation_test + python: "3.7-dev" \ No newline at end of file diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 3e4639db..167c98c7 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -19,6 +19,7 @@ Code Testing ------- * [Samuel Yap] (https://github.com/samuelyap) + * [Patrick Casbon] (https://github.com/patcas) Packaging and Distribution -------------------------- diff --git a/MANIFEST.in b/MANIFEST.in index 1a436383..38a7b04d 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,3 +1,6 @@ -include pyowm/webapi25/cityids/*.gz -include pyowm/webapi25/xsd/*.xsd +include pyowm/pollutionapi30/xsd/*.xsd +include pyowm/stationsapi30/xsd/*.xsd +include pyowm/uvindexapi30/xsd/*.xsd +include pyowm/weatherapi25/cityids/*.gz +include pyowm/weatherapi25/xsd/*.xsd include pyowm/docs/*.md \ No newline at end of file diff --git a/Pipfile b/Pipfile index f956001e..70908c7f 100644 --- a/Pipfile +++ b/Pipfile @@ -12,5 +12,5 @@ virtualenv = "==12.0.7" twine = "==1.11.0" [packages] -requests = "<3,==2.18.2" -geojson = "<2.4,==2.3.0" +requests = "<3,==2.20.0" +geojson = "<3,==2.3.0" diff --git a/Pipfile.lock b/Pipfile.lock index 8483e420..b789287a 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "bf9a6de0ca43ba2df89da2cf2f81f4e6996556cf06e6beb32b495d192b96a704" + "sha256": "07a2fc7ed1b9ecce48553f583b26d86a4b021775fdc5813fdf3c03fb423d2d74" }, "pipfile-spec": 6, "requires": {}, @@ -16,10 +16,10 @@ "default": { "certifi": { "hashes": [ - "sha256:13e698f54293db9f89122b0581843a782ad0934a4fe0172d2a980ba77fc61bb7", - "sha256:9fa520c1bacfb634fa7af20a76bcbd3d5fb390481724c597da32c719a7dca4b0" + "sha256:47f9c83ef4c0c621eaef743f133f09fa8a74a9b75f037e8624f83bd1b6626cb7", + "sha256:993f830721089fef441cdfeb4b2c8c9df86f0c63239f06bd025a76a7daddb033" ], - "version": "==2018.4.16" + "version": "==2018.11.29" }, "chardet": { "hashes": [ @@ -38,34 +38,34 @@ }, "idna": { "hashes": [ - "sha256:3cb5ce08046c4e3a560fc02f138d0ac63e00f8ce5901a56b32ec8b7994082aab", - "sha256:cc19709fd6d0cbfed39ea875d29ba6d4e22c0cebc510a76d6302a28385e8bb70" + "sha256:156a6814fb5ac1fc6850fb002e0852d56c0c8d2531923a51032d1b70760e186e", + "sha256:684a38a6f903c1d71d6d5fac066b58d7768af4de2b832e426ec79c30daa94a16" ], - "version": "==2.5" + "version": "==2.7" }, "requests": { "hashes": [ - "sha256:414459f05392835d4d653b57b8e58f98aea9c6ff2782e37de0a1ee92891ce900", - "sha256:5b26fcc5e72757a867e4d562333f841eddcef93548908a1bb1a9207260618da9" + "sha256:99dcfdaaeb17caf6e526f32b6a7b780461512ab3f1d992187801694cba42770c", + "sha256:a84b8c9ab6239b578f22d1c21d51b696dcfe004032bb80ea832398d6909d7279" ], "index": "pypi", - "version": "==2.18.2" + "version": "==2.20.0" }, "urllib3": { "hashes": [ - "sha256:06330f386d6e4b195fbfc736b297f58c5a892e4440e54d294d7004e3a9bbea1b", - "sha256:cc44da8e1145637334317feebd728bd869a35285b93cbb4cca2577da7e62db4f" + "sha256:61bf29cada3fc2fbefad4fdf059ea4bd1b4a86d2b6d15e1c7c0b582b9752fe39", + "sha256:de9529817c93f27c8ccbfead6985011db27bd0ddfcdb2d86f3f663385c6a9c22" ], - "version": "==1.22" + "version": "==1.24.1" } }, "develop": { "certifi": { "hashes": [ - "sha256:13e698f54293db9f89122b0581843a782ad0934a4fe0172d2a980ba77fc61bb7", - "sha256:9fa520c1bacfb634fa7af20a76bcbd3d5fb390481724c597da32c719a7dca4b0" + "sha256:47f9c83ef4c0c621eaef743f133f09fa8a74a9b75f037e8624f83bd1b6626cb7", + "sha256:993f830721089fef441cdfeb4b2c8c9df86f0c63239f06bd025a76a7daddb033" ], - "version": "==2018.4.16" + "version": "==2018.11.29" }, "chardet": { "hashes": [ @@ -78,18 +78,14 @@ "hashes": [ "sha256:2b9fe71a6bd410940cedb6212cecdf21280d4b3471c3a188f9c5263cfe6fb67b", "sha256:3b50e09002e420eddee1816017c44d0cb08483e3e14d919641b377a7ec83f030", - "sha256:3c7fffb5b756a9e4b6e538af2f9790683f3dff33f8286ce5a68fff7c0e7ad333", "sha256:3e7c8dd920c0dea0b918e3737df8a76c3f604577afc29fea376803f165d99082", "sha256:5164423881464f02904e9254d82631cf39fedc714aab09b6e38dd0e66ce24dc0", "sha256:58aa6d06ec7f784c09c09936d3aac2a9835f3b39fc72b0115753e60b8fee5892", "sha256:778cf461acb1edd251877b21043d3e8089f1de8bee757103cd1124497aa2c4d2", - "sha256:8a19982a9b0901f4a005d0ef3ab557fc086fd296faf9de616690bfd632112b5a", "sha256:985d645e630eefa33c20de1395f649e232eacddfe48d9a124c2e221bbaef6d78", "sha256:a4df4c28207e6a2746b82f8edbf3af03794c31423bcba43f20cd086c06c0ffa9", "sha256:b4fc3047692cc6c47ba9cbf3ae6c1c42fbaef9ab86a62f78bf260eb993afa068", "sha256:d1aea1c4aa61b8366d6a42dd3650622fbf9c634ed24eaf7f379c8b970e5ed44e", - "sha256:e4774a17088aa494cfc47420ba2361e412a81a7a3bd868de16029901d8e26f10", - "sha256:ed2cf90d31ba2fec9051ab079cfc0715770da16ea82f5147655d61cd5f034cc5", "sha256:f88c9ddca928afce3500c5c286c5962ac2f9cda9314af5a6ec284cd67d13a8dd" ], "index": "pypi", @@ -105,10 +101,10 @@ }, "idna": { "hashes": [ - "sha256:3cb5ce08046c4e3a560fc02f138d0ac63e00f8ce5901a56b32ec8b7994082aab", - "sha256:cc19709fd6d0cbfed39ea875d29ba6d4e22c0cebc510a76d6302a28385e8bb70" + "sha256:156a6814fb5ac1fc6850fb002e0852d56c0c8d2531923a51032d1b70760e186e", + "sha256:684a38a6f903c1d71d6d5fac066b58d7768af4de2b832e426ec79c30daa94a16" ], - "version": "==2.5" + "version": "==2.7" }, "jinja2": { "hashes": [ @@ -119,9 +115,36 @@ }, "markupsafe": { "hashes": [ - "sha256:a6be69091dac236ea9c6bc7d012beab42010fa914c459791d627dad4910eb665" - ], - "version": "==1.0" + "sha256:048ef924c1623740e70204aa7143ec592504045ae4429b59c30054cb31e3c432", + "sha256:130f844e7f5bdd8e9f3f42e7102ef1d49b2e6fdf0d7526df3f87281a532d8c8b", + "sha256:19f637c2ac5ae9da8bfd98cef74d64b7e1bb8a63038a3505cd182c3fac5eb4d9", + "sha256:1b8a7a87ad1b92bd887568ce54b23565f3fd7018c4180136e1cf412b405a47af", + "sha256:1c25694ca680b6919de53a4bb3bdd0602beafc63ff001fea2f2fc16ec3a11834", + "sha256:1f19ef5d3908110e1e891deefb5586aae1b49a7440db952454b4e281b41620cd", + "sha256:1fa6058938190ebe8290e5cae6c351e14e7bb44505c4a7624555ce57fbbeba0d", + "sha256:31cbb1359e8c25f9f48e156e59e2eaad51cd5242c05ed18a8de6dbe85184e4b7", + "sha256:3e835d8841ae7863f64e40e19477f7eb398674da6a47f09871673742531e6f4b", + "sha256:4e97332c9ce444b0c2c38dd22ddc61c743eb208d916e4265a2a3b575bdccb1d3", + "sha256:525396ee324ee2da82919f2ee9c9e73b012f23e7640131dd1b53a90206a0f09c", + "sha256:52b07fbc32032c21ad4ab060fec137b76eb804c4b9a1c7c7dc562549306afad2", + "sha256:52ccb45e77a1085ec5461cde794e1aa037df79f473cbc69b974e73940655c8d7", + "sha256:5c3fbebd7de20ce93103cb3183b47671f2885307df4a17a0ad56a1dd51273d36", + "sha256:5e5851969aea17660e55f6a3be00037a25b96a9b44d2083651812c99d53b14d1", + "sha256:5edfa27b2d3eefa2210fb2f5d539fbed81722b49f083b2c6566455eb7422fd7e", + "sha256:7d263e5770efddf465a9e31b78362d84d015cc894ca2c131901a4445eaa61ee1", + "sha256:83381342bfc22b3c8c06f2dd93a505413888694302de25add756254beee8449c", + "sha256:857eebb2c1dc60e4219ec8e98dfa19553dae33608237e107db9c6078b1167856", + "sha256:98e439297f78fca3a6169fd330fbe88d78b3bb72f967ad9961bcac0d7fdd1550", + "sha256:bf54103892a83c64db58125b3f2a43df6d2cb2d28889f14c78519394feb41492", + "sha256:d9ac82be533394d341b41d78aca7ed0e0f4ba5a2231602e2f05aa87f25c51672", + "sha256:e982fe07ede9fada6ff6705af70514a52beb1b2c3d25d4e873e82114cf3c5401", + "sha256:edce2ea7f3dfc981c4ddc97add8a61381d9642dc3273737e756517cc03e84dd6", + "sha256:efdc45ef1afc238db84cb4963aa689c0408912a0239b0721cb172b4016eb31d6", + "sha256:f137c02498f8b935892d5c0172560d7ab54bc45039de8805075e19079c639a9c", + "sha256:f82e347a72f955b7017a39708a3667f106e6ad4d10b25f237396a7115d8ed5fd", + "sha256:fb7c206e01ad85ce57feeaaa0bf784b97fa3cad0d4a5737bc5295785f5c613a1" + ], + "version": "==1.1.0" }, "pkginfo": { "hashes": [ @@ -132,17 +155,17 @@ }, "py": { "hashes": [ - "sha256:29c9fab495d7528e80ba1e343b958684f4ace687327e6f789a94bf3d1915f881", - "sha256:983f77f3331356039fdd792e9220b7b8ee1aa6bd2b25f567a963ff1de5a64f6a" + "sha256:bf92637198836372b520efcba9e020c330123be8ce527e535d185ed4b6f45694", + "sha256:e76826342cefe3c3d5f7e8ee4316b80d1dd8a300781612ddbc765c17ba25a6c6" ], - "version": "==1.5.3" + "version": "==1.7.0" }, "pygments": { "hashes": [ - "sha256:78f3f434bcc5d6ee09020f92ba487f95ba50f1e3ef83ae96b9d5ffa1bab25c5d", - "sha256:dbae1046def0efb574852fab9e90209b23f556367b5a320c0bcb871c77c3e8cc" + "sha256:5ffada19f6203563680669ee7f53b64dabbeb100eb51b61996085e99c03b284a", + "sha256:e8218dd399a61674745138520d0d4cf2621d7e032439341bc3f647bff125818d" ], - "version": "==2.2.0" + "version": "==2.3.1" }, "pytest": { "hashes": [ @@ -154,11 +177,11 @@ }, "requests": { "hashes": [ - "sha256:414459f05392835d4d653b57b8e58f98aea9c6ff2782e37de0a1ee92891ce900", - "sha256:5b26fcc5e72757a867e4d562333f841eddcef93548908a1bb1a9207260618da9" + "sha256:99dcfdaaeb17caf6e526f32b6a7b780461512ab3f1d992187801694cba42770c", + "sha256:a84b8c9ab6239b578f22d1c21d51b696dcfe004032bb80ea832398d6909d7279" ], "index": "pypi", - "version": "==2.18.2" + "version": "==2.20.0" }, "requests-toolbelt": { "hashes": [ @@ -187,10 +210,10 @@ }, "tqdm": { "hashes": [ - "sha256:224291ee0d8c52d91b037fd90806f48c79bcd9994d3b0abc9e44b946a908fccd", - "sha256:77b8424d41b31e68f437c6dd9cd567aebc9a860507cb42fbd880a5f822d966fe" + "sha256:3c4d4a5a41ef162dd61f1edb86b0e1c7859054ab656b2e7c7b77e7fbf6d9f392", + "sha256:5b4d5549984503050883bc126280b386f5f4ca87e6c023c5d015655ad75bdebb" ], - "version": "==4.23.4" + "version": "==4.28.1" }, "twine": { "hashes": [ @@ -202,10 +225,10 @@ }, "urllib3": { "hashes": [ - "sha256:06330f386d6e4b195fbfc736b297f58c5a892e4440e54d294d7004e3a9bbea1b", - "sha256:cc44da8e1145637334317feebd728bd869a35285b93cbb4cca2577da7e62db4f" + "sha256:61bf29cada3fc2fbefad4fdf059ea4bd1b4a86d2b6d15e1c7c0b582b9752fe39", + "sha256:de9529817c93f27c8ccbfead6985011db27bd0ddfcdb2d86f3f663385c6a9c22" ], - "version": "==1.22" + "version": "==1.24.1" }, "virtualenv": { "hashes": [ diff --git a/README.md b/README.md index 85598392..f8a6afe9 100644 --- a/README.md +++ b/README.md @@ -4,12 +4,16 @@ **A Python wrapper around OpenWeatherMap web APIs** [![PyPI version](https://badge.fury.io/py/pyowm.svg)](https://badge.fury.io/py/pyowm) -[![Latest Release Documentation](https://readthedocs.org/projects/pyowm/badge/?version=latest)](https://pyowm.readthedocs.org/en/latest/) +[![PyPI - Downloads](https://img.shields.io/pypi/dm/pyowm.svg)](https://img.shields.io/pypi/dm/pyowm.svg) +
+[![PyPI - Python Version](https://img.shields.io/pypi/pyversions/pyowm.svg)](https://img.shields.io/pypi/pyversions/pyowm.svg) +
+[![Latest Release Documentation](https://readthedocs.org/projects/pyowm/badge/?version=latest)](https://pyowm.readthedocs.io/en/latest/) [![Build Status](https://travis-ci.org/csparpa/pyowm.png?branch=master)](https://travis-ci.org/csparpa/pyowm) [![Coverage Status](https://coveralls.io/repos/github/csparpa/pyowm/badge.svg?branch=master)](https://coveralls.io/github/csparpa/pyowm?branch=master) -[![Say Thanks!](https://img.shields.io/badge/Say%20Thanks-!-1EAEDB.svg)](https://saythanks.io/to/csparpa)
Buy Me A Coffee +[![Say Thanks!](https://img.shields.io/badge/Say%20Thanks-!-1EAEDB.svg)](https://saythanks.io/to/csparpa) ## What is it? PyOWM is a client Python wrapper library for OpenWeatherMap (OWM) web APIs. @@ -18,15 +22,16 @@ It allows quick and easy consumption of OWM data from Python applications via a With PyOWM you can integrate into your code any of the following OpenWeatherMap web APIs: - - *Weather API v2.5*, offering + - **[Weather API v2.5](https://pyowm.readthedocs.io/en/latest/usage-examples-v2/weather-api-usage-examples.html)**, providing - current weather data - weather forecasts - -+ weather history - - *Air Pollution API v3.0*, offering data about CO, O3, NO2 and SO2 - - *UV Index API v3.0*, offering data about Ultraviolet exposition - - *Stations API v3.0*, allowing to create and manage meteostation and publish local weather measurements - - *Weather Alerts API v3.0*, allowing to set triggers on weather conditions and areas and poll for spawned alerts - - *Image tiles* for several map layers provided by OWM + - weather history + - **[Agro API v1.0](https://pyowm.readthedocs.io/en/latest/usage-examples-v2/agro-api-usage-examples.html)**, providing soil data and satellite imagery search and download + - **[Air Pollution API v3.0](https://pyowm.readthedocs.io/en/latest/usage-examples-v2/air-pollution-api-usage-examples.html)**, providing data about CO, O3, NO2 and SO2 + - **[UV Index API v3.0](https://pyowm.readthedocs.io/en/latest/usage-examples-v2/uv-api-usage-examples.html)**, providing data about Ultraviolet exposition + - **[Stations API v3.0](https://pyowm.readthedocs.io/en/latest/usage-examples-v2/stations-api-usage-examples.html)**, allowing to create and manage meteostation and publish local weather measurements + - **[Weather Alerts API v3.0](https://pyowm.readthedocs.io/en/latest/usage-examples-v2/alerts-api-usage-examples.html)**, allowing to set triggers on weather conditions and areas and poll for spawned alerts + - **[Image tiles](https://pyowm.readthedocs.io/en/latest/usage-examples-v2/map-tiles-client-examples.html)** for several map layers provided by OWM PyOWM runs on Python 2.7 and Python 3.4+ (but watch out! Python 2.x will eventually be dropped - [check details out](https://github.com/csparpa/pyowm/wiki/Timeline-for-dropping-Python-2.x-support)) @@ -64,7 +69,7 @@ Please notice that the free API subscription plan is subject to requests throttl ### Examples -That's a simple example of what you can do with PyOWM and a free OWM API Key: +That's a simple example of what you can do with PyOWM Weather API and a free OWM API Key: ```python import pyowm @@ -90,7 +95,7 @@ w.get_temperature('celsius') # {'temp_max': 10.5, 'temp': 9.7, 'temp_min': 9.0} observation_list = owm.weather_around_coords(-22.57, -43.12) ``` -And this is a usage example with a paid OWM API key: +And this is an example using a paid OWM API key: ```python import pyowm @@ -100,14 +105,11 @@ paid_owm = pyowm.OWM(API_key='your-pro-API-key', subscription_type='pro') # Will it be clear tomorrow at this time in Milan (Italy) ? forecast = paid_owm.daily_forecast("Milan,IT") tomorrow = pyowm.timeutils.tomorrow() -forecast.will_be_clear_at(tomorrow) # The sun always shines on Italy, right? ;-) +forecast.will_be_clear_at(tomorrow) # The sun always shines on Italy, right? ;) ``` -More PyOWM usage examples are available [here](https://github.com/csparpa/pyowm/blob/master/pyowm/docs/usage-examples.md). - - ## Documentation -The library software API documentation is available on [Read the Docs](https://pyowm.readthedocs.org/en/stable/). +The library software API documentation is available on [Read the Docs](https://pyowm.readthedocs.io/en/latest/). Each release has its own [changelog](https://github.com/csparpa/pyowm/wiki/Changelog). diff --git a/deploy/deploy_to_pypi.sh b/deploy/deploy_to_pypi.sh new file mode 100644 index 00000000..c42039bb --- /dev/null +++ b/deploy/deploy_to_pypi.sh @@ -0,0 +1,52 @@ +#!/usr/bin/env bash + +# Rules: +# - deploy to Test PyPI upon every push on the develop branch +# - only deploy to the real PyPI upon merged pull requests on the master branch + +if [ $TRAVIS_BRANCH = "develop" ] && [[ $TRAVIS_EVENT_TYPE == "push" ]]; then + echo "*** Will build the DEVELOP branch and publish to Test PyPI at $TEST_PYPI_URL" + + # Get env variables + export INDEX_URL="$TEST_PYPI_URL" + export PYPI_USERNAME="$TEST_PYPI_USERNAME" + export PYPI_PASSWORD="$TEST_PYPI_PASSWORD" + + # Get commit SHA and patch development release number + TS="$(date "+%Y%m%d%H0000")" + echo "*** Development release number is: post$TS" + sed -i -e "s/constants.PYOWM_VERSION/constants.PYOWM_VERSION+\".post${TS}\"/g" setup.py + +elif [ $TRAVIS_BRANCH = "master" ] && [[ $TRAVIS_EVENT_TYPE == "pull_request" ]]; then + echo "*** Will build the MASTER branch and publish to PyPI at $PYPI_URL" + + # Get env variables + export INDEX_URL="$PYPI_URL" + export PYPI_USERNAME="$PYPI_USERNAME" + export PYPI_PASSWORD="$PYPI_PASSWORD" + +else + echo "*** Wrong deployment conditions: branch=$TRAVIS_BRANCH event=$TRAVIS_EVENT_TYPE" + exit 5 +fi + +echo '*** Generating source distribution...' +python setup.py sdist + +echo '*** Generating egg distribution...' +python setup.py bdist_egg + +echo '*** Generating wheel distribution...' +python setup.py bdist_wheel + +echo '*** Uploading all artifacts to PyPi...' +twine upload --repository-url "$INDEX_URL" \ + --username "$PYPI_USERNAME" \ + --password "$PYPI_PASSWORD" \ + --skip-existing \ + dist/* +if [ "$?" -ne 0 ]; then + exit 7 +fi + +echo '*** Done' \ No newline at end of file diff --git a/dev-requirements.txt b/dev-requirements.txt index 26518265..ae5ed4cc 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -1,9 +1,11 @@ -coverage==3.7.1 +coverage +coveralls pip>=18.0 -pytest==3.0.3 +pytest recommonmark Sphinx sphinx-readable-theme==1.3.0 -tox==1.9.2 -virtualenv==12.0.7 -twine==1.11.0 +tox +tox-travis +virtualenv +twine \ No newline at end of file diff --git a/pyowm/__init__.py b/pyowm/__init__.py index e084c267..729c98bd 100644 --- a/pyowm/__init__.py +++ b/pyowm/__init__.py @@ -11,19 +11,22 @@ """ from pyowm import constants -from pyowm.utils import timeutils # Convenience import +from pyowm.utils import timeutils, stringutils + + +stringutils.check_if_running_with_python_2() def OWM(API_key=constants.DEFAULT_API_KEY, version=constants.LATEST_OWM_API_VERSION, config_module=None, language=None, subscription_type=None, use_ssl=None): """ A parametrized factory method returning a global OWM instance that - represents the desired OWM web API version (or the currently supported one + represents the desired OWM Weather API version (or the currently supported one if no version number is specified) - :param API_key: the OWM web API key (defaults to a test value) + :param API_key: the OWM Weather API key (defaults to a test value) :type API_key: str - :param version: the OWM web API version. Defaults to ``None``, which means + :param version: the OWM Weather API version. Defaults to ``None``, which means use the latest web API version :type version: str :param config_module: the Python path of the configuration module you want @@ -36,7 +39,7 @@ def OWM(API_key=constants.DEFAULT_API_KEY, version=constants.LATEST_OWM_API_VERS It's a two-characters string, eg: "en", "ru", "it". Defaults to: ``None``, which means use the default language. :type language: str - :param subscription_type: the type of OWM web API subscription to be wrapped. + :param subscription_type: the type of OWM Weather API subscription to be wrapped. Can be 'free' (free subscription) or 'pro' (paid subscription), Defaults to: 'free' :type subscription_type: str @@ -48,9 +51,9 @@ def OWM(API_key=constants.DEFAULT_API_KEY, version=constants.LATEST_OWM_API_VERS """ if version == '2.5': if config_module is None: - config_module = "pyowm.webapi25.configuration25" + config_module = "pyowm.weatherapi25.configuration25" cfg_module = __import__(config_module, fromlist=['']) - from pyowm.webapi25.owm25 import OWM25 + from pyowm.weatherapi25.owm25 import OWM25 if language is None: language = cfg_module.language if subscription_type is None: @@ -61,4 +64,4 @@ def OWM(API_key=constants.DEFAULT_API_KEY, version=constants.LATEST_OWM_API_VERS use_ssl = cfg_module.USE_SSL return OWM25(cfg_module.parsers, API_key, cfg_module.cache, language, subscription_type, use_ssl) - raise ValueError("Unsupported OWM web API version") + raise ValueError("Unsupported OWM Weather API version") diff --git a/pyowm/abstractions/jsonparser.py b/pyowm/abstractions/jsonparser.py index c9a417d1..100473a1 100644 --- a/pyowm/abstractions/jsonparser.py +++ b/pyowm/abstractions/jsonparser.py @@ -1,5 +1,5 @@ """ -Module containing an abstract base class for JSON OWM web API responses parsing +Module containing an abstract base class for JSON OWM Weather API responses parsing """ from abc import ABCMeta, abstractmethod diff --git a/pyowm/abstractions/owm.py b/pyowm/abstractions/owm.py index 6f721e17..4e92e961 100644 --- a/pyowm/abstractions/owm.py +++ b/pyowm/abstractions/owm.py @@ -7,9 +7,9 @@ class OWM(object): """ - A global abstract class representing the OWM web API. Every query to the + A global abstract class representing the OWM Weather API. Every query to the API is done programmatically via a concrete instance of this class. - Subclasses should provide a method for every OWM web API endpoint. + Subclasses should provide a method for every OWM Weather API endpoint. """ __metaclass__ = ABCMeta @@ -38,9 +38,9 @@ def set_API_key(self, API_key): @abstractmethod def get_API_version(self): """ - Returns the currently supported OWM web API version + Returns the currently supported OWM Weather API version - :returns: the OWM web API version string + :returns: the OWM Weather API version string """ raise NotImplementedError @@ -58,7 +58,7 @@ def get_version(self): @abstractmethod def is_API_online(self): """ - Returns ``True`` if the OWM web API is currently online. A short + Returns ``True`` if the OWM Weather API is currently online. A short timeout is used to determine API service availability. :returns: bool diff --git a/pyowm/abstractions/owmcache.py b/pyowm/abstractions/owmcache.py index 27cfd424..754cf07d 100644 --- a/pyowm/abstractions/owmcache.py +++ b/pyowm/abstractions/owmcache.py @@ -9,8 +9,8 @@ class OWMCache(object): """ A global abstract class representing a caching provider which can be used to lookup the JSON responses to the most recently or most frequently issued - OWM web API requests. - The purpose of the caching mechanism is to avoid OWM web API requests and + OWM Weather API requests. + The purpose of the caching mechanism is to avoid OWM Weather API requests and therefore network traffic: the implementations should be adapted to the time/memory requirements of the OWM data clients (i.e: a "slimmer" cache with lower lookup times but higher miss rates or a "fatter" cache with diff --git a/pyowm/webapi25/__init__.py b/pyowm/agroapi10/__init__.py similarity index 100% rename from pyowm/webapi25/__init__.py rename to pyowm/agroapi10/__init__.py diff --git a/pyowm/agroapi10/agro_manager.py b/pyowm/agroapi10/agro_manager.py new file mode 100644 index 00000000..3ce2f5ed --- /dev/null +++ b/pyowm/agroapi10/agro_manager.py @@ -0,0 +1,323 @@ +""" +Programmatic interface to OWM Agro API endpoints +""" + +from pyowm.constants import AGRO_API_VERSION +from pyowm.commons.http_client import HttpClient +from pyowm.commons.databoxes import ImageType +from pyowm.commons.image import Image +from pyowm.commons.tile import Tile +from pyowm.agroapi10.uris import POLYGONS_URI, NAMED_POLYGON_URI, SOIL_URI, SATELLITE_IMAGERY_SEARCH_URI +from pyowm.agroapi10.enums import PresetEnum, PaletteEnum +from pyowm.agroapi10.polygon import Polygon, GeoPolygon +from pyowm.agroapi10.soil import Soil +from pyowm.agroapi10.imagery import MetaTile, MetaGeoTiffImage, MetaPNGImage, SatelliteImage +from pyowm.agroapi10.search import SatelliteImagerySearchResultSet +from pyowm.utils import timeutils + + +class AgroManager(object): + + """ + A manager objects that provides a full interface to OWM Agro API. + + :param API_key: the OWM Weather API key + :type API_key: str + :returns: an `AgroManager` instance + :raises: `AssertionError` when no API Key is provided + + """ + + def __init__(self, API_key): + assert API_key is not None, 'You must provide a valid API Key' + self.API_key = API_key + self.http_client = HttpClient() + + def agro_api_version(self): + return AGRO_API_VERSION + + # POLYGON API subset methods + + def create_polygon(self, geopolygon, name=None): + """ + Create a new polygon on the Agro API with the given parameters + + :param geopolygon: the geopolygon representing the new polygon + :type geopolygon: `pyowm.utils.geo.Polygon` instance + :param name: optional mnemonic name for the new polygon + :type name: str + :return: a `pyowm.agro10.polygon.Polygon` instance + """ + assert geopolygon is not None + assert isinstance(geopolygon, GeoPolygon) + data = dict() + data['geo_json'] = { + "type": "Feature", + "properties": {}, + "geometry": geopolygon.as_dict() + } + if name is not None: + data['name'] = name + status, payload = self.http_client.post( + POLYGONS_URI, + params={'appid': self.API_key}, + data=data, + headers={'Content-Type': 'application/json'}) + return Polygon.from_dict(payload) + + def get_polygons(self): + """ + Retrieves all of the user's polygons registered on the Agro API. + + :returns: list of `pyowm.agro10.polygon.Polygon` objects + + """ + + status, data = self.http_client.get_json( + POLYGONS_URI, + params={'appid': self.API_key}, + headers={'Content-Type': 'application/json'}) + return [Polygon.from_dict(item) for item in data] + + def get_polygon(self, polygon_id): + """ + Retrieves a named polygon registered on the Agro API. + + :param id: the ID of the polygon + :type id: str + :returns: a `pyowm.agro10.polygon.Polygon` object + + """ + status, data = self.http_client.get_json( + NAMED_POLYGON_URI % str(polygon_id), + params={'appid': self.API_key}, + headers={'Content-Type': 'application/json'}) + return Polygon.from_dict(data) + + def update_polygon(self, polygon): + """ + Updates on the Agro API the Polygon identified by the ID of the provided polygon object. + Currently this only changes the mnemonic name of the remote polygon + + :param polygon: the `pyowm.agro10.polygon.Polygon` object to be updated + :type polygon: `pyowm.agro10.polygon.Polygon` instance + :returns: `None` if update is successful, an exception otherwise + """ + assert polygon.id is not None + status, _ = self.http_client.put( + NAMED_POLYGON_URI % str(polygon.id), + params={'appid': self.API_key}, + data=dict(name=polygon.name), + headers={'Content-Type': 'application/json'}) + + def delete_polygon(self, polygon): + """ + Deletes on the Agro API the Polygon identified by the ID of the provided polygon object. + + :param polygon: the `pyowm.agro10.polygon.Polygon` object to be deleted + :type polygon: `pyowm.agro10.polygon.Polygon` instance + :returns: `None` if deletion is successful, an exception otherwise + """ + assert polygon.id is not None + status, _ = self.http_client.delete( + NAMED_POLYGON_URI % str(polygon.id), + params={'appid': self.API_key}, + headers={'Content-Type': 'application/json'}) + + # SOIL API subset methods + + def soil_data(self, polygon): + """ + Retrieves the latest soil data on the specified polygon + + :param polygon: the reference polygon you want soil data for + :type polygon: `pyowm.agro10.polygon.Polygon` instance + :returns: a `pyowm.agro10.soil.Soil` instance + + """ + assert polygon is not None + assert isinstance(polygon, Polygon) + polyd = polygon.id + status, data = self.http_client.get_json( + SOIL_URI, + params={'appid': self.API_key, + 'polyid': polyd}, + headers={'Content-Type': 'application/json'}) + the_dict = dict() + the_dict['reference_time'] = data['dt'] + the_dict['surface_temp'] = data['t0'] + the_dict['ten_cm_temp'] = data['t10'] + the_dict['moisture'] = data['moisture'] + the_dict['polygon_id'] = polyd + return Soil.from_dict(the_dict) + + # Satellite Imagery subset methods + + def search_satellite_imagery(self, polygon_id, acquired_from, acquired_to, img_type=None, preset=None, + min_resolution=None, max_resolution=None, acquired_by=None, min_cloud_coverage=None, + max_cloud_coverage=None, min_valid_data_coverage=None, max_valid_data_coverage=None): + """ + Searches on the Agro API the metadata for all available satellite images that contain the specified polygon and + acquired during the specified time interval; and optionally matching the specified set of filters: + - image type (eg. GeoTIF) + - image preset (eg. false color, NDVI, ...) + - min/max acquisition resolution + - acquiring satellite + - min/max cloud coverage on acquired scene + - min/max valid data coverage on acquired scene + + :param polygon_id: the ID of the reference polygon + :type polygon_id: str + :param acquired_from: lower edge of acquisition interval, UNIX timestamp + :type acquired_from: int + :param acquired_to: upper edge of acquisition interval, UNIX timestamp + :type acquired_to: int + :param img_type: the desired file format type of the images. Allowed values are given by `pyowm.commons.enums.ImageTypeEnum` + :type img_type: `pyowm.commons.databoxes.ImageType` + :param preset: the desired preset of the images. Allowed values are given by `pyowm.agroapi10.enums.PresetEnum` + :type preset: str + :param min_resolution: minimum resolution for images, px/meters + :type min_resolution: int + :param max_resolution: maximum resolution for images, px/meters + :type max_resolution: int + :param acquired_by: short symbol of the satellite that acquired the image (eg. "l8") + :type acquired_by: str + :param min_cloud_coverage: minimum cloud coverage percentage on acquired images + :type min_cloud_coverage: int + :param max_cloud_coverage: maximum cloud coverage percentage on acquired images + :type max_cloud_coverage: int + :param min_valid_data_coverage: minimum valid data coverage percentage on acquired images + :type min_valid_data_coverage: int + :param max_valid_data_coverage: maximum valid data coverage percentage on acquired images + :type max_valid_data_coverage: int + :return: a list of `pyowm.agro10.imagery.MetaImage` subtypes instances + """ + assert polygon_id is not None + assert acquired_from is not None + assert acquired_to is not None + assert acquired_from <= acquired_to, 'Start timestamp of acquisition window must come before its end' + if min_resolution is not None: + assert min_resolution > 0, 'Minimum resolution must be positive' + if max_resolution is not None: + assert max_resolution > 0, 'Maximum resolution must be positive' + if min_resolution is not None and max_resolution is not None: + assert min_resolution <= max_resolution, 'Mininum resolution must be lower than maximum resolution' + if min_cloud_coverage is not None: + assert min_cloud_coverage >= 0, 'Minimum cloud coverage must be non negative' + if max_cloud_coverage is not None: + assert max_cloud_coverage >= 0, 'Maximum cloud coverage must be non negative' + if min_cloud_coverage is not None and max_cloud_coverage is not None: + assert min_cloud_coverage <= max_cloud_coverage, 'Minimum cloud coverage must be lower than maximum cloud coverage' + if min_valid_data_coverage is not None: + assert min_valid_data_coverage >= 0, 'Minimum valid data coverage must be non negative' + if max_valid_data_coverage is not None: + assert max_valid_data_coverage >= 0, 'Maximum valid data coverage must be non negative' + if min_valid_data_coverage is not None and max_valid_data_coverage is not None: + assert min_valid_data_coverage <= max_valid_data_coverage, 'Minimum valid data coverage must be lower than maximum valid data coverage' + + # prepare params + params = dict(appid=self.API_key, polyid=polygon_id, start=acquired_from, end=acquired_to) + if min_resolution is not None: + params['resolution_min'] = min_resolution + if max_resolution is not None: + params['resolution_max'] = max_resolution + if acquired_by is not None: + params['type'] = acquired_by + if min_cloud_coverage is not None: + params['clouds_min'] = min_cloud_coverage + if max_cloud_coverage is not None: + params['clouds_max'] = max_cloud_coverage + if min_valid_data_coverage is not None: + params['coverage_min'] = min_valid_data_coverage + if max_valid_data_coverage is not None: + params['coverage_max'] = max_valid_data_coverage + + # call API + status, data = self.http_client.get_json(SATELLITE_IMAGERY_SEARCH_URI, params=params) + + result_set = SatelliteImagerySearchResultSet(polygon_id, data, timeutils.now(timeformat='unix')) + + # further filter by img_type and/or preset (if specified) + if img_type is not None and preset is not None: + return result_set.with_img_type_and_preset(img_type, preset) + elif img_type is not None: + return result_set.with_img_type(img_type) + elif preset is not None: + return result_set.with_preset(preset) + else: + return result_set.all() + + def download_satellite_image(self, metaimage, x=None, y=None, zoom=None, palette=None): + """ + Downloads the satellite image described by the provided metadata. In case the satellite image is a tile, then + tile coordinates and zoom must be provided. An optional palette ID can be provided, if supported by the + downloaded preset (currently only NDVI is supported) + + :param metaimage: the satellite image's metadata, in the form of a `MetaImage` subtype instance + :type metaimage: a `pyowm.agroapi10.imagery.MetaImage` subtype + :param x: x tile coordinate (only needed in case you are downloading a tile image) + :type x: int or `None` + :param y: y tile coordinate (only needed in case you are downloading a tile image) + :type y: int or `None` + :param zoom: zoom level (only needed in case you are downloading a tile image) + :type zoom: int or `None` + :param palette: ID of the color palette of the downloaded images. Values are provided by `pyowm.agroapi10.enums.PaletteEnum` + :type palette: str or `None` + :return: a `pyowm.agroapi10.imagery.SatelliteImage` instance containing both image's metadata and data + """ + if palette is not None: + assert isinstance(palette, str) + params = dict(paletteid=palette) + else: + palette = PaletteEnum.GREEN + params = dict() + # polygon PNG + if isinstance(metaimage, MetaPNGImage): + prepared_url = metaimage.url + status, data = self.http_client.get_png( + prepared_url, params=params) + img = Image(data, metaimage.image_type) + return SatelliteImage(metaimage, img, downloaded_on=timeutils.now(timeformat='unix'), palette=palette) + # GeoTIF + elif isinstance(metaimage, MetaGeoTiffImage): + prepared_url = metaimage.url + status, data = self.http_client.get_geotiff( + prepared_url, params=params) + img = Image(data, metaimage.image_type) + return SatelliteImage(metaimage, img, downloaded_on=timeutils.now(timeformat='unix'), palette=palette) + # tile PNG + elif isinstance(metaimage, MetaTile): + assert x is not None + assert y is not None + assert zoom is not None + prepared_url = self._fill_url(metaimage.url, x, y, zoom) + status, data = self.http_client.get_png( + prepared_url, params=params) + img = Image(data, metaimage.image_type) + tile = Tile(x, y, zoom, None, img) + return SatelliteImage(metaimage, tile, downloaded_on=timeutils.now(timeformat='unix'), palette=palette) + else: + raise ValueError("Cannot download: unsupported MetaImage subtype") + + def stats_for_satellite_image(self, metaimage): + """ + Retrieves statistics for the satellite image described by the provided metadata. + This is currently only supported 'EVI' and 'NDVI' presets + + :param metaimage: the satellite image's metadata, in the form of a `MetaImage` subtype instance + :type metaimage: a `pyowm.agroapi10.imagery.MetaImage` subtype + :return: dict + """ + if metaimage.preset != PresetEnum.EVI and metaimage.preset != PresetEnum.NDVI: + raise ValueError("Unsupported image preset: should be EVI or NDVI") + if metaimage.stats_url is None: + raise ValueError("URL for image statistics is not defined") + status, data = self.http_client.get_json(metaimage.stats_url, params={}) + return data + + # Utilities + def _fill_url(self, url_template, x, y, zoom): + return url_template.replace('{x}', str(x)).replace('{y}', str(y)).replace('{z}', str(zoom)) + + def __repr__(self): + return '<%s.%s>' % (__name__, self.__class__.__name__) diff --git a/pyowm/agroapi10/enums.py b/pyowm/agroapi10/enums.py new file mode 100644 index 00000000..e41d313c --- /dev/null +++ b/pyowm/agroapi10/enums.py @@ -0,0 +1,72 @@ +from pyowm.commons.databoxes import Satellite + + +class PresetEnum: + """ + Allowed presets for satellite images on Agro API 1.0 + + """ + TRUE_COLOR = 'truecolor' + FALSE_COLOR = 'falsecolor' + NDVI = 'ndvi' + EVI = 'evi' + + @classmethod + def items(cls): + """ + All values for this enum + :return: list of str + + """ + return [ + cls.TRUE_COLOR, + cls.FALSE_COLOR, + cls.NDVI, + cls.EVI + ] + + +class SatelliteEnum: + """ + Allowed presets for satellite names on Agro API 1.0 + + """ + LANDSAT_8 = Satellite('Landsat 8', 'l8') + SENTINEL_2 = Satellite('Sentinel-2', 's2') + + @classmethod + def items(cls): + """ + All values for this enum + :return: list of str + + """ + return [ + cls.LANDSAT_8, + cls.SENTINEL_2 + ] + + +class PaletteEnum: + """ + Allowed color palettes for satellite images on Agro API 1.0 + + """ + GREEN = '1' # default Agro API 1.0 palette + BLACK_AND_WHITE = '2' + CONTRAST_SHIFTED = '3' + CONTRAST_CONTINUOUS = '4' + + @classmethod + def items(cls): + """ + All values for this enum + :return: list of str + + """ + return [ + cls.GREEN, + cls.BLACK_AND_WHITE, + cls.CONTRAST_SHIFTED, + cls.CONTRAST_CONTINUOUS + ] diff --git a/pyowm/agroapi10/imagery.py b/pyowm/agroapi10/imagery.py new file mode 100644 index 00000000..c834e42a --- /dev/null +++ b/pyowm/agroapi10/imagery.py @@ -0,0 +1,159 @@ +from pyowm.utils import timeformatutils +from pyowm.commons.enums import ImageTypeEnum +from pyowm.commons.image import Image +from pyowm.commons.tile import Tile + + +class MetaImage: + """ + A class representing metadata for a satellite-acquired image + + :param url: the public URL of the image + :type url: str + :param preset: the preset of the image (supported values are listed by `pyowm.agroapi10.enums.PresetEnum`) + :type preset: str + :param satellite_name: the name of the satellite that acquired the image (supported values are listed + by `pyowm.agroapi10.enums.SatelliteEnum`) + :type satellite_name: str + :param acquisition_time: the UTC Unix epoch when the image was acquired + :type acquisition_time: int + :param valid_data_percentage: approximate percentage of valid data coverage + :type valid_data_percentage: float + :param cloud_coverage_percentage: approximate percentage of cloud coverage on the scene + :type cloud_coverage_percentage: float + :param sun_azimuth: sun azimuth angle at scene acquisition time + :type sun_azimuth: float + :param sun_elevation: sun zenith angle at scene acquisition time + :type sun_elevation: float + :param polygon_id: optional id of the polygon the image refers to + :type polygon_id: str + :param stats_url: the public URL of the image statistics, if available + :type stats_url: str or `None` + :returns: an `MetaImage` object + """ + + image_type = None + + def __init__(self, url, preset, satellite_name, acquisition_time, + valid_data_percentage, cloud_coverage_percentage, sun_azimuth, sun_elevation, polygon_id=None, + stats_url=None): + assert isinstance(url, str) + self.url = url + self.preset = preset + self.satellite_name = satellite_name + assert isinstance(acquisition_time, int) + assert acquisition_time >= 0, 'acquisition_time cannot be negative' + self._acquisition_time = acquisition_time + assert isinstance(valid_data_percentage, float) or isinstance(valid_data_percentage, int) + assert valid_data_percentage >= 0., 'valid_data_percentage cannot be negative' + self.valid_data_percentage = valid_data_percentage + assert isinstance(cloud_coverage_percentage, float) or isinstance(cloud_coverage_percentage, int) + assert cloud_coverage_percentage >= 0., 'cloud_coverage_percentage cannot be negative' + self.cloud_coverage_percentage = cloud_coverage_percentage + assert isinstance(sun_azimuth, float) or isinstance(sun_azimuth, int) + assert sun_azimuth >= 0. and sun_azimuth <= 360., 'sun_azimuth must be between 0 and 360 degrees' + self.sun_azimuth = sun_azimuth + assert isinstance(sun_elevation, float) or isinstance(sun_elevation, int) + assert sun_elevation >= 0. and sun_elevation <= 90., 'sun_elevation must be between 0 and 90 degrees' + self.sun_elevation = sun_elevation + self.polygon_id = polygon_id + self.stats_url = stats_url + + def acquisition_time(self, timeformat='unix'): + """Returns the UTC time telling when the image data was acquired by the satellite + + :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 + + """ + return timeformatutils.timeformat(self._acquisition_time, timeformat) + + def __repr__(self): + return "<%s.%s - %s %s image acquired at %s by %s on polygon with id=%s>" % ( + __name__, self.__class__.__name__, + self.image_type.name if self.image_type is not None else '', + self.preset, self.acquisition_time('iso'), self.satellite_name, + self.polygon_id if self.polygon_id is not None else 'None') + + +class MetaPNGImage(MetaImage): + """ + Class representing metadata for a satellite image of a polygon in PNG format + """ + image_type = ImageTypeEnum.PNG + + +class MetaTile(MetaImage): + """ + Class representing metadata for a tile in PNG format + """ + image_type = ImageTypeEnum.PNG + + +class MetaGeoTiffImage(MetaImage): + """ + Class representing metadata for a satellite image of a polygon in GeoTiff format + """ + image_type = ImageTypeEnum.GEOTIFF + + +class SatelliteImage: + """ + Class representing a downloaded satellite image, featuring both metadata and data + + :param metadata: the metadata for this satellite image + :type metadata: a `pyowm.agro10.imagery.MetaImage` subtype instance + :param data: the actual data for this satellite image + :type data: either `pyowm.commons.image.Image` or `pyowm.commons.tile.Tile` object + :param downloaded_on: the UNIX epoch this satellite image was downloaded at + :type downloaded_on: int or `None` + :param palette: ID of the color palette of the downloaded images. Values are provided by `pyowm.agroapi10.enums.PaletteEnum` + :type palette: str or `None` + :returns: a `pyowm.agroapi10.imagery.SatelliteImage` instance + """ + + def __init__(self, metadata, data, downloaded_on=None, palette=None): + assert isinstance(metadata, MetaImage) + self.metadata = metadata + assert isinstance(data, Image) or isinstance(data, Tile) + self.data = data + if downloaded_on is not None: + assert isinstance(downloaded_on, int) + self._downloaded_on = downloaded_on + if palette is not None: + assert isinstance(palette, str) + self.palette = palette + + def downloaded_on(self, timeformat='unix'): + """Returns the UTC time telling when the satellite image was downloaded from the OWM Agro API + + :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 + + """ + return timeformatutils.timeformat(self._downloaded_on, timeformat) + + def persist(self, path_to_file): + """ + Saves the satellite image to disk on a file + + :param path_to_file: path to the target file + :type path_to_file: str + :return: `None` + """ + self.data.persist(path_to_file) + + + def __repr__(self): + return "<%s.%s - %s %s satellite image downloaded on: %s>" % ( + __name__, self.__class__.__name__, + self.metadata.preset, self.metadata.satellite_name, + self.downloaded_on('iso') if self._downloaded_on is not None else 'None') diff --git a/pyowm/agroapi10/polygon.py b/pyowm/agroapi10/polygon.py new file mode 100644 index 00000000..95fe397d --- /dev/null +++ b/pyowm/agroapi10/polygon.py @@ -0,0 +1,68 @@ +from pyowm.utils.geo import Polygon as GeoPolygon +from pyowm.utils.geo import Point as GeoPoint +from pyowm.utils.geo import GeometryBuilder + + +class Polygon: + + """ + A Polygon feature, foundational element for all Agro API operations + + :param id: the polygon's ID + :type id: str + :param name: the polygon's name + :type namr: str + :param geopolygon: the `pyowm.utils.geo.Polygon` instance that represents this polygon + :type geopolygon: `pyowm.utils.geo.Polygon` + :param center: the `pyowm.utils.geo.Point` instance that represents the central point of the polygon + :type center: `pyowm.utils.geo.Point` + :param area: the area of the polygon in hectares + :type area: float or int + :param user_id: the ID of the user owning this polygon + :type user_id: str + :returns: a `Polygon` instance + :raises: `AssertionError` when either id is `None` or geopolygon, center or area have wrong type + """ + + def __init__(self, id, name=None, geopolygon=None, center=None, area=None, user_id=None): + + assert id is not None, 'Polygon ID cannot be None' + if geopolygon is not None: + assert isinstance(geopolygon, GeoPolygon), 'Polygon must be a valid geopolygon type' + if center is not None: + assert isinstance(center, GeoPoint), 'Polygon center must be a valid geopoint type' + if area is not None: + assert isinstance(area, float) or isinstance(area, int), 'Area must be a numeric type' + assert area >= 0, 'Area must not be negative' + self.id = id + self.name = name + self.geopolygon = geopolygon + self.center = center + self.area = area + self.user_id = user_id + + @property + def area_km(self): + if self.area: + return self.area * 0.01 + return None + + @classmethod + def from_dict(cls, the_dict): + assert isinstance(the_dict, dict) + the_id = the_dict.get('id', None) + geojson = the_dict.get('geo_json', {}).get('geometry', None) + name = the_dict.get('name', None) + center = the_dict.get('center', None) + area = the_dict.get('area', None) + user_id =the_dict.get('user_id', None) + geopolygon = GeometryBuilder.build(geojson) + try: + center = GeoPoint(center[0], center[1]) + except: + raise ValueError('Wrong format for polygon center coordinates') + return Polygon(the_id, name, geopolygon, center, area, user_id) + + def __repr__(self): + return "<%s.%s - id=%s, name=%s, area=%s>" % (__name__, \ + self.__class__.__name__, self.id, self.name, str(self.area)) diff --git a/pyowm/agroapi10/search.py b/pyowm/agroapi10/search.py new file mode 100644 index 00000000..8eaefe8b --- /dev/null +++ b/pyowm/agroapi10/search.py @@ -0,0 +1,206 @@ +from pyowm.utils import timeformatutils +from pyowm.agroapi10.imagery import MetaPNGImage, MetaTile, MetaGeoTiffImage +from pyowm.agroapi10.enums import PresetEnum +from pyowm.commons.databoxes import ImageType + + +class SatelliteImagerySearchResultSet: + """ + Class representing a filterable result set by a satellite imagery search against the Agro API 1.0. Each result + is a `pyowm.agroapi10.imagery.MetaImage` subtype instance + + """ + + def __init__(self, polygon_id, list_of_dict, query_timestamp): + """ + Parses raw data dict into a list of `pyowm.agroapi10.imagery.MetaImage` subtypes instances and stores that + list internally for further filtering + + :param polygon_id: the ID of the polygon that has been searched for images + :type polygon_id: str + :param list_of_dict: the input data dictionary + :type list_of_dict: list + :param query_timestamp: UNIX timestamp of the query + :type query_timestamp: int + :returns: a `pyowm.agroapi10.imagery.SatelliteImagerySearchResultSet` instance or an exception is parsing fails + + """ + assert isinstance(polygon_id, str) + self.polygon_id = polygon_id + + assert isinstance(list_of_dict, list) + + assert isinstance(query_timestamp, int) + self.query_timestamp = query_timestamp + + # parse raw data + result = list() + for the_dict in list_of_dict: + # common metadata + acquisition_time = the_dict.get('dt', None) + satellite_name = the_dict.get('type', None) + valid_data_percentage = the_dict.get('dc', None) + cloud_coverage_percentage = the_dict.get('cl', None) + sun = the_dict.get('sun', dict()) + sun_azimuth = sun.get('azimuth', None) + sun_elevation = sun.get('elevation', None) + + # Stats for the images + stats_dict = the_dict.get('stats', dict()) + stats_url_for_ndvi = stats_dict.get('ndvi', None) + stats_url_for_evi = stats_dict.get('evi', None) + + # PNG images for the polygon + png_dict = the_dict.get('image', dict()) + true_color_png_url = png_dict.get('truecolor', None) + false_color_png_url = png_dict.get('falsecolor', None) + ndvi_png_url = png_dict.get('ndvi', None) + evi_png_url = png_dict.get('evi', None) + if true_color_png_url is not None: + result.append( + MetaPNGImage(true_color_png_url, PresetEnum.TRUE_COLOR, satellite_name, acquisition_time, + valid_data_percentage, cloud_coverage_percentage, sun_azimuth, sun_elevation, + polygon_id=polygon_id)) + if false_color_png_url is not None: + result.append( + MetaPNGImage(false_color_png_url, PresetEnum.FALSE_COLOR, satellite_name, acquisition_time, + valid_data_percentage, cloud_coverage_percentage, sun_azimuth, sun_elevation, + polygon_id=polygon_id)) + if ndvi_png_url is not None: + result.append( + MetaPNGImage(ndvi_png_url, PresetEnum.NDVI, satellite_name, acquisition_time, + valid_data_percentage, cloud_coverage_percentage, sun_azimuth, sun_elevation, + polygon_id=polygon_id, stats_url=stats_url_for_ndvi)) + if evi_png_url is not None: + result.append( + MetaPNGImage(evi_png_url, PresetEnum.EVI, satellite_name, acquisition_time, + valid_data_percentage, cloud_coverage_percentage, sun_azimuth, sun_elevation, + polygon_id=polygon_id, stats_url=stats_url_for_evi)) + + # Tiles for the polygon + tiles_dict = the_dict.get('tile', dict()) + true_color_tile_url = tiles_dict.get('truecolor', None) + false_color_tile_url = tiles_dict.get('falsecolor', None) + ndvi_tile_url = tiles_dict.get('ndvi', None) + evi_tile_url = tiles_dict.get('evi', None) + if true_color_tile_url is not None: + result.append( + MetaTile(true_color_tile_url, PresetEnum.TRUE_COLOR, satellite_name, acquisition_time, + valid_data_percentage, cloud_coverage_percentage, sun_azimuth, sun_elevation, + polygon_id=polygon_id)) + if false_color_tile_url is not None: + result.append( + MetaTile(false_color_tile_url, PresetEnum.FALSE_COLOR, satellite_name, acquisition_time, + valid_data_percentage, cloud_coverage_percentage, sun_azimuth, sun_elevation, + polygon_id=polygon_id)) + if ndvi_tile_url is not None: + result.append( + MetaTile(ndvi_tile_url, PresetEnum.NDVI, satellite_name, acquisition_time, + valid_data_percentage, cloud_coverage_percentage, sun_azimuth, sun_elevation, + polygon_id=polygon_id, stats_url=stats_url_for_ndvi)) + if evi_tile_url is not None: + result.append( + MetaTile(evi_tile_url, PresetEnum.EVI, satellite_name, acquisition_time, + valid_data_percentage, cloud_coverage_percentage, sun_azimuth, sun_elevation, + polygon_id=polygon_id, stats_url=stats_url_for_evi)) + + # geoTiff images for the polygon + geotiff_dict = the_dict.get('data', dict()) + true_color_geotiff_url = geotiff_dict.get('truecolor', None) + false_color_geotiff_url = geotiff_dict.get('falsecolor', None) + ndvi_geotiff_url = geotiff_dict.get('ndvi', None) + evi_geotiff_url = geotiff_dict.get('evi', None) + if true_color_geotiff_url is not None: + result.append( + MetaGeoTiffImage(true_color_geotiff_url, PresetEnum.TRUE_COLOR, satellite_name, + acquisition_time, valid_data_percentage, cloud_coverage_percentage, + sun_azimuth, sun_elevation, polygon_id=polygon_id)) + if false_color_geotiff_url is not None: + result.append( + MetaGeoTiffImage(false_color_geotiff_url, PresetEnum.FALSE_COLOR, satellite_name, + acquisition_time, valid_data_percentage, cloud_coverage_percentage, + sun_azimuth, sun_elevation, polygon_id=polygon_id)) + if ndvi_geotiff_url is not None: + result.append( + MetaGeoTiffImage(ndvi_geotiff_url, PresetEnum.NDVI, satellite_name, + acquisition_time, valid_data_percentage, cloud_coverage_percentage, + sun_azimuth, sun_elevation, polygon_id=polygon_id, stats_url=stats_url_for_ndvi)) + if evi_geotiff_url is not None: + result.append( + MetaGeoTiffImage(evi_geotiff_url, PresetEnum.EVI, satellite_name, + acquisition_time, valid_data_percentage, cloud_coverage_percentage, + sun_azimuth, sun_elevation, polygon_id=polygon_id, stats_url=stats_url_for_evi)) + self.metaimages = result + + def issued_on(self, timeformat='unix'): + """Returns the UTC time telling when the query was performed against the OWM Agro API + + :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 + + """ + return timeformatutils.timeformat(self.query_timestamp, timeformat) + + def __len__(self): + return len(self.metaimages) + + def __repr__(self): + return '<%s.%s - %s results for query issued on polygon_id=%s at %s>' % ( + __name__, self.__class__.__name__, + len(self), self.polygon_id, self.issued_on(timeformat='iso')) + + def all(self): + """ + Returns all search results + + :returns: a list of `pyowm.agroapi10.imagery.MetaImage` instances + + """ + return self.metaimages + + def with_img_type(self, image_type): + """ + Returns the search results having the specified image type + + :param image_type: the desired image type (valid values are provided by the + `pyowm.commons.enums.ImageTypeEnum` enum) + :type image_type: `pyowm.commons.databoxes.ImageType` instance + :returns: a list of `pyowm.agroapi10.imagery.MetaImage` instances + + """ + assert isinstance(image_type, ImageType) + return list(filter(lambda x: x.image_type == image_type, self.metaimages)) + + def with_preset(self, preset): + """ + Returns the seach results having the specified preset + + :param preset: the desired image preset (valid values are provided by the + `pyowm.agroapi10.enums.PresetEnum` enum) + :type preset: str + :returns: a list of `pyowm.agroapi10.imagery.MetaImage` instances + + """ + assert isinstance(preset, str) + return list(filter(lambda x: x.preset == preset, self.metaimages)) + + def with_img_type_and_preset(self, image_type, preset): + """ + Returns the search results having both the specified image type and preset + + :param image_type: the desired image type (valid values are provided by the + `pyowm.commons.enums.ImageTypeEnum` enum) + :type image_type: `pyowm.commons.databoxes.ImageType` instance + :param preset: the desired image preset (valid values are provided by the + `pyowm.agroapi10.enums.PresetEnum` enum) + :type preset: str + :returns: a list of `pyowm.agroapi10.imagery.MetaImage` instances + + """ + assert isinstance(image_type, ImageType) + assert isinstance(preset, str) + return list(filter(lambda x: x.image_type == image_type and x.preset == preset, self.metaimages)) diff --git a/pyowm/agroapi10/soil.py b/pyowm/agroapi10/soil.py new file mode 100644 index 00000000..19e6e3a4 --- /dev/null +++ b/pyowm/agroapi10/soil.py @@ -0,0 +1,105 @@ +from pyowm.utils import timeformatutils, temputils + + +class Soil: + + """ + Soil data over a specific Polygon + + :param reference_time: UTC UNIX time of soil data measurement + :type reference_time: int + :param surface_temp: soil surface temperature in Kelvin degrees + :type surface_temp: float + :param ten_cm_temp: soil temperature at 10 cm depth in Kelvin degrees + :type ten_cm_temp: float + :param moisture: soil moisture in m^3/m^3 + :type moisture: float + :param polygon_id: ID of the polygon this soil data was measured upon + :type polygon_id: str + :returns: a `Soil` instance + :raises: `AssertionError` when any of the mandatory fields is `None` or has wrong type + """ + + def __init__(self, reference_time, surface_temp, ten_cm_temp, moisture, polygon_id=None): + assert reference_time is not None + assert isinstance(reference_time, int), 'reference time must be a UNIX int timestamp' + if reference_time < 0: + raise ValueError("reference_time must be greater than 0") + self._reference_time = reference_time + assert surface_temp is not None + assert isinstance(surface_temp, float), 'surface_temp must be a float' + self._surface_temp = surface_temp + assert ten_cm_temp is not None + assert isinstance(ten_cm_temp, float), 'ten_cm_temp must be a float' + self._ten_cm_temp = ten_cm_temp + assert moisture is not None + assert isinstance(moisture, float), 'moisture must be a float' + if moisture < 0.: + raise ValueError("moisture must be greater than 0") + self.moisture = moisture + self.polygon_id = polygon_id + + def reference_time(self, timeformat='unix'): + """Returns the UTC time telling when the soil data was measured + + :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 + + """ + return timeformatutils.timeformat(self._reference_time, timeformat) + + def surface_temp(self, unit='kelvin'): + """Returns the soil surface temperature + + :param unit: the unit of measure for the temperature value. May be: + '*kelvin*' (default), '*celsius*' or '*fahrenheit*' + :type unit: str + :returns: a float + :raises: ValueError when unknown temperature units are provided + + """ + if unit == 'kelvin': + return self._surface_temp + if unit == 'celsius': + return temputils.kelvin_to_celsius(self._surface_temp) + if unit == 'fahrenheit': + return temputils.kelvin_to_fahrenheit(self._surface_temp) + else: + raise ValueError('Wrong temperature unit') + + def ten_cm_temp(self, unit='kelvin'): + """Returns the soil temperature measured 10 cm below surface + + :param unit: the unit of measure for the temperature value. May be: + '*kelvin*' (default), '*celsius*' or '*fahrenheit*' + :type unit: str + :returns: a float + :raises: ValueError when unknown temperature units are provided + + """ + if unit == 'kelvin': + return self._ten_cm_temp + if unit == 'celsius': + return temputils.kelvin_to_celsius(self._ten_cm_temp) + if unit == 'fahrenheit': + return temputils.kelvin_to_fahrenheit(self._ten_cm_temp) + else: + raise ValueError('Wrong temperature unit') + + @classmethod + def from_dict(cls, the_dict): + assert isinstance(the_dict, dict) + reference_time = the_dict['reference_time'] + surface_temp = the_dict['surface_temp'] + ten_cm_temp = the_dict['ten_cm_temp'] + moisture = the_dict['moisture'] + polygon_id = the_dict.get('polygon_id', None) + return Soil(reference_time, surface_temp, ten_cm_temp, moisture, polygon_id) + + def __repr__(self): + return "<%s.%s - polygon_id=%s,reference time=%s,>" % (__name__, self.__class__.__name__, + self.polygon_id, self.reference_time('iso')) diff --git a/pyowm/agroapi10/uris.py b/pyowm/agroapi10/uris.py new file mode 100644 index 00000000..845d8bb4 --- /dev/null +++ b/pyowm/agroapi10/uris.py @@ -0,0 +1,15 @@ +""" +URIs templates for resources exposed by the Agro API 1.0 +""" + +ROOT_AGRO_API = 'http://api.agromonitoring.com/agro/1.0' + +# Polygons API subset +POLYGONS_URI = ROOT_AGRO_API + '/polygons' +NAMED_POLYGON_URI = ROOT_AGRO_API + '/polygons/%s' + +# Soil API subset +SOIL_URI = ROOT_AGRO_API + '/soil' + +# Satellite Imagery Search API subset +SATELLITE_IMAGERY_SEARCH_URI = ROOT_AGRO_API + '/image/search' diff --git a/pyowm/alertapi30/alert.py b/pyowm/alertapi30/alert.py index 3027ab62..4d04eb78 100644 --- a/pyowm/alertapi30/alert.py +++ b/pyowm/alertapi30/alert.py @@ -35,11 +35,11 @@ class Alert: """ def __init__(self, id, trigger_id, met_conditions, coordinates, last_update=None): assert id is not None - stringutils.assert_is_string_or_unicode(id) + assert isinstance(id, str), "Value must be a string" self.id = id assert trigger_id is not None - stringutils.assert_is_string_or_unicode(trigger_id) + assert isinstance(trigger_id, str), "Value must be a string" self.trigger_id = trigger_id assert met_conditions is not None diff --git a/pyowm/alertapi30/alert_manager.py b/pyowm/alertapi30/alert_manager.py index f67fb3ae..47dcd036 100644 --- a/pyowm/alertapi30/alert_manager.py +++ b/pyowm/alertapi30/alert_manager.py @@ -3,7 +3,6 @@ from pyowm.alertapi30.parsers import TriggerParser, AlertParser from pyowm.alertapi30.uris import TRIGGERS_URI, NAMED_TRIGGER_URI, ALERTS_URI, NAMED_ALERT_URI from pyowm.utils import timeformatutils, timeutils -from pyowm.utils import stringutils class AlertManager: @@ -12,7 +11,7 @@ class AlertManager: A manager objects that provides a full interface to OWM Alert API. It implements CRUD methods on Trigger entities and read/deletion of related Alert objects - :param API_key: the OWM web API key + :param API_key: the OWM Weather API key :type API_key: str :returns: an *AlertManager* instance :raises: *AssertionError* when no API Key is provided @@ -111,7 +110,7 @@ def get_trigger(self, trigger_id): :type trigger_id: str :return: a `pyowm.alertapi30.trigger.Trigger` instance """ - stringutils.assert_is_string_or_unicode(trigger_id) + assert isinstance(trigger_id, str), "Value must be a string" status, data = self.http_client.get_json( NAMED_TRIGGER_URI % trigger_id, params={'appid': self.API_key}, @@ -128,7 +127,7 @@ def update_trigger(self, trigger): :return: ``None`` if update is successful, an error otherwise """ assert trigger is not None - stringutils.assert_is_string_or_unicode(trigger.id) + assert isinstance(trigger.id, str), "Value must be a string" the_time_period = { "start": { "expression": "after", @@ -158,7 +157,7 @@ def delete_trigger(self, trigger): :returns: `None` if deletion is successful, an exception otherwise """ assert trigger is not None - stringutils.assert_is_string_or_unicode(trigger.id) + assert isinstance(trigger.id, str), "Value must be a string" status, _ = self.http_client.delete( NAMED_TRIGGER_URI % trigger.id, params={'appid': self.API_key}, @@ -174,7 +173,7 @@ def get_alerts_for(self, trigger): :return: list of `pyowm.alertapi30.alert.Alert` objects """ assert trigger is not None - stringutils.assert_is_string_or_unicode(trigger.id) + assert isinstance(trigger.id, str), "Value must be a string" status, data = self.http_client.get_json( ALERTS_URI % trigger.id, params={'appid': self.API_key}, @@ -193,8 +192,8 @@ def get_alert(self, alert_id, trigger): """ assert trigger is not None assert alert_id is not None - stringutils.assert_is_string_or_unicode(alert_id) - stringutils.assert_is_string_or_unicode(trigger.id) + assert isinstance(alert_id, str), "Value must be a string" + assert isinstance(trigger.id, str), "Value must be a string" status, data = self.http_client.get_json( NAMED_ALERT_URI % (trigger.id, alert_id), params={'appid': self.API_key}, @@ -209,7 +208,7 @@ def delete_all_alerts_for(self, trigger): :return: `None` if deletion is successful, an exception otherwise """ assert trigger is not None - stringutils.assert_is_string_or_unicode(trigger.id) + assert isinstance(trigger.id, str), "Value must be a string" status, _ = self.http_client.delete( ALERTS_URI % trigger.id, params={'appid': self.API_key}, @@ -223,8 +222,8 @@ def delete_alert(self, alert): :return: ``None`` if the deletion was successful, an error otherwise """ assert alert is not None - stringutils.assert_is_string_or_unicode(alert.id) - stringutils.assert_is_string_or_unicode(alert.trigger_id) + assert isinstance(alert.id, str), "Value must be a string" + assert isinstance(alert.trigger_id, str), "Value must be a string" status, _ = self.http_client.delete( NAMED_ALERT_URI % (alert.trigger_id, alert.id), params={'appid': self.API_key}, diff --git a/pyowm/alertapi30/condition.py b/pyowm/alertapi30/condition.py index 4369b9a6..ab7fb6ff 100644 --- a/pyowm/alertapi30/condition.py +++ b/pyowm/alertapi30/condition.py @@ -22,11 +22,11 @@ class Condition: """ def __init__(self, weather_param, operator, amount, id=None): assert weather_param is not None - stringutils.assert_is_string_or_unicode(weather_param) + assert isinstance(weather_param, str), "Value must be a string" self.weather_param = weather_param assert operator is not None - stringutils.assert_is_string_or_unicode(operator) + assert isinstance(operator, str), "Value must be a string" self.operator = operator assert amount is not None diff --git a/pyowm/alertapi30/enums.py b/pyowm/alertapi30/enums.py index f846a5ff..8292e8c3 100644 --- a/pyowm/alertapi30/enums.py +++ b/pyowm/alertapi30/enums.py @@ -13,19 +13,20 @@ class WeatherParametersEnum: WIND_DIRECTION = 'wind_direction' CLOUDS = 'clouds' # Coverage % - def items(self): + @classmethod + def items(cls): """ All values for this enum - :return: list of tuples + :return: list of str """ return [ - ('TEMPERATURE', self.TEMPERATURE), - ('PRESSURE', self.PRESSURE), - ('HUMIDITY', self.HUMIDITY), - ('WIND_SPEED', self.WIND_SPEED), - ('WIND_DIRECTION', self.WIND_DIRECTION), - ('CLOUDS', self.CLOUDS) + cls.TEMPERATURE, + cls.PRESSURE, + cls.HUMIDITY, + cls.WIND_SPEED, + cls.WIND_DIRECTION, + cls.CLOUDS ] @@ -41,19 +42,20 @@ class OperatorsEnum: EQUAL = '$eq' NOT_EQUAL = '$ne' - def items(self): + @classmethod + def items(cls): """ All values for this enum - :return: list of tuples + :return: list of str """ return [ - ('GREATER_THAN', self.GREATER_THAN), - ('GREATER_THAN_EQUAL', self.GREATER_THAN_EQUAL), - ('LESS_THAN', self.LESS_THAN), - ('LESS_THAN_EQUAL', self.LESS_THAN_EQUAL), - ('EQUAL', self.EQUAL), - ('NOT_EQUAL', self.NOT_EQUAL) + cls.GREATER_THAN, + cls.GREATER_THAN_EQUAL, + cls.LESS_THAN, + cls.LESS_THAN_EQUAL, + cls.EQUAL, + cls.NOT_EQUAL ] @@ -64,12 +66,13 @@ class AlertChannelsEnum: """ OWM_API_POLLING = AlertChannel('OWM API POLLING') - def items(self): + @classmethod + def items(cls): """ All values for this enum - :return: list of tuples + :return: list of str """ return [ - ('OWM_API_POLLING', self.OWM_API_POLLING) - ] \ No newline at end of file + cls.OWM_API_POLLING + ] diff --git a/pyowm/caches/lrucache.py b/pyowm/caches/lrucache.py index f095ec7f..eb8d417f 100644 --- a/pyowm/caches/lrucache.py +++ b/pyowm/caches/lrucache.py @@ -11,7 +11,7 @@ class LRUCache(owmcache.OWMCache): """ This cache is made out of a 'table' dict and the 'usage_recency' linked list.'table' maps uses requests' URLs as keys and stores JSON raw responses - as values. 'usage_recency' tracks down the "recency" of the OWM web API + as values. 'usage_recency' tracks down the "recency" of the OWM Weather API requests: the more recent a request, the more the element will be far from the "death" point of the recency list. Items in 'usage_recency' are the requests' URLs themselves. @@ -22,7 +22,7 @@ class LRUCache(owmcache.OWMCache): timestamp is compared to the current one: if the difference is higher than a prefixed value, then the lookup is considered a MISS: the element is removed either from 'table' and from 'usage_recency' and must - be requested again to the OWM web API. If the time difference is ok, + be requested again to the OWM Weather API. If the time difference is ok, then the lookup is considered a HIT. - when a GET results in a HIT, promote the element to the front of the recency list updating its cache insertion timestamp and return the @@ -34,7 +34,7 @@ class LRUCache(owmcache.OWMCache): timestamp and finally add it at the front of the recency list. :param cache_max_size: the maximum size of the cache in terms of cached - OWM web API responses. A reasonable default value is provided. + OWM Weather API responses. A reasonable default value is provided. :type cache_max_size: int :param item_lifetime_millis: the maximum lifetime allowed for a cache item in milliseconds. A reasonable default value is provided. diff --git a/pyowm/caches/nullcache.py b/pyowm/caches/nullcache.py index bb09f342..17114256 100644 --- a/pyowm/caches/nullcache.py +++ b/pyowm/caches/nullcache.py @@ -1,5 +1,5 @@ """ -Module containing a null-object cache for OWM web API responses +Module containing a null-object cache for OWM Weather API responses """ from pyowm.abstractions import owmcache diff --git a/pyowm/commons/databoxes.py b/pyowm/commons/databoxes.py new file mode 100644 index 00000000..c0f01b29 --- /dev/null +++ b/pyowm/commons/databoxes.py @@ -0,0 +1,37 @@ +class ImageType: + """ + Databox class representing an image type + + :param name: the image type name + :type name: str + :param mime_type: the image type MIME type + :type mime_type: str + """ + def __init__(self, name, mime_type): + + self.name = name + self.mime_type = mime_type + + def __repr__(self): + return "<%s.%s - name=%s mime=%s>" % ( + __name__, self.__class__.__name__, self.name, self.mime_type) + + +class Satellite: + """ + Databox class representing a satellite + + :param name: the satellite + :type name: str + :param symbol: the short name of the satellite + :type symbol: str + """ + def __init__(self, name, symbol): + + self.name = name + self.symbol = symbol + + def __repr__(self): + return "<%s.%s - name=%s symbol=%s>" % ( + __name__, self.__class__.__name__, self.name, self.symbol) + diff --git a/pyowm/commons/enums.py b/pyowm/commons/enums.py new file mode 100644 index 00000000..94732964 --- /dev/null +++ b/pyowm/commons/enums.py @@ -0,0 +1,39 @@ +from pyowm.commons.databoxes import ImageType + + +class ImageTypeEnum: + """ + Allowed image types on OWM APIs + + """ + PNG = ImageType('PNG', 'image/png') + GEOTIFF = ImageType('GEOTIFF', 'image/tiff') + + @classmethod + def lookup_by_mime_type(cls, mime_type): + for i in ImageTypeEnum.items(): + if i.mime_type == mime_type: + return i + return None + + @classmethod + def lookup_by_name(cls, name): + for i in ImageTypeEnum.items(): + if i.name == name: + return i + return None + + @classmethod + def items(cls): + """ + All values for this enum + :return: list of `pyowm.commons.enums.ImageType` + + """ + return [ + cls.PNG, + cls.GEOTIFF + ] + + def __repr__(self): + return "<%s.%s>" % (__name__, self.__class__.__name__) diff --git a/pyowm/commons/http_client.py b/pyowm/commons/http_client.py index 636736f2..a794161a 100644 --- a/pyowm/commons/http_client.py +++ b/pyowm/commons/http_client.py @@ -1,8 +1,9 @@ import requests import json from pyowm.caches import nullcache +from pyowm.commons.enums import ImageTypeEnum from pyowm.exceptions import api_call_error, api_response_error, parse_response_error -from pyowm.webapi25.configuration25 import API_AVAILABILITY_TIMEOUT, \ +from pyowm.weatherapi25.configuration25 import API_AVAILABILITY_TIMEOUT, \ API_SUBSCRIPTION_SUBDOMAINS, VERIFY_SSL_CERTS @@ -35,6 +36,48 @@ def get_json(self, uri, params=None, headers=None): raise parse_response_error.ParseResponseError('Impossible to parse' 'API response data') + def get_png(self, uri, params=None, headers=None): + if headers is None: + headers = {'Accept': ImageTypeEnum.PNG.mime_type} + else: + headers.update({'Accept': ImageTypeEnum.PNG.mime_type}) + try: + resp = requests.get(uri, stream=True, params=params, headers=headers, + timeout=self.timeout, verify=self.verify_ssl_certs) + except requests.exceptions.SSLError as e: + raise api_call_error.APIInvalidSSLCertificateError(str(e)) + except requests.exceptions.ConnectionError as e: + raise api_call_error.APIInvalidSSLCertificateError(str(e)) + except requests.exceptions.Timeout: + raise api_call_error.APICallTimeoutError('API call timeouted') + HttpClient.check_status_code(resp.status_code, resp.text) + try: + return resp.status_code, resp.content + except: + raise parse_response_error.ParseResponseError('Impossible to parse' + 'API response data') + + def get_geotiff(self, uri, params=None, headers=None): + if headers is None: + headers = {'Accept': ImageTypeEnum.GEOTIFF.mime_type} + else: + headers.update({'Accept': ImageTypeEnum.GEOTIFF.mime_type}) + try: + resp = requests.get(uri, stream=True, params=params, headers=headers, + timeout=self.timeout, verify=self.verify_ssl_certs) + except requests.exceptions.SSLError as e: + raise api_call_error.APIInvalidSSLCertificateError(str(e)) + except requests.exceptions.ConnectionError as e: + raise api_call_error.APIInvalidSSLCertificateError(str(e)) + except requests.exceptions.Timeout: + raise api_call_error.APICallTimeoutError('API call timeouted') + HttpClient.check_status_code(resp.status_code, resp.text) + try: + return resp.status_code, resp.content + except: + raise parse_response_error.ParseResponseError('Impossible to parse' + 'API response data') + def cacheable_get_json(self, uri, params=None, headers=None): # check if already cached cached_url_key = requests.Request('GET', uri, params=params).prepare().url diff --git a/pyowm/commons/image.py b/pyowm/commons/image.py new file mode 100644 index 00000000..15bd9a18 --- /dev/null +++ b/pyowm/commons/image.py @@ -0,0 +1,48 @@ +from pyowm.commons.enums import ImageTypeEnum +from pyowm.commons.databoxes import ImageType + + +class Image: + + """ + Wrapper class for a generic image + + :param data: raw image data + :type data: bytes + :param image_type: the type of the image, if known + :type image_type: `pyowm.commons.databoxes.ImageType` or `None` + """ + + def __init__(self, data, image_type=None): + self.data = data + if image_type is not None: + assert isinstance(image_type, ImageType) + self.image_type = image_type + + def persist(self, path_to_file): + """ + Saves the image to disk on a file + + :param path_to_file: path to the target file + :type path_to_file: str + :return: `None` + """ + with open(path_to_file, 'wb') as f: + f.write(self.data) + + @classmethod + def load(cls, path_to_file): + """ + Loads the image data from a file on disk and tries to guess the image MIME type + + :param path_to_file: path to the source file + :type path_to_file: str + :return: a `pyowm.image.Image` instance + """ + import mimetypes + mimetypes.init() + mime = mimetypes.guess_type('file://%s' % path_to_file)[0] + img_type = ImageTypeEnum.lookup_by_mime_type(mime) + with open(path_to_file, 'rb') as f: + data = f.read() + return Image(data, image_type=img_type) diff --git a/pyowm/commons/tile.py b/pyowm/commons/tile.py new file mode 100644 index 00000000..2fbcc57c --- /dev/null +++ b/pyowm/commons/tile.py @@ -0,0 +1,107 @@ +import math +from pyowm.utils.geo import Polygon +from pyowm.commons.image import Image + + +class Tile: + + """ + Wrapper class for an image tile + :param x: horizontal tile number in OWM tile reference system + :type x: int + :param y: vertical tile number in OWM tile reference system + :type y: int + :param zoom: zoom level for the tile + :type zoom: int + :param map_layer: the name of the OWM map layer this tile belongs to + :type map_layer: str + :param image: raw image data + :type image: `pyowm.commons.image.Image instance` + """ + + def __init__(self, x, y, zoom, map_layer, image): + assert x >= 0, 'X tile coordinate cannot be negative' + self.x = x + assert y >= 0, 'Y tile coordinate cannot be negative' + self.y = y + assert zoom >= 0, 'Tile zoom level cannot be negative' + self.zoom = zoom + self.map_layer = map_layer + assert isinstance(image, Image), 'The provided image is in invalid format' + self.image = image + + def persist(self, path_to_file): + """ + Saves the tile to disk on a file + + :param path_to_file: path to the target file + :type path_to_file: str + :return: `None` + """ + self.image.persist(path_to_file) + + def bounding_polygon(self): + """ + Returns the bounding box polygon for this tile + + :return: `pywom.utils.geo.Polygon` instance + """ + lon_left, lat_bottom, lon_right, lat_top = Tile.tile_coords_to_bbox(self.x, self.y, self.zoom) + print(lon_left, lat_bottom, lon_right, lat_top) + return Polygon([[[lon_left, lat_top], + [lon_right, lat_top], + [lon_right, lat_bottom], + [lon_left, lat_bottom], + [lon_left, lat_top]]]) + + @classmethod + def tile_coords_for_point(cls, geopoint, zoom): + """ + Returns the coordinates of the tile containing the specified geopoint at the specified zoom level + + :param geopoint: the input geopoint instance + :type geopoint: `pywom.utils.geo.Point` + :param zoom: zoom level + :type zoom: int + :return: a tuple (x, y) containing the tile-coordinates + """ + return Tile.geoocoords_to_tile_coords(geopoint.lon, geopoint.lat, zoom) + + @classmethod + def geoocoords_to_tile_coords(cls, lon, lat, zoom): + """ + Calculates the tile numbers corresponding to the specified geocoordinates at the specified zoom level + Coordinates shall be provided in degrees and using the Mercator Projection (http://en.wikipedia.org/wiki/Mercator_projection) + + :param lon: longitude + :type lon: int or float + :param lat: latitude + :type lat: int or float + :param zoom: zoom level + :type zoom: int + :return: a tuple (x, y) containing the tile-coordinates + """ + n = 2.0 ** zoom + x = int((lon + 180.0) / 360.0 * n) + y = int((1.0 - math.log(math.tan(math.radians(lat)) + (1 / math.cos(math.radians(lat)))) / math.pi) / 2.0 * n) + return x, y + + @classmethod + def tile_coords_to_bbox(cls, x, y, zoom): + """ + Calculates the lon/lat estrema of the bounding box corresponding to specific tile coordinates. Output coodinates + are in degrees and in the Mercator Projection (http://en.wikipedia.org/wiki/Mercator_projection) + + :param x: the x tile coordinates + :param y: the y tile coordinates + :param zoom: the zoom level + :return: tuple with (lon_left, lat_bottom, lon_right, lat_top) + """ + def tile_to_geocoords(x, y, zoom): + n = 2. ** zoom + lon = x / n * 360. - 180. + lat = math.degrees(math.atan(math.sinh(math.pi * (1 - 2 * y / n)))) + return lat, lon + north_west_corner = tile_to_geocoords(x, y, zoom) + south_east_corner = tile_to_geocoords(x+1, y+1, zoom) + return north_west_corner[1], south_east_corner[0], south_east_corner[1], north_west_corner[0] diff --git a/pyowm/constants.py b/pyowm/constants.py index 865062ee..2ca76821 100644 --- a/pyowm/constants.py +++ b/pyowm/constants.py @@ -2,8 +2,9 @@ Constants for the PyOWM library """ -PYOWM_VERSION = '2.9.0' +PYOWM_VERSION = '2.10.0' LATEST_OWM_API_VERSION = '2.5' STATIONS_API_VERSION = (3, 0, 0) ALERT_API_VERSION = (3, 0, 0) +AGRO_API_VERSION = (1, 0, 0) DEFAULT_API_KEY = 'b1b15e88fa797225412429c1c50c122a' \ No newline at end of file diff --git a/pyowm/exceptions/api_call_error.py b/pyowm/exceptions/api_call_error.py index a0c87e3a..c8b5ff25 100644 --- a/pyowm/exceptions/api_call_error.py +++ b/pyowm/exceptions/api_call_error.py @@ -8,7 +8,7 @@ class APICallError(OWMError): """ - Error class that represents network/infrastructural failures when invoking OWM web API, in + Error class that represents network/infrastructural failures when invoking OWM Weather API, in example due to network errors. :param message: the message of the error @@ -23,7 +23,7 @@ def __init__(self, message, triggering_error=None): def __str__(self): """Redefine __str__ hook for pretty-printing""" - return ''.join(['Exception in calling OWM web API.', os.linesep, + return ''.join(['Exception in calling OWM Weather API.', os.linesep, 'Reason: ', self._message, os.linesep, 'Caused by: ', str(self._triggering_error)]) diff --git a/pyowm/exceptions/api_response_error.py b/pyowm/exceptions/api_response_error.py index 2e89bf59..44c7494a 100644 --- a/pyowm/exceptions/api_response_error.py +++ b/pyowm/exceptions/api_response_error.py @@ -8,7 +8,7 @@ class APIResponseError(OWMError): """ - Error class that represents HTTP error status codes in OWM web API + Error class that represents HTTP error status codes in OWM Weather API responses. :param cause: the message of the error diff --git a/pyowm/exceptions/parse_response_error.py b/pyowm/exceptions/parse_response_error.py index b2e66857..81d609b8 100644 --- a/pyowm/exceptions/parse_response_error.py +++ b/pyowm/exceptions/parse_response_error.py @@ -8,7 +8,7 @@ class ParseResponseError(OWMError): """ Error class that represents failures when parsing payload data in HTTP - responses sent by the OWM web API. + responses sent by the OWM Weather API. :param cause: the message of the error :type cause: str @@ -19,5 +19,5 @@ def __init__(self, cause): def __str__(self): """Redefine __str__ hook for pretty-printing""" - return ''.join(['Exception in parsing OWM web API response', + return ''.join(['Exception in parsing OWM Weather API response', os.linesep, 'Reason: ', self._message]) diff --git a/pyowm/pollutionapi30/coindex.py b/pyowm/pollutionapi30/coindex.py index af04227c..873d5849 100644 --- a/pyowm/pollutionapi30/coindex.py +++ b/pyowm/pollutionapi30/coindex.py @@ -24,7 +24,7 @@ class COIndex(object): :param co_samples: the CO samples :type co_samples: list of dicts :param reception_time: GMT UNIXtime telling when the CO observation has - been received from the OWM web API + been received from the OWM Weather API :type reception_time: int :returns: an *COIndex* instance :raises: *ValueError* when negative values are provided as reception time, @@ -64,7 +64,7 @@ def get_reference_time(self, timeformat='unix'): def get_reception_time(self, timeformat='unix'): """ Returns the GMT time telling when the CO observation has been received - from the OWM web API + from the OWM Weather API :param timeformat: the format for the time value. May be: '*unix*' (default) for UNIX time diff --git a/pyowm/pollutionapi30/no2index.py b/pyowm/pollutionapi30/no2index.py index a39c830e..d5cb3c19 100644 --- a/pyowm/pollutionapi30/no2index.py +++ b/pyowm/pollutionapi30/no2index.py @@ -24,7 +24,7 @@ class NO2Index(object): :param no2_samples: the NO2 samples :type no2_samples: list of dicts :param reception_time: GMT UNIXtime telling when the NO2 observation has - been received from the OWM web API + been received from the OWM Weather API :type reception_time: int :returns: a *NO2Index* instance :raises: *ValueError* when negative values are provided as reception time, @@ -64,7 +64,7 @@ def get_reference_time(self, timeformat='unix'): def get_reception_time(self, timeformat='unix'): """ Returns the GMT time telling when the NO2 observation has been received - from the OWM web API + from the OWM Weather API :param timeformat: the format for the time value. May be: '*unix*' (default) for UNIX time diff --git a/pyowm/pollutionapi30/ozone.py b/pyowm/pollutionapi30/ozone.py index 79994352..b3395e27 100644 --- a/pyowm/pollutionapi30/ozone.py +++ b/pyowm/pollutionapi30/ozone.py @@ -19,7 +19,7 @@ class Ozone(object): :param interval: the time granularity of the O3 observation :type interval: str :param reception_time: GMT UNIXtime telling when the observation has - been received from the OWM web API + been received from the OWM Weather API :type reception_time: int :returns: an *Ozone* instance :raises: *ValueError* when negative values are provided as reception time or @@ -58,7 +58,7 @@ def get_reference_time(self, timeformat='unix'): def get_reception_time(self, timeformat='unix'): """ Returns the GMT time telling when the O3 observation - has been received from the OWM web API + has been received from the OWM Weather API :param timeformat: the format for the time value. May be: '*unix*' (default) for UNIX time diff --git a/pyowm/pollutionapi30/parsers.py b/pyowm/pollutionapi30/parsers.py index b385c7c6..bb0ba418 100644 --- a/pyowm/pollutionapi30/parsers.py +++ b/pyowm/pollutionapi30/parsers.py @@ -1,6 +1,6 @@ import json from pyowm.pollutionapi30 import coindex, no2index, ozone, so2index -from pyowm.webapi25 import location +from pyowm.weatherapi25 import location from pyowm.abstractions import jsonparser from pyowm.exceptions import parse_response_error from pyowm.utils import timeformatutils, timeutils @@ -9,7 +9,7 @@ class COIndexParser(jsonparser.JSONParser): """ Concrete *JSONParser* implementation building an *COIndex* instance out - of raw JSON data coming from OWM web API responses. + of raw JSON data coming from OWM Weather API responses. """ @@ -27,7 +27,7 @@ def parse_JSON(self, JSON_string): :returns: an *COIndex* instance or ``None`` if no data is available :raises: *ParseResponseError* if it is impossible to find or parse the data needed to build the result, *APIResponseError* if the JSON - string embeds an HTTP status error (this is an OWM web API 2.5 bug) + string embeds an HTTP status error """ if JSON_string is None: @@ -63,7 +63,7 @@ def __repr__(self): class NO2IndexParser(jsonparser.JSONParser): """ Concrete *JSONParser* implementation building an *NO2Index* instance out - of raw JSON data coming from OWM web API responses. + of raw JSON data coming from OWM Weather API responses. """ @@ -81,7 +81,7 @@ def parse_JSON(self, JSON_string): :returns: an *NO2Index* instance or ``None`` if no data is available :raises: *ParseResponseError* if it is impossible to find or parse the data needed to build the result, *APIResponseError* if the JSON - string embeds an HTTP status error (this is an OWM web API 2.5 bug) + string embeds an HTTP status error """ if JSON_string is None: @@ -119,7 +119,7 @@ def __repr__(self): class OzoneParser(jsonparser.JSONParser): """ Concrete *JSONParser* implementation building an *Ozone* instance out - of raw JSON data coming from OWM web API responses. + of raw JSON data coming from OWM Weather API responses. """ @@ -137,7 +137,7 @@ def parse_JSON(self, JSON_string): :returns: an *Ozone* instance or ``None`` if no data is available :raises: *ParseResponseError* if it is impossible to find or parse the data needed to build the result, *APIResponseError* if the JSON - string embeds an HTTP status error (this is an OWM web API 2.5 bug) + string embeds an HTTP status error """ if JSON_string is None: @@ -176,7 +176,7 @@ def __repr__(self): class SO2IndexParser(jsonparser.JSONParser): """ Concrete *JSONParser* implementation building an *SO2Index* instance out - of raw JSON data coming from OWM web API responses. + of raw JSON data coming from OWM Weather API responses. """ @@ -194,7 +194,7 @@ def parse_JSON(self, JSON_string): :returns: a *SO2Index* instance or ``None`` if no data is available :raises: *ParseResponseError* if it is impossible to find or parse the data needed to build the result, *APIResponseError* if the JSON - string embeds an HTTP status error (this is an OWM web API 2.5 bug) + string embeds an HTTP status error """ if JSON_string is None: diff --git a/pyowm/pollutionapi30/so2index.py b/pyowm/pollutionapi30/so2index.py index 3ccdd5e1..f018e9f7 100644 --- a/pyowm/pollutionapi30/so2index.py +++ b/pyowm/pollutionapi30/so2index.py @@ -24,7 +24,7 @@ class SO2Index(object): :param so2_samples: the SO2 samples :type so2_samples: list of dicts :param reception_time: GMT UNIXtime telling when the SO2 observation has - been received from the OWM web API + been received from the OWM Weather API :type reception_time: int :returns: an *SOIndex* instance :raises: *ValueError* when negative values are provided as reception time, @@ -63,7 +63,7 @@ def get_reference_time(self, timeformat='unix'): def get_reception_time(self, timeformat='unix'): """ Returns the GMT time telling when the SO2 observation has been received - from the OWM web API + from the OWM Weather API :param timeformat: the format for the time value. May be: '*unix*' (default) for UNIX time diff --git a/pyowm/stationsapi30/stations_manager.py b/pyowm/stationsapi30/stations_manager.py index bb0e0f69..9e9524b0 100644 --- a/pyowm/stationsapi30/stations_manager.py +++ b/pyowm/stationsapi30/stations_manager.py @@ -17,7 +17,7 @@ class StationsManager(object): it implements CRUD methods on Station entities and the corresponding measured datapoints. - :param API_key: the OWM web API key + :param API_key: the OWM Weather API key :type API_key: str :returns: a *StationsManager* instance :raises: *AssertionError* when no API Key is provided @@ -149,7 +149,7 @@ def send_measurement(self, measurement): status, _ = self.http_client.post( MEASUREMENTS_URI, params={'appid': self.API_key}, - data=[measurement.to_dict()], + data=[self._structure_dict(measurement)], headers={'Content-Type': 'application/json'}) def send_measurements(self, list_of_measurements): @@ -165,7 +165,7 @@ def send_measurements(self, list_of_measurements): """ assert list_of_measurements is not None assert all([m.station_id is not None for m in list_of_measurements]) - msmts = [m.to_dict() for m in list_of_measurements] + msmts = [self._structure_dict(m) for m in list_of_measurements] status, _ = self.http_client.post( MEASUREMENTS_URI, params={'appid': self.API_key}, @@ -227,42 +227,43 @@ def send_buffer(self, buffer): :returns: `None` if creation is successful, an exception otherwise """ assert buffer is not None - msmts = [] - for x in buffer.measurements: - m = x.to_dict() - item = dict() - item['station_id'] = m['station_id'] - item['dt'] = m['timestamp'] - item['temperature'] = m['temperature'] - item['wind_speed'] = m['wind_speed'] - item['wind_gust'] = m['wind_gust'] - item['wind_deg'] = m['wind_deg'] - item['pressure'] = m['pressure'] - item['humidity'] = m['humidity'] - item['rain_1h'] = m['rain_1h'] - item['rain_6h'] = m['rain_6h'] - item['rain_24h'] = m['rain_24h'] - item['snow_1h'] = m['snow_1h'] - item['snow_6h'] = m['snow_6h'] - item['snow_24h'] = m['snow_24h'] - item['dew_point'] = m['dew_point'] - item['humidex'] = m['humidex'] - item['heat_index'] = m['heat_index'] - item['visibility_distance'] = m['visibility_distance'] - item['visibility_prefix'] = m['visibility_prefix'] - item['clouds'] = [dict(distance=m['clouds_distance']), - dict(condition=m['clouds_condition']), - dict(cumulus=m['clouds_cumulus'])] - item['weather'] = [ - dict(precipitation=m['weather_precipitation']), - dict(descriptor=m['weather_descriptor']), - dict(intensity=m['weather_intensity']), - dict(proximity=m['weather_proximity']), - dict(obscuration=m['weather_obscuration']), - dict(other=m['weather_other'])] - msmts.append(item) + msmts = [self._structure_dict(m) for m in buffer.measurements] status, _ = self.http_client.post( MEASUREMENTS_URI, params={'appid': self.API_key}, data=msmts, headers={'Content-Type': 'application/json'}) + + def _structure_dict(self, measurement): + d = measurement.to_dict() + item = dict() + item['station_id'] = d['station_id'] + item['dt'] = d['timestamp'] + item['temperature'] = d['temperature'] + item['wind_speed'] = d['wind_speed'] + item['wind_gust'] = d['wind_gust'] + item['wind_deg'] = d['wind_deg'] + item['pressure'] = d['pressure'] + item['humidity'] = d['humidity'] + item['rain_1h'] = d['rain_1h'] + item['rain_6h'] = d['rain_6h'] + item['rain_24h'] = d['rain_24h'] + item['snow_1h'] = d['snow_1h'] + item['snow_6h'] = d['snow_6h'] + item['snow_24h'] = d['snow_24h'] + item['dew_point'] = d['dew_point'] + item['humidex'] = d['humidex'] + item['heat_index'] = d['heat_index'] + item['visibility_distance'] = d['visibility_distance'] + item['visibility_prefix'] = d['visibility_prefix'] + item['clouds'] = [dict(distance=d['clouds_distance']), + dict(condition=d['clouds_condition']), + dict(cumulus=d['clouds_cumulus'])] + item['weather'] = [ + dict(precipitation=d['weather_precipitation']), + dict(descriptor=d['weather_descriptor']), + dict(intensity=d['weather_intensity']), + dict(proximity=d['weather_proximity']), + dict(obscuration=d['weather_obscuration']), + dict(other=d['weather_other'])] + return item diff --git a/pyowm/webapi25/parsers/__init__.py b/pyowm/tiles/__init__.py similarity index 100% rename from pyowm/webapi25/parsers/__init__.py rename to pyowm/tiles/__init__.py diff --git a/pyowm/tiles/enums.py b/pyowm/tiles/enums.py new file mode 100644 index 00000000..2f458d05 --- /dev/null +++ b/pyowm/tiles/enums.py @@ -0,0 +1,24 @@ + +class MapLayerEnum: + """ + Allowed map layer values for tiles retrieval + + """ + PRECIPITATION = 'precipitation_new' + WIND = 'wind_new' + TEMPERATURE = 'temp_new' + PRESSURE = 'pressure_new' + + @classmethod + def items(cls): + """ + All values for this enum + :return: list of tuples + + """ + return [ + cls.PRECIPITATION, + cls.WIND, + cls.TEMPERATURE, + cls.PRESSURE + ] diff --git a/pyowm/tiles/tile_manager.py b/pyowm/tiles/tile_manager.py new file mode 100644 index 00000000..78e1b139 --- /dev/null +++ b/pyowm/tiles/tile_manager.py @@ -0,0 +1,54 @@ +""" +Object that can download tile images at various zoom levels +""" + +from pyowm.commons.http_client import HttpClient +from pyowm.commons.image import Image +from pyowm.commons.enums import ImageTypeEnum +from pyowm.commons.tile import Tile +from pyowm.tiles.uris import ROOT_TILE_URL + + +class TileManager(object): + + """ + A manager objects that reads OWM map layers tile images . + + :param API_key: the OWM Weather API key + :type API_key: str + :param map_layer: the layer for which you want tiles fetched. Allowed map layers are specified by the `pyowm.tiles.enum.MapLayerEnum` enumerator class. + :type map_layer: str + :returns: a *TileManager* instance + :raises: *AssertionError* when no API Key or no map layer is provided, or map layer name is not a string + + """ + + def __init__(self, API_key, map_layer): + assert API_key is not None, 'You must provide a valid API Key' + self.API_key = API_key + assert map_layer is not None, 'You must provide a valid map layer name' + assert isinstance(map_layer, str), 'Map layer name must be a string' + self.map_layer = map_layer + self.http_client = HttpClient() + + def get_tile(self, x, y, zoom): + """ + Retrieves the tile having the specified coordinates and zoom level + + :param x: horizontal tile number in OWM tile reference system + :type x: int + :param y: vertical tile number in OWM tile reference system + :type y: int + :param zoom: zoom level for the tile + :type zoom: int + :returns: a `pyowm.tiles.Tile` instance + + """ + status, data = self.http_client.get_png( + ROOT_TILE_URL % self.map_layer + '/%s/%s/%s.png' % (zoom, x, y), + params={'appid': self.API_key}) + img = Image(data, ImageTypeEnum.PNG) + return Tile(x, y, zoom, self.map_layer, img) + + def __repr__(self): + return "<%s.%s - layer name=%s>" % (__name__, self.__class__.__name__, self.map_layer) diff --git a/pyowm/tiles/uris.py b/pyowm/tiles/uris.py new file mode 100644 index 00000000..e19719f3 --- /dev/null +++ b/pyowm/tiles/uris.py @@ -0,0 +1,5 @@ +""" +URIs templates for tiles +""" + +ROOT_TILE_URL = 'http://tile.openweathermap.org/map/%s' diff --git a/pyowm/utils/geo.py b/pyowm/utils/geo.py index 67366ebe..e15e019e 100644 --- a/pyowm/utils/geo.py +++ b/pyowm/utils/geo.py @@ -1,3 +1,4 @@ + import json import math import geojson @@ -159,6 +160,9 @@ def from_dict(self, the_dict): result._geom = geom return result + def __repr__(self): + return "<%s.%s - lon=%s, lat=%s>" % (__name__, self.__class__.__name__, self.lon, self.lat) + class MultiPoint(Geometry): """ @@ -252,6 +256,16 @@ def geojson(self): def as_dict(self): return json.loads(self.geojson()) + @property + def points(self): + """ + Returns the list of *Point* instances representing the points of the polygon + :return: list of *Point* objects + """ + feature = geojson.Feature(geometry=self._geom) + points_coords = list(geojson.utils.coords(feature)) + return [Point(p[0], p[1]) for p in points_coords] + @classmethod def from_dict(self, the_dict): """ @@ -345,6 +359,7 @@ def build(cls, the_dict): :return: a `pyowm.utils.geo.Geometry` subtype instance :raises `ValueError` if unable to the geometry type cannot be recognized """ + assert isinstance(the_dict, dict), 'Geometry must be a dict' geom_type = the_dict.get('type', None) if geom_type == 'Point': return Point.from_dict(the_dict) diff --git a/pyowm/utils/stringutils.py b/pyowm/utils/stringutils.py index cff3d0f7..a9fee0ad 100644 --- a/pyowm/utils/stringutils.py +++ b/pyowm/utils/stringutils.py @@ -1,3 +1,6 @@ +import sys + + def obfuscate_API_key(API_key): """ Return a mostly obfuscated version of the API Key @@ -9,44 +12,24 @@ def obfuscate_API_key(API_key): return (len(API_key)-8)*'*'+API_key[-8:] -def assert_is_string(value): +def check_if_running_with_python_2(): """ - Checks if the provided value is a valid string instance - - :param value: value to be checked - :return: None + Catch Python 2.x usage attempts. If Python2 + :return: `None` + :raise: `ImportError` if running on Python 2 """ - try: # Python 2.x - assert isinstance(value, basestring), "Value must be a string or unicode" - except NameError: # Python 3.x - assert isinstance(value, str), "Value must be a string" - + if sys.version_info < (3,): + raise ImportError( + """You are running PyOWM on Python 2 - how unfortunate! Since version 2.10, +PyOWM does not support Python 2 any more. PyOWM 2.9 has however a Long-Term Support +branch for bug fixing on Python 2 - install it with: -def assert_is_string_or_unicode(value): - """ - Checks if the provided value is a valid string or unicode instance - On Python 3.x it just checks that the value is a string instance. - :param value: value to be checked - :return: None - """ - try: - assert isinstance(value, basestring) or isinstance(value, unicode), \ - "Value must be a string or unicode" - except NameError: - assert isinstance(value, str), "Value must be a string" + $ pip install git+https://github.com/csparpa/pyowm.git@v2.9-LTS +This LTS branch will be maintained until January, 1 2020 -def encode_to_utf8(value): - """ - Turns the provided value to UTF-8 encoding +See details at: - :param value: input value - :return: UTF-8 encoded value - """ - try: # The OWM API expects UTF-8 encoding - if not isinstance(value, unicode): - return value.encode('utf8') - return value - except NameError: - return value +https://github.com/csparpa/pyowm/wiki/Timeline-for-dropping-Python-2.x-support +""") diff --git a/pyowm/uvindexapi30/parsers.py b/pyowm/uvindexapi30/parsers.py index 9927109c..c77771d4 100644 --- a/pyowm/uvindexapi30/parsers.py +++ b/pyowm/uvindexapi30/parsers.py @@ -5,7 +5,7 @@ import json from pyowm.uvindexapi30 import uvindex -from pyowm.webapi25 import location +from pyowm.weatherapi25 import location from pyowm.abstractions import jsonparser from pyowm.exceptions import parse_response_error from pyowm.utils import timeutils @@ -14,7 +14,7 @@ class UVIndexParser(jsonparser.JSONParser): """ Concrete *JSONParser* implementation building an *UVIndex* instance out - of raw JSON data coming from OWM web API responses. + of raw JSON data coming from OWM Weather API responses. """ @@ -32,7 +32,7 @@ def parse_JSON(self, JSON_string): :returns: an *UVIndex* instance or ``None`` if no data is available :raises: *ParseResponseError* if it is impossible to find or parse the data needed to build the result, *APIResponseError* if the JSON - string embeds an HTTP status error (this is an OWM web API 2.5 bug) + string embeds an HTTP status error """ if JSON_string is None: @@ -67,7 +67,7 @@ def __repr__(self): class UVIndexListParser(jsonparser.JSONParser): """ Concrete *JSONParser* implementation building a list of *UVIndex* instances - out of raw JSON data coming from OWM web API responses. + out of raw JSON data coming from OWM Weather API responses. """ @@ -86,7 +86,7 @@ def parse_JSON(self, JSON_string): available :raises: *ParseResponseError* if it is impossible to find or parse the data needed to build the result, *APIResponseError* if the JSON - string embeds an HTTP status error (this is an OWM web API 2.5 bug) + string embeds an HTTP status error """ if JSON_string is None: diff --git a/pyowm/uvindexapi30/uvindex.py b/pyowm/uvindexapi30/uvindex.py index 61162a79..3edd08b0 100644 --- a/pyowm/uvindexapi30/uvindex.py +++ b/pyowm/uvindexapi30/uvindex.py @@ -31,7 +31,7 @@ class UVIndex(object): :param value: the observed UV intensity value :type value: float :param reception_time: GMT UNIXtime telling when the observation has - been received from the OWM web API + been received from the OWM Weather API :type reception_time: int :returns: an *UVIndex* instance :raises: *ValueError* when negative values are provided as reception time or @@ -54,7 +54,7 @@ def __init__(self, reference_time, location, value, reception_time): def get_reference_time(self, timeformat='unix'): """ Returns the GMT time telling when the UV has been observed - from the OWM web API + from the OWM Weather API :param timeformat: the format for the time value. May be: '*unix*' (default) for UNIX time diff --git a/pyowm/webapi25/xsd/__init__.py b/pyowm/weatherapi25/__init__.py similarity index 100% rename from pyowm/webapi25/xsd/__init__.py rename to pyowm/weatherapi25/__init__.py diff --git a/pyowm/webapi25/cityidregistry.py b/pyowm/weatherapi25/cityidregistry.py similarity index 98% rename from pyowm/webapi25/cityidregistry.py rename to pyowm/weatherapi25/cityidregistry.py index eb2165e6..92bfa5ce 100644 --- a/pyowm/webapi25/cityidregistry.py +++ b/pyowm/weatherapi25/cityidregistry.py @@ -1,5 +1,5 @@ import gzip -from pyowm.webapi25.location import Location +from pyowm.weatherapi25.location import Location from pyowm.abstractions.decorators import deprecated from pkg_resources import resource_filename @@ -114,7 +114,7 @@ def locations_for(self, city_name, country=None, matching='nocase'): whose name contains as a substring the string fed to the function, no matter the case). Defaults to `nocase`. :raises ValueError if the value for `matching` is unknown - :return: list of `webapi25.location.Location` objects + :return: list of `weatherapi25.location.Location` objects """ if not city_name: return [] diff --git a/pyowm/webapi25/cityids/097-102.txt.gz b/pyowm/weatherapi25/cityids/097-102.txt.gz similarity index 100% rename from pyowm/webapi25/cityids/097-102.txt.gz rename to pyowm/weatherapi25/cityids/097-102.txt.gz diff --git a/pyowm/webapi25/cityids/103-108.txt.gz b/pyowm/weatherapi25/cityids/103-108.txt.gz similarity index 100% rename from pyowm/webapi25/cityids/103-108.txt.gz rename to pyowm/weatherapi25/cityids/103-108.txt.gz diff --git a/pyowm/webapi25/cityids/109-114.txt.gz b/pyowm/weatherapi25/cityids/109-114.txt.gz similarity index 100% rename from pyowm/webapi25/cityids/109-114.txt.gz rename to pyowm/weatherapi25/cityids/109-114.txt.gz diff --git a/pyowm/webapi25/cityids/115-122.txt.gz b/pyowm/weatherapi25/cityids/115-122.txt.gz similarity index 100% rename from pyowm/webapi25/cityids/115-122.txt.gz rename to pyowm/weatherapi25/cityids/115-122.txt.gz diff --git a/pyowm/webapi25/cityids/__init__.py b/pyowm/weatherapi25/cityids/__init__.py similarity index 100% rename from pyowm/webapi25/cityids/__init__.py rename to pyowm/weatherapi25/cityids/__init__.py diff --git a/pyowm/webapi25/configuration25.py b/pyowm/weatherapi25/configuration25.py similarity index 85% rename from pyowm/webapi25/configuration25.py rename to pyowm/weatherapi25/configuration25.py index 8b91d9c4..6e21d977 100644 --- a/pyowm/webapi25/configuration25.py +++ b/pyowm/weatherapi25/configuration25.py @@ -1,13 +1,13 @@ from pyowm.caches import nullcache -from pyowm.webapi25 import weathercoderegistry, cityidregistry -from pyowm.webapi25.parsers import forecastparser, observationlistparser, observationparser, stationhistoryparser, \ +from pyowm.weatherapi25 import weathercoderegistry, cityidregistry +from pyowm.weatherapi25.parsers import forecastparser, observationlistparser, observationparser, stationhistoryparser, \ stationlistparser, stationparser, weatherhistoryparser from pyowm.uvindexapi30.parsers import UVIndexParser, UVIndexListParser from pyowm.pollutionapi30.parsers import COIndexParser, NO2IndexParser, SO2IndexParser, OzoneParser """ -Configuration for the PyOWM library specific to OWM web API version 2.5 +Configuration for the PyOWM library specific to OWM Weather API version 2.5 """ # Subdomains mapping @@ -20,23 +20,23 @@ USE_SSL = False VERIFY_SSL_CERTS = True -# OWM web API URLs +# OWM Weather API URLs ROOT_API_URL = 'http://%s.openweathermap.org/data/2.5' ROOT_HISTORY_URL = 'http://history.openweathermap.org/data/2.5' -ICONS_BASE_URL = 'http://openweathermap.org/img/w' OBSERVATION_URL = ROOT_API_URL + '/weather' GROUP_OBSERVATIONS_URL = ROOT_API_URL + '/group' STATION_URL = ROOT_API_URL + '/station' FIND_OBSERVATIONS_URL = ROOT_API_URL + '/find' FIND_STATION_URL = ROOT_API_URL + '/station/find' BBOX_STATION_URL = ROOT_API_URL + '/box/station' +BBOX_CITY_URL = ROOT_API_URL + '/box/city' THREE_HOURS_FORECAST_URL = ROOT_API_URL + '/forecast' DAILY_FORECAST_URL = ROOT_API_URL + '/forecast/daily' CITY_WEATHER_HISTORY_URL = ROOT_HISTORY_URL + '/history/city' STATION_WEATHER_HISTORY_URL = ROOT_API_URL + '/history/station' -# Parser objects injection for OWM web API responses parsing +# Parser objects injection for OWM Weather API responses parsing parsers = { 'observation': observationparser.ObservationParser(), 'observation_list': observationlistparser.ObservationListParser(), @@ -59,13 +59,13 @@ # Cache provider to be used cache = nullcache.NullCache() -# Default language for OWM web API queries text results +# Default language for OWM Weather API queries text results language = 'en' # Default API subscription type ('free' or 'pro') API_SUBSCRIPTION_TYPE = 'free' -# OWM web API availability timeout in seconds +# OWM Weather API availability timeout in seconds API_AVAILABILITY_TIMEOUT = 2 # Weather status code registry diff --git a/pyowm/webapi25/forecast.py b/pyowm/weatherapi25/forecast.py similarity index 98% rename from pyowm/webapi25/forecast.py rename to pyowm/weatherapi25/forecast.py index 57ff46b6..389c5ba9 100644 --- a/pyowm/webapi25/forecast.py +++ b/pyowm/weatherapi25/forecast.py @@ -4,7 +4,7 @@ import json import xml.etree.ElementTree as ET -from pyowm.webapi25.xsd.xmlnsconfig import ( +from pyowm.weatherapi25.xsd.xmlnsconfig import ( FORECAST_XMLNS_PREFIX, FORECAST_XMLNS_URL) from pyowm.utils import timeutils, timeformatutils, xmlutils @@ -118,7 +118,7 @@ def set_interval(self, interval): def get_reception_time(self, timeformat='unix'): """Returns the GMT time telling when the forecast was received - from the OWM web API + from the OWM Weather API :param timeformat: the format for the time value. May be: '*unix*' (default) for UNIX time diff --git a/pyowm/webapi25/forecaster.py b/pyowm/weatherapi25/forecaster.py similarity index 99% rename from pyowm/webapi25/forecaster.py rename to pyowm/weatherapi25/forecaster.py index 966cd529..1973c3ca 100644 --- a/pyowm/webapi25/forecaster.py +++ b/pyowm/weatherapi25/forecaster.py @@ -3,7 +3,7 @@ """ from pyowm.utils import timeformatutils, weatherutils -from pyowm.webapi25.configuration25 import weather_code_registry +from pyowm.weatherapi25.configuration25 import weather_code_registry from pyowm.abstractions.decorators import deprecated diff --git a/pyowm/webapi25/historian.py b/pyowm/weatherapi25/historian.py similarity index 100% rename from pyowm/webapi25/historian.py rename to pyowm/weatherapi25/historian.py diff --git a/pyowm/webapi25/location.py b/pyowm/weatherapi25/location.py similarity index 97% rename from pyowm/webapi25/location.py rename to pyowm/weatherapi25/location.py index afd83bd9..e5e7664a 100644 --- a/pyowm/webapi25/location.py +++ b/pyowm/weatherapi25/location.py @@ -4,7 +4,7 @@ import json import xml.etree.ElementTree as ET -from pyowm.webapi25.xsd.xmlnsconfig import ( +from pyowm.weatherapi25.xsd.xmlnsconfig import ( LOCATION_XMLNS_URL, LOCATION_XMLNS_PREFIX) from pyowm.utils import xmlutils, geo @@ -13,7 +13,7 @@ class Location(object): """ A class representing a location in the world. A location is defined through a toponym, a couple of geographic coordinates such as longitude and - latitude and a numeric identifier assigned by the OWM web API that uniquely + latitude and a numeric identifier assigned by the OWM Weather API that uniquely spots the location in the world. Optionally, the country specification may be provided. diff --git a/pyowm/webapi25/observation.py b/pyowm/weatherapi25/observation.py similarity index 96% rename from pyowm/webapi25/observation.py rename to pyowm/weatherapi25/observation.py index 11f99b54..4efb6658 100644 --- a/pyowm/webapi25/observation.py +++ b/pyowm/weatherapi25/observation.py @@ -4,7 +4,7 @@ import json import xml.etree.ElementTree as ET -from pyowm.webapi25.xsd.xmlnsconfig import ( +from pyowm.weatherapi25.xsd.xmlnsconfig import ( OBSERVATION_XMLNS_URL, OBSERVATION_XMLNS_PREFIX) from pyowm.utils import timeformatutils, xmlutils @@ -17,7 +17,7 @@ class Observation(object): the encapsulated *Weather* object. :param reception_time: GMT UNIXtime telling when the weather obervation has - been received from the OWM web API + been received from the OWM Weather API :type reception_time: int :param location: the *Location* relative to this observation :type location: *Location* @@ -38,7 +38,7 @@ def __init__(self, reception_time, location, weather): def get_reception_time(self, timeformat='unix'): """ Returns the GMT time telling when the observation has been received - from the OWM web API + from the OWM Weather API :param timeformat: the format for the time value. May be: '*unix*' (default) for UNIX time diff --git a/pyowm/webapi25/owm25.py b/pyowm/weatherapi25/owm25.py similarity index 81% rename from pyowm/webapi25/owm25.py rename to pyowm/weatherapi25/owm25.py index 64734e98..3d96fc8e 100644 --- a/pyowm/webapi25/owm25.py +++ b/pyowm/weatherapi25/owm25.py @@ -4,12 +4,12 @@ from time import time from pyowm import constants -from pyowm.webapi25.configuration25 import ( +from pyowm.weatherapi25.configuration25 import ( OBSERVATION_URL, GROUP_OBSERVATIONS_URL, FIND_OBSERVATIONS_URL, THREE_HOURS_FORECAST_URL, DAILY_FORECAST_URL, CITY_WEATHER_HISTORY_URL, STATION_WEATHER_HISTORY_URL, - FIND_STATION_URL, STATION_URL, BBOX_STATION_URL) -from pyowm.webapi25.configuration25 import city_id_registry as reg + FIND_STATION_URL, STATION_URL, BBOX_STATION_URL, BBOX_CITY_URL) +from pyowm.weatherapi25.configuration25 import city_id_registry as reg from pyowm.abstractions import owm from pyowm.abstractions.decorators import deprecated from pyowm.caches import nullcache @@ -18,10 +18,12 @@ from pyowm.uvindexapi30 import uv_client from pyowm.exceptions import api_call_error from pyowm.utils import timeformatutils, stringutils, timeutils, geo -from pyowm.webapi25 import forecaster -from pyowm.webapi25 import historian +from pyowm.weatherapi25 import forecaster +from pyowm.weatherapi25 import historian from pyowm.stationsapi30 import stations_manager from pyowm.alertapi30 import alert_manager +from pyowm.tiles import tile_manager +from pyowm.agroapi10 import agro_manager class OWM25(owm.OWM): @@ -29,14 +31,14 @@ class OWM25(owm.OWM): OWM_API_VERSION = '2.5' """ - OWM subclass providing methods for each OWM web API 2.5 endpoint. The class - is instantiated with *jsonparser* subclasses, each one parsing the response + OWM subclass providing methods for each OWM Weather API 2.5 endpoint and ad-hoc API clients for the other + OWM web APis. The class is instantiated with *jsonparser* subclasses, each one parsing the response payload of a specific API endpoint :param parsers: the dictionary containing *jsonparser* concrete instances - to be used as parsers for OWM web API 2.5 responses + to be used as parsers for OWM Weather API 2.5 responses :type parsers: dict - :param API_key: the OWM web API key (defaults to ``None``) + :param API_key: the OWM Weather API key (defaults to ``None``) :type API_key: str :param cache: a concrete implementation of class *OWMCache* serving as the cache provider (defaults to a *NullCache* instance) @@ -44,7 +46,7 @@ class OWM25(owm.OWM): :param language: the language in which you want text results to be returned. It's a two-characters string, eg: "en", "ru", "it". Defaults to: "en" :type language: str - :param subscription_type: the type of OWM web API subscription to be wrapped. + :param subscription_type: the type of OWM Weather API subscription to be wrapped. Can be 'free' (free subscription) or 'pro' (paid subscription), Defaults to: 'free' :type subscription_type: str @@ -56,9 +58,12 @@ class OWM25(owm.OWM): """ def __init__(self, parsers, API_key=None, cache=nullcache.NullCache(), language="en", subscription_type='free', use_ssl=False): + + stringutils.check_if_running_with_python_2() # Python 3 only + self._parsers = parsers if API_key is not None: - stringutils.assert_is_string(API_key) + assert isinstance(API_key, str), "Value must be a string" self._API_key = API_key self._wapi = http_client.HttpClient(cache=cache) self._uvapi = uv_client.UltraVioletHttpClient(API_key, self._wapi) @@ -91,7 +96,7 @@ def set_API_key(self, API_key): @deprecated(will_be='modified', on_version=(3, 0, 0)) def get_API_version(self): """ - Returns the currently supported OWM web API version + Returns the currently supported OWM Weather API version .. deprecated:: 3.0.0 Will return a dict of tuples instead of a str @@ -116,7 +121,7 @@ def get_version(self): def get_language(self): """ - Returns the language in which the OWM web API shall return text results + Returns the language in which the OWM Weather API shall return text results :returns: the language @@ -125,7 +130,7 @@ def get_language(self): def set_language(self, language): """ - Sets the language in which the OWM web API shall return text results + Sets the language in which the OWM Weather API shall return text results :param language: the new two-characters language (eg: "ru") :type API_key: str @@ -166,15 +171,31 @@ def alert_manager(self): """ return alert_manager.AlertManager(self._API_key) + def tile_manager(self, layer_name): + """ + Gives a `pyowm.tiles.tile_manager.TileManager` instance that can be used to fetch tile images. + :param layer_name: the layer name for the tiles (values can be looked up on `pyowm.tiles.enums.MapLayerEnum`) + :return: a `pyowm.tiles.tile_manager.TileManager` instance + """ + return tile_manager.TileManager(self._API_key, map_layer=layer_name) + + def agro_manager(self): + """ + Gives a `pyowm.agro10.agro_manager.AgroManager` instance that can be used to read/write data from the + Agricultural API. + :return: a `pyowm.agro10.agro_manager.AgroManager` instance + """ + return agro_manager.AgroManager(self._API_key) + def is_API_online(self): """ - Returns True if the OWM web API is currently online. A short timeout + Returns True if the OWM Weather API is currently online. A short timeout is used to determine API service availability. :returns: bool """ - params = {'q': 'London,UK'} + params = {'q': 'London,GB'} uri = http_client.HttpClient.to_url(OBSERVATION_URL, self._API_key, self._subscription_type) @@ -188,20 +209,20 @@ def is_API_online(self): def weather_at_place(self, name): """ - Queries the OWM web API for the currently observed weather at the + Queries the OWM Weather API for the currently observed weather at the specified toponym (eg: "London,uk") :param name: the location's toponym :type name: str or unicode :returns: an *Observation* instance or ``None`` if no weather data is available - :raises: *ParseResponseException* when OWM web API responses' data - cannot be parsed or *APICallException* when OWM web API can not be + :raises: *ParseResponseException* when OWM Weather API responses' data + cannot be parsed or *APICallException* when OWM Weather API can not be reached """ - stringutils.assert_is_string_or_unicode(name) - encoded_name = stringutils.encode_to_utf8(name) + assert isinstance(name, str), "Value must be a string" + encoded_name = name params = {'q': encoded_name, 'lang': self._language} uri = http_client.HttpClient.to_url(OBSERVATION_URL, self._API_key, @@ -212,7 +233,7 @@ def weather_at_place(self, name): def weather_at_coords(self, lat, lon): """ - Queries the OWM web API for the currently observed weather at the + Queries the OWM Weather API for the currently observed weather at the specified geographic (eg: 51.503614, -0.107331). :param lat: the location's latitude, must be between -90.0 and 90.0 @@ -221,8 +242,8 @@ def weather_at_coords(self, lat, lon): :type lon: int/float :returns: an *Observation* instance or ``None`` if no weather data is available - :raises: *ParseResponseException* when OWM web API responses' data - cannot be parsed or *APICallException* when OWM web API can not be + :raises: *ParseResponseException* when OWM Weather API responses' data + cannot be parsed or *APICallException* when OWM Weather API can not be reached """ geo.assert_is_lon(lon) @@ -237,7 +258,7 @@ def weather_at_coords(self, lat, lon): def weather_at_zip_code(self, zipcode, country): """ - Queries the OWM web API for the currently observed weather at the + Queries the OWM Weather API for the currently observed weather at the specified zip code and country code (eg: 2037, au). :param zip: the location's zip or postcode @@ -246,14 +267,14 @@ def weather_at_zip_code(self, zipcode, country): :type country: string :returns: an *Observation* instance or ``None`` if no weather data is available - :raises: *ParseResponseException* when OWM web API responses' data - cannot be parsed or *APICallException* when OWM web API can not be + :raises: *ParseResponseException* when OWM Weather API responses' data + cannot be parsed or *APICallException* when OWM Weather API can not be reached """ - stringutils.assert_is_string_or_unicode(zipcode) - stringutils.assert_is_string_or_unicode(country) - encoded_zip = stringutils.encode_to_utf8(zipcode) - encoded_country = stringutils.encode_to_utf8(country) + assert isinstance(zipcode, str), "Value must be a string" + assert isinstance(country, str), "Value must be a string" + encoded_zip = zipcode + encoded_country = country zip_param = encoded_zip + ',' + encoded_country params = {'zip': zip_param, 'lang': self._language} uri = http_client.HttpClient.to_url(OBSERVATION_URL, @@ -265,15 +286,15 @@ def weather_at_zip_code(self, zipcode, country): def weather_at_id(self, id): """ - Queries the OWM web API for the currently observed weather at the + Queries the OWM Weather API for the currently observed weather at the specified city ID (eg: 5128581) :param id: the location's city ID :type id: int :returns: an *Observation* instance or ``None`` if no weather data is available - :raises: *ParseResponseException* when OWM web API responses' data - cannot be parsed or *APICallException* when OWM web API can not be + :raises: *ParseResponseException* when OWM Weather API responses' data + cannot be parsed or *APICallException* when OWM Weather API can not be reached """ assert type(id) is int, "'id' must be an int" @@ -289,15 +310,15 @@ def weather_at_id(self, id): def weather_at_ids(self, ids_list): """ - Queries the OWM web API for the currently observed weathers at the + Queries the OWM Weather API for the currently observed weathers at the specified city IDs (eg: [5128581,87182]) :param ids_list: the list of city IDs :type ids_list: list of int :returns: a list of *Observation* instances or an empty list if no weather data is available - :raises: *ParseResponseException* when OWM web API responses' data - cannot be parsed or *APICallException* when OWM web API can not be + :raises: *ParseResponseException* when OWM Weather API responses' data + cannot be parsed or *APICallException* when OWM Weather API can not be reached """ assert type(ids_list) is list, "'ids_list' must be a list of integers" @@ -316,7 +337,7 @@ def weather_at_ids(self, ids_list): def weather_at_places(self, pattern, searchtype, limit=None): """ - Queries the OWM web API for the currently observed weather in all the + Queries the OWM Weather API for the currently observed weather in all the locations whose name is matching the specified text search parameters. A twofold search can be issued: *'accurate'* (exact matching) and *'like'* (matches names that are similar to the supplied pattern). @@ -332,8 +353,8 @@ def weather_at_places(self, pattern, searchtype, limit=None): :param limit: int or ``None`` :returns: a list of *Observation* objects or ``None`` if no weather data is available - :raises: *ParseResponseException* when OWM web API responses' data - cannot be parsed, *APICallException* when OWM web API can not be + :raises: *ParseResponseException* when OWM Weather API responses' data + cannot be parsed, *APICallException* when OWM Weather API can not be reached, *ValueError* when bad value is supplied for the search type or the maximum number of items retrieved """ @@ -359,15 +380,15 @@ def weather_at_places(self, pattern, searchtype, limit=None): @deprecated(will_be='removed', on_version=(3, 0, 0)) def weather_at_station(self, station_id): """ - Queries the OWM web API for the weather currently observed by a specific + Queries the OWM Weather API for the weather currently observed by a specific meteostation (eg: 29584) :param station_id: the meteostation ID :type station_id: int :returns: an *Observation* instance or ``None`` if no weather data is available - :raises: *ParseResponseException* when OWM web API responses' data - cannot be parsed or *APICallException* when OWM web API can not be + :raises: *ParseResponseException* when OWM Weather API responses' data + cannot be parsed or *APICallException* when OWM Weather API can not be reached """ assert type(station_id) is int, "'station_id' must be an int" @@ -386,7 +407,7 @@ def weather_at_stations_in_bbox(self, lat_top_left, lon_top_left, lat_bottom_right, lon_bottom_right, cluster=False, limit=None): """ - Queries the OWM web API for the weather currently observed by + Queries the OWM Weather API for the weather currently observed by meteostations inside the bounding box of latitude/longitude coords. :param lat_top_left: latitude for top-left of bounding box, must be @@ -408,8 +429,8 @@ def weather_at_stations_in_bbox(self, lat_top_left, lon_top_left, :param limit: int or ``None`` :returns: a list of *Observation* objects or ``None`` if no weather data is available - :raises: *ParseResponseException* when OWM web API responses' data - cannot be parsed, *APICallException* when OWM web API can not be + :raises: *ParseResponseException* when OWM Weather API responses' data + cannot be parsed, *APICallException* when OWM Weather API can not be reached, *ValueError* when coordinates values are out of bounds or negative values are provided for limit """ @@ -436,9 +457,59 @@ def weather_at_stations_in_bbox(self, lat_top_left, lon_top_left, _, json_data = self._wapi.cacheable_get_json(uri, params=params) return self._parsers['observation_list'].parse_JSON(json_data) + def weather_at_places_in_bbox(self, lon_left, lat_bottom, lon_right, lat_top, + zoom=10, cluster=False): + """ + Queries the OWM Weather API for the weather currently observed by + meteostations inside the bounding box of latitude/longitude coords. + + :param lat_top: latitude for top margin of bounding box, must be + between -90.0 and 90.0 + :type lat_top: int/float + :param lon_left: longitude for left margin of bounding box + must be between -180.0 and 180.0 + :type lon_left: int/float + :param lat_bottom: latitude for the bottom margin of bounding box, must + be between -90.0 and 90.0 + :type lat_bottom: int/float + :param lon_right: longitude for the right margin of bounding box, + must be between -180.0 and 180.0 + :type lon_right: int/float + :param zoom: zoom level (defaults to: 10) + :type zoom: int + :param cluster: use server clustering of points + :type cluster: bool + :returns: a list of *Observation* objects or ``None`` if no weather + data is available + :raises: *ParseResponseException* when OWM Weather API responses' data + cannot be parsed, *APICallException* when OWM Weather API can not be + reached, *ValueError* when coordinates values are out of bounds or + negative values are provided for limit + """ + geo.assert_is_lon(lon_left) + geo.assert_is_lon(lon_right) + geo.assert_is_lat(lat_bottom) + geo.assert_is_lat(lat_top) + assert type(zoom) is int, "'zoom' must be an int" + if zoom <= 0: + raise ValueError("'zoom' must greater than zero") + assert type(cluster) is bool, "'cluster' must be a bool" + params = {'bbox': ','.join([str(lon_left), + str(lat_bottom), + str(lon_right), + str(lat_top), + str(zoom)]), + 'cluster': 'yes' if cluster else 'no'} + uri = http_client.HttpClient.to_url(BBOX_CITY_URL, + self._API_key, + self._subscription_type, + self._use_ssl) + _, json_data = self._wapi.cacheable_get_json(uri, params=params) + return self._parsers['observation_list'].parse_JSON(json_data) + def weather_around_coords(self, lat, lon, limit=None): """ - Queries the OWM web API for the currently observed weather in all the + Queries the OWM Weather API for the currently observed weather in all the locations in the proximity of the specified coordinates. :param lat: location's latitude, must be between -90.0 and 90.0 @@ -450,8 +521,8 @@ def weather_around_coords(self, lat, lon, limit=None): :param limit: int or ``None`` :returns: a list of *Observation* objects or ``None`` if no weather data is available - :raises: *ParseResponseException* when OWM web API responses' data - cannot be parsed, *APICallException* when OWM web API can not be + :raises: *ParseResponseException* when OWM Weather API responses' data + cannot be parsed, *APICallException* when OWM Weather API can not be reached, *ValueError* when coordinates values are out of bounds or negative values are provided for limit """ @@ -472,7 +543,7 @@ def weather_around_coords(self, lat, lon, limit=None): def three_hours_forecast(self, name): """ - Queries the OWM web API for three hours weather forecast for the + Queries the OWM Weather API for three hours weather forecast for the specified location (eg: "London,uk"). A *Forecaster* object is returned, containing a *Forecast* instance covering a global streak of five days: this instance encapsulates *Weather* objects, with a time @@ -482,12 +553,12 @@ def three_hours_forecast(self, name): :type name: str or unicode :returns: a *Forecaster* instance or ``None`` if forecast data is not available for the specified location - :raises: *ParseResponseException* when OWM web API responses' data - cannot be parsed, *APICallException* when OWM web API can not be + :raises: *ParseResponseException* when OWM Weather API responses' data + cannot be parsed, *APICallException* when OWM Weather API can not be reached """ - stringutils.assert_is_string_or_unicode(name) - encoded_name = stringutils.encode_to_utf8(name) + assert isinstance(name, str), "Value must be a string" + encoded_name = name params = {'q': encoded_name, 'lang': self._language} uri = http_client.HttpClient.to_url(THREE_HOURS_FORECAST_URL, self._API_key, @@ -503,7 +574,7 @@ def three_hours_forecast(self, name): def three_hours_forecast_at_coords(self, lat, lon): """ - Queries the OWM web API for three hours weather forecast for the + Queries the OWM Weather API for three hours weather forecast for the specified geographic coordinate (eg: latitude: 51.5073509, longitude: -0.1277583). A *Forecaster* object is returned, containing a *Forecast* instance covering a global streak of @@ -516,8 +587,8 @@ def three_hours_forecast_at_coords(self, lat, lon): :type lon: int/float :returns: a *Forecaster* instance or ``None`` if forecast data is not available for the specified location - :raises: *ParseResponseException* when OWM web API responses' data - cannot be parsed, *APICallException* when OWM web API can not be + :raises: *ParseResponseException* when OWM Weather API responses' data + cannot be parsed, *APICallException* when OWM Weather API can not be reached """ geo.assert_is_lon(lon) @@ -537,7 +608,7 @@ def three_hours_forecast_at_coords(self, lat, lon): def three_hours_forecast_at_id(self, id): """ - Queries the OWM web API for three hours weather forecast for the + Queries the OWM Weather API for three hours weather forecast for the specified city ID (eg: 5128581). A *Forecaster* object is returned, containing a *Forecast* instance covering a global streak of five days: this instance encapsulates *Weather* objects, with a time @@ -547,8 +618,8 @@ def three_hours_forecast_at_id(self, id): :type id: int :returns: a *Forecaster* instance or ``None`` if forecast data is not available for the specified location - :raises: *ParseResponseException* when OWM web API responses' data - cannot be parsed, *APICallException* when OWM web API can not be + :raises: *ParseResponseException* when OWM Weather API responses' data + cannot be parsed, *APICallException* when OWM Weather API can not be reached """ assert type(id) is int, "'id' must be an int" @@ -569,7 +640,7 @@ def three_hours_forecast_at_id(self, id): def daily_forecast(self, name, limit=None): """ - Queries the OWM web API for daily weather forecast for the specified + Queries the OWM Weather API for daily weather forecast for the specified location (eg: "London,uk"). A *Forecaster* object is returned, containing a *Forecast* instance covering a global streak of fourteen days by default: this instance encapsulates *Weather* objects, with a @@ -583,12 +654,12 @@ def daily_forecast(self, name, limit=None): :type limit: int or ``None`` :returns: a *Forecaster* instance or ``None`` if forecast data is not available for the specified location - :raises: *ParseResponseException* when OWM web API responses' data - cannot be parsed, *APICallException* when OWM web API can not be + :raises: *ParseResponseException* when OWM Weather API responses' data + cannot be parsed, *APICallException* when OWM Weather API can not be reached, *ValueError* if negative values are supplied for limit """ - stringutils.assert_is_string_or_unicode(name) - encoded_name = stringutils.encode_to_utf8(name) + assert isinstance(name, str), "Value must be a string" + encoded_name = name if limit is not None: assert isinstance(limit, int), "'limit' must be an int or None" if limit < 1: @@ -610,7 +681,7 @@ def daily_forecast(self, name, limit=None): def daily_forecast_at_coords(self, lat, lon, limit=None): """ - Queries the OWM web API for daily weather forecast for the specified + Queries the OWM Weather API for daily weather forecast for the specified geographic coordinate (eg: latitude: 51.5073509, longitude: -0.1277583). A *Forecaster* object is returned, containing a *Forecast* instance covering a global streak of fourteen days by default: this instance @@ -627,8 +698,8 @@ def daily_forecast_at_coords(self, lat, lon, limit=None): :type limit: int or ``None`` :returns: a *Forecaster* instance or ``None`` if forecast data is not available for the specified location - :raises: *ParseResponseException* when OWM web API responses' data - cannot be parsed, *APICallException* when OWM web API can not be + :raises: *ParseResponseException* when OWM Weather API responses' data + cannot be parsed, *APICallException* when OWM Weather API can not be reached, *ValueError* if negative values are supplied for limit """ geo.assert_is_lon(lon) @@ -654,7 +725,7 @@ def daily_forecast_at_coords(self, lat, lon, limit=None): def daily_forecast_at_id(self, id, limit=None): """ - Queries the OWM web API for daily weather forecast for the specified + Queries the OWM Weather API for daily weather forecast for the specified city ID (eg: 5128581). A *Forecaster* object is returned, containing a *Forecast* instance covering a global streak of fourteen days by default: this instance encapsulates *Weather* objects, with a time @@ -668,8 +739,8 @@ def daily_forecast_at_id(self, id, limit=None): :type limit: int or ``None`` :returns: a *Forecaster* instance or ``None`` if forecast data is not available for the specified location - :raises: *ParseResponseException* when OWM web API responses' data - cannot be parsed, *APICallException* when OWM web API can not be + :raises: *ParseResponseException* when OWM Weather API responses' data + cannot be parsed, *APICallException* when OWM Weather API can not be reached, *ValueError* if negative values are supplied for limit """ assert type(id) is int, "'id' must be an int" @@ -697,7 +768,7 @@ def daily_forecast_at_id(self, id, limit=None): def weather_history_at_place(self, name, start=None, end=None): """ - Queries the OWM web API for weather history for the specified location + Queries the OWM Weather API for weather history for the specified location (eg: "London,uk"). A list of *Weather* objects is returned. It is possible to query for weather history in a closed time period, whose boundaries can be passed as optional parameters. @@ -713,16 +784,16 @@ def weather_history_at_place(self, name, start=None, end=None): :type end: int, ``datetime.datetime`` or ISO8601-formatted string :returns: a list of *Weather* instances or ``None`` if history data is not available for the specified location - :raises: *ParseResponseException* when OWM web API responses' data - cannot be parsed, *APICallException* when OWM web API can not be + :raises: *ParseResponseException* when OWM Weather API responses' data + cannot be parsed, *APICallException* when OWM Weather API can not be reached, *ValueError* if the time boundaries are not in the correct chronological order, if one of the time boundaries is not ``None`` and the other is or if one or both of the time boundaries are after the current time """ - stringutils.assert_is_string_or_unicode(name) - encoded_name = stringutils.encode_to_utf8(name) + assert isinstance(name, str), "Value must be a string" + encoded_name = name params = {'q': encoded_name, 'lang': self._language} if start is None and end is None: pass @@ -750,7 +821,7 @@ def weather_history_at_place(self, name, start=None, end=None): def weather_history_at_coords(self, lat, lon, start=None, end=None): """ - Queries the OWM web API for weather history for the specified at the + Queries the OWM Weather API for weather history for the specified at the specified geographic (eg: 51.503614, -0.107331). A list of *Weather* objects is returned. It is possible to query for weather history in a closed time period, whose boundaries can be passed as optional @@ -804,7 +875,7 @@ def weather_history_at_coords(self, lat, lon, start=None, end=None): def weather_history_at_id(self, id, start=None, end=None): """ - Queries the OWM web API for weather history for the specified city ID. + Queries the OWM Weather API for weather history for the specified city ID. A list of *Weather* objects is returned. It is possible to query for weather history in a closed time period, whose boundaries can be passed as optional parameters. @@ -820,8 +891,8 @@ def weather_history_at_id(self, id, start=None, end=None): :type end: int, ``datetime.datetime`` or ISO8601-formatted string :returns: a list of *Weather* instances or ``None`` if history data is not available for the specified location - :raises: *ParseResponseException* when OWM web API responses' data - cannot be parsed, *APICallException* when OWM web API can not be + :raises: *ParseResponseException* when OWM Weather API responses' data + cannot be parsed, *APICallException* when OWM Weather API can not be reached, *ValueError* if the time boundaries are not in the correct chronological order, if one of the time boundaries is not ``None`` and the other is or if one or both of the time boundaries are after @@ -859,7 +930,7 @@ def weather_history_at_id(self, id, start=None, end=None): @deprecated(will_be='removed', on_version=(3, 0, 0)) def station_at_coords(self, lat, lon, limit=None): """ - Queries the OWM web API for weather stations nearest to the + Queries the OWM Weather API for weather stations nearest to the specified geographic coordinates (eg: latitude: 51.5073509, longitude: -0.1277583). A list of *Station* objects is returned, this instance encapsulates a last reported *Weather* object. @@ -874,8 +945,8 @@ def station_at_coords(self, lat, lon, limit=None): :returns: a list of *Station* objects or ``None`` if station data is not available for the specified location - :raises: *ParseResponseException* when OWM web API responses' data - cannot be parsed, *APICallException* when OWM web API can not be + :raises: *ParseResponseException* when OWM Weather API responses' data + cannot be parsed, *APICallException* when OWM Weather API can not be reached """ geo.assert_is_lon(lon) @@ -896,7 +967,7 @@ def station_at_coords(self, lat, lon, limit=None): def station_tick_history(self, station_ID, limit=None): """ - Queries the OWM web API for historic weather data measurements for the + Queries the OWM Weather API for historic weather data measurements for the specified meteostation (eg: 2865), sampled once a minute (tick). A *StationHistory* object instance is returned, encapsulating the measurements: the total number of data points can be limited using the @@ -910,8 +981,8 @@ def station_tick_history(self, station_ID, limit=None): :type limit: int or ``None`` :returns: a *StationHistory* instance or ``None`` if data is not available for the specified meteostation - :raises: *ParseResponseException* when OWM web API responses' data - cannot be parsed, *APICallException* when OWM web API can not be + :raises: *ParseResponseException* when OWM Weather API responses' data + cannot be parsed, *APICallException* when OWM Weather API can not be reached, *ValueError* if the limit value is negative """ @@ -929,7 +1000,7 @@ def station_tick_history(self, station_ID, limit=None): def station_hour_history(self, station_ID, limit=None): """ - Queries the OWM web API for historic weather data measurements for the + Queries the OWM Weather API for historic weather data measurements for the specified meteostation (eg: 2865), sampled once a hour. A *Historian* object instance is returned, encapsulating a *StationHistory* objects which contains the measurements. The total @@ -944,8 +1015,8 @@ def station_hour_history(self, station_ID, limit=None): :type limit: int or ``None`` :returns: a *Historian* instance or ``None`` if data is not available for the specified meteostation - :raises: *ParseResponseException* when OWM web API responses' data - cannot be parsed, *APICallException* when OWM web API can not be + :raises: *ParseResponseException* when OWM Weather API responses' data + cannot be parsed, *APICallException* when OWM Weather API can not be reached, *ValueError* if the limit value is negative """ @@ -963,7 +1034,7 @@ def station_hour_history(self, station_ID, limit=None): def station_day_history(self, station_ID, limit=None): """ - Queries the OWM web API for historic weather data measurements for the + Queries the OWM Weather API for historic weather data measurements for the specified meteostation (eg: 2865), sampled once a day. A *Historian* object instance is returned, encapsulating a *StationHistory* objects which contains the measurements. The total @@ -978,8 +1049,8 @@ def station_day_history(self, station_ID, limit=None): :type limit: int or ``None`` :returns: a *Historian* instance or ``None`` if data is not available for the specified meteostation - :raises: *ParseResponseException* when OWM web API responses' data - cannot be parsed, *APICallException* when OWM web API can not be + :raises: *ParseResponseException* when OWM Weather API responses' data + cannot be parsed, *APICallException* when OWM Weather API can not be reached, *ValueError* if the limit value is negative """ @@ -1018,7 +1089,7 @@ def _retrieve_station_history(self, station_ID, limit, interval): def uvindex_around_coords(self, lat, lon): """ - Queries the OWM web API for Ultra Violet value sampled in the + Queries the OWM Weather API for Ultra Violet value sampled in the surroundings of the provided geocoordinates and in the specified time interval. A *UVIndex* object instance is returned, encapsulating a *Location* object and the UV intensity value. @@ -1028,8 +1099,8 @@ def uvindex_around_coords(self, lat, lon): :param lon: the location's longitude, must be between -180.0 and 180.0 :type lon: int/float :return: a *UVIndex* instance or ``None`` if data is not available - :raises: *ParseResponseException* when OWM web API responses' data - cannot be parsed, *APICallException* when OWM web API can not be + :raises: *ParseResponseException* when OWM Weather API responses' data + cannot be parsed, *APICallException* when OWM Weather API can not be reached, *ValueError* for wrong input values """ geo.assert_is_lon(lon) @@ -1041,7 +1112,7 @@ def uvindex_around_coords(self, lat, lon): def uvindex_forecast_around_coords(self, lat, lon): """ - Queries the OWM web API for forecast Ultra Violet values in the next 8 + Queries the OWM Weather API for forecast Ultra Violet values in the next 8 days in the surroundings of the provided geocoordinates. :param lat: the location's latitude, must be between -90.0 and 90.0 @@ -1049,8 +1120,8 @@ def uvindex_forecast_around_coords(self, lat, lon): :param lon: the location's longitude, must be between -180.0 and 180.0 :type lon: int/float :return: a list of *UVIndex* instances or empty list if data is not available - :raises: *ParseResponseException* when OWM web API responses' data - cannot be parsed, *APICallException* when OWM web API can not be + :raises: *ParseResponseException* when OWM Weather API responses' data + cannot be parsed, *APICallException* when OWM Weather API can not be reached, *ValueError* for wrong input values """ geo.assert_is_lon(lon) @@ -1062,7 +1133,7 @@ def uvindex_forecast_around_coords(self, lat, lon): def uvindex_history_around_coords(self, lat, lon, start, end=None): """ - Queries the OWM web API for UV index historical values in the + Queries the OWM Weather API for UV index historical values in the surroundings of the provided geocoordinates and in the specified time frame. If the end of the time frame is not provided, that is intended to be the current datetime. @@ -1078,8 +1149,8 @@ def uvindex_history_around_coords(self, lat, lon, start, end=None): will be used) :type end: int, ``datetime.datetime`` or ISO8601-formatted string :return: a list of *UVIndex* instances or empty list if data is not available - :raises: *ParseResponseException* when OWM web API responses' data - cannot be parsed, *APICallException* when OWM web API can not be + :raises: *ParseResponseException* when OWM Weather API responses' data + cannot be parsed, *APICallException* when OWM Weather API can not be reached, *ValueError* for wrong input values """ geo.assert_is_lon(lon) @@ -1099,7 +1170,7 @@ def uvindex_history_around_coords(self, lat, lon, start, end=None): def coindex_around_coords(self, lat, lon, start=None, interval=None): """ - Queries the OWM web API for Carbon Monoxide values sampled in the + Queries the OWM Weather API for Carbon Monoxide values sampled in the surroundings of the provided geocoordinates and in the specified time interval. A *COIndex* object instance is returned, encapsulating a @@ -1122,8 +1193,8 @@ def coindex_around_coords(self, lat, lon, start=None, interval=None): `start` (defaults to ``None``). If not provided, 'year' is used :type interval: str among: 'minute', 'hour', 'day', 'month, 'year' :return: a *COIndex* instance or ``None`` if data is not available - :raises: *ParseResponseException* when OWM web API responses' data - cannot be parsed, *APICallException* when OWM web API can not be + :raises: *ParseResponseException* when OWM Weather API responses' data + cannot be parsed, *APICallException* when OWM Weather API can not be reached, *ValueError* for wrong input values """ geo.assert_is_lon(lon) @@ -1138,7 +1209,7 @@ def coindex_around_coords(self, lat, lon, start=None, interval=None): def ozone_around_coords(self, lat, lon, start=None, interval=None): """ - Queries the OWM web API for Ozone (O3) value in Dobson Units sampled in + Queries the OWM Weather API for Ozone (O3) value in Dobson Units sampled in the surroundings of the provided geocoordinates and in the specified time interval. An *Ozone* object instance is returned, encapsulating a *Location* object and the UV intensity value. @@ -1160,8 +1231,8 @@ def ozone_around_coords(self, lat, lon, start=None, interval=None): `start` (defaults to ``None``). If not provided, 'year' is used :type interval: str among: 'minute', 'hour', 'day', 'month, 'year' :return: an *Ozone* instance or ``None`` if data is not available - :raises: *ParseResponseException* when OWM web API responses' data - cannot be parsed, *APICallException* when OWM web API can not be + :raises: *ParseResponseException* when OWM Weather API responses' data + cannot be parsed, *APICallException* when OWM Weather API can not be reached, *ValueError* for wrong input values """ geo.assert_is_lon(lon) @@ -1176,7 +1247,7 @@ def ozone_around_coords(self, lat, lon, start=None, interval=None): def no2index_around_coords(self, lat, lon, start=None, interval=None): """ - Queries the OWM web API for Nitrogen Dioxide values sampled in the + Queries the OWM Weather API for Nitrogen Dioxide values sampled in the surroundings of the provided geocoordinates and in the specified time interval. A *NO2Index* object instance is returned, encapsulating a @@ -1199,8 +1270,8 @@ def no2index_around_coords(self, lat, lon, start=None, interval=None): `start` (defaults to ``None``). If not provided, 'year' is used :type interval: str among: 'minute', 'hour', 'day', 'month, 'year' :return: a *NO2Index* instance or ``None`` if data is not available - :raises: *ParseResponseException* when OWM web API responses' data - cannot be parsed, *APICallException* when OWM web API can not be + :raises: *ParseResponseException* when OWM Weather API responses' data + cannot be parsed, *APICallException* when OWM Weather API can not be reached, *ValueError* for wrong input values """ geo.assert_is_lon(lon) @@ -1215,7 +1286,7 @@ def no2index_around_coords(self, lat, lon, start=None, interval=None): def so2index_around_coords(self, lat, lon, start=None, interval=None): """ - Queries the OWM web API for Sulphur Dioxide values sampled in the + Queries the OWM Weather API for Sulphur Dioxide values sampled in the surroundings of the provided geocoordinates and in the specified time interval. A *SO2Index* object instance is returned, encapsulating a @@ -1238,8 +1309,8 @@ def so2index_around_coords(self, lat, lon, start=None, interval=None): `start` (defaults to ``None``). If not provided, 'year' is used :type interval: str among: 'minute', 'hour', 'day', 'month, 'year' :return: a *SO2Index* instance or ``None`` if data is not available - :raises: *ParseResponseException* when OWM web API responses' data - cannot be parsed, *APICallException* when OWM web API can not be + :raises: *ParseResponseException* when OWM Weather API responses' data + cannot be parsed, *APICallException* when OWM Weather API can not be reached, *ValueError* for wrong input values """ geo.assert_is_lon(lon) @@ -1253,7 +1324,7 @@ def so2index_around_coords(self, lat, lon, start=None, interval=None): return so2index def __repr__(self): - return "<%s.%s - API key=%s, OWM web API version=%s, " \ + return "<%s.%s - API key=%s, OWM Weather API version=%s, " \ "subscription type=%s, PyOWM version=%s, language=%s>" % \ (__name__, self.__class__.__name__, diff --git a/tests/integration/webapi25/__init__.py b/pyowm/weatherapi25/parsers/__init__.py similarity index 100% rename from tests/integration/webapi25/__init__.py rename to pyowm/weatherapi25/parsers/__init__.py diff --git a/pyowm/webapi25/parsers/forecastparser.py b/pyowm/weatherapi25/parsers/forecastparser.py similarity index 92% rename from pyowm/webapi25/parsers/forecastparser.py rename to pyowm/weatherapi25/parsers/forecastparser.py index 399cb552..8c2736ce 100644 --- a/pyowm/webapi25/parsers/forecastparser.py +++ b/pyowm/weatherapi25/parsers/forecastparser.py @@ -5,9 +5,9 @@ import json import time -from pyowm.webapi25 import location -from pyowm.webapi25 import weather -from pyowm.webapi25 import forecast +from pyowm.weatherapi25 import location +from pyowm.weatherapi25 import weather +from pyowm.weatherapi25 import forecast from pyowm.abstractions import jsonparser from pyowm.exceptions import parse_response_error, api_response_error @@ -15,7 +15,7 @@ class ForecastParser(jsonparser.JSONParser): """ Concrete *JSONParser* implementation building a *Forecast* instance out - of raw JSON data coming from OWM web API responses. + of raw JSON data coming from OWM Weather API responses. """ @@ -33,7 +33,7 @@ def parse_JSON(self, JSON_string): :returns: a *Forecast* instance or ``None`` if no data is available :raises: *ParseResponseError* if it is impossible to find or parse the data needed to build the result, *APIResponseError* if the JSON - string embeds an HTTP status error (this is an OWM web API 2.5 bug) + string embeds an HTTP status error """ if JSON_string is None: diff --git a/pyowm/webapi25/parsers/observationlistparser.py b/pyowm/weatherapi25/parsers/observationlistparser.py similarity index 75% rename from pyowm/webapi25/parsers/observationlistparser.py rename to pyowm/weatherapi25/parsers/observationlistparser.py index 74350d84..2a88fe5b 100644 --- a/pyowm/webapi25/parsers/observationlistparser.py +++ b/pyowm/weatherapi25/parsers/observationlistparser.py @@ -5,7 +5,7 @@ import json from pyowm.abstractions.jsonparser import JSONParser -from pyowm.webapi25.parsers.observationparser import ObservationParser +from pyowm.weatherapi25.parsers.observationparser import ObservationParser from pyowm.exceptions.parse_response_error import ParseResponseError from pyowm.exceptions.api_response_error import APIResponseError @@ -13,7 +13,7 @@ class ObservationListParser(JSONParser): """ Concrete *JSONParser* implementation building a list of *Observation* - instances out of raw JSON data coming from OWM web API responses. + instances out of raw JSON data coming from OWM Weather API responses. """ @@ -29,7 +29,7 @@ def parse_JSON(self, JSON_string): available :raises: *ParseResponseError* if it is impossible to find or parse the data needed to build the result, *APIResponseError* if the OWM API - returns a HTTP status error (this is an OWM web API 2.5 bug) + returns a HTTP status error """ if JSON_string is None: @@ -41,16 +41,20 @@ def parse_JSON(self, JSON_string): # of HTTP error status codes by the OWM API 2.5. This mechanism is # supposed to be deprecated as soon as the API fully adopts HTTP for # conveying errors to the clients - if d['cod'] == "404": - print("OWM API: data not found - response payload: " + \ - json.dumps(d)) - return None - if d['cod'] != "200": - raise APIResponseError("OWM API: error - response payload: " + json.dumps(d), d['cod']) + if d['cod'] == "200" or d['cod'] == 200: + pass + else: + if d['cod'] == "404" or d['cod'] == 404: + print("OWM API: data not found - response payload: " + json.dumps(d)) + return None + else: + raise APIResponseError("OWM API: error - response payload: " + json.dumps(d), str(d['cod'])) # Handle the case when no results are found if 'count' in d and d['count'] == "0": return [] + if 'cnt' in d and d['cnt'] == 0: + return [] if 'list' in d: return [observation_parser.parse_JSON(json.dumps(item)) \ for item in d['list']] diff --git a/pyowm/webapi25/parsers/observationparser.py b/pyowm/weatherapi25/parsers/observationparser.py similarity index 91% rename from pyowm/webapi25/parsers/observationparser.py rename to pyowm/weatherapi25/parsers/observationparser.py index a288ec99..805af896 100644 --- a/pyowm/webapi25/parsers/observationparser.py +++ b/pyowm/weatherapi25/parsers/observationparser.py @@ -5,9 +5,9 @@ from json import loads, dumps from time import time -from pyowm.webapi25 import observation -from pyowm.webapi25 import location -from pyowm.webapi25 import weather +from pyowm.weatherapi25 import observation +from pyowm.weatherapi25 import location +from pyowm.weatherapi25 import weather from pyowm.abstractions import jsonparser from pyowm.exceptions import parse_response_error, api_response_error @@ -15,7 +15,7 @@ class ObservationParser(jsonparser.JSONParser): """ Concrete *JSONParser* implementation building an *Observation* instance out - of raw JSON data coming from OWM web API responses. + of raw JSON data coming from OWM Weather API responses. """ @@ -33,7 +33,7 @@ def parse_JSON(self, JSON_string): :returns: an *Observation* instance or ``None`` if no data is available :raises: *ParseResponseError* if it is impossible to find or parse the data needed to build the result, *APIResponseError* if the JSON - string embeds an HTTP status error (this is an OWM web API 2.5 bug) + string embeds an HTTP status error """ if JSON_string is None: diff --git a/pyowm/webapi25/parsers/stationhistoryparser.py b/pyowm/weatherapi25/parsers/stationhistoryparser.py similarity index 95% rename from pyowm/webapi25/parsers/stationhistoryparser.py rename to pyowm/weatherapi25/parsers/stationhistoryparser.py index 5622f12a..518c89a4 100644 --- a/pyowm/webapi25/parsers/stationhistoryparser.py +++ b/pyowm/weatherapi25/parsers/stationhistoryparser.py @@ -5,7 +5,7 @@ import json import time -from pyowm.webapi25 import stationhistory +from pyowm.weatherapi25 import stationhistory from pyowm.abstractions import jsonparser from pyowm.exceptions import parse_response_error, api_response_error @@ -13,7 +13,7 @@ class StationHistoryParser(jsonparser.JSONParser): """ Concrete *JSONParser* implementation building a *StationHistory* instance - out of raw JSON data coming from OWM web API responses. + out of raw JSON data coming from OWM Weather API responses. """ @@ -32,7 +32,7 @@ def parse_JSON(self, JSON_string): available :raises: *ParseResponseError* if it is impossible to find or parse the data needed to build the result, *APIResponseError* if the JSON - string embeds an HTTP status error (this is an OWM web API 2.5 bug) + string embeds an HTTP status error """ if JSON_string is None: diff --git a/pyowm/webapi25/parsers/stationlistparser.py b/pyowm/weatherapi25/parsers/stationlistparser.py similarity index 85% rename from pyowm/webapi25/parsers/stationlistparser.py rename to pyowm/weatherapi25/parsers/stationlistparser.py index cde99bd5..1e95991d 100644 --- a/pyowm/webapi25/parsers/stationlistparser.py +++ b/pyowm/weatherapi25/parsers/stationlistparser.py @@ -6,14 +6,14 @@ import json from pyowm.abstractions.jsonparser import JSONParser -from pyowm.webapi25.parsers.stationparser import StationParser +from pyowm.weatherapi25.parsers.stationparser import StationParser from pyowm.exceptions.parse_response_error import ParseResponseError class StationListParser(JSONParser): """ Concrete *JSONParser* implementation building a list of *Station* - instances out of raw JSON data coming from OWM web API responses. + instances out of raw JSON data coming from OWM Weather API responses. """ @@ -29,7 +29,7 @@ def parse_JSON(self, JSON_string): available :raises: *ParseResponseError* if it is impossible to find or parse the data needed to build the result, *APIResponseError* if the OWM API - returns a HTTP status error (this is an OWM web API 2.5 bug) + returns a HTTP status error """ if JSON_string is None: diff --git a/pyowm/webapi25/parsers/stationparser.py b/pyowm/weatherapi25/parsers/stationparser.py similarity index 91% rename from pyowm/webapi25/parsers/stationparser.py rename to pyowm/weatherapi25/parsers/stationparser.py index a5b09b2e..34fb3c38 100644 --- a/pyowm/webapi25/parsers/stationparser.py +++ b/pyowm/weatherapi25/parsers/stationparser.py @@ -6,8 +6,8 @@ import json import time -from pyowm.webapi25 import station -from pyowm.webapi25 import weather +from pyowm.weatherapi25 import station +from pyowm.weatherapi25 import weather from pyowm.abstractions import jsonparser from pyowm.exceptions import parse_response_error, api_response_error @@ -15,7 +15,7 @@ class StationParser(jsonparser.JSONParser): """ Concrete *JSONParser* implementation building a *Station* instance - out of raw JSON data coming from OWM web API responses. + out of raw JSON data coming from OWM Weather API responses. """ @@ -30,7 +30,7 @@ def parse_JSON(self, JSON_string): :returns: a *Station* instance or ``None`` if no data is available :raises: *ParseResponseError* if it is impossible to find or parse the data needed to build the result, *APIResponseError* if the JSON - string embeds an HTTP status error (this is an OWM web API 2.5 bug) + string embeds an HTTP status error """ if JSON_string is None: diff --git a/pyowm/webapi25/parsers/weatherhistoryparser.py b/pyowm/weatherapi25/parsers/weatherhistoryparser.py similarity index 94% rename from pyowm/webapi25/parsers/weatherhistoryparser.py rename to pyowm/weatherapi25/parsers/weatherhistoryparser.py index ae146a61..56c1f1a5 100644 --- a/pyowm/webapi25/parsers/weatherhistoryparser.py +++ b/pyowm/weatherapi25/parsers/weatherhistoryparser.py @@ -4,7 +4,7 @@ """ import json -from pyowm.webapi25 import weather +from pyowm.weatherapi25 import weather from pyowm.abstractions import jsonparser from pyowm.exceptions import parse_response_error, api_response_error @@ -12,7 +12,7 @@ class WeatherHistoryParser(jsonparser.JSONParser): """ Concrete *JSONParser* implementation building a list of *Weather* instances - out of raw JSON data coming from OWM web API responses. + out of raw JSON data coming from OWM Weather API responses. """ @@ -31,7 +31,7 @@ def parse_JSON(self, JSON_string): available :raises: *ParseResponseError* if it is impossible to find or parse the data needed to build the result, *APIResponseError* if the JSON - string embeds an HTTP status error (this is an OWM web API 2.5 bug) + string embeds an HTTP status error """ if JSON_string is None: diff --git a/pyowm/webapi25/station.py b/pyowm/weatherapi25/station.py similarity index 97% rename from pyowm/webapi25/station.py rename to pyowm/weatherapi25/station.py index 82e22315..9921249b 100644 --- a/pyowm/webapi25/station.py +++ b/pyowm/weatherapi25/station.py @@ -5,8 +5,8 @@ import json import xml.etree.ElementTree as ET -from pyowm.webapi25 import weather -from pyowm.webapi25.xsd.xmlnsconfig import ( +from pyowm.weatherapi25 import weather +from pyowm.weatherapi25.xsd.xmlnsconfig import ( LIST_STATION_XMLNS_PREFIX, LIST_STATION_XMLNS_URL) from pyowm.utils import xmlutils from pyowm.abstractions.decorators import deprecated @@ -40,7 +40,7 @@ class Station(object): """ @deprecated(will_be='removed', on_version=(3, 0, 0), - name='webapi25.station.Station') + name='weatherapi25.station.Station') def __init__(self, name, station_ID, station_type, status, lat, lon, distance=None, last_weather=None): diff --git a/pyowm/webapi25/stationhistory.py b/pyowm/weatherapi25/stationhistory.py similarity index 97% rename from pyowm/webapi25/stationhistory.py rename to pyowm/weatherapi25/stationhistory.py index 18e2d3c7..114a6f43 100644 --- a/pyowm/webapi25/stationhistory.py +++ b/pyowm/weatherapi25/stationhistory.py @@ -5,7 +5,7 @@ import json import xml.etree.ElementTree as ET -from pyowm.webapi25.xsd.xmlnsconfig import ( +from pyowm.weatherapi25.xsd.xmlnsconfig import ( STATION_HISTORY_XMLNS_PREFIX, STATION_HISTORY_XMLNS_URL) from pyowm.utils import timeformatutils, xmlutils @@ -15,7 +15,7 @@ class StationHistory(object): """ A class representing historic weather measurements collected by a meteostation. Three types of historic meteostation data can be obtained by - the OWM web API: ticks (one data chunk per minute) data, hourly and daily + the OWM Weather API: ticks (one data chunk per minute) data, hourly and daily data. :param station_ID: the numeric ID of the meteostation @@ -95,7 +95,7 @@ def get_measurements(self): def get_reception_time(self, timeformat='unix'): """Returns the GMT time telling when the meteostation history data was - received from the OWM web API + received from the OWM Weather API :param timeformat: the format for the time value. May be: '*unix*' (default) for UNIX time diff --git a/pyowm/weatherapi25/uris.py b/pyowm/weatherapi25/uris.py new file mode 100644 index 00000000..3f127c45 --- /dev/null +++ b/pyowm/weatherapi25/uris.py @@ -0,0 +1,5 @@ +""" +URIs templates for resources exposed by the Weather API 2.5 +""" + +ICONS_BASE_URL = 'http://openweathermap.org/img/w/%s.png' diff --git a/pyowm/webapi25/weather.py b/pyowm/weatherapi25/weather.py similarity index 98% rename from pyowm/webapi25/weather.py rename to pyowm/weatherapi25/weather.py index c4aeba3b..9fe65d4d 100644 --- a/pyowm/webapi25/weather.py +++ b/pyowm/weatherapi25/weather.py @@ -4,9 +4,10 @@ import json import xml.etree.ElementTree as ET -from pyowm.webapi25.xsd.xmlnsconfig import ( +from pyowm.weatherapi25.xsd.xmlnsconfig import ( WEATHER_XMLNS_PREFIX, WEATHER_XMLNS_URL) from pyowm.utils import timeformatutils, temputils, xmlutils +from pyowm.weatherapi25.uris import ICONS_BASE_URL class Weather(object): @@ -206,7 +207,7 @@ def get_temperature(self, unit='kelvin'): :raises: ValueError when unknown temperature units are provided """ - # This is due to the fact that the OWM web API responses are mixing + # This is due to the fact that the OWM Weather API responses are mixing # absolute temperatures and temperature deltas together to_be_converted = dict() not_to_be_converted = dict() @@ -251,6 +252,14 @@ def get_weather_icon_name(self): """ return self._weather_icon_name + def get_weather_icon_url(self): + """Returns weather-related icon URL as a Unicode string. + + :returns: the icon URL. + + """ + return ICONS_BASE_URL % self._weather_icon_name + def get_visibility_distance(self): """Returns the visibility distance as a float diff --git a/pyowm/webapi25/weathercoderegistry.py b/pyowm/weatherapi25/weathercoderegistry.py similarity index 100% rename from pyowm/webapi25/weathercoderegistry.py rename to pyowm/weatherapi25/weathercoderegistry.py diff --git a/tests/unit/webapi25/__init__.py b/pyowm/weatherapi25/xsd/__init__.py similarity index 100% rename from tests/unit/webapi25/__init__.py rename to pyowm/weatherapi25/xsd/__init__.py diff --git a/pyowm/webapi25/xsd/forecast.xsd b/pyowm/weatherapi25/xsd/forecast.xsd similarity index 100% rename from pyowm/webapi25/xsd/forecast.xsd rename to pyowm/weatherapi25/xsd/forecast.xsd diff --git a/pyowm/webapi25/xsd/location.xsd b/pyowm/weatherapi25/xsd/location.xsd similarity index 100% rename from pyowm/webapi25/xsd/location.xsd rename to pyowm/weatherapi25/xsd/location.xsd diff --git a/pyowm/webapi25/xsd/observation.xsd b/pyowm/weatherapi25/xsd/observation.xsd similarity index 100% rename from pyowm/webapi25/xsd/observation.xsd rename to pyowm/weatherapi25/xsd/observation.xsd diff --git a/pyowm/webapi25/xsd/station.xsd b/pyowm/weatherapi25/xsd/station.xsd similarity index 100% rename from pyowm/webapi25/xsd/station.xsd rename to pyowm/weatherapi25/xsd/station.xsd diff --git a/pyowm/webapi25/xsd/station_history.xsd b/pyowm/weatherapi25/xsd/station_history.xsd similarity index 100% rename from pyowm/webapi25/xsd/station_history.xsd rename to pyowm/weatherapi25/xsd/station_history.xsd diff --git a/pyowm/webapi25/xsd/weather.xsd b/pyowm/weatherapi25/xsd/weather.xsd similarity index 100% rename from pyowm/webapi25/xsd/weather.xsd rename to pyowm/weatherapi25/xsd/weather.xsd diff --git a/pyowm/webapi25/xsd/xmlnsconfig.py b/pyowm/weatherapi25/xsd/xmlnsconfig.py similarity index 97% rename from pyowm/webapi25/xsd/xmlnsconfig.py rename to pyowm/weatherapi25/xsd/xmlnsconfig.py index 4fe54778..67e693fa 100644 --- a/pyowm/webapi25/xsd/xmlnsconfig.py +++ b/pyowm/weatherapi25/xsd/xmlnsconfig.py @@ -3,7 +3,7 @@ """ # XML Schemas URLs for PyOWM model entities -ROOT_XMLNS_URL = 'http://github.com/csparpa/pyowm/tree/master/pyowm/webapi25/xsd' +ROOT_XMLNS_URL = 'http://github.com/csparpa/pyowm/tree/master/pyowm/weatherapi25/xsd' LOCATION_XMLNS_URL = ROOT_XMLNS_URL + '/location.xsd' WEATHER_XMLNS_URL = ROOT_XMLNS_URL + '/weather.xsd' OBSERVATION_XMLNS_URL = ROOT_XMLNS_URL + '/observation.xsd' diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 94eadf6b..00000000 --- a/requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -geojson>=2.3.0,<2.4 -requests>=2.18.2,<3 \ No newline at end of file diff --git a/scripts/generate_city_id_files.py b/scripts/generate_city_id_files.py index 482d0c81..f9baeeb1 100644 --- a/scripts/generate_city_id_files.py +++ b/scripts/generate_city_id_files.py @@ -23,23 +23,23 @@ """ def download_the_files(): - print 'Downloading file '+city_list_url+' ...' + print('Downloading file '+city_list_url+' ...') with open(city_list_gz, 'wb') as h: response = requests.get(city_list_url, stream=True) for block in response.iter_content(1024): h.write(block) - print 'Downloading file '+us_city_list_url+' ...' + print('Downloading file '+us_city_list_url+' ...') with open(us_city_list_gz, 'wb') as g: response = requests.get(us_city_list_url, stream=True) for block in response.iter_content(1024): g.write(block) - print ' ... done' + print(' ... done') def read_all_cities_into_dict(): - print 'Reading city data from files ...' + print('Reading city data from files ...') all_cities = dict() # All cities @@ -48,7 +48,7 @@ def read_all_cities_into_dict(): for city_dict in cities: # eg. {"id":707860,"name":"Hurzuf","country":"UA","coord":{"lon":34.283333,"lat":44.549999}} if city_dict['id'] in all_cities: - print 'Warning: city ID %d was already processed! Data chunk is: %s' % (city_dict['id'], city_dict) + print('Warning: city ID %d was already processed! Data chunk is: %s' % (city_dict['id'], city_dict)) continue else: all_cities[city_dict['id']] = dict(name=city_dict['name'], @@ -63,7 +63,7 @@ def read_all_cities_into_dict(): for city_dict in cities: # eg. {"id":707860,"name":"Hurzuf","country":"UA","coord":{"lon":34.283333,"lat":44.549999}} if city_dict['id'] in all_cities: - print 'Warning: city ID %d was already processed! Data chunk is: %s' % (city_dict['id'], city_dict) + print('Warning: city ID %d was already processed! Data chunk is: %s' % (city_dict['id'], city_dict)) continue else: all_cities[city_dict['id']] = dict(name=city_dict['name'], @@ -71,16 +71,16 @@ def read_all_cities_into_dict(): lon=city_dict['coord']['lon'], lat=city_dict['coord']['lat']) except Exception as e: - print 'Impossible to read US cities: {}'.format(e) + print('Impossible to read US cities: {}'.format(e)) - print '... done' + print('... done') return all_cities def order_dict_by_city_id(all_cities): - print 'Ordering city dict by city ID ...' + print('Ordering city dict by city ID ...') all_cities_ordered = collections.OrderedDict(sorted(all_cities.items())) - print '... done' + print('... done') return all_cities_ordered @@ -89,11 +89,11 @@ def city_to_string(city_id, city_dict): 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' - print '-> from "g" = ASCII 103 to "l" = ASCII 108' - print '-> from "m" = ASCII 109 to "r" = ASCII 114' - print '-> from "s" = ASCII 115 to "z" = ASCII 122' + 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') + print('-> from "g" = ASCII 103 to "l" = ASCII 108') + print('-> from "m" = ASCII 109 to "r" = ASCII 114') + print('-> from "s" = ASCII 115 to "z" = ASCII 122') ss = [list(), list(), list(), list()] for city_id in cities_dict: name = cities_dict[city_id]['name'].lower() @@ -116,12 +116,12 @@ def split_keyset(cities_dict): continue else: continue # not a letter - print '... done' + print('... done') return ss def write_subsets_to_files(ssets, outdir): - print 'Writing subsets to files ...' + print('Writing subsets to files ...') with codecs.open("%s%s097-102.txt" % (outdir, os.sep), "w", "utf-8") as f: for city_string in sorted(ssets[0]): @@ -138,18 +138,18 @@ def write_subsets_to_files(ssets, outdir): "w", "utf-8") as f: for city_string in sorted(ssets[3]): f.write(city_string+"\n") - print '... done' + print('... done') def gzip_csv_compress(plaintext_csv, target_gzip): - print 'G-zipping: %s -> %s ...' % (plaintext_csv, target_gzip) + print('G-zipping: %s -> %s ...' % (plaintext_csv, target_gzip)) with open(plaintext_csv, 'r') as source: source_rows = csv.reader(source) with gzip.open(target_gzip, "wt") as file: writer = csv.writer(file) for row in source_rows: writer.writerow(row) - print '... done' + print( '... done') def gzip_all(outdir): @@ -162,15 +162,16 @@ def gzip_all(outdir): gzip_csv_compress('%s%s115-122.txt' % (outdir, os.sep), '%s%s115-122.txt.gz' % (outdir, os.sep)) + if __name__ == '__main__': target_folder = os.path.abspath(sys.argv[1]) - print 'Will save output files to folder: %s' % (target_folder,) - print 'Job started' + print('Will save output files to folder: %s' % (target_folder,)) + print('Job started') download_the_files() cities = read_all_cities_into_dict() ordered_cities = order_dict_by_city_id(cities) ssets = split_keyset(ordered_cities) write_subsets_to_files(ssets, target_folder) gzip_all(target_folder) - print 'Job finished' + print('Job finished') diff --git a/scripts/generate_pypi_dist.sh b/scripts/generate_pypi_dist.sh index 79eb433c..4bc85c02 100644 --- a/scripts/generate_pypi_dist.sh +++ b/scripts/generate_pypi_dist.sh @@ -9,9 +9,6 @@ echo 'Generating source distribution...' python2.7 setup.py sdist echo 'Generating .egg distributions...' -python2.7 setup.py bdist_egg -python3.2 setup.py bdist_egg -python3.3 setup.py bdist_egg python3.4 setup.py bdist_egg python3.5 setup.py bdist_egg python3.6 setup.py bdist_egg diff --git a/scripts/generate_sphinx_docs.sh b/scripts/generate_sphinx_docs.sh index bb4dd186..e676a498 100644 --- a/scripts/generate_sphinx_docs.sh +++ b/scripts/generate_sphinx_docs.sh @@ -1,7 +1,13 @@ #!/usr/bin/env bash -echo 'Generating Sphinx HTML documentations...' +echo '*** Generating Sphinx HTML documentations...' cd ../sphinx make clean make html -echo 'Done' + +if [ "$?" -ne 0 ]; then + echo "*** Errors occurred" + exit 1 +fi + +echo '*** Done' diff --git a/scripts/run_integration_tests.sh b/scripts/run_integration_tests.sh deleted file mode 100644 index 66973f13..00000000 --- a/scripts/run_integration_tests.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/usr/bin/env bash - -set -o errexit - -if [ -z "$OWM_API_KEY" ]; then - echo "OWM_API_KEY env variable is not set: aborting" - exit 1 -fi - -export OWM_API_KEY -cd ../tests/integration -tox - -echo "--- End of integration tests ---" diff --git a/scripts/update-pipfilelock.sh b/scripts/update-pipfilelock.sh index fc973758..a324fc8f 100644 --- a/scripts/update-pipfilelock.sh +++ b/scripts/update-pipfilelock.sh @@ -1,6 +1,20 @@ #!/usr/bin/env bash -echo 'Updating Pipfile.lock ...' -cd .. +THIS_FOLDER="$(pwd)" +PYOWM_FOLDER="$(dirname $(pwd))" + +echo '*** Creating temporary virtualenv...' +virtualenv pipfilelocker +source pipfilelocker/bin/activate +pip install pipenv + +echo '*** Updating Pipfile.lock...' +cd "$PYOWM_FOLDER" pipenv lock -echo 'Done' + +echo '*** Removing temporary virtualenv...' +deactivate +cd "$THIS_FOLDER" +rm -rf pipfilelocker + +echo '*** Done' \ No newline at end of file diff --git a/setup.py b/setup.py index 7f32709e..bc7c18c3 100644 --- a/setup.py +++ b/setup.py @@ -10,27 +10,30 @@ author='Claudio Sparpaglione (@csparpa)', author_email='csparpa@gmail.com', url='http://github.com/csparpa/pyowm', - packages=['pyowm', 'pyowm.abstractions', 'pyowm.alertapi30', - 'pyowm.caches', 'pyowm.commons', + packages=['pyowm', + 'pyowm.abstractions', + 'pyowm.agroapi10', + 'pyowm.alertapi30', + 'pyowm.caches', + 'pyowm.commons', 'pyowm.exceptions', 'pyowm.pollutionapi30', 'pyowm.pollutionapi30.xsd', 'pyowm.uvindexapi30', 'pyowm.uvindexapi30.xsd', + 'pyowm.tiles', 'pyowm.utils', - 'pyowm.webapi25', 'pyowm.webapi25.cityids', 'pyowm.webapi25.parsers', 'pyowm.webapi25.xsd', + 'pyowm.weatherapi25', 'pyowm.weatherapi25.cityids', 'pyowm.weatherapi25.parsers', 'pyowm.weatherapi25.xsd', 'pyowm.stationsapi30', 'pyowm.stationsapi30.parsers', 'pyowm.stationsapi30.xsd'], long_description="""PyOWM is a client Python wrapper library for OpenWeatherMap web APIs. It allows quick and easy consumption of OWM data from Python applications via a simple object model and in a human-friendly fashion.""", include_package_data=True, install_requires=[ - 'requests>=2.18.2,<3', - 'geojson>=2.3.0,<2.4' + 'requests>=2.20.0,<3', + 'geojson>=2.3.0,<3' ], + python_requires='>=3.4', classifiers=[ "License :: OSI Approved :: MIT License", "Programming Language :: Python", - "Programming Language :: Python :: 2.7", - "Programming Language :: Python :: 3.2", - "Programming Language :: Python :: 3.3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", @@ -43,6 +46,6 @@ package_data={ '': ['*.gz', '*.xsd', '*.md', '*.txt', '*.json'] }, - keywords='openweathermap web api client wrapper weather forecast data owm pollution sdk meteostation', + keywords='openweathermap web api client weather forecast uv alerting owm pollution meteostation agro agriculture', license='MIT' ) diff --git a/sphinx/index.rst b/sphinx/index.rst index 339b8f56..819ef6eb 100644 --- a/sphinx/index.rst +++ b/sphinx/index.rst @@ -8,6 +8,14 @@ PyOWM Welcome to PyOWM's documentation! +.. image:: ../logos/180x180.png + :width: 180px + :height: 180px + :scale: 100 % + :alt: PyOWM + :align: center + + What is PyOWM? -------------- @@ -15,21 +23,23 @@ PyOWM is a client Python wrapper library for OpenWeatherMap web APIs. It allows consumption of OWM data from Python applications via a simple object model and in a human-friendly fashion. -What APIs does PyOWM allow me to use? -------------------------------------- +What APIs can I access with PyOWM? +---------------------------------- With PyOWM you can interact programmatically with the following OpenWeatherMap web APIs: - **Weather API v2.5**, offering + current weather data + weather forecasts + + weather history + - **Agro API v1.0**, offering polygon editing, soil data, satellite imagery search and download - **Air Pollution API v3.0**, offering data about CO, O3, NO2 and SO2 - **UV Index API v3.0**, offering data about Ultraviolet exposition - **Stations API v3.0**, allowing to create and manage meteostation and publish local weather measurements - **Weather Alerts API v3.0**, allowing to set triggers on weather conditions and areas and poll for spawned alerts +And You can also get **image tiles** for several map layers provided by OWM -and more will be supported in the future. Stay tuned! The documentation of OWM APIs can be found on the OWM Website_ @@ -91,13 +101,7 @@ You can install from source using _setuptools_: either download a release from G $ cd pyowm-x.y.z $ python setup.py install -The .egg will be installed into the system-dependent Python libraries folder: - -.. code:: - - C:\PythonXY\Lib\site-packages # Windows - /usr/local/lib/pythonX.Y/dist-packages # Ubuntu - /usr/local/lib/pythonX.Y/dist-packages # MacOS 10.5.4 +The .egg will be installed into the system-dependent Python libraries folder Distribution packages @@ -112,11 +116,11 @@ Distribution packages Yaourt -S python-owm # Python 3.x (https://aur.archlinux.org/packages/python2-owm) +PyOWM v2 usage documentation +---------------------------- -Examples and Guides -------------------- - -Here are some usage examples for the different OWM APIs +Here are some usage examples for the different OWM APIs: these examples and guides refer to PyOWM library +versions belonging to the 2.x stream Weather API examples @@ -124,8 +128,16 @@ Weather API examples .. toctree:: :maxdepth: 1 - object-model - usage-examples + usage-examples-v2/weather-api-usage-examples + usage-examples-v2/weather-api-object-model + +Agro API examples +~~~~~~~~~~~~~~~~~ + +.. toctree:: + :maxdepth: 1 + + usage-examples-v2/agro-api-usage-examples UV API examples @@ -133,7 +145,7 @@ UV API examples .. toctree:: :maxdepth: 1 - uv-api-usage-examples + usage-examples-v2/uv-api-usage-examples Air Pollution API examples @@ -142,22 +154,33 @@ Air Pollution API examples .. toctree:: :maxdepth: 1 - air-pollution-api-usage-examples + usage-examples-v2/air-pollution-api-usage-examples Stations API examples ~~~~~~~~~~~~~~~~~~~~~ .. toctree:: :maxdepth: 1 - stations-api-usage-examples + usage-examples-v2/stations-api-usage-examples Alerts API examples ~~~~~~~~~~~~~~~~~~~ .. toctree:: :maxdepth: 1 - alerts-api-usage-examples + usage-examples-v2/alerts-api-usage-examples + +Map tiles client examples +~~~~~~~~~~~~~~~~~~~~~~~~~ +.. toctree:: + :maxdepth: 1 + + usage-examples-v2/map-tiles-client-examples.md + +PyOWM v3 usage documentation +---------------------------- +Coming soon! PyOWM software API documentation diff --git a/sphinx/pyowm.agroapi10.rst b/sphinx/pyowm.agroapi10.rst new file mode 100644 index 00000000..2aa27983 --- /dev/null +++ b/sphinx/pyowm.agroapi10.rst @@ -0,0 +1,70 @@ +pyowm.agroapi10 package +======================= + +Submodules +---------- + +pyowm.agroapi10.agro_manager module +----------------------------------- + +.. automodule:: pyowm.agroapi10.agro_manager + :members: + :undoc-members: + :show-inheritance: + +pyowm.agroapi10.enums module +---------------------------- + +.. automodule:: pyowm.agroapi10.enums + :members: + :undoc-members: + :show-inheritance: + +pyowm.agroapi10.imagery module +------------------------------ + +.. automodule:: pyowm.agroapi10.imagery + :members: + :undoc-members: + :show-inheritance: + +pyowm.agroapi10.polygon module +------------------------------ + +.. automodule:: pyowm.agroapi10.polygon + :members: + :undoc-members: + :show-inheritance: + +pyowm.agroapi10.search module +----------------------------- + +.. automodule:: pyowm.agroapi10.search + :members: + :undoc-members: + :show-inheritance: + +pyowm.agroapi10.soil module +--------------------------- + +.. automodule:: pyowm.agroapi10.soil + :members: + :undoc-members: + :show-inheritance: + +pyowm.agroapi10.uris module +--------------------------- + +.. automodule:: pyowm.agroapi10.uris + :members: + :undoc-members: + :show-inheritance: + + +Module contents +--------------- + +.. automodule:: pyowm.alertapi30 + :members: + :undoc-members: + :show-inheritance: diff --git a/sphinx/pyowm.commons.rst b/sphinx/pyowm.commons.rst index ab83c2c1..7e6c3a36 100644 --- a/sphinx/pyowm.commons.rst +++ b/sphinx/pyowm.commons.rst @@ -4,6 +4,22 @@ pyowm.commons package Submodules ---------- +pyowm.commons.databoxes module +------------------------------ + +.. automodule:: pyowm.commons.databoxes + :members: + :undoc-members: + :show-inheritance: + +pyowm.commons.enums module +-------------------------- + +.. automodule:: pyowm.commons.enums + :members: + :undoc-members: + :show-inheritance: + pyowm.commons.frontlinkedlist module ------------------------------------ @@ -20,6 +36,23 @@ pyowm.commons.http_client module :undoc-members: :show-inheritance: +pyowm.commons.image module +-------------------------- + +.. automodule:: pyowm.commons.image + :members: + :undoc-members: + :show-inheritance: + +pyowm.commons.tile module +------------------------- + +.. automodule:: pyowm.commons.tile + :members: + :undoc-members: + :show-inheritance: + + Module contents --------------- diff --git a/sphinx/pyowm.rst b/sphinx/pyowm.rst index cd2f3dad..538feeaf 100644 --- a/sphinx/pyowm.rst +++ b/sphinx/pyowm.rst @@ -7,15 +7,17 @@ Subpackages .. toctree:: pyowm.abstractions + pyowm.agroapi10 pyowm.alertapi30 pyowm.caches pyowm.commons pyowm.exceptions pyowm.pollutionapi30 pyowm.stationsapi30 + pyowm.tiles pyowm.utils pyowm.uvindexapi30 - pyowm.webapi25 + pyowm.weatherapi25 Submodules ---------- diff --git a/sphinx/pyowm.stationsapi30.rst b/sphinx/pyowm.stationsapi30.rst index 570bb678..47dcc32b 100644 --- a/sphinx/pyowm.stationsapi30.rst +++ b/sphinx/pyowm.stationsapi30.rst @@ -55,7 +55,7 @@ pyowm.stationsapi30.stations_manager module Module contents --------------- -.. automodule:: pyowm.webapi25 +.. automodule:: pyowm.stationsapi30 :members: :undoc-members: :show-inheritance: diff --git a/sphinx/pyowm.tiles.rst b/sphinx/pyowm.tiles.rst new file mode 100644 index 00000000..2101fa0d --- /dev/null +++ b/sphinx/pyowm.tiles.rst @@ -0,0 +1,31 @@ +pyowm.tiles package +=================== + +Submodules +---------- + +pyowm.tiles.enums module +------------------------ + +.. automodule:: pyowm.tiles.enums + :members: + :undoc-members: + :show-inheritance: + +pyowm.tiles.tile_manager module +------------------------------- + +.. automodule:: pyowm.tiles.tile_manager + :members: + :undoc-members: + :show-inheritance: + + + +Module contents +--------------- + +.. automodule:: pyowm.tiles + :members: + :undoc-members: + :show-inheritance: diff --git a/sphinx/pyowm.webapi25.cityids.rst b/sphinx/pyowm.weatherapi25.cityids.rst similarity index 50% rename from sphinx/pyowm.webapi25.cityids.rst rename to sphinx/pyowm.weatherapi25.cityids.rst index fdfec264..b58aa10e 100644 --- a/sphinx/pyowm.webapi25.cityids.rst +++ b/sphinx/pyowm.weatherapi25.cityids.rst @@ -1,5 +1,5 @@ -pyowm.webapi25.cityids package -============================== +pyowm.weatherapi25.cityids package +================================== Submodules ---------- @@ -9,7 +9,7 @@ Submodules Module contents --------------- -.. automodule:: pyowm.webapi25.cityids +.. automodule:: pyowm.weatherapi25.cityids :members: :undoc-members: :show-inheritance: diff --git a/sphinx/pyowm.weatherapi25.parsers.rst b/sphinx/pyowm.weatherapi25.parsers.rst new file mode 100644 index 00000000..62507618 --- /dev/null +++ b/sphinx/pyowm.weatherapi25.parsers.rst @@ -0,0 +1,69 @@ +pyowm.weatherapi25.parsers package +================================== + +Submodules +---------- + +pyowm.weatherapi25.parsers.forecastparser module +------------------------------------------------ + +.. automodule:: pyowm.weatherapi25.parsers.forecastparser + :members: + :undoc-members: + :show-inheritance: + +pyowm.weatherapi25.parsers.observationlistparser module +------------------------------------------------------- + +.. automodule:: pyowm.weatherapi25.parsers.observationlistparser + :members: + :undoc-members: + :show-inheritance: + +pyowm.weatherapi25.parsers.observationparser module +--------------------------------------------------- + +.. automodule:: pyowm.weatherapi25.parsers.observationparser + :members: + :undoc-members: + :show-inheritance: + +pyowm.weatherapi25.parsers.stationhistoryparser module +------------------------------------------------------ + +.. automodule:: pyowm.weatherapi25.parsers.stationhistoryparser + :members: + :undoc-members: + :show-inheritance: + +pyowm.weatherapi25.parsers.stationlistparser module +--------------------------------------------------- + +.. automodule:: pyowm.weatherapi25.parsers.stationlistparser + :members: + :undoc-members: + :show-inheritance: + +pyowm.weatherapi25.parsers.stationparser module +----------------------------------------------- + +.. automodule:: pyowm.weatherapi25.parsers.stationparser + :members: + :undoc-members: + :show-inheritance: + +pyowm.weatherapi25.parsers.weatherhistoryparser module +------------------------------------------------------ + +.. automodule:: pyowm.weatherapi25.parsers.weatherhistoryparser + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: pyowm.weatherapi25.parsers + :members: + :undoc-members: + :show-inheritance: diff --git a/sphinx/pyowm.weatherapi25.rst b/sphinx/pyowm.weatherapi25.rst new file mode 100644 index 00000000..3fabf233 --- /dev/null +++ b/sphinx/pyowm.weatherapi25.rst @@ -0,0 +1,119 @@ +pyowm.weatherapi25 package +========================== + +Subpackages +----------- + +.. toctree:: + + pyowm.weatherapi25.cityids + pyowm.weatherapi25.parsers + pyowm.weatherapi25.xsd + +Submodules +---------- + +pyowm.weatherapi25.cityidregistry module +---------------------------------------- + +.. automodule:: pyowm.weatherapi25.cityidregistry + :members: + :undoc-members: + :show-inheritance: + +pyowm.weatherapi25.configuration25 module +----------------------------------------- + +.. automodule:: pyowm.weatherapi25.configuration25 + :members: + :undoc-members: + :show-inheritance: + +pyowm.weatherapi25.forecast module +---------------------------------- + +.. automodule:: pyowm.weatherapi25.forecast + :members: + :undoc-members: + :show-inheritance: + +pyowm.weatherapi25.forecaster module +------------------------------------ + +.. automodule:: pyowm.weatherapi25.forecaster + :members: + :undoc-members: + :show-inheritance: + +pyowm.weatherapi25.historian module +----------------------------------- + +.. automodule:: pyowm.weatherapi25.historian + :members: + :undoc-members: + :show-inheritance: + +pyowm.weatherapi25.location module +---------------------------------- + +.. automodule:: pyowm.weatherapi25.location + :members: + :undoc-members: + :show-inheritance: + +pyowm.weatherapi25.observation module +------------------------------------- + +.. automodule:: pyowm.weatherapi25.observation + :members: + :undoc-members: + :show-inheritance: + +pyowm.weatherapi25.owm25 module +------------------------------- + +.. automodule:: pyowm.weatherapi25.owm25 + :members: + :undoc-members: + :show-inheritance: + +pyowm.weatherapi25.station module +--------------------------------- + +.. automodule:: pyowm.weatherapi25.station + :members: + :undoc-members: + :show-inheritance: + +pyowm.weatherapi25.stationhistory module +---------------------------------------- + +.. automodule:: pyowm.weatherapi25.stationhistory + :members: + :undoc-members: + :show-inheritance: + +pyowm.weatherapi25.weather module +--------------------------------- + +.. automodule:: pyowm.weatherapi25.weather + :members: + :undoc-members: + :show-inheritance: + +pyowm.weatherapi25.weathercoderegistry module +--------------------------------------------- + +.. automodule:: pyowm.weatherapi25.weathercoderegistry + :members: + :undoc-members: + :show-inheritance: + + +Module contents +--------------- + +.. automodule:: pyowm.weatherapi25 + :members: + :undoc-members: + :show-inheritance: diff --git a/sphinx/pyowm.weatherapi25.xsd.rst b/sphinx/pyowm.weatherapi25.xsd.rst new file mode 100644 index 00000000..1997c1cd --- /dev/null +++ b/sphinx/pyowm.weatherapi25.xsd.rst @@ -0,0 +1,22 @@ +pyowm.weatherapi25.xsd package +============================== + +Submodules +---------- + +pyowm.weatherapi25.xsd.xmlnsconfig module +----------------------------------------- + +.. automodule:: pyowm.weatherapi25.xsd.xmlnsconfig + :members: + :undoc-members: + :show-inheritance: + + +Module contents +--------------- + +.. automodule:: pyowm.weatherapi25.xsd + :members: + :undoc-members: + :show-inheritance: diff --git a/sphinx/pyowm.webapi25.parsers.rst b/sphinx/pyowm.webapi25.parsers.rst deleted file mode 100644 index 37cac1d8..00000000 --- a/sphinx/pyowm.webapi25.parsers.rst +++ /dev/null @@ -1,86 +0,0 @@ -pyowm.webapi25.parsers package -============================== - -Submodules ----------- - -pyowm.webapi25.parsers.forecastparser module --------------------------------------------- - -.. automodule:: pyowm.webapi25.parsers.forecastparser - :members: - :undoc-members: - :show-inheritance: - -pyowm.webapi25.parsers.observationlistparser module ---------------------------------------------------- - -.. automodule:: pyowm.webapi25.parsers.observationlistparser - :members: - :undoc-members: - :show-inheritance: - -pyowm.webapi25.parsers.observationparser module ------------------------------------------------ - -.. automodule:: pyowm.webapi25.parsers.observationparser - :members: - :undoc-members: - :show-inheritance: - -pyowm.webapi25.parsers.stationhistoryparser module --------------------------------------------------- - -.. automodule:: pyowm.webapi25.parsers.stationhistoryparser - :members: - :undoc-members: - :show-inheritance: - -pyowm.webapi25.parsers.stationlistparser module ------------------------------------------------ - -.. automodule:: pyowm.webapi25.parsers.stationlistparser - :members: - :undoc-members: - :show-inheritance: - -pyowm.webapi25.parsers.stationparser module -------------------------------------------- - -.. automodule:: pyowm.webapi25.parsers.stationparser - :members: - :undoc-members: - :show-inheritance: - -pyowm.webapi25.parsers.weatherhistoryparser module --------------------------------------------------- - -.. automodule:: pyowm.webapi25.parsers.weatherhistoryparser - :members: - :undoc-members: - :show-inheritance: - -pyowm.webapi25.parsers.coindexparser module -------------------------------------------- - -.. automodule:: pyowm.webapi25.parsers.coindexparser - :members: - :undoc-members: - :show-inheritance: - -pyowm.webapi25.parsers.ozoneparser module ------------------------------------------ - -.. automodule:: pyowm.webapi25.parsers.ozoneparser - :members: - :undoc-members: - :show-inheritance: - - -Module contents ---------------- - -.. automodule:: pyowm.webapi25.parsers - :members: - :undoc-members: - :show-inheritance: diff --git a/sphinx/pyowm.webapi25.rst b/sphinx/pyowm.webapi25.rst deleted file mode 100644 index f0a84446..00000000 --- a/sphinx/pyowm.webapi25.rst +++ /dev/null @@ -1,119 +0,0 @@ -pyowm.webapi25 package -====================== - -Subpackages ------------ - -.. toctree:: - - pyowm.webapi25.cityids - pyowm.webapi25.parsers - pyowm.webapi25.xsd - -Submodules ----------- - -pyowm.webapi25.cityidregistry module ------------------------------------- - -.. automodule:: pyowm.webapi25.cityidregistry - :members: - :undoc-members: - :show-inheritance: - -pyowm.webapi25.configuration25 module -------------------------------------- - -.. automodule:: pyowm.webapi25.configuration25 - :members: - :undoc-members: - :show-inheritance: - -pyowm.webapi25.forecast module ------------------------------- - -.. automodule:: pyowm.webapi25.forecast - :members: - :undoc-members: - :show-inheritance: - -pyowm.webapi25.forecaster module --------------------------------- - -.. automodule:: pyowm.webapi25.forecaster - :members: - :undoc-members: - :show-inheritance: - -pyowm.webapi25.historian module -------------------------------- - -.. automodule:: pyowm.webapi25.historian - :members: - :undoc-members: - :show-inheritance: - -pyowm.webapi25.location module ------------------------------- - -.. automodule:: pyowm.webapi25.location - :members: - :undoc-members: - :show-inheritance: - -pyowm.webapi25.observation module ---------------------------------- - -.. automodule:: pyowm.webapi25.observation - :members: - :undoc-members: - :show-inheritance: - -pyowm.webapi25.owm25 module ---------------------------- - -.. automodule:: pyowm.webapi25.owm25 - :members: - :undoc-members: - :show-inheritance: - -pyowm.webapi25.station module ------------------------------ - -.. automodule:: pyowm.webapi25.station - :members: - :undoc-members: - :show-inheritance: - -pyowm.webapi25.stationhistory module ------------------------------------- - -.. automodule:: pyowm.webapi25.stationhistory - :members: - :undoc-members: - :show-inheritance: - -pyowm.webapi25.weather module ------------------------------ - -.. automodule:: pyowm.webapi25.weather - :members: - :undoc-members: - :show-inheritance: - -pyowm.webapi25.weathercoderegistry module ------------------------------------------ - -.. automodule:: pyowm.webapi25.weathercoderegistry - :members: - :undoc-members: - :show-inheritance: - - -Module contents ---------------- - -.. automodule:: pyowm.webapi25 - :members: - :undoc-members: - :show-inheritance: diff --git a/sphinx/pyowm.webapi25.xsd.rst b/sphinx/pyowm.webapi25.xsd.rst deleted file mode 100644 index 63cd2d87..00000000 --- a/sphinx/pyowm.webapi25.xsd.rst +++ /dev/null @@ -1,22 +0,0 @@ -pyowm.webapi25.xsd package -========================== - -Submodules ----------- - -pyowm.webapi25.xsd.xmlnsconfig module -------------------------------------- - -.. automodule:: pyowm.webapi25.xsd.xmlnsconfig - :members: - :undoc-members: - :show-inheritance: - - -Module contents ---------------- - -.. automodule:: pyowm.webapi25.xsd - :members: - :undoc-members: - :show-inheritance: diff --git a/sphinx/usage-examples-v2/agro-api-usage-examples.md b/sphinx/usage-examples-v2/agro-api-usage-examples.md new file mode 100644 index 00000000..16535f55 --- /dev/null +++ b/sphinx/usage-examples-v2/agro-api-usage-examples.md @@ -0,0 +1,434 @@ +# Agro API examples + +OWM provides an API for Agricultural monitoring that provides soil data, satellite imagery, etc. + +The first thing you need to do to get started with it is to create a *polygon* and store it on the OWM Agro API. +PyOWM will give you this new polygon's ID and you will use it to invoke data queries upon that polygon. + +Eg: you can look up satellite imagery, weather data, historical NDVI for that specific polygon. + +Read further on to get more details. + + +## OWM website technical reference + - [https://agromonitoring.com/api](hhttps://agromonitoring.com/api) + + +## AgroAPI Manager object + +In order to do any kind of operations against the OWM Agro API, you need to obtain a `pyowm.agro10.agro_manager.AgroManager` +instance from the main OWM. You'll need your API Key for that: + +```python +import pyowm +owm = pyowm.OWM('your-API-key') +mgr = owm.agro_manager() +``` + +Read on to discover what you can do with it. + + +## Polygon API operations + +A polygon represents an area on a map upon which you can issue data queries. Each polygon has a unique ID, an optional +name and links back to unique OWM ID of the User that owns that polygon. Each polygon has an area that is expressed +in hacres, but you can also get it in squared kilometers: + +```python +pol # this is a pyowm.agro10.polygon.Polygon instance +pol.id # ID +pol.area # in hacres +pol.area_km # in sq kilometers +pol.user_id # owner ID +``` + +Each polygon also carries along the `pyowm.utils.geo.Polygon` object that represents the geographic polygon and the +`pyowm.utils.geo.Point` object that represents the baricentre of the polygon: + +```python +geopol = pol.geopolygon # pyowm.utils.geo.Polygon object +point = pol.center # pyowm.utils.geo.Point object +``` + +### Reading Polygons +You can either get all of the Polygons you've created on the Agro API or easily get single polygons by specifying +their IDs: + +```python +list_of_polygons = mgr.get_polygons() +a_polygon = mgr.get_polygon('5abb9fb82c8897000bde3e87') +``` + + +### Creating Polygons +Creating polygons is easy: you just need to create a `pyowm.utils.geo.Polygon` instance that describes the coordinates +of the polygon you want to create on the Agro API. Then you just need to pass it (along with an optional name) to the +Agro Manager object: + +```python + +# first create the pyowm.utils.geo.Polygon instance that represents the area (here, a triangle) +from pyowm.utils.geo import Polygon as GeoPolygon +gp = GeoPolygon([[ + [-121.1958, 37.6683], + [-121.1779, 37.6687], + [-121.1773, 37.6792], + [-121.1958, 37.6683]]]) + +# use the Agro Manager to create your polygon on the Agro API +the_new_polygon = mgr.create_polygon(gp, 'my new shiny polygon') + +# the new polygon has an ID and a user_id +the_new_polygon.id +the_new_polygon.user_id +``` + +You get back a `pyowm.agro10.polygon.Polygon` instance and you can use its ID to operate this new polygon on all the +other Agro API methods! + +### Updating a Polygon +Once you've created a polygon, you can only change its mnemonic name, as the rest of its parameters cannot +be changed by the user. In order to do it: + +```python +my_polygon.name # "my new shiny polygon" +my_polygon.name = "changed name" +mgr.update_polygon(my_polygon) +``` + +### Deleting a Polygon +Delete a polygon with + +```python +mgr.delete_polygon(my_polygon) +``` + +Remember that when you delete a polygon, there is no going back! + + +## Soil data API Operations + +Once you've defined a polygon, you can easily get soil data upon it. Just go with: + +```python +soil = mgr.soil_data(polygon) +``` + +`Soil` is an entity of type `pyowm.agro10.soil.Soil` and is basically a wrapper around a Python dict reporting +the basic soil information on that polygon: +```python +soil.polygon_id # str +soil.reference_time(timeformat='unix') # can be: int for UTC Unix time ('unix'), + # ISO8601-formatted str for 'iso' or + # datetime.datetime for 'date' +soil.surface_temp(unit='kelvin') # float (unit can be: 'kelvin', 'celsius' or 'fahrenheit') +soil.ten_cm_temp(unit='kelvin') # float (Kelvins, measured at 10 cm depth) - unit same as for above +soil.moisture # float (m^3/m^3) +``` + +Soil data is updated twice a day. + + +## Satellite Imagery API Operations + +This is the real meat in Agro API: the possibility to obtain **satellite imagery** right upon your polygons! + +### Overwiew + +Satellite Imagery comes in 3 formats: + - **PNG images** + - **PNG tiles** (variable zoom level) + - **GeoTIFF images** + +Tiles can be retrieved by specifying a proper set of tile coordinates (x, y) and a zoom level: please refer to PyOWM's +Map Tiles client documentation for further insights. + +When we say that imagery is upon a polygon we mean that the polygon is fully contained in the scene that was acquired by +the satellite. + +Each image comes with a **preset**: a preset tells how the acquired scene has been post-processed, eg: image has been +put in false colors or image contains values of the Enhanced Vegetation Index (EVI) calculated on the scene + +Imagery is provided by the Agro API for different **satellites** + +Images that you retrieve from the Agro API are `pyowm.agroapi10.imagery.SatelliteImage` instances, and they **contain both +image's data and metadata**. + +You can download NDVI images using several **color palettes** provided by the Agro API, for easier processing on your side. + + +### Operations summary + +Once you've defined a polygon, you can: + - **search for available images** upon the polygon and taken in a specific time frame. The search can be performed with + multiple filters (including: satellite symbol, image type, image preset, min/max resolution, minx/max cloud coverage, ...) + and returns search results, each one being *metadata* for a specific image. + - from those metadata, **download an image**, be it a regular scene or a tile, optionally specifying a color palette for NDVI ones + - if your image has EVI or NDVI presets, you can **query for its statistics**: these include min/max/median/p25/p75 values + for the corresponding index + +**A concrete example**: we want to acquire all NDVI GeoTIFF images acquired by Landsat 8 from July 18, 2017 to October 26, 2017; +then we want to get stats for one such image and to save it to a local file. + + +```python +from pyowm.commons.enums import ImageTypeEnum +from pyowm.agroapi10.enums import SatelliteEnum, PresetEnum + +pol_id = '5abb9fb82c8897000bde3e87' # your polygon's ID +acq_from = 1500336000 # 18 July 2017 +acq_to = 1508976000 # 26 October 2017 +img_type = ImageTypeEnum.GEOTIFF # the image format type +preset = PresetEnum.NDVI # the preset +sat = SatelliteEnum.LANDSAT_8.symbol # the satellite + + +# the search returns images metadata (in the form of `MetaImage` objects) +results = mgr.search_satellite_imagery(pol_id, acq_from, acq_to, img_type=img_type, preset=preset, None, None, acquired_by=sat) + +# download all of the images +satellite_images = [mgr.download_satellite_image(result) for result in results] + +# get stats for the first image +sat_img = satellite_images[0] +stats_dict = mgr.stats_for_satellite_image(sat_img) + +# ...satellite images can be saved to disk +sat_img.persist('/path/to/my/folder/sat_img.tif') +``` + +Let's see in detail all of the imagery-based operations. + + +### Searching images + +Search available imagery upon your polygon by specifying at least a mandatory time window, with from and to timestamps +expressed as UNIX UTC timestamps: + +```python + +pol_id = '5abb9fb82c8897000bde3e87' # your polygon's ID +acq_from = 1500336000 # 18 July 2017 +acq_to = 1508976000 # 26 October 2017 + +# the most basic search ever: search all available images upon the polygon in the specified time frame +metaimages_list = mgr.search_satellite_imagery(pol_id, acq_from, acq_to) + +``` + +What you will get back is actually metadata for the actual imagery, not data. + +The function call will return **a list of `pyowm.agroapi10.imagery.MetaImage` instances, each +one being a bunch of metadata relating to one single satellite image**. + +Keep these objects, as you will need them in order to download the corresponding satellite images from the Agro API: +think of them such as descriptors for the real images. + +But let's get back to search! Search is a parametric affair... **you can specify many more filters**: + + - the image format type (eg. PNG, GEOTIFF) + - the image preset (eg. false color, EVI) + - the satellite that acquired the image (you need to specify its symbol) + - the px/m resolution range for the image (you can specify a minimum value, a maximum value or both of them) + - the % of cloud coverage on the acquired scene (you can specify a minimum value, a maximum value or both of them) + - the % of valid data coverage on the acquired scene (you can specify a minimum value, a maximum value or both of them) + +Sky is the limit... + +As regards image type, image preset and satellite filters please refer to subsequent sections explaining the supported +values. + +Examples of search: + +```python +from pyowm.commons.enums import ImageTypeEnum +from pyowm.agroapi10.enums import SatelliteEnum, PresetEnum + + +# search all Landsat 8 images in the specified time frame +results = mgr.search_satellite_imagery(pol_id, acq_from, acq_to, acquired_by=SatelliteEnum.LANDSAT_8.symbol) + +# search all GeoTIFF images in the specified time frame +results = mgr.search_satellite_imagery(pol_id, acq_from, acq_to, img_type=ImageTypeEnum.GEOTIFF) + +# search all NDVI images acquired by Sentinel 2 in the specified time frame +results = mgr.search_satellite_imagery(pol_id, acq_from, acq_to, acquired_by=SatelliteEnum.SENTINEL_2.symbol, + preset=PresetEnum.NDVI) + +# search all PNG images in the specified time frame with a max cloud coverage of 1% and a min valid data coverage of 98% +results = mgr.search_satellite_imagery(pol_id, acq_from, acq_to, img_type=ImageTypeEnum.PNG, + max_cloud_coverage=1, min_valid_data_coverage=98) + +# search all true color PNG images in the specified time frame, acquired by Sentinel 2, with a range of metric resolution +# from 4 to 16 px/m, and with at least 90% of valid data coverage +results = mgr.search_satellite_imagery(pol_id, acq_from, acq_to, img_type=ImageTypeEnum.PNG, preset=PresetEnum.TRUE_COLOR, + min_resolution=4, max_resolution=16, min_valid_data_coverage=90) +``` + +So, what metadata can be extracted by a `MetaImage` object? Here we go: + +```python +metaimage.polygon_id # the ID of the polygon upon which the image is taken +metaimage.url # the URL the actual satellite image can be fetched from +metaimage.preset # the satellite image preset +metaimage.image_type # the satellite image format type +metaimage.satellite_name # the name of the satellite that acquired the image +metaimage.acquisition_time('unix') # the timestamp when the image was taken (can be specified using: 'iso', 'unix' and 'date') +metaimage.valid_data_percentage # the percentage of valid data coverage on the image +metaimage.cloud_coverage_percentage # the percentage of cloud coverage on the image +metaimage.sun_azimuth # the sun azimuth angle at scene acquisition time +metaimage.sun_elevation # the sun zenith angle at scene acquisition time +metaimage.stats_url # if the image is EVI or NDVI, this is the URL where index statistics can be retrieved (see further on for details) +``` + + +### Download an image + +Once you've got your metaimages ready, you can download the actual satellite images. + +In order to download, you must specify to the Agro API manager object at least the desired metaimage to fetch. If you're +downloading a tile, you must specify tile coordinates (x, y, and zoom level): these are mandatory, and if you forget +to provide them you'll get an `AssertionError`. + +Optionally, you can specify a color palette - but this will be significant only if you're downloading an image with NDVI +preset (otherwise the palette parameter will be safely ignored) - please see further on for reference. + +Once download is complete, you'll get back a `pyowm.agroapi10.imagery.SatelliteImage` object (more on this in a while). + +Here are some examples: + +```python +from pyowm.agroapi10.enums import PaletteEnum + +# Download a NDVI image +ndvi_metaimage # metaimage for a NDVI image +bnw_sat_image = mgr.download_satellite_image(ndvi_metaimage, preset=PaletteEnum.BLACK_AND_WHITE) +green_sat_image = mgr.download_satellite_image(ndvi_metaimage, preset=PaletteEnum.GREEN) + +# Download a tile +tile_metaimage # metaimage for a tile +tile_image = mgr.download_satellite_image(tile_metaimage, x=2, y=3, zoom=5) +tile_image = mgr.download_satellite_image(tile_metaimage) # AssertionError (x, y and zoom are missing!) +``` + +Downloaded satellite images contain both binary image data and and embed *the original `MetaImage` object describing +image metadata*. Furthermore, you can query for the download time of a satellite image, and for its related color palette: + +```python + +# Get satellite image download time - you can as usual specify: 'iso', 'date' and 'unix' time formats +bnw_sat_image.downloaded_on('iso') # '2017-07-18 14:08:23+00' + +# Get its palette +bnw_sat_image.palette # '2' + +# Get satellite image's data and metadata +bnw_sat_image.data # this returns a `pyowm.commons.image.Image` object or a + # `pyowm.commons.tile.Tile` object depending on the satellite image +metaimage = bnw_sat_image.metadata # this gives a `MetaImage` subtype object + +# Use the Metaimage object as usual... +metaimage.polygon_id +metaimage.preset, +metaimage.satellite_name +metaimage.acquisition_time +``` + +You can also save satellite images to disk - it's as easy as: + +```python +bnw_sat_image.persist('C:\myfolder\myfile.png') +``` + +### Querying for NDVI and EVI image stats + +NDVI and EVI preset images have an extra blessing: you can query for statistics about the image index. + +Once you've downloaded such satellite images, you can query for stats and get back a data dictionary for each of them: + +```python +ndvi_metaimage # metaimage for a NDVI image + +# download it +bnw_sat_image = mgr.download_satellite_image(ndvi_metaimage, preset=PaletteEnum.BLACK_AND_WHITE) + +# query for stats +stats_dict = mgr.stats_for_satellite_image(bnw_sat_image) +``` + +Stats dictionaries contain: + - `std`: the standard deviation of the index + - `p25`: the first quartile value of the index + - `num`: the number of pixels in the current polygon + - `min`: the minimum value of the index + - `max`: the maximum value of the index + - `median`: the median value of the index + - `p75`: the third quartile value of the index + - `mean`: the average value of the index + +*What if you try to get stats for a non-NDVI or non-EVI image*? A `ValueError` will be raised! + + +### Supported satellites + +Supported satellites are provided by the `pyowm.agroapi10.enums.SatelliteEnum` enumerator which returns +`pyowm.commons.databoxes.Satellite` objects: + +```python +from pyowm.agroapi10.enums import SatelliteEnum + +sat = SatelliteEnum.SENTINEL_2 +sat.name # 'Sentinel-2' +sat.symbol # 's2' +``` + +Currently only Landsat 8 and Sentinel 2 satellite imagery is available + + +### Supported presets +Supported presets are provided by the `pyowm.agroapi10.enums.PresetEnum` enumerator which returns strings, each +one representing an image preset: + + ```python +from pyowm.agroapi10.enums import PresetEnum + +PresetEnum.TRUE_COLOR # 'truecolor' +``` + +Currently these are the supported presets: true color, false color, NDVI and EVI + + +### Supported image types +Supported image types are provided by the `pyowm.commons.databoxes.ImageTypeEnum` enumerator which returns +`pyowm.commons.databoxes.ImageType` objects: + + ```python +from pyowm.agroapi10.enums import ImageTypeEnum + +png_type = ImageTypeEnum.PNG +geotiff_type = ImageTypeEnum.GEOTIFF +png_type.name # 'PNG' +png_type.mime_type # 'image/png' +``` + +Currently only PNG and GEOTIFF imagery is available + + +### Supported color palettes +Supported color palettes are provided by the `pyowm.agroapi10.enums.PaletteEnum` enumerator which returns strings, +each one representing a color palette for NDVI imges: + + ```python +from pyowm.agroapi10.enums import PaletteEnum + +PaletteEnum.CONTRAST_SHIFTED # '3' +``` + +As said, palettes only apply to NDVI images: if you try to specify palettes when downloading images with different +presets (eg. false color images), *nothing will happen*. + +The default Agro API color palette is `PaletteEnum.GREEN` (which is `1`): if you don't specify any palette at all when +downloading NDVI images, they will anyway be returned with this palette. + +As of today green, black and white and two contrast palettes (one continuous and one continuous but shifted) are +supported by the Agro API. Please check the documentation for palettes' details, including control points. diff --git a/sphinx/air-pollution-api-usage-examples.md b/sphinx/usage-examples-v2/air-pollution-api-usage-examples.md similarity index 100% rename from sphinx/air-pollution-api-usage-examples.md rename to sphinx/usage-examples-v2/air-pollution-api-usage-examples.md diff --git a/sphinx/alerts-api-usage-examples.md b/sphinx/usage-examples-v2/alerts-api-usage-examples.md similarity index 99% rename from sphinx/alerts-api-usage-examples.md rename to sphinx/usage-examples-v2/alerts-api-usage-examples.md index 44b95aa8..5e74ff58 100644 --- a/sphinx/alerts-api-usage-examples.md +++ b/sphinx/usage-examples-v2/alerts-api-usage-examples.md @@ -120,7 +120,7 @@ point.geojson() Defining complex geometries is sometimes difficult, but in most cases you just need to set triggers upon cities: that's -why we've added a method to the `pyowm.webapi25.cityidregistry.CityIDRegistry` registry that returns the geopoints +why we've added a method to the `pyowm.weatherapi25.cityidregistry.CityIDRegistry` registry that returns the geopoints that correspond to one or more named cities: ```python diff --git a/sphinx/usage-examples-v2/map-tiles-client-examples.md b/sphinx/usage-examples-v2/map-tiles-client-examples.md new file mode 100644 index 00000000..4ef4d01e --- /dev/null +++ b/sphinx/usage-examples-v2/map-tiles-client-examples.md @@ -0,0 +1,80 @@ +# Tiles client + +OWM provides tiles for a few map layers displaying world-wide features such as global temperature, pressure, wind speed, +and precipitation amount. + +Each tile is a PNG image that is referenced by a triplet: the (x, y) coordinates and a zoom level + +The zoom level might depend on the type of layers: 0 means no zoom (full globe covered), while usually you can get up +to a zoom level of 18. + +Available map layers are specified by the `pyowm.tiles.enums.MapLayerEnum` values. + + +## OWM website technical reference + - [http://openweathermap.org/api/weathermaps](http://openweathermap.org/api/weathermaps) + + +## Usage examples + +Tiles can be fetched this way: + +```python +from pyowm import OWM +from pyowm.tiles.enums import MapLayerEnum + +owm = OWM('my-API-key') + +# Choose the map layer you want tiles for (eg. temeperature +layer_name = MapLayerEnum.TEMPERATURE + +# Obtain an instance to a tile manager object +tm = owm.tile_manager(layer_name) + +# Now say you want tile at coordinate x=5 y=2 at a zoom level of 6 +tile = tm.get_tile(5, 2, 6) + +# You can now save the tile to disk +tile.persist('/path/to/file.png') + +# Wait! but now I need the pressure layer tile at the very same coordinates and zoom level! No worries... +# Just change the map layer name on the TileManager and off you go! +tm.map_layer = MapLayerEnum.PRESSURE +tile = tm.get_tile(5, 2, 6) +``` + + +## Tile object + +A `pyowm.commons.tile.Tile` object is a wrapper for the tile coordinates and the image data, which is a +`pyowm.commons.image.Image` object instance. + +You can save a tile to disk by specifying a target file: + +```python +tile.persist('/path/to/file.png') +``` + +## Use cases + +### I have the lon/lat of a point and I want to get the tile that contains that point at a given zoom level + +Turn the lon/lat couple to a `pyowm.utils.geo.Point` object and pass it + +```python +from pyowm.utils.geo import Point +from pyowm.commons.tile import Tile + +geopoint = Point(lon, lat) +x_tile, y_tile = Tile.tile_coords_for_point(geopoint, zoom_level): +``` + +### I have a tile and I want to know its bounding box in lon/lat coordinates + +Easy! You'll get back a `pyowm.utils.geo.Polygon` object, from which you can extract lon/lat coordinates this way + +```python +polygon = tile.bounding_polygon() +geopoints = polygon.points +geocoordinates = [(p.lon, p.lat) for p in geopoints] # this gives you tuples with lon/lat +``` diff --git a/sphinx/stations-api-usage-examples.md b/sphinx/usage-examples-v2/stations-api-usage-examples.md similarity index 100% rename from sphinx/stations-api-usage-examples.md rename to sphinx/usage-examples-v2/stations-api-usage-examples.md diff --git a/sphinx/uv-api-usage-examples.md b/sphinx/usage-examples-v2/uv-api-usage-examples.md similarity index 100% rename from sphinx/uv-api-usage-examples.md rename to sphinx/usage-examples-v2/uv-api-usage-examples.md diff --git a/sphinx/object-model.md b/sphinx/usage-examples-v2/weather-api-object-model.md similarity index 84% rename from sphinx/object-model.md rename to sphinx/usage-examples-v2/weather-api-object-model.md index ad535410..f8c16b0c 100644 --- a/sphinx/object-model.md +++ b/sphinx/usage-examples-v2/weather-api-object-model.md @@ -2,33 +2,33 @@ In the following sections you will find a brief explanation of PyOWM's object mo # Abstractions -A few abstract classes are provided in order to allow code reuse for supporting new OWM web API versions and to eventually patch the currently supported ones. +A few abstract classes are provided in order to allow code reuse for supporting new OWM Weather API versions and to eventually patch the currently supported ones. ### The OWM abstract class -The _OWM_ class is an abstract entry-point to the library. Clients can obtain a concrete implementation of this class through a factory method that returns the _OWM_ subclass instance corresponding to the OWM web API version that is specified (or to the latest OWM web API version available). +The _OWM_ class is an abstract entry-point to the library. Clients can obtain a concrete implementation of this class through a factory method that returns the _OWM_ subclass instance corresponding to the OWM Weather API version that is specified (or to the latest OWM Weather API version available). In order to leverage the library features, you need to import the OWM factory and then feed it with an API key, if you have one (read [here](http://openweathermap.org/appid) on how to obtain an API key). Of course you can change your API Key after object instantiation, if you need. -Each kind of weather query you can issue against the OWM web API is done through a correspondent method invocation on your _OWM_ object instance. +Each kind of weather query you can issue against the OWM Weather API is done through a correspondent method invocation on your _OWM_ object instance. -Each OWM web API version may have different features, and therefore the mapping _OWM_ subclass may have different methods. The _OWM_ common parent class provides methods that tells you the PyOWM library version, the supported OWM web API version and the availability of the web API service: these methods are inherited by all the _OWM_ children classes. +Each OWM Weather API version may have different features, and therefore the mapping _OWM_ subclass may have different methods. The _OWM_ common parent class provides methods that tells you the PyOWM library version, the supported OWM Weather API version and the availability of the web API service: these methods are inherited by all the _OWM_ children classes. ### The JSONParser abstract class -This abstract class states the interface for OWM web API responses' JSON parsing: every API endpoint returns a different JSON message that has to be parsed to a specific object from the PyOWM object model. +This abstract class states the interface for OWM Weather API responses' JSON parsing: every API endpoint returns a different JSON message that has to be parsed to a specific object from the PyOWM object model. Subclasses of _JSONParser_ shall implement this contract: instances of these classes shall be used by subclasses of the _OWM_ abstract class. ### The OWMCache abstract class -This abstract class states the interface for OWM web API responses' cache. The target of subclasses is to implement the get/set methods so that the JSON payloads of OWM web API responses are cached and looked up using the correspondent HTTP full URL that originated them. +This abstract class states the interface for OWM Weather API responses' cache. The target of subclasses is to implement the get/set methods so that the JSON payloads of OWM Weather API responses are cached and looked up using the correspondent HTTP full URL that originated them. ### The LinkedList abstract class This abstract class models a generic linked list data structure. -# OWM web API 2.5 object model +# OWM Weather API 2.5 object model ### The configuration25 module -This module contains configuration data for the OWM web API 2.5 object model. Specifically: +This module contains configuration data for the OWM Weather API 2.5 object model. Specifically: - * OWM web API endpoint URLs + * OWM Weather API endpoint URLs * parser objects for API JSON payloads parsing * registry object for City ID lookup * cache providers @@ -42,7 +42,7 @@ As regards cache providers: You can write down your own configuration module and inject it into the PyOWM when you create the OWM global object, provided that you strictly follow the format of the `config25` module - which can be seen from the source code - and you put your own module in a location visible by the PYTHONPATH. ### The OWM25 class -The _OWM25_ class extends the _OWM_ abstract base class and provides a method for each of the OWM web API 2.5 endpoint: +The _OWM25_ class extends the _OWM_ abstract base class and provides a method for each of the OWM Weather API 2.5 endpoint: # CURRENT WEATHER QUERYING * find current weather at a specific location ---> eg: owm.weather_at_place('London,UK') @@ -80,7 +80,7 @@ The _OWM25_ class is injected with _jsonparser_ subclasses instances: each one p In order to interact with the web API, this class leverages an _OWMHTTPClient_ instance. ### The Location class -The _Location_ class represents a location in the world. Each instance stores the geographic name of the location, the longitude/latitude couple and the country name. These data are retrieved from the OWM web API 2.5 responses' payloads. +The _Location_ class represents a location in the world. Each instance stores the geographic name of the location, the longitude/latitude couple and the country name. These data are retrieved from the OWM Weather API 2.5 responses' payloads. _Location_ instances can also be retrieved from City IDs using the _CityIDRegistry_ module. @@ -100,7 +100,7 @@ a list of _Weather_ objects is returned. ### The Observation class An instance of this class is returned whenever a query about currently observed weather in a location is issued (hence, its name). -The _Observation_ class binds information about weather features that are currently being observed in a specific location in the world and that are stored as a _Weather_ object instance and the details about the location, which is stored into the class as a _Location_ object instance. Both current weather and location info are obtained via OWM web API responses parsing, which done by other classes in the PyOWM library: usually this data parsing stage ends with their storage into a newly created _Observation_ instance. +The _Observation_ class binds information about weather features that are currently being observed in a specific location in the world and that are stored as a _Weather_ object instance and the details about the location, which is stored into the class as a _Location_ object instance. Both current weather and location info are obtained via OWM Weather API responses parsing, which done by other classes in the PyOWM library: usually this data parsing stage ends with their storage into a newly created _Observation_ instance. When created, every _Observation_ instance is fed with a timestamp that tells when the weather observation data have been received. @@ -114,9 +114,9 @@ a list of _Observation_ instances is returned to the clients. ### The Forecast class This class represents a weather forecast for a specific location in the world. A weather forecast is made out by location data - encapsulated by a _Location_ object - and a collection of weather conditions - a list of _Weather_ objects. -The OWM web API 2.5 provides two types of forecast intervals: three hours and daily; each _Forecast_ instance has a specific fields that tells the interval of the forecast. +The OWM Weather API 2.5 provides two types of forecast intervals: three hours and daily; each _Forecast_ instance has a specific fields that tells the interval of the forecast. -_Forecast_ instances can also tell the reception timestamp for the weather forecast, that is to say the time when the forecast has been recevied from the OWM web API. +_Forecast_ instances can also tell the reception timestamp for the weather forecast, that is to say the time when the forecast has been recevied from the OWM Weather API. This class also provides an iterator for easily iterating over the encapsulated _Weather_ list: @@ -176,11 +176,11 @@ This is a null-object that does nothing and is used by default as the PyOWM libr This is a Least-Recently Used simple cache with configurable size and elements expiration time. # Commons -A few common classes are provided to be used by all codes supporting different OWM web API versions. +A few common classes are provided to be used by all codes supporting different OWM Weather API versions. ### The OWMHTTPClient class -This class is used to issue HTTP requests to the OWM web API endpoints. +This class is used to issue HTTP requests to the OWM Weather API endpoints. ### The FrontLinkedList class diff --git a/sphinx/usage-examples.md b/sphinx/usage-examples-v2/weather-api-usage-examples.md similarity index 93% rename from sphinx/usage-examples.md rename to sphinx/usage-examples-v2/weather-api-usage-examples.md index e4eb8887..5eca5460 100644 --- a/sphinx/usage-examples.md +++ b/sphinx/usage-examples-v2/weather-api-usage-examples.md @@ -16,18 +16,18 @@ Of course you can change your API key at a later time if you need: 'G09_7IueS-9xN712E' >>> owm.set_API_key('6Lp$0UY220_HaSB45') -The same happens with the language: you can speficy in which language the OWM web API will return textual data of weather queries. Language is specified by passing its corresponding two-characters string, eg: ``es``, ``sk``, etc. The default language is English (``en``): +The same happens with the language: you can speficy in which language the OWM Weather API will return textual data of weather queries. Language is specified by passing its corresponding two-characters string, eg: ``es``, ``sk``, etc. The default language is English (``en``): >>> owm_en = OWM() # default language is English >>> owm_ru = OWM(language='ru') # Russian -You can obtain the OWM global object related to a specific OWM web API version, +You can obtain the OWM global object related to a specific OWM Weather API version, just specify it after the API key parameter(check before that the version is supported!): >>> owm = OWM(API_key='abcdef', version='2.5') If you don't specify an API version number, you'll be provided with the OWM -object that represents the latest available OWM web API version. +object that represents the latest available OWM Weather API version. Advanced users might want to inject into the library a specific configuration: this can be done by injecting the Python path of your personal configuration module as a string into the library instantiation call like this: @@ -36,16 +36,16 @@ Advanced users might want to inject into the library a specific configuration: t Be careful! You must provide a well-formatted configuration module for the library to work properly and your module must be in your PYTHONPATH. More on configuration modules formatting can be found [here](https://github.com/csparpa/pyowm/blob/master/pyowm/docs/usage-examples.md#wiki-the-configuration25-module). # Using a paid (pro) API key subscription -If you purchased a pro subscription on the OWM web API, you can instantiate the global OWM like this: +If you purchased a pro subscription on the OWM Weather API, you can instantiate the global OWM like this: >>> owm = pyowm.OWM('abcdef', subscription_type='pro') When instantiating paid subscription OWM objects, you must provide an API key. -# OWM web API version 2.5 usage examples +# OWM Weather API version 2.5 usage examples ### Setting a local cache provider -The PyOWM library comes with a built-in support for local caches: OWM web API reponses can be cached in order to save time and bandwidth. The default configuration uses no cache, however the library contains a built-in simple LRU cache implementation that can be plugged in by changing the ``configuration25.py`` module and specifying a ``LRUCache`` class instance: +The PyOWM library comes with a built-in support for local caches: OWM Weather API reponses can be cached in order to save time and bandwidth. The default configuration uses no cache, however the library contains a built-in simple LRU cache implementation that can be plugged in by changing the ``configuration25.py`` module and specifying a ``LRUCache`` class instance: ... # Cache provider to be used @@ -180,6 +180,9 @@ and then access weather data using the following methods: >>> w.get_weather_icon_name() # Get weather-related icon name '02d' + + >>> w.get_weather_icon_url() # Get weather-related icon URL + 'http://openweathermap.org/img/w/02d.png' >>> w.get_sunrise_time() # Sunrise time (GMT UNIXtime or ISO 8601) 1377862896L @@ -205,7 +208,7 @@ The last call returns the OWM city ID of the location - refer to the for details. ### Getting weather forecasts -The OWM web API currently provides weather forecasts that are sampled : +The OWM Weather API currently provides weather forecasts that are sampled : + every 3 hours + every day (24 hours) @@ -476,7 +479,7 @@ If you have no specific need to handle the raw data by yourself, you can leverag >>> his.wind_series() [(1381327200, 4.7), (1381327260, 4.7), (1381327320, 4.9), ...] -Each of the ``*_series()`` methods returns a list of tuples, each tuple being a couple in the form: (timestamp, measured value). When in the series values are not provided by the OWM web API, the numeric value is ``None``. These convenience methods are especially useful if you need to chart the historic time series of the measured physical entities. +Each of the ``*_series()`` methods returns a list of tuples, each tuple being a couple in the form: (timestamp, measured value). When in the series values are not provided by the OWM Weather API, the numeric value is ``None``. These convenience methods are especially useful if you need to chart the historic time series of the measured physical entities. You can also get minimum, maximum and average values of each series: @@ -504,15 +507,15 @@ The PyOWM object instances can be dumped to JSON or XML strings: #... and to XML >>> w.to_XML() - + Clouds[...] When you dump to XML you can decide wether or not to print the standard XML encoding declaration line and XML Name Schema prefixes using the relative switches: >>> w.to_XML(xml_declaration=True, xmlns=False) -### Checking if OWM web API is online -You can check out the OWM web API service availability: +### Checking if OWM Weather API is online +You can check out the OWM Weather API service availability: >>> owm.is_API_online() True @@ -521,6 +524,6 @@ You can check out the OWM web API service availability: Most of PyOWM objects can be pretty-printed for a quick introspection: >>> print w - + >>> print w.get_location() - d + d diff --git a/tests/unit/webapi25/parsers/__init__.py b/tests/integration/agroapi10/__init__.py similarity index 100% rename from tests/unit/webapi25/parsers/__init__.py rename to tests/integration/agroapi10/__init__.py diff --git a/tests/integration/agroapi10/test_integration_polygons_api_subset.py b/tests/integration/agroapi10/test_integration_polygons_api_subset.py new file mode 100644 index 00000000..1345eeb6 --- /dev/null +++ b/tests/integration/agroapi10/test_integration_polygons_api_subset.py @@ -0,0 +1,88 @@ +import unittest +import os +from pyowm.constants import DEFAULT_API_KEY +from pyowm.weatherapi25.owm25 import OWM25 +from pyowm.weatherapi25.configuration25 import parsers +from pyowm.agroapi10.polygon import Polygon, GeoPolygon + + +class IntegrationTestsPolygonsAPISubset(unittest.TestCase): + + __owm = OWM25(parsers, os.getenv('OWM_API_KEY', DEFAULT_API_KEY)) + + def test_polygons_CRUD(self): + + mgr = self.__owm.agro_manager() + + # check if any previous polygon exists on this account + n_old_polygons = len(mgr.get_polygons()) + + # create pol1 + geopol1 = GeoPolygon([[ + [-121.1958, 37.6683], + [-121.1779, 37.6687], + [-121.1773, 37.6792], + [-121.1958, 37.6792], + [-121.1958, 37.6683] + ]]) + pol1 = mgr.create_polygon(geopol1, 'polygon_1') + + # create pol2 + geopol2 = GeoPolygon([[ + [-141.1958, 27.6683], + [-141.1779, 27.6687], + [-141.1773, 27.6792], + [-141.1958, 27.6792], + [-141.1958, 27.6683] + ]]) + pol2 = mgr.create_polygon(geopol2, 'polygon_2') + + # Read all + polygons = mgr.get_polygons() + self.assertEqual(n_old_polygons + 2, len(polygons)) + self.assertTrue(all([isinstance(p, Polygon) for p in polygons])) + + # Read one by one + result = mgr.get_polygon(pol1.id) + self.assertEqual(pol1.id, result.id) + self.assertEqual(pol1.name, pol1.name) + self.assertEqual(pol1.area, result.area) + self.assertEqual(pol1.user_id, result.user_id) + self.assertEqual(pol1.center.lon, result.center.lon) + self.assertEqual(pol1.center.lat, result.center.lat) + self.assertEqual(pol1.geopolygon.geojson(), result.geopolygon.geojson()) + + result = mgr.get_polygon(pol2.id) + self.assertEqual(pol2.id, result.id) + self.assertEqual(pol2.name, result.name) + self.assertEqual(pol2.area, result.area) + self.assertEqual(pol2.user_id, result.user_id) + self.assertEqual(pol2.center.lon, result.center.lon) + self.assertEqual(pol2.center.lat, result.center.lat) + self.assertEqual(pol2.geopolygon.geojson(), result.geopolygon.geojson()) + + # Update a polygon + pol2.name = 'a better name' + mgr.update_polygon(pol2) + result = mgr.get_polygon(pol2.id) + self.assertEqual(pol2.id, result.id) + self.assertEqual(pol2.area, result.area) + self.assertEqual(pol2.user_id, result.user_id) + self.assertEqual(pol2.center.lon, result.center.lon) + self.assertEqual(pol2.center.lat, result.center.lat) + self.assertEqual(pol2.geopolygon.geojson(), result.geopolygon.geojson()) + self.assertNotEqual(pol2.name, pol1.name) # of course, the name has changed + + # Delete polygons one by one + mgr.delete_polygon(pol1) + polygons = mgr.get_polygons() + self.assertEqual(n_old_polygons + 1, len(polygons)) + + mgr.delete_polygon(pol2) + polygons = mgr.get_polygons() + self.assertEqual(n_old_polygons, len(polygons)) + + +if __name__ == "__main__": + unittest.main() + diff --git a/tests/integration/agroapi10/test_integration_satellite_imagery_download.py b/tests/integration/agroapi10/test_integration_satellite_imagery_download.py new file mode 100644 index 00000000..8ff2e12e --- /dev/null +++ b/tests/integration/agroapi10/test_integration_satellite_imagery_download.py @@ -0,0 +1,140 @@ +import unittest +import os +import uuid +from pyowm.constants import DEFAULT_API_KEY +from pyowm.weatherapi25.owm25 import OWM25 +from pyowm.weatherapi25.configuration25 import parsers +from pyowm.agroapi10.polygon import GeoPolygon +from pyowm.commons.enums import ImageTypeEnum +from pyowm.commons.image import Image +from pyowm.agroapi10.enums import SatelliteEnum, PresetEnum, PaletteEnum +from pyowm.agroapi10.imagery import MetaPNGImage, MetaTile, MetaGeoTiffImage, SatelliteImage + + +class IntegrationTestsSatelliteImageryDownload(unittest.TestCase): + + __owm = OWM25(parsers, os.getenv('OWM_API_KEY', DEFAULT_API_KEY)) + __polygon = None + __acquired_from = 1500336000 # 18 July 2017 + __acquired_to = 1508976000 # 26 October 2017 + + @classmethod + def setUpClass(cls): + # create a polygon + mgr = cls.__owm.agro_manager() + geopol = GeoPolygon([[ + [-121.1958, 37.6683], + [-121.1779, 37.6687], + [-121.1773, 37.6792], + [-121.1958, 37.6792], + [-121.1958, 37.6683] + ]]) + cls.__polygon = mgr.create_polygon(geopol, 'search_test_polygon') + + @classmethod + def tearDownClass(cls): + # delete the polygon + mgr = cls.__owm.agro_manager() + mgr.delete_polygon(cls.__polygon) + + # Test methods + + def test_download_png(self): + mgr = self.__owm.agro_manager() + + # search PNG, truecolor, non-tile images acquired by Landsat 8 + result_set = mgr.search_satellite_imagery(self.__polygon.id, self.__acquired_from, self.__acquired_to, + ImageTypeEnum.PNG, PresetEnum.TRUE_COLOR, None, None, + SatelliteEnum.LANDSAT_8.symbol, None, None, None, None) + self.assertIsInstance(result_set, list) + self.assertEqual(len(result_set), 14) + + # just keep the non-tile images + non_tile_pngs = [mimg for mimg in result_set if isinstance(mimg, MetaPNGImage)] + self.assertEqual(len(non_tile_pngs), 7) + + # download one + result = mgr.download_satellite_image(non_tile_pngs[0]) + + self.assertIsInstance(result, SatelliteImage) + self.assertIsInstance(result.metadata, MetaPNGImage) + img = result.data + self.assertIsInstance(img, Image) + self.assertEqual(img.image_type, ImageTypeEnum.PNG) + self.assertNotEqual(len(img.data), 0) + + def test_download_geotiff(self): + mgr = self.__owm.agro_manager() + + # search GeoTiff, EVIimages acquired by Landsat 8 + result_set = mgr.search_satellite_imagery(self.__polygon.id, self.__acquired_from, self.__acquired_to, + ImageTypeEnum.GEOTIFF, PresetEnum.EVI, None, None, + SatelliteEnum.LANDSAT_8.symbol, None, None, None, None) + self.assertIsInstance(result_set, list) + self.assertEqual(len(result_set), 7) + + # download one + result = mgr.download_satellite_image(result_set[0], palette=PaletteEnum.CONTRAST_SHIFTED) + + self.assertIsInstance(result, SatelliteImage) + self.assertIsInstance(result.metadata, MetaGeoTiffImage) + img = result.data + self.assertIsInstance(img, Image) + self.assertEqual(img.image_type, ImageTypeEnum.GEOTIFF) + self.assertNotEqual(len(img.data), 0) + + def test_download_tile(self): + mgr = self.__owm.agro_manager() + + # search PNG, truecolor, tile images acquired by Landsat 8 + result_set = mgr.search_satellite_imagery(self.__polygon.id, self.__acquired_from, self.__acquired_to, + ImageTypeEnum.PNG, PresetEnum.TRUE_COLOR, None, None, + SatelliteEnum.LANDSAT_8.symbol, None, None, None, None) + self.assertIsInstance(result_set, list) + self.assertEqual(len(result_set), 14) + + # just keep the tiles + tile_pngs = [mimg for mimg in result_set if isinstance(mimg, MetaTile)] + self.assertEqual(len(tile_pngs), 7) + + # try to download one without specifying x, y and zoom + with self.assertRaises(AssertionError): + mgr.download_satellite_image(tile_pngs[0]) + with self.assertRaises(AssertionError): + mgr.download_satellite_image(tile_pngs[0], x=1) + with self.assertRaises(AssertionError): + mgr.download_satellite_image(tile_pngs[0], x=1, y=2) + + # download one + result = mgr.download_satellite_image(tile_pngs[1], x=1, y=2, zoom=5) + + self.assertIsInstance(result, SatelliteImage) + self.assertIsInstance(result.metadata, MetaTile) + img = result.data + self.assertIsInstance(img, Image) + self.assertEqual(img.image_type, ImageTypeEnum.PNG) + self.assertNotEqual(len(img.data), 0) + + def test_persisting_to_disk(self): + path = '%s.tif' % uuid.uuid4() + mgr = self.__owm.agro_manager() + + # search GeoTiff, EVIimages acquired by Landsat 8 + result_set = mgr.search_satellite_imagery(self.__polygon.id, self.__acquired_from, self.__acquired_to, + ImageTypeEnum.GEOTIFF, PresetEnum.EVI, None, None, + SatelliteEnum.LANDSAT_8.symbol, None, None, None, None) + self.assertTrue(len(result_set) > 1) + metaimg = result_set[0] + sat_img = mgr.download_satellite_image(metaimg) + try: + self.assertFalse(os.path.isfile(path)) + sat_img.persist(path) + self.assertTrue(os.path.isfile(path)) + except: + self.fail() + finally: + os.remove(path) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/integration/agroapi10/test_integration_satellite_imagery_search.py b/tests/integration/agroapi10/test_integration_satellite_imagery_search.py new file mode 100644 index 00000000..4732b174 --- /dev/null +++ b/tests/integration/agroapi10/test_integration_satellite_imagery_search.py @@ -0,0 +1,109 @@ +import unittest +import os +from pyowm.constants import DEFAULT_API_KEY +from pyowm.weatherapi25.owm25 import OWM25 +from pyowm.weatherapi25.configuration25 import parsers +from pyowm.agroapi10.polygon import GeoPolygon +from pyowm.commons.enums import ImageTypeEnum +from pyowm.agroapi10.enums import SatelliteEnum, PresetEnum +from pyowm.agroapi10.imagery import MetaImage + + +class IntegrationTestsSatelliteImagerySearch(unittest.TestCase): + + __owm = OWM25(parsers, os.getenv('OWM_API_KEY', DEFAULT_API_KEY)) + __polygon = None + __acquired_from = 1500336000 # 18 July 2017 + __acquired_to = 1508976000 # 26 October 2017 + + @classmethod + def setUpClass(cls): + # create a polygon + mgr = cls.__owm.agro_manager() + geopol = GeoPolygon([[ + [-121.1958, 37.6683], + [-121.1779, 37.6687], + [-121.1773, 37.6792], + [-121.1958, 37.6792], + [-121.1958, 37.6683] + ]]) + cls.__polygon = mgr.create_polygon(geopol, 'search_test_polygon') + + @classmethod + def tearDownClass(cls): + # delete the polygon + mgr = cls.__owm.agro_manager() + mgr.delete_polygon(cls.__polygon) + + # Test methods + + def test_search_all(self): + mgr = self.__owm.agro_manager() + + # search all images in the specified time frame + result_set = mgr.search_satellite_imagery(self.__polygon.id, self.__acquired_from, self.__acquired_to) + self.assertIsInstance(result_set, list) + self.assertEqual(len(result_set), 132) + self.assertTrue(all([isinstance(i, MetaImage) for i in result_set])) + + def test_search_for_one_satellite(self): + mgr = self.__owm.agro_manager() + + # search all Landsat 8 images in the specified time frame + result_set = mgr.search_satellite_imagery(self.__polygon.id, self.__acquired_from, self.__acquired_to, None, + None, None, None, SatelliteEnum.LANDSAT_8.symbol, None, None, None, None) + self.assertIsInstance(result_set, list) + self.assertEqual(len(result_set), 84) + self.assertTrue(all([isinstance(i, MetaImage) and i.satellite_name == SatelliteEnum.LANDSAT_8.name for i in result_set])) + + def test_search_for_geotiff_type_only(self): + mgr = self.__owm.agro_manager() + + # search all geotiff images in the specified time frame + result_set = mgr.search_satellite_imagery(self.__polygon.id, self.__acquired_from, self.__acquired_to, + ImageTypeEnum.GEOTIFF, None, None, None, None, None, None, None, None) + self.assertIsInstance(result_set, list) + self.assertEqual(len(result_set), 44) + self.assertTrue(all([isinstance(i, MetaImage) and i.image_type == ImageTypeEnum.GEOTIFF for i in result_set])) + + def test_search_for_ndvi_preset_only(self): + mgr = self.__owm.agro_manager() + + # search all NDVI images in the specified time frame + result_set = mgr.search_satellite_imagery(self.__polygon.id, self.__acquired_from, self.__acquired_to, + None, PresetEnum.NDVI, None, None, None, None, None, None, None) + self.assertIsInstance(result_set, list) + self.assertEqual(len(result_set), 33) + self.assertTrue(all([isinstance(i, MetaImage) and i.preset == PresetEnum.NDVI for i in result_set])) + + def test_search_for_falsecolor_png_only(self): + mgr = self.__owm.agro_manager() + + # search all PNG images in falsecolor in the specified time frame + result_set = mgr.search_satellite_imagery(self.__polygon.id, self.__acquired_from, self.__acquired_to, ImageTypeEnum.PNG, + PresetEnum.FALSE_COLOR, None, None, None, None, None, None, None) + self.assertIsInstance(result_set, list) + self.assertEqual(len(result_set), 22) + self.assertTrue(all([isinstance(i, MetaImage) and i.preset == PresetEnum.FALSE_COLOR and + i.image_type == ImageTypeEnum.PNG for i in result_set])) + + def test_detailed_search(self): + mgr = self.__owm.agro_manager() + + # in the specified time frame, search all PNG images in truecolor acquired by Sentinel 2 + # and with a max cloud coverage of 5% and at least 90% of valid data coverage + result_set = mgr.search_satellite_imagery(self.__polygon.id, self.__acquired_from, self.__acquired_to, ImageTypeEnum.PNG, + PresetEnum.TRUE_COLOR, None, None, SatelliteEnum.SENTINEL_2.symbol, + None, 5, 90, None) + self.assertIsInstance(result_set, list) + self.assertEqual(len(result_set), 8) + self.assertTrue(all([isinstance(i, MetaImage) and + i.preset == PresetEnum.TRUE_COLOR and + i.image_type == ImageTypeEnum.PNG and + i.satellite_name == SatelliteEnum.SENTINEL_2.name and + i.cloud_coverage_percentage <= 5 and + i.valid_data_percentage >= 90 for i in result_set])) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/integration/agroapi10/test_integration_satellite_imagery_stats.py b/tests/integration/agroapi10/test_integration_satellite_imagery_stats.py new file mode 100644 index 00000000..d2f66a49 --- /dev/null +++ b/tests/integration/agroapi10/test_integration_satellite_imagery_stats.py @@ -0,0 +1,66 @@ +import unittest +import os +from pyowm.constants import DEFAULT_API_KEY +from pyowm.weatherapi25.owm25 import OWM25 +from pyowm.weatherapi25.configuration25 import parsers +from pyowm.agroapi10.polygon import GeoPolygon +from pyowm.agroapi10.enums import SatelliteEnum, PresetEnum +from pyowm.agroapi10.imagery import MetaImage + + +class IntegrationTestsSatelliteImageryStats(unittest.TestCase): + + __owm = OWM25(parsers, os.getenv('OWM_API_KEY', DEFAULT_API_KEY)) + __polygon = None + __acquired_from = 1500336000 # 18 July 2017 + __acquired_to = 1508976000 # 26 October 2017 + + @classmethod + def setUpClass(cls): + # create a polygon + mgr = cls.__owm.agro_manager() + geopol = GeoPolygon([[ + [-121.1958, 37.6683], + [-121.1779, 37.6687], + [-121.1773, 37.6792], + [-121.1958, 37.6792], + [-121.1958, 37.6683] + ]]) + cls.__polygon = mgr.create_polygon(geopol, 'stats_test_polygon') + + @classmethod + def tearDownClass(cls): + # delete the polygon + mgr = cls.__owm.agro_manager() + mgr.delete_polygon(cls.__polygon) + + def test_stats_for_satellite_image(self): + mgr = self.__owm.agro_manager() + + # search all Landsat 8 images in the specified time frame and with high valid data percentage + result_set = mgr.search_satellite_imagery(self.__polygon.id, self.__acquired_from, self.__acquired_to, None, None, + None, None, SatelliteEnum.LANDSAT_8.symbol, None, 0.5, 99.5, None) + self.assertIsInstance(result_set, list) + self.assertTrue(all([isinstance(i, MetaImage) and i.satellite_name == SatelliteEnum.LANDSAT_8.name for i in result_set])) + + # only keep EVI and NDVI ones + ndvi_only = [mimg for mimg in result_set if mimg.preset == PresetEnum.NDVI] + evi_only = [mimg for mimg in result_set if mimg.preset == PresetEnum.EVI] + + self.assertTrue(len(ndvi_only) > 1) + self.assertTrue(len(evi_only) > 1) + + # now search for stats for both types + stats_ndvi = mgr.stats_for_satellite_image(ndvi_only[0]) + stats_evi = mgr.stats_for_satellite_image(evi_only[0]) + self.assertIsInstance(stats_ndvi, dict) + self.assertIsInstance(stats_evi, dict) + + # try to search for stats of a non NDVI or EVI image + falsecolor_only = [mimg for mimg in result_set if mimg.preset == PresetEnum.FALSE_COLOR] + with self.assertRaises(ValueError): + mgr.stats_for_satellite_image(falsecolor_only[0]) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/integration/agroapi10/test_integration_soil_data.py b/tests/integration/agroapi10/test_integration_soil_data.py new file mode 100644 index 00000000..538f724b --- /dev/null +++ b/tests/integration/agroapi10/test_integration_soil_data.py @@ -0,0 +1,44 @@ +import unittest +import os +from pyowm.constants import DEFAULT_API_KEY +from pyowm.weatherapi25.owm25 import OWM25 +from pyowm.weatherapi25.configuration25 import parsers +from pyowm.agroapi10.polygon import Polygon, GeoPolygon +from pyowm.agroapi10.soil import Soil + + +class IntegrationTestsSoilData(unittest.TestCase): + + __owm = OWM25(parsers, os.getenv('OWM_API_KEY', DEFAULT_API_KEY)) + + def test_call_soil_data(self): + + mgr = self.__owm.agro_manager() + + # check if any previous polygon exists on this account + n_old_polygons = len(mgr.get_polygons()) + + # create pol1 + geopol1 = GeoPolygon([[ + [-121.1958, 37.6683], + [-121.1779, 37.6687], + [-121.1773, 37.6792], + [-121.1958, 37.6792], + [-121.1958, 37.6683] + ]]) + test_pol = mgr.create_polygon(geopol1, 'soil_data_test_pol') + + soil = mgr.soil_data(test_pol) + + self.assertTrue(isinstance(soil, Soil)) + self.assertEqual(test_pol.id, soil.polygon_id) + + # Delete test polygon + mgr.delete_polygon(test_pol) + polygons = mgr.get_polygons() + self.assertEqual(n_old_polygons, len(polygons)) + + +if __name__ == "__main__": + unittest.main() + diff --git a/tests/integration/alertapi30/test_integration_alertapi30.py b/tests/integration/alertapi30/test_integration_alertapi30.py index e224b893..aaad8bdb 100644 --- a/tests/integration/alertapi30/test_integration_alertapi30.py +++ b/tests/integration/alertapi30/test_integration_alertapi30.py @@ -2,8 +2,8 @@ import os import copy from pyowm.constants import DEFAULT_API_KEY -from pyowm.webapi25.configuration25 import parsers -from pyowm.webapi25.owm25 import OWM25 +from pyowm.weatherapi25.configuration25 import parsers +from pyowm.weatherapi25.owm25 import OWM25 from pyowm.alertapi30.condition import Condition from pyowm.alertapi30.enums import WeatherParametersEnum, OperatorsEnum from pyowm.utils import geo diff --git a/tests/integration/commons/256x256.png b/tests/integration/commons/256x256.png new file mode 100644 index 00000000..71e73ad7 Binary files /dev/null and b/tests/integration/commons/256x256.png differ diff --git a/tests/integration/commons/test_http_client.py b/tests/integration/commons/test_http_client.py index 9e0feb98..14fb978c 100644 --- a/tests/integration/commons/test_http_client.py +++ b/tests/integration/commons/test_http_client.py @@ -54,6 +54,18 @@ def test_ssl_certs_verification_failure(self): HttpClient.get_json, client, 'https://wrong.host.badssl.com') + def test_get_png(self): + # http://httpbin.org/image/png + status, data = self.instance.get_png('http://httpbin.org/image/png') + self.assertIsNotNone(data) + self.assertIsInstance(data, bytes) + + def test_get_geotiff(self): + # https://download.osgeo.org/geotiff/samples/made_up/bogota.tif + status, data = self.instance.get_geotiff('https://download.osgeo.org/geotiff/samples/made_up/bogota.tif') + self.assertIsNotNone(data) + self.assertIsInstance(data, bytes) + if __name__ == "__main__": unittest.main() diff --git a/tests/integration/commons/test_image.py b/tests/integration/commons/test_image.py new file mode 100644 index 00000000..bdcd2fb4 --- /dev/null +++ b/tests/integration/commons/test_image.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- + +import unittest +import os +import uuid +from pyowm.commons.image import Image +from pyowm.commons.enums import ImageTypeEnum + + +class TestImage(unittest.TestCase): + + def test_load(self): + path = '256x256.png' + i = Image.load(path) + self.assertIsNotNone(i) + self.assertEqual(i.image_type.mime_type, ImageTypeEnum.PNG.mime_type) + self.assertIsInstance(i.data, bytes) + + def test_persist(self): + path = '%s.png' % uuid.uuid4() + try: + data = b'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x01\x00\x00\x00\x01\x00\x08\x06\x00\x00\x00\\r\xa8f\x00\x00\x80\x00IDATx\x9c\xec\x9d\x07\x98\x15\xd5\xd9\xc7\xd7\xc4\x16\x1b\x88\x15\xec]\x13\x13K\x12c\x8aFc\xd7\xa8Q\x13\x8d%\xc58,\xbd)(\x8a\x88\xd8\x15E\xc4n\xec\xbd"b\xef\xbdw\x10D\xa4\xa9XQX\x94K\xb9\x9a\xbc\xdf\xfc\xce\xfa\xf7{\x9d\xdc\xbe\xf7\xee.0\xe7y\xe6\xb9m\xee\xcc\x99s\xde^\xeb\xa6N\x9dj\x1c\xef\xbf\xff\xbe}\xf0\xc1\x076m\xda\xb4\xef\x8e\x0f?\xfc0\x1cu\xcd0\xb8\x0f\xf3\x988q\xa2\x8d\x1b7\xce\xde|\xf3M{\xe3\x8d7\xec\xb5\xd7^\xb3W^y\xc5^~\xf9\xe5\xf0\xfa\xea\xab\xaf\x86\xef\xdfy\xe7\x9dp~\xbe\xeb\xbd\xf7\xde{\xe1Z\x93&M2\xde\xd7\xe29\xb8&\xeb\xc4\xbaq\x0f^\x0b\x9d\xaf9\xf1l<\xc7\xeb\xaf\xbfn\x93\'O\xfe\xde\x7f\xf8\xfe\xc9\'\x9f\xb4\xdbn\xbb\xcd.\xbf\xfcr;\xf5\xd4Sm\xb5\xe8\x19[\xba\xdbgv\xc9%\x97\xd8\xddw\xdf\x1d\xd6\x815`\x9d8\xc6\x8e\x1d\xfb\xdd{\x8e\xf1\xe3\xc7\xdb\x84\t\x13\xc2\x1aq\xf0\x99\x83\xf7|\xff\xee\xbb\xef\x86ypo\xbf\xf7\xd5^\x9fr\x87\xe0\x8d5eN\x1c\xcc\x91\xf9\xf2\x8c\xac\x1b\xf0\xc0\x1a\x01\x0f\x1c/\xbd\xf4\xd2\xf7\x8e\x17_|\xd1\x9e\x7f\xfey{\xfa\xe9\xa7\xed\xf1\xc7\x1f\xb7\x07\x1f|\xd0\xee\xbb\xef\xbe\xb0n\xb7\xdez\xab]}\xf5\xd5v\xc5\x15WX\xbf~\xfdl\x8b\xe8F;8\xeac\xbd{\xf7\xb6\xfe\xfd\xfb\xdbI\'\x9dd\xa7\x9cr\x8a\xf5\xea\xd5\xcb\xf6\x8e\x06\xd9_\xe3\xdf\x8e\x88"\xeb\xda\xb5k\xf8\xfe\xd2K/\xb5\xeb\xae\xbb.\xec\xcdM7\xdd\x14\xf6\xe7\xc2\x0b/\xb4\x0b.\xb8\xc0\xce?\xff\xfc\xb0?|\xf7\xe8\xa3\x8f\xda#\x8f\xfah\x1b0`\x80\xf5\xed\xdb7F\xfc#m\xe3h\xa4\xfd$\xba\xd5\xb6\x8c\xae\xb7\xc3\xa3N6d\xc8\x90\x80\xfc\xb7\xdf~\xbb\xdd{\xef\xbd\xe1\xb8\xe3\x8e;\xec\xc6\x1bo\xb4\x9bo\xbe9\xdcc\xc4\x88\x11\xe1~B\xe0\'\x9ex"\xcc\x05\x82\xc0\xf1\xd8c\x8f\x85\xef\x98\xa3\x10\x9cy\xeb\x198 b\x109\x1d<3{U\xed\xf5\xfd\x8e\xb9\xa7\x04\xa0\xf2\x91\x12\x80\xda\x8d\x94\x004\x13\x01\xf0\x00\xec\x8fj\xdf\xb0\xd0D\xb4\xd9B\x0c\x1dc\xc6\x8c\xb1\xb7\xdf~;\x00-@\x9a\x9c\x97DE\x111\x0e\x90\x02d\xe0}\xad\x9f\x85k\x97\x8b8\xfa\x0f\xcf\xe3\x917y\x1d\xc4V\x01\x1a@\x03\xd0\x17Re\xf8\x1eda\xcdt\x80\xdc\xfe\xe0>\x9c\xe3\xd5\x03\xde\xf3\x1b\xeb\xd6\x9c\xfb^l\xb0\xb7\xcc\xc9\x8b\xff\x88\xc2B~\xc4|\x90\x07$\xf2\xc8\x86\xd8\xcd\xda\x81\x88\x88\xd9 &b?\xa2:\xa2\xfe\x8f\xa3\xdb\xad\xae\xcf7\xb6\\\x97\xf7m\x99\xae\x1f\xc6\xaa\xd5\x871\x82\xd7\x07\xc4G-\xe8\xd8\xb1\xa3\xfd<\xba\xda\xdatz+>\xc6\xda\n\xf1\x81:0l\xd8\xb0\xef\x90\x9f\xfb\x80\xc4 5\xf7\x82\xd8p\x7f\x10\x9fy1G\x11\'\xa9"\xfc\xcew<\x03\xcf"\x02\xde\xd2\xeb\x9c\x12\x80&\xce=%\x00\xb5\x19)\x01h\xa6\x91\x12\x80\xa6\xcd=%\x00\xb5\x19)\x01h\xa6\x91\x12\x80\xa6\xcd=%\x00\xb5\x19)\x01h\xc6\x85N"\x7fK\x10\x00\x10\x80\xcd\xc60\x06\xd2{#\x15\x80\xeb\x81\xdf\xbb\xdfD\x00d\xf4\xf3H\xc5\xe1\x8d\x85\xcd\xfdl\xf9\x9e\xd7\xcf]s\x96\xa1\x92\xdfy\x16\x8c\x7f\x18\x99p\x1f\x01D\x18\x06\xf9\xde\x1b\x0c\x93\x83k\xe8\xb9\xe5\xfeL\x1e"\x90Z\x1b\xb9\x01[z]\x92C\x84\x12\x18\x00\x1eD\x000\x92\x81d\xde\xe0\x87\xcb\r$\x04!!\x00\x18\xffp\xab\xc9\xdd\x86\x91\xee\x9ak\xae\xb1\xcb.\xbb\xcc\x86\x0e\x1dj\x03\x07\x0e\xb4\xce\x9d;[\x14E\xdf\x1d\x83\x06\r\n\xae\xbc\xb3\xcf>;\x18\x04\x0f\x8bz\xd8\xbe\xd1@\xdb):;&\x16\x13\x03a\x80\x88p-!\xbc\xdc\x8c\xdcW\x86=\xe6\x95\xcbE)\xd7-D\xd73\xa8\x96^\xe7\xbar\x91\xbf\x10\x00Vr=\x8f\x10\xb2d\x03\x94|\xf6\xd2\x81Gb\xef\'\xce\x05\xd8\x00?\x04D\xbeo-\xb8\xaeY\xc9:Uk$\xef/"\xa0ue\x8e\x10\xc2\xd3N;\xcdn\xb8\xe1\x86\x00lp:\x80\xdf\xafK%\xf7\xd5:\xebhnoO\xb9\x83\xf9\xb1w\x92\x0cY\x03\xb8?\xeb\x01\xb2aUg}\xe0\xc2 \xbd|\xefxK\xe0\xd4\xf8\xd8\x91\x04 \x04\x10S\xf9\xde\xf9\r\x02\xc1w\x17]tQ \x00xY\x90\x14 \x14x\x0bN>\xf9d;\xee\xb8\xe3\xacO\x9f>\xb6x\xcf\xd9\xd6\xb6~\xb4\xd5\xd7\xd7\xdb\xbf\xff\xfd\xefp?\xee-)@\x12\x81\x0e\xc5\xabp0oO\xe0[\xddH\t@\xf3\x8e\x94\x00\x94>R\x02\xd0J\x86"\xb2J\xf5\xa9\xfb\x88\xae$\x90y\x84\x16\xe2\xca\xd7\x0b\xe0\x83\xbc\xb9t}/\xc2k>\xd2\xa3s\x89\xfe^\xef\x95\xe8\xdc\x9a\x80=\xa9\n\xf8\xb9\xf1\x19 $b\r\xff5b&>\xe1b\xc4\xb7\x94{\xea\xbe\xadi-\x92\x83\xf9\xb1\x9f\xec\x9fb\x1b\x14\x13\x02ry\x1b\x00\x84\x00\x04\x94\x8f]\xefAL\x88\x03\x04\x81\xf5\x83@\x80\xf8"\x08\xac+\xc8\xdc>z2F\xee\xf1\xb6|\xe7)\xb6J\xfdX[\xa7\xfft[\xe3\xc8i\xb6j\x97w\xec\x07\xbd\xe7\xdb\x9f\xa3\xa3m\xed\xe8!\xeb\xd4\xa9S \xcaD\xfa\x11Q\x08\x91a\x0e\xe8\xf5\x12\xf1%\xf63O\xc5y\x00\xdbM\xdd\xb7\x16\x1f)\x01\xa8\xfeH\t@\xfe\x91\x12\x80V4@\x1e\x19\xe7x\x98RE\x99\xa4\xa8.\x91\x93\x8d\x95\xe5Y\xa2:\xd7f\xc10\xf6\x14\xb2\x8c\n\x809\'i$\x14A\xf0\xefE\xb0Z#\xc0\x0b\xf9\xf3\xcd\xed\xaa\xab\xae\nF\xa7\x13O<1\x88\xaa\x88\x93-1\xcf\xe6\x1c\xac\x05{\xc6\x9e\x02\x0b >\xde\x0c\x1f\x11\xaa\\\x00\xe5\x03xb\x80\x01\x8e\x03\xa3\xa9\xdeK,\x878@\x0c\x88\xc7\x07\x81\x11\xfd\x89\n\\\xbe\xf3{\xb6\xe3e\x19\xdb\xf3\xea\x8c]9!k\xbb\x8f\xdb\xdd\xee\x9c\x9e\xb5\xc1Ofm\xedc\xa6\xdb&\xd1\x9d!B\xf0\xbc\xf3\xce\x0b\x04\xf9\xca+\xaf\x0c\x07\x84\x04\xa4g\x9e\xc0\xa1\x8fZ\x84h\xe5\xf2Z-\x90\x83\x87Q\x18&\xef+y(6\x96\xc5\x01\xd1\x15\xe6\xcbB\tY\xf9M\xae\xbeR\x08\x8cBj\x15\xea\xab\xf7\xb2\x03(\xbc\xb59t\xfeZ]\x9f\x10\xd3\xc1\x83\x07\xdb\x8f\xa3\xdb\x82G\xa0\x16\t!-9$\xc9y\xe9\x8d}\x13\x12\x01o \xb9t~\xb9\xfb\xf4\x1e\x84\x96\xfe/w\x1f\x84\x12\xbb\t\x08\nw\'y\x07I\n\xae\xcd\x1aBTAb}\xcf\xebj\xd1\xd3\xb1\x040\xc1\xda\xf7\x9cj\x1dz\xbfo\xed:\x8e\x0b.\xc2%{4\xd8\xca\xd1\xf3!I\x88\xff\xb3\x1fH\x0e\x84\xfbr\x1f\xae\xc5w\x10\x15\xa4\x00\xef\x91j5\x16\xfe\x02\xa3d\x8fXJ\x00\n\x8f\x94\x00T6R\x02\xd0\xb2\xa3d\x02 \xeb+\xa2\x17\t&\x95\xde\x0cqN\t\x0e\\\x0f\xd1Nb\xba\xd7\x91\xcaY8\xa9\x19\xde\xcf\xcd5\x01"^\x9bC\xf7\x92\x18_\xea\xf9\xa5>\x1fA\'\xa4\xa5\x92\xb0B2\x10\x84\xb3\xf2Y\xb6\xae\xc1\x9a\xb1?\x10}\x90])\xcdJd\x026\x10\xafA,\x10\x1d}[:<\x96~>c\xc5\x07\xe9\x95,u\xce9\xe7\x04\x95\t\x1f\xfe\xae\xd1\x99\xb6YtG\x8c\xcc\xaf\xd8\x8a\x1d_\r\x9fY\xcb\xa3\x8e:\xca\x8e9\xe6\x98\x80\xd4 ?>\xfd\xe1\xc3\x87\x07\x1f\xff\xb2]\xa6\x84t`\xd4\x06\x11\x17\xae\x0f\xc1\x00\xe9\xb9\xa7\xec\r\xfcF\x9a6IDx\x0e\x98Sk\xd6\xf3\xbd\xad\xa9l\x0fPJ\x00\n\x8f\x94\x00\x94?R\x02\xd0\xbc#%\x005\x1c)\x01(\x7f\xa4\x04\xa0yGJ\x00j8R\x02P\xfeH\t@\xf3\x8e&\x11\x00\xdcml\n\x00\x88\x91\xa6\xd2\t(QE%\xab\xf8,\xc3]\xa5\x8b\xe7\xf3\x08T:J\x06@\x11\x16_$$\xf9\xc0\xde/\xde\\1\xf1\xa5^\x1f_6\x05A\x00P\\W\xac]-\xe7\xd5\x9c\xc3Gxz\x03\xa0\x8c\xc2\xec#L\x02"@B\x8d\xe2\xec9\xf4\x19\xa4\x83(\x10\xed\x07\x82b\xa0\x93\x8b\x8fh>\x8ct\x18\x03!\x10 :\x86;\xceS4 \xb1\x01 :\xd7\xe07\xfe\x03Q\xc1\xf0\x88[\x91\xdf\x88\x15\xc0\x05\x08\x81\xe1>\x18$Uv\x8c{q=\xe2\rpGV\xb2\x0er\x93\xfb|\x07\xf0\x8c\xbd\xc6`.wyS\x8a\xb5x\xe4\xf7\x91\xb3:\x8a^\xc0\'\xad\x94b\xa1\xcfEm\xfc\x04<\x020\x81dE\x9cr\x86,\xc9\xaa&\xa4*8\xf2\xff\xcb[\xa0\xa0\x12\x9fP$\xc9A\xbf\xc3}\xf8o\xa9\xcfY\xe9(\x95\x00\xf8\xa0\x17\xa4\xa6\x85\x89\x00\x14\x1b\xec){)O\x80\x02mT!\x87\xcf\xbe\xea\x0e\x9e\x01\xad\x91\x10\x98sx\x0f\\\xe8P\r\xc4B\xfb\x0b\\p-\xae\t\x01\xd9>\x1an=z\xf4\x08\x84EY|Ma\x12I\xa6\x85gM\xb5\x0e\x95D\xc4\xa1\xda\x97\x10\x04\xe0\xb2\x12\x98LJ\xc7\xc9\x90\xf9d=\xca\x9c#%\x00\xd5\x1d)\x01(>R\x02\xd0\x8a\x08\x00\xc3\x87\xef\x96*\x8e$\x13\x82|\xe4[r!\x9a\xb2\xa0\xba\x9er\xec\xbd\x1fV\xd1Y,$\x8b\xca"k\x13\x95 \xa4\xa83-tk\xd1\xe5\x00x%\x95\x08\xf0[zN\x95\x8e|>g\xbe\xf3!\xdd\x8a\x0cU\x94(\xfb\xa1\n\xd1\x10h\x90\x85\xfdd\xafd\x97Rue\xd4J\xae!\xc2\xc1\x7fx_I\xf2\x13\xd7\xe2\x9a\xa8\x19\xa8a\xa8\r\x10\x9bR\xc5q_\x97R\xcf\xa5z\x97\xb9\xa2\x1a!V\xbe\xbe!\x07\x84_\xb5\x03X\x87R\x92\xf4<\xd3\xd3\xfa)bV\xefy6\xcd\xa3d\x9b^J\x00\x9a\x7f\xa4\x04 %\x00\xad\x86\x00$\xa3\xb6*\x11G|rP\xadc\xf3\x85\xc4\xaa\xc0\xcb\x02\xb3\x81\xaa\xd6\xc2\xe2\xf3=\xcf\xc1\xa2\xa8\x02\x11\x0bU\xab9\x953\x98?sU\x11\t\x01JK\xcf\xab\xd4\xe1\xd5\xac|\xc8\xa7\xdf \xbe\x8a\x9fWN\x08\x9f[{$]\xae\xa1<\x15\x192y\x0e=\x93zA\xc8\x88\x98Lh\xe2\xf0)\xc5\x10\x011\xacBp\xe9\x8d\xd8\xc2O\xe5\xd7@(U`\'\x99W\xa1k\x97\xfc`)\x01h\xbe\x91\x12\x80\x94\x00\xb4*\x02 w\x85nP\x8e[B\xa2\xb8\x0c\x10\xb9\x88G2k0\xdfu|\xdd?\xcd\xa3\x10\xa0\xc88\xc8\x82\xe2\xb2\xc1\xb5\xc6&\xb0\x18J;V\xf6Y9\x99\x8e\xb5\x1e\xccK~p\xd5\x99\x03pZ[\xdd\xbej\x0c\xe5s\xf8\x18\x8e\x92\xdcSU\x1c\xdeU\xcc!\xb5P"\xbf\n\x92\x08i\xf8\r$BD\xf7\x86J!\xb5\x0e\x90X)\xc3\x1cBx\x1d\xca\\\xf4\x88\x9f\x8b\x00@L\xfc\\}\xfe\x84p\xca3W\x11\x01\xa5R\'\xfbl0o\x19\xbdK^ o1/u\x83\x84\xb4\xbe0e.\xe2!\x8b\xa8\xceI\xfa\xecEY\xd5\xd8\x82\x07c\x13J\xf1 \xf0?|\xbe$t\x10H\xc2\xa2\xb6\xf6\n-\xa2JA\x9c\xc3\xc1{\x0e~\x87 pM\x08\x80\x90^u\x0b\xd5\x1d\x18\xc2#\xf7 \x07\xeej\x11\x00y$\x80\xeb\xa4H\xcfs\x89\x08H\xb5-\x04\xe3\xf2|)\x0b\xb3\xac\xc5\xce\xf7\xbd\xa2\x8fT\xc3]~]\xc5\xdf\xf3^\xdf{\xbf\xa8\xb2\x9f\xf4\x00\xa5\xb8\xb9|y1\xc52{\xca\xa6MV=v~\xe7A\x9b\x92\xce\x9ck\x88\xbb\xa8\x84\x95\xf7\xbb\xf2\xbc\xcaL\x94dR\x8d\xfby\xee\x01\xf0\xf2LZ\xebEE\x1aH"\xbd\x0e\xdf\x16\xce\xdb\x89<\xa7\xd7\x91t\xdb\x81\x84\xaa\xf1\xaf\xde\x02\x10\x02^\xf5^}\x06@|\xdeC\x10\xf8\x0c\xf2\x8b\x00\xe8\xe03H\xcf9H\x00\xea\x1e\x94\xe4\xf2B|\xb9\n\x85\xfc\xaa4\xac\x08P\xb9\xad\xc5\\\xd5\xd2\xcd3\x83\xdc^\xfc\xf7\xb5\x06$\xf2\xab_!\xd7\xe5\xfa\n\x08RP\x90\xff,\x02\xa0\xdfd\xfb\x91\x8a,\x02\x90lz\xeb\t\xa2\xca\xe5K\xfd\xe6\xf9}\xf9t\xae\r\xa1c^RsJZ\xf4|\xdf+\x9dW\x08,\xca\x94l~\xe8\x13"\xc4-9\xfb\x93\x0c{\x15\xd3\x11\xa1\x96\xb1O\x91x\xbe\xc9\xa8\xda\x8b\xe9\x90KOR\x01\xe7p\xf0\x9b\x8c||\xcf\xffAx\x19k\xbd\x9e\xefu\xfc\\\x88\x9f\xfc-)\x01$\x8b\xa6\xc8\x88\'x\xe4y\x81\x01O\xe0\xe4\xa2\x94\xd1R\x9d\x93\x91\\\xb0O`S*i\xe1\xf3}\x9f\x12\x80\x94\x004\xd7H\t@+#\x00*e\xe4\x9bpr4w\x80MR\x17\xac\xf6\xf5e\xdf\x10q\xf1\xa5\xaa\xd8(\x80In:\x90_DL9\x06\xf2\x80(\xb1\x87\xeb\xa4\xc1<\xa5\x8dBV~!D\xd2\xbd%\xc4\xd7\xa1\xc0\x17\x11\x01\xb5\xef\x029\xbd\xb8\xef\x1b\x8b\x82$\x88\xf8\x88\xc8\xbc\x17\xd2\'\x11\x1c\xa2\xc1g\x95*\xf3\xba\xbcT\x8ed\x9bp\xd9\x1e\x92\x88\x9f<\x04/b(\xbe\xe9\xad\x12\xa8|\xff\x04\x15\x8f\x91:\xc3\xfc\x98\xaf\xd4\x16T\x13\xbcGx\x91@|J\xa5\xd1;\xa1\xa4M\xc8\xf5=\x1b\xa1z\x81,\xfa\xc2\x08\xd8\xaaM\xe7)\xb1\xe7*J\x06\x91\xae\x9f\xac\xb0\xe23\x1fY#\xb5\x8c\x92d\xe2\x93\xa0t\x1e\xd7\x90\xdd\x80W\xfe\xa7C9\xdc\x92*\xb4\x07*\xa6\xa9\xc2\x9a\x02\x10\xf9\x7f}\x0b\xb5\xa6\xd4`l\xee\x91L\xd2I\xd6\xb4\xf3\xcd`\x92\xd9w\x8az\x13r\x08\x11\x85|\xf9\xb2\xf0$\r\x80\xe4\x8a\xd0\xcb\xe5\xa7\x07\xd1\xd4\xa4$\xc9\xe5\x93F\xbd$\x01\xf0\x95\x7f\xbc-\xc2\xff\xcf\xbb\x01eW\xf3\xc8\xee\xed\x17\xaa\x8e\xa4\x82\xa5\xe8\xf7\x18+\xb1I\x80\xf07\xddtS\xa8y\xa8f)\xb4E\xa31\n\xafE7!%\x00)\x01h\xa9\x91\x12\x80\x94\x00\xb4\xe8H\t@\xcb\x8e\x94\x00\xb4\x02\x02\x90o\xa8\xd8\x86\\-%\xe7\x16\xe7\xd9h\x1f\xcaX\xcd\xd1\x14\x9b\x04\xcf\xa5\x8da\xe1U\xd2\\\xc5C\xa5\x8b\x96:\xefdH\xb5Z\xa3\xcb\x05\xca\xf5XO\x11\x1co\xbc\x12\xb0\xa8X\xa4\x88\x81l\x122D\xca\x1e!\xe2\xa4C:1\x80\xa4\x9c\xfb\xd6\x9c\x10U\x08\xf1\x05+\x9e@\x88\x08@\x00<\xf2{\xa4W\x8e~\xbe#\xe9\x8a\xf3\xfa|r\x8d\xfd\x9e\xc8\xa0(\x02\xa0\xeb\xe5"\x04\xb2\x19\xf9kyd\xd6\x7fT\xc2\xce\xef!\xe7\xe6\x8a[\x10\xd2+6\x01]\x9f\xbe\x85\xd7]w]H\x82S\xdf\x04:M\xd3\x13\xa1w\xef\xde\xa1\xef\xe4Yg\x9d\x95\x12\x80B#%\x00-7R\x02\xb0\x00\x10\x00\xb9]\xe4\xa6\xa8\xf4Z*\xcb\xd5\x14"\x92k\xc8}W\xe9\xffA06\x82\ra\xb3@N\x058\xf9R\xe7"\x00\xbex\xaaw\x8f\xcaS"b\xe9\xad\xd2\xda|\x01\x9d\x12Q\xbc\xd5Yb(\xaf\xb2\\\xf3*1U\xd1\x81\x12G\x15k\xee\xa3\xcc<\xf0y\x02\xc1\xba\xaba\x8b\xf7Z\x14\x8b)\xaf\xd5\xf0\x91\xa5\xfe\x90\xdbK\x91nZg\x9d+\xc4\xf7\xd6~\x8f\xcc\xfeH\x06\xdex\xd7[R4\x97E^H\xab\xdf\xb4\x96~\x0f\xf5}.\xb1?ym\xef\xf6\xf3\x84\xc9\xef\x97\'4\x82C/\xee{\xcf\x05V~\\{\xea\x96$Q\x9fnGtC\xea\xde\xbd\xbb\xfd#\xeabQ\x14Y}}}hxr\xc2\t\'\x14\xde\xdfB>\xf9d\xd4Q\xa5\x88\x06\x90\x01\x84<`9\xed\xafJ\x89\x17\x90dQ\xc9\xbc\x18 1\x00\xe59/\x04\xc0W6\xf6\x99g\xea\xf0\xe2[\xaa\xfb "\xa4\xa7*A\x05"\xa2\xa85\xe6\xc2\x9c\x00.\xe6\x9aK=@5\x10!\xa8\x96Z\xe0\xabL+\x82O\xde$_7?i\xbf\x00F|\x02\x8cT\x01\xae!\x0b\xb9De/\xe6{\x11\xdc#\xa7Gl\xe5p\xf8C\xc9C\x9e\x88K\xc5\xd0\xffu\x8dd\xa4a\xf2H\xda"\x80D\xc0\xe3\x83,<\xe0)}R-\xa2@&\x16\xf0\xb0\xa8G\x10\xc7\x11\xc5A*\xc4mU\x81\xe1Ub\xb7>c\x99\xe5\x1cDyz\xd8\x9fy\xe6\x996p\xe0@;\xfd\xf4\xd3\xed\x82\x0b.\x08\xd7\xed\xd6\xad\x9b\xb5\x8b^\x8a7\xf2M[\xa1\xf3\xf8 \xcea\xb1]?\xba\xd7V\xac\x7f\xdd\x96\xe9\xfa\x9e\xfd4\xba\xd9\xfe\x18\r\xb6N\x9d:Y\xd7\xae]\x83e\x97\xde\xf6\x88w\x7f\x8a\x8e\xb7\x03\xa3\xbean\x7f\x8d\x8e\x0c\xdf\xfd-\xea\x166\x9fkc\t>\xf9\xe4\x93\x03\xb1\x80\x18 6Bx\xb0!(}\xd5\xab\x05\x12K\x15\x93\x0e\x02\x88p\xcbm\xe9K\xbd\xf9\xa0\x1c\xdf\x82Jb}>$I\x8a\xe8\xf9\x10_H\xea]\xa6\xf2\x04\xf0Y6\x97dr\x96G\xfad\x1d\n\x1fd%D\xf7\xc8\xeeKo\xe9\x1a\xc9\xc4\xa2\\D+\x97\xf8\xef-\xffI\x02\xa5\x80\x1fU\xe9Q\xc0\x99\xee\xa5\xa4\x1e\x18\t\xb0\x05\xcc\xb1\xf7Kw\xfb\xc8~\xd0{n|\xcc\xb7\xc5{\xce\x0e\x9f7\x8fn\xb1_\xc6z>MMw\x88\x86\x05\xf8@%\x80\xa1`\xf5\x87I\x9cv\xdaiv\xf6\xd9g\x07\x18\xae\x18\x91R\x02\x90\x12\x80\x94\x00,\xa2\x04\x00\xc4\x16\xf2\x03@\x88\xaf\x88\xb8L\\\x88\xe6+\xe6\xe8\x01}\x19\xe3\xa6\xa6\x00W2\x14\xbb\xa0\x85\x96\xef\x98M`c\x14`!\xbf\xba\x0c\x7f \x08\xcf\x08\xe2\xfe"\xba2,:\xd6\xd4?E\x03\x022\xb2\xb0 6\xa2\xb6\x0cx2\xd6\xe9\x182dH\x10\xc3~\x15\xfd\xdb~\x1d]\x1a\x8bf\x17\xd9\xc1Q\x1f\xeb\xd5\xabW\x08\xccX\xbcg\x83\xb5\xeb8\xd6\x96\xeb\xf2\x81-\xd1sV\x10\xd1~\xd4\xed\x03[)\x1acKw\x9fnK\xf4\x98\x11\xbf\x7f\xd1\xd6\x8e\xc5\xba\x9fD\xb7\xd9\xaa\xd1\xb3\xb1Z\xf0V|\xce4[\xb2\xc7\xf4\x98@\xbco+t\x1a\x17\xceY\xae\xcb\xc4p>\xff_\xbe\xf3\x84\x98\xa8\xbc\x11\xff\xe7V\xdb7\x1a\x18\x8cC\x88\x80\xcc\x07\x15\x02\x15\x03B\x00\xa1\x82\x90+\xfb\x8dgWQLo\x08\x13\xf2\x08QD\x14XO\x19\xf2\xbcH/\xf1U\xd6n\xc53\xf8u\xe6>R?\x92\xa2{\xf2\x9e\xbe\xc6\xa4b0\xd8WU\xff\xf1G\x12\xe95_\xe6\xe93\xeat\xf0\x9d\xcf*\xf4\xc8\xef\x03\x8b\x92\xb1\x15\xc9#\xe9\x1d\xc8G\xd4\x84\x13I\x02\xaa\x06\xa8Z?\xd6\x8d=\xc1\x90\x0b\x01g\x0fA\xfa\xf6=\xa6\xd8Z\xfd>\xb1u\x8f\xfb<\xde\xe3wl\xb1\xf8\xbb\x1fG\xb7\x07\xb1\x7f\xaf\xe8\xa4X\x95\xac\x0f\x9e\x00\x89\xfb\x1c\xc00\xc8\x0f\xbc\x02\xd3\x15!RJ\x00R\x02\x90\x12\x80E\x98\x000\xd8T\x08\x80\x10DUT\xf5\xbb\xf7\xebz\xc3\r\x9f\x9b\xa39\xa8\xee\xeb\x01\xc0#\xba\nA\xa89\x83\n&\xf0\x0c\xbe\x04\x94\\t\x88\xcc\x84S"j7"\xe3K\xc1\xb7\xbaKtV \x02\x18X$b!\xbe#\xe2\xa3. z\x1fu\xd4Q1\xc1\xb8< \xecb\xbd\xe7\xd9R\xdd?\x89\x11\xf3\x1d[#z\xcc\xd6\x8d\x1e\x08\x06\x9c\xc5{~e+v|;F\xdaO\xe2\xcd\x9d\x17T\x81\xc5zgm\xd9\xae\x1f\xda\xe2\xbdf\xc7Daf\x8c\xd8\xef\xdar1B\x83\xd4Kw\xe7\xbc9V\xd7\xe7\x9b\xb0\xf1\x9c\xcb\xfb\xba>\xff\xf9V\x1c\xfc,\xfe\xcf\x97\xe1\xfb\xc5{}i\xcbv\x99\x1c\xe6\xbcq4\xd2v\x8fN\x0b.!\x0c\x8a\xcc\x19b\x00\xd1\xc2@\x88\xba\x92T\t<\x82z1\xd6\xbb\xb1\x92=\xf1\xbc\xb1+\xe9\xf2\xf2\x11y\x85\xc4}!\xbd\x0f\xc1\xf6\xc8\xaf\x83\xef\x94\x08\x94\x14\xdf%\xde\x17B\xfc$\x11\x10s\xd0\x91\xcf\xf0\x97\xcf\xa8\x98\xcb\x95\xe9\x89\x91\x1av&\xdb\xd4\xfb\x06\xa92\xaecL\x860\xb37\x88\xee+t~;\xc0\xc7\xea\xdd&\xda\xd6g\x7fi\xbf<\xf7+\xdbv\xf8\xec\x186\xde\x8f\xe1\xea\xb3`@F=@\xec\xc7\xd5\x87\x9a\xc9\xfe\x82\xf8\xfc\x1f\x18\xc6x\x88:\x8a\x01\xbab$\x03\x00\xbc\xce\x08u\xe2\xa2\xb9\xe2\xcb\x9bS\xc7\x97\x95\x99\xcdd\x93\xbc\x1e\xa6\xfa\xee\xbe\xa6\x9b\xfc\xf1*\xf1\x8cD#\xbd]\xef\xb1\xb8b\xb5\xc7\xaa\x0e\xb2\xaf\x18sT\xb8+\x88\xbbv\xf4p\xb0\xb8\xee\x1a\x9d\x19\x82.\xd0\xb9\xd1\xdd\xd1\xc1w\x8a\xce\xb1\x9fE7\x85\x00\x8c\x1f\x04\xc4\xff"p\xf8%{\xcc\xb4\xc5\xfa|\x1d\xbf~\x1e\x10\x96\xef\x97\xed\xc2\xf5&\x84\xf7 \xf4r\x9d\xdf\x8b\xdf\x7f\x1e\xfe\xd7\x88\xd4\xb3\x02\xe1\xf9a\xafL\xfc\xf9\xebo\xf5\xbe\xaf\x82\xb4\xc0\x7f\x96\x8e\xcf\x05\xe9! +t\x9ad+G\xa3c\x82\xf3n\xf8\x0cA\x80\x10\xfc\xa0\xd7\x9c\xf8\x9e_\x04\xa9\xe1G\xdd>\x0cDf\xfd\xe8>\xdb3:9\xd8\x14|\xec\x81l\x03\x10A\x85%+\xecX\x87\x8af\xfa\x82\x19:\xe4\t\xf16\x05!\xbcGv\x15\x92\xf1e\xe4}\xe5\x1b\x05\xfex\xc4O\xe6\x08\xc8#%)D\xc8\xee\x11\xde\xdfG\xefs\x1d"\x02\x9e`\xf8\xeb\xf8yr\xe8\xbd\xe6\xabC\xd9\x89:_\x05bs\xc5S\xf0^\xe1\xf0\n\x18\x83\xe8\xaa\x90\x07:?\xf6\x9bv\xd1\xcbAJ\xec\xd0\xfb=\xfb\xf99_Zt\xe7\\;\xfc\x8e\xb9v\xd8ms\xc2\xde\xb7\xeb\xf8\xaa\x1dw\xdcq\x81\xd3\x0f\x1b6,py\xecK\xc3\x87\x0f\x0f\x88\x8fw\x88\xef=\xb1\xaf\xf3\xc9)\xfe(Va\x87\xc9\xc2\t\xd8hqI\x10E\x85>\x93\x88YK\xc4\x17\x05\xf5\x15R\x004\x80.\x99&\xc9\\=\x92s\xe8=\x80\x0fU\xe4\xc0h\x87\xc1\x85\xc3\x1b\xf0\xe0\xe6\x88\xf1\x18\\\x96\x88\xc5v\x90\x15\xee\x0cRa\xa8C\x14\xe7=\x1cw\xf9\xce\xe3\xadM\xfd\x98\x98\xb3\xbf\x16#|6F\xde9\x8d\x08\x1e#0\x04\x04\x8e\x0eB\x83\xa0py\xde\x8b\x93/\xd9\xa3! ?\xd7\x07q\xe1\xe2|\xcfw\x9c\xcbu\x96\x89\xa5\x83e\xbbL\x8b\xef3% {\x9b\xfaw\x03\x11A\x1c\\\xb1>\xbew\xa7\x8911`\x1eS\xc3\x01Q\xe1|T\n\x88P\xe3\xb5\x90\x0e&\xc5\xd2\xc1\x0b\x81x!\xb1\x00(<+\xaa\x0b\x01F\xac\x8b\x0f(\xf2\x84SUr\x15$\xc5:\x8bP(\xc2Mm\xd9\xbdkQ\xdcP\xc8)\xc3\xa1\xc4p}\xef\x11\xccK\x8f\xbeD\x98*\xe0\n)\x93\xc8\xe9[\xd3\xe5:\x92e\xb6|\xf9\xad\xa4\x913)}x\x82\xe4\x89\x92\xaf\xd0\xeb\t\x97ObR\x14\xa98\xbeR\x94\x91HY_`\x0edEM\\#z>\n\x08\x0c2C\x1c \x04\xe8\xfd\x8d\xa2\xfb\xdc\xa0\xbf\xa3\x16 \xd2a\xc0\x83 p=D\xf2\xa5\xc3y\xff\t\x04\x03\xc3\xe0\xd2\xdd?\x8eE\xff/\xe2{\xcd\x0e\x84\x02\xc4E\xdc\x03\x99Ar\xec\x06+Gcl\xd5.\xef\xc4\xc7x[5\xd6\x11\x11\x13A|\x08@\xbb\xe8\xad\xf8\xbb\xf1\xb6z\x8f\xc9\xb6F\x9fi\xd6\xa1\xd7{\xb6J\xfd\xd8\xf8\x7f\xe3\xbe\xd5\x1b\xbf\x08\xaa\x04*E\xa3\x9d\xe0E\xdb*Vi\x0e\x89z\x07\xe3$\x06$\x9e\x1b\x03\'FC\x05\x14a@\x82(\xa8\xfb\r{\xafR\xd8>\xbc\xd8\xbb\xbaT<\xc5\x8b\xe7\x1e\xd9}\r{\x19\xec|a\xd5d^\x82\xb73%\x91\xd3\xa7\x0f\xe7*"\xe2S\x8bs\x95ko\n\xac\x16\xb3qy\xe2\x05\xbeI\xdf\x07VewB\xe5d\xfd\xd1\xe91\xea\xb2O\x18\x84\xd7?\xfe\x0b\xdbd\xf0L\xfb\xc3e\x19\xeb6j\x9e\xed{}\xc6v\x8c\xdf\xb3\xd70\x1e\xe2\xfbQ\x03\xb0EA\x00\xd8\'\xc1/{\x08\x11\x808H\xc5\xfd\x9f\x84\x96d\xb5U\xbf\xb0\xfe\xa1\xf8\x1dD\x87Z\x01\x04L\\E\x12y\xa0J\x17\xaf\x9c\xa1\x04\x1co\xe0S\xb1G\x08\x93\x92g\x00T\xc5L\xb3\x10,\xae"\xfa\xb4@\x00\xb6*\xa6\xb0P\x1c\x8a\xa2\xe3{^1\x9e\xa0\x8b!\x05\x80\xa4 \xbd\xb8\xeb\xf21B\xf2\n\xf2/\xd5}F@vl\x04\xabD\xcf\xd9\xc6\xd1]\xc1\x00\xd7\xa8\xc3\xbdb\x9bDw\xda\x1f\xa2\xa1A\x92h\x1f=\x11\x10\x1f_?\x92\xc2\xb2]\xa7\x06}\x1fB\x01\xf7o4\xf2e\x03\xb7\xc6 \xc8=\xe0\xf2\xed\xa2\xb11"\xbfe\xabt\x1ag\xabu}7 8\xc8\xbdF\x9f\x0f\xac}\xcf\xa9\xb6z\xf7\x89\xb6f\xdf\x8fm\x9d\xfe\xd3m\xbd\x01\x9f\xdb\x061\xe0l8pF\x00\xa0\xb5\x8e\xfe4\x9c\xbbj\'\x88\xc5\xb8pM\x88\n\xf7Z\xaa\xfb\xa7a\xde\x1bD\xf7\xc4s<\'\x10;"\xcd\x94\x8b\xc0:I*\xf0\xcd/\xc4\xf9\xd5\xb4D\xc95>\x02/W\xb4\x9d\x8f\xb8\x93\x01O\xd5\x93\x92\xba\x7f\x92\x19\x89\x00\xe4\xe2\xc8\xf98\xb4\x97p\xab\x1da\xea%\x93\\\xd7\x95\xc7B=,\xa4\xefC8\x81S\xcfl@`\x0c\xc7Hf\xec\xcb\x8f\xba}\x1a$=\x887\x06@\xf6t\x8b3g\xd9\x8fOi\x08\x04\x01\x86\x80\xb7\x07\xf8Dz\xe3\x1a\xec\x11\xd7\xf6\x89fji&\x0fL\xce\xc9\xab\xa9`R\x8c\xf2\x99uJ\xc0\xc0X\xc1\xc5\xb9\x18\x88\x0fU\xa9J\xc7\x91\x12\x16[\xee\x12\x05%1\x07y&@~\xb8>\\\x1c$Wa\x04\x90X\x1c\xcd\x87\xc8"\x16A\x1d9\x00vB,\x11\xa7\xa0\xa6|Gh-\x86\x14\xac\xaa\xb8\xd1\x16\x8f\xc5u\x90\x1d\x0e\x0c\xc7\x85:\xf3\xcaw\xfc\x868\x86\xdbM\x07\x19Y\x7f\x8e\x8e\tb\xb6\x0e\xf2\xb3\x15\xc4\x83\xb1\x10#\x0e\xd2\x02\x06:8=R\xc4\x12\xf1\xb1d8f\x06\xae\xbdr\xc711\xc2O\x08\x08\xde\xde!}\x87\xf8X\xeb\xe8\xcfl\xddc?\xb7\xf5b\x11q\xfd\x01_\xd8ONm\x08@\x82\xb5\x98c\xab!_\xdaOO\x9fe\x1b\r\x9a\x11~\x878\xacy\xd4G\x81\x88\xa0B4\xaa\x05\xf3\x83\xe4\x81:\xd3h\xe4|\xc8\xbat\xe9\x12\x88\x00\xc6$\xd6\x0b\x15\t\xae/\xe3\x9f*\xe3\xfa,9\x89\xfa\n\xb4I\x06\xd4\xe4\xb2\x90\x0b\xf9\x95\x80T\n\'\xcd\xa7\xc2&U\xd9Z\x86\x93\xfb\xca\xd4\x85\xdc\xdarC+\x81\x0e\xd5\x08\x86\x05s\x02aa8X\xea\x81;`\x025\x11F\x83\xe8\x0f\\!a"\t@\x0cd\xe8E\xf2k\xdfsJ\x90\x04\x82\x8d\x0f\x01A+\x04\xc2=6d\x8a\xee\x1c\r\t\xf1\xfe05\x8c\xf2\xec\x13\x04\x9a{\xfa\x06\xbc\xf2|\x88h\x15\x9d8H\xae\xc4\x04\x1dI+?\x17b\x13\x01\x00\x10\x0f\xc3 \x16\xdc)\xdfv\xe7\xad\xd6\xf0.\x1f\x80E\x86\x14\x80\x8fE\xe5\x811r\xc0\xf91\xa8`\xe8\xc3r\r\xd7\x87\xb2\xc2\xc5X`\xb89\x1c\x9e\x80\t\x10\x9c\xc8)\x90Z\t:,$\xdf\x11r\t5\xc6j\xda\xa7O\x9f\xc0\xc9\x95`\x83e\x16\xb7\x1eH\x8f\x0b\x06\x97\x1c\xc18\xc1m\xf7\xed\xa6q4F\xf0M\x0cF5\x8c:{D\xa7\xdaN\xd1\xd9\xb1Dp~\x08\xd7\xfc]ta\xe0\xb2\x18\xfe@~\xa8:\x06\x1e\xb89\x94\x1d\xa3\xcfj\xdd\xde\r\xc1\x1f \xeeF\'\xcc\xb0\x8dO\x9c\x19\x0e\x8c? \xf4\xaeWd\xec\x80\x1b3!0\xa4\xef\xfd\xf3\xad\xf7\xbd\xf3\xad\xfb\xdd\xf3\xec\xa0\x9b3\xf6\xa7\x1b2\xf6\xc7k3\xb6\xf3\xe5\x8dD\x80\x00\x12\xae\x1f\xbc\x011\x00\xad\xd8q| 8<\x03\xdc\x05\x8e\x83\xf4\x027!Z\x90\xf5\x92\xc5\x1f\xc4\xc7\xd0\x07\x10\'\x11^a\xad\xb9\n{\xf8\xe89\x9f\x9a\xcb\xa1n\xcaJ\r_PJ\x96\x973T\x16\x1e\x18U\xc5\'\x10_\x05``F\x10Y`\x148\x839\x10\xb1\t\x01\xc0\xd5\x8ca\x16\x89l\xc3\xe8\xee\xefR{\x81e\x12\xc90Z\x03\xe72p{B\x8dWA^9!}\xd9\x93O\t@J\x00R\x02\xd0\xb4\xb1@\x13\x00\x15\xc9P\xca\xac6PUd\xa5G \xb6q\x0e\xee\x0cD\x1b\xdc\x0c\xc5\x8a\x84\x94;d\x97P\xd8\xa7\x08\x13"\xa8/\xb5\x85\x8f\x13\x02 \xdf=\x08\xcf\xe2\xe2"a\xe1@rb\xe0An\x10\x1d_+)\xb3,,\x1b@X,\xe1\xbc\x7f\x8cN\x0c\xf1\xf2\x10\x00\xd4\x02\xd4\x00\xfe\x07R\xac\x1e=\x15\x10\x9dp]\x88@\xbbo\xfd\xea\x10\x85F\xbdmf\xf0\xab\xff"\xba*\\\x9b\x8d\xfbg\xd4)\xa4\xea\xea@\x1d@E ?\x80x\x02\xf4{\x10\x7f\x93\x18\xb9\x11\xef\x11\xf5q\xd9\xa1\xd3\xa3\xb7\xa3\xbfK\xa7\'\xfe\x1b\xd1~\xff\x18\xf1\x07=\x9e\xb53\x9e\xcb\xda\x85of\xed\x9aIY\xbbl\\\xd6\xce\x7f#kG\xde7?\x04\x8a\xec~U&\x9c\xbf\xd9\xc9\r\x81\x88\xe0F\xc2\x9e\x00\xe2+0\x08=\x93\xe7Y\xb1\xe3\xab!W\x809\xf3\xac\x10P\xf6\x14\xbb\nk,q?W\xa2\x8e\x92hr!|R%\xe0\\\x82\x7f\xe4\xe6\xabu~Hs\x0f\xb9\xa8yf\x05\xa5)&E\x15|AT\x10\x96\xd8\x12\x18\x11\xb0\x87z\xb8Yt\xbb\xad\x19=\x12\xab\x80\xa3\x83\xc8\xbfZ\xf4tH>\x03&\x81_TV\x10\x1f\xe2\xc15\x80u\xd4]\xde\xe3F$\xdeE\xb5$!:\x10\x81RK\xf6}g\xd0\xd0f\xf8\xa0\x05qy_D2Y\xca[1\xf5\x1c\x18\x03\xab\xd5\xa1\x97\xa1h?\x80\xc7\xe7\x7f\xc3\x91\xd0I\xbd\xdf\x1fC\x15\x8b\x8b\x01\x8f\x05\x83\x00p\xc0\xf5Yp8>q\xfd\xe4\xcf\xef\x1d#:\x8b{p\xd4\xfb{\x86;t\xe0#b\xe2\xa0\xc88\x19\x12\xa1\xd8\xbf\x8e.\t\x9c\x1d\xff<\xfa3~x\xfc\xf3\x04\xd5\xe0\xb7_\xa6\xeb\xd4x#\xef\x08D\x06$\x82\xf8`?\xc0\xaf\x8ed\x01\x91\xe1=\x86\x1e\x8c:pc\x0cs\xe2\xea\xf2\xdb\x83\xf8?;c\x96\xfd\xfa\xfc\xd9\xf6\x9b\x0bf\xdb\xef.j\xd4\xe9\x89\xfe"\x0e\xfc\x9c\x97\xb2vw\xc3<\x1b\xd9\xf0\x95\rkx\xc0\x8el\x18d\xe74\xc4\xdc\xa5\xe1\xe3@\x14\xfe5b\xae\xed\xf4\xefL \x1e\xd8\r\x90*\x08\x12\x82\xe0@\xbc\x14qH\xb4!\x80vPtT\x90\x80\xe0H\xcc\x1b.%}_\x052}\xbd@\x05\xf2(f^1\x19\xbe\xbe\x9d`E\xba\xfe\xc2\xd8E\xca\x0fpF\xb1\xfd\x18F\x91\x88A~\xe5\x9a(\xea\x14\xe33\\\x1c&\x04\xfc\xed\x1f\x1d\x17\x90\x1f\x9d\x1e\xc6\x80dH\xc0\x18\x92"\xf6(\xe0\x16\xf8\x83\xe3\x03\xdf\\\x8bC0\xcf\xc1u\x15\xb1\xc9+L\x11\xe2\x03\x11.y\xf2\xc9\xae\xac>\xe1\xc2\xd7_\xf7\xc5\x0c\xf8\x8d\xf3\xe0\xfc*{\x05\xc7`!\xaa\xb5\xa8*6\xa1\x96YJ\xf6Q\xb1\x04\xee\xcb\x82(\xe2\x0f\x84\xc5\xf8\x07\xf2\xa2\x06\x80\xc8p5(-\x9c\x1dc\x0b\x0bO\xd4\x1b\x8b\xfc\xf3\xe8\xea\x90\xa1\x07\xb5\x85\x00 \x9e\x93R\x89\'\x00\x82\xa1\xff\xc0\xcd\xa1\xcc\x88fpoBx\x1b\x03i\xfec;F\xe7\x86\xcd\xc2\xa8\xe8-\xb2\xde\x18\xa9\x03\xa2\x80\xf5\x9f\xa4 \x0cpx\x14\xda\xf7\x9a\x1aDt\xb8>\xaf\x8a\xfc\x82(\xc0\xc9\x0f\xb9u\x8e\x1d\x15s\xf6a\xaff\xed\x8eO\xb3\xd6\xaf\xe1\x14k{M[[\xf9\xa6\x95\xad\xcd5ml\x93\xfb7\xb1\x83?:\xd8\xcej\xb8=D\x8a\xedve&\xfcw\x9dc\xa7\xc7@5:p\xfd`\xed\xef5\xf7[I\xa5!\x04\'i\xde \xbe\x00\x0c \x02p}q\x90d\xb6^\xb2hF2[N\xd9o\xc9B\x9e\x0b\x13\xb7\xd7\x00W|%i%\x9f\x81\x84\x88\xfd\nAW\xc0\x0f\\\x1c\xe6\x04c@\xfa\x84!!\x15\xee\x12\x9d\x19\n\xbf\xa0.\xb2/0\x0b\xe0\x16\xc6\xc3\xff|\xfa6\\^\x01>:\xd8;\xae\x8d\x04\xcc9\xa5\x94\xec\xfbn\xa4\x04 %\x00)\x01\xa8l,\x14\x04\x80\xe1\xe3\x01\x14\x13\xa0vX|\xcf{\x85\xde\xaa(\x02\x9b/\x83!@\xa3"!|.t\x9fRT\x04\xcd\xc7\x1b\x00Q\x03\x98\x83D,\xee\xc5"\x83l\xbe-\x12\x0b\x81(\xcb!\xdf>H\xaf\xb2Z\xbc\xf2\x19\xe4\x07\x11p\x81\xad\x19=\x1a\x08\x00D\xe1\x9fQ\xe7\xa0\x0fo\x1b]\xda\xe8~\t\xae\x97\xb7\x82\xbb\x0c\xb7\x0c\x86\x9a\xe5\xbaL\x8a\xbf\x1fk\xbf\x8f\xce\x0b6\x07\x88\x1f\xe2\x9e\x0f\x91E\xf7\x93X\x86\x08\x88\xb8\xc6\x9c (\xfc\x8f\xeb\x12>\x8c\x1d\x01\xa3\x1fn\xbau\xbeu\xf7\xa1\n\xa0\xbf\xff\xe3\xf69v\xda\xb3Y\xbb\xe9\x83\xac\xf5j8\xd6\xda^\xdb\xd6\xd6\x18\xb9\x86\xfdf\xdc\xf6\xb6\xfbG{\xd8O\x9f\xdb\xd2V\xbbm5\xdb\xf8\xde\x8d\xadgC\xff`\xfc\xfb\xc5\xd0\xaf\x02\x11!l\xb81\xee\x7fv\x8c\xfc\x99\x90c\xc0sP8\x12\x82\x86j\x84x\xc9\xda\xb1\x8e\xcc\x1b\xb1_\x05A\n\x15\xd4\xf4\x053<\xe2\xcbNTM5\xb05\x0f\x9e\x95\xe7\x16\xe2\x03\x930%\x90\x90\xbdW\xe7\x1e`S\xea)\x8c\xc2\xd7\x8d\xd0![\x14{\x03\x03R)7\x8f\xf8\\[e\xea}:6\xcc\x86\xff\xb3\xa7\xdc\x0f\x97cY\xe1\xf9)\x01H\t@J\x00\xca\x1f\x0b$\x01\xc8\xd7\x943\xa9\x02$S(\x15\x81\'\xa3\xa0\\:\x88\xe8X\x8ayx&\xc7\x81\x91(\x9f\xd1\xa7T\xe0PAO_lA\x11UR\x01\xe4\x02T\xf4\x1f\xc8\x8f\x91\x05/\x00\x15QX\xd8\x03\xa3~\x01\xe19\x8e\x88\x0f\x8ax\x10\xa1\x87\xc1\x8fd\n\xc4z2\xe2(\xa8\xf8\x9b\xe8\x92\xa0\x06\x90\x14\x83U\x16\xb1\x9f\xe4\x1e\n\x80 \x9e\xb1Al\x9e<\x0b\xa8\x19l\xae\xcan\xfbTY%a\xc8`\xa3 %\xe6\xc7F\x1d\x1e\xab\x1a\x14\x05\xc5\x8b\x801\x11q}\xb5\xae\xef\x84\x08\xbf\rO\x98\x11*\xbf\\<&k\xc76\x9cm\xab\xdc\xbc\x8am\xfe\xec\x16\xb6\xfb\xc7{\xda\xae\xd3v\xb7\xed\'\xech\xdb\x8e\xd9.\x10\x83-\x1f\xdf\xd2nh\x98bC_\xce\x06\x03"\xff\xe7z\x88\xfdd\x15b\xb8$ \tu\x06\x15\x04\x82\xc5Z\x01(\xcc\x8f\xfdB\x8d\x03\x88\x93b\x7f\x12\xe1s%\xfa(\x91GE_[s\x03\xd2j\x0c\xe0Ri\xe8*\xe0\x89\xd1Tb\xbf\xac\xfe\x88\xff0\x06\xf6\x1d\x15\x0b\x15\x10\xb8Td)\x04\x005\x005\x0c\xe4\x85Q\xe1\x96\xe67\x15\xf0\xe4\x7f\xc0\x96\xaf\xd8\xa4\xc2+\xfe=\xbf\xcb+\xc0\x9eB\x8c\xc0\x93\x9c8\x98\x12\x80\x94\x00\xa4\x04\xa0\xf2\xb1\xc0\x13\x80b\xfd\xdf\x92m\x9c\x10\xeb\xe4\xbbU>\x80/\xf4\xa0:\x01\x00\x02\xc0\x04\x90\xf3\x1b\x8b\x94\xeb\xfa\xa5\x02\x88\x08\x00\xf7\x86\x00\xf9\x00%\x80\x95\x05\x00\xc9\x00d\x1e\\.\x16\xc4~\xb9\xfdh\x91\x84x\x8f\xfb\x8d\x80\x1e\x10\x1f1\x9f\xc0\x1c\x02/p\xbf\x10\xc2\xbbb\xc7\xd7c$\xbf&\xd4\xfc\xeb\x10=\x11\x08\x02>Y\nxl\x1e\xdd\x1c\x08\x07\xd7\xe6\xd9\x94W\xed\xcb\x8a%\x0f\x90\x1e`\x10\x11\xc0H\xc9\x7f\x11\xb7y\xcf|1\x06A\x04\x08\xf8\xc0\xa0\x88o\x9e\x10`\xf2\xfbAd\\y\xc75\x0c\xb5\xb5F\xade\x076\xfc\xc5N\xc8\xf4\xb7+\xe6\x9co\x833\x03l\xc7\xc9;\xdb\xa6O\xfc\xd8\xda\\\xdd\xc6Nj\xb8\xc2\xcez!\x1b\x0c\x86m\xeb\xc7\x07\xb1\xff\x87\xbd\xbe\n\xa1\xc6\x10.\x9e\x17c(\xeb\xc3\xbcY7\x10^\xfe}_\xb2+\x9f\xe8\x9f\xab.\x9e:D/*\xe2>\x03\x98\x04/\x14\x8b\xa2\x10_\xb5\xe9f\xdfu\xc8\xef\xcf\xba\xa3j\x81\xfc\x88\xf5\x84\x99\xc3\x00 \x00*\xf5\x0e\x11\x80\xb1\xa8|\x97\xea0@\xa8!\x00\xc0\x91\xea.\xb0w\xea/\xc9\xbdU\x88\x05\x17\xbc\xd2\x80Q\x91\xd9[p\xe6\x7f\xf0M\x9d^\n=\xa4\x8a.\x80\xd8 \xb2\xa8;\x1b\xaex|Y{\x85\xa8 )\x8b\x02\x11`\xc2X\x92\xab\xb1\xd8\xb2\x01p_I\x00<0\xf7Q\xfc\xbfb\xab\xd5\'\r+?\x91mtM\xa1"/\x8b\x0c\x95\xe57\x0e\xac\xb0\xbcRL\x01*\xcc\xef\xe8\xfd|\xc75\x08\x02B\x0f\xe3?\x10\x136M\xf1\xd6,\xb0\x90\x88\x85f3\xb4!\xccI:\xa0\xcf\xf9\x06\xe9\xe1\x06j\xf4\te\x07\x08\xb07 a\x10MH\xf2\x07Q\x80\x04\x04\xedyM\xc6\xaex\'kk\xdc\xb5F\xd0\xf3\x7f?q\'\xdb#\xe6\xfe\x074\xeco;M\xdd\xc5V\xbduU;h\xdaAv\xf5\xc4\xacu\xbcsn\x88\x19\xc0\xeaO$"\xf1\x08\xebE\xf7\x05[\x07q\x10\x00\x1f\xeb\x04p\x00@\x85\xb8|\x92\xd3\xfb\xe0\x1e\xdfL\x14\x98\x00\x16\x9a\xb3\xc2sk\x18\xc0!k\xc4\x1a\xb2\x96\xe2\xfc\x9e\xf8\xfb\xba\x92 ?\xc8\x0cl\xc2@\xf0\x12q\xc0\xe1\xe1\xf6 \xbe\x0eb1\x803U\xf5QW\'\x05\xf8\xb0\x7f\xbe\xaa\xb2Z\xc2\xf3Y\x15\x98T\x9c\x95\xf9\xb1\x7fH\xe7\xff\x83\xeb\xa5\x10\x00\x05\x02\xc9\x15\xa8j\xa6^$\xd7\xe1\xff+\t\x01\xa0\xf3\x15\x83+\x19\xccC\xc6H\x1fp\xa2\xc5\xc7\xe0\x86h\rgU\xf3\r\x16\x12N\x0f\x07\xc7\xc0FA\x8e\x83\xa2\xbe\xdf!6\x14\x18*\x8b\xe8\xae\x86\x9bPY\xe6\x8bh\x86\n\xc1\xc6\xb1\x81pk6\x017\x0cD\x06\xa4\x86\n\xb3\xf8PW!\x90\xd4"6A\x01 H\x07l\x18\xd2\x02\xae\x19\xae)1\x8e9s\x1fD\xbd\xbfG]c\xb5\xe3\xe2@\x04(\xd2\xb1\xc6Q\x1f\x86\x00\x9e\xb3_\xca\x06W\xdf>_\xecc\xbf\x1b\xbf\x83\xfd\xe4\xe9\x9f\xda\x06\x0fo\x14\xa4\x81\xf6w\xb4\xb7\xc1\r\x97\xd9\xf0\xd7\xb2\xb6\xc7\xd5\x99\x10@\x04\xe1 \xc8\x87\xe6\x10q\xaa\x1c\xa4\xfai\xfe\x7f\xdc\x8cs\x998\x1cQ\x9e\x82r\x17[\x04\x08\x80MVO\xe5\xe1%\x01\xb08 \'\xc8\x0c\x85E\xec\'\xe4\x16D\xd8(\xba+ \x02\xe2\xaf\x90X\x9c\xd8w\xf2\xe1\x90\x9f\x96W\xd5\xca\x07qT\xf6J\xdc\xd3\xd7\xccWy+\xa9%l\x8c\xdat+>A\x80\xa0\xaa\xbb\xbc\x02\x14Pz\xfc\xbcl\x84\x0bi\x03\x11\x11\x8e\xc0\x1cs\xed\xbb<-\xec\xad\xe2=\xc4\xf9\xbd\x84\xa0\xec\xb2|\xf6\xa3\x85}\xf0\xcc\x10P\x10\x8d}\x96\xc5_\xd5\xa6U7\x91C\xd5\xa8TnN\x15\xa9x//\x15\xfb\x0f\xb2\xc3t \x040\x03\xa45\x88\x05\xbf\x8b\xfb\xfbNN\xe0\x93\x8f\xcf\xe0\x15\x86\x03\x11\x02\x1f$\xa1\t7\xd5\x85\xeb\x7f\x1e\xa6\x94\x96\xcfR\x03$\x82\xab\xd4\xb1\x90\xde\x17=\xcc%\x02\xaa\\\x18\x14\n\x00*g\xb1}\xfa\xaf\nG\xaa\xf0\xa7z\xad\x8b\x02\x83h\x92\x02@,\xa8(z>\xe2/\xf5\xd5p\xbb\xb0\x11\xde`\xe2\x0f8\xbaJ\'\xb1\x91\x1cl,\x0b\xcb\xa2J\xb4\x92MCi\xab:\x98\'\x88\x03\xb2\xf0\x1f\xf5K\x80\x10p_\xa4\x08\x85k\n\xf9\x11\xfd\xc8-P\xbbfj\x03b\x00\xc4}7\xf2\xf3\xacm\xfb\xd6v\xb6\xd6\xddk\xd9\xaa7\xaej#>\xcb\xda\xa8\x19Y\x1b\xd10\xd3\x864\x8c\xb0\xae\r\xbd\x83Kp\xfb\x8b3\x81``3@\xd2!\x9e\x9f\xb0f\xb9\x8d\xd4\xce\\)\xa1\xc5\xc4t\xf6\x13\xd1>i\xe4SC\xd7$\xe1X\x94\xc4~\xf6\x995`]TwR\xc8\xaf~\x12\xde\xe0\x0b\xd2\xaa\xf4\x9c\xda\xb6\xeb\xe0{\x10\x1c\xc4\x076Q\r@z\xde\xf3\n|@0|\x13Y\x18\x14p\x05>y\xdb\x9b\x0c\xef\xfc\xce~\x03\xa7\x10\xea\xa2\x0f\x94\x12\x80\x94\x00$GJ\x00\xf2\x8f\x94\x00\xa4\x04 %\x00)\x01Xx\x08@\xb1!\x17\xa0\xafq.W\x9c\xd7-\nY\x80U\x9f\x1f$\x13\x11(\xd5\x16 \x02 \xb7\x9f\xf2\x10|\x87VE^\xc9\x0e\xc0b\x81p\x18\xf8\xb0\xdec\xdd\xc7\xc5\x82\xa5\x9dE\xe5\x1c\xb9GdI\xf5\xbe[\x0e!.\xbf3\x7f\xe5\xbf\xcb\xe5\xc5\x1a\x08\x11\x92\x19\x93\xde5\xa9\x0cE\x90\x11}\x1c\xe3#\x9e\x06J>\xd1\x91\x87x\x02\xe2\xbe\xe90\x84\x0b\x90\xfc|b\xff\xaf\x9f\x9a\xb5]\xc7\xecj\xeb=\xb0\xbem\xff\xca\xf6v\xfb\'Y\x1b\xf2b\xd6\x86\xbf\x9e\xb5K\xc7\xfe\xffA\r\x00\xaa\xff\xaa\x7f \xe9\xc7\xf8\x90\x01,\xe5\xef\xcb\n\\\xcc\xe0\xab\xa1hO\x95\x8fRk7\xff\xcc\x8b\xeaP\x1fB\xf6\xd8\xc3\xb5\xaf\xbc+\xdf?\xfb\x8eA\x10[\x130\t"\xa3\xff+o\x1f\x84\x07FI\x05\x86Y\xa9\'\x83:4\xc1,\xbc\x9bY\x95\x98\x80I\x08\xba\xf7\xd0\xa8\xe5\x1d0\xcdo\x95\xda\xdb\xfeg(\x07_\xb9\xdb2\x08\x8a(\xc8\x98\xe4\x91_\xe7(\xad\x98\xf7L\x8e\x07\xf0D\xa0\x94\xfb\x0b\xb1\xb4\xe8\xde\x1a\xad\x90S6\x82E\x91\x14\x00\xd7\x93{\r+*\\\x96\xc0\x1f\xb9V\xa0\xbePi\xdf\xbc\x02dU+&\x05\xf0p=\x16V\x88\xef{\xb9%\xe7\xa9R\xd4\xc9\x1e\x84\\\x87\xfb1\x0f\xdc\x8b\x18\xf9H\x17\xa6\x00Gc]\xc1\xa9!\xc0\x08\xc3\x1fe\xbf(\xf4A\x11\x8f\x8d\x1e\xdd\xc4:\xdc\xd9\xc1\xda]\xdf.H\x02\x9dF\xce\r\xa9\xbd}\xee\x9do\x03\x1f\xcdZ\xcf{\xe6\x85\x96P\xd4\xf9\xc3\xe8GR\x11\x92\x0e\xa1\xa5\x00\x1a\xf7e\xbd@\xe0J\xfc\xf3r\xfb\xca\xd2\xbf(q\xf9B\xc3\x97\xa1\x07\xd1\x92D\x00)\xc0\xb7\x99S\x13\x15\x10\x19x\x04\xb9\xb1\xfa\x13\xf7\xa1:\x130\'\x92\x80(\x15\x8f\x14\x08q\x00\xf9\x15H\x87d\xca\xf5\x81U\xb5k\x97\xdb\xdd\x1b\xc4y\x85h\x03\xa7\xc9 \xacB\xe5\xca\x0b\x0e\xdfu\xc7[\xff\x93\xf5\xd5\xfd\x7f\x84\xf4\xfa\r\x00T\x17!\xf9\xc6\xa1TL>\x1fG\xf1\xa2\xbf\xdc\x89\xbe\xeb\xaa\xb7LK\r\x90\x15\x94E\x87\xea\xc2\xedA<,\xacX\xd9Y\\\xa8-\x9b "\x00\xd23\'\xe6\xc6B\xfb\x05\x87@\xf8\x1cx\xf9\xbcY\x83\xa4\xe8/\x82\xe8\x0b\xa7 \x86q]\x00\x01\x8fDc\x90\xd1\xdb\xb6Z\x97w\x02\xd7\x0e\xb9\xf9\x9d\'\x7f[\xf0s|(\xd0\xf9\xcf;\xe6Z\xc7\x86z\xdb\xfa\x95ml\x8b\xc7\xb6\xb0k\'gC\x85\x9f\x1d.m\xac\xea\xf3\xe7\x9b2\x01\xf9i\x08A]\x00\xf2\x06\x00\x1e8\t"%\x80\xc6\x1a@|\x98\x0bsM\xf6t(uh\x8fS\xe4o\x1c\xac\x85WA\xd5MZ\x91x\x92\x02\x80+\x1f\xf9\x077\x07\xf9q\xfb\xe1\x9d\xf2n@\x0e\x89\xfe*\xba\xca\x7f\xe0\xf8j\xb0\x02\xc3\x94\xb5\x9f{\xab6\xa7\xefe\xa8(\\IyU\xdd3\x05\xfc\x00PJ\xed,v\x03\x8f LL\xa2:\x8b\x83x\x0eUTtR._\xb2\xef\x98\xca}\x85P\xbe\xc0\x84\\T\xbe\xc2\xaa\xc4nD.\x16V\xa9\xb7\xbc\x87\x08 \t\xa0o\xabA"\x9b\x84\xde\x04\xa2\xaa\xa5\x95\xba\xe1\xaa\xf4U\xb2r-\xf3\xf1\x9df\xa4\x12IRR\xa7Z\xceW\x93G\x92\x8dH\xc5\xa5\xb8\x07\xe9\xb9T\xf5A\xcf\'\xed\x17\x9d\x1f\x1d\x9e\x84\x9fQ\rsl\xef\xcf\xf7\xb6\xed\xdf\xfd\x83\x9d\xdbp\xaf\xedqU&\x14\xf3D2\xe0\x7fD\x04\xc2\xf9A~T\x05\xba\x13i=y\x06\xb9&Y\xd7\x85\xad\xcaNK\x0e\xa5\xfa\xaa\xe2\x11p(x\x83a\xf8\xd0wI\xa1\xf8\xeeaBp{\xc5\x9c\x10\x01(\xd7\x1e\x92\x9a\x02\xcaT\xd8C\x07\xd7\x93?_e\xd3\xc4\x80}H~\xb3Hg)\x01H\t\xc0\xa2>\x16i\x02\xa0\xfe\xeb*\xf6\xc8\x84\xf2\x95\xfd\xf6m\xc6\x04\x80\x9c\xab\xa8)\x80T\x81\x12Jh\x00y}\x11Q\xa9\r*\xfc\xe1\x91\xdd[\xa5\xb5\x11\x8a\x82\x02\xd9\xb8\x1eF\xb9\x9eW.>\xef\n-\xd4\xec\x04\x02 [\x07D\x874O\xb2\xbb\xf0\xef\xae\x1c\x8d\t%\xbaNk\xb8!\x14\xf3\xa4\xb8\x07E<\x11\xf9\xaf\x9as\xa1\x9d\x99\x19l\xbdgw\xb7\x9d\xdf\xdb\xd5\xd6\x7fp\x83\x90\xd1\xb7l\x97\x0fB\xc9\xeeP\xbc3&\x02+tz;d\x8c\xa1\xd2\xa0\xba\x00\x80\xac\x85\xcal\xa7.\xbb\xda\x0c\xd6W\xf0(\xe6\xe3#Q\xa5~\xa2B\x8a p(=\xd7\x13\x02\xbd\x97_\x9f\xffz\x1b\x93O\xb6R\t\xbe\x96~\xfe\xefFJ\x00R\x02\xb0(\x8eE\x96\x00\xe4\x02&Y\xf5Alo\x98\x03\xc9A\x14\xd5\x08P\xf9nDS\x10\'I\x04x`D"_,\x13\xb1\x88\x05U\xbe\xbf\x16\xdc\x87\xfd\xfaH@\x11\x00\xff\x1e\xa4P\x81\x04\xbf\t>\xeaO\xc6>\x1f)Wl-T\x14\xa5\x10\x82\xb1Y\x00\x05\x1b\x8c\xa1\x910d\x9al\xd2\x84\xa3~\xe4\\\xdb\xe9\xcd\x9dB5\x9f\xbf\xcf:\xcc\xea\xbf<\xc2\xce\xcb\x9cn7\xcf\xbb\xd2.\xcb\x0c\xb3\xbf\xce:\xc86zdc;\xf8\xc3\x83\xed\x827\xb2!\xae\x9fn=\x14)\xdd>:?\x84\x89\xe2\xc1\xc0p\x8az\xa3\xceK\xac\x93\x02\xb3R\xe4\xaf\xfe`O\x05\xc7\xde(\xed\x19\x13\xb0\xa4\xa8@`\x8b\x038\xf3\xb0(\x15A\x05<\xbc\xd1/\xd9(\xd5\x07\x9b\xb5\xe8\x83\xe7+\xf6\x00\xa0\xf9\xde|\x1e\x10}\xe1\x10\x16N\xfd\xe2d\xc9\xd4\x01\xe2\xa9l\x91\x12(\x94x\x93\xac1\xef#\x00}q\nYc\xb5\x01\xbc\xe7\xd0{\xe9V\x9c\xe7-\xfb\xbe5\x15T\xb6\x9ak\xc63A\xd4\xf0\xf5\xd2\x01\x88(?\xaa\xf2\xc2\xed\xfb\xcd\xeem\x13\xe7\\g\xdf|:\xc0l\xcc\xc1f\x13\xeb\xed\xc5\xff>l\x17d\xce\xb2\x9f\xbf\xfa+\xeb\xdfp\x96\xf5\xb8{^(\x01FY0\xba\xf5\xd0Y\x98\xdcp$%\xe5/\xc8\xd7\xaf\xa8\xcc\xd4\xdf_\x9b!\tP0\x0c<\xfb\x8eT\x92D\x93D\x80C\xf0\xa7x\x12U\xec\xf1\xcc\xc7#\xbew\xf5\xb5xi5_\x08\xb4P\xafs\x15\xe7\x14\x97\xd7\xe4yU\xe1\x10\x1fB\xec\x7fcQ%\xb6Kt\x92\x11E\xa2~\x12\xe9}9*\x11\x01\x19Nr\x89c\xa2\xb4\xbeb\x0f\x87/ZYM*\xcb\xb5!d\xc4\x1c\xc0\xfd7\x8dF\x84\xd6\xdb4\xe5<9s\xbcM\x9f9\xcc\xec\x8e\xd5\xcd\xae\xa83\x1b\x1c\x1f7,c\x9f6\x0c\xb7K2CC\xc1\x0f\xc2{\t\xeeY)\xa2\xecx6\x94\n\xffU\xd4\xd8\x8eL\xee"!?\x80\xa8\\\x8c\x14\xf9\xab;\x92\xf5\'AN\x05\xc2\xa9\xe0\x8b$\x80$\x01\xf0\x0cH\xb0\x08\x01H\xba\xf9<\xc7W\xb2\x95\xaal\xb5\xb8\xd8\xef\xd3~K\xe5.\xb2\x94*UX\x89$\x8a\\R6\xa1\xaf\xea\xab \x1f\x1d,\x1c\xe23\x8b\xc5{\xaf\xf3\'\t@\xb2J\x8d\x823\x84\xfc"& \r\xd7Q\x0f;u0\xaa\xb6\xc8\xcc:1g\xa2\xf2\x884\xdc!:\xd7\x96\xea1=\x04\xf1\x90\xb17w\xfa\xe0\x18\xe1\x7fd\xf6\xdb:\xfb\xfagu6k\xc3\x98\x00\xec\x1b\x1fS\xba\x85\xe2\x9eW5\x8c\t\xc1=$\t\x91\xd2\xbbr\xf4\x9cm\x19]\x17\xfa\x13 \xf6+)\x89u\xf2II\x8bzv^-\x86l>\xc0\xa9\x97d\x15\x05\xe8%P!\xbf\x08\x80bF\xbc4\xe0\x99\x90\x02\xe9\xfb\xa3]\xdb0!$\ta\xfc#T\x19\xc4W1\x0f\xb5f\xf3\xebZ\xcbgZ\x14\x07\xeb\x0b\xbcH\xd7O\xc2\x98\x17\xff\x85\xe0\xb9\x90\xdf\xdb\xa8\x14:.\x97\xb9\xb7\x87\x891\xb6\xf4sWu(\x030\xd9aX\xef\xf3\xfd\xc7\x1b]|\x0c\x808_\xbe\xc3\x97\x07\x93\xf5U\x84\x80\xff\xb2\xd8\xb5\xd2\x93\x01\x18\x80\x85\xfb\x11c\x80\xfeO\xc4\x1e\xba4]Id\xde%-\xe4\xf7-\xd4\xd2\x1a\r\x05F\xd2f\xe0\xbd\x05*\t\xaeE\xf6\xe1\xc7\xb2\x86+pI\x04\x81\xf7\xb5\x9c/\x12\x07A?Tx\xa5\xb0#\x8d=\xe9\xecK\x86\x1f-\xbb>k8\xcf\xec\x80:\xfbl\xad:{8\xe6\xf0 =\x12\xc0\xfc\xd7_\xb6\x8f^x\xccn\x1a\xd4\xcf\xdeX\xa6\xce^\xf9\xe6~;+s\x92m\xf0\xd0\x86\xa1\xec\xf7z\x03>\x0f\xd7R\xc0\x8f\xdaw\xd5\xf2Y\x16\xe5\x91+\xcb\x95\xc3\x13\x83$G\x97\x87\xca\x97\xe5\xf6\x9d\x94\xfc9|\xa7\xe21)\xe7\xcf1<\xe2\'\x0f_VLTZ\x81F\xe2\x8c\xa2\xa8:_\xee\xc5Z-\xb6\xea\xc2\x13\x96KR\x0eA?K\xf4\x9ca\xabt\x1a\x17R}\xff\xf9\xf9?m\x97\x0fv3\x9b\xda\xddl\x97:\xbb;F\xfe\x13\x0e\xfc\xa3}\xf5\xf6\xeb6\xf5\xd1\xbb\xed\xfa\xe3\x8f\xb4K\x97Y:\xb8\x04\xe7L?\xc9\xf6\x9b\xbc_(\xfc\xb9\xeb\x15\x19\xeb\xd0\xeb=k[?:T+\x02\xe0\x00\xc44\xd0\xa7\xfaC\xe5\xbd|\t78tR\xcdL\x86\xa3{\x02\xe0\xad\xfb^\xf4\xd7\xb9\xdem\x9b\xb7-W:R\x02\x90\x12\x80\xe6\x1f)\x01hE\xc3#z!Wak\x19\xaa\x0bG!\x13\xea\xba\xd3\xcd\x97z}\x7f\xbbm\x8e]3)k?\x7fm\xdb\x10\xf7o\xcf\xedb\xff\xdd\xaa\xce:\xef\xb0\x8d}\xfc\xd2\x13\xf6\xc9+O\x05\x83\xdf\x85+,gve\x9d\xed\xf6\xd1\x1e\xa1\xf9G\x08\xff\xed\xfc\x9e\xad\x19=\x1a\x8a\x87\x90\x15I^\x84\xd2A[\xfay\x17\xb6\xa1z\x93\nC\x07\xe9\x11\xdf}X\xba\x0f6KvF\x96h\xaf\x92]\xfe|!\xbfj\xf9)\xe4<\x15\xfd\xf3\x0cq\xf6d\r\xc1\x96\x9eW\xa1\xc1\x06c\xfc#Jo\xd5\xe8YkS?\xc1\x0e\xbfcnh\xe0\xf1\xb3\xe7\xb7\xb2/\xbf8=\xf8\xfdG\xc5\x9c\xff\xca\xa3\xbb\xdb\xe8\x11\xd7\xdb\x8c\xf1\xaf\xd9I\x87\xfc\xc9\xee\xc5\xda\xff\xe7:{\xe4\x9bQ\x81X\xd0\xd4\x83<\xff\r\xa2{\x021Q\x1b/\x00\xadP\x92\xcf\x82B,[\xdb`M}e\x9f\\\xd1\xa5\xfe\xc8\x95\x89\x9a\xab>\xa5\xaf\x13 \xc2\xb0\xd0Ue\xae\x05\xd0\xc9\xed\xa7\xa8\xa8\xd6.&1?\\\x8dX\xfei\xbfE\xb9\xae_\x9e\xfb\x95\xed2f\x17\xbb03\xc4\xee\x9a\x7f\x93\xd9\xc1u6\xac]\x1b{\xff\x89\xfb\xec\xb3\xc1\x83\xed\xeb\x07\x1f\xb4/\xc7\xbfaw\x81\xfc\xf5u!0h\xed\x11k\xdb:\xfd\xa7\x87n@\x84\xfc\x92\xed\x87\x1b\x11\xce\x0f\xf2\x17*:"5\'M\x04*\x7f \xee\'9u\xb1\x8cSq\x7f_\x11\x1a"\xe2]\x86\xfez|N]}%\x0e\xb9\xfdt\xb4\xd6E\x83\xe8!&\xca\xe7O\xc5\xe1\xe5\xbaL\xb4\x0e}>\xb0\xfdn\xc8\xd8J\xd7\xafd\xc33g\x9a=\xbb\xb3E\xbf\xdb\xda>x\xea\x81\xe0\xf6\x9b3\xe6U{\xee\xea\x8bl\xc0\x01{\x98\x1dUgo\xcf\xbb\xd5:\x8c\xe8\x10Z\x7f\xe11\xc0m\xa8\x12\xe5\x10\x15|\xca\x00P\xbeuPN\xba"\xcaZ;\xc1lm\x03\x04\xf5)\xe2\xc9\x98~\xef\xda\xf3\xc8\x9f\xab/\x84\n\xe2x\x0f\x81\xda\xc7-4\\\xbf\xd6C\xfa\xbf\xc2[[z>\xb9\x86\xd2\xa3\xd9d\x80G\x8dM\x96\xef<5\xf8\xfcO{&k\xab\xdf\xb1z(\xeaa\xfb\xd7\xd9\xe7c_\xb6OO8\xc1\xe6\x8c~\xd5\xae9\xb6\x97\xf5\xdaj3\x9b\xd4\xae\xce\x9e\xfe\xcf\x83\xb6\xf2M+\xdb\xef.j\xf4\xf3\xd3*\x8c\x96e\x18\x12)\x89\x86\xe8\x0f0\x01@\xf9\xe6\xe2\xf3\xc8!\x02\xa9_\xb9\xf4\xa1tm\xe2+(\xc7\xa5\xfc\xfcd\xda\xae\xd2y\xbd\xce\x0f\xd1UM\x00o+\xf0j\x02\xe7\xf1[\xa5\xf3\x13.T\xf3\x99\xd3Q\x85!\x02\x00P\x008\x94\xfa\xc2\xf2\xbf\xe1\xc0\x19v\xf5\xc4\xac\xfdf\xdc\xf6f\x1f\x1fc\xb6g\x9d\x9d\xb8\xde\x1a\xf6\xd9\x1b\xcfY\xe6\xad\xd7\xec\x8eS\x07X\xc7\xed~n\x13W\x8cE\xff\xf3\xebl\xe3\xc76\r\xdd|\x89\x12\xa4\x99\'\x8dJ\xe1\xfeT*\xa6\xd8\x07A?\x85\x0c\x7fR?\x94W\x0e\x17J%\x80\xe2\x83\xfd\x83k\xfb.\xd2\xa8ZBv\xe5\x8e(\x7fD\xc8\xefm\x04\xbe$\x98\xf7\x0ex{\x80\x9a\xe5\x94;?_w2M\xf2j\xe2\xf0\x12E\xb58\xa3j \x0204u\xa0\xc5\xf7\xba\xd1\x03\x8d\xd5{\x1e\xdf\xcc\xa6d\xae\xb2\xf9\x9b\xd7\xd9\xd1\xfb\xeclcG\xdeh\x1f\xbf\xfc\xa4\xfd\xfb\xa8.A\x15xe\xa9\x18\xf9O"\xd1\xe7f\xeb\xff`\xd6~}\xfel[\xe3\xc8\x0fC\xba/\xcdB\x08"\x82\xa8\x00p\x00i\xa1lI$\x03\xd4\x0f\x15\x02\x05\xe8R`)>P-Y/\xd5\x87T\xbd>\xcf\xf5=\xf2\xfb\xf2\xf0p|\\\x85\xb8\xf2\xbc\xb5_z\xbf\xba\xf1\xe6\xab)\xe9\xdd\xd8Ix\xf4Q\xafI;X\xba\xaf\x15\x8e\x94\x00\xa4#9R\x02P\x83\xd1\x92zgs\xbbV\xd0\xb5\xd9htG\x8as\xec\x11\x9dj\xcbv\xfd0$\xee\xd8G\xfd\xcc\xfePgW.\xb1\xb8]\xdd\xbf\xa7\xdd=d\xb0]\xd8\xe3\x08\xeb\xff\xa7]\xed\xbe\xba\xc6\x02 \xf6~o\xfb\xcd\xd8\xed\xed\xa4\'\x1b\xdd~m:\xbdk\xcbu\x9e\x10\x08\x00m\xcb \x002\x1e\xe5\x9b\x03@\xc9y\xeaaX\xccX\x98\x8e\xff\x1f\xac\xab\xfa4\xaal\xbc\x8a\xc5\xf8\xa2\xb1\xbe%\x97\x02x\xd8{\x15\xbf\xcde\xf1W\xb2Y\xae}\xf0\xb6-\xc5\x02\xa8\xc8\x8eo(\xab\xa06\x95xk\xf5{\xaa\xe4\x89J\xf4\xcf\xa6"\xae\xfa\x0e4gd\x15\x9b\x0e\xa0\xe0\xa3\xc7W\xbfX\xcc\xbd\xd7\x1f\xf0\x85\xcd\xff\xec\x04\xb3m\xeb\xec\xc8\x9fmlC\x8e8\xd8\xde{\xfc^\xbb\xa8g\x14\xf4\xfe\x81\x1b\xaem\x9f\xae\x19#\xff\x8d\xcb\x868\xff\xa1/gm\xc8\x8b\xd9o\xbb\xf9\xce\xb1M\xa2\x916h\xd0\xa0\xe0\xfe\x83\xab\xab\xa4z\xbe9Pi\x08C!\xc1GH!\xea\x85\xd0\xea\x81\xa5\x85\x07\x08\'k\xbd\x88\x80\xaa\xf3\xb2\x86\xc9\xc6\xafB|\xb8\xbe"OU\xa9\xca\'\xfa\xf0?\xce/\xc4\xf9=\xf2K\xbf\x07v}\xc1\\\xf6]\x84&\x1f!iu\xc3\xa7\xa56\xf7\xbd}\xd7\xe1\xe6\xba\'\x9b\x8d\x85\x9eF\x9c\xfbG\xc7\xd9Z\xfd>\r\x08\x8dxO\x98\xef\xf0\xb6+\xd8Q{\xed\x18\x90\x9f\x82\x1f\'l\xb0\x96\xbd\xbat\x8c\xfc\xff\xae\xb3\xfb\xb2\xb7\xdb\xcf^\xd8\xda\xae\x9c\x90\r\xad\xbd\x97\xef\xfc\x9e\xfd)\x1a`\x1bF\xa3l\x85\xce\xe3C\xfa0\xc0X\xcc\x9aO\x81S\\\x8f\xb8\x0bi\x07\x868\x0b 6\xd7\x1a,\x88C\xe5\xe9\x93\xe5\xe2\xd4\xfd9\x99\xcc\xe3\x8b\xad\xaa\xa5\x9d/\x03\xee\x0b\xd4\xf0\x19#l.8\xf4\xa9\xed*\x8b\'\x11_\xd7\x15\x11P1\xd0\x96p\x81K5\xa9\xe8\xcf\xc58\xb9\xa4\x84jW\xddm\xeeHA\xee\x07\xb2a\xa9\x87c/\xd5\xfd\xb3\xd0\xdd\x87\xcc\xbd\xc7/\x19\x1aD\xfd[c"\xd0\x7f\xd3\xf5\xac\xd7n\xdb\xd9)\x87\xedoW/\xfeC\xb3]\xeblJ\xe6j\xdb\xf4\xfeMC\xad\xff\xdb?\xc9\x06\xd7\xdfO\xa3\x9bC\xbf\xf8\xf5\xa2\xfb\x82\x1b\x91\x9e\x81\x00f\xa19\xf0\xbc\xcc\x81@!z\r@\x08\x90\x1a\x00\xe0\xe6Z\x87\x05m\xa8\x957\xdc\x15\x84E\xccW\x07^U\x9bV\xb0\x8fRuAH16\xb5\xa6\x17\xf2\xab6\x85\xb2\xfb\xf2\x11l\xd9\xa0\x84\xfcBtU\xcbV\x05`}\xdfR\xe5\xc0\x98\xa72\x14kr\x03m\x80\x161I\x15Ur\\\x13PIq\x95K\xe2;\xceM\xf6\x13(\xc5\xef\xed\x0b;j\x91+\xa5tl:>\xff\xfa\xfa\xfa\x10\xef\x8f!oH\xe6d\xb3\x8b\xea\xec\xd6\x93\xfa\xdb1\xfb\xeeb\xe7\xb5k\x13T\x80O_{\xc6.\xe8\xfe/\xb3\x9d\xealr\x8c\xfc4\xfb\xbcyZ\xd6N~*k\x7f\xbe)c?\x89n\xb5\x95\xa2\x17Bc\xcfe\xbbL\x0e\xed\xbdq\x03r\x8fbk\t\x01\xa0\xd5\x18*\x03\xaf\x00s\xb1\xff-\nC0\xe5\xf7W\xfb\xaf\xda~*\xdf-#\x9fO\xd4\xf1\xfd,\xa4V\xea\xbf\xc9\xf4^_\xd6+\x1f\xf2KB\xf5\x89F:\xa4\xe7\xfb\xba\x8e-iK\xabi\xecMJ\x00R\x02\xd0\x1c#%\x00\x95\x8f\x9a\x12\x005\x10U\xd3N\xc5\xfc\xaba(\x8b\xabM`A0\xd4h\x81\xd94\xae\xa1\x06\xa4\x12\xd1\xf8\x0f\x8bZl\xd1\xb8\x8fZ\x8b\xf3_mn\xb9\x0f\xcb}\xb0\x16\xd3\xdek\x99\xae\x1f\x06\xbd\xff\x88/\x0f7\xeb\xd3X\xe0\xf3\xe2^\x1d\x83\n\xd0\xe3\xe7?\xb6i\xcf\xdc\xde{\xec\x1e\xfb\xec\xcd\xe7C\xe0\xcf\xb4g\x1f\x0e\xd5~z\xef\xbe\xbdM[\xad\xce\xec\xda\xc5m\x9f/\xf6\xb1n\rGZ\xd7Q\xf3\x82\xe8O\xb3\xcfU\xa2\xe7\xec\xd0\xa8\xe7w\xd1\x7f\x10.\x95\x89.e>\x00:\x84\xd4\x1b\xb2\xe0HM\xd9/\x19J\x85,j\x9f\xd6Z\x08\x80\xdcq:\x98\xa78\xb9\xa4\xbcd\xe8\xae\x08\x80O\x9b.\xe6n\xf3\x05e\xbd\xbbO\r?\xb8W2\xfa4\xe9\xee\xd3\x1c\x93\xfe\xfd\xd6\x10\xd9W\xd5\x1a\x1e\xd2\x93\xfcw\xda(\xe9\xf5l\x92t,\x89>pd\x00\x98:z /\xc8~n\xac3\x93\x12\xbbUt\xad\xf5\xef\xdf? \xc6\x99g\x9ei}\xfa\xf4\xb1\xb5\xa2\x87\x03\xb7\\\xba\xfb\xc7!\xee\xfe\x90\xa8w\xc8\x9e;\xef\xbc\xf3\xc2qA\xccu)\xc3M,=\xd9yd\xd3\xc1\x1d!2\xbe\x15\xb8r\xb7K\xf5w")\x00\x00\x88\xdb\xdc\xe3\xb0\xdb\xe6\x98\x1d]g\x17\xb4Y\xce\xae:\xa6G\xc8\xef\xcf\x8c}\xcd\xbe\x1a\xf7z\xa8\xf2s\xcb\xe0c\x82\xff\x7f\xeaJ1\xf2\x8f\\\xc7\xe6\x7f6\xc86zt\x13\xbbht\xd6v\xb84c\xabw\x9f\x18\x8b\xfd\x93m\xf1^\xb3C\x1c\x01:?\xf3+\xc6\xf95\x00"\xd6\x15\xc4\x87\xc8\xf1?\xe9\xbcM\x01,\xb8A\xb2\r\x16\xeb\xa5p\xd4\xd6\x90b\x0c2\xa9&\xbf\xcf\xbd\xf7Uz\xe5\xcb\xf7\x15z\xd4n[:w\xa1{\x88\xf3Kl\xe7\x9a\xbe\xa57\xd7\xce\xc5\xf9\xbd\xca f\xe7C{\x17\xdaR`j\x9b\xec\x89\x00\x0f.\x1d\x9f\x05S\xc0\x85\xcf\xb5&\x90\x06\xee\r\xc2\xc2\xb9y\x8f$\xc0+v\x80+\xaf\xbc2pv"\xdd \x04p\x7f\x82f0\x96\xad\xd8\xf15;0\xeak\x03\x06\x0c\x08\x12\x02D\x83\x08:\x92r8\x1f\xa4Rr\x87\xe7\xfej\xca(\x82P\xac\xc6\x1e\xe7`\x7f@<\']w\xbb\x8b3\xb6\xef\xcc}mr\xbb:\xeb\xb3\xc7\xef\xed\xd4\xbf\x1d\x10\xf4\xfek\x8f\xeb\x1d\x92}\xf8|\xd1\xf2\xcb\xda\xc7\x1db\xe4\xef\x19\x1f/\xeea\xcf\xfe\xf7a;\xf4\xe3CC\x91\x90u\x8f\xfd\x83\xa8C\x86\x0c\t\xa2>\xef\xf9M\xc6<\xce\xb9\xe7\x9e{\x82\xc8\r\xa1@\xa4\xc7\xf7~p\xcc\xf5\x97\xe8\xf9\xa5-\xd7\xf9]\xfbKttP\x07\x90\x02v\x8d\xce\xb4\xbe}\xfb\xda\xb0a\xc3\x824\x81=@\x1c\x01d\xe7\xbaJ\xf8P\xdc\xb7\xd4\x81|\x1b\xca\x00!\x90*\xa8\xd1\xd7\xf9\xaey\xb6\xf3\xfb\xbb\x99\xbd\xb0\xbb\x9d\xd9~\x95`\xf4\xeb\xb7\xf7N\xd6{\xcbM\xed\xf8\x8d\xd6\x0e\xdf\x85\x12_\xbf\x8b\x8f\xc7b\xdd\xff\x8d\xbf\x98M;*\x14\x059\xef\xb5l\xb0\x1b\x10\xf9\xb7mtY\x98+\xd7\xad\xc4w\xcf\xba\xf2L\xfc\x17D\x15\x87\xaed\xef\xe4\x99a\x9f@|\x88\x1dD\x18\t\x8aud\xcd\xf9,\x9b\x0b{S\xc9}\x9a2\x80/\xf6\x08"\x8eD\x87\xe4\xc8<|\xff=\x89\xfb\xde\xc8\xc6sI\xe7WD_1$\x94\xc5_\x8cK\\\x9f\xfbp\x9dB\xff\xcd\xd7\xbbb\xa1\xb5\xf2k\xa1\x10\x17e\xd5TM:5\xec\x10\x12\xb2qX\xf9\xd1s\x11\xa5Aj\xda\\\x01d\xc4\xd3c\xc9\xe6w\x89\xeb ,\xc8\nP\xa2"p>\xc0\x88\x14\xb0Ato\xe0\xf8\xd8\x0c\x90\x1c\x00Vl\x01\xbc\xc2Q\x01b\xb8\x96\x825\x00\x14]O\x87W\x0b\x00\xae$`\xc8U\xc4\xdc\xea\xfa\xfc\' /F\xbcY_\x9c\x19:\xf6<\x14#\xfa\xc9k\xb7\x0f\t>\xfd7[\xdf\xaeXr\x89\x80\xfc_\xac[\x17\xaa\xfe\xda\x1f\xe3\xe3\x9a\xc5\xcc\xde:\xc4:~\xf9/\xdb\xec\xe4\x86p\x9d\xb5\xa3\x87\x82\x9d\x03\xe4\xc7V\xc1\xda\xc8\xc5Y\xea\x90\x04\xe0\x8dQ\xe5r\x17\x80\x93\xe7\x96!\x915f?\x90\xba\x90v\x98\xdf\xf0\xe1\xc3\xed\x9cs\xce\t\xf6\x17T3\xa42\xf6I\x86\xd4ZwX\x92\xb5]:7\xcf\xca\xbe\xfan\xbb:@N\x19u%~\xab\xb47\xaf\xcc\xb5\x14\x02\x00\t\x10\xdc\xcbW\x92\xcc\xc3\x9d\xaeS\x16,\xca/\x9e\xf4\xa7J\xfc\xf2u\xd5E\xc1AX\x10\x9e\x05\x06\xc18\xbcZ\xc0\xf7l\xbc\x17\xe9\x00r\xa81\xc0\xc0\xefH\x03l\x92\x90Z\xa2\xa08\xbd\ncH\xfcW\xc6\x97\xf2\xbe\xa5\n\xf8\x04\x10\xa9+\\\x83M\x87\x1b\xa2N\xacX?\xde\x8e}(\x1b\x8a||>\xf3\xdc\xe0\xf3\x07\xf9\x9f\\\xac\x91\x00<\x15\xbf\xde\x16#\xff\xcd\xf1qc]\xa3]\x80\xd7\x89mc\xe4\xbf\xef\xc7v\xe4\xec\x1ev\xea3\xd9\xe0\xf2\x83\xf8\x81\xf8 \x1c\xcf\xc0<*\xe1\xa0^D\xe5Ys]C\xde\x16_\x94R\xc6/\xbe\x07\x99\x98\x0f\xdc\x1eu\x04U\x0c\xe4\xc6#\x81]\xe5\x98c\x8e\t\xaf\'\x9cpBpY.\xd1\xa3\xc1\x16\xeb\x9d\xb5\x8e\x1d;\x06\xe2\xcb>2\x07\xdf\xa3\xb0\xd8\xf0\xb9\xef\x9eC\xfa0[\xe9\xed\xec/p\xc1\xbe\xb3W\xda_\xf6\xd4\xfb\xf3e\xdd\xf7)\xb6\x9e\x18\x94SV\x9e\xf5\xe4\xbe\xa8\x8f\xc0\xa3\xaa1\x97\xb3G\xb2\x03T\x92\xa0\xa6\x14c\x95u\xcb\xf5\x0c\xb2\'\xb0\x96*E\x87\xed\x0b\xa6\x02\xcc2g\xae\xc59\xac%u*\x88-\x01\x1fu\xf8\xf0\xfad\xb8t9\xf3\xfd\x9e\xfe(\xab\xab\\-:d\x9c\xe2\xa6l"@\x0f\x82K\xbc\xd2\x01!PCME\xb5\xb1\x18L\x92\x8d\x16\xa2\xca\xd0\xc3}y\xc8\xa4\xcb\xcaWg\xf1\x95]}\xe1\x07\xee\xaf "\x11\x10\x89\x9bl<\x06D\x00\x9f\x08\xbd\x9f\x9f\xf3\xa5m\xf3\xec6v\xfd\xdc\xcb\xcc\x9e\xde\xc1l\xe7\xc6\x9e~ <\x04\x80\x14\xdfG\xe3\xf7\x97,\xfb\xa3\xe0\xf3\xffo|\x8d\xc1\x07\xefk\xb36\xac\xb3\xb7\xe6\xdfa\x17\x8f\xc9Z\xbbhlx&\xee\x0b\x01\x83\x83\x02h\x95f\xee\x81,\xbe\xe6|.@c\xf3y6\xee\xa9\xd8\x02\xd6\x8as\xf9\x1e`9\xfd\xf4\xd3-\x8a"[9z\xdez\xf6\xec\x19\x08\x1e\xb5\rxv\x08\x16\x80\xc5wk\xf6\xfd8t\'Z\xad\xcb;aM\x90\xc2@\x12IO\xda\xe3B\xa9\xa4\xfc\xee]b\x8a\x84S\x80\x8eD~\x15\xdcD\xe2\x93D\xa8~\x88\x10\x08e\xf3\x89\xf3s\x7f\x19\xf5\x92:\xb8\x8f\xe6+\xb6\xa6\x9c\x87t\x01Qd\xffY\xb7J\xa4\xb3|\x95~\n\r!6\xc4\x8d9\xf0*\xb8\x06\x9fx^\xef\xb9\x90*\x00\xf2\xd3;b\xc5\x8e\xe3B\xf7\xe9\xfd\xa2\x01a\xee\x9c\xc3\x7fY\xc7\rO\x98\x11\\\xcd\xbe\xa7\x81z\x14T%\xc8K\xc99\xd2\xc1\x14\xf5$\x1f\xa8\xa8\x97\xac\xb8\x9eS\xb3\xb9\x10\x02\x90\x0eq\x0b\xce\x82\x98\x8c\x8d@\\\x9c\tJ\x9d\x90>\xe7#\xf8|\x92\x85\xf7\xd7Jd\x84\x8b\x80l\x00\x13\x92\x07D\x06$\xe4\xe03\x9c\x0c\x8e\xcf\x9c\xd8t83\x06IBr\x97\xe9\xfa\x9e]7%k\x97d\x86\x9a]\xff#\xfbd\x8d\xba\xa0\xf3S\xd6k\xd8Jm\xec\xc1\x18\xf1\xc9\xf5\xc7\x058\xef\x8dWB\x97\xdf\x93\x0f\xdd\xcf\xec\x97uv\xd3\xdc+\xec\xae/\xb2!\xbd\x17\x9d\x9f\xe7\xe4y\x99\x0f\xf7\x84\xf8\x94\xea\xf6K\x0e\xd6R\xfeo\x80%i\xd5\xe63\xcf\xc3\xbarp/\xce\x93\xa7\x81\xb5\x01\xc1PG\x10\xf9\xf7\x89\x06\x05q\x1f\x9b\nj\x16*\xd6\xef\xa2\x0bl\xe7h\x88\xed\x16\x9dn?9\xb5!\xb4&\xa7t\xd9F\x83f\x84\xb2\xe7\xcbv\x9d\x12\xd6P-\xaeADq\x18\x88\xb1\xa4\x0f_\x17_\xc18\x9c\xeb\x11_n6\xc1\x03\xfb\x05\x81\x82)\xf8\xeb\x8bk\xc9\xad\xe6[\xa4\'+I\x97#~\x83d\xb2S\x89\xf3\xf3\xb9\xd6\xc8\xcf\x1c\xb9\x0f\xf7\x94\xa1\x1b\xc2\xca\xfdY+q\xff\\nK\xee\x03\xfc\xd25j\xbb\xe8\xfcp\xe0\x01C\x8a\xe6wp\x82\xfd\xee\xd2\xa5K \n\xeaY\xc0u\x95\x08\xe5[\x9a\xb3\xc6\xac%\xff)\x96\x8a\xfe?C\xae\x13\x1f>\xa9\xf7~Q\x98\x94\xa7\xde<(\x88\xc9\xc3c|\x82\xe3`l\x02I\xbdn^J\x91\x02\xee\xc9u\xbd+K\xc6F\x88\x0b\\\x17\xf1[!\xc6z\x05\xd0@H\xfe\xc3+\x12\x08\xbe\xfe\xbfE\xddm\xc9\x1e_\x04\xd1\x9f\xba\xfeo.S\x17|\xfb\xaf\xddrU@zb\xfd1\x02~\xf3\xf2\x8b\xf6\xe2v\xdb\xd9\xab;\xc7\xe7\xec\xb5W\xe8\xfa{\xc7\xbc\xebB?@2\xfb\xb8\xbf\x8cf\xb2?\xc8\x97\\)\xf5\x95>\xa8\xe7\xf5\xa2\x9c8-\xc4\x0c$g\rT.\x8c{\x02\x04\xcc\x05\xe4E\x15A\xf4\x07\xf9\x11\xf9\xb1\x03p>F?\x88\xdf\x12=\x1b\x82\xcbu\xb7+3\xf6\xaf\x11sC\x06\xe3\xbf\xdf\xce\xda\xa8\x19Y\xdb\xfc\xb4YA\x12\xf0\xc6T\x1d\xda\x03IZ\xb2\xd8\xfbx\x10\x19\xe8t.s\x04xe\x1cf\xaf@\xc8\xe4s\xe7\x13\xaf\x93\x04\xa0\x1cDT_\x00\x10\xb1\x12/G%5\'\x15\xcc\xc5\x1a\xe1\xe2\xc6\x9b\x05\xdc\xb1\x07\xc0a)!\xdd *\xe7\x02\xdbH\x03\xd8\xd6\x90p\xfd9I\x11\x1f$g/T\x01Y._\x080\x12\x16\xbfq\xcdr\xd7\xe0{\x06\x05\xbf \n\x87\xe4\x10g\x90n\xce\x06\x83\xecP(,\xcc\x04\xfb \x0109$\x8ab\x11q\xdcG\xa2\x0e\x13g\x13\xb9\xa6\xbc\x06\x00\x11H\xce\xf5\xbd+\x0b \xe7\x90\x14\xc0\x7f\xb4\x98p@D`\xfa\xf2\xed|y\xc6\xec\xb1_\xdb\xb3?\xac\xb3\xeb\x06\xf4\t\xc9=p\xfaY\xef\xbc\x11\xc2}\xb31\xc0\xbes\xd0A\xf6\xc2o\x7fk_\xc7\xffE\x05\xe87\xbb\x8f\x1d\xfd\xc0|k\xd7\xf1\x95\xc0Q\xd9\x10Y\xb1\x15\x96\xcc{\x16\xbb\xecEv\x83\xf5UYkE\x99y\xe3\x17\x08\x85\x87\x05\x80b-\xa4\n\xb0\x07\xe2v|\x8f\r\x80|\x84\xdd\xa3\xd3\xec\x17\xd1U\xc1\xb5\xda>z"&~\x9f\x07\xc3%9\n\xc3^mL_&\xed\x19c\xe8\x89\r\x97\xdaq\x0fe\x83}\xa4}\xf4\xa4\xfd3\xea\x1cl\x07\x00\xa1\xec:p2\x10\x99\xfd\x95\xdbN\xaa\x9e\xc4z\xd6\x05n\x0f1F\xeab\xbd\xd83\xa9K<[1X\xcb\x05s\xa5"\xbf\x02\xd5D\x84*\x8ds\xf0\x12H)^\x06\xe0\x00\xf8co\xc8\xff@\xfd\x02\x89\xf9\x0e\t\x91}*\x06\xfb\x0c\x95\xa5\x03\x89!"\xc4\xc5\x00\xef\x8d\x98W\x19\x9c8\x1fi\x00\xc4\xa5C\xcf\xaf\x9e\xfd\x95\xd9\x9b\x07\x06\xce?/\xe6\xf4\x1f\x1du\x94\xbd\xb2\xd3N\xf6\xe2\xf6\xdb\x7f\x87\xf0\xe3\x0e8\xc0>\x89\xcf\xa7\xd9\xe7\xc5\xcb-c3g\x9cm\xbf\x18\xfaU\xd0\xe5\xe0|\xbeD7\x84L\xc4O1\x13\xd5X_\xae\xab\x1a\x07\xf2b\xc8\xed\xc7\xf3\xa1\xe3\x138\xa5\x1e\x83\xac\x8fBc\x15\x9b\x01\xa7\xe6<\xac\xfeK\xf4\x98e\xab\xc4@\xd9\xae\xe3\xd8\x80\xd0\x1cm:M\xb4\xdd\xaf\xca\xd8\xf6\x17g\xc2\xf3\xfd\xfe\x92\x8c\xedqu&d4\x92\xd0\x04\xf0\xee\x13\x9d\x10\x90\x18;\x0e\xd7\x83\xb0\xb3\xe6J\xc8\x92\x97\x85\xfb)K\x93\xfd\xe5<8\xceJ\xd1\x8b\x81\xeb@\x1c\xd8\x0b\xce\xafd=D\x04J9\xd7\x1b\'u\x94\xc2u\xf3\xddS\xdc\xbf\x10\x11`\xff\xa5w\xe3m\x80\xd8a \xc6\x10\x0b\xdc\xc3\xbc\xa4\x9b\x03\xd3\xe5\xcc\x83\xff\x92K\x03\xec\x95"\xc9(\x0e\x84\xfbp?\xf6\x8d\xfdC\n\x03\x1e\xca\xb9w\xc1!\xc3\x83\x0f\xc9\x85;qC\xb8\xfe\xae\xd1\x19\x81\xdb\xfc1\x1alP1\x16BAA\x9c\xab\xf4\xc9\\H#\xe2\xa0\x04\x11o\x80R\xc8\xa8\x8cP\xca\x0b\xf0u\x03@~\x16\x0e.\x05\xe7\'\xc4\x98h?\xbbs\xcd\xe0\xcf\x9f\x13K#\x9e\xcb\xfbc\xd2\xdf\xffn\x8f^t\xb6\xd9\x1d\xedC\x94\xdf\x8f\xba}\x188\x1f\x00\xcfs\xfa\xd8|\x08\x95< \xc5\xc2I+\x19\x002\xcf\x04B\xc9g\xceg\xd6\x12\xa2\xc6\xc6\n\xb8T\xf2J\x04\t`D\r\xc2\x03\xc3>\xb4\xef5\xd5\xd6<\xea#[\xfb\x98\xe9\x81\x18\xe0\xc5\xe0X)\x1a\x1d~\xc3\xa5I\xff\x83\xb6\xf5o\x04\xc9\x82}b\x9dYW\x19\x94|N\xbe\xe7D2\xe2)J\x14\xa2\xb4mti,5\xcd\x0c\x16l\xd6\x08\t\xa6\xda\xeb\x93\x1c\xcc\x83\xfdg\x9f*m\xdd\x95L\xfd-E\xf5\xd0:\xf9\xccB\x886\xaa\x1a\x01Wxe\x80!\xc5\xc8\x943\x1fI\x83\xec;\xf8U\x8c\x80H}P\x94#\xf3\x01v\xca\xb9g\xc9\x83EQ\xe4\x9ab\x01\x10;\x14\x14\xc3\x83\xf3^E?\x14\xe0\xc3y\xbe\xf0\x87b\x00\x14p\xa4l+U\x15Rp\x88\'6J\x95\xd5bs\x1d\xb9\xfa\xa0\x98\x07G}BN>b\xcf^\xd3\xff\x18\x12x\xb0\xf6\xcf\x1e\xf7\xba\xbd{\xe8\xa19\x91\x7f\xec~\xfb\xd9(b\xfe\xdf\x8bU\x96\x86\xc9v@tl\xa0\xe6\x10\x14\xb9T<\'\x028\xc4\xa5\x9b\xd2(2\xdf\x90\x0b\x13\xc2\xc9\xe6K\xc2\x81\xcb\xb0\xbeHZ\xbcg\xcdYWu\xade\xae\xfc\x0f\x02\xc0y\xcbw\x9e`\xcbt\xfd(H\x00 ;\xaa\x00\xdc\x99\xda\x0c$a\xad\x19=\x12*!\x11t\xc2\xf3\xc2m\x90\xb4 \xb6\nJ\xe2\x9aj\x98\x99+\x0c\x15b\xa8<\x11\x88\x07{\x8f\xf7\x07\x86\x00!\xafu3X\xd9M\x84\x88\x95\xa4:K\xaa\x93\x8bN\xe1\xef\xc5$\x00Ig\xbe\xa7 \xf8\x80\xa4\n\xf3C\x1aF\x14\x87\x08$= BR\xe6\x9c\x94T\x98\x0b\xb0\r\xd1P\xb8<\xaaU!w\x9e\xf2\x1c\x84\x1f\xf2\xc8\xf1[M\xf6\x80E\x02A\x15k\xcd\r\x01H\x85y\xf2\xbd\x1a%\xf0\x80\x9c\x0bpz\xe3\x9d\x8f\x0bg!\x15\x17\xa0\x0c/_\x84Ai\x9c\n|\x10pr_\xb8\xfd\xe1\xb1\x9e\x0f\xc7[\xb1\xe3\xf8\xd0\x96\x8bX\x7f\xcavS\xc2{n\xbcx\x13\x0f;\xcc^\xdea\x87\x9c\x04`\xfa\xe8\x17l\xbf\xc9\xfb\x05_9\xc8\x05\xb0+,7W\xf0\x88\x0c/\xd50\xfe\xe5\x1a\xaa\xa6\x04\x12\x81\xcc\xac\xab"\'Y7\xb9A\xf9\x9d\xf3|}\x04\x00\x8d\xff`\x0c\xdc(\x1a\x15$\x99\xa5\xbbO\xb7\x95;b\x13\x98\x14r\x01\xe0P<\'\xd7P\xb7b\xfe\x8bT\xc1\xf7\xdc\xab\xd4\xb6\xe4"\x00\xac\x03\xc4\x8a{7g1S\xe0\x00\xb8jJ\xd7^\x15\tI\xe6\xf7\xe73N\x16\xba\x16k\t\xc7g\x8d\xb1\x99\xa0\n#\xb9)\xa2O\x83\xf9\xb2^\xacU\xd2V\x00A\xe17\x08r\xa7N\x9d\x82$\x8bJV\xc8\x96\xa2\xae\xd6\n\xc4\xe3\\\xf0\x8c\xdfjR\x16\x9c\x05\x92\xe8\xa7zj\xf2!\xab\xaa\xaa\xce\x95\xf7 \xd9\xbb\xcd\x07\x94\xa8\xa4\x97?$\x11\xe8\xf0\x91T\n\x12\xe2z\x18\xa8\x88u_\xa6\xeb\xc7\xc1\xd8w\xc4\xcc#\xec\x94\xcc@;r\xcf\x1d\xec\xbf\xf4\xd8\x8b\xf5}\x7f \xee\xcf\x8d\xa9\xf2G/>n\xc7\xee\xb7\x9b\xd9\xcdmm\xe5h\xccw\xe5\xbb\xe4\xe1`n\xb96\x1c\x00\xa9%\x01PB\x0f\x9c\x04\xe9\x06\x80R*\xaf\xaa$\x81\xbc +\x80\x86q\x10\x0e\x8ej\x001\xe4=\xc0\x07\xd0\x00\x80\x04A\xe1\x19@BB\'\x04\xe9yN\x885\x80\xa8\x00\x1eY\x90A(\xde\xb3\xb6\xbc/6_\xed\xaf\xf24T\xa2\xad\xda\xeb\xe2\x87\x8a\x87\x88\xfb\x17\xea\xdd\x97o\xf8ri\x92nr!\xbf\x02\xd4\xf8\x9d{\x14\xe2\xa8\xc0\x05\x84T\xf9\x182\xd8&\x1b\xbcHZ\x90\r\xc5KW\xec\x87\xe2^0\xfa.\xd1sf\x88\xf2\xccG\x00 `J&\x93T!/\x9a\xe6_\xce\xba\x944R\x02\x90\x12\x80\xe4\xfe\xa6\x04`\x11"\x00\x0c\xe5u\xfb\xa2\x8cl|\xbe\x05R\xc4\x9b\xf4,\x1eT\x0b\xea{\t(\xccQv\x81\\)\x982\x14\xb2P\x18\xbc\x10mW\xed\xfcv\xa8\xea\xfb\xe0\xd7w\x9a\xed\xd3h\xf1\x1f\xb3\xf7\xde\xdf\x89\xfa\xaf\xed\xba\xab\xbd\x1f\xeb\xbc\x93\x1f\xbe\xabQ\xe7\x9f\xd2\xcd\xce\x7f=\x1btd\x90\xc2\xd7vS\x02J\xae\xe7\xe0{\x85\xbc\x96\x9b\xfa[\xca\x80\x00 JcGA\x0cD\xdfW\xadD\x80\x02\x02\x80\xa1\x13dF?\xfcQ\xb7i!\xc3o\xf3\xe8\x16\xdb3:9\xf47\xa0\xa8\n\x00\x08\x81\xc0\x16\xc35\xf9/\xb6\x12\xf6,\x97\x11V\x84\x00\xc4\x87\xe8p_\x08M\xb5\x9f\xaf\xa9C\xbd\x00}\x01\x91J\xae\xa3\xa4%`M]\x9bs\x11|\x85\xc7\xab\x02p!w6\xe7\xb1\xce\x10e\x19\x91\x93\xf1\x0f^\xb5RJ=\xaa\x83\x98\xa6\xeaC\xa2\x06\x10\x1a\x8c\xdd\x06\xa3j>\x02\x00.h-\x9aRB\xae\xec\x91\xa4\xa0\xa5\xb4AJ\xa6,&K1\xf9\x12L\xa2\xc8\xb2\xc8\xaa\xbe\x1d\x0f\t\xa5\x03P\xd1u\x17\xef\x95\xb1U\xea\xc7Z\xf7\x86\xbe\xf6\xe4\x7f\xee7;\xb0\xce\x06\xad\xbf\xa6}\x1ds\xcd\x0fc\xee7f\xdf}\xed\x93\x98\x1b\xfe\x17W\xd5\x85CB\xcc\xbf\xdd\xbb\xa9m;f\xbb\x90\x1a\xbcb\xc7\xd7\x03\x15\xf6\x15f\nY\x80U\xb0\xb2)zg\xa1\xa1>\x85 \x04\x00\xc9D\x95\x94\x01.tJ\x0cL\xb8(A\xf6\xc5{\xce\n\xa5\xd50\xecm\x12\xdd\x19\x12J\x06\x0f\x1e\x1c\xec\x05\\G\xa1\xca\n\x8f\xcdW\xb9V\xfa<\x80\xa4Tk\x08l\xb5\x9f\xaf)#\xe9*\xf5%\xe6K\x1d\xc9"7\x8a\xbb\xc8\x97o\xa0\x9e\x8dHC\xc5*5q\r\xd6\x1b\x03.\xffc\x8e\x85\xa4D85D\x80}\x92\xa5\x1f\x1cR\x9dM\x0c\xb6\x1b\x9d0\xc3\x16\xeb\x93\r\xb0.\xc3\xb3\xdf?%\xe7)\x9d\xbb\x9c\xb5h\x95\xc3Ga\xf9\x80\x10e**X\x08\x00\xdf0\xba\xdb\xb6\xbf$cg<\x9f\xb5\xc7\xbf\xb97T\xef\xe9\xbb\xf9F6\xfa\x8e\xeb\xec\xabX\x1c\xfe&\xa6\xa2sG\xbfj\xcf^ua\x10\xf7\xa7\xb4\x8b\x91\xff\xf5\x03l\xe3\xc76\xb5\xbd\xae\xc9\x04\xc4\xc1r\xcd\xe2+\x15\xb6\x98\xc8\x042)\n\xb0\xd2\x04\xa0B\x83\xe7\x83\xb8\xc1\x19\x00<\x15FE\xfc\x87\x00\x00\x08\n\xb4\xf9ete(LB\'\xa2-\xa3\xeb\x82;\x8f\xde\x86\x10\x07\x00\xd0KY\xea\xe2T\xed\xf96\xe7P\x1f\n%\xbeTB\x80%q\xca\x88\xcd5\n\xd5\xf3G\xe2\xa2\xca\x15\xaf\xa5X\xd4U.\xbfT\xeb;\x84\x05B\x9f$\xb6\xec\x1b]\xa6\t\xdaZ\xa7\xff\xf4\xe0\xe1\x92\xc1\xb3\xa9\xf5"[\xf5Pb\x85\xf4\x1a\x16\x06\xceE\x9e;\t9\x04\xb4\x90\x9c3\xf0\xd1\xacm\xfe\xec\x16f#\xd6\xb0\xaf\x7fVggtX5\x14\xf0$\xcc\xf7\x8a~\xddBq\x8f\tm\xea\xc2o\xdfl\xd1X\xd9\x07\xf5\xe0\xaaw\xb3\xa1\x86\x9f\x92_re\xe0\xe5\x1a\xa2\xd6Jm\xad\x95\xae\xeb3 \xa5\xd3\xc1\x91\xd1\'\xa9\x9c\x04\x82\xb7\xad\x1f\x1f"\xc5\x88\xee#\xfe\x02)\xc1Gb"\xfeC0\xbcK\x88k\xb4\xf6\xb2\xe0\x8c|{\xa1\x00\xacR"\xe3\x92Cu.\x151\xe7\xeb\r\xc2\xa1\x11\xbbu\x1eD\x13x\x93\n\x05\xb3i\xa9\x1a\x80\xdc\x1bU\x10iP\xc1X\xa8\x06\xd5l\x1a\xd3*\x86\nY"\x92\xc1]\xe5\xcf\xc6\x10\x86/\x95\x08+\x02t\x06?\x91\xb5\xad\x9f\xdc\xda\xf6\xf8dO\x1b5\xfff\xb3\x87\xb6\xb6\xb1\xcb\xd5\xd9M1\xb2c\xf4\xbb\xba\x7fO\x1bp\xc0\x1e!\xbbo\xfa\xda1\xb7\xff{|\x8c\xda\xc0\xec\xadCm\xdaW\x97\xd9\xe5\xe3\xb3\xb6Z\xb7wC^<\x8b\xa9\xbc\xf4bs\x03\xd1\xd9\x04\x02q(\xb4\xc1fT\x1amVl \xca)\xc3R\xc5Y\xd8p\xc5\x01`\xd4k\x0c\xf5\x9d\x14\xf4\x7f\x08\x19\x04\xc0\xf7R@rP\xce\x84\xfa.\xa0\x8f*\x8d\xb8\x16\xf3\xae\xd6H"\x9a\x82\xcf\x84\xb8\xe5\xac\xbb\xef\x04\xc4\xff\xd9G\x98\x0b\x84\x90}W\xc7`\xa9\xae\n\xa8a\xadT\xe1\nxl\xa9\x02\xa0rs\xaa\xe0\x8d21e\xfb\xd0\xe1\x8d\xee\x0b\xe4\x90h\x07\xc7\xc2j\n\xf0b\xd1&\x16\x1a`\x87\xebo\xf5\xc4V6 \xd3\xcf\xbe\xfa\xfc\xb4\xc0\xf5)\xd1E\x05\x1fD{\xfc\xfd\xa4\xf5^\xd8\xe3\x88X\x05\xd8\xd0\xc6\xaf\x10#~T\x17\n~>\xf4\xf5\xc8\xd0\xc4\xf3\xf0\x19\x87\x87PW\xf4gEW\x95\xd2\xc9\x05\xe0an\x10\r\xc2kw\x88\x86\x05\xc0\x107\xad\xc5z\xa8.\x82< \xccU\x85U\xe1Jp}\x92\xad\x88\xd7\x87(`\x14\x94\x8a\xc0\x011\x80sp\x1e^\x01\xde\xf3\x1d\xc0,"P\x8b\xf2_\xb5\x18S\xbe-\x9b\xaer\xe1\xe5\x88\xd7\xf2\xf1\x8bx\xc8\xe3\xa4J:\xbe\xb2\x12p\xc0:\xcb?\xaf`\xb3\xd6\xc6i\xd5\xdb@\xd5\xaeTv\xaf\xa5\xe7\xd5\xa4\x91\x12\x80\xef\x8f\x94\x00\xfc\xffH\t\xc0\xf7\xc7BC\x00\x94\xf0\xc2b\x03\xb4\xaa(|PtT(^\x81e\x9f\xca5\xa7>\x9d\xb5\x9d\xde\xdc\xc9\x1e\xf9f\x94\xd9\xbb\x1d\xcd\xae[24\xef$\xd2\xef\xf3u\xea\xec\xd5\xa5\xea\xec\xb5\xa5\xeb\xec\xa5%\xeb\xec\xb9\xc5\xe3\xef\xb7\x8e\x8f\x0b\xeb\xec\xd6yW[\xbb\xeb\xda\xd9\x8e\x97el\xc3\x813B\xfc;n0\x90W\x06\xbcRD;\xa5\x98\x92]\xb5\xc9\xe0\x99\xf6\x83\xdes\x82-\x80\x8d\xa8%\x02\xf9\x86\x94\xa8*"\x8c\x88\xfc$\\\xfd&\xba$\x84\xfbR\xf1\x07C\xa6tV\xd4\x01\xb5\x18C\x8c\xe5\xbd\xd2z\x95e\xc9y\x84\x00\xb3\xee\xad\xd5\xa0$\xb7\xb1"\x15\x11\x83\xd9\x87R\x91\x9f\xff+]W.25\xb2Q\xac\x89\xef\x1f\x08B)d]\x05O8\x9a#\x9f\xa1\x9c!#\xa3\xef\x9d\x08\x81k\x8d{Xp\x00\xa0\x00#:*\xb1\xd3\xdbDWX\xdb\xfa\xd1\xb6r\xc7\xb7BZ\xeae\xe3\xb2\xb6\xd6\xa8\xb5\xec\xcc\xcc`\xfbd\xd6\x05fW\xc4\x88}x|l\x13\x1f\xbf\xfe\xb6^\xff\x01\xf1qQ]\xa3D0r\x1d\xb3\xbb74\x9b\xdc\xd5^\xfa\xcfCv\xc6s\xd9\xd0\xb0\x13\xc0A\x8f\x02\xf8\x01\x02\xdfe\xb6\xd8P\xe2\x0f\x00H>=\x86G\x8co\xa5\x04\xc8Ts\xc0\xb9X+<\x02\xb8\xf8@\xfee\xbbL\x0e\xe1\xcftT"\xd0\x87\xa0\x11\xec\x13\x10Q\x9eU\xff\x15\xc7PU!\xae\xb1et}\xa8;G\xf6^-%\x99\xa6\x0c\xe9\xfd\xd2\xc9\x95PV\xca\x7fE<\xf9\xaf\xfe\xa7R\xe2r)\xfb"\x9d\xec\xb3\xca\x87\xf9\xfe\x12\xb9\xdcj"L\xcd\x91\xebP\xea\x90\xc4\xd8Z\xe6\x93s\xc8\xba\xaf\xc0!81\xa2+n-\x02X\xf0\xe7\xaf\x14\x8d\xb1}\xaf\xcf\xd8\xf5S\xb3\xb6\xd1#\x1b\x87\xfa}o|\x1ds\xfe\x89\x9dBU^\xfa\xf7\xd9\xfd\x9b\x87\x0e=6\xb1\xde\xec\xa3\xa3c\x95\xe0\xf4\xa0\x16\x8c\x9d\x7f\xbb=\xfc\xf5]\xf6\xf7Y\x87\xd9J7\xaed?\xea\xf6\xa9\x9d}\xf6\xd9\xdfKl\x11\x10\x94:g\xce\x95[\x0e\x84#\x8dv\xb1\xde\xf3k\x97i\x95g\x80\xc4p&u5"F\x9c\xfap\xca:#\x01\x07\x95\x80\x80($\x01\x80^H\xad\xb6n\x8a\x17G\x92\xe0\xdc\xdd\x8e\x9a\xdd\xd3.\xcd\x9ck\xfb7\xecok\xdd\xbd\x96]9!\x1b\xaa\xff\x020\xa5r\x8c|\x83EU`\x0e\x15vH\xab\xe5\xb5\xd2\x1c\xf7\xa6\x0c$\x11\x9e\x87\x98\x00\xf2\xed\x89\x04T\xbbu\x88\xc0\x11Q\x14\nS(\xfc4_T\x18\x00\rB\x01\xf0\xfc\x1f$\xa94\x9a\xaeV\x03\xa4P\x9dC\x0e\xd5&(\xa6\xeb\xaaV%\xcf\x0eGG\x12J\x06\xa8\xc93\xa0\xd0^%\x9c\xfd0f@d\x91\xfe\xb0\xd7\\[\xac\xcf\xd7\xb6t\xb7\x0f\x83\xb4\xe5\xeb\n\x82|\x84Y\x93B\xfd\xf4\x7f\x1e\xb4\x83n\xce\x04D.\xb5\xf6 \xf6\x05\xf6\x87\xc8\xbe\xa3\x8f>:\xbcB\x10\xcaY\x1b\xe6\xec\xab\x00/0-\xca\xd4w\x00\xe4\x81\x9b\xa1\x8f\xb3\xc0\xdbG\xe7\x87\xfez\xed{L\xb6m\x87\xcf\xb6\xcb\x1a^\xb4UnY\xc5\xd6\xbew\x1d\xfb\xe9s[\xda\x8eSv\xb6\x83g\xfd\xd5\xfe\xf5\xe5?l\xaf\xcf\xfeh\xbf}\xfb\xf7\xa19\xc7/_\xff\xb5m\xf3\xe6om\xbb\t;\xdaNSw\xb1\xd5o_\xdd\x8em\x18\x12b\x04H\t\xce\xd5\xf8\xb1\x9c\xe1\xc5H(\xf6\xafc\xb1\xbbC\xef\xf7\x03\xa2\xd52\xdb-\xdf\x86\x02\xc8*\x0e\xa2Z\xf7\xbc\x17W\x87\xa0B\xf4T\x142\x9fX\xcf> \xd1\xf8\xccB\xd4\xb1\xd6 >\xaa\xef\xa4z.\xa0\xce`\xaf P\x06\xe4)\xe4\xeeRw$5\x9ea\x8dX\x1b\x88\x01\x08\xee\x91E{\xcbw\xe8\xfd\x10\x9b\xb6\xf5o\x86\xaa>\x94J\xa7\xb22D\x96\x98\n\x15\x83Q\xdc\x00\xe7bH\xfe\xd5y_\xc5p\xf6R\xc9\xa5\xc7\x98?\xaa\xda\xf2\x9d\xdf\x0b\xc1[\x1bG#C~F2S\xb0\xd8\x90\xfaQ\xadB4\xcd6\x04\xc0\xaa"\x04%\xa51\x05\x0bN"\xcf\x1aGN\xb3\xdf^8\xdbF|\x96\xb5\x9dG\xef\x1cj\xd7\xfd\xe1\xb2Lh\xc2\xd9\xeb\x9eyv\xe1\x9bY\xbb\xf1\xfd\xac\xdd\xd6\xf0\x99m\xf1\xd8\x16\xb6\xee\xfd\xeb\x055\xa1\xfd\x1d\xedm\x95\x1bV\xb1k\x1b\xc6\x07\x83!z?\xd7W\xc9\xf1Js\xc4%b\xb1\xc1\x88\xa1\xbf\x8b.\xb4U\xbb\x8c\x0f\x94\xbb\x96\xc9.M\xb1\xceK?-\x86\xcc\xea\x01\x01\x92\xa0\x0e\xf0\xea\x93iZ\x8a\x18\x08\xf9\x11\xc5\x89\x83GBDJAT.V\xdeZ\x0c\x06\xa2\x06c\x81h\xa8\x1c\xbd\xca\x90\xe7Jq\xe6\xda\xc0"=\x1e\x7f\x1f\x9dg{D\xa7\x84\xcf\xcc\x03\xc2\xa8\xcaWjS^i\xf87\x0c\x8fb,\xc0\xf2z\x03>\x0fD\x80\xb8\x12\x08P\xae\xf3\xf3\xe5%T\xd2\xa4\xa4U\x0c\xd50W\xb9/\xac\xd5\xe8\xa0P\xc1=\xe3E\xa7p%\x066\xc4\xf7Co\x9d\x13B|\xc9\xeb\x87\xd2\xeery\xc6\x0e\xb81c\xfb\\\x97\t!\xbcD\xf3\xad=bm\xeb0\xa2\x83\xady\xd7\x9a\xd6\xf6\x9a\xb6\xf6\xcf;\xa8\x07\xf0v\xc8\x82c\xb3\x9a\xb2X"\x00\x1c\x12\xdb0\x02RF\x0b\xe9\xa2\x16\x12\x80\xca:Ig\xad\xf4:^\xc4-\x94\xd3\xaf\x9av \x00\xaf-\xa1Cj\x9d}M\x02D~<\x1b\x10&tq\x10\x1a\x82^h\xcd\xf9\xbf$#<%\xe4K\xf0^n\xb2d\xb9u\x86rO\x80E$\x06\x18\x11\x95\xacH\xb0R\x19se\xb7\xf2\x7f\xb9!\xcb\xcd:\x84x`\x93Y\xaa\xfb\xe7!\x82\xb3}\xcf)\xb6B\xe7\xf1A\xbaA\n\xcb\xe5f\x94t\x92\x84\xdd\x05\xc5u\x9bs\xc8%\xa3D\x13eB\xb1\x08\xbe\x80\x02\xba\x18\xf6\x80\xd5c\x95\x00\xa9`\xfd\xe3\xbf\xb0\x1f\x9f\xd2\x10j\xd7\xafw\xdc\xe7\xe1\xfb?\xc5R\xc1\xd9\rwZ\xdbk\xdb\x06\xbd\x9fW\x12{ (\x88\xebM\xcd\xd2\x13`\xb2\xd9\x88|\xf8\xd31P\xf6\xbd\x7f~(\xd0\x00\xb0V{#@~5OMr\xbbr\xef\x05\xa0\xa8\xa8hk\x12\x15}\x92\x15\xafpeDt\xa5@Ch\xe1\xdej\x13\x8fWC\x11\x9b\x85\xa4.\xfe\x0f\xc7V\x93V`\x0b\x15Im\xeb\x90\xe2\x92\xcc@\x89g">\xe8\xff\x87D\xbd\x02\x0c\xf1?\x954S\x8cJ\xa5y\x14\xcc\x03I\x86~\x0c\xb8\xa2\xa9\xd8\xcc\x1c\x0b\xed\x8b\xe6\xe6%1Iw\x0b\x14\xf7W\xea\xac\xaf\xed\x07\x90\xab\xe0\'\x8b\xa3\xca\xbf\x88HP}\xd2\x1fI\x82\xa0\x92\r"\x13nA\xea\xda\xd1\xca\n$l\xd3\xe9]\xdbx\xd0\xcc\xa0\x1a \x15@\x18\xf8\x8d\xae7\xe8\xc0lv\xa5u\xe1T\xe3@\xed\x9b\x00P\x00\x13\xa0X\xa1\xf3d\xebx\xe7\xdc\xd0[\x0f\xa9\xa0\x9an \x10C\xd5}UB\xdb\'B\x11\x8bPN\xaa\xa7*(\xd5*a\xa9\x92\xe1\xdb|\xa9\xf4\x9bJ_\x81\xbc\xec?\xea\x15z\xf5^\xd1I!\x04\x1cDd?\x93\xed\xb44\x84\x14"\x9c\xbe\xfd<\x12\x00\xfb\xe4\x1b\xb7x\xe2\xa3nK\xdc\x1b\xc2\xb3B\xa7\xb1\xc1\xba\xaf\xc62\xda_5\xc7-\x97\x90\xaa^\x05\xd7\xa6\xf4\xda\x0fz\xcf\x0f\xf1\x1b\xb9\x12\x7fr\x8d|\rS\xca\x99C\x8b\x0f\x80\xd6\x17\x98dCy\xe5\xb3\n\x1e\x82\xb4l\x1a\xa2\x1b\xe2\x1f\x99W\xbf\x89.\x0e\x1b\x82\x9eD\xc2\x0b\xd5l\xd1\xed\x97\xe89+\x14\x9dl[?!\x16\xa5\xa6\x86\x9awXR\xe5\xeeiJN\xb4|\xfe\x12\xfb\x94@\x82\x95\x16c\xd0\xd2\xdd\xa6\xdb_n\xca\x04B\x00\xd1\xaa\xb4\x00er(\xd6\x1d\xa4\x95\xdf[n+!\x8b\xfa\x06\x96Z~\x9c\xb9\xb1\xbe\x10\\\xf4\xd6j\x94-o\xca\xf0E5\xd4l\x96\x03N\x8d\xf4\x87Q\x8dd/\xb23\xe9]\x80\x8b\x12\xc4)\xd5\x96!\x98\x02\x06T\xae\x1cD\x83\xd1\xf8\xf6bB >so\\\xa6H\x1a\xd4\xf3\xdf):;\xfc\x0f\xae_\r\xc2\xce\xbdQ_\xc8\xcc\xa43\x13\x84\xa6\xb9\xdd\xc75\x19\x85\x80I\xa5\xc3%\xb2\x02\xc8\xf2{\x824jo\xc5g5\xfd\x80Zc\xe8a3\xa8|\xb2V\xf4H,\x01|\x15<\x03\xb8[\x96\xed2-\x1cB~\x08\x02\xe2\xdar]\xde\rFD\x16\x16\x8a\xad,\xb1J\x90R\xd1~\xbe\x8b\xb1\x9eUn\xc0\xa5z|\x16\xcah/\xdek\xf6w\x05D\xaa)\x01\xa8\xc9\x87"$\x93m\xba\xca1<\xc2\xddXg\x92\xa9\xd0\xa3!\xaa\xacS5\xe6Z\xc9\xf0\x04N)\xdd ,\x84\x15N\xbfw4(\xc4W\xac\x1b=\x10\x08\x01D\xa1\x14\xf7\x98\xea\x03\xa8&\xa2\xb8:\xcf\x9d\xab\x12\x0fC6\x12ewr/\x90T=\x0cJ\xd5\xb3\x81Y\x10\\\x92\x06\xc4\x87\xef\xb0?\xa8W\x05\xdf+\xf8\xaaX\xbc\x000\xa80\xedV\xeb\xdeS\xd6^>qHq\xdbBB\xb8\x0f\\\x9f\x85Rq\x0b\xc4}m\x90*\xdc \xfe\xd1G\x90\xe0\x16\xb8>\x9c\x1d.O)k\xf2\x00\xa8hK\x93\x0e\x0c)pb,\xb6\xf8\xc1\x85\xfc\xdc\xa7)\xa2\xae\x82}@\xbe\xe4\xe6\xab1\xe3N\xd19\xc1\x16\xb1l\x97I\xdf\xf3\xfb\x16#\x88\xa5\xceA\x11\x87\xaa\xca\xe3\xfb\x00\x96\xfbl\x10W\xd64\x14L\xe9\xd9\x10\xf2\xc9\t\x14j\t\xeb\xbeW\xadT\xbe\x1cda\xefP\xf9\xc8\xf7\xa0\xe0\xc5\x1a}\xa6\xd9\x1f\xa2\xa1e5\x12\x11\xbc\xc1\xb5\x95K\xc2\x9a\x81\x94\xf9|\xf3\xb2\xefH2\x80\x19A$\x93\xe5\xb7\x0b\xed\x1d\xbfm\x13]\x1e\x02\xb2 \x1e 8pN\x03V\xaa-cD\xe49\x80\xabd\x01\x8f|\x83s\xd5y\xaaX\xe1\x91\xb2G5\xad\x87\xc5,\xcbp*u\x0f\x02x1\xc2\x80\xf8\xea\x16\x8cA\x8d\xbcv\x16\x1d$\x86\xf2\xf2\x8a\x01\x87\xf6\xd6Kw\xff$\xe6\xf63\x828\xb8QtW\xb0\xce\x12\x7fO\xb0\x10=\xed!\x10\x14\xbe\xc0bK-@(8T\x96\rT\xfe|\xb9\xae\x1a%W\xa8\xae\xba\xb7\xf4*\x1d\x18\t\x85\xd8{Bn\xd5\xb4Q\x1d\x8d\xf2U\x90-\xc7\x0b\x01\x92\x80\xfcH\x17p1\x08$\xf7Q\x8f\xf9r\x9e\x07B\x06wS\xe4 \xeb\n\xa0\xb2\xc6\xcd\xc1Y\xa4>\xa9\xeb/\xcf\xa5j\xc7H|\xc0\x03\xed\xc7h\x1e\x83\xda\x07!\x80\xf3\xc3A\xcb\t\xb2\x92\xc4\xa9>\r\xea\x19Q\xcc7/\xfb\n\xf3\x04v\x98G.B\xabd,\xff\x1d6\x1a\xe6O\xb8:\x92)R(0\xc8=A|\xde\xabu\x1b\xd2I1\x89T5\x08$\x19\xa9\xdc\xbfsC7\xfb\xe9\xe9\xb3l\xab\xa8\xb1\xcc\xba\xd6\xbb\\b\xad\xffq\x8f\x9a\xed\x8b\xac\xf1\xe2\x8eI\xaa\x94\x0b\x88Af\x16 Wc\xcf\\\xc15\xaa\xda\xeb\x0bZ@\x00\x10\x8dT`\x12\xfd\x08d\xa5\xc0\x07\xfa>\xc0@\x98\'\x06\x1f\xe9\x83\x88\x84H\t\xf8\xf3Q\t\xa8\x80\xb3|\xe7)\xb1\x0e>#\xe8\x89+\xd6\xbf\x1e\x08\x02u\xef\x0f\x8f\xea\x03gV\xd3JI\x00\xe5v\xec\x11\xe1\x82\xd3\xe7\xaa\xad\xc6\xc6\xa82\x0fD\xc6\xbb\xe9\x92\xfa\x9a\x10\xbf\x9c\x9e\xf6~\xa8\x17"\xdc\x9a\xf5\x82\x18Tb\xa4\x87+\xadV!\xca$e\x11\xe7\x01Q\xdb:\xba6\x18r\xf1\xb9\xb3w\xd5\xd8\xb3J\xae!_\xbb\x1a\xdd\xe6:\x87\xf9\xefs}\xc6\x8e\xcf\x1cm\xeb?\xb8\x81]\xdf0)\x84\xabc\x08\xae\xa6\x8b\xb5\xd4\x0e\xc8\x15\x0f\xf4\x12\x80\x00\x04S\xab.5-\x14g\x93\xe8+\xd1\x8d\x85Q\xf9$\x89gjD\x99K_J\xb6\xf8\x92\xd4\x01\x17\x90ET\xad\xa6Ap\xdfYV-\xa79\xe0X\xe2\x1a\x8a\nC5X#z,\xf4\x1e$\x03\x0e\x7f-\xcf\x04@\xf3\x0c\xcdi5\x85\xb8\x15*#\xed%\x00I\x16\xf9\xa2\xc7r!\xa1"\xd3T\xdf\x1fb\xc0\x9a\x95\xfb|\\\x87\xf5\x81\xa0\xb0\xd6HY\xac\xa7\xca\x84{\xfb\x84\xd4\x8cR\xae\xeb\x9bg\xfa\xefyF\xd5q\xe4\xde\xec\xb7l\x19\xcc\x83\xfdT\x91S\x08\x01\x0c\x80\xfd\xaeD\xbd)u(\xf4\\\x1d\xa8\xd4\xdf\xd0\xbb\x06!\x86\xf9<,\xc1\x90\xd8\xe7\x9b\xe0\xfd\xa1\xa6\xc4\xca\xd1\xe8\xc0\x80\xb0\xabT\x12\x15\xd8b\x83\xd2\xd7\xe4\xdb\xb3\xf8\xea\xe6\xab\xa6\x85*\x17-7\x85\xac\xb4\xc9\x10E\x80U\r(U\xf2\xb8\x94{\xcb\xe5\xc3}9\xb0\xba\x02\x94\x8a\xf1\x96\xf5]\xa2\xa98\xa8D2\x00\x84y\xa1.\x008\xf8\x8c\xd5\xa6\x1a`kn\x1f\xb7\xda\xa4\x17+\xa4!\xfb\x80\x88i\xaes\xf2\x95rVR\x0c\xcf\t\x01\x04\x81\xca\x11-AN\xd6\x96xz\x10\x1f\xe4C\x05 \xc1\x85u\x04)}\xd5\xe5\xa4T\x93o(\xd5V{\xa5{\xa9\xcc\xb6\x0f\xf8\xe2\x1e\xeam\xa8J\xcaR\xd3\xf8\xbf\xd2\xb4A\xa4r\xecS\xe5p{\xd5\x03\x80\x01\xa9\'%\xf0\xc83\x94\x027\xcc\x8d\xe7P\\\xc1\x02\xeb\xcb\'\x84\x16\xdd\x05\x80b!X\x10\x90\x9e\ra\xc3\xd4\xf3\xddW\xa2M\x1a\xc2\xd8h\xf9*s5\xc5\xc8\xe7\xb6P\xa7SQ`q\x08\xb5\x9eV\xebp\x89\xf0\xbeK+\x1b\x05\xc7\xc2n\x80(\x8c\x14\x032\xc8e\xd3R\xe5\x9a\xc4\xdd\x8b\x9d\xc7\xb3\xa8\xcb\x91\xff^\x99k>\xc2LH\x8f;\x14B\xcdsb\x0b\xa1\xd6?\x06\xd2R\t\xae\x06{\x8b\xa4%"\xcb~\xb3\xe6\n5\xf6\xa5\xd7e\xbb\xc9\x87\\j\x8e\xa1&\x98\xea\xc4\xe3\xab\x16)\x88I\x1d\x9cU:]\xb5\xf1\xe5\x02\xe4\xb3\x0e\xf6\xaf\x9c\xcaJ\xca\xcf(E\x1a\xe2<\x90\xd5\xdb\x87de\x87X\x95B\x00\x98\x9b\x9a\xb5\x8a\x804W\xdf\xc3\xaa\x0e\xaa\xe9\xb6\xad\x7f\'\x00\x95\xba\xf4B\xa5y \x1e0\xd9.\xaa\x12\x8bl\xb9\x11T\xdcSm\x9d\x01\x84\xe4\x7fQ#\x006\xb8\x18Q\x80X\x8e\xa1\xc2M\xf5\xf9\x97:j\xa9R\x80,\xea\x0c+@T\xe4\xd8\xb2]>\xb0\xadb=\x19\x15`\xef\xe8\xc4\x10\x16\x8d=D\xae\xc7R\x87\xea\xdb\xb1\xbf\xea\xb0\xa45\xf6\xbd\x17t\xbe\xdcb\xb9\x90\xd1{\x14\x94\x18\xa3\xfd\x96(\xcd\x9e\xf8\xe8C\xdfaI\xe7\xa9\xc0\np\xc7\xfc\x14pU\xca\x90\xb8^j\t3\x88+\x88\x8b\x9a\xc9\xda\t\xe9\x99O\xbe\xd0\xe2\x85v\xc0\xfd\xf1\xbbB\xc5\xd4\xd5F\xd4Y\x19h\x8a\xa4\xd2\x06C\xe5\xcb\x11\xb7\xb8^9\xd6wI\x1a\xf9\xcaT3\x1f\xb5\xba\xa6\x14\xb6\xda,\'\xfd\xf4\xd5\x1c\xe2\xcc h\xb2\\\xb8\xc4\xdfj\x04h\xe8>\xfeZ \x0f\x04z\xff\xe8X\xfbitS\xb0\x94S\xfa\x0b7\'b|>\xce\x93\xcf\xf5\xa5\x16\xd4\xaco\x12\xd9\xd5[O\xf6\x8cb"\xb8\x02\xc0\xd4\xb2]\x9dt\xfc9\\C\x96x\xae)\xe9B\xc4F\xd1\x80\xf2w+\xdc\xba\xd8Z\xa9[\xb3\x92sr\xc1\xa4\x7f6\xc5t\x00?H\x97\xca\x05\x90]F\xf9\x08\xc5\xee\xbbP\r\xc5\x97\'\xfb\xee\xa9\xd1&\x1b\xa6\x82\x07r\xe1\xa8\xdfz\xa9\xf7\x80\xdaV\xb3\xb2\x0c"+z+.\x1a\x8cb\\\xbb\xd6\x9dmX\x0f\xc5\xdd\'\x11\x0e\x00\xcf\xe5\x12\xad\xd6Pa\n\xd44\\d$A\xd1\xf9\x08W`\xb2\xbf\xbc\x1f\x88\xf9\xe5\xb6\xf0\x02\x06\xd4a\xa7\\\xd7Y%\xde\x16\x10\x14\xe4U\xa2U\xa9\\\xdcK\x0c u>C\xa5\x8fm\x00v\xd5n+Ynk\x81\xcd\xa5o\xea\x00@\xd4\xfe\xcaw\xdcU\xaf=\x0eq\x04\xf9\xf0y_\x0e\xb7S\x0eu\xa9\xe7\xe7\xf3}B\xac\xd0SU\xd2I\x8d\x11\xf25\xb5\xac\xe6\xe0\xd9E\xfc\x92\x86;\xd5b\xab\x95\xbfV%\xbf\x88\x01 sl\xab!_\x06\xb5\x8d^\x7f\x18\xa1\xf2\xfdOE+\xca\xb9\x97\xd2\x85\xd9\xf7r\x8b\x82V\x82@J\x00\x02\xa6J\xd1\xf9\x99\x93\xbcQ\n\xd0\x92+Z\xe7\xb0\x1f\xea\xb2\xeb\xbdR\xdaC\x8eZ\xf4n\\ \x07\x96PD!\x89z\xa2\xe4Pb\x88\x80\xba\x9f*\x87\xbcT+\xa9\x1f\xf8\xe512\x95z\xbe\xba\xf2$\xef\x03\xf2\xe3\xdf/\'&\xbc\x1a\x835Q\xf0\x92/"\x99\x1c\xb5\xe6"X\xe9A\xfc\x1ew\xcf\x0b\xe1\xa6\xe4\x90S4%\xdf\xf9\xad\xa9:m\xbe\xa1t\xe6Ruo\xaf\xa6\xe6\xb2\xf7\xc8\xbd\n\xe2c\xe7P\x18xmf\xbf\x10\x0c\xd59O\x06\xad\xc8\x82+\xdd\xc8w\xe3-\xf7\x1e \x7f\xbe\xf2F\xc9\xa12\xcc\xb9\xe2\xbeUA\xa5%\noJ\x7f\x84\xf3\xd4\x02\xc9U\xcf\x1dD\xc8G`\xd1\xf9\xc9|$\x81\x874h\xf2 \x90\nr\xcd\x15\x04i\xed\xc8\xafv\xde2,\x0b\xce\n\xad/\xe7\xa3n\xe5\xb3O\xe8\xd9\xa5\xebW"\x05\xa5\xa3\x85\x86\xa87\x81 \xcd]_\xbf\xd8P\xd9\xb2rC\x89K\x1d"0H?\xf9,\xe0\x8d5\x12\xa7\xda\xd2\xdd?\xb6\x9fE7\x86\xb0il \xc9\xf3d\xe5\xaf\xc5<\xab5`$\xb2\x9d\xc8p+\x83bS\xe27\xd43O\xa2\xbf\x8a\xa8\xf8s\x16I]\xbf5\x0fQmY\x7fsmZK\r\xdfj\xa9\x96\xd2\x07b*\xdc\n\xae\x95\xcf\x95I\xbc\x03\xd1\x8e[G\xd7\x84\xd8\x87\\F>U\xb3\xc5\xc7\xdd\x1c\xc6\xd1J\x07\xb6\x05\xe6&\x95\xd2\xd7+\xcc\x85\xa0"\x0e\xf9\xae\xa7PkE\xaf\xa2\xd6\xaa\xf1\x8b\x88ASt\xdep\xc3C\x03\xa8 \xd6\xc2f\xad\xe59\xd9\xd0Bb\xa3\x8aU(\xde^M&\xc4\xa9\xe0`X\xfe\x89X\x93\x0f\x9bu*\xa6")2\x0e\xae\xc2\xff \xac\x9e\x00H\xffV+6\xf9\xfc\xfd5T\x8a\\A_\xde(\\\x8d\xf5)g\x94zO\xe6\xa7\xda\x8d\x95D\x84zCo-\x0c\xa8*\xee\xa1z\x18\x82}`\xa0\x90tRM\xc2\x9bsR)\x01\xa8\xfeH\t@\xf5FJ\x00jH\x00\x00\xbcr\xe2\xae+\x1d2\xf4\xf9\x8da\x01\xca\xe9\x9f\xb6\xa0\x0cmX\xb1MSw[_\xa6J\xeb\xc4{\xa2\xff\xa8\xd6C\x04 \x06BU\xd1\xe1\xc8\'\x0e\xabL\x99\x00-\x17`\t\xf9E\x1c\x00\xc2\xa4\xd7Eu\xe8\x16\xb4\x04\x17Dh\x9e\xbf\x12\x83\x9d\n\xa5@\xf4\xaaY\x13A\xf0 \xe2\xa4\xac[%\xdc\xe5\xfb\x9fO|\xabYLGsY\x8a\x05t^\x0f\x93\xb5\xba\xa9\x8d:\x0b\x8d\xe6\xf4\x85\x034\x18q\x00 8(\x80\xa8\xc85\xbeg\x9d}b\x94\xec\n ?k\xa1J\xb1\xe8\xf9\x10\x07\xc2\x9d;DO\x04\xc3\x1f\xe7\xaaw\xbd\x07\x06E\xc4qo\x00\x0b\xe3\x9f*\xd0\xaa\x90h\xaey2?\x19\x0b\x910\xa8\x07\xa1\xa6,\x18\x1byUbN1\xa2\xd3\x1a\x86\xa4\x1a\xd5\x9c\xa8$^\xc5W\xe0\xa9\xb6\xabP\xa9\xef =\x872l\x93\x81e\xdc[\xb57|\xc9\xb1\x9a\x15\xb5\x010k\xe9\xde(4\x14\xe2Z\xcd\xd4]U\xd0\x85\xfb\x15Ro\xd4\xd4\xa1\x9a\xbd\xd3\x95\xc9\xa7<\x7fY\xba\x15\xae*\xd1\x0fw\xa2\xaa\x1f\x8b\xc3"\x06\xaa\x04\x15\x1dp\x90\x8c \x04\xa4;+\xef\x81y\xca\xcd\xc53\xca\xdf\xcf\xf5\x00*R\x85\x89\r \xb7\x9f\xffA4\xb4\x16\xb9\xc2\x97\xf9\r\x0f\x07\xc6E\xee\x81a\x11\xe2\x81\x91\x11/\x03\xf3\x83\x10@X\x987\xd7SuZ\x10\xa5\x96U\x96\x92\xf5\xfa\x8b\r\x08k\xa9\xf9\x18\x9c[Kb\x06\xb2\x93o@\x86\xaa\x82\xda\x94j\xcf\xab\x82\xef@t\x15\xd3Q\x1e\x8e\xd4B\xa5D7KI\xb0\x96\x1a\xca\x04\xab&\x97V\xc7^(l!\xbdJ\x16_\x01M5J/)\xb7]q\xed|\'\x91T}\x04@X\xea\xddQ\xd2\x0b\xcf\x07\xe7\xf8\xe7\x07\xe1\xf8\x1d\xe0Q\x86#\xc8\xc8\xf9\xcc\x13\xae\x8cj \xe4T\xcf;\xae\x85\xbd\x80Z\x89\x84K#I\xf8*J\x85\xec+Ha\x14_en\\OE. R\xaa\xb1\xc0\xb5 4H\x0c\x00r2e\xb9\x9aCp\xe1c\x04\n\x9d\xcf9*\xd2\x99D\xec\\DJ\x12M\xb5\xe7\xad\x01\xec\x11\xa1\xf9\x8f\xa8K \xac\xaa\xaf\xa1^\x0e\x92\xb8A~\x95\xd7\x93\xa7\x02\xe2\xa1\x800q\xff\x05\xb6\xe7_)\xa3\xda\x00$\t@\x8d\x14\xf2\xb9\x7fT7\x1e\xaa\xdb\x14}\xcfo\x0e\xd7\xe3\xfe\xbc\x82p\xaai\xa0$*\x10\t\xc4"\xd2\x11\xee\x0e\xa2\x89X\xe9<\x80\x85\x92f\xfc\x0e\x12\xc3I(3E]y\xef\xe3\xe7\xe0w\x90\x1d\x8e\xad\xf7\x04\n\x01|\xaa\xb5\xc8\xf5\x8bu\xd0\x85H\x80\xec\x00\xa9J\xb4\xf1\x9e\xf9C<\xc4\xb5\xb8\x16\xaa\x05\xcf\x00aP\x99r\xbe\x87@\x94\x9b<\xe4\xd7\xd0Kbj\xbd\x96K\xec\xe5\x1eBr\x11Wej\xe6Sy\x92\x03\xa2Z\x8bd \x19\x11U\xbfR\x12\x80\xa40\xee\xeb\x0b\xbd(-[)\xd0J\xc1\xf6!\xe1\xd5@\xfeV\x1d\xf9X\xca\xe4*]\x84\\\x86G\x7f\xdf\xa6\xb6T\x96\xde)"\xf6\x7f\xed\xdd\x07x\xa5U\xb9>\xfcxDAE\x81\xa17\x0b \x88\x85\xa2R\x14D\x04Q\x9a\xe8\xb1\x0b\x82\xede`\x18\x8aC\x93"\xa2\xd2\xc4\x06\x82\x8a(*\xbdWQ\x9a\xa0"`\xa1x\x14\x05)\xd2{\x0f%\x0cDt\xfd\xf7o\x85\x9b\xef=9\xc9L\xdaLf\xfcx\xafk_Iv\xf6~\xcbZO\xb9\x9f\x8e\xa8\xb2\x91\x99E\x8f\x812d\xd2\xdf\xb44\xe2\xc0\\\xe9\x81\x87\xf90\x93{E04>\xbb_\x97#\xd0\x1f\xa4\x7fUs~%\x1e\xccIp\xa4\xcf\xfc\xe7\x9a\xa6\xec\xb2\xcb.\xd5\x8e\'\x08L\xfa\xf1?fB\xb4\xf8\xf4\xdaU\xc5\x07\xd1\x16X\x84\x8d\xff%\x97\xc0O\xc8\xc3}y\x86\x83\x0f>\xb8^_O\x86D*F:"\x1b\xf3\xa7\x93P\xfe\x1e\xac3O\xbb\x11-\x93+\x9as8y\xff4\xf0\x8c\x18\xe3\x9e\xa41\xebO\x90F\x88\r\x86p\xd3\xe9)p\x7f\xb4i\xd0\x03\x1dI\xb2J\x13\x96\xb1<\xf7\xa8\x8f$x\x0c\xc4\x80\xf1\x0f`\xa4\x81\x8a\x83?\xe1\xe1\x9eu=~Ssb\x1d\x8aiB\xae\xfe\xfd\x86\xa6d\xdc\xf5\x9a\xcd\xf7\xcab\x9f\xbf\xbd\x8eJ\xe7\'\xc0\x00\xdeW9\xa8\x0c{\x91\xe6\xb7ul\xb9\xf6\xdb\xd3\x1b%\xd6\x1e\xd4\xd9\xceQ@\xa8!@/\xeb\x99\xc9M\xee=\xfb\x90\xbe\x82\xc3\xad\xe3H\x1d\xc0`\xffO\x9b\xee8\xc9\xfa\xcfDt\xbf\xfe7V\xc3;\x87s\xdf\xee\xcb\xf3\xda/\x8a&!\xdd\xb1\x0c\xadCD\x19s\x96\xa1\xb0\xe9\xc24\xd3\x1c\x84\xed\t\xae\xe9\xcf?\x10\xe4\x1b\x8e\xd3bZ\x89%I\x12\xc2P#E\x00I\x0f\xc5xc\x9dh\x14\x01\x90\\\xf3\x94\x9fb\xf44Fm7KmG\x01R~\xed\xd9\t\x100\x1d\x84f\xabG\xa0x\x05r\xeb}h\x1a\x92IHz\x00\xea\x9bo\xecT\xe6%\x9a\x93\xb8\xe0\xc4\xbfU\x01\xe1{\xb4\xb2z\x8174\'W\xdf\x81);\x10A\x9aq\x0e\x06\x7f\xb3^\xed.P\xf1T\xb7SX\xdd#\xa4\x01\xcd`\xfc\x0c-\xb5_)fB\x98\xaeG \xe43#I:KX\x0c\xdd\xb9\x0f\xfe\x884\xf4\x88\xc9\x10\x0123\x99?\xfc\xd0N\xe9\xcd\x10\xd9\xb1J\xaeK\xe2P\x84^|L\t\xcd\x8e4\xdc9\xec#\x95X\x89O\xd2B\x1fl\xf6\x18\x90\xa9\x86\xdb\xcay\xa0#m\xa2\x10\xe2p\xdaD\xcd\xcc\xa3-\x00\x10x\xc6u\xb5\xed>\xcf\x91\xc9\xbe\xbe\x13\x88o3=\x935\xf5=6#o\xb1\xd0\x1d\xc4\xe3\x9c\x9e\xdf\xf91\x10/=\xed\xba\xcf>\xfb\xd40\x9f\xf1hsO\xfaGy\xe1\x0e=\xb5/\xc0|[\xfe\xbd\x0eI\xd5\x1c\x04\xd3\x81\xe8\xbb\xed\xb6[\x1d\x9e\x02-`~\xc2\x04\x83N+\xb9\xc7=\xc7\xb4!\xc0\x06\x1bk\x96A\x9bq\x0e\x06\x92gpe\xf2\x0c\xdc7\xff\x00\xc7\xa1gr\xde\xe1\xda\xde\xd0GZ\xd6\xf9\xdbs\x10\xb83z\x8c\xd9\xf4\x0e\xfb\x13?\xc9PZ\xc2\r\xd7\xd7\xe4\xb3\xf1\x1d\xa5Gg\x98\x7f\xa6\xf78H\x12O$\x11\x8d\x85P\x07\xb2\xb9\xc6*l\x91F\x9b\x83\xcd\xd0\xcb\xe0\xc4\x99\xd1\x05x\xa0#\xad\xa5\xda\x1dj\xc2\xdc6\xaf\xff\xe0\t\xdfI\xef;\x8c\xef\x990C"\x03\xecis\r\xd8\xf9\t\xdf\x11\x0eP\x01\xa2\'\x00\xcc\x01\x80\x006l\xf6)+6\xc7\x95\xf9\x9b?\x949\xb7\xbd\xaf2\xff\xcb\xb7\xbe\xb5\xac\xd5\x1cZ\xa3\x07\xec~\xa3\xa9\xd6j\x0e)\x1fiv\xadB\xc0\xf9#\xa8\xdc\xd7@\x1a*N\xcd@\xfe\xc1\x9e=\xde\xebv\xceC\x04\xa2\x9f\x89\xac$\xe7\x01\xc12\x158=c\xb2\xf9\xceP\x10Y\xda\x8e\xe7~\x83\xa0f&\xf3\xb7C\xd5\xe9\x98E\xc0E(\xa5\xb0\xa9\x1d\xc5h\x7f?\xe8i\xa8\xd7K\xf6_4~:.\x87\xd6F\xeal\x1d\xf1\xd1n\x88\xd9\xce\x1c\x1c\x8f\xbcpG\xa0j\xc6M\x8d\xc7=\x0c\x94\xc5\x15\xa1\x90\xdf3\\\xc5\x9a\r\x04Om0\x01\xc0\xa4\xc2\xb8\xec\xfaL4J\x86^\xc6\xa2\xf9\x9d\x00\x80\xbe\xfe\xbb\x83\xbe\xd8\xfb`>\xaf\xff\x0b\xeaX\xb4k\xeb\xa4\x9a\x8f7;\x96\xe5\x9b\xd3\xca+\xb6\xbe\xae:\x10\xd7l\xbe[\xdf\xd7\xf9\x99O\x85VvOc\x05S\xb3\x17\x98?!\xcd\x81\x88S\xe8S\x1f\x03\xe6\x89\xbfS\xfd\xe8\xf7\xcc{\x18\xc9\xf5G"\x08\x86\x92\xa6\xdd\xfel\xdb\xd6NYpBu\x03}\xa7\xff\xda\x0e\'\xa1)\xfe\xad\xb6\x10\xee/`\xc7\\\xf8\x916\x88"\x83!\x06\xbb\xa9\xdc\xd8x\x87\x13B04\xc3\x8c\xea\xd03\xbd#\xder\x9b\x1bm\x1f\xed\xe4\xff\xd1\x92\xc9\xf8J\xd3\xd0h\xb4h@L\xc3\x9fB\x00\xb0\xf71>\xb8\xcc\x83\x0f:\xb3\xb3\xf3\xe2\xd4\xcb\x80Tp\x1fS{\xef\x15[]S^\xba\xcd\xdde\x8e\xed\x1f\xafm\xc2\xf8\x05\xb4{\xe77\x10\x01\xc0x"\x04\xae\x01\xbd\xa5O@\xff\x01/\xc3=\xb2\x0f\x98"p5Q\x91x\xb8\x83\x8a\x081\x83M\xf8\x0e2\x1e\x0c\xbde\xfa\xed@\xe7\x1f/\xb3/^w\xeb\xe3\xfe\xda>\x08\xfbh\xff\x06BIqF\x8e$o?\x887c\xd3s\x9e\xfeE[3D\xf3#V\x9a\x87\xbd6\xb3\x1bn\xce\xee\x07\xa2H\xdd>\xe8>\x98V@0y\xa5i\x05\xa1K\xb3ct\xcc\xc9\xeb\x8fAh\xe9\x9dw\xde\xb9&\xf6\x10\x0e\x89\xe5s\xf2\xd9#\xda\x94\xcf@,\x9ew\x1f\x83\xbfp\x87\xa9\xe5%\x93\xef+\xf3luc\x99{\xeb\xdb:\x02\xe1\x9e*$\xa0\x06\xceC?u\x11\x8a=N\x9b\xb4\x93fF\xc2l\x08>\xda-Z)\x8d;\xe2\xc4\xe5\xc0\xcdP\x104\x16a\xe6w\xcf>X\xf2N\x92\x83\xc6\x03e\x06~\'z\x13\xe5\x97\xc6\xb8\x11\xe6\xfd\x87\xa7do}\x7f\xb8Z\xda\xba\xe1\xc3\xa4\x04\xbb\x86\xeb\xa5\x96\xa4\x9dA8\xe6\x87\x13g6\xdf\xb42\xc6\xd2\x19w\xbc\xc6n\x8d\xf5\x11\xc2%}1\x16\xc6\x8b\xe6\xb2\x0eB]B\x90\xd3r\x8a9\x92pd}\x06CG\x89Z\xb4\xab\xc1h@\xcc\xcdI\x871\xbd\xf8\x00\xdc\x07a\x80\xd1\t\x04\x9f\xf1\xfe\x9e{\xeeY\x0e<\xf0\xc0j*\xb8/\x9f\x97\xc2\xab?\xe0\xcb&\xddT\'\xeb.\xb0\xe5_;\xbf\xdfY\xa7%\x13 F\xa9\x9b"\xe4;\xfc\x08\x04|{\xe0\xebh\xd7\xaf\xff{\xf1\x8a\x83\xaa\x18?S\xa5\xfc\xf4\xbcL\x1a\xcc\x1f\x819X\x91Q\x12\xb5\xc6\xc3\xd1\x17\xef;!m\xad2\xc5\xa8\x1d\x8bo\xdfW\xb4t^\xc3uP\xe6\xdc\x04G{\x80n\xdew\xfe\xd1L\x95\x9e\xee\x91! \xd3\xb3\x0bgD\xf1\xceX\x1d\xb1\xcd\xf2\x9a\xdebES\xd1\x86\xb4\xebB\x1d\x9b\x99\x87=\xc9\x1c\x84!\xed\xcay\x16\x18?=-9P\xb6U\xfc\'\x04\x88\xf3bh^\x7f\xe7\xc5\xe0\x1c\x7f\xc2t\x985qt\xcc\x02\xb2\xbfb\xabk\xeb\xacy1\xfe\xf5~\xdcS\x16\xdf\xf1\xae:\n\x1d\xbc\x0f\xacg\x128\x07\xf3\x81\x83p\x9e\x89\x7f-\x9a\x87\xbel\x9b[\xca\xee\xbb\xef^\xd6i\xbe\xd5\x11\x087\xd7\xc4!\x1a&\x13\x9e\xc6\xaa\xdc5\xb9\x16Ys\xeb\x9a$\xa2v\xdd{\xe6\xe7a\xfa\xfe\xb9\x10\xb3\xd2\xd1\x8e\x84\xb4\xfb$\xf4\xef\x94\xdd^\xbb(\r\xeb;\x92\xe8\xc4x\x9b\xd4\xcf\x15\xb1\x8c4\x97{V82&*\xaf\xe9\xc1\xa5\xd8\xe0 6h\xbd\xcc\x97\x1e.+5\xc7>\x97\xc9E\x00\x0c\xb7Ly\xa0\x14\xd3v\xba\'\xc8\x9b)\xc6\x87\x1ezh\xd5\xf2\x9cx\xe0\xfb\xde{\xef]\xe12F\xa9(l\xca3\xe5\xb5{?\\\xbe\xf6\xbb\xde\xb2\xf0)\x0b\x97\xfd{\xf6.G\\\xd7\xfb\xac \xb8\xb3jy\xdd\x81\xf9\x00 \x868\x131:\xe7 !\xb0dsa\x15\x08/\xdd\xe6\xb6\x8a\x180jF\xbf\x8fd\x8d\x07:R{\x91\x10_\xec\xff\xacq\xbc\xd7>3\xab*\x8f\xf6\x81\x19\x93b\x8c\xa6\x92\x8f\x9f\x17\rMQ\xb6\xf9$\tJ3\xa3\xaf\xc6\x98\x1f\xe9\x9e\x1a\x08;\xd4Y\xf0qre\x86\xe0h\xe1\xe4h\x8eh\x16\xaf8\xa1h\xa6L\xb9\xf1\\)\xc9\xc4dI1\xc6\xe8\xe05\xe6xcsRe\x1e\xffK?\xb9\xa1\x12k `\xbb\xbf[\xd6\x91@\xa1\xf11g\xe2\xfd\xec}\xdep(`\xc3f\xdf\x1a\xaf\xc7\xb4\xc2\x81\x9ce\xee\x8b\xbd>\xd7\xe4\xbb;\xc8\xe4\xefe\xb5\xef<^\xf6\xfeuo\xf9\xd0\xf1=U(@\x01\x0b6\x97\xd5\x04!\xa1\xb5\x8cs\'\xb4\xa0\x08\xd7\xe0Cp\x0e\xb9\x05\xd0\x85\xe7t/\xed1m\xa9\x01H\xf2\xd2H\xd6\xbe=\xcf \x08 \x1a-M:\xdb\xaf\xa1\xd2W\x8ed\x01\xce,\x87`\xb2\xfb\x12\xca\xec\x7f\x1f\x89\x00\xa4\xf20\xb9\xf63"\x8f\x7f\xa6\x1c\x08\x06\xe1$\\5\xd4<\xee\xd4\xaf#\xaa\xe14\xcb\x9c\x11\x07\xcd\xea\x19\xd2T3c\xce2\xe7\x8e`h\xd7\xbd\x8b\xad\xb3C\xfd\x8d1\xad\x81\xeff\x1c\xf9\xf4\x92U\xfa\x13c\x12|h\xfa\x8c+K\xb2\x8d\xeb@\x18<\xf2|\n\xec}N\xb0\x94\xfd\xbe\xb2\xb9\xa0c\xafO\xad\xa5\xa3\xf9?{?\tB\x98\xf8\xa3\xcd.\x15\xc6\xbfq\xffG\xab@0\x1eL$\xc0\xf5bW\xa7\xe4\xd4\xb3&\xe6\x8e\xe9#\xf0\\\xcfu\xfd\x1d\xf8\x9d\x86,\xa9\xf9\x1f\xeb}\xa1-\xe3\xd8\x1a\x89C\x0fs\x11\xaehrF\r}\x1d\xea\xe1\xde\x93\xc7\x11\xf3\xa0\xffH\xb2\xd9\xf2Hs\x89@\x9e\xfe&@\xecR\x8c\x8e\xe0b\xd3q&\xf9_\xb2\x92F\xd3w=\xfd\xeb\xd21g\xb8\xdfO\xe3\x8bh\xff\xc4y\x93\x87\x1fF\xc9LA\x9fM\xb2\x8dgI|5\xa1\xbd\xe1\xdadA\x00\xaec]"\x18\xbd\xa0\x8b\xffn\xf6\xacZ\x18\xc3cpL\xe7\x9a\x84\x83\xec>y\xfb\xcax\xd9\xf0\xc2\x7f\x18\x95y\xe0\xe5;\x93\xcez\xaa\x9c\xdb{Z\xb9\xe0\x9fg\x96\x83\xbb\xcf-\xf3N\xbc\xa1V\x00\xa6\xa3\x90\xeb\xa54\xd7s\x12\x88\x84\x9b\xf7\xe2\xed\xa7\xe5\x83\x94\x10/Ml\xbd\xd2\xeccF\xf4c\x0cd\x1eM7\x9b\x99\x8d\x00\x06;\xda\xe1\xb8 \xdf\xa1\x0eN\x9dm\x0f\x12X\xac\x19\xf1\x06\xc6F{\x82\xb0RN\x11\xf9hb\xf266\xf09\x99ecu\xffq\xe6\x0c%\xc21\x16\x07\x06\xa3}\xad\x11&\xb6>Bp2\xf6hd\xd0\x9f\xe7\x9e\x10\xf2\x9c\x98\xd7:b\xf2\x9dv\xda\xa9V\xfc\xd1\xec\x04+\x87\xa4u\xf7\xff=zv.\x87\xce\xfb\xf2r\xf3\x84\xaer\xfc\xd4#\xca+\xbf\xf0@M\xf8\x89\xf0N\x98\x8d`I\x13\x0f\xa8`\xa0\x8c\xb1\xf4\nl\xc3r\xfb6[\xda\xaf3\xf1h\xcf\x1c\x88#u$\xcaj\x96>\xda\xe1\rp\x12\xf1\xf1T#j\x04\x96b\x0f\x04\x97\x1c\xf8h\x8e$2\x0c\xf5Z\xb4lb\x9c\xed\xc2\x93\xb1l\xd5\xe4~R\x12\xdcN\xd2\x19\xab#\xf6b\x92>\x10\x85u\xa2y\xad\x9bL<\x8c\xfd\xb1f\xa7\xba\x86\xe9\xeb\x9fF\x1f\x98\xd4{\x1c\x91L\x13\xcc\x9fD\x99E\xb6\xbd\xa9\xfc\xac{j9\xb0\xe7+\xa5\x9c\xbdt\xb9}\xc1\xaeR6\xeb*\x87\xf4|\xad\xac\xff\xd3\x9eZ\xe1G(;/\x01\xe7;~\x12*\xa9L\x1b\xc8\xa9k}\xbd\x9f\xb8v\xc6\xc1\x0f\x07\xca\xa66\xa4\x9d\xa6:\xb3\x06Z\xa4 m\xa4\x8d?\x87s\xb4s\x11\xa2\xe0\x92\x91\x07Q\rwJ\xf6,{\xb4\xc3\x80\x81\xbf\x08\x89SjZ\x0e\xbe\x81j\xb6\x11W\xca2\xe39\x1dH\x13\xc5\xdb\xea\xfd\xd1\x16\x0f\x8d\xd7\xe1\x19\x98-\xe9\xee\x9b\xdc}L\xc9\xc1\x07=\x89\xdb\xf3\xf8C\x03\x98\\\xdc\xde\xffRO\x91\x06\x9c^m;~B\xf3\xb7\xb2U\xf76\xe5\xd2\x7f_P\xb6{|RY\xf4\xf4E\xcb~={\x95\xd7\x9c\xb7T9\xfd\xfe\xde\xf2\xda/=\\\xde\xd4\x9cP}\x0b\x89\xed\'59\xbe\x8f\xb1~\xde\xd4\n$Fc\xebk\xc4\x00\x006\x94IDAT\x9e\xd2\xe7\x99\t\x87\xdb\xd9x3\xda\xfe\x8e\x833\x99\x9bmS*\x91\x8f(\xae\x99]\x8a<\xa6\xc7\xf3\x02`d\xc7\xf3\x02\xe0y\x01\xf0\x1f!\x00\x02\xe3<\xc8HaU\xe0R\xf2\xdf1F\xf2\x99S\x0f>\xd4s\rV\x018\xda#\x89\x1cc\x95]\x96\x8a\xc4t\xf3M/?v>\x07\x1e\x07 \xfb\x1e\xc3\x8b\xd7\x0b\xcd\xc5\xd1\xc7N\xe7S\xe1\xb4K\x99.\x08\xef\\IBz]szy\xf1v\x8f\xd4p\xe0*\xcdO\xca)\xf7\xf4\x96\xd3\xba\x1f*\x87u_Z\xde\xfc\xcd\xc7:f\xc0\xc3\xd5yH\x88\xb4\x9b\x90\xc4\xa72\x16\xcf\xd8>\xe2\x08K\x82\xcf\xcc\xf0\xab\x8c\xe7\x91$\xa7\xb6\xfd\x1f\x138m\xd2\xdaC]\xe2\x13\x98\xed"\x03\xed\n\xa4\xd1f%\xd1F/\xdd\xe6\xf6\xb2\xcaA\x8fW\x02e\xef\x8eW\xedvr\x15\x94\xc5.\xf7\x95G\xca\xc1W\xf5\x96\x85N^\xa8|\xe2\xa4\'\xcb\x1c\x1d\x1b\xba\x9d?\x90>nC}~\x8e \xda\xba\xdd\xb5\x88\xa3/y\xfd\x84\x81\x97\xf7\x08\x01i\xbc|*\xfds\xf0\x93E\xe6=Z\xd4=q\xe6\xa9\xf6{CsJ]\xcb\x17m\xffh\xf5\xfc\xcf=\xe9\x8e\xda\x03\xe0%\x93\xef\xa8\x8c\x0fQ@\x12\x89:$\xc57}\xf2F#D\x93Y\x89\xe9\xfb\xef]\xee\xd9\xb5\x92\xfd\x16\x149\xbb\xa2\xb9\x81\x8e\xf8\x8f\xdaZ>!\xd5\x08\xdc4\x7f\x19\xef{\x1d\xd5\x91xk\x92g\x92\xf6\x98>w1\x01\xd2;nZ-\x8f0\x06\xa2\x7fOs`\xd5|\x08zf0\x7f2\xd2\xd2\x1f\x003c\x96w\x1c\xd6S\xbe\xf2\x9b\xde\xf2\xcd\xee3\xca\xb2\xe7,[\x16?s\xf1\xb2\xf8Nw\x95O6\xdb=\x17\xb7\xc7@^\xc3\xc9\x90Kr\x91s\xd0\xc04>\x86TS\x90l?\xc5:\xc2|\xf2\x00$\xed\x88\xef\x07>\x86\xb0 \x08\xeb\x9bW\x08l\xd5\xe6\xc7\x9d\xfb\x7f\xb2,\xd0\\]s\xfc1=A\x06E0)\x08\x19("\x89CL6\xdfm\x0b\x80\xd4\xea\x0f\xd6o\xae]\xd7\xef\xf3\xed\xf4\xe0\xc0\xdfh|\xc2%\x08\x85\xa9\x13&Hjo\xbb\x1b\x12\xe7X\x04\xc2H\x9a\x80\xcc\nG\xbb\xa6\xc1Z\xc4\xd9\x1b3 \x95y\x84\xe1x\xcd\xd5\x18\xd3#\xb6\x15\x82$\xd1\xf2j\'\xf9$sl(\xb9\xdc3\x1b\x06!@p:\xad\xc3h\xe2\x1f\xfc\xad\xb7\x9c\xfd\xf4I\xe5\x90\x9e\x03\xcb\xe7\x9f\xd8\xb6\xac{\xebz\xe5\xd5\xe7\xbc\xbav\xd3\xc1\xa8\x987v\xfbp\xd1O\x9aBX\x07\xa8\x87\xb6wN)\xbd\xba\xf8b\xfa\xbe\xe6\x1d\xfb\x96\xc5\x9a_W\xa1(>o=\xbd\xd2M\'\xb01\x11\x0b\xc4E(\xcd;\xf1/\xd5\x0f\xb0\xf27\x1e\xabU~_\xf8\xc2\x17\x9e;\x87\xff\'\x87\x81\xdf\xc0\xf5\x13\xcfO[\xaa\x94(\'\xe47\x10\x13&\x99+\xdd\x80\xdb\xc4\x1c\xd8\xeb\\\xee\x17*I\x14\xc3\xda\xf2iX\xbbt,\xf2\xbb{\x88O \xa5\xc0\xed\xa4\xa8\xd9\xe5\xc0\xe8\x98~Z\xfd&R\xb03\xb3\xefm\xcc\x8f\x10\x87\r\xa4\xb1\xe5\x98oq\xea\x93\xe5M\x07&I\xab\xdd5\x06\xf1Fs\'D\xe8\xba\xf64\xc3F3\xa1\xc8\xf7\xd3s?\xed\xa8\x9c\xcfZ\ni\xeaD\x0c\xd5@2\x9e%)\xcdL\x9f\xcc&\xe0\xdb`\x0e\xb9\xa6gI\x8f\t\xe7A/\x04f\x1b\x99d(F\x8anf\x15(\x9d\xf0n\xba"\xb7\xdf\x0f:\x1eN\x93\x8fY\xfa\xb0\xf1\x1e\x92v\x91n\xbak\xf7\xfe\xe5\x99\xfb\xf6,\x07\xf7\x1cP\x16=m\xd1\xe9>`\x9c\x7f\xe3)\x04\x10R\x98\x10$fk\xbf\xb79\xa0\xbcz\x8f\x07\xcbZ\x1d\x13\xe0U\xbb=P\x05\x9b\xb6Yz\xe8\x89\xb7\xd3`\x81\xfd\xa3A+\xed\xe7\x86B0\x04\xfb\\\xc3N\xad\xb84\xef\xfcL3\xb1^\x93\t\xd0\xd6\xcaITJ\x16c\xbb\xeb\x92\xfb\x8b3\x91\x89\x81\x81iZ>\x87\x14\r\xa5\xb489\x1a\x11\x00iN\xe2w\x0c\xec{I}\x86\x16\x08\x02B\xc3\xdf\xaa\x08\xfdLrR\x9a\x97\xa4\xf7\x9ck\x8bf,\xdf\x9cZ\xd3\x95\xf5\x1a\xd4[\xc0\xf3@%\x9eQ\x8b\xb2\xb76?-\xafn\xce\xad\xd5\x89\xbeG\xa9\xa4\xfa\xd1=$\x01\xc9\xbd\x11\x10\xee\xcd{\xed\xe1\xa43\xb5\xcf\xddt\x8e\xf6\x90U\x7f\xa7<9e\xbb\xe3\xad\xf4\xc6\xec\xd8\xb8\xf9J\x07n\xfe\xb92\x86\x0c\xb3\r:Z\xf3\'7\xf4\x96\xcdN~\xb2\xe6\x9e\x0f\xf4\x1dH \x91\x03v\xe4x-F\xa0Z\xbb\x9f>-K;\xed\xb7\xdf~\xe5\xc3\xcd\x17\xca\x1a\xcd\xf7k2\x0e\xed\xa50\xe6\xdb\xdf\xfev\x85\xfc\x08.\xa9\xc3#\xb9v\xbb\td\xe02\x82\xcft\x1e\xf9\xfa\x8bn\x7fK-\xdd\xc5$\xfc\x01\x980\xccO\xdb\xf5\x9f\x05\x87\xf1!\x04\x82\x89\xb0Ho\xff8\xfa\xd2_\xaf]\xf4\x94\xc2\xa6\xc0\xf8v\x17\xd9$\x08%u\xdb9\xad\x8f\x9f\xed>\x03\x04\x8d5sn?1\xb4n\xc2z\x0c\x12`\n\x8ft\x1b\x9as\xdb\x07\xcb\xcb\xb6\xb9\xab6\x1b\x99k\xf2\xfd\x1d\x81z]-;\x9ek\xdb{\xcb\xfc\xcd\xd5\x155\x8aZ8\x07$`\x0f\x96k\xce\xa8\xcf@\xc0\x10D\xd0\x04S)IL\xae\xe9\xd9\xda\xbd\xf4\xc7\x96J\x86\x7f\x04\x01@)\xe8<\xbe\xb1\xfc/\xfe\xb1\xf1\xbe\xcf19\x96\xdc\xf5\xfe\xb2\xce\x8fz\xca\x9e\x17\xf6\x96-\xcf\x98\xda\x11\x06\xd7\xd7\xd8\xb5\x97L\xb6\x81\xbe\x83\xb82\xdb|<\x8b\x80\xda\xc5\x19\x88\x1fc <\xf7\x8dyfd\x88\xca\xf5\x92\xfa\x1c\xdf\x08\x02\xc7\xe8\x04\xc0\xa2\xdb\xdd\\v\xbf\xa0\xb7,\xbc\xcd\r\xd5\x17\x00:\xd3\xa6\xee5\x03Y\x93\r\xd9\x86\xc143\xa6d\xeb{\x06&\x05\xa6\x01\xa51JJj\xe3\x84\xcd\x18\xef 9\x9a+/\xdfq_\xf1\xe5\x10@\x98\x1dC\xd2\xfc\xae\x035A-\x9c\xa2\x04\xc2\x8b\xb7{\xa82\xb2<\x03\xc8\xe9\xe5[\xdf\xd6a\xf0\x07\xcaB[\xfd\xbd,\xfd\xc5\x87\xca\xea\x87L\x02L\xc7\x86\xa7\x99\x13II\xa8)i\xa5\xfe\xef\xbb"\t\x1a~\xb0\xff\xe5\x040\x05\xe2u\xcf\xf0\x89\xfe\xf7\x144\x94\t>\xedW\n\xa10\xb9\xfd\xc5\xfc\x991H`\xbdx\xbb\x87+\xb3cz\xcc\xbd\xc91=e\xe7s\x9f.\xab\xfdn\xb52\xdf\xd1\xf3\x95\x9f\xde\xd8[\x0e\xfdso\xf9\xfa\x1fz\xcb\x17\xba\x0f(\x8b\x9d\xb1XmN\n\xe5\xec\xd2\xf9\xdcNO\xecP\xca9\xaf/\xef\xfc\xc7:\xe5\x1b\x7f\xec-\x8b\x9c\xb8H\xede\x00E\xf6\xb5.\xffS\x99{\xeb\x1b\xeaz07\xa10(\x86\x10\xcc`\x8dY\xc5Y\xd8\xf6u\xcd\xaa\x8dK\xc6\xecH(\x8c\xcd\t\x1e\xb3\x9f\xe3\x19\xc6\xe4\xde\'\x0c\x08\x89\xc4>3\xe5\x16\xb1\xd2,\xed\x06\x1c\x81\x9f\xd1t#\x91\xe6#i\xaa8#\x0e\xf7\x9fv\xdf\x9e\x13#A>\x19\xf4\x19G\\F?\xd3\xd4\xb4\\lk\x8e\xbc/\x93\xf0-\xcdQU[ch\xbe\x13\xd3\x86\xc0\xfe\xb9&\xdfUM\xc2\xbd{v/\x7f\x7f\xea\xa4\xf2\xc9G7+\x9b\xde\xbdi9\xeb\xa1\xde\xca\xe4/\xda\xfe\x91\xb2@\xf3\xd7\x9a\x94\xf4\xf7\xa7N.\x17tu\x95\xe3;\xaf\xf2\xcb\xb7\x94}\xbb\x8f,+|\xed\xd1\x0e\xe3_U>\xdblY\xcd\x01!Q\xebC\x081E\xd0\x15\xe1G8\xb97\x08\n\xdd\xa5U\xd8\xcc\xdf\xf1\xbe\xa3=\x81\xc8Z\xce\xb6\xb5\xfeC9\xd8ai\x13\xddn~\xe8\x85\xa9\x11\t\xc2\x0fD\xcd\xdc;\xda\x8e$\xf73\xf0\x13\xb3\xfb\x1f(\x17a1\x12\x010\xab$\x94xV\x82\xcf\xf3+\xd8I\x06_Z`c"\xff\xf3\x1e\xe6\xf7\x7f\x84L\x18`b\xfd\xf8\x10\x8f\x01\xadA\x92\xad2\x01&#\xb8y\xd1k\x7f\x80-\xaf\xa8i\xc0\xceA\xe8\x8e\xa6\xa49y\x12i\xd0\xe9\xde\t.{Dx\x11t\xee\x9f\tH\x90-\xd3\x9c]\x87\x8e\xf6v\xee\xe7\x0fk\xacQ\xbe\xf6\xe9\x8f\x96\xb2fW\xedMx\xf6#\xbd\xd5\xc9)\xbc\xe9\x99<;d\xe1\xfbL\x89e\xbf\xfcHYr\x97\xfbj\x02\xd3\x02\xcd\xef\xeb\xf4\xa2\xf9&^W\x85\x86\xecF9\x12\xc2\x85\xd6\x96 `\xe2x^(\xc0\xef\xeegV\xe8\x1d\x98L\xc9\xb6\xc3wv:\x86T\xf7B\xd3\r\x94\x06\x9bB\x97\xa4W\xd1G\xe8\x19\xd7\xbd\xf0\xa4\xeb\xcb\xbb~\xd8S\x0bv\x9c\x03\xcc\x7f\xfd\xbe\xdd5\x87\xc2\x84\x1ekGC.\xda\\\\\xe3\xeab\xea\x04%\xe8\x8d\xd9\x99\x02\x89\x83#\xfad\x13\xda\x0fB\x06S"b(!N\xc2\xe1\xf4\xcbG\xec\xf6.\xfd\x1e\xdbS~\xd9\xf8\x9e=L\x9f\x90\xa1\xf7\x10\xfd\xdb\x9b\xc3\xca\xeb\xbe\xda]\xcb\x8f\x1f\xe8\xfeN\xf9\xf3?\xcf.\xef\xbcq\x9dr\xd4M\xbd\xe5\x9d?\xe8\xa95\n\x04\x1bg%\x85\x10\xc1\xc9\x17\xc2\xdf\xb0`\xf3\xbb\xca\xfc\x9e\x0f\xbaxAg\r\t\x00\x1d\x8a1=\x81\xe6~\xd0\x1f\xe1A\x88xo\xa4(\'\x83H\xd2"\xcd\xda\xf6\x9f(\x1d$;=\x87\x1e\xe6\'\xa03\xd2\xdd\xfa\xffG\xa4\xfd\x8e\xe4\x18h\xa6\xbc\x05J\x02Jr\xc8gV;\xf1\xd1xc\x118\x8d\x87\xe8h/\x02l\xae\xc9\x0fTG\x97\x94[Z\xcb\xa4\xdde\xf7~\xa4ltTOMx\xa1\xf5i\xe4\xcd\x9b\xc9U\xb3-\xd2\xd1\xee\xba\xf4~\xa8\xfb\x83e\xa3\xfb7.\xeb\xdc\xf2\xee\xf2\xf1\x93\x9e\xec\xc0\xf5++\xd3\xcf=\xe9\xf62\xa1\xb9\xa6&\xceLh\xae\xa8!@\x1a\x0f\x92\xe0\xd5\xa7\xd51\tg\x1e\x08\xce|\xc0\xf8\x04\x85^\xff\x84L\x8a\xa8\x86\x12\x16K\xf3\xd3\xe9\xf52\x8c@G\xd8\xf6-\xf3\x07\x92\x89\xe7\xf7\x985^I8\xf2\xfc\xcb\xef\xd3]N\xea\xbe\xab|\xf2\xd1M;\xc2\xe0\xe0\xb2VG\x10\xec\xf3\xdb\xde\n\xf3\t4B\xc5y\x98\x13\xd6\x16\xcc\'\x08\xe7o\xfe\xdaAD\x0f\xd7\xb5\xcb{K\xef\xf5P\x91{2o\xc7$\xf0^\x9a\xa8\x0e\xd6\x859I^y\r\xf4l\xf1\xd9\xb8\x07\xc2\x88\xc9\x069Y\xd3d\x1e&\xaa\x15DK\x10`\xec\xe1\xf8\x9b\xc6;?a\xa0#\x99\xb83\xec\x02\xcf\x0b\x80\xe7\x05\xc0\xf3\x02\xa0\xef\xf8\x8f\x16\x00qx\xf4\xef\xdc3P\xb7_Dg\xe38}\x10\x8a\x8dCP\x92U,\xea\x8c\x1ac\x94\xea\xb5\xc4\xdf\x11.\x08\x9b!\x9d\xed\xcf\xa6L5\x7f[(\xcc\xc6vgoJY]\xab\x03e\xbf\xda}D\xd9\xe4\xe1M\xca\xa2\xdb\xdfZ\x96\xd8\xe9\xee\xf2\xca\xdd\x1e(\xaf\xd9\xe3\xc1J\xa8\x86s\xf0g\x80\xe2\x12d\x0c\xe8`\x07\xb3\x87o\x7f\xa2c\xb7\xf6\x1c\\\xdeu\xf3\xbb\xcb\xfb\x1fy\x7fi\x1e\xfbly\xcf\x9d\xeb\xd7\xec?\xf3\xf9\x08\x17\xce;&\x038/\x06.w^b\x0f\xe2d\x06\x80\xc8\x19\xf6IHH"Jte(\xc3M\x1cI\xbajWs\x0e$$\xe3\xd3\xb1\x86 7\xbf\x85\xfdJ\xc2W\x98\x889@H\xba\xbf\xbeuz\xb0\n\xc3co\xed-\xef\xb8\xe1]\xe5\xb2\x7f\xff\xb2,s\xe1\xb2e\xdb\xb3\x9f\xaa\xe1?\xbe\n\xe7\xb5\'\xcc\x17\x0c\x88\xb1%\x13\x11\x00\xcc\x9f\x0f4_\xac)\xd1\x8a\xcb\x98\x8cs\xec\xf0XG@v\xd7s\xb7\xab\x11\xdb\x8a\x06\xcd\xf9\xdb\x1e\x12\x96)\xf2J\xaas\xea\x08\xf8+\xf8\r\x08U\xcf\xc5\x8c\xdb\xbc\xd9\xb6\xbc\xa7\xe9\xcboH\x85b\xea-R.\x9d\xf1\xde)\xc6\x9a\x1dK\x96\x1d\x11\xec\xa3:\t\xe2H\x19j\xdaY\'\x0c3\x10\x02\xb0\x88\x99\xee\x9a\xc4\x18v\xdd\x84-\xaf\xaa\x9b\x11\xad\x96\x16\xd5\xceg\xa3\x08\x0c\x9b\x96\xf3\xd8\x14\x047\x98d\xef\x7f\xa4\xe9\xa6\xfb\xc4\xfc6\x9d\x93\xcdu\xe2\x85\xe7\xa4\xfbL\xb3Ue86e\xbe\x8bQ\xdc3G\xd5\xfc\xcd\xdf\xcaAW\xf6V\x86\xbd\xe3\xf1\x1f\xd6\xf0\xd6\xbcG\xcd[\x8e\xe8\xfeS\xd9\xef\xd2\xde\xaa\xa98\xad\x84\xdf\x12\x06]l\xca\x1de\xa9\xf3\x97.\xe5\xf6\x1d\xca?W\xe8*\xd7=uRY\xff\xee\r\xca\xcf\x1e\xee-\xef\xbdk\xfd\xf2?\xcf\xfc\xa2\\\xf8\xcc\xcf\xea9\x10:\xaf\xb78\xb9\xc9<\xb4:\xe6\xc6\xe4\x84\x81\xf3n\xdc|\xb9:\x05\xd31\x08\xf3\xb4\xb5\xa15\x1dn\x95\\\x98|Z(\tC\xa9\x89PiHC\x86\xf9\xa1\x08\xf7\xc09\x87\xf1\xf9t$\x04\xcd\xd9\xd1\xe0\xaf\xde\xfd\xc1\x1a\xf77\x98\xe4\x98[\xfe\xbf\x01%\x1c\x9d\xec\xfdDM\x083?\xd5\x96\xf8?\x81\xaa\xa4y\xa1I\xd7\x97E&\xff\xa3\n\x0cL/\'\x82p\\\xa4\xf9\xeds\tQ\xfd{\xff\xc79\x9b\x89GA%\xf6=~\x13\x8eY\xf7\xb7ZsDMe\x162\xe5k\xf0\x9ek\xba\x07\x91\x9b\x84m!\x94$\xba\x11\xae\xae\x97\xa2\xa7TJ\x8edx\xc9x\x1e\x89\xae\x8d\xea$q\x86\x91\x96\xa4?\xc6B\x8c\xfeG\xc3\xf6\x8f\x10\xf8\x9c\xff#\x1a\xc8\xc1\r\xd8x\xe1/\x8c\x076Zh\x1a\xce&\x81\x85\xb4\x9c\xfco\x02"\xc5\x14\x08\x07\x11\xba\xc6p\xee\x17\xb1 \x0e\xd7T\xb8B\x93!DN8y\xeb6^\xa1\nfj\x7f\xcf\x86\xbb\xd7\x1d\xcfy\xba\xbc\xea\x17\xaf*\xe7\xfd\xf3\xf4\xd2\xf3\xe0\xbe5\x89\xe5\xcc\xa7\x8f\xef0\xf2\x06e\xed?\xad]\x1d{4\x96\xe7\x082\x02\xdf\t\x0e\x8e,\xf9\xee\xeb\xdc\xba^\r\x87\xc9\x82\xa3!w?\xbf/UV\n-\xb4@\x88\xac\xf9\xfd\x9e:\xa9W\xd8\x8b\xa0J\xe5\x1d\xd8L\x03b\xb4\x0c\x1d\xcd\x0c\x83\x94\xf0\x06\xb6\xb6\'\xf9\x8c\xc5\xe1\x9c\xf6E\x92\x11\xc6\xf2^\x92~\x08#B\x8a\x80\x82T8-7h\xf6\xada\xdd\x97L\xbe\xb3\x9aI\x90\x93\xf8\xfe+\xb6\xba\xb9~\x8e\xc6\xb5\xfe\xc2\xc0\xf3luM\x15.\x9f\xeb\x98W\x18\xdf\xb85\xa3\xcc>p\\OY\xfb\xf0\x9e\x1a\x1eTD\xc4\x1czGshY\xbc\xf9\xd5\x80\xcf\x95\xb6f\x84_\x04\x00\x1a\xb1&~\xa65\xbd\xe4\xa5U\x0f~\xbc\x8eV\x9bo\xe2\xff\xd4\xf9\x8e\x0bN\xfc[uP\xda\xa3\xcf\x9e>\xb5\xae\xb1\xe7\xe2\xa4\xf4l\x1c\xb8\x14E\x06o:g\xfa8&G\xa2\xadQg%\xd8?X\x11\xd2\xa8\xd16\xc2\x8b\xc6\x06\xb7\xda\xa5\x99\xfd\xa1\x99\x03C\x80\xaa\x88\xdaO\xdf\xb1I\x08\x01C\xb3\xaf\x118\xef\xf9\x9c\xdb>\\\xed\xc5\x8d\x8f\xee\xa9\x9b\xc2s\x0e\x1ekk\xad\xf0\x08\xf4F\x8c\xedX\xeb@Z,YYl7L\x84\xd9i\x11Z\x8a\x96ET\x84\x800\xd4\xda\xcd\xc1u\xd3=C\xbb3\x8dM\xe6\xa9^j\xcf\x87j\x7f\x80\xd7\xfd\xe6\xf5\x15\xd6/\xf5\xb3\xa5\xcaw\xfe\xd4[\x8e\xbb\xad\xb7c\xeb\xdeSV\xfaz\x1fD\x15\xefO\r;\xd8\xee\x9eu\xe6\xd1\xad\xe7\x1d\xdf\xef\x1b\xd6\xe9=Z\x87\x7f@\xb1\xcc\xfb:\xcf\xf9\xd6o?^\t^?\x82E\xb6\xbb\xa9\xc3,\x7f/\x9b5\xdbW!h\xbd\xd28\xc4\xef\x9e\xc5\x9aeh\x88\xbf\xddg"\x04\xd6v\xb4\xb3\xfc\x10\x88\xf5K\x06 \xe6!<\t{B\xd2u\x10\xbfka\x141y\x15\x8c\x1fiv\xadL\xfe\xc1f\x8f\xaa\xedkQ\xd0\xa4\xebj\xcdHWG\x18|\xa8\xd9\xbd"\x05\xf1|\x82\x1e\xd4W\xf8d\xed\x14AaxEB\x92\x82\xa4\x05[\xaf\xbej\xc2\xdbj\xf8\xb4k\xca\xbf\x06\xad6u\xafi\x9c\xd2\xee\x99h\xcd\xb6h\xd4.tW_\x8c\x06\xa9+\x1e\xf8hElj\x0e\xb4M#d\xf8b\xfc\x9d!\xac\x14\x8fg\xb6\xd6\x04@\x9a\xa7\xf8\xe9\xdc\xd0\xa2\xf5 \x0c\xa6\x15\xcd\xcaL\xc9\xb4_k\xff/\xe3\xde\xfb\x7f\'\xd1\x87\x91F\x12\x12\x95HO\x876O&*4\xaaTe\x8b\x8d\xe80q\x7f\xad9\xd0\x81Am\xf8\x84-\xaf\xe9\xc0\xaf>m\x1f(\x0fBb\x08\xcc\xfe\x86\xfd\xba\xeb\xeb\xf6\xc7\x8f(\xdb?\xbeMY\xf97+\x97\xfd\xba\x8f.\'\xdc\xd1[\xcex\xa0\xb7\x9c\xdd\xfdT\xdd@\xda\xc4\xf7\xdd\x87\xc5\xc2\xac4R;\xd4\x15\x8dH\x83\x93\xe2\x06d\xd2F5\xe9f\xbb\x9b\xab\x96\xca\x9c\xbc\x14)\xa5w]:\xd3`@Z\x0c\xe1!\xce\x03\x7f\xdf[m}\xb0\x946\x12\xf7\xfe\xf6\x15\xbd\x95\x911-\xa6\x8c0Bmj\x85\xce\x08r\xa1\xe6\xb2\xaa\xf9}\x9f}\xed; )\x86!(\xe7\xde\xfa\xc6ZEG0\xa4YI\x86\x94\x8e&-6\xfd\x1e\xad\x1f\x01\x8f\x88\xfc\x9e\xda\x86\x0cy\xb1v>\x9fQf\xd0\x9a\xd0%\xa7%&\xf7l|\x1b\xd0\x15&v\xaf\xcb4?\xab\x9fc\xca\xc8\xf6\x93\xc8\xa3\xf2\xcf~bv\xce\xc3u:h\xea]\x1daH\xe8r\xfcAD\x92\xc58X\xbd\x06\xbag\xc4\x9dA\'a\xc6\xd4\x9f\xa8]\xa08>u\xea\x93\xe5\xcb\xdd?(+_\xb1J\rM\xaa:$\x04\xfe\xab\x83\x06^\xb8\xc3\xe3\xb5\xae!>\x15\x0c\x8f\xb1\xady\n\xa0\xb2\x16\x81\xfd~O\xb6\xa4\xb5\x19l=c\x96\xe0\x97\xfe\x9f\xb3v\xd6\xaa\xfd^{\xa4\xd8H\xb3\tc\xfa\xa6\x8c;\xa5\xd3\xfe\x87_2\x9bp(9\x0ei\x8c\xfb\xbf\xde\xc4$NN\x92{\x88\x81\xec\xfe\xfe\x0f\x85x\xd9\xb6^6\xdeBC\x11\xe6\xd8\x91\xf6\xeb\xfee\xddZ1\xb6\xe6uk\x97G\x1f:\xb0\xfc\xf6_\xe7\x95\xef\xf7|\xb3|\xb3g\x9f\xf2\xe5\x9e=\xca\xca\x97\xbf\xb5|\xf8\xb6\x0f\x97O=\xf0\xa9\xda\xed\xf6\xab\x17\xf7V\xe2"<,,[\xde\x84[\x84\xe9zi\x96a\x11\x11\xa0b\x13^h\xda4IH$\xbd\xfbJ\xf5[68]qh\x01\xcfH\x0b|\xb0\xa3\xbd\x08\x030\x17\xc3yA\x11\x1c\x7f\xd0\x05mL#\xa6\xa2\xd1y9\xf4\xfc\xef\xa3\xcd\xce\xb59\x06x\x89H!&L\xcal@\xec\x04\xc8\x1a\xdf{\xa2l~\xca\x93e\xb1\x1dn\xab-\xc1$\x16I\x8be\x06@J\x04e\xa6\x07yV\xefgz\x90k#0\xeb\xd9\xees8\xd2\x83\x10H\xcc?\xc9?\xd6\nC f\xf7\xc3L\xf1\xff\x0c0a&@\x02\xd6\x8a\xe3\x940`\x06\xd0\xbcK7?\xaf\xf6~\x1a\x9fx\x85\xd9\xac\x89\xcf\xb1\xf7\xed\'\x04\xc0L\xf27\xb4\x04\xfd\xf1\x11\xf8\x0c\x08\x9f\xd1l\xc9?\xc0\xe8\xe9D\x84\xd0\xe3\x00\xcc=\x11HL\x10\xb9\x08\x1c\x93\x9a\xd6\xf0\xe7\x88\xde\xc8D\xb5\x7fsl\xffDY\xaa\xf9EE\xa8AW\xee-\xfe\xa9\xcc\x82l;\x053\x16~z%\xee\x11\x004=a\x8e\x9e\xe49\xa4\x8c;\x05_\x9e!\x15\xb1A\x00\xa9\xf9\xc8\xab?OQ61\xa9\xfd\x0c\xb4\x87B\t\x01\xeba\x7f\xb2&\xee\xd7\x9a\xf9^\xff\xf3\r+\xc9\x8e\x84B\x10\xdaW\xc9\xca\x1a\xc8\xee\xcf\x11\xad\x9c2V\x8b\xa0\xe2\xcb\xc6X\x140\xf1\xe4\xbb{\xcb1S\x0f/\x0fTn\xdb\xbe|\xac\xfb\xa3\xb5\xd0\xc4\x06!\xb4\xcft4(!\xd0\x8e*\xf8]\x9f\xbc\x9f>\xf9\xbd\n\xb3-6\x06\xb1)4Y^\x98\xc8\xfd$\xf5\xd5\xc6f\x06`<\xbf\xed,\xc7\xf4\xb1\x1b\xac\x10)-\xd3\xb4\xe8f\xe3\xd3\x88\x186\xd9\x92lG\x1aM7d?\xd9\xbf\xc2\x83\xf3L\xbc\xb1\xa6\x07G\xcb"H/\xc4L\xd8Z\xe7vV\xa0\xe7!\x14|~Z\x9ah\xb8\x87\xebd~\xa0\xfb\xc0\x1c\x98\xca5\xdd\xcf\xcb&\xddR\xf7\x12\x83x\x1e\xf7\x06\xe1\x10H\x84\x13\x1b\x9f@\x80\x0e\xa0\x9e\xf8u\x92\t\x8a\xf1\x93\xd0\xd47\x9e\xfc\xd6\xaa\xf5\t\x00\x82@cS\xbd\x18\xd7m\xbeY\x056\x01*\xd1\x8a\x10$\x90\xda\xb3\x1b\xe3\xf1G\xe0\xf6\xd0=@F+<;\x07A\x88\x15\xba\xa04V\xbdl\xd5\xf2\xc5\x0b{\xabR\x90\xbcD\xa8\x10\x10L\x16\xf7&\xda\x816\x99\x87\xe9c\x08\r@\x93\x98\x12\xa3%Sr(\xce4\x9fs\x7f\xd0\x82\xf4n&\xee&M\x1fj\x13\x01\xb1~\xf6v\xa0>\x93A\x03ma\x90\xff\xc5\x9b\x9fb;?\xfb;#\xedU\x98\x1f\x8f\xa4\xabU\x7f\x05\x11\x813d!`\x91\x95un}\xd6Se\xc1\x8e\xa4\xb6\x816&\xccN\x82\xbe\xa8c\xd7!|L\t\xde\xd3\x00\xfe\xc7\x91b\x11\x11\x14\x93@\xfe\xf8J\x7f|K)\xe7,_\xca\x1dS\xca\xef\xff}Q)\xef\xee*e\xab\xce\xeb\xb0\xae\x0e\x1a\xf8Z5\x07\xb6\xec\x9eX\t\x84\xb7\x9c\x04\xe7\x80c\x16\xd8X\xde\xe3\x8f\x9d\xd8S\xbe\xd7}q\x85\xf9\xae\x03\n+(\xe1\xf0\xf9\xfe\xd5\xbde\xff\x9e/\x95=~\xd9[^\xd3\x9cS\xb3\xd1\xe6=z\xdeRv\xee\\\xe3\xaa\xf7\xd7A\x9a\x1aW$54\xbd\xdb\xdb\x9d\x8c-\x1a\xad\xf3\xea\xe6\xbcj\xf3\xb3Y\xc1\xd8\xfek\x13\x08\x9d\xf4Y?\xfb\x87\x8c00\x82\xb5\x8e\xec[\xeb4G\x87\x109\xd2\x98\r\xb4\x9e\x9cx\xcf\xc2\x86\xa6\t\xe5\xda\x9b\xfck3\x13\xd6jw6\x1a\xd2\xc6\r\xe1H\xc6clD\xd7C\xac\x18\x16j\xe2\xb4\xa3U2\xc57\xe3\xc2\x10X\xf2\xf2!\x04\x85M\x08\x1b\x81\xa3\x07\xe9\xbd\x9cj|.\xcc\x01Z\xd0\xf9=G\x06\xaf:?\xc7 \x13\xc1\xff)\x19\x11\x00\xe6\x1a\xd4\x00\x11}\xb8\xd9\xf5\xb9\xe6-\xfe\x8f\xf6\xa0#\xcedJ\xe0E\xdb=Z\xd7\x8b\xcf@^\xc5\x02[\xfe\xb5\xbc\xe7\'=5\xfa\xa2\xdb\xf3\x06?\xed\xa9\xad\xdf\xa0/L/\xd5X\x18\xd2u?\xdeL\xa9\xb4a\x8e"t\xe0\x99\x93*\x1c\xa6!\x08b\x16\x0c\xc5\xfc\xcd\x01J\xbbg\x02+\xe6\xc5\xf4\x1c\xb6\xd9\xdf\xf4\xd4\xec\xdfsp\xb0\xc3z\xc6|A\x1bA\x15c\xe6\xa0D\xb8\xec\xaa\xf7\x1d\xd3S\xde\xf3\xe3\x9e\xea\xe0B\xb46\x9ed\xb61\x16\xf2%\x93\xef\xaa3\xebhy\xc4\xe0fx\xc5\x97\xbdh\xb9\xea\x11\xe7\xecy\xc1\x94\xbeM\x99\xff\xf8\xf9\xcb\xc2\'/\\\xdbp\x97s\xdfP\xae\xee=\xb3l\xfa\xe8\xc7k\xb8\xed\xbb\x7f\xee\xad\x9b\xc8\x16\'H\x84\x99\x0e\xe9\xbe\xb0l\xf4\xc0\xc6\xe5\xf2\x7f]P\xbeuE\xdf9\xd8\xe7 \xba\x85r=5\xe8_\xea\xd9\xad\xdc\xbdhW\xb9\xe6\xac\x13J9o\x85\x0e1]W\t`\xcb\xc7:\x08cJW9\xa8g\xff*<^\xd1!\x80\xcc\xae\xcbt\x97\xb6\xa3\xc4\xc6#n\xa1\xba/\xfd\xaa\xb72\'8\xd7\x7fBr\xe2\xec`X\xfc\nm\xa9n#\x98\x1c\x88\x8b\xe3\x8c\x9d+\x91\xc83\x81\xff\x92^\xe4\x15H2\xb2\xa6\x08t\xa9\x8e\xd0\xe2\xf8D\xf8\x192\x19\xad0T\xa2\x18\xea\xe1\xfe\x9c7\xcf\x8e\x00\xed7\x84A\xc8\xa7\xc7 M\x12t\xe4\xb3\xf66c\xca\x118-\x9c\xa9\xc6\xee\x1b\x03C\x0c\x84\x18S \xe9\xbd\xc9%\x80 \xe4=\xd8cZ\xd8w]\x8f\xd0X\xa99\xb6\xa2\t\xeb\xcd|$(\x9c\x93`\x826\xdc\x9f\xf3\xad\xda\x1cQ\x1d\x91\xceO\xe0@R\xfc\x10\xe8\x02\x9d.\xb1\xcb\xbdU!\x98\xf1@\x90s\xfe\x8a\x06@\x01\xefh\xbe[\x05\xba\xf3I\xc6R\x90\xc4\xdf\x02\xfd\xa47\xa3\xf5H\n\xf1H\xba\x17\xa7E8A\x10t\xd9\xdf\xce\x8f\x03/\xb9\x08\xa1\xc5\xec\xcbPr=b\xa2\x04\xcd\xa6\xfc~8\xf7:\xcd\xc3\x82\xf3\x9a\xb2\xabN\xbb\xaf\xb7&z\xf0\xda\xcb\x83O\xb1\x87\x8b\xdbp\xe1\x1b0\x10A\x90\xeab\xe9\xbf\x9b\xa3\xa3yo\x99\\\x0e\xef\xfeCu\x82\xc8\x9bg_/\xd9\xd9\xa0\r\x8f\xea)\x97\xfc\xeb\xfc\xda\x9d\xb7\xcf\x13|g\xbd\x96Zq\xda\x07c\xbd\xbe9\xa56\xef\xbcn\xea\x89\xe5\xd2\xff\xea\xaa\x8e\x1d\xce#\x1bG\xf8Xd\xda\xff\xd4\xa7\x8e.w.\xdcU\x9e\xee\xdc\xaf\n\xb5\x1d7\\\xbb\x86\x816\xbbw\xb3>\xa4\xd1\x11\x00\x1b_\xbf\xf1s\xa5\xc8\xa9\xb2\x1b\xa8\x96\x1bA\x8a\xc9C!;v\x7f\xa9,\xd2\\\\\x05\xdep\x06=\xa43\xb0{T?\x80\xc9\xc1]\x88B4\xe0\xcb\xbf~\xb6\x0b\xf1\xc9O\xd6Y\x04\x84#?\x02\x8d\x08^c\xac1\xdb\xc4i\x1c\xf1\x12#\xba$\xed\xb4\x1dl\x19\x04\x9b\x9c\x0e\xefe\xceB:\x0c\'S\xcf3\xdbw\x8dJ|\x87\xe0\xc3h\x14D\x9a}`0\xbf+\x80"X\xads\xb2\x0f}^\xf4\x86\xef\x08m\x11\xec\xe0\xfdg\x9b\x89\xf5\xf74\x9fAs|#\xf6\xa4\xdd\xbfP"\x91!)\x0bN\xecs@{?\xfe\n\xa6\x05:!\xfc\tX\xd7J\x81\x12\xd3\x8d\xa9\xcaiLh\x8d%\xca\x9a\xd6\xd1F\x87hr\xa8\xdf\x1b\xac\xd6\xc3\x1e\x11\xae\x04\xca\x98\xdc \x89Dj\x83O<\xd6\x9cX\xe2\xe4b\xe1\xecX\xe1+\x0cE:\xda\x18\x9b\xcf\xa1BZ\xdbdv\xf8f\x8f~\xa2L8vB9\xfc\x9a\xde\x8a\x10\xc0/\x92WB\x8ep\x11\x87\x18\'\x8d\x84\x8d\xd8h6\x92\x16\xb1\xd1\x98a\xc5\xe6\xf8\xf2\xb1;?V\xca\xe5\x1b\xd5D\x9b\xaf\xf5|\xb9\xa2\x0bhC\x81\x0eM\xfd\xf6k\xdfY\xdbN}g\xc2<\xe5\xd1\x1b\xfeRn\x9c\xb7\xabl\xfb\xf8\xd6\xa5\x9c\xb1DE\x19\xe5\x92\xb5\xcb7/\xef\xad\x0b\x9dV\xd7\x83\xcdlK\xd3\x0e\xd0\x16\xe3"FP8\xed\xb9\xb2\x01~b\x9e\xc4\xe91\x0f\xc6\r\x1a\x80\x16\x9c\x0ble\x1b[/\x19h\x04 Be\x8a\xc8\x0f\x10\xa9\x10*\x84h\xac\x8ds\xcc\x8c\xa6\x17\x99\xe7G\xeb\xb8_\x02\x00\xe3\xd3\xfa\xd1\x82!*D\x9a\x81\x1f\xe96\x9c\x81#\x81\xcd\xd0\x10\x82\x8e\xbd\xeb\'m\x8d\xd1\xac\x01\x7f\x02a\x80\xf9\x08:!\xe1\xe4z8W\x1a\xc9\x88\xbcp\x96\xf2-\xfcw\xb3gyesAE\x07\xee!\x08\x02\xf3\xa7\x11JZ\x99\x11\xa0\x1c\xc5\xa2\x0c\x1a\x8eB\x15>\xe7\x85\x96\xd0%\xd3\xc5:\xeb?\xf0\xb9\xa6\xa9\x7f\xa7X\t\x1a!0 \x9e\xb4\xf9\x9e\x91m\xeb\xda\x08\xa0\x7f\x8f\x83\xb4p\x1bN\xbf\x81\xfe\xed\xd3\xd2\x90v\xb8\xf7\xe5\xdaU\x08\xd2\xae^$\xa8\x04\n\x10V\x18\x873KH\x8c\xedeQ\xf3E\x8b\x95\xee\xb1\x1c+`\x19i\xce\xcbKp\x1cpYo\xd5t\x9ckn\x14\x91\xa4\xd6\xddul\x00\xa6\xe6zb_\xfe\x80\xd1^B\x88\xfbul\xff\x9b{\x8e*\xe5\xa2\xd5J9e\xc1R\xbe\xd3U\xa6\xac\xb8\\)\x1bu\x95+:\xe6\x03\x14c\xd1\x11\x06\x86\x1el\x01\xd2\xcb\xc0O\x02\x02A\xd8\xa48\x06cc\x05\xa6a\x00\xcfC\x1b\xf1x\'[\xaf=L%v\x9dM\xf2\xdc\x04$\x7f\xc6A\xdd\xbf\xa8\xe5\xb3BT\xeb7\xfbW\xa6\x19\xee\x86\x8d\xf4h{\x97\xe3\x07\xc9`\xcf\x10\xa4ga\n\xb4C\x94\x89\x89{?\xf0\xd8\xb3\xb5\xa7\x0c\x11\x12\x18\x1b\xd3\xa6\xaf\x00-\xcc\xe6\xc6l\x89\xc1c>{\x11\xb8l\x9du\x11^\xe7\xcf\xebT_\x08\x85\xf1\xe2\xed\x1e\xa8\x02\xc3\xb5\xe3\xa9\xb7\xd6~\x12\x08~OC\x95\xd76gU\xb3j\xb1\xe67\xf5\xff\x90\x88\xf71\x91{u\xefP\x06\xff\x12\xa1K\xe0\xba\xdf \x1d\xf7\xe3\x9c\x84w^\xe3\xd1\xbb2\xc3[\x86\xda\\\x17\x92\xb3\x7f\xed=\xb0\xa7#A\x03\xf6\xb2\xd2!\x06\xb0y\xbc\xabl6\xce\x17\x1aY.5\xfbM\xbc\x1f\xb3\xfb\x0c\tl1\xfd\xdfF\x83V\x9c6^\xd20\xe7\xeb00/\xfeJ\xbf^\xa9\xacv\xf5\x9a\xe5\x80\xee\xe3\xcbGN\xe8\xa9\xf9\xf1\xb49\'\x0em\x8e\td\xf0m\xd4|\xb5\xd6\xd4#\x12\x0b\xc0\xc1\x03I\xdc\xf4\xe4\xd1e\xb9_/_>x|O\xf5\xf8/\xb1\xf3=5a\x88\xd3O=\xfa\xb5O\x9fR\xca\xde]5\x83\xef\x8e\xc7\x7fT\xcayo\xea \x81Ie\x8f\xceu\x08!\xceL\x1a\xc2y\xa7\xb5\xf8)=\xcd\x04\xe3\xb4\x7f\x8a\xb3\xd0\xef\xb4\x05S\xe8\x8d\xcd\x89U\xab@\x0c4<"M\xbe<[\x96\xf6B\xf4\x18&\x9b\x82(\x85\x00!+\x84\x08\x1d bHj\xb8\x1b6\x9c#\xf0\xdd\xef)\xdfnk\x0e\x02\xae\x1dy\xf0,\x98"\xad\xcd\xec\xb1=\'\x100Sr\xe53\x87 \xc4\x9a\xc2"\x82"\x9d\xa1|\x87\xa3\x0f3[\x1b\x0cg\x9d\xda\xfe\x06\xcf\xcf\xaeg\xcf\xbf|\xeb\x1b*\xb3R\x0c\xf13@T>\x07\xf2\x13\x1c\x985B\x01B\xb1\xde\x9c\xbf\xa0|\xa6,\xbb\x06\xfa\xccK\x01\x12\'\xa2\xb2d\xcfb\xafS.\x9c\x14\xf6\x81\x10\x80\xdf\xed\x7f\xca\xb0\x07\x1b\x817\x9a#\xeb\x9fz\x16\xeb\xeey\xddSZ\xbb\xd3\xd0\xccK\xca\xac\xed\x93JBW\xf2b\xe2\xbf\xc8p\x99\xccj\x9c\xde=P\\\xf6\xb2K6\x15;\x18\x1c\'!\x93\xe1\x94F\x8d6\x08\xf1\x82\xed\x18\x98\x80\xc0\x14\x08&\x12>\x9ddl\xa0.\xb1\x0b\x1e\xb7`\xb9\xbf\xfb\x90R\x8e\x99\xb3<\xb3b\x87Q\xbf\xd7U\x1ey\xf8\x1b\xd5Q\xb7w\xc7.&X\xfaBE7Tg\x19\x9f\x80\xbet|\x04\'t\xdf^\xaf\x81@\xf4\xf4O\xba)\xa4\xa05\xb5\xa9E\x9fy\xe83e\xbd;\xde[N\xeb~\xb8f\xdd1\x0fh\x14\x82b\xdf\xee\xa3\xca\x0fz\xbe]\x05AR]\x87z\x84YH\xd7xyWi\xfa\xb48&\x96q\xf6\xee#zj\x82\x8fx3\x98\x0f\x8e\xca8c\xe3\xf3X\xa7\xda\x0f\x11\x93\xd4q\xa6\t\x7f!\\D\xdc.R\x9a\x11G4K\xff\xf7\xda9\x05\t}\xba?\x0c#\x8a\x93voI\x07f\xc3\xfb_\x98\x80\xf0h\xa7\xca&\x84\xd6\x7f\x02\x11\x86M>F2\xf9R\xa7\x9fpl\xe2\xfeh\xc8gS\xe3\xe1\x95f%\xf6\xcf\xdaarB\x83\xc0\xc2\xbc\x04\t\xa1C\xe0\xa4\x1d\x1b\x7f\x92\xb0\x1f\xe1C\x99\xf1+\xf0U\xc5\xa9\xd8\xee\xa3\x08\x11%\xaf$\x11"\xcfB\xc8\xb5\x993\xb3\x103D\xd6{\x99\x0bh=G\x9a\xd9\x971v\xc9\xeaK\xf6\xadkD\x00\xa0\x1d\x02\x80\x10\nC\x13Nq<\xc6Ob\x9d\xf2\x0cy\r\xab\x96\xe1[=\xfb\x96\xcd\xef\xdf\xbcj\xb9\xe4C\xdb\x0c\x92\x9c\x96\xe6q\xd5\x18\x02\xf1s\xae\xd8\xd4\x84\xc3\xda\x8b\xea\xe6}_h\x87}\xff\xde\x9f\xf4T\r\xbd\xdb\x07\xdeS\xca\xfa]}!\xba\x13^^6\xb8w\xa3z>\xb9\xfa\x84\n\x0f\xbcx9_\xc2\'Oy\xb2|\xe5\xe2\xde\x1a\xfb\xa7=\xda%\xa26\x839\xe2>\x98* \xa30\x1b\x02\xb2 \x84\xc0Q\xdd\xd7\x95\xd5\x7f\xbfzY\xe4\xd4E\xaa`i\x17\x1e\r\xe7\x00\xff-\xbagBL\x84\x13!\xa0\xd1\xc7\xb1\xdd\xff(K\x9e\xbdd5K*\xac\x7f\xb6\xf7=\xc7\x9f"\x14\xce\'\xe8\x89\x10hKo\xfd\xfdh9\x0c5\x1cg\xd0\x8c8\x10n;\xb4Ix\x13\xb0\xd6\x12B\xb1\xf7\x04\xc0|[\xfeO\xdd\x87\xc1l\xcc\xf6\xe0O\x82e\xa0\xb4\xf1\x1c\tG\x86i"L\xfc\x1e4\x96Wr\xfe\x93\x99\xc7\xbeGw>\x87\xe9\xa1\x01\x02\x00\x1a\xb0\x96\x10\x00\xb3\x8c\xf0\x85<<\x0b%\xe5\xbbiz\xdb?\xee\xde\xf6\xfe{e\xac}2G\xdd\x1b&ez\xd0\xd01\x8b2\x885\xf3\x16F\xb2\xfe#u\xe4\xb5S~\x13\xbel\x87.\xd3\xc9hX\xe5\xc1\x9b<\xb4Iy\xdb5kU\x9b\x9f\xf4\xa4\x95%qHO\x15\xafek\x81\xd3\xbc\xae\xb4\x1d\xd8\xef\xc2IJHG\x19\x0b\x93\xce:4\xc8\xfc\xcd\x1fk\xb8\xef\xe4\xa7\x8e,\xcb\xfc|\x99\x9a\xf0#\xef^]\x00\xe6T*\xcaS\xbe\xe0\xf1\x0b\xd6<\x00\xde\xfc\xed\xce~\xaaf\x05\x9a\xe1G\x9b3\x118\xea\x92\x1c\x81\x19\x93\xbd\x17\xef0B\xb0!\xb2\xf8\xc4\xd89\xdd\xf6\xba\xa8\xb7\n\xa1\xe1\xc4us\xc4\xe9\x87`\x10\x90\xf3zf\xa9\xc1RX]\x03\x1a\xf0\xe2\xc7\xe8\x0b1\xdd[S\\%\x00\xbd|\xeb[*\x1aa2`v\xeb\x83\xa88\xb3\x10P\xdb\xf6\x9eYG\x86Xx\xd14\xe9.\x9c\xb4`\xc2\x8as\xcd\xbdr\xf0\xf2\xeb@`\x12|0R\xd0D\xff1p\xed\x1e\x04)\xdf\x9d\x16\xf1\xb5\xe7D\x10>\xd1\xb0I J\n7\xc6\xb0Vi\x18\xca\x0cMwe\x02\x8av\'d\x99\xacP\x15\xed\xef>\xfd/\xe6F\xd2f\xfd\xde\x1eM\x16-\x1e\xf8\x8d~|\xce\xf5\x93\x1c\x16d\x90\xb9\x18\x84H^i\x7f\x86\x81#\x80\xd2\xcd\t\x9d\x0ee?b"F\xb0\x0e\xb5\xe7`\xdbT\x89\x00k\x87.3\xd1hX\xf9\x01K\xff\xf2\xb5\xe5\xf0\x9e\x83\xcav\xdd\xbbV\x06\x7f\xcb\xb7\x1e\xab\xa1@p\xfc\x05\x1d\xed\xa6>^\xcb/\x8e=\r*e\\\xd1\xf2.\x8e\x90\xe2\xf9\xb5 \xb1\xa9c\x8fdj\xae\x8d\x031\xf9\th\xfdE\xb6\xfdG\xd5\x98+\xfej\xc5\xb2\xf5\xe3[\x96C\xba/\xa8\x9a\x1d\x1a\xd8\xec\x9e\xcd\xca\xbe={\x95\x83\xbb\xcf+K}\xf1\xa1\xf2\xa6\x0e#\xa5 \xc6\x82\xd3R\xb1\xa1\x93\x8dHR\xdbh\x84<\xdf\xc4?\xd7P#_\xc3H\x9c#\x16/\xce\x16\x02qBsy%>\x02\x07s\xb8\x06"[b\xe7{\xeb\x04_\xef\ts\xb1c\x992\xea\xe0U\xc0A)\xe2\xd8\xbe;\x12A4VG\x98\x96\xe6\xf5r\xefA"4\xa95\xf2\x0c\x84\x1c\x86&\xa8\x08\xbd\xbc0X\x90\x14\xa1\xd8\x1e\x896\x9a#\xf9\xedL\x0eL\xd6\x0eK\xa6\xb9\x88\x97\xb5}Msn5Q\xfd\x8e\xe9\xe5R\x10\xc4\x04\xb0\xb4b\x19\xac\xd6\x19\xd3cvN\xdb\xd8\xd6\xc9\x9dO\xb9{\x1b9`p\xfb\x99\xf6kI\x1c\xa3E\xddG\xd2\xc7cn\xc4D\xf2y4\x8f\xb6}?\x1d\xa3\t\x81\x91\xac\xc5p\x84\xc7@\xc7\xa8\x1c\x98j\xd8\xff\xda{F\x99\xfc\xf8Ve\xd3{6-\'v\xdfU\xc3y4\xbf\xd0\x9cx\xbd4N\xd9U\xaf\xe8h7u\xdd4sf\xd7\x89\xd5\x92\xd0\x08\xca\xa2\x83c\xf1\xdc\x06\x15\xc4<\xc0D\xa0\x9a\xd0\x8f\x02\x19\x1e\xffk\x9e>\xad\x96\xe5\x1e\xdas`Y\xf5/k\xd4P\xa0\xf6\xd2\x9b?\xfa\xc9\xf2\xb6\xdf\xbf\xad\x9a\x12\xa05TaC\xda#\xc0#\x00R\x9c\x81xm\x8c\xfbq\xad\xe1\x86\xd9\xd2\xb84\x9e|PR\xd8R\x8c\x9avI\x03U\xcf%j\xe2\xf9\x11\x92k#\x84y\'^]\x8b\x8c\x82B\xbe\xdb\xfd\xab\x8a\n\xc6{\xe2M\x7f\x04\xe0^\xdb\x897\xc9\xe9Hr\x94p\xa6\xc4\x9b\x0f5\xbb\xd5\xdf=s 2\x01\x90\xdc|\x0c\xea\xf7\x81\x12`\xa6w\xb4\xf3\xdb3\xbc\xd4\xfeb\xde\x84h\x93k\x0f\xf9\xa5I\xab{\xde\xa0\xd9\xaf\xd2\x84\x9e\x06\xea\x148\xf9\xec\x81gK\x1dK\x04@|\x04\xe8\x13\xf3:\xa7g\xf5\x1d\x89ALQ\xb4h\x9f\xd1fr\xedS\x81\xe8\x95\xfb 8\xa1#\xc9k\xfcT\x9c\x90\xe8\x92\x00\x18M\xd1\x96\xb5\x8d\xd0\xe9\x7f\xc4\x1f\x95\xaa\xbf\xb1\xca\xfe{\xce\x1c\xaa)\xbb\xd7}\xba\xa6\xe8\xfe\xfb\xde\xdd\xca\x86\xf7mT\xc7?\xf1\xba\xb3\x03-\x10\xa7\x9d\x84\x0e\x8d.@_\x7fc|H\x00#\xac\xd3\xf4u\xd9\x01!}\x07$CT\x16\xb2?$\xb4\xc1\x98\x96$_\xfa\x82eJ\xb9p\x95R\xf6\xed*\xe5]]\xe5\xfa\xa9\'\x94\x8f\xde\xf1\xd1R\xae\xdd\xbc\xdc\xfd\xd8\xf7\xcb\x05\xff<\xb3\xbc\xf6\xc2e\xab\xe3p\x8e\x1dz\x9e\x0b\xbd\xc5\xc9\x81\x88\x02%m@\xbb\xe1\x08\x89?\\\xa9\n>9gB~\x98\xa0\xa6"Oy\xa6\x12\x8cE\xf3\x7f\xf7\xd0\xce\xb5\x8eS\x89\xfd\xa9q\x88t\xe6W\x9f\xfb\x9a\xd2{\xdf^5\xa1j,6l,\x0f\x0cf\x7f0\xd4\xb2M\x9fV\xb3\x8ei\x13N\x88\x0b\xf1r\\\xdack\xdb.L\x02\x81\t\xc5\x08\xc1\xd1\xcc\x88\xb4v\x187\x03P\xad#:J\x82Pz(\x88\xb0p\xf2\x11\x16\x18O\xf5$\x9b\xdf\xdf)\xbe\xc9\x94%\xfb\x93\xe4\x1b\xe7\xc2\xc0L5>#L\x8f\x99\xd7m\xbeU\x91\xed\xabv\x7f\xe0\xb9Y\x87\x9c\xb8\xae\x8b\xa1\xdb6:\x87\xb5,C\xe1J\x03g\xdd\x97s}\xa0\xd9\xb3l\xd1\xf4\x8dn\x1f\xa8\x9b\xd6h\x8f6z#\x08\xa0\xd2\xb1j\xbd\xf7\\~\xcc\xf6o^\xbe|s\xa1\t\xf5%\x9e~LG&\x18\xf1Tv\xec\xaa\xc8\x80M\xbbDsQ]4\x92\x0f\x83Kp\x81\x10^\xda1\x07\xd4\xc7\xb3\xc9\xbd@F\x0b\t\xe6#.LN\xdbK\xc8\xc8\x8bm\xbc\xce\xb3\x8b\xbf\xc6\xe5k\x94#\x9f\xfc~)?\xed*W\xbc\xb8\xab\xdc\xf6\xc4\x8f\xcb\x94_<]c\xfc+]\xfe\xd6j\x02\xb0\xafis\x9b\x08}\x18@!\xef;\x0e\x11\x8b\xd4\xbfs\xf1X\x1c\xceO\xd8\xf1]\xec\x7fY_>D\x86\xa5\x0c\xb6\t\x88\xc6\xab\xadqS\x857\x96\xf76\x9a#\xd3\x8e\xda}\x06\x12\rh{\xf0\x93\x1f\x9f\x8a\xbc$\n%\x91\x07\x0c\xc6\x88\xa0\xb1s\xc4\xb1\xe5\xf3\xc3ML\x89\x10O\xb5\x1b\x81\x93\xf6\xe4\x0b7\x97\xd4(\x95f,\xea+*\x02}`\xe3R\xbe\xd2UN\x7f\xfa\xd8j\xae\xfe\xe8\xef\xbd\x15\xa5\xfaNz\x03Zs\xf7C@\x11b\x988a\\\xf5\x19\x92\xdc\x14\x13\x1d}so\xc7\xdc<\xb7\x9c\xdc}o\xcd!\x91\x12o\xaf\t\x0c\xc2 }\x0ei|\x8a\x8e\xe0Lf\xa4\xdf\x95Cs\xfcf\xe6\xa1g \x08\xc6\xaaQ\xe7h5~\x1c\xac\x83\x16:\xfd\xb1\xc3x\x17u\x18\xfe\xc0E\x17(\xdb\xac\xb3z9`\xf1\x85\xcaY\x04\xc0\x1e]\xe5\xfc\x7f\x9eQ\x1d\x80\x980P\x1f\xfcg\xbfs\xf0\t\x89\t\x7f\xfd\xd7\xb3\r1\x98\t\xd0\x83N82\xdf,\xe6\\\xdb>X\x1dJ\xefk\xbe\\K4y\xd3\xf5\x93\xe3kp\x8e\xb5oZ\xb7\xce\x92\xbb\xfa\xa5\xa2\x04\xaf(G=yX\xf9\xb9\xeb_\xf1\xbe\xf2\x89\xbb>Q7\x85\xcdW\x9bn\xd4Z\xef\x9e\x8a6\x12\xbb\x07\xc1\xd3:k8\xed\xb3\xfa\x1f\x99]\x1f?\x06&\x97\xf6\xac\xcb\x8cg#\x80bG\x0f\x16\xfe\xc9\xe0\xce\xfe\x1b0\xd4\x96g3\xe3H\x88\x17\xa3e\xe20FC\xe8\xc9\xd7\x07\x91\x93\x00\x94D1\xbf\xfb\x0e\xa2\x87\x0e\xd4=\xd0|\x84\x03\xf8\n}\xa1\x0f&\x18&H]\xc1\xf4\x9aUX\x1f\xf7\xe3\\\x18\xcd\xf93d\xc4\x9a\xa7\xb7\x04T%\x19\x0c\xb3/\xf7\xab\xd7\x95\xa3\xa7\xfe\xa0\x96\x9b3\xb5taB{)\xb5\x8e\xaf\x06Ra2\x08#cPZ\xdb0\x13\xa6\x9a\xf0\xf7\x19\xdd\x8f\xd5~\x14\'\xde\xd9[\xcd^\xf4\xccA\xcd\t\xae\xa8K\xae\x8bsQf\xb2Z\x97oN\xad\xa6`\xfa\':\xafD&\xa8B\xf6#\x01@hB\x1f\t\x7f\x8e\xe5p\xd1\x91\xd8\xfa\x99p4\xe8h3!\xba{\x17\xef*\xdf\xea \x00\xb9\xf5;\xae\xb0l9\x05\x03\x1e\xd0U\xeb\xf7-<\x06 \x00<\xa8\xec=\xe5\x97\xcb_\xfc\x86\xb2g\xf7A\xb5\xf9\x05fg2\x80\xbf\x1f\xb8\xe9\x03\xb50H\x16\xe1\xfb\x8f\xed\xa9\xad\xb1\xd2\xa9G\x1e\xf6\xe7;\x1a^5\xa0f\x1c\x9cek\xfcq\x8dRn\xda\xaa\xdc0OW\xf9\xf7\xca}U\x83W\x1c\xf7\xa3r\xd7"]\xe5\xf8\xa9G\x94\xd5\x7f\xb7z\x15\x16\xa6\xcc\xaa\x1ck\x9b\x00\x98?\xc9>q*\x8d\xd4\xc3\x9e\xae8q\x8cqx%D\xc4\xbe\x05\xbf2\xd0dF4<\x9dY\x87\xf5\xc3\xf8`\xbe\xb5\xcb\x04\xa4x\xd0\xbd7X\xa8\x92\xa0@\xf4\x18IAS&\rYs\x10\x9d\xd3X\xee\x86s\xa4V=\xe9\xc7\x83\xad\x19\x81\xeb\xba\xcc-&\x94\x12c\x02(9\xfc\x10\xe8;\x0e\xeb\xa9\xa1b\x11$\x9d\x98\x17;y\xb1\xf2\xdd\xbf\xf4\xd5Y\xf0\x11\x19e\xceI\xadS\x10e\x83\xd9\xf9\x06\xe2\xe3pN9\x01iu\xc6\xace\x9a-\x7f\xde\xf2\xe5\xa8\xeekk\x14J\x12\x99s\xf1\xdf\xe83\xc1w\x83^\xa1\x80%\x9a\x0b;H\xe4\xd2\xb2@\xf3\xbbz\xbe\x08C\x9a_J3t\x11\xe1\xe5Ex\x11\xa4c\x99\xb3\x9fF\x1ec\x9e\xb1x\xd6\xd3\'\xf4\xd5\xed\x9f\xfb\xc6\xbe\xd4\xda\x1b\x9a\xf2\x87\x7f_TC\x83G\\\xd7[%0I\xc8\x0e\xa3y\xc1z\xd0x\xd9s\x96-\x8b\x9c\xb6H\xd9\xe2\xfe-j\xa3H1r\xdd\x7f\xbe\xd6a\xec5\xafX\xb3\xb6\x8e~\xf3\x95\xabV\x1b^\xe2\x0c$\xc1\x86R,\xf3\xc3k\xfb*\x07\xc1\xfd7_\xfc\xe6z\xbd\xb2k\x9f\xd0)?\xe8\xbc\xce_\xa9|\xfa\xb1-Js\xc6\xd4\xb2\xd8\xe7o\xab^`\x12\x9e#\x88\xb6\tQY`\xd0\x8c\xdd\x98\xda\xf1\xa1,8\x86\xee\xaf\xc5\x9d\x0b\xf3\x13\x02$|\xbb\t\t\xa6\x8f\xe6\x1f\xe9&`\xb8\x9ay5\xce\x07O\xbf5\xa2\xb9\x13~\x03\xeb1m\xda\x83\xfb\x9f5\xe8_S\x9e\xf4i\xb4@\xd3fR\x14MK[s\xa2\xa1\x11\xcf\x8aA\x08\x08\xcf\xec\\\xfd\xab\xdeb\xd3\xfa\x1c-\xcb\xc4$D2W\x92@\x82((\x1f\xe5\xd5\xa2Dhf\xe7\xee\xaf\xd6Q\xe5\x14\xc9\':BA\xd5(\xe5 B%\xc3T\xf5\xa0\xfc\x7ft\x9a\x1e\x00\xce\xeboM`\xe6o.\xaf\xe5\xc5LU\x8c/s\xd5\xf9\xd4\xabHb\xd3\x15j\xc2\xb3\xe6\x86\x81/\xda\xbf1mE\xb1\xd2L\x86\x10\xf0\x13\xa2\x15\x15\xe3\x9b\xb0.i\xb8\xa25\x9a\xfb\x87\x04\xa0\'\xeb\xc8,\x98\xd6\x80\xd1\xe4Q\xcc\xf4\x11d\x98P\xf6\xdd\xfa\xd7\xae_\x8e\xeb\xbe\xa9\xc2)5\xfd\x1aYX(\x1b\x9d\x8e\xb5~\xd6\xba\x80)\xcf\xd4\x11\xd1\xeb\xde\xba^y\xdf\x83\x1d\xa8\xfe\xe8\xc7\xcb\xa4\xee\x1d\xea(i\x0b\xb9\xc4\xcf\x96(\xffx\xf2\x98\xf2\xd8C}\x8d>x\xf8\x0f\xf9So\xfd\x9e:\x009\xda\x04\x018\xf7\xfa\xf3__\xe7\xcd\x95;>_\xca\x1f6(\xe5\xe7\xcb\xd5\x9c\x03\xcd#8\x13A*\x84\x06\x81XT\x9a\x19\x03\xa7\x11\xa9p_$rB1\xd3cP\x1a\xab\xbf\xd7\xd5s\x92\xda\x19\x93\xd6\xdf\xf6r\xce\xd1\x8c2\x93\x06\xcc\xd15\xd2\xef\x8f\xd5\x91\x86\x9bL\x810\xa5\xf5\xf3\xec~\x87r\xe2Lk\x8f-o\x1f)p\xf19\xcc\x0e\x19bb\xb6w2\xd4\x08\x05\xcf\x9b\xea\xbe\xb6\rjm\t"\xefYw\xfb\xe8\xfb~f\xf0,AB\xe0\x0b\xf3\xe9\xf3\x07e\x8a\xb0h\xb9\xa6\xd7"\xc6_\xff\xd9\x97\x1c\x12\xd1*\rF\xde\xd6\xfc\xa0:\xec\xa0\x01\x08\x82\x8f"\xc2D\'#\x02\x05\xfdIF\xe3\xfc\xdb\xf9\xdc\xa7\xab\x19@\xeb\xcf\xb5\xed=5\x87C\xbeG__\x81\x9eJ\xaf\xe9\xdf\x984d\x13\x95\t\x05\x9f1\'\x02\x12H\xf4\xc1\xb3\xa9\xa5\x11-H8\xdckz\rG\xd2\xf2l4\xe1\xc0\x11\x1d\xa9S\xf6\x93\x16\xcd\xb4[7\x13\xe7A\x9aU\xa4\xf5\x10;;=\xe3\xa5\xe7\xda\x80\x89gN-G\xfe\xa3\xb76\xdb|\xe5\xcf_Y\xcd\x87\x8b\xffuny\xf0\x91\x83\xcaU\xcf\x9cS.z\xe6\xecr\xcaSG\xd5\x81\x1a\x0b\x9d\xb4P\xcd\xa6\x93\xb1\xb7\xc4\xa9K\xd4\xf2\xe3\xf5\xef\xd9\xb0|\xbd\xe7\xab\xf5s\x87u_Z\xed0\xdaC\xfc\xd7\xa6\xf0=\xa4\xba/\xe3\xb9\xd5\x11\x88L\xd8\x14\x84f\xf1\xc0\xc7$\x88\xc4>%\xbc\x10e\x1cY/{\xb6[\x0c\xcf>\xe2\xf0Z\xe8\xe4\x85\xaa\x1d(!j\xa1g\x1b\x97\xb6\x8b\xa0\xfa\x1f\x89-Oo}S\xcb=\xb6\xbb6\xb6G\xd2_\x87\xfa\xf9\x84\xf0@]\xe6\x82u\xc4\xec\xd6\x04\x03c\xb6\xf6\xb8o\x88!EA\x10\x9bN\xc3\x10\x83}i\x87\xec\x92SB\xdb\x82\xe9:\xff\xa8"\xd5S\x81\x86Wd\xc4\xf7\xa0\nP}\xbfN<\x1c\x91i\xb1\x86&\xed\x19E\xe0o\xda\xdf\xfdBqh\x87P\xc2\xcc\x18}\xa9g\xdb\xb6A\xad\x9c\x8c\xafzv\x1e\x04sBz9s\x80O@Y\xba\xf4sy1\x1a\xdaJDK\xeb8\x11\x14\x82A(\x90\x83\x91C\xd0\xfd\xa5\xd5Z2f\xd35jV\x18x\xfa\x7f\x8eH\xa9\xb4#"\x0c2)7\xa9\x9e\x1e\x00\xe3\x81\xc0\xa9\x81gG\xdaD\x9b\xa5\x06\xdb\x82\x88\x0e|\xe4\xf6\x8fT\xe8\xff\xd5\x9e=\xabvW\xd0\xc3)\x88\xb18s\xfa\xa6\xc2\\_3\x01\xf5\x0b\xe0\x8d\xfd\xe2E}\x05D\x90\xc3\xdb\xaf]\xabz|\rw\x00\xc95\xd5X\xe5\xd2U**Yt\x87[\xcb6\x1d\xa4\xa1\x8cT\x92\x0e\xbb\x8f\x04~e\xd3Gx\x9e\xc1\xc2gx\xa9\xec6\xb6\xdfr\x1d\xcd\xc17\xc1\xe3;\xf9gOUG\xa3a L\x18]\x8b\x9ezCW\xf9\xf0\t=\x15\xfd@1<\xca\x08\x890\x1c\xcc\xe1\'"\x82\xd0\xa67\xd7\x00\xcc\x1dKG\xd0\x8c8\x12\xfe\x1c\xea\xe7\xd3 %\xf9\xfd\xed\xe6\xa2\xcc\x80w6\xdf\xa9Z\x92\x16$\x90\xd9\xe0l|{\x02\x96c\xc2\x84\xdb\xac\r\xc1\x81Y!>\xdewv<\x01M\xb3\xe6\xe5\xbb\x04\t\xa1\x93\xf4\xe0$\x10\xb9\x07\xc2$\x85F\x99m\x91\xd6\xf6i\xe3\x05\xe5\xd8W\x0e=I[\x0boscm#\x0e\xf2S$\x92\xde\xd0#A\xa0x\x8d\xff\xcb\xf0\x184\x01-@\r\xea>D\x11\xe4!\x10P\x84\x91\xb4th\x19\x12\xa2h\x08\xfch|(\x8b\xb9\xe3\xe7P\x12\xa8\x92\xdb\x82\xa6r\x8evAR\x1a\xd4\xcc\xb0\xeaE\x1a>\xce\xaevaL\xff\xcf\xa5P!\xa3\x95HX\x9bcx\xc6:\xb7\xacW\xb6|\xecsu1\xa7\xa5%iq!\x1aU\x80\xfb]\xd2\x17\xce\xd1\x16,\xed\x8fl\xb4\xa6\x1a\xb7\xf4\x1cYv=\xef\xe9\xaa\xa5\xcb{\xbbj!\x90\x88\x80j2Q\x05\x9b\x89\xc82\xd1\x95\x10\x83\x00\xfa\xb2\x0eo\xaa\xa8Cy\xb1\xf0\xa2A gvu\x95;\x16\xea\xaa%\xc4\xb7^tv\xf9\xd3z\xeb\xd5\x8e\xb2\xcc\x92\xad\xce\x9cZ\xfb\xfde3\x06Kp\x01\xffT\x9c\xd1X\x18`\xecv`\xf69R-\x87\x060"3\x8d\x03N\xc3O\x8d9iC\x9a\x1f,\x97b\xce\xc9\xc7[\x9eRr\xc2\x19\xb1\xb3\x9f\xa10L\xff\xb2mn\xad\x89>A\x0fi\x996\x9a\xaa<\xcc\x94y\x02h\x0bmHU\xd7\xe6\x8e\xe2\xe2_RK"\xfd\x9c\x7fK{<\x0c-\xa3\x94\xaf\x00R\xd0\xdb\x90 \xa0\xb4\x84\xa1_\xbe\xf5m\xd5T5\x06\x0e\x8d-\xb9\xeb}\xf5yF\xbb\xa6\t\xc9\xa2\xa9\xf8b\x08I\xfc\x16\xde\xec_!8\xe6G<\x8e^\xedn2m\xa9\x93\xca\xb9\x08\x87\xc0,\x92\x94\xc3O\x9b.\x040XqH\x1a\x91\xd8\x10\x90\xd1F@\x07lB\t\x1a\xc5p\x99\x14\xed3\x84\x1c\xb4\x83\t\xe3\x9c\x1e\xa9m\xee{\xf8\xc2\xf9\\/\x93\x990;G\xbb\xa8D\xdf\\\xc5\x87k\x83\x13S\xa4\xddO\xc6\xa5\xe1\xb1\x81\xaeK\tg\xb2\xd5@\xad\xee\xff\xd7\x11\xe7\x1eXN\xf3&\x93j\xb0PP\xff#\x19G\t\xa3y\xa8\x84\x9b\xbc\x12\xa7O\xbc\x14\x93\xc9\xb4\x93l\xc1\x06\xe3\xe9\xe7\xe8\xab\rB\xb6\xbe\xb125a\x00\xcas\x18\xaa\xae\x03\xb9D\x03\xd8\xeb4\xbe\xef\xbb\x1e\x14\xa0K\xac\xccB\xe3\xbd\x845%\x1b\x95\x8b\xdfQ{\x10\xd4\x9e\x81\xe7\xafT\xb6\xe9\xfe|\xd5L\xcb\xfeb\xd9\xb2\xc2\xefW.\x9fyl\x8b\xea\xa4$\xd1\xdd\x1f\xbfC\xdaN\r\xab\xaf\xfalr\x8c\x95\x00\x88c\x13\x93\xb4\x99\x1f\xaaK\x06 \xc1\x1e\xc72\x02MOE?g\x07\xe1\x19\x94\xc3\x01\x8ey(\xa9\xa4P\x87\xc63]\xb8\x1d\xdf\x1fN\xean\xe6\x04\xa4]\x9b\xb5B\x83\xd2\xace,\xf6\x15\xe0\xdd\\}\x15L\x15\xdd\x94\xdf\xda\xf4\r\xb0!\x840?\x81\xe9\xde\x98Z\xcc\x82D\xeb0\xbf\xbd!\xc4\x98\x12i\'\x9e\xb5\xff?\x08\x16\xc3\n\x0791\xc9,\xa6\xca\x9b;\xd4\x87\t\xfc\xcbD\x9d\xc4\xea3t\xc1\xa2\xb53\xa3h\x03\x8c\xcb\xbb\xaa{\x8f>\x00|\x07*\xae\x94yjQ\xbe\xe4iKV/-\xa4\x00"\xa9Nc\x0e\xf0\xfc\x13\x0c\x12;<0\'\xa2P\xa4L1\xfd\xe2W\xbbl\xb5\xb2\xc0\t\x0b\xd4D\x11\x9eZ\x0e\x1e3\x0b\xf8\x07\x96m\xce\xac\x1d\x88\xe5=\xec\xdd\xb3{\xdf\x18\xf0\xae\xff]\x92\x99\x8a\xc0\xe9J\xcd\xd9\xf8\xe8\xcf\x84\xb3\x03S\x8e\xd7\x81\xa1\xd0-\xe5\x95\xd7X\x84v3\xd9\xd7\x8b\xe9L\xe1d\x06 \xa4\xc1\xd7`\xcc\x99\xc8\n\x06f\x9a\x9aJ\xc5\x81JP\x10B\xde\xc7\xf8>\x07\x15\xb4#\x07\x90\xcd\xa7\x9b\xad+\xea\xc2{ib\xeb\xda\xff\xc7\x91\xf8\xbc\x00x^\x00\x8c\xd7\xbd\xcc\xea\xc7\x7f\xbc\x00\x00[Rr8\xdc\x87Hz(On_\xad\xc0e5$\x07\xee\'\x01\xc4\x03y\xb06 90 % + results = instance.search_satellite_imagery('test_pol', 1480699083, 1480782083, ImageTypeEnum.PNG, + PresetEnum.EVI, 10, 20, SatelliteEnum.SENTINEL_2.symbol, 0, + 10, 90, 100) + self.assertEqual(2, len(results)) + self.assertTrue(all([i.image_type == ImageTypeEnum.PNG and i.preset == PresetEnum.EVI for i in results])) + + # all Sentinel2 EVI images available for the polygon, 10 < px/m < 20, cloud coverage < 10%, data doverage > 90 % + results = instance.search_satellite_imagery('test_pol', 1480699083, 1480782083, None, + PresetEnum.EVI, 10, 20, SatelliteEnum.SENTINEL_2.symbol, 0, + 10, 90, 100) + self.assertEqual(3, len(results)) + self.assertTrue(all([i.preset == PresetEnum.EVI for i in results])) + + def test_repr(self): + instance = AgroManager('APIKey') + repr(instance) diff --git a/tests/unit/agroapi10/test_imagery.py b/tests/unit/agroapi10/test_imagery.py new file mode 100644 index 00000000..d0438a73 --- /dev/null +++ b/tests/unit/agroapi10/test_imagery.py @@ -0,0 +1,95 @@ +import unittest +from datetime import datetime +from pyowm.commons.image import Image, ImageTypeEnum +from pyowm.agroapi10.enums import PresetEnum, SatelliteEnum +from pyowm.agroapi10.imagery import MetaImage, SatelliteImage +from pyowm.utils.timeformatutils import UTC + + +class TestMetaImage(unittest.TestCase): + + test_acquisition_time = 1378459200 + test_iso_acquisition_time = "2013-09-06 09:20:00+00" + test_date_acquisition_time = datetime.strptime(test_iso_acquisition_time, '%Y-%m-%d %H:%M:%S+00').replace( + tzinfo=UTC()) + + test_instance = MetaImage('http://a.com', PresetEnum.FALSE_COLOR, + SatelliteEnum.SENTINEL_2.name, test_acquisition_time, 98.2, 0.3, 11.7, 7.89, + polygon_id='my_polygon', stats_url='http://stats.com') + + def test_init_fails_with_wrong_parameters(self): + self.assertRaises(AssertionError, MetaImage, None, PresetEnum.FALSE_COLOR, + SatelliteEnum.SENTINEL_2.name, self.test_acquisition_time, 98.2, 0.3, 11.7, 7.89) + self.assertRaises(AssertionError, MetaImage, 'http://a.com', PresetEnum.FALSE_COLOR, + SatelliteEnum.SENTINEL_2.name, 'a_string', 98.2, 0.3, 11.7, 7.89) + self.assertRaises(AssertionError, MetaImage, 'http://a.com', PresetEnum.FALSE_COLOR, + SatelliteEnum.SENTINEL_2.name, -567, 98.2, 0.3, 11.7, 7.89) + self.assertRaises(AssertionError, MetaImage, 'http://a.com', PresetEnum.FALSE_COLOR, + SatelliteEnum.SENTINEL_2.name, self.test_acquisition_time, 'a_string', 0.3, 11.7, 7.89) + self.assertRaises(AssertionError, MetaImage, 'http://a.com', PresetEnum.FALSE_COLOR, + SatelliteEnum.SENTINEL_2.name, self.test_acquisition_time, -32.1, 0.3, 11.7, 7.89) + + self.assertRaises(AssertionError, MetaImage, 'http://a.com', PresetEnum.FALSE_COLOR, + SatelliteEnum.SENTINEL_2.name, self.test_acquisition_time, 98.2, 'a_string', 11.7, 7.89) + self.assertRaises(AssertionError, MetaImage, 'http://a.com', PresetEnum.FALSE_COLOR, + SatelliteEnum.SENTINEL_2.name, self.test_acquisition_time, 98.2, -21.1, 11.7, 7.89) + # sun azimuth + self.assertRaises(AssertionError, MetaImage, 'http://a.com', PresetEnum.FALSE_COLOR, + SatelliteEnum.SENTINEL_2.name, self.test_acquisition_time, 98.2, 21.1, 'a_string', 7.89) + self.assertRaises(AssertionError, MetaImage, 'http://a.com', PresetEnum.FALSE_COLOR, + SatelliteEnum.SENTINEL_2, self.test_acquisition_time, 98.2, 21.1, -54.4, 7.89) + self.assertRaises(AssertionError, MetaImage, 'http://a.com', PresetEnum.FALSE_COLOR, + SatelliteEnum.SENTINEL_2.name, self.test_acquisition_time, 98.2, 21.1, 368.4, 7.89) + # sun elevation + self.assertRaises(AssertionError, MetaImage, 'http://a.com', PresetEnum.FALSE_COLOR, + SatelliteEnum.SENTINEL_2.name, self.test_acquisition_time, 98.2, 21.1, 54.4, 'a_string') + self.assertRaises(AssertionError, MetaImage, 'http://a.com', PresetEnum.FALSE_COLOR, + SatelliteEnum.SENTINEL_2.name, self.test_acquisition_time, 98.2, 21.1, 54.4, -32.2) + self.assertRaises(AssertionError, MetaImage, 'http://a.com', PresetEnum.FALSE_COLOR, + SatelliteEnum.SENTINEL_2.name, self.test_acquisition_time, 98.2, 21.1, 54.4, 100.3) + + def test_acquisition_time_returning_different_formats(self): + + self.assertEqual(self.test_instance.acquisition_time(timeformat='unix'), + self.test_acquisition_time) + self.assertEqual(self.test_instance.acquisition_time(timeformat='iso'), + self.test_iso_acquisition_time) + self.assertEqual(self.test_instance.acquisition_time(timeformat='date'), + self.test_date_acquisition_time) + + def test_acquisition_time_fails_with_unknown_timeformat(self): + self.assertRaises(ValueError, MetaImage.acquisition_time, self.test_instance, 'xyz') + + def test_repr(self): + repr(self.test_instance) + + +class TestSatelliteImage(unittest.TestCase): + + test_image = Image(b'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x01\x00\x00\x00\x01', ImageTypeEnum.PNG) + test_instance = SatelliteImage(TestMetaImage.test_instance, test_image, + downloaded_on=TestMetaImage.test_acquisition_time) + + def test_init_fails_with_wrong_parameters(self): + with self.assertRaises(AssertionError): + SatelliteImage(None, self.test_image) + with self.assertRaises(AssertionError): + SatelliteImage(TestMetaImage.test_instance, None) + with self.assertRaises(AssertionError): + SatelliteImage(TestMetaImage.test_instance, self.test_image, downloaded_on='not_an_int') + with self.assertRaises(AssertionError): + SatelliteImage(TestMetaImage.test_instance, self.test_image, downloaded_on=1234567, palette=888) + + def test_downloaded_on_returning_different_formats(self): + self.assertEqual(self.test_instance.downloaded_on(timeformat='unix'), + TestMetaImage.test_acquisition_time) + self.assertEqual(self.test_instance.downloaded_on(timeformat='iso'), + TestMetaImage.test_iso_acquisition_time) + self.assertEqual(self.test_instance.downloaded_on(timeformat='date'), + TestMetaImage.test_date_acquisition_time) + + def test_downloaded_on_fails_with_unknown_timeformat(self): + self.assertRaises(ValueError, SatelliteImage.downloaded_on, self.test_instance, 'xyz') + + def test_repr(self): + repr(self.test_instance) diff --git a/tests/unit/agroapi10/test_polygon.py b/tests/unit/agroapi10/test_polygon.py new file mode 100644 index 00000000..f7039b99 --- /dev/null +++ b/tests/unit/agroapi10/test_polygon.py @@ -0,0 +1,91 @@ +import unittest +from pyowm.agroapi10.polygon import Polygon, GeoPoint, GeoPolygon + + +class TestPolygon(unittest.TestCase): + + geopoint= GeoPoint(34, -56.3) + geopolygon = GeoPolygon([ + [[2.3, 57.32], [23.19, -20.2], [-120.4, 19.15], [2.3, 57.32]] + ]) + + def test_polygon_fails_with_wrong_parameters(self): + + self.assertRaises(AssertionError, Polygon, None, 'polygon', self.geopolygon, self.geopoint, 123.4, 'user') + self.assertRaises(AssertionError, Polygon, 'id', 'polygon', 'wrong', self.geopoint, 123.4, 'user') + self.assertRaises(AssertionError, Polygon, None, 'polygon', self.geopolygon, 'wrong', 123.4, 'user') + self.assertRaises(AssertionError, Polygon, None, 'polygon', self.geopolygon, self.geopoint, None, 'user') + self.assertRaises(AssertionError, Polygon, None, 'polygon', self.geopolygon, self.geopoint, -77, 'user') + + def test_area_kilometers_property(self): + area_hs = 456.78 + expected = area_hs * 0.01 + instance = Polygon('id', 'polygon', self.geopolygon, self.geopoint, area_hs, 'user') + self.assertEqual(expected, instance.area_km) + instance = Polygon('id', 'polygon', self.geopolygon, self.geopoint, None, 'user') + self.assertIsNone(instance.area_km) + + def test_from_dict(self): + _id = "5abb9fb82c8897000bde3e87" + name = "Polygon Sample" + coords = [121.1867, 37.6739] + geopolygon = GeoPolygon([[ + [-121.1958, 37.6683], + [-121.1779, 37.6687], + [-121.1773, 37.6792], + [-121.1958, 37.6792], + [-121.1958, 37.6683]]]) + center = GeoPoint(coords[0], coords[1]) + area = 190.6343 + user_id = "557066d0ff7a7e3897531d94" + the_dict = { + "id": _id, + "geo_json": { + "type": "Feature", + "properties": { + + }, + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [-121.1958, 37.6683], + [-121.1779, 37.6687], + [-121.1773, 37.6792], + [-121.1958, 37.6792], + [-121.1958, 37.6683] + ] + ] + } + }, + "name": name, + "center": coords, + "area": area, + "user_id": user_id + } + expected = Polygon(_id, name, geopolygon, center, area, user_id) + result = Polygon.from_dict(the_dict) + self.assertEqual(expected.id, result.id) + self.assertEqual(expected.name, result.name) + self.assertEqual(expected.area, result.area) + self.assertEqual(expected.user_id, result.user_id) + self.assertEqual(expected.center.lat, result.center.lat) + self.assertEqual(expected.center.lon, result.center.lon) + self.assertEqual(expected.geopolygon.geojson(), result.geopolygon.geojson()) + + # now testing with dirty data + self.assertRaises(AssertionError, Polygon.from_dict, None) + + the_dict['center'] = ['no_lon', 'no_lat'] + self.assertRaises(ValueError, Polygon.from_dict, the_dict) + the_dict['center'] = coords + + del the_dict['id'] + self.assertRaises(AssertionError, Polygon.from_dict, the_dict) + + def test_repr(self): + instance = Polygon('id', 'polygon', self.geopolygon, self.geopoint, 1.2, 'user') + repr(instance) + instance = Polygon('id') + repr(instance) + diff --git a/tests/unit/agroapi10/test_search.py b/tests/unit/agroapi10/test_search.py new file mode 100644 index 00000000..3cbc5482 --- /dev/null +++ b/tests/unit/agroapi10/test_search.py @@ -0,0 +1,110 @@ +import unittest +import json +from datetime import datetime +from pyowm.commons.enums import ImageTypeEnum +from pyowm.agroapi10.search import SatelliteImagerySearchResultSet +from pyowm.agroapi10.enums import PresetEnum +from pyowm.utils.timeformatutils import UTC + + +class TestSatelliteImagerySearchResultSet(unittest.TestCase): + + test_data = json.loads('''[{ + "dt":1500940800, + "type":"Landsat 8", + "dc":100, + "cl":1.56, + "sun":{ + "azimuth":126.742, + "elevation":63.572}, + "image":{ + "truecolor":"http://api.agromonitoring.com/image/1.0/00059768a00/5ac22f004b1ae4000b5b97cf?appid=bb0664ed43c153aa072c760594d775a7", + "falsecolor":"http://api.agromonitoring.com/image/1.0/01059768a00/5ac22f004b1ae4000b5b97cf?appid=bb0664ed43c153aa072c760594d775a7", + "ndvi":"http://api.agromonitoring.com/image/1.0/02059768a00/5ac22f004b1ae4000b5b97cf?appid=bb0664ed43c153aa072c760594d775a7", + "evi":"http://api.agromonitoring.com/image/1.0/03059768a00/5ac22f004b1ae4000b5b97cf?appid=bb0664ed43c153aa072c760594d775a7"}, + "tile":{ + "truecolor":"http://api.agromonitoring.com/tile/1.0/{z}/{x}/{y}/00059768a00/5ac22f004b1ae4000b5b97cf?appid=bb0664ed43c153aa072c760594d775a7", + "falsecolor":"http://api.agromonitoring.com/tile/1.0/{z}/{x}/{y}/01059768a00/5ac22f004b1ae4000b5b97cf?appid=bb0664ed43c153aa072c760594d775a7", + "ndvi":"http://api.agromonitoring.com/tile/1.0/{z}/{x}/{y}/02059768a00/5ac22f004b1ae4000b5b97cf?appid=bb0664ed43c153aa072c760594d775a7", + "evi":"http://api.agromonitoring.com/tile/1.0/{z}/{x}/{y}/03059768a00/5ac22f004b1ae4000b5b97cf?appid=bb0664ed43c153aa072c760594d775a7"}, + "stats":{ + "ndvi":"http://api.agromonitoring.com/stats/1.0/02359768a00/5ac22f004b1ae4000b5b97cf?appid=bb0664ed43c153aa072c760594d775a7", + "evi":"http://api.agromonitoring.com/stats/1.0/03359768a00/5ac22f004b1ae4000b5b97cf?appid=bb0664ed43c153aa072c760594d775a7"}, + "data":{ + "truecolor":"http://api.agromonitoring.com/data/1.0/00159768a00/5ac22f004b1ae4000b5b97cf?appid=bb0664ed43c153aa072c760594d775a7", + "falsecolor":"http://api.agromonitoring.com/data/1.0/01159768a00/5ac22f004b1ae4000b5b97cf?appid=bb0664ed43c153aa072c760594d775a7", + "ndvi":"http://api.agromonitoring.com/data/1.0/02259768a00/5ac22f004b1ae4000b5b97cf?appid=bb0664ed43c153aa072c760594d775a7", + "evi":"http://api.agromonitoring.com/data/1.0/03259768a00/5ac22f004b1ae4000b5b97cf?appid=bb0664ed43c153aa072c760594d775a7"}}]''') + test_issuing_time = 1378459200 + test_iso_issuing_time = "2013-09-06 09:20:00+00" + test_date_issuing_time = datetime.strptime(test_iso_issuing_time, '%Y-%m-%d %H:%M:%S+00').replace(tzinfo=UTC()) + + test_instance = SatelliteImagerySearchResultSet('my_polygon', test_data, test_issuing_time) + + def test_instantiation_fails_with_wrong_arguments(self): + self.assertRaises(AssertionError, SatelliteImagerySearchResultSet, None, [], 1234567) + self.assertRaises(AssertionError, SatelliteImagerySearchResultSet, 'my_polygon', None, 1234567) + self.assertRaises(AssertionError, SatelliteImagerySearchResultSet, 'my_polygon', [], None) + + def test_instantiation(self): + self.assertEqual(12, len(self.test_instance.metaimages)) + self.assertTrue(all([mi.stats_url is not None for mi in self.test_instance.metaimages if mi.preset in + [PresetEnum.EVI, PresetEnum.NDVI]])) + + def test_issued_on_returning_different_formats(self): + self.assertEqual(self.test_instance.issued_on(timeformat='unix'), + self.test_issuing_time) + self.assertEqual(self.test_instance.issued_on(timeformat='iso'), + self.test_iso_issuing_time) + self.assertEqual(self.test_instance.issued_on(timeformat='date'), + self.test_date_issuing_time) + + def test_issued_on_fails_with_unknown_timeformat(self): + self.assertRaises(ValueError, SatelliteImagerySearchResultSet.issued_on, self.test_instance, 'xyz') + + def test_all(self): + result = self.test_instance.all() + self.assertEqual(result, self.test_instance.metaimages) + + def test_with_img_type(self): + # failure + with self.assertRaises(AssertionError): + self.test_instance.with_img_type(1234) + + # success + result = self.test_instance.with_img_type(ImageTypeEnum.PNG) + self.assertEqual(8, len(result)) + result = self.test_instance.with_img_type(ImageTypeEnum.GEOTIFF) + self.assertEqual(4, len(result)) + + def test_with_preset(self): + # failure + with self.assertRaises(AssertionError): + self.test_instance.with_preset(1234) + + # success + result = self.test_instance.with_preset(PresetEnum.TRUE_COLOR) + self.assertEqual(3, len(result)) + result = self.test_instance.with_preset(PresetEnum.FALSE_COLOR) + self.assertEqual(3, len(result)) + result = self.test_instance.with_preset(PresetEnum.NDVI) + self.assertEqual(3, len(result)) + result = self.test_instance.with_preset(PresetEnum.EVI) + self.assertEqual(3, len(result)) + + def test_with_img_type_and_preset(self): + # failure + with self.assertRaises(AssertionError): + self.test_instance.with_img_type_and_preset(1234, 1234) + with self.assertRaises(AssertionError): + self.test_instance.with_img_type_and_preset(1234, PresetEnum.TRUE_COLOR) + with self.assertRaises(AssertionError): + self.test_instance.with_img_type_and_preset(ImageTypeEnum.PNG, 1234) + + # success + result = self.test_instance.with_img_type_and_preset(ImageTypeEnum.PNG, PresetEnum.TRUE_COLOR) + self.assertEqual(2, len(result)) + result = self.test_instance.with_img_type_and_preset(ImageTypeEnum.GEOTIFF, PresetEnum.EVI) + self.assertEqual(1, len(result)) + result = self.test_instance.with_img_type_and_preset(ImageTypeEnum.GEOTIFF, PresetEnum.FALSE_COLOR) + self.assertEqual(1, len(result)) diff --git a/tests/unit/agroapi10/test_soil.py b/tests/unit/agroapi10/test_soil.py new file mode 100644 index 00000000..8418549b --- /dev/null +++ b/tests/unit/agroapi10/test_soil.py @@ -0,0 +1,88 @@ +import unittest +from datetime import datetime +from pyowm.agroapi10.soil import Soil +from pyowm.utils.timeformatutils import UTC + + +class TestSoil(unittest.TestCase): + + test_reference_time = 1378459200 + test_iso_reference_time = "2013-09-06 09:20:00+00" + test_date_reference_time = datetime.strptime(test_iso_reference_time, '%Y-%m-%d %H:%M:%S+00').replace( + tzinfo=UTC()) + test_temperature = 294.199 + test_celsius_temperature = 21.049 + test_fahrenheit_temperature = 69.888 + test_instance = Soil(test_reference_time, test_temperature, test_temperature, 45.6, 'my-polygon') + + def test_soil_fails_with_wrong_parameters(self): + self.assertRaises(AssertionError, Soil, None, 12.4, 11.8, 80.2, 'my-polygon') + self.assertRaises(AssertionError, Soil, 'wrong', 12.4, 11.8, 80.2, 'my-polygon') + self.assertRaises(ValueError, Soil, -345, 12.4, 11.8, 80.2, 'my-polygon') + self.assertRaises(AssertionError, Soil, 1234567, None, 11.8, 80.2, 'my-polygon') + self.assertRaises(AssertionError, Soil, 1234567, 'wrong', 11.8, 80.2, 'my-polygon') + self.assertRaises(AssertionError, Soil, 1234567, 12.4, None, 80.2, 'my-polygon') + self.assertRaises(AssertionError, Soil, 1234567, 12.4, 'wrong', 80.2, 'my-polygon') + self.assertRaises(AssertionError, Soil, 1234567, 12.4, 11.8, None, 'my-polygon') + self.assertRaises(AssertionError, Soil, 1234567, 12.4, 11.8, "'wrong", 'my-polygon') + self.assertRaises(ValueError, Soil, 1234567, 12.4, 11.8, -45.6, 'my-polygon') + + def test_reference_time_returning_different_formats(self): + + self.assertEqual(self.test_instance.reference_time(timeformat='unix'), + self.test_reference_time) + self.assertEqual(self.test_instance.reference_time(timeformat='iso'), + self.test_iso_reference_time) + self.assertEqual(self.test_instance.reference_time(timeformat='date'), + self.test_date_reference_time) + + def test_reference_time_fails_with_unknown_timeformat(self): + self.assertRaises(ValueError, Soil.reference_time, self.test_instance, 'xyz') + + def test_from_dict(self): + ref_time = 12345567 + surf_temp = 11.2 + ten_cm_temp = 9.5 + moisture = 8.2 + pol_id = "5abb9fb82c8897000bde3e87" + the_dict = { + "reference_time": ref_time, + "surface_temp": surf_temp, + "ten_cm_temp": ten_cm_temp, + "moisture": moisture, + "polygon_id": pol_id + } + expected = Soil(ref_time, surf_temp, ten_cm_temp, moisture, pol_id) + result = Soil.from_dict(the_dict) + self.assertEqual(expected.reference_time(), result.reference_time()) + self.assertEqual(expected.surface_temp(), result.surface_temp()) + self.assertEqual(expected.ten_cm_temp(), result.ten_cm_temp()) + self.assertEqual(expected.moisture, result.moisture) + self.assertEqual(expected.polygon_id, result.polygon_id) + + def test_returning_different_units_for_temperatures(self): + # Surface temp + result_kelvin = self.test_instance.surface_temp(unit='kelvin') + result_celsius = self.test_instance.surface_temp(unit='celsius') + result_fahrenheit = self.test_instance.surface_temp(unit='fahrenheit') + self.assertAlmostEqual(result_kelvin, self.test_temperature, delta=0.1) + self.assertAlmostEqual(result_celsius, self.test_celsius_temperature, delta=0.1) + self.assertAlmostEqual(result_fahrenheit, self.test_fahrenheit_temperature, delta=0.1) + + # 10 cm temp + result_kelvin = self.test_instance.ten_cm_temp(unit='kelvin') + result_celsius = self.test_instance.ten_cm_temp(unit='celsius') + result_fahrenheit = self.test_instance.ten_cm_temp(unit='fahrenheit') + self.assertAlmostEqual(result_kelvin, self.test_temperature, delta=0.1) + self.assertAlmostEqual(result_celsius, self.test_celsius_temperature, delta=0.1) + self.assertAlmostEqual(result_fahrenheit, self.test_fahrenheit_temperature, delta=0.1) + + def test_trying_unknown_units_for_temperatures(self): + self.assertRaises(ValueError, Soil.surface_temp, self.test_instance, 'xyz') + self.assertRaises(ValueError, Soil.ten_cm_temp, self.test_instance, 'xyz') + + def test_repr(self): + instance = Soil(1234567, 12.4, 11.8, 80.2, 'my-polygon') + repr(instance) + instance = Soil(1234567, 12.4, 11.8, 80.2, None) + repr(instance) \ No newline at end of file diff --git a/tests/unit/commons/test_databoxes.py b/tests/unit/commons/test_databoxes.py new file mode 100644 index 00000000..d0d140e0 --- /dev/null +++ b/tests/unit/commons/test_databoxes.py @@ -0,0 +1,17 @@ +import unittest +from pyowm.commons.databoxes import ImageType, Satellite + + +class TestImageType(unittest.TestCase): + + def test_repr(self): + instance = ImageType('PDF', 'application/pdf') + repr(instance) + + +class TestSatellite(unittest.TestCase): + + def test_repr(self): + instance = Satellite('Terrasat', 'tst') + repr(instance) + diff --git a/tests/unit/commons/test_enums.py b/tests/unit/commons/test_enums.py new file mode 100644 index 00000000..c226bc05 --- /dev/null +++ b/tests/unit/commons/test_enums.py @@ -0,0 +1,23 @@ +import unittest +from pyowm.commons.enums import ImageTypeEnum +from pyowm.commons.databoxes import ImageType + + +class TestImageTypeEnum(unittest.TestCase): + + def test_lookup_by_mime_type(self): + mime_not_found = 'unexistent/xyz' + mime_found = 'image/png' + result = ImageTypeEnum.lookup_by_mime_type(mime_found) + self.assertTrue(isinstance(result, ImageType)) + result = ImageTypeEnum.lookup_by_mime_type(mime_not_found) + self.assertIsNone(result) + + def test_lookup_by_name(self): + name_not_found = 'ZOOMOOO' + name_found = 'GEOTIFF' + result = ImageTypeEnum.lookup_by_name(name_found) + self.assertTrue(isinstance(result, ImageType)) + result = ImageTypeEnum.lookup_by_name(name_not_found) + self.assertIsNone(result) + diff --git a/tests/unit/commons/test_http_client.py b/tests/unit/commons/test_http_client.py index 28704a89..278d17c3 100644 --- a/tests/unit/commons/test_http_client.py +++ b/tests/unit/commons/test_http_client.py @@ -11,6 +11,7 @@ class MockResponse: def __init__(self, status, payload): self.status_code = status self.text = payload + self.content = payload def json(self): return json.loads(self.text) @@ -233,3 +234,28 @@ def monkey_patched_get_timeouting(uri, params=None, headers=None, except api_call_error.APICallTimeoutError: requests.get = self.requests_original_get + def test_get_png(self): + expected_data = b'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01\x01\x03\x00\x00\x00%\xdbV\xca\x00\x00\x00\x03PLTE\x00p\xff\xa5G\xab\xa1\x00\x00\x00\x01tRNS\xcc\xd24V\xfd\x00\x00\x00\nIDATx\x9ccb\x00\x00\x00\x06\x00\x0367|\xa8\x00\x00\x00\x00IEND\xaeB`\x82' + + def monkey_patched_get(uri, stream=True, params=None, headers=None, timeout=None, + verify=False): + return MockResponse(200, expected_data) + + requests.get = monkey_patched_get + status, data = HttpClient().get_png('http://anyurl.com') + self.assertIsInstance(data, bytes) + self.assertEqual(expected_data, data) + requests.get = self.requests_original_get + + def test_get_geotiff(self): + expected_data = b'II*\x00\x08\x00\x04\x00k{\x84s\x84\x84\x8c\x84\x84\x84k\x84k\x84\x84k{s\x9c\x94k\x84' + + def monkey_patched_get(uri, stream=True, params=None, headers=None, timeout=None, + verify=False): + return MockResponse(200, expected_data) + + requests.get = monkey_patched_get + status, data = HttpClient().get_geotiff('http://anyurl.com') + self.assertIsInstance(data, bytes) + self.assertEqual(expected_data, data) + requests.get = self.requests_original_get diff --git a/tests/unit/commons/test_tile.py b/tests/unit/commons/test_tile.py new file mode 100644 index 00000000..4f98b8fa --- /dev/null +++ b/tests/unit/commons/test_tile.py @@ -0,0 +1,20 @@ +import unittest +from pyowm.commons.tile import Tile +from pyowm.utils.geo import Polygon +from pyowm.commons.image import Image + + +class TestTile(unittest.TestCase): + + def test_instantiation_fails_with_wrong_arguments(self): + i = Image(b'x/1') + self.assertRaises(AssertionError, Tile, -1, 2, 3, 'layer', i) + self.assertRaises(AssertionError, Tile, 1, -2, 3, 'layer', i) + self.assertRaises(AssertionError, Tile, 1, 2, -3, 'layer', i) + self.assertRaises(AssertionError, Tile, 1, 2, 3, 'layer', 'not-an-image') + + def test_bounding_box(self): + instance = Tile(0, 0, 18, 'temperature', Image(b'x/1')) + result = instance.bounding_polygon() + self.assertIsInstance(result, Polygon) + print(result.geojson()) \ No newline at end of file diff --git a/tests/unit/pollutionapi30/test_coindex.py b/tests/unit/pollutionapi30/test_coindex.py index 5277a4e9..b3c37ca4 100644 --- a/tests/unit/pollutionapi30/test_coindex.py +++ b/tests/unit/pollutionapi30/test_coindex.py @@ -1,6 +1,6 @@ import unittest from datetime import datetime -from pyowm.webapi25.location import Location +from pyowm.weatherapi25.location import Location from pyowm.pollutionapi30.coindex import COIndex from pyowm.utils.timeformatutils import UTC, _datetime_to_UNIXtime from tests.unit.pollutionapi30.json_test_dumps import COINDEX_JSON_DUMP diff --git a/tests/unit/pollutionapi30/test_no2index.py b/tests/unit/pollutionapi30/test_no2index.py index 6cd2ba88..f5c29d00 100644 --- a/tests/unit/pollutionapi30/test_no2index.py +++ b/tests/unit/pollutionapi30/test_no2index.py @@ -1,6 +1,6 @@ import unittest from datetime import datetime -from pyowm.webapi25.location import Location +from pyowm.weatherapi25.location import Location from pyowm.pollutionapi30.no2index import NO2Index from pyowm.utils.timeformatutils import UTC, _datetime_to_UNIXtime from tests.unit.pollutionapi30.json_test_dumps import NO2INDEX_JSON_DUMP diff --git a/tests/unit/pollutionapi30/test_ozone.py b/tests/unit/pollutionapi30/test_ozone.py index 44aa26e4..085357ee 100644 --- a/tests/unit/pollutionapi30/test_ozone.py +++ b/tests/unit/pollutionapi30/test_ozone.py @@ -1,6 +1,6 @@ import unittest from datetime import datetime -from pyowm.webapi25.location import Location +from pyowm.weatherapi25.location import Location from pyowm.pollutionapi30.ozone import Ozone from pyowm.utils.timeformatutils import UTC, _datetime_to_UNIXtime from tests.unit.pollutionapi30.json_test_dumps import OZONE_JSON_DUMP diff --git a/tests/unit/pollutionapi30/test_so2index.py b/tests/unit/pollutionapi30/test_so2index.py index 561a1f42..47cf92f4 100644 --- a/tests/unit/pollutionapi30/test_so2index.py +++ b/tests/unit/pollutionapi30/test_so2index.py @@ -1,6 +1,6 @@ import unittest from datetime import datetime -from pyowm.webapi25.location import Location +from pyowm.weatherapi25.location import Location from pyowm.pollutionapi30.so2index import SO2Index from pyowm.utils.timeformatutils import UTC, _datetime_to_UNIXtime from tests.unit.pollutionapi30.json_test_dumps import SO2INDEX_JSON_DUMP diff --git a/tests/unit/stationsapi30/test_stations_manager.py b/tests/unit/stationsapi30/test_stations_manager.py index 86cf5544..61c06ed6 100644 --- a/tests/unit/stationsapi30/test_stations_manager.py +++ b/tests/unit/stationsapi30/test_stations_manager.py @@ -268,3 +268,39 @@ def test_send_buffer_failing(self): with self.assertRaises(AssertionError): instance.send_buffer(None) + + def test__structure_dict(self): + temp = dict(min=0, max=100) + msmt = Measurement('test_station', 1378459200, + temperature=temp, wind_speed=2.1, wind_gust=67, + humidex=77, weather_other=dict(key='val')) + expected = { + 'station_id': 'test_station', + 'dt': 1378459200, + 'temperature': temp, + 'wind_speed': 2.1, + 'wind_gust': 67, + 'humidex': 77, + 'weather': [ + { + 'other': { + 'key': 'val' + } + } + ] + } + instance = StationsManager('API-Key') + result = instance._structure_dict(msmt) + self.assertEqual(expected['station_id'], result['station_id']) + self.assertEqual(expected['dt'], result['dt']) + self.assertEqual(expected['wind_speed'], result['wind_speed']) + self.assertEqual(expected['wind_gust'], result['wind_gust']) + self.assertEqual(expected['humidex'], result['humidex']) + self.assertEqual(expected['temperature'], result['temperature']) + for item in result['weather']: + content = item.get('other') + if content: + self.assertEqual(expected['weather'][0]['other'], content) + return + self.fail() + diff --git a/pyowm/webapi25/parsers/so2indexparser.py b/tests/unit/tiles/__init__.py similarity index 100% rename from pyowm/webapi25/parsers/so2indexparser.py rename to tests/unit/tiles/__init__.py diff --git a/tests/unit/tiles/test_tile_manager.py b/tests/unit/tiles/test_tile_manager.py new file mode 100644 index 00000000..21af118d --- /dev/null +++ b/tests/unit/tiles/test_tile_manager.py @@ -0,0 +1,29 @@ +import unittest +from pyowm.commons.http_client import HttpClient +from pyowm.tiles.tile_manager import TileManager +from pyowm.commons.tile import Tile +from pyowm.tiles.enums import MapLayerEnum + + +class MockHttpClientReturningTile(HttpClient): + + d = b'1234567890' + + def get_png(self, uri, params=None, headers=None): + return 200, self.d + + +class TestTileManager(unittest.TestCase): + + def test_instantiation_fails_with_wrong_arguments(self): + self.assertRaises(AssertionError, TileManager, None, MapLayerEnum.PRESSURE) + self.assertRaises(AssertionError, TileManager, 'apikey', None) + self.assertRaises(AssertionError, TileManager, 'apikey', 1234) + + def test_get_tile(self): + mocked = MockHttpClientReturningTile() + instance = TileManager('Api_key', 'a_layer') + instance.http_client = mocked + result = instance.get_tile(1, 2, 3) + self.assertIsInstance(result, Tile) + self.assertEqual(mocked.d, result.image.data) diff --git a/tests/unit/utils/test_geo.py b/tests/unit/utils/test_geo.py index 7b592fb1..8ba01ecb 100644 --- a/tests/unit/utils/test_geo.py +++ b/tests/unit/utils/test_geo.py @@ -166,6 +166,12 @@ def test_polygon_as_dict(self): p = geo.Polygon([[[2.3, 57.32], [23.19, -20.2], [-120.4, 19.15], [2.3, 57.32]]]) self.assertEqual(expected, p.as_dict()) + def test_polygon_points(self): + p = geo.Polygon([[[2.3, 57.32], [23.19, -20.2], [-120.4, 19.15], [2.3, 57.32]]]) + result = p.points + self.assertTrue(result) + self.assertTrue(all([isinstance(p, geo.Point) for p in result])) + def test_polygon_from_points(self): expected = geo.Polygon([[[2.3, 57.32], [23.19, -20.2], [2.3, 57.32]]]) list_of_lists = [ @@ -245,6 +251,7 @@ def test_from_polygons(self): class TestGeometryBuilder(unittest.TestCase): def test_unrecognized_geom_type(self): + self.assertRaises(AssertionError, geo.GeometryBuilder.build, None) self.assertRaises(ValueError, geo.GeometryBuilder.build, {"type": "Unknown"}) self.assertRaises(ValueError, geo.GeometryBuilder.build, {}) diff --git a/tests/unit/utils/test_stringutils.py b/tests/unit/utils/test_stringutils.py index 25302cbb..16cd0021 100644 --- a/tests/unit/utils/test_stringutils.py +++ b/tests/unit/utils/test_stringutils.py @@ -11,37 +11,3 @@ def test_obfuscate_API_key(self): self.assertEqual(expected, stringutils.obfuscate_API_key(API_key)) self.assertIsNone(stringutils.obfuscate_API_key(None)) - - def test_encode_to_utf8(self): - name = 'testname' - if sys.version_info > (3, 0): - result = stringutils.encode_to_utf8(name) - self.assertEqual(result, name) - else: # Python 2 - result = stringutils.encode_to_utf8(name) - try: - result.decode('ascii') - except: - self.fail() - - def test_assert_is_string(self): - a_string = 'test' - a_non_string = 123 - stringutils.assert_is_string(a_string) - self.assertRaises(AssertionError, stringutils.assert_is_string, - a_non_string) - - def test_assert_is_string_or_unicode(self): - a_string = 'test' - a_non_string = 123 - stringutils.assert_is_string_or_unicode(a_string) - self.assertRaises(AssertionError, - stringutils.assert_is_string_or_unicode, - a_non_string) - - try: # only for Python 2 - unicode_value = unicode('test') - stringutils.assert_is_string_or_unicode(unicode_value) - except: - pass - diff --git a/tests/unit/utils/test_weatherutils.py b/tests/unit/utils/test_weatherutils.py index f2ef4a48..0264c14e 100644 --- a/tests/unit/utils/test_weatherutils.py +++ b/tests/unit/utils/test_weatherutils.py @@ -3,9 +3,9 @@ """ import unittest -from pyowm.webapi25.weather import Weather +from pyowm.weatherapi25.weather import Weather from pyowm.utils import weatherutils -from pyowm.webapi25.weathercoderegistry import WeatherCodeRegistry +from pyowm.weatherapi25.weathercoderegistry import WeatherCodeRegistry from pyowm.exceptions.api_response_error import NotFoundError class TestWeatherUtils(unittest.TestCase): diff --git a/tests/unit/uvindexapi30/test_uvindex.py b/tests/unit/uvindexapi30/test_uvindex.py index 450cb845..9113985b 100644 --- a/tests/unit/uvindexapi30/test_uvindex.py +++ b/tests/unit/uvindexapi30/test_uvindex.py @@ -1,6 +1,6 @@ import unittest from datetime import datetime -from pyowm.webapi25.location import Location +from pyowm.weatherapi25.location import Location from pyowm.uvindexapi30.uvindex import UVIndex, uv_intensity_to_exposure_risk from pyowm.utils.timeformatutils import UTC diff --git a/tests/unit/weatherapi25/__init__.py b/tests/unit/weatherapi25/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/unit/webapi25/json_test_dumps.py b/tests/unit/weatherapi25/json_test_dumps.py similarity index 100% rename from tests/unit/webapi25/json_test_dumps.py rename to tests/unit/weatherapi25/json_test_dumps.py diff --git a/tests/unit/webapi25/json_test_responses.py b/tests/unit/weatherapi25/json_test_responses.py similarity index 92% rename from tests/unit/webapi25/json_test_responses.py rename to tests/unit/weatherapi25/json_test_responses.py index 1349c1fe..fa1d8586 100644 --- a/tests/unit/webapi25/json_test_responses.py +++ b/tests/unit/weatherapi25/json_test_responses.py @@ -26,6 +26,16 @@ ',"dt":1419210000,"id":7343,"main":{"pressure":1007,"temp":0},"name":'\ '"UWSS","rang":50,"type":1,"wind":{"deg":180,"speed":3}}]}' +WEATHER_AT_PLACES_IN_BBOX_JSON = '{"cod":"200","calctime":0.3107,"cnt":2,' \ + '"list":[{"id":2208791,"name":"Yafran","coord":{"lon":12.52859,"lat":32.06329},"main":{"temp":9.68,"temp_min":9.681,' \ + '"temp_max":9.681,"pressure":961.02,"sea_level":1036.82,"grnd_level":961.02,"humidity":85},"dt":1485784982,' \ + '"wind":{"speed":3.96,"deg":356.5},"rain":{"3h":0.255},"clouds":{"all":88},"weather":[{"id":500,"main":"Rain",' \ + '"description":"lightrain","icon":"10d"}]},{"id":2208425,"name":"Zuwarah","coord":{"lon":12.08199,"lat":32.931198},' \ + '"main":{"temp":15.36,"temp_min":15.356,"temp_max":15.356,"pressure":1036.81,"sea_level":1037.79,' \ + '"grnd_level":1036.81,"humidity":89},"dt":1485784982,"wind":{"speed":5.46,"deg":30.0002},"clouds":{"all":56},' \ + '"weather":[{"id":803,"main":"Clouds","description":"brokenclouds","icon":"04d"}]}]}' + + SEARCH_RESULTS_JSON = '{"cod": "200", "count": 2, "list": [{"clouds": {"all": ' \ '20}, "coord": {"lat": 51.50853, "lon": -0.12573999999999999}, "dt": 1378237178,' \ ' "id": 2643743, "main": {"humidity": 56, "pressure": 1025, "temp": ' \ diff --git a/tests/unit/weatherapi25/parsers/__init__.py b/tests/unit/weatherapi25/parsers/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/unit/webapi25/parsers/test_forecastparser.py b/tests/unit/weatherapi25/parsers/test_forecastparser.py similarity index 95% rename from tests/unit/webapi25/parsers/test_forecastparser.py rename to tests/unit/weatherapi25/parsers/test_forecastparser.py index d36476ab..7c276c59 100644 --- a/tests/unit/webapi25/parsers/test_forecastparser.py +++ b/tests/unit/weatherapi25/parsers/test_forecastparser.py @@ -3,10 +3,10 @@ """ import unittest -from pyowm.webapi25.parsers.forecastparser import ForecastParser +from pyowm.weatherapi25.parsers.forecastparser import ForecastParser from pyowm.exceptions.parse_response_error import ParseResponseError from pyowm.exceptions.api_response_error import APIResponseError -from tests.unit.webapi25.json_test_responses import ( +from tests.unit.weatherapi25.json_test_responses import ( THREE_HOURS_FORECAST_JSON, FORECAST_NOT_FOUND_JSON, INTERNAL_SERVER_ERROR_JSON, FORECAST_MALFORMED_JSON) diff --git a/tests/unit/webapi25/parsers/test_observationlistparser.py b/tests/unit/weatherapi25/parsers/test_observationlistparser.py similarity index 94% rename from tests/unit/webapi25/parsers/test_observationlistparser.py rename to tests/unit/weatherapi25/parsers/test_observationlistparser.py index 95080c4a..bbc47648 100644 --- a/tests/unit/webapi25/parsers/test_observationlistparser.py +++ b/tests/unit/weatherapi25/parsers/test_observationlistparser.py @@ -2,10 +2,10 @@ Test case for observationlistparser.py module """ import unittest -from pyowm.webapi25.parsers.observationlistparser import ObservationListParser +from pyowm.weatherapi25.parsers.observationlistparser import ObservationListParser from pyowm.exceptions.parse_response_error import ParseResponseError from pyowm.exceptions.api_response_error import APIResponseError -from tests.unit.webapi25.json_test_responses import ( +from tests.unit.weatherapi25.json_test_responses import ( SEARCH_RESULTS_JSON, SEARCH_WITH_NO_RESULTS_JSON, INTERNAL_SERVER_ERROR_JSON) diff --git a/tests/unit/webapi25/parsers/test_observationparser.py b/tests/unit/weatherapi25/parsers/test_observationparser.py similarity index 92% rename from tests/unit/webapi25/parsers/test_observationparser.py rename to tests/unit/weatherapi25/parsers/test_observationparser.py index c1bad99f..a3939059 100644 --- a/tests/unit/webapi25/parsers/test_observationparser.py +++ b/tests/unit/weatherapi25/parsers/test_observationparser.py @@ -2,10 +2,10 @@ Test case for observationparser.py module """ import unittest -from pyowm.webapi25.parsers.observationparser import ObservationParser +from pyowm.weatherapi25.parsers.observationparser import ObservationParser from pyowm.exceptions.parse_response_error import ParseResponseError from pyowm.exceptions.api_response_error import APIResponseError -from tests.unit.webapi25.json_test_responses import ( +from tests.unit.weatherapi25.json_test_responses import ( OBSERVATION_JSON, OBSERVATION_NOT_FOUND_JSON, OBSERVATION_MALFORMED_JSON) diff --git a/tests/unit/webapi25/parsers/test_stationhistoryparser.py b/tests/unit/weatherapi25/parsers/test_stationhistoryparser.py similarity index 90% rename from tests/unit/webapi25/parsers/test_stationhistoryparser.py rename to tests/unit/weatherapi25/parsers/test_stationhistoryparser.py index 258c4332..6b8fd30a 100644 --- a/tests/unit/webapi25/parsers/test_stationhistoryparser.py +++ b/tests/unit/weatherapi25/parsers/test_stationhistoryparser.py @@ -2,11 +2,11 @@ Test case for stationhistoryparser.py module """ import unittest -from pyowm.webapi25.parsers.stationhistoryparser import StationHistoryParser -from pyowm.webapi25.stationhistory import StationHistory +from pyowm.weatherapi25.parsers.stationhistoryparser import StationHistoryParser +from pyowm.weatherapi25.stationhistory import StationHistory from pyowm.exceptions.parse_response_error import ParseResponseError from pyowm.exceptions.api_response_error import APIResponseError -from tests.unit.webapi25.json_test_responses import ( +from tests.unit.weatherapi25.json_test_responses import ( STATION_TICK_WEATHER_HISTORY_JSON, STATION_WEATHER_HISTORY_NOT_FOUND_JSON, INTERNAL_SERVER_ERROR_JSON) diff --git a/tests/unit/webapi25/parsers/test_weatherhistoryparser.py b/tests/unit/weatherapi25/parsers/test_weatherhistoryparser.py similarity index 83% rename from tests/unit/webapi25/parsers/test_weatherhistoryparser.py rename to tests/unit/weatherapi25/parsers/test_weatherhistoryparser.py index 77921708..aba8f1d7 100644 --- a/tests/unit/webapi25/parsers/test_weatherhistoryparser.py +++ b/tests/unit/weatherapi25/parsers/test_weatherhistoryparser.py @@ -2,12 +2,12 @@ Test case for weatherhistoryparser.py module """ import unittest -from pyowm.webapi25.parsers.weatherhistoryparser import WeatherHistoryParser +from pyowm.weatherapi25.parsers.weatherhistoryparser import WeatherHistoryParser from pyowm.exceptions.parse_response_error import ParseResponseError from pyowm.exceptions.api_response_error import APIResponseError -from tests.unit.webapi25.json_test_responses import (CITY_WEATHER_HISTORY_JSON, - CITY_WEATHER_HISTORY_NO_RESULTS_JSON, CITY_WEATHER_HISTORY_NOT_FOUND_JSON, - INTERNAL_SERVER_ERROR_JSON) +from tests.unit.weatherapi25.json_test_responses import (CITY_WEATHER_HISTORY_JSON, + CITY_WEATHER_HISTORY_NO_RESULTS_JSON, CITY_WEATHER_HISTORY_NOT_FOUND_JSON, + INTERNAL_SERVER_ERROR_JSON) class TestWeatherHistoryParser(unittest.TestCase): diff --git a/tests/unit/webapi25/test_cityidregistry.py b/tests/unit/weatherapi25/test_cityidregistry.py similarity index 99% rename from tests/unit/webapi25/test_cityidregistry.py rename to tests/unit/weatherapi25/test_cityidregistry.py index 43aa2fde..09f3e7e5 100644 --- a/tests/unit/webapi25/test_cityidregistry.py +++ b/tests/unit/weatherapi25/test_cityidregistry.py @@ -7,8 +7,8 @@ from StringIO import StringIO except ImportError: from io import StringIO -from pyowm.webapi25.cityidregistry import CityIDRegistry -from pyowm.webapi25.location import Location +from pyowm.weatherapi25.cityidregistry import CityIDRegistry +from pyowm.weatherapi25.location import Location from pyowm.utils.geo import Point diff --git a/tests/unit/webapi25/test_forecast.py b/tests/unit/weatherapi25/test_forecast.py similarity index 95% rename from tests/unit/webapi25/test_forecast.py rename to tests/unit/weatherapi25/test_forecast.py index 127fcc5d..adbfaef9 100644 --- a/tests/unit/webapi25/test_forecast.py +++ b/tests/unit/weatherapi25/test_forecast.py @@ -4,12 +4,12 @@ import unittest from datetime import datetime -from pyowm.webapi25.location import Location -from pyowm.webapi25.weather import Weather -from pyowm.webapi25.forecast import Forecast +from pyowm.weatherapi25.location import Location +from pyowm.weatherapi25.weather import Weather +from pyowm.weatherapi25.forecast import Forecast from pyowm.utils.timeformatutils import UTC -from tests.unit.webapi25.json_test_dumps import FORECAST_JSON_DUMP -from tests.unit.webapi25.xml_test_dumps import FORECAST_XML_DUMP +from tests.unit.weatherapi25.json_test_dumps import FORECAST_JSON_DUMP +from tests.unit.weatherapi25.xml_test_dumps import FORECAST_XML_DUMP class TestForecast(unittest.TestCase): diff --git a/tests/unit/webapi25/test_forecaster.py b/tests/unit/weatherapi25/test_forecaster.py similarity index 98% rename from tests/unit/webapi25/test_forecaster.py rename to tests/unit/weatherapi25/test_forecaster.py index 1bedf7de..1fff96c7 100644 --- a/tests/unit/webapi25/test_forecaster.py +++ b/tests/unit/weatherapi25/test_forecaster.py @@ -4,10 +4,10 @@ import unittest from datetime import datetime -from pyowm.webapi25.location import Location -from pyowm.webapi25.weather import Weather -from pyowm.webapi25.forecast import Forecast -from pyowm.webapi25.forecaster import Forecaster +from pyowm.weatherapi25.location import Location +from pyowm.weatherapi25.weather import Weather +from pyowm.weatherapi25.forecast import Forecast +from pyowm.weatherapi25.forecaster import Forecaster from pyowm.utils.timeformatutils import UTC diff --git a/tests/unit/webapi25/test_historian.py b/tests/unit/weatherapi25/test_historian.py similarity index 98% rename from tests/unit/webapi25/test_historian.py rename to tests/unit/weatherapi25/test_historian.py index 15b1e6a8..0017d8a7 100644 --- a/tests/unit/webapi25/test_historian.py +++ b/tests/unit/weatherapi25/test_historian.py @@ -3,8 +3,8 @@ """ import unittest -from pyowm.webapi25.stationhistory import StationHistory -from pyowm.webapi25.historian import Historian +from pyowm.weatherapi25.stationhistory import StationHistory +from pyowm.weatherapi25.historian import Historian from pyowm.utils import temputils diff --git a/tests/unit/webapi25/test_location.py b/tests/unit/weatherapi25/test_location.py similarity index 96% rename from tests/unit/webapi25/test_location.py rename to tests/unit/weatherapi25/test_location.py index 022f907f..0d2c8921 100644 --- a/tests/unit/webapi25/test_location.py +++ b/tests/unit/weatherapi25/test_location.py @@ -4,10 +4,10 @@ import unittest import json -from pyowm.webapi25.location import Location, location_from_dictionary +from pyowm.weatherapi25.location import Location, location_from_dictionary from pyowm.utils.geo import Point -from tests.unit.webapi25.json_test_dumps import LOCATION_JSON_DUMP -from tests.unit.webapi25.xml_test_dumps import LOCATION_XML_DUMP +from tests.unit.weatherapi25.json_test_dumps import LOCATION_JSON_DUMP +from tests.unit.weatherapi25.xml_test_dumps import LOCATION_XML_DUMP class TestLocation(unittest.TestCase): diff --git a/tests/unit/webapi25/test_observation.py b/tests/unit/weatherapi25/test_observation.py similarity index 90% rename from tests/unit/webapi25/test_observation.py rename to tests/unit/weatherapi25/test_observation.py index db8cea77..549ffaa1 100644 --- a/tests/unit/webapi25/test_observation.py +++ b/tests/unit/weatherapi25/test_observation.py @@ -4,12 +4,12 @@ import unittest from datetime import datetime -from pyowm.webapi25.location import Location -from pyowm.webapi25.weather import Weather -from pyowm.webapi25.observation import Observation +from pyowm.weatherapi25.location import Location +from pyowm.weatherapi25.weather import Weather +from pyowm.weatherapi25.observation import Observation from pyowm.utils.timeformatutils import UTC -from tests.unit.webapi25.json_test_dumps import OBSERVATION_JSON_DUMP -from tests.unit.webapi25.xml_test_dumps import OBSERVATION_XML_DUMP +from tests.unit.weatherapi25.json_test_dumps import OBSERVATION_JSON_DUMP +from tests.unit.weatherapi25.xml_test_dumps import OBSERVATION_XML_DUMP class TestObservation(unittest.TestCase): diff --git a/tests/unit/webapi25/test_owm25.py b/tests/unit/weatherapi25/test_owm25.py similarity index 93% rename from tests/unit/webapi25/test_owm25.py rename to tests/unit/weatherapi25/test_owm25.py index 468c27b3..8a195685 100644 --- a/tests/unit/webapi25/test_owm25.py +++ b/tests/unit/weatherapi25/test_owm25.py @@ -14,44 +14,44 @@ import unittest import time -from tests.unit.webapi25.json_test_responses import (OBSERVATION_JSON, - SEARCH_RESULTS_JSON, THREE_HOURS_FORECAST_JSON, DAILY_FORECAST_JSON, - THREE_HOURS_FORECAST_AT_COORDS_JSON, DAILY_FORECAST_AT_COORDS_JSON, - THREE_HOURS_FORECAST_AT_ID_JSON, DAILY_FORECAST_AT_ID_JSON, - CITY_WEATHER_HISTORY_JSON, STATION_TICK_WEATHER_HISTORY_JSON, - STATION_WEATHER_HISTORY_JSON, THREE_HOURS_FORECAST_NOT_FOUND_JSON, - DAILY_FORECAST_NOT_FOUND_JSON, STATION_HISTORY_NO_ITEMS_JSON, - STATION_OBSERVATION_JSON, STATION_AT_COORDS_JSON, - WEATHER_AT_STATION_IN_BBOX_JSON) +from tests.unit.weatherapi25.json_test_responses import (OBSERVATION_JSON, + SEARCH_RESULTS_JSON, THREE_HOURS_FORECAST_JSON, DAILY_FORECAST_JSON, + THREE_HOURS_FORECAST_AT_COORDS_JSON, DAILY_FORECAST_AT_COORDS_JSON, + THREE_HOURS_FORECAST_AT_ID_JSON, DAILY_FORECAST_AT_ID_JSON, + CITY_WEATHER_HISTORY_JSON, STATION_TICK_WEATHER_HISTORY_JSON, + STATION_WEATHER_HISTORY_JSON, THREE_HOURS_FORECAST_NOT_FOUND_JSON, + DAILY_FORECAST_NOT_FOUND_JSON, STATION_HISTORY_NO_ITEMS_JSON, + STATION_OBSERVATION_JSON, STATION_AT_COORDS_JSON, + WEATHER_AT_STATION_IN_BBOX_JSON, WEATHER_AT_PLACES_IN_BBOX_JSON) from tests.unit.uvindexapi30.test_uvindexparser import UVINDEX_JSON from tests.unit.uvindexapi30.test_uvindexlistparser import UVINDEX_LIST_JSON from tests.unit.pollutionapi30.test_parsers import COINDEX_JSON, OZONE_JSON, NO2INDEX_JSON, SO2INDEX_JSON -from pyowm.webapi25.owm25 import OWM25 +from pyowm.weatherapi25.owm25 import OWM25 from pyowm.constants import PYOWM_VERSION from pyowm.commons.http_client import HttpClient from pyowm.uvindexapi30.uv_client import UltraVioletHttpClient from pyowm.pollutionapi30.airpollution_client import AirPollutionHttpClient from pyowm.exceptions.api_call_error import APICallTimeoutError -from pyowm.webapi25.forecast import Forecast -from pyowm.webapi25.observation import Observation -from pyowm.webapi25.weather import Weather -from pyowm.webapi25.location import Location -from pyowm.webapi25.forecaster import Forecaster -from pyowm.webapi25.station import Station -from pyowm.webapi25.stationhistory import StationHistory -from pyowm.webapi25.historian import Historian +from pyowm.weatherapi25.forecast import Forecast +from pyowm.weatherapi25.observation import Observation +from pyowm.weatherapi25.weather import Weather +from pyowm.weatherapi25.location import Location +from pyowm.weatherapi25.forecaster import Forecaster +from pyowm.weatherapi25.station import Station +from pyowm.weatherapi25.stationhistory import StationHistory +from pyowm.weatherapi25.historian import Historian from pyowm.uvindexapi30.uvindex import UVIndex from pyowm.pollutionapi30.coindex import COIndex from pyowm.pollutionapi30.ozone import Ozone from pyowm.pollutionapi30.no2index import NO2Index from pyowm.pollutionapi30.so2index import SO2Index -from pyowm.webapi25.parsers.forecastparser import ForecastParser -from pyowm.webapi25.parsers.observationparser import ObservationParser -from pyowm.webapi25.parsers.observationlistparser import ObservationListParser -from pyowm.webapi25.parsers.stationparser import StationParser -from pyowm.webapi25.parsers.stationlistparser import StationListParser -from pyowm.webapi25.parsers.stationhistoryparser import StationHistoryParser -from pyowm.webapi25.parsers.weatherhistoryparser import WeatherHistoryParser +from pyowm.weatherapi25.parsers.forecastparser import ForecastParser +from pyowm.weatherapi25.parsers.observationparser import ObservationParser +from pyowm.weatherapi25.parsers.observationlistparser import ObservationListParser +from pyowm.weatherapi25.parsers.stationparser import StationParser +from pyowm.weatherapi25.parsers.stationlistparser import StationListParser +from pyowm.weatherapi25.parsers.stationhistoryparser import StationHistoryParser +from pyowm.weatherapi25.parsers.weatherhistoryparser import WeatherHistoryParser from pyowm.uvindexapi30.parsers import UVIndexParser, UVIndexListParser from pyowm.pollutionapi30.parsers import COIndexParser, NO2IndexParser, SO2IndexParser, OzoneParser from pyowm.stationsapi30.stations_manager import StationsManager @@ -135,6 +135,9 @@ def mock_call_api_returning_station_history_with_no_items(self, uri, params=None def mock_api_call_returning_weather_at_stations_in_bbox(self, uri, params=None, headers=None): return 200, WEATHER_AT_STATION_IN_BBOX_JSON + def mock_api_call_returning_weather_at_places_in_bbox(self, uri, params=None, headers=None): + return 200, WEATHER_AT_PLACES_IN_BBOX_JSON + def mock_api_call_returning_station_at_coords(self, uri, params=None, headers=None): return 200, STATION_AT_COORDS_JSON @@ -742,6 +745,29 @@ def test_weather_at_station_in_bbox(self): self.assertTrue(isinstance(result.get_location(), Location)) self.assertTrue(result.get_reception_time() is not None) + def test_weather_at_places_in_bbox_fails_with_wrong_params(self): + self.assertRaises(AssertionError, OWM25.weather_at_places_in_bbox, + self.__test_instance, 12, 32, 15, 37, 'zoom') + self.assertRaises(ValueError, OWM25.weather_at_places_in_bbox, + self.__test_instance, 12, 32, 15, 37, -30) + self.assertRaises(AssertionError, OWM25.weather_at_places_in_bbox, + self.__test_instance, 12, 32, 15, 37, 10, 'cluster') + + def test_weather_at_places_in_bbox(self): + original_func = HttpClient.cacheable_get_json + HttpClient.cacheable_get_json = \ + self.mock_api_call_returning_weather_at_places_in_bbox + results = self.__test_instance\ + .weather_at_places_in_bbox(12,32,15,37,10) + HttpClient.cacheable_get_json = original_func + self.assertTrue(isinstance(results, list)) + for result in results: + self.assertTrue(isinstance(result, Observation)) + self.assertTrue(isinstance(result.get_weather(), Weather)) + self.assertTrue(isinstance(result.get_location(), Location)) + self.assertTrue(result.get_reception_time() is not None) + + def test_station_tick_history(self): original_func = HttpClient.cacheable_get_json HttpClient.cacheable_get_json = \ diff --git a/tests/unit/webapi25/test_station.py b/tests/unit/weatherapi25/test_station.py similarity index 92% rename from tests/unit/webapi25/test_station.py rename to tests/unit/weatherapi25/test_station.py index 7136ad22..9d3b9b7c 100644 --- a/tests/unit/webapi25/test_station.py +++ b/tests/unit/weatherapi25/test_station.py @@ -4,10 +4,10 @@ import unittest -from pyowm.webapi25.station import Station -from pyowm.webapi25.weather import Weather -from tests.unit.webapi25.json_test_dumps import STATION_JSON_DUMP -from tests.unit.webapi25.xml_test_dumps import STATION_XML_DUMP +from pyowm.weatherapi25.station import Station +from pyowm.weatherapi25.weather import Weather +from tests.unit.weatherapi25.json_test_dumps import STATION_JSON_DUMP +from tests.unit.weatherapi25.xml_test_dumps import STATION_XML_DUMP class TestStation(unittest.TestCase): diff --git a/tests/unit/webapi25/test_stationhistory.py b/tests/unit/weatherapi25/test_stationhistory.py similarity index 93% rename from tests/unit/webapi25/test_stationhistory.py rename to tests/unit/weatherapi25/test_stationhistory.py index 56b459b6..706d1cc3 100644 --- a/tests/unit/webapi25/test_stationhistory.py +++ b/tests/unit/weatherapi25/test_stationhistory.py @@ -4,10 +4,10 @@ import unittest from datetime import datetime -from pyowm.webapi25.stationhistory import StationHistory +from pyowm.weatherapi25.stationhistory import StationHistory from pyowm.utils.timeformatutils import UTC -from tests.unit.webapi25.json_test_dumps import STATIONHISTORY_JSON_DUMP -from tests.unit.webapi25.xml_test_dumps import STATIONHISTORY_XML_DUMP +from tests.unit.weatherapi25.json_test_dumps import STATIONHISTORY_JSON_DUMP +from tests.unit.weatherapi25.xml_test_dumps import STATIONHISTORY_XML_DUMP class TestStationHistory(unittest.TestCase): diff --git a/tests/unit/webapi25/test_weather.py b/tests/unit/weatherapi25/test_weather.py similarity index 99% rename from tests/unit/webapi25/test_weather.py rename to tests/unit/weatherapi25/test_weather.py index 9bb63193..d7e02b4e 100644 --- a/tests/unit/webapi25/test_weather.py +++ b/tests/unit/weatherapi25/test_weather.py @@ -3,10 +3,9 @@ """ import unittest -from pyowm.webapi25.weather import Weather, weather_from_dictionary +from pyowm.weatherapi25.weather import Weather, weather_from_dictionary from pyowm.utils.timeformatutils import UTC -from tests.unit.webapi25.json_test_dumps import WEATHER_JSON_DUMP -from tests.unit.webapi25.xml_test_dumps import WEATHER_XML_DUMP +from tests.unit.weatherapi25.json_test_dumps import WEATHER_JSON_DUMP from datetime import datetime diff --git a/tests/unit/webapi25/test_weathercoderegistry.py b/tests/unit/weatherapi25/test_weathercoderegistry.py similarity index 89% rename from tests/unit/webapi25/test_weathercoderegistry.py rename to tests/unit/weatherapi25/test_weathercoderegistry.py index e93963e3..6a8eeeaf 100644 --- a/tests/unit/webapi25/test_weathercoderegistry.py +++ b/tests/unit/weatherapi25/test_weathercoderegistry.py @@ -3,7 +3,7 @@ """ import unittest -from pyowm.webapi25.weathercoderegistry import WeatherCodeRegistry +from pyowm.weatherapi25.weathercoderegistry import WeatherCodeRegistry class TestWeatherCodeRegistry(unittest.TestCase): diff --git a/tests/unit/weatherapi25/xml_test_dumps.py b/tests/unit/weatherapi25/xml_test_dumps.py new file mode 100644 index 00000000..376f4b5d --- /dev/null +++ b/tests/unit/weatherapi25/xml_test_dumps.py @@ -0,0 +1,22 @@ +""" +XML dumps for PyOWM test objects +""" + +LOCATION_XML_DUMP = """ +London12.343.71234UK""" + +WEATHER_XML_DUMP = """ +Clouds8042001030.1191038.589137844960004d67-1.899294.199294.199296.098Overcast clouds13784592001378496400571.1252.0022.091000300.0298.040.0""" + +OBSERVATION_XML_DUMP = """ +1234567test12.343.7987UKClouds8042001030.1191038.589137844960004d67-1.899294.199294.199296.098Overcast clouds13784592001378496400571.1252.0021000300.0298.0296.0""" + +FORECAST_XML_DUMP = """ +daily1234567test12.343.7987ITClouds8042001030.1191038.589137844960004d67-1.899294.199294.199296.098Overcast clouds13784592001378496400571.1252.0021000300.0298.0296.0Clear8041001070.1191078.589137844951002d23-1.899295.6297.199299.0Sky is clear13784596901378496480124.2103.41000300.0298.0296.0""" + +STATIONHISTORY_XML_DUMP = """ +2865tick1378684800266.85136293404327.71010.094.7266.25136293398327.31010.024.7""" + + +STATION_XML_DUMP = """ +KNGU286515036.9375-76.289318.95Clouds8042001030.1191038.589137844960004d67-1.899294.199294.199296.098Overcast clouds13784592001378496400571.1252.0021000300.0298.0296.0""" diff --git a/tests/unit/webapi25/xml_test_dumps.py b/tests/unit/webapi25/xml_test_dumps.py deleted file mode 100644 index 0575a4ac..00000000 --- a/tests/unit/webapi25/xml_test_dumps.py +++ /dev/null @@ -1,22 +0,0 @@ -""" -XML dumps for PyOWM test objects -""" - -LOCATION_XML_DUMP = """ -London12.343.71234UK""" - -WEATHER_XML_DUMP = """ -Clouds8042001030.1191038.589137844960004d67-1.899294.199294.199296.098Overcast clouds13784592001378496400571.1252.0022.091000300.0298.040.0""" - -OBSERVATION_XML_DUMP = """ -1234567test12.343.7987UKClouds8042001030.1191038.589137844960004d67-1.899294.199294.199296.098Overcast clouds13784592001378496400571.1252.0021000300.0298.0296.0""" - -FORECAST_XML_DUMP = """ -daily1234567test12.343.7987ITClouds8042001030.1191038.589137844960004d67-1.899294.199294.199296.098Overcast clouds13784592001378496400571.1252.0021000300.0298.0296.0Clear8041001070.1191078.589137844951002d23-1.899295.6297.199299.0Sky is clear13784596901378496480124.2103.41000300.0298.0296.0""" - -STATIONHISTORY_XML_DUMP = """ -2865tick1378684800266.85136293404327.71010.094.7266.25136293398327.31010.024.7""" - - -STATION_XML_DUMP = """ -KNGU286515036.9375-76.289318.95Clouds8042001030.1191038.589137844960004d67-1.899294.199294.199296.098Overcast clouds13784592001378496400571.1252.0021000300.0298.0296.0""" diff --git a/tox.ini b/tox.ini index fbd17d71..5f3f083a 100644 --- a/tox.ini +++ b/tox.ini @@ -1,17 +1,16 @@ [tox] envlist = - py27, py34, py35, py36, py37 + py34, py35, py36, py37, coverage skip_missing_interpreters = True [testenv] commands = - pip install -r requirements.txt python setup.py test -s tests.unit -[testenv:docs] -changedir = sphinx -deps = - sphinx +[testenv:coverage] +whitelist_externals = coverage commands = - make html + coverage run --rcfile=.coveragerc setup.py test -s tests.unit + coverage html + coverage report