diff -Nru pygeoapi-0.11.0/debian/changelog pygeoapi-0.12.0/debian/changelog --- pygeoapi-0.11.0/debian/changelog 2021-10-30 09:00:00.000000000 +0000 +++ pygeoapi-0.12.0/debian/changelog 2022-02-07 18:00:00.000000000 +0000 @@ -1,8 +1,8 @@ -pygeoapi (0.11.0-1~focal1) focal; urgency=medium +pygeoapi (0.12.0-1~focal0) focal; urgency=medium + + * New upstream release - * Adding pydantic and elasticsearch-dsl to suggested packages. - - -- Angelos Tzotsos Sat, 30 Oct 2021 12:00:00 +0300 + -- Angelos Tzotsos Mon, 07 Feb 2022 20:00:00 +0200 pygeoapi (0.11.0-1~focal0) focal; urgency=medium diff -Nru pygeoapi-0.11.0/docker/default.config.yml pygeoapi-0.12.0/docker/default.config.yml --- pygeoapi-0.11.0/docker/default.config.yml 2021-10-24 14:33:20.000000000 +0000 +++ pygeoapi-0.12.0/docker/default.config.yml 2022-02-07 20:05:13.000000000 +0000 @@ -41,6 +41,7 @@ url: http://localhost:5000 mimetype: application/json; charset=UTF-8 encoding: utf-8 + gzip: false language: en-US cors: true pretty_print: true diff -Nru pygeoapi-0.11.0/docker/examples/elastic/pygeoapi/docker.config.yml pygeoapi-0.12.0/docker/examples/elastic/pygeoapi/docker.config.yml --- pygeoapi-0.11.0/docker/examples/elastic/pygeoapi/docker.config.yml 2021-10-24 14:33:20.000000000 +0000 +++ pygeoapi-0.12.0/docker/examples/elastic/pygeoapi/docker.config.yml 2022-02-07 20:05:13.000000000 +0000 @@ -38,6 +38,7 @@ url: http://localhost:5000/ mimetype: application/json; charset=UTF-8 encoding: utf-8 + gzip: false language: en-US cors: true pretty_print: true diff -Nru pygeoapi-0.11.0/docker/examples/geosparql/test.pygeoapi.config.yml pygeoapi-0.12.0/docker/examples/geosparql/test.pygeoapi.config.yml --- pygeoapi-0.11.0/docker/examples/geosparql/test.pygeoapi.config.yml 2021-10-24 14:33:20.000000000 +0000 +++ pygeoapi-0.12.0/docker/examples/geosparql/test.pygeoapi.config.yml 2022-02-07 20:05:13.000000000 +0000 @@ -34,6 +34,7 @@ url: http://localhost:5000 #change to host URL if running your own instance mimetype: application/json; charset=UTF-8 encoding: utf-8 + gzip: false language: en-US cors: true pretty_print: true diff -Nru pygeoapi-0.11.0/docker/examples/mongo/pygeoapi/mongo-entrypoint.sh pygeoapi-0.12.0/docker/examples/mongo/pygeoapi/mongo-entrypoint.sh --- pygeoapi-0.11.0/docker/examples/mongo/pygeoapi/mongo-entrypoint.sh 1970-01-01 00:00:00.000000000 +0000 +++ pygeoapi-0.12.0/docker/examples/mongo/pygeoapi/mongo-entrypoint.sh 2022-02-07 20:05:13.000000000 +0000 @@ -0,0 +1,43 @@ +#!/bin/sh +# ================================================================= +# +# Authors: Just van den Broecke > +# Jorge Samuel Mendes de Jesus +# +# Copyright (c) 2019 Just van den Broecke +# Copyright (c) 2019 Jorge Samuel Mendes de Jesus +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation +# files (the "Software"), to deal in the Software without +# restriction, including without limitation the rights to use, +# copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following +# conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +# OTHER DEALINGS IN THE SOFTWARE. +# +# ================================================================= + +set +e + +echo "Installing NC" + +apt-get update ; +apt-get install -y netcat; + +echo "Waiting for Mongo container..." + +# First wait for MDB to be up and then execute the original pygeoapi entrypoint. +/wait-for-mongo.sh /entrypoint.sh || echo "MDB failed: $?, exit" && exit 1 diff -Nru pygeoapi-0.11.0/docker/examples/sensorthings/brgm.sta.pygeoapi.config.yml pygeoapi-0.12.0/docker/examples/sensorthings/brgm.sta.pygeoapi.config.yml --- pygeoapi-0.11.0/docker/examples/sensorthings/brgm.sta.pygeoapi.config.yml 2021-10-24 14:33:20.000000000 +0000 +++ pygeoapi-0.12.0/docker/examples/sensorthings/brgm.sta.pygeoapi.config.yml 2022-02-07 20:05:13.000000000 +0000 @@ -34,6 +34,7 @@ url: http://localhost:5000 #change to host URL if running your own instance mimetype: application/json; charset=UTF-8 encoding: utf-8 + gzip: false languages: # First language is the default language - en-US diff -Nru pygeoapi-0.11.0/docker/examples/sensorthings/iow.sta.pygeoapi.config.yml pygeoapi-0.12.0/docker/examples/sensorthings/iow.sta.pygeoapi.config.yml --- pygeoapi-0.11.0/docker/examples/sensorthings/iow.sta.pygeoapi.config.yml 2021-10-24 14:33:20.000000000 +0000 +++ pygeoapi-0.12.0/docker/examples/sensorthings/iow.sta.pygeoapi.config.yml 2022-02-07 20:05:13.000000000 +0000 @@ -34,6 +34,7 @@ url: http://localhost:5000 #change to host URL if running your own instance mimetype: application/json; charset=UTF-8 encoding: utf-8 + gzip: false languages: # First language is the default language - en-US diff -Nru pygeoapi-0.11.0/docker/examples/sensorthings/sta.pygeoapi.config.yml pygeoapi-0.12.0/docker/examples/sensorthings/sta.pygeoapi.config.yml --- pygeoapi-0.11.0/docker/examples/sensorthings/sta.pygeoapi.config.yml 2021-10-24 14:33:20.000000000 +0000 +++ pygeoapi-0.12.0/docker/examples/sensorthings/sta.pygeoapi.config.yml 2022-02-07 20:05:13.000000000 +0000 @@ -34,6 +34,7 @@ url: http://localhost:5000 #change to host URL if running your own instance mimetype: application/json; charset=UTF-8 encoding: utf-8 + gzip: false languages: # First language is the default language - en-US diff -Nru pygeoapi-0.11.0/docker/examples/simple/my.config.yml pygeoapi-0.12.0/docker/examples/simple/my.config.yml --- pygeoapi-0.11.0/docker/examples/simple/my.config.yml 2021-10-24 14:33:20.000000000 +0000 +++ pygeoapi-0.12.0/docker/examples/simple/my.config.yml 2022-02-07 20:05:13.000000000 +0000 @@ -36,6 +36,7 @@ url: http://localhost:5000 mimetype: application/json; charset=UTF-8 encoding: utf-8 + gzip: false language: en-US cors: true pretty_print: true diff -Nru pygeoapi-0.11.0/docs/source/administration.rst pygeoapi-0.12.0/docs/source/administration.rst --- pygeoapi-0.11.0/docs/source/administration.rst 2021-10-24 14:33:20.000000000 +0000 +++ pygeoapi-0.12.0/docs/source/administration.rst 2022-02-07 20:05:13.000000000 +0000 @@ -6,9 +6,9 @@ Now that you have pygeoapi installed and a basic configuration setup, it's time to complete the administrative steps required before starting up the server. The remaining steps are: -- create OpenAPI document -- validate OpenAPI document -- set system environment variables +* create OpenAPI document +* validate OpenAPI document +* set system environment variables Creating the OpenAPI document ----------------------------- diff -Nru pygeoapi-0.11.0/docs/source/configuration.rst pygeoapi-0.12.0/docs/source/configuration.rst --- pygeoapi-0.11.0/docs/source/configuration.rst 2021-10-24 14:33:20.000000000 +0000 +++ pygeoapi-0.12.0/docs/source/configuration.rst 2022-02-07 20:05:13.000000000 +0000 @@ -41,6 +41,7 @@ mimetype: application/json; charset=UTF-8 # default MIME type encoding: utf-8 # default server encoding language: en-US # default server language + gzip: false # default server config to gzip/compress responses to requests with gzip in the Accept-Encoding header cors: true # boolean on whether server should support CORS pretty_print: true # whether JSON responses should be pretty-printed limit: 10 # server limit on number of items to return @@ -262,6 +263,7 @@ The optional configuration options for collections, at the level of an item of items, are: - If ``uri_field`` is specified, JSON-LD will be updated such that the ``@id`` has the value of ``uri_field`` for each item in a collection + .. note:: While this is enough to provide valid RDF (as GeoJSON-LD), it does not allow the *properties* of your items to be unambiguously interpretable. diff -Nru pygeoapi-0.11.0/docs/source/conf.py pygeoapi-0.12.0/docs/source/conf.py --- pygeoapi-0.11.0/docs/source/conf.py 2021-10-24 14:33:20.000000000 +0000 +++ pygeoapi-0.12.0/docs/source/conf.py 2022-02-07 20:05:13.000000000 +0000 @@ -2,7 +2,7 @@ # # Authors: Tom Kralidis # -# Copyright (c) 2021 Tom Kralidis +# Copyright (c) 2022 Tom Kralidis # # Permission is hereby granted, free of charge, to any person # obtaining a copy of this software and associated documentation @@ -98,7 +98,7 @@ project = 'pygeoapi' author = 'pygeoapi team' license = 'This work is licensed under a Creative Commons Attribution 4.0 International License' # noqa -copyright = '2018-2021, ' + author + ' ' + license +copyright = '2018-2022, ' + author + ' ' + license today_fmt = '%Y-%m-%d' @@ -107,7 +107,7 @@ # built documents. # # The short X.Y version. -version = '0.11.0' +version = '0.12.0' # The full version, including alpha/beta/rc tags. release = version diff -Nru pygeoapi-0.11.0/docs/source/cql.rst pygeoapi-0.12.0/docs/source/cql.rst --- pygeoapi-0.11.0/docs/source/cql.rst 2021-10-24 14:33:20.000000000 +0000 +++ pygeoapi-0.12.0/docs/source/cql.rst 2022-02-07 20:05:13.000000000 +0000 @@ -9,9 +9,9 @@ The support to CQL is limited to `Simple CQL filter `_ and thus it allows to query with the following predicates: -- comparison predicates -- spatial predicates -- temporal predicates +* comparison predicates +* spatial predicates +* temporal predicates Formats ------- @@ -28,9 +28,9 @@ The following type of queries are supported right now: -- ``between`` predicate query -- Logical ``and`` query with ``between`` and ``eq`` expression -- Spatial query with ``bbox`` +* ``between`` predicate query +* Logical ``and`` query with ``between`` and ``eq`` expression +* Spatial query with ``bbox`` Examples ^^^^^^^^ diff -Nru pygeoapi-0.11.0/docs/source/data-publishing/ogcapi-coverages.rst pygeoapi-0.12.0/docs/source/data-publishing/ogcapi-coverages.rst --- pygeoapi-0.11.0/docs/source/data-publishing/ogcapi-coverages.rst 2021-10-24 14:33:20.000000000 +0000 +++ pygeoapi-0.12.0/docs/source/data-publishing/ogcapi-coverages.rst 2022-02-07 20:05:13.000000000 +0000 @@ -15,7 +15,7 @@ parameters. .. csv-table:: - :header: Provider, rangeSubset, subset, bbox, datetime + :header: Provider, range-subset, subset, bbox, datetime :align: left rasterio,✅,✅,✅, @@ -84,22 +84,26 @@ Data access examples -------------------- -- list all collections - - http://localhost:5000/collections -- overview of dataset - - http://localhost:5000/collections/foo -- coverage rangetype - - http://localhost:5000/collections/foo/coverage/rangetype -- coverage domainset - - http://localhost:5000/collections/foo/coverage/domainset -- coverage access via CoverageJSON (default) - - http://localhost:5000/collections/foo/coverage?f=json -- coverage access via native format (as defined in ``provider.format.name``) - - http://localhost:5000/collections/foo/coverage?f=GRIB -- coverage access with comma-separated rangeSubset - - http://localhost:5000/collections/foo/coverage?rangeSubset=1,3 -- coverage access with subsetting - - http://localhost:5000/collections/foo/coverage?subset=lat(10,20)&subset=long(10,20) +* list all collections + * http://localhost:5000/collections +* overview of dataset + * http://localhost:5000/collections/foo +* coverage rangetype + * http://localhost:5000/collections/foo/coverage/rangetype +* coverage domainset + * http://localhost:5000/collections/foo/coverage/domainset +* coverage access via CoverageJSON (default) + * http://localhost:5000/collections/foo/coverage?f=json +* coverage access via native format (as defined in ``provider.format.name``) + * http://localhost:5000/collections/foo/coverage?f=GRIB +* coverage access with comma-separated range-subset + * http://localhost:5000/collections/foo/coverage?range-subset=1,3 +* coverage access with subsetting + * http://localhost:5000/collections/foo/coverage?subset=lat(10,20)&subset=long(10,20) + +.. note:: + ``.../coverage`` queries which return an alternative representation to CoverageJSON (which prompt a download) + will have the response filename matching the collection name and appropriate file extension (e.g. ``my-dataset.nc``) .. _`OGC API - Coverages`: https://github.com/opengeospatial/ogcapi-coverages .. _`rasterio`: https://rasterio.readthedocs.io diff -Nru pygeoapi-0.11.0/docs/source/data-publishing/ogcapi-edr.rst pygeoapi-0.12.0/docs/source/data-publishing/ogcapi-edr.rst --- pygeoapi-0.11.0/docs/source/data-publishing/ogcapi-edr.rst 2021-10-24 14:33:20.000000000 +0000 +++ pygeoapi-0.12.0/docs/source/data-publishing/ogcapi-edr.rst 2022-02-07 20:05:13.000000000 +0000 @@ -64,16 +64,16 @@ Data access examples -------------------- -- list all collections - - http://localhost:5000/collections -- overview of dataset - - http://localhost:5000/collections/foo -- dataset position query - - http://localhost:5000/collections/foo/position?coords=POINT(-75%2045) -- dataset position query for a specific parameter - - http://localhost:5000/collections/foo/position?coords=POINT(-75%2045)¶meter-name=SST -- dataset position query for a specific parameter and time step - - http://localhost:5000/collections/foo/position?coords=POINT(-75%2045)¶meter-name=SST&datetime=2000-01-16 +* list all collections + * http://localhost:5000/collections +* overview of dataset + * http://localhost:5000/collections/foo +* dataset position query + * http://localhost:5000/collections/foo/position?coords=POINT(-75%2045) +* dataset position query for a specific parameter + * http://localhost:5000/collections/foo/position?coords=POINT(-75%2045)¶meter-name=SST +* dataset position query for a specific parameter and time step + * http://localhost:5000/collections/foo/position?coords=POINT(-75%2045)¶meter-name=SST&datetime=2000-01-16 .. _`xarray`: https://xarray.pydata.org diff -Nru pygeoapi-0.11.0/docs/source/data-publishing/ogcapi-features.rst pygeoapi-0.12.0/docs/source/data-publishing/ogcapi-features.rst --- pygeoapi-0.11.0/docs/source/data-publishing/ogcapi-features.rst 2021-10-24 14:33:20.000000000 +0000 +++ pygeoapi-0.12.0/docs/source/data-publishing/ogcapi-features.rst 2022-02-07 20:05:13.000000000 +0000 @@ -76,8 +76,8 @@ To publish an Elasticsearch index, the following are required in your index: -- indexes must be documents of valid GeoJSON Features -- index mappings must define the GeoJSON ``geometry`` as a ``geo_shape`` +* indexes must be documents of valid GeoJSON Features +* index mappings must define the GeoJSON ``geometry`` as a ``geo_shape`` .. code-block:: yaml @@ -138,6 +138,30 @@ CPL_DEBUG: NO id_field: gml_id layer: rdinfo:stations + +.. code-block:: yaml + + providers: + - type: feature + name: OGR + data: + source_type: ESRIJSON + source: https://map.bgs.ac.uk/arcgis/rest/services/GeoIndex_Onshore/boreholes/MapServer/0/query?where=BGS_ID+%3D+BGS_ID&outfields=*&orderByFields=BGS_ID+ASC&f=json + source_srs: EPSG:27700 + target_srs: EPSG:4326 + source_capabilities: + paging: True + open_options: + FEATURE_SERVER_PAGING: YES + gdal_ogr_options: + EMPTY_AS_NULL: NO + GDAL_CACHEMAX: 64 + # GDAL_HTTP_PROXY: (optional proxy) + # GDAL_PROXY_AUTH: (optional auth for remote WFS) + CPL_DEBUG: NO + id_field: BGS_ID + layer: ESRIJSON + MongoDB @@ -157,6 +181,8 @@ PostgreSQL ^^^^^^^^^^ +Must have PostGIS installed. + .. todo:: add overview and requirements .. code-block:: yaml @@ -166,6 +192,7 @@ name: PostgreSQL data: host: 127.0.0.1 + port: 3010 # Default 5432 if not provided dbname: test user: postgres password: postgres @@ -245,29 +272,33 @@ Data access examples -------------------- -- list all collections - - http://localhost:5000/collections -- overview of dataset - - http://localhost:5000/collections/foo -- queryables - - http://localhost:5000/collections/foo/queryables -- browse features - - http://localhost:5000/collections/foo/items -- paging - - http://localhost:5000/collections/foo/items?startIndex=10&limit=10 -- CSV outputs - - http://localhost:5000/collections/foo/items?f=csv -- query features (spatial) - - http://localhost:5000/collections/foo/items?bbox=-180,-90,180,90 -- query features (attribute) - - http://localhost:5000/collections/foo/items?propertyname=foo -- query features (temporal) - - http://localhost:5000/collections/foo/items?datetime=2020-04-10T14:11:00Z -- query features (temporal) and sort ascending by a property (if no +/- indicated, + is assumed) - - http://localhost:5000/collections/foo/items?datetime=2020-04-10T14:11:00Z&sortby=+datetime -- query features (temporal) and sort descending by a property - - http://localhost:5000/collections/foo/items?datetime=2020-04-10T14:11:00Z&sortby=-datetime -- fetch a specific feature - - http://localhost:5000/collections/foo/items/123 +* list all collections + * http://localhost:5000/collections +* overview of dataset + * http://localhost:5000/collections/foo +* queryables + * http://localhost:5000/collections/foo/queryables +* browse features + * http://localhost:5000/collections/foo/items +* paging + * http://localhost:5000/collections/foo/items?startIndex=10&limit=10 +* CSV outputs + * http://localhost:5000/collections/foo/items?f=csv +* query features (spatial) + * http://localhost:5000/collections/foo/items?bbox=-180,-90,180,90 +* query features (attribute) + * http://localhost:5000/collections/foo/items?propertyname=foo +* query features (temporal) + * http://localhost:5000/collections/foo/items?datetime=2020-04-10T14:11:00Z +* query features (temporal) and sort ascending by a property (if no +/- indicated, + is assumed) + * http://localhost:5000/collections/foo/items?datetime=2020-04-10T14:11:00Z&sortby=+datetime +* query features (temporal) and sort descending by a property + * http://localhost:5000/collections/foo/items?datetime=2020-04-10T14:11:00Z&sortby=-datetime +* fetch a specific feature + * http://localhost:5000/collections/foo/items/123 + +.. note:: + ``.../items`` queries which return an alternative representation to GeoJSON (which prompt a download) + will have the response filename matching the collection name and appropriate file extension (e.g. ``my-dataset.csv``) .. _`OGC API - Features`: https://www.ogc.org/standards/ogcapi-features diff -Nru pygeoapi-0.11.0/docs/source/data-publishing/ogcapi-processes.rst pygeoapi-0.12.0/docs/source/data-publishing/ogcapi-processes.rst --- pygeoapi-0.11.0/docs/source/data-publishing/ogcapi-processes.rst 2021-10-24 14:33:20.000000000 +0000 +++ pygeoapi-0.12.0/docs/source/data-publishing/ogcapi-processes.rst 2022-02-07 20:05:13.000000000 +0000 @@ -52,26 +52,26 @@ To summarize how pygeoapi processes and managers work together:: -- process plugins implement the core processing / workflow functionality -- manager plugins control and manage how processes are executed +* process plugins implement the core processing / workflow functionality +* manager plugins control and manage how processes are executed Processing examples ------------------- -- list all processes - - http://localhost:5000/processes -- describe the ``hello-world`` process - - http://localhost:5000/processes/hello-world -- show all jobs for the ``hello-world`` process - - http://localhost:5000/processes/hello-world/jobs -- execute a job for the ``hello-world`` process - - ``curl -X POST "http://localhost:5000/processes/hello-world/execution" -H "Content-Type: application/json" -d "{\"inputs\":{\"name\": \"hi there2\"}}"`` -- execute a job for the ``hello-world`` process with a raw response (default) - - ``curl -X POST "http://localhost:5000/processes/hello-world/execution" -H "Content-Type: application/json" -d "{\"inputs\":{\"name\": \"hi there2\"}}"`` -- execute a job for the ``hello-world`` process with a response document - - ``curl -X POST "http://localhost:5000/processes/hello-world/execution" -H "Content-Type: application/json" -d "{\"inputs\":{\"name\": \"hi there2\"},\"response\":\"document\"}"`` -- execute a job for the ``hello-world`` process in asynchronous mode - - ``curl -X POST "http://localhost:5000/processes/hello-world/execution" -H "Content-Type: application/json" -d "{\"mode\": \"async\", \"inputs\":{\"name\": \"hi there2\"}}"`` +* list all processes + * http://localhost:5000/processes +* describe the ``hello-world`` process + * http://localhost:5000/processes/hello-world +* show all jobs + * http://localhost:5000/jobs +* execute a job for the ``hello-world`` process + * ``curl -X POST "http://localhost:5000/processes/hello-world/execution" -H "Content-Type: application/json" -d "{\"inputs\":{\"name\": \"hi there2\"}}"`` +* execute a job for the ``hello-world`` process with a raw response (default) + * ``curl -X POST "http://localhost:5000/processes/hello-world/execution" -H "Content-Type: application/json" -d "{\"inputs\":{\"name\": \"hi there2\"}}"`` +* execute a job for the ``hello-world`` process with a response document + * ``curl -X POST "http://localhost:5000/processes/hello-world/execution" -H "Content-Type: application/json" -d "{\"inputs\":{\"name\": \"hi there2\"},\"response\":\"document\"}"`` +* execute a job for the ``hello-world`` process in asynchronous mode + * ``curl -X POST "http://localhost:5000/processes/hello-world/execution" -H "Content-Type: application/json" -d "{\"mode\": \"async\", \"inputs\":{\"name\": \"hi there2\"}}"`` .. todo:: add more examples once OAProc implementation is complete diff -Nru pygeoapi-0.11.0/docs/source/data-publishing/ogcapi-records.rst pygeoapi-0.12.0/docs/source/data-publishing/ogcapi-records.rst --- pygeoapi-0.11.0/docs/source/data-publishing/ogcapi-records.rst 2021-10-24 14:33:20.000000000 +0000 +++ pygeoapi-0.12.0/docs/source/data-publishing/ogcapi-records.rst 2022-02-07 20:05:13.000000000 +0000 @@ -36,8 +36,8 @@ To publish an Elasticsearch index, the following are required in your index: -- indexes must be documents of valid `OGC API - Records GeoJSON Features`_ -- index mappings must define the GeoJSON ``geometry`` as a ``geo_shape`` +* indexes must be documents of valid `OGC API - Records GeoJSON Features`_ +* index mappings must define the GeoJSON ``geometry`` as a ``geo_shape`` .. code-block:: yaml @@ -57,7 +57,7 @@ To publish a TinyDB index, the following are required in your index: -- indexes must be documents of valid `OGC API - Records GeoJSON Features`_ +* indexes must be documents of valid `OGC API - Records GeoJSON Features`_ .. code-block:: yaml @@ -72,28 +72,28 @@ Metadata search examples ------------------------ -- overview of record collection - - http://localhost:5000/collections/metadata-records -- queryables - - http://localhost:5000/collections/foo/queryables -- browse records - - http://localhost:5000/collections/foo/items -- paging - - http://localhost:5000/collections/foo/items?startIndex=10&limit=10 -- CSV outputs - - http://localhost:5000/collections/foo/items?f=csv -- query records (spatial) - - http://localhost:5000/collections/foo/items?bbox=-180,-90,180,90 -- query records (attribute) - - http://localhost:5000/collections/foo/items?propertyname=foo -- query records (temporal) - - http://localhost:5000/collections/my-metadata/items?datetime=2020-04-10T14:11:00Z -- query features (temporal) and sort ascending by a property (if no +/- indicated, + is assumed) - - http://localhost:5000/collections/my-metadata/items?datetime=2020-04-10T14:11:00Z&sortby=datetime -- query features (temporal) and sort descending by a property - - http://localhost:5000/collections/my-metadata/items?datetime=2020-04-10T14:11:00Z&sortby=-datetime -- fetch a specific record - - http://localhost:5000/collections/my-metadata/items/123 +* overview of record collection + * http://localhost:5000/collections/metadata-records +* queryables + * http://localhost:5000/collections/foo/queryables +* browse records + * http://localhost:5000/collections/foo/items +* paging + * http://localhost:5000/collections/foo/items?startIndex=10&limit=10 +* CSV outputs + * http://localhost:5000/collections/foo/items?f=csv +* query records (spatial) + * http://localhost:5000/collections/foo/items?bbox=-180,-90,180,90 +* query records (attribute) + * http://localhost:5000/collections/foo/items?propertyname=foo +* query records (temporal) + * http://localhost:5000/collections/my-metadata/items?datetime=2020-04-10T14:11:00Z +* query features (temporal) and sort ascending by a property (if no +/- indicated, + is assumed) + * http://localhost:5000/collections/my-metadata/items?datetime=2020-04-10T14:11:00Z&sortby=datetime +* query features (temporal) and sort descending by a property + * http://localhost:5000/collections/my-metadata/items?datetime=2020-04-10T14:11:00Z&sortby=-datetime +* fetch a specific record + * http://localhost:5000/collections/my-metadata/items/123 .. _`OGC API - Records`: https://www.ogc.org/standards/ogcapi-records .. _`OGC API - Records GeoJSON Features`: https://raw.githubusercontent.com/opengeospatial/ogcapi-records/master/core/openapi/schemas/recordGeoJSON.yaml diff -Nru pygeoapi-0.11.0/docs/source/data-publishing/ogcapi-tiles.rst pygeoapi-0.12.0/docs/source/data-publishing/ogcapi-tiles.rst --- pygeoapi-0.11.0/docs/source/data-publishing/ogcapi-tiles.rst 2021-10-24 14:33:20.000000000 +0000 +++ pygeoapi-0.12.0/docs/source/data-publishing/ogcapi-tiles.rst 2022-02-07 20:05:13.000000000 +0000 @@ -11,8 +11,8 @@ that a directory tree of static tiles has been created on disk. Examples of tile generation software include (but are not limited to): -- `MapProxy`_ -- `tippecanoe`_ +* `MapProxy`_ +* `tippecanoe`_ Providers --------- @@ -58,16 +58,16 @@ Data access examples -------------------- -- list all collections - - http://localhost:5000/collections -- overview of dataset - - http://localhost:5000/collections/foo -- overview of dataset tiles - - http://localhost:5000/collections/foo/tiles -- tile matrix metadata - - http://localhost:5000/collections/lakes/tiles/WorldCRS84Quad/metadata -- tiles URI template - - `http://localhost:5000/collections/lakes/tiles/{tileMatrixSetId}/{tileMatrix}/{tileRow}/{tileCol}?f=mvt `_ +* list all collections + * http://localhost:5000/collections +* overview of dataset + * http://localhost:5000/collections/foo +* overview of dataset tiles + * http://localhost:5000/collections/foo/tiles +* tile matrix metadata + * http://localhost:5000/collections/lakes/tiles/WorldCRS84Quad/metadata +* tiles URI template + * `http://localhost:5000/collections/lakes/tiles/{tileMatrixSetId}/{tileMatrix}/{tileRow}/{tileCol}?f=mvt `_ .. _`OGC API - Tiles`: https://github.com/opengeospatial/ogcapi-tiles diff -Nru pygeoapi-0.11.0/docs/source/data-publishing/stac.rst pygeoapi-0.12.0/docs/source/data-publishing/stac.rst --- pygeoapi-0.11.0/docs/source/data-publishing/stac.rst 2021-10-24 14:33:20.000000000 +0000 +++ pygeoapi-0.12.0/docs/source/data-publishing/stac.rst 2022-02-07 20:05:13.000000000 +0000 @@ -51,8 +51,8 @@ Data access examples -------------------- -- STAC root page - - http://localhost:5000/stac +* STAC root page + * http://localhost:5000/stac From here, browse the filesystem accordingly. diff -Nru pygeoapi-0.11.0/docs/source/further-reading.rst pygeoapi-0.12.0/docs/source/further-reading.rst --- pygeoapi-0.11.0/docs/source/further-reading.rst 2021-10-24 14:33:20.000000000 +0000 +++ pygeoapi-0.12.0/docs/source/further-reading.rst 2022-02-07 20:05:13.000000000 +0000 @@ -5,5 +5,5 @@ The following list provides information on pygeoapi and OGC API efforts. -- `Default pygeoapi presentation `_ -- `OGC API `_ +* `Default pygeoapi presentation `_ +* `OGC API `_ diff -Nru pygeoapi-0.11.0/docs/source/introduction.rst pygeoapi-0.12.0/docs/source/introduction.rst --- pygeoapi-0.11.0/docs/source/introduction.rst 2021-10-24 14:33:20.000000000 +0000 +++ pygeoapi-0.12.0/docs/source/introduction.rst 2022-02-07 20:05:13.000000000 +0000 @@ -8,33 +8,33 @@ Features -------- -- out of the box modern OGC API server -- certified OGC Compliant and Reference Implementation for OGC API - Features -- additionally implements - - OGC API - Coverages - - OGC API - Tiles - - OGC API - Processes - - OGC API - Environmental Data Retrieval - - SpatioTemporal Asset Library -- out of the box data provider plugins for rasterio, GDAL/OGR, Elasticsearch, PostgreSQL/PostGIS -- easy to use OpenAPI / Swagger documentation for developers -- supports JSON, GeoJSON, HTML and CSV output -- supports data filtering by spatial, temporal or attribute queries -- easy to install: install a full implementation via ``pip`` or ``git`` -- simple YAML configuration -- easy to deploy: via UbuntuGIS or the official Docker image -- flexible: built on a robust plugin framework to build custom data connections, formats and processes -- supports any Python web framework (included are Flask [default], Starlette) -- supports asynchronous processing and job management (OGC API - Processes) +* out of the box modern OGC API server +* certified OGC Compliant and Reference Implementation for OGC API - Features +* additionally implements + * OGC API - Coverages + * OGC API - Tiles + * OGC API - Processes + * OGC API - Environmental Data Retrieval + * SpatioTemporal Asset Library +* out of the box data provider plugins for rasterio, GDAL/OGR, Elasticsearch, PostgreSQL/PostGIS +* easy to use OpenAPI / Swagger documentation for developers +* supports JSON, GeoJSON, HTML and CSV output +* supports data filtering by spatial, temporal or attribute queries +* easy to install: install a full implementation via ``pip`` or ``git`` +* simple YAML configuration +* easy to deploy: via UbuntuGIS or the official Docker image +* flexible: built on a robust plugin framework to build custom data connections, formats and processes +* supports any Python web framework (included are Flask [default], Starlette) +* supports asynchronous processing and job management (OGC API - Processes) Standards Support ----------------- Standards are at the core of pygeoapi. Below is the project's standards support matrix. -- Implementing: implements standard (good) -- Compliant: conforms to OGC compliance requirements (great) -- Reference Implementation: provides a reference for the standard (awesome!) +* Implementing: implements standard (good) +* Compliant: conforms to OGC compliance requirements (great) +* Reference Implementation: provides a reference for the standard (awesome!) .. csv-table:: :header: "Standard", "Support" diff -Nru pygeoapi-0.11.0/docs/source/language.rst pygeoapi-0.12.0/docs/source/language.rst --- pygeoapi-0.11.0/docs/source/language.rst 2021-10-24 14:33:20.000000000 +0000 +++ pygeoapi-0.12.0/docs/source/language.rst 2022-02-07 20:05:13.000000000 +0000 @@ -36,27 +36,27 @@ Notes ^^^^^ -- If pygeoapi cannot find a good match to the requested language, the response is returned in the default language (US English mostly). +* If pygeoapi cannot find a good match to the requested language, the response is returned in the default language (US English mostly). The default language is the *first* language defined in pygeoapi's server configuration YAML (see `maintainer guide`_). -- Even if pygeoapi *itself* supports the requested language, provider plugins may not support that particular language or perhaps don't even +* Even if pygeoapi *itself* supports the requested language, provider plugins may not support that particular language or perhaps don't even support any language at all. In that case the provider will reply in its own "unknown" language, which may not be the same language as the default pygeoapi server language set in the ``Content-Language`` HTTP response header. -- It is up to the creator of the provider to properly define at least 1 supported language in the provider configuration, as described +* It is up to the creator of the provider to properly define at least 1 supported language in the provider configuration, as described in the `developer guide`_. This will ensure that the ``Content-Language`` HTTP response header is always set properly. -- If pygeoapi found a match to the requested language, the response will include a ``Content-Language`` HTTP header, +* If pygeoapi found a match to the requested language, the response will include a ``Content-Language`` HTTP header, set to the best-matching server language code. This is the default behavior for most pygeoapi requests. However, note that some responses (e.g. exceptions) always have a ``Content-Language: en-US`` header, regardless of the requested language. -- For results returned by a **provider**, the ``Content-Language`` HTTP header will be set to the best-matching +* For results returned by a **provider**, the ``Content-Language`` HTTP header will be set to the best-matching provider language or the best-matching pygeoapi server language if the provider is not language-aware. -- If the provider supports a requested language, but pygeoapi does *not* support that same language, the ``Content-Language`` +* If the provider supports a requested language, but pygeoapi does *not* support that same language, the ``Content-Language`` header will contain both the provider language *and* the best-matching pygeoapi server language. -- Please note that the ``Content-Language`` HTTP response header only *indicates the language of the intended audience*. +* Please note that the ``Content-Language`` HTTP response header only *indicates the language of the intended audience*. It does not necessarily mean that the content is actually written in that particular language. @@ -94,14 +94,14 @@ Notes ^^^^^ -- The **first** language you define in the configuration determines the default language, i.e. the language that pygeoapi will +* The **first** language you define in the configuration determines the default language, i.e. the language that pygeoapi will use if no other language was requested or no best match for the requested language could be found. -- It is not possible to **disable** language support in pygeoapi. The functionality is always on and a ``Content-Language`` +* It is not possible to **disable** language support in pygeoapi. The functionality is always on and a ``Content-Language`` HTTP response header is always set. If results should be available in a single language, you'd have to set that language only in the pygeoapi configuration. -- Results returned from a provider may be in a different language than pygeoapi's own server language. The "raw" requested language +* Results returned from a provider may be in a different language than pygeoapi's own server language. The "raw" requested language is always passed on to the provider, even if pygeoapi itself does not support it. For more information, see the `end user guide`_ and the `developer guide`_. @@ -248,20 +248,20 @@ Notes ^^^^^ -- If your provider implements any of the aforementioned ``query``, ``get`` and ``get_metadata`` methods, +* If your provider implements any of the aforementioned ``query``, ``get`` and ``get_metadata`` methods, it **must** add a ``**kwargs`` or ``language=None`` parameter, even if it does not need to use the language parameter. -- Contrary to the pygeoapi server configuration, adding a ``language`` or ``languages`` (both are supported) property to the +* Contrary to the pygeoapi server configuration, adding a ``language`` or ``languages`` (both are supported) property to the provider definition is **not** required and may be omitted. In that case, the passed-in ``language`` parameter language-aware provider methods (``query``, ``get``, etc.) will be set to ``None``. This results in the following behavior: - - HTML responses returned from the providers will have the ``Content-Language`` header set to the best-matching pygeoapi server language. - - JSON(-LD) responses returned from providers will **not** have a ``Content-Language`` header if ``language`` is ``None``. + * HTML responses returned from the providers will have the ``Content-Language`` header set to the best-matching pygeoapi server language. + * JSON(-LD) responses returned from providers will **not** have a ``Content-Language`` header if ``language`` is ``None``. -- If the provider supports a requested language, the passed-in ``language`` will be set to the best matching +* If the provider supports a requested language, the passed-in ``language`` will be set to the best matching `Babel Locale instance `_. Note that this may be the provider default language if no proper match was found. No matter the output format, API responses returned from providers will always contain a best-matching ``Content-Language`` header if one ore more supported provider languages were defined. -- For general information about building plugins, please visit the :ref:`plugins` page. +* For general information about building plugins, please visit the :ref:`plugins` page. diff -Nru pygeoapi-0.11.0/docs/source/plugins.rst pygeoapi-0.12.0/docs/source/plugins.rst --- pygeoapi-0.11.0/docs/source/plugins.rst 2021-10-24 14:33:20.000000000 +0000 +++ pygeoapi-0.12.0/docs/source/plugins.rst 2022-02-07 20:05:13.000000000 +0000 @@ -72,6 +72,10 @@ bbox=[], datetime_=None, properties=[], sortby=[], select_properties=[], skip_geometry=False, **kwargs): + # optionally specify the output filename pygeoapi can use as part + of the response (HTTP Content-Disposition header) + self.filename = "my-cool-filename.dat" + # open data file (self.data) and process, return return { 'type': 'FeatureCollection', @@ -174,6 +178,11 @@ def query(self, bands=[], subsets={}, format_='json', **kwargs): # process bands and subsets parameters # query/extract coverage data + + # optionally specify the output filename pygeoapi can use as part + of the response (HTTP Content-Disposition header) + self.filename = "my-cool-filename.dat" + if format_ == 'json': # return a CoverageJSON representation return {'type': 'Coverage', ...} # trimmed for brevity diff -Nru pygeoapi-0.11.0/LICENSE.md pygeoapi-0.12.0/LICENSE.md --- pygeoapi-0.11.0/LICENSE.md 2021-10-24 14:33:20.000000000 +0000 +++ pygeoapi-0.12.0/LICENSE.md 2022-02-07 20:05:13.000000000 +0000 @@ -1,6 +1,6 @@ # The MIT License (MIT) -Copyright © 2018-2021 Tom Kralidis +Copyright © 2018-2022 Tom Kralidis * * * diff -Nru pygeoapi-0.11.0/pygeoapi/api.py pygeoapi-0.12.0/pygeoapi/api.py --- pygeoapi-0.11.0/pygeoapi/api.py 2021-10-24 14:33:20.000000000 +0000 +++ pygeoapi-0.12.0/pygeoapi/api.py 2022-02-07 20:05:13.000000000 +0000 @@ -4,7 +4,7 @@ # Francesco Bartoli # Sander Schaminee # -# Copyright (c) 2021 Tom Kralidis +# Copyright (c) 2022 Tom Kralidis # Copyright (c) 2020 Francesco Bartoli # # Permission is hereby granted, free of charge, to any person @@ -38,6 +38,7 @@ from copy import deepcopy from datetime import datetime, timezone from functools import partial +from gzip import compress import json import logging import os @@ -81,9 +82,11 @@ 'X-Powered-By': 'pygeoapi {}'.format(__version__) } +CHARSET = ['utf-8'] F_JSON = 'json' F_HTML = 'html' F_JSONLD = 'jsonld' +F_GZIP = 'gzip' #: Formats allowed for ?f= requests (order matters for complex MIME types) FORMAT_TYPES = OrderedDict(( @@ -144,6 +147,33 @@ return inner +def gzip(func): + """ + Decorator that compresses the content of an outgoing API result + instance if the Content-Encoding response header was set to gzip. + + :param func: decorated function + + :returns: `func` + """ + + def inner(*args, **kwargs): + headers, status, content = func(*args, **kwargs) + if F_GZIP in headers.get('Content-Encoding', []): + try: + charset = CHARSET[0] + headers['Content-Type'] = \ + f"{headers['Content-Type']}; charset={charset}" + content = compress(content.encode(charset)) + except TypeError as err: + headers.pop('Content-Encoding') + LOGGER.error('Error in compression: {}'.format(err)) + + return headers, status, content + + return inner + + class APIRequest: """ Transforms an incoming server-specific Request into an object @@ -345,13 +375,14 @@ # Format not specified: get from Accept headers (MIME types) # e.g. format_ = 'text/html' - for h in (v.strip() for k, v in headers.items() if k.lower() == 'accept'): # noqa - for fmt, mime in FORMAT_TYPES.items(): - # basic support for complex types (i.e. with "q=0.x") - types_ = (t.split(';')[0].strip() for t in h.split(',') if t) - if mime.strip() in types_: - format_ = fmt - break + h = headers.get('accept', headers.get('Accept', '')).strip() # noqa + (fmts, mimes) = zip(*FORMAT_TYPES.items()) + # basic support for complex types (i.e. with "q=0.x") + for type_ in (t.split(';')[0].strip() for t in h.split(',') if t): + if type_ in mimes: + idx_ = mimes.index(type_) + format_ = fmts[idx_] + break return format_ or None @@ -469,7 +500,8 @@ return False def get_response_headers(self, force_lang: l10n.Locale = None, - force_type: str = None) -> dict: + force_type: str = None, + force_encoding: str = None) -> dict: """ Prepares and returns a dictionary with Response object headers. @@ -492,6 +524,7 @@ :param force_lang: An optional Content-Language header override. :param force_type: An optional Content-Type header override. + :param force_encoding: An optional Content-Encoding header override. :returns: A header dict """ @@ -503,6 +536,13 @@ elif self.is_valid() and self._format: # Set MIME type for valid formats headers['Content-Type'] = FORMAT_TYPES[self._format] + + if F_GZIP in FORMAT_TYPES: + if force_encoding: + headers['Content-Encoding'] = force_encoding + elif F_GZIP in self._headers.get('Accept-Encoding', ''): + headers['Content-Encoding'] = F_GZIP + return headers def get_request_headers(self, headers) -> dict: @@ -534,6 +574,11 @@ self.config = config self.config['server']['url'] = self.config['server']['url'].rstrip('/') + CHARSET[0] = config['server'].get('encoding', 'utf-8') + if config['server'].get('gzip') is True: + FORMAT_TYPES[F_GZIP] = 'application/gzip' + FORMAT_TYPES.move_to_end(F_JSON) + # Process language settings (first locale is default!) self.locales = l10n.get_locales(config) self.default_locale = self.locales[0] @@ -563,6 +608,7 @@ self.manager = load_plugin('process_manager', manager_def) LOGGER.info('Process manager plugin loaded') + @gzip @pre_process @jsonldify def landing_page(self, @@ -629,6 +675,16 @@ 'type': FORMAT_TYPES[F_JSON], 'title': 'Collections', 'href': '{}/collections'.format(self.config['server']['url']) + }, { + 'rel': 'http://www.opengis.net/def/rel/ogc/1.0/processes', + 'type': FORMAT_TYPES[F_JSON], + 'title': 'Processes', + 'href': '{}/processes'.format(self.config['server']['url']) + }, { + 'rel': 'http://www.opengis.net/def/rel/ogc/1.0/job-list', + 'type': FORMAT_TYPES[F_JSON], + 'title': 'Jobs', + 'href': '{}/jobs'.format(self.config['server']['url']) }] headers = request.get_response_headers() @@ -654,6 +710,7 @@ return headers, 200, to_json(fcm, self.pretty_print) + @gzip @pre_process def openapi(self, request: Union[APIRequest, Any], openapi) -> Tuple[dict, int, str]: @@ -692,6 +749,7 @@ else: return headers, 200, openapi + @gzip @pre_process def conformance(self, request: Union[APIRequest, Any]) -> Tuple[dict, int, str]: @@ -718,6 +776,7 @@ return headers, 200, to_json(conformance, self.pretty_print) + @gzip @pre_process @jsonldify def describe_collections(self, request: Union[APIRequest, Any], @@ -748,8 +807,15 @@ return self.get_exception( 404, headers, request.format, 'NotFound', msg) + if dataset is not None: + collections_dict = { + k: v for k, v in collections.items() if k == dataset + } + else: + collections_dict = collections + LOGGER.debug('Creating collections') - for k, v in collections.items(): + for k, v in collections_dict.items(): collection_data = get_provider_default(v['providers']) collection_data_type = collection_data['type'] @@ -826,7 +892,7 @@ self.config['server']['url'], k, F_HTML) }) - if collection_data_type in ['feature', 'record']: + if collection_data_type in ['feature', 'record', 'tile']: # TODO: translate collection['itemType'] = collection_data_type LOGGER.debug('Adding feature/record based links') @@ -1063,6 +1129,7 @@ return headers, 200, to_json(fcm, self.pretty_print) + @gzip @pre_process @jsonldify def get_collection_queryables(self, request: Union[APIRequest, Any], @@ -1147,6 +1214,7 @@ return headers, 200, to_json(queryables, self.pretty_print) + @gzip @pre_process def get_collection_items( self, request: Union[APIRequest, Any], @@ -1481,7 +1549,12 @@ headers['Content-Type'] = '{}; charset={}'.format( formatter.mimetype, self.config['server']['encoding']) - cd = 'attachment; filename="{}.csv"'.format(dataset) + if p.filename is None: + filename = '{}.csv'.format(dataset) + else: + filename = '{}'.format(p.filename) + + cd = 'attachment; filename="{}"'.format(filename) headers['Content-Disposition'] = cd return headers, 200, content @@ -1493,6 +1566,7 @@ return headers, 200, to_json(content, self.pretty_print) + @gzip @pre_process def post_collection_items( self, request: Union[APIRequest, Any], @@ -1732,6 +1806,7 @@ return headers, 200, to_json(content, self.pretty_print) + @gzip @pre_process def get_collection_item(self, request: Union[APIRequest, Any], dataset, identifier) -> Tuple[dict, int, str]: @@ -1953,9 +2028,9 @@ # Format explicitly set using a query parameter query_args['format_'] = format_ = request.format - range_subset = request.params.get('rangeSubset') + range_subset = request.params.get('range-subset') if range_subset: - LOGGER.debug('Processing rangeSubset parameter') + LOGGER.debug('Processing range-subset parameter') query_args['range_subset'] = [rs for rs in range_subset.split(',') if rs] LOGGER.debug('Fields: {}'.format(query_args['range_subset'])) @@ -1967,30 +2042,21 @@ 400, headers, format_, 'InvalidParameterValue', msg) if 'subset' in request.params: - subsets = {} LOGGER.debug('Processing subset parameter') - for s in (request.params['subset'] or '').split(','): - try: - if '"' not in s: - m = re.search(r'(.*)\((.*):(.*)\)', s) - else: - m = re.search(r'(.*)\(\"(\S+)\":\"(\S+.*)\"\)', s) - - subset_name = m.group(1) - - if subset_name not in p.axes: - msg = 'Invalid axis name' - return self.get_exception( - 400, headers, format_, - 'InvalidParameterValue', msg) - - subsets[subset_name] = list(map( - get_typed_value, m.group(2, 3))) - except AttributeError: - msg = 'subset should be like "axis(min:max)"' - return self.get_exception( + try: + subsets = validate_subset(request.params['subset'] or '') + except (AttributeError, ValueError) as err: + msg = 'Invalid subset: {}'.format(err) + LOGGER.error(msg) + return self.get_exception( 400, headers, format_, 'InvalidParameterValue', msg) + if not set(subsets.keys()).issubset(p.axes): + msg = 'Invalid axis name' + LOGGER.error(msg) + return self.get_exception( + 400, headers, format_, 'InvalidParameterValue', msg) + query_args['subsets'] = subsets LOGGER.debug('Subsets: {}'.format(query_args['subsets'])) @@ -2012,6 +2078,10 @@ mt = collection_def['format']['name'] if format_ == mt: # native format + if p.filename is not None: + cd = 'attachment; filename="{}"'.format(p.filename) + headers['Content-Disposition'] = cd + headers['Content-Type'] = collection_def['format']['mimetype'] return headers, 200, data elif format_ == F_JSON: @@ -2020,6 +2090,7 @@ else: return self.get_format_exception(request) + @gzip @pre_process @jsonldify def get_collection_coverage_domainset( @@ -2073,6 +2144,7 @@ else: return self.get_format_exception(request) + @gzip @pre_process @jsonldify def get_collection_coverage_rangetype( @@ -2125,6 +2197,7 @@ else: return self.get_format_exception(request) + @gzip @pre_process @jsonldify def get_collection_tiles(self, request: Union[APIRequest, Any], @@ -2229,6 +2302,7 @@ return headers, 200, to_json(tiles, self.pretty_print) + @gzip @pre_process @jsonldify def get_collection_tiles_data( @@ -2313,6 +2387,7 @@ return self.get_exception( 500, headers, format_, 'NoApplicableCode', msg) + @gzip @pre_process @jsonldify def get_collection_tiles_metadata( @@ -2395,6 +2470,7 @@ return headers, 200, to_json(tiles_metadata, self.pretty_print) + @gzip @pre_process @jsonldify def describe_processes(self, request: Union[APIRequest, Any], @@ -2449,13 +2525,32 @@ p2['outputTransmission'] = ['value'] p2['links'] = p2.get('links', []) - jobs_url = '{}/processes/{}/jobs'.format( + jobs_url = '{}/jobs'.format(self.config['server']['url']) + process_url = '{}/processes/{}'.format( self.config['server']['url'], key) # TODO translation support link = { + 'type': FORMAT_TYPES[F_JSON], + 'rel': request.get_linkrel(F_JSON), + 'href': '{}?f={}'.format(process_url, F_JSON), + 'title': 'Process description as JSON', + 'hreflang': self.default_locale + } + p2['links'].append(link) + + link = { 'type': FORMAT_TYPES[F_HTML], - 'rel': 'collection', + 'rel': request.get_linkrel(F_HTML), + 'href': '{}?f={}'.format(process_url, F_HTML), + 'title': 'Process description as HTML', + 'hreflang': self.default_locale + } + p2['links'].append(link) + + link = { + 'type': FORMAT_TYPES[F_HTML], + 'rel': 'http://www.opengis.net/def/rel/ogc/1.0/job-list', 'href': '{}?f={}'.format(jobs_url, F_HTML), 'title': 'jobs for this process as HTML', 'hreflang': self.default_locale @@ -2464,13 +2559,22 @@ link = { 'type': FORMAT_TYPES[F_JSON], - 'rel': 'collection', + 'rel': 'http://www.opengis.net/def/rel/ogc/1.0/job-list', 'href': '{}?f={}'.format(jobs_url, F_JSON), 'title': 'jobs for this process as JSON', 'hreflang': self.default_locale } p2['links'].append(link) + link = { + 'type': FORMAT_TYPES[F_JSON], + 'rel': 'http://www.opengis.net/def/rel/ogc/1.0/execute', + 'href': '{}/execution?f={}'.format(process_url, F_JSON), + 'title': 'Execution for this process as JSON', + 'hreflang': self.default_locale + } + p2['links'].append(link) + processes.append(p2) if process is not None: @@ -2494,14 +2598,14 @@ return headers, 200, to_json(response, self.pretty_print) + @gzip @pre_process - def get_process_jobs(self, request: Union[APIRequest, Any], - process_id, job_id=None) -> Tuple[dict, int, str]: + def get_jobs(self, request: Union[APIRequest, Any], + job_id=None) -> Tuple[dict, int, str]: """ Get process jobs :param request: A request object - :param process_id: id of process :param job_id: id of job :returns: tuple of headers, status code, content @@ -2511,30 +2615,35 @@ return self.get_format_exception(request) headers = request.get_response_headers(SYSTEM_LOCALE) - processes = filter_dict_by_key_value( - self.config['resources'], 'type', 'process') - - if process_id not in processes: - msg = 'identifier not found' - return self.get_exception( - 404, headers, request.format, 'NoSuchProcess', msg) - - p = load_plugin('process', processes[process_id]['processor']) - if self.manager: if job_id is None: - jobs = sorted(self.manager.get_jobs(process_id), + print(self.manager.get_jobs()) + jobs = sorted(self.manager.get_jobs(), key=lambda k: k['job_start_datetime'], reverse=True) else: - jobs = [self.manager.get_job(process_id, job_id)] + jobs = [self.manager.get_job(job_id)] else: LOGGER.debug('Process management not configured') jobs = [] - serialized_jobs = [] + serialized_jobs = { + 'jobs': [], + 'links': [{ + 'href': '{}/jobs?f={}'.format(self.config['server']['url'], F_HTML), # noqa + 'rel': request.get_linkrel(F_HTML), + 'type': FORMAT_TYPES[F_HTML], + 'title': 'Jobs list as HTML' + }, { + 'href': '{}/jobs?f={}'.format(self.config['server']['url'], F_JSON), # noqa + 'rel': request.get_linkrel(F_JSON), + 'type': FORMAT_TYPES[F_JSON], + 'title': 'Jobs list as JSON' + }] + } for job_ in jobs: job2 = { + 'processID': job_['process_id'], 'jobID': job_['identifier'], 'status': job_['status'], 'message': job_['message'], @@ -2548,9 +2657,8 @@ if JobStatus[job_['status']] in ( JobStatus.successful, JobStatus.running, JobStatus.accepted): - job_result_url = '{}/processes/{}/jobs/{}/results'.format( - self.config['server']['url'], - process_id, job_['identifier']) + job_result_url = '{}/jobs/{}/results'.format( + self.config['server']['url'], job_['identifier']) job2['links'] = [{ 'href': '{}?f={}'.format(job_result_url, F_HTML), @@ -2574,21 +2682,16 @@ job_id, job_['mimetype']) }) - serialized_jobs.append(job2) + serialized_jobs['jobs'].append(job2) if job_id is None: - j2_template = 'processes/jobs/index.html' + j2_template = 'jobs/index.html' else: - serialized_jobs = serialized_jobs[0] - j2_template = 'processes/jobs/job.html' + serialized_jobs = serialized_jobs['jobs'][0] + j2_template = 'jobs/job.html' if request.format == F_HTML: data = { - 'process': { - 'id': process_id, - 'title': l10n.translate(p.metadata['title'], - SYSTEM_LOCALE) - }, 'jobs': serialized_jobs, 'now': datetime.now(timezone.utc).strftime(DATETIME_FORMAT) } @@ -2598,6 +2701,7 @@ return headers, 200, to_json(serialized_jobs, self.pretty_print) + @gzip @pre_process def execute_process(self, request: Union[APIRequest, Any], process_id) -> Tuple[dict, int, str]: @@ -2660,8 +2764,8 @@ LOGGER.debug(data_dict) job_id = data.get("job_id", str(uuid.uuid1())) - url = '{}/processes/{}/jobs/{}'.format( - self.config['server']['url'], process_id, job_id) + url = '{}/jobs/{}'.format( + self.config['server']['url'], job_id) headers['Location'] = url @@ -2699,16 +2803,21 @@ else: http_status = 200 - return headers, http_status, to_json(response, self.pretty_print) + if mime_type == 'application/json': + response2 = to_json(response, self.pretty_print) + else: + response2 = response + return headers, http_status, response2 + + @gzip @pre_process - def get_process_job_result(self, request: Union[APIRequest, Any], - process_id, job_id) -> Tuple[dict, int, str]: + def get_job_result(self, request: Union[APIRequest, Any], + job_id) -> Tuple[dict, int, str]: """ Get result of job (instance of a process) :param request: A request object - :param process_id: name of process :param job_id: ID of job :returns: tuple of headers, status code, content @@ -2718,23 +2827,7 @@ return self.get_format_exception(request) headers = request.get_response_headers(SYSTEM_LOCALE) - processes_config = filter_dict_by_key_value(self.config['resources'], - 'type', 'process') - - if process_id not in processes_config: - msg = 'identifier not found' - return self.get_exception( - 404, headers, request.format, 'NoSuchProcess', msg) - - process = load_plugin('process', - processes_config[process_id]['processor']) - - if not process: - msg = 'identifier not found' - return self.get_exception( - 404, headers, request.format, 'NoSuchProcess', msg) - - job = self.manager.get_job(process_id, job_id) + job = self.manager.get_job(job_id) if not job: msg = 'job not found' @@ -2759,7 +2852,7 @@ return self.get_exception( 400, headers, request.format, 'InvalidParameterValue', msg) - mimetype, job_output = self.manager.get_job_result(process_id, job_id) + mimetype, job_output = self.manager.get_job_result(job_id) if mimetype not in (None, FORMAT_TYPES[F_JSON]): headers['Content-Type'] = mimetype @@ -2771,31 +2864,25 @@ else: # HTML data = { - 'process': { - 'id': process_id, - 'title': l10n.translate(process.metadata['title'], - SYSTEM_LOCALE) - }, 'job': {'id': job_id}, 'result': job_output } content = render_j2_template( - self.config, 'processes/jobs/results/index.html', + self.config, 'jobs/results/index.html', data, SYSTEM_LOCALE) return headers, 200, content - def delete_process_job(self, process_id, job_id) -> Tuple[dict, int, str]: + def delete_job(self, job_id) -> Tuple[dict, int, str]: """ Delete a process job - :param process_id: process identifier :param job_id: job identifier :returns: tuple of headers, status code, content """ - success = self.manager.delete_job(process_id, job_id) + success = self.manager.delete_job(job_id) if not success: http_status = 404 @@ -2805,8 +2892,7 @@ } else: http_status = 200 - jobs_url = '{}/processes/{}/jobs'.format( - self.config['server']['url'], process_id) + jobs_url = '{}/jobs'.format(self.config['server']['url']) response = { 'jobID': job_id, @@ -2825,6 +2911,7 @@ # TODO: this response does not have any headers return {}, http_status, response + @gzip @pre_process def get_collection_edr_query( self, request: Union[APIRequest, Any], @@ -2950,6 +3037,7 @@ return headers, 200, content + @gzip @pre_process @jsonldify def get_stac_root( @@ -3005,6 +3093,7 @@ return headers, 200, to_json(content, self.pretty_print) + @gzip @pre_process @jsonldify def get_stac_path(self, request: Union[APIRequest, Any], @@ -3169,11 +3258,15 @@ LOGGER.debug(msg) raise - if bbox[0] > bbox[2] or bbox[1] > bbox[3]: - msg = 'min values should be less than max values' + if bbox[1] > bbox[3]: + msg = 'miny should be less than maxy' LOGGER.debug(msg) raise ValueError(msg) + if bbox[0] > bbox[2]: + msg = 'minx is greater than maxx (possibly antimeridian bbox)' + LOGGER.debug(msg) + return bbox @@ -3261,3 +3354,48 @@ raise ValueError(msg) return datetime_ + + +def validate_subset(value: str) -> dict: + """ + Helper function to validate subset parameter + + :param value: `subset` parameter + + :returns: dict of axis/values + """ + + subsets = {} + + for s in value.split(','): + LOGGER.debug('Processing subset {}'.format(s)) + m = re.search(r'(.*)\((.*)\)', s) + subset_name, values = m.group(1, 2) + + if '"' in values: + LOGGER.debug('Values are strings') + if values.count('"') % 2 != 0: + msg = 'Invalid format: subset should be like axis("min"[:"max"])' # noqa + LOGGER.error(msg) + raise ValueError(msg) + try: + LOGGER.debug('Value is an interval') + m = re.search(r'"(\S+)":"(\S+)"', values) + values = list(m.group(1, 2)) + except AttributeError: + LOGGER.debug('Value is point') + m = re.search(r'"(.*)"', values) + values = [m.group(1)] + else: + LOGGER.debug('Values are numbers') + try: + LOGGER.debug('Value is an interval') + m = re.search(r'(\S+):(\S+)', values) + values = list(m.group(1, 2)) + except AttributeError: + LOGGER.debug('Value is point') + values = [values] + + subsets[subset_name] = list(map(get_typed_value, values)) + + return subsets diff -Nru pygeoapi-0.11.0/pygeoapi/flask_app.py pygeoapi-0.12.0/pygeoapi/flask_app.py --- pygeoapi-0.11.0/pygeoapi/flask_app.py 2021-10-24 14:33:20.000000000 +0000 +++ pygeoapi-0.12.0/pygeoapi/flask_app.py 2022-02-07 20:05:13.000000000 +0000 @@ -3,7 +3,7 @@ # Authors: Tom Kralidis # Norman Barker # -# Copyright (c) 2020 Tom Kralidis +# Copyright (c) 2022 Tom Kralidis # # Permission is hereby granted, free of charge, to any person # obtaining a copy of this software and associated documentation @@ -296,27 +296,25 @@ return get_response(api_.describe_processes(request, process_id)) -@BLUEPRINT.route('/processes//jobs') -@BLUEPRINT.route('/processes//jobs/', +@BLUEPRINT.route('/jobs') +@BLUEPRINT.route('/jobs/', methods=['GET', 'DELETE']) -def get_process_jobs(process_id=None, job_id=None): +def get_jobs(job_id=None): """ OGC API - Processes jobs endpoint - :param process_id: process identifier :param job_id: job identifier :returns: HTTP response """ if job_id is None: - return get_response(api_.get_process_jobs(request, process_id)) + return get_response(api_.get_jobs(request)) else: if request.method == 'DELETE': # dismiss job - return get_response(api_.delete_process_job(process_id, job_id)) + return get_response(api_.delete_job(job_id)) else: # Return status of a specific job - return get_response(api_.get_process_jobs( - request, process_id, job_id)) + return get_response(api_.get_jobs(request, job_id)) @BLUEPRINT.route('/processes//execution', methods=['POST']) @@ -332,35 +330,32 @@ return get_response(api_.execute_process(request, process_id)) -@BLUEPRINT.route('/processes//jobs//results', +@BLUEPRINT.route('/jobs//results', methods=['GET']) -def get_process_job_result(process_id=None, job_id=None): +def get_job_result(job_id=None): """ OGC API - Processes job result endpoint - :param process_id: process identifier :param job_id: job identifier :returns: HTTP response """ - return get_response(api_.get_process_job_result( - request, process_id, job_id)) + return get_response(api_.get_job_result(request, job_id)) -@BLUEPRINT.route('/processes//jobs//results/', +@BLUEPRINT.route('/jobs//results/', methods=['GET']) -def get_process_job_result_resource(process_id, job_id, resource): +def get_job_result_resource(job_id, resource): """ OGC API - Processes job result resource endpoint - :param process_id: process identifier :param job_id: job identifier :param resource: job resource :returns: HTTP response """ - return get_response(api_.get_process_job_result_resource( - request, process_id, job_id, resource)) + return get_response(api_.get_job_result_resource( + request, job_id, resource)) @BLUEPRINT.route('/collections//position') diff -Nru pygeoapi-0.11.0/pygeoapi/__init__.py pygeoapi-0.12.0/pygeoapi/__init__.py --- pygeoapi-0.11.0/pygeoapi/__init__.py 2021-10-24 14:33:20.000000000 +0000 +++ pygeoapi-0.12.0/pygeoapi/__init__.py 2022-02-07 20:05:13.000000000 +0000 @@ -27,7 +27,7 @@ # # ================================================================= -__version__ = '0.11.0' +__version__ = '0.12.0' import click from pygeoapi.config import config diff -Nru pygeoapi-0.11.0/pygeoapi/openapi.py pygeoapi-0.12.0/pygeoapi/openapi.py --- pygeoapi-0.11.0/pygeoapi/openapi.py 2021-10-24 14:33:20.000000000 +0000 +++ pygeoapi-0.12.0/pygeoapi/openapi.py 2022-02-07 20:05:13.000000000 +0000 @@ -2,7 +2,7 @@ # # Authors: Tom Kralidis # -# Copyright (c) 2021 Tom Kralidis +# Copyright (c) 2022 Tom Kralidis # # Permission is hereby granted, free of charge, to any person # obtaining a copy of this software and associated documentation @@ -47,12 +47,12 @@ OPENAPI_YAML = { 'oapif': 'http://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml', # noqa - 'oapip': 'https://raw.githubusercontent.com/opengeospatial/ogcapi-processes/master/core/openapi', # noqa + 'oapip': 'http://schemas.opengis.net/ogcapi/processes/part1/1.0/openapi', 'oacov': 'https://raw.githubusercontent.com/tomkralidis/ogcapi-coverages-1/fix-cis/yaml-unresolved', # noqa 'oapit': 'https://raw.githubusercontent.com/opengeospatial/ogcapi-tiles/master/openapi/swaggerhub/tiles.yaml', # noqa 'oapimt': 'https://raw.githubusercontent.com/opengeospatial/ogcapi-tiles/master/openapi/swaggerhub/map-tiles.yaml', # noqa 'oapir': 'https://raw.githubusercontent.com/opengeospatial/ogcapi-records/master/core/openapi', # noqa - 'oaedr': 'https://raw.githubusercontent.com/opengeospatial/ogcapi-environmental-data-retrieval/master/candidate-standard/openapi', # noqa + 'oaedr': 'http://schemas.opengis.net/ogcapi/edr/1.0/openapi', # noqa 'oat': 'https://raw.githubusercontent.com/opengeospatial/ogcapi-tiles/master/openapi/swaggerHubUnresolved/ogc-api-tiles.yaml', # noqa } @@ -908,19 +908,6 @@ } } } - paths['{}/jobs'.format(process_name_path)] = { - 'get': { - 'summary': 'Retrieve job list for process', - 'description': md_desc, - 'tags': [name], - 'operationId': 'get{}Jobs'.format(name.capitalize()), - 'responses': { - '200': {'$ref': '#/components/responses/200'}, - '404': {'$ref': '{}/responses/NotFound.yaml'.format(OPENAPI_YAML['oapip'])}, # noqa - 'default': {'$ref': '#/components/responses/default'} - } - } - } paths['{}/execution'.format(process_name_path)] = { 'post': { @@ -962,57 +949,70 @@ } } - if has_manager: - # TODO: define jobId as parameter in dict - paths[f'{process_name_path}/jobs/{{jobId}}'] = { - 'get': { - 'summary': 'Retrieve job details', - 'description': '', - 'tags': [name], - 'parameters': [ - name_in_path, - {'$ref': '#/components/parameters/f'} - ], - 'operationId': f'get{name.capitalize()}Job', - 'responses': { - '200': {'$ref': '#/components/responses/200'}, - '404': {'$ref': '{}/responses/NotFound.yaml'.format(OPENAPI_YAML['oapip'])}, # noqa - 'default': {'$ref': '#/components/responses/default'} # noqa - } - }, - 'delete': { - 'summary': 'Cancel / delete job', - 'description': '', - 'tags': [name], - 'parameters': [ - name_in_path - ], - 'operationId': f'delete{name.capitalize()}Job', - 'responses': { - '204': {'$ref': '#/components/responses/204'}, - '404': {'$ref': '{}/responses/NotFound.yaml'.format(OPENAPI_YAML['oapip'])}, # noqa - 'default': {'$ref': '#/components/responses/default'} # noqa - } - }, + if has_manager: + paths['/jobs'] = { + 'get': { + 'summary': 'Retrieve jobs list', + 'description': 'Retrieve a list of jobs', + 'tags': ['server'], + 'operationId': 'getJobs', + 'responses': { + '200': {'$ref': '#/components/responses/200'}, + '404': {'$ref': '{}/responses/NotFound.yaml'.format(OPENAPI_YAML['oapip'])}, # noqa + 'default': {'$ref': '#/components/responses/default'} + } } + } - paths[f'{process_name_path}/jobs/{{jobId}}/results'] = { - 'get': { - 'summary': 'Retrieve job results', - 'description': '', - 'tags': [name], - 'parameters': [ - name_in_path, - {'$ref': '#/components/parameters/f'} - ], - 'operationId': f'get{name.capitalize()}JobResults', - 'responses': { - '200': {'$ref': '#/components/responses/200'}, - '404': {'$ref': '{}/responses/NotFound.yaml'.format(OPENAPI_YAML['oapip'])}, # noqa - 'default': {'$ref': '#/components/responses/default'} # noqa - } - }, + paths['/jobs/{jobId}'] = { + 'get': { + 'summary': 'Retrieve job details', + 'description': 'Retrieve job details', + 'tags': ['server'], + 'parameters': [ + name_in_path, + {'$ref': '#/components/parameters/f'} + ], + 'operationId': 'getJob', + 'responses': { + '200': {'$ref': '#/components/responses/200'}, + '404': {'$ref': '{}/responses/NotFound.yaml'.format(OPENAPI_YAML['oapip'])}, # noqa + 'default': {'$ref': '#/components/responses/default'} # noqa + } + }, + 'delete': { + 'summary': 'Cancel / delete job', + 'description': 'Cancel / delete job', + 'tags': ['server'], + 'parameters': [ + name_in_path + ], + 'operationId': 'deleteJob', + 'responses': { + '204': {'$ref': '#/components/responses/204'}, + '404': {'$ref': '{}/responses/NotFound.yaml'.format(OPENAPI_YAML['oapip'])}, # noqa + 'default': {'$ref': '#/components/responses/default'} # noqa + } + }, + } + + paths['/jobs/{jobId}/results'] = { + 'get': { + 'summary': 'Retrieve job results', + 'description': 'Retrive job resiults', + 'tags': ['server'], + 'parameters': [ + name_in_path, + {'$ref': '#/components/parameters/f'} + ], + 'operationId': 'getJobResults', + 'responses': { + '200': {'$ref': '#/components/responses/200'}, + '404': {'$ref': '{}/responses/NotFound.yaml'.format(OPENAPI_YAML['oapip'])}, # noqa + 'default': {'$ref': '#/components/responses/default'} # noqa + } } + } oas['paths'] = paths diff -Nru pygeoapi-0.11.0/pygeoapi/process/hello_world.py pygeoapi-0.12.0/pygeoapi/process/hello_world.py --- pygeoapi-0.11.0/pygeoapi/process/hello_world.py 2021-10-24 14:33:20.000000000 +0000 +++ pygeoapi-0.12.0/pygeoapi/process/hello_world.py 2022-02-07 20:05:13.000000000 +0000 @@ -53,7 +53,7 @@ 'keywords': ['hello world', 'example', 'echo'], 'links': [{ 'type': 'text/html', - 'rel': 'canonical', + 'rel': 'about', 'title': 'information', 'href': 'https://example.org/process', 'hreflang': 'en-US' diff -Nru pygeoapi-0.11.0/pygeoapi/process/manager/base.py pygeoapi-0.12.0/pygeoapi/process/manager/base.py --- pygeoapi-0.11.0/pygeoapi/process/manager/base.py 2021-10-24 14:33:20.000000000 +0000 +++ pygeoapi-0.12.0/pygeoapi/process/manager/base.py 2022-02-07 20:05:13.000000000 +0000 @@ -2,7 +2,7 @@ # # Authors: Tom Kralidis # -# Copyright (c) 2020 Tom Kralidis +# Copyright (c) 2022 Tom Kralidis # # Permission is hereby granted, free of charge, to any person # obtaining a copy of this software and associated documentation @@ -56,11 +56,10 @@ self.connection = manager_def.get('connection', None) self.output_dir = manager_def.get('output_dir', None) - def get_jobs(self, process_id=None, status=None): + def get_jobs(self, status=None): """ Get process jobs, optionally filtered by status - :param process_id: process identifier :param status: job status (accepted, running, successful, failed, results) (default is all) @@ -80,11 +79,10 @@ raise NotImplementedError() - def update_job(self, process_id, job_id, update_dict): + def update_job(self, job_id, update_dict): """ Updates a job - :param process_id: process identifier :param job_id: job identifier :param update_dict: `dict` of property updates @@ -93,11 +91,10 @@ raise NotImplementedError() - def get_job(self, process_id, job_id): + def get_job(self, job_id): """ Get a job (!) - :param process_id: process identifier :param job_id: job identifier :returns: `dict` of job result @@ -105,11 +102,10 @@ raise NotImplementedError() - def get_job_result(self, process_id, job_id): + def get_job_result(self, job_id): """ Returns the actual output from a completed process - :param process_id: process identifier :param job_id: job identifier :returns: `tuple` of mimetype and raw output @@ -117,11 +113,10 @@ raise NotImplementedError() - def delete_job(self, process_id, job_id): + def delete_job(self, job_id): """ Deletes a job and associated results/outputs - :param process_id: process identifier :param job_id: job identifier :returns: `bool` of status result @@ -193,7 +188,7 @@ current_status = JobStatus.running jfmt, outputs = p.execute(data_dict) - self.update_job(process_id, job_id, { + self.update_job(job_id, { 'status': current_status.value, 'message': 'Writing job output', 'progress': 95 @@ -201,8 +196,16 @@ if self.output_dir is not None: LOGGER.debug('writing output to {}'.format(job_filename)) - with io.open(job_filename, 'w', encoding='utf-8') as fh: - fh.write(json.dumps(outputs, sort_keys=True, indent=4)) + if isinstance(outputs, dict): + mode = 'w' + data = json.dumps(outputs, sort_keys=True, indent=4) + encoding = 'utf-8' + elif isinstance(outputs, bytes): + mode = 'wb' + data = outputs + encoding = None + with io.open(job_filename, mode, encoding=encoding) as fh: + fh.write(data) current_status = JobStatus.successful @@ -216,7 +219,7 @@ 'progress': 100 } - self.update_job(process_id, job_id, job_update_metadata) + self.update_job(job_id, job_update_metadata) except Exception as err: # TODO assess correct exception type and description to help users @@ -245,7 +248,7 @@ jfmt = 'application/json' - self.update_job(process_id, job_id, job_metadata) + self.update_job(job_id, job_metadata) return jfmt, outputs, current_status diff -Nru pygeoapi-0.11.0/pygeoapi/process/manager/dummy.py pygeoapi-0.12.0/pygeoapi/process/manager/dummy.py --- pygeoapi-0.11.0/pygeoapi/process/manager/dummy.py 2021-10-24 14:33:20.000000000 +0000 +++ pygeoapi-0.12.0/pygeoapi/process/manager/dummy.py 2022-02-07 20:05:13.000000000 +0000 @@ -2,7 +2,7 @@ # # Authors: Tom Kralidis # -# Copyright (c) 2020 Tom Kralidis +# Copyright (c) 2022 Tom Kralidis # # Permission is hereby granted, free of charge, to any person # obtaining a copy of this software and associated documentation @@ -49,11 +49,10 @@ super().__init__(manager_def) - def get_jobs(self, process_id=None, status=None): + def get_jobs(self, status=None): """ Get process jobs, optionally filtered by status - :param process_id: process identifier :param status: job status (accepted, running, successful, failed, results) (default is all) diff -Nru pygeoapi-0.11.0/pygeoapi/process/manager/tinydb_.py pygeoapi-0.12.0/pygeoapi/process/manager/tinydb_.py --- pygeoapi-0.11.0/pygeoapi/process/manager/tinydb_.py 2021-10-24 14:33:20.000000000 +0000 +++ pygeoapi-0.12.0/pygeoapi/process/manager/tinydb_.py 2022-02-07 20:05:13.000000000 +0000 @@ -2,7 +2,7 @@ # # Authors: Tom Kralidis # -# Copyright (c) 2020 Tom Kralidis +# Copyright (c) 2022 Tom Kralidis # # Permission is hereby granted, free of charge, to any person # obtaining a copy of this software and associated documentation @@ -77,11 +77,10 @@ self.db.close() return True - def get_jobs(self, process_id=None, status=None): + def get_jobs(self, status=None): """ Get jobs - :param process_id: process identifier :param status: job status (accepted, running, successful, failed, results) (default is all) @@ -89,12 +88,7 @@ """ self._connect() - if process_id is None: - jobs_list = [doc.doc_id for doc in self.db.all()] - else: - query = tinydb.Query() - jobs_list = self.db.search(query.process_id == process_id) - + jobs_list = self.db.all() self.db.close() return jobs_list @@ -114,11 +108,10 @@ return doc_id - def update_job(self, process_id, job_id, update_dict): + def update_job(self, job_id, update_dict): """ Updates a job - :param process_id: process identifier :param job_id: job identifier :param update_dict: `dict` of property updates @@ -131,17 +124,16 @@ return True - def delete_job(self, process_id, job_id): + def delete_job(self, job_id): """ Deletes a job - :param process_id: process identifier :param job_id: job identifier :return `bool` of status result """ # delete result file if present - job_result = self.get_job(process_id, job_id) + job_result = self.get_job(job_id) if job_result: location = job_result.get('location', None) if location and self.output_dir is not None: @@ -153,38 +145,35 @@ return removed - def get_job(self, process_id, job_id): + def get_job(self, job_id): """ Get a single job - :param process_id: process identifier - :param jobid: job identifier + :param job_id: job identifier :returns: `dict` # `pygeoapi.process.manager.Job` """ self._connect() query = tinydb.Query() - result = self.db.search(( - query.process_id == process_id) & (query.identifier == job_id)) + result = self.db.search(query.identifier == job_id) result = result[0] if result else None self.db.close() return result - def get_job_result(self, process_id, job_id): + def get_job_result(self, job_id): """ Get a job's status, and actual output of executing the process - :param process_id: process identifier :param jobid: job identifier :returns: `tuple` of mimetype and raw output """ - job_result = self.get_job(process_id, job_id) + job_result = self.get_job(job_id) if not job_result: - # processs/job does not exist + # job does not exist return None location = job_result.get('location', None) diff -Nru pygeoapi-0.11.0/pygeoapi/provider/base_edr.py pygeoapi-0.12.0/pygeoapi/provider/base_edr.py --- pygeoapi-0.11.0/pygeoapi/provider/base_edr.py 1970-01-01 00:00:00.000000000 +0000 +++ pygeoapi-0.12.0/pygeoapi/provider/base_edr.py 2022-02-07 20:05:13.000000000 +0000 @@ -0,0 +1,97 @@ +# ================================================================= +# +# Authors: Tom Kralidis +# +# Copyright (c) 2021 Tom Kralidis +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation +# files (the "Software"), to deal in the Software without +# restriction, including without limitation the rights to use, +# copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following +# conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +# OTHER DEALINGS IN THE SOFTWARE. +# +# ================================================================= + +import logging + +from pygeoapi.provider.base import BaseProvider + +LOGGER = logging.getLogger(__name__) + + +class BaseEDRProvider(BaseProvider): + """Base EDR Provider""" + + query_types = [] + + def __init__(self, provider_def): + """ + Initialize object + + :param provider_def: provider definition + + :returns: pygeoapi.provider.base_edr.BaseEDRProvider + """ + + super().__init__(provider_def) + + self.instances = [] + + @classmethod + def register(cls): + def inner(fn): + cls.query_types.append(fn.__name__) + return fn + return inner + + def get_instance(self, instance): + """ + Validate instance identifier + + :returns: `bool` of whether instance is valid + """ + + return NotImplementedError() + + def get_query_types(self): + """ + Provide supported query types + + :returns: `list` of EDR query types + """ + + return self.query_types + + def query(self, **kwargs): + """ + Extract data from collection collection + + :param query_type: query type + :param wkt: `shapely.geometry` WKT geometry + :param datetime_: temporal (datestamp or extent) + :param select_properties: list of parameters + :param z: vertical level(s) + :param format_: data format of output + + :returns: coverage data as `dict` of CoverageJSON or native format + """ + + try: + return getattr(self, kwargs.get('query_type'))(**kwargs) + except AttributeError: + raise NotImplementedError('Query not implemented!') diff -Nru pygeoapi-0.11.0/pygeoapi/provider/base.py pygeoapi-0.12.0/pygeoapi/provider/base.py --- pygeoapi-0.11.0/pygeoapi/provider/base.py 2021-10-24 14:33:20.000000000 +0000 +++ pygeoapi-0.12.0/pygeoapi/provider/base.py 2022-02-07 20:05:13.000000000 +0000 @@ -61,6 +61,7 @@ self.properties = provider_def.get('properties', []) self.file_types = provider_def.get('file_types', []) self.fields = {} + self.filename = None # for coverage providers self.axes = [] diff -Nru pygeoapi-0.11.0/pygeoapi/provider/elasticsearch_.py pygeoapi-0.12.0/pygeoapi/provider/elasticsearch_.py --- pygeoapi-0.11.0/pygeoapi/provider/elasticsearch_.py 2021-10-24 14:33:20.000000000 +0000 +++ pygeoapi-0.12.0/pygeoapi/provider/elasticsearch_.py 2022-02-07 20:05:13.000000000 +0000 @@ -136,6 +136,9 @@ LOGGER.debug('ES index looks generated by GDAL') self.is_gdal = True p = ii[self.index_name]['mappings'] + except IndexError: + LOGGER.warning('could not get fields; returning empty set') + return {} for k, v in p['properties'].items(): if 'type' in v: @@ -143,6 +146,8 @@ fields_[k] = {'type': 'string'} elif v['type'] == 'date': fields_[k] = {'type': 'string', 'format': 'date'} + elif v['type'] == 'float': + fields_[k] = {'type': 'number', 'format': v['type']} else: fields_[k] = {'type': v['type']} @@ -310,30 +315,25 @@ query = update_query(input_query=query, cql=filterq) LOGGER.debug(json.dumps(query, indent=4)) - LOGGER.debug('Setting ES paging zero-based') - if startindex > 0: - startindex2 = startindex - 1 - else: - startindex2 = startindex - - if startindex2 + limit > 10000: + LOGGER.debug('Testing for ES scrolling') + if startindex + limit > 10000: gen = helpers.scan(client=self.es, query=query, preserve_order=True, index=self.index_name) results = {'hits': {'total': limit, 'hits': []}} - for i in range(startindex2 + limit): + for i in range(startindex + limit): try: - if i >= startindex2: + if i >= startindex: results['hits']['hits'].append(next(gen)) else: next(gen) except StopIteration: break results['hits']['total'] = \ - len(results['hits']['hits']) + startindex2 + len(results['hits']['hits']) + startindex else: results = self.es.search(index=self.index_name, - from_=startindex2, size=limit, + from_=startindex, size=limit, body=query) results['hits']['total'] = results['hits']['total']['value'] diff -Nru pygeoapi-0.11.0/pygeoapi/provider/postgresql.py pygeoapi-0.12.0/pygeoapi/provider/postgresql.py --- pygeoapi-0.11.0/pygeoapi/provider/postgresql.py 2021-10-24 14:33:20.000000000 +0000 +++ pygeoapi-0.12.0/pygeoapi/provider/postgresql.py 2022-02-07 20:05:13.000000000 +0000 @@ -60,7 +60,7 @@ The class returns a connection object. """ - def __init__(self, conn_dic, table, context="query"): + def __init__(self, conn_dic, table, properties=[], context="query"): """ PostgreSQLProvider Class constructor returning @@ -81,15 +81,17 @@ :param table: table name containing the data. This variable is used to assemble column information + :param properties: User-specified subset of column names to expose :param context: query or hits, if query then it will determine table column otherwise will not do it - :returns: psycopg2.extensions.connection + :returns: DatabaseConnection """ self.conn_dic = conn_dic self.table = table self.context = context self.columns = None + self.properties = properties self.fields = {} # Dict of columns. Key is col name, value is type self.conn = None @@ -110,13 +112,26 @@ self.cur = self.conn.cursor() if self.context == 'query': - # Getting columns - query_cols = "SELECT column_name, udt_name FROM information_schema.columns \ - WHERE table_name = '{}' and udt_name != 'geometry';".format( + # Get table column names and types, excluding geometry and + # transaction ID columns + query_cols = "SELECT attr.attname, tp.typname \ + FROM pg_catalog.pg_class as cls \ + INNER JOIN pg_catalog.pg_attribute as attr \ + ON cls.oid = attr.attrelid \ + INNER JOIN pg_catalog.pg_type as tp \ + ON tp.oid = attr.atttypid \ + WHERE cls.relname = '{}' \ + AND tp.typname != 'geometry' \ + AND tp.typname != 'cid' \ + AND tp.typname != 'oid' \ + AND tp.typname != 'tid' \ + AND tp.typname != 'xid';".format( self.table) self.cur.execute(query_cols) result = self.cur.fetchall() + if self.properties: + result = [res for res in result if res[0] in self.properties] self.columns = SQL(', ').join( [Identifier(item[0]) for item in result] ) @@ -173,7 +188,9 @@ :returns: dict of fields """ if not self.fields: - with DatabaseConnection(self.conn_dic, self.table) as db: + with DatabaseConnection(self.conn_dic, + self.table, + properties=self.properties) as db: self.fields = db.fields return self.fields @@ -246,7 +263,9 @@ if resulttype == 'hits': with DatabaseConnection(self.conn_dic, - self.table, context="hits") as db: + self.table, + properties=self.properties, + context="hits") as db: cursor = db.conn.cursor(cursor_factory=RealDictCursor) where_clause = self.__get_where_clauses( @@ -266,7 +285,9 @@ end_index = startindex + limit - with DatabaseConnection(self.conn_dic, self.table) as db: + with DatabaseConnection(self.conn_dic, + self.table, + properties=self.properties) as db: cursor = db.conn.cursor(cursor_factory=RealDictCursor) props = db.columns if select_properties == [] else \ @@ -364,7 +385,9 @@ """ LOGGER.debug('Get item from Postgis') - with DatabaseConnection(self.conn_dic, self.table) as db: + with DatabaseConnection(self.conn_dic, + self.table, + properties=self.properties) as db: cursor = db.conn.cursor(cursor_factory=RealDictCursor) sql_query = SQL("SELECT {},ST_AsGeoJSON({}) \ diff -Nru pygeoapi-0.11.0/pygeoapi/provider/rasterio_.py pygeoapi-0.12.0/pygeoapi/provider/rasterio_.py --- pygeoapi-0.11.0/pygeoapi/provider/rasterio_.py 2021-10-24 14:33:20.000000000 +0000 +++ pygeoapi-0.12.0/pygeoapi/provider/rasterio_.py 2022-02-07 20:05:13.000000000 +0000 @@ -72,23 +72,23 @@ """ domainset = { - 'type': 'DomainSetType', + 'type': 'DomainSet', 'generalGrid': { - 'type': 'GeneralGridCoverageType', + 'type': 'GeneralGridCoverage', 'srsName': self._coverage_properties['bbox_crs'], 'axisLabels': [ self._coverage_properties['x_axis_label'], self._coverage_properties['y_axis_label'] ], 'axis': [{ - 'type': 'RegularAxisType', + 'type': 'RegularAxis', 'axisLabel': self._coverage_properties['x_axis_label'], 'lowerBound': self._coverage_properties['bbox'][0], 'upperBound': self._coverage_properties['bbox'][2], 'uomLabel': self._coverage_properties['bbox_units'], 'resolution': self._coverage_properties['resx'] }, { - 'type': 'RegularAxisType', + 'type': 'RegularAxis', 'axisLabel': self._coverage_properties['y_axis_label'], 'lowerBound': self._coverage_properties['bbox'][1], 'upperBound': self._coverage_properties['bbox'][3], @@ -96,16 +96,16 @@ 'resolution': self._coverage_properties['resy'] }], 'gridLimits': { - 'type': 'GridLimitsType', + 'type': 'GridLimits', 'srsName': 'http://www.opengis.net/def/crs/OGC/0/Index2D', 'axisLabels': ['i', 'j'], 'axis': [{ - 'type': 'IndexAxisType', + 'type': 'IndexAxis', 'axisLabel': 'i', 'lowerBound': 0, 'upperBound': self._coverage_properties['width'] }, { - 'type': 'IndexAxisType', + 'type': 'IndexAxis', 'axisLabel': 'j', 'lowerBound': 0, 'upperBound': self._coverage_properties['height'] @@ -126,7 +126,7 @@ """ rangetype = { - 'type': 'DataRecordType', + 'type': 'DataRecord', 'field': [] } @@ -143,9 +143,11 @@ rangetype['field'].append({ 'id': i, - 'type': 'QuantityType', + 'type': 'Quantity', 'name': name, - 'definition': dtype, + 'encodingInfo': { + 'dataType': 'http://www.opengis.net/def/dataType/OGC/0/{}'.format(dtype) # noqa + }, 'nodata': nodataval, 'uom': { 'id': 'http://www.opengis.net/def/uom/UCUM/{}'.format( diff -Nru pygeoapi-0.11.0/pygeoapi/provider/sensorthings.py pygeoapi-0.12.0/pygeoapi/provider/sensorthings.py --- pygeoapi-0.11.0/pygeoapi/provider/sensorthings.py 2021-10-24 14:33:20.000000000 +0000 +++ pygeoapi-0.12.0/pygeoapi/provider/sensorthings.py 2022-02-07 20:05:13.000000000 +0000 @@ -27,13 +27,13 @@ # # ================================================================= -from requests import get, codes -from requests.compat import urljoin +from requests import Session, get, codes import logging from pygeoapi.provider.base import (BaseProvider, ProviderQueryError, ProviderConnectionError, ProviderItemNotFoundError) -from pygeoapi.util import yaml_load +from json.decoder import JSONDecodeError +from pygeoapi.util import yaml_load, url_join LOGGER = logging.getLogger(__name__) @@ -46,26 +46,18 @@ } _EXPAND = { 'Things': """ - Locations, - Datastreams( - $select=@iot.id,properties - ) + Locations + ,Datastreams """, 'Observations': """ - Datastream( - $select=@iot.id,properties - ), - FeatureOfInterest + Datastream + ,FeatureOfInterest """, 'Datastreams': """ Sensor ,ObservedProperty - ,Thing( - $select=@iot.id,properties - ) - ,Thing/Locations( - $select=location - ) + ,Thing + ,Thing/Locations ,Observations( $select=@iot.id; $orderby=phenomenonTime_desc @@ -97,7 +89,7 @@ super().__init__(provider_def) try: self.entity = provider_def['entity'] - self._url = ''.join((self.data, self.entity)) + self._url = url_join(self.data, self.entity) except KeyError: raise RuntimeError('name/type/data are required') @@ -124,7 +116,12 @@ if not self.fields: p = {'$expand': EXPAND[self.entity], '$top': 1} r = get(self._url, params=p) - results = r.json()['value'][0] + try: + results = r.json()['value'][0] + except JSONDecodeError as err: + LOGGER.error('Entity {} error: {}'.format(self.entity, err)) + LOGGER.error('Bad url response at {}'.format(r.url)) + raise ProviderQueryError(err) for (n, v) in results.items(): if isinstance(v, (int, float)) or \ @@ -192,7 +189,8 @@ for p in rs['providers']: # Validate linkable provider if (p['name'] != 'SensorThings' - or not p.get('intralink', False)): + or not p.get('intralink', False) + or p['data'] != self.data): continue if p.get('default', False) is True: @@ -238,34 +236,44 @@ if sortby: params['$orderby'] = self._make_orderby(sortby) + # Start session + s = Session() + # Form URL for GET request LOGGER.debug('Sending query') if identifier: - r = get(f'{self._url}({identifier})', params=params) + r = s.get(f'{self._url}({identifier})', params=params) else: - r = get(self._url, params=params) + r = s.get(self._url, params=params) if r.status_code == codes.bad: LOGGER.error('Bad http response code') raise ProviderConnectionError('Bad http response code') - response = r.json() + # if hits, return count if resulttype == 'hits': LOGGER.debug('Returning hits') feature_collection['numberMatched'] = response.get('@iot.count') return feature_collection + # Query if values are less than expected v = [response, ] if identifier else response.get('value') - # if values are less than expected, query for more hits_ = 1 if identifier else min(limit, response.get('@iot.count')) while len(v) < hits_: LOGGER.debug('Fetching next set of values') - r = get(response.get('@iot.nextLink'), params={'$skip': len(v)}) - response = r.json() - v.extend(response.get('value')) + next_ = response.get('@iot.nextLink', None) + if next_ is None: + break + else: + with s.get(next_) as r: + response = r.json() + v.extend(response.get('value')) + + # End session + s.close() - # properties filter & display + # Properties filter & display keys = (() if not self.properties and not select_properties else set(self.properties) | set(select_properties)) @@ -400,6 +408,10 @@ if self.entity == 'Things': extra_props = entity['Locations'][0].get('properties', {}) entity['properties'].update(extra_props) + elif 'Thing' in entity.keys(): + t = entity.get('Thing') + extra_props = t['Locations'][0].get('properties', {}) + t['properties'].update(extra_props) for k, v in entity.items(): # Create intra links @@ -416,7 +428,7 @@ for i, _v in enumerate(v): id = _v[self.id_field] id = f"'{id}'" if isinstance(id, str) else str(id) - v[i] = urljoin( + v[i] = url_join( self._rel_link, path_.format( self._linkables[k]['n'], id @@ -429,7 +441,7 @@ continue id = v[self.id_field] id = f"'{id}'" if isinstance(id, str) else str(id) - entity[k] = urljoin( + entity[k] = url_join( self._rel_link, path_.format( self._linkables[ks]['n'], id diff -Nru pygeoapi-0.11.0/pygeoapi/provider/tinydb_.py pygeoapi-0.12.0/pygeoapi/provider/tinydb_.py --- pygeoapi-0.11.0/pygeoapi/provider/tinydb_.py 2021-10-24 14:33:20.000000000 +0000 +++ pygeoapi-0.12.0/pygeoapi/provider/tinydb_.py 2022-02-07 20:05:13.000000000 +0000 @@ -56,7 +56,7 @@ '_metadata-anytext', ] - BaseProvider.__init__(self, provider_def) + super().__init__(provider_def) LOGGER.debug('Connecting to TinyDB db at {}'.format(self.data)) diff -Nru pygeoapi-0.11.0/pygeoapi/provider/xarray_edr.py pygeoapi-0.12.0/pygeoapi/provider/xarray_edr.py --- pygeoapi-0.11.0/pygeoapi/provider/xarray_edr.py 2021-10-24 14:33:20.000000000 +0000 +++ pygeoapi-0.12.0/pygeoapi/provider/xarray_edr.py 2022-02-07 20:05:13.000000000 +0000 @@ -30,12 +30,13 @@ import logging from pygeoapi.provider.base import ProviderNoDataError +from pygeoapi.provider.base_edr import BaseEDRProvider from pygeoapi.provider.xarray_ import _to_datetime_string, XarrayProvider LOGGER = logging.getLogger(__name__) -class XarrayEDRProvider(XarrayProvider): +class XarrayEDRProvider(BaseEDRProvider, XarrayProvider): """EDR Provider""" def __init__(self, provider_def): @@ -47,8 +48,8 @@ :returns: pygeoapi.provider.rasterio_.RasterioProvider """ + BaseEDRProvider.__init__(self, provider_def) XarrayProvider.__init__(self, provider_def) - self.instances = [] def get_fields(self): """ @@ -59,25 +60,8 @@ return self.get_coverage_rangetype() - def get_instance(self, instance): - """ - Validate instance identifier - - :returns: `bool` of whether instance is valid - """ - - return NotImplementedError() - - def get_query_types(self): - """ - Provide supported query types - - :returns: list of EDR query types - """ - - return ['position'] - - def query(self, **kwargs): + @BaseEDRProvider.register() + def position(self, **kwargs): """ Extract data from collection collection diff -Nru pygeoapi-0.11.0/pygeoapi/provider/xarray_.py pygeoapi-0.12.0/pygeoapi/provider/xarray_.py --- pygeoapi-0.11.0/pygeoapi/provider/xarray_.py 2021-10-24 14:33:20.000000000 +0000 +++ pygeoapi-0.12.0/pygeoapi/provider/xarray_.py 2022-02-07 20:05:13.000000000 +0000 @@ -83,9 +83,9 @@ c_props = self._coverage_properties domainset = { - 'type': 'DomainSetType', + 'type': 'DomainSet', 'generalGrid': { - 'type': 'GeneralGridCoverageType', + 'type': 'GeneralGridCoverage', 'srsName': c_props['bbox_crs'], 'axisLabels': [ c_props['x_axis_label'], @@ -93,14 +93,14 @@ c_props['time_axis_label'] ], 'axis': [{ - 'type': 'RegularAxisType', + 'type': 'RegularAxis', 'axisLabel': c_props['x_axis_label'], 'lowerBound': c_props['bbox'][0], 'upperBound': c_props['bbox'][2], 'uomLabel': c_props['bbox_units'], 'resolution': c_props['resx'] }, { - 'type': 'RegularAxisType', + 'type': 'RegularAxis', 'axisLabel': c_props['y_axis_label'], 'lowerBound': c_props['bbox'][1], 'upperBound': c_props['bbox'][3], @@ -108,7 +108,7 @@ 'resolution': c_props['resy'] }, { - 'type': 'RegularAxisType', + 'type': 'RegularAxis', 'axisLabel': c_props['time_axis_label'], 'lowerBound': c_props['time_range'][0], 'upperBound': c_props['time_range'][1], @@ -117,16 +117,16 @@ } ], 'gridLimits': { - 'type': 'GridLimitsType', + 'type': 'GridLimits', 'srsName': 'http://www.opengis.net/def/crs/OGC/0/Index2D', 'axisLabels': ['i', 'j'], 'axis': [{ - 'type': 'IndexAxisType', + 'type': 'IndexAxis', 'axisLabel': 'i', 'lowerBound': 0, 'upperBound': c_props['width'] }, { - 'type': 'IndexAxisType', + 'type': 'IndexAxis', 'axisLabel': 'j', 'lowerBound': 0, 'upperBound': c_props['height'] @@ -148,7 +148,7 @@ """ rangetype = { - 'type': 'DataRecordType', + 'type': 'DataRecord', 'field': [] } @@ -164,9 +164,11 @@ rangetype['field'].append({ 'id': name, - 'type': 'QuantityType', + 'type': 'Quantity', 'name': var.attrs.get('long_name') or desc, - 'definition': str(var.dtype), + 'encodingInfo': { + 'dataType': 'http://www.opengis.net/def/dataType/OGC/0/{}'.format(str(var.dtype)) # noqa + }, 'nodata': 'null', 'uom': { 'id': 'http://www.opengis.net/def/uom/UCUM/{}'.format( diff -Nru pygeoapi-0.11.0/pygeoapi/schemas/config/pygeoapi-config-0.x.yml pygeoapi-0.12.0/pygeoapi/schemas/config/pygeoapi-config-0.x.yml --- pygeoapi-0.11.0/pygeoapi/schemas/config/pygeoapi-config-0.x.yml 2021-10-24 14:33:20.000000000 +0000 +++ pygeoapi-0.12.0/pygeoapi/schemas/config/pygeoapi-config-0.x.yml 2022-02-07 20:05:13.000000000 +0000 @@ -31,6 +31,9 @@ encoding: type: string description: default server encoding + gzip: + type: boolean + description: default server config to gzip/compress responses to requests with gzip in the Accept-Encoding header language: type: string description: default server language diff -Nru pygeoapi-0.11.0/pygeoapi/starlette_app.py pygeoapi-0.12.0/pygeoapi/starlette_app.py --- pygeoapi-0.11.0/pygeoapi/starlette_app.py 2021-10-24 14:33:20.000000000 +0000 +++ pygeoapi-0.12.0/pygeoapi/starlette_app.py 2022-02-07 20:05:13.000000000 +0000 @@ -4,7 +4,7 @@ # Tom Kralidis # # Copyright (c) 2020 Francesco Bartoli -# Copyright (c) 2020 Tom Kralidis +# Copyright (c) 2022 Tom Kralidis # # Permission is hereby granted, free of charge, to any person # obtaining a copy of this software and associated documentation @@ -321,33 +321,29 @@ return get_response(api_.describe_processes(request, process_id)) -@app.route('/processes/{process_id}/jobs') -@app.route('/processes/{process_id}/jobs/{job_id}', methods=['GET', 'DELETE']) -@app.route('/processes/{process_id}/jobs/{job_id}/', methods=['GET', 'DELETE']) -async def get_process_jobs(request: Request, process_id=None, job_id=None): +@app.route('/jobs') +@app.route('/jobs/{job_id}', methods=['GET', 'DELETE']) +@app.route('/jobs/{job_id}/', methods=['GET', 'DELETE']) +async def get_jobs(request: Request, job_id=None): """ OGC API - Processes jobs endpoint :param request: Starlette Request instance - :param process_id: process identifier :param job_id: job identifier :returns: Starlette HTTP Response """ - if 'process_id' in request.path_params: - process_id = request.path_params['process_id'] if 'job_id' in request.path_params: job_id = request.path_params['job_id'] if job_id is None: # list of submit job - return get_response(api_.get_process_jobs(request, process_id)) + return get_response(api_.get_jobs(request)) else: # get or delete job if request.method == 'DELETE': - return get_response(api_.delete_process_job(process_id, job_id)) + return get_response(api_.delete_job(job_id)) else: # Return status of a specific job - return get_response(api_.get_process_jobs( - request, process_id, job_id)) + return get_response(api_.get_jobs(request, job_id)) @app.route('/processes/{process_id}/execution', methods=['POST']) @@ -368,55 +364,47 @@ return get_response(api_.execute_process(request, process_id)) -@app.route('/processes/{process_id}/jobs/{job_id}/results', methods=['GET']) -@app.route('/processes/{process_id}/jobs/{job_id}/results/', methods=['GET']) -async def get_process_job_result(request: Request, process_id=None, - job_id=None): +@app.route('/jobs/{job_id}/results', methods=['GET']) +@app.route('/jobs/{job_id}/results/', methods=['GET']) +async def get_job_result(request: Request, job_id=None): """ OGC API - Processes job result endpoint :param request: Starlette Request instance - :param process_id: process identifier :param job_id: job identifier :returns: HTTP response """ - if 'process_id' in request.path_params: - process_id = request.path_params['process_id'] if 'job_id' in request.path_params: job_id = request.path_params['job_id'] - return get_response(api_.get_process_job_result( - request, process_id, job_id)) + return get_response(api_.get_job_result(request, job_id)) -@app.route('/processes/{process_id}/jobs/{job_id}/results/{resource}', +@app.route('/jobs/{job_id}/results/{resource}', methods=['GET']) -@app.route('/processes/{process_id}/jobs/{job_id}/results/{resource}/', +@app.route('/jobs/{job_id}/results/{resource}/', methods=['GET']) -async def get_process_job_result_resource(request: Request, process_id=None, - job_id=None, resource=None): +async def get_job_result_resource(request: Request, + job_id=None, resource=None): """ OGC API - Processes job result resource endpoint :param request: Starlette Request instance - :param process_id: process identifier :param job_id: job identifier :param resource: job resource :returns: HTTP response """ - if 'process_id' in request.path_params: - process_id = request.path_params['process_id'] if 'job_id' in request.path_params: job_id = request.path_params['job_id'] if 'resource' in request.path_params: resource = request.path_params['resource'] - return get_response(api_.get_process_job_result_resource( - request, process_id, job_id, resource)) + return get_response(api_.get_job_result_resource( + request, job_id, resource)) @app.route('/collections/{collection_id}/position') diff -Nru pygeoapi-0.11.0/pygeoapi/templates/jobs/index.html pygeoapi-0.12.0/pygeoapi/templates/jobs/index.html --- pygeoapi-0.11.0/pygeoapi/templates/jobs/index.html 1970-01-01 00:00:00.000000000 +0000 +++ pygeoapi-0.12.0/pygeoapi/templates/jobs/index.html 2022-02-07 20:05:13.000000000 +0000 @@ -0,0 +1,52 @@ +{% extends "_base.html" %} +{% block title %}{{ super() }} {% trans %}Jobs{% endtrans %} {% endblock %} +{% block crumbs %}{{ super() }} +/ {% trans %}Jobs{% endtrans %} +{% endblock %} +{% block body %} +
+
+
+ + + + + + + + + + + + + + + {% for job in data.jobs.jobs %} + + + + + + + + + + {% endfor %} + +
{% trans %}Jobs{% endtrans %}
{% trans %}Job ID{% endtrans %}{% trans %}Process ID{% endtrans %}{% trans %}Start{% endtrans %}{% trans %}Duration{% endtrans %}{% trans %}Progress{% endtrans %}{% trans %}Status{% endtrans %}{% trans %}Message{% endtrans %}
{{ job.jobID }}{{ job.processID }}{{ job.job_start_datetime|format_datetime }} + {% if job.status == 'running' %} + {{ job.job_start_datetime|format_duration(data.now) }} + {% else %} + {{ job.job_start_datetime|format_duration(job.job_end_datetime) }} + {% endif %} + + + + {{ job.status }} + + {{ job.message }} +
+
+
+
+{% endblock %} diff -Nru pygeoapi-0.11.0/pygeoapi/templates/jobs/job.html pygeoapi-0.12.0/pygeoapi/templates/jobs/job.html --- pygeoapi-0.11.0/pygeoapi/templates/jobs/job.html 1970-01-01 00:00:00.000000000 +0000 +++ pygeoapi-0.12.0/pygeoapi/templates/jobs/job.html 2022-02-07 20:05:13.000000000 +0000 @@ -0,0 +1,61 @@ +{% extends "_base.html" %} +{% block title %}{{ super() }} {% trans %}Job status{% endtrans %} {% endblock %} +{% block crumbs %}{{ super() }} +/ {% trans %}Jobs{% endtrans %} +/ {{ data['jobs']['jobID'] }} +{% endblock %} +{% block body %} +
+
+

