Skip to content

Commit ec902ac

Browse files
committed
Merge branch 'issue195-processesv2'
2 parents 8488216 + d8fca31 commit ec902ac

14 files changed

+528
-346
lines changed

.gitmodules

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
11
[submodule "openeo_driver/specs/openeo-processes/1.0"]
22
path = openeo_driver/specs/openeo-processes/1.x
33
url = https://github.com/Open-EO/openeo-processes.git
4-
[submodule "openeo_driver/specs/openeo-processes/0.4"]
5-
path = openeo_driver/specs/openeo-processes/0.4
6-
url = https://github.com/Open-EO/openeo-processes.git
74
[submodule "openeo_driver/specs/openeo-api/1.0"]
85
path = openeo_driver/specs/openeo-api/1.x
96
url = https://github.com/Open-EO/openeo-api.git

CHANGELOG.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# Changelog
2+
3+
All notable changes to this project will be documented in this file.
4+
5+
This project does not have a real release cycle (yet).
6+
Upstream projects usually depend on development snapshots of this project.
7+
Still, to have some kind of indicator of small versus big change,
8+
we try to bump the version number (in `openeo_driver/_version.py`)
9+
roughly according to [Semantic Versioning](https://semver.org/).
10+
11+
When adding a feature/bugfix without bumping the version number:
12+
just describe it under the "In progress" section.
13+
When bumping the version number in `openeo_driver/_version.py`
14+
(possibly accompanying a feature/bugfix):
15+
"close" the "In Progress" section by changing its title to the new version number
16+
(and describe accompanying changes, if any, under it too)
17+
and start a new "In Progress" section above it.
18+
19+
20+
## In progress
21+
22+
23+
24+
## 0.70.0
25+
26+
- Initial support for openeo-processes v2.0, when requesting version 1.2 of the openEO API ([#195](https://github.com/Open-EO/openeo-python-driver/issues/195))
27+
- Drop support for 0.4 version of openeo-processes ([#47](https://github.com/Open-EO/openeo-python-driver/issues/47))
28+
29+
30+
## 0.69.1
31+
32+
- Add backoff to ensure EJR deletion ([#163](https://github.com/Open-EO/openeo-python-driver/issues/163))
33+
34+
35+
## 0.69.0
36+
37+
- Support job deletion in EJR ([#163](https://github.com/Open-EO/openeo-python-driver/issues/163))

openeo_driver/ProcessGraphDeserializer.py

Lines changed: 139 additions & 94 deletions
Large diffs are not rendered by default.

openeo_driver/_version.py

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

openeo_driver/processes.py

Lines changed: 36 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ def returns(self, description: str, schema: dict) -> 'ProcessSpec':
5757

5858
def to_dict_040(self) -> dict:
5959
"""Generate process spec as (JSON-able) dictionary (API 0.4.0 style)."""
60+
# TODO #47 drop this
6061
if len(self._parameters) == 0:
6162
warnings.warn("Process with no parameters")
6263
assert self._returns is not None
@@ -142,7 +143,8 @@ def add_process(self, name: str, function: Callable = None, spec: dict = None, n
142143
if self.contains(name, namespace):
143144
raise ProcessRegistryException(f"Process {name!r} already defined in namespace {namespace!r}")
144145
if spec:
145-
assert name == spec['id']
146+
if name != spec["id"]:
147+
raise ProcessRegistryException(f"Process {name!r} has unexpected id {spec['id']!r}")
146148
if function and self._argument_names:
147149
sig = inspect.signature(function)
148150
arg_names = [n for n, p in sig.parameters.items() if p.kind == inspect.Parameter.POSITIONAL_OR_KEYWORD]
@@ -188,7 +190,7 @@ def add_simple_function(
188190
self, f: Optional[Callable] = None, name: Optional[str] = None, spec: Optional[dict] = None
189191
):
190192
"""
191-
Register a simple function that uses normal arguments instead of `args: dict, env: EvalEnv`:
193+
Register a simple function that uses normal arguments instead of `args: ProcessArgs, env: EvalEnv`:
192194
wrap it in a wrapper that automatically extracts these arguments
193195
:param f:
194196
:param name: process_id (when guessing from `f.__name__` doesn't work)
@@ -265,8 +267,9 @@ def get_function(self, name: str, namespace: str = DEFAULT_NAMESPACE) -> Callabl
265267
return self._get(name, namespace).function
266268

267269

268-
# Type annotation for an argument value
270+
# Type annotation aliases
269271
ArgumentValue = Any
272+
Validator = Callable[[Any], bool]
270273

271274

272275
class ProcessArgs(dict):
@@ -294,7 +297,7 @@ def get_required(
294297
name: str,
295298
*,
296299
expected_type: Optional[Union[type, Tuple[type, ...]]] = None,
297-
validator: Optional[Callable[[Any], bool]] = None,
300+
validator: Optional[Validator] = None,
298301
) -> ArgumentValue:
299302
"""
300303
Get a required argument by name.
@@ -315,33 +318,35 @@ def _check_value(
315318
name: str,
316319
value: Any,
317320
expected_type: Optional[Union[type, Tuple[type, ...]]] = None,
318-
validator: Optional[Callable[[Any], bool]] = None,
321+
validator: Optional[Validator] = None,
319322
):
320323
if expected_type:
321324
if not isinstance(value, expected_type):
322325
raise ProcessParameterInvalidException(
323326
parameter=name, process=self.process_id, reason=f"Expected {expected_type} but got {type(value)}."
324327
)
325328
if validator:
329+
reason = None
326330
try:
327331
valid = validator(value)
328-
reason = "Failed validation."
329332
except OpenEOApiException:
330333
# Preserve original OpenEOApiException
331334
raise
332335
except Exception as e:
333336
valid = False
334337
reason = str(e)
335338
if not valid:
336-
raise ProcessParameterInvalidException(parameter=name, process=self.process_id, reason=reason)
339+
raise ProcessParameterInvalidException(
340+
parameter=name, process=self.process_id, reason=reason or "Failed validation."
341+
)
337342

338343
def get_optional(
339344
self,
340345
name: str,
341346
default: Union[Any, Callable[[], Any]] = None,
342347
*,
343348
expected_type: Optional[Union[type, Tuple[type, ...]]] = None,
344-
validator: Optional[Callable[[Any], bool]] = None,
349+
validator: Optional[Validator] = None,
345350
) -> ArgumentValue:
346351
"""
347352
Get an optional argument with default
@@ -364,7 +369,7 @@ def get_deep(
364369
self,
365370
*steps: str,
366371
expected_type: Optional[Union[type, Tuple[type, ...]]] = None,
367-
validator: Optional[Callable[[Any], bool]] = None,
372+
validator: Optional[Validator] = None,
368373
) -> ArgumentValue:
369374
"""
370375
Walk recursively through a dictionary to get to a value.
@@ -431,10 +436,26 @@ def get_enum(self, name: str, options: Collection[ArgumentValue]) -> ArgumentVal
431436
return value
432437

433438
@staticmethod
434-
def validator_one_of(options: list, show_value: bool = True):
435-
"""Build a validator function that check that the value is in given list"""
439+
def validator_generic(condition: Callable[[Any], bool], error_message: str) -> Validator:
440+
"""
441+
Build validator function based on a condition (another validator)
442+
and a custom error message when validation returns False.
443+
(supports interpolation of actual value with "{actual}").
444+
"""
436445

437446
def validator(value):
447+
valid = condition(value)
448+
if not valid:
449+
raise ValueError(error_message.format(actual=value))
450+
return valid
451+
452+
return validator
453+
454+
@staticmethod
455+
def validator_one_of(options: list, show_value: bool = True) -> Validator:
456+
"""Build a validator function that check that the value is in given list"""
457+
458+
def validator(value) -> bool:
438459
if value not in options:
439460
if show_value:
440461
message = f"Must be one of {options!r} but got {value!r}."
@@ -446,7 +467,7 @@ def validator(value):
446467
return validator
447468

448469
@staticmethod
449-
def validator_file_format(formats: Union[List[str], Dict[str, dict]]):
470+
def validator_file_format(formats: Union[List[str], Dict[str, dict]]) -> Validator:
450471
"""
451472
Build validator for input/output format (case-insensitive check)
452473
@@ -455,7 +476,7 @@ def validator_file_format(formats: Union[List[str], Dict[str, dict]]):
455476
formats = list(formats)
456477
options = set(f.lower() for f in formats)
457478

458-
def validator(value: str):
479+
def validator(value: str) -> bool:
459480
if value.lower() not in options:
460481
raise OpenEOApiException(
461482
message=f"Invalid file format {value!r}. Allowed formats: {', '.join(formats)}",
@@ -469,10 +490,10 @@ def validator(value: str):
469490
@staticmethod
470491
def validator_geojson_dict(
471492
allowed_types: Optional[Collection[str]] = None,
472-
):
493+
) -> Validator:
473494
"""Build validator to verify that provided structure looks like a GeoJSON-style object"""
474495

475-
def validator(value):
496+
def validator(value) -> bool:
476497
issues = validate_geojson_basic(value=value, allowed_types=allowed_types, raise_exception=False)
477498
if issues:
478499
raise ValueError(f"Invalid GeoJSON: {', '.join(issues)}.")

openeo_driver/save_result.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -556,7 +556,6 @@ def stats(band):
556556

557557
def to_csv(self, destination=None):
558558
csv_paths = glob.glob(self._csv_dir + "/*.csv")
559-
print(csv_paths)
560559
# TODO: assumption there is only one CSV?
561560
if(destination == None):
562561
return csv_paths[0]
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1+
0.4
12
1.0
Lines changed: 0 additions & 1 deletion
This file was deleted.

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@
4646
"flask",
4747
"werkzeug>=1.0.1,<2.3.0", # https://github.com/Open-EO/openeo-python-driver/issues/187
4848
"requests>=2.28.0",
49-
"openeo>=0.15.0",
49+
"openeo>=0.24.0.a2.dev",
5050
"openeo_processes==0.0.4", # 0.0.4 is special build/release, also see https://github.com/Open-EO/openeo-python-driver/issues/152
5151
"gunicorn>=20.0.1",
5252
"numpy>=1.22.0",

tests/data/pg/1.0/apply_polygon.json

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
{
2+
"collection": {
3+
"process_id": "load_collection",
4+
"arguments": {"id": "S2_FOOBAR"}
5+
},
6+
"apply": {
7+
"process_id": "apply_polygon",
8+
"arguments": {
9+
"data": {"from_node": "collection"},
10+
"chunks": {
11+
"type": "FeatureCollection",
12+
"features": [
13+
{
14+
"type": "Feature",
15+
"properties": {},
16+
"geometry": {
17+
"type": "Polygon",
18+
"coordinates": [[[1, 5], [2, 5], [2, 6], [1, 6], [1, 5]]]
19+
}
20+
},
21+
{
22+
"type": "Feature",
23+
"properties": {},
24+
"geometry": {
25+
"type": "Polygon",
26+
"coordinates": [
27+
[[10, 15], [12, 15], [12, 16], [10, 16], [10, 15]]
28+
]
29+
}
30+
}
31+
]
32+
},
33+
"mask_value": -5,
34+
"process": {
35+
"process_graph": {
36+
"runudf1": {
37+
"process_id": "run_udf",
38+
"arguments": {
39+
"data": {"from_parameter": "data"},
40+
"udf": "print('hello world')",
41+
"runtime": "Python",
42+
"context": {"param": {"from_parameter": "udfparam"}}
43+
},
44+
"result": true
45+
}
46+
}
47+
}
48+
},
49+
"result": true
50+
}
51+
}

tests/test_ProcessGraphDeserializer.py

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313

1414
def test_period_to_intervals():
1515
weekly_intervals = _period_to_intervals("2021-06-08", "2021-06-24", "week")
16-
print(list(weekly_intervals))
1716
weekly_intervals = [(i[0].isoformat(), i[1].isoformat()) for i in weekly_intervals]
1817
assert 3 == len(weekly_intervals)
1918
assert weekly_intervals[0] == ('2021-06-06T00:00:00', '2021-06-13T00:00:00')
@@ -59,7 +58,6 @@ def test_period_to_intervals_monthly_tz():
5958

6059
def test_period_to_intervals_yearly():
6160
intervals = _period_to_intervals("2018-06-08", "2021-08-24", "year")
62-
print(list(intervals))
6361
intervals = [(i[0].isoformat(), i[1].isoformat()) for i in intervals]
6462
assert 4 == len(intervals)
6563
assert intervals[0] == ('2018-01-01T00:00:00', '2019-01-01T00:00:00')
@@ -70,7 +68,6 @@ def test_period_to_intervals_yearly():
7068

7169
def test_period_to_intervals_monthly_full_year():
7270
intervals = _period_to_intervals("2020-01-01", "2021-01-01", "month")
73-
print(list(intervals))
7471
intervals = [(i[0].isoformat(), i[1].isoformat()) for i in intervals]
7572
assert 12 == len(intervals)
7673
assert intervals[0] == ('2020-01-01T00:00:00', '2020-02-01T00:00:00')
@@ -81,7 +78,6 @@ def test_period_to_intervals_monthly_full_year():
8178

8279
def test_period_to_intervals_daily():
8380
intervals = _period_to_intervals("2021-06-08", "2021-06-11", "day")
84-
print(list(intervals))
8581
intervals = [(i[0].isoformat(), i[1].isoformat()) for i in intervals]
8682
assert 4 == len(intervals)
8783
assert intervals[0] == ('2021-06-07T00:00:00', '2021-06-08T00:00:00')
@@ -92,7 +88,6 @@ def test_period_to_intervals_daily():
9288

9389
def test_period_to_intervals_dekad():
9490
intervals = _period_to_intervals("2021-06-08", "2021-07-20", "dekad")
95-
print(list(intervals))
9691
intervals = [(i[0].isoformat(), i[1].isoformat()) for i in intervals]
9792
assert 5 == len(intervals)
9893
assert intervals[0] == ('2021-06-01T00:00:00', '2021-06-11T00:00:00')
@@ -104,7 +99,6 @@ def test_period_to_intervals_dekad():
10499

105100
def test_period_to_intervals_dekad_first_of_month():
106101
intervals = _period_to_intervals("2021-06-01", "2021-07-20", "dekad")
107-
print(list(intervals))
108102
intervals = [(i[0].isoformat(), i[1].isoformat()) for i in intervals]
109103
assert 5 == len(intervals)
110104
assert intervals[0] == ('2021-06-01T00:00:00', '2021-06-11T00:00:00')

tests/test_processes.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -615,6 +615,43 @@ def test_get_enum(self):
615615
):
616616
_ = args.get_enum("color", options=["R", "G", "B"])
617617

618+
def test_validator_generic(self):
619+
args = ProcessArgs({"size": 11}, process_id="wibble")
620+
621+
validator = ProcessArgs.validator_generic(lambda v: v > 1, error_message="Should be stricly positive.")
622+
value = args.get_required("size", expected_type=int, validator=validator)
623+
assert value == 11
624+
625+
validator = ProcessArgs.validator_generic(lambda v: v % 2 == 0, error_message="Should be even.")
626+
with pytest.raises(
627+
ProcessParameterInvalidException,
628+
match=re.escape("The value passed for parameter 'size' in process 'wibble' is invalid: Should be even."),
629+
):
630+
_ = args.get_required("size", expected_type=int, validator=validator)
631+
632+
validator = ProcessArgs.validator_generic(
633+
lambda v: v % 2 == 0, error_message="Should be even but got {actual}."
634+
)
635+
with pytest.raises(
636+
ProcessParameterInvalidException,
637+
match=re.escape(
638+
"The value passed for parameter 'size' in process 'wibble' is invalid: Should be even but got 11."
639+
),
640+
):
641+
_ = args.get_required("size", expected_type=int, validator=validator)
642+
643+
def test_validator_one_of(self):
644+
args = ProcessArgs({"color": "red", "size": 5}, process_id="wibble")
645+
with pytest.raises(
646+
ProcessParameterInvalidException,
647+
match=re.escape(
648+
"The value passed for parameter 'color' in process 'wibble' is invalid: Must be one of ['yellow', 'violet'] but got 'red'."
649+
),
650+
):
651+
_ = args.get_required(
652+
"color", expected_type=str, validator=ProcessArgs.validator_one_of(["yellow", "violet"])
653+
)
654+
618655
def test_validator_geojson_dict(self):
619656
polygon = {"type": "Polygon", "coordinates": [[1, 2]]}
620657
args = ProcessArgs({"geometry": polygon, "color": "red"}, process_id="wibble")

tests/test_save_result_netcdf.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,9 @@ def test_aggregate_polygon_result_basic(tmp_path):
3838
assert ["red", "green", "blue"] == [b['name'] for b in theAsset['bands']]
3939

4040
timeseries_ds = xr.open_dataset(filename)
41-
print(timeseries_ds)
4241
assert_array_equal(timeseries_ds.red.coords['t'].data, np.asarray([ np.datetime64('2019-10-15T08:15:45'),np.datetime64('2019-11-11T01:11:11')]))
4342
timeseries_ds.red.sel(feature=1)
4443
timeseries_ds.red.sel( t='2019-10-16')
45-
print(timeseries_ds)
4644
assert_array_equal(
4745
4, timeseries_ds.red.sel(feature=1).sel(t="2019-10-15T08:15:45").data
4846
)
@@ -146,7 +144,6 @@ def test_aggregate_polygon_result_CSV(tmp_path):
146144
assert 100.0 == theAsset['raster:bands'][0]["statistics"]['valid_percent']
147145

148146
timeseries_ds = xr.open_dataset(filename)
149-
print(timeseries_ds)
150147

151148
assert_array_equal(timeseries_ds.red.coords['t'].data, np.asarray([ np.datetime64('2017-09-05T00:00:00'),np.datetime64('2017-09-06T00:00:00')]))
152149

0 commit comments

Comments
 (0)