paskia 0.7.1__py3-none-any.whl → 0.8.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- paskia/_version.py +2 -2
- paskia/authsession.py +12 -49
- paskia/bootstrap.py +30 -25
- paskia/db/__init__.py +163 -401
- paskia/db/background.py +128 -0
- paskia/db/jsonl.py +132 -0
- paskia/db/operations.py +1241 -0
- paskia/db/structs.py +148 -0
- paskia/fastapi/admin.py +456 -215
- paskia/fastapi/api.py +16 -15
- paskia/fastapi/authz.py +7 -2
- paskia/fastapi/mainapp.py +2 -1
- paskia/fastapi/remote.py +20 -20
- paskia/fastapi/reset.py +9 -10
- paskia/fastapi/user.py +10 -18
- paskia/fastapi/ws.py +22 -19
- paskia/frontend-build/auth/admin/index.html +3 -3
- paskia/frontend-build/auth/assets/AccessDenied-aTdCvz9k.js +8 -0
- paskia/frontend-build/auth/assets/admin-BeNu48FR.css +1 -0
- paskia/frontend-build/auth/assets/admin-tVs8oyLv.js +1 -0
- paskia/frontend-build/auth/assets/{auth-BU_O38k2.css → auth-BKX7shEe.css} +1 -1
- paskia/frontend-build/auth/assets/auth-Dk3q4pNS.js +1 -0
- paskia/frontend-build/auth/index.html +3 -3
- paskia/globals.py +7 -10
- paskia/migrate/__init__.py +274 -0
- paskia/migrate/sql.py +381 -0
- paskia/util/permutil.py +16 -5
- paskia/util/sessionutil.py +3 -2
- paskia/util/userinfo.py +12 -26
- paskia-0.8.0.dist-info/METADATA +94 -0
- {paskia-0.7.1.dist-info → paskia-0.8.0.dist-info}/RECORD +33 -29
- {paskia-0.7.1.dist-info → paskia-0.8.0.dist-info}/entry_points.txt +1 -0
- paskia/db/sql.py +0 -1424
- paskia/frontend-build/auth/assets/AccessDenied-C-lL9vbN.js +0 -8
- paskia/frontend-build/auth/assets/admin-Cs6Mg773.css +0 -1
- paskia/frontend-build/auth/assets/admin-Df5_Damp.js +0 -1
- paskia/frontend-build/auth/assets/auth-Df3pjeSS.js +0 -1
- paskia/util/tokens.py +0 -44
- paskia-0.7.1.dist-info/METADATA +0 -22
- {paskia-0.7.1.dist-info → paskia-0.8.0.dist-info}/WHEEL +0 -0
paskia/db/jsonl.py
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"""
|
|
2
|
+
JSONL persistence layer for the database.
|
|
3
|
+
|
|
4
|
+
Handles file I/O, JSON diffs, and persistence. Works with plain JSON/dict data.
|
|
5
|
+
Uses aiofiles for async I/O operations.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
from collections import deque
|
|
10
|
+
from datetime import datetime, timezone
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
import aiofiles
|
|
14
|
+
import jsondiff
|
|
15
|
+
import msgspec
|
|
16
|
+
|
|
17
|
+
_logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
# Default database path
|
|
20
|
+
DB_PATH_DEFAULT = "paskia.jsonl"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class _ChangeRecord(msgspec.Struct, omit_defaults=True):
|
|
24
|
+
"""A single change record in the JSONL file."""
|
|
25
|
+
|
|
26
|
+
ts: datetime
|
|
27
|
+
a: str # action - describes the operation (e.g., "migrate", "login", "create_user")
|
|
28
|
+
u: str | None = None # user UUID who performed the action (None for system)
|
|
29
|
+
diff: dict = {}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
# msgspec encoder for change records
|
|
33
|
+
_change_encoder = msgspec.json.Encoder()
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
async def load_jsonl(db_path: Path) -> dict:
|
|
37
|
+
"""Load data from disk by applying change log.
|
|
38
|
+
|
|
39
|
+
Replays all changes from JSONL file using plain dicts (to handle
|
|
40
|
+
schema evolution).
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
db_path: Path to the JSONL database file
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
The final state after applying all changes
|
|
47
|
+
|
|
48
|
+
Raises:
|
|
49
|
+
ValueError: If file doesn't exist or cannot be loaded
|
|
50
|
+
"""
|
|
51
|
+
if not db_path.exists():
|
|
52
|
+
raise ValueError(f"Database file not found: {db_path}")
|
|
53
|
+
data_dict: dict = {}
|
|
54
|
+
try:
|
|
55
|
+
# Read entire file at once and split into lines
|
|
56
|
+
async with aiofiles.open(db_path, "rb") as f:
|
|
57
|
+
content = await f.read()
|
|
58
|
+
for line_num, line in enumerate(content.split(b"\n"), 1):
|
|
59
|
+
line = line.strip()
|
|
60
|
+
if not line:
|
|
61
|
+
continue
|
|
62
|
+
try:
|
|
63
|
+
change = msgspec.json.decode(line)
|
|
64
|
+
# Apply the diff to current state (marshal=True for $-prefixed keys)
|
|
65
|
+
data_dict = jsondiff.patch(data_dict, change["diff"], marshal=True)
|
|
66
|
+
except Exception as e:
|
|
67
|
+
raise ValueError(f"Error parsing line {line_num}: {e}")
|
|
68
|
+
except (OSError, ValueError, msgspec.DecodeError) as e:
|
|
69
|
+
raise ValueError(f"Failed to load database: {e}")
|
|
70
|
+
return data_dict
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def compute_diff(previous: dict, current: dict) -> dict | None:
|
|
74
|
+
"""Compute JSON diff between two states.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
previous: Previous state (JSON-compatible dict)
|
|
78
|
+
current: Current state (JSON-compatible dict)
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
The diff, or None if no changes
|
|
82
|
+
"""
|
|
83
|
+
diff = jsondiff.diff(previous, current, marshal=True)
|
|
84
|
+
return diff if diff else None
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def create_change_record(
|
|
88
|
+
action: str, diff: dict, user: str | None = None
|
|
89
|
+
) -> _ChangeRecord:
|
|
90
|
+
"""Create a change record for persistence."""
|
|
91
|
+
return _ChangeRecord(
|
|
92
|
+
ts=datetime.now(timezone.utc),
|
|
93
|
+
a=action,
|
|
94
|
+
u=user,
|
|
95
|
+
diff=diff,
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
async def flush_changes(
|
|
100
|
+
db_path: Path,
|
|
101
|
+
pending_changes: deque[_ChangeRecord],
|
|
102
|
+
) -> bool:
|
|
103
|
+
"""Write all pending changes to disk.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
db_path: Path to the JSONL database file
|
|
107
|
+
pending_changes: Queue of pending change records (will be cleared on success)
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
True if flush succeeded, False otherwise
|
|
111
|
+
"""
|
|
112
|
+
if not pending_changes:
|
|
113
|
+
return True
|
|
114
|
+
|
|
115
|
+
# Collect all pending changes
|
|
116
|
+
changes_to_write = list(pending_changes)
|
|
117
|
+
pending_changes.clear()
|
|
118
|
+
|
|
119
|
+
try:
|
|
120
|
+
# Build lines to append (keep as bytes, join with \n)
|
|
121
|
+
lines = [_change_encoder.encode(change) for change in changes_to_write]
|
|
122
|
+
|
|
123
|
+
# Append all lines in a single write (binary mode for Windows compatibility)
|
|
124
|
+
async with aiofiles.open(db_path, "ab") as f:
|
|
125
|
+
await f.write(b"\n".join(lines) + b"\n")
|
|
126
|
+
return True
|
|
127
|
+
except OSError:
|
|
128
|
+
_logger.exception("Failed to flush database changes")
|
|
129
|
+
# Re-queue the changes on failure
|
|
130
|
+
for change in reversed(changes_to_write):
|
|
131
|
+
pending_changes.appendleft(change)
|
|
132
|
+
return False
|