Skip to content

Commit 2a83448

Browse files
committed
feat(api): Add edge and node apis
feat(api): Refactor node and edge models
1 parent b7dbdd9 commit 2a83448

7 files changed

Lines changed: 769 additions & 85 deletions

File tree

src/lsst/cmservice/db/campaigns_v2.py

Lines changed: 7 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1+
"""ORM Models for v2 tables and objects."""
2+
13
from datetime import datetime
24
from typing import Any
35
from uuid import NAMESPACE_DNS, UUID, uuid5
46

5-
from pydantic import AliasChoices, ValidationInfo, model_validator
7+
from pydantic import AliasChoices
68
from sqlalchemy.dialects import postgresql
79
from sqlalchemy.ext.mutable import MutableDict, MutableList
810
from sqlalchemy.types import PickleType
@@ -43,15 +45,6 @@ def jsonb_column(name: str, aliases: list[str] | None = None) -> Any:
4345
)
4446

4547

46-
# NOTES
47-
# - model validation is not triggered when table=True
48-
# - Every object model needs to have three flavors:
49-
# 1. the declarative model of the object's database table
50-
# 2. the model of the manifest when creating a new object
51-
# 3. the model of the manifest when updating an object
52-
# 4. a response model for APIs related to the object
53-
54-
5548
class BaseSQLModel(SQLModel):
5649
__table_args__ = {"schema": config.db.table_schema}
5750
metadata = metadata
@@ -72,26 +65,7 @@ class CampaignBase(BaseSQLModel):
7265
configuration: dict = jsonb_column("configuration", aliases=["configuration", "data", "spec"])
7366

7467

75-
class CampaignModel(CampaignBase):
76-
"""model used for resource creation."""
77-
78-
@model_validator(mode="before")
79-
@classmethod
80-
def custom_model_validator(cls, data: Any, info: ValidationInfo) -> Any:
81-
"""Validates the model based on different types of raw inputs,
82-
where some default non-optional fields can be auto-populated.
83-
"""
84-
if isinstance(data, dict):
85-
if "name" not in data:
86-
raise ValueError("'name' must be specified.")
87-
if "namespace" not in data:
88-
data["namespace"] = _default_campaign_namespace
89-
if "id" not in data:
90-
data["id"] = uuid5(namespace=data["namespace"], name=data["name"])
91-
return data
92-
93-
94-
class Campaign(CampaignModel, table=True):
68+
class Campaign(CampaignBase, table=True):
9569
"""Model used for database operations involving campaigns_v2 table rows"""
9670

9771
__tablename__: str = "campaigns_v2" # type: ignore[misc]
@@ -127,25 +101,7 @@ class NodeBase(BaseSQLModel):
127101
configuration: dict = jsonb_column("configuration", aliases=["configuration", "data", "spec"])
128102

129103

130-
class NodeModel(NodeBase):
131-
"""model validating class for Nodes"""
132-
133-
@model_validator(mode="before")
134-
@classmethod
135-
def custom_model_validator(cls, data: Any, info: ValidationInfo) -> Any:
136-
if isinstance(data, dict):
137-
if "version" not in data:
138-
data["version"] = 1
139-
if "name" not in data:
140-
raise ValueError("'name' must be specified.")
141-
if "namespace" not in data:
142-
data["namespace"] = _default_campaign_namespace
143-
if "id" not in data:
144-
data["id"] = uuid5(namespace=data["namespace"], name=f"""{data["name"]}.{data["version"]}""")
145-
return data
146-
147-
148-
class Node(NodeModel, table=True):
104+
class Node(NodeBase, table=True):
149105
__tablename__: str = "nodes_v2" # type: ignore[misc]
150106

151107
machine: UUID | None = Field(foreign_key="machines_v2.id", default=None, ondelete="CASCADE")
@@ -163,28 +119,12 @@ class EdgeBase(BaseSQLModel):
163119
configuration: dict = jsonb_column("configuration", aliases=["configuration", "data", "spec"])
164120

165121

