Skip to content

Commit 00b4769

Browse files
committed
feat(api): Include order in paginated queries
- add crtime to element metadata
1 parent fbd79f3 commit 00b4769

4 files changed

Lines changed: 48 additions & 22 deletions

File tree

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

Lines changed: 21 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
from ...common.graph import graph_from_edge_list_v2, graph_to_dict
1717
from ...common.logging import LOGGER
18+
from ...common.timestamp import element_time
1819
from ...db.campaigns_v2 import Campaign, CampaignUpdate, Edge, Node
1920
from ...db.manifests_v2 import CampaignManifest
2021
from ...db.session import db_session_dependency
@@ -42,10 +43,16 @@ async def read_campaign_collection(
4243
offset: Annotated[int, Query()] = 0,
4344
) -> Sequence[Campaign]:
4445
"""A paginated API returning a list of all Campaigns known to the
45-
application.
46+
application, from newest to oldest.
4647
"""
4748
try:
48-
campaigns = await session.exec(select(Campaign).offset(offset).limit(limit))
49+
statement = (
50+
select(Campaign)
51+
.order_by(Campaign.metadata_["crtime"].desc().nulls_last())
52+
.offset(offset)
53+
.limit(limit)
54+
)
55+
campaigns = await session.exec(statement)
4956

