fusion-platform-python-sdk 1.12.2__tar.gz → 1.13.5__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 (57) hide show
  1. {fusion-platform-python-sdk-1.12.2/fusion_platform_python_sdk.egg-info → fusion-platform-python-sdk-1.13.5}/PKG-INFO +1 -1
  2. {fusion-platform-python-sdk-1.12.2 → fusion-platform-python-sdk-1.13.5}/fusion_platform/__init__.py +5 -2
  3. {fusion-platform-python-sdk-1.12.2 → fusion-platform-python-sdk-1.13.5}/fusion_platform/common/utilities.py +44 -0
  4. {fusion-platform-python-sdk-1.12.2 → fusion-platform-python-sdk-1.13.5}/fusion_platform/fusion_platform_sdk.pdf +0 -0
  5. {fusion-platform-python-sdk-1.12.2 → fusion-platform-python-sdk-1.13.5}/fusion_platform/models/data.py +87 -6
  6. {fusion-platform-python-sdk-1.12.2 → fusion-platform-python-sdk-1.13.5}/fusion_platform/models/data_file.py +177 -1
  7. {fusion-platform-python-sdk-1.12.2 → fusion-platform-python-sdk-1.13.5}/fusion_platform/models/model.py +25 -5
  8. {fusion-platform-python-sdk-1.12.2 → fusion-platform-python-sdk-1.13.5}/fusion_platform/models/organisation.py +4 -3
  9. {fusion-platform-python-sdk-1.12.2 → fusion-platform-python-sdk-1.13.5}/fusion_platform/models/process.md +2 -0
  10. {fusion-platform-python-sdk-1.12.2 → fusion-platform-python-sdk-1.13.5}/fusion_platform/models/process.py +12 -5
  11. {fusion-platform-python-sdk-1.12.2 → fusion-platform-python-sdk-1.13.5}/fusion_platform/models/service.py +2 -1
  12. {fusion-platform-python-sdk-1.12.2 → fusion-platform-python-sdk-1.13.5}/fusion_platform/translations.py +2 -0
  13. {fusion-platform-python-sdk-1.12.2 → fusion-platform-python-sdk-1.13.5/fusion_platform_python_sdk.egg-info}/PKG-INFO +1 -1
  14. {fusion-platform-python-sdk-1.12.2 → fusion-platform-python-sdk-1.13.5}/setup.py +1 -1
  15. {fusion-platform-python-sdk-1.12.2 → fusion-platform-python-sdk-1.13.5}/LICENSE.txt +0 -0
  16. {fusion-platform-python-sdk-1.12.2 → fusion-platform-python-sdk-1.13.5}/MANIFEST.in +0 -0
  17. {fusion-platform-python-sdk-1.12.2 → fusion-platform-python-sdk-1.13.5}/README.md +0 -0
  18. {fusion-platform-python-sdk-1.12.2 → fusion-platform-python-sdk-1.13.5}/fusion_platform/__main__.py +0 -0
  19. {fusion-platform-python-sdk-1.12.2 → fusion-platform-python-sdk-1.13.5}/fusion_platform/base.py +0 -0
  20. {fusion-platform-python-sdk-1.12.2 → fusion-platform-python-sdk-1.13.5}/fusion_platform/command.py +0 -0
  21. {fusion-platform-python-sdk-1.12.2 → fusion-platform-python-sdk-1.13.5}/fusion_platform/common/__init__.py +0 -0
  22. {fusion-platform-python-sdk-1.12.2 → fusion-platform-python-sdk-1.13.5}/fusion_platform/common/raise_thread.py +0 -0
  23. {fusion-platform-python-sdk-1.12.2 → fusion-platform-python-sdk-1.13.5}/fusion_platform/detailed_example.py +0 -0
  24. {fusion-platform-python-sdk-1.12.2 → fusion-platform-python-sdk-1.13.5}/fusion_platform/documentation.py +0 -0
  25. {fusion-platform-python-sdk-1.12.2 → fusion-platform-python-sdk-1.13.5}/fusion_platform/glasgow.geojson +0 -0
  26. {fusion-platform-python-sdk-1.12.2 → fusion-platform-python-sdk-1.13.5}/fusion_platform/lake_district.geojson +0 -0
  27. {fusion-platform-python-sdk-1.12.2 → fusion-platform-python-sdk-1.13.5}/fusion_platform/localisations.py +0 -0
  28. {fusion-platform-python-sdk-1.12.2 → fusion-platform-python-sdk-1.13.5}/fusion_platform/localise.py +0 -0
  29. {fusion-platform-python-sdk-1.12.2 → fusion-platform-python-sdk-1.13.5}/fusion_platform/models/__init__.py +0 -0
  30. {fusion-platform-python-sdk-1.12.2 → fusion-platform-python-sdk-1.13.5}/fusion_platform/models/credit.md +0 -0
  31. {fusion-platform-python-sdk-1.12.2 → fusion-platform-python-sdk-1.13.5}/fusion_platform/models/credit.py +0 -0
  32. {fusion-platform-python-sdk-1.12.2 → fusion-platform-python-sdk-1.13.5}/fusion_platform/models/data.md +0 -0
  33. {fusion-platform-python-sdk-1.12.2 → fusion-platform-python-sdk-1.13.5}/fusion_platform/models/data_file.md +0 -0
  34. {fusion-platform-python-sdk-1.12.2 → fusion-platform-python-sdk-1.13.5}/fusion_platform/models/fields.py +0 -0
  35. {fusion-platform-python-sdk-1.12.2 → fusion-platform-python-sdk-1.13.5}/fusion_platform/models/organisation.md +0 -0
  36. {fusion-platform-python-sdk-1.12.2 → fusion-platform-python-sdk-1.13.5}/fusion_platform/models/process_execution.md +0 -0
  37. {fusion-platform-python-sdk-1.12.2 → fusion-platform-python-sdk-1.13.5}/fusion_platform/models/process_execution.py +0 -0
  38. {fusion-platform-python-sdk-1.12.2 → fusion-platform-python-sdk-1.13.5}/fusion_platform/models/process_service_execution.md +0 -0
  39. {fusion-platform-python-sdk-1.12.2 → fusion-platform-python-sdk-1.13.5}/fusion_platform/models/process_service_execution.py +0 -0
  40. {fusion-platform-python-sdk-1.12.2 → fusion-platform-python-sdk-1.13.5}/fusion_platform/models/process_service_execution_log.md +0 -0
  41. {fusion-platform-python-sdk-1.12.2 → fusion-platform-python-sdk-1.13.5}/fusion_platform/models/process_service_execution_log.py +0 -0
  42. {fusion-platform-python-sdk-1.12.2 → fusion-platform-python-sdk-1.13.5}/fusion_platform/models/service.md +0 -0
  43. {fusion-platform-python-sdk-1.12.2 → fusion-platform-python-sdk-1.13.5}/fusion_platform/models/user.md +0 -0
  44. {fusion-platform-python-sdk-1.12.2 → fusion-platform-python-sdk-1.13.5}/fusion_platform/models/user.py +0 -0
  45. {fusion-platform-python-sdk-1.12.2 → fusion-platform-python-sdk-1.13.5}/fusion_platform/quick_example.py +0 -0
  46. {fusion-platform-python-sdk-1.12.2 → fusion-platform-python-sdk-1.13.5}/fusion_platform/session.py +0 -0
  47. {fusion-platform-python-sdk-1.12.2 → fusion-platform-python-sdk-1.13.5}/fusion_platform_python_sdk.egg-info/SOURCES.txt +0 -0
  48. {fusion-platform-python-sdk-1.12.2 → fusion-platform-python-sdk-1.13.5}/fusion_platform_python_sdk.egg-info/dependency_links.txt +0 -0
  49. {fusion-platform-python-sdk-1.12.2 → fusion-platform-python-sdk-1.13.5}/fusion_platform_python_sdk.egg-info/entry_points.txt +0 -0
  50. {fusion-platform-python-sdk-1.12.2 → fusion-platform-python-sdk-1.13.5}/fusion_platform_python_sdk.egg-info/requires.txt +0 -0
  51. {fusion-platform-python-sdk-1.12.2 → fusion-platform-python-sdk-1.13.5}/fusion_platform_python_sdk.egg-info/top_level.txt +0 -0
  52. {fusion-platform-python-sdk-1.12.2 → fusion-platform-python-sdk-1.13.5}/setup.cfg +0 -0
  53. {fusion-platform-python-sdk-1.12.2 → fusion-platform-python-sdk-1.13.5}/tests/test_base.py +0 -0
  54. {fusion-platform-python-sdk-1.12.2 → fusion-platform-python-sdk-1.13.5}/tests/test_command.py +0 -0
  55. {fusion-platform-python-sdk-1.12.2 → fusion-platform-python-sdk-1.13.5}/tests/test_init.py +0 -0
  56. {fusion-platform-python-sdk-1.12.2 → fusion-platform-python-sdk-1.13.5}/tests/test_localise.py +0 -0
  57. {fusion-platform-python-sdk-1.12.2 → fusion-platform-python-sdk-1.13.5}/tests/test_session.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: fusion-platform-python-sdk
