-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathstorage.py
More file actions
130 lines (99 loc) · 3.89 KB
/
Copy pathstorage.py
File metadata and controls
130 lines (99 loc) · 3.89 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
"""Persistence for FermentOps batches.
Two interchangeable stores share one interface:
* :class:`SqliteStore` keeps every batch as a JSON document in a local SQLite
file (kept out of git). Used when running the app for real.
* :class:`NullStore` persists nothing. Used for the public demo
(``FERMENTOPS_DEMO=1``) so visitors get a private sandbox and cannot touch
anyone's real data.
"""
import os
import sys
from contextlib import contextmanager
from pathlib import Path
from typing import TYPE_CHECKING, Iterator, List, Mapping, Union
from schemas import Batch
if TYPE_CHECKING: # sqlite3 is imported lazily; see SqliteStore._connect
import sqlite3
DEFAULT_DB_PATH = Path(__file__).parent / "data" / "fermentops.db"
_SCHEMA = """
CREATE TABLE IF NOT EXISTS batches (
batch_id TEXT PRIMARY KEY,
data TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS meta (
key TEXT PRIMARY KEY,
value TEXT NOT NULL
);
"""
class SqliteStore:
"""Stores each :class:`Batch` as one JSON row, in creation order."""
persistent = True
def __init__(self, path: Union[str, Path]) -> None:
self.path = Path(path)
self.path.parent.mkdir(parents=True, exist_ok=True)
with self._connect() as conn:
conn.executescript(_SCHEMA)
@contextmanager
def _connect(self) -> "Iterator[sqlite3.Connection]":
# Imported here, not at module level: the in-browser (Pyodide) build
# never touches SQLite and does not ship the module by default.
import sqlite3
conn = sqlite3.connect(self.path)
try:
with conn: # commit on success, roll back on error
yield conn
finally:
conn.close()
def load_batches(self) -> List[Batch]:
with self._connect() as conn:
rows = conn.execute(
"SELECT data FROM batches ORDER BY rowid"
).fetchall()
return [Batch.model_validate_json(data) for (data,) in rows]
def save_batch(self, batch: Batch) -> None:
"""Insert or update a batch (an update keeps its position)."""
with self._connect() as conn:
conn.execute(
"INSERT INTO batches (batch_id, data) VALUES (?, ?) "
"ON CONFLICT(batch_id) DO UPDATE SET data = excluded.data",
(batch.batch_id, batch.model_dump_json()),
)
def delete_batch(self, batch_id: str) -> None:
with self._connect() as conn:
conn.execute("DELETE FROM batches WHERE batch_id = ?", (batch_id,))
def is_seeded(self) -> bool:
with self._connect() as conn:
row = conn.execute(
"SELECT value FROM meta WHERE key = 'seeded'"
).fetchone()
return row is not None
def mark_seeded(self) -> None:
with self._connect() as conn:
conn.execute(
"INSERT OR REPLACE INTO meta (key, value) "
"VALUES ('seeded', '1')"
)
class NullStore:
"""A store that keeps nothing; the session holds all data."""
persistent = False
def load_batches(self) -> List[Batch]:
return []
def save_batch(self, batch: Batch) -> None:
pass
def delete_batch(self, batch_id: str) -> None:
pass
def is_seeded(self) -> bool:
return False
def mark_seeded(self) -> None:
pass
Store = Union[SqliteStore, NullStore]
def make_store(env: Mapping[str, str] = os.environ) -> Store:
"""Pick a store from the environment.
``FERMENTOPS_DEMO=1`` selects the non-persistent demo store, as does
running in the browser (Pyodide / stlite, where there is no real disk to
persist to). Otherwise the SQLite file at ``FERMENTOPS_DB`` (default
``data/fermentops.db``) is used.
"""
if env.get("FERMENTOPS_DEMO") == "1" or sys.platform == "emscripten":
return NullStore()
return SqliteStore(env.get("FERMENTOPS_DB", DEFAULT_DB_PATH))