Skip to content

Commit e67eb14

Browse files
committed
Merge branch 'persistence/manager' into persistence/module-states
2 parents 8b35b14 + 7e48ee0 commit e67eb14

File tree

40 files changed

+1110
-640
lines changed

40 files changed

+1110
-640
lines changed

backend_py/primary/poetry.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

backend_py/primary/primary/routers/persistence/sessions/__init__.py

Whitespace-only changes.

backend_py/primary/primary/routers/persistence/sessions/converters.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ def to_api_session_metadata_summary(metadata: SessionMetadataWithId) -> schemas.
1111
createdAt=metadata.created_at.isoformat(),
1212
updatedAt=metadata.updated_at.isoformat(),
1313
version=metadata.version,
14+
hash=metadata.hash,
1415
)
1516

1617

backend_py/primary/primary/routers/persistence/sessions/router.py

Lines changed: 25 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,12 @@
55

66
from primary.middleware.add_browser_cache import no_cache
77
from primary.services.database_access.session_access.session_access import SessionAccess
8+
from primary.services.database_access.query_collation_options import SortDirection
89
from primary.auth.auth_helper import AuthHelper, AuthenticatedUser
910
from primary.services.database_access.session_access.types import (
1011
NewSession,
1112
SessionUpdate,
12-
SortBy,
13-
SortDirection,
13+
SessionSortBy,
1414
)
1515
from primary.routers.persistence.sessions.converters import (
1616
to_api_session_metadata_summary,
@@ -28,21 +28,28 @@
2828
@no_cache
2929
async def get_sessions_metadata(
3030
user: AuthenticatedUser = Depends(AuthHelper.get_authenticated_user),
31-
sort_by: Optional[SortBy] = Query(None, description="Sort the result by"),
31+
sort_by: Optional[SessionSortBy] = Query(None, description="Sort the result by"),
3232
sort_direction: Optional[SortDirection] = Query(SortDirection.ASC, description="Sort direction: 'asc' or 'desc'"),
33-
limit: Optional[int] = Query(10, ge=1, le=100, description="Limit the number of results"),
34-
):
33+
limit: int = Query(10, ge=1, le=100, description="Limit the number of results"),
34+
page: int = Query(0, ge=0),
35+
) -> list[schemas.SessionMetadataWithId]:
3536
access = SessionAccess.create(user.get_user_id())
3637
async with access:
3738
items = await access.get_filtered_sessions_metadata_for_user_async(
38-
sort_by=sort_by, sort_direction=sort_direction, limit=limit
39+
sort_by=sort_by,
40+
sort_direction=sort_direction,
41+
limit=limit,
42+
offset=limit * page,
3943
)
44+
4045
return [to_api_session_metadata_summary(item) for item in items]
4146

4247