3
- Version: 1.12.2
3
+ Version: 1.13.5
4
4
  Summary: Python SDK used to interact with the Fusion Platform(r)
5
5
  Home-page: https://github.com/d-cat-support/fusion-platform-python-sdk
6
6
  Author: Digital Content Analysis Technology Ltd
@@ -11,8 +11,8 @@ API, and this is defined within the corresponding OpenAPI 3.0 specification, whi
11
11
  """
12
12
 
13
13
  # Do not modify the following two lines as they are maintained by the version.sh script.
14
- __version__ = '1.12.2'
15
- __version_date__ = '2024-06-11T07:57:10Z'
14
+ __version__ = '1.13.5'
15
+ __version_date__ = '2024-08-08T11:19:48Z'
16
16
 
17
17
  # Exclude certain sub-modules from documentation.
18
18
  # @formatter:off
@@ -102,6 +102,9 @@ FILE_TYPE_CSV = 'CSV'
102
102
  FILE_TYPE_ESRI_SHAPEFILE = 'ESRI Shapefile'
103
103
  FILE_TYPE_JPEG = 'JPEG'
104
104
  FILE_TYPE_PNG = 'PNG'
105
+ FILE_TYPE_PDF = 'PDF'
106
+ FILE_TYPE_GZIP = 'GZIP'
107
+ FILE_TYPE_ZIP = 'ZIP'
105
108
  FILE_TYPE_OTHER = 'Other'
106
109
 
107
110
  # Run types.
@@ -8,9 +8,40 @@ author: Matthew Casey
8
8
 
9
9
  from datetime import date, datetime, time, timezone
10
10
  from decimal import Decimal
11
+ import re
11
12
  from types import MappingProxyType
12
13
 
13
14
 
15
+ def datetime_parse(string_or_blank):
16
+ """
17
+ Attempts to parse an ISO8601 datetime from a string using common formats.
18
+
19
+ Args:
20
+ string_or_blank: The datetime to parse, or blank.
21
+
22
+ Returns:
23
+ The parsed datetime or None if not parsable or blank.
24
+ """
25
+ parsed = None
26
+
27
+ if not string_blank(string_or_blank):
28
+ # Strip out all optional delimiters and then attempt to parse with and without the "Z".
29
+ stripped = re.sub(r"[:]|([-](?!((\d{2}[:]\d{2})|(\d{4}))$))", '', str(string_or_blank))
30
+ formats = ['%Y%m%dT%H%M%S.%f', '%Y%m%dT%H%M%S.%f%z', '%Y%m%dT%H%M%S', '%Y%m%dT%H%M%S%z']
31
+
32
+ for format in formats:
33
+ try:
34
+ parsed = datetime.strptime(stripped, format)
35
+ parsed = parsed.astimezone(timezone.utc) if parsed.tzinfo is None else parsed # Assume UTC if no timezone is provided.
36
+ except:
37
+ pass
38
+
39
+ if parsed is not None:
40
+ break
41
+
42
+ return parsed
43
+
44
+
14
45
  def dict_nested_get(dictionary_or_value, keys, default=None):
15
46
  """
