Skip to content

Commit 27e9a57

Browse files
committed
add support for custom formatters
1 parent f64ddb7 commit 27e9a57

File tree

8 files changed

+82
-38
lines changed

8 files changed

+82
-38
lines changed

docs/source/configuration.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,10 @@ default.
251251
options: # optional options to pass to provider (i.e. GDAL creation)
252252
option_name: option_value
253253
include_extra_query_parameters: false # include extra query parameters that are not part of the collection properties (default: false)
254+
formatters: # list of 1..n formatter definitions
255+
- name: formatter
256+
attachment: true # whether or not to provide as an attachment or normal response
257+
geom: False # whether or not to include geometry
254258
255259
hello-world: # name of process
256260
type: process # REQUIRED (collection, process, or stac-collection)

docs/source/plugins.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -433,6 +433,7 @@ The below template provides a minimal example (let's call the file ``mycooljsonf
433433
434434
super().__init__({'name': 'cooljson', 'geom': None})
435435
self.mimetype = 'application/json; subtype:mycooljson'
436+
self.attachment = False
436437
437438
def write(self, options={}, data=None):
438439
"""custom writer"""

pygeoapi/api/itemtypes.py

Lines changed: 31 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737

3838
from collections import ChainMap
3939
from copy import deepcopy
40-
from datetime import datetime
40+
import datetime
4141
from http import HTTPStatus
4242
import logging
4343
from typing import Any, Tuple, Union, Optional
@@ -246,9 +246,6 @@ def get_collection_items(
246246
:returns: tuple of headers, status code, content
247247
"""
248248

249-
if not request.is_valid(PLUGINS['formatter'].keys()):
250-
return api.get_format_exception(request)
251-
252249
# Set Content-Language to system locale until provider locale
253250
# has been determined
254251
headers = request.get_response_headers(SYSTEM_LOCALE,
@@ -357,6 +354,18 @@ def get_collection_items(
357354
err.http_status_code, headers, request.format,
358355
err.ogc_exception_code, err.message)
359356

357+
LOGGER.debug('Validating requested format')
358+
dataset_formatters = {}
359+
for key, value in PLUGINS['formatter'].items():
360+
df2 = load_plugin('formatter', {'name': key})
361+
dataset_formatters[df2.name] = df2
362+
for df in collections[dataset].get('formatters', []):
363+
df2 = load_plugin('formatter', df)
364+
dataset_formatters[df2.name] = df2
365+
366+
if not request.is_valid(dataset_formatters.keys()):
367+
return api.get_format_exception(request)
368+
360369
crs_transform_spec = None
361370
if provider_type == 'feature':
362371
# crs query parameter is only available for OGC API - Features
@@ -586,6 +595,14 @@ def get_collection_items(
586595
'href': f'{uri}?f={F_HTML}{serialized_query_params}'
587596
}])
588597

598+
for key, value in dataset_formatters.items():
599+
content['links'].append({
600+
'type': value.mimetype,
601+
'rel': 'alternate',
602+
'title': f'This document as {key}',
603+
'href': f'{uri}?f={value.name}{serialized_query_params}'
604+
})
605+
589606
next_link = False
590607
prev_link = False
591608

@@ -630,7 +647,7 @@ def get_collection_items(
630647
'href': '/'.join(uri.split('/')[:-1])
631648
})
632649

633-
content['timeStamp'] = datetime.utcnow().strftime(
650+
content['timeStamp'] = datetime.datetime.now(datetime.UTC).strftime(
634651
'%Y-%m-%dT%H:%M:%S.%fZ')
635652

636653
# Set response language to requested provider locale
@@ -661,9 +678,8 @@ def get_collection_items(
661678
'collections/items/index.html',
662679
content, request.locale)
663680
return headers, HTTPStatus.OK, content
664-
elif request.format == 'csv': # render
665-
formatter = load_plugin('formatter',
666-
{'name': 'CSV', 'geom': True})
681+
elif request.format in dataset_formatters: # render
682+
formatter = dataset_formatters[request.format]
667683

668684
try:
669685
content = formatter.write(
@@ -682,13 +698,14 @@ def get_collection_items(
682698

683699
headers['Content-Type'] = formatter.mimetype
684700

685-
if p.filename is None:
686-
filename = f'{dataset}.csv'
687-
else:
688-
filename = f'{p.filename}'
701+
if formatter.attachment:
702+
if p.filename is None:
703+
filename = f'{dataset}.{formatter.extension}'
704+
else:
705+
filename = f'{p.filename}'
689706

690-
cd = f'attachment; filename="{filename}"'
691-
headers['Content-Disposition'] = cd
707+
cd = f'attachment; filename="{filename}"'
708+
headers['Content-Disposition'] = cd
692709

693710
return headers, HTTPStatus.OK, content
694711

pygeoapi/formatter/base.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,18 +44,18 @@ def __init__(self, formatter_def: dict):
4444
:returns: pygeoapi.formatter.base.BaseFormatter
4545
"""
4646

47+
self.extension = None
4748
self.mimetype = None
48-
self.geom = False
4949

5050
self.name = formatter_def['name']
51-
if 'geom' in formatter_def:
52-
self.geom = formatter_def['geom']
51+
self.geom = formatter_def.get('geom', False)
52+
self.attachment = formatter_def.get('attachment', False)
5353

5454
def write(self, options: dict = {}, data: dict | None = None) -> str:
5555
"""
5656
Generate data in specified format
5757
58-
:param options: CSV formatting options
58+
:param options: formatting options
5959
:param data: dict representation of GeoJSON object
6060
6161
:returns: string representation of format

pygeoapi/formatter/csv_.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,12 +48,11 @@ def __init__(self, formatter_def: dict):
4848
:returns: `pygeoapi.formatter.csv_.CSVFormatter`
4949
"""
5050

51-
geom = False
52-
if 'geom' in formatter_def:
53-
geom = formatter_def['geom']
51+
geom = formatter_def.get('geom', False)
5452

5553
super().__init__({'name': 'csv', 'geom': geom})
5654
self.mimetype = 'text/csv; charset=utf-8'
55+
self.extension = 'csv'
5756

5857
def write(self, options: dict = {}, data: dict = None) -> str:
5958
"""

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

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -581,6 +581,23 @@ properties:
581581
- type
582582
- name
583583
- data
584+
formatters:
585+
type: object
586+
description: custom formatters to apply to output
587+
properties:
588+
name:
589+
type: string
590+
description: name of formatter
591+
geom:
592+
type: boolean
593+
default: true
594+
description: whether to include geometry
595+
attachment:
596+
type: boolean
597+
default: false
598+
description: whether to provide as an attachment
599+
required:
600+
- name
584601
required:
585602
- type
586603
- title

tests/api/test_itemtypes.py

Lines changed: 21 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -217,16 +217,17 @@ def test_get_collection_items(config, api_):
217217
assert features['features'][1]['properties']['stn_id'] == 35
218218

219219
links = features['links']
220-
assert len(links) == 5
220+
assert len(links) == 6
221221
assert '/collections/obs/items?f=json' in links[0]['href']
222222
assert links[0]['rel'] == 'self'
223223
assert '/collections/obs/items?f=jsonld' in links[1]['href']
224224
assert links[1]['rel'] == 'alternate'
225225
assert '/collections/obs/items?f=html' in links[2]['href']
226226
assert links[2]['rel'] == 'alternate'
227227
assert '/collections/obs' in links[3]['href']
228-
assert links[3]['rel'] == 'next'
229-
assert links[4]['rel'] == 'collection'
228+
assert links[3]['rel'] == 'alternate'
229+
assert links[4]['rel'] == 'next'
230+
assert links[5]['rel'] == 'collection'
230231

231232
# Invalid offset
232233
req = mock_api_request({'offset': -1})
@@ -243,17 +244,19 @@ def test_get_collection_items(config, api_):
243244
assert features['features'][1]['properties']['stn_id'] == 2147
244245

245246
links = features['links']
246-
assert len(links) == 5
247+
assert len(links) == 6
247248
assert '/collections/obs/items?f=json' in links[0]['href']
248249
assert links[0]['rel'] == 'self'
249250
assert '/collections/obs/items?f=jsonld' in links[1]['href']
250251
assert links[1]['rel'] == 'alternate'
251252
assert '/collections/obs/items?f=html' in links[2]['href']
252253
assert links[2]['rel'] == 'alternate'
253-
assert '/collections/obs/items?offset=0' in links[3]['href']
254-
assert links[3]['rel'] == 'prev'
255-
assert '/collections/obs' in links[4]['href']
256-
assert links[4]['rel'] == 'collection'
254+
assert '/collections/obs/items?f=csv' in links[3]['href']
255+
assert links[3]['rel'] == 'alternate'
256+
assert '/collections/obs/items?offset=0' in links[4]['href']
257+
assert links[4]['rel'] == 'prev'
258+
assert '/collections/obs' in links[5]['href']
259+
assert links[5]['rel'] == 'collection'
257260

258261
req = mock_api_request({
259262
'offset': '1',
@@ -266,7 +269,7 @@ def test_get_collection_items(config, api_):
266269
assert len(features['features']) == 1
267270

268271
links = features['links']
269-
assert len(links) == 6
272+
assert len(links) == 7
270273
assert '/collections/obs/items?f=json&limit=1&bbox=-180,-90,180,90' in \
271274
links[0]['href']
272275
assert links[0]['rel'] == 'self'
@@ -276,13 +279,16 @@ def test_get_collection_items(config, api_):
276279
assert '/collections/obs/items?f=html&limit=1&bbox=-180,-90,180,90' in \
277280
links[2]['href']
278281
assert links[2]['rel'] == 'alternate'
279-
assert '/collections/obs/items?offset=0&limit=1&bbox=-180,-90,180,90' \
282+
assert '/collections/obs/items?f=csv&limit=1&bbox=-180,-90,180,90' \
280283
in links[3]['href']
281-
assert links[3]['rel'] == 'prev'
282-
assert '/collections/obs' in links[4]['href']
283-
assert links[3]['rel'] == 'prev'
284-
assert links[4]['rel'] == 'next'
285-
assert links[5]['rel'] == 'collection'
284+
assert links[3]['rel'] == 'alternate'
285+
assert '/collections/obs/items?offset=0&limit=1&bbox=-180,-90,180,90' \
286+
in links[4]['href']
287+
assert links[4]['rel'] == 'prev'
288+
assert '/collections/obs' in links[5]['href']
289+
assert links[4]['rel'] == 'prev'
290+
assert links[5]['rel'] == 'next'
291+
assert links[6]['rel'] == 'collection'
286292

287293
req = mock_api_request({
288294
'sortby': 'bad-property',

tests/load_tinydb_records.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
#
2828
# =================================================================
2929

30-
from datetime import datetime
30+
import datetime
3131
from pathlib import Path
3232
import sys
3333
from typing import Union
@@ -228,7 +228,7 @@ def get_anytext(bag: Union[list, str]) -> str:
228228
'geometry': geometry,
229229
'properties': {
230230
'created': issued,
231-
'updated': datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%SZ'),
231+
'updated': datetime.datetime.now(datetime.UTC).strftime('%Y-%m-%dT%H:%M:%SZ'), # noqa
232232
'type': type_,
233233
'title': title,
234234
'description': description,

0 commit comments

Comments
 (0)