Skip to content

model

DataclassType

Bases: Protocol

Protocol for plain (non-pydantic) dataclass models.

Source code in src/embar/model.py
52
53
54
55
class DataclassType(Protocol):
    """Protocol for plain (non-pydantic) dataclass models."""

    __dataclass_fields__: ClassVar[dict[str, Any]]

HasAnnotations

Bases: Protocol

Protocol satisfied by any class that carries __annotations__ — i.e. every Python class that declares at least one field-level type hint.

This is the minimal structural requirement for to_sql_columns and load_dataclass to work: they only need get_type_hints() to succeed.

Source code in src/embar/model.py
58
59
60
61
62
63
64
65
66
67
class HasAnnotations(Protocol):
    """
    Protocol satisfied by any class that carries `__annotations__` — i.e. every
    Python class that declares at least one field-level type hint.

    This is the minimal structural requirement for `to_sql_columns` and
    `load_dataclass` to work: they only need `get_type_hints()` to succeed.
    """

    __annotations__: ClassVar[dict[str, Any]]

SelectAllDataclass

SelectAll version that doesn't validate (plain dataclass).

Source code in src/embar/model.py
81
82
83
84
85
86
class SelectAllDataclass:
    """
    `SelectAll` version that doesn't validate (plain dataclass).
    """

    __dataclass_fields__: ClassVar[dict[str, Any]] = {}

SelectAllPydantic

Bases: BaseModel

SelectAll version that validates with Pydantic.

Source code in src/embar/model.py
73
74
75
76
77
78
class SelectAllPydantic(BaseModel):
    """
    `SelectAll` version that validates with Pydantic.
    """

    ...

generate_dataclass_model(cls)

Create a plain dataclass based on a Table (no Pydantic validation).

Fields are typed as Annotated[py_type, column] so that to_sql_columns can discover the SQL column reference.

Note the new dataclass has the same exact name, maybe something to revisit.

from embar.table import Table
from embar.model import generate_dataclass_model
class MyTable(Table): ...
generate_dataclass_model(MyTable)
Source code in src/embar/model.py
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
def generate_dataclass_model(cls: type[TableBase]) -> type[DataclassType]:
    """
    Create a plain dataclass based on a `Table` (no Pydantic validation).

    Fields are typed as `Annotated[py_type, column]` so that `to_sql_columns`
    can discover the SQL column reference.

    Note the new dataclass has the same exact name, maybe something to revisit.

    ```python
    from embar.table import Table
    from embar.model import generate_dataclass_model
    class MyTable(Table): ...
    generate_dataclass_model(MyTable)
    ```
    """
    dc_fields: list[Any] = []
    for field_name, column in cls._fields.items():  # pyright:ignore[reportPrivateUsage]
        field_type = column.info.py_type
        # Use Annotated so to_sql_columns can find the column reference
        annotated_type = Annotated[field_type, column]
        dc_fields.append(
            (
                field_name,
                annotated_type,
                field(default=None),
            )
        )

    data_class = make_dataclass(cls.__name__, dc_fields)
    return data_class

generate_pydantic_model(cls)

Create a Pydantic model based on a Table.

Note the new model has the same exact name, maybe something to revisit.

from embar.table import Table
from embar.model import generate_pydantic_model
class MyTable(Table): ...
generate_pydantic_model(MyTable)
Source code in src/embar/model.py
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
def generate_pydantic_model(cls: type[TableBase]) -> type[BaseModel]:
    """
    Create a Pydantic model based on a `Table`.

    Note the new model has the same exact name, maybe something to revisit.

    ```python
    from embar.table import Table
    from embar.model import generate_pydantic_model
    class MyTable(Table): ...
    generate_pydantic_model(MyTable)
    ```
    """
    _require_pydantic("generate_pydantic_model")
    from pydantic import BeforeValidator, create_model
    from pydantic import Field as PydanticField

    fields_dict: dict[str, Any] = {}
    for field_name, column in cls._fields.items():  # pyright:ignore[reportPrivateUsage]
        field_type = column.info.py_type

        if column.info.col_type == "VECTOR":
            field_type = Annotated[field_type, BeforeValidator(_parse_json_list)]

        fields_dict[field_name] = (
            Annotated[field_type, column],
            PydanticField(default_factory=lambda a=column: column.info.fqn()),
        )

    model = create_model(cls.__name__, **fields_dict)
    model.model_rebuild()
    return model

