Skip to content
This repository was archived by the owner on May 18, 2026. It is now read-only.

Commit 32fdbbf

Browse files
committed
Optimize Visualization Page load
1 parent 5990995 commit 32fdbbf

5 files changed

Lines changed: 252 additions & 0 deletions

File tree

domains/sta/services/datastream.py

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,105 @@ def list(
204204
for datastream in queryset.all()
205205
]
206206

207+
def list_visualization_bootstrap(
208+
self,
209+
principal: Optional[User | APIKey],
210+
filtering: Optional[dict] = None,
211+
) -> dict[str, Sequence[dict]]:
212+
filtering = filtering or {}
213+
queryset = Datastream.objects.visible(principal=principal)
214+
215+
if "thing__workspace_id" in filtering:
216+
queryset = self.apply_filters(
217+
queryset,
218+
"thing__workspace_id",
219+
filtering["thing__workspace_id"],
220+
)
221+
222+
datastream_rows = list(
223+
queryset.select_related("thing", "observed_property", "processing_level")
224+
.order_by("id")
225+
.values(
226+
"id",
227+
"name",
228+
"thing_id",
229+
"thing__workspace_id",
230+
"thing__name",
231+
"thing__sampling_feature_code",
232+
"observed_property_id",
233+
"observed_property__name",
234+
"observed_property__code",
235+
"processing_level_id",
236+
"processing_level__definition",
237+
"unit_id",
238+
"no_data_value",
239+
"value_count",
240+
"phenomenon_begin_time",
241+
"phenomenon_end_time",
242+
"intended_time_spacing",
243+
"intended_time_spacing_unit",
244+
)
245+
.distinct()
246+
)
247+
248+
things_by_id: dict[str, dict] = {}
249+
observed_properties_by_id: dict[str, dict] = {}
250+
processing_levels_by_id: dict[str, dict] = {}
251+
datastreams: list[dict] = []
252+
253+
for row in datastream_rows:
254+
thing_id = str(row["thing_id"])
255+
observed_property_id = str(row["observed_property_id"])
256+
processing_level_id = str(row["processing_level_id"])
257+
258+
things_by_id.setdefault(
259+
thing_id,
260+
{
261+
"id": thing_id,
262+
"workspace_id": str(row["thing__workspace_id"]),
263+
"name": row["thing__name"],
264+
"sampling_feature_code": row["thing__sampling_feature_code"],
265+
},
266+
)
267+
observed_properties_by_id.setdefault(
268+
observed_property_id,
269+
{
270+
"id": observed_property_id,
271+
"name": row["observed_property__name"],
272+
"code": row["observed_property__code"],
273+
},
274+
)
275+
processing_levels_by_id.setdefault(
276+
processing_level_id,
277+
{
278+
"id": processing_level_id,
279+
"definition": row["processing_level__definition"],
280+
},
281+
)
282+
datastreams.append(
283+
{
284+
"id": str(row["id"]),
285+
"name": row["name"],
286+
"thing_id": thing_id,
287+
"observed_property_id": observed_property_id,
288+
"processing_level_id": processing_level_id,
289+
"unit_id": str(row["unit_id"]),
290+
"no_data_value": row["no_data_value"],
291+
"value_count": row["value_count"],
292+
"phenomenon_begin_time": row["phenomenon_begin_time"],
293+
"phenomenon_end_time": row["phenomenon_end_time"],
294+
"intended_time_spacing": row["intended_time_spacing"],
295+
"intended_time_spacing_unit": row["intended_time_spacing_unit"],
296+
}
297+
)
298+
299+
return {
300+
"things": list(things_by_id.values()),
301+
"datastreams": datastreams,
302+
"observed_properties": list(observed_properties_by_id.values()),
303+
"processing_levels": list(processing_levels_by_id.values()),
304+
}
305+
207306
def get(
208307
self,
209308
principal: Optional[User | APIKey],

interfaces/api/schemas/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,8 @@
7777
UnitPatchBody,
7878
)
7979
from .datastream import (
80+
DatastreamVisualizationBootstrapQueryParameters,
81+
DatastreamVisualizationBootstrapResponse,
8082
DatastreamSummaryResponse,
8183
DatastreamDetailResponse,
8284
DatastreamQueryParameters,

interfaces/api/schemas/datastream.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
BaseGetResponse,
88
BasePostBody,
99
BasePatchBody,
10+
BaseQueryParameters,
1011
CollectionQueryParameters,
1112
)
1213
from .attachment import TagGetResponse, FileAttachmentGetResponse
@@ -164,6 +165,54 @@ class DatastreamQueryParameters(CollectionQueryParameters):
164165
)
165166

166167

