Skip to content
Draft
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
4 changes: 2 additions & 2 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@
- Dropped subpackage `cuiman.gui.component`.
- Added a pixi tool to demonstrate and debug generated UIs from
OpenAPI Schema: `pixi run schema2ui`.

- Enhanced the **Appligator** package with Dockerfile generation and
improved Airflow integration:
improved Airflow DAG generation:
- Added `appligator.airflow.gen_dockerfile.generate` for Jinja2-template-based
Dockerfile generation. Produces a two-stage pixi build with support for
non-editable local package installs. The runtime base image is configurable
Expand Down
8 changes: 5 additions & 3 deletions gavicore/src/gavicore/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,6 @@ class Schema(BaseModel):
example: Any | None = None
examples: Any | None = None
deprecated: bool | None = False
field_ref: str | None = Field(None, alias="$ref")
# type "number" and "integer"
minimum: int | float | None = None
maximum: int | float | None = None
Expand Down Expand Up @@ -116,11 +115,14 @@ class Schema(BaseModel):
oneOf: list[Schema] | None = None
anyOf: list[Schema] | None = None
discriminator: Discriminator | None = None
# refs
id: str | None = Field(None, alias="$id", min_length=1)
ref: str | None = Field(None, alias="$ref", min_length=1)


class Discriminator(BaseModel):
propertyName: str | None = Field(None, min_length=1)
mapping: dict[str, Schema] | None = None
propertyName: str = Field(..., min_length=1)
mapping: dict[str, str] | None = None


# ---------------------------------------------------------------------
Expand Down
28 changes: 19 additions & 9 deletions gavicore/src/gavicore/ui/field/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,25 @@
# https://opensource.org/license/apache-2-0.

from abc import ABC, abstractmethod
from typing import Any, TypeAlias
from typing import Generic, TypeVar

from gavicore.util.ensure import ensure_type

from ..vm import ViewModel
from .meta import FieldMeta

View: TypeAlias = Any
VT = TypeVar("VT")
"""
A concrete piece of UI of typically data-bound UI
(`view` such as widget, panel, control).
"""


class Field(ABC):
class Field(ABC, Generic[VT]):
"""
A binding unit between data (`view_model`) and a concrete piece
of typically data-bound UI (`view` such as widget, panel, control).
A binding unit between data, represented by property `view_model`,
and a concrete piece of typically data-bound UI, represented by the
`view` property of type `T`, such as a widget, panel, control.
"""

@property
Expand All @@ -33,14 +36,21 @@ def view_model(self) -> ViewModel:

@property
@abstractmethod
def view(self) -> View:
def view(self) -> VT:
"""The view used by this field."""


class FieldBase(Field, ABC):
FT = TypeVar("FT", bound=Field)
"""
A field type for a specific view type.
"""


class FieldBase(Field[VT], ABC, Generic[VT]):
"""Abstract base class for UI fields."""

def __init__(self, view_model: ViewModel, view: View):
def __init__(self, view_model: ViewModel, view: VT):
ensure_type("view_model", view_model, ViewModel)
self._view_model = view_model
self._view = view
self._bind()
Expand All @@ -50,7 +60,7 @@ def view_model(self) -> ViewModel:
return self._view_model

@property
def view(self) -> View:
def view(self) -> VT:
return self._view

def _bind(self) -> None:
Expand Down
54 changes: 39 additions & 15 deletions gavicore/src/gavicore/ui/field/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,37 +2,39 @@
# Permissions are hereby granted under the terms of the Apache 2.0 License:
# https://opensource.org/license/apache-2-0.

from typing import TYPE_CHECKING, Any
from typing import TYPE_CHECKING, Any, Generic

from gavicore.models import DataType, Schema
from gavicore.util.undefined import Undefined

