From 0b19a93e0da04c8d6423c78b0428650f88d1ede9 Mon Sep 17 00:00:00 2001 From: zwitschi Date: Mon, 20 Oct 2025 18:51:23 +0200 Subject: [PATCH] Implement distribution management API and UI forms; add distribution model and tests --- README.md | 2 +- main.py | 4 +++ models/distribution.py | 14 +++++++++ requirements.txt | 3 +- routes/distributions.py | 44 +++++++++++++++++++++++++++++ routes/ui.py | 20 +++++++++++++ templates/Dashboard.html | 25 +++++++++++++++++ templates/ParameterInput.html | 50 +++++++++++++++++++++++++++++++++ templates/ScenarioForm.html | 36 ++++++++++++++++++++++++ tests/unit/test_distribution.py | 33 ++++++++++++++++++++++ 10 files changed, 229 insertions(+), 2 deletions(-) create mode 100644 models/distribution.py create mode 100644 routes/distributions.py create mode 100644 routes/ui.py create mode 100644 templates/Dashboard.html create mode 100644 templates/ParameterInput.html create mode 100644 templates/ScenarioForm.html create mode 100644 tests/unit/test_distribution.py diff --git a/README.md b/README.md index e3b040a..e77a798 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ The project is organized into several key directories: - `config/`: Configuration files and settings. - `middleware/`: Custom middleware for request/response processing. - `tests/`: Unit and integration tests. -- `templates/`: HTML templates (if applicable). +- `templates/`: Jinja2 HTML templates for server-side rendering. - `docs/`: Documentation files. Key files include: diff --git a/main.py b/main.py index b336f2e..bc7b1ad 100644 --- a/main.py +++ b/main.py @@ -1,3 +1,5 @@ +from routes.distributions import router as distributions_router +from routes.ui import router as ui_router from routes.parameters import router as parameters_router from fastapi import FastAPI from fastapi.middleware import Middleware @@ -15,3 +17,5 @@ app.middleware("http")(validate_json) # Include API routers app.include_router(scenarios_router) app.include_router(parameters_router) +app.include_router(distributions_router) +app.include_router(ui_router) diff --git a/models/distribution.py b/models/distribution.py new file mode 100644 index 0000000..9f9832a --- /dev/null +++ b/models/distribution.py @@ -0,0 +1,14 @@ +from sqlalchemy import Column, Integer, String, JSON +from config.database import Base + + +class Distribution(Base): + __tablename__ = "distribution" + + id = Column(Integer, primary_key=True, index=True) + name = Column(String, nullable=False) + distribution_type = Column(String, nullable=False) + parameters = Column(JSON, nullable=True) + + def __repr__(self): + return f"" diff --git a/requirements.txt b/requirements.txt index 935896e..2c4baca 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,4 +4,5 @@ sqlalchemy psycopg2-binary python-dotenv pytest -pytest-cov \ No newline at end of file +pytest-cov +jinja2 \ No newline at end of file diff --git a/routes/distributions.py b/routes/distributions.py new file mode 100644 index 0000000..2a6acb5 --- /dev/null +++ b/routes/distributions.py @@ -0,0 +1,44 @@ +from fastapi import APIRouter, HTTPException, Depends +from sqlalchemy.orm import Session +from typing import List +from pydantic import BaseModel +from config.database import SessionLocal +from models.distribution import Distribution + +router = APIRouter(prefix="/api/distributions", tags=["Distributions"]) + + +def get_db(): + db = SessionLocal() + try: + yield db + finally: + db.close() + + +class DistributionCreate(BaseModel): + name: str + distribution_type: str + parameters: dict + + +class DistributionRead(DistributionCreate): + id: int + + class Config: + orm_mode = True + + +@router.post("/", response_model=DistributionRead) +async def create_distribution(dist: DistributionCreate, db: Session = Depends(get_db)): + db_dist = Distribution(**dist.dict()) + db.add(db_dist) + db.commit() + db.refresh(db_dist) + return db_dist + + +@router.get("/", response_model=List[DistributionRead]) +async def list_distributions(db: Session = Depends(get_db)): + dists = db.query(Distribution).all() + return dists diff --git a/routes/ui.py b/routes/ui.py new file mode 100644 index 0000000..b67b4af --- /dev/null +++ b/routes/ui.py @@ -0,0 +1,20 @@ +from fastapi import APIRouter, Request +from fastapi.responses import HTMLResponse +from fastapi.templating import Jinja2Templates + +router = APIRouter() + +# Set up Jinja2 templates directory +templates = Jinja2Templates(directory="templates") + + +@router.get("/ui/scenarios", response_class=HTMLResponse) +async def scenario_form(request: Request): + """Render the scenario creation form.""" + return templates.TemplateResponse("ScenarioForm.html", {"request": request}) + + +@router.get("/ui/parameters", response_class=HTMLResponse) +async def parameter_form(request: Request): + """Render the parameter input form.""" + return templates.TemplateResponse("ParameterInput.html", {"request": request}) diff --git a/templates/Dashboard.html b/templates/Dashboard.html new file mode 100644 index 0000000..d4f06bf --- /dev/null +++ b/templates/Dashboard.html @@ -0,0 +1,25 @@ + + + + + CalMiner Dashboard + + +

Simulation Results Dashboard

+
+ + + + \ No newline at end of file diff --git a/templates/ParameterInput.html b/templates/ParameterInput.html new file mode 100644 index 0000000..348fc12 --- /dev/null +++ b/templates/ParameterInput.html @@ -0,0 +1,50 @@ + + + + + Process Parameters Input + + +

Enter Parameters for a Scenario

+
+
+
+
+ +
+
+ + + diff --git a/templates/ScenarioForm.html b/templates/ScenarioForm.html new file mode 100644 index 0000000..c0412de --- /dev/null +++ b/templates/ScenarioForm.html @@ -0,0 +1,36 @@ + + + + + Scenario Management + + +

Create a New Scenario

+
+
+
+ +
+
+ + + diff --git a/tests/unit/test_distribution.py b/tests/unit/test_distribution.py new file mode 100644 index 0000000..8341605 --- /dev/null +++ b/tests/unit/test_distribution.py @@ -0,0 +1,33 @@ +from fastapi.testclient import TestClient +from main import app +from config.database import Base, engine +from models.distribution import Distribution + +# Setup and teardown + + +def setup_module(module): + Base.metadata.create_all(bind=engine) + + +def teardown_module(module): + Base.metadata.drop_all(bind=engine) + + +client = TestClient(app) + + +def test_create_and_list_distribution(): + # Create distribution + payload = {"name": "NormalDist", "distribution_type": "normal", + "parameters": {"mu": 0, "sigma": 1}} + resp = client.post("/api/distributions/", json=payload) + assert resp.status_code == 200 + data = resp.json() + assert data["name"] == "NormalDist" + + # List distributions + resp2 = client.get("/api/distributions/") + assert resp2.status_code == 200 + data2 = resp2.json() + assert any(d["name"] == "NormalDist" for d in data2)