Skip to content

Commit 1ec72d3

Browse files
committed
Issue #280 simplify BatchJob methods (start, stop, describe., ...)
Keep legacy aliases (without triggering warnings for now) Introduce decorator for openEO endpoint cross-referencing
1 parent e43fbfa commit 1ec72d3

File tree

8 files changed

+135
-57
lines changed

8 files changed

+135
-57
lines changed

CHANGELOG.md

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

1414
### Changed
1515

16+
- Simplified `BatchJob` methods `start()`, `stop()`, `describe()`, ...
17+
Legacy aliases `start_job()`, `describe_job()`, ... are still available and don't trigger a deprecation warning for now.
18+
([#280](https://github.com/Open-EO/openeo-python-client/issues/280))
19+
1620
### Removed
1721

1822
### Fixed

docs/batch_jobs.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -168,11 +168,11 @@ Run a batch job
168168
=================
169169
170170
Starting a batch job is pretty straightforward with the
171-
:py:meth:`~openeo.rest.job.BatchJob.start_job()` method:
171+
:py:meth:`~openeo.rest.job.BatchJob.start()` method:
172172
173173
.. code-block:: python
174174
175-
job.start_job()
175+
job.start()
176176
177177
If this didn't raise any errors or exceptions your job
178178
should now have started (status "running")

openeo/extra/job_management.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -336,7 +336,7 @@ def _launch_job(self, start_job, df, i, backend_name):
336336
if status == "created":
337337
# start job if not yet done by callback
338338
try:
339-
job.start_job()
339+
job.start()
340340
df.loc[i, "status"] = job.status()
341341
except OpenEoApiError as e:
342342
_log.error(e)
@@ -355,7 +355,7 @@ def on_job_done(self, job: BatchJob, row):
355355
"""
356356
# TODO: param `row` is never accessed in this method. Remove it? Is this intended for future use?
357357

358-
job_metadata = job.describe_job()
358+
job_metadata = job.describe()
359359
job_dir = self.get_job_dir(job.job_id)
360360
metadata_path = self.get_job_metadata_path(job.job_id)
361361

@@ -415,7 +415,7 @@ def _update_statuses(self, df: pd.DataFrame):
415415
try:
416416
con = self._get_connection(backend_name)
417417
the_job = con.job(job_id)
418-
job_metadata = the_job.describe_job()
418+
job_metadata = the_job.describe()
419419
_log.info(
420420
f"Status of job {job_id!r} (on backend {backend_name}) is {job_metadata['status']!r}"
421421
)

openeo/internal/documentation.py

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,16 @@
22
Utilities to build/automate/extend documentation
33
"""
44
import collections
5+
import inspect
56
import textwrap
67
from functools import partial
7-
from typing import Callable, Optional
8+
from typing import Callable, Optional, Tuple
89

910
# TODO: give this a proper public API?
1011
_process_registry = collections.defaultdict(list)
1112

1213

13-
def openeo_process(f: Callable = None, process_id: Optional[str] = None, mode: Optional[str] = None):
14+
def openeo_process(f: Optional[Callable] = None, process_id: Optional[str] = None, mode: Optional[str] = None):
1415
"""
1516
Decorator for function or method to associate it with a standard openEO process
1617
@@ -34,3 +35,22 @@ def openeo_process(f: Callable = None, process_id: Optional[str] = None, mode: O
3435

3536
_process_registry[process_id].append((f, mode))
3637
return f
38+
39+
40+
def openeo_endpoint(endpoint: str) -> Callable[[Callable], Callable]:
41+
"""
42+
Parameterized decorator to annotate given function or method with the openEO endpoint it interacts with
43+
44+
:param endpoint: REST endpoint (e.g. "GET /jobs", "POST /result", ...)
45+
:return:
46+
"""
47+
# TODO: automatically parse/normalize endpoint (to method+path)
48+
# TODO: wrap this in some markup/directive to make this more a "small print" note.
49+
50+
def decorate(f: Callable) -> Callable:
51+
is_method = list(inspect.signature(f).parameters.keys())[:1] == ["self"]
52+
seealso = f"This {'method' if is_method else 'function'} uses openEO endpoint ``{endpoint}``"
53+
f.__doc__ = textwrap.dedent(f.__doc__ or "") + "\n\n" + seealso + "\n"
54+
return f
55+
56+
return decorate

openeo/internal/warnings.py

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -34,15 +34,19 @@ def test_warnings(stacklevel=1):
3434
)
3535

3636

37-
def legacy_alias(orig: Callable, name: str, since: str):
37+
def legacy_alias(orig: Callable, name: str = "n/a", *, since: str, mode: str = "full"):
3838
"""
3939
Create legacy alias of given function/method/classmethod/staticmethod
4040
4141
:param orig: function/method to create legacy alias for
42-
:param name: name of the alias
42+
:param name: name of the alias (unused)
4343
:param since: version since when this is alias is deprecated
44+
:param mode:
45+
- "full": raise warnings on calling, only have deprecation note as doc
46+
- "soft": don't raise warning on calling, just add deprecation note to doc
4447
:return:
4548
"""
49+
# TODO: drop `name` argument?
4650
post_process = None
4751
if isinstance(orig, classmethod):
4852
post_process = classmethod
@@ -64,13 +68,18 @@ def legacy_alias(orig: Callable, name: str, since: str):
6468
def wrapper(*args, **kwargs):
6569
return orig(*args, **kwargs)
6670

67-
# Drop original doc block, just show deprecation note.
68-
wrapper.__doc__ = ""
6971
ref = f":py:{'meth' if 'method' in kind else 'func'}:`.{orig.__name__}`"
70-
wrapper = deprecated(
71-
reason=f"Use of this legacy {kind} is deprecated, use {ref} instead.",
72-
version=since,
73-
)(wrapper)
72+
message = f"Usage of this legacy {kind} is deprecated. Use {ref} instead."
73+
74+
if mode == "full":
75+
# Drop original doc block, just show deprecation note.
76+
wrapper.__doc__ = ""
77+
wrapper = deprecated(reason=message, version=since)(wrapper)
78+
elif mode == "soft":
79+
# Only keep first paragraph of original doc block
80+
wrapper.__doc__ = "\n\n".join(orig.__doc__.split("\n\n")[:1] + [f".. deprecated:: {since}\n {message}\n"])
81+
else:
82+
raise ValueError(mode)
7483

7584
if post_process:
7685
wrapper = post_process(wrapper)

openeo/rest/job.py

Lines changed: 55 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,9 @@
99
import requests
1010

1111
from openeo.api.logs import LogEntry, normalize_log_level, log_level_name
12+
from openeo.internal.documentation import openeo_endpoint
1213
from openeo.internal.jupyter import render_component, render_error, VisualDict, VisualList
13-
from openeo.internal.warnings import deprecated
14+
from openeo.internal.warnings import deprecated, legacy_alias
1415
from openeo.rest import OpenEoClientException, JobFailedException, OpenEoApiError
1516
from openeo.util import ensure_dir
1617

@@ -44,60 +45,81 @@ def __repr__(self):
4445
return '<{c} job_id={i!r}>'.format(c=self.__class__.__name__, i=self.job_id)
4546

4647
def _repr_html_(self):
47-
data = self.describe_job()
48+
data = self.describe()
4849
currency = self.connection.capabilities().currency()
4950
return render_component('job', data=data, parameters={'currency': currency})
5051

51-
def describe_job(self) -> dict:
52-
""" Get all job information."""
53-
# GET /jobs/{job_id}
54-
# TODO: rename to just `describe`? #280
52+
@openeo_endpoint("GET /jobs/{job_id}")
53+
def describe(self) -> dict:
54+
"""
55+
Get detailed metadata about a submitted batch job
56+
(title, process graph, status, progress, ...).
57+
58+
.. versionadded:: 0.20.0
59+
This method was previously called :py:meth:`describe_job`.
60+
"""
5561
return self.connection.get(f"/jobs/{self.job_id}", expected_status=200).json()
5662

63+
describe_job = legacy_alias(describe, since="0.20.0", mode="soft")
64+
5765
def status(self) -> str:
5866
"""
5967
Get the status of the batch job
6068
6169
:return: batch job status, one of "created", "queued", "running", "canceled", "finished" or "error".
6270
"""
63-
return self.describe_job().get("status", "N/A")
64-
65-
def update_job(self, process_graph=None, output_format=None,
66-
output_parameters=None, title=None, description=None,
67-
plan=None, budget=None, additional=None):
68-
""" Update a job."""
69-
# PATCH /jobs/{job_id}
70-
# TODO: rename to just `update`? #280
71-
raise NotImplementedError
72-
73-
def delete_job(self):
74-
""" Delete a job."""
75-
# DELETE /jobs/{job_id}
76-
# TODO: rename to just `delete`? #280
71+
return self.describe().get("status", "N/A")
72+
73+
@openeo_endpoint("DELETE /jobs/{job_id}")
74+
def delete(self):
75+
"""
76+
Delete this batch job.
77+
78+
.. versionadded:: 0.20.0
79+
This method was previously called :py:meth:`delete_job`.
80+
"""
7781
self.connection.delete(f"/jobs/{self.job_id}", expected_status=204)
7882

79-
def estimate_job(self):
83+
delete_job = legacy_alias(delete, since="0.20.0", mode="soft")
84+
85+
@openeo_endpoint("GET /jobs/{job_id}/estimate")
86+
def estimate(self):
8087
"""Calculate time/cost estimate for a job."""
81-
# GET /jobs/{job_id}/estimate
8288
data = self.connection.get(
8389
f"/jobs/{self.job_id}/estimate", expected_status=200
8490
).json()
8591
currency = self.connection.capabilities().currency()
8692
return VisualDict('job-estimate', data=data, parameters={'currency': currency})
8793

88-
def start_job(self):
89-
""" Start / queue a job for processing."""
90-
# POST /jobs/{job_id}/results
91-
# TODO: rename to just `start`? #280
92-
# TODO: return self, to allow chained calls
94+
estimate_job = legacy_alias(estimate, since="0.20.0", mode="soft")
95+
96+
@openeo_endpoint("POST /jobs/{job_id}/results")
97+
def start(self) -> "BatchJob":
98+
"""
99+
Start this batch job.
100+
101+
:return: Started batch job
102+
103+
.. versionadded:: 0.20.0
104+
This method was previously called :py:meth:`start_job`.
105+
"""
93106
self.connection.post(f"/jobs/{self.job_id}/results", expected_status=202)
107+
return self
94108

95-
def stop_job(self):
96-
""" Stop / cancel job processing."""
97-
# DELETE /jobs/{job_id}/results
98-
# TODO: rename to just `stop`? #280
109+
start_job = legacy_alias(start, since="0.20.0", mode="soft")
110+
111+
@openeo_endpoint("DELETE /jobs/{job_id}/results")
112+
def stop(self):
113+
"""
114+
Stop this batch job.
115+
116+
.. versionadded:: 0.20.0
117+
This method was previously called :py:meth:`stop_job`.
118+
"""
99119
self.connection.delete(f"/jobs/{self.job_id}/results", expected_status=204)
100120

121+
stop_job = legacy_alias(stop, since="0.20.0", mode="soft")
122+
101123
def get_results_metadata_url(self, *, full: bool = False) -> str:
102124
"""Get results metadata URL"""
103125
url = f"/jobs/{self.job_id}/results"
@@ -232,7 +254,7 @@ def print_status(msg: str):
232254

233255
# TODO: make `max_poll_interval`, `connection_retry_interval` class constants or instance properties?
234256
print_status("send 'start'")
235-
self.start_job()
257+
self.start()
236258

237259
# TODO: also add `wait` method so you can track a job that already has started explicitly
238260
# or just rename this method to `wait` and automatically do start if not started yet?
@@ -254,7 +276,7 @@ def soft_error(message: str):
254276
while True:
255277
# TODO: also allow a hard time limit on this infinite poll loop?
256278
try:
257-
job_info = self.describe_job()
279+
job_info = self.describe()
258280
except requests.ConnectionError as e:
259281
soft_error("Connection error while polling job status: {e}".format(e=e))
260282
continue

tests/internal/test_warnings.py

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ def add(x, y):
3030
assert do_plus.__doc__ == (
3131
"\n"
3232
".. deprecated:: v1.2\n"
33-
" Use of this legacy function is deprecated, use :py:func:`.add`\n"
33+
" Usage of this legacy function is deprecated. Use :py:func:`.add`\n"
3434
" instead.\n"
3535
)
3636

@@ -41,7 +41,7 @@ def add(x, y):
4141
UserDeprecationWarning,
4242
match=re.escape(
4343
"Call to deprecated function (or staticmethod) add."
44-
" (Use of this legacy function is deprecated, use `.add` instead.)"
44+
" (Usage of this legacy function is deprecated. Use `.add` instead.)"
4545
" -- Deprecated since version v1.2."
4646
),
4747
):
@@ -61,7 +61,7 @@ def add(self, x, y):
6161
assert Foo.do_plus.__doc__ == (
6262
"\n"
6363
".. deprecated:: v1.2\n"
64-
" Use of this legacy method is deprecated, use :py:meth:`.add`\n"
64+
" Usage of this legacy method is deprecated. Use :py:meth:`.add`\n"
6565
" instead.\n"
6666
)
6767

@@ -72,7 +72,7 @@ def add(self, x, y):
7272
UserDeprecationWarning,
7373
match=re.escape(
7474
"Call to deprecated method add."
75-
" (Use of this legacy method is deprecated, use `.add` instead.)"
75+
" (Usage of this legacy method is deprecated. Use `.add` instead.)"
7676
" -- Deprecated since version v1.2."
7777
),
7878
):
@@ -94,7 +94,7 @@ def add(cls, x, y):
9494
assert Foo.do_plus.__doc__ == (
9595
"\n"
9696
".. deprecated:: v1.2\n"
97-
" Use of this legacy class method is deprecated, use\n"
97+
" Usage of this legacy class method is deprecated. Use\n"
9898
" :py:meth:`.add` instead.\n"
9999
)
100100

@@ -104,7 +104,7 @@ def add(cls, x, y):
104104
expected_warning = re.escape(
105105
# Workaround for bug in classmethod detection before Python 3.9 (see https://wrapt.readthedocs.io/en/latest/decorators.html#decorating-class-methods
106106
f"Call to deprecated {'class method' if sys.version_info >= (3, 9) else 'function (or staticmethod)'} add."
107-
" (Use of this legacy class method is deprecated, use `.add` instead.)"
107+
" (Usage of this legacy class method is deprecated. Use `.add` instead.)"
108108
" -- Deprecated since version v1.2."
109109
)
110110

@@ -130,7 +130,7 @@ def add(x, y):
130130
assert Foo.do_plus.__doc__ == (
131131
"\n"
132132
".. deprecated:: v1.2\n"
133-
" Use of this legacy static method is deprecated, use\n"
133+
" Usage of this legacy static method is deprecated. Use\n"
134134
" :py:meth:`.add` instead.\n"
135135
)
136136

@@ -139,7 +139,7 @@ def add(x, y):
139139

140140
expected_warning = re.escape(
141141
"Call to deprecated function (or staticmethod) add."
142-
" (Use of this legacy static method is deprecated, use `.add` instead.)"
142+
" (Usage of this legacy static method is deprecated. Use `.add` instead.)"
143143
" -- Deprecated since version v1.2."
144144
)
145145
with pytest.warns(UserDeprecationWarning, match=expected_warning):
@@ -151,6 +151,29 @@ def add(x, y):
151151
assert res == 5
152152

153153

154+
def test_legacy_alias_method_soft(recwarn):
155+
class Foo:
156+
def add(self, x, y):
157+
"""Add x and y."""
158+
return x + y
159+
160+
do_plus = legacy_alias(add, since="v1.2", mode="soft")
161+
162+
assert Foo.add.__doc__ == "Add x and y."
163+
assert Foo.do_plus.__doc__ == (
164+
"Add x and y.\n"
165+
"\n"
166+
".. deprecated:: v1.2\n"
167+
" Usage of this legacy method is deprecated. Use :py:meth:`.add` instead.\n"
168+
)
169+
170+
assert Foo().add(2, 3) == 5
171+
assert len(recwarn) == 0
172+
173+
res = Foo().do_plus(2, 3)
174+
assert len(recwarn) == 0
175+
assert res == 5
176+
154177

155178
def test_deprecated_decorator():
156179
class Foo:

tests/rest/datacube/test_datacube100.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2555,7 +2555,7 @@ def test_legacy_send_job(self, con100, requests_mock):
25552555
"""Legacy `DataCube.send_job` alis for `create_job"""
25562556
requests_mock.post(API_URL + "/jobs", json=self._get_handler_post_jobs())
25572557
cube = con100.load_collection("S2")
2558-
expected_warning = "Call to deprecated method create_job. (Use of this legacy method is deprecated, use `.create_job` instead.) -- Deprecated since version 0.10.0."
2558+
expected_warning = "Call to deprecated method create_job. (Usage of this legacy method is deprecated. Use `.create_job` instead.) -- Deprecated since version 0.10.0."
25592559
with pytest.warns(UserDeprecationWarning, match=re.escape(expected_warning)):
25602560
job = cube.send_job(out_format="GTiff")
25612561
assert job.job_id == "myj0b1"

0 commit comments

Comments
 (0)