eodash_catalog 0.1.12__tar.gz → 0.1.14__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.

Potentially problematic release.


This version of eodash_catalog might be problematic. Click here for more details.

Files changed (39) hide show
  1. {eodash_catalog-0.1.12 → eodash_catalog-0.1.14}/.bumpversion.cfg +1 -1
  2. {eodash_catalog-0.1.12 → eodash_catalog-0.1.14}/.github/workflows/ci.yml +11 -0
  3. {eodash_catalog-0.1.12 → eodash_catalog-0.1.14}/.github/workflows/python-publish.yml +2 -3
  4. {eodash_catalog-0.1.12 → eodash_catalog-0.1.14}/Dockerfile +1 -1
  5. {eodash_catalog-0.1.12 → eodash_catalog-0.1.14}/PKG-INFO +1 -1
  6. {eodash_catalog-0.1.12 → eodash_catalog-0.1.14}/src/eodash_catalog/__about__.py +1 -1
  7. {eodash_catalog-0.1.12 → eodash_catalog-0.1.14}/src/eodash_catalog/endpoints.py +113 -39
  8. {eodash_catalog-0.1.12 → eodash_catalog-0.1.14}/src/eodash_catalog/generate_indicators.py +25 -13
  9. {eodash_catalog-0.1.12 → eodash_catalog-0.1.14}/src/eodash_catalog/stac_handling.py +123 -86
  10. {eodash_catalog-0.1.12 → eodash_catalog-0.1.14}/src/eodash_catalog/thumbnails.py +1 -1
  11. {eodash_catalog-0.1.12 → eodash_catalog-0.1.14}/src/eodash_catalog/utils.py +29 -6
  12. {eodash_catalog-0.1.12 → eodash_catalog-0.1.14}/tests/test_generate.py +1 -1
  13. {eodash_catalog-0.1.12 → eodash_catalog-0.1.14}/.dockerignore +0 -0
  14. {eodash_catalog-0.1.12 → eodash_catalog-0.1.14}/.gitignore +0 -0
  15. {eodash_catalog-0.1.12 → eodash_catalog-0.1.14}/.vscode/extensions.json +0 -0
  16. {eodash_catalog-0.1.12 → eodash_catalog-0.1.14}/.vscode/settings.json +0 -0
  17. {eodash_catalog-0.1.12 → eodash_catalog-0.1.14}/LICENSE.txt +0 -0
  18. {eodash_catalog-0.1.12 → eodash_catalog-0.1.14}/README.md +0 -0
  19. {eodash_catalog-0.1.12 → eodash_catalog-0.1.14}/pyproject.toml +0 -0
  20. {eodash_catalog-0.1.12 → eodash_catalog-0.1.14}/requirements.txt +0 -0
  21. {eodash_catalog-0.1.12 → eodash_catalog-0.1.14}/ruff.toml +0 -0
  22. {eodash_catalog-0.1.12 → eodash_catalog-0.1.14}/src/eodash_catalog/__init__.py +0 -0
  23. {eodash_catalog-0.1.12 → eodash_catalog-0.1.14}/src/eodash_catalog/duration.py +0 -0
  24. {eodash_catalog-0.1.12 → eodash_catalog-0.1.14}/src/eodash_catalog/sh_endpoint.py +0 -0
  25. {eodash_catalog-0.1.12 → eodash_catalog-0.1.14}/tests/__init__.py +0 -0
  26. {eodash_catalog-0.1.12 → eodash_catalog-0.1.14}/tests/test-data/regional_forecast.json +0 -0
  27. {eodash_catalog-0.1.12 → eodash_catalog-0.1.14}/tests/test_geoparquet.py +0 -0
  28. {eodash_catalog-0.1.12 → eodash_catalog-0.1.14}/tests/testing-catalogs/testing-json.json +0 -0
  29. {eodash_catalog-0.1.12 → eodash_catalog-0.1.14}/tests/testing-catalogs/testing.yaml +0 -0
  30. {eodash_catalog-0.1.12 → eodash_catalog-0.1.14}/tests/testing-collections/test_CROPOMAT1.yaml +0 -0
  31. {eodash_catalog-0.1.12 → eodash_catalog-0.1.14}/tests/testing-collections/test_locations_processing.json +0 -0
  32. {eodash_catalog-0.1.12 → eodash_catalog-0.1.14}/tests/testing-collections/test_see_solar_energy.yaml +0 -0
  33. {eodash_catalog-0.1.12 → eodash_catalog-0.1.14}/tests/testing-collections/test_tif_demo_1.yaml +0 -0
  34. {eodash_catalog-0.1.12 → eodash_catalog-0.1.14}/tests/testing-collections/test_tif_demo_1_json.json +0 -0
  35. {eodash_catalog-0.1.12 → eodash_catalog-0.1.14}/tests/testing-collections/test_tif_demo_2.yaml +0 -0
  36. {eodash_catalog-0.1.12 → eodash_catalog-0.1.14}/tests/testing-collections/test_wms_no_time.yaml +0 -0
  37. {eodash_catalog-0.1.12 → eodash_catalog-0.1.14}/tests/testing-indicators/test_indicator.yaml +0 -0
  38. {eodash_catalog-0.1.12 → eodash_catalog-0.1.14}/tests/testing-layers/baselayers.yaml +0 -0
  39. {eodash_catalog-0.1.12 → eodash_catalog-0.1.14}/tests/testing-layers/overlays.yaml +0 -0
@@ -1,5 +1,5 @@
1
1
  [bumpversion]
2
- current_version = 0.1.12
2
+ current_version = 0.1.14
3
3
  commit = True
4
4
  tag = True
5
5
  parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(\-(?P<release>[a-z]+)\.(?P<build>\d+))?
@@ -10,6 +10,9 @@ jobs:
10
10
  matrix:
11
11
  python-version: ["3.10", "3.11", "3.12"]
12
12
 
13
+ permissions:
14
+ contents: read
15
+ packages: write
13
16
  steps:
14
17
  - uses: actions/checkout@v4
15
18
  - name: Set up Python ${{ matrix.python-version }}
@@ -25,3 +28,11 @@ jobs:
25
28
  run: python -m pip install .
26
29
  - name: Test
27
30
  run: cd tests && python -m pytest -p no:cacheprovider
31
+ - if: github.ref == 'refs/heads/main'
32
+ name: Build and push latest docker image
33
+ run: |
34
+ IMAGE_ID=ghcr.io/${{ github.repository }}
35
+ VERSION=${{ github.ref_name }}
36
+ echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u $ --password-stdin
37
+ docker build -t $IMAGE_ID:latest .
38
+ docker push $IMAGE_ID:latest
@@ -1,7 +1,7 @@
1
1
  # This workflows will upload a Python Package using Twine when a release is created
2
2
  # For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries
3
3
 
4
- name: Upload Python Package and Docker image
4
+ name: Upload Python Package and Docker image on tag
5
5
 
6
6
  on:
7
7
  push:
@@ -37,6 +37,5 @@ jobs:
37
37
  IMAGE_ID=ghcr.io/${{ github.repository }}
38
38
  VERSION=${{ github.ref_name }}
39
39
  echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u $ --password-stdin
40
- docker build -t $IMAGE_ID:latest -t $IMAGE_ID:$VERSION .
41
- docker push $IMAGE_ID:latest
40
+ docker build -t $IMAGE_ID:$VERSION .
42
41
  docker push $IMAGE_ID:$VERSION
@@ -29,4 +29,4 @@ RUN eodash_catalog --help
29
29
 
30
30
  CMD ["eodash_catalog"]
31
31
 
