Skip to content
Open
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
5 changes: 5 additions & 0 deletions spp_api_v2_simulation/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Part of OpenSPP. See LICENSE file for full copyright and licensing details.
from . import models
from . import routers
from . import schemas
from . import services
29 changes: 29 additions & 0 deletions spp_api_v2_simulation/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Part of OpenSPP. See LICENSE file for full copyright and licensing details.
{
"name": "OpenSPP Simulation API",
"category": "OpenSPP/Integration",
"version": "19.0.2.0.0",
"sequence": 1,
"author": "OpenSPP.org",
"website": "https://github.com/OpenSPP/OpenSPP2",
"license": "LGPL-3",
"development_status": "Production/Stable",
"maintainers": ["jeremi", "gonzalesedwin1123", "reichie020212"],
"depends": [
"spp_api_v2",
"spp_simulation",
"spp_aggregation",
],
"data": [
"security/ir.model.access.csv",
],
"assets": {},
"demo": [],
"images": [],
"application": False,
"installable": True,
"auto_install": False,
"summary": """
REST API for simulation scenario management.
""",
}
3 changes: 3 additions & 0 deletions spp_api_v2_simulation/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Part of OpenSPP. See LICENSE file for full copyright and licensing details.
from . import api_client_scope
from . import fastapi_endpoint
29 changes: 29 additions & 0 deletions spp_api_v2_simulation/models/api_client_scope.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Part of OpenSPP. See LICENSE file for full copyright and licensing details.
"""Extends API client scope to support simulation and aggregation resources."""

from odoo import fields, models


class ApiClientScope(models.Model):
"""Extend API client scope to include simulation and aggregation resources."""

_inherit = "spp.api.client.scope"

resource = fields.Selection(
selection_add=[
("simulation", "Simulation"),
("aggregation", "Aggregation"),
],
ondelete={
"simulation": "cascade",
"aggregation": "cascade",
},
)

action = fields.Selection(
selection_add=[
("execute", "Execute"),
("convert", "Convert"),
],
ondelete={"execute": "cascade", "convert": "cascade"},
)
37 changes: 37 additions & 0 deletions spp_api_v2_simulation/models/fastapi_endpoint.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Part of OpenSPP. See LICENSE file for full copyright and licensing details.
"""Extend FastAPI endpoint to include simulation and aggregation routers."""

import logging

from odoo import models

from fastapi import APIRouter

_logger = logging.getLogger(__name__)


class SppApiV2SimulationEndpoint(models.Model):
"""Extend FastAPI endpoint for Simulation and Aggregation API."""

_inherit = "fastapi.endpoint"

def _get_fastapi_routers(self) -> list[APIRouter]:
"""Add simulation and aggregation routers to API V2."""
routers = super()._get_fastapi_routers()
if self.app == "api_v2":
from ..routers.aggregation import aggregation_router
from ..routers.comparison import comparison_router
from ..routers.run import run_router
from ..routers.scenario import scenario_router
from ..routers.simulation import simulation_router

routers.extend(
[
scenario_router,
run_router,
comparison_router,
simulation_router,
aggregation_router,
]
)
return routers
3 changes: 3 additions & 0 deletions spp_api_v2_simulation/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[build-system]
requires = ["whool"]
build-backend = "whool.buildapi"
16 changes: 16 additions & 0 deletions spp_api_v2_simulation/routers/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Part of OpenSPP. See LICENSE file for full copyright and licensing details.
"""FastAPI routers for simulation and aggregation API."""

from . import aggregation
from . import simulation
from .comparison import comparison_router
from .run import run_router
from .scenario import scenario_router

__all__ = [
"aggregation",
"simulation",
"scenario_router",
"run_router",
"comparison_router",
]
112 changes: 112 additions & 0 deletions spp_api_v2_simulation/routers/aggregation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
# Part of OpenSPP. See LICENSE file for full copyright and licensing details.
"""Aggregation API endpoints for population analytics."""

import logging
from typing import Annotated

from odoo.api import Environment

from odoo.addons.fastapi.dependencies import odoo_env
from odoo.addons.spp_api_v2.middleware.auth import get_authenticated_client

from fastapi import APIRouter, Depends, HTTPException, Query, status

from ..schemas.aggregation import (
AggregationResponse,
ComputeAggregationRequest,
DimensionsListResponse,
)
from ..services.aggregation_api_service import AggregationApiService

_logger = logging.getLogger(__name__)

aggregation_router = APIRouter(tags=["Aggregation"], prefix="/aggregation")


@aggregation_router.post(
"/compute",
response_model=AggregationResponse,
summary="Compute population aggregation",
description="Compute population counts and statistics with optional demographic breakdowns.",
)
async def compute_aggregation(
request: ComputeAggregationRequest,
env: Annotated[Environment, Depends(odoo_env)],
api_client: Annotated[dict, Depends(get_authenticated_client)],
):
"""Compute aggregation for a scope with optional breakdown.

Requires:
aggregation:read scope

Response:
AggregationResponse with total_count, statistics, and optional breakdown
"""
if not api_client.has_scope("aggregation", "read"):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Client does not have aggregation:read scope",
)

try:
service = AggregationApiService(env)
result = service.compute_aggregation(
scope_dict=request.scope.model_dump(),
statistics=request.statistics,
group_by=request.group_by,
)
return result

except ValueError as e:
_logger.warning("Invalid aggregation request: %s", str(e))
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=str(e),
) from e
except Exception as e:
_logger.error("Aggregation computation failed: %s", str(e), exc_info=True)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to compute aggregation",
) from e


@aggregation_router.get(
"/dimensions",
response_model=DimensionsListResponse,
summary="List available dimensions",
description="Returns active demographic dimensions available for group_by.",
)
async def list_dimensions(
env: Annotated[Environment, Depends(odoo_env)],
api_client: Annotated[dict, Depends(get_authenticated_client)],
applies_to: Annotated[
str | None,
Query(description="Filter: 'individuals', 'groups', or None for all"),
] = None,
):
"""List active demographic dimensions.

Requires:
aggregation:read scope

Response:
DimensionsListResponse with available dimensions
"""
if not api_client.has_scope("aggregation", "read"):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Client does not have aggregation:read scope",
)

try:
service = AggregationApiService(env)
dimensions = service.list_dimensions(applies_to=applies_to)
return {"dimensions": dimensions}

except Exception as e:
_logger.error("Failed to list dimensions: %s", str(e), exc_info=True)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to list dimensions",
) from e
Loading
Loading