diff --git a/.gitignore b/.gitignore index 7cd99ce..97d8eef 100644 --- a/.gitignore +++ b/.gitignore @@ -114,3 +114,5 @@ ENV/ *.code-workspace .DS_Store + +pygeoapi/django_pygeoapi/sample_project/db.sqlite3 diff --git a/docs/source/downstream.rst b/docs/source/downstream.rst new file mode 100644 index 0000000..1957351 --- /dev/null +++ b/docs/source/downstream.rst @@ -0,0 +1,103 @@ +.. _downstream: + +Downstream Projects +======= + +Downstreaming `pygeoapi` project with various python frameworks. + +------------------ + +In this page, we'll demonstrate how to downstream `pygeoapi` project with various python frameworks. + + +Django +^^^^^^ + +Django is a high-level Python web framework that encourages rapid development and clean, pragmatic design. Click `here `_ to read more about Django. + +In this section we create a sample django project and use `pygeoapi` package as a pluggable django application and serve all the capabilities of `pygeoapi` using Django. For the truly impatient developers, there is a Django `sample_project` in the source code. + +To create everything from scratch please follow these steps : + +- Create a Project folder and create a fresh virtual environment using your preferred tool. e.g. + +.. code-block:: bash + + python3 -m venv env + +Once created, activate it. + +- Install the following dependencies + +.. code-block:: bash + + pip install Django pygeoapi + +- Create a django project in a directory and cd into it. + +.. code-block:: python + + django-admin startproject sampleproject + cd /sampleproject + +- Download `pygeoapi-config.yml` using + +.. code-block:: bash + + curl -O https://raw.githubusercontent.com/geopython/pygeoapi/django_pygeoapi/sample_project/pygeoapi-config.yml + +and put it in the same folder at root level. + +- Set environment variable + +.. code-block:: bash + + export PYGEOAPI_CONFIG=pygeoapi-config.yml + export PYGEOAPI_OPENAPI=example-openapi.yml + +- Run `python manage.py collectstatic` to get all static files. +- Generate OpenAPI document using following `pygeoapi` command + +.. code-block:: bash + + pygeoapi openapi generate $PYGEOAPI_CONFIG --output-file $PYGEOAPI_OPENAPI + +- Update Django `sampleproject/settings.py` file as per following + +.. code-block:: python + + import os + from pygeoapi.django_app import config + + INSTALLED_APPS = [ + # other apps + .... + #pygeoapi app + 'pygeoapi' + ] + + # Put following setting after STATIC_URL + STATIC_ROOT = os.path.join( BASE_DIR / 'assets') + + # Specific pygeoapi setting + PYGEOAPI_CONFIG = config() + ... + +- Update Django `sampleproject/urls.py` file to run pygeoapi at e.g. `pga` path + +.. code-block:: python + + from django.contrib import admin + from django.urls import path, include + from pygeoapi.django_pygeoapi import urls + urlpatterns = [ + path('admin/', admin.site.urls), + path('pga/', include(urls)) # added here + ] + +- Update pygeoapi `pygeoapi-config.yml` file with following settings + +1. Update the `url` property under `server` in `pygeoapi-config.yml` accordingly to your django project url. e.g. In this case the path set is `pga` . +2. Update all data paths e.g. `tests/data/ne_110m_lakes.geojson` to match with the absolute path of the pygeoapi project directory. + +- Run Django project using `python manage.py runserver`. Once server starts, head over to `localhost:8000/pga` to see `pygeoapi` running. \ No newline at end of file diff --git a/docs/source/index.rst b/docs/source/index.rst index 6194b36..0651f15 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -23,6 +23,7 @@ pygeoapi |release| documentation configuration administration running + downstream running-with-docker tour openapi diff --git a/pygeoapi-openapi.yml b/pygeoapi-openapi.yml new file mode 100644 index 0000000..e00b19d --- /dev/null +++ b/pygeoapi-openapi.yml @@ -0,0 +1,880 @@ +components: + parameters: + f: + description: The optional f parameter indicates the output format which the + server shall provide as part of the response document. The default format + is GeoJSON. + explode: false + in: query + name: f + required: false + schema: + default: json + enum: + - json + - html + - jsonld + type: string + style: form + lang: + description: The optional lang parameter instructs the server return a response + in a certain language, if supported. If the language is not among the available + values, the Accept-Language header language will be used if it is supported. + If the header is missing, the default server language is used. Note that providers + may only support a single language (or often no language at all), that can + be different from the server language. Language strings can be written in + a complex (e.g. "fr-CA,fr;q=0.9,en-US;q=0.8,en;q=0.7"), simple (e.g. "de") + or locale-like (e.g. "de-CH" or "fr_BE") fashion. + in: query + name: lang + required: false + schema: + default: en-US + enum: + - en-US + - fr-CA + type: string + properties: + description: The properties that should be included for each feature. The parameter + value is a comma-separated list of property names. + explode: false + in: query + name: properties + required: false + schema: + items: + type: string + type: array + style: form + skipGeometry: + description: This option can be used to skip response geometries for each feature. + explode: false + in: query + name: skipGeometry + required: false + schema: + default: false + type: boolean + style: form + startindex: + description: The optional startindex parameter indicates the index within the + result set from which the server shall begin presenting results in the response + document. The first element has an index of 0 (default). + explode: false + in: query + name: startindex + required: false + schema: + default: 0 + minimum: 0 + type: integer + style: form + responses: + '200': + description: successful operation + Queryables: + content: + application/json: + schema: + $ref: '#/components/schemas/queryables' + description: successful queryables operation + default: + content: + application/json: + schema: + $ref: http://schemas.opengis.net/ogcapi/processes/part1/1.0/openapi/schemas/exception.yaml + description: Unexpected error + schemas: + queryable: + properties: + description: + description: a human-readable narrative describing the queryable + type: string + language: + default: + - en + description: the language used for the title and description + type: string + queryable: + description: the token that may be used in a CQL predicate + type: string + title: + description: a human readable title for the queryable + type: string + type: + description: the data type of the queryable + type: string + type-ref: + description: a reference to the formal definition of the type + format: url + type: string + required: + - queryable + - type + type: object + queryables: + properties: + queryables: + items: + $ref: '#/components/schemas/queryable' + type: array + required: + - queryables + type: object +info: + contact: + email: you@example.org + name: Organization Name + url: https://pygeoapi.io + description: pygeoapi provides an API to geospatial data + license: + name: CC-BY 4.0 license + url: https://creativecommons.org/licenses/by/4.0/ + termsOfService: https://creativecommons.org/licenses/by/4.0/ + title: pygeoapi default instance + version: 0.13.dev0 + x-keywords: + - geospatial + - data + - api +openapi: 3.0.2 +paths: + /: + get: + description: Landing page + operationId: getLandingPage + parameters: + - $ref: '#/components/parameters/f' + - $ref: '#/components/parameters/lang' + responses: + '200': + $ref: http://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/LandingPage + '400': + $ref: http://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/InvalidParameter + '500': + $ref: http://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/ServerError + summary: Landing page + tags: + - server + /collections: + get: + description: Collections + operationId: getCollections + parameters: + - $ref: '#/components/parameters/f' + - $ref: '#/components/parameters/lang' + responses: + '200': + $ref: http://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/Collections + '400': + $ref: http://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/InvalidParameter + '500': + $ref: http://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/ServerError + summary: Collections + tags: + - server + /collections/canada-metadata: + get: + description: Sample metadata records from open.canada.ca + operationId: describeCanada-metadataCollection + parameters: + - $ref: '#/components/parameters/f' + - $ref: '#/components/parameters/lang' + responses: + '200': + $ref: http://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/Collection + '400': + $ref: http://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/InvalidParameter + '404': + $ref: http://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/NotFound + '500': + $ref: http://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/ServerError + summary: Get Open Canada sample data metadata + tags: + - canada-metadata + /collections/canada-metadata/items: + get: + description: Sample metadata records from open.canada.ca + operationId: getCanada-metadataFeatures + parameters: + - &id001 + description: The optional f parameter indicates the output format which the + server shall provide as part of the response document. The default format + is GeoJSON. + explode: false + in: query + name: f + required: false + schema: + default: json + enum: + - json + - html + - jsonld + - csv + type: string + style: form + - &id002 + description: The optional lang parameter instructs the server return a response + in a certain language, if supported. If the language is not among the available + values, the Accept-Language header language will be used if it is supported. + If the header is missing, the default server language is used. Note that + providers may only support a single language (or often no language at all), + that can be different from the server language. Language strings can be + written in a complex (e.g. "fr-CA,fr;q=0.9,en-US;q=0.8,en;q=0.7"), simple + (e.g. "de") or locale-like (e.g. "de-CH" or "fr_BE") fashion. + in: query + name: lang + required: false + schema: + default: en-US + enum: + - en-US + - fr-CA + type: string + - $ref: http://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/parameters/bbox + - $ref: http://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/parameters/limit + - description: The properties that should be included for each feature. The + parameter value is a comma-separated list of property names. + explode: false + in: query + name: properties + required: false + schema: + items: + enum: + - recordCreated + - recordUpdated + - type + - title + - description + - contactPoint + - associations + - externalId + - themes + - q + type: string + type: array + style: form + - $ref: '#/components/parameters/skipGeometry' + - $ref: https://raw.githubusercontent.com/opengeospatial/ogcapi-records/master/core/openapi/parameters/sortby.yaml + - $ref: '#/components/parameters/startindex' + - $ref: https://raw.githubusercontent.com/opengeospatial/ogcapi-records/master/core/openapi/parameters/q.yaml + - $ref: http://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/parameters/datetime + - explode: false + in: query + name: recordCreated + required: false + schema: + type: string + style: form + - explode: false + in: query + name: recordUpdated + required: false + schema: + type: string + style: form + - explode: false + in: query + name: type + required: false + schema: + type: string + style: form + - explode: false + in: query + name: title + required: false + schema: + type: string + style: form + - explode: false + in: query + name: description + required: false + schema: + type: string + style: form + - explode: false + in: query + name: contactPoint + required: false + schema: + type: string + style: form + - explode: false + in: query + name: associations + required: false + schema: + type: string + style: form + - explode: false + in: query + name: externalId + required: false + schema: + type: string + style: form + - explode: false + in: query + name: themes + required: false + schema: + type: string + style: form + responses: + '200': + $ref: http://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/Features + '400': + $ref: http://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/InvalidParameter + '404': + $ref: http://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/NotFound + '500': + $ref: http://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/ServerError + summary: Get Open Canada sample data items + tags: + - canada-metadata + /collections/canada-metadata/items/{featureId}: + get: + description: Sample metadata records from open.canada.ca + operationId: getCanada-metadataFeature + parameters: + - $ref: http://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/parameters/featureId + - $ref: '#/components/parameters/f' + - $ref: '#/components/parameters/lang' + responses: + '200': + $ref: http://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/Feature + '400': + $ref: http://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/InvalidParameter + '404': + $ref: http://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/NotFound + '500': + $ref: http://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/ServerError + summary: Get Open Canada sample data item by id + tags: + - canada-metadata + /collections/canada-metadata/queryables: + get: + description: Sample metadata records from open.canada.ca + operationId: getCanada-metadataQueryables + parameters: + - *id001 + - *id002 + responses: + '200': + $ref: '#/components/responses/Queryables' + '400': + $ref: http://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/InvalidParameter + '404': + $ref: http://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/NotFound + '500': + $ref: http://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/ServerError + summary: Get Open Canada sample data queryables + tags: + - canada-metadata + /collections/gdps-temperature: + get: + description: Global Deterministic Prediction System sample + operationId: describeGdps-temperatureCollection + parameters: + - $ref: '#/components/parameters/f' + - $ref: '#/components/parameters/lang' + responses: + '200': + $ref: http://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/Collection + '400': + $ref: http://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/InvalidParameter + '404': + $ref: http://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/NotFound + '500': + $ref: http://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/ServerError + summary: Get Global Deterministic Prediction System sample metadata + tags: + - gdps-temperature + /collections/gdps-temperature/coverage: + get: + description: Global Deterministic Prediction System sample + operationId: getGdps-temperatureCoverage + parameters: + - *id001 + - *id002 + responses: + '200': + $ref: http://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/Features + '400': + $ref: http://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/InvalidParameter + '404': + $ref: http://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/NotFound + '500': + $ref: http://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/ServerError + summary: Get Global Deterministic Prediction System sample coverage + tags: + - gdps-temperature + /collections/gdps-temperature/coverage/domainset: + get: + description: Global Deterministic Prediction System sample + operationId: getGdps-temperatureCoverageDomainSet + parameters: + - *id001 + - *id002 + responses: + '200': + $ref: https://raw.githubusercontent.com/tomkralidis/ogcapi-coverages-1/fix-cis/yaml-unresolved/schemas/cis_1.1/domainSet.yaml + '400': + $ref: http://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/InvalidParameter + '404': + $ref: http://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/NotFound + '500': + $ref: http://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/ServerError + summary: Get Global Deterministic Prediction System sample coverage domain set + tags: + - gdps-temperature + /collections/gdps-temperature/coverage/rangetype: + get: + description: Global Deterministic Prediction System sample + operationId: getGdps-temperatureCoverageRangeType + parameters: + - *id001 + - *id002 + responses: + '200': + $ref: https://raw.githubusercontent.com/tomkralidis/ogcapi-coverages-1/fix-cis/yaml-unresolved/schemas/cis_1.1/rangeType.yaml + '400': + $ref: http://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/InvalidParameter + '404': + $ref: http://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/NotFound + '500': + $ref: http://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/ServerError + summary: Get Global Deterministic Prediction System sample coverage range type + tags: + - gdps-temperature + /collections/lakes: + get: + description: lakes of the world, public domain + operationId: describeLakesCollection + parameters: + - $ref: '#/components/parameters/f' + - $ref: '#/components/parameters/lang' + responses: + '200': + $ref: http://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/Collection + '400': + $ref: http://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/InvalidParameter + '404': + $ref: http://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/NotFound + '500': + $ref: http://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/ServerError + summary: Get Large Lakes metadata + tags: + - lakes + /collections/lakes/items: + get: + description: lakes of the world, public domain + operationId: getLakesFeatures + parameters: + - *id001 + - *id002 + - $ref: http://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/parameters/bbox + - $ref: http://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/parameters/limit + - description: The properties that should be included for each feature. The + parameter value is a comma-separated list of property names. + explode: false + in: query + name: properties + required: false + schema: + items: + enum: + - id + - scalerank + - name + - name_alt + - admin + - featureclass + type: string + type: array + style: form + - $ref: '#/components/parameters/skipGeometry' + - $ref: https://raw.githubusercontent.com/opengeospatial/ogcapi-records/master/core/openapi/parameters/sortby.yaml + - $ref: '#/components/parameters/startindex' + - explode: false + in: query + name: id + required: false + schema: + type: string + style: form + - explode: false + in: query + name: scalerank + required: false + schema: + type: string + style: form + - explode: false + in: query + name: name + required: false + schema: + type: string + style: form + - explode: false + in: query + name: name_alt + required: false + schema: + type: string + style: form + - explode: false + in: query + name: admin + required: false + schema: + type: string + style: form + - explode: false + in: query + name: featureclass + required: false + schema: + type: string + style: form + responses: + '200': + $ref: http://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/Features + '400': + $ref: http://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/InvalidParameter + '404': + $ref: http://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/NotFound + '500': + $ref: http://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/ServerError + summary: Get Large Lakes items + tags: + - lakes + /collections/lakes/items/{featureId}: + get: + description: lakes of the world, public domain + operationId: getLakesFeature + parameters: + - $ref: http://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/parameters/featureId + - $ref: '#/components/parameters/f' + - $ref: '#/components/parameters/lang' + responses: + '200': + $ref: http://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/Feature + '400': + $ref: http://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/InvalidParameter + '404': + $ref: http://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/NotFound + '500': + $ref: http://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/ServerError + summary: Get Large Lakes item by id + tags: + - lakes + /collections/lakes/queryables: + get: + description: lakes of the world, public domain + operationId: getLakesQueryables + parameters: + - *id001 + - *id002 + responses: + '200': + $ref: '#/components/responses/Queryables' + '400': + $ref: http://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/InvalidParameter + '404': + $ref: http://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/NotFound + '500': + $ref: http://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/ServerError + summary: Get Large Lakes queryables + tags: + - lakes + /collections/obs: + get: + description: My cool observations + operationId: describeObsCollection + parameters: + - $ref: '#/components/parameters/f' + - $ref: '#/components/parameters/lang' + responses: + '200': + $ref: http://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/Collection + '400': + $ref: http://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/InvalidParameter + '404': + $ref: http://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/NotFound + '500': + $ref: http://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/ServerError + summary: Get Observations metadata + tags: + - obs + /collections/obs/items: + get: + description: My cool observations + operationId: getObsFeatures + parameters: + - *id001 + - *id002 + - $ref: http://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/parameters/bbox + - $ref: http://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/parameters/limit + - description: The properties that should be included for each feature. The + parameter value is a comma-separated list of property names. + explode: false + in: query + name: properties + required: false + schema: + items: + enum: + - id + - stn_id + - datetime + - value + - lat + - long + type: string + type: array + style: form + - $ref: '#/components/parameters/skipGeometry' + - $ref: https://raw.githubusercontent.com/opengeospatial/ogcapi-records/master/core/openapi/parameters/sortby.yaml + - $ref: '#/components/parameters/startindex' + - explode: false + in: query + name: id + required: false + schema: + type: string + style: form + - explode: false + in: query + name: stn_id + required: false + schema: + type: string + style: form + - explode: false + in: query + name: datetime + required: false + schema: + type: string + style: form + - explode: false + in: query + name: value + required: false + schema: + type: string + style: form + - explode: false + in: query + name: lat + required: false + schema: + type: string + style: form + - explode: false + in: query + name: long + required: false + schema: + type: string + style: form + responses: + '200': + $ref: http://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/Features + '400': + $ref: http://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/InvalidParameter + '404': + $ref: http://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/NotFound + '500': + $ref: http://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/ServerError + summary: Get Observations items + tags: + - obs + /collections/obs/items/{featureId}: + get: + description: My cool observations + operationId: getObsFeature + parameters: + - $ref: http://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/parameters/featureId + - $ref: '#/components/parameters/f' + - $ref: '#/components/parameters/lang' + responses: + '200': + $ref: http://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/Feature + '400': + $ref: http://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/InvalidParameter + '404': + $ref: http://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/NotFound + '500': + $ref: http://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/ServerError + summary: Get Observations item by id + tags: + - obs + /collections/obs/queryables: + get: + description: My cool observations + operationId: getObsQueryables + parameters: + - *id001 + - *id002 + responses: + '200': + $ref: '#/components/responses/Queryables' + '400': + $ref: http://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/InvalidParameter + '404': + $ref: http://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/NotFound + '500': + $ref: http://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/ServerError + summary: Get Observations queryables + tags: + - obs + /conformance: + get: + description: API conformance definition + operationId: getConformanceDeclaration + parameters: + - $ref: '#/components/parameters/f' + - $ref: '#/components/parameters/lang' + responses: + '200': + $ref: http://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/ConformanceDeclaration + '400': + $ref: http://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/InvalidParameter + '500': + $ref: http://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/ServerError + summary: API conformance definition + tags: + - server + /openapi: + get: + description: This document + operationId: getOpenapi + parameters: + - $ref: '#/components/parameters/f' + - $ref: '#/components/parameters/lang' + - description: UI to render the OpenAPI document + explode: false + in: query + name: ui + required: false + schema: + default: swagger + enum: + - swagger + - redoc + type: string + style: form + responses: + '200': + $ref: '#/components/responses/200' + '400': + $ref: http://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/InvalidParameter + default: + $ref: '#/components/responses/default' + summary: This document + tags: + - server + /processes: + get: + description: Processes + operationId: getProcesses + parameters: + - $ref: '#/components/parameters/f' + responses: + '200': + $ref: http://schemas.opengis.net/ogcapi/processes/part1/1.0/openapi/responses/ProcessList.yaml + default: + $ref: '#/components/responses/default' + summary: Processes + tags: + - server + /processes/hello-world: + get: + description: An example process that takes a name as input, and echoes it back + as output. Intended to demonstrate a simple process with a single literal + input. + operationId: describeHello-worldProcess + parameters: + - $ref: '#/components/parameters/f' + responses: + '200': + $ref: '#/components/responses/200' + default: + $ref: '#/components/responses/default' + summary: Get process metadata + tags: + - hello-world + /processes/hello-world/execution: + post: + description: An example process that takes a name as input, and echoes it back + as output. Intended to demonstrate a simple process with a single literal + input. + operationId: executeHello-worldJob + requestBody: + content: + application/json: + example: + inputs: + message: An optional message. + name: World + schema: + $ref: http://schemas.opengis.net/ogcapi/processes/part1/1.0/openapi/schemas/execute.yaml + description: Mandatory execute request JSON + required: true + responses: + '200': + $ref: '#/components/responses/200' + '201': + $ref: http://schemas.opengis.net/ogcapi/processes/part1/1.0/openapi/responses/ExecuteAsync.yaml + '404': + $ref: http://schemas.opengis.net/ogcapi/processes/part1/1.0/openapi/responses/NotFound.yaml + '500': + $ref: http://schemas.opengis.net/ogcapi/processes/part1/1.0/openapi/responses/ServerError.yaml + default: + $ref: '#/components/responses/default' + summary: Process Hello World execution + tags: + - hello-world + /stac: + get: + description: SpatioTemporal Asset Catalog + operationId: getStacCatalog + parameters: [] + responses: + '200': + $ref: '#/components/responses/200' + default: + $ref: '#/components/responses/default' + summary: SpatioTemporal Asset Catalog + tags: + - stac +servers: +- description: pygeoapi provides an API to geospatial data + url: http://localhost:5000 +tags: +- description: pygeoapi provides an API to geospatial data + externalDocs: + description: information + url: https://example.org + name: server +- description: SpatioTemporal Asset Catalog + name: stac +- description: My cool observations + name: obs +- description: lakes of the world, public domain + name: lakes +- description: Global Deterministic Prediction System sample + name: gdps-temperature +- description: Sample metadata records from open.canada.ca + name: canada-metadata +- description: An example process that takes a name as input, and echoes it back as + output. Intended to demonstrate a simple process with a single literal input. + name: hello-world + diff --git a/pygeoapi/__init__.py b/pygeoapi/__init__.py index cbd6962..9d32872 100644 --- a/pygeoapi/__init__.py +++ b/pygeoapi/__init__.py @@ -43,6 +43,7 @@ def cli(): @cli.command() @click.option('--flask', 'server', flag_value="flask", default=True) @click.option('--starlette', 'server', flag_value="starlette") +@click.option('--django', 'server', flag_value="django") @click.pass_context def serve(ctx, server): """Run the server with different daemon type (--flask is the default)""" @@ -55,8 +56,11 @@ def serve(ctx, server): from pygeoapi.starlette_app import serve as serve_starlette ctx.forward(serve_starlette) ctx.invoke(serve_starlette) + elif server == "django": + from pygeoapi.django_app import main as serve_django + ctx.invoke(serve_django) else: - raise click.ClickException('--flask/--starlette is required') + raise click.ClickException('--flask/--starlette/--django is required') cli.add_command(config) diff --git a/pygeoapi/api.py b/pygeoapi/api.py index b761f7d..35ca548 100644 --- a/pygeoapi/api.py +++ b/pygeoapi/api.py @@ -276,6 +276,8 @@ class APIRequest: self._path_info = request.scope['path'].strip('/') elif hasattr(request.headers, 'environ'): self._path_info = request.headers.environ['PATH_INFO'].strip('/') + elif hasattr(request, 'path_info'): + self._path_info = request.path_info # Extract locale from params or headers self._raw_locale, self._locale = self._get_locale(request.headers, @@ -309,17 +311,22 @@ class APIRequest: # Set data from Flask request api_req._data = request.data elif hasattr(request, 'body'): - try: - import nest_asyncio - nest_asyncio.apply() - # Set data from Starlette request after async - # coroutine completion - # TODO: this now blocks, but once Flask v2 with async support - # has been implemented, with_data() can become async too - loop = asyncio.get_event_loop() - api_req._data = loop.run_until_complete(request.body()) - except ModuleNotFoundError: - LOGGER.error("Module nest-asyncio not found") + if "django" in str(request.__class__): + # Set data from Django request + api_req._data = request.body + else: + try: + import nest_asyncio + nest_asyncio.apply() + # Set data from Starlette request after async + # coroutine completion + # TODO: + # this now blocks, but once Flask v2 with async support + # has been implemented, with_data() can become async too + loop = asyncio.get_event_loop() + api_req._data = loop.run_until_complete(request.body()) + except ModuleNotFoundError: + LOGGER.error("Module nest-asyncio not found") return api_req @staticmethod @@ -337,6 +344,12 @@ class APIRequest: elif hasattr(request, 'query_params'): # Return ImmutableMultiDict from Starlette request return request.query_params + elif hasattr(request, 'GET'): + # Return QueryDict from Django GET request + return request.GET + elif hasattr(request, 'POST'): + # Return QueryDict from Django GET request + return request.POST LOGGER.debug('No query parameters found') return {} diff --git a/pygeoapi/db.sqlite3 b/pygeoapi/db.sqlite3 new file mode 100644 index 0000000..e69de29 diff --git a/pygeoapi/django_app.py b/pygeoapi/django_app.py new file mode 100644 index 0000000..69f1b0c --- /dev/null +++ b/pygeoapi/django_app.py @@ -0,0 +1,43 @@ +#!/usr/bin/env python +"""Django's command-line utility for administrative tasks.""" +import os +import sys +from pathlib import Path + + +def config(): + from pygeoapi.util import yaml_load + + if not os.environ.get('PYGEOAPI_CONFIG'): + raise RuntimeError('PYGEOAPI_CONFIG environment variable not set') + + with open(os.environ.get('PYGEOAPI_CONFIG'), encoding='utf8') as fh: + CONFIG = yaml_load(fh) + + return CONFIG + + +def main(): + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'django_pygeoapi.settings') + django_app_path = Path(os.path.dirname(__file__)) + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) from exc + + CONFIG = config() + sys.argv = [str(django_app_path / "django_app.py"), + "runserver", + f"{CONFIG['server']['bind'].get('host')}:" + f"{CONFIG['server']['bind'].get('port')}"] + sys.path.append(str(django_app_path)) + + execute_from_command_line(sys.argv) + + +if __name__ == '__main__': + main() diff --git a/pygeoapi/django_pygeoapi/__init__.py b/pygeoapi/django_pygeoapi/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pygeoapi/django_pygeoapi/sample_project/manage.py b/pygeoapi/django_pygeoapi/sample_project/manage.py new file mode 100755 index 0000000..6af9e26 --- /dev/null +++ b/pygeoapi/django_pygeoapi/sample_project/manage.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python +"""Django's command-line utility for administrative tasks.""" +import os +import sys + + +def main(): + """Run administrative tasks.""" + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'sample_project.settings') + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) from exc + execute_from_command_line(sys.argv) + + +if __name__ == '__main__': + main() diff --git a/pygeoapi/django_pygeoapi/sample_project/pygeoapi-config.yml b/pygeoapi/django_pygeoapi/sample_project/pygeoapi-config.yml new file mode 100644 index 0000000..8b7515b --- /dev/null +++ b/pygeoapi/django_pygeoapi/sample_project/pygeoapi-config.yml @@ -0,0 +1,275 @@ +# ================================================================= +# +# Authors: Tom Kralidis +# +# Copyright (c) 2020 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. +# +# ================================================================= + +server: + bind: + host: 0.0.0.0 + port: 8000 + url: http://localhost:8000/sample-project + mimetype: application/json; charset=UTF-8 + encoding: utf-8 + gzip: false + languages: + # First language is the default language + - en-US + - fr-CA + # cors: true + pretty_print: true + limit: 10 + # templates: + # path: /path/to/Jinja2/templates + # static: /path/to/static/folder # css/js/img + map: + url: https://tile.openstreetmap.org/{z}/{x}/{y}.png + attribution: '© OpenStreetMap contributors' +# manager: +# name: TinyDB +# connection: /tmp/pygeoapi-process-manager.db +# output_dir: /tmp/ + # ogc_schemas_location: /opt/schemas.opengis.net + +logging: + level: ERROR + #logfile: /tmp/pygeoapi.log + +metadata: + identification: + title: + en: pygeoapi default instance + fr: instance par défaut de pygeoapi + description: + en: pygeoapi provides an API to geospatial data + fr: pygeoapi fournit une API aux données géospatiales + keywords: + en: + - geospatial + - data + - api + fr: + - géospatiale + - données + - api + keywords_type: theme + terms_of_service: https://creativecommons.org/licenses/by/4.0/ + url: https://example.org + license: + name: CC-BY 4.0 license + url: https://creativecommons.org/licenses/by/4.0/ + provider: + name: Organization Name + url: https://pygeoapi.io + contact: + name: Lastname, Firstname + position: Position Title + address: Mailing Address + city: City + stateorprovince: Administrative Area + postalcode: Zip or Postal Code + country: Country + phone: +xx-xxx-xxx-xxxx + fax: +xx-xxx-xxx-xxxx + email: you@example.org + url: Contact URL + hours: Mo-Fr 08:00-17:00 + instructions: During hours of service. Off on weekends. + role: pointOfContact + +resources: + obs: + type: collection + title: Observations + description: My cool observations + keywords: + - observations + - monitoring + context: + - datetime: https://schema.org/DateTime + - vocab: https://example.com/vocab# + stn_id: "vocab:stn_id" + value: "vocab:value" + links: + - type: text/csv + rel: canonical + title: data + href: https://github.com/mapserver/mapserver/blob/branch-7-0/msautotest/wxs/data/obs.csv + hreflang: en-US + - type: text/csv + rel: alternate + title: data + href: https://raw.githubusercontent.com/mapserver/mapserver/branch-7-0/msautotest/wxs/data/obs.csv + hreflang: en-US + extents: + spatial: + bbox: [-180,-90,180,90] + crs: http://www.opengis.net/def/crs/OGC/1.3/CRS84 + temporal: + begin: 2000-10-30T18:24:39Z + end: 2007-10-30T08:57:29Z + providers: + - type: feature + name: CSV + data: tests/data/obs.csv + id_field: id + geometry: + x_field: long + y_field: lat + + lakes: + type: collection + title: + en: Large Lakes + fr: Grands Lacs + description: + en: lakes of the world, public domain + fr: lacs du monde, domaine public + keywords: + en: + - lakes + - water bodies + fr: + - lacs + - plans d'eau + links: + - type: text/html + rel: canonical + title: information + href: http://www.naturalearthdata.com/ + hreflang: en-US + extents: + spatial: + bbox: [-180,-90,180,90] + crs: http://www.opengis.net/def/crs/OGC/1.3/CRS84 + temporal: + begin: 2011-11-11T11:11:11Z + end: null # or empty (either means open ended) + providers: + - type: feature + name: GeoJSON + data: tests/data/ne_110m_lakes.geojson + id_field: id + title_field: name + + gdps-temperature: + type: collection + title: Global Deterministic Prediction System sample + description: Global Deterministic Prediction System sample + keywords: + - gdps + - global + extents: + spatial: + bbox: [-180,-90,180,90] + crs: http://www.opengis.net/def/crs/OGC/1.3/CRS84 + links: + - type: text/html + rel: canonical + title: information + href: https://eccc-msc.github.io/open-data/msc-data/nwp_gdps/readme_gdps_en + hreflang: en-CA + providers: + - type: coverage + name: rasterio + data: tests/data/CMC_glb_TMP_TGL_2_latlon.15x.15_2020081000_P000.grib2 + options: + DATA_ENCODING: COMPLEX_PACKING + format: + name: GRIB + mimetype: application/x-grib2 + + test-data: + type: stac-collection + title: pygeoapi test data + description: pygeoapi test data + keywords: + - poi + - portugal + links: + - type: text/html + rel: canonical + title: information + href: https://github.com/geopython/pygeoapi/tree/master/tests/data + hreflang: en-US + extents: + spatial: + bbox: [-180,-90,180,90] + crs: http://www.opengis.net/def/crs/OGC/1.3/CRS84 + providers: + - type: stac + name: FileSystem + data: tests/data + file_types: + - .gpkg + - .sqlite + - .csv + - .grib2 + - .tif + - .shp + + canada-metadata: + type: collection + title: + en: Open Canada sample data + fr: Exemple de donn\u00e9es Canada Ouvert + description: + en: Sample metadata records from open.canada.ca + fr: Exemples d'enregistrements de m\u00e9tadonn\u00e9es sur ouvert.canada.ca + keywords: + en: + - canada + - open data + fr: + - canada + - donn\u00e9es ouvertes + links: + - type: text/html + rel: canonical + title: information + href: https://open.canada.ca/en/open-data + hreflang: en-CA + - type: text/html + rel: alternate + title: informations + href: https://ouvert.canada.ca/fr/donnees-ouvertes + hreflang: fr-CA + extents: + spatial: + bbox: [-180,-90,180,90] + crs: http://www.opengis.net/def/crs/OGC/1.3/CRS84 + providers: + - type: record + name: TinyDBCatalogue + data: tests/data/open.canada.ca/sample-records.tinydb + id_field: externalId + time_field: recordCreated + title_field: title + + hello-world: + type: process + processor: + name: HelloWorld diff --git a/pygeoapi/django_pygeoapi/sample_project/sample_project/__init__.py b/pygeoapi/django_pygeoapi/sample_project/sample_project/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pygeoapi/django_pygeoapi/sample_project/sample_project/asgi.py b/pygeoapi/django_pygeoapi/sample_project/sample_project/asgi.py new file mode 100644 index 0000000..8b03eb3 --- /dev/null +++ b/pygeoapi/django_pygeoapi/sample_project/sample_project/asgi.py @@ -0,0 +1,16 @@ +""" +ASGI config for sample_project project. + +It exposes the ASGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/3.2/howto/deployment/asgi/ +""" + +import os + +from django.core.asgi import get_asgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'sample_project.settings') + +application = get_asgi_application() diff --git a/pygeoapi/django_pygeoapi/sample_project/sample_project/settings.py b/pygeoapi/django_pygeoapi/sample_project/sample_project/settings.py new file mode 100644 index 0000000..f12c215 --- /dev/null +++ b/pygeoapi/django_pygeoapi/sample_project/sample_project/settings.py @@ -0,0 +1,129 @@ +""" +Django settings for sample_project project. + +Generated by 'django-admin startproject' using Django 3.2.12. + +For more information on this file, see +https://docs.djangoproject.com/en/3.2/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/3.2/ref/settings/ +""" + +from pathlib import Path +from pygeoapi.django_app import config + +# Build paths inside the project like this: BASE_DIR / 'subdir'. +BASE_DIR = Path(__file__).resolve().parent.parent + + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = 'django-insecure-um1sc7k4ovzdhp2r3kwz#%ta-l+kn$grk&9#7_(a0f)q$6u_ra' # noqa + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = True + +ALLOWED_HOSTS = ["localhost", "127.0.0.1"] + + +# Application definition + +INSTALLED_APPS = [ + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', +] + +MIDDLEWARE = [ + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', +] + +ROOT_URLCONF = 'sample_project.urls' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, +] + +WSGI_APPLICATION = 'sample_project.wsgi.application' + + +# Database +# https://docs.djangoproject.com/en/3.2/ref/settings/#databases + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': BASE_DIR / 'db.sqlite3', + } +} + + +# Password validation +# https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ + { + 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', # noqa + }, + { + 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', # noqa + }, + { + 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', # noqa + }, + { + 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', # noqa + }, +] + + +# Internationalization +# https://docs.djangoproject.com/en/3.2/topics/i18n/ + +LANGUAGE_CODE = 'en-us' + +TIME_ZONE = 'UTC' + +USE_I18N = True + +USE_L10N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/3.2/howto/static-files/ + +STATIC_URL = '/static/' + +# Default primary key field type +# https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field + +DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' + +# pygeoapi +PYGEOAPI_CONFIG = config() diff --git a/pygeoapi/django_pygeoapi/sample_project/sample_project/urls.py b/pygeoapi/django_pygeoapi/sample_project/sample_project/urls.py new file mode 100644 index 0000000..a890da5 --- /dev/null +++ b/pygeoapi/django_pygeoapi/sample_project/sample_project/urls.py @@ -0,0 +1,24 @@ +"""sample_project URL Configuration + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/3.2/topics/http/urls/ +Examples: +Function views + 1. Add an import: from my_app import views + 2. Add a URL to urlpatterns: path('', views.home, name='home') +Class-based views + 1. Add an import: from other_app.views import Home + 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') +Including another URLconf + 1. Import the include() function: from django.urls import include, path + 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) +""" +from django.contrib import admin +from django.urls import path, include + +from pygeoapi.django_pygeoapi import urls as pygeoapi_urls + +urlpatterns = [ + path('admin/', admin.site.urls), + path('sample-project/', include(pygeoapi_urls)), +] diff --git a/pygeoapi/django_pygeoapi/sample_project/sample_project/wsgi.py b/pygeoapi/django_pygeoapi/sample_project/sample_project/wsgi.py new file mode 100644 index 0000000..97a1db8 --- /dev/null +++ b/pygeoapi/django_pygeoapi/sample_project/sample_project/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for sample_project project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/3.2/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'sample_project.settings') + +application = get_wsgi_application() diff --git a/pygeoapi/django_pygeoapi/settings.py b/pygeoapi/django_pygeoapi/settings.py new file mode 100644 index 0000000..000ddca --- /dev/null +++ b/pygeoapi/django_pygeoapi/settings.py @@ -0,0 +1,133 @@ +""" +Django settings for django_pygeoapi project. + +Generated by 'django-admin startproject' using Django 2.2.18. + +For more information on this file, see +https://docs.djangoproject.com/en/2.2/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/2.2/ref/settings/ +""" + +import os +# pygeoapi specific +from pygeoapi.django_app import config + +# Build paths inside the project like this: os.path.join(BASE_DIR, ...) +BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/2.2/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = ")#cqm2jfato)gk((nm#%r7h&#n&aqy00_21pavfp=l8)4%cegb" + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = True + +ALLOWED_HOSTS = [] + + +# Application definition + +INSTALLED_APPS = [ + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.staticfiles", +] + +MIDDLEWARE = [ + "django.middleware.security.SecurityMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.common.CommonMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + "django.middleware.clickjacking.XFrameOptionsMiddleware", +] + +ROOT_URLCONF = "django_pygeoapi.urls" + +TEMPLATES = [ + { + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [], + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + "django.template.context_processors.debug", + "django.template.context_processors.request", + "django.contrib.auth.context_processors.auth", + "django.contrib.messages.context_processors.messages", + ], + }, + }, +] + +WSGI_APPLICATION = "django_pygeoapi.wsgi.application" + + +# Database +# https://docs.djangoproject.com/en/2.2/ref/settings/#databases + +DATABASES = { + "default": { + "ENGINE": "django.db.backends.sqlite3", + "NAME": os.path.join(BASE_DIR, "db.sqlite3"), + } +} + + +# Password validation +# https://docs.djangoproject.com/en/2.2/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ + { + "NAME": + "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", # noqa + }, + { + "NAME": + "django.contrib.auth.password_validation.MinimumLengthValidator", + }, + { + "NAME": + "django.contrib.auth.password_validation.CommonPasswordValidator", + }, + { + "NAME": + "django.contrib.auth.password_validation.NumericPasswordValidator", + }, +] + + +# Internationalization +# https://docs.djangoproject.com/en/2.2/topics/i18n/ + +LANGUAGE_CODE = "en-us" + +TIME_ZONE = "UTC" + +USE_I18N = True + +USE_L10N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/2.2/howto/static-files/ + +STATICFILES_DIRS = [ + os.path.join(BASE_DIR, "static") +] +STATIC_ROOT = os.path.join(BASE_DIR, "assets") +STATIC_URL = "/static/" + +# pygeoapi specific +PYGEOAPI_CONFIG = config() diff --git a/pygeoapi/django_pygeoapi/urls.py b/pygeoapi/django_pygeoapi/urls.py new file mode 100644 index 0000000..29c1e3f --- /dev/null +++ b/pygeoapi/django_pygeoapi/urls.py @@ -0,0 +1,150 @@ +"""django_pygeoapi URL Configuration + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/2.2/topics/http/urls/ +Examples: +Function views + 1. Add an import: from my_app import views + 2. Add a URL to urlpatterns: path('', views.home, name='home') +Class-based views + 1. Add an import: from other_app.views import Home + 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') +Including another URLconf + 1. Import the include() function: from django.urls import include, path + 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) +""" + +from django.urls import ( + path, +) +from django.conf import settings +from django.conf.urls.static import static + +from . import views + +urlpatterns = [ + path("", views.landing_page, name="landing-page"), + path("openapi/", views.openapi, name="openapi"), + path("conformance/", views.conformance, name="conformance"), + path("collections/", views.collections, name="collections"), + path( + "collections/", + views.collections, + name="collection-detail", + ), + path( + "collections//queryables/", + views.collection_queryables, + name="collection-queryables", + ), + path( + "collections//items/", + views.collection_items, + name="collection-items", + ), + path( + "collections//items/", + views.collection_item, + name="collection-item", + ), + path( + "collections//coverage/", + views.collection_coverage, + name="collection-coverage", + ), + path( + "collections//coverage/domainset/", + views.collection_coverage_domainset, + name="collection-coverage-domainset", + ), + path( + "collections//coverage/rangetype/", + views.collection_coverage_rangetype, + name="collection-coverage-rangetype", + ), + path( + "collections//tiles/", + views.collection_tiles, + name="collection-tiles", + ), + path( + "collections//tiles//metadata", + views.collection_tiles_metadata, + name="collection-tiles-metadata", + ), + path( + "collections//tiles/\ + ///", + views.collection_item_tiles, + name="collection-item-tiles", + ), + path( + "collections//position", + views.get_collection_edr_query, + name="collection-edr-position", + ), + path( + "collections//area", + views.get_collection_edr_query, + name="collection-edr-area", + ), + path( + "collections//cube", + views.get_collection_edr_query, + name="collection-edr-cube", + ), + path( + "collections//trajectory", + views.get_collection_edr_query, + name="collection-edr-trajectory", + ), + path( + "collections//corridor", + views.get_collection_edr_query, + name="collection-edr-corridor", + ), + path( + "collections//instances//position", + views.get_collection_edr_query, + name="collection-edr-instance-position", + ), + path( + "collections//instances//area", + views.get_collection_edr_query, + name="collection-edr-instance-area", + ), + path( + "collections//instances//cube", + views.get_collection_edr_query, + name="collection-edr-instance-cube", + ), + path( + "collections//instances//trajectory", # noqa + views.get_collection_edr_query, + name="collection-edr-instance-trajectory", + ), + path( + "collections//instances//corridor", + views.get_collection_edr_query, + name="collection-edr-instance-corridor", + ), + path("processes/", views.processes, name="processes"), + path("processes/", views.processes, name="process-detail"), + path("jobs/", views.jobs, name="jobs"), + path("jobs/", views.jobs, name="job"), + path( + "jobs//results/", + views.job_results, + name="job-results", + ), + path( + "jobs//results/", + views.job_results_resource, + name="job-results-resource", + ), + path("stac/", views.stac_catalog_root, name="stac-catalog-root"), + path("stac/", views.stac_catalog_path, name="stac-catalog-path"), + path( + "stac/search/", views.stac_catalog_search, name="stac-catalog-search" + ), +] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) diff --git a/pygeoapi/django_pygeoapi/views.py b/pygeoapi/django_pygeoapi/views.py new file mode 100644 index 0000000..70b4ad7 --- /dev/null +++ b/pygeoapi/django_pygeoapi/views.py @@ -0,0 +1,449 @@ +"""Integration module for Django""" +from typing import Tuple, Dict, Mapping, Optional +from django.conf import settings +from django.http import HttpRequest, HttpResponse +from pygeoapi.api import API +from pygeoapi.openapi import get_oas + + +def landing_page(request: HttpRequest) -> HttpResponse: + """ + OGC API landing page endpoint + + :request Django HTTP Request + + :returns: Django HTTP Response + """ + + response_ = _feed_response(request, "landing_page") + response = _to_django_response(*response_) + + return response + + +def openapi(request: HttpRequest) -> HttpResponse: + """ + OpenAPI endpoint + + :request Django HTTP Request + + :returns: Django HTTP Response + """ + + openapi_config = get_oas(settings.PYGEOAPI_CONFIG) + response_ = _feed_response(request, "openapi", openapi_config) + response = _to_django_response(*response_) + + return response + + +def conformance(request: HttpRequest) -> HttpResponse: + """ + OGC API conformance endpoint + + :request Django HTTP Request + + :returns: Django HTTP Response + """ + + response_ = _feed_response(request, "conformance") + response = _to_django_response(*response_) + + return response + + +def collections( + request: HttpRequest, + collection_id: Optional[str] = None, +) -> HttpResponse: + """ + OGC API collections endpoint + + :request Django HTTP Request + :param collection_id: collection identifier + + :returns: Django HTTP Response + """ + + response_ = _feed_response(request, "describe_collections", collection_id) + response = _to_django_response(*response_) + + return response + + +def collection_queryables( + request: HttpRequest, + collection_id: Optional[str] = None, +) -> HttpResponse: + """ + OGC API collections queryables endpoint + + :request Django HTTP Request + :param collection_id: collection identifier + + :returns: Django HTTP Response + """ + + response_ = _feed_response( + request, "get_collection_queryables", collection_id + ) + response = _to_django_response(*response_) + + return response + + +def collection_items( + request: HttpRequest, + collection_id: str, +) -> HttpResponse: + """ + OGC API collections items endpoint + + :request Django HTTP Request + :param collection_id: collection identifier + + :returns: Django HTTP response + """ + + response_ = _feed_response( + request, + "get_collection_items", + collection_id, + ) + response = _to_django_response(*response_) + + return response + + +def collection_item( + request: HttpRequest, + collection_id: str, + item_id: str, +) -> HttpResponse: + """ + OGC API collections items endpoint + + :request Django HTTP Request + :param collection_id: collection identifier + :param item_id: item identifier + + :returns: Django HTTP response + """ + + response_ = _feed_response( + request, "get_collection_item", collection_id, item_id + ) + response = _to_django_response(*response_) + + return response + + +def collection_coverage( + request: HttpRequest, + collection_id: str, +) -> HttpResponse: + """ + OGC API - Coverages coverage endpoint + + :request Django HTTP Request + :param collection_id: collection identifier + + :returns: Django HTTP response + """ + + response_ = _feed_response( + request, "get_collection_coverage", collection_id + ) + response = _to_django_response(*response_) + + return response + + +def collection_coverage_domainset( + request: HttpRequest, + collection_id: str, +) -> HttpResponse: + """ + OGC API - Coverages coverage domainset endpoint + + :request Django HTTP Request + :param collection_id: collection identifier + + :returns: Django HTTP response + """ + + response_ = _feed_response( + request, "get_collection_coverage_domainset", collection_id + ) + response = _to_django_response(*response_) + + return response + + +def collection_coverage_rangetype( + request: HttpRequest, + collection_id: str, +) -> HttpResponse: + """ + OGC API - Coverages coverage rangetype endpoint + + :request Django HTTP Request + :param collection_id: collection identifier + + :returns: Django HTTP response + """ + + response_ = _feed_response( + request, "get_collection_coverage_rangetype", collection_id + ) + response = _to_django_response(*response_) + + return response + + +def collection_tiles( + request: HttpRequest, + collection_id: str, +) -> HttpResponse: + """ + OGC API - Tiles collection tiles endpoint + + :request Django HTTP Request + :param collection_id: collection identifier + + :returns: Django HTTP response + """ + + response_ = _feed_response(request, "get_collection_tiles", collection_id) + response = _to_django_response(*response_) + + return response + + +def collection_tiles_metadata( + request: HttpRequest, + collection_id: str, + tileMatrixSetId: str, +) -> HttpResponse: + """ + OGC API - Tiles collection tiles metadata endpoint + + :request Django HTTP Request + :param collection_id: collection identifier + :param tileMatrixSetId: identifier of tile matrix set + + :returns: Django HTTP response + """ + + response_ = _feed_response( + request, + "get_collection_tiles_metadata", + collection_id, + tileMatrixSetId, + ) + response = _to_django_response(*response_) + + return response + + +def collection_item_tiles( + request: HttpRequest, + collection_id: str, + tileMatrixSetId: str, + tileMatrix: str, + tileRow: str, + tileCol: str, +) -> HttpResponse: + """ + OGC API - Tiles collection tiles data endpoint + + :request Django HTTP Request + :param collection_id: collection identifier + :param tileMatrixSetId: identifier of tile matrix set + :param tileMatrix: identifier of {z} matrix index + :param tileRow: identifier of {y} matrix index + :param tileCol: identifier of {x} matrix index + + :returns: Django HTTP response + """ + + response_ = _feed_response( + request, + "get_collection_tiles_metadata", + collection_id, + tileMatrixSetId, + tileMatrix, + tileRow, + tileCol, + ) + response = _to_django_response(*response_) + + return response + + +def processes( + request: HttpRequest, + process_id: Optional[str] = None, +) -> HttpResponse: + """ + OGC API - Processes description endpoint + + :request Django HTTP Request + :param process_id: process identifier + + :returns: Django HTTP response + """ + + response_ = _feed_response(request, "describe_processes", process_id) + response = _to_django_response(*response_) + + return response + + +def jobs( + request: HttpRequest, + job_id: Optional[str] = None, +) -> HttpResponse: + """ + OGC API - Jobs endpoint + + :request Django HTTP Request + :param process_id: process identifier + :param job_id: job identifier + + :returns: Django HTTP response + """ + response_ = _feed_response(request, "get_jobs", job_id) + response = _to_django_response(*response_) + + return response + + +def job_results( + request: HttpRequest, + job_id: Optional[str] = None, +) -> HttpResponse: + """ + OGC API - Job result endpoint + + :request Django HTTP Request + :param job_id: job identifier + + :returns: Django HTTP response + """ + response_ = _feed_response(request, "get_job_result", job_id) + response = _to_django_response(*response_) + + return response + + +def job_results_resource( + request: HttpRequest, + process_id: str, + job_id: str, + resource: str, +) -> HttpResponse: + """ + OGC API - Job result resource endpoint + + :request Django HTTP Request + :param job_id: job identifier + :param resource: job resource + + :returns: Django HTTP response + """ + response_ = _feed_response( + request, + "get_job_result_resource", + job_id, + resource + ) + response = _to_django_response(*response_) + + return response + + +def get_collection_edr_query( + request: HttpRequest, + collection_id: str, + instance_id: str, +) -> HttpResponse: + """ + OGC API - EDR endpoint + + :request Django HTTP Request + :param job_id: job identifier + :param resource: job resource + + :returns: Django HTTP response + """ + query_type = request.path.split('/')[-1] + response_ = _feed_response( + request, + "get_collection_edr_query", + collection_id, + instance_id, + query_type + ) + response = _to_django_response(*response_) + + return response + + +def stac_catalog_root(request: HttpRequest) -> HttpResponse: + """ + STAC root endpoint + + :request Django HTTP Request + + :returns: Django HTTP response + """ + + response_ = _feed_response(request, "get_stac_root") + response = _to_django_response(*response_) + + return response + + +def stac_catalog_path( + request: HttpRequest, + path: str, +) -> HttpResponse: + """ + STAC path endpoint + + :request Django HTTP Request + :param path: path + + :returns: Django HTTP response + """ + + response_ = _feed_response(request, "get_stac_path", path) + response = _to_django_response(*response_) + + return response + + +def stac_catalog_search(request: HttpRequest) -> HttpResponse: + pass + + +def _feed_response( + request: HttpRequest, api_definition: str, *args, **kwargs +) -> Tuple[Dict, int, str]: + """Use pygeoapi api to process the input request""" + api_ = API(settings.PYGEOAPI_CONFIG) + api = getattr(api_, api_definition) + return api(request, *args, **kwargs) + + +def _to_django_response( + headers: Mapping, + status_code: int, + content: str, +) -> HttpResponse: + """Convert API payload to a django response""" + response = HttpResponse(content, status=status_code) + for key, value in headers.items(): + response[key] = value + return response diff --git a/pygeoapi/django_pygeoapi/wsgi.py b/pygeoapi/django_pygeoapi/wsgi.py new file mode 100644 index 0000000..3407543 --- /dev/null +++ b/pygeoapi/django_pygeoapi/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for django_pygeoapi project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/2.2/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "django_pygeoapi.settings") + +application = get_wsgi_application() diff --git a/requirements-django.txt b/requirements-django.txt new file mode 100644 index 0000000..efb1e80 --- /dev/null +++ b/requirements-django.txt @@ -0,0 +1,2 @@ +Django <= 4, > 3 +pydantic