Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 29 additions & 23 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,35 @@

### Enhancements

The Cuiman client package has been enhanced by _job result openers_,
which ease working with the results of a process job (#65):
- Client classes now have a method
`open_job_result(job_id, **options)` that is used to open the results
of a job. Both sync and async versions of the method are available in
`cuiman.Client` and `cuiman.AsyncClient`.
- Applications can customize how job results are opened by
adding their application-specific openers using the new
`register_job_result_opener(opener)` in class `cuiman.ClientConfig`.
- Added a new `notebooks/cuiman-openers.ipynb` that demonstrates a
custom opener.
- Added a new section in Cuiman usage documentation.
- Added some default openers for `xarray.Dataset`,
`pandas.DataFrame`, and `geopandas.GeoDataFrame`
given that a respective job result is a link.

The model classes that correspond to the OGC API - Processes in
`gavicore.models` are no longer generated and have been adjusted to
be more user-friendly (#71):
- `InlineValue` is now a simple type alias instead of a `pydantic.RootModel`.
- Same for `InlineValueOrRef` which has also been renamed to `JobResult`.
- Replaced type of optional fields `Optional[T]` by `T | None`.

- The Cuiman client package has been enhanced by _job result openers_,
which ease working with the results of a process job (#65):
- Client classes now have a method
`open_job_result(job_id, **options)` that is used to open the results
of a job. Both sync and async versions of the method are available in
`cuiman.Client` and `cuiman.AsyncClient`.
- Applications can customize how job results are opened by
adding their application-specific openers using the new
`register_job_result_opener(opener)` in class `cuiman.ClientConfig`.
- Added a new `notebooks/cuiman-openers.ipynb` that demonstrates a
custom opener.
- Added a new section in Cuiman usage documentation.
- Added some default openers for `xarray.Dataset`,
`pandas.DataFrame`, and `geopandas.GeoDataFrame`
given that a respective job result is a link.

- The model classes that correspond to the OGC API - Processes in
`gavicore.models` are no longer generated and have been adjusted to
be more user-friendly (#71):
- `InlineValue` is now a simple type alias instead of a `pydantic.RootModel`.
- Same for `InlineValueOrRef` which has also been renamed to `JobResult`.
- Replaced type of optional fields `Optional[T]` by `T | None`.
- The following models are now extendable, e.g., using `"x-"` prefixed fields:
- `gavicore.models.InputDescription` (e.g., extra "x-ui")
- `gavicore.models.OutputDescription` (e.g., extra "x-ui")
- `gavicore.models.JobStatus` (e.g., extra "x-traceback")
- `gavicore.models.ApiError` (e.g., extra "x-traceback")

- Lifted some mypy restrictions and enabled mypy pydantic plugin.

## Changes in version 0.0.9

Expand Down
4 changes: 2 additions & 2 deletions cuiman/tests/cli/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ def test_fail_with_client_error(self):
title="Not found",
status=404,
detail="Something was not found",
traceback=["a", "b", "c"],
**{"x-traceback": ["a", "b", "c"]},
),
)

Expand All @@ -51,7 +51,7 @@ def test_fail_with_client_error_and_traceback(self):
title="Not found",
status=404,
detail="Something was not found",
traceback=["a", "b", "c"],
**{"x-traceback": ["a", "b", "c"]},
),
)

Expand Down
19 changes: 13 additions & 6 deletions gavicore/src/gavicore/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

from datetime import date
from enum import Enum
from typing import Annotated, Any, TypeAlias, Union
from typing import Any, TypeAlias, Union

from pydantic import AnyUrl, AwareDatetime, BaseModel, ConfigDict, Field, RootModel

Expand Down Expand Up @@ -152,6 +152,11 @@ class AdditionalParameters(Metadata):


class DescriptionType(BaseModel):
model_config = ConfigDict(
# allow for extensions, e.g., using field name prefix "x-"
extra="allow",
)

title: str | None = None
description: str | None = None
keywords: list[str] | None = None
Expand Down Expand Up @@ -263,6 +268,7 @@ class JobControlOptions(Enum):

class JobInfo(BaseModel):
model_config = ConfigDict(
# allow for extensions, e.g., using field name prefix "x-"
extra="allow",
)

Expand All @@ -276,10 +282,10 @@ class JobInfo(BaseModel):
finished: AwareDatetime | None = None
updated: AwareDatetime | None = None
# noinspection Pydantic
progress: Annotated[int | None, Field(None, ge=0, le=100)] = None
progress: int | None = Field(None, ge=0, le=100)
links: list[Link] | None = None
# --- Enhancements to the standard
traceback: list[str] | None = None
# -- recognized extensions
traceback: str | list[str] | None = Field(None, alias="x-traceback")


