Skip to content

Commit a498fbc

Browse files
committed
add support for custom formatters
1 parent e4b5ced commit a498fbc

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
@@ -245,9 +245,6 @@ def get_collection_items(
245245
:returns: tuple of headers, status code, content
246246
"""
247247

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

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

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

@@ -629,7 +646,7 @@ def get_collection_items(
629646
'href': '/'.join(uri.split('/')[:-1])
630647
})
631648

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

635652
# Set response language to requested provider locale
@@ -660,9 +677,8 @@ def get_collection_items(
660677
'collections/items/index.html',
661678
content, request.locale)
662679
return headers, HTTPStatus.OK, content
663-
elif request.format == 'csv': # render
664-
formatter = load_plugin('formatter',
665-
{'name': 'CSV', 'geom': True})
680+
elif request.format in dataset_formatters: # render
681+
formatter = dataset_formatters[request.format]
666682

667683
try:
668684
content = formatter.write(
@@ -681,13 +697,14 @@ def get_collection_items(
681697

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

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

689-
cd = f'attachment; filename="{filename}"'
690-
headers['Content-Disposition'] = cd
706+
cd = f'attachment; filename="{filename}"'
707+
headers['Content-Disposition'] = cd
691708

692709
return headers, HTTPStatus.OK, content
693710

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) -> 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)