Skip to content

Commit 49f9698

Browse files
feat(api): add FastAPI backend for GUI (M1)
Implement the complete backend structure for FUSION GUI web interface: Structure: - fusion/api/main.py: FastAPI app with lifespan, CORS, static serving - fusion/api/config.py: pydantic-settings configuration - fusion/api/db/: SQLite + SQLAlchemy ORM for run persistence - fusion/api/schemas/: Pydantic models for request/response validation - fusion/api/routes/: REST endpoints (runs, configs, artifacts, system) - fusion/api/services/: Business logic (run lifecycle, file access) Features: - Run CRUD with subprocess management - Log streaming via Server-Sent Events (SSE) - Secure artifact access with path traversal protection - Config template listing and retrieval - Cross-platform process termination (POSIX + Windows) - Orphaned run recovery on startup Also: - Update fusion/cli/run_gui.py to launch uvicorn server - Add requirements-gui.txt for GUI dependencies - Add data/gui_runs/ to .gitignore Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent e2361f1 commit 49f9698

25 files changed

Lines changed: 1419 additions & 12 deletions

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ data/output
2020
data/plots
2121
data/excel
2222
data/training_data
23+
data/gui_runs/
2324

2425
logs/*
2526

fusion/api/README.md

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
# FUSION API Module
2+
3+
FastAPI backend for the FUSION GUI web interface.
4+
5+
## Overview
6+
7+
This module provides a REST API for managing simulation runs, streaming logs, and accessing artifacts. It serves as the backend for the FUSION GUI.
8+
9+
## Quick Start
10+
11+
```bash
12+
# Install GUI dependencies
13+
pip install -r requirements-gui.txt
14+
15+
# Start the server
16+
python -m fusion.cli.run_gui
17+
18+
# Or with reload for development
19+
python -m fusion.cli.run_gui --reload
20+
```
21+
22+
The API will be available at `http://127.0.0.1:8765`.
23+
24+
## API Documentation
25+
26+
Once the server is running, visit:
27+
- Swagger UI: `http://127.0.0.1:8765/docs`
28+
- ReDoc: `http://127.0.0.1:8765/redoc`
29+
30+
## Module Structure
31+
32+
```
33+
fusion/api/
34+
├── __init__.py # Package exports
35+
├── main.py # FastAPI app, lifespan, middleware
36+
├── config.py # Settings (pydantic-settings)
37+
├── dependencies.py # Dependency injection
38+
├── routes/ # API endpoints
39+
│ ├── runs.py # /api/runs - Run management
40+
│ ├── configs.py # /api/configs - Templates
41+
│ ├── artifacts.py # /api/runs/{id}/artifacts
42+
│ └── system.py # /api/health, /api/version
43+
├── schemas/ # Pydantic models
44+
│ ├── run.py # Run request/response models
45+
│ ├── config.py # Config models
46+
│ └── common.py # Shared models
47+
├── services/ # Business logic
48+
│ ├── run_manager.py # Simulation lifecycle
49+
│ └── artifact_service.py # Secure file access
50+
├── db/ # Database layer
51+
│ ├── database.py # SQLite + SQLAlchemy
52+
│ └── models.py # ORM models
53+
├── static/ # Built React frontend
54+
└── tests/ # Unit tests
55+
```
56+
57+
## Key Endpoints
58+
59+
| Method | Endpoint | Description |
60+
|--------|----------|-------------|
61+
| POST | /api/runs | Create and start a run |
62+
| GET | /api/runs | List all runs |
63+
| GET | /api/runs/{id} | Get run details |
64+
| DELETE | /api/runs/{id} | Cancel/delete run |
65+
| GET | /api/runs/{id}/logs | Stream logs (SSE) |
66+
| GET | /api/runs/{id}/artifacts | List artifacts |
67+
| GET | /api/configs/templates | List config templates |
68+
| GET | /api/health | Health check |
69+
70+
## Configuration
71+
72+
Environment variables (prefix: `FUSION_GUI_`):
73+
74+
| Variable | Default | Description |
75+
|----------|---------|-------------|
76+
| HOST | 127.0.0.1 | Server bind address |
77+
| PORT | 8765 | Server port |
78+
| DATABASE_URL | sqlite:///data/gui_runs/runs.db | SQLite database path |
79+
| MAX_CONCURRENT_RUNS | 1 | Maximum simultaneous runs |
80+
81+
## Development
82+
83+
```bash
84+
# Run with auto-reload
85+
python -m fusion.cli.run_gui --reload --log-level debug
86+
87+
# Run tests
88+
pytest fusion/api/tests/
89+
```
90+
91+
## See Also
92+
93+
- [GUI Documentation](../../docs/gui/) - Full GUI design documentation
94+
- [Backend Standards](../../docs/gui/06-backend-standards.md) - Coding conventions

fusion/api/__init__.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
"""
2+
fusion.api: FastAPI backend for the FUSION GUI.
3+
4+
Provides a REST API and Server-Sent Events (SSE) for:
5+
- Creating and managing simulation runs
6+
- Streaming logs and progress updates
7+
- Browsing and downloading artifacts
8+
- Configuration management
9+
10+
Entry Point:
11+
Run via CLI: `fusion gui` or `python -m fusion.cli.run_gui`
12+
Direct: `uvicorn fusion.api.main:app --host 127.0.0.1 --port 8765`
13+
"""
14+
15+
from .config import settings
16+
from .main import app
17+
18+
__all__ = ["app", "settings"]

fusion/api/config.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
"""
2+
Application configuration using pydantic-settings.
3+
4+
Settings can be overridden via environment variables with FUSION_GUI_ prefix.
5+
Example: FUSION_GUI_PORT=9000 fusion gui
6+
"""
7+
8+
from pathlib import Path
9+
10+
from pydantic_settings import BaseSettings
11+
12+
13+
class Settings(BaseSettings):
14+
"""Application settings with environment variable support."""
15+
16+
# Server
17+
host: str = "127.0.0.1"
18+
port: int = 8765
19+
debug: bool = False
20+
21+
# Database
22+
database_url: str = "sqlite:///data/gui_runs/runs.db"
23+
24+
# Paths
25+
runs_dir: Path = Path("data/gui_runs")
26+
templates_dir: Path = Path("fusion/configs/templates")
27+
28+
# Limits
29+
max_concurrent_runs: int = 1
30+
max_log_size_bytes: int = 10 * 1024 * 1024 # 10MB
31+
32+
class Config:
33+
"""Pydantic settings configuration."""
34+
35+
env_prefix = "FUSION_GUI_"
36+
37+
38+
settings = Settings()

fusion/api/db/__init__.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
"""
2+
Database layer for FUSION GUI.
3+
4+
Uses SQLite with SQLAlchemy ORM for run metadata persistence.
5+
"""
6+
7+
from .database import Base, SessionLocal, engine, get_db, init_db
8+
from .models import Run
9+
10+
__all__ = ["Base", "SessionLocal", "engine", "get_db", "init_db", "Run"]

fusion/api/db/database.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
"""
2+
SQLite database setup and session management.
3+
4+
Provides:
5+
- SQLAlchemy engine and session factory
6+
- Database initialization
7+
- Session dependency for FastAPI
8+
"""
9+
10+
from collections.abc import Generator
11+
12+
from sqlalchemy import create_engine
13+
from sqlalchemy.orm import DeclarativeBase, Session, sessionmaker
14+
15+
from ..config import settings
16+
17+
18+
class Base(DeclarativeBase):
19+
"""Base class for SQLAlchemy ORM models."""
20+
21+
pass
22+
23+
24+
engine = create_engine(
25+
settings.database_url,
26+
connect_args={"check_same_thread": False}, # SQLite specific
27+
)
28+
29+
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
30+
31+
32+
def init_db() -> None:
33+
"""
34+
Initialize the database.
35+
36+
Creates the runs directory and all tables if they don't exist.
37+
"""
38+
settings.runs_dir.mkdir(parents=True, exist_ok=True)
39+
Base.metadata.create_all(bind=engine)
40+
41+
42+
def get_db() -> Generator[Session, None, None]:
43+
"""
44+
Dependency for database sessions.
45+
46+
Yields a database session and ensures it is closed after use.
47+
"""
48+
db = SessionLocal()
49+
try:
50+
yield db
51+
finally:
52+
db.close()

fusion/api/db/models.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
"""
2+
SQLAlchemy ORM models for FUSION GUI.
3+
4+
Defines the database schema for simulation runs.
5+
"""
6+
7+
from datetime import datetime
8+
9+
from sqlalchemy import DateTime, Float, Integer, String, Text
10+
from sqlalchemy.orm import Mapped, mapped_column
11+
12+
from .database import Base
13+
14+
15+
class Run(Base):
16+
"""
17+
SQLAlchemy model for simulation runs.
18+
19+
Tracks run metadata, process information, and progress state.
20+
"""
21+
22+
__tablename__ = "runs"
23+
24+
# Primary key - 12 character hex string
25+
id: Mapped[str] = mapped_column(String(12), primary_key=True)
26+
27+
# User-provided name (optional)
28+
name: Mapped[str | None] = mapped_column(String(255), nullable=True)
29+
30+
# Status: PENDING, RUNNING, COMPLETED, FAILED, CANCELLED
31+
status: Mapped[str] = mapped_column(String(20), nullable=False, index=True)
32+
33+
# Configuration stored as JSON string
34+
config_json: Mapped[str] = mapped_column(Text, nullable=False)
35+
36+
# Template used to create this run
37+
template: Mapped[str] = mapped_column(String(100), nullable=False, default="default")
38+
39+
# Process tracking (for cancellation)
40+
pid: Mapped[int | None] = mapped_column(Integer, nullable=True)
41+
pgid: Mapped[int | None] = mapped_column(Integer, nullable=True)
42+
43+
# Timestamps
44+
created_at: Mapped[datetime] = mapped_column(
45+
DateTime, nullable=False, default=datetime.utcnow
46+
)
47+
started_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
48+
completed_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
49+
50+
# Error information (if FAILED)
51+
error_message: Mapped[str | None] = mapped_column(Text, nullable=True)
52+
53+
# Progress cache (updated periodically from progress.jsonl)
54+
current_erlang: Mapped[float | None] = mapped_column(Float, nullable=True)
55+
total_erlangs: Mapped[int | None] = mapped_column(Integer, nullable=True)
56+
current_iteration: Mapped[int | None] = mapped_column(Integer, nullable=True)
57+
total_iterations: Mapped[int | None] = mapped_column(Integer, nullable=True)
58+
59+
def __repr__(self) -> str:
60+
"""Return string representation of the run."""
61+
return f"<Run(id={self.id!r}, name={self.name!r}, status={self.status!r})>"

fusion/api/dependencies.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
"""
2+
FastAPI dependency injection providers.
3+
4+
Provides database sessions and other shared resources.
5+
"""
6+
7+
from collections.abc import Generator
8+
from typing import Annotated
9+
10+
from fastapi import Depends
11+
from sqlalchemy.orm import Session
12+
13+
from .db.database import SessionLocal
14+
15+
16+
def get_db() -> Generator[Session, None, None]:
17+
"""
18+
Dependency for database sessions.
19+
20+
Yields a database session and ensures it is closed after use.
21+
"""
22+
db = SessionLocal()
23+
try:
24+
yield db
25+
finally:
26+
db.close()
27+
28+
29+
# Type alias for dependency injection
30+
DbSession = Annotated[Session, Depends(get_db)]

0 commit comments

Comments
 (0)