Skip to content

Commit 5d7909f

Browse files
authored
Merge pull request #72 from OpenSPP/feat/api-v2-simulation
feat(spp_api_v2_simulation): add simulation REST API
2 parents 624e3cd + 7872c87 commit 5d7909f

35 files changed

+5516
-1
lines changed

spp_api_v2_simulation/__init__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Part of OpenSPP. See LICENSE file for full copyright and licensing details.
2+
from . import models
3+
from . import routers
4+
from . import schemas
5+
from . import services
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# Part of OpenSPP. See LICENSE file for full copyright and licensing details.
2+
{
3+
"name": "OpenSPP Simulation API",
4+
"category": "OpenSPP/Integration",
5+
"version": "19.0.2.0.0",
6+
"sequence": 1,
7+
"author": "OpenSPP.org",
8+
"website": "https://github.com/OpenSPP/OpenSPP2",
9+
"license": "LGPL-3",
10+
"development_status": "Production/Stable",
11+
"maintainers": ["jeremi", "gonzalesedwin1123", "reichie020212"],
12+
"depends": [
13+
"spp_api_v2",
14+
"spp_simulation",
15+
"spp_aggregation",
16+
],
17+
"data": [
18+
"security/ir.model.access.csv",
19+
],
20+
"assets": {},
21+
"demo": [],
22+
"images": [],
23+
"application": False,
24+
"installable": True,
25+
"auto_install": False,
26+
"summary": """
27+
REST API for simulation scenario management.
28+
""",
29+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Part of OpenSPP. See LICENSE file for full copyright and licensing details.
2+
from . import api_client_scope
3+
from . import fastapi_endpoint
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# Part of OpenSPP. See LICENSE file for full copyright and licensing details.
2+
"""Extends API client scope to support simulation and aggregation resources."""
3+
4+
from odoo import fields, models
5+
6+
7+
class ApiClientScope(models.Model):
8+
"""Extend API client scope to include simulation and aggregation resources."""
9+
10+
_inherit = "spp.api.client.scope"
11+
12+
resource = fields.Selection(
13+
selection_add=[
14+
("simulation", "Simulation"),
15+
("aggregation", "Aggregation"),
16+
],
17+
ondelete={
18+
"simulation": "cascade",
19+
"aggregation": "cascade",
20+
},
21+
)
22+
23+
action = fields.Selection(
24+
selection_add=[
25+
("execute", "Execute"),
26+
("convert", "Convert"),
27+
],
28+
ondelete={"execute": "cascade", "convert": "cascade"},
29+
)
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# Part of OpenSPP. See LICENSE file for full copyright and licensing details.
2+
"""Extend FastAPI endpoint to include simulation and aggregation routers."""
3+
4+
import logging
5+
6+
from odoo import models
7+
8+
from fastapi import APIRouter
9+
10+
_logger = logging.getLogger(__name__)
11+
12+
13+
class SppApiV2SimulationEndpoint(models.Model):
14+
"""Extend FastAPI endpoint for Simulation and Aggregation API."""
15+
16+
_inherit = "fastapi.endpoint"
17+
18+
def _get_fastapi_routers(self) -> list[APIRouter]:
19+
"""Add simulation and aggregation routers to API V2."""
20+
routers = super()._get_fastapi_routers()
21+
if self.app == "api_v2":
22+
from ..routers.aggregation import aggregation_router
23+
from ..routers.comparison import comparison_router
24+
from ..routers.run import run_router
25+
from ..routers.scenario import scenario_router
26+
from ..routers.simulation import simulation_router
27+
28+
routers.extend(
29+
[
30+
scenario_router,
31+
run_router,
32+
comparison_router,
33+
simulation_router,
34+
aggregation_router,
35+
]
36+
)
37+
return routers
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[build-system]
2+
requires = ["whool"]
3+
build-backend = "whool.buildapi"
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# Part of OpenSPP. See LICENSE file for full copyright and licensing details.
2+
"""FastAPI routers for simulation and aggregation API."""
3+
4+
from . import aggregation
5+
from . import simulation
6+
from .comparison import comparison_router
7+
from .run import run_router
8+
from .scenario import scenario_router
9+
10+
__all__ = [
11+
"aggregation",
12+
"simulation",
13+
"scenario_router",
14+
"run_router",
15+
"comparison_router",
16+
]
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
# Part of OpenSPP. See LICENSE file for full copyright and licensing details.
2+
"""Aggregation API endpoints for population analytics."""
3+
4+
import logging
5+
from typing import Annotated
6+
7+
from odoo.api import Environment
8+
9+
from odoo.addons.fastapi.dependencies import odoo_env
10+
from odoo.addons.spp_api_v2.middleware.auth import get_authenticated_client
11+
12+
from fastapi import APIRouter, Depends, HTTPException, Query, status
13+
14+
from ..schemas.aggregation import (
15+
AggregationResponse,
16+
ComputeAggregationRequest,
17+
DimensionsListResponse,
18+
)
19+
from ..services.aggregation_api_service import AggregationApiService
20+
21+
_logger = logging.getLogger(__name__)
22+
23+
aggregation_router = APIRouter(tags=["Aggregation"], prefix="/aggregation")
24+
25+
26+
@aggregation_router.post(
27+
"/compute",
28+
response_model=AggregationResponse,
29+
summary="Compute population aggregation",
30+
description="Compute population counts and statistics with optional demographic breakdowns.",
31+
)
32+
async def compute_aggregation(
33+
request: ComputeAggregationRequest,
34+
env: Annotated[Environment, Depends(odoo_env)],
35+
api_client: Annotated[dict, Depends(get_authenticated_client)],
36+
):
37+
"""Compute aggregation for a scope with optional breakdown.
38+
39+
Requires:
40+
aggregation:read scope
41+
42+
Response:
43+
AggregationResponse with total_count, statistics, and optional breakdown
44+
"""
45+
if not api_client.has_scope("aggregation", "read"):
46+
raise HTTPException(
47+
status_code=status.HTTP_403_FORBIDDEN,
48+
detail="Client does not have aggregation:read scope",
49+
)
50+
51+
try:
52+
service = AggregationApiService(env)
53+
result = service.compute_aggregation(
54+
scope_dict=request.scope.model_dump(),
55+
statistics=request.statistics,
56+
group_by=request.group_by,
57+
)
58+
return result
59+
60+
except ValueError as e:
61+
_logger.warning("Invalid aggregation request: %s", str(e))
62+
raise HTTPException(
63+
status_code=status.HTTP_400_BAD_REQUEST,
64+
detail=str(e),
65+
) from e
66+
except Exception as e:
67+
_logger.error("Aggregation computation failed: %s", str(e), exc_info=True)
68+
raise HTTPException(
69+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
70+
detail="Failed to compute aggregation",
71+
) from e
72+
73+
74+
@aggregation_router.get(
75+
"/dimensions",
76+
response_model=DimensionsListResponse,
77+
summary="List available dimensions",
78+
description="Returns active demographic dimensions available for group_by.",
79+
)
80+
async def list_dimensions(
81+
env: Annotated[Environment, Depends(odoo_env)],
82+
api_client: Annotated[dict, Depends(get_authenticated_client)],
83+
applies_to: Annotated[
84+
str | None,
85+
Query(description="Filter: 'individuals', 'groups', or None for all"),
86+
] = None,
87+
):
88+
"""List active demographic dimensions.
89+
90+
Requires:
91+
aggregation:read scope
92+
93+
Response:
94+
DimensionsListResponse with available dimensions
95+
"""
96+
if not api_client.has_scope("aggregation", "read"):
97+
raise HTTPException(
98+
status_code=status.HTTP_403_FORBIDDEN,
99+
detail="Client does not have aggregation:read scope",
100+
)
101+
102+
try:
103+
service = AggregationApiService(env)
104+
dimensions = service.list_dimensions(applies_to=applies_to)
105+
return {"dimensions": dimensions}
106+
107+
except Exception as e:
108+
_logger.error("Failed to list dimensions: %s", str(e), exc_info=True)
109+
raise HTTPException(
110+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
111+
detail="Failed to list dimensions",
112+
) from e

0 commit comments

Comments
 (0)