{% trans %}Job status{% endtrans %}

+
+
+
+
+
+

{% trans %}Status{% endtrans %}: {{ data['jobs']['status'] }}

+

{% trans %}Progress{% endtrans %}: {{ data['jobs']['progress'] }}%

+
+
+
+

{% trans %}Message{% endtrans %}

+

{{ data['jobs']['message'] }}

+
+ {% if data['jobs']['parameters'] %} +
+

{% trans %}Parameters{% endtrans %}

+

+              
+ + {% endif %} +
+

+ +

+

+ {% if data['jobs']['status'] == 'running' %} + {{ data['jobs']['job_start_datetime']|format_duration(data.now) }} + {% else %} + {{ data['jobs']['job_start_datetime']|format_duration(data['jobs']['job_end_datetime']) }} + {% endif %} +

+

+

{{ data['jobs']['job_start_datetime']|format_datetime }}

+

+

{{ data['jobs']['job_end_datetime']|format_datetime }}

+ +

{% trans %}Links{% endtrans %}

+ +
+
+
+
+
+
+{% endblock %} diff -Nru pygeoapi-0.11.0/pygeoapi/templates/jobs/results/index.html pygeoapi-0.12.0/pygeoapi/templates/jobs/results/index.html --- pygeoapi-0.11.0/pygeoapi/templates/jobs/results/index.html 1970-01-01 00:00:00.000000000 +0000 +++ pygeoapi-0.12.0/pygeoapi/templates/jobs/results/index.html 2022-02-07 20:05:13.000000000 +0000 @@ -0,0 +1,16 @@ +{% extends "_base.html" %} +{% block title %}{{ super() }} {% trans %}Job result{% endtrans %} {% endblock %} +{% block crumbs %}{{ super() }} +/ {% trans %}Jobs{% endtrans %} +/ {{ data.job.id }} +/ {% trans %}Results{% endtrans %} +{% endblock %} +{% block body %} +
+

