Skip to content

Commit e3c05a6

Browse files
committed
Merge remote-tracking branch 'origin/master' into 747-robustranged-download-support
2 parents 85c48ca + 4aba08a commit e3c05a6

File tree

17 files changed

+278
-80
lines changed

17 files changed

+278
-80
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
### Added
1111

12+
- Support `collection_property` based property filtering in `load_stac` ([#246](https://github.com/Open-EO/openeo-python-client/issues/246))
13+
1214
### Changed
1315

16+
- Eliminate deprecated `utcnow` usage patterns. Introduce `Rfc3339.now_utc()` method (as replacement for deprecated `utcnow()` method) to simplify finding deprecated `utcnow` usage in user code. ([#760](https://github.com/Open-EO/openeo-python-client/issues/760))
17+
1418
### Removed
1519

1620
### Fixed
1721

22+
- Preserve original non-spatial dimensions in `CubeMetadata.resample_cube_spatial()` ([Open-EO/openeo-python-driver#397](https://github.com/Open-EO/openeo-python-driver/issues/397))
23+
1824

1925
## [0.40.0] - 2025-04-14
2026

docs/conf.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@
5858

5959
# General information about the project.
6060
project = 'openEO Python Client'
61-
copyright = '2017 - {d}, Jeroen Dries'.format(d=datetime.datetime.utcnow().strftime("%Y"))
61+
copyright = '2017 - {d}, Jeroen Dries'.format(d=datetime.date.today().strftime("%Y"))
6262
author = 'Jeroen Dries'
6363

6464
# The version info for the project you're documenting, acts as replacement for

openeo/_version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "0.41.0a1"
1+
__version__ = "0.41.0a2"

openeo/extra/job_management/__init__.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -595,7 +595,7 @@ def _launch_job(self, start_job, df, i, backend_name, stats: Optional[dict] = No
595595
df.loc[i, "status"] = "start_failed"
596596
stats["start_job error"] += 1
597597
else:
598-
df.loc[i, "start_time"] = rfc3339.utcnow()
598+
df.loc[i, "start_time"] = rfc3339.now_utc()
599599
if job:
600600
df.loc[i, "id"] = job.job_id
601601
with ignore_connection_errors(context="get status"):
@@ -675,7 +675,7 @@ def _cancel_prolonged_job(self, job: BatchJob, row):
675675
job_running_start_time = rfc3339.parse_datetime(row.get("running_start_time"), with_timezone=True)
676676

677677
# Parse the current time into a datetime object with timezone info
678-
current_time = rfc3339.parse_datetime(rfc3339.utcnow(), with_timezone=True)
678+
current_time = rfc3339.parse_datetime(rfc3339.now_utc(), with_timezone=True)
679679

680680
# Calculate the elapsed time between job start and now
681681
elapsed = current_time - job_running_start_time
@@ -751,15 +751,15 @@ def _track_statuses(self, job_db: JobDatabaseInterface, stats: Optional[dict] =
751751

752752
if previous_status in {"created", "queued"} and new_status == "running":
753753
stats["job started running"] += 1
754-
active.loc[i, "running_start_time"] = rfc3339.utcnow()
754+
active.loc[i, "running_start_time"] = rfc3339.now_utc()
755755

756756
if self._cancel_running_job_after and new_status == "running":
757757
if (not active.loc[i, "running_start_time"] or pd.isna(active.loc[i, "running_start_time"])):
758758
_log.warning(
759759
f"Unknown 'running_start_time' for running job {job_id}. Using current time as an approximation."
760760
)
761761
stats["job started running"] += 1
762-
active.loc[i, "running_start_time"] = rfc3339.utcnow()
762+
active.loc[i, "running_start_time"] = rfc3339.now_utc()
763763

764764
self._cancel_prolonged_job(the_job, active.loc[i])
765765

openeo/extra/spectral_indices/spectral_indices.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from typing import Dict, List, Optional, Set
55

66
from openeo import BaseOpenEoException
7+
from openeo.metadata import CollectionMetadata
78
from openeo.processes import ProcessBuilder, array_create, array_modify
89
from openeo.rest.datacube import DataCube
910

@@ -273,7 +274,7 @@ def compute_and_rescale_indices(
273274
# Automatic band mapping
274275
band_mapping = _BandMapping()
275276
if platform is None:
276-
if datacube.metadata and datacube.metadata.get("id"):
277+
if isinstance(datacube.metadata, CollectionMetadata) and datacube.metadata.get("id"):
277278
platform = band_mapping.guess_platform(name=datacube.metadata.get("id"))
278279
else:
279280
raise BandMappingException("Unable to determine satellite platform from data cube metadata")

openeo/metadata.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -486,7 +486,11 @@ def resample_spatial(
486486
return self._clone_and_update(dimensions=dimensions)
487487

488488
def resample_cube_spatial(self, target: CubeMetadata) -> CubeMetadata:
489-
return self._clone_and_update(dimensions=list(target._dimensions))
489+
# Replace spatial dimensions with ones from target, but keep other dimensions
490+
dimensions = [d for d in (self._dimensions or []) if not isinstance(d, SpatialDimension)]
491+
dimensions.extend(target.spatial_dimensions)
492+
return self._clone_and_update(dimensions=dimensions)
493+
490494

491495
class CollectionMetadata(CubeMetadata):
492496
"""

openeo/rest/auth/config.py

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -59,11 +59,6 @@ def assert_private_file(path: Path):
5959
raise PermissionError(message)
6060

6161

62-
def utcnow_rfc3339() -> str:
63-
"""Current datetime formatted as RFC-3339 string."""
64-
return rfc3339.datetime(datetime.utcnow())
65-
66-
6762
def _normalize_url(url: str) -> str:
6863
"""Normalize a url (trim trailing slash), to simplify equality checking."""
6964
return url.rstrip("/") or "/"
@@ -146,7 +141,7 @@ def _write(self, data: dict):
146141
if "metadata" not in data:
147142
data["metadata"] = {
148143
"type": "AuthConfig",
149-
"created": utcnow_rfc3339(),
144+
"created": rfc3339.now_utc(),
150145
"created_by": "openeo-python-client {v}".format(v=__version__),
151146
"version": 1,
152147
}
@@ -169,7 +164,7 @@ def set_basic_auth(self, backend: str, username: str, password: Union[str, None]
169164
"basic",
170165
)
171166
# TODO: support multiple basic auth credentials? (pick latest by default for example)
172-
deep_set(data, *keys, "date", value=utcnow_rfc3339())
167+
deep_set(data, *keys, "date", value=rfc3339.now_utc())
173168
deep_set(data, *keys, "username", value=username)
174169
if password:
175170
deep_set(data, *keys, "password", value=password)
@@ -203,7 +198,7 @@ def set_oidc_client_config(
203198
data = self.load()
204199
keys = ("backends", _normalize_url(backend), "oidc", "providers", provider_id)
205200
# TODO: support multiple clients? (pick latest by default for example)
206-
deep_set(data, *keys, "date", value=utcnow_rfc3339())
201+
deep_set(data, *keys, "date", value=rfc3339.now_utc())
207202
deep_set(data, *keys, "client_id", value=client_id)
208203
deep_set(data, *keys, "client_secret", value=client_secret)
209204
if issuer:
@@ -233,7 +228,7 @@ def set_refresh_token(self, issuer: str, client_id: str, refresh_token: str):
233228
_normalize_url(issuer),
234229
client_id,
235230
value={
236-
"date": utcnow_rfc3339(),
231+
"date": rfc3339.now_utc(),
237232
"refresh_token": refresh_token,
238233
},
239234
)

openeo/rest/connection.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1097,7 +1097,7 @@ def load_collection(
10971097
temporal_extent: Union[Sequence[InputDate], Parameter, str, None] = None,
10981098
bands: Union[Iterable[str], Parameter, str, None] = None,
10991099
properties: Union[
1100-
None, Dict[str, Union[str, PGNode, Callable]], List[CollectionProperty], CollectionProperty
1100+
Dict[str, Union[PGNode, Callable]], List[CollectionProperty], CollectionProperty, None
11011101
] = None,
11021102
max_cloud_cover: Optional[float] = None,
11031103
fetch_metadata: bool = True,
@@ -1198,7 +1198,9 @@ def load_stac(
11981198
spatial_extent: Union[dict, Parameter, shapely.geometry.base.BaseGeometry, str, Path, None] = None,
11991199
temporal_extent: Union[Sequence[InputDate], Parameter, str, None] = None,
12001200
bands: Union[Iterable[str], Parameter, str, None] = None,
1201-
properties: Optional[Dict[str, Union[str, PGNode, Callable]]] = None,
1201+
properties: Union[
1202+
Dict[str, Union[PGNode, Callable]], List[CollectionProperty], CollectionProperty, None
1203+
] = None,
12021204
) -> DataCube:
12031205
"""
12041206
Loads data from a static STAC catalog or a STAC API Collection and returns the data as a processable :py:class:`DataCube`.
@@ -1292,6 +1294,8 @@ def load_stac(
12921294
The value must be a condition (user-defined process) to be evaluated against a STAC API.
12931295
This parameter is not supported for static STAC.
12941296
1297+
See :py:func:`~openeo.rest.graph_building.collection_property` for easy construction of property filters.
1298+
12951299
.. versionadded:: 0.17.0
12961300
12971301
.. versionchanged:: 0.23.0
@@ -1300,6 +1304,9 @@ def load_stac(
13001304
13011305
.. versionchanged:: 0.37.0
13021306
Argument ``spatial_extent``: add support for passing a Shapely geometry or a local path to a GeoJSON file.
1307+
1308+
.. versionchanged:: 0.41.0
1309+
Add support for :py:func:`~openeo.rest.graph_building.collection_property` based property filters
13031310
"""
13041311
return DataCube.load_stac(
13051312
url=url,

openeo/rest/datacube.py

Lines changed: 67 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ def process_with_node(self, pg: PGNode, metadata: Optional[CollectionMetadata] =
157157

158158
def _do_metadata_normalization(self) -> bool:
159159
"""Do metadata-based normalization/validation of dimension names, band names, ..."""
160-
return isinstance(self.metadata, CollectionMetadata)
160+
return isinstance(self.metadata, CubeMetadata)
161161

162162
def _assert_valid_dimension_name(self, name: str) -> str:
163163
if self._do_metadata_normalization():
@@ -175,7 +175,7 @@ def load_collection(
175175
bands: Union[Iterable[str], Parameter, str, None] = None,
176176
fetch_metadata: bool = True,
177177
properties: Union[
178-
None, Dict[str, Union[str, PGNode, typing.Callable]], List[CollectionProperty], CollectionProperty
178+
Dict[str, Union[PGNode, typing.Callable]], List[CollectionProperty], CollectionProperty, None
179179
] = None,
180180
max_cloud_cover: Optional[float] = None,
181181
) -> DataCube:
@@ -250,27 +250,13 @@ def load_collection(
250250
metadata = metadata.filter_bands(bands)
251251
arguments['bands'] = bands
252252

253-
if isinstance(properties, list):
254-
# TODO: warn about items that are not CollectionProperty objects instead of silently dropping them.
255-
properties = {p.name: p.from_node() for p in properties if isinstance(p, CollectionProperty)}
256-
if isinstance(properties, CollectionProperty):
257-
properties = {properties.name: properties.from_node()}
258-
elif properties is None:
259-
properties = {}
260-
if max_cloud_cover:
261-
properties["eo:cloud_cover"] = lambda v: v <= max_cloud_cover
262-
if properties:
263-
summaries = metadata and metadata.get("summaries") or {}
264-
undefined_properties = set(properties.keys()).difference(summaries.keys())
265-
if undefined_properties:
266-
warnings.warn(
267-
f"{collection_id} property filtering with properties that are undefined "
268-
f"in the collection metadata (summaries): {', '.join(undefined_properties)}.",
269-
stacklevel=2,
270-
)
271-
arguments["properties"] = {
272-
prop: build_child_callback(pred, parent_parameters=["value"]) for prop, pred in properties.items()
273-
}
253+
properties = cls._build_load_properties_argument(
254+
properties=properties,
255+
supported_properties=(metadata.get("summaries", default={}).keys() if metadata else None),
256+
max_cloud_cover=max_cloud_cover,
257+
)
258+
if properties is not None:
259+
arguments["properties"] = properties
274260

275261
pg = PGNode(
276262
process_id='load_collection',
@@ -282,6 +268,50 @@ def load_collection(
282268
load_collection, name="create_collection", since="0.4.6"
283269
)
284270

271+
@classmethod
272+
def _build_load_properties_argument(
273+
cls,
274+
properties: Union[
275+
Dict[str, Union[PGNode, typing.Callable]],
276+
List[CollectionProperty],
277+
CollectionProperty,
278+
None,
279+
],
280+
*,
281+
supported_properties: Optional[typing.Collection[str]] = None,
282+
max_cloud_cover: Optional[float] = None,
283+
) -> Union[Dict[str, PGNode], None]:
284+
"""
285+
Helper to convert/build the ``properties`` argument
286+
for ``load_collection`` and ``load_stac`` from user input
287+
"""
288+
if isinstance(properties, CollectionProperty):
289+
properties = [properties]
290+
if isinstance(properties, list):
291+
if not all(isinstance(p, CollectionProperty) for p in properties):
292+
raise ValueError(
293+
f"When providing load properties as a list, all items must be CollectionProperty objects, but got {properties}"
294+
)
295+
properties = {p.name: p.from_node() for p in properties}
296+
297+
if max_cloud_cover is not None:
298+
properties = properties or {}
299+
properties["eo:cloud_cover"] = lambda v: v <= max_cloud_cover
300+
301+
if isinstance(properties, dict):
302+
if supported_properties:
303+
unsupported_properties = set(properties.keys()).difference(supported_properties)
304+
if unsupported_properties:
305+
warnings.warn(
306+
f"Property filtering with unsupported properties according to collection/STAC metadata: {unsupported_properties} (supported: {supported_properties}).",
307+
stacklevel=3,
308+
)
309+
properties = {
310+
prop: build_child_callback(pred, parent_parameters=["value"]) for prop, pred in properties.items()
311+
}
312+
313+
return properties
314+
285315
@classmethod
286316
@deprecated(reason="Depends on non-standard process, replace with :py:meth:`openeo.rest.connection.Connection.load_stac` where possible.",version="0.25.0")
287317
def load_disk_collection(cls, connection: Connection, file_format: str, glob_pattern: str, **options) -> DataCube:
@@ -314,7 +344,9 @@ def load_stac(
314344
spatial_extent: Union[dict, Parameter, shapely.geometry.base.BaseGeometry, str, pathlib.Path, None] = None,
315345
temporal_extent: Union[Sequence[InputDate], Parameter, str, None] = None,
316346
bands: Union[Iterable[str], Parameter, str, None] = None,
317-
properties: Optional[Dict[str, Union[str, PGNode, Callable]]] = None,
347+
properties: Union[
348+
Dict[str, Union[PGNode, typing.Callable]], List[CollectionProperty], CollectionProperty, None
349+
] = None,
318350
connection: Optional[Connection] = None,
319351
) -> DataCube:
320352
"""
@@ -409,14 +441,19 @@ def load_stac(
409441
The value must be a condition (user-defined process) to be evaluated against a STAC API.
410442
This parameter is not supported for static STAC.
411443
444+
See :py:func:`~openeo.rest.graph_building.collection_property` for easy construction of property filters.
445+
412446
:param connection: The connection to use to connect with the backend.
413447
414448
.. versionadded:: 0.33.0
415449
416450
.. versionchanged:: 0.37.0
417451
Argument ``spatial_extent``: add support for passing a Shapely geometry or a local path to a GeoJSON file.
452+
453+
.. versionchanged:: 0.41.0
454+
Add support for :py:func:`~openeo.rest.graph_building.collection_property` based property filters
418455
"""
419-
arguments = {"url": url}
456+
arguments: dict = {"url": url}
420457
if spatial_extent:
421458
arguments["spatial_extent"] = _get_geometry_argument(
422459
argument=spatial_extent,
@@ -434,10 +471,11 @@ def load_stac(
434471
bands = cls._get_bands(bands, process_id="load_stac")
435472
if bands is not None:
436473
arguments["bands"] = bands
437-
if properties:
438-
arguments["properties"] = {
439-
prop: build_child_callback(pred, parent_parameters=["value"]) for prop, pred in properties.items()
440-
}
474+
475+
properties = cls._build_load_properties_argument(properties=properties)
476+
if properties is not None:
477+
arguments["properties"] = properties
478+
441479
graph = PGNode("load_stac", arguments=arguments)
442480
try:
443481
metadata = metadata_from_stac(url)

openeo/util.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@
2323
import shapely.geometry.base
2424
from deprecated import deprecated
2525

26+
from openeo.internal.warnings import legacy_alias
27+
2628
try:
2729
# pyproj is an optional dependency
2830
import pyproj
@@ -194,12 +196,15 @@ def today(self) -> str:
194196
"""Today (date) in RFC3339 format"""
195197
return self.date(dt.date.today())
196198

197-
def utcnow(self) -> str:
198-
"""Current UTC datetime in RFC3339 format."""
199+
def now_utc(self) -> str:
200+
"""Current datetime (in UTC timezone) in RFC3339 format."""
199201
# Current time in UTC timezone (instead of naive `datetime.datetime.utcnow()`, per `datetime` documentation)
200202
now = dt.datetime.now(tz=dt.timezone.utc)
201203
return self.datetime(now)
202204

205+
# Legacy alias
206+
utcnow = legacy_alias(now_utc, name="utcnow", since="0.41.0")
207+
203208

204209
# Default RFC3339 date-time formatter
205210
rfc3339 = Rfc3339()

0 commit comments

Comments
 (0)