Skip to content

Commit 7cb6475

Browse files
committed
Issue #668 support federation extension on job and service listings
1 parent 9870d80 commit 7cb6475

File tree

10 files changed

+384
-213
lines changed

10 files changed

+384
-213
lines changed

docs/api.rst

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,9 @@ openeo.rest.models
7979
.. automodule:: openeo.rest.models.federation_extension
8080
:members: FederationExtension
8181

82+
.. automodule:: openeo.rest.models.logs
83+
:members: LogEntry, normalize_log_level
84+
8285

8386
openeo.api.process
8487
--------------------
@@ -87,12 +90,6 @@ openeo.api.process
8790
:members: Parameter
8891

8992

90-
openeo.api.logs
91-
-----------------
92-
93-
.. automodule:: openeo.api.logs
94-
:members: LogEntry, normalize_log_level
95-
9693

9794
openeo.rest.connection
9895
----------------------

openeo/api/logs.py

Lines changed: 9 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -1,99 +1,13 @@
1-
import logging
2-
from typing import Optional, Union
1+
import warnings
32

3+
from openeo.internal.warnings import UserDeprecationWarning
4+
from openeo.rest.models.logs import LogEntry, log_level_name, normalize_log_level
45

5-
class LogEntry(dict):
6-
"""
7-
Log message and info for jobs and services
6+
warnings.warn(
7+
message="Submodule `openeo.api.logs` is deprecated in favor of `openeo.rest.models.logs`.",
8+
category=UserDeprecationWarning,
9+
stacklevel=2,
10+
)
811

9-
Fields:
10-
- ``id``: Unique ID for the log, string, REQUIRED
11-
- ``code``: Error code, string, optional
12-
- ``level``: Severity level, string (error, warning, info or debug), REQUIRED
13-
- ``message``: Error message, string, REQUIRED
14-
- ``time``: Date and time of the error event as RFC3339 date-time, string, available since API 1.1.0
15-
- ``path``: A "stack trace" for the process, array of dicts
16-
- ``links``: Related links, array of dicts
17-
- ``usage``: Usage metrics available as property 'usage', dict, available since API 1.1.0
18-
May contain the following metrics: cpu, memory, duration, network, disk, storage and other custom ones
19-
Each of the metrics is also a dict with the following parts: value (numeric) and unit (string)
20-
- ``data``: Arbitrary data the user wants to "log" for debugging purposes.
21-
Please note that this property may not exist as there's a difference
22-
between None and non-existing. None for example refers to no-data in
23-
many cases while the absence of the property means that the user did
24-
not provide any data for debugging.
25-
"""
2612

27-
_required = {"id", "level", "message"}
28-
29-
def __init__(self, *args, **kwargs):
30-
super().__init__(*args, **kwargs)
31-
32-
# Check required fields
33-
missing = self._required.difference(self.keys())
34-
if missing:
35-
raise ValueError("Missing required fields: {m}".format(m=sorted(missing)))
36-
37-
@property
38-
def id(self):
39-
return self["id"]
40-
41-
# Legacy alias
42-
log_id = id
43-
44-
@property
45-
def message(self):
46-
return self["message"]
47-
48-
@property
49-
def level(self):
50-
return self["level"]
51-
52-
# TODO: add properties for "code", "time", "path", "links" and "data" with sensible defaults?
53-
54-
55-
def normalize_log_level(
56-
log_level: Union[int, str, None], default: int = logging.DEBUG
57-
) -> int:
58-
"""
59-
Helper function to convert a openEO API log level (e.g. string "error")
60-
to the integer constants defined in Python's standard library ``logging`` module (e.g. ``logging.ERROR``).
61-
62-
:param log_level: log level to normalize: a log level string in the style of
63-
the openEO API ("error", "warning", "info", or "debug"),
64-
an integer value (e.g. a ``logging`` constant), or ``None``.
65-
66-
:param default: fallback log level to return on unknown log level strings or ``None`` input.
67-
68-
:raises TypeError: when log_level is any other type than str, an int or None.
69-
:return: One of the following log level constants from the standard module ``logging``:
70-
``logging.ERROR``, ``logging.WARNING``, ``logging.INFO``, or ``logging.DEBUG`` .
71-
"""
72-
if isinstance(log_level, str):
73-
log_level = log_level.upper()
74-
if log_level in ["CRITICAL", "ERROR", "FATAL"]:
75-
return logging.ERROR
76-
elif log_level in ["WARNING", "WARN"]:
77-
return logging.WARNING
78-
elif log_level == "INFO":
79-
return logging.INFO
80-
elif log_level == "DEBUG":
81-
return logging.DEBUG
82-
else:
83-
return default
84-
elif isinstance(log_level, int):
85-
return log_level
86-
elif log_level is None:
87-
return default
88-
else:
89-
raise TypeError(
90-
f"Value for log_level is not an int or str: type={type(log_level)}, value={log_level!r}"
91-
)
92-
93-
94-
def log_level_name(log_level: Union[int, str, None]) -> str:
95-
"""
96-
Get the name of a normalized log level.
97-
This value conforms to log level names used in the openEO API.
98-
"""
99-
return logging.getLevelName(normalize_log_level(log_level)).lower()
13+
__all__ = ["LogEntry", "normalize_log_level", "log_level_name"]

