2026-08-06 16:10:56 +00:00
|
|
|
import json
|
|
|
|
|
from datetime import datetime, timezone
|
|
|
|
|
from uuid import uuid4
|
2026-08-04 19:20:19 +00:00
|
|
|
|
2026-08-06 16:10:56 +00:00
|
|
|
from fastapi import APIRouter, Depends, HTTPException, Query
|
|
|
|
|
from sqlalchemy.orm import Session
|
|
|
|
|
|
|
|
|
|
from app.database import get_db
|
|
|
|
|
from app.models import NetObjectModel, RelationModel
|
|
|
|
|
from app.schemas import (
|
|
|
|
|
EntityResponse, ListResponse, Relation,
|
|
|
|
|
RelationCreate, RelationUpdate,
|
|
|
|
|
)
|
|
|
|
|
from app.services.inventory import _to_relation
|
2026-08-04 19:20:19 +00:00
|
|
|
|
|
|
|
|
router = APIRouter(prefix='/relations', tags=['relations'])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get('', response_model=ListResponse[Relation])
|
|
|
|
|
def list_relations(
|
|
|
|
|
source_id: str | None = None,
|
|
|
|
|
target_id: str | None = None,
|
|
|
|
|
layer: str | None = None,
|
|
|
|
|
type: str | None = Query(default=None),
|
|
|
|
|
min_confidence: float | None = None,
|
2026-08-06 16:10:56 +00:00
|
|
|
status: str | None = None,
|
|
|
|
|
db: Session = Depends(get_db),
|
|
|
|
|
):
|
|
|
|
|
q = db.query(RelationModel)
|
2026-08-04 19:20:19 +00:00
|
|
|
if source_id:
|
2026-08-06 16:10:56 +00:00
|
|
|
q = q.filter(RelationModel.source_id == source_id)
|
2026-08-04 19:20:19 +00:00
|
|
|
if target_id:
|
2026-08-06 16:10:56 +00:00
|
|
|
q = q.filter(RelationModel.target_id == target_id)
|
2026-08-04 19:20:19 +00:00
|
|
|
if layer:
|
2026-08-06 16:10:56 +00:00
|
|
|
q = q.filter(RelationModel.layer == layer)
|
2026-08-04 19:20:19 +00:00
|
|
|
if type:
|
2026-08-06 16:10:56 +00:00
|
|
|
q = q.filter(RelationModel.type == type)
|
2026-08-04 19:20:19 +00:00
|
|
|
if min_confidence is not None:
|
2026-08-06 16:10:56 +00:00
|
|
|
q = q.filter(RelationModel.confidence >= min_confidence)
|
|
|
|
|
if status:
|
|
|
|
|
q = q.filter(RelationModel.status == status)
|
|
|
|
|
rows = q.all()
|
|
|
|
|
return ListResponse(count=len(rows), items=[_to_relation(r) for r in rows])
|
2026-08-04 19:20:19 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get('/{relation_id}', response_model=EntityResponse[Relation])
|
2026-08-06 16:10:56 +00:00
|
|
|
def get_relation(relation_id: str, db: Session = Depends(get_db)):
|
|
|
|
|
row = db.query(RelationModel).filter(RelationModel.id == relation_id).first()
|
|
|
|
|
if not row:
|
|
|
|
|
raise HTTPException(status_code=404, detail='Relation not found')
|
|
|
|
|
return EntityResponse(item=_to_relation(row))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post('', response_model=EntityResponse[Relation], status_code=201)
|
|
|
|
|
def create_relation(body: RelationCreate, db: Session = Depends(get_db)):
|
|
|
|
|
for fk, val in [('source_id', body.source_id), ('target_id', body.target_id)]:
|
|
|
|
|
if not db.query(NetObjectModel).filter(NetObjectModel.id == val).first():
|
|
|
|
|
raise HTTPException(status_code=422, detail=f'{fk} {val!r} not found')
|
|
|
|
|
now = datetime.now(timezone.utc)
|
|
|
|
|
row = RelationModel(
|
|
|
|
|
id=f'rel_{uuid4().hex[:8]}',
|
|
|
|
|
type=body.type,
|
|
|
|
|
layer=body.layer,
|
|
|
|
|
source_id=body.source_id,
|
|
|
|
|
target_id=body.target_id,
|
|
|
|
|
direction=body.direction,
|
|
|
|
|
source=body.source,
|
|
|
|
|
confidence=body.confidence,
|
|
|
|
|
status=body.status,
|
|
|
|
|
attributes_json=json.dumps(body.attributes),
|
|
|
|
|
created_at=now,
|
|
|
|
|
updated_at=now,
|
|
|
|
|
)
|
|
|
|
|
db.add(row)
|
|
|
|
|
db.commit()
|
|
|
|
|
db.refresh(row)
|
|
|
|
|
return EntityResponse(item=_to_relation(row))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.patch('/{relation_id}', response_model=EntityResponse[Relation])
|
|
|
|
|
def update_relation(relation_id: str, body: RelationUpdate, db: Session = Depends(get_db)):
|
|
|
|
|
row = db.query(RelationModel).filter(RelationModel.id == relation_id).first()
|
|
|
|
|
if not row:
|
|
|
|
|
raise HTTPException(status_code=404, detail='Relation not found')
|
|
|
|
|
data = body.model_dump(exclude_unset=True)
|
|
|
|
|
for fk in ('source_id', 'target_id'):
|
|
|
|
|
if fk in data and data[fk]:
|
|
|
|
|
if not db.query(NetObjectModel).filter(NetObjectModel.id == data[fk]).first():
|
|
|
|
|
raise HTTPException(status_code=422, detail=f'{fk} {data[fk]!r} not found')
|
|
|
|
|
if 'attributes' in data:
|
|
|
|
|
data['attributes_json'] = json.dumps(data.pop('attributes'))
|
|
|
|
|
for field, value in data.items():
|
|
|
|
|
setattr(row, field, value)
|
|
|
|
|
row.updated_at = datetime.now(timezone.utc)
|
|
|
|
|
db.commit()
|
|
|
|
|
db.refresh(row)
|
|
|
|
|
return EntityResponse(item=_to_relation(row))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.delete('/{relation_id}', status_code=204)
|
|
|
|
|
def delete_relation(relation_id: str, db: Session = Depends(get_db)):
|
|
|
|
|
row = db.query(RelationModel).filter(RelationModel.id == relation_id).first()
|
|
|
|
|
if not row:
|
|
|
|
|
raise HTTPException(status_code=404, detail='Relation not found')
|
|
|
|
|
db.delete(row)
|
|
|
|
|
db.commit()
|