Files
delphi-database/app/api/advanced_variables.py
HotSwapp bac8cc4bd5 changes
2025-08-18 20:20:04 -05:00

552 lines
17 KiB
Python

"""
Advanced Template Variables API
This API provides comprehensive variable management for document templates including:
- Variable definition and configuration
- Context-specific value management
- Advanced processing with conditional logic and calculations
- Variable testing and validation
"""
from __future__ import annotations
from typing import List, Optional, Dict, Any, Union
from fastapi import APIRouter, Depends, HTTPException, status, Query, Body
from sqlalchemy.orm import Session, joinedload
from sqlalchemy import func, or_, and_
from pydantic import BaseModel, Field
from datetime import datetime
from app.database.base import get_db
from app.auth.security import get_current_user
from app.models.user import User
from app.models.template_variables import (
TemplateVariable, VariableContext, VariableAuditLog,
VariableType, VariableTemplate, VariableGroup
)
from app.services.advanced_variables import VariableProcessor
from app.services.query_utils import paginate_with_total
router = APIRouter()
# Pydantic schemas for API
class VariableCreate(BaseModel):
name: str = Field(..., max_length=100, description="Unique variable name")
display_name: Optional[str] = Field(None, max_length=200)
description: Optional[str] = None
variable_type: VariableType = VariableType.STRING
required: bool = False
default_value: Optional[str] = None
formula: Optional[str] = None
conditional_logic: Optional[Dict[str, Any]] = None
data_source_query: Optional[str] = None
lookup_table: Optional[str] = None
lookup_key_field: Optional[str] = None
lookup_value_field: Optional[str] = None
validation_rules: Optional[Dict[str, Any]] = None
format_pattern: Optional[str] = None
depends_on: Optional[List[str]] = None
scope: str = "global"
category: Optional[str] = None
tags: Optional[List[str]] = None
cache_duration_minutes: int = 0
class VariableUpdate(BaseModel):
display_name: Optional[str] = None
description: Optional[str] = None
required: Optional[bool] = None
active: Optional[bool] = None
default_value: Optional[str] = None
formula: Optional[str] = None
conditional_logic: Optional[Dict[str, Any]] = None
data_source_query: Optional[str] = None
lookup_table: Optional[str] = None
lookup_key_field: Optional[str] = None
lookup_value_field: Optional[str] = None
validation_rules: Optional[Dict[str, Any]] = None
format_pattern: Optional[str] = None
depends_on: Optional[List[str]] = None
category: Optional[str] = None
tags: Optional[List[str]] = None
cache_duration_minutes: Optional[int] = None
class VariableResponse(BaseModel):
id: int
name: str
display_name: Optional[str]
description: Optional[str]
variable_type: VariableType
required: bool
active: bool
default_value: Optional[str]
scope: str
category: Optional[str]
tags: Optional[List[str]]
created_at: datetime
updated_at: Optional[datetime]
class Config:
from_attributes = True
class VariableContextSet(BaseModel):
variable_name: str
value: Any
context_type: str = "global"
context_id: str = "default"
class VariableTestRequest(BaseModel):
variables: List[str]
context_type: str = "global"
context_id: str = "default"
test_context: Optional[Dict[str, Any]] = None
class VariableTestResponse(BaseModel):
resolved: Dict[str, Any]
unresolved: List[str]
processing_time_ms: float
errors: List[str]
class VariableAuditResponse(BaseModel):
id: int
variable_name: str
context_type: Optional[str]
context_id: Optional[str]
old_value: Optional[str]
new_value: Optional[str]
change_type: str
change_reason: Optional[str]
changed_by: Optional[str]
changed_at: datetime
class Config:
from_attributes = True
@router.post("/variables/", response_model=VariableResponse)
async def create_variable(
variable_data: VariableCreate,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""Create a new template variable with advanced features"""
# Check if variable name already exists
existing = db.query(TemplateVariable).filter(
TemplateVariable.name == variable_data.name
).first()
if existing:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Variable with name '{variable_data.name}' already exists"
)
# Validate dependencies
if variable_data.depends_on:
for dep_name in variable_data.depends_on:
dep_var = db.query(TemplateVariable).filter(
TemplateVariable.name == dep_name,
TemplateVariable.active == True
).first()
if not dep_var:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Dependency variable '{dep_name}' not found"
)
# Create variable
variable = TemplateVariable(
name=variable_data.name,
display_name=variable_data.display_name,
description=variable_data.description,
variable_type=variable_data.variable_type,
required=variable_data.required,
default_value=variable_data.default_value,
formula=variable_data.formula,
conditional_logic=variable_data.conditional_logic,
data_source_query=variable_data.data_source_query,
lookup_table=variable_data.lookup_table,
lookup_key_field=variable_data.lookup_key_field,
lookup_value_field=variable_data.lookup_value_field,
validation_rules=variable_data.validation_rules,
format_pattern=variable_data.format_pattern,
depends_on=variable_data.depends_on,
scope=variable_data.scope,
category=variable_data.category,
tags=variable_data.tags,
cache_duration_minutes=variable_data.cache_duration_minutes,
created_by=current_user.username,
active=True
)
db.add(variable)
db.commit()
db.refresh(variable)
return variable
@router.get("/variables/", response_model=List[VariableResponse])
async def list_variables(
skip: int = Query(0, ge=0),
limit: int = Query(50, ge=1, le=200),
category: Optional[str] = Query(None),
variable_type: Optional[VariableType] = Query(None),
active_only: bool = Query(True),
search: Optional[str] = Query(None),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""List template variables with filtering options"""
query = db.query(TemplateVariable)
if active_only:
query = query.filter(TemplateVariable.active == True)
if category:
query = query.filter(TemplateVariable.category == category)
if variable_type:
query = query.filter(TemplateVariable.variable_type == variable_type)
if search:
search_filter = f"%{search}%"
query = query.filter(
or_(
TemplateVariable.name.ilike(search_filter),
TemplateVariable.display_name.ilike(search_filter),
TemplateVariable.description.ilike(search_filter)
)
)
query = query.order_by(TemplateVariable.category, TemplateVariable.name)
variables, _ = paginate_with_total(query, skip, limit, False)
return variables
@router.get("/variables/{variable_id}", response_model=VariableResponse)
async def get_variable(
variable_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""Get a specific variable by ID"""
variable = db.query(TemplateVariable).filter(
TemplateVariable.id == variable_id
).first()
if not variable:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Variable not found"
)
return variable
@router.put("/variables/{variable_id}", response_model=VariableResponse)
async def update_variable(
variable_id: int,
variable_data: VariableUpdate,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""Update a template variable"""
variable = db.query(TemplateVariable).filter(
TemplateVariable.id == variable_id
).first()
if not variable:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Variable not found"
)
# Update fields that are provided
update_data = variable_data.dict(exclude_unset=True)
for field, value in update_data.items():
setattr(variable, field, value)
db.commit()
db.refresh(variable)
return variable
@router.delete("/variables/{variable_id}")
async def delete_variable(
variable_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""Delete a template variable (soft delete by setting active=False)"""
variable = db.query(TemplateVariable).filter(
TemplateVariable.id == variable_id
).first()
if not variable:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Variable not found"
)
# Soft delete
variable.active = False
db.commit()
return {"message": "Variable deleted successfully"}
@router.post("/variables/test", response_model=VariableTestResponse)
async def test_variables(
test_request: VariableTestRequest,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""Test variable resolution with given context"""
import time
start_time = time.time()
errors = []
try:
processor = VariableProcessor(db)
resolved, unresolved = processor.resolve_variables(
variables=test_request.variables,
context_type=test_request.context_type,
context_id=test_request.context_id,
base_context=test_request.test_context or {}
)
processing_time = (time.time() - start_time) * 1000
return VariableTestResponse(
resolved=resolved,
unresolved=unresolved,
processing_time_ms=processing_time,
errors=errors
)
except Exception as e:
processing_time = (time.time() - start_time) * 1000
errors.append(str(e))
return VariableTestResponse(
resolved={},
unresolved=test_request.variables,
processing_time_ms=processing_time,
errors=errors
)
@router.post("/variables/set-value")
async def set_variable_value(
context_data: VariableContextSet,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""Set a variable value in a specific context"""
processor = VariableProcessor(db)
success = processor.set_variable_value(
variable_name=context_data.variable_name,
value=context_data.value,
context_type=context_data.context_type,
context_id=context_data.context_id,
user_name=current_user.username
)
if not success:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Failed to set variable value"
)
return {"message": "Variable value set successfully"}
@router.get("/variables/{variable_id}/contexts")
async def get_variable_contexts(
variable_id: int,
context_type: Optional[str] = Query(None),
skip: int = Query(0, ge=0),
limit: int = Query(50, ge=1, le=200),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""Get all contexts where this variable has values"""
query = db.query(VariableContext).filter(
VariableContext.variable_id == variable_id
)
if context_type:
query = query.filter(VariableContext.context_type == context_type)
query = query.order_by(VariableContext.context_type, VariableContext.context_id)
contexts, total = paginate_with_total(query, skip, limit, True)
return {
"items": [
{
"context_type": ctx.context_type,
"context_id": ctx.context_id,
"value": ctx.value,
"computed_value": ctx.computed_value,
"is_valid": ctx.is_valid,
"validation_errors": ctx.validation_errors,
"last_computed_at": ctx.last_computed_at
}
for ctx in contexts
],
"total": total
}
@router.get("/variables/{variable_id}/audit", response_model=List[VariableAuditResponse])
async def get_variable_audit_log(
variable_id: int,
skip: int = Query(0, ge=0),
limit: int = Query(20, ge=1, le=100),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""Get audit log for a variable"""
query = db.query(VariableAuditLog, TemplateVariable.name).join(
TemplateVariable, VariableAuditLog.variable_id == TemplateVariable.id
).filter(
VariableAuditLog.variable_id == variable_id
).order_by(VariableAuditLog.changed_at.desc())
audit_logs, _ = paginate_with_total(query, skip, limit, False)
return [
VariableAuditResponse(
id=log.id,
variable_name=var_name,
context_type=log.context_type,
context_id=log.context_id,
old_value=log.old_value,
new_value=log.new_value,
change_type=log.change_type,
change_reason=log.change_reason,
changed_by=log.changed_by,
changed_at=log.changed_at
)
for log, var_name in audit_logs
]
@router.get("/templates/{template_id}/variables")
async def get_template_variables(
template_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""Get all variables associated with a template"""
processor = VariableProcessor(db)
variables = processor.get_variables_for_template(template_id)
return {"variables": variables}
@router.post("/templates/{template_id}/variables/{variable_id}")
async def associate_variable_with_template(
template_id: int,
variable_id: int,
override_default: Optional[str] = Body(None),
override_required: Optional[bool] = Body(None),
display_order: int = Body(0),
group_name: Optional[str] = Body(None),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""Associate a variable with a template"""
# Check if association already exists
existing = db.query(VariableTemplate).filter(
VariableTemplate.template_id == template_id,
VariableTemplate.variable_id == variable_id
).first()
if existing:
# Update existing association
existing.override_default = override_default
existing.override_required = override_required
existing.display_order = display_order
existing.group_name = group_name
else:
# Create new association
association = VariableTemplate(
template_id=template_id,
variable_id=variable_id,
override_default=override_default,
override_required=override_required,
display_order=display_order,
group_name=group_name
)
db.add(association)
db.commit()
return {"message": "Variable associated with template successfully"}
@router.delete("/templates/{template_id}/variables/{variable_id}")
async def remove_variable_from_template(
template_id: int,
variable_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""Remove variable association from template"""
association = db.query(VariableTemplate).filter(
VariableTemplate.template_id == template_id,
VariableTemplate.variable_id == variable_id
).first()
if not association:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Variable association not found"
)
db.delete(association)
db.commit()
return {"message": "Variable removed from template successfully"}
@router.get("/categories")
async def get_variable_categories(
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""Get list of variable categories"""
categories = db.query(
TemplateVariable.category,
func.count(TemplateVariable.id).label('count')
).filter(
TemplateVariable.active == True,
TemplateVariable.category.isnot(None)
).group_by(TemplateVariable.category).order_by(TemplateVariable.category).all()
return [
{"category": cat, "count": count}
for cat, count in categories
]