Skip to content

Commit

Permalink
Merge branch 'issue668-federation-extension'
Browse files Browse the repository at this point in the history
  • Loading branch information
soxofaan committed Jan 31, 2025
2 parents b62f7fa + 632ad1c commit eb40735
Show file tree
Hide file tree
Showing 24 changed files with 1,124 additions and 294 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- 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))

### Changed

- Improved tracking of metadata changes with `resample_spatial` and `resample_cube_spatial` ([#690](https://github.com/Open-EO/openeo-python-client/issues/690))
Expand Down
16 changes: 10 additions & 6 deletions docs/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -70,19 +70,23 @@ openeo.rest.capabilities
:members: OpenEoCapabilities


openeo.rest.models
-------------------

.. automodule:: openeo.rest.models.general
:members:

.. automodule:: openeo.rest.models.logs
:members: LogEntry, normalize_log_level


openeo.api.process
--------------------

.. automodule:: openeo.api.process
:members: Parameter


openeo.api.logs
-----------------

.. automodule:: openeo.api.logs
:members: LogEntry, normalize_log_level


openeo.rest.connection
----------------------
Expand Down
70 changes: 70 additions & 0 deletions docs/federation-extension.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@

.. _federation-extension:

===========================
openEO Federation Extension
===========================


The `openEO Federation extension <https://github.com/Open-EO/openeo-api/tree/master/extensions/federation>`_
is a set of additional specifications,
on top of the standard openEO API specification,
to address the need for extra metadata in the context
of federated openEO processing,
where multiple (separately operated) openEO services are bundled together
behind a single API endpoint.


Accessing federation extension metadata
========================================

The openEO Python client library provides access to this additional metadata
in a couple of resources.

.. versionadded:: 0.38.0
initial support to access federation extension related metadata.

.. warning:: this API is experimental and subject to change.


Backend details
---------------

Participating backends in a federation are listed under the ``federation`` field
of the capabilities document (``GET /``) and can be inspected
using :py:meth:`OpenEoCapabilities.ext_federation_backend_details() <openeo.rest.capabilities.OpenEoCapabilities.ext_federation_backend_details>`:

.. code-block:: python
import openeo
connection = openeo.connect(url=...)
capabilities = connection.capabilities()
print("Federated backends:", capabilities.ext_federation_backend_details())
Unavailable backends (``federation:missing``)
----------------------------------------------

When listing resources like
collections (with :py:meth:`Connection.list_collections() <openeo.rest.connection.Connection.list_collections>`),
processes (with :py:meth:`Connection.list_processes() <openeo.rest.connection.Connection.list_processes>`),
jobs (with :py:meth:`Connection.list_jobs() <openeo.rest.connection.Connection.list_jobs>`),
etc.,
there might be items missing due to federation participants being temporarily unavailable.
These missing federation components are listed in the response under the ``federation:missing`` field
and can be inspected as follows:

.. code-block:: python
import openeo
connection = openeo.connect(url=...)
collections = connection.list_collections()
print("Number of collections:", len(collections))
print("Missing federation components:", collections.ext_federation_missing())
Note that the ``collections`` object in this example, returned by
:py:meth:`Connection.list_collections() <openeo.rest.connection.Connection.list_collections>`,
acts at the surface as a simple list of dictionaries with collection metadata,
but also provides additional properties/methods like
:py:attr:`ext_federation_missing() <openeo.rest.models.general.CollectionListingResponse.ext_federation_missing>`.
1 change: 1 addition & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ Table of contents
process_mapping
development
best_practices
Federation extension <federation-extension>
changelog


Expand Down
104 changes: 9 additions & 95 deletions openeo/api/logs.py
Original file line number Diff line number Diff line change
@@ -1,99 +1,13 @@
import logging
from typing import Optional, Union
import warnings

from openeo.internal.warnings import UserDeprecationWarning
from openeo.rest.models.logs import LogEntry, log_level_name, normalize_log_level

class LogEntry(dict):
"""
Log message and info for jobs and services
warnings.warn(
message="Submodule `openeo.api.logs` is deprecated in favor of `openeo.rest.models.logs`.",
category=UserDeprecationWarning,
stacklevel=2,
)

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

_required = {"id", "level", "message"}

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)

# Check required fields
missing = self._required.difference(self.keys())
if missing:
raise ValueError("Missing required fields: {m}".format(m=sorted(missing)))

@property
def id(self):
return self["id"]

# Legacy alias
log_id = id

@property
def message(self):
return self["message"]

@property
def level(self):
return self["level"]

# TODO: add properties for "code", "time", "path", "links" and "data" with sensible defaults?


def normalize_log_level(
log_level: Union[int, str, None], default: int = logging.DEBUG
) -> int:
"""
Helper function to convert a openEO API log level (e.g. string "error")
to the integer constants defined in Python's standard library ``logging`` module (e.g. ``logging.ERROR``).
:param log_level: log level to normalize: a log level string in the style of
the openEO API ("error", "warning", "info", or "debug"),
an integer value (e.g. a ``logging`` constant), or ``None``.
:param default: fallback log level to return on unknown log level strings or ``None`` input.
:raises TypeError: when log_level is any other type than str, an int or None.
:return: One of the following log level constants from the standard module ``logging``:
``logging.ERROR``, ``logging.WARNING``, ``logging.INFO``, or ``logging.DEBUG`` .
"""
if isinstance(log_level, str):
log_level = log_level.upper()
if log_level in ["CRITICAL", "ERROR", "FATAL"]:
return logging.ERROR
elif log_level in ["WARNING", "WARN"]:
return logging.WARNING
elif log_level == "INFO":
return logging.INFO
elif log_level == "DEBUG":
return logging.DEBUG
else:
return default
elif isinstance(log_level, int):
return log_level
elif log_level is None:
return default
else:
raise TypeError(
f"Value for log_level is not an int or str: type={type(log_level)}, value={log_level!r}"
)


def log_level_name(log_level: Union[int, str, None]) -> str:
"""
Get the name of a normalized log level.
This value conforms to log level names used in the openEO API.
"""
return logging.getLevelName(normalize_log_level(log_level)).lower()
__all__ = ["LogEntry", "normalize_log_level", "log_level_name"]
37 changes: 35 additions & 2 deletions openeo/rest/auth/testing.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,11 @@
"""

import base64
import contextlib
import dataclasses
import json
import urllib.parse
import uuid
from typing import List, Optional, Union
from unittest import mock

import requests
import requests_mock.request
Expand Down Expand Up @@ -290,3 +289,37 @@ def get_request_history(
for r in self.requests_mock.request_history
if (method is None or method.lower() == r.method.lower()) and (url is None or url == r.url)
]


def build_basic_auth_header(username: str, password: str) -> str:
"""Generate basic auth header (per RFC 7617) from given username and password."""
credentials = base64.b64encode(f"{username}:{password}".encode("utf-8")).decode("utf-8")
return f"Basic {credentials}"


@dataclasses.dataclass(frozen=True)
class SimpleBasicAuthMocker:
"""
Helper to create a test fixture for simple basic auth handling in openEO context:
set up `/credentials/basic` handling with a fixed username/password/access_token combo.
"""

username: str = "john"
password: str = "j0hn!"
access_token: str = "6cc3570k3n"

def expected_auth_header(self) -> str:
return build_basic_auth_header(username=self.username, password=self.password)

def setup_credentials_basic_handler(self, *, api_root: str, requests_mock):
"""Set up `requests_mock` handler for `/credentials/basic` endpoint."""
expected_auth_header = self.expected_auth_header()

def credentials_basic_handler(request, context):
assert request.headers["Authorization"] == expected_auth_header
return json.dumps({"access_token": self.access_token})

return requests_mock.get(
url_join(api_root, "/credentials/basic"),
text=credentials_basic_handler,
)
10 changes: 6 additions & 4 deletions openeo/rest/capabilities.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from typing import Dict, List, Optional, Union

from openeo.internal.jupyter import render_component
from openeo.rest.models import federation_extension
from openeo.util import deep_get
from openeo.utils.version import ApiVersionException, ComparableVersion

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

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

0 comments on commit eb40735

Please sign in to comment.