|
4 | 4 | representing campaign objects within CM-Service. |
5 | 5 | """ |
6 | 6 |
|
7 | | -from collections.abc import Sequence |
| 7 | +from collections.abc import Mapping, Sequence |
8 | 8 | from typing import TYPE_CHECKING, Annotated |
9 | 9 | from uuid import UUID, uuid5 |
10 | 10 |
|
|
13 | 13 | from sqlmodel import col, select |
14 | 14 | from sqlmodel.ext.asyncio.session import AsyncSession |
15 | 15 |
|
| 16 | +from ...common.graph import graph_from_edge_list_v2, graph_to_dict |
16 | 17 | from ...common.logging import LOGGER |
17 | 18 | from ...db.campaigns_v2 import Campaign, CampaignUpdate, Edge, Node |
18 | 19 | from ...db.manifests_v2 import CampaignManifest |
@@ -40,7 +41,9 @@ async def read_campaign_collection( |
40 | 41 | limit: Annotated[int, Query(le=100)] = 10, |
41 | 42 | offset: Annotated[int, Query()] = 0, |
42 | 43 | ) -> Sequence[Campaign]: |
43 | | - """...""" |
| 44 | + """A paginated API returning a list of all Campaigns known to the |
| 45 | + application. |
| 46 | + """ |
44 | 47 | try: |
45 | 48 | campaigns = await session.exec(select(Campaign).offset(offset).limit(limit)) |
46 | 49 |
|
@@ -187,7 +190,9 @@ async def read_campaign_node_collection( |
187 | 190 | limit: Annotated[int, Query(le=100)] = 10, |
188 | 191 | offset: Annotated[int, Query()] = 0, |
189 | 192 | ) -> Sequence[Node]: |
190 | | - # This is a convenience api that could also be `/nodes?campaign=... |
| 193 | + """A paginated API returning a list of all Nodes in the namespace of a |
| 194 | + single Campaign. |
| 195 | + """ |
191 | 196 |
|
192 | 197 | # The input could be a campaign UUID or it could be a literal name. |
193 | 198 | # TODO this could just as well be a campaign query with a join to nodes |
@@ -222,7 +227,10 @@ async def read_campaign_edge_collection( |
222 | 227 | *, |
223 | 228 | resolve_names: bool = False, |
224 | 229 | ) -> Sequence[Edge]: |
225 | | - # This is a convenience api that could also be `/edges?campaign=... |
| 230 | + """A paginated API returning a list of all Edges in the namespace of a |
| 231 | + single Campaign. This list of Edges can be used to construct the Campaign |
| 232 | + graph. |
| 233 | + """ |
226 | 234 |
|
227 | 235 | # The input could be a campaign UUID or it could be a literal name. |
228 | 236 | # This is why raw SQL is better than ORMs |
@@ -301,6 +309,7 @@ async def create_campaign_resource( |
301 | 309 | session: Annotated[AsyncSession, Depends(db_session_dependency)], |
302 | 310 | manifest: CampaignManifest, |
303 | 311 | ) -> Campaign: |
| 312 | + """An API to create a Campaign from an appropriate Manifest.""" |
304 | 313 | # Create a campaign spec from the manifest, delegating the creation of new |
305 | 314 | # dynamic fields to the model validation method, -OR- create new dynamic |
306 | 315 | # fields here. |
@@ -333,3 +342,46 @@ async def create_campaign_resource( |
333 | 342 | ) |
334 | 343 |
|
335 | 344 | return campaign |
| 345 | + |
| 346 | + |
| 347 | +@router.get( |
| 348 | + "/{campaign_name_or_id}/graph", |
| 349 | + status_code=200, |
| 350 | + summary="Construct and return a Campaign's graph of nodes", |
| 351 | +) |
| 352 | +async def read_campaign_graph( |
| 353 | + request: Request, |
| 354 | + response: Response, |
| 355 | + campaign_name_or_id: str, |
| 356 | + session: Annotated[AsyncSession, Depends(db_session_dependency)], |
| 357 | +) -> Mapping: |
| 358 | + """Reads the graph resource for a campaign and returns its JSON represent- |
| 359 | + ation as serialized by the ``networkx.node_link_data()` function, i.e, the |
| 360 | + "node-link format". |
| 361 | + """ |
| 362 | + |
| 363 | + # The input could be a campaign UUID or it could be a literal name. |
| 364 | + campaign_id: UUID | None |
| 365 | + try: |
| 366 | + campaign_id = UUID(campaign_name_or_id) |
| 367 | + except ValueError: |
| 368 | + s = select(Campaign.id).where(Campaign.name == campaign_name_or_id) |
| 369 | + campaign_id = (await session.exec(s)).one_or_none() |
| 370 | + |
| 371 | + if campaign_id is None: |
| 372 | + raise HTTPException(status_code=404, detail="No such campaign found.") |
| 373 | + |
| 374 | + # Fetch the Edges for the campaign |
| 375 | + statement = select(Edge).filter_by(namespace=campaign_id) |
| 376 | + edges = (await session.exec(statement)).all() |
| 377 | + |
| 378 | + # 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 |
| 384 | + graph = await graph_from_edge_list_v2(edges=edges, node_type=Node, session=session, node_view="simple") |
| 385 | + |
| 386 | + response.headers["Self"] = "" |
| 387 | + return graph_to_dict(graph) |
0 commit comments