Skip to content

Commit 9d60950

Browse files
authored
Support GeoJSON-LD templating (#1927)
* Support JSON-LD template for items * Create `item_list.jsonld` template Create `item_list.jsonld` template to backport current /items json-ld content offerings * Reduce number of times templated geojsonld is serialized * Add test for linked_data.py * Use json serializer in test * Run test in CI * Add test for single item * Use single quotes * Fix flake8 * Use collection level template for GeoJSON-LD * Un-nest JSON-LD context from resource config * Respond to PR feedback * Update test_linked_data.py * Revert "Un-nest JSON-LD context from resource config" This reverts commit 5b371b2. * Update documentation
1 parent 3a9d853 commit 9d60950

File tree

13 files changed

+242
-46
lines changed

13 files changed

+242
-46
lines changed

.github/workflows/main.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@ jobs:
128128
pytest tests/test_esri_provider.py
129129
pytest tests/test_filesystem_provider.py
130130
pytest tests/test_geojson_provider.py
131+
pytest tests/test_linked_data.py
131132
pytest tests/test_mongo_provider.py
132133
pytest tests/test_ogr_csv_provider.py
133134
pytest tests/test_ogr_esrijson_provider.py

docs/source/configuration.rst

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,6 @@ default.
197197
- observations
198198
- monitoring
199199
linked-data: # linked data configuration (see Linked Data section)
200-
item_template: tests/data/base.jsonld
201200
context:
202201
- datetime: https://schema.org/DateTime
203202
- vocab: https://example.com/vocab#
@@ -646,23 +645,19 @@ This relationship can further be maintained in the JSON-LD structured data using
646645
ssn: "http://www.w3.org/ns/ssn/"
647646
Datastream: sosa:isMemberOf
648647
649-
Sometimes, the JSON-LD desired for an individual feature in a collection is more complicated than can be achieved by
650-
aliasing properties using a context. In this case, it is possible to specify a Jinja2 template. When ``item_template``
651-
is defined for a feature collection, the json-ld prepared by pygeoapi will be used to render the Jinja2 template
652-
specified by the path. The path specified can be absolute or relative to pygeoapi's template folder. For even more
653-
deployment flexibility, the path can be specified with string interpolation of environment variables.
648+
Sometimes, the JSON-LD desired for an individual feature in a collection is more complicated than can
649+
be achieved by aliasing properties using a context. In this case, it is possible to implement a custom
650+
Jinja2 template. GeoJSON-LD is rendered using the Jinja2 templates defined in ``collections/items/item.jsonld``
651+
and ``collections/items/index.jsonld``. A pygeoapi collection requiring custom GeoJSON-LD can overwrite these
652+
templates using dataset level templating. To learn more about Jinja2 templates, see :ref:`html-templating`.
654653

655654

656655
.. code-block:: yaml
657656
658657
linked-data:
659-
item_template: tests/data/base.jsonld
660658
context:
661659
- datetime: https://schema.org/DateTime
662660
663-
.. note::
664-
The template ``tests/data/base.jsonld`` renders the unmodified JSON-LD. For more information on the capacities
665-
of Jinja2 templates, see :ref:`html-templating`.
666661
667662
Validating the configuration
668663
----------------------------

pygeoapi/api/itemtypes.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -648,6 +648,8 @@ def get_collection_items(
648648
api, content, dataset, id_field=(p.uri_field or 'id')
649649
)
650650

651+
return headers, HTTPStatus.OK, content
652+
651653
return headers, HTTPStatus.OK, to_json(content, api.pretty_print)
652654

653655

@@ -930,6 +932,8 @@ def get_collection_item(api: API, request: APIRequest,
930932
api, content, dataset, uri, (p.uri_field or 'id')
931933
)
932934

935+
return headers, HTTPStatus.OK, content
936+
933937
return headers, HTTPStatus.OK, to_json(content, api.pretty_print)
934938

935939

pygeoapi/linked_data.py

Lines changed: 32 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,10 @@
3131
Returns content as linked data representations
3232
"""
3333

34-
import json
3534
import logging
3635
from typing import Callable
3736

38-
from pygeoapi.util import is_url, render_j2_template
37+
from pygeoapi.util import is_url, render_j2_template, url_join
3938
from pygeoapi import l10n
4039
from shapely.geometry import shape
4140
from shapely.ops import unary_union
@@ -189,30 +188,22 @@ def geojson2jsonld(cls, data: dict, dataset: str,
189188
:returns: string of rendered JSON (GeoJSON-LD)
190189
"""
191190

192-
LOGGER.debug('Fetching context and template from resource configuration')
193-
jsonld = cls.config['resources'][dataset].get('linked-data', {})
194-
ds_url = f"{cls.get_collections_url()}/{dataset}"
195-
196-
context = jsonld.get('context', []).copy()
197-
template = jsonld.get('item_template', None)
191+
LOGGER.debug('Fetching context from resource configuration')
192+
context = cls.config['resources'][dataset].get('context', []).copy()
193+
templates = cls.get_dataset_templates(dataset)
198194

199195
defaultVocabulary = {
200196
'schema': 'https://schema.org/',
197+
'gsp': 'http://www.opengis.net/ont/geosparql#',
201198
'type': '@type'
202199
}
203200

204201
if identifier:
205-
# Single jsonld
206-
defaultVocabulary.update({
207-
'gsp': 'http://www.opengis.net/ont/geosparql#'
208-
})
209-
210202
# Expand properties block
211203
data.update(data.pop('properties'))
212204

213205
# Include multiple geometry encodings
214206
if (data.get('geometry') is not None):
215-
data['type'] = 'schema:Place'
216207
jsonldify_geometry(data)
217208

218209
data['@id'] = identifier
@@ -224,6 +215,7 @@ def geojson2jsonld(cls, data: dict, dataset: str,
224215
'FeatureCollection': 'schema:itemList'
225216
})
226217