from ...util.ensure import ensure_condition
from ..vm import (
from gavicore.ui.vm import (
AnyViewModel,
ArrayViewModel,
NullableViewModel,
ObjectViewModel,
PrimitiveViewModel,
ViewModel,
)
from .base import Field, View
from gavicore.util.ensure import ensure_condition
from gavicore.util.undefined import Undefined

from .base import FT, VT
from .meta import FieldMeta

if TYPE_CHECKING:
from .generator import FieldGenerator
from .layout import LayoutFunction


class FieldContext:
class FieldContext(Generic[FT, VT]):
"""The context object passed to a UI field factory."""

def __init__(
self,
*,
generator: "FieldGenerator",
generator: "FieldGenerator[FT, VT]",
meta: FieldMeta,
initial_value: Any | Undefined = Undefined.value,
parent_ctx: "FieldContext | None" = None,
label_hidden: bool = False,
parent_ctx: "FieldContext[FT, VT] | None" = None,
):
self._parent_ctx = parent_ctx
self._generator = generator
Expand All @@ -43,6 +45,7 @@ def __init__(
if isinstance(initial_value, Undefined)
else initial_value
)
self._label_hidden = label_hidden

@property
def meta(self) -> FieldMeta:
Expand All @@ -54,6 +57,19 @@ def name(self) -> str:
"""The name from field metadata."""
return self._meta.name

@property
def label_hidden(self) -> bool:
"""A flag indicating that the label for the field should not be shown."""
return self._label_hidden

@property
def label(self) -> str:
"""
A label for the field.
It is an empty string if the [label_hidden][label_hidden] flag is set.
"""
return "" if self._label_hidden else self._meta.label

@property
def schema(self) -> Schema:
"""The OpenAPI Schema from field metadata."""
Expand All @@ -76,13 +92,13 @@ def path(self) -> list[str]:
return self._parent_ctx.path + [self.name]
return [self.name]

def layout(self, layout_function: "LayoutFunction", views: dict[str, View]) -> View:
def layout(self, layout_function: "LayoutFunction", views: dict[str, VT]) -> VT:
"""Lay out the given views using the field metadata's `layout` property."""
from .layout import LayoutManager

return LayoutManager(layout_function, views).layout(self)

def create_property_fields(self) -> dict[str, Field]:
def create_property_fields(self) -> dict[str, FT]:
"""Create property fields given that this
context's field is of type "object".
"""
Expand All @@ -97,7 +113,7 @@ def create_property_fields(self) -> dict[str, Field]:
for prop_name, prop_meta in self.meta.properties.items()
}

def create_item_field(self) -> Field:
def create_item_field(self) -> FT:
"""Create a new item field given that this
context's field is of type "array".
"""
Expand All @@ -109,13 +125,17 @@ def create_item_field(self) -> Field:
assert self.meta.items is not None
return self.create_child_field(self.meta.items)

def create_child_field(self, child_meta: FieldMeta) -> Field:
def create_child_field(
self, child_meta: FieldMeta, label_hidden: bool = False
) -> FT:
"""Create a new field for the given child field metadata."""
child_ctx = self._create_child_ctx(child_meta)
child_ctx = self._create_child_ctx(child_meta, label_hidden=label_hidden)
# noinspection PyProtectedMember
return self._generator._generate_field(child_ctx)

def _create_child_ctx(self, child_meta: FieldMeta) -> "FieldContext":
def _create_child_ctx(
self, child_meta: FieldMeta, label_hidden: bool = False
) -> "FieldContext[FT, VT]":
initial_value = self.initial_value
child_name = child_meta.name
if (
Expand All @@ -130,6 +150,7 @@ def _create_child_ctx(self, child_meta: FieldMeta) -> "FieldContext":
generator=self._generator,
meta=child_meta,
initial_value=child_value,
label_hidden=label_hidden,
parent_ctx=self,
)

Expand All @@ -143,6 +164,9 @@ class ViewModelFactory:
def __init__(self, ctx: FieldContext):
self._ctx = ctx

def any(self):
return AnyViewModel(self._ctx.meta, value=self._ctx.initial_value)

def primitive(self) -> PrimitiveViewModel:
return PrimitiveViewModel(self._ctx.meta, value=self._ctx.initial_value)

Expand Down
Loading
Loading