16
47
  Performs a dictionary.get(key, default) using the supplied list of keys assuming that each successive key is nested.
@@ -60,6 +91,19 @@ def json_default(value):
60
91
  return str(value)
61
92
 
62
93
 
94
+ def string_blank(string_or_blank):
95
+ """
96
+ Checks if a string is None or blank.
97
+
98
+ Args:
99
+ string_or_blank: The string to check.
100
+
101
+ Returns:
102
+ True if the string is None, blank ("") or just made up with spaces.
103
+ """
104
+ return (string_or_blank is None) or (not bool(string_or_blank.strip()))
105
+
106
+
63
107
  def string_camel_to_underscore(string):
64
108
  """
65
109
  Converts a camel case string to underscore case.
@@ -44,10 +44,12 @@ class DataSchema(Schema):
44
44
  bounds = fields.List(fields.Decimal(required=True), allow_none=True, metadata={'read_only': True}) # Changed to prevent this being updated.
45
45
  file_with_preview = fields.UUID(allow_none=True, metadata={'read_only': True}) # Changed to prevent this being updated.
46
46
 
47
+ # Removed uploaded.
47
48
  uploaded_organisation_id = fields.UUID(allow_none=True, metadata={'read_only': True}) # Changed to prevent this being updated.
48
49
  deletable = fields.String(allow_none=True, metadata={'read_only': True}) # Changed to prevent this being updated.
49
50
 
50
51
  # Removed creator.
52
+
51
53
  # Removed search.
52
54
 
53
55
  class Meta:
@@ -213,7 +215,7 @@ class Data(Model):
213
215
  # Return the copy.
214
216
  return self.__class__._model_from_api_id(self._session, organisation_id=self.organisation_id, id=copy_data_id)
215
217
 
216
- def _create(self, name, file_type, files, wait=False):
218
+ def _create(self, name, file_type, files, wait=False, **kwargs):
217
219
  """
218
220
  Attempts to create the model object with the given values. This assumes the model template has been loaded first using the _new method, and that the model
219
221
  is then created using a POST RESTful request. This assumes that the post body contains key names which include the name of the model class.
@@ -221,10 +223,11 @@ class Data(Model):
221
223
  This method is overridden to also upload the corresponding files and optionally wait for the upload and analysis to complete.
222
224
 
223
225
  Args:
224
- name: The name of the data object.
225
- file_type: The type of files that the data object will hold.
226
- files: The list of file paths to be uploaded.
227
- wait: Optionally wait for the upload and analysis to complete? Default False.
226
+ name: The name of the data object.
227
+ file_type: The type of files that the data object will hold.
228
+ files: The list of file paths to be uploaded.
229
+ wait: Optionally wait for the upload and analysis to complete? Default False.
230
+ kwargs: The model attributes which override those already in the template.
228
231
 
229
232
  Returns:
230
233
  The created data object.
@@ -234,7 +237,7 @@ class Data(Model):
234
237
  ModelError: if the model could not be created and validated by the Fusion Platform<sup>&reg;</sup>.
235
238
  """
236
239
  # Use the super method to create the data item with the correct attributes. This will raise an exception if anything fails.
237
- super(Data, self)._create(name=name)
240
+ super(Data, self)._create(name=name, **kwargs)
238
241
 
239
242
  # Add each of the files, assuming that each is of the same file type, and start its upload in a thread.
240
243
  try:
@@ -341,3 +344,81 @@ class Data(Model):
341
344
  ModelError: if a model could not be loaded or validated from the Fusion Platform<sup>&reg;</sup>.
342
345
  """
343
346
  return DataFile._models_from_api_path(self._session, self._get_path(self.__class__._PATH_FILES), organisation_id=self.organisation_id)
