devdb-sql 1.0.7__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: devdb-sql
3
- Version: 1.0.7
3
+ Version: 1.0.9
4
4
  Summary: Lightweight SQLite SDK with a MongoDB-like developer experience
5
5
  Author: Mahadev
6
6
  License: MIT
@@ -18,7 +18,7 @@ It provides a MongoDB-like API while using SQLite internally. Application code d
18
18
  ## Install
19
19
 
20
20
  ```bash
21
- pip install devdb-dql
21
+ pip install devdb-sql
22
22
  ```
23
23
 
24
24
  ## Usage
@@ -7,7 +7,7 @@ It provides a MongoDB-like API while using SQLite internally. Application code d
7
7
  ## Install
8
8
 
9
9
  ```bash
10
- pip install devdb-dql
10
+ pip install devdb-sql
11
11
  ```
12
12
 
13
13
  ## Usage
@@ -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 config.get("type") == "boolean" and result[field] is not None:
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
- cursor = self.db.sqlite.execute(sql, values)
80
- self.db.sqlite.commit()
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
- # Preserve the JS SDK's insert -> findById behavior.
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
- row = self.db.sqlite.execute(sql, (id,)).fetchone()
94
- return self._row_to_dict(row)
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
- rows = self.db.sqlite.execute(
98
- f"SELECT * FROM {self.name}"
99
- ).fetchall()
100
- return [self._row_to_dict(row) for row in rows]
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(self._serialize_query_value(field, value))
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(self._serialize_query_value(field, value))
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(self._serialize_query_value(field, value))
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(self._serialize_query_value(field, value))
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(self._serialize_query_value(field, value))
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(self._serialize_query_value(field, value))
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("$in requires a non-empty array")
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(f'Unsupported operator "{operator}"')
207
+ raise ValueError(
208
+ f'Unsupported operator "{operator}"'
209
+ )
210
+
159
211
  else:
160
212
  conditions.append(f"{field} = ?")
161
- values.append(self._serialize_query_value(field, condition))
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
- order = "DESC" if options.get("order") == "desc" else "ASC"
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
- if isinstance(limit, bool) or not isinstance(limit, int) or limit < 0:
177
- raise ValueError("limit must be a positive integer")
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
- if isinstance(offset, bool) or not isinstance(offset, int) or offset < 0:
183
- raise ValueError("offset must be a positive integer")
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
- rows = self.db.sqlite.execute(sql, values).fetchall()
187
- return [self._row_to_dict(row) for row in rows]
263
+ with self.db.lock:
264
+ rows = self.db.sqlite.execute(
265
+ sql,
266
+ values,
267
+ ).fetchall()
188
268
 
189
- def _serialize_query_value(self, field, value):
190
- field_type = self.schema[field].get("type")
191
- if field_type == "boolean":
192
- if not isinstance(value, bool):
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("Primary key cannot be updated")
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
- cursor = self.db.sqlite.execute(sql, values)
228
- self.db.sqlite.commit()
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
- if cursor.rowcount == 0:
231
- return None
317
+ if cursor.rowcount == 0:
318
+ return None
232
319
 
233
- return self.find_by_id(id)
320
+ return self.find_by_id(id)
234
321
 
235
322
  def delete_by_id(self, id):
236
- cursor = self.db.sqlite.execute(
237
- f"DELETE FROM {self.name} WHERE {self.primary_key} = ?",
238
- (id,),
239
- )
240
- self.db.sqlite.commit()
241
- return cursor.rowcount > 0
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.sqlite.execute(f"DROP TABLE IF EXISTS {self.name}")
245
- self.db.sqlite.commit()
246
- self.db._tables.pop(self.name, None)
247
- return True
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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: devdb-sql
3
- Version: 1.0.7
3
+ Version: 1.0.9
4
4
  Summary: Lightweight SQLite SDK with a MongoDB-like developer experience
5
5
  Author: Mahadev
6
6
  License: MIT
@@ -18,7 +18,7 @@ It provides a MongoDB-like API while using SQLite internally. Application code d
18
18
  ## Install
19
19
 
20
20
  ```bash
21
- pip install devdb-dql
21
+ pip install devdb-sql
22
22
  ```
23
23
 
24
24
  ## Usage
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "devdb-sql"
7
- version = "1.0.7"
7
+ version = "1.0.9"
8
8
  description = "Lightweight SQLite SDK with a MongoDB-like developer experience"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -1,4 +1,4 @@
1
- from dev_sql import DB
1
+ from devdb_sql import DB
2
2
 
3
3
  db = DB("./data/msg.db")
4
4
 
@@ -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