openeo/rest/job.py

Lines changed: 5 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010

1111
import requests
1212

13-
from openeo.api.logs import LogEntry, log_level_name, normalize_log_level
1413
from openeo.internal.documentation import openeo_endpoint
1514
from openeo.internal.jupyter import (
1615
VisualDict,
@@ -26,6 +25,8 @@
2625
OpenEoApiPlainError,
2726
OpenEoClientException,
2827
)
28+
from openeo.rest.models.general import LogsResponse
29+
from openeo.rest.models.logs import LogEntry, log_level_name, normalize_log_level
2930
from openeo.util import ensure_dir
3031

3132
if typing.TYPE_CHECKING:
@@ -184,9 +185,7 @@ def get_results(self) -> JobResults:
184185
"""
185186
return JobResults(job=self)
186187

187-
def logs(
188-
self, offset: Optional[str] = None, level: Optional[Union[str, int]] = None
189-
) -> List[LogEntry]:
188+
def logs(self, offset: Optional[str] = None, level: Union[str, int, None] = None) -> LogsResponse:
190189
"""Retrieve job logs.
191190
192191
:param offset: The last identifier (property ``id`` of a LogEntry) the client has received.
@@ -217,22 +216,8 @@ def logs(
217216
params["offset"] = offset
218217
if level is not None:
219218
params["level"] = log_level_name(level)
220-
response = self.connection.get(url, params=params, expected_status=200)
221-
logs = response.json()["logs"]
222-
223-
# Only filter logs when specified.
224-
# We should still support client-side log_level filtering because not all backends
225-
# support the minimum log level parameter.
226-
if level is not None:
227-
log_level = normalize_log_level(level)
228-
logs = (
229-
log
230-
for log in logs
231-
if normalize_log_level(log.get("level")) >= log_level
232-
)
233-
234-
entries = [LogEntry(log) for log in logs]
235-
return VisualList("logs", data=entries)
219+
response_data = self.connection.get(url, params=params, expected_status=200).json()
220+
return LogsResponse(response_data=response_data, log_level=level)
236221

237222
def run_synchronous(
238223
self,

openeo/rest/models/federation_extension.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,23 @@ def missing(self) -> Union[List[str], None]:
2020
Get the ``federation:missing`` property (if any) of the resource,
2121
which lists back-ends that were not available during the request.
2222
23+
Example usage with collection listing request
24+
(using :py:meth:`~openeo.rest.connection.Connection.list_collections()`):
25+
26+
.. code-block:: pycon
27+
28+
>>> collections = connection.list_collections()
29+
>>> collections.ext_federation.missing
30+
["backend1"]
31+
2332
:return: list of back-end IDs that were not available.
2433
Or None, when ``federation:missing`` is not present in response.
2534
"""
2635
return self._data.get("federation:missing", None)
2736

2837
def warn_on_missing(self, resource_name: str) -> None:
2938
"""
30-
Warn about presence of ``federation:missing`` in the resource.
39+
Warn about presence of non-empty ``federation:missing`` in the resource.
3140
"""
3241
missing = self.missing
3342
if missing:

openeo/rest/models/general.py

Lines changed: 87 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
from __future__ import annotations
22

3+
import functools
34
from dataclasses import dataclass
45
from typing import List, Optional, Union
56

67
from openeo.internal.jupyter import render_component
78
from openeo.rest.models.federation_extension import FederationExtension
9+
from openeo.rest.models.logs import LogEntry, normalize_log_level
810

911

1012
@dataclass(frozen=True)
@@ -34,12 +36,15 @@ class CollectionListingResponse(list):
3436
from a ``GET /collections`` request.
3537
3638
.. note::
37-
This object mimics a simple list of collection metadata dictionaries,
39+
This object mimics, for backward compatibility reasons,
40+
the interface of simple list of collection metadata dictionaries (``List[dict]``),
3841
which was the original return API of
3942
:py:meth:`~openeo.rest.connection.Connection.list_collections()`,
4043
but now also provides methods/properties to access additional response data.
4144
4245
:param response_data: response data from a ``GET /collections`` request
46+
:param warn_on_federation_missing: whether to automatically warn
47+
about missing federation components.
4348
4449
.. seealso:: :py:meth:`openeo.rest.connection.Connection.list_collections()`
4550
@@ -75,12 +80,14 @@ class ProcessListingResponse(list):
7580
from a ``GET /processes`` request.
7681
7782
.. note::
78-
This object mimics a simple list of collection metadata dictionaries,
79-
which was the original return API of
83+
This object mimics, for backward compatibility reasons,
84+
the interface of simple list of process metadata dictionaries (``List[dict]``),
8085
:py:meth:`~openeo.rest.connection.Connection.list_processes()`,
8186
but now also provides methods/properties to access additional response data.
8287
8388
:param response_data: response data from a ``GET /processes`` request
89+
:param warn_on_federation_missing: whether to automatically warn
90+
about missing federation components.
8491
8592
.. seealso:: :py:meth:`openeo.rest.connection.Connection.list_processes()`
8693
@@ -118,12 +125,15 @@ class JobListingResponse(list):
118125
from a ``GET /jobs`` request.
119126
120127
.. note::
121-
This object mimics a simple ``List[dict]`` with job metadata,
128+
This object mimics, for backward compatibility reasons,
129+
the interface of simple list of job metadata dictionaries (``List[dict]``),
122130
which was the original return API of
123131
:py:meth:`~openeo.rest.connection.Connection.list_jobs()`,
124132
but now also provides methods/properties to access additional response data.
125133
126134
:param response_data: response data from a ``GET /jobs`` request
135+
:param warn_on_federation_missing: whether to automatically warn
136+
about missing federation components.
127137
128138
.. seealso:: :py:meth:`openeo.rest.connection.Connection.list_jobs()`
129139
@@ -151,3 +161,76 @@ def links(self) -> List[Link]:
151161
def ext_federation(self) -> FederationExtension:
152162
"""Accessor for federation extension data related to this resource."""
153163
return FederationExtension(self._data)
164+
165+
166+
class LogsResponse(list):
167+
"""
168+
Container for job/service logs as received
169+
from a ``GET /jobs/{job_id}/logs`` or ``GET /services/{service_id}/logs`` request.
170+
171+
.. note::
172+
This object mimics, for backward compatibility reasons,
173+
the interface of a simple list (``List[LogEntry]``)
174+
which was the original return API of
175+
:py:meth:`~openeo.rest.job.BatchJob.logs()`
176+
and :py:meth:`~openeo.rest.service.Service.logs()`,
177+
but now also provides methods/properties to access additional response data.
178+
179+
:param response_data: response data from a ``GET /jobs/{job_id}/logs``
180+
or ``GET /services/{service_id}/logs`` request.
181+
:param warn_on_federation_missing: whether to automatically warn
182+
about missing federation components.
183+
184+
.. seealso:: :py:meth:`~openeo.rest.job.BatchJob.logs()`
185+
and :py:meth:`~openeo.rest.service.Service.logs()`
186+
187+
.. versionadded:: 0.38.0
188+
"""
189+
190+
__slots__ = ["_data"]
191+
192+
def __init__(
193+
self, response_data: dict, *, log_level: Optional[str] = None, warn_on_federation_missing: bool = True
194+
):
195+
self._data = response_data
196+
197+
logs = response_data.get("logs", [])
198+
# Extra client-side level filtering (in case the back-end does not support that)
199+
if log_level:
200+
201+
@functools.lru_cache
202+
def accept_level(level: str) -> bool:
203+
return normalize_log_level(level) >= normalize_log_level(log_level)
204+
205+
if (
206+
# Backend does not list effective lowest level
207+
"level" not in response_data
208+
# Or effective lowest level is still too low
209+
or not accept_level(response_data["level"])
210+
):
211+
logs = (log for log in logs if accept_level(log.get("level")))
212+
logs = [LogEntry(log) for log in logs]
213+
214+
# Mimic original list of process metadata dictionaries
215+
super().__init__(logs)
216+
217+
if warn_on_federation_missing:
218+
self.ext_federation.warn_on_missing(resource_name="log listing")
219+
220+
def _repr_html_(self):
221+
return render_component(component="logs", data=self)
222+
223+
@property
224+
def logs(self) -> List[LogEntry]:
225+
"""Get the log entries."""
226+
return self
227+
228+
@property
229+
def links(self) -> List[Link]:
230+
"""Get links related to this resource."""
231+
return [Link.from_dict(d) for d in self._data.get("links", [])]
232+
233+
@property
234+
def ext_federation(self) -> FederationExtension:
235+
"""Accessor for federation extension data related to this resource."""
236+
return FederationExtension(self._data)

0 commit comments

Comments
 (0)