168+
class DatastreamVisualizationBootstrapQueryParameters(BaseQueryParameters):
169+
thing__workspace_id: list[uuid.UUID] = Query(
170+
[], description="Filter visualization bootstrap datastreams by workspace ID.", alias="workspace_id"
171+
)
172+
173+
174+
class VisualizationThingResponse(BaseGetResponse):
175+
id: uuid.UUID
176+
workspace_id: uuid.UUID
177+
name: str = Field(..., max_length=200)
178+
sampling_feature_code: str = Field(..., max_length=200)
179+
180+
181+
class VisualizationObservedPropertyResponse(BaseGetResponse):
182+
id: uuid.UUID
183+
name: str = Field(..., max_length=255)
184+
code: str = Field(..., max_length=255)
185+
186+
187+
class VisualizationProcessingLevelResponse(BaseGetResponse):
188+
id: uuid.UUID
189+
definition: Optional[str] = None
190+
191+
192+
class VisualizationDatastreamResponse(BaseGetResponse):
193+
id: uuid.UUID
194+
name: str = Field(..., max_length=255)
195+
thing_id: uuid.UUID
196+
observed_property_id: uuid.UUID
197+
processing_level_id: uuid.UUID
198+
unit_id: uuid.UUID
199+
no_data_value: float
200+
value_count: Optional[int] = Field(None, ge=0)
201+
phenomenon_begin_time: Optional[ISODatetime] = None
202+
phenomenon_end_time: Optional[ISODatetime] = None
203+
intended_time_spacing: Optional[float] = None
204+
intended_time_spacing_unit: Optional[
205+
Literal["seconds", "minutes", "hours", "days"]
206+
] = None
207+
208+
209+
class DatastreamVisualizationBootstrapResponse(BaseGetResponse):
210+
things: list[VisualizationThingResponse]
211+
datastreams: list[VisualizationDatastreamResponse]
212+
observed_properties: list[VisualizationObservedPropertyResponse]
213+
processing_levels: list[VisualizationProcessingLevelResponse]
214+
215+
167216
class DatastreamSummaryResponse(
168217
BaseGetResponse, DatastreamFields, DatastreamRelatedFields
169218
):

interfaces/api/views/datastream.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
from interfaces.http.request import HydroServerHttpRequest
99
from interfaces.api.schemas import VocabularyQueryParameters
1010
from interfaces.api.schemas import (
11+
DatastreamVisualizationBootstrapQueryParameters,
12+
DatastreamVisualizationBootstrapResponse,
1113
DatastreamSummaryResponse,
1214
DatastreamDetailResponse,
1315
DatastreamQueryParameters,
@@ -55,6 +57,29 @@ def get_datastreams(
5557
)
5658

5759

60+
@datastream_router.get(
61+
"/visualization-bootstrap",
62+
auth=[session_auth, bearer_auth, apikey_auth, anonymous_auth],
63+
response={
64+
200: DatastreamVisualizationBootstrapResponse,
65+
401: str,
66+
},
67+
by_alias=True,
68+
)
69+
def get_datastream_visualization_bootstrap(
70+
request: HydroServerHttpRequest,
71+
query: Query[DatastreamVisualizationBootstrapQueryParameters],
72+
):
73+
"""
74+
Get the lean metadata required to bootstrap the visualization page.
75+
"""
76+
77+
return 200, datastream_service.list_visualization_bootstrap(
78+
principal=request.principal,
79+
filtering=query.dict(exclude_unset=True),
80+
)
81+
82+
5883
@datastream_router.post(
5984
"",
6085
auth=[session_auth, bearer_auth, apikey_auth],

tests/sta/services/test_datastream.py

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,83 @@ def test_list_datastream(
170170
assert (DatastreamSummaryResponse.from_orm(thing) for thing in result)
171171

172172

173+
def test_list_datastream_visualization_bootstrap_returns_lean_metadata():
174+
bootstrap = datastream_service.list_visualization_bootstrap(principal=None)
175+
176+
assert bootstrap["things"] == [
177+
{
178+
"id": "3b7818af-eff7-4149-8517-e5cad9dc22e1",
179+
"workspace_id": "6e0deaf2-a92b-421b-9ece-86783265596f",
180+
"name": "Public Thing",
181+
"sampling_feature_code": "UWRL",
182+
}
183+
]
184+
assert Counter(datastream["name"] for datastream in bootstrap["datastreams"]) == Counter(
185+
["Public Datastream 1", "Public Datastream 2"]
186+
)
187+
assert bootstrap["observed_properties"] == [
188+
{
189+
"id": "d66414cc-ff33-4e12-bbe0-2c048abd1f40",
190+
"name": "Public Assigned Observed Property",
191+
"code": "Public",
192+
},
193+
{
194+
"id": "a5746e4e-f479-4476-a462-6a8f7874794d",
195+
"name": "System Assigned Observed Property",
196+
"code": "System",
197+
},
198+
]
199+
assert bootstrap["processing_levels"] == [
200+
{
201+
"id": "a7ff1528-e485-4def-b325-45330c1c448c",
202+
"definition": "Public Assigned Processing Level",
203+
},
204+
{
205+
"id": "90777619-9d5c-44a5-87f0-ccd5844f9cc0",
206+
"definition": "System Assigned Processing Level",
207+
},
208+
]
209+
assert all(
210+
set(datastream) == {
211+
"id",
212+
"name",
213+
"thing_id",
214+
"observed_property_id",
215+
"processing_level_id",
216+
"unit_id",
217+
"no_data_value",
218+
"value_count",
219+
"phenomenon_begin_time",
220+
"phenomenon_end_time",
221+
"intended_time_spacing",
222+
"intended_time_spacing_unit",
223+
}
224+
for datastream in bootstrap["datastreams"]
225+
)
226+
227+
228+
def test_list_datastream_visualization_bootstrap_filters_by_workspace(get_principal):
229+
bootstrap = datastream_service.list_visualization_bootstrap(
230+
principal=get_principal("owner"),
231+
filtering={"thing__workspace_id": ["b27c51a0-7374-462d-8a53-d97d47176c10"]},
232+
)
233+
234+
assert Counter(datastream["name"] for datastream in bootstrap["datastreams"]) == Counter(
235+
[
236+
"Private Datastream 4",
237+
"Private Datastream 5",
238+
"Private Datastream 6",
239+
"Private Datastream 7",
240+
]
241+
)
242+
assert Counter(thing["name"] for thing in bootstrap["things"]) == Counter(
243+
[
244+
"Private Thing",
245+
"Public Thing Private Workspace",
246+
]
247+
)
248+
249+
173250
@pytest.mark.parametrize(
174251
"principal, datastream, message, error_code",
175252
[

0 commit comments

Comments
 (0)