class JobList(BaseModel):
Expand Down Expand Up @@ -310,6 +316,7 @@ class ApiError(BaseModel):
"""

model_config = ConfigDict(
# allow for extensions, e.g., using field name prefix "x-"
extra="allow",
)

Expand All @@ -318,8 +325,8 @@ class ApiError(BaseModel):
status: int | None = None
detail: str | None = None
instance: str | None = None
# --- Enhancements to the standard
traceback: list[str] | None = None
# -- recognized extensions
traceback: str | list[str] | None = Field(None, alias="x-traceback")


Format.model_rebuild()
Expand Down
67 changes: 62 additions & 5 deletions gavicore/tests/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@

import inspect
from enum import Enum
from typing import Any, TypeVar
from unittest import TestCase

from pydantic import BaseModel

import gavicore.models as gc_models
import gavicore.models as m

REQUIRED_ENUMS = {
"CRS",
Expand Down Expand Up @@ -37,20 +38,22 @@
"Schema",
}

T = TypeVar("T", bound=BaseModel)


class ModelsTest(TestCase):
def test_enums(self):
all_enums = set(
name
for name, obj in inspect.getmembers(gc_models, inspect.isclass)
for name, obj in inspect.getmembers(m, inspect.isclass)
if issubclass(obj, Enum)
)
self.assertSetIsOk(REQUIRED_ENUMS, all_enums)

def test_classes(self):
all_classes = set(
name
for name, obj in inspect.getmembers(gc_models, inspect.isclass)
for name, obj in inspect.getmembers(m, inspect.isclass)
if issubclass(obj, BaseModel)
)
self.assertSetIsOk(REQUIRED_CLASSES, all_classes)
Expand All @@ -60,11 +63,12 @@ def assertSetIsOk(self, required: set[str], actual: set[str]):
self.assertSetEqual(required, contained_items, "contained")

def test_models_have_repr_json(self):
for name, obj in inspect.getmembers(gc_models, inspect.isclass):
for name, obj in inspect.getmembers(m, inspect.isclass):
if name in REQUIRED_CLASSES and issubclass(obj, BaseModel):
self.assertTrue(hasattr(obj, "_repr_json_"), msg=f"model {name}")

obj = gc_models.Bbox(bbox=[10, 20, 30, 40])
obj = m.Bbox(bbox=[10, 20, 30, 40])
# noinspection PyUnresolvedReferences
json_repr = obj._repr_json_()
self.assertEqual(
(
Expand All @@ -73,3 +77,56 @@ def test_models_have_repr_json(self):
),
json_repr,
)

def test_models_with_extensions(self):
api_error = self._assert_extendable_model(
m.ApiError,
{
"title": "Key not found",
"status": 500,
"type": "KeyError",
"x-traceback": ["hello", "world"],
},
)
self.assertEqual(["hello", "world"], api_error.traceback)

job_info = self._assert_extendable_model(
m.JobInfo,
{
"type": "process",
"jobID": "job_1",
"status": "failed",
"x-traceback": "hello\nworld",
},
)
self.assertEqual("hello\nworld", job_info.traceback)

input_description = self._assert_extendable_model(
m.InputDescription,
{
"title": "Threshold",
"schema": {"type": "number"},
"x-ui": {"widget": "slider"},
},
)
self.assertEqual(
{"widget": "slider"}, input_description.model_extra.get("x-ui")
)

output_description = self._assert_extendable_model(
m.OutputDescription,
{
"title": "Output URL",
"schema": {"type": "string", "format": "uri"},
"x-ui": {"widget": "text"},
},
)
self.assertEqual({"widget": "text"}, output_description.model_extra.get("x-ui"))

def _assert_extendable_model(self, model_cls: type[T], data: dict[str, Any]) -> T:
model_instance = model_cls(**data)
self.assertEqual(
data,
model_instance.model_dump(mode="json", by_alias=True, exclude_unset=True),
)
return model_instance
6 changes: 2 additions & 4 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,8 @@ omit = [
[tool.mypy]
python_version = "3.10"
check_untyped_defs = true
disable_error_code = ["valid-type", "import-untyped"]
disable_error_code = ["import-untyped"]
plugins = ["pydantic.mypy"]
exclude = [
"appligator/tests",
"cuiman/tests",
Expand All @@ -156,9 +157,6 @@ exclude = [
"tools",
"_pkg_reservations",
]
[[tool.mypy.overrides]]
module = "gavicore.models"
disable_error_code = ["valid-type"]

########################## Tasks ###############################

Expand Down
14 changes: 8 additions & 6 deletions wraptile/src/wraptile/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,15 @@ def __init__(
status=status_code,
title=HTTPStatus(status_code).phrase,
detail=detail,
traceback=(
traceback.format_exception(
type(exception), exception, exception.__traceback__
**{
"x-traceback": (
traceback.format_exception(
type(exception), exception, exception.__traceback__
)
if exception is not None
else None
)
if exception is not None
else None
),
},
)


Expand Down
Loading