Skip to content

Commit eb40735

Browse files
committed
Merge branch 'issue668-federation-extension'
2 parents b62f7fa + 632ad1c commit eb40735

24 files changed

+1124
-294
lines changed

CHANGELOG.md

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

1010
### Added
1111

12+
- Add initial support for accessing [Federation Extension](https://github.com/Open-EO/openeo-api/tree/master/extensions/federation) related metadata ([#668](https://github.com/Open-EO/openeo-python-client/issues/668))
13+
1214
### Changed
1315

1416
- Improved tracking of metadata changes with `resample_spatial` and `resample_cube_spatial` ([#690](https://github.com/Open-EO/openeo-python-client/issues/690))

docs/api.rst

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -70,19 +70,23 @@ openeo.rest.capabilities
7070
:members: OpenEoCapabilities
7171

7272

73+
openeo.rest.models
74+
-------------------
75+
76+
.. automodule:: openeo.rest.models.general
77+
:members:
78+
79+
.. automodule:: openeo.rest.models.logs
80+
:members: LogEntry, normalize_log_level
81+
82+
7383
openeo.api.process
7484
--------------------
7585

7686
.. automodule:: openeo.api.process
7787
:members: Parameter
7888

7989

80-
openeo.api.logs
81-
-----------------
82-
83-
.. automodule:: openeo.api.logs
84-
:members: LogEntry, normalize_log_level
85-
8690

8791
openeo.rest.connection
8892
----------------------

docs/federation-extension.rst

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
2+
.. _federation-extension:
3+
4+
===========================
5+
openEO Federation Extension
6+
===========================
7+
8+
9+
The `openEO Federation extension <https://github.com/Open-EO/openeo-api/tree/master/extensions/federation>`_
10+
is a set of additional specifications,
11+
on top of the standard openEO API specification,
12+
to address the need for extra metadata in the context
13+
of federated openEO processing,
14+
where multiple (separately operated) openEO services are bundled together
15+
behind a single API endpoint.
16+
17+
18+
Accessing federation extension metadata
19+
========================================
20+
21+
The openEO Python client library provides access to this additional metadata
22+
in a couple of resources.
23+
24+
.. versionadded:: 0.38.0
25+
initial support to access federation extension related metadata.
26+
27+
.. warning:: this API is experimental and subject to change.
28+
29+
30+
Backend details
31+
---------------
32+
33+
Participating backends in a federation are listed under the ``federation`` field
34+
of the capabilities document (``GET /``) and can be inspected
35+
using :py:meth:`OpenEoCapabilities.ext_federation_backend_details() <openeo.rest.capabilities.OpenEoCapabilities.ext_federation_backend_details>`:
36+
37+
.. code-block:: python
38+
39+
import openeo
40+
connection = openeo.connect(url=...)
41+
capabilities = connection.capabilities()
42+
print("Federated backends:", capabilities.ext_federation_backend_details())
43+
44+
45+
Unavailable backends (``federation:missing``)
46+
----------------------------------------------
47+
48+
When listing resources like
49+
collections (with :py:meth:`Connection.list_collections() <openeo.rest.connection.Connection.list_collections>`),
50+
processes (with :py:meth:`Connection.list_processes() <openeo.rest.connection.Connection.list_processes>`),
51+
jobs (with :py:meth:`Connection.list_jobs() <openeo.rest.connection.Connection.list_jobs>`),
52+
etc.,
53+
there might be items missing due to federation participants being temporarily unavailable.
54+
These missing federation components are listed in the response under the ``federation:missing`` field
55+
and can be inspected as follows:
56+
57+
.. code-block:: python
58+
59+
import openeo
60+
connection = openeo.connect(url=...)
61+
collections = connection.list_collections()
62+
print("Number of collections:", len(collections))
63+
print("Missing federation components:", collections.ext_federation_missing())
64+
65+
66+
Note that the ``collections`` object in this example, returned by
67+
:py:meth:`Connection.list_collections() <openeo.rest.connection.Connection.list_collections>`,
68+
acts at the surface as a simple list of dictionaries with collection metadata,
69+
but also provides additional properties/methods like
70+
:py:attr:`ext_federation_missing() <openeo.rest.models.general.CollectionListingResponse.ext_federation_missing>`.

docs/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ Table of contents
6464
process_mapping
6565
development
6666
best_practices
67+
Federation extension <federation-extension>
6768
changelog
6869

6970

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/auth/testing.py

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,11 @@
33
"""
44

55
import base64
6-
import contextlib
6+
import dataclasses
77
import json
88
import urllib.parse
99
import uuid
1010
from typing import List, Optional, Union
11-
from unittest import mock
1211

1312
import requests
1413
import requests_mock.request
@@ -290,3 +289,37 @@ def get_request_history(
290289
for r in self.requests_mock.request_history
291290
if (method is None or method.lower() == r.method.lower()) and (url is None or url == r.url)
292291
]
292+
293+
294+
def build_basic_auth_header(username: str, password: str) -> str:
295+
"""Generate basic auth header (per RFC 7617) from given username and password."""
296+
credentials = base64.b64encode(f"{username}:{password}".encode("utf-8")).decode("utf-8")
297+
return f"Basic {credentials}"
298+
299+
300+
@dataclasses.dataclass(frozen=True)
301+
class SimpleBasicAuthMocker:
302+
"""
303+
Helper to create a test fixture for simple basic auth handling in openEO context:
304+
set up `/credentials/basic` handling with a fixed username/password/access_token combo.
305+
"""
306+
307+
username: str = "john"
308+
password: str = "j0hn!"
309+
access_token: str = "6cc3570k3n"
310+
311+
def expected_auth_header(self) -> str:
312+
return build_basic_auth_header(username=self.username, password=self.password)
313+
314+
def setup_credentials_basic_handler(self, *, api_root: str, requests_mock):
315+
"""Set up `requests_mock` handler for `/credentials/basic` endpoint."""
316+
expected_auth_header = self.expected_auth_header()
317+
318+
def credentials_basic_handler(request, context):
319+
assert request.headers["Authorization"] == expected_auth_header
320+
return json.dumps({"access_token": self.access_token})
321+
322+
return requests_mock.get(
323+
url_join(api_root, "/credentials/basic"),
324+
text=credentials_basic_handler,
325+
)

openeo/rest/capabilities.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from typing import Dict, List, Optional, Union
22

33
from openeo.internal.jupyter import render_component
4+
from openeo.rest.models import federation_extension
45
from openeo.util import deep_get
56
from openeo.utils.version import ApiVersionException, ComparableVersion
67

@@ -54,12 +55,13 @@ def list_plans(self) -> List[dict]:
5455
def _repr_html_(self):
5556
return render_component("capabilities", data=self.capabilities, parameters={"url": self.url})
5657

57-
def get_federation(self) -> Union[Dict[str, dict], None]:
58+
def ext_federation_backend_details(self) -> Union[Dict[str, dict], None]:
5859
"""
5960
Lists all back-ends (with details, such as URL) that are part of the federation
6061
if this backend acts as a federated backend,
6162
as specified in the openEO Federation Extension.
62-
Returns ``None`` otherwise
63+
Returns ``None`` otherwise.
64+
65+
.. versionadded:: 0.38.0
6366
"""
64-
# TODO: also check related conformance class in `/conformance`?
65-
return self.get("federation")
67+
return federation_extension.get_backend_details(data=self.capabilities)

0 commit comments

Comments
 (0)