218+
ds_url = url_join(cls.get_collections_url(), dataset)
227219
data['@id'] = ds_url
228220

229221
for i, feature in enumerate(data['features']):
@@ -233,9 +225,15 @@ def geojson2jsonld(cls, data: dict, dataset: str,
233225
if not is_url(str(identifier_)):
234226
identifier_ = f"{ds_url}/items/{feature['id']}" # noqa
235227

228+
# Include multiple geometry encodings
229+
if feature.get('geometry') is not None:
230+
jsonldify_geometry(feature)
231+
236232
data['features'][i] = {
237233
'@id': identifier_,
238-
'type': 'schema:Place'
234+
'type': 'schema:Place',
235+
**feature.pop('properties'),
236+
**feature
239237
}
240238

241239
if data.get('timeStamp', False):
@@ -248,17 +246,21 @@ def geojson2jsonld(cls, data: dict, dataset: str,
248246
**data
249247
}
250248

251-
if None in (template, identifier):
252-
return ldjsonData
249+
if identifier:
250+
# Render jsonld template for single item
251+
LOGGER.debug('Rendering JSON-LD item template')
252+
content = render_j2_template(
253+
cls.tpl_config, templates,
254+
'collections/items/item.jsonld', ldjsonData)
255+
253256
else:
254-
# Render jsonld template for single item with template configured
255-
LOGGER.debug(f'Rendering JSON-LD template: {template}')
257+
# Render jsonld template for /items
258+
LOGGER.debug('Rendering JSON-LD items template')
256259
content = render_j2_template(
257-
cls.config, cls.config['server']['templates'],
258-
template, ldjsonData)
260+
cls.tpl_config, templates,
261+
'collections/items/index.jsonld', ldjsonData)
259262

260-
ldjsonData = json.loads(content)
261-
return ldjsonData
263+
return content
262264

263265

264266
def jsonldify_geometry(feature: dict) -> None:
@@ -271,6 +273,8 @@ def jsonldify_geometry(feature: dict) -> None:
271273
:returns: None
272274
"""
273275

276+
feature['type'] = 'schema:Place'
277+
274278
geo = feature.get('geometry')
275279
geom = shape(geo)
276280

@@ -287,7 +291,11 @@ def jsonldify_geometry(feature: dict) -> None:
287291
}
288292

289293
# Schema geometry
290-
feature['schema:geo'] = geom2schemageo(geom)
294+
try:
295+
feature['schema:geo'] = geom2schemageo(geom)
296+
except AttributeError:
297+
msg = f'Unable to parse schema geometry for {feature["id"]}'
298+
LOGGER.warning(msg)
291299

292300

293301
def geom2schemageo(geom: shape) -> dict:

pygeoapi/schemas/config/pygeoapi-config-0.x.yml

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -366,9 +366,6 @@ properties:
366366
type: object
367367
description: linked data configuration
368368
properties:
369-
item_template:
370-
type: string
371-
description: path to JSON-LD Jinja2 template
372369
context:
373370
type: array
374371
description: additional JSON-LD context
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"@context": [
3+
{
4+
"schema": "https://schema.org/",
5+
"type": "@type",
6+
"features": "schema:itemListElement",
7+
"FeatureCollection": "schema:itemList"
8+
}
9+
],
10+
"type": "FeatureCollection",
11+
"@id": "{{ data["@id"] }}",
12+
{%- if data.features %}
13+
"features": [
14+
{%- for ft in data.features %}
15+
{
16+
"@type": "{{ ft.type }}",
17+
"@id": "{{ ft["@id"] }}"
18+
}
19+
{%- if not loop.last -%},{%- endif -%}
20+
{%- endfor %}
21+
]
22+
{%- endif %}
23+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{{ to_json(data, config.server.pretty_print) | safe }}

tests/api/test_itemtypes.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -548,8 +548,8 @@ def test_get_collection_items_json_ld(config, api_):
548548
assert '@context' in collection
549549
assert all((f in collection['@context'][0] for
550550
f in ('schema', 'type', 'features', 'FeatureCollection')))
551-
assert len(collection['@context']) > 1
552-
assert collection['@context'][1]['schema'] == 'https://schema.org/'
551+
assert len(collection['@context']) == 1
552+
assert collection['@context'][0]['schema'] == 'https://schema.org/'
553553
expanded = jsonld.expand(collection)[0]
554554
featuresUri = 'https://schema.org/itemListElement'
555555
assert len(expanded[featuresUri]) == 2

tests/data/base.jsonld

Lines changed: 0 additions & 1 deletion
This file was deleted.

tests/pygeoapi-test-config-apirules.yml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -293,8 +293,6 @@ resources:
293293
title: data source
294294
href: https://en.wikipedia.org/wiki/GeoJSON
295295
hreflang: en-US
296-
linked-data:
297-
item_template: tests/data/base.jsonld
298296
extents:
299297
spatial:
300298
bbox: [-180,-90,180,90]

0 commit comments

Comments
 (0)