166-
class EdgeModel(EdgeBase):
167-
"""model validating class for Edges"""
168-
169-
@model_validator(mode="before")
170-
@classmethod
171-
def custom_model_validator(cls, data: Any, info: ValidationInfo) -> Any:
172-
if isinstance(data, dict):
173-
if "name" not in data:
174-
raise ValueError("'name' must be specified.")
175-
if "namespace" not in data:
176-
raise ValueError("Edges may only exist in a 'namespace'.")
177-
if "id" not in data:
178-
data["id"] = uuid5(namespace=data["namespace"], name=data["name"])
179-
return data
180-
181-
182-
class EdgeResponseModel(EdgeModel):
122+
class EdgeResponseModel(EdgeBase):
183123
source: Any
184124
target: Any
185125

186126

187-
class Edge(EdgeModel, table=True):
127+
class Edge(EdgeBase, table=True):
188128
__tablename__: str = "edges_v2" # type: ignore[misc]
189129

190130

@@ -216,24 +156,6 @@ class ManifestBase(BaseSQLModel):
216156
spec: dict = jsonb_column("spec", aliases=["spec", "configuration", "data"])
217157

218158

219-
class ManifestModel(ManifestBase):
220-
"""model validating class for Manifests"""
221-
222-
@model_validator(mode="before")
223-
@classmethod
224-
def custom_model_validator(cls, data: Any, info: ValidationInfo) -> Any:
225-
if isinstance(data, dict):
226-
if "version" not in data:
227-
data["version"] = 1
228-
if "name" not in data:
229-
raise ValueError("'name' must be specified.")
230-
if "namespace" not in data:
231-
data["namespace"] = _default_campaign_namespace
232-
if "id" not in data:
233-
data["id"] = uuid5(namespace=data["namespace"], name=f"""{data["name"]}.{data["version"]}""")
234-
return data
235-
236-
237159
class Manifest(ManifestBase, table=True):
238160
__tablename__: str = "manifests_v2" # type: ignore[misc]
239161

src/lsst/cmservice/db/manifests_v2.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
"""
66

77
from typing import Self
8+
from uuid import uuid4
89

910
from pydantic import AliasChoices, BaseModel, ConfigDict, Field, ValidationInfo, model_validator
1011

@@ -99,3 +100,43 @@ def custom_model_validator(self, info: ValidationInfo) -> Self:
99100
raise ValueError("Campaigns may only be created from a <campaign> manifest")
100101

101102
return self
103+
104+
105+
class EdgeMetadata(ManifestMetadata):
106+
"""Metadata model for an Edge Manifest.
107+
108+
A default random alphanumeric 8-byte name is generated if no name provided.
109+
"""
110+
111+
name: str = Field(default_factory=lambda: uuid4().hex[:8])
112+
113+
114+
class EdgeSpec(ManifestSpec):
115+
"""Spec model for an Edge Manifest."""
116+
117+
source: str
118+
target: str
119+
120+
121+
class EdgeManifest(Manifest[EdgeMetadata, EdgeSpec]):
122+
"""validating model for Edges"""
123+
124+
@model_validator(mode="after")
125+
def custom_model_validator(self, info: ValidationInfo) -> Self:
126+
"""Validate an Edge Manifest after a model has been created."""
127+
if self.kind is not ManifestKind.edge:
128+
raise ValueError("Edges may only be created from an <edge> manifest")
129+
130+
return self
131+
132+
133+
class NodeManifest(Manifest[VersionedMetadata, ManifestSpec]):
134+
"""validating model for Nodes"""
135+
136+
@model_validator(mode="after")
137+
def custom_model_validator(self, info: ValidationInfo) -> Self:
138+
"""Validate a Node Manifest after a model has been created."""
139+
if self.kind is not ManifestKind.node:
140+
raise ValueError("Nodes may only be created from an <node> manifest")
141+
142+
return self

src/lsst/cmservice/routers/v2/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,16 @@
22

33
from . import (
44
campaigns,
5+
edges,
56
manifests,
7+
nodes,
68
)
79

810
router = APIRouter(
911
prefix="/v2",
1012
)
1113

1214
router.include_router(campaigns.router)
15+
router.include_router(edges.router)
1316
router.include_router(manifests.router)
17+
router.include_router(nodes.router)

0 commit comments

Comments
 (0)