Akoda35 commited on
Commit
fafc54e
·
verified ·
1 Parent(s): 3ebb32f

Upload 2 files

Browse files
core/script_helpers/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ # Script helper modules for PyRunner
core/script_helpers/pyrunner_datastore.py ADDED
@@ -0,0 +1,326 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ PyRunner DataStore API for scripts.
3
+
4
+ This module provides a simple key-value data store interface backed by
5
+ the PyRunner SQLite database. Data stores must be created through the
6
+ PyRunner web interface before they can be used in scripts.
7
+
8
+ Usage:
9
+ from pyrunner_datastore import DataStore
10
+
11
+ # Open a data store (must exist in PyRunner UI)
12
+ store = DataStore("my_store")
13
+
14
+ # Store values (any JSON-serializable type)
15
+ store["key"] = "value"
16
+ store["config"] = {"retries": 3, "timeout": 30}
17
+ store["results"] = [1, 2, 3, 4, 5]
18
+
19
+ # Retrieve values
20
+ value = store["key"]
21
+ value = store.get("key", default=None)
22
+
23
+ # Check existence
24
+ if "key" in store:
25
+ print(store["key"])
26
+
27
+ # Delete
28
+ del store["key"]
29
+
30
+ # Iterate
31
+ for key, value in store.items():
32
+ print(f"{key}: {value}")
33
+
34
+ # Utilities
35
+ store.keys() # List all keys
36
+ store.values() # List all values
37
+ store.clear() # Delete all entries
38
+ len(store) # Entry count
39
+ """
40
+
41
+ import json
42
+ import os
43
+ import sqlite3
44
+ from typing import Any, Iterator, List, Optional, Tuple
45
+
46
+
47
+ class DataStore:
48
+ """
49
+ Simple key-value data store backed by SQLite.
50
+ Provides dict-like access to stored data.
51
+ """
52
+
53
+ def __init__(self, name: str):
54
+ """
55
+ Open a data store by name.
56
+
57
+ Args:
58
+ name: The name of the data store (must exist in PyRunner)
59
+
60
+ Raises:
61
+ RuntimeError: If PYRUNNER_DB_PATH is not set
62
+ ValueError: If the data store does not exist
63
+ """
64
+ self.name = name
65
+ self._db_path = os.environ.get("PYRUNNER_DB_PATH")
66
+ if not self._db_path:
67
+ raise RuntimeError(
68
+ "PYRUNNER_DB_PATH not set. This module must be run from PyRunner."
69
+ )
70
+
71
+ # Verify the data store exists
72
+ self._store_id = self._get_store_id()
73
+ if not self._store_id:
74
+ raise ValueError(f"Data store '{name}' does not exist. Create it in the PyRunner UI first.")
75
+
76
+ def _get_connection(self) -> sqlite3.Connection:
77
+ """Get a database connection."""
78
+ conn = sqlite3.connect(self._db_path)
79
+ conn.row_factory = sqlite3.Row
80
+ return conn
81
+
82
+ def _get_store_id(self) -> Optional[str]:
83
+ """Get the UUID of this data store."""
84
+ with self._get_connection() as conn:
85
+ cursor = conn.execute(
86
+ "SELECT id FROM datastores WHERE name = ?",
87
+ (self.name,)
88
+ )
89
+ row = cursor.fetchone()
90
+ return row["id"] if row else None
91
+
92
+ def __getitem__(self, key: str) -> Any:
93
+ """
94
+ Get a value by key.
95
+
96
+ Args:
97
+ key: The key to retrieve
98
+
99
+ Returns:
100
+ The stored value (deserialized from JSON)
101
+
102
+ Raises:
103
+ KeyError: If the key does not exist
104
+ """
105
+ with self._get_connection() as conn:
106
+ cursor = conn.execute(
107
+ "SELECT value_json FROM datastore_entries "
108
+ "WHERE datastore_id = ? AND key = ?",
109
+ (self._store_id, key)
110
+ )
111
+ row = cursor.fetchone()
112
+ if row is None:
113
+ raise KeyError(key)
114
+ return json.loads(row["value_json"])
115
+
116
+ def __setitem__(self, key: str, value: Any) -> None:
117
+ """
118
+ Set a value. Creates or updates the entry.
119
+
120
+ Args:
121
+ key: The key to set
122
+ value: Any JSON-serializable value
123
+ """
124
+ value_json = json.dumps(value)
125
+ with self._get_connection() as conn:
126
+ # Use INSERT OR REPLACE for SQLite upsert
127
+ conn.execute(
128
+ """
129
+ INSERT INTO datastore_entries (id, datastore_id, key, value_json, created_at, updated_at)
130
+ VALUES (lower(hex(randomblob(16))), ?, ?, ?, datetime('now'), datetime('now'))
131
+ ON CONFLICT(datastore_id, key) DO UPDATE SET
132
+ value_json = excluded.value_json,
133
+ updated_at = datetime('now')
134
+ """,
135
+ (self._store_id, key, value_json)
136
+ )
137
+ conn.commit()
138
+
139
+ def __delitem__(self, key: str) -> None:
140
+ """
141
+ Delete a key.
142
+
143
+ Args:
144
+ key: The key to delete
145
+
146
+ Raises:
147
+ KeyError: If the key does not exist
148
+ """
149
+ with self._get_connection() as conn:
150
+ cursor = conn.execute(
151
+ "DELETE FROM datastore_entries WHERE datastore_id = ? AND key = ?",
152
+ (self._store_id, key)
153
+ )
154
+ conn.commit()
155
+ if cursor.rowcount == 0:
156
+ raise KeyError(key)
157
+
158
+ def __contains__(self, key: str) -> bool:
159
+ """
160
+ Check if a key exists.
161
+
162
+ Args:
163
+ key: The key to check
164
+
165
+ Returns:
166
+ True if the key exists, False otherwise
167
+ """
168
+ with self._get_connection() as conn:
169
+ cursor = conn.execute(
170
+ "SELECT 1 FROM datastore_entries WHERE datastore_id = ? AND key = ?",
171
+ (self._store_id, key)
172
+ )
173
+ return cursor.fetchone() is not None
174
+
175
+ def __len__(self) -> int:
176
+ """
177
+ Return the number of entries.
178
+
179
+ Returns:
180
+ The count of entries in this data store
181
+ """
182
+ with self._get_connection() as conn:
183
+ cursor = conn.execute(
184
+ "SELECT COUNT(*) as count FROM datastore_entries WHERE datastore_id = ?",
185
+ (self._store_id,)
186
+ )
187
+ return cursor.fetchone()["count"]
188
+
189
+ def __iter__(self) -> Iterator[str]:
190
+ """
191
+ Iterate over keys.
192
+
193
+ Yields:
194
+ Each key in the data store
195
+ """
196
+ return iter(self.keys())
197
+
198
+ def __repr__(self) -> str:
199
+ return f"DataStore('{self.name}')"
200
+
201
+ def get(self, key: str, default: Any = None) -> Any:
202
+ """
203
+ Get a value with a default if not found.
204
+
205
+ Args:
206
+ key: The key to retrieve
207
+ default: Value to return if key doesn't exist (default: None)
208
+
209
+ Returns:
210
+ The stored value or the default
211
+ """
212
+ try:
213
+ return self[key]
214
+ except KeyError:
215
+ return default
216
+
217
+ def keys(self) -> List[str]:
218
+ """
219
+ Return all keys in the data store.
220
+
221
+ Returns:
222
+ A list of all keys
223
+ """
224
+ with self._get_connection() as conn:
225
+ cursor = conn.execute(
226
+ "SELECT key FROM datastore_entries WHERE datastore_id = ? ORDER BY key",
227
+ (self._store_id,)
228
+ )
229
+ return [row["key"] for row in cursor.fetchall()]
230
+
231
+ def values(self) -> List[Any]:
232
+ """
233
+ Return all values in the data store.
234
+
235
+ Returns:
236
+ A list of all values (deserialized from JSON)
237
+ """
238
+ with self._get_connection() as conn:
239
+ cursor = conn.execute(
240
+ "SELECT value_json FROM datastore_entries WHERE datastore_id = ? ORDER BY key",
241
+ (self._store_id,)
242
+ )
243
+ return [json.loads(row["value_json"]) for row in cursor.fetchall()]
244
+
245
+ def items(self) -> List[Tuple[str, Any]]:
246
+ """
247
+ Return all key-value pairs.
248
+
249
+ Returns:
250
+ A list of (key, value) tuples
251
+ """
252
+ with self._get_connection() as conn:
253
+ cursor = conn.execute(
254
+ "SELECT key, value_json FROM datastore_entries WHERE datastore_id = ? ORDER BY key",
255
+ (self._store_id,)
256
+ )
257
+ return [(row["key"], json.loads(row["value_json"])) for row in cursor.fetchall()]
258
+
259
+ def clear(self) -> int:
260
+ """
261
+ Delete all entries in the data store.
262
+
263
+ Returns:
264
+ The number of entries deleted
265
+ """
266
+ with self._get_connection() as conn:
267
+ cursor = conn.execute(
268
+ "DELETE FROM datastore_entries WHERE datastore_id = ?",
269
+ (self._store_id,)
270
+ )
271
+ conn.commit()
272
+ return cursor.rowcount
273
+
274
+ def setdefault(self, key: str, default: Any = None) -> Any:
275
+ """
276
+ Get a value, setting it to default if it doesn't exist.
277
+
278
+ Args:
279
+ key: The key to get/set
280
+ default: Value to set if key doesn't exist (default: None)
281
+
282
+ Returns:
283
+ The existing value or the newly set default
284
+ """
285
+ try:
286
+ return self[key]
287
+ except KeyError:
288
+ self[key] = default
289
+ return default
290
+
291
+ def update(self, other: dict = None, **kwargs) -> None:
292
+ """
293
+ Update the data store with key-value pairs.
294
+
295
+ Args:
296
+ other: A dictionary of key-value pairs
297
+ **kwargs: Additional key-value pairs
298
+ """
299
+ if other:
300
+ for key, value in other.items():
301
+ self[key] = value
302
+ for key, value in kwargs.items():
303
+ self[key] = value
304
+
305
+ def pop(self, key: str, *default) -> Any:
306
+ """
307
+ Remove and return a value.
308
+
309
+ Args:
310
+ key: The key to remove
311
+ default: Optional default value if key doesn't exist
312
+
313
+ Returns:
314
+ The removed value or the default
315
+
316
+ Raises:
317
+ KeyError: If the key doesn't exist and no default is provided
318
+ """
319
+ try:
320
+ value = self[key]
321
+ del self[key]
322
+ return value
323
+ except KeyError:
324
+ if default:
325
+ return default[0]
326
+ raise