Skip to content

Commit e312d8c

Browse files
authored
Add /nodes/statistics/user endpoint to REST API (#80)
This PR ports the old statistics endpoints over to the new REST API.
1 parent ca7036d commit e312d8c

2 files changed

Lines changed: 93 additions & 0 deletions

File tree

aiida_restapi/routers/nodes.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,62 @@ async def get_node_projectable_properties(
8686
raise HTTPException(status_code=422, detail=str(err)) from err
8787

8888

89+
class NodeStatistics(pdt.BaseModel):
90+
"""Pydantic model representing node statistics."""
91+
92+
total: int = pdt.Field(
93+
description='Total number of nodes.',
94+
examples=[47],
95+
)
96+
types: dict[str, int] = pdt.Field(
97+
description='Number of nodes by type.',
98+
examples=[
99+
{
100+
'data.core.int.Int.': 42,
101+
'data.core.singlefile.SinglefileData.': 5,
102+
}
103+
],
104+
)
105+
ctime_by_day: dict[str, int] = pdt.Field(
106+
description='Number of nodes created per day (YYYY-MM-DD).',
107+
examples=[
108+
{
109+
'2012-01-01': 10,
110+
'2012-01-02': 15,
111+
}
112+
],
113+
)
114+
115+
116+
@read_router.get('/nodes/statistics', response_model=NodeStatistics)
117+
@with_dbenv()
118+
async def get_nodes_statistics(user: int | None = None) -> dict[str, t.Any]:
119+
"""Get node statistics.
120+
121+
:param user: Optional user PK to filter statistics by user.
122+
:return: A dictionary containing total node count, counts by node type, and creation time statistics.
123+
124+
>>> {
125+
>>> "total": 47,
126+
>>> "types": {
127+
>>> "data.core.int.Int.": 42,
128+
>>> "data.core.singlefile.SinglefileData.": 5,
129+
>>> ...
130+
>>> },
131+
>>> "ctime_by_day": {
132+
>>> "2012-01-01": 10,
133+
>>> "2012-01-02": 15,
134+
>>> ...
135+
>>> },
136+
>>> }
137+
"""
138+
139+
from aiida.manage import get_manager
140+
141+
backend = get_manager().get_profile_storage()
142+
return backend.query().get_creation_statistics(user_pk=user)
143+
144+
89145
@read_router.get('/nodes/download_formats')
90146
async def get_nodes_download_formats() -> dict[str, t.Any]:
91147
"""Get download formats for AiiDA nodes.

tests/test_nodes.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -640,3 +640,40 @@ async def test_get_download_node(async_client: AsyncClient, array_data_node: orm
640640
response = await async_client.get(f'/nodes/{array_data_node.pk}/download')
641641
assert response.status_code == 422, response.json()
642642
assert 'Please specify the download format' in response.json()['detail']
643+
644+
645+
@pytest.mark.usefixtures('default_nodes')
646+
@pytest.mark.anyio
647+
async def test_get_statistics(async_client: AsyncClient):
648+
"""Test get statistics for nodes."""
649+
650+
from datetime import datetime
651+
652+
default_user_reference_json = {
653+
'total': 4,
654+
'types': {
655+
'data.core.float.Float.': 1,
656+
'data.core.str.Str.': 1,
657+
'data.core.bool.Bool.': 1,
658+
'data.core.int.Int.': 1,
659+
},
660+
'ctime_by_day': {datetime.today().strftime('%Y-%m-%d'): 4},
661+
}
662+
663+
# Test without specifying user, should use default user
664+
response = await async_client.get('/nodes/statistics')
665+
assert response.status_code == 200, response.json()
666+
assert response.json() == default_user_reference_json
667+
668+
# Test that the output is the same when we use the pk of the default user
669+
from aiida import orm
670+
671+
default_user_pk = orm.User(email='').collection.get_default().pk
672+
response = await async_client.get(f'/nodes/statistics?user={default_user_pk}')
673+
assert response.status_code == 200, response.json()
674+
assert response.json() == default_user_reference_json
675+
676+
# Test empty response for nonexisting user
677+
response = await async_client.get('/nodes/statistics?user=99999')
678+
assert response.status_code == 200, response.json()
679+
assert response.json() == {'total': 0, 'types': {}, 'ctime_by_day': {}}

0 commit comments

Comments
 (0)