From 844d77b9448d4d1dc7a7174bf6c5646cc8c4c4ce Mon Sep 17 00:00:00 2001 From: snowman2 Date: Sun, 27 Jan 2019 21:45:44 -0600 Subject: [PATCH] initial support for geopackage metatata issue#713 --- fiona/collection.py | 81 +++++++++++++++++++++++ fiona/ogrext.pyx | 147 +++++++++++++++++++++++++++++++++++++++++- tests/test_session.py | 53 +++++++++++++++ 3 files changed, 280 insertions(+), 1 deletion(-) diff --git a/fiona/collection.py b/fiona/collection.py index 17a8d7e4b..a5e85e5b3 100644 --- a/fiona/collection.py +++ b/fiona/collection.py @@ -221,6 +221,87 @@ def crs_wkt(self): self._crs_wkt = self.session.get_crs_wkt() return self._crs_wkt + def tags(self, ns=None): + """Returns a dict containing copies of the dataset or layers's + tags. Tags are pairs of key and value strings. Tags belong to + namespaces. The standard namespaces are: default (None) and + 'IMAGE_STRUCTURE'. Applications can create their own additional + namespaces. + + Parameters + ---------- + ns: str, optional + Can be used to select a namespace other than the default. + + Returns + ------- + dict + """ + if self.session: + return self.session.tags(ns=ns) + return None + + def get_tag_item(self, key, ns=None): + """Returns tag item value + + Parameters + ---------- + key: str + The key for the metadata item to fetch. + ns: str, optional + Used to select a namespace other than the default. + + Returns + ------- + str + """ + if self.session: + return self.session.get_tag_item(key=key, ns=ns) + return None + + def set_tags(self, tags, ns=None): + """Writes a dict containing the dataset or layers's tags. + Tags are pairs of key and value strings. Tags belong to + namespaces. The standard namespaces are: default (None) and + 'IMAGE_STRUCTURE'. Applications can create their own additional + namespaces. + + Parameters + ---------- + tags: dict + The dict of metadata items to set. + ns: str, optional + Used to select a namespace other than the default. + + Returns + ------- + int + """ + if isinstance(self.session, WritingSession): + return self.session.set_tags(tags, ns=ns) + raise RuntimeError("Unable to set tags as not in writing mode.") + + def set_tag_item(self, key, tag, ns=None): + """Sets the tag item value + + Parameters + ---------- + key: str + The key for the metadata item to set. + tag: str + The value of the metadata item to set. + ns: str, optional + Used to select a namespace other than the default. + + Returns + ------- + int + """ + if isinstance(self.session, WritingSession): + return self.session.set_tag_item(key=key, tag=tag, ns=ns) + raise RuntimeError("Unable to set tag item as not in writing mode.") + + @property def meta(self): """Returns a mapping with the driver, schema, crs, and additional diff --git a/fiona/ogrext.pyx b/fiona/ogrext.pyx index c073f73f8..40f05a13c 100644 --- a/fiona/ogrext.pyx +++ b/fiona/ogrext.pyx @@ -1,7 +1,8 @@ # These are extension functions and classes using the OGR C API. - from __future__ import absolute_import +include "gdal.pxi" + import datetime import json import locale @@ -822,6 +823,76 @@ cdef class Session: return 0 + def tags(self, ns=None): + """Returns a dict containing copies of the dataset or layers's + tags. Tags are pairs of key and value strings. Tags belong to + namespaces. The standard namespaces are: default (None) and + 'IMAGE_STRUCTURE'. Applications can create their own additional + namespaces. + + Parameters + ---------- + ns: str, optional + Can be used to select a namespace other than the default. + + Returns + ------- + dict + """ + cdef GDALMajorObjectH obj = NULL + if self.cogr_layer != NULL: + obj = self.cogr_layer + else: + obj = self.cogr_ds + + cdef const char *domain = NULL + if ns: + ns = ns.encode('utf-8') + domain = ns + + cdef char **metadata = NULL + metadata = GDALGetMetadata(obj, domain) + num_items = CSLCount(metadata) + + return dict(metadata[i].decode('utf-8').split('=', 1) for i in range(num_items)) + + + def get_tag_item(self, key, ns=None): + """Returns tag item value + + Parameters + ---------- + key: str + The key for the metadata item to fetch. + ns: str, optional + Used to select a namespace other than the default. + + Returns + ------- + str + """ + + key = key.encode('utf-8') + cdef const char *name = key + + cdef const char *domain = NULL + if ns: + ns = ns.encode('utf-8') + domain = ns + + cdef GDALMajorObjectH obj = NULL + if self.cogr_layer != NULL: + obj = self.cogr_layer + else: + obj = self.cogr_ds + + cdef char *value = NULL + value = GDALGetMetadataItem(obj, name, domain) + if value == NULL: + return None + return value.decode("utf-8") + + cdef class WritingSession(Session): cdef object _schema_mapping @@ -1210,6 +1281,80 @@ cdef class WritingSession(Session): gdal_flush_cache(cogr_ds) log.debug("Flushed data source cache") + def set_tags(self, tags, ns=None): + """Writes a dict containing the dataset or layers's tags. + Tags are pairs of key and value strings. Tags belong to + namespaces. The standard namespaces are: default (None) and + 'IMAGE_STRUCTURE'. Applications can create their own additional + namespaces. + + Parameters + ---------- + tags: dict + The dict of metadata items to set. + ns: str, optional + Used to select a namespace other than the default. + + Returns + ------- + int + """ + cdef GDALMajorObjectH obj = NULL + if self.cogr_layer != NULL: + obj = self.cogr_layer + else: + obj = self.cogr_ds + + cdef const char *domain = NULL + if ns: + ns = ns.encode('utf-8') + domain = ns + + cdef char **metadata = NULL + try: + for key, value in tags.items(): + key = key.encode("utf-8") + value = value.encode("utf-8") + metadata = CSLAddNameValue(metadata, key, value) + return GDALSetMetadata(obj, metadata, domain) + finally: + CSLDestroy(metadata) + + def set_tag_item(self, key, tag, ns=None): + """Sets the tag item value + + Parameters + ---------- + key: str + The key for the metadata item to set. + tag: str + The value of the metadata item to set. + ns: str + Used to select a namespace other than the default. + + Returns + ------- + int + """ + key = key.encode('utf-8') + cdef const char *name = key + tag = tag.encode("utf-8") + cdef char *value = tag + + cdef const char *domain = NULL + if ns: + ns = ns.encode('utf-8') + domain = ns + + cdef GDALMajorObjectH obj = NULL + if self.cogr_layer != NULL: + obj = self.cogr_layer + else: + obj = self.cogr_ds + + return GDALSetMetadataItem(obj, name, value, domain) + + cdef class Iterator: """Provides iterated access to feature data. diff --git a/tests/test_session.py b/tests/test_session.py index 2a7eeab8c..f5dbef6fd 100644 --- a/tests/test_session.py +++ b/tests/test_session.py @@ -1,9 +1,62 @@ """Tests of the ogrext.Session class""" +import pytest import fiona +from .conftest import gdal_version + def test_get(path_coutwildrnp_shp): with fiona.open(path_coutwildrnp_shp) as col: feat3 = col.get(2) assert feat3['properties']['NAME'] == 'Mount Zirkel Wilderness' + + +@pytest.mark.parametrize("layer, namespace, tags", [ + (None, None, {"test_tag1": "test_value1", "test_tag2": "test_value2"}), + (None, "test", {"test_tag1": "test_value1", "test_tag2": "test_value2"}), + (None, None, {}), + (None, "test", {}), + ("layer", None, {"test_tag1": "test_value1", "test_tag2": "test_value2"}), + ("layer", "test", {"test_tag1": "test_value1", "test_tag2": "test_value2"}), + ("layer", None, {}), + ("layer", "test", {}), +]) +@pytest.mark.xfail(not gdal_version.major >= 2, reason="Broken on GDAL 1.x") +def test_set_tags(layer, namespace, tags, tmpdir): + test_geopackage = str(tmpdir.join("test.gpkg")) + schema = {'properties': {'CDATA1': 'str:254'}, 'geometry': 'Polygon'} + with fiona.Env(), fiona.open( + test_geopackage, "w", driver="GPKG", schema=schema, layer=layer) as gpkg: + assert gpkg.tags() == {} + gpkg.set_tags(tags, ns=namespace) + + with fiona.Env(), fiona.open(test_geopackage, layer=layer) as gpkg: + assert gpkg.tags(ns=namespace) == tags + if namespace is not None: + assert gpkg.tags() == {} + with pytest.raises(RuntimeError): + gpkg.set_tags({}, ns=namespace) + + +@pytest.mark.parametrize("layer, namespace", [ + (None, None), + (None, "test"), + ("test", None), + ("test", "test"), +]) +@pytest.mark.xfail(not gdal_version.major >= 2, reason="Broken on GDAL 1.x") +def test_set_tag_item(layer, namespace, tmpdir): + test_geopackage = str(tmpdir.join("test.gpkg")) + schema = {'properties': {'CDATA1': 'str:254'}, 'geometry': 'Polygon'} + with fiona.Env(), fiona.open( + test_geopackage, "w", driver="GPKG", schema=schema, layer=layer) as gpkg: + assert gpkg.get_tag_item("test_tag1", ns=namespace) is None + gpkg.set_tag_item("test_tag1", "test_value1", ns=namespace) + + with fiona.Env(), fiona.open(test_geopackage, layer=layer) as gpkg: + if namespace is not None: + assert gpkg.get_tag_item("test_tag1") is None + assert gpkg.get_tag_item("test_tag1", ns=namespace) == "test_value1" + with pytest.raises(RuntimeError): + gpkg.set_tag_item("test_tag1", "test_value1", ns=namespace)