347
+
348
+ def get_stac_collection(self, items, collection_file_name=DataFile._STAC_COLLECTION_FILE_NAME, owner=None, created_at=None, detail=None):
349
+ """
350
+ Converts the data representation into a STAC collection with the specified STAC item file names.
351
+
352
+ Args:
353
+ items: The list of items to be put into the collection. This should be a tuple of (STAC item, item_file_name).
354
+ collection_file_name: The optional collection file name.
355
+ owner: The optional owner of the collection. Default None.
356
+ created_at: Optionally when the collection was created. Default None.
357
+ detail: The optional collection detail as a dictionary. Default None.
358
+
359
+ Returns:
360
+ A tuple of the STAC collection definition as a dictionary and the collection file name used in the definition.
361
+ """
362
+ stac_extensions = []
363
+
364
+ # Form the providers.
365
+ provider = {
366
+ 'name': i18n.t('fusion_platform.organisation'),
367
+ 'roles': ['producer', 'licensor'],
368
+ 'url': i18n.t('fusion_platform.url')
369
+ }
370
+
371
+ # Add in the optional processing information, if any.
372
+ if owner is not None:
373
+ provider['processing:lineage'] = owner
374
+
375
+ if created_at is not None:
376
+ provider['processing.datetime'] = created_at
377
+
378
+ if detail is not None:
379
+ provider['processing:software'] = detail
380
+
381
+ if (owner is not None) or (created_at is not None) or (detail is not None):
382
+ stac_extensions.append('https://stac-extensions.github.io/processing/v1.2.0/schema.json')
383
+
384
+ # Form the links.
385
+ links = [
386
+ {'rel': 'self', 'href': collection_file_name, 'type': 'application/json'},
387
+ {'rel': 'root', 'href': collection_file_name, 'type': 'application/json'},
388
+ ]
389
+
390
+ # Add in the files calculating the maximal spatial and temporal extents.
391
+ bbox = None
392
+ interval_start = None
393
+ interval_end = None
394
+
395
+ for item, item_file_name in items:
396
+ # Calculate the spatial extent.
397
+ item_bbox = item.get('bbox')
398
+
399
+ if (bbox is None) or (len(bbox) < 4):
400
+ bbox = item_bbox
401
+
402
+ if (bbox is not None) and (len(bbox) >= 4) and (item_bbox is not None) and (len(item_bbox) >= 4):
403
+ bbox = [min(bbox[0], item_bbox[0]), min(bbox[1], item_bbox[1]), max(bbox[2], item_bbox[2]), max(bbox[3], item_bbox[3])]
404
+
405
+ # Calculate the temporal extent.
406
+ item_datetime = dict_nested_get(item, ['properties', 'datetime'])
407
+ interval_start = item_datetime if interval_start is None else min(interval_start, item_datetime)
408
+ interval_end = item_datetime if interval_end is None else max(interval_end, item_datetime)
409
+
410
+ # Add in the link.
411
+ links.append({'rel': 'item', 'href': item_file_name, 'type': 'application/geo+json'})
412
+
413
+ # Build the STAC collection.
414
+ return {
415
+ 'type': 'Collection',
416
+ 'stac_version': '1.0.0',
417
+ 'stac_extensions': stac_extensions,
418
+ 'id': self.id,
419
+ 'description': self.name,
420
+ 'license': 'proprietary',
421
+ 'providers': [provider],
422
+ 'extent': {'spatial': {'bbox': [bbox]}, 'temporal': {'interval': [[interval_start, interval_end]]}},
423
+ 'links': links
424
+ }, collection_file_name
@@ -8,9 +8,11 @@ author: Matthew Casey
8
8
 
9
9
  import i18n
10
10
  from marshmallow import Schema, EXCLUDE
11
+ import os
11
12
 
13
+ import fusion_platform
12
14
  from fusion_platform.common.raise_thread import RaiseThread
13
- from fusion_platform.common.utilities import dict_nested_get
15
+ from fusion_platform.common.utilities import datetime_parse, dict_nested_get
14
16
  from fusion_platform.models import fields
15
17
  from fusion_platform.models.model import Model, ModelError
16
18
 
@@ -65,6 +67,7 @@ class DataFileSchema(Schema):
65
67
  organisation_id = fields.UUID(allow_none=True, metadata={'read_only': True, 'title': i18n.t('models.data_file.organisation_id.title'), 'description': i18n.t(
66
68
  'models.data_file.organisation_id.description')}) # Added parameter so that it can be used.
67
69
  data_id = fields.UUID(required=True, metadata={'read_only': True}) # Changed to prevent this being updated.
70
+ # Removed organisation_id_file_type.
68
71
 
69
72
  file_id = fields.UUID(required=True, metadata={'read_only': True}) # Changed to prevent this being updated.
70
73
  preview_file_id = fields.UUID(allow_none=True, metadata={'read_only': True}) # Changed to prevent this being updated.
@@ -120,6 +123,34 @@ class DataFile(Model):
120
123
  # Plus query parameters.
121
124
  _QUERY_PREVIEW = 'preview'
122
125
 
126
+ # Geospatial file types.
127
+ _GEOSPATIAL_RASTER = 'raster'
128
+ _GEOSPATIAL_VECTOR = 'vector'
129
+
130
+ # File type information - a tuple of typical extension and whether the file type is a geospatial vector, raster or not.
131
+ # @formatter:off
132
+ _FILE_TYPES = {
133
+ fusion_platform.FILE_TYPE_GEOTIFF: ('tif', _GEOSPATIAL_RASTER),
134
+ fusion_platform.FILE_TYPE_JPEG2000: ('jpeg', _GEOSPATIAL_RASTER),
135
+ fusion_platform.FILE_TYPE_DEM: ('dem', _GEOSPATIAL_RASTER),
136
+ fusion_platform.FILE_TYPE_GEOJSON: ('json', _GEOSPATIAL_VECTOR),
137
+ fusion_platform.FILE_TYPE_KML: ('kml', _GEOSPATIAL_VECTOR),
138
+ fusion_platform.FILE_TYPE_KMZ: ('kmz', _GEOSPATIAL_VECTOR),
139
+ fusion_platform.FILE_TYPE_CSV: ('csv', None),
140
+ fusion_platform.FILE_TYPE_ESRI_SHAPEFILE: ('zip', _GEOSPATIAL_VECTOR),
141
+ fusion_platform.FILE_TYPE_JPEG: ('jpg', None),
142
+ fusion_platform.FILE_TYPE_PNG: ('png', None),
143
+ fusion_platform.FILE_TYPE_PDF: ('pdf', None),
144
+ fusion_platform.FILE_TYPE_GZIP: ('gz', None),
145
+ fusion_platform.FILE_TYPE_ZIP: ('zip', None),
146
+ fusion_platform.FILE_TYPE_OTHER: ('*', None),
147
+ }
148
+ # @formatter:on
149
+
150
+ # STAC file name constants. We do not use ".json" because this is used by services and specific file types. Similarly, the name starts with "." to hide it.
151
+ _STAC_COLLECTION_FILE_NAME = '.collection.stac'
152
+ _STAC_ITEM_FILE_NAME_FORMAT = '.{file_name}.stac'
153
+
123
154
  def __init__(self, session):
