Files
flowsint/flowsint-api/app/api/sketch_utils.py
dextmorgn cf69a7e088 fix(ci): make isort's first-party grouping invocation-independent
Without known_first_party, isort's own-package detection depends on
which files it's given: sorted alone, a file inside flowsint_enrichers
gets flowsint_enrichers imports grouped separately from flowsint_core/
flowsint_types; sorted together with the rest of the workspace (as
`make lint` and CI do), they collapse into one group. lint-staged only
ever passes the staged subset, so a commit could pass the pre-commit
hook locally and still fail `make lint` in CI — silently, since both
are "isort says it's fine," just about different file sets.

Pin the four packages as known_first_party in all four pyproject.toml
so grouping is the same regardless of invocation scope. The rest of
the diff is isort re-normalizing ~100 files to the now-consistent
order — import statements only, verified against the diff.
2026-08-25 09:33:33 +02:00

134 lines
4.5 KiB
Python

"""Utilities for sketch operations, including automatic timestamp updates."""
from datetime import datetime
from functools import wraps
from typing import Callable
from uuid import UUID
from fastapi import BackgroundTasks
from sqlalchemy.orm import Session
from flowsint_core.core.models import Investigation, Sketch
def update_sketch_last_modified(db: Session, sketch_id: str | UUID) -> None:
"""
Update the last_updated_at timestamp for a sketch and its parent investigation.
This function is designed to be run as a background task to avoid
blocking the response. It updates both the sketch's and its parent
investigation's last_updated_at fields to the current time.
Args:
db: SQLAlchemy database session
sketch_id: The ID of the sketch to update
"""
try:
sketch = db.query(Sketch).filter(Sketch.id == sketch_id).first()
if sketch:
current_time = datetime.now()
# Update sketch timestamp
sketch.last_updated_at = current_time
# Update parent investigation timestamp if it exists
if sketch.investigation_id:
investigation = (
db.query(Investigation)
.filter(Investigation.id == sketch.investigation_id)
.first()
)
if investigation:
investigation.last_updated_at = current_time
db.commit()
except Exception as e:
# Log error but don't raise to avoid disrupting background task
print(f"Error updating sketch/investigation timestamp for {sketch_id}: {e}")
db.rollback()
def update_sketch_timestamp(func: Callable) -> Callable:
"""
Decorator to automatically update sketch's last_updated_at timestamp.
This decorator:
1. Extracts the sketch_id from route parameters
2. Schedules a background task to update last_updated_at
3. Returns the response immediately (non-blocking)
Usage:
@router.post("/{sketch_id}/nodes/add")
@update_sketch_timestamp
def add_node(
sketch_id: str,
node: NodeInput,
background_tasks: BackgroundTasks,
db: Session = Depends(get_db),
...
):
# Your route logic here
pass
Requirements:
- Route must have a 'sketch_id' parameter (path or query)
- Route must have 'background_tasks: BackgroundTasks' parameter
- Route must have 'db: Session' parameter
"""
@wraps(func)
async def async_wrapper(*args, **kwargs):
# Extract required dependencies from kwargs
sketch_id = kwargs.get("sketch_id")
background_tasks: BackgroundTasks = kwargs.get("background_tasks")
db: Session = kwargs.get("db")
if not sketch_id:
raise ValueError(
"sketch_id parameter is required for @update_sketch_timestamp"
)
if not background_tasks:
raise ValueError(
"background_tasks parameter is required for @update_sketch_timestamp"
)
if not db:
raise ValueError("db parameter is required for @update_sketch_timestamp")
# Schedule the timestamp update as a background task
background_tasks.add_task(update_sketch_last_modified, db, sketch_id)
# Execute the original route function
return await func(*args, **kwargs)
@wraps(func)
def sync_wrapper(*args, **kwargs):
# Extract required dependencies from kwargs
sketch_id = kwargs.get("sketch_id")
background_tasks: BackgroundTasks = kwargs.get("background_tasks")
db: Session = kwargs.get("db")
if not sketch_id:
raise ValueError(
"sketch_id parameter is required for @update_sketch_timestamp"
)
if not background_tasks:
raise ValueError(
"background_tasks parameter is required for @update_sketch_timestamp"
)
if not db:
raise ValueError("db parameter is required for @update_sketch_timestamp")
# Schedule the timestamp update as a background task
background_tasks.add_task(update_sketch_last_modified, db, sketch_id)
# Execute the original route function
return func(*args, **kwargs)
# Return the appropriate wrapper based on whether the function is async
import inspect
if inspect.iscoroutinefunction(func):
return async_wrapper
else:
return sync_wrapper