-
Notifications
You must be signed in to change notification settings - Fork 3
RDPS implementation with additional extensions #114
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 73 commits
d737a9d
6bde69e
911cbe9
b5f0de9
76b37a7
5e5541a
4d6aba0
3866246
138f0dd
cd5e801
eb86c50
bfdd825
49804f5
99868fc
24066be
90c30c1
9ea89d7
fc8806b
ad2bba9
6c45ef2
730763c
0ca5211
2e6a69e
a8c299d
ec29535
933352f
27e2a40
92bc676
57a6836
d8c7b97
2c207a7
c03b5cf
126aa4c
40fda07
c968b28
b75bb54
f22b369
7e6a764
c462ecd
e38ad09
dd7e480
4597fe9
29dc7e2
ea75a9b
26d9135
acaeea8
af3d1fe
ffa1021
5b60a09
af32a48
e24834a
e442359
85f70f1
faae3a1
24a546d
0b10d2a
0443acd
dfe1437
387baa7
11029e4
f904c4c
01ac7da
43a5d0b
29b2890
293a37e
64e39de
660265d
afbe940
64c33b6
b73c216
5b668e9
84ab682
1a0b8aa
4366e6b
227a7d5
30054ea
0dadae7
49f0d4a
185ddac
8cdbc59
3f033c2
1fea5bd
07696c4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -24,10 +24,14 @@ Provided implementations of `STACpopulatorBase`: | |
|
|
||
| | Implementation | Description | | ||
| |----------------------------------------------|-------------------------------------------------------------------------------------------------------------------------| | ||
| | [RDPS_CRIM][RDPS_CRIM] | Crawls a THREDDS Catalog for RDPS NCML-annotated NetCDF references to publish corresponding STAC Collection and Items. | | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add another entry for |
||
| | [HRDPS_CRIM][HRDPS_CRIM] | Crawls a THREDDS Catalog for HRDPS NCML-annotated NetCDF references to publish corresponding STAC Collection and Items. | | ||
| | [CMIP6_UofT][CMIP6_UofT] | Crawls a THREDDS Catalog for CMIP6 NCML-annotated NetCDF references to publish corresponding STAC Collection and Items. | | ||
| | [DirectoryLoader][DirLoader] | Crawls a subdirectory hierarchy of pre-generated STAC Collections and Items to publish to a STAC API endpoint. | | ||
| | [CORDEX-CMIP6_Ouranos][CORDEX-CMIP6_Ouranos] | Crawls a THREDDS Catalog for CORDEX-CMIP6 NetCDF references to publish corresponding STAC Collection and Items. | | ||
| | [CORDEX-CMIP6_Ouranos][CORDEX-CMIP6_Ouranos] | Crawls a THREDDS Catalog for CORDEX-CMIP6 NetCDF references to publish corresponding STAC Collection and Items. | | ||
|
|
||
| [RDPS_CRIM]: STACpopulator/implementations/RDPS_CRIM/add_RDPS.py | ||
| [HRDPS_CRIM]: STACpopulator/implementations/HRDPS_CRIM/add_HRDPS.py | ||
| [CMIP6_UofT]: STACpopulator/implementations/CMIP6_UofT/add_CMIP6.py | ||
| [DirLoader]: STACpopulator/implementations/DirectoryLoader/crawl_directory.py | ||
| [CORDEX-CMIP6_Ouranos]: STACpopulator/implementations/CORDEX-CMIP6_Ouranos/add_CORDEX-CMIP6.py | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -64,6 +64,16 @@ | |
| class Helper: | ||
| """Class to be subclassed by extension helpers.""" | ||
|
|
||
| @classmethod | ||
| @abstractmethod | ||
| def from_data( | ||
| cls, | ||
| data: dict[str, Any], | ||
| **kwargs, | ||
| ) -> "Helper": | ||
| """Create a Helper instance from raw data.""" | ||
| pass | ||
|
|
||
|
|
||
| class ExtensionHelper(BaseModel, Helper): | ||
| """Base class for dataset properties going into the catalog. | ||
|
|
@@ -190,7 +200,8 @@ def create_uid(self) -> str: | |
| @model_validator(mode="after") | ||
| def find_helpers(self) -> "BaseSTAC": | ||
| """Populate the list of extensions.""" | ||
| for key, field in self.model_fields.items(): | ||
| # Access model fields from class. From obj will be removed in pydantic v3 | ||
| for key, field in type(self).model_fields.items(): | ||
| if isinstance(field.annotation, type) and issubclass(field.annotation, Helper): | ||
| self._helpers.append(key) | ||
| return self | ||
|
|
@@ -328,7 +339,7 @@ def get_assets( | |
| return { | ||
| key: asset | ||
| for key, asset in self.item.get_assets().items() | ||
| if (service_type is ServiceType and service_type.value in asset.extra_fields) | ||
| if (isinstance(service_type, ServiceType) and service_type.value in asset.extra_fields) | ||
| or any(ServiceType.from_value(field, default=None) is ServiceType for field in asset.extra_fields) | ||
|
||
| } | ||
|
|
||
|
|
||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is good and aligned with https://github.com/stac-utils/pystac/tree/main/pystac/extensions. Maybe consider opening a PR directly over there and push the change upstream.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For cross ref, I created this PR: stac-utils/pystac#1592
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You mean using the PR code version directly to import cf from pystac?
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes. Using the latest commit hash of the branch.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just did, but the presence of the I can think of (1) adding a
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The Try this first, and if that doesn't work, will see how to handle it.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes that's what I did and it didn't work
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. OK. I suggest we wait and see what comes out from stac-utils/pystac#1592 (comment). Maybe it could be done fairly soon and avoid the issue altogether. If not, I guess it would be easier to keep the code as is in the meantime with a FIXME note until the PR is merged. Since the commit reference is not simple replacement in the dependencies, I think other workarounds would imply too many changes or manual steps leading to setup errors. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,239 @@ | ||
| """CF Extension Module.""" | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| import functools | ||
| from typing import ( | ||
| Any, | ||
| Dict, | ||
| Generic, | ||
| Iterable, | ||
| List, | ||
| Literal, | ||
| Optional, | ||
| TypeVar, | ||
| Union, | ||
| cast, | ||
| get_args, | ||
| ) | ||
|
|
||
| import pystac | ||
| from pydantic import BaseModel | ||
| from pystac.extensions import item_assets | ||
| from pystac.extensions.base import ExtensionManagementMixin, PropertiesExtension | ||
|
|
||
| from STACpopulator.extensions.base import ExtensionHelper | ||
| from STACpopulator.stac_utils import ServiceType | ||
|
|
||
| T = TypeVar("T", pystac.Collection, pystac.Item, pystac.Asset) | ||
| SchemaName = Literal["cf"] | ||
| SCHEMA_URI = "https://stac-extensions.github.io/cf/v0.2.0/schema.json" | ||
| PREFIX = f"{get_args(SchemaName)[0]}:" | ||
| PARAMETER_PROP = PREFIX + "parameter" | ||
|
|
||
|
|
||
| class CFParameter(BaseModel): | ||
| """CFParameter.""" | ||
|
|
||
| name: str | ||
| unit: Optional[str] | ||
|
|
||
| def __repr__(self) -> str: | ||
| """Return string repr.""" | ||
| return f"<CFParameter name={self.name}, unit={self.unit}>" | ||
|
|
||
|
|
||
| class CFHelper(ExtensionHelper): | ||
| """CFHelper.""" | ||
|
|
||
| _prefix: str = "cf" | ||
| variables: Dict[str, Any] | ||
|
|
||
| @functools.cached_property | ||
| def parameters(self) -> List[CFParameter]: | ||
| """Extracts cf:parameter-like information from item_data.""" | ||
| parameters = [] | ||
|
|
||
| for _, var in self.variables.items(): | ||
|
||
| attrs = var.get("attributes", {}) | ||
| name = attrs.get("standard_name") # Get the required standard name | ||
| if not name: | ||
| continue # Skip if no valid name | ||
| unit = attrs.get("units") or "" | ||
|
||
| parameters.append(CFParameter(name=name, unit=unit)) | ||
|
|
||
| return parameters | ||
|
|
||
| @classmethod | ||
| def from_data( | ||
| cls, | ||
| data: dict[str, Any], | ||
| **kwargs, | ||
| ) -> "CFHelper": | ||
| """Create a CFHelper instance from raw data.""" | ||
| return cls(variables=data["data"]["variables"], **kwargs) | ||
|
|
||
| def apply(self, item: T, add_if_missing: bool = True) -> T: | ||
| """Apply the Datacube extension to an item.""" | ||
| ext = CFExtension.ext(item, add_if_missing=add_if_missing) | ||
| ext.apply(parameters=self.parameters) | ||
|
|
||
| # FIXME: This temporary workaround has been added to comply with the (most certainly buggy) validation schema for CF extension | ||
| # It should be remove once the PR is integrated since applying on the item should be enough | ||
| asset = item.assets["HTTPServer"] | ||
| cf_asset_ext = CFExtension.ext(asset, add_if_missing=True) | ||
| cf_asset_ext.apply(parameters=self.parameters) | ||
| return item | ||
|
|
||
|
|
||
| class CFExtension( | ||
| Generic[T], | ||
| PropertiesExtension, | ||
| ExtensionManagementMixin[Union[pystac.Asset, pystac.Item, pystac.Collection]], | ||
| ): | ||
| """CF Metadata Extension.""" | ||
|
|
||
| @property | ||
| def name(self) -> SchemaName: | ||
| """Return the schema name.""" | ||
| return get_args(SchemaName)[0] | ||
|
|
||
| @property | ||
| def parameter(self) -> List[dict[str, Any]] | None: | ||
| """Get or set the CF parameter(s).""" | ||
| return self._get_property(PARAMETER_PROP, int) | ||
|
|
||
| @parameter.setter | ||
| def parameter(self, v: List[dict[str, Any]] | None) -> None: | ||
| self._set_property(PARAMETER_PROP, v) | ||
|
|
||
| def apply( | ||
| self, | ||
| parameters: Union[List[CFParameter], List[dict[str, Any]]], | ||
| ) -> None: | ||
| """Apply CF Extension properties to the extended :class:`~pystac.Item` or :class:`~pystac.Asset`.""" | ||
| if not isinstance(parameters[0], dict): | ||
| parameters = [p.model_dump() for p in parameters] | ||
| self.parameter = parameters | ||
|
|
||
| @classmethod | ||
| def get_schema_uri(cls) -> str: | ||
| """Return this extension's schema URI.""" | ||
| return SCHEMA_URI | ||
|
|
||
| @classmethod | ||
| def ext(cls, obj: T, add_if_missing: bool = False) -> CFExtension[T]: | ||
| """Extend the given STAC Object with properties from the :stac-ext:`CF Extension <cf>`. | ||
| This extension can be applied to instances of :class:`~pystac.Item`, :class:`~pystac.Asset`, or :class:`~pystac.Collection`. | ||
| Raises | ||
| ------ | ||
| pystac.ExtensionTypeError : If an invalid object type is passed. | ||
| """ | ||
| if isinstance(obj, pystac.Collection): | ||
| cls.ensure_has_extension(obj, add_if_missing) | ||
| return cast(CFExtension[T], CollectionCFExtension(obj)) | ||
| elif isinstance(obj, pystac.Item): | ||
| cls.ensure_has_extension(obj, add_if_missing) | ||
| return cast(CFExtension[T], ItemCFExtension(obj)) | ||
| elif isinstance(obj, pystac.Asset): | ||
| cls.ensure_owner_has_extension(obj, add_if_missing) | ||
| return cast(CFExtension[T], AssetCFExtension(obj)) | ||
| elif isinstance(obj, item_assets.AssetDefinition): | ||
| cls.ensure_owner_has_extension(obj, add_if_missing) | ||
| return cast(CFExtension[T], ItemAssetsCFExtension(obj)) | ||
| else: | ||
| raise pystac.ExtensionTypeError(cls._ext_error_message(obj)) | ||
|
|
||
|
|
||
| class ItemCFExtension(CFExtension[pystac.Item]): | ||
| """ | ||
| A concrete implementation of :class:`CFExtension` on an :class:`~pystac.Item`. | ||
| Extends the properties of the Item to include properties defined in the | ||
| :stac-ext:`CF Extension <cf>`. | ||
| This class should generally not be instantiated directly. Instead, call | ||
| :meth:`CFExtension.ext` on an :class:`~pystac.Item` to extend it. | ||
| """ | ||
|
|
||
| def __init__(self, item: pystac.Item) -> None: | ||
| self.item = item | ||
| self.properties = item.properties | ||
|
|
||
| def get_assets( | ||
| self, | ||
| service_type: Optional[ServiceType] = None, | ||
| ) -> dict[str, pystac.Asset]: | ||
| """Get the item's assets where eo:bands are defined. | ||
| Args: | ||
| service_type: If set, filter the assets such that only those with a | ||
| matching :class:`~STACpopulator.stac_utils.ServiceType` are returned. | ||
| Returns | ||
| ------- | ||
| Dict[str, Asset]: A dictionary of assets that match ``service_type`` | ||
| if set or else all of this item's assets were service types are defined. | ||
| """ | ||
| return { | ||
| key: asset | ||
| for key, asset in self.item.get_assets().items() | ||
| if (service_type in ServiceType and service_type.value in asset.extra_fields) | ||
|
||
| or any(ServiceType.from_value(field, default=None) is ServiceType for field in asset.extra_fields) | ||
| } | ||
|
|
||
| def __repr__(self) -> str: | ||
| """Return repr.""" | ||
| return f"<ItemCFExtension Item id={self.item.id}>" | ||
|
|
||
|
|
||
| class ItemAssetsCFExtension(CFExtension[item_assets.AssetDefinition]): | ||
| """Extention for CF item assets.""" | ||
|
|
||
| properties: dict[str, Any] | ||
| asset_defn: item_assets.AssetDefinition | ||
|
|
||
| def __init__(self, item_asset: item_assets.AssetDefinition) -> None: | ||
| self.asset_defn = item_asset | ||
| self.properties = item_asset.properties | ||
|
|
||
|
|
||
| class AssetCFExtension(CFExtension[pystac.Asset]): | ||
| """ | ||
| A concrete implementation of :class:`CFExtension` on an :class:`~pystac.Asset`. | ||
| Extends the Asset fields to include properties defined in the | ||
| :stac-ext:`CF Extension <cf>`. | ||
| This class should generally not be instantiated directly. Instead, call | ||
| :meth:`CFExtension.ext` on an :class:`~pystac.Asset` to extend it. | ||
| """ | ||
|
|
||
| asset_href: str | ||
| """The ``href`` value of the :class:`~pystac.Asset` being extended.""" | ||
|
|
||
| properties: dict[str, Any] | ||
| """The :class:`~pystac.Asset` fields, including extension properties.""" | ||
|
|
||
| additional_read_properties: Optional[Iterable[dict[str, Any]]] = None | ||
| """If present, this will be a list containing 1 dictionary representing the | ||
| properties of the owning :class:`~pystac.Item`.""" | ||
|
|
||
| def __init__(self, asset: pystac.Asset) -> None: | ||
| self.asset_href = asset.href | ||
| self.properties = asset.extra_fields | ||
| if asset.owner and isinstance(asset.owner, pystac.Item): | ||
| self.additional_read_properties = [asset.owner.properties] | ||
|
|
||
| def __repr__(self) -> str: | ||
| """Return repr.""" | ||
| return f"<AssetCFExtension Asset href={self.asset_href}>" | ||
|
|
||
|
|
||
| class CollectionCFExtension(CFExtension[pystac.Collection]): | ||
| """Extension for CF data.""" | ||
|
|
||
| def __init__(self, collection: pystac.Collection) -> None: | ||
| self.collection = collection | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -141,6 +141,15 @@ def __init__(self, attrs: MutableMapping[str, Any]) -> None: | |
| }, | ||
| } | ||
|
|
||
| @classmethod | ||
| def from_data( | ||
| cls, | ||
| data: dict[str, Any], | ||
| **kwargs, | ||
| ) -> "DataCubeHelper": | ||
| """Create a DataCubeHelper instance from raw data.""" | ||
| return cls(attrs=data["data"]) | ||
|
|
||
| @property | ||
| @functools.cache | ||
| def dimensions(self) -> dict[str, Dimension]: | ||
|
|
@@ -213,9 +222,11 @@ def variables(self) -> dict[str, Variable]: | |
| else: | ||
| dtype = VariableType.DATA.value | ||
|
|
||
| dimensions = meta.get("shape", []) | ||
|
|
||
| variables[name] = Variable( | ||
| properties=dict( | ||
| dimensions=meta["shape"], | ||
| dimensions=[] if dimensions == [""] else dimensions, | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @fmigneault this change fixes the empty string dimension issue in "rotated_pole": {
"type": "data",
"unit": "",
"dimensions": [], # instead of [""] previously
"description": ""
},For the
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nice finding. 👍 I logged the issue: stac-utils/pystac#1593 |
||
| type=dtype, | ||
| description=attrs.get("description", attrs.get("long_name", "")), | ||
| unit=attrs.get("units", ""), | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is missing the added
contactsandprovidersfeatures.Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should I also update the package version to 0.10.0 in
pyproject.toml? Or is versioning handled automatically?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It will be done post-merge with
bump-my-version.