1016 lines
28 KiB
Markdown
1016 lines
28 KiB
Markdown
# TJWater ServerBinary - Secondary Development Documentation Guide
|
|
|
|
## Executive Summary
|
|
|
|
TJWaterServerBinary is a **FastAPI-based water distribution network management system** with integrated EPANET simulation, SCADA data management, and comprehensive security architecture. This guide identifies key components and patterns essential for secondary development.
|
|
|
|
---
|
|
|
|
## 1. Project Structure & Key Directories
|
|
|
|
### Overall Architecture
|
|
```
|
|
TJWaterServerBinary/
|
|
├── app/
|
|
│ ├── main.py # FastAPI application entry + lifecycle management
|
|
│ ├── api/v1/ # API routes and endpoints
|
|
│ ├── algorithms/ # Core algorithms (simulation, leakage detection, etc.)
|
|
│ ├── auth/ # Authentication logic and dependencies
|
|
│ ├── core/ # Core configuration, security, encryption
|
|
│ ├── domain/ # Domain models and Pydantic schemas
|
|
│ ├── infra/ # Infrastructure layer (DB, cache, audit)
|
|
│ ├── services/ # Business logic services
|
|
│ ├── native/ # Native module integration (binary/compiled code)
|
|
│ └── utils/ # Utility functions
|
|
├── infra/docker/ # Docker and deployment configs
|
|
├── resources/sql/ # SQL initialization scripts
|
|
├── tests/ # Unit and integration tests
|
|
└── requirements.txt # Python dependencies
|
|
```
|
|
|
|
### Key Directory Details
|
|
|
|
| Directory | Purpose | Key Files |
|
|
|-----------|---------|-----------|
|
|
| **api/v1** | REST API endpoints organized by domain | `router.py` (centralized registration), `endpoints/` (individual controllers) |
|
|
| **domain** | Data models and schemas | `models/role.py`, `schemas/user.py`, `schemas/audit.py` |
|
|
| **infra** | Database access, caching, audit | `db/postgresql/`, `db/timescaledb/`, `cache/`, `audit/` |
|
|
| **auth** | JWT/OAuth2 validation, permissions | `dependencies.py`, `permissions.py`, `keycloak_dependencies.py` |
|
|
| **core** | Security, encryption, config | `config.py`, `security.py`, `encryption.py`, `audit.py` |
|
|
| **native** | Python-to-binary integration | `api/` module wrapping compiled code |
|
|
| **services** | Business logic & orchestration | `tjnetwork.py` (project management), `simulation.py`, etc. |
|
|
|
|
---
|
|
|
|
## 2. API Endpoint Addition (Router Registration)
|
|
|
|
### How to Add New API Endpoints
|
|
|
|
#### Step 1: Create Endpoint File
|
|
Create a new file in `app/api/v1/endpoints/` with your endpoint handlers:
|
|
|
|
```python
|
|
# app/api/v1/endpoints/my_feature.py
|
|
from fastapi import APIRouter, Depends, HTTPException, status
|
|
from app.auth.dependencies import get_current_active_user
|
|
from app.domain.schemas.user import UserInDB
|
|
from app.infra.repositories.my_repository import MyRepository
|
|
from app.auth.dependencies import get_user_repository
|
|
|
|
router = APIRouter()
|
|
|
|
@router.get("/my-feature/")
|
|
async def get_my_feature(user: UserInDB = Depends(get_current_active_user)):
|
|
"""Get feature data"""
|
|
return {"feature": "data"}
|
|
|
|
@router.post("/my-feature/")
|
|
async def create_my_feature(
|
|
data: dict,
|
|
user: UserInDB = Depends(get_current_active_user),
|
|
):
|
|
"""Create new feature"""
|
|
return {"status": "created"}
|
|
```
|
|
|
|
#### Step 2: Register Router in Central Router
|
|
Edit `app/api/v1/router.py` to include your new router:
|
|
|
|
```python
|
|
# app/api/v1/router.py
|
|
from app.api.v1.endpoints import my_feature # Import your endpoint module
|
|
|
|
api_router = APIRouter()
|
|
|
|
# Add your router
|
|
api_router.include_router(
|
|
my_feature.router,
|
|
prefix="/my-feature", # Optional: URL prefix
|
|
tags=["My Feature"] # For OpenAPI documentation
|
|
)
|
|
```
|
|
|
|
#### Step 3: Authentication & Authorization
|
|
- **No Auth Required**: Endpoint is public
|
|
- **Authenticated Users**: Use `Depends(get_current_active_user)`
|
|
- **Role-Based Access**: Use `Depends(require_admin)` or `Depends(require_role(UserRole.OPERATOR))`
|
|
|
|
```python
|
|
from app.auth.permissions import require_admin, require_role
|
|
from app.domain.models.role import UserRole
|
|
|
|
@router.post("/admin-only/")
|
|
async def admin_only(user: UserInDB = Depends(require_admin)):
|
|
return {"admin": user.username}
|
|
```
|
|
|
|
#### Step 4: Response Models
|
|
Use Pydantic schemas for validation:
|
|
|
|
```python
|
|
from pydantic import BaseModel
|
|
from typing import Optional
|
|
|
|
class MyFeatureResponse(BaseModel):
|
|
id: int
|
|
name: str
|
|
status: Optional[str] = None
|
|
|
|
@router.get("/my-feature/{id}", response_model=MyFeatureResponse)
|
|
async def get_feature(id: int):
|
|
return MyFeatureResponse(id=id, name="Feature", status="active")
|
|
```
|
|
|
|
### Router Registration Pattern
|
|
The central router (`app/api/v1/router.py`) uses `include_router()` to compose all endpoints:
|
|
|
|
```python
|
|
# Current structure (98 lines):
|
|
api_router.include_router(auth.router, prefix="/auth", tags=["Auth"])
|
|
api_router.include_router(project.router, tags=["Project"])
|
|
api_router.include_router(simulation.router, tags=["Simulation Control"])
|
|
# ... 30+ more routers
|
|
```
|
|
|
|
---
|
|
|
|
## 3. Database Interaction Patterns
|
|
|
|
### ORM & Database Strategy
|
|
**TJWater uses psycopg (PostgreSQL async driver) directly, NOT SQLAlchemy ORM**
|
|
|
|
#### Multiple Database Types
|
|
- **PostgreSQL**: Main relational data (users, metadata)
|
|
- **TimescaleDB**: Time-series data (SCADA, sensor readings)
|
|
- **InfluxDB**: Alternative time-series storage (optional)
|
|
- **Metadata DB**: Separate instance (`system_hub` database)
|
|
|
|
### Database Layer Architecture
|
|
|
|
#### 1. Database Instances (`app/infra/db/postgresql/database.py`)
|
|
|
|
```python
|
|
class Database:
|
|
"""Manages async connection pooling"""
|
|
|
|
def __init__(self, db_name=None):
|
|
self.pool = None
|
|
self.db_name = db_name
|
|
|
|
def init_pool(self, db_name=None):
|
|
"""Initialize connection pool"""
|
|
self.pool = psycopg_pool.AsyncConnectionPool(...)
|
|
|
|
async def get_connection(self) -> AsyncGenerator:
|
|
"""Yield connection from pool"""
|
|
async with self.pool.connection() as conn:
|
|
yield conn
|
|
|
|
# Global instances
|
|
db = Database() # Default PostgreSQL
|
|
_database_instances = {} # Per-project caches
|
|
```
|
|
|
|
#### 2. Repository Pattern (`app/infra/repositories/`)
|
|
|
|
```python
|
|
class UserRepository:
|
|
"""Data access layer for users"""
|
|
|
|
def __init__(self, db: Database):
|
|
self.db = db
|
|
|
|
async def create_user(self, user: UserCreate) -> Optional[UserInDB]:
|
|
query = """
|
|
INSERT INTO users (username, email, hashed_password, role)
|
|
VALUES (%(username)s, %(email)s, %(hashed_password)s, %(role)s)
|
|
RETURNING id, username, email, role, created_at
|
|
"""
|
|
async with self.db.get_connection() as conn:
|
|
async with conn.cursor() as cur:
|
|
await cur.execute(query, {
|
|
'username': user.username,
|
|
'email': user.email,
|
|
'hashed_password': get_password_hash(user.password),
|
|
'role': user.role.value
|
|
})
|
|
row = await cur.fetchone()
|
|
return UserInDB(**row) if row else None
|
|
```
|
|
|
|
#### 3. Query Execution Pattern
|
|
|
|
```python
|
|
# Reading data
|
|
async with db.get_connection() as conn:
|
|
async with conn.cursor() as cur:
|
|
await cur.execute("SELECT * FROM users WHERE id = %s", (user_id,))
|
|
row = await cur.fetchone() # Returns dict_row
|
|
|
|
# Writing data
|
|
async with db.get_connection() as conn:
|
|
async with conn.cursor() as cur:
|
|
await cur.execute(
|
|
"UPDATE users SET email = %s WHERE id = %s",
|
|
(new_email, user_id)
|
|
)
|
|
# conn auto-commits (if autocommit=True in pool config)
|
|
```
|
|
|
|
### Database Configuration
|
|
|
|
Via `app/core/config.py` and `.env`:
|
|
|
|
```python
|
|
# Main PostgreSQL (users, metadata)
|
|
DB_HOST = "localhost"
|
|
DB_PORT = "5432"
|
|
DB_NAME = "tjwater"
|
|
DB_USER = "postgres"
|
|
DB_PASSWORD = "password"
|
|
|
|
# TimescaleDB (time-series)
|
|
TIMESCALEDB_DB_HOST = "localhost"
|
|
TIMESCALEDB_DB_PORT = "5433"
|
|
TIMESCALEDB_DB_NAME = "szh"
|
|
|
|
# Metadata DB (system info)
|
|
METADATA_DB_NAME = "system_hub"
|
|
```
|
|
|
|
### Dynamic Multi-Database Support
|
|
|
|
Project-specific databases are managed dynamically:
|
|
|
|
```python
|
|
# app/infra/db/dynamic_manager.py
|
|
async def get_database_instance(db_name: Optional[str] = None) -> Database:
|
|
"""Get or create DB instance for project"""
|
|
if not db_name:
|
|
return db # Default instance
|
|
|
|
if db_name not in _database_instances:
|
|
instance = Database(db_name=db_name)
|
|
instance.init_pool()
|
|
await instance.open()
|
|
_database_instances[db_name] = instance
|
|
|
|
return _database_instances[db_name]
|
|
```
|
|
|
|
### Migrations & Schema Creation
|
|
|
|
- **Location**: `resources/sql/` contains initialization scripts
|
|
- **Naming**: Numbered order (e.g., `002_create_audit_logs_table.sql`)
|
|
- **Approach**: SQL scripts are manually executed; no Alembic/Flyway framework
|
|
- **Example**: `resources/sql/002_create_audit_logs_table.sql`
|
|
|
|
---
|
|
|
|
## 4. Native Module Integration
|
|
|
|
### Architecture: Python ↔ Binary Code
|
|
|
|
TJWater integrates compiled C/Go binaries for water network simulation:
|
|
|
|
```
|
|
Python (FastAPI)
|
|
↓
|
|
app/native/wndb/
|
|
├── project.py (list, create, delete, open projects)
|
|
├── s2_junctions.py (CRUD for junctions)
|
|
├── s5_pipes.py (CRUD for pipes)
|
|
└── [40+ schema modules] (network elements)
|
|
↓
|
|
app/native/api_encap/ (Low-level binary wrappers)
|
|
└── api.so / api.dll (Compiled binary library)
|
|
```
|
|
|
|
### How Python Calls Native Code
|
|
|
|
#### 1. Native API Initialization
|
|
```python
|
|
# app/native/wndb/__init__.py
|
|
from app.native.api_encap.api import (
|
|
ChangeSet, # Change tracking
|
|
API_ADD, API_UPDATE, API_DELETE, # Operations
|
|
list_project, create_project, open_project, close_project,
|
|
# ... 100+ functions
|
|
)
|
|
```
|
|
|
|
#### 2. Example: Project Management
|
|
```python
|
|
# app/services/tjnetwork.py
|
|
from app.native.wndb import list_project, open_project, close_project
|
|
|
|
def list_project() -> list[str]:
|
|
"""List all project databases"""
|
|
# Calls into compiled binary to enumerate DB names
|
|
return api.list_project()
|
|
|
|
def open_project(name: str) -> None:
|
|
"""Open project and load into memory"""
|
|
# Binary loads .inp file or DB data
|
|
api.open_project(name)
|
|
```
|
|
|
|
#### 3. ChangeSet Tracking
|
|
```python
|
|
# For tracking network modifications
|
|
class ChangeSet:
|
|
"""Tracks ADD/UPDATE/DELETE operations on network elements"""
|
|
def __init__(self):
|
|
self.adds = []
|
|
self.updates = []
|
|
self.deletes = []
|
|
|
|
# Usage in services
|
|
def update_junction(name: str, demand: float):
|
|
change = api.set_junction(name, {"demand": demand})
|
|
# Returns ChangeSet for auditing/undo-redo
|
|
```
|
|
|
|
#### 4. Network CRUD Operations
|
|
```python
|
|
# All operations follow this pattern:
|
|
from app.native.wndb import get_all_junctions, add_junction, set_junction
|
|
|
|
# Read
|
|
junctions = get_all_junctions() # Returns list of dicts
|
|
|
|
# Create
|
|
add_junction({"id": "J1", "x": 0.0, "y": 0.0, "demand": 100.0})
|
|
|
|
# Update
|
|
set_junction("J1", {"demand": 150.0})
|
|
|
|
# Delete
|
|
delete_junction("J1")
|
|
```
|
|
|
|
### Data Flow Example: Simulation
|
|
```
|
|
FastAPI Request (POST /simulation/run)
|
|
↓
|
|
app/api/v1/endpoints/simulation.py
|
|
↓
|
|
app/services/simulation.py (business logic)
|
|
↓
|
|
app/native/wndb/ (calls binary functions)
|
|
├── open_project("szh")
|
|
├── set_option("QUALITY", "CHEMICAL")
|
|
├── run_simulation()
|
|
└── get_result()
|
|
↓
|
|
TimescaleDB/InfluxDB (store results)
|
|
↓
|
|
Response to client
|
|
```
|
|
|
|
### Python Module Constants
|
|
Native module exports constants for enumerations:
|
|
|
|
```python
|
|
from app.native.wndb import (
|
|
OPTION_UNITS_LPS, # Flow units
|
|
OPTION_PRESSURE_KPA, # Pressure units
|
|
OPTION_QUALITY_CHEMICAL, # Quality type
|
|
PIPE_STATUS_OPEN, # Pipe status
|
|
SCADA_DEVICE_TYPE_PRESSURE,
|
|
)
|
|
```
|
|
|
|
---
|
|
|
|
## 5. Authentication & Authorization Mechanisms
|
|
|
|
### Architecture: JWT + OAuth2 + Role-Based Access Control (RBAC)
|
|
|
|
### 1. Authentication Flow
|
|
|
|
#### User Registration
|
|
```python
|
|
# app/api/v1/endpoints/auth.py
|
|
@router.post("/register", response_model=UserResponse)
|
|
async def register(user_data: UserCreate):
|
|
# 1. Hash password
|
|
hashed = get_password_hash(user_data.password)
|
|
|
|
# 2. Store in DB via repository
|
|
user = await user_repo.create_user({
|
|
'username': user_data.username,
|
|
'email': user_data.email,
|
|
'hashed_password': hashed,
|
|
'role': 'USER'
|
|
})
|
|
return UserResponse.model_validate(user)
|
|
```
|
|
|
|
#### User Login & Token Generation
|
|
```python
|
|
@router.post("/login", response_model=Token)
|
|
async def login(form_data: OAuth2PasswordRequestForm = Depends()):
|
|
# 1. Verify credentials
|
|
user = await user_repo.get_user_by_username(form_data.username)
|
|
if not verify_password(form_data.password, user.hashed_password):
|
|
raise HTTPException(status_code=401, detail="Invalid credentials")
|
|
|
|
# 2. Generate JWT tokens
|
|
access_token = create_access_token(subject=user.username)
|
|
refresh_token = create_refresh_token(subject=user.username)
|
|
|
|
return Token(
|
|
access_token=access_token,
|
|
refresh_token=refresh_token,
|
|
token_type="bearer",
|
|
expires_in=settings.ACCESS_TOKEN_EXPIRE_MINUTES * 60
|
|
)
|
|
```
|
|
|
|
### 2. JWT Token Configuration
|
|
```python
|
|
# app/core/config.py
|
|
SECRET_KEY: str = "your-secret-key" # Change in production
|
|
ALGORITHM: str = "HS256"
|
|
ACCESS_TOKEN_EXPIRE_MINUTES: int = 30
|
|
REFRESH_TOKEN_EXPIRE_DAYS: int = 7
|
|
```
|
|
|
|
### 3. Token Validation & Current User
|
|
```python
|
|
# app/auth/dependencies.py
|
|
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/v1/auth/login")
|
|
|
|
async def get_current_user(token: str = Depends(oauth2_scheme)) -> UserInDB:
|
|
"""Validate JWT token and extract user"""
|
|
try:
|
|
payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM])
|
|
username: str = payload.get("sub")
|
|
token_type: str = payload.get("type", "access")
|
|
|
|
if username is None or token_type != "access":
|
|
raise HTTPException(status_code=401, detail="Invalid token")
|
|
|
|
# Fetch user from DB
|
|
user = await user_repo.get_user_by_username(username)
|
|
if user is None:
|
|
raise HTTPException(status_code=401, detail="User not found")
|
|
|
|
return user
|
|
except JWTError:
|
|
raise HTTPException(status_code=401, detail="Invalid token")
|
|
```
|
|
|
|
### 4. Role-Based Access Control (RBAC)
|
|
|
|
#### User Roles Enum
|
|
```python
|
|
# app/domain/models/role.py
|
|
class UserRole(str, Enum):
|
|
ADMIN = "ADMIN" # Full permissions
|
|
OPERATOR = "OPERATOR" # Modify data
|
|
USER = "USER" # Read/write
|
|
VIEWER = "VIEWER" # Read-only
|
|
|
|
# Hierarchy
|
|
get_hierarchy() = {
|
|
VIEWER: 1,
|
|
USER: 2,
|
|
OPERATOR: 3,
|
|
ADMIN: 4,
|
|
}
|
|
```
|
|
|
|
#### Permission Decorators
|
|
```python
|
|
# app/auth/permissions.py
|
|
|
|
def require_admin(current_user: UserInDB = Depends(get_current_active_user)) -> UserInDB:
|
|
"""Restrict to admin users"""
|
|
if current_user.role != UserRole.ADMIN:
|
|
raise HTTPException(status_code=403, detail="Admin access required")
|
|
return current_user
|
|
|
|
def require_role(role: UserRole):
|
|
"""Factory for role-based permission checks"""
|
|
async def permission_checker(
|
|
current_user: UserInDB = Depends(get_current_active_user)
|
|
) -> UserInDB:
|
|
if not UserRole(current_user.role).has_permission(role):
|
|
raise HTTPException(status_code=403, detail=f"Role {role} required")
|
|
return current_user
|
|
return permission_checker
|
|
|
|
def require_operator(current_user: UserInDB = Depends(get_current_active_user)) -> UserInDB:
|
|
return current_user if require_role(UserRole.OPERATOR) else None
|
|
```
|
|
|
|
#### Using Permissions in Endpoints
|
|
```python
|
|
# Require admin
|
|
@router.delete("/users/{user_id}")
|
|
async def delete_user(
|
|
user_id: int,
|
|
admin: UserInDB = Depends(require_admin)
|
|
):
|
|
await user_repo.delete_user(user_id)
|
|
return {"status": "deleted"}
|
|
|
|
# Require operator or higher
|
|
@router.post("/projects/")
|
|
async def create_project(
|
|
data: dict,
|
|
operator: UserInDB = Depends(require_operator)
|
|
):
|
|
return {"project": "created"}
|
|
```
|
|
|
|
### 5. Optional Keycloak Integration
|
|
```python
|
|
# app/auth/keycloak_dependencies.py
|
|
# Alternative: Validate tokens from external Keycloak server
|
|
|
|
KEYCLOAK_PUBLIC_KEY: str = "" # From .env
|
|
KEYCLOAK_ALGORITHM: str = "RS256"
|
|
|
|
# Similar JWT validation but with Keycloak's public key
|
|
```
|
|
|
|
### 6. Token Schema
|
|
```python
|
|
# app/domain/schemas/user.py
|
|
class Token(BaseModel):
|
|
access_token: str
|
|
refresh_token: Optional[str] = None
|
|
token_type: str = "bearer"
|
|
expires_in: int # Seconds
|
|
|
|
class UserInDB(BaseModel):
|
|
id: int
|
|
username: str
|
|
email: str
|
|
role: str # "ADMIN", "USER", etc.
|
|
is_active: bool
|
|
is_superuser: bool
|
|
```
|
|
|
|
---
|
|
|
|
## 6. Configuration Management
|
|
|
|
### Configuration Hierarchy
|
|
```
|
|
Environment Variables (.env file)
|
|
↓
|
|
app/core/config.py (Pydantic Settings)
|
|
↓
|
|
Injected via Depends() in endpoints/services
|
|
↓
|
|
Runtime usage
|
|
```
|
|
|
|
### Config File: `app/core/config.py`
|
|
Uses Pydantic v2 `BaseSettings` with `.env` support:
|
|
|
|
```python
|
|
from pydantic_settings import BaseSettings
|
|
|
|
class Settings(BaseSettings):
|
|
# App
|
|
PROJECT_NAME: str = "TJWater Server"
|
|
API_V1_STR: str = "/api/v1"
|
|
|
|
# Security
|
|
SECRET_KEY: str = "your-secret"
|
|
ALGORITHM: str = "HS256"
|
|
ACCESS_TOKEN_EXPIRE_MINUTES: int = 30
|
|
|
|
# Encryption (Fernet symmetric)
|
|
ENCRYPTION_KEY: str = "" # Required from .env
|
|
DATABASE_ENCRYPTION_KEY: str = ""
|
|
|
|
# PostgreSQL
|
|
DB_NAME: str = "tjwater"
|
|
DB_HOST: str = "localhost"
|
|
DB_PORT: str = "5432"
|
|
DB_USER: str = "postgres"
|
|
DB_PASSWORD: str = "password"
|
|
|
|
# TimescaleDB
|
|
TIMESCALEDB_DB_NAME: str = "tjwater"
|
|
TIMESCALEDB_DB_HOST: str = "localhost"
|
|
TIMESCALEDB_DB_PORT: str = "5433"
|
|
|
|
# Metadata DB
|
|
METADATA_DB_NAME: str = "system_hub"
|
|
METADATA_DB_HOST: str = "localhost"
|
|
METADATA_DB_PORT: str = "5432"
|
|
|
|
# Cache sizing
|
|
PROJECT_PG_CACHE_SIZE: int = 50
|
|
PROJECT_TS_CACHE_SIZE: int = 50
|
|
|
|
# Connection pooling
|
|
PROJECT_PG_POOL_SIZE: int = 5
|
|
PROJECT_TS_POOL_MIN_SIZE: int = 1
|
|
PROJECT_TS_POOL_MAX_SIZE: int = 10
|
|
|
|
# InfluxDB (optional)
|
|
INFLUXDB_URL: str = "http://localhost:8086"
|
|
INFLUXDB_TOKEN: str = "token"
|
|
|
|
@property
|
|
def SQLALCHEMY_DATABASE_URI(self) -> str:
|
|
return f"postgresql://{self.DB_USER}:{self.DB_PASSWORD}@{self.DB_HOST}:{self.DB_PORT}/{self.DB_NAME}"
|
|
|
|
model_config = SettingsConfigDict(
|
|
env_file=".env",
|
|
extra="ignore",
|
|
)
|
|
|
|
settings = Settings()
|
|
```
|
|
|
|
### Environment File: `.env.example`
|
|
```bash
|
|
# Security
|
|
SECRET_KEY=your-secret-key-change-in-production
|
|
ENCRYPTION_KEY=<generated via Fernet.generate_key()>
|
|
DATABASE_ENCRYPTION_KEY=
|
|
|
|
# PostgreSQL
|
|
DB_HOST=localhost
|
|
DB_PORT=5432
|
|
DB_NAME=tjwater
|
|
DB_USER=postgres
|
|
DB_PASSWORD=password
|
|
|
|
# TimescaleDB
|
|
TIMESCALEDB_DB_NAME=szh
|
|
TIMESCALEDB_DB_HOST=localhost
|
|
TIMESCALEDB_DB_PORT=5433
|
|
TIMESCALEDB_DB_USER=tjwater
|
|
TIMESCALEDB_DB_PASSWORD=Tjwater@123456
|
|
|
|
# Metadata DB
|
|
METADATA_DB_NAME=system_hub
|
|
METADATA_DB_HOST=localhost
|
|
|
|
# Cache/Pool
|
|
PROJECT_PG_CACHE_SIZE=50
|
|
PROJECT_TS_CACHE_SIZE=50
|
|
```
|
|
|
|
### Using Configuration in Code
|
|
```python
|
|
from app.core.config import settings
|
|
|
|
# Direct access
|
|
db_url = settings.SQLALCHEMY_DATABASE_URI
|
|
secret = settings.SECRET_KEY
|
|
|
|
# In FastAPI lifespan
|
|
@asynccontextmanager
|
|
async def lifespan(app: FastAPI):
|
|
# Initialize based on settings
|
|
db.init_pool(settings.DB_NAME)
|
|
yield
|
|
await db.close()
|
|
```
|
|
|
|
### Encryption Configuration
|
|
```python
|
|
# app/core/encryption.py
|
|
from app.core.config import settings
|
|
|
|
class Encryptor:
|
|
def __init__(self, key: Optional[bytes] = None):
|
|
if key is None:
|
|
key_str = settings.ENCRYPTION_KEY
|
|
if not key_str:
|
|
raise ValueError("ENCRYPTION_KEY not configured")
|
|
key = key_str.encode()
|
|
self.fernet = Fernet(key)
|
|
|
|
def encrypt(self, data: str) -> str:
|
|
return self.fernet.encrypt(data.encode()).decode()
|
|
|
|
def decrypt(self, data: str) -> str:
|
|
return self.fernet.decrypt(data.encode()).decode()
|
|
|
|
@staticmethod
|
|
def generate_key() -> str:
|
|
"""Generate new encryption key"""
|
|
return Fernet.generate_key().decode()
|
|
```
|
|
|
|
---
|
|
|
|
## 7. Testing Setup
|
|
|
|
### Test Structure
|
|
```
|
|
tests/
|
|
├── conftest.py # Pytest configuration & fixtures
|
|
├── api/ # API integration tests
|
|
│ ├── test_api_integration.py
|
|
│ └── test_leakage_endpoints.py
|
|
├── unit/ # Unit tests
|
|
│ ├── test_burst_location_service.py
|
|
│ ├── test_metadata_repository_dsn_decrypt.py
|
|
│ └── test_pipeline_health_analyzer.py
|
|
└── auth/ # Auth tests
|
|
└── test_encryption.py
|
|
```
|
|
|
|
### Configuration: `tests/conftest.py`
|
|
```python
|
|
import pytest
|
|
import sys
|
|
import os
|
|
|
|
# Add project root to path
|
|
sys.path.insert(0, os.path.dirname(os.path.dirname(__file__)))
|
|
|
|
def run_this_test(test_file):
|
|
"""Run a single test file"""
|
|
test_name = os.path.splitext(os.path.basename(test_file))[0]
|
|
pytest.main([test_file, "-v"])
|
|
```
|
|
|
|
### Testing Tools
|
|
```python
|
|
# requirements.txt includes:
|
|
pytest==8.3.5 # Test runner
|
|
# httpx (via FastAPI) for async HTTP client
|
|
```
|
|
|
|
### Example: API Integration Test
|
|
```python
|
|
# tests/api/test_api_integration.py
|
|
import pytest
|
|
|
|
@pytest.mark.parametrize(
|
|
"module_name, desc",
|
|
[
|
|
("app.core.encryption", "Encryption"),
|
|
("app.auth.permissions", "Permissions"),
|
|
("app.api.v1.endpoints.auth", "Auth endpoints"),
|
|
],
|
|
)
|
|
def test_module_imports(module_name, desc):
|
|
"""Verify critical modules can be imported"""
|
|
try:
|
|
__import__(module_name)
|
|
except ImportError as e:
|
|
pytest.fail(f"Cannot import {desc}: {e}")
|
|
|
|
def test_router_configuration():
|
|
"""Verify router is properly configured"""
|
|
from app.api.v1 import router
|
|
api_router = router.api_router
|
|
routes = [r.path for r in api_router.routes if hasattr(r, "path")]
|
|
assert len(routes) > 0
|
|
```
|
|
|
|
### Example: Encryption Test
|
|
```python
|
|
# tests/auth/test_encryption.py
|
|
from app.core.encryption import Encryptor
|
|
|
|
def test_encrypt_decrypt():
|
|
"""Test encryption roundtrip"""
|
|
encryptor = Encryptor.generate_key()
|
|
enc = Encryptor(encryptor)
|
|
|
|
plaintext = "sensitive_data"
|
|
encrypted = enc.encrypt(plaintext)
|
|
decrypted = enc.decrypt(encrypted)
|
|
|
|
assert decrypted == plaintext
|
|
```
|
|
|
|
### Running Tests
|
|
```bash
|
|
# Run all tests
|
|
pytest tests/ -v
|
|
|
|
# Run specific test file
|
|
pytest tests/api/test_api_integration.py -v
|
|
|
|
# Run specific test
|
|
pytest tests/auth/test_encryption.py::test_encrypt_decrypt -v
|
|
```
|
|
|
|
---
|
|
|
|
## 8. Audit & Security Features
|
|
|
|
### Audit Middleware
|
|
Automatically logs all critical operations:
|
|
|
|
```python
|
|
# app/infra/audit/middleware.py
|
|
class AuditMiddleware(BaseHTTPMiddleware):
|
|
AUDIT_METHODS = ["POST", "PUT", "DELETE", "PATCH"]
|
|
AUDIT_TAGS = ["Audit", "Users", "Project", "Junctions", "Pipes", ...]
|
|
|
|
async def dispatch(self, request: Request, call_next):
|
|
# 1. Capture request body (for writes)
|
|
# 2. Execute request
|
|
# 3. Log to audit DB if matches criteria
|
|
# 4. Return response
|
|
```
|
|
|
|
### Audit Logging
|
|
```python
|
|
# app/core/audit.py
|
|
async def log_audit_event(
|
|
action: AuditAction, # "CREATE", "UPDATE", "DELETE", "LOGIN"
|
|
resource_type: str, # "user", "project", "pipe"
|
|
resource_id: str,
|
|
actor_id: int, # User ID
|
|
details: dict = None
|
|
):
|
|
"""Log operation to audit table"""
|
|
# Persists to audit_logs table via AuditRepository
|
|
```
|
|
|
|
### Audit Schema
|
|
```python
|
|
# app/domain/schemas/audit.py
|
|
class AuditLog(BaseModel):
|
|
id: int
|
|
timestamp: datetime
|
|
action: str
|
|
resource_type: str
|
|
resource_id: str
|
|
actor_id: int
|
|
details: Optional[dict] = None
|
|
```
|
|
|
|
---
|
|
|
|
## 9. Key Dependencies & Tech Stack
|
|
|
|
### Core Framework
|
|
```
|
|
FastAPI==0.128.0 # Web framework
|
|
uvicorn==0.34.0 # ASGI server
|
|
Pydantic==2.10.6 # Data validation
|
|
pydantic-settings==2.12.0 # Configuration management
|
|
```
|
|
|
|
### Database
|
|
```
|
|
psycopg==3.2.5 # PostgreSQL async driver
|
|
psycopg-pool==3.3.0 # Connection pooling
|
|
GeoAlchemy2==0.17.1 # GIS support
|
|
SQLAlchemy==2.0.41 # ORM (optional)
|
|
```
|
|
|
|
### Security & Auth
|
|
```
|
|
PyJWT==2.10.1 # JWT handling
|
|
python-jose==3.5.0 # Token validation
|
|
passlib==1.7.4 # Password hashing
|
|
cryptography==46.0.3 # Encryption (Fernet)
|
|
Authlib==1.6.6 # OAuth2 support
|
|
```
|
|
|
|
### Data Science / Simulation
|
|
```
|
|
wntr==1.3.2 # Water network toolkit (EPANET)
|
|
numpy==1.26.2 # Numerical computing
|
|
pandas==2.2.3 # Data frames
|
|
scipy==1.15.2 # Scientific computing
|
|
scikit-learn==1.6.1 # ML algorithms
|
|
```
|
|
|
|
### Time-Series & Data Storage
|
|
```
|
|
influxdb-client==1.48.0 # InfluxDB client
|
|
redis==5.2.1 # Caching & sessions
|
|
```
|
|
|
|
### Utilities
|
|
```
|
|
python-dotenv==1.2.1 # .env file support
|
|
python-multipart==0.0.20 # File upload handling
|
|
email-validator==2.3.0 # Email validation
|
|
```
|
|
|
|
---
|
|
|
|
## 10. Documentation & Deployment
|
|
|
|
### Key Documentation Files
|
|
- **SECURITY_README.md**: Complete security implementation guide
|
|
- **DEPLOYMENT.md**: Production deployment checklist
|
|
- **INTEGRATION_CHECKLIST.md**: System integration steps
|
|
- **setup_server.md**: Initial server setup
|
|
- **setup_influxdb.md**: InfluxDB configuration
|
|
|
|
### Docker Deployment
|
|
```yaml
|
|
# infra/docker/docker-compose.yml
|
|
services:
|
|
api:
|
|
build: ...
|
|
ports: ["8000:8000"]
|
|
environment:
|
|
- DB_HOST=postgis
|
|
- TIMESCALEDB_HOST=timescaledb
|
|
- REDIS_HOST=redis
|
|
|
|
postgis:
|
|
image: postgis/postgis:14-3.5
|
|
|
|
timescaledb:
|
|
image: timescale/timescaledb:latest-pg15
|
|
|
|
redis:
|
|
image: redis:latest
|
|
|
|
influxdb:
|
|
image: influxdb:2.7
|
|
|
|
keycloak:
|
|
image: keycloak/keycloak:latest
|
|
|
|
grafana:
|
|
image: grafana/grafana:latest
|
|
```
|
|
|
|
---
|
|
|
|
## 11. Recommended Documentation to Create
|
|
|
|
### For Secondary Development Team:
|
|
|
|
1. **API Development Guide**
|
|
- How to add new endpoints (covered in Section 2)
|
|
- Common endpoint patterns
|
|
- Error handling standards
|
|
- Response format conventions
|
|
|
|
2. **Database Development Guide**
|
|
- Repository pattern usage
|
|
- Query building best practices
|
|
- Migration procedures
|
|
- Multi-database considerations
|
|
|
|
3. **Native Module Integration Guide**
|
|
- How to add new native functions
|
|
- ChangeSet tracking
|
|
- Binary library updates
|
|
- Debugging native code calls
|
|
|
|
4. **Authentication & Authorization**
|
|
- Token generation and validation
|
|
- Role-based access control implementation
|
|
- Permission decorator usage
|
|
- Keycloak integration
|
|
|
|
5. **Testing & CI/CD**
|
|
- Unit test patterns
|
|
- Integration test setup
|
|
- Test database configuration
|
|
- GitHub Actions workflow
|
|
|
|
6. **Deployment & DevOps**
|
|
- Docker build & deployment
|
|
- Kubernetes manifests
|
|
- Environment variable management
|
|
- Production security checklist
|
|
|
|
7. **Troubleshooting Guide**
|
|
- Common issues and solutions
|
|
- Database connection problems
|
|
- Authentication debugging
|
|
- Native module errors
|
|
|
|
---
|
|
|
|
## Summary Table: Key Components
|
|
|
|
| Component | Location | Purpose | Key Technology |
|
|
|-----------|----------|---------|-----------------|
|
|
| **API Routes** | `app/api/v1/endpoints/` | REST endpoints | FastAPI, Pydantic |
|
|
| **Router** | `app/api/v1/router.py` | Central route registration | APIRouter |
|
|
| **Auth** | `app/auth/` | JWT/OAuth2 validation | PyJWT, passlib |
|
|
| **Database** | `app/infra/db/` | Data persistence | psycopg, async pools |
|
|
| **Repository** | `app/infra/repositories/` | Data access layer | psycopg cursor |
|
|
| **Domain** | `app/domain/` | Models & schemas | Pydantic, Enum |
|
|
| **Services** | `app/services/` | Business logic | Python, native APIs |
|
|
| **Native API** | `app/native/wndb/` | Binary integration | ctypes/cffi, .so/.dll |
|
|
| **Config** | `app/core/config.py` | Settings management | Pydantic Settings |
|
|
| **Security** | `app/core/security.py` | Encryption, hashing | cryptography, passlib |
|
|
| **Audit** | `app/infra/audit/` | Operation logging | Middleware, Repository |
|
|
| **Tests** | `tests/` | Test suite | pytest |
|
|
|
|
---
|
|
|
|
*This guide provides the essential patterns and structures needed for secondary development on TJWaterServerBinary. Refer to individual source files for detailed implementation examples.*
|