124
155
  """
125
156
  Initialises the object.
@@ -249,3 +280,148 @@ class DataFile(Model):
249
280
  raise ModelError(i18n.t('models.data_file.failed_download_url'))
250
281
 
251
282
  return url
283
+
284
+ def get_stac_item(self, item_file_name_format=_STAC_ITEM_FILE_NAME_FORMAT, collection_file_name=_STAC_COLLECTION_FILE_NAME):
285
+ """
286
+ Converts the data file representation into a STAC item.
287
+
288
+ Args:
289
+ item_file_name_format: The optional item file name format string, where the "file_name" placeholder is replaced with the item's file name.
290
+ collection_file_name: The optional owning collection file name.
291
+
292
+ Returns:
293
+ A tuple of the STAC item definition as a dictionary and the item file name used in the definition.
294
+ """
295
+
296
+ # Obtain all the relevant information.
297
+ bounds = self.bounds if hasattr(self, Model._FIELD_BOUNDS) else None
298
+ crs = self.crs if hasattr(self, Model._FIELD_CRS) else None
299
+ file_name = self.file_name if hasattr(self, Model._FIELD_FILE_NAME) else None
300
+ size = self.size if hasattr(self, Model._FIELD_SIZE) else None
301
+
302
+ # Attempt to find the file type to get its extension and geospatial type. Default to "Other" if not found.
303
+ found_file_type = DataFile._FILE_TYPES.get(self.file_type)
304
+
305
+ if found_file_type is None:
306
+ found_file_type = DataFile._FILE_TYPES.get(fusion_platform.FILE_TYPE_OTHER)
307
+
308
+ geospatial = found_file_type[1]
309
+
310
+ # Get the file extension.
311
+ extension = os.path.splitext(file_name)[1] if file_name is not None else None
312
+ extension = found_file_type[0] if extension is None else extension
313
+
314
+ if (extension is None) or (extension == '*'):
315
+ extension = ''
316
+ else:
317
+ extension = f".{extension}" if not extension.startswith('.') else extension
318
+
319
+ # Work out a suitable name for the file if one is not available.
320
+ if file_name is None:
321
+ file_name = f"{self.id}{extension}"
322
+
323
+ item_file_name = item_file_name_format.format(file_name=file_name)
324
+
325
+ # Start to build the STAC extensions list.
326
+ stac_extensions = ['https://stac-extensions.github.io/processing/v1.2.0/schema.json'] # For properties#processing.[software,version].
327
+
328
+ # Get the bounding box and geometry, if there is one.
329
+ bbox = []
330
+ geometry = {}
331
+
332
+ if (bounds is not None) and (len(bounds) >= 4):
333
+ bbox = bounds
334
+ geometry = {'type': 'Polygon', 'coordinates': [[[bbox[0], bbox[1]], [bbox[2], bbox[1]], [bbox[2], bbox[3]], [bbox[0], bbox[3]], [bbox[0], bbox[1]]]]}
335
+
336
+ # Get the datetime from the filename, if possible. We try various iterations of sections the file name until we find something.
337
+ for file_part in file_name.split('_'):
338
+ extracted_datetime = datetime_parse(file_part) or datetime_parse(f"{file_part}T00:00:00Z") # Assume UTC.
339
+
340
+ if extracted_datetime is not None:
341
+ break
342
+
343
+ if extracted_datetime is None:
344
+ extracted_datetime = self.created_at
345
+
346
+ # Start to build the properties.
347
+ properties = {'processing.software': i18n.t('fusion_platform.sdk'), 'processing.version': fusion_platform.__version__, 'datetime': extracted_datetime}
348
+
349
+ # Add in the CRS, if there is one.
350
+ if crs is not None:
351
+ properties['proj:epsg'] = crs
352
+ stac_extensions.append('https://stac-extensions.github.io/projection/v1.1.0/schema.json') # For properties#proj:epsg.
353
+
354
+ # Build the links.
355
+ links = [{'rel': 'self', 'href': item_file_name, 'type': 'application/json'}]
356
+
357
+ # Add in the optional owning collection.
358
+ if collection_file_name is not None:
359
+ links.append({'rel': 'root', 'href': collection_file_name, 'type': 'application/json'})
360
+ links.append({'rel': 'parent', 'href': collection_file_name, 'type': 'application/json'})
361
+
362
+ # Start to build the asset from the associated file.
363
+ asset = {'href': file_name, 'roles': []}
364
+
365
+ # Extract out the relevant values depending upon the file type.
366
+ raster_bands = None
367
+ eo_bands = None
368
+
369
+ if (geospatial == DataFile._GEOSPATIAL_RASTER) and hasattr(self, Model._FIELD_SELECTORS) and (self.selectors is not None):
370
+ for selector in self.selectors:
371
+ raster_band = {}
372
+ eo_band = {}
373
+
374
+ if hasattr(self, Model._FIELD_RESOLUTION):
375
+ raster_band['spatial_resolution'] = self.resolution
376
+
377
+ if selector.get(Model._FIELD_UNIT) is not None:
378
+ raster_band['unit'] = selector.get(Model._FIELD_UNIT)
379
+
380
+ if (selector.get(Model._FIELD_MINIMUM) is not None) and (selector.get(Model._FIELD_MAXIMUM) is not None) and (
381
+ selector.get(Model._FIELD_MEAN) is not None) and (selector.get(Model._FIELD_SD) is not None):
382
+ raster_band['statistics'] = {
383
+ 'minimum': selector.get(Model._FIELD_MINIMUM),
384
+ 'maximum': selector.get(Model._FIELD_MAXIMUM),
385
+ 'mean': selector.get(Model._FIELD_MEAN),
386
+ 'stddev': selector.get(Model._FIELD_SD)
387
+ }
388
+
389
+ if (selector.get(Model._FIELD_HISTOGRAM_MINIMUM) is not None) and (selector.get(Model._FIELD_HISTOGRAM_MAXIMUM) is not None) and (
390
+ selector.get(Model._FIELD_HISTOGRAM) is not None):
391
+ raster_band['histogram'] = {
392
+ 'count': len(selector.get(Model._FIELD_HISTOGRAM)),
393
+ 'min': selector.get(Model._FIELD_HISTOGRAM_MINIMUM),
394
+ 'max': selector.get(Model._FIELD_HISTOGRAM_MAXIMUM),
395
+ 'buckets': selector.get(Model._FIELD_HISTOGRAM),
396
+ }
397
+
398
+ if selector.get(Model._FIELD_CATEGORY) is not None:
399
+ eo_band['name'] = selector.get(Model._FIELD_CATEGORY)
400
+
401
+ raster_bands = [raster_band] if raster_bands is None else [raster_band] + raster_bands
402
+ eo_bands = [eo_band] if eo_bands is None else [eo_band] + eo_bands
403
+
404
+ stac_extensions.append('https://stac-extensions.github.io/eo/v1.1.0/schema.json') # For assets#eo:bands.
405
+ stac_extensions.append('https://stac-extensions.github.io/raster/v1.1.0/schema.json') # For assets#raster:bands.
406
+
407
+ if raster_bands is not None:
408
+ asset['raster:bands'] = raster_bands
409
+ asset['eo:bands'] = eo_bands
410
+
411
+ # Add in the file size, if there is one.
412
+ if size is not None:
413
+ asset['file:size'] = size
414
+ stac_extensions.append('https://stac-extensions.github.io/file/v2.1.0/schema.json') # For assets#file:size.
415
+
416
+ # Build the STAC item.
417
+ return {
418
+ 'type': 'Feature',
419
+ 'stac_version': '1.0.0',
420
+ 'stac_extensions': stac_extensions,
421
+ 'id': self.id,
422
+ 'bbox': bbox,
423
+ 'geometry': geometry,
424
+ 'properties': properties,
425
+ 'links': links,
426
+ 'assets': {'asset': asset}
427
+ }, item_file_name
@@ -41,8 +41,11 @@ class Model(Base):
41
41
 
42
42
  # Useful fields and templates.
43
43
  _FIELD_AVAILABLE_DISPATCHERS = 'available_dispatchers'
44
+ _FIELD_BOUNDS = 'bounds'
45
+ _FIELD_CATEGORY = 'category'
44
46
  _FIELD_CONSTRAINED_NAMES = 'constrained_names'
45
47
  _FIELD_CONSTRAINED_VALUES = 'constrained_values'
48
+ _FIELD_CRS = 'crs'
46
49
  _FIELD_DATA_TYPE = 'data_type'
47
50
  _FIELD_DISPATCH_INTERMEDIATE = 'dispatch_intermediate'
48
51
  _FIELD_DISPATCHERS = 'dispatchers'
@@ -50,23 +53,37 @@ class Model(Base):
50
53
  _FIELD_DOCUMENTATION_DESCRIPTION = 'documentation_description'
51
54
  _FIELD_ERROR = 'error'
52
55
  _FIELD_EXECUTIONS = 'executions'
56
+ _FIELD_EXTENSIONS = 'extensions'
57
+ _FIELD_FILE_NAME = 'file_name'
53
58
  _FIELD_FILE_TYPE = 'file_type'
59
+ _FIELD_GEOSPATIAL = 'geospatial'
54
60
  _FIELD_GROUP_COUNT = 'group_count'
55
61
  _FIELD_GROUP_ID = 'group_id'
56
62
  _FIELD_GROUP_INDEX = 'group_index'
57
63
  _FIELD_HAS_EXECUTIONS = 'has_executions'
64
+ _FIELD_HISTOGRAM = 'histogram'
65
+ _FIELD_HISTOGRAM_MINIMUM = 'histogram_minimum'
66
+ _FIELD_HISTOGRAM_MAXIMUM = 'histogram_maximum'
58
67
  _FIELD_ID = 'id'
59
68
  _FIELD_INPUT = 'input'
60
69
  _FIELD_INPUTS = 'inputs'
61
70
  _FIELD_KEYWORDS = 'keywords'
71
+ _FIELD_MAXIMUM = 'maximum'
72
+ _FIELD_MEAN = 'mean'
73
+ _FIELD_MINIMUM = 'minimum'
62
74
  _FIELD_NAME = 'name'
63
75
  _FIELD_OPTIONS = 'options'
64
76
  _FIELD_OUTPUTS = 'outputs'
65
77
  _FIELD_PROCESS_STATUS = 'process_status'
78
+ _FIELD_RESOLUTION = 'resolution'
79
+ _FIELD_SD = 'sd'
80
+ _FIELD_SELECTOR = 'selector'
81
+ _FIELD_SELECTORS = 'selectors'
66
82
  _FIELD_SSD_ID = 'ssd_id'
67
83
  _FIELD_STARTED_AT = 'started_at'
68
84
  _FIELD_PUBLISHABLE = 'publishable'
69
85
  _FIELD_SIZE = 'size'
86
+ _FIELD_UNIT = 'unit'
70
87
  _FIELD_VALIDATION = 'validation'
71
88
  _FIELD_VALUE = 'value'
72
89
 
@@ -148,11 +165,12 @@ class Model(Base):
148
165
 
149
166
  return attributes
150
167
 
151
- def __build_body(self, **kwargs):
168
+ def __build_body(self, create=False, **kwargs):
152
169
  """
