Skip to content

Commit 451c5e5

Browse files
committed
Fixes
- use "uuid" instead of "id" in graph node simple mode - require namespace when GET node by name - include additional links in campaign response header - campaign post returns existing campaign on duplicate - improve consistency in route parameter naming - delegate more route typing to pydantic models - use sorting in activity log route
1 parent a9020f6 commit 451c5e5

10 files changed

Lines changed: 196 additions & 182 deletions

File tree

src/lsst/cmservice/common/graph.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,17 +79,17 @@ async def graph_from_edge_list_v2(
7979
# for the simple node view, the goal is to minimize the amount of
8080
# data attached to the node and ensure that this data is json-
8181
# serializable and otherwise appropriate for an API response
82-
g.nodes[node]["id"] = str(db_node.id)
82+
g.nodes[node]["uuid"] = str(db_node.id)
8383
g.nodes[node]["status"] = db_node.status.name
8484
g.nodes[node]["kind"] = db_node.kind.name
85+
g.nodes[node]["version"] = db_node.version
8586
relabel_mapping[node] = db_node.name
8687
else:
8788
g.nodes[node]["model"] = db_node
8889

8990
if relabel_mapping:
9091
g = nx.relabel_nodes(g, mapping=relabel_mapping, copy=False)
9192

92-
# TODO validate graph now raise exception, or leave it to the caller?
9393
return g
9494

9595

src/lsst/cmservice/db/campaigns_v2.py

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
"""ORM Models for v2 tables and objects."""
22

3-
from datetime import datetime
3+
from collections.abc import MutableSequence
44
from typing import Any
55
from uuid import NAMESPACE_DNS, UUID, uuid4, uuid5
66

7-
from pydantic import AliasChoices, ValidationInfo, model_validator
7+
from pydantic import AliasChoices, AwareDatetime, ValidationInfo, model_validator
88
from sqlalchemy.dialects import postgresql
99
from sqlalchemy.ext.mutable import MutableDict, MutableList
1010
from sqlalchemy.types import PickleType
@@ -99,6 +99,20 @@ class CampaignUpdate(BaseSQLModel):
9999
status: StatusField | None = None
100100

101101

102+
class CampaignSummary(CampaignBase):
103+
"""Model for the response of a Campaign Summary route."""
104+
105+
node_summary: MutableSequence["NodeStatusSummary"]
106+
107+
108+
class NodeStatusSummary(BaseSQLModel):
109+
"""Model for a Node Status Summary."""
110+
111+
status: StatusField = Field(description="A state name")
112+
count: int = Field(description="Count of nodes in this state")
113+
mtime: AwareDatetime | None = Field(description="The most recent update time for nodes in this state")
114+
115+
102116
class NodeBase(BaseSQLModel):
103117
"""nodes_v2 db table"""
104118

@@ -212,17 +226,17 @@ class Task(BaseSQLModel, table=True):
212226
namespace: UUID = Field(foreign_key="campaigns_v2.id", description="The ID of a Campaign")
213227
node: UUID = Field(foreign_key="nodes_v2.id", description="The ID of the target node")
214228
priority: int | None = Field(default=None)
215-
created_at: datetime = Field(
229+
created_at: AwareDatetime = Field(
216230
description="The `datetime` (UTC) at which this Task was first added to the queue",
217231
default_factory=now_utc,
218232
sa_column=Column(DateTime(timezone=True)),
219233
)
220-
submitted_at: datetime | None = Field(
234+
submitted_at: AwareDatetime | None = Field(
221235
description="The `datetime` (UTC) at which this Task was first submitted as work to the event loop",
222236
default=None,
223237
sa_column=Column(DateTime(timezone=True)),
224238
)
225-
finished_at: datetime | None = Field(
239+
finished_at: AwareDatetime | None = Field(
226240
description=(
227241
"The `datetime` (UTC) at which this Task successfully finalized. "
228242
"A Task whose `finished_at` is not `None` is tombstoned and is subject to deletion."
@@ -251,12 +265,12 @@ class ActivityLogBase(BaseSQLModel):
251265
namespace: UUID = Field(foreign_key="campaigns_v2.id", description="The ID of a Campaign")
252266
node: UUID | None = Field(default=None, foreign_key="nodes_v2.id", description="The ID of a Node")
253267
operator: str = Field(description="The name of the operator or pilot who triggered the activity")
254-
created_at: datetime = Field(
268+
created_at: AwareDatetime = Field(
255269
description="The `datetime` in UTC at which this log entry was created.",
256270
default_factory=now_utc,
257271
sa_column=Column(DateTime(timezone=True)),
258272
)
259-
finished_at: datetime | None = Field(
273+
finished_at: AwareDatetime | None = Field(
260274
description="The `datetime` in UTC at which this log entry was finalized.",
261275
default=None,
262276
sa_column=Column(DateTime(timezone=True), nullable=True),

src/lsst/cmservice/db/manifests_v2.py

Lines changed: 21 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from pydantic import AliasChoices, BaseModel, ConfigDict, Field, ValidationInfo, model_validator
1111

1212
from ..common.enums import DEFAULT_NAMESPACE, ManifestKind
13+
from ..common.timestamp import element_time
1314
from ..common.types import KindField
1415

1516

@@ -30,17 +31,6 @@ class Manifest[MetadataT, SpecT](BaseModel):
3031
)
3132

3233

33-
class ManifestMetadata(BaseModel):
34-
"""Generic metadata model for Manifests.
35-
36-
Conventionally denormalized fields are excluded from the model_dump when
37-
serialized for ORM use.
38-
"""
39-
40-
name: str
41-
namespace: str
42-
43-
4434
class ManifestSpec(BaseModel):
4535
"""Generic spec model for Manifests.
4636
@@ -55,18 +45,30 @@ class ManifestSpec(BaseModel):
5545
model_config = ConfigDict(extra="allow")
5646

5747

48+
class ManifestMetadata(BaseModel):
49+
"""Generic metadata model for Manifests.
50+
51+
Conventionally denormalized fields are excluded from the model_dump when
52+
serialized for ORM use.
53+
"""
54+
55+
name: str = Field(exclude=True)
56+
namespace: str = Field(exclude=True)
57+
crtime: int = Field(default_factory=element_time)
58+
59+
5860
class VersionedMetadata(ManifestMetadata):
5961
"""Metadata model for versioned Manifests."""
6062

61-
version: int = 0
63+
version: int = Field(exclude=True, default=0)
6264

6365

6466
class ManifestModelMetadata(VersionedMetadata):
6567
"""Manifest model for general Manifests. These manifests are versioned but
66-
a namespace is optional.
68+
a namespace is optional (defaultable).
6769
"""
6870

69-
namespace: str = Field(default=str(DEFAULT_NAMESPACE))
71+
namespace: str = Field(default=str(DEFAULT_NAMESPACE), exclude=True)
7072

7173

7274
class ManifestModel(Manifest[ManifestModelMetadata, ManifestSpec]):
@@ -81,16 +83,7 @@ def custom_model_validator(self, info: ValidationInfo) -> Self:
8183
return self
8284

8385

84-
class CampaignMetadata(BaseModel):
85-
"""Metadata model for a Campaign Manifest.
86-
87-
Campaign metadata does not require a namespace field.
88-
"""
89-
90-
name: str
91-
92-
93-
class CampaignManifest(Manifest[CampaignMetadata, ManifestSpec]):
86+
class CampaignManifest(Manifest[ManifestModelMetadata, ManifestSpec]):
9487
"""validating model for campaigns"""
9588

9689
@model_validator(mode="after")
@@ -108,14 +101,15 @@ class EdgeMetadata(ManifestMetadata):
108101
A default random alphanumeric 8-byte name is generated if no name provided.
109102
"""
110103

111-
name: str = Field(default_factory=lambda: uuid4().hex[:8])
104+
name: str = Field(default_factory=lambda: uuid4().hex[:8], exclude=True)
105+
crtime: int = Field(default_factory=element_time)
112106

113107

114108
class EdgeSpec(ManifestSpec):
115109
"""Spec model for an Edge Manifest."""
116110

117-
source: str
118-
target: str
111+
source: str = Field(exclude=True)
112+
target: str = Field(exclude=True)
119113

120114

121115
class EdgeManifest(Manifest[EdgeMetadata, EdgeSpec]):

src/lsst/cmservice/machines/campaign.py

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -105,11 +105,6 @@ async def prepare_activity_log(self, event: EventData) -> None:
105105
if TYPE_CHECKING:
106106
assert self.db_model is not None
107107

108-
# TODO the activity log doesn't really support campaign-level entries
109-
# because the node field has a FK constraint, so the convention intro-
110-
# duced here is to use the mandatory and deterministic "START" node
111-
# for these entries.
112-
113108
if self.activity_log_entry is not None:
114109
return None
115110

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from uuid import UUID
77

88
from fastapi import APIRouter, Depends, HTTPException, Query, Request, Response
9-
from sqlmodel import select
9+
from sqlmodel import col, select
1010
from sqlmodel.ext.asyncio.session import AsyncSession
1111

1212
from ...common.logging import LOGGER
@@ -56,7 +56,7 @@ async def read_activity_collection(
5656
if since is not None:
5757
statement = statement.where(ActivityLog.finished_at >= since) # type: ignore[operator]
5858

59-
statement = statement.offset(offset).limit(limit)
59+
statement = statement.order_by(col(ActivityLog.created_at).desc()).offset(offset).limit(limit)
6060
activity_logs = (await session.exec(statement)).all()
6161
return activity_logs
6262

@@ -77,7 +77,7 @@ async def read_activity_resource(
7777
# set the response headers
7878
if activity_log is not None:
7979
response.headers["Campaign"] = str(
80-
request.url_for("read_campaign_resource", campaign_name=activity_log.namespace)
80+
request.url_for("read_campaign_resource", campaign_name_or_id=activity_log.namespace)
8181
)
8282
response.headers["Node"] = str(request.url_for("read_node_resource", node_name=activity_log.node))
8383
response.headers["Self"] = str(

0 commit comments

Comments
 (0)