load_dataclass(model, data)

Load a list of row dicts into plain dataclass/annotated-class instances (no Pydantic validation).

Handles nested dataclasses (from ManyTable/OneTable annotations) by recursively loading JSON objects/arrays from the database into the appropriate types.

Source code in src/embar/model.py
340
341
342
343
344
345
346
347
348
def load_dataclass[T](model: type[T], data: list[dict[str, Any]]) -> list[T]:
    """
    Load a list of row dicts into plain dataclass/annotated-class instances
    (no Pydantic validation).

    Handles nested dataclasses (from ManyTable/OneTable annotations) by recursively
    loading JSON objects/arrays from the database into the appropriate types.
    """
    return [_load_one(model, row) for row in data]

load_results(model, data)

Load query result rows into model instances.

Dispatches between Pydantic validation (for BaseModel subclasses) and the plain dict→dataclass loader for everything else.

Source code in src/embar/model.py
325
326
327
328
329
330
331
332
333
334
335
336
337
def load_results[T](model: type[T], data: list[dict[str, Any]]) -> list[T]:
    """
    Load query result rows into model instances.

    Dispatches between Pydantic validation (for ``BaseModel`` subclasses) and
    the plain dict→dataclass loader for everything else.
    """
    if _PYDANTIC_AVAILABLE and isinstance(model, type) and issubclass(model, BaseModel):
        from pydantic import TypeAdapter

        adapter = TypeAdapter(list[model])
        return adapter.validate_python(data)
    return load_dataclass(model, data)

upgrade_model_nested_fields(model, use_pydantic)

Upgrade a model so that nested ManyTable/OneTable fields are resolved to concrete models.

For Pydantic models, creates a new subclass via create_model. For plain dataclasses/annotated classes, creates a new dataclass with upgraded field types.

use_pydantic controls whether nested table models are generated as Pydantic models or plain dataclasses, and must be supplied explicitly by the caller.

Source code in src/embar/model.py
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
def upgrade_model_nested_fields[B: DataModel](model: type[B], use_pydantic: bool) -> type[B]:
    """
    Upgrade a model so that nested `ManyTable`/`OneTable` fields are resolved to concrete models.

    For Pydantic models, creates a new subclass via `create_model`.
    For plain dataclasses/annotated classes, creates a new dataclass with upgraded field types.

    ``use_pydantic`` controls whether nested table models are generated as Pydantic models
    or plain dataclasses, and must be supplied explicitly by the caller.
    """
    type_hints = get_type_hints(model, include_extras=True)

    # Without pydantic, BaseModel is the stub class; no real subclass of it can exist,
    # so this branch is only reachable when _PYDANTIC_AVAILABLE is True anyway.
    if isinstance(model, type) and issubclass(model, BaseModel):
        from pydantic import create_model

        fields_dict: dict[str, Any] = {}
        for field_name, field_type in type_hints.items():
            new_type = _convert_annotation(field_type, use_pydantic=True)
            if new_type:
                fields_dict[field_name] = (new_type, None)
            else:
                fields_dict[field_name] = (field_type, None)

        new_class = create_model(model.__name__, __base__=model, **fields_dict)
        new_class.model_rebuild()
        return new_class

    # Plain dataclass / annotated-class path
    dc_fields: list[Any] = []
    for field_name, field_type in type_hints.items():
        new_type = _convert_annotation(field_type, use_pydantic=use_pydantic)
        resolved_type = new_type if new_type else field_type
        dc_fields.append((field_name, resolved_type, field(default=None)))

    new_class = make_dataclass(model.__name__, dc_fields)
    return new_class  # type: ignore[return-value]