Skip to content

Commit ba26d4a

Browse files
committed
feat(db): Extend v2 model fields with pydantic aliases
1 parent 12c126b commit ba26d4a

2 files changed

Lines changed: 72 additions & 15 deletions

File tree

src/lsst/cmservice/db/campaigns_v2.py

Lines changed: 38 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
from typing import Annotated, Any
33
from uuid import NAMESPACE_DNS, UUID, uuid5
44

5-
from pydantic import PlainSerializer, PlainValidator, ValidationInfo, model_validator
5+
from pydantic import AliasChoices, PlainSerializer, PlainValidator, ValidationInfo, model_validator
66
from sqlalchemy.dialects import postgresql
77
from sqlalchemy.ext.mutable import MutableDict
88
from sqlalchemy.types import PickleType
@@ -15,11 +15,27 @@
1515
"""Default UUID5 namespace for campaigns"""
1616

1717

18-
def jsonb_column(name: str) -> Any:
19-
"""Constructor for a Field based on a JSONB database column."""
18+
def jsonb_column(name: str, aliases: list[str] | None = None) -> Any:
19+
"""Constructor for a Field based on a JSONB database column.
20+
21+
If provided, a list of aliases will be used to construct a pydantic
22+
``AliasChoices`` object for the field's validation alias, which improves
23+
usability by making model validation more flexible (e.g., having "metadata"
24+
and "metadata_" refer to the same field).
25+
26+
Additionally, the first alias in the list will be used for the model's
27+
serialization alias.
28+
"""
29+
schema_extra = {}
30+
if aliases:
31+
schema_extra = {
32+
"validation_alias": AliasChoices(*aliases),
33+
"serialization_alias": aliases[0],
34+
}
2035
return Field(
2136
sa_column=Column(name, MutableDict.as_mutable(postgresql.JSONB)),
2237
default_factory=dict,
38+
schema_extra=schema_extra,
2339
)
2440

2541

@@ -68,8 +84,8 @@ class CampaignBase(BaseSQLModel):
6884
default=StatusEnum.waiting,
6985
sa_column=Column("status", Enum(StatusEnum, length=20, native_enum=False, create_constraint=False)),
7086
)
71-
metadata_: dict = jsonb_column("metadata")
72-
configuration: dict = jsonb_column("configuration")
87+
metadata_: dict = jsonb_column("metadata", aliases=["metadata", "metadata_"])
88+
configuration: dict = jsonb_column("configuration", aliases=["configuration", "data", "spec"])
7389

7490

7591
class CampaignModel(CampaignBase):
@@ -114,8 +130,8 @@ class NodeBase(BaseSQLModel):
114130
default=StatusEnum.waiting,
115131
sa_column=Column("status", Enum(StatusEnum, length=20, native_enum=False, create_constraint=False)),
116132
)
117-
metadata_: dict = jsonb_column("metadata")
118-
configuration: dict = jsonb_column("configuration")
133+
metadata_: dict = jsonb_column("metadata", aliases=["metadata", "metadata_"])
134+
configuration: dict = jsonb_column("configuration", aliases=["configuration", "data", "spec"])
119135

120136

121137
class NodeModel(NodeBase):
@@ -150,8 +166,8 @@ class EdgeBase(BaseSQLModel):
150166
namespace: UUID = Field(foreign_key="campaigns_v2.id")
151167
source: UUID = Field(foreign_key="nodes_v2.id")
152168
target: UUID = Field(foreign_key="nodes_v2.id")
153-
metadata_: dict = jsonb_column("metadata")
154-
configuration: dict = jsonb_column("configuration")
169+
metadata_: dict = jsonb_column("metadata", aliases=["metadata", "metadata_"])
170+
configuration: dict = jsonb_column("configuration", aliases=["configuration", "data", "spec"])
155171

156172

157173
class EdgeModel(EdgeBase):
@@ -197,8 +213,8 @@ class ManifestBase(BaseSQLModel):
197213
default=ManifestKind.other,
198214
sa_column=Column("kind", Enum(ManifestKind, length=20, native_enum=False, create_constraint=False)),
199215
)
200-
metadata_: dict = jsonb_column("metadata")
201-
spec: dict = jsonb_column("spec")
216+
metadata_: dict = jsonb_column("metadata", aliases=["metadata", "metadata_"])
217+
spec: dict = jsonb_column("spec", aliases=["spec", "configuration", "data"])
202218

203219

204220
class ManifestModel(ManifestBase):
@@ -236,10 +252,17 @@ class Task(SQLModel, table=True):
236252
last_processed_at: datetime
237253
finished_at: datetime
238254
wms_id: str
239-
# TODO site_affinity should probably be a mutable array
240-
site_affinity: list[str] = Field(sa_column=Column(postgresql.ARRAY(String())))
241-
status: int # TODO make enum
242-
previous_status: int # TODO make enum
255+
site_affinity: list[str] = Field(
256+
sa_column=Column("site_affinity", MutableList.as_mutable(postgresql.ARRAY(String())))
257+
)
258+
status: StatusField = Field(
259+
sa_column=Column("status", Enum(StatusEnum, length=20, native_enum=False, create_constraint=False)),
260+
)
261+
previous_status: StatusField = Field(
262+
sa_column=Column(
263+
"previous_status", Enum(StatusEnum, length=20, native_enum=False, create_constraint=False)
264+
),
265+
)
243266

244267

245268
class ActivityLogBase(BaseSQLModel):
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
from typing import Annotated
2+
3+
from pydantic import AliasChoices
4+
from sqlmodel import Field, SQLModel
5+
6+
from ..common.enums import ManifestKind
7+
from .campaigns_v2 import EnumSerializer, ManifestKindEnumValidator
8+
9+
10+
# this can probably be a BaseModel since this is not a db relation, but the
11+
# distinction probably doesn't matter
12+
class ManifestWrapper(SQLModel):
13+
"""a model for an object's Manifest wrapper, used by APIs where the `spec`
14+
should be the kind's table model, more or less.
15+
"""
16+
17+
apiversion: str = Field(default="io.lsst.cmservice/v1")
18+
kind: Annotated[ManifestKind, ManifestKindEnumValidator, EnumSerializer] = Field(
19+
default=ManifestKind.other,
20+
)
21+
metadata_: dict = Field(
22+
default_factory=dict,
23+
schema_extra={
24+
"validation_alias": AliasChoices("metadata", "metadata_"),
25+
"serialization_alias": "metadata",
26+
},
27+
)
28+
spec: dict = Field(
29+
default_factory=dict,
30+
schema_extra={
31+
"validation_alias": AliasChoices("spec", "configuration", "data"),
32+
"serialization_alias": "spec",
33+
},
34+
)

0 commit comments

Comments
 (0)