32
- LABEL version="0.1.12"
32
+ LABEL version="0.1.14"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: eodash_catalog
3
- Version: 0.1.12
3
+ Version: 0.1.14
4
4
  Summary: This package is intended to help create a compatible STAC catalog for the eodash dashboard client. It supports configuration of multiple endpoint types for information extraction.
5
5
  Project-URL: Documentation, https://github.com/eodash/eodash_catalog#readme
6
6
  Project-URL: Issues, https://github.com/eodash/eodash_catalog/issues
@@ -1,4 +1,4 @@
1
1
  # SPDX-FileCopyrightText: 2024-present Daniel Santillan <daniel.santillan@eox.at>
2
2
  #
3
3
  # SPDX-License-Identifier: MIT
4
- __version__ = "0.1.12"
4
+ __version__ = "0.1.14"
@@ -11,6 +11,8 @@ from operator import itemgetter
11
11
  import requests
12
12
  from pystac import Asset, Catalog, Collection, Item, Link, SpatialExtent, Summaries
13
13
  from pystac_client import Client
14
+ from shapely import wkt
15
+ from shapely.geometry import mapping
14
16
  from structlog import get_logger
15
17
 
16
18
  from eodash_catalog.sh_endpoint import get_SH_token
@@ -26,7 +28,6 @@ from eodash_catalog.thumbnails import generate_thumbnail
26
28
  from eodash_catalog.utils import (
27
29
  Options,
28
30
  create_geojson_from_bbox,
29
- create_geojson_point,
30
31
  filter_time_entries,
31
32
  format_datetime_to_isostring_zulu,
32
33
  generate_veda_cog_link,
@@ -147,7 +148,7 @@ def handle_STAC_based_endpoint(
147
148
  options: Options,
148
149
  headers=None,
149
150
  ) -> Collection:
150
- if "Locations" in collection_config:
151
+ if collection_config.get("Locations"):
151
152
  root_collection = get_or_create_collection(
152
153
  catalog, collection_config["Name"], collection_config, catalog_config, endpoint_config
153
154
  )
@@ -169,7 +170,7 @@ def handle_STAC_based_endpoint(
169
170
  collection.id = location.get("Identifier", uuid.uuid4())
170
171
  collection.title = location.get("Name")
171
172
  # See if description should be overwritten
172
- if "Description" in location:
173
+ if location.get("Description"):
173
174
  collection.description = location["Description"]
174
175
  else:
175
176
  collection.description = location["Name"]
@@ -184,7 +185,7 @@ def handle_STAC_based_endpoint(
184
185
  # eodash v4 compatibility
185
186
  add_visualization_info(collection, collection_config, endpoint_config)
186
187
  add_process_info_child_collection(collection, catalog_config, collection_config)
187
- if "OverwriteBBox" in location:
188
+ if location.get("OverwriteBBox"):
188
189
  collection.extent.spatial = SpatialExtent(
189
190
  [
190
191
  location["OverwriteBBox"],
@@ -197,7 +198,7 @@ def handle_STAC_based_endpoint(
197
198
  root_collection.extent.spatial.bboxes.append(c_child.extent.spatial.bboxes[0])
198
199
  else:
199
200
  bbox = None
200
- if "Bbox" in endpoint_config:
201
+ if endpoint_config.get("Bbox"):
201
202
  bbox = ",".join(map(str, endpoint_config["Bbox"]))
202
203
  root_collection = process_STACAPI_Endpoint(
203
204
  catalog_config=catalog_config,
@@ -263,17 +264,17 @@ def process_STACAPI_Endpoint(
263
264
  added_times[iso_date] = True
264
265
  link = collection.add_item(item)
265
266
  if options.tn:
266
- if "cog_default" in item.assets:
267
+ if item.assets.get("cog_default"):
267
268
  generate_thumbnail(
268
269
  item, collection_config, endpoint_config, item.assets["cog_default"].href
269
270
  )
270
271
  else:
271
272
  generate_thumbnail(item, collection_config, endpoint_config)
272
273
  # Check if we can create visualization link
273
- if "Assets" in endpoint_config:
274
+ if endpoint_config.get("Assets"):
274
275
  add_visualization_info(item, collection_config, endpoint_config, item.id)
275
276
  link.extra_fields["item"] = item.id
276
- elif "cog_default" in item.assets:
277
+ elif item.assets.get("cog_default"):
277
278
  add_visualization_info(
278
279
  item, collection_config, endpoint_config, item.assets["cog_default"].href
279
280
  )
@@ -282,7 +283,7 @@ def process_STACAPI_Endpoint(
282
283
  add_visualization_info(
283
284
  item, collection_config, endpoint_config, datetimes=[item_datetime]
284
285
  )
285
- elif "start_datetime" in item.properties and "end_datetime" in item.properties:
286
+ elif item.properties.get("start_datetime") and item.properties.get("end_datetime"):
286
287
  add_visualization_info(
287
288
  item,
288
289
  collection_config,
@@ -323,7 +324,7 @@ def process_STACAPI_Endpoint(
323
324
  add_collection_information(catalog_config, collection, collection_config)
324
325
 
325
326
  # Check if we need to overwrite the bbox after update from items
326
- if "OverwriteBBox" in endpoint_config:
327
+ if endpoint_config.get("OverwriteBBox"):
327
328
  collection.extent.spatial = SpatialExtent(
328
329
  [
329
330
  endpoint_config["OverwriteBBox"],
@@ -377,7 +378,7 @@ def handle_SH_WMS_endpoint(
377
378
  root_collection = get_or_create_collection(
378
379
  catalog, collection_config["Name"], collection_config, catalog_config, endpoint_config
379
380
  )
380
- if "Locations" in collection_config:
381
+ if collection_config.get("Locations"):
381
382
  for location in collection_config["Locations"]:
382
383
  # create and populate location collections based on times
383
384
  # TODO: Should we add some new description per location?
@@ -483,7 +484,7 @@ def handle_GeoDB_endpoint(
483
484
  collection = get_or_create_collection(
484
485
  catalog, collection_config["Name"], collection_config, catalog_config, endpoint_config
485
486
  )
486
- select = "?select=aoi,aoi_id,country,city,time"
487
+ select = "?select=aoi,aoi_id,country,city,time,input_data,sub_aoi"
487
488
  url = (
488
489
  endpoint_config["EndPoint"]
489
490
  + endpoint_config["Database"]
@@ -501,12 +502,12 @@ def handle_GeoDB_endpoint(
501
502
  for key, value in groupby(sorted_locations, key=itemgetter("aoi_id")):
502
503
  # Finding min and max values for date
503
504
  values = list(value)
504
- times = [datetime.fromisoformat(t["time"]) for t in values]
505
505
  unique_values = next(iter({v["aoi_id"]: v for v in values}.values()))
506
506
  country = unique_values["country"]
507
507
  city = unique_values["city"]
508
508
  IdKey = endpoint_config.get("IdKey", "city")
509
509
  IdValue = unique_values[IdKey]
510
+
510
511
  if country not in countries:
511
512
  countries.append(country)
512
513
  # sanitize unique key identifier to be sure it is saveable as a filename
@@ -520,28 +521,92 @@ def handle_GeoDB_endpoint(
520
521
  IdValue = key
521
522
  if city not in cities:
522
523
  cities.append(city)
523
- min_date = min(times)
524
- max_date = max(times)
525
524
  latlon = unique_values["aoi"]
526
525
  [lat, lon] = [float(x) for x in latlon.split(",")]
527
526
  # create item for unique locations
528
527
  buff = 0.01
529
528
  bbox = [lon - buff, lat - buff, lon + buff, lat + buff]
530
- item = Item(
531
- id=IdValue,
532
- bbox=bbox,
533
- properties={},
534
- geometry=create_geojson_point(lon, lat)["geometry"],
535
- datetime=None,
536
- start_datetime=min_date,
537
- end_datetime=max_date,
529
+
530
+ # create collection per available inputdata information
531
+ sc_config = {
532
+ "Title": city,
533
+ "Description": f"{city} - {country}",
534
+ }
535
+ locations_collection = get_or_create_collection(
536
+ collection, key, sc_config, catalog_config, endpoint_config
538
537
  )
539
- link = collection.add_item(item)
538
+ input_data = endpoint_config.get("InputData")
539
+ if input_data:
540
+ for v in values:
541
+ # add items based on inputData fields for each time step available in values
542
+ first_match = next(
543
+ (item for item in input_data if item.get("Identifier") == v["input_data"]), None
544
+ )
545
+ time_object = datetime.fromisoformat(v["time"])
546
+ # extract wkt geometry from sub_aoi
547
+ if "sub_aoi" in v and v["sub_aoi"] != "/":
548
+ # create geometry from wkt
549
+ geometry = mapping(wkt.loads(v["sub_aoi"]))
550
+ else:
551
+ geometry = create_geojson_from_bbox(bbox)
552
+ item = Item(
553
+ id=v["time"],
554
+ bbox=bbox,
555
+ properties={},
556
+ geometry=geometry,
557
+ datetime=time_object,
558
+ )
559
+ if first_match:
560
+ match first_match["Type"]:
561
+ case "WMS":
562
+ url = first_match["Url"]
563
+ extra_fields = {
564
+ "wms:layers": [first_match["Layers"]],
565
+ "role": ["data"],
566
+ }
567
+ if url.startswith("https://services.sentinel-hub.com/ogc/wms/"):
568
+ instanceId = os.getenv("SH_INSTANCE_ID")
569
+ if "InstanceId" in endpoint_config:
570
+ instanceId = endpoint_config["InstanceId"]
571
+ start_date = format_datetime_to_isostring_zulu(time_object)
572
+ used_delta = timedelta(days=1)
573
+ if "TimeDelta" in first_match:
574
+ used_delta = timedelta(minutes=first_match["TimeDelta"])
575
+ end_date = format_datetime_to_isostring_zulu(
576
+ time_object + used_delta - timedelta(milliseconds=1)
577
+ )
578
+ extra_fields.update(
579
+ {"wms:dimensions": {"TIME": f"{start_date}/{end_date}"}}
580
+ )
581
+ # we add the instance id to the url
582
+ url = f"https://services.sentinel-hub.com/ogc/wms/{instanceId}"
583
+ else:
584
+ extra_fields.update({"wms:dimensions": {"TIME": v["time"]}})
585
+ link = Link(
586
+ rel="wms",
587
+ target=url,
588
+ media_type=(endpoint_config.get("MimeType", "image/png")),
589
+ title=collection_config["Name"],
590
+ extra_fields=extra_fields,
591
+ )
592
+ item.add_link(link)
593
+ itemlink = locations_collection.add_item(item)
594
+ itemlink.extra_fields["datetime"] = (
595
+ f"{format_datetime_to_isostring_zulu(time_object)}Z"
596
+ )
597
+
598
+ # add_visualization_info(
599
+ # item, collection_config, endpoint_config, file_url=first_match.get("FileUrl")
600
+ # )
601
+ locations_collection.extra_fields["subcode"] = key
602
+ link = collection.add_child(locations_collection)
603
+ locations_collection.update_extent_from_items()
604
+ # collection.update_extent_from_items()
540
605
  # bubble up information we want to the link
541
606
  link.extra_fields["id"] = key
542
607
  link.extra_fields["latlng"] = latlon
543
608
  link.extra_fields["country"] = country
544
- link.extra_fields["city"] = city
609
+ link.extra_fields["name"] = city
545
610
 
546
611
  if "yAxis" not in collection_config:
547
612
  # fetch yAxis and store it to data, preventing need to save it per dataset in yml
@@ -557,7 +622,7 @@ def handle_GeoDB_endpoint(
557
622
  collection_config["yAxis"] = yAxis
558
623
  add_collection_information(catalog_config, collection, collection_config)
559
624
  add_example_info(collection, collection_config, endpoint_config, catalog_config)
560
- collection.extra_fields["geoDBID"] = endpoint_config["CollectionId"]
625
+ collection.extra_fields["locations"] = True
561
626
 
562
627
  collection.update_extent_from_items()
563
628
  collection.summaries = Summaries(
@@ -580,7 +645,7 @@ def handle_SH_endpoint(
580
645
  headers = {"Authorization": f"Bearer {token}"}
581
646
  endpoint_config["EndPoint"] = "https://services.sentinel-hub.com/api/v1/catalog/1.0.0/"
582
647
  # Overwrite collection id with type, such as ZARR or BYOC
583
- if "Type" in endpoint_config:
648
+ if endpoint_config.get("Type"):
584
649
  endpoint_config["CollectionId"] = (
585
650
  endpoint_config["Type"] + "-" + endpoint_config["CollectionId"]
586
651
  )
@@ -637,7 +702,7 @@ def handle_WMS_endpoint(
637
702
  LOGGER.warn(f"NO datetimes returned for collection: {collection_config['Name']}!")
638
703
 
639
704
  # Check if we should overwrite bbox
640
- if "OverwriteBBox" in endpoint_config:
705
+ if endpoint_config.get("OverwriteBBox"):
641
706
  collection.extent.spatial = SpatialExtent(
642
707
  [
643
708
  endpoint_config["OverwriteBBox"],
@@ -655,10 +720,10 @@ def generate_veda_tiles_link(endpoint_config: dict, item: str | None) -> str:
655
720
  for asset in endpoint_config["Assets"]:
656
721
  assets += f"&assets={asset}"
657
722
  color_formula = ""
658
- if "ColorFormula" in endpoint_config:
723
+ if endpoint_config.get("ColorFormula"):
659
724
  color_formula = "&color_formula={}".format(endpoint_config["ColorFormula"])
660
725
  no_data = ""
661
- if "NoData" in endpoint_config:
726
+ if endpoint_config.get("NoData"):
662
727
  no_data = "&no_data={}".format(endpoint_config["NoData"])
663
728
  item = item if item else "{item}"
664
729
  target_url = f"https://openveda.cloud/api/raster/collections/{collection}/items/{item}/tiles/WebMercatorQuad/{{z}}/{{x}}/{{y}}?{assets}{color_formula}{no_data}"
@@ -673,7 +738,7 @@ def add_visualization_info(
673
738
  datetimes: list[datetime] | None = None,
674
739
  ) -> None:
675
740
  extra_fields: dict[str, list[str] | dict[str, str]] = {}
676
- if "Attribution" in endpoint_config:
741
+ if endpoint_config.get("Attribution"):
677
742
  stac_object.stac_extensions.append(
678
743
  "https://stac-extensions.github.io/attribution/v0.1.0/schema.json"
679
744
  )
@@ -681,7 +746,7 @@ def add_visualization_info(
681
746
  # add extension reference
682
747
  if endpoint_config["Name"] == "Sentinel Hub" or endpoint_config["Name"] == "Sentinel Hub WMS":
683
748
  instanceId = os.getenv("SH_INSTANCE_ID")
684
- if "InstanceId" in endpoint_config:
749
+ if endpoint_config.get("InstanceId"):
685
750
  instanceId = endpoint_config["InstanceId"]
686
751
  if env_id := endpoint_config.get("CustomSHEnvId"):
687
752
  # special handling for custom environment
@@ -733,16 +798,21 @@ def add_visualization_info(
733
798
  dimensions = {}
734
799
  if dimensions_config := endpoint_config.get("Dimensions", {}):
735
800
  for key, value in dimensions_config.items():
801
+ # special replace for world_settlement_footprint
802
+ if collection_config["EodashIdentifier"] == "WSF":
803
+ value = value.replace(
804
+ "{time}", datetimes is not None and str(datetimes[0].year) or "{time}"
805
+ )
736
806
  dimensions[key] = value
737
807
  if datetimes is not None:
738
808
  dimensions["TIME"] = format_datetime_to_isostring_zulu(datetimes[0])
739
809
  if dimensions != {}:
740
810
  extra_fields["wms:dimensions"] = dimensions
741
- if "Styles" in endpoint_config:
811
+ if endpoint_config.get("Styles"):
742
812
  extra_fields["wms:styles"] = endpoint_config["Styles"]
743
- if "TileSize" in endpoint_config:
813
+ if endpoint_config.get("TileSize"):
744
814
  extra_fields["wms:tilesize"] = endpoint_config["TileSize"]
745
- if "Version" in endpoint_config:
815
+ if endpoint_config.get("Version"):
746
816
  extra_fields["wms:version"] = endpoint_config["Version"]
747
817
  media_type = endpoint_config.get("MediaType", "image/jpeg")
748
818
  endpoint_url = endpoint_config["EndPoint"]
@@ -775,7 +845,7 @@ def add_visualization_info(
775
845
  dimensions["TIME"] = format_datetime_to_isostring_zulu(datetimes[0])
776
846
  if dimensions != {}:
777
847
  extra_fields["wms:dimensions"] = dimensions
778
- if "Styles" in endpoint_config:
848
+ if endpoint_config.get("Styles"):
779
849
  extra_fields["wms:styles"] = endpoint_config["Styles"]
780
850
  media_type = endpoint_config.get("MediaType", "image/png")
781
851
  endpoint_url = endpoint_config["EndPoint"]
@@ -821,14 +891,18 @@ def add_visualization_info(
821
891
  # either preset Rescale of left as a template
822
892
  vmin = "{vmin}"
823
893
  vmax = "{vmax}"
824
- if "Rescale" in endpoint_config:
894
+ if endpoint_config.get("Rescale"):
825
895
  vmin = endpoint_config["Rescale"][0]
826
896
  vmax = endpoint_config["Rescale"][1]
827
897
  # depending on numerical input only
828
898
  data_projection = str(endpoint_config.get("DataProjection", 3857))
829
899
  epsg_prefix = "" if "EPSG:" in data_projection else "EPSG:"
830
900
  crs = f"{epsg_prefix}{data_projection}"
831
- time = stac_object.get_datetime() if isinstance(stac_object, Item) else "{time}"
901
+ time = (
902
+ stac_object.get_datetime().strftime("%Y-%m-%dT%H:%M:%SZ") # type: ignore
903
+ if isinstance(stac_object, Item)
904
+ else "{time}"
905
+ )
832
906
  target_url = (
833
907
  "{}/tiles/{}/{}/{{z}}/{{y}}/{{x}}" "?crs={}&time={}&vmin={}&vmax={}&cbar={}"
834
908
  ).format(
@@ -998,7 +1072,7 @@ def handle_raw_source(
998
1072
  assets=assets,
999
1073
  extra_fields={},
1000
1074
  )
1001
- if "Attribution" in endpoint_config:
1075
+ if endpoint_config.get("Attribution"):
1002
1076
  item.stac_extensions.append(
1003
1077
  "https://stac-extensions.github.io/attribution/v0.1.0/schema.json"
1004
1078
  )
@@ -40,6 +40,7 @@ from eodash_catalog.utils import (
40
40
  RaisingThread,
41
41
  add_single_item_if_collection_empty,
42
42
  iter_len_at_least,
43
+ merge_bboxes,
43
44
  read_config_file,
44
45
  recursive_save,
45
46
  retry,
@@ -88,7 +89,7 @@ def process_catalog_file(file_path: str, options: Options):
88
89
  )
89
90
  except FileNotFoundError:
90
91
  LOGGER.info(f"Warning: neither collection nor indicator found for {collection}")
91
- if "MapProjection" in catalog_config:
92
+ if catalog_config.get("MapProjection"):
92
93
  catalog.extra_fields["eodash:mapProjection"] = catalog_config["MapProjection"]
93
94
 
94
95
  strategy = TemplateLayoutStrategy(item_template="${collection}/${year}")
@@ -163,10 +164,11 @@ def process_indicator_file(
163
164
  ):
164
165
  LOGGER.info(f"Processing indicator: {file_path}")
165
166
  indicator_config = read_config_file(file_path)
167
+
166
168
  parent_indicator = get_or_create_collection(
167
169
  catalog, indicator_config["Name"], indicator_config, catalog_config, {}
168
170
  )
169
- if "Collections" in indicator_config:
171
+ if indicator_config.get("Collections"):
170
172
  for collection in indicator_config["Collections"]:
171
173
  process_collection_file(
172
174
  catalog_config,
@@ -181,9 +183,19 @@ def process_indicator_file(
181
183
  add_collection_information(catalog_config, parent_indicator, indicator_config, True)
182
184
  if iter_len_at_least(parent_indicator.get_items(recursive=True), 1):
183
185
  parent_indicator.update_extent_from_items()
186
+ # get shared extent of all of the collections
187
+ # they might have OverwriteBBox and that would discard it for indicator
188
+ merged_bbox = merge_bboxes(
189
+ [
190
+ c_child.extent.spatial.bboxes[0]
191
+ for c_child in parent_indicator.get_children()
192
+ if isinstance(c_child, Collection)
193
+ ]
194
+ )
195
+ parent_indicator.extent.spatial.bboxes = [merged_bbox]
184
196
  # Add bbox extents from children
185
197
  for c_child in parent_indicator.get_children():
186
- if isinstance(c_child, Collection): # typing reason
198
+ if isinstance(c_child, Collection) and merged_bbox != c_child.extent.spatial.bboxes[0]:
187
199
  parent_indicator.extent.spatial.bboxes.append(c_child.extent.spatial.bboxes[0])
188
200
  # extract collection information and add it to summary indicator level
189
201
  extract_indicator_info(parent_indicator)
@@ -203,7 +215,7 @@ def process_collection_file(
203
215
  ):
204
216
  LOGGER.info(f"Processing collection: {file_path}")
205
217
  collection_config = read_config_file(file_path)
206
- if "Resources" in collection_config:
218
+ if collection_config.get("Resources"):
207
219
  for endpoint_config in collection_config["Resources"]:
208
220
  try:
209
221
  collection = None
@@ -275,7 +287,7 @@ def process_collection_file(
275
287
  LOGGER.warn(f"""Exception: {e.args[0]} with config: {endpoint_config}""")
276
288
  raise e
277
289
 
278
- elif "Subcollections" in collection_config:
290
+ elif collection_config.get("Subcollections"):
279
291
  # if no endpoint is specified we check for definition of subcollections
280
292
  parent_collection = get_or_create_collection(
281
293
  catalog, collection_config["Name"], collection_config, catalog_config, {}
@@ -286,7 +298,7 @@ def process_collection_file(
286
298
  for sub_coll_def in collection_config["Subcollections"]:
287
299
  # Subcollection has only data on one location which
288
300
  # is defined for the entire collection
289
- if "Name" in sub_coll_def and "Point" in sub_coll_def:
301
+ if sub_coll_def.get("Name") and sub_coll_def.get("Point"):
290
302
  locations.append(sub_coll_def["Name"])
291
303
  if isinstance(sub_coll_def["Country"], list):
292
304
  countries.extend(sub_coll_def["Country"])
@@ -302,7 +314,7 @@ def process_collection_file(
302
314
  for link in parent_collection.links:
303
315
  if (
304
316
  link.rel == "child"
305
- and "id" in link.extra_fields
317
+ and link.extra_fields.get("id")
306
318
  and link.extra_fields["id"] == sub_coll_def["Identifier"]
307
319
  ):
308
320
  latlng = "{},{}".format(
@@ -330,9 +342,9 @@ def process_collection_file(
330
342
  links = tmp_catalog.get_child(sub_coll_def["Identifier"]).get_links() # type: ignore
331
343
  for link in links:
332
344
  # extract summary information
333
- if "city" in link.extra_fields:
345
+ if link.extra_fields.get("city"):
334
346
  locations.append(link.extra_fields["city"])
335
- if "country" in link.extra_fields:
347
+ if link.extra_fields.get("country"):
336
348
  if isinstance(link.extra_fields["country"], list):
337
349
  countries.extend(link.extra_fields["country"])
338
350
  else:
@@ -340,7 +352,7 @@ def process_collection_file(
340
352
 
341
353
  parent_collection.add_links(links)
342
354
 
343
- add_collection_information(catalog_config, parent_collection, collection_config)
355
+ add_collection_information(catalog_config, parent_collection, collection_config, True)
344
356
  add_process_info(parent_collection, catalog_config, collection_config)
345
357
  parent_collection.update_extent_from_items()
346
358
  # Add bbox extents from children
@@ -368,7 +380,7 @@ def add_to_catalog(
368
380
 
369
381
  link: Link = catalog.add_child(collection)
370
382
  # bubble fields we want to have up to collection link and add them to collection
371
- if endpoint and "Type" in endpoint:
383
+ if endpoint and endpoint.get("Type") and endpoint["Type"] not in ["GeoDB"]:
372
384
  collection.extra_fields["endpointtype"] = "{}_{}".format(
373
385
  endpoint["Name"],
374
386
  endpoint["Type"],
@@ -380,12 +392,12 @@ def add_to_catalog(
380
392
  elif endpoint:
381
393
  collection.extra_fields["endpointtype"] = endpoint["Name"]
382
394
  link.extra_fields["endpointtype"] = endpoint["Name"]
383
- if "Subtitle" in collection_config:
395
+ if collection_config.get("Subtitle"):
384
396
  link.extra_fields["subtitle"] = collection_config["Subtitle"]
385
397
  link.extra_fields["title"] = collection.title
386
398
  link.extra_fields["code"] = collection_config["EodashIdentifier"]
387
399
  link.extra_fields["id"] = collection_config["Name"]
388
- if "Themes" in collection_config:
400
+ if collection_config.get("Themes"):
389
401
  link.extra_fields["themes"] = collection_config["Themes"]
390
402
  # Check for summaries and bubble up info
391
403
  if disable:
@@ -51,9 +51,9 @@ def get_or_create_collection(
51
51
  temporal_extent = TemporalExtent([[times_datetimes[0], times_datetimes[-1]]])
52
52
 
53
53
  extent = Extent(spatial=spatial_extent, temporal=temporal_extent)
54
-
54
+ description = ""
55
55
  # Check if description is link to markdown file
56
- if "Description" in collection_config:
56
+ if collection_config.get("Description"):
57
57
  description = collection_config["Description"]
58
58
  if description.endswith((".md", ".MD")):
59
59
  if description.startswith("http"):
@@ -61,7 +61,7 @@ def get_or_create_collection(
61
61
  response = requests.get(description)
62
62
  if response.status_code == 200:
63
63
  description = response.text
64
- elif "Subtitle" in collection_config:
64
+ elif collection_config.get("Subtitle"):
65
65
  LOGGER.warn("Markdown file could not be fetched")
66
66
  description = collection_config["Subtitle"]
67
67
  else:
@@ -69,10 +69,10 @@ def get_or_create_collection(
69
69
  response = requests.get(f'{catalog_config["assets_endpoint"]}/{description}')
70
70
  if response.status_code == 200:
71
71
  description = response.text
72
- elif "Subtitle" in collection_config:
72
+ elif collection_config.get("Subtitle"):
73
73
  LOGGER.warn("Markdown file could not be fetched")
74
74
  description = collection_config["Subtitle"]
75
- elif "Subtitle" in collection_config:
75
+ elif collection_config.get("Subtitle"):
76
76
  # Try to use at least subtitle to fill some information
77
77
  description = collection_config["Subtitle"]
78
78
 
@@ -90,12 +90,28 @@ def create_service_link(endpoint_config: dict, catalog_config: dict) -> Link:
90
90
  "id": endpoint_config["Identifier"],
91
91
  "method": endpoint_config.get("Method", "GET"),
92
92
  }
93
- if "EndPoint" in endpoint_config:
93
+ if endpoint_config.get("EndPoint"):
94
94
  extra_fields["endpoint"] = endpoint_config["EndPoint"]
95
- if "Body" in endpoint_config:
95
+ if endpoint_config.get("Body"):
96
96
  extra_fields["body"] = get_full_url(endpoint_config["Body"], catalog_config)
97
- if "Flatstyle" in endpoint_config:
98
- extra_fields["eox:flatstyle"] = get_full_url(endpoint_config["Flatstyle"], catalog_config)
97
+ if endpoint_config.get("Flatstyle"):
98
+ # either a string
99
+ if isinstance(endpoint_config["Flatstyle"], str):
100
+ # update URL if needed
101
+ extra_fields["eox:flatstyle"] = get_full_url(
102
+ endpoint_config["Flatstyle"], catalog_config
103
+ )
104
+ elif isinstance(endpoint_config["Flatstyle"], list):
105
+ # or a list of objects - update URL if needed
106
+ extra_fields["eox:flatstyle"] = []
107
+ for flatstyle_config in endpoint_config["Flatstyle"]:
108
+ flatstyle_obj = {
109
+ "Identifier": flatstyle_config.get("Identifier"),
110
+ "Url": get_full_url(flatstyle_config.get("Url"), catalog_config),
111
+ }
112
+ extra_fields["eox:flatstyle"].append(flatstyle_obj)
113
+ else:
114
+ LOGGER.warn("Flatstyle is invalid type", endpoint_config["Flatstyle"])
99
115
  sl = Link(
100
116
  rel="service",
101
117
  target=endpoint_config["Url"],
@@ -121,15 +137,15 @@ def create_web_map_link(layer_config: dict, role: str) -> Link:
121
137
  case "wms":
122
138
  # handle wms special config options
123
139
  extra_fields["wms:layers"] = layer_config["layers"]
124
- if "styles" in layer_config:
140
+ if layer_config.get("styles"):
125
141
  extra_fields["wms:styles"] = layer_config["styles"]
126
- if "dimensions" in layer_config:
142
+ if layer_config.get("dimensions"):
127
143
  extra_fields["wms:dimensions"] = layer_config["dimensions"]
128
144
  case "wmts":
129
145
  extra_fields["wmts:layer"] = layer_config["layer"]
130
- if "dimensions" in layer_config:
146
+ if layer_config.get("dimensions"):
131
147
  extra_fields["wmts:dimensions"] = layer_config["dimensions"]
132
- if "Attribution" in layer_config:
148
+ if layer_config.get("Attribution"):
133
149
  extra_fields["attribution"] = layer_config["Attribution"]
134
150
  wml = Link(
135
151
  rel=layer_config["protocol"],
@@ -148,7 +164,7 @@ def add_example_info(
148
164
  endpoint_config: dict,
149
165
  catalog_config: dict,
150
166
  ) -> None:
151
- if "Services" in collection_config:
167
+ if collection_config.get("Services"):
152
168
  for service in collection_config["Services"]:
153
169
  if service["Name"] == "Statistical API":
154
170
  service_type = service.get("Type", "byoc")
@@ -182,7 +198,7 @@ def add_example_info(
182
198
  Link(
183
199
  rel="example",
184
200
  target=service["Url"],
185
- title=(service["Title"] if "Title" in service else service["Name"]),
201
+ title=service.get("Title", service.get("Name")),
186
202
  media_type="application/x-ipynb+json",
187
203
  extra_fields={
188
204
  "example:language": "Jupyter Notebook",
@@ -190,26 +206,6 @@ def add_example_info(
190
206
  },
191
207
  )
192
208
  )
193
- elif "Resources" in collection_config:
194
- for service in collection_config["Resources"]:
195
- if service.get("Name") == "xcube":
196
- target_url = "{}/timeseries/{}/{}?aggMethods=median".format(
197
- endpoint_config["EndPoint"],
198
- endpoint_config["DatacubeId"],
199
- endpoint_config["Variable"],
200
- )
201
- stac_object.add_link(
202
- Link(
203
- rel="example",
204
- target=target_url,
205
- title=service["Name"] + " analytics",
206
- media_type="application/json",
207
- extra_fields={
208
- "example:language": "JSON",
209
- "example:method": "POST",
210
- },
211
- )
212
- )
213
209
 
214
210
 
215
211
  def add_collection_information(
@@ -220,7 +216,7 @@ def add_collection_information(
220
216
  ) -> None:
221
217
  # Add metadata information
222
218
  # Check license identifier
223
- if "License" in collection_config:
219
+ if collection_config.get("License"):
224
220
  # Check if list was provided
225
221
  if isinstance(collection_config["License"], list):
226
222
  if len(collection_config["License"]) == 1:
@@ -230,7 +226,7 @@ def add_collection_information(
230
226
  target=collection_config["License"][0]["Url"],
231
227
  media_type=(collection_config["License"][0].get("Type", "text/html")),
232
228
  )
233
- if "Title" in collection_config["License"][0]:
229
+ if collection_config["License"][0].get("Title"):
234
230
  link.title = collection_config["License"][0]["Title"]
235
231
  collection.links.append(link)
236
232
  elif len(collection_config["License"]) > 1:
@@ -239,11 +235,9 @@ def add_collection_information(
239
235
  link = Link(
240
236
  rel="license",
241
237
  target=license_entry["Url"],
242
- media_type="text/html"
243
- if "Type" in license_entry
244
- else license_entry["Type"],
238
+ media_type=license_entry.get("Type", "text/html"),
245
239
  )
246
- if "Title" in license_entry:
240
+ if license_entry.get("Title"):
247
241
  link.title = license_entry["Title"]
248
242
  collection.links.append(link)
249
243
  else:
@@ -267,7 +261,7 @@ def add_collection_information(
267
261
  else:
268
262
  pass
269
263
 
270
- if "Provider" in collection_config:
264
+ if collection_config.get("Provider"):
271
265
  try:
272
266
  collection.providers = [
273
267
  Provider(
@@ -279,21 +273,21 @@ def add_collection_information(
279
273
  except Exception:
280
274
  LOGGER.warn(f"Issue creating provider information for collection: {collection.id}")
281
275
 
282
- if "Citation" in collection_config:
283
- if "DOI" in collection_config["Citation"]:
276
+ if collection_config.get("Citation"):
277
+ if collection_config["Citation"].get("DOI"):
284
278
  collection.extra_fields["sci:doi"] = collection_config["Citation"]["DOI"]
285
- if "Citation" in collection_config["Citation"]:
279
+ if collection_config["Citation"].get("Citation"):
286
280
  collection.extra_fields["sci:citation"] = collection_config["Citation"]["Citation"]
287
- if "Publication" in collection_config["Citation"]:
281
+ if collection_config["Citation"].get("Publication"):
288
282
  collection.extra_fields["sci:publications"] = [
289
283
  # convert keys to lower case
290
284
  {k.lower(): v for k, v in publication.items()}
291
285
  for publication in collection_config["Citation"]["Publication"]
292
286
  ]
293
287
 
294
- if "Subtitle" in collection_config:
288
+ if collection_config.get("Subtitle"):
295
289
  collection.extra_fields["subtitle"] = collection_config["Subtitle"]
296
- if "Legend" in collection_config:
290
+ if collection_config.get("Legend"):
297
291
  collection.add_asset(
298
292
  "legend",
299
293
  Asset(
@@ -302,7 +296,7 @@ def add_collection_information(
302
296
  roles=["metadata"],
303
297
  ),
304
298
  )
305
- if "Story" in collection_config:
299
+ if collection_config.get("Story"):
306
300
  collection.add_asset(
307
301
  "story",
308
302
  Asset(
@@ -311,7 +305,7 @@ def add_collection_information(
311
305
  roles=["metadata"],
312
306
  ),
313
307
  )
314
- if "Image" in collection_config:
308
+ if collection_config.get("Image"):
315
309
  collection.add_asset(
316
310
  "thumbnail",
317
311
  Asset(
@@ -327,10 +321,10 @@ def add_collection_information(
327
321
  # Add extra fields to collection if available
328
322
  add_extra_fields(collection, collection_config, is_root_collection)
329
323
 
330
- if "References" in collection_config:
324
+ if collection_config.get("References"):
331
325
  generic_counter = 1
332
326
  for ref in collection_config["References"]:
333
- if "Key" in ref:
327
+ if ref.get("Key"):
334
328
  key = ref["Key"]
335
329
  else:
336
330
  key = f"reference_{generic_counter}"
@@ -344,14 +338,14 @@ def add_collection_information(
344
338
  roles=["metadata"],
345
339
  ),
346
340
  )
347
- if "Colorlegend" in collection_config:
341
+ if collection_config.get("Colorlegend"):
348
342
  collection.extra_fields["eox:colorlegend"] = collection_config["Colorlegend"]
349
343
 
350
344
 
351
345
  def add_process_info(collection: Collection, catalog_config: dict, collection_config: dict) -> None:
352
- if any(key in collection_config for key in ["Locations", "Subcollections"]):
346
+ if any(collection_config.get(key) for key in ["Locations", "Subcollections"]):
353
347
  # add the generic geodb-like selection process on the root collection instead of Processes
354
- if "geodb_default_form" in catalog_config:
348
+ if catalog_config.get("geodb_default_form"):
355
349
  # adding default geodb-like map handling for Locations
356
350
  collection.extra_fields["eodash:jsonform"] = get_full_url(
357
351
  catalog_config["geodb_default_form"], catalog_config
@@ -369,29 +363,44 @@ def add_process_info(collection: Collection, catalog_config: dict, collection_co
369
363
  },
370
364
  )
371
365
  collection.add_link(sl)
366
+ # adding additional service links
367
+ if collection_config["Name"] == "GeoDB" and collection_config.get("Process", {}).get(
368
+ "EndPoints"
369
+ ):
370
+ for endpoint in collection_config["Process"]["EndPoints"]:
371
+ collection.add_link(create_service_link(endpoint, catalog_config))
372
+
373
+ # for geodb collections now based on locations, we want to make sure
374
+ # also manually defined processes are added to the collection
375
+ if collection_config["Name"] == "GeoDB" and collection_config.get("Process", {}).get(
376
+ "VegaDefinition"
377
+ ):
378
+ collection.extra_fields["eodash:vegadefinition"] = get_full_url(
379
+ collection_config["Process"]["VegaDefinition"], catalog_config
380
+ )
372
381
  # elif is intentional for cases when Process is defined on collection with Locations
373
382
  # then we want to only add it to the "children", not the root
374
- elif "Process" in collection_config:
375
- if "EndPoints" in collection_config["Process"]:
383
+ elif collection_config.get("Process"):
384
+ if collection_config["Process"].get("EndPoints"):
376
385
  for endpoint in collection_config["Process"]["EndPoints"]:
377
386
  collection.add_link(create_service_link(endpoint, catalog_config))
378
- if "JsonForm" in collection_config["Process"]:
387
+ if collection_config["Process"].get("JsonForm"):
379
388
  collection.extra_fields["eodash:jsonform"] = get_full_url(
380
389
  collection_config["Process"]["JsonForm"], catalog_config
381
390
  )
382
- if "VegaDefinition" in collection_config["Process"]:
391
+ if collection_config["Process"].get("VegaDefinition"):
383
392
  collection.extra_fields["eodash:vegadefinition"] = get_full_url(
384
393
  collection_config["Process"]["VegaDefinition"], catalog_config
385
394
  )
386
- elif "Resources" in collection_config:
395
+ elif collection_config.get("Resources"):
387
396
  # see if geodb resource configured use defaults if available
388
397
  for resource in collection_config["Resources"]:
389
398
  if resource["Name"] == "GeoDB":
390
- if "geodb_default_form" in catalog_config:
399
+ if catalog_config.get("geodb_default_form"):
391
400
  collection.extra_fields["eodash:jsonform"] = get_full_url(
392
401
  catalog_config["geodb_default_form"], catalog_config
393
402
  )
394
- if "geodb_default_vega" in catalog_config:
403
+ if catalog_config.get("geodb_default_vega"):
395
404
  collection.extra_fields["eodash:vegadefinition"] = get_full_url(
396
405
  catalog_config["geodb_default_vega"], catalog_config
397
406
  )
@@ -412,21 +421,49 @@ def add_process_info(collection: Collection, catalog_config: dict, collection_co
412
421
  },
413
422
  )
414
423
  )
424
+ elif resource["Name"] == "xcube" and catalog_config.get("default_xcube_process"):
425
+ target_url = "{}/timeseries/{}/{}?aggMethods=median".format(
426
+ resource["EndPoint"],
427
+ resource["DatacubeId"],
428
+ resource["Variable"],
429
+ )
430
+ process_endpoint_config = catalog_config["default_xcube_process"]["EndPoints"][0]
431
+ extra_fields = {
432
+ "id": process_endpoint_config["Identifier"],
433
+ "method": process_endpoint_config.get("Method", "GET"),
434
+ }
435
+ extra_fields["body"] = get_full_url(process_endpoint_config["Body"], catalog_config)
436
+ if catalog_config["default_xcube_process"].get("JsonForm"):
437
+ collection.extra_fields["eodash:jsonform"] = get_full_url(
438
+ catalog_config["default_xcube_process"]["JsonForm"], catalog_config
439
+ )
440
+ if catalog_config["default_xcube_process"].get("VegaDefinition"):
441
+ collection.extra_fields["eodash:vegadefinition"] = get_full_url(
442
+ catalog_config["default_xcube_process"]["VegaDefinition"], catalog_config
443
+ )
444
+
445
+ sl = Link(
446
+ rel="service",
447
+ target=target_url,
448
+ media_type=process_endpoint_config["Type"],
449
+ extra_fields=extra_fields,
450
+ )
451
+ collection.add_link(sl)
415
452
 
416
453
 
417
454
  def add_process_info_child_collection(
418
455
  collection: Collection, catalog_config: dict, collection_config: dict
419
456
  ) -> None:
420
457
  # in case of locations, we add the process itself on a child collection
421
- if "Process" in collection_config:
422
- if "EndPoints" in collection_config["Process"]:
458
+ if collection_config.get("Process"):
459
+ if collection_config["Process"].get("EndPoints"):
423
460
  for endpoint in collection_config["Process"]["EndPoints"]:
424
461
  collection.add_link(create_service_link(endpoint, catalog_config))
425
- if "JsonForm" in collection_config["Process"]:
462
+ if collection_config["Process"].get("JsonForm"):
426
463
  collection.extra_fields["eodash:jsonform"] = get_full_url(
427
464
  collection_config["Process"]["JsonForm"], catalog_config
428
465
  )
429
- if "VegaDefinition" in collection_config["Process"]:
466
+ if collection_config["Process"].get("VegaDefinition"):
430
467
  collection.extra_fields["eodash:vegadefinition"] = get_full_url(
431
468
  collection_config["Process"]["VegaDefinition"], catalog_config
432
469
  )
@@ -436,20 +473,20 @@ def add_base_overlay_info(
436
473
  collection: Collection, catalog_config: dict, collection_config: dict
437
474
  ) -> None:
438
475
  # add custom baselayers specially for this indicator
439
- if "BaseLayers" in collection_config:
476
+ if collection_config.get("BaseLayers"):
440
477
  for layer in collection_config["BaseLayers"]:
441
478
  collection.add_link(create_web_map_link(layer, role="baselayer"))
442
479
  # alternatively use default base layers defined
443
- elif "default_base_layers" in catalog_config:
480
+ elif catalog_config.get("default_base_layers"):
444
481
  base_layers = read_config_file(catalog_config["default_base_layers"])
445
482
  for layer in base_layers:
446
483
  collection.add_link(create_web_map_link(layer, role="baselayer"))
447
484
  # add custom overlays just for this indicator
448
- if "OverlayLayers" in collection_config:
485
+ if collection_config.get("OverlayLayers"):
449
486
  for layer in collection_config["OverlayLayers"]:
450
487
  collection.add_link(create_web_map_link(layer, role="overlay"))
451
488
  # check if default overlay layers defined
452
- elif "default_overlay_layers" in catalog_config:
489
+ elif catalog_config.get("default_overlay_layers"):
453
490
  overlay_layers = read_config_file(catalog_config["default_overlay_layers"])
454
491
  for layer in overlay_layers:
455
492
  collection.add_link(create_web_map_link(layer, role="overlay"))
@@ -458,41 +495,41 @@ def add_base_overlay_info(
458
495
  def add_extra_fields(
459
496
  stac_object: Collection | Link, collection_config: dict, is_root_collection: bool = False
460
497
  ) -> None:
461
- if "yAxis" in collection_config:
498
+ if collection_config.get("yAxis"):
462
499
  stac_object.extra_fields["yAxis"] = collection_config["yAxis"]
463
- if "Themes" in collection_config:
500
+ if collection_config.get("Themes"):
464
501
  stac_object.extra_fields["themes"] = collection_config["Themes"]
465
502
  if (
466
- "Locations" in collection_config or "Subcollections" in collection_config
503
+ collection_config.get("Locations") or collection_config.get("Subcollections")
467
504
  ) and is_root_collection:
468
505
  stac_object.extra_fields["locations"] = True
469
- if "Tags" in collection_config:
506
+ if collection_config.get("Tags"):
470
507
  stac_object.extra_fields["tags"] = collection_config["Tags"]
471
- if "Satellite" in collection_config:
508
+ if collection_config.get("Satellite"):
472
509
  stac_object.extra_fields["satellite"] = collection_config["Satellite"]
473
- if "Sensor" in collection_config:
510
+ if collection_config.get("Sensor"):
474
511
  stac_object.extra_fields["sensor"] = collection_config["Sensor"]
475
- if "Agency" in collection_config:
512
+ if collection_config.get("Agency"):
476
513
  stac_object.extra_fields["agency"] = collection_config["Agency"]
477
- if "EodashIdentifier" in collection_config:
514
+ if collection_config.get("EodashIdentifier"):
478
515
  stac_object.extra_fields["subcode"] = collection_config["EodashIdentifier"]
479
- if "CollectionGroup" in collection_config:
516
+ if collection_config.get("CollectionGroup"):
480
517
  stac_object.extra_fields["collection_group"] = collection_config["CollectionGroup"]
481
- if "DataSource" in collection_config:
482
- if "Spaceborne" in collection_config["DataSource"]:
483
- if "Sensor" in collection_config["DataSource"]["Spaceborne"]:
518
+ if collection_config.get("DataSource"):
519
+ if collection_config["DataSource"].get("Spaceborne"):
520
+ if collection_config["DataSource"]["Spaceborne"].get("Sensor"):
484
521
  stac_object.extra_fields["sensor"] = collection_config["DataSource"]["Spaceborne"][
485
522
  "Sensor"
486
523
  ]
487
- if "Satellite" in collection_config["DataSource"]["Spaceborne"]:
524
+ if collection_config["DataSource"]["Spaceborne"].get("Satellite"):
488
525
  stac_object.extra_fields["satellite"] = collection_config["DataSource"][
489
526
  "Spaceborne"
490
527
  ]["Satellite"]
491
- if "InSitu" in collection_config["DataSource"]:
528
+ if collection_config["DataSource"].get("InSitu"):
492
529
  stac_object.extra_fields["insituSources"] = collection_config["DataSource"]["InSitu"]
493
- if "Other" in collection_config["DataSource"]:
530
+ if collection_config["DataSource"].get("Other"):
494
531
  stac_object.extra_fields["otherSources"] = collection_config["DataSource"]["Other"]
495
- if "MapProjection" in collection_config:
532
+ if collection_config.get("MapProjection"):
496
533
  stac_object.extra_fields["eodash:mapProjection"] = collection_config["MapProjection"]
497
534
 
498
535
 
@@ -31,7 +31,7 @@ def generate_thumbnail(
31
31
  ) -> None:
32
32
  if endpoint_config["Name"] == "Sentinel Hub" or endpoint_config["Name"] == "WMS":
33
33
  instanceId = os.getenv("SH_INSTANCE_ID")
34
- if "InstanceId" in endpoint_config:
34
+ if endpoint_config.get("InstanceId"):
35
35
  instanceId = endpoint_config["InstanceId"]
36
36
  # Build example url
37
37
  wms_config = (
@@ -82,6 +82,7 @@ def retrieveExtentFromWCS(
82
82
  template = "An exception of type {0} occurred. Arguments:\n{1!r}"
83
83
  message = template.format(type(e).__name__, e.args)
84
84
  LOGGER.warn(message)
85
+ raise e
85
86
 
86
87
  bbox = [-180.0, -90.0, 180.0, 90.0]
87
88
  owsnmspc = "{http://www.opengis.net/ows/2.0}"
@@ -143,6 +144,7 @@ def retrieveExtentFromWMSWMTS(
143
144
  template = "An exception of type {0} occurred. Arguments:\n{1!r}"
144
145
  message = template.format(type(e).__name__, e.args)
145
146
  LOGGER.warn(message)
147
+ raise e
146
148
 
147
149
  bbox = [-180.0, -90.0, 180.0, 90.0]
148
150
  if service and service[layer].boundingBoxWGS84:
@@ -285,7 +287,7 @@ def iter_len_at_least(i, n: int) -> int:
285
287
 
286
288
  def generate_veda_cog_link(endpoint_config: dict, file_url: str | None) -> str:
287
289
  bidx = ""
288
- if "Bidx" in endpoint_config:
290
+ if endpoint_config.get("Bidx"):
289
291
  # Check if an array was provided
290
292
  if hasattr(endpoint_config["Bidx"], "__len__"):
291
293
  for band in endpoint_config["Bidx"]:
@@ -294,25 +296,29 @@ def generate_veda_cog_link(endpoint_config: dict, file_url: str | None) -> str:
294
296
  bidx = "&bidx={}".format(endpoint_config["Bidx"])
295
297
 
296
298
  colormap = ""
297
- if "Colormap" in endpoint_config:
299
+ if endpoint_config.get("Colormap"):
298
300
  colormap = "&colormap={}".format(endpoint_config["Colormap"])
299
301
  # TODO: For now we assume a already urlparsed colormap definition
300
302
  # it could be nice to allow a json and better convert it on the fly
301
303
  # colormap = "&colormap=%s"%(urllib.parse.quote(str(endpoint_config["Colormap"])))
302
304
 
305
+ Nodata = ""
306
+ if endpoint_config.get("Nodata"):
307
+ Nodata = "&nodata={}".format(endpoint_config["Nodata"])
308
+
303
309
  colormap_name = ""
304
- if "ColormapName" in endpoint_config:
310
+ if endpoint_config.get("ColormapName"):
305
311
  colormap_name = "&colormap_name={}".format(endpoint_config["ColormapName"])
306
312
 
307
313
  rescale = ""
308
- if "Rescale" in endpoint_config:
314
+ if endpoint_config.get("Rescale"):
309
315
  rescale = "&rescale={},{}".format(
310
316
  endpoint_config["Rescale"][0], endpoint_config["Rescale"][1]
311
317
  )
312
318
 
313
319
  file_url = f"url={file_url}&" if file_url else ""
314
320
 
315
- target_url = f"https://openveda.cloud/api/raster/cog/tiles/WebMercatorQuad/{{z}}/{{x}}/{{y}}?{file_url}resampling_method=nearest{bidx}{colormap}{colormap_name}{rescale}"
321
+ target_url = f"https://openveda.cloud/api/raster/cog/tiles/WebMercatorQuad/{{z}}/{{x}}/{{y}}?{file_url}resampling_method=nearest{bidx}{colormap}{colormap_name}{rescale}{Nodata}"
316
322
  return target_url
317
323
 
318
324
 
@@ -336,7 +342,7 @@ def add_single_item_if_collection_empty(collection: Collection) -> None:
336
342
  else:
337
343
  item = Item(
338
344
  id=str(uuid.uuid4()),
339
- bbox=[-180, -85, 180, 85],
345
+ bbox=[-180.0, -90.0, 180.0, 90.0],
340
346
  properties={},
341
347
  geometry=None,
342
348
  datetime=datetime(1970, 1, 1, 0, 0, 0, tzinfo=pytztimezone("UTC")),
@@ -458,3 +464,20 @@ def _load_file(filepath):
458
464
  return yaml.safe_load(content)
459
465
  except yaml.YAMLError as err:
460
466
  raise ValueError(f"Failed to parse '{filepath}' as JSON or YAML: {err}") from err
467
+
468
+
469
+ def merge_bboxes(bboxes: list[list[float]]) -> list[float]:
470
+ """
471
+ Merge bounding boxes into one bounding box that contains them all.
472
+ Returns:
473
+ A list representing the merged bbox: [min_lon, min_lat, max_lon, max_lat]
474
+ """
475
+ if not bboxes:
476
+ raise ValueError("No bounding boxes provided.")
477
+
478
+ min_lon = min(b[0] for b in bboxes)
479
+ min_lat = min(b[1] for b in bboxes)
480
+ max_lon = max(b[2] for b in bboxes)
481
+ max_lat = max(b[3] for b in bboxes)
482
+
483
+ return [min_lon, min_lat, max_lon, max_lat]
@@ -105,7 +105,7 @@ def test_collection_no_wms_has_a_single_item(catalog_output_folder):
105
105
  with open(os.path.join(root_collection_path, "collection.json")) as fp:
106
106
  collection_json = json.load(fp)
107
107
  # test that custom bbox is set
108
- assert [-180, -85, 180, 85] in collection_json["extent"]["spatial"]["bbox"]
108
+ assert [-180.0, -90.0, 180.0, 90.0] in collection_json["extent"]["spatial"]["bbox"]
109
109
  # test that time interval is 1970-today
110
110
  assert collection_json["extent"]["temporal"]["interval"][0][0] == start_date
111
111
  assert (