geoservercloud 0.7.2.dev1__tar.gz → 0.7.2.dev10__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. {geoservercloud-0.7.2.dev1 → geoservercloud-0.7.2.dev10}/PKG-INFO +1 -1
  2. {geoservercloud-0.7.2.dev1 → geoservercloud-0.7.2.dev10}/geoservercloud/geoservercloud.py +3 -0
  3. geoservercloud-0.7.2.dev10/geoservercloud/models/common.py +376 -0
  4. {geoservercloud-0.7.2.dev1 → geoservercloud-0.7.2.dev10}/geoservercloud/models/featuretype.py +22 -0
  5. {geoservercloud-0.7.2.dev1 → geoservercloud-0.7.2.dev10}/pyproject.toml +1 -1
  6. geoservercloud-0.7.2.dev1/geoservercloud/models/common.py +0 -208
  7. {geoservercloud-0.7.2.dev1 → geoservercloud-0.7.2.dev10}/LICENSE +0 -0
  8. {geoservercloud-0.7.2.dev1 → geoservercloud-0.7.2.dev10}/README.md +0 -0
  9. {geoservercloud-0.7.2.dev1 → geoservercloud-0.7.2.dev10}/geoserver_acceptance_tests/.gitignore +0 -0
  10. {geoservercloud-0.7.2.dev1 → geoservercloud-0.7.2.dev10}/geoserver_acceptance_tests/README.md +0 -0
  11. {geoservercloud-0.7.2.dev1 → geoservercloud-0.7.2.dev10}/geoserver_acceptance_tests/__init__.py +0 -0
  12. {geoservercloud-0.7.2.dev1 → geoservercloud-0.7.2.dev10}/geoserver_acceptance_tests/cli.py +0 -0
  13. {geoservercloud-0.7.2.dev1 → geoservercloud-0.7.2.dev10}/geoserver_acceptance_tests/compose/example.compose.yaml +0 -0
  14. {geoservercloud-0.7.2.dev1 → geoservercloud-0.7.2.dev10}/geoserver_acceptance_tests/compose/geodatabase/001_create_schemas.sql +0 -0
  15. {geoservercloud-0.7.2.dev1 → geoservercloud-0.7.2.dev10}/geoserver_acceptance_tests/config.py +0 -0
  16. {geoservercloud-0.7.2.dev1 → geoservercloud-0.7.2.dev10}/geoserver_acceptance_tests/data/sampledata.tgz +0 -0
  17. {geoservercloud-0.7.2.dev1 → geoservercloud-0.7.2.dev10}/geoserver_acceptance_tests/example.config.yaml +0 -0
  18. {geoservercloud-0.7.2.dev1 → geoservercloud-0.7.2.dev10}/geoserver_acceptance_tests/tests/__init__.py +0 -0
  19. {geoservercloud-0.7.2.dev1 → geoservercloud-0.7.2.dev10}/geoserver_acceptance_tests/tests/conftest.py +0 -0
  20. {geoservercloud-0.7.2.dev1 → geoservercloud-0.7.2.dev10}/geoserver_acceptance_tests/tests/resources/__init__.py +0 -0
  21. {geoservercloud-0.7.2.dev1 → geoservercloud-0.7.2.dev10}/geoserver_acceptance_tests/tests/resources/i18n/__init__.py +0 -0
  22. {geoservercloud-0.7.2.dev1 → geoservercloud-0.7.2.dev10}/geoserver_acceptance_tests/tests/resources/i18n/labels/default_locale/default_value/language_None_expected.png +0 -0
  23. {geoservercloud-0.7.2.dev1 → geoservercloud-0.7.2.dev10}/geoserver_acceptance_tests/tests/resources/i18n/labels/default_locale/default_value/language__expected.png +0 -0
  24. {geoservercloud-0.7.2.dev1 → geoservercloud-0.7.2.dev10}/geoserver_acceptance_tests/tests/resources/i18n/labels/default_locale/default_value/language_de_expected.png +0 -0
  25. {geoservercloud-0.7.2.dev1 → geoservercloud-0.7.2.dev10}/geoserver_acceptance_tests/tests/resources/i18n/labels/default_locale/default_value/language_fr_expected.png +0 -0
  26. {geoservercloud-0.7.2.dev1 → geoservercloud-0.7.2.dev10}/geoserver_acceptance_tests/tests/resources/i18n/labels/default_locale/default_value/language_it_expected.png +0 -0
  27. {geoservercloud-0.7.2.dev1 → geoservercloud-0.7.2.dev10}/geoserver_acceptance_tests/tests/resources/i18n/labels/default_locale/no_default_value/language_None_expected.png +0 -0
  28. {geoservercloud-0.7.2.dev1 → geoservercloud-0.7.2.dev10}/geoserver_acceptance_tests/tests/resources/i18n/labels/default_locale/no_default_value/language__expected.png +0 -0
  29. {geoservercloud-0.7.2.dev1 → geoservercloud-0.7.2.dev10}/geoserver_acceptance_tests/tests/resources/i18n/labels/default_locale/no_default_value/language_it_expected.png +0 -0
  30. {geoservercloud-0.7.2.dev1 → geoservercloud-0.7.2.dev10}/geoserver_acceptance_tests/tests/resources/i18n/labels/no_default_locale/default_value/language_None_expected.png +0 -0
  31. {geoservercloud-0.7.2.dev1 → geoservercloud-0.7.2.dev10}/geoserver_acceptance_tests/tests/resources/i18n/labels/no_default_locale/default_value/language__expected.png +0 -0
  32. {geoservercloud-0.7.2.dev1 → geoservercloud-0.7.2.dev10}/geoserver_acceptance_tests/tests/resources/i18n/labels/no_default_locale/default_value/language_de_expected.png +0 -0
  33. {geoservercloud-0.7.2.dev1 → geoservercloud-0.7.2.dev10}/geoserver_acceptance_tests/tests/resources/i18n/labels/no_default_locale/default_value/language_fr_expected.png +0 -0
  34. {geoservercloud-0.7.2.dev1 → geoservercloud-0.7.2.dev10}/geoserver_acceptance_tests/tests/resources/i18n/labels/no_default_locale/default_value/language_it_expected.png +0 -0
  35. {geoservercloud-0.7.2.dev1 → geoservercloud-0.7.2.dev10}/geoserver_acceptance_tests/tests/resources/i18n/labels/no_default_locale/no_default_value/language_None_expected.png +0 -0
  36. {geoservercloud-0.7.2.dev1 → geoservercloud-0.7.2.dev10}/geoserver_acceptance_tests/tests/resources/i18n/labels/no_default_locale/no_default_value/language__expected.png +0 -0
  37. {geoservercloud-0.7.2.dev1 → geoservercloud-0.7.2.dev10}/geoserver_acceptance_tests/tests/resources/i18n/labels/no_default_locale/no_default_value/language_it_expected.png +0 -0
  38. {geoservercloud-0.7.2.dev1 → geoservercloud-0.7.2.dev10}/geoserver_acceptance_tests/tests/resources/i18n/localized_labels.sld +0 -0
  39. {geoservercloud-0.7.2.dev1 → geoservercloud-0.7.2.dev10}/geoserver_acceptance_tests/tests/resources/i18n/localized_no_default.sld +0 -0
  40. {geoservercloud-0.7.2.dev1 → geoservercloud-0.7.2.dev10}/geoserver_acceptance_tests/tests/resources/i18n/localized_with_default.sld +0 -0
  41. {geoservercloud-0.7.2.dev1 → geoservercloud-0.7.2.dev10}/geoserver_acceptance_tests/tests/resources/wfs/__init__.py +0 -0
  42. {geoservercloud-0.7.2.dev1 → geoservercloud-0.7.2.dev10}/geoserver_acceptance_tests/tests/resources/wfs/wfs_delete_payload.xml +0 -0
  43. {geoservercloud-0.7.2.dev1 → geoservercloud-0.7.2.dev10}/geoserver_acceptance_tests/tests/resources/wfs/wfs_insert_payload.xml +0 -0
  44. {geoservercloud-0.7.2.dev1 → geoservercloud-0.7.2.dev10}/geoserver_acceptance_tests/tests/resources/wms/__init__.py +0 -0
  45. {geoservercloud-0.7.2.dev1 → geoservercloud-0.7.2.dev10}/geoserver_acceptance_tests/tests/resources/wms/getmap_expected.png +0 -0
  46. {geoservercloud-0.7.2.dev1 → geoservercloud-0.7.2.dev10}/geoserver_acceptance_tests/tests/test_cascaded_stores.py +0 -0
  47. {geoservercloud-0.7.2.dev1 → geoservercloud-0.7.2.dev10}/geoserver_acceptance_tests/tests/test_cog.py +0 -0
  48. {geoservercloud-0.7.2.dev1 → geoservercloud-0.7.2.dev10}/geoserver_acceptance_tests/tests/test_datastore.py +0 -0
  49. {geoservercloud-0.7.2.dev1 → geoservercloud-0.7.2.dev10}/geoserver_acceptance_tests/tests/test_gwc.py +0 -0
  50. {geoservercloud-0.7.2.dev1 → geoservercloud-0.7.2.dev10}/geoserver_acceptance_tests/tests/test_i18n.py +0 -0
  51. {geoservercloud-0.7.2.dev1 → geoservercloud-0.7.2.dev10}/geoserver_acceptance_tests/tests/test_imagemosaic.py +0 -0
  52. {geoservercloud-0.7.2.dev1 → geoservercloud-0.7.2.dev10}/geoserver_acceptance_tests/tests/test_imagemosaic_cog.py +0 -0
  53. {geoservercloud-0.7.2.dev1 → geoservercloud-0.7.2.dev10}/geoserver_acceptance_tests/tests/test_wfs.py +0 -0
  54. {geoservercloud-0.7.2.dev1 → geoservercloud-0.7.2.dev10}/geoserver_acceptance_tests/tests/test_wms.py +0 -0
  55. {geoservercloud-0.7.2.dev1 → geoservercloud-0.7.2.dev10}/geoserver_acceptance_tests/tests/test_workspace.py +0 -0
  56. {geoservercloud-0.7.2.dev1 → geoservercloud-0.7.2.dev10}/geoserver_acceptance_tests/tests/utils.py +0 -0
  57. {geoservercloud-0.7.2.dev1 → geoservercloud-0.7.2.dev10}/geoservercloud/__init__.py +0 -0
  58. {geoservercloud-0.7.2.dev1 → geoservercloud-0.7.2.dev10}/geoservercloud/geoservercloudsync.py +0 -0
  59. {geoservercloud-0.7.2.dev1 → geoservercloud-0.7.2.dev10}/geoservercloud/gridsets/2056.xml +0 -0
  60. {geoservercloud-0.7.2.dev1 → geoservercloud-0.7.2.dev10}/geoservercloud/gridsets/21781.xml +0 -0
  61. {geoservercloud-0.7.2.dev1 → geoservercloud-0.7.2.dev10}/geoservercloud/gridsets/3857.xml +0 -0
  62. {geoservercloud-0.7.2.dev1 → geoservercloud-0.7.2.dev10}/geoservercloud/models/__init__.py +0 -0
  63. {geoservercloud-0.7.2.dev1 → geoservercloud-0.7.2.dev10}/geoservercloud/models/abstractlayer.py +0 -0
  64. {geoservercloud-0.7.2.dev1 → geoservercloud-0.7.2.dev10}/geoservercloud/models/coverage.py +0 -0
  65. {geoservercloud-0.7.2.dev1 → geoservercloud-0.7.2.dev10}/geoservercloud/models/coverages.py +0 -0
  66. {geoservercloud-0.7.2.dev1 → geoservercloud-0.7.2.dev10}/geoservercloud/models/coveragestore.py +0 -0
  67. {geoservercloud-0.7.2.dev1 → geoservercloud-0.7.2.dev10}/geoservercloud/models/datastore.py +0 -0
  68. {geoservercloud-0.7.2.dev1 → geoservercloud-0.7.2.dev10}/geoservercloud/models/datastores.py +0 -0
  69. {geoservercloud-0.7.2.dev1 → geoservercloud-0.7.2.dev10}/geoservercloud/models/featuretypes.py +0 -0
  70. {geoservercloud-0.7.2.dev1 → geoservercloud-0.7.2.dev10}/geoservercloud/models/layer.py +0 -0
  71. {geoservercloud-0.7.2.dev1 → geoservercloud-0.7.2.dev10}/geoservercloud/models/layergroup.py +0 -0
  72. {geoservercloud-0.7.2.dev1 → geoservercloud-0.7.2.dev10}/geoservercloud/models/layergroups.py +0 -0
  73. {geoservercloud-0.7.2.dev1 → geoservercloud-0.7.2.dev10}/geoservercloud/models/resourcedirectory.py +0 -0
  74. {geoservercloud-0.7.2.dev1 → geoservercloud-0.7.2.dev10}/geoservercloud/models/style.py +0 -0
  75. {geoservercloud-0.7.2.dev1 → geoservercloud-0.7.2.dev10}/geoservercloud/models/styles.py +0 -0
  76. {geoservercloud-0.7.2.dev1 → geoservercloud-0.7.2.dev10}/geoservercloud/models/wmslayer.py +0 -0
  77. {geoservercloud-0.7.2.dev1 → geoservercloud-0.7.2.dev10}/geoservercloud/models/wmssettings.py +0 -0
  78. {geoservercloud-0.7.2.dev1 → geoservercloud-0.7.2.dev10}/geoservercloud/models/wmsstore.py +0 -0
  79. {geoservercloud-0.7.2.dev1 → geoservercloud-0.7.2.dev10}/geoservercloud/models/workspace.py +0 -0
  80. {geoservercloud-0.7.2.dev1 → geoservercloud-0.7.2.dev10}/geoservercloud/models/workspaces.py +0 -0
  81. {geoservercloud-0.7.2.dev1 → geoservercloud-0.7.2.dev10}/geoservercloud/services/__init__.py +0 -0
  82. {geoservercloud-0.7.2.dev1 → geoservercloud-0.7.2.dev10}/geoservercloud/services/owsservice.py +0 -0
  83. {geoservercloud-0.7.2.dev1 → geoservercloud-0.7.2.dev10}/geoservercloud/services/restclient.py +0 -0
  84. {geoservercloud-0.7.2.dev1 → geoservercloud-0.7.2.dev10}/geoservercloud/services/restlogger.py +0 -0
  85. {geoservercloud-0.7.2.dev1 → geoservercloud-0.7.2.dev10}/geoservercloud/services/restservice.py +0 -0
  86. {geoservercloud-0.7.2.dev1 → geoservercloud-0.7.2.dev10}/geoservercloud/templates.py +0 -0
  87. {geoservercloud-0.7.2.dev1 → geoservercloud-0.7.2.dev10}/geoservercloud/utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: geoservercloud