153
- Builds a request body suitable for sending to the API. All current model attributes are included, with the keywords arguments overridding their value.
170
+ Builds a request body suitable for sending to the API. All current model attributes are included, with the keywords arguments overriding their value.
154
171
 
155
172
  Args:
173
+ create: Whether the body is being built for a create request. This allows the id to be overridden.
156
174
  kwargs: The model attributes which override those already in the template.
157
175
 
158
176
  Returns:
@@ -167,9 +185,11 @@ class Model(Base):
167
185
  if (self.__model is not None) and (key in self.__model):
168
186
  body[Model._REQUEST_KEY_NAME.format(model=model_name, key=key)] = self.__model.get(key)
169
187
 
170
- # Now override any of the passed in attributes, ignoring anything which is read-only or hidden.
188
+ # Now override any of the passed in attributes, ignoring anything which is read-only or hidden. The exception here is the id, which can be overridden
189
+ # during a create operation.
171
190
  for key in kwargs:
172
- if (Model._METADATA_READ_ONLY not in schema.fields[key].metadata) and (Model._METADATA_HIDE not in schema.fields[key].metadata):
191
+ if (create and (key == Model._FIELD_ID)) or (
192
+ (Model._METADATA_READ_ONLY not in schema.fields[key].metadata) and (Model._METADATA_HIDE not in schema.fields[key].metadata)):
173
193
  body[Model._REQUEST_KEY_NAME.format(model=model_name, key=key)] = kwargs[key]