{% trans %}Results of job{% endtrans %} {{data.job.id}}

+

+  
+ +{% endblock %} diff -Nru pygeoapi-0.11.0/pygeoapi/templates/landing_page.html pygeoapi-0.12.0/pygeoapi/templates/landing_page.html --- pygeoapi-0.11.0/pygeoapi/templates/landing_page.html 2021-10-24 14:33:20.000000000 +0000 +++ pygeoapi-0.12.0/pygeoapi/templates/landing_page.html 2022-02-07 20:05:13.000000000 +0000 @@ -64,6 +64,12 @@ {% trans %}View the processes in this service{% endtrans %}

+
+

{% trans %}Jobs{% endtrans %}

+

+ {% trans %}Browse jobs{% endtrans %} +

+
{% endif %}

{% trans %}API Definition{% endtrans %}

diff -Nru pygeoapi-0.11.0/pygeoapi/templates/openapi/swagger.html pygeoapi-0.12.0/pygeoapi/templates/openapi/swagger.html --- pygeoapi-0.11.0/pygeoapi/templates/openapi/swagger.html 2021-10-24 14:33:20.000000000 +0000 +++ pygeoapi-0.12.0/pygeoapi/templates/openapi/swagger.html 2022-02-07 20:05:13.000000000 +0000 @@ -36,7 +36,7 @@ window.onload = function() { // Begin Swagger UI call region ui = SwaggerUIBundle({ - url: '{{ data['openapi-document-path'] }}', + url: '{{ data['openapi-document-path'] }}?f=json', dom_id: '#swagger-ui', deepLinking: true, presets: [ diff -Nru pygeoapi-0.11.0/pygeoapi/templates/processes/jobs/index.html pygeoapi-0.12.0/pygeoapi/templates/processes/jobs/index.html --- pygeoapi-0.11.0/pygeoapi/templates/processes/jobs/index.html 2021-10-24 14:33:20.000000000 +0000 +++ pygeoapi-0.12.0/pygeoapi/templates/processes/jobs/index.html 1970-01-01 00:00:00.000000000 +0000 @@ -1,52 +0,0 @@ -{% extends "_base.html" %} -{% block title %}{{ super() }} {% trans %}Jobs{% endtrans %} {% endblock %} -{% block crumbs %}{{ super() }} -/ {% trans %}Processes{% endtrans %} -/ {{ data.process.title }} -/ {% trans %}Jobs{% endtrans %} -{% endblock %} -{% block body %} -
-
-
- - - - - - - - - - - - - - {% for job in data.jobs %} - - - - - - - - - {% endfor %} - -
{% trans %}Jobs{% endtrans %}
{% trans %}ID{% endtrans %}{% trans %}Start{% endtrans %}{% trans %}Duration{% endtrans %}{% trans %}Progress{% endtrans %}{% trans %}Status{% endtrans %}{% trans %}Message{% endtrans %}
{{ job.jobID }}{{ job.job_start_datetime|format_datetime }} - {% if job.status == 'running' %} - {{ job.job_start_datetime|format_duration(data.now) }} - {% else %} - {{ job.job_start_datetime|format_duration(job.job_end_datetime) }} - {% endif %} - - - - {{ job.status }} - - {{ job.message }} -
-
-
-
-{% endblock %} diff -Nru pygeoapi-0.11.0/pygeoapi/templates/processes/jobs/job.html pygeoapi-0.12.0/pygeoapi/templates/processes/jobs/job.html --- pygeoapi-0.11.0/pygeoapi/templates/processes/jobs/job.html 2021-10-24 14:33:20.000000000 +0000 +++ pygeoapi-0.12.0/pygeoapi/templates/processes/jobs/job.html 1970-01-01 00:00:00.000000000 +0000 @@ -1,63 +0,0 @@ -{% extends "_base.html" %} -{% block title %}{{ super() }} {% trans %}Job status{% endtrans %} {% endblock %} -{% block crumbs %}{{ super() }} -/ {% trans %}Processes{% endtrans %} -/ {{ data['process']['title'] }} -/ {% trans %}Jobs{% endtrans %} -/ {{ data['jobs']['jobID'] }} -{% endblock %} -{% block body %} -
-
-

{% trans %}Job status{% endtrans %}

-
-
-
-
-
-

{% trans %}Status{% endtrans %}: {{ data['jobs']['status'] }}

-

{% trans %}Progress{% endtrans %}: {{ data['jobs']['progress'] }}%

-
-
-
-

{% trans %}Message{% endtrans %}

-

{{ data['jobs']['message'] }}

-
- {% if data['jobs']['parameters'] %} -
-

{% trans %}Parameters{% endtrans %}

-

-              
- - {% endif %} -
-

- -

-

- {% if data['jobs']['status'] == 'running' %} - {{ data['jobs']['job_start_datetime']|format_duration(data.now) }} - {% else %} - {{ data['jobs']['job_start_datetime']|format_duration(data['jobs']['job_end_datetime']) }} - {% endif %} -

-

-

{{ data['jobs']['job_start_datetime']|format_datetime }}

-

-

{{ data['jobs']['job_end_datetime']|format_datetime }}

- -

{% trans %}Links{% endtrans %}

- -
-
-
-
-
-
-{% endblock %} diff -Nru pygeoapi-0.11.0/pygeoapi/templates/processes/jobs/results/index.html pygeoapi-0.12.0/pygeoapi/templates/processes/jobs/results/index.html --- pygeoapi-0.11.0/pygeoapi/templates/processes/jobs/results/index.html 2021-10-24 14:33:20.000000000 +0000 +++ pygeoapi-0.12.0/pygeoapi/templates/processes/jobs/results/index.html 1970-01-01 00:00:00.000000000 +0000 @@ -1,18 +0,0 @@ -{% extends "_base.html" %} -{% block title %}{{ super() }} {% trans %}Job result{% endtrans %} {% endblock %} -{% block crumbs %}{{ super() }} -/ {% trans %}Processes{% endtrans %} -/ {{ data.process.title }} -/ {% trans %}Jobs{% endtrans %} -/ {{ data.job.id }} -/ {% trans %}Results{% endtrans %} -{% endblock %} -{% block body %} -
-

{% trans %}Results of job{% endtrans %} {{data.job.id}}

-

-  
- -{% endblock %} diff -Nru pygeoapi-0.11.0/pygeoapi/templates/processes/process.html pygeoapi-0.12.0/pygeoapi/templates/processes/process.html --- pygeoapi-0.11.0/pygeoapi/templates/processes/process.html 2021-10-24 14:33:20.000000000 +0000 +++ pygeoapi-0.12.0/pygeoapi/templates/processes/process.html 2022-02-07 20:05:13.000000000 +0000 @@ -74,7 +74,7 @@ {% if 'async-execute' in data.jobControlOptions %}
  • {% trans %}Asynchronous{% endtrans %}
  • {% endif %}

    {% trans %}Jobs{% endtrans %}

    - {% trans %}Browse jobs{% endtrans %} + {% trans %}Browse jobs{% endtrans %}

    {% trans %}Links{% endtrans %}

      {% for link in data['links'] %} diff -Nru pygeoapi-0.11.0/pygeoapi/util.py pygeoapi-0.12.0/pygeoapi/util.py --- pygeoapi-0.11.0/pygeoapi/util.py 2021-10-24 14:33:20.000000000 +0000 +++ pygeoapi-0.12.0/pygeoapi/util.py 2022-02-07 20:05:13.000000000 +0000 @@ -122,7 +122,8 @@ def path_constructor(loader, node): env_var = path_matcher.match(node.value).group(1) if env_var not in os.environ: - raise EnvironmentError('Undefined environment variable in config') + msg = 'Undefined environment variable {} in config'.format(env_var) + raise EnvironmentError(msg) return get_typed_value(os.path.expandvars(node.value)) class EnvVarLoader(yaml.SafeLoader): diff -Nru pygeoapi-0.11.0/pygeoapi-config.yml pygeoapi-0.12.0/pygeoapi-config.yml --- pygeoapi-0.11.0/pygeoapi-config.yml 2021-10-24 14:33:20.000000000 +0000 +++ pygeoapi-0.12.0/pygeoapi-config.yml 2022-02-07 20:05:13.000000000 +0000 @@ -34,6 +34,7 @@ url: http://localhost:5000 mimetype: application/json; charset=UTF-8 encoding: utf-8 + gzip: false languages: # First language is the default language - en-US @@ -76,7 +77,7 @@ - api keywords_type: theme terms_of_service: https://creativecommons.org/licenses/by/4.0/ - url: http://example.org + url: https://example.org license: name: CC-BY 4.0 license url: https://creativecommons.org/licenses/by/4.0/ diff -Nru pygeoapi-0.11.0/tests/cite/ogcapi-features/cite.config.yml pygeoapi-0.12.0/tests/cite/ogcapi-features/cite.config.yml --- pygeoapi-0.11.0/tests/cite/ogcapi-features/cite.config.yml 2021-10-24 14:33:20.000000000 +0000 +++ pygeoapi-0.12.0/tests/cite/ogcapi-features/cite.config.yml 2022-02-07 20:05:13.000000000 +0000 @@ -8,7 +8,7 @@ language: en-US cors: true pretty_print: true - limit: 10 + limit: 100 # templates: /path/to/templates map: url: https://maps.wikimedia.org/osm-intl/{z}/{x}/{y}.png diff -Nru pygeoapi-0.11.0/tests/cite/ogcapi-features/README.md pygeoapi-0.12.0/tests/cite/ogcapi-features/README.md --- pygeoapi-0.11.0/tests/cite/ogcapi-features/README.md 2021-10-24 14:33:20.000000000 +0000 +++ pygeoapi-0.12.0/tests/cite/ogcapi-features/README.md 2022-02-07 20:05:13.000000000 +0000 @@ -3,7 +3,7 @@ ## Test data Test data used is a subset of the [Canadian National Water Data Archive](https://www.canada.ca/en/environment-climate-change/services/water-overview/quantity/monitoring/survey/data-products-services/national-archive-hydat.html) -as extracted from the [MSC Geomet OGC API](https://eccc-msc.github.io/open-data/msc-geomet/web-services_en/#ogc-api-features) service. +as extracted from the [MSC GeoMet OGC API](https://eccc-msc.github.io/open-data/msc-geomet/web-services_en/#ogc-api-features) service. ## Running diff -Nru pygeoapi-0.11.0/tests/data/hotosm_bdi_waterways.sql pygeoapi-0.12.0/tests/data/hotosm_bdi_waterways.sql --- pygeoapi-0.11.0/tests/data/hotosm_bdi_waterways.sql 2021-10-24 14:33:20.000000000 +0000 +++ pygeoapi-0.12.0/tests/data/hotosm_bdi_waterways.sql 2022-02-07 20:05:13.000000000 +0000 @@ -2,8 +2,8 @@ -- PostgreSQL database dump -- --- Dumped from database version 11.5 (Debian 11.5-3.pgdg100+1) --- Dumped by pg_dump version 11.5 (Debian 11.5-3.pgdg100+1) +-- Dumped from database version 11.5 (Debian 11.5-1.pgdg100+1) +-- Dumped by pg_dump version 11.5 (Debian 11.5-1.pgdg100+1) SET statement_timeout = 0; SET lock_timeout = 0; @@ -42,103 +42,48 @@ -- --- Name: hstore; Type: EXTENSION; Schema: -; Owner: +-- Name: hstore; Type: EXTENSION; Schema: -; Owner: -- CREATE EXTENSION IF NOT EXISTS hstore WITH SCHEMA public; -- --- Name: EXTENSION hstore; Type: COMMENT; Schema: -; Owner: +-- Name: EXTENSION hstore; Type: COMMENT; Schema: -; Owner: -- COMMENT ON EXTENSION hstore IS 'data type for storing sets of (key, value) pairs'; -- --- Name: postgis; Type: EXTENSION; Schema: -; Owner: +-- Name: postgis; Type: EXTENSION; Schema: -; Owner: -- CREATE EXTENSION IF NOT EXISTS postgis WITH SCHEMA public; -- --- Name: EXTENSION postgis; Type: COMMENT; Schema: -; Owner: +-- Name: EXTENSION postgis; Type: COMMENT; Schema: -; Owner: -- COMMENT ON EXTENSION postgis IS 'PostGIS geometry, geography, and raster spatial types and functions'; -- --- Name: postgis_topology; Type: EXTENSION; Schema: -; Owner: +-- Name: postgis_topology; Type: EXTENSION; Schema: -; Owner: -- CREATE EXTENSION IF NOT EXISTS postgis_topology WITH SCHEMA topology; -- --- Name: EXTENSION postgis_topology; Type: COMMENT; Schema: -; Owner: +-- Name: EXTENSION postgis_topology; Type: COMMENT; Schema: -; Owner: -- COMMENT ON EXTENSION postgis_topology IS 'PostGIS topology spatial types and functions'; -- --- Name: asbinary(public.geometry); Type: FUNCTION; Schema: public; Owner: postgres --- - -CREATE FUNCTION public.asbinary(public.geometry) RETURNS bytea - LANGUAGE c IMMUTABLE STRICT - AS '$libdir/postgis-2.5', 'LWGEOM_asBinary'; - - -ALTER FUNCTION public.asbinary(public.geometry) OWNER TO postgres; - --- --- Name: asbinary(public.geometry, text); Type: FUNCTION; Schema: public; Owner: postgres --- - -CREATE FUNCTION public.asbinary(public.geometry, text) RETURNS bytea - LANGUAGE c IMMUTABLE STRICT - AS '$libdir/postgis-2.5', 'LWGEOM_asBinary'; - - -ALTER FUNCTION public.asbinary(public.geometry, text) OWNER TO postgres; - --- --- Name: astext(public.geometry); Type: FUNCTION; Schema: public; Owner: postgres --- - -CREATE FUNCTION public.astext(public.geometry) RETURNS text - LANGUAGE c IMMUTABLE STRICT - AS '$libdir/postgis-2.5', 'LWGEOM_asText'; - - -ALTER FUNCTION public.astext(public.geometry) OWNER TO postgres; - --- --- Name: estimated_extent(text, text); Type: FUNCTION; Schema: public; Owner: postgres --- - -CREATE FUNCTION public.estimated_extent(text, text) RETURNS public.box2d - LANGUAGE c IMMUTABLE STRICT SECURITY DEFINER - AS '$libdir/postgis-2.5', 'geometry_estimated_extent'; - - -ALTER FUNCTION public.estimated_extent(text, text) OWNER TO postgres; - --- --- Name: estimated_extent(text, text, text); Type: FUNCTION; Schema: public; Owner: postgres --- - -CREATE FUNCTION public.estimated_extent(text, text, text) RETURNS public.box2d - LANGUAGE c IMMUTABLE STRICT SECURITY DEFINER - AS '$libdir/postgis-2.5', 'geometry_estimated_extent'; - - -ALTER FUNCTION public.estimated_extent(text, text, text) OWNER TO postgres; - --- -- Name: geomfromtext(text); Type: FUNCTION; Schema: public; Owner: postgres -- @@ -161,39 +106,6 @@ ALTER FUNCTION public.geomfromtext(text, integer) OWNER TO postgres; -- --- Name: ndims(public.geometry); Type: FUNCTION; Schema: public; Owner: postgres --- - -CREATE FUNCTION public.ndims(public.geometry) RETURNS smallint - LANGUAGE c IMMUTABLE STRICT - AS '$libdir/postgis-2.5', 'LWGEOM_ndims'; - - -ALTER FUNCTION public.ndims(public.geometry) OWNER TO postgres; - --- --- Name: setsrid(public.geometry, integer); Type: FUNCTION; Schema: public; Owner: postgres --- - -CREATE FUNCTION public.setsrid(public.geometry, integer) RETURNS public.geometry - LANGUAGE c IMMUTABLE STRICT - AS '$libdir/postgis-2.5', 'LWGEOM_set_srid'; - - -ALTER FUNCTION public.setsrid(public.geometry, integer) OWNER TO postgres; - --- --- Name: srid(public.geometry); Type: FUNCTION; Schema: public; Owner: postgres --- - -CREATE FUNCTION public.srid(public.geometry) RETURNS integer - LANGUAGE c IMMUTABLE STRICT - AS '$libdir/postgis-2.5', 'LWGEOM_get_srid'; - - -ALTER FUNCTION public.srid(public.geometry) OWNER TO postgres; - --- -- Name: st_asbinary(text); Type: FUNCTION; Schema: public; Owner: postgres -- @@ -267,7 +179,7 @@ CREATE TABLE osm.hotosm_bdi_waterways ( osm_id integer, - geom public.geometry(MultiLineString,4326), + foo_geom public.geometry(MultiLineString,4326), name character varying(80), waterway character varying(80), covered character varying(80), @@ -285,10 +197,26 @@ ALTER TABLE osm.hotosm_bdi_waterways OWNER TO postgres; -- +-- Name: hotosm_bdi_drains; Type: MATERIALIZED VIEW; Schema: osm; Owner: postgres +-- + +CREATE MATERIALIZED VIEW osm.hotosm_bdi_drains AS + SELECT hotosm_bdi_waterways.osm_id, + hotosm_bdi_waterways.foo_geom, + hotosm_bdi_waterways.width, + hotosm_bdi_waterways.depth + FROM osm.hotosm_bdi_waterways + WHERE ((hotosm_bdi_waterways.waterway)::text = 'drain'::text) + WITH NO DATA; + + +ALTER TABLE osm.hotosm_bdi_drains OWNER TO postgres; + +-- -- Data for Name: hotosm_bdi_waterways; Type: TABLE DATA; Schema: osm; Owner: postgres -- -COPY osm.hotosm_bdi_waterways (osm_id, geom, name, waterway, covered, width, depth, layer, blockage, tunnel, "natural", water, z_index) FROM stdin; +COPY osm.hotosm_bdi_waterways (osm_id, foo_geom, name, waterway, covered, width, depth, layer, blockage, tunnel, "natural", water, z_index) FROM stdin; 13990765 0105000020E610000001000000010200000055000000A9424D3E875F3D40EE934847DE2E0BC030C9B72D805F3D40A85D02A6C22E0BC062D5C5127B5F3D40387FB8509E2E0BC0B1C6342E775F3D404C659B65602E0BC0EFF08DD7725F3D40BFF2203D452E0BC0A56B26DF6C5F3D4024C67C8A2D2E0BC0C39B35785F5F3D4008DB9953132E0BC0E01BF972555F3D405CE3D81FCD2D0BC0E197FA79535F3D40D5A425A0D32D0BC0B36785D84F5F3D40337B8FE9BF2D0BC021A6E9584A5F3D40B57691F8CB2D0BC07FCD8305415F3D4008670124E42D0BC026AD5340355F3D405137AB99102E0BC040B91226315F3D40B38D89DEF32D0BC015C95702295F3D40D6E1E82ADD2D0BC0F14520031E5F3D40E1AAFC7CA52D0BC0A9D898D7115F3D400285D5B37B2D0BC02BD615D8085F3D409E40D829562D0BC0A5660FB4025F3D40EF99DB734F2D0BC084189E4DFD5E3D401A7CABBF6F2D0BC0E1EB10A4F75E3D40110D41C4BC2D0BC0F612BE52F15E3D40CBBE2B82FF2D0BC001704793E65E3D40812D65CF542E0BC01A9B6736D95E3D40D9F4FB59872E0BC0C89F0326CB5E3D40DD22D51CC52E0BC0E6875B4FBE5E3D4016681C8FBE2E0BC0C3FEA14F9A5E3D40BFF2203D452E0BC0318A2F24715E3D40366CA521F42D0BC05E1844FF5F5E3D400CAAB294D12D0BC024319CC6515E3D4009F368F4B42D0BC0F9C1548E245E3D403A19C16B3C2D0BC041423976F55D3D407845F0BF952C0BC04178FEFEDE5D3D4043A5B679882B0BC01E7FB3DEB95D3D404165FCFB8C2B0BC085A636829E5D3D402F8507CDAE2B0BC086014BAE625D3D409E9A26C7F82B0BC09605137F145D3D40FB230C03962C0BC078DFE7AEDB5C3D4054F19073AC2C0BC0DBB639DF995C3D40EE57A604692B0BC065A2ADEF685C3D40E107E753C72A0BC09A2732CE3A5C3D403755F7C8E62A0BC0B9967FE3215C3D402D3D3F31572B0BC014DCFE4B085C3D40011E0714A02C0BC084F57F0EF35B3D40A22CD736202D0BC03EDC1F39E35B3D40A85890C1E52C0BC05D4B6D4ECA5B3D40030C2659E22C0BC05469D5F8965B3D408879A05FA52C0BC085775ECF7C5B3D40D2641069652C0BC09D514E0F655B3D404C09771D602C0BC0D16E3EBF395B3D40AC7E5B66C72C0BC05717015F1B5B3D40AC7E5B66C72C0BC05F75898FF45A3D40030C2659E22C0BC024905C48D15A3D403E7958A8352D0BC0066E3887C65A3D40AA8889BC9B2D0BC020ED7F80B55A3D40B0F3250AE32D0BC0EC4905BA9B5A3D40FE800706102E0BC02299C40F845A3D400EB0A07AB52D0BC0A9D1F58E645A3D40B57691F8CB2D0BC0CA24D9D9465A3D40943DF83F762D0BC0511553E9275A3D40900F1F7D382D0BC0DB2626B90D5A3D40E9DCA3ED4E2D0BC0EFC8586DFE593D40EA7019DC7B2D0BC0B8E52329E9593D409C99AAC5F12D0BC0F6B8CA6EC1593D407E665AACF22E0BC0D0EC5FFEAF593D40CD6ACBCA402F0BC00E2E1D739E593D405C74B2D47A2F0BC042716CE289593D408AF0E5F4AB2F0BC00BD869FF5E593D407B6D910FD52F0BC084E3E8E04F593D40B5D2B540CC2F0BC07FD877EA36593D40E551E053EF2F0BC021E120C610593D40E87CC2233B300BC0CDFBA47FEE583D403FD5D7A9A8300BC0845C4EAECE583D4014877D4CB5300BC0B3A899FFA1583D4034982B28B1300BC09B1BD31396583D40E9A4AD0095300BC0AE89BB3088583D40ED3E11D5A5300BC0AED3ED9172583D40C7B205291D310BC077F0B84D5D583D409FFB17AD48310BC0AD9B9CEB45583D40D2A52490B7310BC0E0AAA1C332583D4052499D8026320BC0AAFBB6161C583D40825C3D8276320BC0E902A8F3F2573D40B8C5A1235E330BC0DE5C5727C2573D407A81A32E63340BC0DBC2F352B1573D401D3FAFD3A3340BC0EBADDC6685573D408379D9C067350BC0 Muha stream \N \N \N \N \N \N \N \N 0 25469515 0105000020E610000001000000010200000031040000B50309E5D8D93D401A3C026ECC3106C0FE24E3BD0FDA3D4078CB7AD6DA3006C04C4D277E31DA3D408783296FEC2E06C06F8E290E56DA3D403A68C06A762E06C014ED855D6FDA3D40455E7B0B902D06C05BA3D414B7DA3D409D4DA2B9A92C06C095A76219D1DA3D4099D53BDC0E2D06C0399E2A3AEDDA3D40F3EC4D67822C06C0DFFC868906DB3D4055AA9F92CE2B06C0E030D12005DB3D405B728170AA2A06C04650EBEC1ADB3D4013190C642C2906C00611A96917DB3D40C94D1E60F72706C02D25CB4928DB3D4022D1BAFC3D2706C07C8159A148DB3D407C7C4276DE2606C0CADDE7F868DB3D402A77FA9C162706C06FD4AF1985DB3D40D8BB3FDEAB2606C013FFC1D19FDB3D402E39EE940E2606C066FA25E2ADDB3D4093DA30AFC82406C0A5396865B1DB3D4099C40F841A2406C0A8DD544909DC3D40A576D036482206C0D3C4967906DC3D400CAC883F2F2106C0A7A90AB20ADC3D40150ECEF11B2006C095EDE82400DC3D40BDB2C178BC1F06C0A7A90AB20ADC3D40D47C3AC3791D06C0222125D126DC3D40DB8E0475251D06C0B2F336363BDC3D4031783D3D5B1C06C0571E49EE55DC3D40981F6E3DF91A06C05852938554DC3D404C4E9253E01906C0144031B264DC3D4055D2D4FC421906C0938A6B216DDC3D40B2CDD776241806C05B2ACA00ABDC3D40BDC392173E1706C0EBFCDB65BFDC3D4069E0EC7B791606C0056454BE0CDD3D40103B53E8BC1606C02DACC0351CDD3D406ECACB50CB1506C0E6FA88A93EDD3D4071D0B936AF1506C08B259B6159DD3D40C97553CA6B1506C0C28A53AD85DD3D406ECACB50CB1506C0DC25169DD1DD3D402A9F2F078B1406C04111E6D1E8DD3D407B32FFE89B1406C0940C4AE2F6DD3D40D46B0E6B851406C08793EAF131DE3D407BE871CE3E1506C01A02DB1C42DE3D4072F8A413091606C0FCCF509264DE3D4015B3B9C5571606C077476BB180DE3D40C38B74F5191606C043869F49ACDE3D40BDA19520C81606C0AF1CC64906DF3D4069965F611C1706C0153CE0151CDF3D40BABDA4315A1706C0594E42E90BDF3D40B1396288F71706C0B0C91AF510DF3D40B8AF03E78C1806C0258D768137DF3D405B0641ECF11706C0D12E956D96DF3D400B5174136B1706C0F4A3E194B9DF3D405C2E2CC94B1806C00841A264CDDF3D405D047C6DEC1806C091A1BEC007E03D40B8AF03E78C1806C07FEDAA5159E03D40163F7C4F9B1706C08F3E41BD74E03D40B8F99001EA1706C0DE0D582888E03D403E2883FE9D1806C062FE65529EE03D40FD71B158791806C0F18A3B399EE03D4066D24B31AC1706C02F62E98DA4E03D4076D4E29D8D1606C014008860C1E03D40CCBD1B66C31506C0BDFDB968C8E03D4084AE8E2D151506C0594E42E90BE13D4040CD7FFE311306C0D630E82917E13D404729D7CA3A1206C01A434AFD06E13D40F4D9A61DA31106C07279628CFEE03D40546F0D6C951006C0BE5B6ACE0BE13D40BE1182FA3B1006C0296096D123E13D40B12083CB191006C039B12C3D3FE13D406043CB15390F06C00AF7CABC55E13D40191241E6140E06C09BFD26B968E13D40C7568627AA0D06C06770A5E892E13D4021020EA14A0D06C0FC15D79D93E13D4022E010AAD40C06C0011D41857EE13D40D6C4A7A55E0C06C06FE29AF16CE13D40860FDBCCD70B06C09FD0460955E13D4028CAEF7E260C06C079F06EC042E13D402EB4CE53780B06C0FFAC9E3825E13D40E004F060D50A06C0557B22991FE13D40419A56AFC70906C09D60A4BCFBE03D40A335ABE39D0806C05E55ACD0F6E03D40AF09698D410706C01E166A4DF3E03D40B7F93548770606C0C7139C55FAE03D40107DD2E4BD0506C0C474C69C0CE13D406F34362A260506C0EA549EE51EE13D407A2AF1CA3F0406C061F998B44EE13D40205D6C5A290406C044FB58C16FE13D407B08F4D3C90306C07F677BF486E13D40CC2F39A4070406C0E41E01929FE13D40BEC11726530506C008944DB9C2E13D40C43F6CE9D10406C0C0E2152DE5E13D401B070374040506C0CC608C4814E23D40C94B48B5990406C0989FC0E03FE23D4079967BDC120406C0E6C704A161E23D402C312A04CD0206C0DA4EA5B09CE23D40805E5DBAEE0206C014877D4CB5E23D40884E2A75240206C0B5DE6FB4E3E23D408C7C0338620206C0BFF451A115E33D40992EC4EA8F0006C0733CB02D4DE33D409FAC18AE0E0006C0AE7488C965E33D40F3D94B64300006C029ECA2E881E33D4042FBA24E8A0006C024E5380197E33D4037BB5A93130206C0201219B1AAE33D4030A990E1670206C043531B41CFE33D4035B56CAD2F0206C0FB6D991DF3E33D40F245D675030006C0A098ABD50DE43D40FBC9181F66FF05C0B4F0AB2814E43D400498E8E225FE05C0D35E8E684CE43D400B163DA6A4FD05C046729E5676E43D405F63F26151FC05C046729E5676E43D40AB0A687D25FB05C0208D542987E43D409DF17D71A9FA05C0878D0D38A6E43D402A96B61380FA05C04F858950B6E43D40D26F5F07CEF905C0BC783F6EBFE43D403D10B45DFCF805C0BEB8F9EBBAE43D40380C8BF61DF805C0E59D4319AAE43D4058B9612530F705C081CCCEA277E43D4058B9612530F705C0FF00101C3CE43D40BEFB3E77DDF605C08D7BF31B26E43D404D6C996780F605C09800570D1DE43D4028846973F7F405C0EAF648394EE43D40CD71135A7BF405C08A55287744E43D40058A58C4B0F305C0D91BC6EE4CE43D400B009F69D3F205C0D457A19284E43D4081278815C9F205C06BDCF698A3E43D40DABFFC5FBFF105C066D58CB1B8E43D409166875341F005C027CA94C5B3E43D404E19EE128BEE05C08DE9AE91C9E43D40ACA8667B99ED05C0A0ED878ED3E43D401D1EC2F869EC05C04A0B9755D8E43D401B8AF150CAEA05C014DA28A1AAE43D407F03498E2AE905C0C1AA7AF99DE43D408D93670355E805C00990FC1C7AE43D4094EFBECF5DE705C00990FC1C7AE43D40F6189B0C7DE605C09A9658198DE43D40FE0868C7B2E505C0F7DBE8ADCBE43D4056F88E75CCE405C01F589FBCD9E43D40B993E3A9A2E305C0242BBF0CC6E43D40203B14AA40E205C03214C149E4E43D407FA88F1BD9E005C055890D7107E53D40E165E14625E005C039BF171527E53D408D38AE9003E005C074C3A51941E53D4086BA59CD84E005C07E0DD29D71E53D40979AF342F0DE05C0D643EA2C69E53D40F72F5A91E2DD05C0AB5CA8FC6BE53D40FC85C37707DD05C08448861C5BE53D400A16E2EC31DC05C09231885979E53D40C7A64BB505DA05C01F65C405A0E53D40CFB81567B1D905C0ED0B8DCCC8E53D40D5A2F43B03D905C000FCF8F0D1E53D403C003D68D1D605C0E80423E5DDE53D403E062B4EB5D605C0A01FA1C101E63D403C94B256FED605C07031F5A919E63D403B223A5F47D705C06C921FF12BE63D40E8D209B2AFD605C0A796ADF545E63D40410C193499D605C05094DFFD4CE63D40F43A3D4A80D505C0E202D0285DE63D40535E2BA1BBD405C01968887489E63D400C2DA17197D305C06B63EC8497E63D40C0EF3A76ABD205C07E5358A9A0E63D4077B8C260A3D105C077E162A0C6E63D402FF3C24252D005C00B879455C7E63D40395508F53ECF05C0E5A6BC0CB5E63D403F3FE7C990CE05C0B98B3045B9E63D40ED832C0B26CE05C08AD1CEC4CFE63D404529C69EE2CD05C0710B4C5DE8E63D40707209D16BCD05C0C0023D79FDE63D4057314BF1A7CC05C02FFCE07CEAE63D4009EEF60FD8CB05C059AFD815E9E63D40BB88A53792CA05C059AFD815E9E63D4075EB90F69AC905C0A80B676D09E73D407AF76CC262C905C0A80B676D09E73D403A8EC4701AC705C042EC4CA1F3E63D4046F6F708EBC505C080C3FAF5F9E63D40F6402B3064C505C07CF0DAA50DE73D404B4C61EF0FC505C0224F37F526E73D40F3A6C75B53C505C0C645FF1543E73D40A519E65F26C505C06CA45B655CE73D400ABB287AE0C305C02892F9916CE73D40B76BF8CC48C305C02F641AF274E73D4036BD7ACB8BC205C03444BA449AE73D40782A3B585AC105C0FF4EA445C7E73D402DEDD45C6EC005C0F8DCAE3CEDE73D408320F6F873C005C0B12B77B00FE83D402ECBD765F8BF05C0D6D7046223E83D403E61E4C006BF05C03D2B69C537E83D40A34CB4F51DBD05C078978BF84EE83D405C87B4D7CCBB05C0EDD3F19881E83D40175C188E8CBA05C0E0264811BEE83D4078F17EDC7EB905C0C45C52B5DDE83D4074571B086EB905C012859675FFE83D40873DA363BDB705C04DBD6E1118E93D40E35AA3D414B705C0DFF714A529E93D4045847F1134B605C035C6980524E93D405486167E15B505C0EA4141295AE93D40CC796B05F2B105C0717500C45DE93D4052E5C5D33CB105C0D24A6B1D66E93D403B7DF3D198AF05C0CE774BCD79E93D40E99F3B1CB8AE05C0A25CBF057EE93D40FB134B8050AD05C086C613419CE93D401DFC694881AA05C06629FE94CFE93D40D5A2F43B03A905C0B31DF8BDF2E93D402F4E7CB5A3A805C02FC95C740DEA3D4088878B378DA805C07CBD569D30EA3D40EB22E06B63A705C039DF3E613FEA3D404BB846BA55A605C048FC8A355CEA3D4053A813758BA505C081BEE3CA8FEA3D40D31C0F6C4BA305C04959D0C5F0EA3D4018B5560FF3A005C0EE66FCB1FFEA3D4097587446EF9F05C097FA1E9A1EEB3D406DD800B6CD9E05C006932C712EEB3D40FFD59931BB9D05C0C653EAED2AEB3D40C194DCBCCC9B05C0DA77A0A932EB3D4084C5973F959905C08748F20126EB3D40DF4E22C2BF9805C0F775E09C11EB3D40EBD85251069805C061681AB913EB3D40FC4C62B59E9605C0887C3C9924EB3D40FEBEDAAC559605C007C776082DEB3D40AC0320EEEA9505C09CA0F2542CEB3D4062CCA7D8E29405C0C4E85ECC3BEB3D40251F6052219305C0446ADAC534EB3D408DA4935B499105C0BFE1F4E450EB3D4041672D605D9005C055EFBAC84EEB3D404ECF60F82D8F05C01211A38C5DEB3D40B06AB52C048E05C0A7EA1ED95CEB3D40BE665EB3018D05C0B73BB54478EB3D40C8C8A365EE8B05C0CA2B216981EB3D40850F8013658A05C0307F85CC95EB3D40EB44381C4C8905C03F688709B4EB3D404762388DA38805C0D47A64181CEC3D40D15D1267458405C0A48CB80034EC3D40DE59BBED428305C09CE678605BEC3D403B55BE67248205C001D2489572EC3D404BEBCAC2328105C0AACF7A9D79EC3D405A59EC40E77F05C08E05854199EC3D40C95C740D8E7D05C08F39CFD897EC3D4092770E65A87A05C0E13433E9A5EC3D40A3EB1DC9407905C01B39C1EDBFEC3D40B17B3C3E6B7805C0F558E9A4ADEC3D40109F2A95A67705C0F558E9A4ADEC3D4067B04E3A367705C06E9CB92CCBEC3D4075406DAF607605C091A97125F1EC3D40C8D92A77557605C0A4CD27E1F8EC3D4027FD18CE907505C0F0BA2308FCEC3D4036AD1402B97405C0D4BBD3F8E0EC3D408E10D4DF017405C095B0DB0CDCEC3D40F4458CE8E87205C0CBADFF290BED3D40D6E6FF55476E05C045251A4927ED3D40337678BE556D05C02E628ED431ED3D40E9AA8ABA206C05C0699A66704AED3D40F73AA92F4B6B05C011644EE152ED3D40043752B6486A05C0FD3F98254BED3D40178964236B6805C0E175A2C96AED3D4025ADF886C26705C021B5E44C6EED3D408CC0B398336605C01CAE7A6583ED3D40F3D36EAAA46405C030D230218BED3D4009C0E4EBD76205C092527534B3ED3D4023B836F9D26005C0531333B1AFED3D40E0FE12A7495F05C0FD44AF50B5ED3D4055A2EC2DE55C05C0FD44AF50B5ED3D40C539EAE8B85A05C0F83D4569CAED3D40D78BFC55DB5805C062307F85CCED3D4056911B8F635505C033761D05E3ED3D406A0B19D9DF5305C0ACB9ED8C00EE3D4075954968265305C0BBD639611DEE3D407707C25FDD5205C0A3DF635529EE3D407FF78E1A135205C0A413AEEC27EE3D4038C604EBEE5005C060CD018239EE3D4043BCBF8B085005C01D2334DD46EE3D40049DFF0D904E05C0493EC0A442EE3D406BB0BA1F014D05C039ED293927EE3D40CEDF8442044C05C01F526749DBED3D407E96427B504B05C0D15D6D20B8ED3D4059034AF9FF4705C0D0F5D8F1BAED3D40B6204A6A574705C04B6DF310D7ED3D4065D707A3A34605C04B6DF310D7ED3D40299C3814994405C06364C91CCBED3D40F506A925674205C0AC85A3F501EE3D405DAED925054105C07CCB417518EE3D40BAA9DC9FE63F05C0F23BF2AC49EE3D4021730A97FA3E05C0E353008C67EE3D406C1F97DD383E05C03DB1F3DB7CEE3D40C2A15CBF053E05C0B446F3B688EE3D4011D3742CA53D05C0095A379490EE3D4044801D49933C05C01865918197EE3D40171FB0BCF53B05C0F0C979B592EE3D402F015361C73A05C0BFA9FEF796EE3D400C83E8FF0B3A05C01BEB877DA7EE3D40BAC21616373905C0C0F8B369B6EE3D404D86E3F90C3805C05517F032C3EE3D404584DACAA63705C0DC798780E8EE3D406DC438245A3705C01AC1C6F5EFEE3D40CD83AA871E3705C0B96466B0F3EE3D40EC67565B563605C066136058FEEE3D40B623E6A3D63505C024B149230BEF3D40A4E430F3783505C0A2235E4315EF3D402CE400B0F03405C0766D6FB724EF3D402A85E574FE3205C07AF365B334EF3D40BF29AC54503105C056DE44E33FEF3D406E69DA6A7B3005C00671D4C045EF3D40EB85A7FC103005C04E3D772744EF3D4039ACBA58622F05C0EDE016E247EF3D40D9E7D610C12D05C0CACBF51153EF3D403B884270012D05C03192E2885AEF3D406D8C9DF0122C05C0582DFA545FEF3D40ED89647E242B05C01E6CB1DB67EF3D40B9CC446C0B2A05C02A93759373EF3D4070EF75AD182905C09D8026C286EF3D4078EC0CF8A12705C0682BE56B85EF3D40F491EFADA32505C0AE6F16E588EF3D400951BEA0852405C072CA373696EF3D40F00F00C1C12305C0FE47A643A7EF3D40FCD3FCE71F2305C004B23275B2EF3D4060556243DC2105C0EF3C4C56A0EF3D40D7F6764B722005C0F217699890EF3D4015C4E51D4C1F05C0904DF2237EEF3D4024A188A0C51D05C01E6CB1DB67EF3D402C15AF0D701C05C0C40EBE8B52EF3D4049B48938421B05C03227C34C36EF3D4094580861241A05C0DBCD42F119EF3D4037589302661905C0881C1142F6EE3D40C8DE9C5B321805C0429E1331DBEE3D404A3E1B5EB51705C0FA3207F7B9EE3D40A88306AC661705C05E4D9EB29AEE3D401EBBB0DAA11605C07F051F3988EE3D40B47CB83F721605C04078A3456CEE3D40821C9430D31605C04DA7D0D446EE3D402EEF607AB11605C01321BFC815EE3D40E3D6EE682D1605C08485EEEDF1ED3D40200BD121701405C0890AD5CDC5ED3D4038222933EF1205C0DFD8582EC0ED3D406B60504B181205C06140E54E9FED3D40AEC8F9AC7C1005C0E74A4E379AED3D4044C769E3E30E05C01DB00683C6ED3D402B082F0B370A05C084036BE6DAED3D40F2D24D62100805C0029CDEC5FBED3D40D5A59075820705C02A66CE4422EE3D4043A6214F370505C06466553A0EEE3D40A3CC0699640405C0B6DF354305EE3D408A833A9B440305C00E1A55E12AEE3D4075CDE49B6DFE04C00420EEEA55EE3D406F4D15E703FD04C08069AC5857EE3D4002959636C1FC04C0DFE1766858EE3D40986E6D3C33FC04C0A0D67E7C53EE3D407610E09975FB04C0DEA925C22BEE3D40ACE86A86AFFA04C0F6A0FBCD1FEE3D40B8729B15F6F904C03E8A84002AEE3D4049F9A46EC2F804C0DA8CD31055EE3D40952C27A1F4F504C0D9243FE257EE3D4078FF69B466F504C05B0A48FB1FEE3D40634914B58FF004C0573728AB33EE3D408B620333F0ED04C00E1A55E12AEE3D404BAFCDC64AEC04C07D957CEC2EEE3D4057A87F5A56EB04C074396FAD40EE3D40A59762580FE904C0DE2BA9C942EE3D404995720866E604C08DE612584AEE3D404435255987E304C0926F00474CEE3D4063CAD12D85E104C0CF90E0FC3CEE3D40A7A1FC93AEDF04C02054F3661EEE3D403B2EF4D25EDE04C0CF0E5DF525EE3D40C29C45949BDD04C0AAB706B64AEE3D400D41C4BC7DDC04C00669215C5CEE3D40C9224DBC03DC04C0A3B899C063EE3D40969526A5A0DB04C0A18C4C1B69EE3D40662BD43F2DDB04C097AEBBD463EE3D40F33977BB5EDA04C0444BC39558EE3D407BA8C87C9BD904C0D503E62153EE3D40586C38D153D904C065C405A051EE3D40323F92EDD7D804C0263218C858EE3D40284C73863DD804C023AAA6DA5DEE3D409FFAACD799D704C028FF493261EE3D40A65B1BCF0CD704C0ABB58BC45FEE3D409F1F46088FD604C0C8354FBF55EE3D4098E3704111D604C0FABFD99C39EE3D400AE5338FB2D404C05F7CD11E2FEE3D40F7AA3A504CD304C0363AE7A738EE3D402FBE688F17D204C0FEBEDAAC55EE3D407EA59828E7D004C04CC1BFAD60EE3D4003F1153834D004C012A452EC68EE3D40C320FAFF82CE04C08908FF2268EE3D40DD5F3DEE5BCD04C0AB764D486BEE3D40FD29FAE879CC04C0C6D33C9171EE3D408AC91B60E6CB04C048F718517FEE3D4017693DD752CB04C00206932C71EE3D407C0F971C77CA04C0B59C95FE6FEE3D40A9EF46301FCA04C08390871975EE3D40220038F6ECC904C02689CA2B7CEE3D40F1B2CB5C85C904C0508479347AEE3D409A35C2ECAFC804C0A465EE6B6EEE3D40C0AF912408C704C04CC1BFAD60EE3D40A9D83D1E9FC504C0EE201BED5DEE3D4064213A048EC404C01F9441FF4EEE3D4026F5AFF6C1C304C0480A760D44EE3D40C6032560CFC204C0FCA9F1D24DEE3D40A236BBFFD9C004C0181CCAF55BEE3D4057F43DD983BF04C07044F7AC6BEE3D40E4935F50F0BE04C004B463D982EE3D40401DEAD21ABE04C0F415A4198BEE3D406EC6C4419DBD04C06469F1738DEE3D40BFDDEDD522BD04C084296FEC7EEE3D400116F9F543BC04C061BB20C77BEE3D408F7E45C88ABB04C048F718517FEE3D4097A8836165B904C0E585CF317BEE3D40B8C777CEBBB604C0DA441CA155EE3D40411F76F05DB404C09FD8F96D3EEE3D40652F8095E8B104C09B3924B550EE3D40EA8A636E08AF04C01E577E7445EE3D40098F9147CBAC04C0A8B283A515EE3D402927350FBBAA04C069DBD5500FEE3D40A6704E362FA804C08DD31055F8ED3D40BC6C85FAA7A504C0C978EF03EBED3D407959130B7CA504C01EF8BDF2C5ED3D40F7D9B749FBA504C0F2B6D26BB3ED3D408A41BB9E43A404C03595EAA7A4ED3D407CB4386398A304C0544CEF2C8BED3D4038995D9C9DA104C0EAE0AAFC7CED3D4065790DB045A104C055151A8865ED3D409B41D7193AA104C0F2CA9AFD26ED3D408E507D31A59F04C03A5D71CC0DED3D4021A00C9FBE9F04C0DE077B6EFCEC3D40F9E010058E9F04C04569CAA9F8EC3D408406AC66E79E04C03F30911CFAEC3D409BB3999E0B9E04C0A4C8B5FC1BED3D4087FDF9111A9C04C0636F18BB33ED3D401C2B8C88BD9B04C0EEF77AAD3AED3D401CE9B1D2499B04C0156D33BA28ED3D400E1A55E12A9A04C0936BAFAA03ED3D4050453BB8859804C08847E2E5E9EC3D4011CFC8D6E99604C0A9836165D9EC3D402C7D8DC9879504C0DE4B2BCFCDEC3D40B2DBC24E0C9404C08F68A78BB9EC3D40A54E4013619304C0B7BEFEDAB0EC3D404FC7084DB79104C00CFA1C69BAEC3D40F4DC4257229004C0188BF040AEEC3D403BCF8E0A528F04C0D675036097EC3D40EF4F4E59F28D04C03E7B2E5393EC3D40DD1D6338E88C04C09131D2E693EC3D40D36FBAC0408B04C0D65D8F7868EC3D4074644051348A04C0A0B998B047EC3D401FD55526A18904C01252126D22EC3D40869E831DB58804C0ABFEAD090EEC3D404713398E698704C09CE16135F1EB3D4055A35703948604C099428C7C03EC3D40BF5076E9158504C0FF61A64819EC3D402380400C198404C0111EC8D523EC3D40F90269B5768104C0D47A64181CEC3D406C34CA445B7F04C06CF3B51D09EC3D40D503E621537E04C02D150AC7E2EB3D4034D53840D57D04C0929735B1C0EB3D409066D1E05B7D04C02771B1FDBFEB3D40F4739E0CE97B04C0D0A22D9DC5EB3D406049A8CFC47A04C0A95AC125B6EB3D402330D637307904C07EA7C98CB7EB3D4031C0F4AC5A7804C0667CA9E9C4EB3D403F501322857704C0538C3DC5BBEB3D40AB6FAAFFBD7504C089BDAB79E9EB3D40143FC6DCB57404C03D39549D1FEC3D40D9976C3CD87204C03832EAB534EC3D40597BE9375D7004C061AEA0C442EC3D400F666E19CB6F04C0AFD6E48464EC3D40670B08AD876F04C07B4963B48EEC3D402474E151746E04C0DC2DC901BBEC3D408915246C2E6D04C004763579CAEC3D40FDDAFAE93F6B04C0FBCFF5D8F1EC3D4087D62F7D546804C03ADBEDC4F6EC3D40F9951815826604C0A0C6BDF90DED3D406AC18BBE826404C00113B87537ED3D4037C071BE7D6204C0387870C163ED3D40A20106932C6104C03B1C5DA5BBED3D4060DC572FD05F04C01F526749DBED3D40BD8DCD8E545F04C02EA3FDB4F6ED3D4079F6A633415E04C042C7B370FEED3D40EBB58FCB6E5C04C09D9CA1B8E3ED3D406281542F645A04C037493D55CFED3D40F8E4BC5A495604C0F49E6FB0DCED3D406D3E09C7875404C0038871EDFAED3D40E7A331FF8D5204C069A78BB910EE3D40F32D628ED45104C038BF61A241EE3D409036E911FE5004C0267ACF3758EE3D40D76A0F7BA15004C0706FC8505FEE3D40D6E65A0FBA4F04C08373A14D69EE3D40082A99524D4F04C0C0D7C68E7CEE3D4078DBE09FAD4E04C0EC18B2158FEE3D40536A40CE454D04C0E438A6DD9EEE3D409B0DE83BAE4C04C065598BAABAEE3D4006CB225EE84B04C0D8857A55C2EE3D403C026ECCA14B04C05D797F72CAEE3D4064AC36FFAF4A04C0046B4194D4EE3D406D7F1DEED94904C0C5991540E7EE3D40830C2E67004904C0347568A219EF3D4059B44AFAC54604C0BA490C022BEF3D40508A56EE054604C0ECE291D332EF3D40A92AD956584504C0623BCE2335EF3D40A5F4F175BE4404C0A2ED98BA2BEF3D40075B913DE74304C0EB5D17D929EF3D402B5327FB424304C00D00B0952BEF3D4014A97A432B4104C0345310F230EF3D4071975874463F04C0E5A9584634EF3D404712CEB8BA3D04C058D647F13BEF3D40CEF4B7A9263D04C08A6FCDC243EF3D403AB2F2CB603C04C0E43C52335AEF3D40F2F917F7D53B04C0B9BAAD3E68EF3D409CD6B7DD153B04C07F411C3570EF3D40F13389D57A3A04C0A2E3B4F171EF3D40E2F5AADF963904C09245F5317AEF3D40B09D3935E13804C0311DDF837CEF3D4095568D6F393804C06FA35C7578EF3D407269A109B93704C0D47C3AC379EF3D405F37B6E8AE3604C0E1EEACDD76EF3D40B87361A4173504C0AEAE54617DEF3D40D3212697B53304C07435C35785EF3D40D60DCAEA0C3204C0142F055A70EF3D40C7BAB88D063004C0E6689FD854EF3D40567D53FDEF2D04C05DA9674128EF3D4062CB3C03342B04C0CAE7CBC122EF3D4066F9BA0CFF2904C093F6ABA525EF3D40E3642CE45B2904C07A97416B34EF3D40BB14B24EF02804C0230E7E8747EF3D40B620A523CA2804C0F1AEC44D57EF3D402BB92F0CFD2804C078E68AF788EF3D40199C7752152904C08CA9AA86B3EF3D4057E883656C2804C06C6D2B07C4EF3D40B16609D7482704C0F7B1DD98F9EF3D40BD18CA89762504C068565B5606F03D40D899E72DC32404C0B4D3C55C08F03D4067E03609942304C03AA869BC19F03D40CEA96400A82204C01FFE501F37F03D401D9C3EF1522004C0E4DDEC6A4DF03D4092617028D71F04C002CCC69459F03D40CCB96F10081F04C0A7576F7951F03D40B524F6BF121E04C029417FA147F03D40C64E78094E1D04C0B344679945F03D4090C01F7EFE1B04C0D2D9136E43F03D40C0102851E51A04C0EFE9454838F03D40931B45D61A1A04C07DC04D9022F03D40B26E72AE171904C06157EE601FF03D4026D2EC702A1804C0AB645B6155F03D40BE2DFDF0981604C0DB78663854F03D4001DD9733DB1504C0279AACF655F03D40448C32761D1504C0983E2AB462F03D40CA390C41691304C0D74922A067F03D4062687572861204C0E0E8746A78F03D400CE13DACDC1004C069E9656F84F03D40E60DE665030F04C0BD2484FD8DF03D408C3853D7900E04C0D0285DFA97F03D40910A630B410E04C0A1F5F065A2F03D40BDF7DCAE3C0D04C04A6C2D82B5F03D40709A99F4520C04C05EF58079C8F03D405553ED2EAB0B04C0FBA01D26D0F03D402D0373993F0B04C0C5347804DCF03D40858EC29A6F0904C0E19DD733DFF03D40ADC909C9B80804C0B94780E4E7F03D4037291609B70604C03A8ABD61ECF03D40D95F764F1E0604C06F302F1BF8F03D40C27B58B9610504C0FEA325451AF13D406B161EEA2D0404C09EB5DB2E34F13D402CC2A8FF070304C0B95510035DF13D40033054B4280204C0B482F0B270F13D4077442A31740104C00D912E9166F13D40E8154F3DD20004C049360D4059F13D406ED85B6F400004C08AE8D7D64FF13D405AE0E12CDBFD03C0A2A06F664FF13D404D535FF12FFD03C0F907DB9953F13D40FBD467BDCEFC03C001C68E215BF13D408248D0A975FC03C0905BDDFB65F13D40BD73284355FC03C0A31B06989EF13D40C1255BB807FC03C0FB63100EACF13D40472A42A0E9FB03C0196A5E1FE7F13D40BABA63B14DFA03C0029365D2F0F13D40105A0F5F26FA03C02D0B26FE28F23D406F92301D84FA03C0809F71E140F23D400351418F62FA03C00674154152F23D40A0B591A119FA03C0E9E85D6166F23D40B2906FA58DF903C04AEF1B5F7BF23D40A0E238F06AF903C0A215CE24A0F23D40028BB26D07F903C0DAFF006BD5F23D408038FEFAD7F803C0AE10566309F33D4041AA17320DF903C0267387A81DF33D406AD894D002F903C02C1D627259F33D40E74306A85FF803C0DEF824C165F33D401D7B511619F803C0A47F93B76DF33D40768A558330F703C0FE4C182884F33D4035B22B2D23F503C0F66C0CF093F33D408ACD226F14F403C0ADDA3521ADF33D400303AD65D7F203C0C5707500C4F33D40E70AA5E5F6F103C0F88E75CCD4F33D40EFDD8BD420F103C0CBE08332E8F33D4013526D2695EF03C0E41D96BCF0F33D406840BD1935EF03C02FF42D1801F43D40FB4D72D1DAEE03C04AB6BA9C12F43D402C222F10A9EE03C0B5E0455F41F43D401B74F85A86EE03C0BE2374E151F43D40C14FC1864CEE03C0DA82EF479BF43D408A3DB48F15EC03C0961F5D11A1F43D405D8AABCABEEB03C06EC905C2A9F43D405340DAFF00EB03C01D797B6BBBF43D40C8050C3785EA03C0647CF376DFF43D40D19E81470AEB03C09ED32CD0EEF43D401EBAEA4B80EB03C019BECF02FEF43D40E31F11AEDBEB03C042F053B021F53D40AAA0FDA3CAEB03C0B916D28668F53D40AFACD96F92EB03C076BA3D528EF53D405C829DAC62EB03C068098DBB9CF53D40618E79782AEB03C02E0906C6B0F53D40C042E6CAA0EA03C0CB89D1CEC4F53D401DF45B2A25EA03C0F6741A0EF0F53D407D3F355EBAE903C0451960D50FF63D40F8E69205A7E903C0B4E2C0502CF63D40989BB9D111EA03C06E991DF34BF63D40911D650E93EA03C02CF3565D87F63D40E75086AA98EA03C01FC429CEACF63D40EC5C627660EA03C0672F3608CEF63D401C261AA4E0E903C0FDBE7FF3E2F63D40A831C62C20E903C09EC838EBF8F63D4067A08DB7F0E703C051A4FB3905F73D407CD3F4D901E703C043BF000C15F73D4013FA4FEDC2E503C03A63F3CC26F73D40BFCC1C37A1E503C0410E4A9869F73D40F12C414640E503C03D3B2A487DF73D4075B7A1180FE503C0BFA6BD778AF73D4050E4A48BA8E403C0718280C696F73D40FF2BE1BF2FE403C0760B6EB598F73D4063809FCC9AE303C0CD8FBFB4A8F73D40CF55A98F76E203C08F3A9567B9F73D40B10001C68EE103C0A91B83A9C1F73D40E5D594BFD6E003C03F29498DBFF73D401D458C8D2FE003C0FB9463B2B8F73D40A95038166FDF03C042C81E46ADF73D408D237B29E1DE03C0C5E57805A2F73D40C0F5173037DE03C0EA0F83AF9EF73D40A6CE48298DDD03C0FE33396BA6F73D40DC3A4904F4DC03C00E85CFD6C1F73D4002733E86D8DA03C0A91B83A9C1F73D40693C6C7DECD903C03F29498DBFF73D40291AB50C82D803C0DDE05520C4F73D4039414063CBD703C0EDAF6884C8F73D40146E43D664D703C081559A39C9F73D40A00A6E5ADFD603C07D00F7E1C5F73D40F81E89F260D603C0001E51A1BAF73D402F8B89CDC7D503C0502F9D7BB4F73D403BA92F4B3BD503C05163E712B3F73D40ECF65965A6D403C0F5D72B2CB8F73D40FAA875D146D403C05219B5B1C8F73D4051BA9976D6D303C0CFFB5AF2D3F73D40093C8B3963D303C0A99D06B1D8F73D40E6DA06A4B3D203C021F7BE02E2F73D40C716DDD506D203C023E1D638F6F73D407F07509D58D103C036D1425DFFF73D4066E37789A0D003C06BCA5F6B00F83D4076768DF1BCCF03C0FD8282F7FAF73D40887E124482CE03C0EEE7B92AF5F73D4049F3C7B436CD03C0309287CFE7F73D40805FC88F9DCC03C0FAE29C22D1F73D403BEA9E2B00CC03C06A108BBDBCF73D401514ABABA7CB03C0DE921CB0ABF73D40BF4F0B1467CB03C0FE33396BA6F73D40A01C6041F5CA03C0CFF753E3A5F73D40044C2A64F8C903C01F09A0BD9FF73D401A828879FBC803C0B08D78B29BF73D402306CB225EC803C06D2DDD6E93F73D40B217659181C703C0AB8207BC82F73D409AF68370BBC603C028E329F576F73D4080AAC07FA9C503C0C379DDC776F73D400F50D0DCF9C403C00B6366FA80F73D40ED82C13577C403C0E6385C5084F73D4026F5AFF6C1C303C08524B37A87F73D40B394D16D2EC303C05ABFF451A1F73D40F1F09E03CBC103C039EA8DFFA7F73D408093B76D29C103C0DC2A8881AEF73D40BF83FAF1F2BF03C0B27790E8AFF73D40C70446A863BF03C01C6ACA04B2F73D402FD16A9269BE03C0C69B46A4B7F73D40BD0402F802BE03C0B64EB747CAF73D4069465046A6BD03C0695EC42DD5F73D40F1D995F14ABD03C0B96C1915DDF73D40CD069964E4BC03C088C89F5EDFF73D40AED3ED9172BC03C05858703FE0F73D406939D0436DBB03C0A869BC19DAF73D40FDEABB6C85BA03C053CF8250DEF73D40611A868F88B903C0B3AFE18EDCF73D4074B680D07AB803C03AA462ADEBF73D4006627E13AFB703C01A0346F2F0F73D403325485B4BB703C0231521D0F4F73D40EF1EA0FB72B603C0CD1253D8FBF73D407EC4AF58C3B503C0AF5E454607F83D40650C6256DEB403C05DE5643D10F83D404C570B47EBB303C006E3964517F83D40A66E1DD25EB303C0079964E42CF83D402F055A70F5B203C08FC12F9A3AF83D40E27A6F67BAB203C04CE3175E49F83D406268757286B203C05F07CE1951F83D403D26F7E05AB203C034EC415255F83D40402FDCB930B203C09E12C60556F83D40EF0797E9F2B103C0E72F99CF5EF83D4020D4450A65B103C0304D6C9967F83D402AECFDA1F4B003C03FE834666DF83D4060E97C7896B003C07836F5CB71F83D40B9910DFF44B003C0D94A9EA16EF83D400F31B9AC1DB003C07D09151C5EF83D4095C10765D0AF03C0E679CB3049F83D40226129DC3CAF03C0207A522635F83D40D84BAEBDAAAE03C0FEEE1D3526F83D400A1B54D10EAE03C01129BCBA19F83D406DDB9CEF4CAD03C0E58BACEB06F83D4053B4CDE8A2AC03C036D1425DFFF73D40089C5BD71EAC03C036D1425DFFF73D40BBEC7CE47BAB03C096B1A19BFDF73D4068C240214CAB03C022AD8CA1F7F73D40EB49AA0029AB03C0A041F971EAF73D4075745C8DECAA03C05858703FE0F73D407FFB952941AA03C0AAD15048D7F73D40B5D320167BA903C06EE7B11CD7F73D40A0B82DDB98A803C0A0BF756AD3F73D4085FDE8E5C1A703C0E31F11AEDBF73D4090F3A386DBA603C061360186E5F73D407FE48C17C1A503C0364FBF55E8F73D40E6ADBA0ED5A403C0D0B12891E9F73D40A2139DC0CFA303C0A1754309E9F73D40B037312427A303C0F743C769E3F73D409376FE486CA203C073709F0BD9F73D406F1283C0CAA103C03E7782FDD7F73D40FAAEAD4445A103C088C89F5EDFF73D408FCF1A7222A003C0222B099AE0F73D40F08975AA7C9F03C0B838CF7DDEF73D407EBD0C10169F03C0097E65EFD6F73D40A5710399F89E03C0332D5679A7F73D407D299721E99E03C06E61270692F73D40D6D127A8979E03C00B6366FA80F73D400FD594641D9E03C0843A9B4473F73D40CB3D6E090A9D03C0A5434C2E6BF73D40893ABD9C239C03C062AF665364F73D40103A8D599B9B03C0E955AE015BF73D401DECA8C53B9B03C027778EB74BF73D404F4CCDD4DA9A03C0DA38622D3EF73D402B7CC73A669A03C057653ACF33F73D403606F8C9AC9903C0C614AC7136F73D40ED5FFEAFDF9803C09BF91FAA3AF73D400327DBC01D9803C0AAC8320E3FF73D405C3EED4B919703C057E7BDD64AF73D40974748EEFA9603C0199293895BF73D4074E6C3584B9603C0248E869D73F73D40FC790904F09503C0327788DA91F73D408B8BA372139503C0B1C1C2499AF73D409E4C929D6D9403C0FE33396BA6F73D40088B2F7F2A9303C01258EF26AEF73D409599D2FA5B9203C057201F99B3F73D404A8160E9D79103C0C11259B5B5F73D4026B15A4F639103C0BB55212FB5F73D40B4E4F1B4FC9003C047510C35AFF73D4065C39ACAA29003C0A35A441493F73D401E238F96B98F03C0502B966C86F73D405186AA984A8F03C032C1BA3B7CF73D403965C977848E03C0EE601FF873F73D40C3D9081FA58D03C0801942846EF73D405F9DBEF9688C03C037FC6EBA65F73D406EC1525DC08B03C0DF43D32357F73D402ABBAAFDE78A03C0E3E2A8DC44F73D40DF11BAF0288A03C013D154F42CF73D40BD44AB49A68903C0956C2BAC0AF73D404DEDB199E88803C094821376F6F63D400AE7093A108803C06490604FE0F63D40690DEF833D8703C025856863DBF63D40289EB305848603C0BEB38707DEF63D4083B8BC83E98503C085E74AA9F0F63D403937B753848503C0D816F950FDF63D40C3CDF3F11A8503C0B74192FE03F73D401D797B6BBB8403C0C01F234509F73D4022F4D83B488403C065C8B1F50CF73D4030A6F4A7E88303C05BEA20AF07F73D40DBE44B039A8303C0D7602BB2E7F63D40C6C958C8B78203C03C48F4D7D0F63D4053697A3F248203C049772167ABF63D40F898FEDB1B8203C0B2B38DE497F63D402E05FFB6828103C035D1E7A38CF63D4095CE2CAE968003C0E2A139FC7FF63D40B68BC45FEE7E03C0D9C3A8B57AF63D40995E0773607E03C02580513871F63D40270120DDBE7D03C077F9314168F63D40831E204E167D03C0AD2699E667F63D40BD2484FD8D7C03C04D463AA869F63D4043215DC7137C03C02A093F2773F63D40A7751BD47E7B03C09984663277F63D40ADF078A40B7B03C0C9F4955176F63D40E1568B998E7A03C0BB8D171C6FF63D4014BAA69B1F7A03C00D07F82466F63D407D1A4174527903C09A362DC25EF63D408FB63BB5447803C08612770657F63D406CE6351BD07703C0D723C3E050F63D404B1C1E673F7703C0D7570D784FF63D40AAD381ACA77603C0D7570D784FF63D409BC761307F7503C00D85741D4FF63D40FE87AA4EBD7403C051195AF855F63D40DA4823B0837403C0AC3C81B053F63D40B97E0BFCF27303C0386CB64D4CF63D403F0C63C1B37303C089B14CBF44F63D4019366F415B7303C055EC794842F63D40B66B9D13D67103C01B6A6F4B3FF63D40430BBF8A427103C0989647ED34F63D40FF04172B6A7003C0281B20E230F63D403A7AFCDEA66F03C01F0945042DF63D401C284B08B16E03C07BC84A8226F63D4080E89326EF6D03C0CC0DE1F31EF63D402EC14E56B16D03C019CA897615F63D4034D021156B6D03C0AF0B9AF111F63D40BC6367C00F6D03C04F2B3BB313F63D40F0C979B5926C03C09C9DB1D41FF63D40A6457D923B6C03C0B0C1679027F63D402DD6CB4AEE6B03C01F3D8F9B2BF63D402B3F5F69CF6B03C084A6DBC82BF63D40DE23F664596B03C0D6EB713A24F63D401E392DD38A6A03C0DBA8A9C024F63D40A538FD8F026A03C04AF086342AF63D40D99E0F85856903C0B4E2C0502CF63D4009F945AE406903C07A9400EB27F63D40B8D4F7D0F46803C05809CCF918F63D4015866D30796803C061318FA108F63D40A83462669F6703C0238E2BE400F63D4004E3E0D2316703C08D80650003F63D406E46729E566603C0281719D302F63D40229A8A9EA56503C05CA8A1B206F63D4059068B790C6503C0A5F9BE130EF63D4010858549A76403C0F36B35351AF63D406A300DC3476403C0294F6A792FF63D40C0CFB870206403C0461D67E43DF63D4021AF0793E26303C0C33357BC47F63D40F83EB03E796303C00373993F4BF63D40035A5FC9FA6203C0C33357BC47F63D40B9D85999956203C063D1747632F63D4078472124666103C0FD7D10131EF63D40085CB2857B6003C0F39F7FCC18F63D40E92807B3096003C08979FB1818F63D40FFCAEFD9DF5E03C0F36B35351AF63D406C37667EDA5D03C0D162844B22F63D40466172FE815D03C0A1F2542C23F63D40F63C2421365D03C0621BA7D71CF63D4055850662D95C03C0DF477F7912F63D40DF8AC404355C03C091A1BEC007F63D40EDAB61759A5B03C05DDCEB4905F63D4023186250015B03C09B9F2CC60AF63D4046915ACDDF5903C00A2FC1A90FF63D4055FF7B4B945803C0534C947318F63D406EAA494B405703C08845B18119F63D4052EC0D63775603C0C2C7BB7E1CF63D4014F538C2585503C001D3B36A21F63D40A8A624EB705403C04567994528F63D4007CA1242AC5303C032F9B02836F63D4015EBAFB2115303C04F2F42C241F63D4051F701486D5203C05ECA0A8F47F63D40003F3E7CF45103C0299DA3E947F63D40BA32A836385103C08F6E844545F63D40EB92CC45D75003C076C1E09A3BF63D407D3ECA880B5003C0F4554D6B2EF63D40E7A15B54304F03C0E03197AF26F63D401E0E5C2F974E03C0023B48991EF63D407CC2C8810D4E03C006DA1D520CF63D408649021E624D03C077C0D07EFFF53D4061E28FA2CE4C03C07E2FCF94E7F53D40ADA6EB89AE4B03C03F582140E1F53D40BB360AFFD84A03C09626A5A0DBF53D404B4B9B60EE4903C0C6CA1E57D9F53D408DD541B9C84803C0B7FB0BF3D4F53D40259012BBB64703C073672618CEF53D40E38C614ED04603C0FF62111EC8F53D40C5CB2E73154603C01BAF8A81BFF53D4050D4E308634503C09ECCE440B4F53D40B9347EE1954403C0FA57A027AFF53D4020FEABD8A94303C01B2D077AA8F53D4036340AEEAC4203C07B0D66B8A6F53D40F02774A8F04103C0D1370E61A1F53D400089CB96514103C063940CA59BF53D40B73C7BD3994003C0859DBD8E93F53D40155D7237E33F03C09D94939A87F53D408BDEA9807B3E03C0754C272378F53D409F0E1AB09A3D03C0DFBCDD3763F53D405902846ADE3C03C0DED2C5014FF53D40E61027E60F3C03C005172B6A30F53D402704615DCB3A03C09516896423F53D4067C2E567D93903C009EA4ABA1CF53D4072A3C85A433903C02F489FFB17F53D40D6F78667AE3803C0B1FD648C0FF53D40E17EC003033803C0BA5972CBFDF43D4021037976F93603C03C77CC8AF2F43D405A06E6327F3603C071EE6591DCF43D40EF95D464213503C014799274CDF43D40E28FA2CEDC3303C0AF8DC23FB6F43D40024A438D423203C0D0622992AFF43D403BBC314E8D3103C0134141CEA0F43D40D882DE1B433003C05753A3A190F43D406A9D5D633C2F03C00D02864089F43D40272B2BF2902E03C0BF5BC5877EF43D40B8B1344B5D2D03C0DD0FD31973F43D40580F4EFA312C03C0F95B4C7D6AF43D4079CFDC9E7B2A03C0CD40C0B56EF43D40B241CB5FC62903C06D60617770F43D404C6E14596B2803C03E247CEF6FF43D4058F844E8B12703C0A7E26B7473F43D409370218FE02603C0B7B17ED877F43D405C881FAD7B2503C0CE740A4D6DF43D4039D384ED272303C0E0AEA8D260F43D40ADBD4F55A12103C0ACE9D55B5EF43D4013848659C32003C01C9947FE60F43D40FE68931EE11F03C08B146F0965F43D40B7C887EAF71E03C05BA43FEA65F43D401714611E8D1E03C0BA50549165F43D4079623145141E03C0B03E79B361F43D4057981991831D03C0D37B743458F43D40E12E562F1A1D03C04A1F5FE74BF43D40182FCCF8AD1C03C0679F22E241F43D407143E7902F1C03C0E988320A38F43D408507CDAE7B1B03C0A06B5F402FF43D408F8E064BD01A03C057BE1D9727F43D40A2E07371651A03C0A0E9DB3818F43D40D2A92B9FE51903C04EEE77280AF43D4026B26A6B9F1903C02672C119FCF33D40336486D73F1903C0B85E2E3DF5F33D40633035F8B11803C009A4C4AEEDF33D40CA8AE1EA001803C08B8DD4D6E3F33D4029424530691703C09060F426D0F33D403E062B4EB51603C0DC1C9DA9C6F33D406ECFE27B351603C02FCAC749BCF33D40A844C82F721503C01FFBB4E5B7F33D4037EAD78CC21403C094675E0EBBF33D409B3E96992D1403C077B3507CC6F33D400208C490411303C0256EBA0ACEF33D40EDECD0555F1203C0D33E0C63C1F33D407C92E0B2AF1103C0F99C60A4BCF33D40820D3E833C1103C0B5087BC9B5F33D40634914B58F1003C043FC68DDABF33D408DFEE1F8EB0F03C0937D46D8A6F33D40A6643909A50F03C0501DAB949EF33D40D4BB783F6E0F03C08E72D5E18DF33D402E6700B90E0F03C0BD2C376277F33D4066D6F786670E03C0CB11329067F33D40A5E83702A70D03C0180225AA5CF33D4033FAD170CA0C03C05C14877D4CF33D40F88FE0A1830903C083A6255646F33D40DC6223B5F50803C09EBE54223FF33D408E44C3BD8D0803C063867C8626F33D40C647307A130803C00A01AF850FF33D404BCD7921780703C0B0726891EDF23D40647D5E4C8E0603C00A140C42D4F23D40709B04CA010603C030BC92E4B9F23D40F79AD486790503C0445EC598AAF23D40D6613BCE230503C0901A6E1BA1F23D400AC84DC3A60403C05B21510DA0F23D40742BDF8ECB0303C0F6B704E09FF23D40AD9DCD4F160303C05243C0C69AF23D406FA6F8AEF70103C0D3F8855792F23D40FB422333720103C090CC34AB88F23D40065ED2BDF30003C059B5B5CF74F23D408C5AAB87790003C063F7D33549F23D404D60DFF368FF02C00AE7093A10F23D400CE47E2CE9FD02C0269D92BDEFF13D40CC6262F371FD02C0609D19B3DBF13D40D408A2934AFD02C0787709F3C3F13D40E777F585EBFC02C0376B9505B8F13D408FFF024180FC02C0B988EFC4ACF13D40713ED065C5FB02C076285481A4F13D40110B6B195FFA02C02D0B81B79BF13D40FED071DAF8F802C03BBC314E8DF13D40375BD48272F602C09CD0DA238AF13D404C8E3BA583F502C0449AC29492F13D404E03AB8F2CF502C037E911FEA0F13D40581B6327BCF402C0541FA397ACF13D4013A342D02CF402C0F3D6AF2AB1F13D40D09F916346F302C0FD1CD59FB3F13D408F9FD7E951F202C0BEDD921CB0F13D40F76BFCD357F102C0B5334C6DA9F13D40B2F6D26FBAF002C02DD736209DF13D40BD7D0C0C0FF002C0B9D2212697F13D4023D8B8FE5DEF02C04FE0E70995F13D40DECE19AC93EE02C02D0B81B79BF13D4097BF8C73E5ED02C0D20149D8B7F13D40FC85C37707ED02C0B0F897EEBFF13D405FD78A9180EC02C0030B60CAC0F13D4062122EE411EC02C0C823B891B2F13D40ECFDA1F4E0E902C0290417D0B0F13D40A888789043E902C0772C5B90D2F13D405FE5756968E802C07D9F60B5E8F13D40125B8B602DE802C0439FD9BFFCF13D40BA217CDE43E802C0B404190115F23D406D9791D508E802C0A61F1ED324F23D4074A95B87B4E702C0983A23A534F23D4031A6AA1ACEE602C001F9122A38F23D40986FD811E2E502C00C3F389F3AF23D40084EC7AD93E402C0C7F88B344CF23D40C03E3A75E5E302C00D77894567F23D407CC9101148E302C07017AB178DF23D4020D9B7EE41E302C0B7E0568B99F23D40299FD44D18E302C060AA3EFCA1F23D40DA7D7D63BEE202C0CA9C7818A4F23D40F3B9C95EA5E102C013EE9579ABF23D4068380AB54BE002C0DEF4786BAAF23D40CEFE40B96DDF02C0B3D9ECA3AEF23D40315008D3E6DE02C0D0DB33A6BBF23D40919BE1067CDE02C061E28FA2CEF23D40E9407B9A38DE02C03B3A0900E9F23D409A1F24B0DEDD02C02E895869F7F23D4048F8DEDFA0DD02C089624DC00AF33D4058B0E83125DD02C0B0766FA01BF33D400D07F82466DC02C0E2D0B6F52EF33D4021C8E64FC0DB02C039D78BFC55F33D40807F4A9528DB02C01E41E03774F33D40E64581994ADA02C008B6A569ABF33D4016BB229D26D802C02FCAC749BCF33D40C99FB998B0D702C0D3F4D901D7F33D408121AB5B3DD702C0CF21BAB1EAF33D408E3F51D9B0D602C078EBA122F3F33D404630C4A002D602C0B72AE4A5F6F33D40B39943520BD502C0615C6045FCF33D40657BE35AA3D402C06DE8C13813F43D40B50E588341D302C02C674A4629F43D409B6BE22E0CD202C04A9DDBDF34F43D400FE72B92C0D002C0BD6DA6423CF43D407A4DB450D7CF02C0BB021B214DF43D40E219D93ADDCE02C015A8C5E061F43D404E8061F9F3CD02C0C4E4B27680F43D40A728F27FA2CD02C0BD72BD6DA6F43D40046BE6DA61CD02C06169858EC2F43D400D839E72F1CC02C01E8B6D52D1F43D40159568249DCC02C0DBAC5516E0F43D40C5DF9B4B16CC02C0CDFBA47FEEF43D40D50627A25FCB02C04B129557F8F43D409191FD3DC2CA02C08A1D8D43FDF43D40A3BE767AEFC902C04BDE4AC0F9F43D400882B68B1FC902C016197849F7F43D401EB814A122C802C0C9F43A9803F53D40D6A8876874C702C0A7EB89AE0BF53D40EC6F6479B2C602C07C3892150DF53D40574277499CC502C01C5833D70EF53D4083AE3374A2C302C03DF94F9209F53D40954737C2A2C202C0EAFDEB81FBF43D4000AEBF80B9C102C0EE9CC13AE9F43D40C24769DBD5C002C0D19A7A38DCF43D40D3DD7536E4BF02C0A6E7829FDDF43D403F47F5E7ECBE02C0E5BE30F4E3F43D40A39BB3F457BE02C07A98AC40E3F43D40598638D6C5BD02C0758D3B4ACAF43D4085EFFD0DDABB02C013848659C3F43D407AD7EABCD7BA02C08DB85BEDBCF43D40561CBDD012BA02C0C6504EB4ABF43D40B8D90EFC5EB902C0B6FFB74890F43D4028BBF48A02B802C0AD2127028BF43D403DEE5BAD13B702C0823AE5D18DF43D40B066AE1DD6B502C01848ABB58BF43D401A36CAFACDB402C0D93CB3C986F43D40DED5613BCEB302C04463377D87F43D404839F306F3B202C05C5E1498A9F43D40B9AB5791D1B102C084A6800FB9F43D40BF29AC5450B102C03682435EC5F43D40227B736EC9B002C014799274CDF43D4087D2286E26B002C04AA6F919CDF43D40E58695C09CAF02C0E0B3BFFDCAF43D40A61D4828C7AE02C0CB5BBFAAC4F43D406217A0C8EEAD02C05757AAB0BEF43D40C26279FC83AD02C00F6E217EB4F43D40221ADD41ECAC02C031AB1CFFAAF43D40E1AD98B624AC02C06670EF75ADF43D409604A8A965AB02C0D0622992AFF43D40036E275B6EAA02C0D1967329AEF43D40C26D6DE179A902C06670EF75ADF43D40EDD632198EA702C01D531CACA4F43D40583DBBD7A4A602C014758B659FF43D406C6D2B07C4A502C0B22C98F8A3F43D40D6D0BCD2E8A402C0653CA5DEAEF43D4043A9BD88B6A302C00835E8F0B5F43D404753E2DF0CA302C0D8857A55C2F43D406E3C8ED59CA202C0A6B33808DFF43D400CC1BBA651A202C029D599D601F53D4074B2D47ABFA102C02B757FAB2BF53D40874ECFBBB1A002C0B77471C053F53D40F1B16087D69F02C051C9EF236DF53D40673DC626439F02C0EED9290C80F53D4010FD7F41779E02C0 Akanyaru river \N \N \N \N \N \N \N \N 0 29698243 0105000020E6100000010000000102000000BF020000131736B9CE773D40AB8C90CBC9150AC0FE11E1BABD773D404B9B60EE49160AC087904EB8B2773D409B481FA974160AC034A95780A5773D40EAF5DD639F160AC02A32DF6696773D40B1EA6289BD160AC09A6846F58C773D40825660C8EA160AC0266431FB86773D409C7521B138170AC0D84AE82E89773D407C8967BFA4170AC03B1856968D773D4096A828A8F2170AC038EC08F192773D406E0E21F829180AC07CC8A53B99773D40BDBBDFB254180AC09E36F4609C773D401C11E73CBE180AC06B91FEA897773D402F36AD1402190AC027B5615E91773D4048556EFD4F190AC0266431FB86773D40D10DA8925C190AC05D4CD82379773D400F4AF3226E190AC01B9C887E6D773D40696E2AF7A7190AC05501ADAF64773D4033E02C25CB190AC0D66EBBD05C773D407F106ED7F0190AC0BF3D192D51773D4053F9E81E231A0AC09C7E9AA443773D40661EAFF6661A0AC05425805138773D40794375CEAA1A0AC0C45BE7DF2E773D40A8D7778F7D1A0AC074BD231928773D40379F84E3431A0AC0F672E9A91F773D40E151746E241A0AC0A4BF3C0911773D408AC4A97B091A0AC050148D5A06773D40DE7D4402FC190AC02A90D959F4763D40DDE9CE13CF190AC0037CB779E3763D40865C0421B4190AC085C1EBE9D9763D40DF9513A39D190AC0330E3F49CB763D40E269430FC6190AC0B55373B9C1763D4087F0790FE1190AC03799A729B8763D40E3FDB8FDF2190AC0E5E5FA88A9763D408C0464F9041A0AC0BC218D0A9C763D40E1BDFE7FF7190AC06B4606B98B763D40E269430FC6190AC06EF9484A7A763D40334FAE2990190AC04875954968763D40DE019EB470190AC020D1048A58763D4082F45EC65E190AC0CED5A0794A763D40DE019EB470190AC0A6E9584A3B763D40310FF4AB94190AC080AD5CB928763D408C70EE0AD8190AC059993AD917763D408FD893652D1A0AC0DA0649FA0F763D40906C09545A1A0AC0877B760A03763D408FC0C4C48B1A0AC00A99D0C9F7753D40E98D4935A21A0AC0613F7A79F0753D40401B1428BD1A0AC00FFC5EF9E2753D40E7E104A6D31A0AC065CAE259DD753D40E7E104A6D31A0AC01387C7D9CF753D40401B1428BD1A0AC0BF6B860AC4753D40401B1428BD1A0AC043194FA9B7753D40E98D4935A21A0AC09A7741E9B0753D4091007F42871A0AC047A4B789A4753D408CEC9458631A0AC0C9C1114999753D408B581F6A361A0AC0A1455B3A8B753D408F441E77001A0AC07AE981EA7A753D4087F0790FE1190AC027A6666A6D753D403623DE95B8190AC0A9EB9ADA63753D4082F45EC65E190AC057F036CA55753D40854CE8E4FB180AC0DA0D91894A753D40D63153FFC5180AC006CDF80846753D402D2BA803B4180AC0B26900CA3A753D4081E4428AA6180AC034677DCA31753D402BEBED85B8180AC0B784D78926753D40D4F19881CA180AC039CA0BFA1C753D408278B878D3180AC0E686F0790F753D402D2BA803B4180AC094FB1D8A02753D40D089F32675180AC0154152FAF8743D40D475F2333F180AC041704B9AF5743D40CE61084A1B180AC09816F549EE743D4077D43D5700180AC01AA4E029E4743D4077D43D5700180AC0714A8AD9DC743D407F28E2BE1F180AC09DC13AE9D8743D407B3CE3B155180AC0C8F03389D5743D40D8DD978E94180AC0F41F2D29D2743D402BEBED85B8180AC076AD1809C8743D402B7F6374E5180AC0F83A04E9BD743D402EBF1DF2E0180AC07A3881E9B4743D407D64CE8EAF180AC0FB35FEE9AB743D40D089F32675180AC07D7B325AA2743D40282F8DBA31180AC0FFC066CA98743D407914F8D4FB170AC0D8648D7A88743D40788082E6CE170AC033260EEA6C743D4075AC527AA6170AC0632827DA55743D40CEE561FC8F170AC011757A3947743D40CEE561FC8F170AC0930266193D743D40CA65ED0099170AC0410702092F743D40CAF962EFC5170AC0198B4BFA20743D40D00D4DD9E9170AC09C38149914743D407C54B252F7170AC0CAD2985A00743D4024C7E75FDC170AC0A3BE767AEF733D40CDCD925BEE170AC0FB84FDE8E5733D40255B5D4E09180AC07D827AE9DC733D40279B17CC04180AC0D30847DAD7733D40D24D0757E5170AC02B3F5F69CF733D40205F420587170AC0AD8493D9C5733D40217711A628170AC05A417859B8733D401D0F6C4BD3160AC0332D5679A7733D406B20A7F974160AC0E01115AA9B733D406A8C310B48160AC08D8642BA8E733D406C38769A16160AC0667220DA7D733D40BADD2637E5150AC0403624496B733D401657F036CA150AC043311E4A59733D401657F036CA150AC01C8D8D8A49733D40BF5D9B32DC150AC0CB694FC939733D401813510251160AC04DAF833930733D4071C806D2C5160AC050AA7D3A1E733D40C83D02243F170AC0FEF6D0990F733D40205F420587170AC0ABDB8FCA03733D4075AC527AA6170AC02ED10FD9F6723D4026732CEFAA170AC0065559CAE8723D4023337271AF170AC0E0185D39D6723D401E1F88878B170AC0B9043B59C5723D407998518770170AC09160AA99B5723D40CA7DBCA13A170AC03E8D203AA9723D40C955D1C4E0160AC0173147EA98723D40C185860F36160AC0C5ED2B6A8B723D40BEC92544AF150AC07262597A7E723D400FAF905E79150AC01FD7868A71723D40B54D96FF35150AC0CD936B0A64723D400E87A5811F150AC07A50508A56723D400B47EB0324150AC029E55A5947723D40B821C66B5E150AC00141CA9937723D40BEC92544AF150AC0AE6D403A2B723D40C231CB9E04160AC05C72DC291D723D40C72DE6E786160AC08C0464F904723D40C4D55CC9E9160AC065A88AA9F4713D401B6327BC04170AC06933F389E1713D40CCBD761F36170AC043F7F6F8CE713D40220B879455170AC047825FD9BB713D40CD51EC0D63170AC0C87FDCD9B2713D407418DD8B79170AC04B9D3699A7713D4023337271AF170AC0236937FA98713D40D1A1C2C716180AC04FB80D5993713D402783482B63180AC027F49FDA85713D40DCD9B2D716190AC07E2AB8697D713D4032BB383B63190AC0D618198975713D408A48032E7E190AC0571696896C713D40880849B082190AC08640892A57713D40DE019EB470190AC0DE0610994D713D40310FF4AB94190AC08BEBCEC941713D408930348DDC190AC039A8B34934713D408C0464F9041A0AC0BBC50D0929713D408C0464F9041A0AC0BF5076E915713D4035779906EA190AC0151FFA4910713D403623DE95B8190AC041063C7A0D713D40880849B082190AC06C35351A0A713D403227C34C36190AC0EE7A698A00713D40D7C5C8EDF2180AC046F93889F7703D40D4F19881CA180AC0F3DDF7B9EB703D40D63153FFC5180AC0A0E293A9DD703D4030FFD76FDC180AC0A46DFC89CA703D40D5850E70F7180AC0278B5649BF703D403227C34C36190AC0A8F8646AB7703D408774D3C155190AC000773469AE703D4032BB383B63190AC0812CFAF9A5703D4082F45EC65E190AC0D88AEC399F703D40DD6D28C643190AC02FA1270A99703D40D5850E70F7180AC086FF194A92703D408038FEFAD7180AC00274A95B87703D40DF4E22C2BF180AC0B0C0FCBA78703D402F88484DBB180AC0B4BBF6BB66703D4086819D51A9180AC03769BF5A5A703D4086819D51A9180AC0E3BD0FAC4F703D4083AD6DE580180AC03A64B95B48703D40DB5207793D180AC0BCA9EDCB3E703D4080F10C1AFA170AC03FC7478B33703D402DE4B622D6170AC096B5A8AA2B703D40D656EC2FBB170AC0420AF9FB20703D4028D0CC38B2170AC01C3E8E8B0F703D4028D0CC38B2170AC0F5B9DA8AFD6F3D402A1087B6AD170AC0A20E2BDCF26F3D407EC9213DA0170AC07B8A77DBE06F3D40D5C276418E170AC0FE5F1A2BD66F3D407E35AC4E73170AC05506C4DACE6F3D407821C2644F170AC0D793AFBAC46F3D40CC46E7FC14170AC058B9066CBD6F3D40CDF22B8CE3160AC0AECF413CB76F3D4021185124A9160AC031A5E48BAC6F3D4023580BA2A4160AC0B3EA18FCA26F3D40CC5EB69DB6160AC060A7FD7B956F3D40CC5EB69DB6160AC0B8955E9B8D6F3D40C8DE41A2BF160AC03AB3B85A826F3D407325A71BCD160AC090F1CDDB7D6F3D40C8DE41A2BF160AC0E74FC01B776F3D40C8DE41A2BF160AC03D66FBEB706F3D402480F67EFE160AC0940CA59B696F3D407821C2644F170AC0179A907B5F6F3D40D5C276418E170AC06ED0A80A576F3D407A49AD41A9170AC04654F2FB486F3D40D656EC2FBB170AC0F4588EEB3A6F3D407F5D972BCD170AC04B8FA67A326F3D402DE4B622D6170AC0CCFCB49B2A6F3D40D41632B2BF170AC0235BA7DB236F3D40D042024697170AC07B4908FB1B6F3D407E35AC4E73170AC0A6C0B80A186F3D40D2EE46D565170AC0547D9D8A0A6F3D4026A8E15B58170AC0D6C2D1FA006F3D4028E89BD953170AC083EF479BF46E3D407821C2644F170AC05B03006CE56E3D407821C2644F170AC009089C5BD76E3D407821C2644F170AC08B05195CCE6E3D4026A8E15B58170AC0B70C384BC96E3D40297C11C880170AC0E3CB9FCAC46E3D4028D0CC38B2170AC00DDBBBABC36E3D4029644227DF170AC0E1630B9CC76E3D402C38729307180AC0E2F3797BC66E3D402B20A3F265180AC00D23731BC36E3D4031C802CBB6180AC0399A232BBF6E3D40E0E297B0EC180AC064F1F67BBD6E3D40348463963D190AC06481655BBC6E3D40E5DEB2F96E190AC090F8156BB86E3D40E4C6E358CD190AC0E6C699CBB26E3D40EADACD42F1190AC03D6D437BAB6E3D409475EE2C301A0AC094837E4BA56E3D404278B471C41A0AC0EA99B91B9F6E3D40EDA64A4A301B0AC042881A3B976E3D404A48FF266F1B0AC0C415061B8D6E3D40465C001AA51B0AC071427CBB806E3D40F84AC56B031C0AC0C710001C7B6E3D40FCB26AC6581C0AC01D273BEC746E3D40FF869A32811C0AC0A0FCDD3B6A6E3D40A40DD1329C1C0AC0F832F6CA616E3D40505436ACA91C0AC079A004EC596E3D40FEDA55A3B21C0AC0FBBD5EAB4E6E3D40505436ACA91C0AC0A9C2FA9A406E3D40A7E1009FC41C0AC0573728AB336E3D40FF6ECB91DF1C0AC0AD951AEB2C6E3D40583C5002F61C0AC02F4BE07B246E3D400483B57B031D0AC0B2683A3B196E3D40A909EC7B1E1D0AC05FDD674B0C6E3D4006ABA0585D1D0AC0E2FAC10A016E3D400593D1B7BB1D0AC08F2738ABF46D3D400F0F61FC341E0AC010DDFD3BEC6D3D40BBE93B646F1E0AC03CE41C2BE76D3D400CB7D7DD971E0AC093420F6BE06D3D400DDFC2BAF11E0AC040DF162CD56D3D40BFCD870C501F0AC0EDE3B21BC76D3D401BC3F759C01F0AC070B9556BBC6D3D40205388916F200AC09C78BDEAB76D3D40CAEDA87BAE200AC0F246414BB26D3D40D0019365D2200AC0CA1242ACA36D3D407CDC6DCD0C210AC04D309C6B986D3D407B30293E3E210AC0FBC4A63A896D3D407CC49E2C6B210AC0D390A79B7A6D3D402BDF3312A1210AC08225B26A6B6D3D402F47D96CF6210AC02E0A719B5F6D3D402D9B94DD27220AC0DB36E73B536D3D40DA09E5338F220AC0DF09078C3F6D3D40DDC545FF15230AC08E565AEB306D3D40E36DA5D766230AC01074B4AA256D3D403BFB6FCA81230AC092297A3B1D6D3D40367BFBCE8A230AC0E9CF23EB156D3D40E2C1604898230AC06A3D320C0E6D3D409048803FA1230AC0968C086B086D3D40E2C1604898230AC0ED5A8CCB026D3D4091F4C4CE6F230AC0191AF44AFE6C3D40DE59BBED42230AC0C6D6D8CAF06C3D408D8C1F741A230AC01D35CB0AEA6C3D40E245BAFA0C230AC09EEA909BE16C3D40E245BAFA0C230AC02030C50BD86C3D408D8C1F741A230AC077D66EBBD06C3D403793CA6F2C230AC0507A956BC06C3D4039D384ED27230AC0FE7E315BB26C3D403793CA6F2C230AC0D64A32BCA36C3D4039D384ED27230AC02DF1DB6B9C6C3D403793CA6F2C230AC0844FCEAB956C3D40E12DEB596B230AC0B09EA40A906C3D409048803FA1230AC031C4FBBB886C3D403AE3A029E0230AC00B4048BB766C3D40942CCCE781240AC08CF50D4C6E6C3D40EF213C35F2240AC08D5DA27A6B6C3D404B17AC8262250AC0E3E36E6B666C3D404DD30C4EE9250AC03AB2F2CB606C3D40FA415DA450260AC091A053EB586C3D40AAF06778B3260AC0E88EB40A516C3D40AB84DD66E0260AC069B40BBC496C3D400212A859FB260AC0EBF93F2C406C3D40AE580DD308270AC01891CDFA396C3D40AC1853550D270AC06E179AEB346C3D40A9D898D711270AC0EF1417EC2B6C3D40AE580DD308270AC09E616A4B1D6C3D40AC1853550D270AC04BD6975B106C3D40AFEC82C135270AC0A2348A9B096C3D40067A4DB450270AC0F8020EFC036C3D40B1146E9E8F270AC05039268BFB6B3D40B51089E711280AC0D1A634ACF36B3D4060ABA9D150280AC0FCD52D4CF06B3D400EC63EB786280AC0537CD7FBE86B3D40B8CCE9B298280AC07FF3870BE56B3D40BC4C5EAE8F280AC0D69931BBDD6B3D4067FF4D3970280AC0570740DCD56B3D4062EB634F4C280AC0849ECDAACF6B3D40BCB8E8BF62280AC0DA249A9BCA6B3D40BC4C5EAE8F280AC0069C4AABC66B3D4010EE2994E0280AC03113FBBAC26B3D40C09C346843290AC0B28009DCBA6B3D40172AFF5A5E290AC08C440D4BA86B3D4018BE74498B290AC0E72D686B8E6B3D401C261AA4E0290AC094A2957B816B3D401FFA4910092A0AC04117C38B746B3D40CC608C48142A0AC01C6B35DA606B3D4032B08EE3872A0AC0491927744D6B3D40B6F86FB9552B0AC0491927744D6B3D407F32C687D92B0AC028FC08B2546B3D40DDB98B8BFE2B0AC029DDA7F45D6B3D4025308CAA152C0AC09FD9BFFC5F6B3D40327B34304D2C0AC09D68B2DA576B3D40DBD8918F822C0AC0B11E5267496B3D4035E0E297B02C0AC0AE145D72376B3D40307D5468C52C0AC0B46CF7BC2C6B3D40E4A3C519C32C0AC05F31D92E236B3D40C82F7205CA2C0AC0AAE111CB1C6B3D4033FAD170CA2C0AC0A20FF16A146B3D40BF6955A6F32C0AC0AEFCE88A086B3D4080DB6EDD282D0AC046161F55036B3D401C1318A18E2D0AC05220FD40036B3D40C9B32671B12D0AC01C3B4D0B036B3D4000C1C1830B2E0AC038A4AC3A066B3D40BBAF6FCC572E0AC099CCC2E8036B3D40F95F538C982E0AC07D6363B9006B3D406EF8DD74CB2E0AC03E586BCDFB6A3D407F86376BF02E0AC00586AC6EF56A3D40C7FC378A072F0AC081368309ED6A3D40DD0DFD6E152F0AC0B6A2282BE16A3D40F41EC253232F0AC0F9DC64AFD26A3D40F98150830E2F0AC052EBA28DC86A3D404E21FC30E72E0AC01A19E42EC26A3D40221F4F26C92E0AC09389004CBE6A3D409C4F1DAB942E0AC08E90813CBB6A3D403048FAB48A2E0AC071A2A712AF6A3D4035AB88E4752E0AC0EEBA12DCA36A3D409C4F1DAB942E0AC070D86C9B986A3D40F19C2D20B42E0AC0F3AD0FEB8D6A3D40F230A30EE12E0AC0741B1E0C866A3D40F1845E7F122F0AC0CB79104C7F6A3D4049A69E605A2F0AC022D8028C786A3D40A25B5430CF2F0AC04EDF217B736A3D40FFFC080D0E300AC0A585CB2A6C6A3D40FD50C47D3F300AC0FB0B981B676A3D40AA2B9FE579300AC05342B0AA5E6A3D40594634CBAF300AC0A910340B596A3D40AB538AC2D3300AC02AC6F99B506A3D4006B5842117310AC0AD9B9CEB456A3D405ED6C4025F310AC02F713F3B3B6A3D40B323D5777E310AC086877A0B356A3D400BB19F6A99310AC0DC55FE6B2F6A3D40B323D5777E310AC0326C393C296A3D405ED6C4025F310AC08AA251CB206A3D400509409248310AC0E0281EBC1B6A3D405B02959636310AC037177FDB136A3D4008F53E9F12310AC08E2DBAAB0D6A3D4005210F33EA300AC0E58BACEB066A3D4059DAA9B9DC300AC067614F3BFC693D4003E154B5EE300AC0E95ECC3BF3693D4008F53E9F12310AC06C7C26FBE7693D40B38F5F8951310AC0C2DA183BE1693D40B323D5777E310AC0ED31EC8BDF693D40623E6A5DB4310AC0C102F3EBE2693D401159FF42EA310AC06C7C26FBE7693D400D6D003620320AC040BDBE7BEC693D401381EA1F44320AC014460E6CF0693D401415600E71320AC0151E34BBEE693D40BEAF80F8AF320AC041DD9B3AEA693D40BF43F6E6DC320AC0C24AAA5BE2693D40C217265305330AC019F1530BDB693D406CB2463D44330AC045B0BB8AD6693D40C4D3861E8C330AC0704F464BD4693D40CDBBA074D8330AC09C0EAECACF693D40C9CFA1670E340AC0F2DC312BCA693D40CA6317563B340AC0734A404CC2693D4024319CC651340AC0CAF0E9FBBA693D40267156444D340AC0F73FC05AB5693D4079967BDC12340AC04DC68C4BB0693D4071AE6186C6330AC0CE0BC1BBA6693D401D0D96A075330AC0FB5A971AA1693D40C8BF852B56330AC051E1630B9C693D4018F9ABB651330AC0A76730FC96693D40C33F11305F330AC0D36E4FEB91693D40759A609390330AC0293DD34B8C693D4021753BFBCA330AC0568CA9AA86693D4078EA364D44340AC081BBA24A83693D40255987A3AB340AC0D7F9B7CB7E693D4080BA8102EF340AC02DC83B2C79693D4029C12CFE00350AC059CF5A1B74693D40CFF3A78DEA340AC0858EC29A6F693D40282DB70FD4340AC0DB8420AC6B693D40282DB70FD4340AC00744882B67693D40D233620BE6340AC00744882B67693D40D3C7D7F912350AC006FCD0BB67693D40DBAFF14F5F350AC0322BCA5B64693D40319177B3AB350AC08989BC9B5D693D40E0AB0C99E1350AC036B6323C51693D40DCBF0D8C17360AC0B88BD58B46693D408BDAA2714D360AC03B6178DB3B693D403AF5375783360AC0E8D5A5EB2E693D40E3FBE25295360AC06AAB483B24693D4038B57DD987360AC0ECA8C53B1B693D40E027B3E66C360AC0991DF34B0E693D40310D1E0137360AC01B6327BC04693D408C86E7001C360AC09E38CA0BFA683D40E13F82870E360AC0F66EE29AF1683D408A462D8320360AC04BF5AE8BEC683D408D1A5DEF48360AC077B4160BE8683D403AF5375783360AC0CE829A6BE2683D403B89AD45B0360AC050583DBBD7683D403E5DDDB1D8360AC0D32DE00ACD683D403E5DDDB1D8360AC054E3A59BC4683D40E6CF12BFBD360AC0D700005BB9683D4092167838CB360AC058B6C5EBB0683D40E76388ADEA360AC0DA43B1CBA6683D40916A33A9FC360AC08890042B98683D403B1D2334DD360AC00AD6389B8E683D403609394AB9360AC0B84A66AB81683D408A2E5EE27E360AC03948E3AB78683D4034E14D6D5F360AC01329CDE671683D40A0E8706369360AC069AF99D76C683D40F63581D888360AC094DE927769683D409D90E744CC360AC0C1E5B16664683D40F8F1E1A30F370AC0176C7E575F683D40F98557923C370AC06D82B92759683D404ED367075C370AC0C4E0AB6752683D40FC5987FE64370AC01B3F9EA74B683D405353DC0253370AC0729D90E744683D40FD05CC8D33370AC0C86B14483F683D40F6B1272614370AC04B896E0734683D40512BF125F9360AC0CC3E34982B683D404CAB7C2A02370AC0F9D5C16625683D40A1F88C9F21370AC07A43D0871D683D40FC5987FE64370AC0A64AEF7618683D4057BB815DA8370AC0D13131A715683D4000C22C59BA370AC0529F3FC80D683D4003965CC5E2370AC0AA8DA0E705683D40B070372D1D380AC000EC9227FF673D40B004AD1B4A380AC0ADD05158F3673D40B4D8DC8772380AC08824C4A6DF673D405F077360DE380AC00A6AF816D6673D400EB67D3441390AC08BD70638CE673D40B8509E1E80390AC0E355D636C5673D406A3F6370DE390AC090CA0347B8673D406EA708CB333A0AC0E770ADF6B0673D40132E3FCB4E3A0AC0930DB5B7A5673D40C4F41840533A0AC04182E2C798673D40BF74A4445C3A0AC09900B2C68F673D40C4888E2E803A0AC0EE3EC7478B673D4071636996BA3A0AC0C39F3C878D673D401BFE8980F93A0AC0EF867EB78A673D401C92FF6E263B0AC09DD3D1167C673D4020FAA4C97B3B0AC01ED14E1773673D40CCD47F31B63B0AC0A08614A86A673D4024624A24D13B0AC0F69C4F7864673D40D2E8691BDA3B0AC023EC25D75E673D40D2E8691BDA3B0AC04E1B1F775B673D40D2E8691BDA3B0AC0A4A1EB6756673D407A5B9F28BF3B0AC0FB4795174F673D40218E1AB8A83B0AC052A6875748673D40CA94C5B3BA3B0AC0D35B4DE83F673D4076DB2A2DC83B0AC02A02F79738673D40222290A6D53B0AC08160E9D731673D4076DB2A2DC83B0AC0AD1F51572D673D40D154F42CAD3B0AC0D94E4AF729673D401EBAEA4B803B0AC0D9DEB8D628673D401D26755D533B0AC05A4CC7F720673D40738B5473143B0AC00809AC7713673D40C744EFF9063B0AC05ED72FD80D673D4072F7DE84E73A0AC08ADE4EC708673D401DAACE0FC83A0AC0E1F4899702673D401DAACE0FC83A0AC00DFCA886FD663D401A6A1492CC3A0AC00C248337FF663D406FB72407EC3A0AC0E1ACD22703673D402112746A1D3B0AC0E1ACD22703673D4021A6E9584A3B0AC0E184F87601673D40257A19C5723B0AC0370BC567FC663D401EBAEA4B803B0AC08E210038F6663D401EBAEA4B803B0AC0BA281F27F1663D40C90050C58D3B0AC010F7A287EB663D40218E1AB8A83B0AC0670DDE57E5663D4076DB2A2DC83B0AC0BEB38707DE663D40D2E8691BDA3B0AC0143A54F8D8663D4027367A90F93B0AC040F9BB77D4663D40CD68F51FE33B0AC06C706C87D0663D40CA94C5B3BA3B0AC06C00DB66CF663D40CC400A43893B0AC096C73FD8CE663D401D26755D533B0AC0C286A757CA663D402112746A1D3B0AC0EE450FD7C5663D401D3E44FEF43A0AC0455C4AA7BF663D4072F7DE84E73A0AC09B728577B9663D40207EFE7BF03A0AC0F288C047B3663D40C984A977023B0AC0485744A8AD663D4072F7DE84E73A0AC074A61A07A8663D401A6A1492CC3A0AC0CB749E67A2663D4071636996BA3A0AC0218BD9379C663D406B4F7FAC963A0AC0783183E794663D40C4888E2E803A0AC0CFFF06488F663D40C4F41840533A0AC0FB0626378A663D406C674E4D383A0AC026EE676787663D4067536463143A0AC05265187783663D4068FFA8F2E2390AC07D4C5AA780663D40676B3304B6390AC0FE01203878663D401172DEFFC7390AC0FF69B46675663D4068FFA8F2E2390AC0FF21FDF675663D40171A3ED8183A0AC0D36295777A663D40C1B45EC2573A0AC07D94111780663D40C85CBE9AA83A0AC05265187783663D40207EFE7BF03A0AC07D4C5AA780663D401C92FF6E263B0AC0D43ABBC678663D40CA181F662F3B0AC02A093F2773663D401BFE8980F93A0AC05560127871663D406F23AF18BF3A0AC0563838C76F663D4070CFF3A78D3A0AC05560127871663D406F3B7EB9603A0AC05560127871663D40129AC9DC213A0AC081D7C2876D663D401686C8E9EB390AC0ACBE04B86A663D40676B3304B6390AC02E04392861663D40121E238F96390AC0B02193E755663D40B9E4130DAD390AC00738CEB74F663D40BC24CE8AA8390AC0333FEDA64A663D4067D7BD1589390AC0897D022846663D40BDD0121A77390AC0E06B63473E663D400F4AF3226E390AC0363AE7A738663D40B8509E1E80390AC0385AC46636663D40B9E4130DAD390AC037CA558737663D4068FFA8F2E2390AC0612129D835663D406EA708CB333A0AC0B9C7D2872E663D406DFBC33B653A0AC00FDE0D5828663D4016026F37773A0AC0928BD6F61B663D40C3DC499FB13A0AC03FE0264811663D406F23AF18BF3A0AC06BE745370C663D40C85CBE9AA83A0AC06B77B4160B663D4016026F37773A0AC0C1B5C99706663D40BEE02E562F3A0AC0ECE4C23703663D40C08C73E5FD390AC0704AD466F7653D401686C8E9EB390AC0779A71BFE8653D4010351B75BE390AC023EFC110DE653D4062AEFB7DB5390AC050864FDFD7653D40BEBB3A6CC7390AC05411B8BFC4653D4067C2E567D9390AC02D454D4FB3653D4068565B56063A0AC058E4D70FB1653D40BB63B14D2A3A0AC0582C8F7FB0653D401385F12E723A0AC08313D1AFAD653D4068D201A4913A0AC031D0B52FA0653D40C29F8614A83A0AC0DF8C9AAF92653D401419671D9F3A0AC08C497F2F85653D406D52769F883A0AC03A0664AF77653D401385F12E723A0AC0118AADA069653D40C0779B374E3A0AC06A50340F60653D401771F03B3C3A0AC01635F33F54653D4010B1C1C2493A0AC099524DFF48653D406CBE00B15B3A0AC0470F327F3B653D40C34BCBA3763A0AC01E937B702D653D401805662A693A0AC075A9B64027653D4069EAD044333A0AC0A1B0D52F22653D406A9615D4013A0AC0CC9717601F653D4067C2E567D9390AC024CE2FEF16653D40B8A75082A3390AC07A0C457012653D40098DBB9C6D390AC0D02280400C653D40613255302A390AC0285998CF03653D400865D0BF13390AC07E6FD39FFD643D400865D0BF13390AC0D65D34BFF5643D400D79BAA937390AC05713FA4FED643D40608610A15B390AC0838AAA5FE9643D40659AFA8A7F390AC05BE619A0D9643D40B6679604A8390AC0B31C322FD1643D40B7FB0BF3D4390AC034D2F7BFC8643D40BACF3B5FFD390AC0B587BD50C0643D406616A1D80A3A0AC0E21E4B1FBA643D40BFE32549213A0AC09093782FAD643D4066AA16C7373A0AC03C3080F0A1643D4010B1C1C2493A0AC0697F564F9C643D40BCF7263C573A0AC0BF05234097643D40C10B11267B3A0AC0EA341CE093643D401C6D0B85BE3A0AC0151C5E1091643D401C018173EB3A0AC015D4A6A091643D401CE9B1D2493B0AC0EA7CD34F93643D407AB2518CE23B0AC0E9A4AD0095643D40CFE79260603C0AC0EBC48ABF92643D407E022846963C0AC0426B346F8B643D40DB375211023D0AC0C320FAFF82643D4088122D793C3D0AC045AEE5DF78643D40DD5F3DEE5B3D0AC0C8CB3F9F6D643D4032AD4D637B3D0AC04AC9BC9F64643D40864E1949CC3D0AC0F785A11F57643D40E0AF13A80F3E0AC0783B67B04E643D40910A630B413E0AC0CF9959F047643D40E417B902653E0AC026F84B3041643D40929ED8F96D3E0AC07D0E87003B643D40E2D7FE84693E0AC0D36C794034643D40E2D7FE84693E0AC056D28A6F28643D403B110E07533E0AC0D817BFDF1E643D403B110E07533E0AC05A153CE015643D40E417B902653E0AC0B24B546F0D643D40E36B7473963E0AC0DD7A4D0F0A643D403B8DB454DE3E0AC0B1BBE58F0E643D40ECE703B80F3F0AC0048F6FEF1A643D409B02999D453F0AC0AC58576023643D409956540E773F0AC056FA64202A643D4098AA0F7FA83F0AC0FF2BE1BF2F643D404485EAE6E23F0AC02A83B4102E643D409DA62AC82A400AC08171153026643D40A58E441E77400AC0048F6FEF1A643D40FEAF84FFBE400AC05ACD847016643D40A7B62FFBD0400AC0B12B77B00F643D4050296508B6400AC008AA46AF06643D40F92F1004C8400AC08A1755D0FE633D4053FD9474DE400AC0E105B6EFF6633D40FC034070F0400AC06273C410EF633D40AA8A5F67F9400AC0E400B0F0E4633D40AA8A5F67F9400AC0668E9BD0DA633D4056D1C4E006410AC0149337C0CC633D40FD97B55E1D410AC06BA97290C6633D4057653ACF33410AC0C2DF8A1FBE633D40006CE5CA45410AC019F6C5EFB7633D40FE2B2B4D4A410AC0C66AF3FFAA633D4057F9AFBD60410AC01E59541FA3633D40A97290C657410AC09E7EABD09B633D4057F9AFBD60410AC0CBCD812F96633D40AA0606B584410AC04C3B90508E633D4007A8BA91C3410AC0CEC87B3084633D405809CCF918420AC0256F25E07C633D405C7171546E420AC0512E8D5F78633D4060D916AFC3420AC0527644CF77633D40B5262724E3420AC0512E8D5F78633D400E4867052B430AC0500EB0A07A633D400FDCDCF357430AC0500EB0A07A633D40C1CAA145B6430AC07C15CF8F75633D4019ECE126FE430AC029B2D6506A633D40C806770C34440AC02942453069633D4072A197F672440AC02942453069633D40C8821D5ABF440AC05529876066633D40745DF8C1F9440AC0AC17E87F5E633D40D1FEAC9E38450AC00306499F56633D40264CBD1358450AC0598C159051633D402920ED7F80450AC0B032BF3F4A633D402874A8F0B1450AC007D968EF42633D402B48D85CDA450AC05D177E703E633D40D20EC9DAF0450AC0B47570B037633D40D5E2F84619460AC00CAC883F2F633D408369183E22460AC08DF1BCAF25633D40D20EC9DAF0450AC00FEF39B01C633D40D44E8358EC450AC065BDBD1017633D40D3A23EC91D460AC092C4DCFF11633D4084FD8D2C4F460AC0E89260600C633D402E04392861460AC014E236BF06633D408351499D80460AC06BB0BA1F01633D4083E5BE8BAD460AC0C1368710FC623D4087B9EEF7D5460AC0EDF5EE8FF7623D4038143E5B07470AC0186D9F9FF3623D40E4EE18C341470AC0449C983FF0623D408C497F2F85470AC09A6A1CA0EA623D403DA4CE92B6470AC0F0805770E4623D40E46ABF10CD470AC0727ED470DB623D4090B1248ADA470AC0C9DCC6B0D4623D4091459A7807480AC0F5E3E59FCF623D40E966DA594F480AC0F673547FCE623D40EAFA4F487C480AC04C8A8F4FC8623D40421C9029C4480AC021A34D1FCB623D404B04AA7F10490AC09F15623FD5623D40F59ECA694F490AC07356FABFD9623D40A279A5D189490AC07356FABFD9623D404E548039C4490AC09EF58480D7623D404DA83BAAF5490AC0C9247E20D4623D4052BC2594194A0AC020CB27D0CC623D40FCC2D08F2B4A0AC04CFA2070C9623D40A7093609394A0AC0F96E4E80BC623D40A7093609394A0AC0505DAF9FB4623D405110E1044B4A0AC051A5660FB4623D40026B30687C4A0AC0252EB6FFB7623D40AC055152BB4A0AC0F9B605F0BB623D40AB590CC3EC4A0AC0CF5F329FBD623D405D48D1144B4B0AC0240ED940BA623D400923AC7C854B0AC07BFC3960B2623D4063F030ED9B4B0AC0FD416ED0A8623D400CF7DBE8AD4B0AC029B91EE0A4623D40B691FCD2EC4B0AC05358A9A0A2623D40BCA5E6BC104C0AC07F1711209E623D406880C1244B4C0AC0D675036097623D4010DB27918E4C0AC0594BA6AF8C623D40C11DA8531E4D0AC084C256BF88623D401AD35D23934D0AC0AF192A1087623D40CE01DDF2EC4D0AC0B0A998EF85623D40789CFDDC2B4E0AC006C0D3BF7F623D40D4916D2A9C4E0AC08805083076623D40D3799E89FA4E0AC0DFABB1DF6E623D40D64DCEF5224F0AC036525B8F67623D40301B5366394F0AC08C20DFEF61623D40DDF52DCE734F0AC0E40E400F5A623D40E15DD328C94F0AC03ADDC36F54623D40E285BE0523500AC090AB47D04E623D4093C83EC8B2500AC0E7C182A048623D409344E5153E510AC0123933B044623D40ECF99AE5B2510AC06907B7103F623D40F1F5B52E35520AC0C0AD60C037623D404E2BE0F9A0520AC042F394302E623D40F9C500E4DF520AC099993EE026623D40FD2DA63E35530AC0C410EFEF22623D40AA9CF6949C530AC01A97BBE01D623D40ACC4E171F6530AC0472E49AF17623D4009FA0B3D62540AC09DFCCC0F12623D4062AFC10CD7540AC04A2943B005623D40BCF8ECCA78550AC0F8E52730F8613D401C5602733E560AC0A5129ED0EB613D4024BAC21616570AC0FC709010E5613D407CC33357BC570AC0 Mubazi stream \N \N \N \N \N \N \N \N 0 @@ -15093,11 +15021,10 @@ -- --- Name: DEFAULT PRIVILEGES FOR TABLES; Type: DEFAULT ACL; Schema: public; Owner: postgres +-- Name: hotosm_bdi_drains; Type: MATERIALIZED VIEW DATA; Schema: osm; Owner: postgres -- -ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public REVOKE ALL ON TABLES FROM postgres; -ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT SELECT ON TABLES TO replicator; +REFRESH MATERIALIZED VIEW osm.hotosm_bdi_drains; -- Binary files /tmp/tmpaxshhvqx/7gy0Pxiz0k/pygeoapi-0.11.0/tests/data/hotosm_bdi_waterways.sql.gz and /tmp/tmpaxshhvqx/mynj__8aJU/pygeoapi-0.12.0/tests/data/hotosm_bdi_waterways.sql.gz differ diff -Nru pygeoapi-0.11.0/tests/pygeoapi-test-config-envvars.yml pygeoapi-0.12.0/tests/pygeoapi-test-config-envvars.yml --- pygeoapi-0.11.0/tests/pygeoapi-test-config-envvars.yml 2021-10-24 14:33:20.000000000 +0000 +++ pygeoapi-0.12.0/tests/pygeoapi-test-config-envvars.yml 2022-02-07 20:05:13.000000000 +0000 @@ -35,6 +35,7 @@ mimetype: application/json; charset=UTF-8 encoding: utf-8 language: en-US + gzip: false cors: true pretty_print: true limit: 10 diff -Nru pygeoapi-0.11.0/tests/pygeoapi-test-config.yml pygeoapi-0.12.0/tests/pygeoapi-test-config.yml --- pygeoapi-0.11.0/tests/pygeoapi-test-config.yml 2021-10-24 14:33:20.000000000 +0000 +++ pygeoapi-0.12.0/tests/pygeoapi-test-config.yml 2022-02-07 20:05:13.000000000 +0000 @@ -34,6 +34,7 @@ url: http://localhost:5000/ mimetype: application/json; charset=UTF-8 encoding: utf-8 + gzip: false languages: # First language is the default language - en-US diff -Nru pygeoapi-0.11.0/tests/pygeoapi-test-ogr-config.yml pygeoapi-0.12.0/tests/pygeoapi-test-ogr-config.yml --- pygeoapi-0.11.0/tests/pygeoapi-test-ogr-config.yml 2021-10-24 14:33:20.000000000 +0000 +++ pygeoapi-0.12.0/tests/pygeoapi-test-ogr-config.yml 2022-02-07 20:05:13.000000000 +0000 @@ -36,6 +36,7 @@ encoding: utf-8 language: en-US cors: true + gzip: false pretty_print: true limit: 10 # templates: /path/to/templates diff -Nru pygeoapi-0.11.0/tests/test_api.py pygeoapi-0.12.0/tests/test_api.py --- pygeoapi-0.11.0/tests/test_api.py 2021-10-24 14:33:20.000000000 +0000 +++ pygeoapi-0.12.0/tests/test_api.py 2022-02-07 20:05:13.000000000 +0000 @@ -2,7 +2,7 @@ # # Authors: Tom Kralidis # -# Copyright (c) 2021 Tom Kralidis +# Copyright (c) 2022 Tom Kralidis # # Permission is hereby granted, free of charge, to any person # obtaining a copy of this software and associated documentation @@ -30,12 +30,13 @@ import json import logging import time +import gzip from pyld import jsonld import pytest from pygeoapi.api import ( API, APIRequest, FORMAT_TYPES, validate_bbox, validate_datetime, - F_HTML, F_JSON, F_JSONLD + validate_subset, F_HTML, F_JSON, F_JSONLD, F_GZIP ) from pygeoapi.util import yaml_load @@ -115,6 +116,17 @@ assert apireq.get_linkrel(F_HTML) == 'self' assert apireq.get_linkrel(F_JSON) == 'alternate' + # Test accept header with multiple valid formats + hh = 'plain/text,application/ld+json,application/json;q=0.9,' + req = mock_request(HTTP_ACCEPT=hh) + apireq = APIRequest(req, api_.locales) + assert apireq.is_valid() + assert apireq.format == F_JSONLD + assert apireq.get_response_headers()['Content-Type'] == \ + FORMAT_TYPES[F_JSONLD] + assert apireq.get_linkrel(F_JSONLD) == 'self' + assert apireq.get_linkrel(F_HTML) == 'alternate' + # Overrule HTTP content negotiation req = mock_request({'f': 'html'}, HTTP_ACCEPT='application/json') # noqa apireq = APIRequest(req, api_.locales) @@ -237,6 +249,101 @@ assert code == 400 +def test_gzip(config, api_): + # Requests for each response type and gzip encoding + req_gzip_json = mock_request(HTTP_ACCEPT=FORMAT_TYPES[F_JSON], + HTTP_ACCEPT_ENCODING=F_GZIP) + req_gzip_jsonld = mock_request(HTTP_ACCEPT=FORMAT_TYPES[F_JSONLD], + HTTP_ACCEPT_ENCODING=F_GZIP) + req_gzip_html = mock_request(HTTP_ACCEPT=FORMAT_TYPES[F_HTML], + HTTP_ACCEPT_ENCODING=F_GZIP) + req_gzip_gzip = mock_request(HTTP_ACCEPT='application/gzip', + HTTP_ACCEPT_ENCODING=F_GZIP) + + # Responses from server config without gzip compression + rsp_headers, _, rsp_json = api_.landing_page(req_gzip_json) + assert rsp_headers['Content-Type'] == FORMAT_TYPES[F_JSON] + rsp_headers, _, rsp_jsonld = api_.landing_page(req_gzip_jsonld) + assert rsp_headers['Content-Type'] == FORMAT_TYPES[F_JSONLD] + rsp_headers, _, rsp_html = api_.landing_page(req_gzip_html) + assert rsp_headers['Content-Type'] == FORMAT_TYPES[F_HTML] + rsp_headers, _, _ = api_.landing_page(req_gzip_gzip) + assert rsp_headers['Content-Type'] == FORMAT_TYPES[F_JSON] + + # Add gzip to server and use utf-16 encoding + config['server']['gzip'] = True + enc_16 = 'utf-16' + config['server']['encoding'] = enc_16 + api_ = API(config) + + # Responses from server with gzip compression + rsp_json_headers, _, rsp_gzip_json = api_.landing_page(req_gzip_json) + rsp_jsonld_headers, _, rsp_gzip_jsonld = api_.landing_page(req_gzip_jsonld) + rsp_html_headers, _, rsp_gzip_html = api_.landing_page(req_gzip_html) + rsp_gzip_headers, _, rsp_gzip_gzip = api_.landing_page(req_gzip_gzip) + + # Validate compressed json response + assert rsp_json_headers['Content-Type'] == \ + f'{FORMAT_TYPES[F_JSON]}; charset={enc_16}' + assert rsp_json_headers['Content-Encoding'] == F_GZIP + + parsed_gzip_json = gzip.decompress(rsp_gzip_json).decode(enc_16) + assert isinstance(parsed_gzip_json, str) + parsed_gzip_json = json.loads(parsed_gzip_json) + assert isinstance(parsed_gzip_json, dict) + assert parsed_gzip_json == json.loads(rsp_json) + + # Validate compressed jsonld response + assert rsp_jsonld_headers['Content-Type'] == \ + f'{FORMAT_TYPES[F_JSONLD]}; charset={enc_16}' + assert rsp_jsonld_headers['Content-Encoding'] == F_GZIP + + parsed_gzip_jsonld = gzip.decompress(rsp_gzip_jsonld).decode(enc_16) + assert isinstance(parsed_gzip_jsonld, str) + parsed_gzip_jsonld = json.loads(parsed_gzip_jsonld) + assert isinstance(parsed_gzip_jsonld, dict) + assert parsed_gzip_jsonld == json.loads(rsp_jsonld) + + # Validate compressed html response + assert rsp_html_headers['Content-Type'] == \ + f'{FORMAT_TYPES[F_HTML]}; charset={enc_16}' + assert rsp_html_headers['Content-Encoding'] == F_GZIP + + parsed_gzip_html = gzip.decompress(rsp_gzip_html).decode(enc_16) + assert isinstance(parsed_gzip_html, str) + assert parsed_gzip_html == rsp_html + + # Validate compressed gzip response + assert rsp_gzip_headers['Content-Type'] == \ + f'{FORMAT_TYPES[F_GZIP]}; charset={enc_16}' + assert rsp_gzip_headers['Content-Encoding'] == F_GZIP + + parsed_gzip_gzip = gzip.decompress(rsp_gzip_gzip).decode(enc_16) + assert isinstance(parsed_gzip_gzip, str) + parsed_gzip_gzip = json.loads(parsed_gzip_gzip) + assert isinstance(parsed_gzip_gzip, dict) + + # Requests without content encoding header + req_json = mock_request(HTTP_ACCEPT=FORMAT_TYPES[F_JSON]) + req_jsonld = mock_request(HTTP_ACCEPT=FORMAT_TYPES[F_JSONLD]) + req_html = mock_request(HTTP_ACCEPT=FORMAT_TYPES[F_HTML]) + + # Responses without content encoding + _, _, rsp_json_ = api_.landing_page(req_json) + _, _, rsp_jsonld_ = api_.landing_page(req_jsonld) + _, _, rsp_html_ = api_.landing_page(req_html) + + # Confirm each request is the same when decompressed + assert rsp_json_ == rsp_json == \ + gzip.decompress(rsp_gzip_json).decode(enc_16) + + assert rsp_jsonld_ == rsp_jsonld == \ + gzip.decompress(rsp_gzip_jsonld).decode(enc_16) + + assert rsp_html_ == rsp_html == \ + gzip.decompress(rsp_gzip_html).decode(enc_16) + + def test_root(config, api_): req = mock_request() rsp_headers, code, response = api_.landing_page(req) @@ -256,7 +363,7 @@ for link in root['links']) assert any(link['href'].endswith('f=html') and link['rel'] == 'alternate' for link in root['links']) - assert len(root['links']) == 7 + assert len(root['links']) == 9 assert 'title' in root assert root['title'] == 'pygeoapi default instance' assert 'description' in root @@ -850,7 +957,7 @@ domainset = json.loads(response) - assert domainset['type'] == 'DomainSetType' + assert domainset['type'] == 'DomainSet' assert domainset['generalGrid']['axisLabels'] == ['Long', 'Lat'] assert domainset['generalGrid']['gridLimits']['axisLabels'] == ['i', 'j'] assert domainset['generalGrid']['gridLimits']['axis'][0]['upperBound'] == 2400 # noqa @@ -869,7 +976,7 @@ rangetype = json.loads(response) - assert rangetype['type'] == 'DataRecordType' + assert rangetype['type'] == 'DataRecord' assert len(rangetype['field']) == 1 assert rangetype['field'][0]['id'] == 1 assert rangetype['field'][0]['name'] == 'Temperature [C]' @@ -883,7 +990,7 @@ assert code == 400 - req = mock_request({'rangeSubset': '12'}) + req = mock_request({'range-subset': '12'}) rsp_headers, code, response = api_.get_collection_coverage( req, 'gdps-temperature') @@ -1009,7 +1116,7 @@ assert process['version'] == '0.2.0' assert process['title'] == 'Hello World' assert len(process['keywords']) == 3 - assert len(process['links']) == 3 + assert len(process['links']) == 6 assert len(process['inputs']) == 2 assert len(process['outputs']) == 1 assert len(process['outputTransmission']) == 1 @@ -1241,15 +1348,13 @@ # Cleanup time.sleep(2) # Allow time for any outstanding async jobs - for process_id, job_id in cleanup_jobs: - rsp_headers, code, response = api_.delete_process_job( - process_id, job_id) + for _, job_id in cleanup_jobs: + rsp_headers, code, response = api_.delete_job(job_id) assert code == 200 -def test_delete_process_job(api_): - rsp_headers, code, response = api_.delete_process_job( - 'does-not-exist', 'does-not-exist') +def test_delete_job(api_): + rsp_headers, code, response = api_.delete_job('does-not-exist') assert code == 404 @@ -1276,13 +1381,11 @@ assert data['value'] == 'Hello Sync Test Deletion!' job_id = rsp_headers['Location'].split('/')[-1] - rsp_headers, code, response = api_.delete_process_job( - 'hello-world', job_id) + rsp_headers, code, response = api_.delete_job(job_id) assert code == 200 - rsp_headers, code, response = api_.delete_process_job( - 'hello-world', job_id) + rsp_headers, code, response = api_.delete_job(job_id) assert code == 404 req = mock_request(data=req_body_async) @@ -1294,12 +1397,10 @@ time.sleep(2) # Allow time for async execution to complete job_id = rsp_headers['Location'].split('/')[-1] - rsp_headers, code, response = api_.delete_process_job( - 'hello-world', job_id) + rsp_headers, code, response = api_.delete_job(job_id) assert code == 200 - rsp_headers, code, response = api_.delete_process_job( - 'hello-world', job_id) + rsp_headers, code, response = api_.delete_job(job_id) assert code == 404 @@ -1397,6 +1498,9 @@ assert (validate_bbox('-142.1,42.12,-52.22,84.4') == [-142.1, 42.12, -52.22, 84.4]) + assert (validate_bbox('177.0,65.0,-177.0,70.0') == + [177.0, 65.0, -177.0, 70.0]) + with pytest.raises(ValueError): validate_bbox('1,2,4') @@ -1448,6 +1552,23 @@ _ = validate_datetime(config, '../1999') +@pytest.mark.parametrize("value, expected", [ + ('time(2000-11-11)', {'time': ['2000-11-11']}), + ('time("2000-11-11")', {'time': ['2000-11-11']}), + ('time("2000-11-11T00:11:11")', {'time': ['2000-11-11T00:11:11']}), + ('time("2000-11-11T11:12:13":"2021-12-22T:13:33:33")', {'time': ['2000-11-11T11:12:13', '2021-12-22T:13:33:33']}), # noqa + ('lat(40)', {'lat': [40]}), + ('lat(0:40)', {'lat': [0, 40]}), + ('foo("bar")', {'foo': ['bar']}), + ('foo("bar":"baz")', {'foo': ['bar', 'baz']}) +]) +def test_validate_subset(value, expected): + assert validate_subset(value) == expected + + with pytest.raises(ValueError): + validate_subset('foo("bar)') + + def test_get_exception(config, api_): d = api_.get_exception(500, {}, 'json', 'NoApplicableCode', 'oops') assert d[0] == {} diff -Nru pygeoapi-0.11.0/tests/test_elasticsearch__provider.py pygeoapi-0.12.0/tests/test_elasticsearch__provider.py --- pygeoapi-0.11.0/tests/test_elasticsearch__provider.py 2021-10-24 14:33:20.000000000 +0000 +++ pygeoapi-0.12.0/tests/test_elasticsearch__provider.py 2022-02-07 20:05:13.000000000 +0000 @@ -150,7 +150,8 @@ fields = p.get_fields() assert len(fields) == 37 assert fields['scalerank']['type'] == 'long' - assert fields['changed']['type'] == 'float' + assert fields['changed']['type'] == 'number' + assert fields['changed']['format'] == 'float' assert fields['ls_name']['type'] == 'string' results = p.query() @@ -171,7 +172,7 @@ results = p.query(startindex=2, limit=1) assert len(results['features']) == 1 - assert results['features'][0]['id'] == 3168070 + assert results['features'][0]['id'] == 3042030 results = p.query(sortby=[{'property': 'nameascii', 'order': '+'}]) assert results['features'][0]['properties']['nameascii'] == 'Abidjan' diff -Nru pygeoapi-0.11.0/tests/test_postgresql_provider.py pygeoapi-0.12.0/tests/test_postgresql_provider.py --- pygeoapi-0.11.0/tests/test_postgresql_provider.py 2021-10-24 14:33:20.000000000 +0000 +++ pygeoapi-0.12.0/tests/test_postgresql_provider.py 2022-02-07 20:05:13.000000000 +0000 @@ -57,9 +57,22 @@ } +@pytest.fixture() +def config_with_properties(config): + config_ = {'properties': ['name', 'waterway', 'width', 'does_not_exist']} + config_.update(config) + return config_ + + +@pytest.fixture() +def config_materialised_view(config): + config_ = config.copy() + config_['table'] = 'hotosm_bdi_drains' + return config_ + + def test_query(config): """Testing query for a valid JSON object with geometry""" - p = PostgreSQLProvider(config) feature_collection = p.query() assert feature_collection.get('type', None) == 'FeatureCollection' @@ -72,8 +85,24 @@ assert geometry is not None +def test_query_materialised_view(config, config_materialised_view): + """Testing query using a materialised view""" + p = PostgreSQLProvider(config_materialised_view) + features = p.query(limit=14776).get("features", None) + properties = features[0].get("properties", None) + # Only width and depth properties should be available + assert list(properties.keys()) == ["osm_id", "width", "depth"] + p_full = PostgreSQLProvider(config) + full_features = p_full.query(limit=14776).get("features", None) + drain_features = [ + f for f in full_features if f["properties"]["waterway"] == "drain" + ] + # All drains from the original dataset should be in the view + assert len(features) == len(drain_features) + + def test_query_with_property_filter(config): - """Test query valid features when filtering by property""" + """Test query valid features when filtering by property""" p = PostgreSQLProvider(config) feature_collection = p.query(properties=[("waterway", "stream")]) features = feature_collection.get('features', None) @@ -94,6 +123,20 @@ assert (len(other_features) != 0) +def test_query_with_config_properties(config_with_properties): + """ + Test that query is restricted by properties in the config. + No properties should be returned that are not requested. + Note that not all requested properties have to exist in the query result. + """ + p = PostgreSQLProvider(config_with_properties) + feature_collection = p.query() + feature = feature_collection.get('features', None)[0] + properties = feature.get('properties', None) + for property_name in properties.keys(): + assert property_name in config_with_properties["properties"] + + def test_query_hits(config): """Test query resulttype=hits with properties""" psp = PostgreSQLProvider(config)