Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 13 additions & 5 deletions dp3/api/routers/entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ def _validate_sort_params(etype: str, sort: list[str] | None) -> list[tuple[str,
sort: list of sort specifications in format 'attribute:direction'
where direction is 1 (ascending) or -1 (descending)
e.g., ['hostname:-1', 'ip:1']
Special attribute 'eid' is also supported for sorting by entity ID.

Returns:
List of (attribute, direction) tuples for sorting, or None if no sorting specified
Expand Down Expand Up @@ -172,6 +173,11 @@ def _validate_sort_params(etype: str, sort: list[str] | None) -> list[tuple[str,
attr, direction_str = match.groups()
direction = int(direction_str) if direction_str else 1 # Default to ascending (1)

# Support sorting by entity ID using pseudo-attribute 'eid'
if attr == "eid":
sort_criteria.append((attr, direction))
continue

# Check if attribute exists
if attr not in entity_attribs:
raise RequestValidationError(
Expand Down Expand Up @@ -306,11 +312,13 @@ async def get_entity_type_eids(

Generic and fulltext filters are merged - fulltext overrides conflicting keys.

Sorting is supported for plain and observations attributes with primitive data types
(excluding json and multi_value observations). To sort by multiple attributes, provide
multiple sort parameters in the format 'attribute:direction' where direction is 1 (ascending)
or -1 (descending). Direction defaults to 1 (ascending) if not provided. Example:
`?sort=hostname:-1&sort=ip:1`
Sorting is supported by entity ID using the special attribute name `eid`,
as well as for plain and observations attributes with primitive data types
(excluding json and multi_value observations). To sort by multiple attributes,
provide multiple sort parameters in the format 'attribute:direction' where
direction is 1 (ascending) or -1 (descending). Direction defaults to 1 (ascending)
if not provided. Examples:
`?sort=eid:1`, `?sort=hostname:-1&sort=ip:1`
"""
fulltext_filters, generic_filter = _validate_snapshot_filters(fulltext_filters, generic_filter)
sort_criteria = _validate_sort_params(etype, sort)
Expand Down
66 changes: 66 additions & 0 deletions tests/test_api/test_get_entity_eids.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,3 +60,69 @@ def test_get_entity_eids_generic_filters_eid(self):
{5, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59},
{x["eid"] for x in eids.data},
)

def test_get_entity_eids_sort_by_eid_asc(self):
eids = self.get_entity_data("entity/A/get", EntityEidList, sort="eid:1", limit=0)
self.assertEqual(100, len(eids.data))
self.assertEqual(list(range(100)), [x["eid"] for x in eids.data])

def test_get_entity_eids_sort_by_eid_desc(self):
eids = self.get_entity_data("entity/A/get", EntityEidList, sort="eid:-1", limit=0)
self.assertEqual(100, len(eids.data))
self.assertEqual(list(range(99, -1, -1)), [x["eid"] for x in eids.data])

def test_get_entity_eids_pagination_with_sort(self):
received_eids = []
for i in range(0, 100, 10):
eids = self.get_entity_data(
"entity/A/get", EntityEidList, sort="eid:1", skip=i, limit=10
)
self.assertEqual(10, len(eids.data), f"Failed at {i}")
received_eids.extend(x["eid"] for x in eids.data)
self.assertEqual(list(range(100)), received_eids)

received_eids = []
for i in range(0, 100, 10):
eids = self.get_entity_data(
"entity/A/get", EntityEidList, sort="eid:-1", skip=i, limit=10
)
self.assertEqual(10, len(eids.data), f"Failed at {i}")
received_eids.extend(x["eid"] for x in eids.data)
self.assertEqual(list(range(99, -1, -1)), received_eids)

def _get_sorted_eids(self, sort_params: list[str], **kwargs) -> EntityEidList:
"""Fetch entity EIDs with multiple sort query parameters.

The shared `get_request` helper joins kwargs with '&' and cannot emit
multiple values for the same key, which is required for multi-column
sorting. Build the query string explicitly here.
"""
query_parts = [f"sort={param}" for param in sort_params]
for key, value in kwargs.items():
query_parts.append(f"{key}={value}")
response = self.get_request(f"entity/A/get?{'&'.join(query_parts)}")
self.assertEqual(response.status_code, 200)
return EntityEidList.model_validate_json(response.content)

def test_get_entity_eids_sort_by_multiple_attrs(self):
res = self.push_datapoints(
[
{"src": "setup@test", "attr": "data2", "type": "A", "id": i, "v": f"g{i % 5}"}
for i in range(0, 100)
]
)
self.assertEqual(res.status_code, 200)
sleep(8)
self.get_request("control/make_snapshots")
sleep(6)

expected = sorted(range(100), key=lambda i: (f"g{i % 5}", i))
# omit :1 if sort is ascending
eids = self._get_sorted_eids(["data2", "eid"], limit=0)
self.assertEqual(100, len(eids.data))
self.assertEqual(expected, [x["eid"] for x in eids.data])

expected = sorted(range(100), key=lambda i: (f"g{i % 5}", -i))
eids = self._get_sorted_eids(["data2:1", "eid:-1"], limit=0)
self.assertEqual(100, len(eids.data))
self.assertEqual(expected, [x["eid"] for x in eids.data])
Loading