4348
@router.get("/sessions/{session_id}", response_model=schemas.SessionDocument)
4449
@no_cache
45-
async def get_session(session_id: str, user: AuthenticatedUser = Depends(AuthHelper.get_authenticated_user)):
50+
async def get_session(
51+
session_id: str, user: AuthenticatedUser = Depends(AuthHelper.get_authenticated_user)
52+
) -> schemas.SessionDocument:
4653
access = SessionAccess.create(user.get_user_id())
4754
async with access:
4855
session = await access.get_session_by_id_async(session_id)
@@ -53,7 +60,9 @@ async def get_session(session_id: str, user: AuthenticatedUser = Depends(AuthHel
5360

5461
@router.get("/sessions/metadata/{session_id}", response_model=schemas.SessionMetadata)
5562
@no_cache
56-
async def get_session_metadata(session_id: str, user: AuthenticatedUser = Depends(AuthHelper.get_authenticated_user)):
63+
async def get_session_metadata(
64+
session_id: str, user: AuthenticatedUser = Depends(AuthHelper.get_authenticated_user)
65+
) -> schemas.SessionMetadata:
5766
access = SessionAccess.create(user.get_user_id())
5867
async with access:
5968
metadata = await access.get_session_metadata_async(session_id)
@@ -63,26 +72,29 @@ async def get_session_metadata(session_id: str, user: AuthenticatedUser = Depend
6372

6473

6574
@router.post("/sessions", response_model=str)
66-
async def create_session(session: NewSession, user: AuthenticatedUser = Depends(AuthHelper.get_authenticated_user)):
75+
async def create_session(
76+
session: NewSession, user: AuthenticatedUser = Depends(AuthHelper.get_authenticated_user)
77+
) -> str:
6778
access = SessionAccess.create(user.get_user_id())
6879
async with access:
6980
session_id = await access.insert_session_async(session)
7081
return session_id
7182

7283

73-
@router.put("/sessions/{session_id}")
84+
@router.put("/sessions/{session_id}", description="Updates a session object. Allows for partial update objects")
7485
async def update_session(
7586
session_id: str,
7687
session_update: SessionUpdate,
7788
user: AuthenticatedUser = Depends(AuthHelper.get_authenticated_user),
78-
):
89+
) -> schemas.SessionDocument:
7990
access = SessionAccess.create(user.get_user_id())
8091
async with access:
81-
await access.update_session_async(session_id, session_update)
92+
updated_session = await access.update_session_async(session_id, session_update)
93+
return to_api_session_record(updated_session)
8294

8395

8496
@router.delete("/sessions/{session_id}")
85-
async def delete_session(session_id: str, user: AuthenticatedUser = Depends(AuthHelper.get_authenticated_user)):
97+
async def delete_session(session_id: str, user: AuthenticatedUser = Depends(AuthHelper.get_authenticated_user)) -> None:
8698
access = SessionAccess.create(user.get_user_id())
8799
async with access:
88100
await access.delete_session_async(session_id)

backend_py/primary/primary/routers/persistence/sessions/schemas.py

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,6 @@
22
from pydantic import BaseModel
33

44

5-
class SessionMetadataWithId(BaseModel):
6-
id: str
7-
title: str
8-
description: Optional[str]
9-
createdAt: str
10-
updatedAt: str
11-
version: int
12-
13-
145
class SessionMetadata(BaseModel):
156
title: str
167
description: Optional[str]
@@ -20,6 +11,10 @@ class SessionMetadata(BaseModel):
2011
hash: str
2112

2213

14+
class SessionMetadataWithId(SessionMetadata):
15+
id: str
16+
17+
2318
class SessionDocument(BaseModel):
2419
id: str
2520
ownerId: str

backend_py/primary/primary/routers/persistence/snapshots/__init__.py

Whitespace-only changes.

backend_py/primary/primary/routers/persistence/snapshots/converters.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from primary.services.database_access.snapshot_access.models import SnapshotAccessLog
1+
from primary.services.database_access.snapshot_access.models import SnapshotAccessLogDocument
22
from primary.services.database_access.snapshot_access.types import Snapshot, SnapshotMetadata, SnapshotMetadataWithId
33

44
from . import schemas
@@ -35,12 +35,13 @@ def to_api_snapshot(snapshot: Snapshot) -> schemas.Snapshot:
3535
)
3636

3737

38-
def to_api_snapshot_access_log(access_log: SnapshotAccessLog, metadata: SnapshotMetadata) -> schemas.SnapshotAccessLog:
38+
def to_api_snapshot_access_log(access_log: SnapshotAccessLogDocument) -> schemas.SnapshotAccessLog:
3939
return schemas.SnapshotAccessLog(
4040
visitorId=access_log.visitor_id,
4141
snapshotId=access_log.snapshot_id,
4242
visits=access_log.visits,
4343
firstVisitedAt=access_log.first_visited_at.isoformat() if access_log.first_visited_at else None,
4444
lastVisitedAt=access_log.last_visited_at.isoformat() if access_log.last_visited_at else None,
45-
snapshotMetadata=to_api_snapshot_metadata(metadata),
45+
snapshotDeleted=access_log.snapshot_deleted,
46+
snapshotMetadata=to_api_snapshot_metadata(access_log.snapshot_metadata),
4647
)
Lines changed: 43 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,18 @@
11
import logging
22
from typing import List, Optional
33

4-
from fastapi import APIRouter, Depends, HTTPException, Query
4+
from fastapi import APIRouter, BackgroundTasks, Depends, Query
55

66
from primary.services.database_access.snapshot_access.types import (
77
NewSnapshot,
8-
SnapshotUpdate,
9-
SortBy,
10-
SortDirection,
8+
SnapshotSortBy,
9+
SnapshotAccessLogSortBy,
1110
)
1211
from primary.middleware.add_browser_cache import no_cache
1312
from primary.services.database_access.snapshot_access.snapshot_access import SnapshotAccess
14-
from primary.services.database_access.snapshot_access.snapshot_logs_access import SnapshotLogsAccess
15-
from primary.services.database_access.snapshot_access.query_collation_options import QueryCollationOptions
13+
from primary.services.database_access.snapshot_access.snapshot_log_access import SnapshotLogAccess
14+
from primary.services.database_access.query_collation_options import QueryCollationOptions, SortDirection
15+
from primary.services.database_access.workers.mark_logs_deleted import mark_logs_deleted_worker
1616

1717

1818
from primary.auth.auth_helper import AuthHelper, AuthenticatedUser
@@ -33,41 +33,31 @@
3333
@router.get("/recent_snapshots", response_model=list[schemas.SnapshotAccessLog])
3434
async def get_recent_snapshots(
3535
user: AuthenticatedUser = Depends(AuthHelper.get_authenticated_user),
36-
sort_by: Optional[SortBy] = Query(SortBy.LAST_VISIT, description="Sort the result by"),
37-
sort_direction: Optional[SortDirection] = Query(SortDirection.DESC, description="Sort direction: 'asc' or 'desc'"),
38-
limit: Optional[int] = Query(5, ge=1, le=100, description="Limit the number of results"),
39-
offset: Optional[int] = Query(0, ge=0, description="The offset of the results"),
36+
sort_by: Optional[SnapshotAccessLogSortBy] = Query(None, description="Sort the result by"),
37+
sort_direction: Optional[SortDirection] = Query(None, description="Sort direction: 'asc' or 'desc'"),
38+
limit: Optional[int] = Query(None, ge=1, le=100, description="Limit the number of results"),
39+
offset: Optional[int] = Query(None, ge=0, description="The offset of the results"),
4040
) -> list[schemas.SnapshotAccessLog]:
41-
async with (
42-
SnapshotAccess.create(user.get_user_id()) as snapshot_access,
43-
SnapshotLogsAccess.create(user.get_user_id()) as log_access,
44-
):
41+
async with SnapshotLogAccess.create(user.get_user_id()) as log_access:
4542
collation_options = QueryCollationOptions(sort_by=sort_by, sort_dir=sort_direction, limit=limit, offset=offset)
4643

4744
recent_logs = await log_access.get_access_logs_for_user_async(collation_options)
4845

49-
payload: list[schemas.SnapshotAccessLog] = []
50-
51-
for log in recent_logs:
52-
metadata = await snapshot_access.get_snapshot_metadata_async(log.snapshot_id, log.snapshot_owner_id)
53-
54-
payload.append(to_api_snapshot_access_log(log, metadata))
55-
56-
return payload
46+
return [to_api_snapshot_access_log(log) for log in recent_logs]
5747

5848

5949
@router.get("/snapshots", response_model=List[schemas.SnapshotMetadata])
6050
@no_cache
6151
async def get_snapshots_metadata(
6252
user: AuthenticatedUser = Depends(AuthHelper.get_authenticated_user),
63-
sort_by: Optional[SortBy] = Query(SortBy.LAST_VISIT, description="Sort the result by"),
53+
sort_by: Optional[SnapshotSortBy] = Query(SnapshotSortBy.UPDATED_AT, description="Sort the result by"),
6454
sort_direction: Optional[SortDirection] = Query(SortDirection.DESC, description="Sort direction: 'asc' or 'desc'"),
6555
limit: Optional[int] = Query(10, ge=1, le=100, description="Limit the number of results"),
6656
) -> List[schemas.SnapshotMetadata]:
6757
access = SnapshotAccess.create(user.get_user_id())
6858
async with access:
6959
items = await access.get_filtered_snapshots_metadata_for_user_async(
70-
sort_by=sort_by, sort_direction=sort_direction, limit=limit
60+
sort_by=sort_by, sort_direction=sort_direction, limit=limit, offset=0
7161
)
7262
return [to_api_snapshot_metadata_summary(item) for item in items]
7363

@@ -77,58 +67,54 @@ async def get_snapshots_metadata(
7767
async def get_snapshot(
7868
snapshot_id: str, user: AuthenticatedUser = Depends(AuthHelper.get_authenticated_user)
7969
) -> schemas.Snapshot:
80-
access = SnapshotAccess.create(user.get_user_id())
81-
logs_access = SnapshotLogsAccess.create(user_id=user.get_user_id())
82-
83-
async with access, logs_access:
84-
snapshot = await access.get_snapshot_by_id_async(snapshot_id)
85-
if not snapshot:
86-
raise HTTPException(status_code=404, detail="Snapshot not found")
87-
88-
await logs_access.log_snapshot_visit_async(snapshot_id, snapshot.owner_id)
89-
70+
snapshot_access = SnapshotAccess.create(user.get_user_id())
71+
log_access = SnapshotLogAccess.create(user_id=user.get_user_id())
72+
73+
async with snapshot_access, log_access:
74+
snapshot = await snapshot_access.get_snapshot_by_id_async(snapshot_id)
75+
# Should we clear the log if a snapshot was not found? This could mean that the snapshot was
76+
# deleted but deletion of logs has failed
77+
await log_access.log_snapshot_visit_async(snapshot_id, snapshot.owner_id)
9078
return to_api_snapshot(snapshot)
9179

9280

9381
@router.get("/snapshots/metadata/{snapshot_id}", response_model=schemas.SnapshotMetadata)
9482
@no_cache
95-
async def get_snapshot_metadata(snapshot_id: str, user: AuthenticatedUser = Depends(AuthHelper.get_authenticated_user)):
83+
async def get_snapshot_metadata(
84+
snapshot_id: str, user: AuthenticatedUser = Depends(AuthHelper.get_authenticated_user)
85+
) -> schemas.SnapshotMetadata:
9686
access = SnapshotAccess.create(user.get_user_id())
9787
async with access:
9888
metadata = await access.get_snapshot_metadata_async(snapshot_id)
99-
if not metadata:
100-
raise HTTPException(status_code=404, detail="Session metadata not found")
10189
return to_api_snapshot_metadata(metadata)
10290

10391

10492
@router.post("/snapshots", response_model=str)
10593
async def create_snapshot(
10694
session: NewSnapshot, user: AuthenticatedUser = Depends(AuthHelper.get_authenticated_user)
10795
) -> str:
108-
access = SnapshotAccess.create(user.get_user_id())
109-
logs_access = SnapshotLogsAccess.create(user.get_user_id())
96+
snapshot_access = SnapshotAccess.create(user.get_user_id())
97+
log_access = SnapshotLogAccess.create(user.get_user_id())
11098

111-
async with access, logs_access:
112-
snapshot_id = await access.insert_snapshot_async(session)
99+
async with snapshot_access, log_access:
100+
snapshot_id = await snapshot_access.insert_snapshot_async(session)
113101

114-
# We count snapshot creation as implicit visit. This also makes it so
115-
await logs_access.log_snapshot_visit_async(snapshot_id=snapshot_id, snapshot_owner_id=user.get_user_id())
102+
# We count snapshot creation as implicit visit. This also makes it so we can get recently created ones alongside other shared screenshots
103+
await log_access.log_snapshot_visit_async(snapshot_id=snapshot_id, snapshot_owner_id=user.get_user_id())
116104
return snapshot_id
117105

118106

119-
@router.put("/snapshots/{snapshot_id}")
120-
async def update_snapshot(
107+
@router.delete("/snapshots/{snapshot_id}")
108+
async def delete_snapshot(
121109
snapshot_id: str,
122-
snapshot_update: SnapshotUpdate,
110+
background_tasks: BackgroundTasks,
123111
user: AuthenticatedUser = Depends(AuthHelper.get_authenticated_user),
124-
):
125-
access = SnapshotAccess.create(user.get_user_id())
126-
async with access:
127-
await access.update_snapshot_metadata_async(snapshot_id, snapshot_update)
128-
129-
130-
@router.delete("/snapshots/{snapshot_id}")
131-
async def delete_snapshot(snapshot_id: str, user: AuthenticatedUser = Depends(AuthHelper.get_authenticated_user)):
132-
access = SnapshotAccess.create(user.get_user_id())
133-
async with access:
134-
await access.delete_snapshot_async(snapshot_id)
112+
) -> None:
113+
snapshot_access = SnapshotAccess.create(user.get_user_id())
114+
async with snapshot_access:
115+
await snapshot_access.delete_snapshot_async(snapshot_id)
116+
117+
# This is the fastest solution for the moment. As we are expecting <= 150 logs per snapshot
118+
# and consistency is not critical, we can afford to do this in the background and without
119+
# a safety net. We can later consider adding this to a queue for better reliability.
120+
background_tasks.add_task(mark_logs_deleted_worker, snapshot_id=snapshot_id)

backend_py/primary/primary/routers/persistence/snapshots/schemas.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ class SnapshotAccessLog(BaseModel):
2929
visits: int
3030
firstVisitedAt: str | None
3131
lastVisitedAt: str | None
32+
snapshotDeleted: bool
3233

3334
snapshotMetadata: SnapshotMetadata
3435

backend_py/primary/primary/services/database_access/_utils.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import hashlib
2+
from typing import Any, cast
23

34

45
# Utility function to hash a JSON string using SHA-256
@@ -11,3 +12,7 @@ def hash_json_string(json_string: str) -> str:
1112
hash_bytes = hashlib.sha256(data).digest()
1213
hash_hex = "".join(f"{b:02x}" for b in hash_bytes)
1314
return hash_hex
15+
16+
17+
def cast_query_params(params: list[dict[str, Any]]) -> list[dict[str, object]]:
18+
return cast(list[dict[str, object]], params)

0 commit comments

Comments
 (0)