devdb-sql 1.0.8__tar.gz → 1.0.9__tar.gz
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.
- {devdb_sql-1.0.8 → devdb_sql-1.0.9}/PKG-INFO +1 -1
- devdb_sql-1.0.9/devdb_sql/db.py +104 -0
- {devdb_sql-1.0.8 → devdb_sql-1.0.9}/devdb_sql/table.py +155 -57
- {devdb_sql-1.0.8 → devdb_sql-1.0.9}/devdb_sql.egg-info/PKG-INFO +1 -1
- {devdb_sql-1.0.8 → devdb_sql-1.0.9}/pyproject.toml +1 -1
- {devdb_sql-1.0.8 → devdb_sql-1.0.9}/test/test.py +1 -1
- devdb_sql-1.0.8/devdb_sql/db.py +0 -61
- {devdb_sql-1.0.8 → devdb_sql-1.0.9}/LICENSE +0 -0
- {devdb_sql-1.0.8 → devdb_sql-1.0.9}/README.md +0 -0
- {devdb_sql-1.0.8 → devdb_sql-1.0.9}/devdb_sql/__init__.py +0 -0
- {devdb_sql-1.0.8 → devdb_sql-1.0.9}/devdb_sql/schema.py +0 -0
- {devdb_sql-1.0.8 → devdb_sql-1.0.9}/devdb_sql.egg-info/SOURCES.txt +0 -0
- {devdb_sql-1.0.8 → devdb_sql-1.0.9}/devdb_sql.egg-info/dependency_links.txt +0 -0
- {devdb_sql-1.0.8 → devdb_sql-1.0.9}/devdb_sql.egg-info/top_level.txt +0 -0
- {devdb_sql-1.0.8 → devdb_sql-1.0.9}/setup.cfg +0 -0
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import sqlite3
|
|
2
|
+
import threading
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from .schema import compile_schema
|
|
6
|
+
from .table import Table
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class DB:
|
|
10
|
+
def __init__(self, path):
|
|
11
|
+
if not path:
|
|
12
|
+
raise ValueError("Database path is required")
|
|
13
|
+
|
|
14
|
+
self.path = path
|
|
15
|
+
|
|
16
|
+
parent = Path(path).parent
|
|
17
|
+
|
|
18
|
+
if str(parent) not in ("", "."):
|
|
19
|
+
parent.mkdir(
|
|
20
|
+
parents=True,
|
|
21
|
+
exist_ok=True,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
# SQLite connection can now be accessed
|
|
25
|
+
# from different threads.
|
|
26
|
+
self.sqlite = sqlite3.connect(
|
|
27
|
+
path,
|
|
28
|
+
check_same_thread=False,
|
|
29
|
+
timeout=30,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
self.sqlite.row_factory = sqlite3.Row
|
|
33
|
+
|
|
34
|
+
# RLock is important because methods like insert()
|
|
35
|
+
# acquire the lock and then call find_by_id().
|
|
36
|
+
self.lock = threading.RLock()
|
|
37
|
+
|
|
38
|
+
self._tables = {}
|
|
39
|
+
|
|
40
|
+
def create(self, name, schema):
|
|
41
|
+
if not name:
|
|
42
|
+
raise ValueError("Table name is required")
|
|
43
|
+
|
|
44
|
+
if not isinstance(schema, dict):
|
|
45
|
+
raise ValueError(
|
|
46
|
+
f'Schema is required for table "{name}"'
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
with self.lock:
|
|
50
|
+
if name in self._tables:
|
|
51
|
+
raise ValueError(
|
|
52
|
+
f'Table "{name}" is already registered'
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
sql = compile_schema(name, schema)
|
|
56
|
+
|
|
57
|
+
self.sqlite.executescript(sql)
|
|
58
|
+
self.sqlite.commit()
|
|
59
|
+
|
|
60
|
+
table = Table(
|
|
61
|
+
self,
|
|
62
|
+
name,
|
|
63
|
+
schema,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
self._tables[name] = table
|
|
67
|
+
|
|
68
|
+
return table
|
|
69
|
+
|
|
70
|
+
def drop(self, name):
|
|
71
|
+
with self.lock:
|
|
72
|
+
self.sqlite.execute(
|
|
73
|
+
f"DROP TABLE IF EXISTS {name}"
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
self.sqlite.commit()
|
|
77
|
+
|
|
78
|
+
self._tables.pop(name, None)
|
|
79
|
+
|
|
80
|
+
return True
|
|
81
|
+
|
|
82
|
+
def tables(self):
|
|
83
|
+
with self.lock:
|
|
84
|
+
rows = self.sqlite.execute(
|
|
85
|
+
"""
|
|
86
|
+
SELECT name
|
|
87
|
+
FROM sqlite_master
|
|
88
|
+
WHERE type = 'table'
|
|
89
|
+
AND name NOT LIKE 'sqlite_%'
|
|
90
|
+
ORDER BY name
|
|
91
|
+
"""
|
|
92
|
+
).fetchall()
|
|
93
|
+
|
|
94
|
+
return [
|
|
95
|
+
row["name"]
|
|
96
|
+
for row in rows
|
|
97
|
+
]
|
|
98
|
+
|
|
99
|
+
def show_tables(self):
|
|
100
|
+
return self.tables()
|
|
101
|
+
|
|
102
|
+
def close(self):
|
|
103
|
+
with self.lock:
|
|
104
|
+
self.sqlite.close()
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import json
|
|
2
|
-
import sqlite3
|
|
3
2
|
|
|
4
3
|
from .schema import get_primary_key
|
|
5
4
|
|
|
@@ -37,6 +36,19 @@ class Table:
|
|
|
37
36
|
|
|
38
37
|
return value
|
|
39
38
|
|
|
39
|
+
def _serialize_query_value(self, field, value):
|
|
40
|
+
field_type = self.schema[field].get("type")
|
|
41
|
+
|
|
42
|
+
if field_type == "boolean":
|
|
43
|
+
if not isinstance(value, bool):
|
|
44
|
+
raise ValueError(f"{field} must be a boolean")
|
|
45
|
+
return 1 if value else 0
|
|
46
|
+
|
|
47
|
+
if field_type == "json":
|
|
48
|
+
return json.dumps(value, separators=(",", ":"))
|
|
49
|
+
|
|
50
|
+
return value
|
|
51
|
+
|
|
40
52
|
def _row_to_dict(self, row):
|
|
41
53
|
if row is None:
|
|
42
54
|
return None
|
|
@@ -47,7 +59,10 @@ class Table:
|
|
|
47
59
|
if field not in result:
|
|
48
60
|
continue
|
|
49
61
|
|
|
50
|
-
if
|
|
62
|
+
if (
|
|
63
|
+
config.get("type") == "boolean"
|
|
64
|
+
and result[field] is not None
|
|
65
|
+
):
|
|
51
66
|
result[field] = bool(result[field])
|
|
52
67
|
|
|
53
68
|
return result
|
|
@@ -66,6 +81,7 @@ class Table:
|
|
|
66
81
|
|
|
67
82
|
columns = ", ".join(fields)
|
|
68
83
|
placeholders = ", ".join("?" for _ in fields)
|
|
84
|
+
|
|
69
85
|
values = [
|
|
70
86
|
self._serialize_value(field, data[field])
|
|
71
87
|
for field in fields
|
|
@@ -76,11 +92,12 @@ INSERT INTO {self.name} ({columns})
|
|
|
76
92
|
VALUES ({placeholders})
|
|
77
93
|
"""
|
|
78
94
|
|
|
79
|
-
|
|
80
|
-
self.db.
|
|
95
|
+
# RLock is used because this method calls find_by_id().
|
|
96
|
+
with self.db.lock:
|
|
97
|
+
cursor = self.db.sqlite.execute(sql, values)
|
|
98
|
+
self.db.sqlite.commit()
|
|
81
99
|
|
|
82
|
-
|
|
83
|
-
return self.find_by_id(cursor.lastrowid)
|
|
100
|
+
return self.find_by_id(cursor.lastrowid)
|
|
84
101
|
|
|
85
102
|
def find_by_id(self, id):
|
|
86
103
|
sql = f"""
|
|
@@ -90,18 +107,29 @@ WHERE {self.primary_key} = ?
|
|
|
90
107
|
LIMIT 1
|
|
91
108
|
"""
|
|
92
109
|
|
|
93
|
-
|
|
94
|
-
|
|
110
|
+
with self.db.lock:
|
|
111
|
+
row = self.db.sqlite.execute(
|
|
112
|
+
sql,
|
|
113
|
+
(id,),
|
|
114
|
+
).fetchone()
|
|
115
|
+
|
|
116
|
+
return self._row_to_dict(row)
|
|
95
117
|
|
|
96
118
|
def find_all(self):
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
119
|
+
with self.db.lock:
|
|
120
|
+
rows = self.db.sqlite.execute(
|
|
121
|
+
f"SELECT * FROM {self.name}"
|
|
122
|
+
).fetchall()
|
|
123
|
+
|
|
124
|
+
return [
|
|
125
|
+
self._row_to_dict(row)
|
|
126
|
+
for row in rows
|
|
127
|
+
]
|
|
101
128
|
|
|
102
129
|
def find(self, query=None, options=None):
|
|
103
130
|
if query is None:
|
|
104
131
|
query = {}
|
|
132
|
+
|
|
105
133
|
if options is None:
|
|
106
134
|
options = {}
|
|
107
135
|
|
|
@@ -119,46 +147,72 @@ LIMIT 1
|
|
|
119
147
|
|
|
120
148
|
if isinstance(condition, dict):
|
|
121
149
|
for operator, value in condition.items():
|
|
150
|
+
|
|
122
151
|
if operator == "$eq":
|
|
123
152
|
conditions.append(f"{field} = ?")
|
|
124
|
-
values.append(
|
|
153
|
+
values.append(
|
|
154
|
+
self._serialize_query_value(field, value)
|
|
155
|
+
)
|
|
125
156
|
|
|
126
157
|
elif operator == "$ne":
|
|
127
158
|
conditions.append(f"{field} != ?")
|
|
128
|
-
values.append(
|
|
159
|
+
values.append(
|
|
160
|
+
self._serialize_query_value(field, value)
|
|
161
|
+
)
|
|
129
162
|
|
|
130
163
|
elif operator == "$gt":
|
|
131
164
|
conditions.append(f"{field} > ?")
|
|
132
|
-
values.append(
|
|
165
|
+
values.append(
|
|
166
|
+
self._serialize_query_value(field, value)
|
|
167
|
+
)
|
|
133
168
|
|
|
134
169
|
elif operator == "$gte":
|
|
135
170
|
conditions.append(f"{field} >= ?")
|
|
136
|
-
values.append(
|
|
171
|
+
values.append(
|
|
172
|
+
self._serialize_query_value(field, value)
|
|
173
|
+
)
|
|
137
174
|
|
|
138
175
|
elif operator == "$lt":
|
|
139
176
|
conditions.append(f"{field} < ?")
|
|
140
|
-
values.append(
|
|
177
|
+
values.append(
|
|
178
|
+
self._serialize_query_value(field, value)
|
|
179
|
+
)
|
|
141
180
|
|
|
142
181
|
elif operator == "$lte":
|
|
143
182
|
conditions.append(f"{field} <= ?")
|
|
144
|
-
values.append(
|
|
183
|
+
values.append(
|
|
184
|
+
self._serialize_query_value(field, value)
|
|
185
|
+
)
|
|
145
186
|
|
|
146
187
|
elif operator == "$in":
|
|
147
188
|
if not isinstance(value, list) or not value:
|
|
148
|
-
raise ValueError(
|
|
189
|
+
raise ValueError(
|
|
190
|
+
"$in requires a non-empty array"
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
placeholders = ", ".join(
|
|
194
|
+
"?" for _ in value
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
conditions.append(
|
|
198
|
+
f"{field} IN ({placeholders})"
|
|
199
|
+
)
|
|
149
200
|
|
|
150
|
-
placeholders = ", ".join("?" for _ in value)
|
|
151
|
-
conditions.append(f"{field} IN ({placeholders})")
|
|
152
201
|
values.extend(
|
|
153
202
|
self._serialize_query_value(field, item)
|
|
154
203
|
for item in value
|
|
155
204
|
)
|
|
156
205
|
|
|
157
206
|
else:
|
|
158
|
-
raise ValueError(
|
|
207
|
+
raise ValueError(
|
|
208
|
+
f'Unsupported operator "{operator}"'
|
|
209
|
+
)
|
|
210
|
+
|
|
159
211
|
else:
|
|
160
212
|
conditions.append(f"{field} = ?")
|
|
161
|
-
values.append(
|
|
213
|
+
values.append(
|
|
214
|
+
self._serialize_query_value(field, condition)
|
|
215
|
+
)
|
|
162
216
|
|
|
163
217
|
sql = f"SELECT * FROM {self.name}"
|
|
164
218
|
|
|
@@ -166,35 +220,56 @@ LIMIT 1
|
|
|
166
220
|
sql += " WHERE " + " AND ".join(conditions)
|
|
167
221
|
|
|
168
222
|
order_by = options.get("orderBy")
|
|
223
|
+
|
|
169
224
|
if order_by:
|
|
170
225
|
self._check_field(order_by)
|
|
171
|
-
|
|
226
|
+
|
|
227
|
+
order = (
|
|
228
|
+
"DESC"
|
|
229
|
+
if options.get("order") == "desc"
|
|
230
|
+
else "ASC"
|
|
231
|
+
)
|
|
232
|
+
|
|
172
233
|
sql += f" ORDER BY {order_by} {order}"
|
|
173
234
|
|
|
174
235
|
if "limit" in options:
|
|
175
236
|
limit = options["limit"]
|
|
176
|
-
|
|
177
|
-
|
|
237
|
+
|
|
238
|
+
if (
|
|
239
|
+
isinstance(limit, bool)
|
|
240
|
+
or not isinstance(limit, int)
|
|
241
|
+
or limit < 0
|
|
242
|
+
):
|
|
243
|
+
raise ValueError(
|
|
244
|
+
"limit must be a positive integer"
|
|
245
|
+
)
|
|
246
|
+
|
|
178
247
|
sql += f" LIMIT {limit}"
|
|
179
248
|
|
|
180
249
|
if "offset" in options:
|
|
181
250
|
offset = options["offset"]
|
|
182
|
-
|
|
183
|
-
|
|
251
|
+
|
|
252
|
+
if (
|
|
253
|
+
isinstance(offset, bool)
|
|
254
|
+
or not isinstance(offset, int)
|
|
255
|
+
or offset < 0
|
|
256
|
+
):
|
|
257
|
+
raise ValueError(
|
|
258
|
+
"offset must be a positive integer"
|
|
259
|
+
)
|
|
260
|
+
|
|
184
261
|
sql += f" OFFSET {offset}"
|
|
185
262
|
|
|
186
|
-
|
|
187
|
-
|
|
263
|
+
with self.db.lock:
|
|
264
|
+
rows = self.db.sqlite.execute(
|
|
265
|
+
sql,
|
|
266
|
+
values,
|
|
267
|
+
).fetchall()
|
|
188
268
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
raise ValueError(f"{field} must be a boolean")
|
|
194
|
-
return 1 if value else 0
|
|
195
|
-
if field_type == "json":
|
|
196
|
-
return json.dumps(value, separators=(",", ":"))
|
|
197
|
-
return value
|
|
269
|
+
return [
|
|
270
|
+
self._row_to_dict(row)
|
|
271
|
+
for row in rows
|
|
272
|
+
]
|
|
198
273
|
|
|
199
274
|
def update_by_id(self, id, data):
|
|
200
275
|
if not isinstance(data, dict):
|
|
@@ -209,13 +284,20 @@ LIMIT 1
|
|
|
209
284
|
self._check_field(field)
|
|
210
285
|
|
|
211
286
|
if field == self.primary_key:
|
|
212
|
-
raise ValueError(
|
|
287
|
+
raise ValueError(
|
|
288
|
+
"Primary key cannot be updated"
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
set_clause = ", ".join(
|
|
292
|
+
f"{field} = ?"
|
|
293
|
+
for field in fields
|
|
294
|
+
)
|
|
213
295
|
|
|
214
|
-
set_clause = ", ".join(f"{field} = ?" for field in fields)
|
|
215
296
|
values = [
|
|
216
297
|
self._serialize_value(field, data[field])
|
|
217
298
|
for field in fields
|
|
218
299
|
]
|
|
300
|
+
|
|
219
301
|
values.append(id)
|
|
220
302
|
|
|
221
303
|
sql = f"""
|
|
@@ -224,30 +306,46 @@ SET {set_clause}
|
|
|
224
306
|
WHERE {self.primary_key} = ?
|
|
225
307
|
"""
|
|
226
308
|
|
|
227
|
-
|
|
228
|
-
|
|
309
|
+
with self.db.lock:
|
|
310
|
+
cursor = self.db.sqlite.execute(
|
|
311
|
+
sql,
|
|
312
|
+
values,
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
self.db.sqlite.commit()
|
|
229
316
|
|
|
230
|
-
|
|
231
|
-
|
|
317
|
+
if cursor.rowcount == 0:
|
|
318
|
+
return None
|
|
232
319
|
|
|
233
|
-
|
|
320
|
+
return self.find_by_id(id)
|
|
234
321
|
|
|
235
322
|
def delete_by_id(self, id):
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
323
|
+
with self.db.lock:
|
|
324
|
+
cursor = self.db.sqlite.execute(
|
|
325
|
+
f"""
|
|
326
|
+
DELETE FROM {self.name}
|
|
327
|
+
WHERE {self.primary_key} = ?
|
|
328
|
+
""",
|
|
329
|
+
(id,),
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
self.db.sqlite.commit()
|
|
333
|
+
|
|
334
|
+
return cursor.rowcount > 0
|
|
242
335
|
|
|
243
336
|
def drop(self):
|
|
244
|
-
self.db.
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
337
|
+
with self.db.lock:
|
|
338
|
+
self.db.sqlite.execute(
|
|
339
|
+
f"DROP TABLE IF EXISTS {self.name}"
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
self.db.sqlite.commit()
|
|
343
|
+
self.db._tables.pop(self.name, None)
|
|
344
|
+
|
|
345
|
+
return True
|
|
248
346
|
|
|
249
347
|
# JS-compatible aliases
|
|
250
348
|
findById = find_by_id
|
|
251
349
|
findAll = find_all
|
|
252
350
|
updateById = update_by_id
|
|
253
|
-
deleteById = delete_by_id
|
|
351
|
+
deleteById = delete_by_id
|
devdb_sql-1.0.8/devdb_sql/db.py
DELETED
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
import sqlite3
|
|
2
|
-
from pathlib import Path
|
|
3
|
-
|
|
4
|
-
from .schema import compile_schema
|
|
5
|
-
from .table import Table
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
class DB:
|
|
9
|
-
def __init__(self, path):
|
|
10
|
-
if not path:
|
|
11
|
-
raise ValueError("Database path is required")
|
|
12
|
-
|
|
13
|
-
self.path = path
|
|
14
|
-
|
|
15
|
-
parent = Path(path).parent
|
|
16
|
-
if str(parent) not in ("", "."):
|
|
17
|
-
parent.mkdir(parents=True, exist_ok=True)
|
|
18
|
-
|
|
19
|
-
self.sqlite = sqlite3.connect(path)
|
|
20
|
-
self.sqlite.row_factory = sqlite3.Row
|
|
21
|
-
self._tables = {}
|
|
22
|
-
|
|
23
|
-
def create(self, name, schema):
|
|
24
|
-
if not name:
|
|
25
|
-
raise ValueError("Table name is required")
|
|
26
|
-
|
|
27
|
-
if not isinstance(schema, dict):
|
|
28
|
-
raise ValueError(f'Schema is required for table "{name}"')
|
|
29
|
-
|
|
30
|
-
if name in self._tables:
|
|
31
|
-
raise ValueError(f'Table "{name}" is already registered')
|
|
32
|
-
|
|
33
|
-
sql = compile_schema(name, schema)
|
|
34
|
-
self.sqlite.executescript(sql)
|
|
35
|
-
self.sqlite.commit()
|
|
36
|
-
|
|
37
|
-
table = Table(self, name, schema)
|
|
38
|
-
self._tables[name] = table
|
|
39
|
-
return table
|
|
40
|
-
|
|
41
|
-
def drop(self, name):
|
|
42
|
-
self.sqlite.execute(f"DROP TABLE IF EXISTS {name}")
|
|
43
|
-
self.sqlite.commit()
|
|
44
|
-
self._tables.pop(name, None)
|
|
45
|
-
return True
|
|
46
|
-
|
|
47
|
-
def tables(self):
|
|
48
|
-
rows = self.sqlite.execute("""
|
|
49
|
-
SELECT name
|
|
50
|
-
FROM sqlite_master
|
|
51
|
-
WHERE type = 'table'
|
|
52
|
-
AND name NOT LIKE 'sqlite_%'
|
|
53
|
-
ORDER BY name
|
|
54
|
-
""").fetchall()
|
|
55
|
-
return [row["name"] for row in rows]
|
|
56
|
-
|
|
57
|
-
def show_tables(self):
|
|
58
|
-
return self.tables()
|
|
59
|
-
|
|
60
|
-
def close(self):
|
|
61
|
-
self.sqlite.close()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|