Skip to content

Commit be9967a

Browse files
committed
Merge branch 'issue195-apiv12-processesv20'
2 parents bbb36c6 + 2855b46 commit be9967a

File tree

11 files changed

+137
-40
lines changed

11 files changed

+137
-40
lines changed

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,15 @@ and start a new "In Progress" section above it.
2121

2222
## In progress
2323

24+
- Add STAC collections conformance class ([#195](https://github.com/Open-EO/openeo-python-driver/issues/195))
25+
- update openeo_driver/specs/openeo-api/1.x submodule to tag `1.2.0` ([#195](https://github.com/Open-EO/openeo-python-driver/issues/195))
26+
27+
28+
## 0.125.0
29+
30+
- Add log level to batch job logs response ([#195](https://github.com/Open-EO/openeo-python-driver/issues/195))
31+
32+
2433
## 0.124.0
2534

2635
- Better argument validation in `resample_spatial`/`resample_cube_spatial` (related to [Open-EO/openeo-python-client#690](https://github.com/Open-EO/openeo-python-client/issues/690))

openeo_driver/_version.py

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

openeo_driver/backend.py

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
from openeo_driver.datastructs import SarBackscatterArgs
3232
from openeo_driver.dry_run import SourceConstraint
3333
from openeo_driver.errors import CollectionNotFoundException, ServiceUnsupportedException, FeatureUnsupportedException
34-
from openeo_driver.constants import JOB_STATUS
34+
from openeo_driver.constants import JOB_STATUS, DEFAULT_LOG_LEVEL_RETRIEVAL
3535
from openeo_driver.processes import ProcessRegistry
3636
from openeo_driver.users import User
3737
from openeo_driver.users.oidc import OidcProvider
@@ -142,7 +142,15 @@ def remove_service(self, user_id: str, service_id: str) -> None:
142142
"""https://openeo.org/documentation/1.0/developers/api/reference.html#operation/delete-service"""
143143
raise NotImplementedError()
144144

145-
def get_log_entries(self, service_id: str, user_id: str, offset: str) -> List[dict]:
145+
def get_log_entries(
146+
self,
147+
service_id: str,
148+
*,
149+
user_id: str,
150+
offset: Union[str, None] = None,
151+
limit: Union[int, None] = None,
152+
level: str = DEFAULT_LOG_LEVEL_RETRIEVAL,
153+
) -> List[dict]:
146154
"""https://openeo.org/documentation/1.0/developers/api/reference.html#operation/debug-service"""
147155
# TODO require auth/user handle?
148156
return []
@@ -178,12 +186,12 @@ class LoadParameters(dict):
178186
"""
179187
A buffer provided in the units of the target CRS. If target CRS is not provided, then it is assumed to be the native CRS
180188
of the collection.
181-
182-
This buffer is applied to AOI when constructing the datacube, allowing operations that require neighbouring pixels
189+
190+
This buffer is applied to AOI when constructing the datacube, allowing operations that require neighbouring pixels
183191
to be implemented correctly. Examples are apply_kernel and apply_neighborhood, but also certain resampling operations
184192
could be affected by this.
185-
186-
The buffer has to be considered in the global extent!
193+
194+
The buffer has to be considered in the global extent!
187195
"""
188196
pixel_buffer = dict_item(default=None)
189197

@@ -552,9 +560,11 @@ def get_results(self, job_id: str, user_id: str) -> Dict[str, dict]:
552560
def get_log_entries(
553561
self,
554562
job_id: str,
563+
*,
555564
user_id: str,
556-
offset: Optional[str] = None,
557-
level: Optional[str] = None,
565+
offset: Union[str, None] = None,
566+
limit: Union[str, None] = None,
567+
level: str = DEFAULT_LOG_LEVEL_RETRIEVAL,
558568
) -> Iterable[dict]:
559569
"""
560570
https://openeo.org/documentation/1.0/developers/api/reference.html#operation/debug-job
@@ -760,6 +770,9 @@ class OpenEoBackendImplementation:
760770
"https://api.openeo.org/1.2.0",
761771
# Support the "remote process definition" extension (originally known as the "remote-udp" extension)
762772
"https://api.openeo.org/extensions/remote-process-definition/0.1.0",
773+
# STAC API conformance classes
774+
# "https://api.stacspec.org/v1.0.0/core", # TODO #363 can we claim this conformance class already?
775+
"https://api.stacspec.org/v1.0.0/collections",
763776
]
764777

765778
def __init__(

openeo_driver/constants.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,3 +49,9 @@ class JOB_STATUS:
4949
"lower-right",
5050
"upper-right",
5151
]
52+
53+
54+
# Default value for `level` parameter in `POST /result`, `POST /jobs`, ... requests
55+
DEFAULT_LOG_LEVEL_PROCESSING = "info"
56+
# Default value for `level in `GET /jobs/{job_id}/logs`, `GET /services/{service_id}/logs` requests
57+
DEFAULT_LOG_LEVEL_RETRIEVAL = "debug"

openeo_driver/dummy/dummy_backend.py

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@
4343
UserDefinedProcessMetadata,
4444
)
4545
from openeo_driver.config import OpenEoBackendConfig
46-
from openeo_driver.constants import JOB_STATUS, STAC_EXTENSION
46+
from openeo_driver.constants import JOB_STATUS, STAC_EXTENSION, DEFAULT_LOG_LEVEL_RETRIEVAL
4747
from openeo_driver.datacube import DriverDataCube, DriverMlModel, DriverVectorCube
4848
from openeo_driver.datastructs import StacAsset
4949
from openeo_driver.delayed_vector import DelayedVector
@@ -172,9 +172,17 @@ def list_services(self, user_id: str) -> List[ServiceMetadata]:
172172
def service_info(self, user_id: str, service_id: str) -> ServiceMetadata:
173173
return next(s for s in self._registry if s.id == service_id)
174174

175-
def get_log_entries(self, service_id: str, user_id: str, offset: str) -> List[dict]:
175+
def get_log_entries(
176+
self,
177+
service_id: str,
178+
*,
179+
user_id: str,
180+
offset: Union[str, None] = None,
181+
limit: Union[int, None] = None,
182+
level: str = DEFAULT_LOG_LEVEL_RETRIEVAL,
183+
) -> List[dict]:
176184
return [
177-
{"id": 3, "level": "info", "message": "Loaded data."}
185+
{"id": 3, "level": "info", "message": "Loaded data."},
178186
]
179187

180188

@@ -921,16 +929,18 @@ def get_result_assets(self, job_id: str, user_id: str) -> Dict[str, dict]:
921929
def get_log_entries(
922930
self,
923931
job_id: str,
932+
*,
924933
user_id: str,
925-
offset: Optional[str] = None,
926-
level: Optional[str] = None,
934+
offset: Union[str, None] = None,
935+
limit: Union[str, None] = None,
936+
level: str = DEFAULT_LOG_LEVEL_RETRIEVAL,
927937
) -> Iterable[dict]:
928938
self._get_job_info(job_id=job_id, user_id=user_id)
929939
default_logs = [{"id": "1", "level": "info", "message": "hello world"}]
930940
requested_level = normalize_log_level(level)
931941
for log in self._custom_job_logs.get(job_id, default_logs):
932942
if isinstance(log, dict):
933-
actual_level = normalize_log_level(log.get("log_level"))
943+
actual_level = normalize_log_level(log.get("level"))
934944
if actual_level < requested_level:
935945
continue
936946
yield log

openeo_driver/errors.py

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -360,7 +360,7 @@ class ProcessGraphMissingException(OpenEOApiException):
360360
status_code = 400
361361
code = 'ProcessGraphMissing'
362362
message = "Invalid process specified. It doesn't contain a process graph."
363-
_description = "The parameter `process` doesn't contain a valid process."
363+
_description = "The process doesn't contain a process graph. For jobs, services, and sync. processing the parameter `process` must contain a `process_graph`."
364364
_tags = ['Batch Jobs', 'Data Processing', 'Secondary Services', 'User-Defined Processes']
365365

366366

@@ -442,19 +442,19 @@ class ProcessUnsupportedException(OpenEOApiException):
442442
status_code = 400
443443
code = 'ProcessUnsupported'
444444
message = "Process with identifier '{process}' is not available in namespace '{namespace}'."
445-
_description = 'A process (pre-defined or user-defined) with the specified identifier is not available. To be used when validating or executing process graphs.'
446-
_tags = ['Data Processing']
445+
_description = "A process (predefined or user-defined) with the specified identifier is not available. To be used when validating or executing process graphs."
446+
_tags = ["Data Processing"]
447447

448448
def __init__(self, process: str, namespace: str = "backend"):
449449
super().__init__(message=self.message.format(process=process, namespace=namespace))
450450

451451

452452
class PredefinedProcessExistsException(OpenEOApiException):
453453
status_code = 400
454-
code = 'PredefinedProcessExists'
455-
message = 'A predefined process with the given identifier exists.'
456-
_description = 'If a user wants to store a user-defined process with the id of a pre-defined process.'
457-
_tags = ['User-Defined Processes']
454+
code = "PredefinedProcessExists"
455+
message = "A predefined process with the given identifier exists."
456+
_description = "If a user wants to store a user-defined process with the id of a predefined process."
457+
_tags = ["User-Defined Processes"]
458458

459459

460460
class ProcessParameterRequiredException(OpenEOApiException):
@@ -542,6 +542,24 @@ def __init__(self, parameter: str, reason: str):
542542
super().__init__(message=self.message.format(parameter=parameter, reason=reason))
543543

544544

545+
class EstimateComplexityException(OpenEOApiException):
546+
status_code = 500
547+
code = "EstimateComplexity"
548+
message = "The process is too complex to calculate an estimate."
549+
_description = "The process is too complex to calculate an estimate, e.g. due to a UDF or other processes that are complex to estimate costs reliably."
550+
_tags = ["Batch Jobs"]
551+
552+
553+
class BillingPlanMissingException(OpenEOApiException):
554+
status_code = 400
555+
code = "BillingPlanMissing"
556+
message = "A billing plan must be specified."
557+
_description = (
558+
"No billing plan has been specified by the user and the billing plan can't be determined unambiguously."
559+
)
560+
_tags = ["Batch Jobs", "Data Processing", "Secondary Services"]
561+
562+
545563
# --- End of semi-autogenerated openEO exception classes ------------------------------------------------
546564

547565

openeo_driver/testing.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -206,9 +206,11 @@ def ensure_auth_header(self):
206206
if not self.default_request_headers.get("Authorization"):
207207
self.set_auth_bearer_token()
208208

209-
def get(self, path: str, headers: dict = None) -> ApiResponse:
209+
def get(self, path: str, headers: dict = None, params: Optional[dict] = None) -> ApiResponse:
210210
"""Do versioned GET request, given non-versioned path"""
211-
return ApiResponse(self.client.get(path=self.url(path), headers=self._request_headers(headers)))
211+
return ApiResponse(
212+
self.client.get(path=self.url(path), headers=self._request_headers(headers), query_string=params)
213+
)
212214

213215
def head(self, path: str, headers: dict = None) -> ApiResponse:
214216
"""Do versioned GET request, given non-versioned path"""

openeo_driver/views.py

Lines changed: 27 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@
3939
JobListing,
4040
)
4141
from openeo_driver.config import get_backend_config, OpenEoBackendConfig
42-
from openeo_driver.constants import STAC_EXTENSION
42+
from openeo_driver.constants import STAC_EXTENSION, DEFAULT_LOG_LEVEL_RETRIEVAL, DEFAULT_LOG_LEVEL_PROCESSING
4343
from openeo_driver.datacube import DriverMlModel
4444
from openeo_driver.errors import (
4545
OpenEOApiException,
@@ -404,7 +404,8 @@ def index():
404404
"version": api_version, # Deprecated pre-0.4.0 API version field
405405
"api_version": api_version, # API version field since 0.4.0
406406
"backend_version": backend_version,
407-
"stac_version": "0.9.0",
407+
"stac_version": "0.9.0", # TODO #363 bump to 1.x.y?
408+
"type": "Catalog",
408409
"conformsTo": backend_implementation.conformance_classes(),
409410
"id": service_id,
410411
"title": title,
@@ -654,7 +655,7 @@ def result(user: User):
654655
process_graph = _extract_process_graph(post_data)
655656
budget = post_data.get("budget")
656657
plan = post_data.get("plan")
657-
log_level = post_data.get("log_level", "info")
658+
log_level = post_data.get("log_level", DEFAULT_LOG_LEVEL_PROCESSING)
658659
job_options = _extract_job_options(
659660
post_data, to_ignore=["process", "process_graph", "budget", "plan", "log_level"]
660661
)
@@ -880,7 +881,7 @@ def create_job(user: User):
880881
post_data, to_ignore=["process", "process_graph", "title", "description", "plan", "budget", "log_level"]
881882
)
882883
metadata = {k: post_data[k] for k in ["title", "description", "plan", "budget"] if k in post_data}
883-
metadata["log_level"] = post_data.get("log_level", "info")
884+
metadata["log_level"] = post_data.get("log_level", DEFAULT_LOG_LEVEL_PROCESSING)
884885
job_info = backend_implementation.batch_jobs.create_job(
885886
# TODO: remove `filter_supported_kwargs` (when all implementations have migrated to `user` iso `user_id`)
886887
**filter_supported_kwargs(
@@ -1513,16 +1514,26 @@ def download_job_result_signed(job_id, user_base64, secure_key, filename):
15131514
@blueprint.route("/jobs/<job_id>/logs", methods=["GET"])
15141515
@auth_handler.requires_bearer_auth
15151516
def get_job_logs(job_id, user: User):
1516-
offset = request.args.get("offset")
1517-
level = request.args.get("level", "debug")
1517+
offset = request.args.get("offset", default=None)
1518+
limit = request.args.get("limit", default=None, type=int)
1519+
level = request.args.get("level", default=DEFAULT_LOG_LEVEL_RETRIEVAL)
15181520
request_id = FlaskRequestCorrelationIdLogging.get_request_id()
15191521
# TODO: implement paging support: `limit`, next/prev/first/last `links`, ...
1520-
logs = backend_implementation.batch_jobs.get_log_entries(
1522+
1523+
# TODO: remove this `function_has_argument` once all implementations are migrated
1524+
if function_has_argument(backend_implementation.batch_jobs.get_log_entries, argument="limit"):
1525+
logs = backend_implementation.batch_jobs.get_log_entries(
1526+
job_id=job_id, user_id=user.user_id, offset=offset, level=level, limit=limit
1527+
)
1528+
else:
1529+
logs = backend_implementation.batch_jobs.get_log_entries(
15211530
job_id=job_id, user_id=user.user_id, offset=offset, level=level
15221531
)
15231532

15241533
def generate():
1525-
yield """{"logs":["""
1534+
yield "{"
1535+
yield f'"level": {json.dumps(level)},'
1536+
yield '"logs":['
15261537

15271538
sep = ""
15281539
try:
@@ -1550,7 +1561,9 @@ def generate():
15501561
}
15511562
yield sep + json.dumps(log)
15521563

1553-
yield """],"links":[]}"""
1564+
yield "],"
1565+
# TODO: add pagination links (next, prev, first, last)
1566+
yield '"links":[]}'
15541567

15551568
return current_app.response_class(generate(), mimetype="application/json")
15561569

@@ -1665,12 +1678,13 @@ def service_delete(service_id, user: User):
16651678
@blueprint.route('/services/<service_id>/logs', methods=['GET'])
16661679
@auth_handler.requires_bearer_auth
16671680
def service_logs(service_id, user: User):
1668-
level = request.args.get("level", "debug")
1669-
offset = request.args.get('offset', 0)
1681+
offset = request.args.get("offset", default=None)
1682+
limit = request.args.get("limit", default=None, type=int)
1683+
level = request.args.get("level", default=DEFAULT_LOG_LEVEL_RETRIEVAL)
16701684
logs = backend_implementation.secondary_services.get_log_entries(
1671-
service_id=service_id, user_id=user.user_id, offset=offset
1685+
service_id=service_id, user_id=user.user_id, offset=offset, limit=limit, level=level
16721686
)
1673-
return jsonify({"logs": logs, "links": []})
1687+
return jsonify({"logs": logs, "links": [], "level": level})
16741688

16751689

16761690
def register_views_udp(

tests/test_errors.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,8 @@ def test_error_code(error_code):
8484
raise Exception("Exception class {c} has placeholder in message but no custom __init__".format(
8585
c=exception_cls.__name__))
8686

87+
assert exception_cls._description == error_spec["description"]
88+
8789

8890
def test_flask_request_id_as_api_error_id():
8991
app = flask.Flask(__name__)

0 commit comments

Comments
 (0)