5057
response.headers["Next"] = str(
5158
request.url_for("read_campaign_collection").include_query_params(
@@ -196,15 +203,16 @@ async def read_campaign_node_collection(
196203

197204
# The input could be a campaign UUID or it could be a literal name.
198205
# TODO this could just as well be a campaign query with a join to nodes
199-
s = select(Node)
206+
statement = select(Node).order_by(Node.metadata_["crtime"].asc().nulls_last())
207+
200208
try:
201209
if campaign_id := UUID(campaign_name):
202-
s = s.where(Node.namespace == campaign_id)
210+
statement = statement.where(Node.namespace == campaign_id)
203211
except ValueError:
204212
# FIXME get an id from a name
205213
raise HTTPException(status_code=422, detail="campaign_name must be a uuid")
206-
s = s.offset(offset).limit(limit)
207-
nodes = await session.exec(s)
214+
statement = statement.offset(offset).limit(limit)
215+
nodes = await session.exec(statement)
208216
response.headers["Next"] = str(
209217
request.url_for(
210218
"read_campaign_node_collection",
@@ -252,7 +260,7 @@ async def read_campaign_edge_collection(
252260
.join_from(Edge, target_nodes, Edge.target == target_nodes.id)
253261
)
254262
else:
255-
s = select(Edge)
263+
s = select(Edge).order_by(col(Edge.name).asc().nulls_last())
256264
try:
257265
if campaign_id := UUID(campaign_name):
258266
s = s.where(Edge.namespace == campaign_id)
@@ -313,10 +321,12 @@ async def create_campaign_resource(
313321
# Create a campaign spec from the manifest, delegating the creation of new
314322
# dynamic fields to the model validation method, -OR- create new dynamic
315323
# fields here.
324+
campaign_metadata = manifest.metadata_.model_dump()
325+
campaign_metadata |= {"crtime": element_time()}
316326
campaign = Campaign.model_validate(
317327
dict(
318-
name=manifest.metadata_.name,
319-
metadata_=manifest.metadata_.model_dump(),
328+
name=campaign_metadata.pop("name"),
329+
metadata_=campaign_metadata,
320330
# owner = ... # TODO Get username from gafaelfawr # noqa: ERA001
321331
)
322332
)
@@ -376,12 +386,8 @@ async def read_campaign_graph(
376386
edges = (await session.exec(statement)).all()
377387

378388
# Organize the edges into a graph. The graph nodes are annotated with their
379-
# current database attributes.
380-
# TODO it makes sense for the graph to include expunged Nodes in the meta-
381-
# data for campaign processing, but for the purposes of this api route,
382-
# only the most relevant information should be associated with each node,
383-
# e.g., its name, status, id, and its URL
389+
# current database attributes according to the "simple" node view.
384390
graph = await graph_from_edge_list_v2(edges=edges, node_type=Node, session=session, node_view="simple")
385391

386-
response.headers["Self"] = ""
392+
response.headers["Self"] = str(request.url_for("read_campaign_resource", campaign_name=campaign_id))
387393
return graph_to_dict(graph)

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from uuid import UUID, uuid5
1010

1111
from fastapi import APIRouter, Depends, HTTPException, Query, Request, Response
12-
from sqlmodel import select
12+
from sqlmodel import col, select
1313
from sqlmodel.ext.asyncio.session import AsyncSession
1414

1515
from ...common.logging import LOGGER
@@ -46,7 +46,8 @@ async def read_edges_collection(
4646
For campaign-scoped edges, one should use the /campaigns/{}/edges route.
4747
"""
4848
try:
49-
edges = await session.exec(select(Edge).offset(offset).limit(limit))
49+
statement = select(Edge).order_by(col(Edge.name).desc()).offset(offset).limit(limit)
50+
edges = await session.exec(statement)
5051
response.headers["Next"] = (
5152
request.url_for("read_edges_collection")
5253
.include_query_params(offset=(offset + limit), limit=limit)

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

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
from ...common.jsonpatch import JSONPatch, JSONPatchError, apply_json_patch
1616
from ...common.logging import LOGGER
17+
from ...common.timestamp import element_time
1718
from ...db.campaigns_v2 import Campaign, Manifest, _default_campaign_namespace
1819
from ...db.manifests_v2 import ManifestModel
1920
from ...db.session import db_session_dependency
@@ -51,7 +52,13 @@ async def read_manifest_collection(
5152
)
5253
)
5354
try:
54-
nodes = await session.exec(select(Manifest).offset(offset).limit(limit))
55+
statement = (
56+
select(Manifest)
57+
.order_by(Manifest.metadata_["crtime"].desc().nulls_last())
58+
.offset(offset)
59+
.limit(limit)
60+
)
61+
nodes = await session.exec(statement)
5562
return nodes.all()
5663
except Exception as msg:
5764
logger.exception()
@@ -150,13 +157,16 @@ async def create_one_or_more_manifests(
150157
_previous = (await session.exec(s)).one_or_none()
151158
_version = _previous.version if _previous else manifest.metadata_.version
152159
_version += 1
160+
161+
_manifest_metadata = manifest.metadata_.model_dump()
162+
_manifest_metadata |= {"crtime": element_time()}
153163
_manifest = Manifest(
154164
id=uuid5(_namespace_uuid, f"{_name}.{_version}"),
155-
name=_name,
165+
name=_manifest_metadata.pop("name"),
156166
namespace=_namespace_uuid,
157167
kind=manifest.kind,
158168
version=_version,
159-
metadata_=manifest.metadata_.model_dump(),
169+
metadata_=_manifest_metadata,
160170
spec=manifest.spec.model_dump(),
161171
)
162172

@@ -240,6 +250,7 @@ async def update_manifest_resource(
240250
)
241251

242252
# create Manifest from new_manifest, add to session, and commit
253+
new_manifest["metadata"] |= {"crtime": element_time()}
243254
new_manifest_db = Manifest.model_validate(new_manifest)
244255
session.add(new_manifest_db)
245256
await session.commit()

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

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
from ...common.jsonpatch import JSONPatch, JSONPatchError, apply_json_patch
1616
from ...common.logging import LOGGER
17+
from ...common.timestamp import element_time
1718
from ...db.campaigns_v2 import Campaign, Node
1819
from ...db.manifests_v2 import NodeManifest
1920
from ...db.session import db_session_dependency
@@ -47,7 +48,10 @@ async def read_nodes_collection(
4748
For campaign-scoped nodes, one should use the /campaigns/{}/nodes route.
4849
"""
4950
try:
50-
nodes = await session.exec(select(Node).offset(offset).limit(limit))
51+
statement = (
52+
select(Node).order_by(Node.metadata_["crtime"].desc().nulls_last()).offset(offset).limit(limit)
53+
)
54+
nodes = await session.exec(statement)
5155
response.headers["Next"] = (
5256
request.url_for("read_nodes_collection")
5357
.include_query_params(offset=(offset + limit), limit=limit)
@@ -136,12 +140,15 @@ async def create_node_resource(
136140

137141
node_version = previous_node.version if previous_node else node_version
138142
node_version += 1
143+
node_metadata = manifest.metadata_.model_dump()
144+
node_metadata |= {"crtime": element_time()}
139145
node = Node(
140146
id=uuid5(node_namespace_uuid, f"{node_name}.{node_version}"),
141-
name=node_name,
147+
name=node_metadata.pop("name"),
142148
namespace=node_namespace_uuid,
143149
version=node_version,
144150
configuration=manifest.spec.model_dump(),
151+
metadata_=node_metadata,
145152
)
146153

147154
# Put the node in the database
@@ -228,6 +235,7 @@ async def update_node_resource(
228235
)
229236

230237
# create Manifest from new_manifest, add to session, and commit
238+
new_manifest["metadata"] |= {"crtime": element_time()}
231239
new_manifest_db = Node.model_validate(new_manifest)
232240
session.add(new_manifest_db)
233241
await session.commit()

0 commit comments

Comments
 (0)