Coverage for node / src / stigmem_node / routes / cards.py: 94%

25 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-05-25 01:49 +0000

1"""Memory cards route — spec §20 (Phase 9). 

2 

3GET /v1/cards/{entity_uri} Fetch (and optionally force-refresh) the memory card 

4 for a specific entity. 

5""" 

6 

7from __future__ import annotations 

8 

9from typing import Annotated 

10 

11from fastapi import APIRouter, Depends, HTTPException, Query, status 

12 

13from ..auth import Identity, resolve_identity 

14from ..card_materializer import get_fresh_card, refresh_card 

15from ..db import db 

16from ..entity_normalizer import NormalizationError, normalize_entity_uri 

17from ..models.cards import MemoryCardResponse 

18from ..models.constants import VALID_SCOPES 

19 

20router = APIRouter(prefix="/v1/cards", tags=["cards"]) 

21 

22 

23@router.get("/{entity_uri:path}", response_model=MemoryCardResponse) 

24def get_card( 

25 entity_uri: str, 

26 identity: Annotated[Identity, Depends(resolve_identity)], 

27 scope: str = Query("local"), 

28 refresh: bool = Query(False, description="Force refresh even if card is fresh"), 

29) -> MemoryCardResponse: 

30 """Fetch the synthesized memory card for an entity (Spec-X11-Recall-Graph). 

31 

32 Returns 404 when the entity has no live facts. 

33 """ 

34 if not identity.can_read(): 34 ↛ 35line 34 didn't jump to line 35 because the condition on line 34 was never true

35 raise HTTPException( 

36 status_code=status.HTTP_403_FORBIDDEN, 

37 detail="read permission required", 

38 ) 

39 if scope not in VALID_SCOPES: 

40 raise HTTPException( 

41 status_code=status.HTTP_400_BAD_REQUEST, 

42 detail=f"scope must be one of {sorted(VALID_SCOPES)}", 

43 ) 

44 

45 try: 

46 entity_uri = normalize_entity_uri(entity_uri) 

47 except NormalizationError as exc: 

48 raise HTTPException( 

49 status_code=status.HTTP_400_BAD_REQUEST, 

50 detail=f"invalid_entity_uri: {exc}", 

51 ) from exc 

52 

53 with db() as conn: 

54 card = ( 

55 refresh_card(entity_uri, scope, identity.tenant_id, conn) 

56 if refresh 

57 else get_fresh_card(entity_uri, scope, identity.tenant_id, conn) 

58 ) 

59 

60 if card is None: 

61 raise HTTPException( 

62 status_code=status.HTTP_404_NOT_FOUND, 

63 detail="no facts found for entity", 

64 ) 

65 

66 return MemoryCardResponse( 

67 entity_uri=card.entity_uri, 

68 scope=card.scope, 

69 summary=card.summary, 

70 fact_hashes=card.fact_hashes, 

71 avg_confidence=card.avg_confidence, 

72 refreshed_at=card.refreshed_at, 

73 is_stale=card.is_stale, 

74 has_contradictions=card.has_contradictions, 

75 )