From 82e9f3d62ff0e29fa523d59ca4af657c022d8da9 Mon Sep 17 00:00:00 2001 From: Francesco Bartoli Date: Tue, 21 Oct 2025 11:02:08 +0200 Subject: [PATCH 1/8] Add early implementation of items payload --- pygeoapi/api/__init__.py | 5 +- pygeoapi/api/itemtypes.py | 48 +++++++- pygeoapi/formatter/jsonfg.py | 128 ++++++++++++++++++++++ pygeoapi/plugin.py | 3 +- requirements-provider.txt | 1 - requirements.txt | 1 + tests/formatter/test_jsonfg__formatter.py | 32 ++++++ 7 files changed, 213 insertions(+), 5 deletions(-) create mode 100644 pygeoapi/formatter/jsonfg.py create mode 100644 tests/formatter/test_jsonfg__formatter.py diff --git a/pygeoapi/api/__init__.py b/pygeoapi/api/__init__.py index c530cc2d8..3dfaee5b2 100644 --- a/pygeoapi/api/__init__.py +++ b/pygeoapi/api/__init__.py @@ -82,6 +82,7 @@ F_JSON = 'json' F_COVERAGEJSON = 'json' F_HTML = 'html' +F_JSONFG = 'jsonfg' F_JSONLD = 'jsonld' F_GZIP = 'gzip' F_PNG = 'png' @@ -94,7 +95,7 @@ (F_HTML, 'text/html'), (F_JSONLD, 'application/ld+json'), (F_JSON, 'application/json'), - (F_PNG, 'image/png'), + (F_JSONFG, 'application/vnd.geo+json'), (F_JPEG, 'image/jpeg'), (F_MVT, 'application/vnd.mapbox-vector-tile'), (F_NETCDF, 'application/x-netcdf'), @@ -108,7 +109,7 @@ 'http://www.opengis.net/spec/ogcapi-common-2/1.0/conf/collections', 'http://www.opengis.net/spec/ogcapi-common-1/1.0/conf/landing-page', 'http://www.opengis.net/spec/ogcapi-common-1/1.0/conf/json', - 'http://www.opengis.net/spec/ogcapi-common-1/1.0/conf/html', + 'http://www.opengis.net/spec/json-fg-1/0.2/conf/core', 'http://www.opengis.net/spec/ogcapi-common-1/1.0/conf/oas30' ] diff --git a/pygeoapi/api/itemtypes.py b/pygeoapi/api/itemtypes.py index dc54a5c19..da97533de 100644 --- a/pygeoapi/api/itemtypes.py +++ b/pygeoapi/api/itemtypes.py @@ -50,6 +50,7 @@ from pygeoapi import l10n from pygeoapi.api import evaluate_limit from pygeoapi.formatter.base import FormatterSerializationError +from pygeoapi.formatter.jsonfg import geojson2jsonfg from pygeoapi.linked_data import geojson2jsonld from pygeoapi.plugin import load_plugin, PLUGINS from pygeoapi.provider.base import ( @@ -62,7 +63,7 @@ to_json, transform_bbox) from . import ( - APIRequest, API, SYSTEM_LOCALE, F_JSON, FORMAT_TYPES, F_HTML, F_JSONLD, + APIRequest, API, SYSTEM_LOCALE, F_JSON, FORMAT_TYPES, F_HTML, F_JSONFG, F_JSONLD, validate_bbox, validate_datetime ) @@ -698,6 +699,27 @@ def get_collection_items( return headers, HTTPStatus.OK, content + elif request.format == F_JSONFG: + formatter = load_plugin('formatter', + {'name': F_JSONFG, 'geom': True}) + + try: + content = formatter.write( + data=content, + options={ + 'provider_def': get_provider_by_type( + collections[dataset]['providers'], + 'feature') + } + ) + except FormatterSerializationError: + msg = 'Error serializing output' + return api.get_exception( + HTTPStatus.INTERNAL_SERVER_ERROR, headers, request.format, + 'NoApplicableCode', msg) + + return headers, HTTPStatus.OK, content + return headers, HTTPStatus.OK, to_json(content, api.pretty_print) @@ -982,6 +1004,30 @@ def get_collection_item(api: API, request: APIRequest, return headers, HTTPStatus.OK, content + elif request.format == F_JSONFG: + # content = geojson2jsonfg( + # api, content, dataset, id_field=(p.uri_field or 'id') + # ) + formatter = load_plugin('formatter', + {'name': F_JSONFG, 'geom': True}) + + try: + content = formatter.write( + data=content, + options={ + 'provider_def': get_provider_by_type( + collections[dataset]['providers'], + 'feature') + } + ) + except FormatterSerializationError: + msg = 'Error serializing output' + return api.get_exception( + HTTPStatus.INTERNAL_SERVER_ERROR, headers, request.format, + 'NoApplicableCode', msg) + + return headers, HTTPStatus.OK, content + return headers, HTTPStatus.OK, to_json(content, api.pretty_print) diff --git a/pygeoapi/formatter/jsonfg.py b/pygeoapi/formatter/jsonfg.py new file mode 100644 index 000000000..89f82e71c --- /dev/null +++ b/pygeoapi/formatter/jsonfg.py @@ -0,0 +1,128 @@ +# ================================================================= +# +# Authors: Francesco Bartoli +# +# Copyright (c) 2025 Francesco Bartoli +# +# 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. +# +# ================================================================= +"""JSON-FG capabilities +Returns content as JSON-FG representations +""" + +import json +import logging +import uuid +from typing import Union + +from osgeo import gdal + +from pygeoapi.formatter.base import BaseFormatter, FormatterSerializationError + +LOGGER = logging.getLogger(__name__) + + +class JSONFGFormatter(BaseFormatter): + """JSON-FG formatter""" + + def __init__(self, formatter_def: dict): + """ + Initialize object + + :param formatter_def: formatter definition + + :returns: `pygeoapi.formatter.jsonfg.JSONFGFormatter` + """ + + geom = False + if "geom" in formatter_def: + geom = formatter_def["geom"] + + super().__init__({"name": "jsonfg", "geom": geom}) + self.mimetype = "application/vnd.ogc.fg+json" + + def write(self, data: dict, options: dict = {}) -> str: + """ + Generate data in JSON-FG format + + :param options: JSON-FG formatting options + :param data: dict of GeoJSON data + + :returns: string representation of format + """ + + try: + fields = list(data["features"][0]["properties"].keys()) + except IndexError: + LOGGER.error("no features") + return str() + + LOGGER.debug(f"JSONFG fields: {fields}") + + try: + output = geojson2jsonfg(data=data, dataset="items") + return output + except ValueError as err: + LOGGER.error(err) + raise FormatterSerializationError("Error writing JSONFG output") + + def __repr__(self): + return f" {self.name}" + + +def geojson2jsonfg( + data: dict, + dataset: str, + identifier: Union[str, None] = None, + id_field: str = "id", +) -> str: + """ + Return JSON-FG from a GeoJSON content. + + :param cls: API object + :param data: dict of data: + + :returns: string of rendered JSON (JSON-FG) + """ + gdal.UseExceptions() + LOGGER.debug("Dump GeoJSON content into a data source") + # breakpoint() + try: + with gdal.OpenEx(json.dumps(data)) as srcDS: + tmpfile = f"/vsimem/{uuid.uuid1()}.json" + LOGGER.debug("Translate GeoJSON into a JSONFG memory file") + gdal.VectorTranslate(tmpfile, srcDS, format="JSONFG") + LOGGER.debug("Read JSONFG content from a memory file") + data = gdal.VSIFOpenL(tmpfile, "rb") + if not data: + raise ValueError("Failed to read JSONFG content") + gdal.VSIFSeekL(data, 0, 2) + length = gdal.VSIFTellL(data) + gdal.VSIFSeekL(data, 0, 0) + jsonfg = json.loads(gdal.VSIFReadL(1, length, data).decode()) + return jsonfg + except Exception as e: + LOGGER.error(f"Failed to convert GeoJSON to JSON-FG: {e}") + raise + finally: + gdal.VSIFCloseL(data) diff --git a/pygeoapi/plugin.py b/pygeoapi/plugin.py index 54f71be0e..42d2ed941 100644 --- a/pygeoapi/plugin.py +++ b/pygeoapi/plugin.py @@ -73,7 +73,8 @@ 'xarray-edr': 'pygeoapi.provider.xarray_edr.XarrayEDRProvider' }, 'formatter': { - 'CSV': 'pygeoapi.formatter.csv_.CSVFormatter' + 'CSV': 'pygeoapi.formatter.csv_.CSVFormatter', + 'jsonfg': 'pygeoapi.formatter.jsonfg.JSONFGFormatter', }, 'process': { 'HelloWorld': 'pygeoapi.process.hello_world.HelloWorldProcessor', diff --git a/requirements-provider.txt b/requirements-provider.txt index c22fc5fd1..075184756 100644 --- a/requirements-provider.txt +++ b/requirements-provider.txt @@ -4,7 +4,6 @@ cftime elasticsearch elasticsearch-dsl fiona -GDAL<=3.11.3 geoalchemy2 geopandas netCDF4 diff --git a/requirements.txt b/requirements.txt index 82244eb4b..36460dcfe 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,6 +2,7 @@ Babel click filelock Flask +GDAL<=3.11.3 jinja2 jsonschema pydantic diff --git a/tests/formatter/test_jsonfg__formatter.py b/tests/formatter/test_jsonfg__formatter.py new file mode 100644 index 000000000..a3d46f348 --- /dev/null +++ b/tests/formatter/test_jsonfg__formatter.py @@ -0,0 +1,32 @@ +# ================================================================= +# +# Authors: Francesco Bartoli +# +# Copyright (c) 2025 Francesco Bartoli +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation +# files (the "Software"), to deal in the Software without +# restriction, including without limitation the rights to use, +# copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following +# conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +# OTHER DEALINGS IN THE SOFTWARE. +# +# ================================================================= + +import pytest + +from pygeoapi.formatter.jsonfg import JSONFGFormatter From ba9511a5f1e5f1ea79e2ab62fe46c2e7bcb16f25 Mon Sep 17 00:00:00 2001 From: Francesco Bartoli Date: Tue, 21 Oct 2025 14:39:40 +0200 Subject: [PATCH 2/8] Fix payload for a single item --- pygeoapi/formatter/jsonfg.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pygeoapi/formatter/jsonfg.py b/pygeoapi/formatter/jsonfg.py index 89f82e71c..488648b22 100644 --- a/pygeoapi/formatter/jsonfg.py +++ b/pygeoapi/formatter/jsonfg.py @@ -72,7 +72,10 @@ def write(self, data: dict, options: dict = {}) -> str: """ try: - fields = list(data["features"][0]["properties"].keys()) + if data.get("features"): + fields = list(data["features"][0]["properties"].keys()) + else: + fields = data["properties"].keys() except IndexError: LOGGER.error("no features") return str() From 1e46834734a1f381d01b581c40fbaa15bc79d472 Mon Sep 17 00:00:00 2001 From: Francesco Bartoli Date: Wed, 22 Oct 2025 00:14:45 +0200 Subject: [PATCH 3/8] Fix mime type and links for items and item id --- pygeoapi/api/__init__.py | 14 +++++++++++++- pygeoapi/api/itemtypes.py | 5 +++++ pygeoapi/formatter/jsonfg.py | 2 +- 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/pygeoapi/api/__init__.py b/pygeoapi/api/__init__.py index 3dfaee5b2..4854aedfe 100644 --- a/pygeoapi/api/__init__.py +++ b/pygeoapi/api/__init__.py @@ -95,7 +95,7 @@ (F_HTML, 'text/html'), (F_JSONLD, 'application/ld+json'), (F_JSON, 'application/json'), - (F_JSONFG, 'application/vnd.geo+json'), + (F_JSONFG, 'application/geo+json'), (F_JPEG, 'image/jpeg'), (F_MVT, 'application/vnd.mapbox-vector-tile'), (F_NETCDF, 'application/x-netcdf'), @@ -1128,6 +1128,12 @@ def describe_collections(api: API, request: APIRequest, 'title': l10n.translate('Items as GeoJSON', request.locale), # noqa 'href': f'{api.get_collections_url()}/{k}/items?f={F_JSON}' # noqa }) + collection['links'].append({ + 'type': FORMAT_TYPES[F_JSONFG], + 'rel': 'items', + 'title': l10n.translate('Items as JSON-FG', request.locale), # noqa + 'href': f'{api.get_collections_url()}/{k}/items?f={F_JSONFG}' # noqa + }) collection['links'].append({ 'type': FORMAT_TYPES[F_JSONLD], 'rel': 'items', @@ -1334,6 +1340,12 @@ def describe_collections(api: API, request: APIRequest, 'title': l10n.translate('This document as JSON', request.locale), # noqa 'href': f'{api.get_collections_url()}?f={F_JSON}' }) + fcm['links'].append({ + 'type': FORMAT_TYPES[F_JSONFG], + 'rel': request.get_linkrel(F_JSONFG), + 'title': l10n.translate('This document as JSON-FG', request.locale), # noqa + 'href': f'{api.get_collections_url()}?f={F_JSONFG}' + }) fcm['links'].append({ 'type': FORMAT_TYPES[F_JSONLD], 'rel': request.get_linkrel(F_JSONLD), diff --git a/pygeoapi/api/itemtypes.py b/pygeoapi/api/itemtypes.py index da97533de..1683bdd5a 100644 --- a/pygeoapi/api/itemtypes.py +++ b/pygeoapi/api/itemtypes.py @@ -574,6 +574,11 @@ def get_collection_items( 'rel': request.get_linkrel(F_JSON), 'title': l10n.translate('This document as GeoJSON', request.locale), 'href': f'{uri}?f={F_JSON}{serialized_query_params}' + }, { + 'rel': request.get_linkrel(F_JSONFG), + 'type': FORMAT_TYPES[F_JSONFG], + 'title': l10n.translate('This document as JSON-FG', request.locale), # noqa + 'href': f'{uri}?f={F_JSONFG}{serialized_query_params}' }, { 'rel': request.get_linkrel(F_JSONLD), 'type': FORMAT_TYPES[F_JSONLD], diff --git a/pygeoapi/formatter/jsonfg.py b/pygeoapi/formatter/jsonfg.py index 488648b22..80983f958 100644 --- a/pygeoapi/formatter/jsonfg.py +++ b/pygeoapi/formatter/jsonfg.py @@ -59,7 +59,7 @@ def __init__(self, formatter_def: dict): geom = formatter_def["geom"] super().__init__({"name": "jsonfg", "geom": geom}) - self.mimetype = "application/vnd.ogc.fg+json" + self.mimetype = "application/geo+json" def write(self, data: dict, options: dict = {}) -> str: """ From b25dc277b3c6e3a3e0aac813a65b93f0b640485c Mon Sep 17 00:00:00 2001 From: Francesco Bartoli Date: Wed, 22 Oct 2025 13:47:14 +0200 Subject: [PATCH 4/8] Fix missing links in items and item id --- pygeoapi/api/__init__.py | 2 +- pygeoapi/api/itemtypes.py | 20 +++++++++++++++----- pygeoapi/formatter/jsonfg.py | 17 +++++++++++------ 3 files changed, 27 insertions(+), 12 deletions(-) diff --git a/pygeoapi/api/__init__.py b/pygeoapi/api/__init__.py index 4854aedfe..655869644 100644 --- a/pygeoapi/api/__init__.py +++ b/pygeoapi/api/__init__.py @@ -109,7 +109,7 @@ 'http://www.opengis.net/spec/ogcapi-common-2/1.0/conf/collections', 'http://www.opengis.net/spec/ogcapi-common-1/1.0/conf/landing-page', 'http://www.opengis.net/spec/ogcapi-common-1/1.0/conf/json', - 'http://www.opengis.net/spec/json-fg-1/0.2/conf/core', + 'http://www.opengis.net/spec/json-fg-1/0.3/conf/core', 'http://www.opengis.net/spec/ogcapi-common-1/1.0/conf/oas30' ] diff --git a/pygeoapi/api/itemtypes.py b/pygeoapi/api/itemtypes.py index 1683bdd5a..14e4c55b4 100644 --- a/pygeoapi/api/itemtypes.py +++ b/pygeoapi/api/itemtypes.py @@ -710,7 +710,10 @@ def get_collection_items( try: content = formatter.write( + api=api, data=content, + dataset=dataset, + id_field=(p.uri_field or 'id'), options={ 'provider_def': get_provider_by_type( collections[dataset]['providers'], @@ -723,7 +726,9 @@ def get_collection_items( HTTPStatus.INTERNAL_SERVER_ERROR, headers, request.format, 'NoApplicableCode', msg) - return headers, HTTPStatus.OK, content + headers['Content-Type'] = formatter.mimetype + + return headers, HTTPStatus.OK, to_json(content, api.pretty_print) return headers, HTTPStatus.OK, to_json(content, api.pretty_print) @@ -947,6 +952,11 @@ def get_collection_item(api: API, request: APIRequest, 'title': l10n.translate('This document as JSON', request.locale), 'href': f'{uri}?f={F_JSON}' }, { + 'rel': request.get_linkrel(F_JSONFG), + 'type': FORMAT_TYPES[F_JSONFG], + 'title': l10n.translate('This document as JSON-FG (JSON-FG)', request.locale), # noqa + 'href': f'{uri}?f={F_JSONFG}' + }, { 'rel': request.get_linkrel(F_JSONLD), 'type': FORMAT_TYPES[F_JSONLD], 'title': l10n.translate('This document as RDF (JSON-LD)', request.locale), # noqa @@ -1010,15 +1020,15 @@ def get_collection_item(api: API, request: APIRequest, return headers, HTTPStatus.OK, content elif request.format == F_JSONFG: - # content = geojson2jsonfg( - # api, content, dataset, id_field=(p.uri_field or 'id') - # ) formatter = load_plugin('formatter', {'name': F_JSONFG, 'geom': True}) try: content = formatter.write( + api=api, data=content, + dataset=dataset, + id_field=(p.uri_field or 'id'), options={ 'provider_def': get_provider_by_type( collections[dataset]['providers'], @@ -1031,7 +1041,7 @@ def get_collection_item(api: API, request: APIRequest, HTTPStatus.INTERNAL_SERVER_ERROR, headers, request.format, 'NoApplicableCode', msg) - return headers, HTTPStatus.OK, content + return headers, HTTPStatus.OK, to_json(content, api.pretty_print) return headers, HTTPStatus.OK, to_json(content, api.pretty_print) diff --git a/pygeoapi/formatter/jsonfg.py b/pygeoapi/formatter/jsonfg.py index 80983f958..084306dde 100644 --- a/pygeoapi/formatter/jsonfg.py +++ b/pygeoapi/formatter/jsonfg.py @@ -38,6 +38,7 @@ from osgeo import gdal from pygeoapi.formatter.base import BaseFormatter, FormatterSerializationError +from pygeoapi.api import APIRequest LOGGER = logging.getLogger(__name__) @@ -61,7 +62,10 @@ def __init__(self, formatter_def: dict): super().__init__({"name": "jsonfg", "geom": geom}) self.mimetype = "application/geo+json" - def write(self, data: dict, options: dict = {}) -> str: + def write( + self, api: APIRequest, data: dict, + dataset: str, id_field: str, options: dict = {} + ) -> dict: """ Generate data in JSON-FG format @@ -78,12 +82,14 @@ def write(self, data: dict, options: dict = {}) -> str: fields = data["properties"].keys() except IndexError: LOGGER.error("no features") - return str() + return dict() LOGGER.debug(f"JSONFG fields: {fields}") try: - output = geojson2jsonfg(data=data, dataset="items") + links = data.get("links") + output = geojson2jsonfg(data=data, dataset=dataset) + output["links"] = links return output except ValueError as err: LOGGER.error(err) @@ -98,18 +104,17 @@ def geojson2jsonfg( dataset: str, identifier: Union[str, None] = None, id_field: str = "id", -) -> str: +) -> dict: """ Return JSON-FG from a GeoJSON content. :param cls: API object :param data: dict of data: - :returns: string of rendered JSON (JSON-FG) + :returns: dict of converted GeoJSON (JSON-FG) """ gdal.UseExceptions() LOGGER.debug("Dump GeoJSON content into a data source") - # breakpoint() try: with gdal.OpenEx(json.dumps(data)) as srcDS: tmpfile = f"/vsimem/{uuid.uuid1()}.json" From 2359c2e71a973d3745feee151fbf133bf1a7b583 Mon Sep 17 00:00:00 2001 From: Francesco Bartoli Date: Wed, 22 Oct 2025 16:35:52 +0200 Subject: [PATCH 5/8] Add a basic test for the formatter --- pygeoapi/api/itemtypes.py | 2 - pygeoapi/formatter/jsonfg.py | 9 +++-- tests/formatter/test_jsonfg__formatter.py | 49 +++++++++++++++++++++++ 3 files changed, 54 insertions(+), 6 deletions(-) diff --git a/pygeoapi/api/itemtypes.py b/pygeoapi/api/itemtypes.py index 14e4c55b4..11f7cdba1 100644 --- a/pygeoapi/api/itemtypes.py +++ b/pygeoapi/api/itemtypes.py @@ -710,7 +710,6 @@ def get_collection_items( try: content = formatter.write( - api=api, data=content, dataset=dataset, id_field=(p.uri_field or 'id'), @@ -1025,7 +1024,6 @@ def get_collection_item(api: API, request: APIRequest, try: content = formatter.write( - api=api, data=content, dataset=dataset, id_field=(p.uri_field or 'id'), diff --git a/pygeoapi/formatter/jsonfg.py b/pygeoapi/formatter/jsonfg.py index 084306dde..140c19355 100644 --- a/pygeoapi/formatter/jsonfg.py +++ b/pygeoapi/formatter/jsonfg.py @@ -63,8 +63,7 @@ def __init__(self, formatter_def: dict): self.mimetype = "application/geo+json" def write( - self, api: APIRequest, data: dict, - dataset: str, id_field: str, options: dict = {} + self, data: dict, dataset: str, id_field: str, options: dict = {} ) -> dict: """ Generate data in JSON-FG format @@ -108,8 +107,10 @@ def geojson2jsonfg( """ Return JSON-FG from a GeoJSON content. - :param cls: API object - :param data: dict of data: + :param data: dict of data + :param dataset: dataset name + :param identifier: identifier field name + :param id_field: id field name :returns: dict of converted GeoJSON (JSON-FG) """ diff --git a/tests/formatter/test_jsonfg__formatter.py b/tests/formatter/test_jsonfg__formatter.py index a3d46f348..9d216257d 100644 --- a/tests/formatter/test_jsonfg__formatter.py +++ b/tests/formatter/test_jsonfg__formatter.py @@ -30,3 +30,52 @@ import pytest from pygeoapi.formatter.jsonfg import JSONFGFormatter + + +@pytest.fixture() +def fixture(): + data = { + 'type': 'FeatureCollection', + 'features': [{ + 'type': 'Feature', + 'id': '123-456', + 'geometry': { + 'type': 'Point', + 'coordinates': [125.6, 10.1]}, + 'properties': { + 'name': 'Dinagat Islands', + 'foo': 'bar' + }} + ], + 'links': [{ + 'rel': 'self', + 'type': 'application/geo+json', + 'title': 'GeoJSON', + 'href': 'http://example.com' + }] + } + + return data + + +def test_jsonfg__formatter(fixture): + f = JSONFGFormatter({'geom': True}) + f_jsonfg = f.write(data=fixture, dataset='test', id_field='id', options={}) + + assert f.mimetype == "application/geo+json" + + assert f_jsonfg['type'] == 'FeatureCollection' + assert f_jsonfg['features'][0]['type'] == 'Feature' + assert f_jsonfg['features'][0]['geometry']['type'] == 'Point' + assert f_jsonfg['features'][0]['geometry']['coordinates'] == [125.6, 10.1] + assert f_jsonfg['features'][0]['properties']['id'] == '123-456' + assert f_jsonfg['features'][0]['properties']['name'] == 'Dinagat Islands' + assert f_jsonfg['features'][0]['properties']['foo'] == 'bar' + + assert f_jsonfg['featureType'] == 'OGRGeoJSON' + assert f_jsonfg['conformsTo'] + assert f_jsonfg['coordRefSys'] == '[EPSG:4326]' + assert f_jsonfg['features'][0]['place'] is None + assert f_jsonfg['features'][0]['time'] is None + + assert len(f_jsonfg['links']) == 1 From 8103174cabbc3e2e128aa674904c15e540f101c3 Mon Sep 17 00:00:00 2001 From: Francesco Bartoli Date: Wed, 22 Oct 2025 16:46:48 +0200 Subject: [PATCH 6/8] Fix flake8 errors --- pygeoapi/api/itemtypes.py | 4 ++-- pygeoapi/formatter/jsonfg.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pygeoapi/api/itemtypes.py b/pygeoapi/api/itemtypes.py index 11f7cdba1..96777f659 100644 --- a/pygeoapi/api/itemtypes.py +++ b/pygeoapi/api/itemtypes.py @@ -50,7 +50,6 @@ from pygeoapi import l10n from pygeoapi.api import evaluate_limit from pygeoapi.formatter.base import FormatterSerializationError -from pygeoapi.formatter.jsonfg import geojson2jsonfg from pygeoapi.linked_data import geojson2jsonld from pygeoapi.plugin import load_plugin, PLUGINS from pygeoapi.provider.base import ( @@ -63,7 +62,8 @@ to_json, transform_bbox) from . import ( - APIRequest, API, SYSTEM_LOCALE, F_JSON, FORMAT_TYPES, F_HTML, F_JSONFG, F_JSONLD, + APIRequest, API, SYSTEM_LOCALE, F_JSON, + FORMAT_TYPES, F_HTML, F_JSONFG, F_JSONLD, validate_bbox, validate_datetime ) diff --git a/pygeoapi/formatter/jsonfg.py b/pygeoapi/formatter/jsonfg.py index 140c19355..9ec28da9b 100644 --- a/pygeoapi/formatter/jsonfg.py +++ b/pygeoapi/formatter/jsonfg.py @@ -38,7 +38,7 @@ from osgeo import gdal from pygeoapi.formatter.base import BaseFormatter, FormatterSerializationError -from pygeoapi.api import APIRequest + LOGGER = logging.getLogger(__name__) From 0d98f1d26588253b1c42145fccd7e8aab31973f0 Mon Sep 17 00:00:00 2001 From: Francesco Bartoli Date: Wed, 22 Oct 2025 17:04:55 +0200 Subject: [PATCH 7/8] Set gdal constraints for jsonfg --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 36460dcfe..bfac5e754 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ Babel click filelock Flask -GDAL<=3.11.3 +GDAL<4 jinja2 jsonschema pydantic From ae0b39b1818d2367ef8d911c345b4a4d17fd610e Mon Sep 17 00:00:00 2001 From: Francesco Bartoli Date: Wed, 22 Oct 2025 20:17:03 +0200 Subject: [PATCH 8/8] Force the build to fail until gdal 3.12 is available --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index bfac5e754..5d7216a1b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ Babel click filelock Flask -GDAL<4 +GDAL>=3.12.0,<4.0.0 jinja2 jsonschema pydantic