174
194
 
175
195
  return body
@@ -211,7 +231,7 @@ class Model(Base):
211
231
  raise ModelError(i18n.t('models.model.already_persisted'))
212
232
 
213
233
  # Form the post body dictionary from the current object and the keyword arguments.
214
- body = self.__build_body(**kwargs)
234
+ body = self.__build_body(create=True, **kwargs)
215
235
 
216
236
  # Make sure there is a create which can be performed. This requires a non-empty body.
217
237
  if len(body) <= 0:
@@ -113,7 +113,7 @@ class Organisation(Model):
113
113
  _PATH_SERVICES = f"{_PATH_BASE}/services/latest"
114
114
  _PATH_DISPATCHERS = f"{_PATH_BASE}/services/dispatchers"
115
115
 
116
- def create_data(self, name, file_type, files, wait=False):
116
+ def create_data(self, name, file_type, files, wait=False, **kwargs):
117
117
  """
118
118
  Creates a data object for the organisation and uploads the corresponding files. Optionally waits for the upload and analysis to complete.
119
119
 
@@ -122,6 +122,7 @@ class Organisation(Model):
122
122
  file_type: The type of files that the data object will hold.
123
123
  files: The list of file paths to be uploaded.
124
124
  wait: Optionally wait for the upload and analysis to complete? Default False.
125
+ kwargs: The model attributes which override those already in the template.
125
126
 
126
127
  Returns:
127
128
  The created data object.
@@ -134,8 +135,8 @@ class Organisation(Model):
134
135
  data = Data(self._session)
135
136
  data._new(organisation_id=self.id)
136
137
 
137
- # Now attempt to create the data item.
138
- data._create(name, file_type, files, wait)
138
+ # Now attempt to create the data item with any overriding keyword arguments.
139
+ data._create(name, file_type, files, wait=wait, **kwargs)
139
140
 
140
141
  return data
141
142
 
@@ -29,3 +29,5 @@
29
29
  * **test_run**: Is this a test run of a service?
30
30
  * **price**: The price in FPUs to execute this process given its inputs and options.
31
31
  * **deletable**: Is this process scheduled for deletion?
32
+ * **non_aggregator_count**: The count of non-aggregator executions within the current or last execution.
33
+ * **aggregator_count**: The count of aggregator executions within the current or last execution.
@@ -473,8 +473,10 @@ class ProcessSchema(Schema):
473
473
  has_executions = fields.String(required=True, metadata={'hide': True}) # Changed to hide as an attribute.
474
474
  has_executions_organisation_id = fields.String(required=True, metadata={'hide': True}) # Changed to hide as an attribute.
475
475
  executions = fields.List(fields.Nested(ProcessExecutionStatusSchema()), allow_none=True, metadata={'hide': True}) # Changed to hide as an attribute.
476
-
476
+ non_aggregator_count = fields.Integer(allow_none=True, metadata={'read_only': True}) # Changed to prevent this being updated.
477
+ aggregator_count = fields.Integer(allow_none=True, metadata={'read_only': True}) # Changed to prevent this being updated.
477
478
  # Removed creator.
479
+
478
480
  # Removed search.
479
481
 
480
482
  available_dispatchers = fields.List(fields.Nested(ProcessDispatcherSchema()), allow_none=True, metadata={'hide': True}) # Added pseudo-field.
@@ -528,14 +530,16 @@ class Process(Model):
528
530
  fusion_platform.FILE_TYPE_GEOTIFF: [fusion_platform.FILE_TYPE_GEOTIFF, fusion_platform.FILE_TYPE_DEM, fusion_platform.FILE_TYPE_JPEG2000],
529
531
  fusion_platform.FILE_TYPE_JPEG2000: [fusion_platform.FILE_TYPE_JPEG2000],
530
532
  fusion_platform.FILE_TYPE_DEM: [fusion_platform.FILE_TYPE_DEM],
531
- fusion_platform.FILE_TYPE_GEOJSON: [fusion_platform.FILE_TYPE_GEOJSON, fusion_platform.FILE_TYPE_KML, fusion_platform.FILE_TYPE_KMZ,
532
- fusion_platform.FILE_TYPE_ESRI_SHAPEFILE],
533
+ fusion_platform.FILE_TYPE_GEOJSON: [fusion_platform.FILE_TYPE_GEOJSON, fusion_platform.FILE_TYPE_KML, fusion_platform.FILE_TYPE_KMZ, fusion_platform.FILE_TYPE_ESRI_SHAPEFILE],
533
534
  fusion_platform.FILE_TYPE_KML: [fusion_platform.FILE_TYPE_KML],
534
535
  fusion_platform.FILE_TYPE_KMZ: [fusion_platform.FILE_TYPE_KMZ],
535
536
  fusion_platform.FILE_TYPE_CSV: [fusion_platform.FILE_TYPE_CSV],
536
537
  fusion_platform.FILE_TYPE_ESRI_SHAPEFILE: [fusion_platform.FILE_TYPE_ESRI_SHAPEFILE],
537
538
  fusion_platform.FILE_TYPE_JPEG: [fusion_platform.FILE_TYPE_JPEG],
538
539
  fusion_platform.FILE_TYPE_PNG: [fusion_platform.FILE_TYPE_PNG],
540
+ fusion_platform.FILE_TYPE_PDF: [fusion_platform.FILE_TYPE_PDF],
541
+ fusion_platform.FILE_TYPE_GZIP: [fusion_platform.FILE_TYPE_GZIP],
542
+ fusion_platform.FILE_TYPE_ZIP: [fusion_platform.FILE_TYPE_ZIP],
539
543
  fusion_platform.FILE_TYPE_OTHER: [fusion_platform.FILE_TYPE_OTHER],
540
544
  }
541
545
  # @formatter:on
@@ -655,16 +659,19 @@ class Process(Model):
655
659
  # Return the copy.
656
660
  return process
657
661
 
658
- def create(self):
662
+ def create(self, **kwargs):
659
663
  """