3
- Version: 0.7.2.dev1
3
+ Version: 0.7.2.dev10
4
4
  Summary: Lightweight Python client to interact with GeoServer Cloud REST API, GeoServer ACL and OGC services
5
5
  License: BSD-2-Clause
6
6
  Author: Camptocamp
@@ -7,6 +7,7 @@ from owslib.wmts import WebMapTileService
7
7
  from requests import Response
8
8
 
9
9
  from geoservercloud import utils
10
+ from geoservercloud.models.common import TimeDimensionInfo
10
11
  from geoservercloud.models.coverage import Coverage
11
12
  from geoservercloud.models.coveragestore import CoverageStore
12
13
  from geoservercloud.models.datastore import PostGisDataStore
@@ -639,6 +640,7 @@ class GeoServerCloud:
639
640
  attributes: dict | None = None,
640
641
  epsg: int = 4326,
641
642
  keywords: list[str] = [],
643
+ time_dimension_info: TimeDimensionInfo | None = None,
642
644
  ) -> tuple[str, int]:
643
645
  """
644
646
  Create a feature type or update it if it already exist.
@@ -660,6 +662,7 @@ class GeoServerCloud:
660
662
  attributes=utils.convert_attributes(attributes) if attributes else None,
661
663
  epsg_code=epsg,
662
664
  keywords=keywords,
665
+ time_dimension_info=time_dimension_info,
663
666
  )
664
667
  return self.rest_service.create_feature_type(feature_type=feature_type)
665
668
 
@@ -0,0 +1,376 @@
1
+ import json
2
+ from enum import Enum
3
+ from typing import Any, Generic, TypeVar
4
+
5
+ T = TypeVar("T")
6
+
7
+
8
+ class BaseModel:
9
+ @classmethod
10
+ def from_get_response_payload(cls, content: dict):
11
+ raise NotImplementedError
12
+
13
+
14
+ class EntityModel(BaseModel):
15
+ def asdict(self) -> dict[str, Any]:
16
+ raise NotImplementedError
17
+
18
+ def post_payload(self) -> dict[str, Any]:
19
+ raise NotImplementedError
20
+
21
+ def put_payload(self) -> dict[str, Any]:
22
+ raise NotImplementedError
23
+
24
+ @staticmethod
25
+ def add_items_to_dict(content: dict, items: dict[str, Any]) -> dict[Any, Any]:
26
+ for key, value in items.items():
27
+ content = EntityModel.add_item_to_dict(content, key, value)
28
+ return content
29
+
30
+ @staticmethod
31
+ def add_item_to_dict(content: dict, key: str, value: Any) -> dict[Any, Any]:
32
+ if value is not None:
33
+ content[key] = value
34
+ return content
35
+
36
+
37
+ class ListModel(BaseModel, Generic[T]):
38
+ """Base class for list-based models with configurable list and item names."""
39
+
40
+ # These should be overridden in subclasses
41
+ _list_key: str = "" # e.g., "workspaces", "dataStores"
42
+ _item_key: str = "" # e.g., "workspace", "dataStore"
43
+
44
+ def __init__(self, items: list[T] | None = None) -> None:
45
+ self._items: list[T] = items or []
46
+
47
+ def aslist(self) -> list[T]:
48
+ """Return the list of items."""
49
+ return self._items
50
+
51
+ def find(self, name: str) -> T | None:
52
+ """Find an item by name (assumes items are dicts with 'name' key)."""
53
+ for item in self._items:
54
+ if isinstance(item, dict) and item.get("name") == name:
55
+ return item
56
+ return None
57
+
58
+ @classmethod
59
+ def from_get_response_payload(cls, content: dict):
60
+ """Create instance from API response payload."""
61
+ if not cls._list_key or not cls._item_key:
62
+ raise NotImplementedError("Subclasses must define _list_key and _item_key")
63
+
64
+ data = content.get(cls._list_key)
65
+ if not data:
66
+ return cls()
67
+
68
+ items = data[cls._item_key]
69
+ return cls(items)
70
+
71
+ def __repr__(self) -> str:
72
+ """String representation of the list."""
73
+ return json.dumps(self._items, indent=4)
74
+
75
+
76
+ class ReferencedObjectModel(BaseModel):
77
+ def __init__(self, name: str, href: str | None = None):
78
+ self.name: str = name
79
+ self.href: str | None = href
80
+
81
+ @classmethod
82
+ def from_get_response_payload(cls, content: dict):
83
+ cls.name = content["name"]
84
+ cls.href = content["href"]
85
+
86
+ def asdict(self) -> dict[str, str]:
87
+ return EntityModel.add_item_to_dict({"name": self.name}, "href", self.href)
88
+
89
+
90
+ class KeyDollarListDict(dict):
91
+
92
+ key_prefix: str = "@key"
93
+ value_prefix: str = "$"
94
+
95
+ def __init__(
96
+ self,
97
+ input_list: list | None = None,
98
+ input_dict: dict | None = None,
99
+ *args,
100
+ **kwargs
101
+ ):
102
+ super().__init__(*args, **kwargs)
103
+ if input_list:
104
+ self.deserialize(input_list)
105
+ if input_dict:
106
+ self.update(input_dict)
107
+
108
+ def deserialize(self, input_list: list):
109
+ for item in input_list:
110
+ key = item[self.key_prefix]
111
+ if self.value_prefix in item:
112
+ value = item[self.value_prefix]
113
+ else:
114
+ value = None
115
+ super().__setitem__(key, value)
116
+
117
+ def serialize(self):
118
+ return [
119
+ {self.key_prefix: key, self.value_prefix: value}
120
+ for key, value in self.items()
121
+ ]
122
+
123
+ def __repr__(self) -> str:
124
+ return str(self.serialize())
125
+
126
+ def __str__(self):
127
+ return json.dumps(self.serialize())
128
+
129
+ def update(self, other: dict): # type: ignore
130
+ for key, value in other.items():
131
+ super().__setitem__(key, value)
132
+
133
+
134
+ class I18N:
135
+ """
136
+ Geoserver handles internationalization with 2 possible (mutually exclusive) keys in the rest payload:
137
+ either:
138
+ - [key: string]
139
+ or
140
+ - [internationalKey: dictionary]
141
+
142
+ example:
143
+ a) as key: string we get {"title": "Test Title"}
144
+ b) as key: dict we get {"internationalTitle": {"en": "Test Title", "es": "Título de Prueba"}}
145
+
146
+ This class gives a layer of abstraction to handle both cases.
147
+
148
+ Usage:
149
+ Call the class by adding both possible keys in a tuple and the value.
150
+
151
+ Parameters:
152
+ keys: tuple[str, str] example : ("title", "internationalTitle")
153
+ value: str | dict example: "Test Title" | {"en": "Test Title", "es": "Título de Prueba"}
154
+
155
+ Example:
156
+
157
+ my_i18n = I18N(("title", "internationalTitle"), "Test Title")
158
+ """
159
+
160
+ def __init__(self, keys: tuple[str, Any], value: str | dict) -> None:
161
+ self._str_key = keys[0]
162
+ self._i18n_key = keys[1]
163
+ self._value = value
164
+ if isinstance(value, str):
165
+ self._content = {self.str_key: self._value}
166
+ elif isinstance(value, dict):
167
+ self._content = {self._i18n_key: self._value}
168
+ else:
169
+ raise ValueError("Invalid value type")
170
+
171
+ @property
172
+ def str_key(self):
173
+ return self._str_key
174
+
175
+ @property
176
+ def i18n_key(self):
177
+ return self._i18n_key
178
+
179
+ @property
180
+ def value(self):
181
+ return self._value
182
+
183
+ def asdict(self):
184
+ return self._content
185
+
186
+ def __repr__(self):
187
+ return json.dumps(self._content, indent=4)
188
+
189
+
190
+ class MetadataLink:
191
+ def __init__(self, url: str, metadata_type="TC211", mime_type: str = "text/xml"):
192
+ self.url: str = url
193
+ self.metadata_type: str = metadata_type
194
+ self.type: str = mime_type
195
+
196
+ @classmethod
197
+ def from_get_response_payload(cls, content: dict):
198
+ return cls(
199
+ url=content["content"],
200
+ metadata_type=content["metadataType"],
201
+ mime_type=content["type"],
202
+ )
203
+
204
+ def asdict(self) -> dict[str, str]:
205
+ return {
206
+ "content": self.url,
207
+ "metadataType": self.metadata_type,
208
+ "type": self.type,
209
+ }
210
+
211
+
212
+ class TimeDimensionInfo(BaseModel):
213
+ """
214
+ This class represents the required configuration needed for the dimension "time" to create a
215
+ time enabled :py:class:`geoservercloud.models.FeatureType`
216
+
217
+ Parameters
218
+ ----------
219
+ attribute: str
220
+ Name of the attribute that will be used for the temporal dimension
221
+ presentation: str
222
+ How the time dimension will be represented in the capabilities document
223
+ Possible values are: LIST, DISCRETE_INTERVAL, CONTINUOUS_INTERVAL
224
+ This property will be resolved as a :py:class:`geoservercloud.models.common.Presentation` enum.
225
+ end_attribute: str | None, optional
226
+ Defines the attribute that will be used to define the end of the temporal interval.
227
+ start_value: str | None, optional
228
+ If presentation is DISCRETE_INTERVAL or CONTINUOUS_INTERVAL, this attribute will be used to define the start of the temporal interval.
229
+ end_value: str | None, optional
230
+ If presentation is DISCRETE_INTERVAL or CONTINUOUS_INTERVAL, this attribute will be used to define the end of the temporal interval.
231
+ resolution: int | None, optional
232
+ If presentation is DISCRETE_INTERVAL, this attribute will be used to define the resolution of the temporal interval.
233
+ default_value_strategy: str | None, optional
234
+ Defines which value of the attribute will be used as a default
235
+ Possible values are: MAXIMUM, MINIMUM, NEAREST, FIXED, EMPTY
236
+ This property will be resolved as a :py:class:`geoservercloud.models.common.DefaultValueStrategy` enum.
237
+ reference_value: str | None, optional
238
+ This value will be used as the reference value for the :py:class:`geoservercloud.models.common.DefaultValueStrategy`
239
+ NEAREST and FIXED strategy.
240
+ nearest_match_enabled: bool | None, optional
241
+ Defines whether the nearest match should be used for the attribute values.
242
+ nearest_fail_behavior: str | None, optional
243
+ Fail behavior for the nearest match.
244
+ Possible values are: IGNORE, EXCEPTION.
245
+ This property will be resolved as a :py:class:`geoservercloud.models.common.NearestFailBehavior` enum.
246
+ acceptable_interval: str | None, optional
247
+ Defines the acceptable interval for the nearest match behavior.
248
+ A single value, or two values separated by slash. Time values must use the ISO period syntax (e.g., PT1H)
249
+ """
250
+
251
+ def __init__(
252
+ self,
253
+ attribute: str,
254
+ presentation: str,
255
+ end_attribute: str | None = None,
256
+ start_value: str | None = None,
257
+ end_value: str | None = None,
258
+ resolution: int | None = None,
259
+ default_value_strategy: str | None = None,
260
+ reference_value: str | None = None,
261
+ nearest_match_enabled: bool | None = None,
262
+ nearest_fail_behavior: str | None = None,
263
+ acceptable_interval: str | None = None,
264
+ ):
265
+ self.dimension: str = "time"
266
+ self.enabled: bool = True
267
+ self.attribute: str = attribute
268
+ self.end_attribute: str | None = end_attribute
269
+ self.presentation: Presentation = Presentation(presentation)
270
+ self.start_value: str | None = start_value
271
+ self.end_value: str | None = end_value
272
+ self.resolution: int | None = resolution
273
+ self.units: str | None = "ISO8601"
274
+ self.default_value_strategy: DefaultValueStrategy | None = DefaultValueStrategy(
275
+ default_value_strategy
276
+ )
277
+ self.reference_value: str | None = reference_value
278
+ self.nearest_match_enabled: bool | None = nearest_match_enabled
279
+ self.nearest_fail_behavior: NearestFailBehavior | None = (
280
+ NearestFailBehavior(nearest_fail_behavior)
281
+ if nearest_fail_behavior
282
+ else None
283
+ )
284
+ self.acceptable_interval: str | None = acceptable_interval
285
+
286
+ @classmethod
287
+ def from_get_response_payload(cls, content: dict):
288
+ time_dimension_info: dict[str, Any] = content["dimensionInfo"]
289
+ return cls(
290
+ attribute=time_dimension_info["attribute"],
291
+ presentation=time_dimension_info["presentation"],
292
+ end_attribute=time_dimension_info.get("endAttribute"),
293
+ start_value=time_dimension_info.get("startValue"),
294
+ end_value=time_dimension_info.get("endValue"),
295
+ resolution=time_dimension_info.get("resolution"),
296
+ default_value_strategy=time_dimension_info.get("defaultValue", {}).get(
297
+ "strategy"
298
+ ),
299
+ reference_value=time_dimension_info.get("defaultValue", {}).get(
300
+ "referenceValue"
301
+ ),
302
+ nearest_match_enabled=time_dimension_info.get("nearestMatchEnabled"),
303
+ nearest_fail_behavior=time_dimension_info.get(
304
+ "nearestFailBehavior", {}
305
+ ).get("value"),
306
+ acceptable_interval=time_dimension_info.get("acceptableInterval"),
307
+ )
308
+
309
+ def asdict(self) -> dict[str, Any]:
310
+ content: dict[str, Any] = {
311
+ "@key": self.dimension,
312
+ }
313
+
314
+ dimension_info: dict[str, Any] = {
315
+ "enabled": self.enabled,
316
+ "attribute": self.attribute,
317
+ "presentation": self.presentation.value,
318
+ "units": self.units,
319
+ }
320
+
321
+ optional_items: dict[str, Any] = {
322
+ "endAttribute": self.end_attribute,
323
+ "nearestMatchEnabled": self.nearest_match_enabled,
324
+ "nearestFailBehavior": (
325
+ self.nearest_fail_behavior.value if self.nearest_fail_behavior else None
326
+ ),
327
+ "acceptableInterval": self.acceptable_interval,
328
+ }
329
+
330
+ if (
331
+ self.presentation == Presentation.DISCRETE_INTERVAL
332
+ or self.presentation == Presentation.CONTINUOUS_INTERVAL
333
+ ):
334
+ EntityModel.add_item_to_dict(optional_items, "startValue", self.start_value)
335
+ EntityModel.add_item_to_dict(optional_items, "endValue", self.end_value)
336
+ if self.presentation == Presentation.DISCRETE_INTERVAL:
337
+ EntityModel.add_item_to_dict(
338
+ optional_items, "resolution", self.resolution
339
+ )
340
+
341
+ if self.default_value_strategy:
342
+ default_value_strategy: dict[str, str] = {
343
+ "strategy": self.default_value_strategy.value,
344
+ }
345
+ if (
346
+ self.default_value_strategy == DefaultValueStrategy.FIXED
347
+ or self.default_value_strategy == DefaultValueStrategy.NEAREST
348
+ ):
349
+ EntityModel.add_item_to_dict(
350
+ default_value_strategy, "referenceValue", self.reference_value
351
+ )
352
+ EntityModel.add_item_to_dict(
353
+ dimension_info, "defaultValue", default_value_strategy
354
+ )
355
+
356
+ EntityModel.add_items_to_dict(dimension_info, optional_items)
357
+ EntityModel.add_item_to_dict(content, "dimensionInfo", dimension_info)
358
+ return content
359
+
360
+
361
+ class DefaultValueStrategy(Enum):
362
+ MAXIMUM = "MAXIMUM"
363
+ MINIMUM = "MINIMUM"
364
+ NEAREST = "NEAREST"
365
+ FIXED = "FIXED"
366
+
367
+
368
+ class Presentation(Enum):
369
+ LIST = "LIST"
370
+ DISCRETE_INTERVAL = "DISCRETE_INTERVAL"
371
+ CONTINUOUS_INTERVAL = "CONTINUOUS_INTERVAL"
372
+
373
+
374
+ class NearestFailBehavior(Enum):
375
+ IGNORE = "IGNORE"
376
+ EXCEPTION = "EXCEPTION"
@@ -5,10 +5,14 @@ from geoservercloud.models.abstractlayer import AbstractLayer
5
5
  from geoservercloud.models.common import (
6
6
  EntityModel,
7
7
  MetadataLink,
8
+ TimeDimensionInfo,
8
9
  )
9
10
 
10
11
 
11
12
  class FeatureType(AbstractLayer):
13
+
14
+ TIME_DIMENSION_KEY: str = "time"
15
+
12
16
  def __init__(
13
17
  self,
14
18
  # Mandatory fields
@@ -40,6 +44,7 @@ class FeatureType(AbstractLayer):
40
44
  circular_arc_present: bool | None = None,
41
45
  encode_measures: bool | None = None,
42
46
  metadata_links: list[MetadataLink] | None = None,
47
+ time_dimension_info: TimeDimensionInfo | None = None,
43
48
  ) -> None:
44
49
  super().__init__(
45
50
  name=name,
@@ -70,6 +75,7 @@ class FeatureType(AbstractLayer):
70
75
  self.circular_arc_present: bool | None = circular_arc_present
71
76
  self.encode_measures: bool | None = encode_measures
72
77
  self.metadata_links: list[MetadataLink] | None = metadata_links
78
+ self.time_dimension_info: TimeDimensionInfo | None = time_dimension_info
73
79
 
74
80
  @classmethod
75
81
  def from_get_response_payload(cls, content: dict):
@@ -89,6 +95,16 @@ class FeatureType(AbstractLayer):
89
95
  else:
90
96
  metadata_links = None
91
97
 
98
+ # Check if the feature type contains information about the dimensions "time"
99
+ time_dimension_info: TimeDimensionInfo | None = None
100
+ if feature_type.get("metadata"):
101
+ metadata: list[Any] = feature_type["metadata"]["entry"]
102
+ for metadata_entry in metadata:
103
+ if metadata_entry["@key"] == FeatureType.TIME_DIMENSION_KEY:
104
+ time_dimension_info = TimeDimensionInfo.from_get_response_payload(
105
+ metadata_entry
106
+ )
107
+
92
108
  return cls(
93
109
  namespace_name=feature_type["namespace"]["name"],
94
110
  name=feature_type["name"],
@@ -114,6 +130,7 @@ class FeatureType(AbstractLayer):
114
130
  forced_decimals=feature_type.get("forcedDecimals"),
115
131
  simple_conversion_enabled=feature_type.get("simpleConversionEnabled"),
116
132
  skip_number_match=feature_type.get("skipNumberMatch"),
133
+ time_dimension_info=time_dimension_info,
117
134
  )
118
135
 
119
136
  def asdict(self) -> dict[str, Any]:
@@ -132,6 +149,11 @@ class FeatureType(AbstractLayer):
132
149
  "circularArcPresent": self.circular_arc_present,
133
150
  "encodeMeasures": self.encode_measures,
134
151
  }
152
+
153
+ if self.time_dimension_info:
154
+ metadata = {"entry": [self.time_dimension_info.asdict()]}
155
+ EntityModel.add_item_to_dict(optional_items, "metadata", metadata)
156
+
135
157
  return EntityModel.add_items_to_dict(content, optional_items)
136
158
 
137
159
  def post_payload(self) -> dict[str, Any]:
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "geoservercloud"
3
- version = "0.7.2.dev1"
3
+ version = "0.7.2.dev10"
4
4
  description = "Lightweight Python client to interact with GeoServer Cloud REST API, GeoServer ACL and OGC services"
5
5
  authors = ["Camptocamp <info@camptocamp.com>"]
6
6
  license = "BSD-2-Clause"
@@ -1,208 +0,0 @@
1
- import json
2
- from typing import Any, Generic, TypeVar
3
-
4
- T = TypeVar("T")
5
-
6
-
7
- class BaseModel:
8
- @classmethod
9
- def from_get_response_payload(cls, content: dict):
10
- raise NotImplementedError
11
-
12
-
13
- class EntityModel(BaseModel):
14
- def asdict(self) -> dict[str, Any]:
15
- raise NotImplementedError
16
-
17
- def post_payload(self) -> dict[str, Any]:
18
- raise NotImplementedError
19
-
20
- def put_payload(self) -> dict[str, Any]:
21
- raise NotImplementedError
22
-
23
- @staticmethod
24
- def add_items_to_dict(content: dict, items: dict[str, Any]) -> dict[Any, Any]:
25
- for key, value in items.items():
26
- content = EntityModel.add_item_to_dict(content, key, value)
27
- return content
28
-
29
- @staticmethod
30
- def add_item_to_dict(content: dict, key: str, value: Any) -> dict[Any, Any]:
31
- if value is not None:
32
- content[key] = value
33
- return content
34
-
35
-
36
- class ListModel(BaseModel, Generic[T]):
37
- """Base class for list-based models with configurable list and item names."""
38
-
39
- # These should be overridden in subclasses
40
- _list_key: str = "" # e.g., "workspaces", "dataStores"
41
- _item_key: str = "" # e.g., "workspace", "dataStore"
42
-
43
- def __init__(self, items: list[T] | None = None) -> None:
44
- self._items: list[T] = items or []
45
-
46
- def aslist(self) -> list[T]:
47
- """Return the list of items."""
48
- return self._items
49
-
50
- def find(self, name: str) -> T | None:
51
- """Find an item by name (assumes items are dicts with 'name' key)."""
52
- for item in self._items:
53
- if isinstance(item, dict) and item.get("name") == name:
54
- return item
55
- return None
56
-
57
- @classmethod
58
- def from_get_response_payload(cls, content: dict):
59
- """Create instance from API response payload."""
60
- if not cls._list_key or not cls._item_key:
61
- raise NotImplementedError("Subclasses must define _list_key and _item_key")
62
-
63
- data = content.get(cls._list_key)
64
- if not data:
65
- return cls()
66
-
67
- items = data[cls._item_key]
68
- return cls(items)
69
-
70
- def __repr__(self) -> str:
71
- """String representation of the list."""
72
- return json.dumps(self._items, indent=4)
73
-
74
-
75
- class ReferencedObjectModel(BaseModel):
76
- def __init__(self, name: str, href: str | None = None):
77
- self.name: str = name
78
- self.href: str | None = href
79
-
80
- @classmethod
81
- def from_get_response_payload(cls, content: dict):
82
- cls.name = content["name"]
83
- cls.href = content["href"]
84
-
85
- def asdict(self) -> dict[str, str]:
86
- return EntityModel.add_item_to_dict({"name": self.name}, "href", self.href)
87
-
88
-
89
- class KeyDollarListDict(dict):
90
-
91
- key_prefix: str = "@key"
92
- value_prefix: str = "$"
93
-
94
- def __init__(
95
- self,
96
- input_list: list | None = None,
97
- input_dict: dict | None = None,
98
- *args,
99
- **kwargs
100
- ):
101
- super().__init__(*args, **kwargs)
102
- if input_list:
103
- self.deserialize(input_list)
104
- if input_dict:
105
- self.update(input_dict)
106
-
107
- def deserialize(self, input_list: list):
108
- for item in input_list:
109
- key = item[self.key_prefix]
110
- if self.value_prefix in item:
111
- value = item[self.value_prefix]
112
- else:
113
- value = None
114
- super().__setitem__(key, value)
115
-
116
- def serialize(self):
117
- return [
118
- {self.key_prefix: key, self.value_prefix: value}
119
- for key, value in self.items()
120
- ]
121
-
122
- def __repr__(self) -> str:
123
- return str(self.serialize())
124
-
125
- def __str__(self):
126
- return json.dumps(self.serialize())
127
-
128
- def update(self, other: dict): # type: ignore
129
- for key, value in other.items():
130
- super().__setitem__(key, value)
131
-
132
-
133
- class I18N:
134
- """
135
- Geoserver handles internationalization with 2 possible (mutually exclusive) keys in the rest payload:
136
- either:
137
- - [key: string]
138
- or
139
- - [internationalKey: dictionary]
140
-
141
- example:
142
- a) as key: string we get {"title": "Test Title"}
143
- b) as key: dict we get {"internationalTitle": {"en": "Test Title", "es": "Título de Prueba"}}
144
-
145
- This class gives a layer of abstraction to handle both cases.
146
-
147
- Usage:
148
- Call the class by adding both possible keys in a tuple and the value.
149
-
150
- Parameters:
151
- keys: tuple[str, str] example : ("title", "internationalTitle")
152
- value: str | dict example: "Test Title" | {"en": "Test Title", "es": "Título de Prueba"}
153
-
154
- Example:
155
-
156
- my_i18n = I18N(("title", "internationalTitle"), "Test Title")
157
- """
158
-
159
- def __init__(self, keys: tuple[str, Any], value: str | dict) -> None:
160
- self._str_key = keys[0]
161
- self._i18n_key = keys[1]
162
- self._value = value
163
- if isinstance(value, str):
164
- self._content = {self.str_key: self._value}
165
- elif isinstance(value, dict):
166
- self._content = {self._i18n_key: self._value}
167
- else:
168
- raise ValueError("Invalid value type")
169
-
170
- @property
171
- def str_key(self):
172
- return self._str_key
173
-
174
- @property
175
- def i18n_key(self):
176
- return self._i18n_key
177
-
178
- @property
179
- def value(self):
180
- return self._value
181
-
182
- def asdict(self):
183
- return self._content
184
-
185
- def __repr__(self):
186
- return json.dumps(self._content, indent=4)
187
-
188
-
189
- class MetadataLink:
190
- def __init__(self, url: str, metadata_type="TC211", mime_type: str = "text/xml"):
191
- self.url: str = url
192
- self.metadata_type: str = metadata_type
193
- self.type: str = mime_type
194
-
195
- @classmethod
196
- def from_get_response_payload(cls, content: dict):
197
- return cls(
198
- url=content["content"],
199
- metadata_type=content["metadataType"],
200
- mime_type=content["type"],
201
- )
202
-
203
- def asdict(self) -> dict[str, str]:
204
- return {
205
- "content": self.url,
206
- "metadataType": self.metadata_type,
207
- "type": self.type,
208
- }