660
664
  Attempts to persist the template process in the Fusion Platform<sup>&reg;</sup> so that it can be executed.
661
665
 
666
+ Args:
667
+ kwargs: The model attributes which override those already in the template.
668
+
662
669
  Raises:
663
670
  RequestError: if the create fails.
664
671
  ModelError: if the model could not be created and validated by the Fusion Platform<sup>&reg;</sup>.
665
672
  """
666
673
  # Attempt to issue the create.
667
- self._create()
674
+ self._create(**kwargs)
668
675
 
669
676
  def __dispatchers(self, dispatchers):
670
677
  """
@@ -203,8 +203,8 @@ class ServiceSchema(Schema):
203
203
  version = fields.Integer(required=True, metadata={'read_only': True}) # Changed to prevent this being updated.
204
204
  approval_status = fields.String(required=True, metadata={'read_only': True}) # Changed to prevent this being updated.
205
205
  # Removed latest.
206
- # Removed dispatcher.
207
206
  featured = fields.String(allow_none=True)
207
+ # Removed dispatcher.
208
208
  show_in_latest = fields.Boolean(allow_none=True) # Changed to optional.
209
209
  name = fields.String(required=True)
210
210
 
@@ -234,6 +234,7 @@ class ServiceSchema(Schema):
234
234
 
235
235
  # Removed creator.
236
236
  # Removed approver.
237
+
237
238
  # Removed search.
238
239
 
239
240
  documentation_summary = fields.String(required=True)
@@ -15,10 +15,12 @@ i18n.add_translation('session.request_failed', 'API request failed: %{message}',
15
15
  i18n.add_translation('session.login_failed', 'Login failed', 'en')
16
16
  i18n.add_translation('session.missing_password', 'Password must be specified', 'en')
17
17
  i18n.add_translation('session.missing_email_user_id', 'Either an email address or a user id must be specified', 'en')
18
+ i18n.add_translation('fusion_platform.url', 'https://www.d-cat.co.uk', 'en')
18
19
  i18n.add_translation('fusion_platform.support', 'Support: support@d-cat.co.uk', 'en')
19
20
  i18n.add_translation('fusion_platform.version_date', 'Date: %{version_date}', 'en')
20
21
  i18n.add_translation('fusion_platform.version', 'Version: %{version}', 'en')
21
22
  i18n.add_translation('fusion_platform.sdk', 'Fusion Platform(r) SDK', 'en')
23
+ i18n.add_translation('fusion_platform.organisation', 'Digital Content Analysis Technology Ltd', 'en')
22
24
  i18n.add_translation('models.data_file.failed_download_url', 'Failed to get URL from download file response', 'en')
23
25
  i18n.add_translation('models.data_file.no_download', 'No download is in progress', 'en')
24
26
  i18n.add_translation('models.data_file.download_already_in_progress', 'Cannot download file as the download is already in progress', 'en')
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: fusion-platform-python-sdk
3
- Version: 1.12.2
3
+ Version: 1.13.5
4
4
  Summary: Python SDK used to interact with the Fusion Platform(r)
5
5
  Home-page: https://github.com/d-cat-support/fusion-platform-python-sdk
6
6
  Author: Digital Content Analysis Technology Ltd
@@ -19,7 +19,7 @@ PACKAGE_DIR = pathlib.Path(__file__).parent
19
19
  # @formatter:off
20
20
  setup(
21
21
  name='fusion-platform-python-sdk',
22
- version='1.12.2',
22
+ version='1.13.5',
23
23
  description='Python SDK used to interact with the Fusion Platform(r)',
24
24
  long_description=(PACKAGE_DIR / 'README.md').read_text(),
25
25
  long_description_content_type='text/markdown',