sqliter-py 0.3.0__py3-none-any.whl → 0.5.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.
- sqliter/__init__.py +5 -1
- sqliter/constants.py +18 -1
- sqliter/exceptions.py +40 -13
- sqliter/helpers.py +35 -0
- sqliter/model/__init__.py +3 -2
- sqliter/model/model.py +49 -19
- sqliter/query/__init__.py +5 -1
- sqliter/query/query.py +297 -46
- sqliter/sqliter.py +322 -43
- sqliter_py-0.5.0.dist-info/METADATA +199 -0
- sqliter_py-0.5.0.dist-info/RECORD +13 -0
- sqliter_py-0.3.0.dist-info/METADATA +0 -601
- sqliter_py-0.3.0.dist-info/RECORD +0 -12
- {sqliter_py-0.3.0.dist-info → sqliter_py-0.5.0.dist-info}/WHEEL +0 -0
- {sqliter_py-0.3.0.dist-info → sqliter_py-0.5.0.dist-info}/licenses/LICENSE.txt +0 -0
sqliter/sqliter.py
CHANGED
|
@@ -1,9 +1,16 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""Core module for SQLiter, providing the main database interaction class.
|
|
2
|
+
|
|
3
|
+
This module defines the SqliterDB class, which serves as the primary
|
|
4
|
+
interface for all database operations in SQLiter. It handles connection
|
|
5
|
+
management, table creation, and CRUD operations, bridging the gap between
|
|
6
|
+
Pydantic models and SQLite database interactions.
|
|
7
|
+
"""
|
|
2
8
|
|
|
3
9
|
from __future__ import annotations
|
|
4
10
|
|
|
11
|
+
import logging
|
|
5
12
|
import sqlite3
|
|
6
|
-
from typing import TYPE_CHECKING, Optional
|
|
13
|
+
from typing import TYPE_CHECKING, Any, Optional, TypeVar
|
|
7
14
|
|
|
8
15
|
from typing_extensions import Self
|
|
9
16
|
|
|
@@ -14,8 +21,11 @@ from sqliter.exceptions import (
|
|
|
14
21
|
RecordInsertionError,
|
|
15
22
|
RecordNotFoundError,
|
|
16
23
|
RecordUpdateError,
|
|
24
|
+
SqlExecutionError,
|
|
17
25
|
TableCreationError,
|
|
26
|
+
TableDeletionError,
|
|
18
27
|
)
|
|
28
|
+
from sqliter.helpers import infer_sqlite_type
|
|
19
29
|
from sqliter.query.query import QueryBuilder
|
|
20
30
|
|
|
21
31
|
if TYPE_CHECKING: # pragma: no cover
|
|
@@ -23,9 +33,21 @@ if TYPE_CHECKING: # pragma: no cover
|
|
|
23
33
|
|
|
24
34
|
from sqliter.model.model import BaseDBModel
|
|
25
35
|
|
|
36
|
+
T = TypeVar("T", bound="BaseDBModel")
|
|
37
|
+
|
|
26
38
|
|
|
27
39
|
class SqliterDB:
|
|
28
|
-
"""
|
|
40
|
+
"""Main class for interacting with SQLite databases.
|
|
41
|
+
|
|
42
|
+
This class provides methods for connecting to a SQLite database,
|
|
43
|
+
creating tables, and performing CRUD operations.
|
|
44
|
+
|
|
45
|
+
Arguements:
|
|
46
|
+
db_filename (str): The filename of the SQLite database.
|
|
47
|
+
auto_commit (bool): Whether to automatically commit transactions.
|
|
48
|
+
debug (bool): Whether to enable debug logging.
|
|
49
|
+
logger (Optional[logging.Logger]): Custom logger for debug output.
|
|
50
|
+
"""
|
|
29
51
|
|
|
30
52
|
def __init__(
|
|
31
53
|
self,
|
|
@@ -33,8 +55,24 @@ class SqliterDB:
|
|
|
33
55
|
*,
|
|
34
56
|
memory: bool = False,
|
|
35
57
|
auto_commit: bool = True,
|
|
58
|
+
debug: bool = False,
|
|
59
|
+
logger: Optional[logging.Logger] = None,
|
|
60
|
+
reset: bool = False,
|
|
36
61
|
) -> None:
|
|
37
|
-
"""Initialize
|
|
62
|
+
"""Initialize a new SqliterDB instance.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
db_filename: The filename of the SQLite database.
|
|
66
|
+
memory: If True, create an in-memory database.
|
|
67
|
+
auto_commit: Whether to automatically commit transactions.
|
|
68
|
+
debug: Whether to enable debug logging.
|
|
69
|
+
logger: Custom logger for debug output.
|
|
70
|
+
reset: Whether to reset the database on initialization. This will
|
|
71
|
+
basically drop all existing tables.
|
|
72
|
+
|
|
73
|
+
Raises:
|
|
74
|
+
ValueError: If no filename is provided for a non-memory database.
|
|
75
|
+
"""
|
|
38
76
|
if memory:
|
|
39
77
|
self.db_filename = ":memory:"
|
|
40
78
|
elif db_filename:
|
|
@@ -46,10 +84,98 @@ class SqliterDB:
|
|
|
46
84
|
)
|
|
47
85
|
raise ValueError(err)
|
|
48
86
|
self.auto_commit = auto_commit
|
|
87
|
+
self.debug = debug
|
|
88
|
+
self.logger = logger
|
|
49
89
|
self.conn: Optional[sqlite3.Connection] = None
|
|
90
|
+
self.reset = reset
|
|
91
|
+
|
|
92
|
+
if self.debug:
|
|
93
|
+
self._setup_logger()
|
|
94
|
+
|
|
95
|
+
if self.reset:
|
|
96
|
+
self._reset_database()
|
|
97
|
+
|
|
98
|
+
def _reset_database(self) -> None:
|
|
99
|
+
"""Drop all user-created tables in the database."""
|
|
100
|
+
with self.connect() as conn:
|
|
101
|
+
cursor = conn.cursor()
|
|
102
|
+
|
|
103
|
+
# Get all table names, excluding SQLite system tables
|
|
104
|
+
cursor.execute(
|
|
105
|
+
"SELECT name FROM sqlite_master WHERE type='table' "
|
|
106
|
+
"AND name NOT LIKE 'sqlite_%';"
|
|
107
|
+
)
|
|
108
|
+
tables = cursor.fetchall()
|
|
109
|
+
|
|
110
|
+
# Drop each user-created table
|
|
111
|
+
for table in tables:
|
|
112
|
+
cursor.execute(f"DROP TABLE IF EXISTS {table[0]}")
|
|
113
|
+
|
|
114
|
+
conn.commit()
|
|
115
|
+
|
|
116
|
+
if self.debug and self.logger:
|
|
117
|
+
self.logger.debug(
|
|
118
|
+
"Database reset: %s user-created tables dropped.", len(tables)
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
def _setup_logger(self) -> None:
|
|
122
|
+
"""Set up the logger for debug output.
|
|
123
|
+
|
|
124
|
+
This method configures a logger for the SqliterDB instance, either
|
|
125
|
+
using an existing logger or creating a new one specifically for
|
|
126
|
+
SQLiter.
|
|
127
|
+
"""
|
|
128
|
+
# Check if the root logger is already configured
|
|
129
|
+
root_logger = logging.getLogger()
|
|
130
|
+
|
|
131
|
+
if root_logger.hasHandlers():
|
|
132
|
+
# If the root logger has handlers, use it without modifying the root
|
|
133
|
+
# configuration
|
|
134
|
+
self.logger = root_logger.getChild("sqliter")
|
|
135
|
+
else:
|
|
136
|
+
# If no root logger is configured, set up a new logger specific to
|
|
137
|
+
# SqliterDB
|
|
138
|
+
self.logger = logging.getLogger("sqliter")
|
|
139
|
+
|
|
140
|
+
handler = logging.StreamHandler() # Output to console
|
|
141
|
+
formatter = logging.Formatter(
|
|
142
|
+
"%(levelname)-8s%(message)s"
|
|
143
|
+
) # Custom format
|
|
144
|
+
handler.setFormatter(formatter)
|
|
145
|
+
self.logger.addHandler(handler)
|
|
146
|
+
|
|
147
|
+
self.logger.setLevel(logging.DEBUG)
|
|
148
|
+
self.logger.propagate = False
|
|
149
|
+
|
|
150
|
+
def _log_sql(self, sql: str, values: list[Any]) -> None:
|
|
151
|
+
"""Log the SQL query and its values if debug mode is enabled.
|
|
152
|
+
|
|
153
|
+
The values are inserted into the SQL query string to replace the
|
|
154
|
+
placeholders.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
sql: The SQL query string.
|
|
158
|
+
values: The list of values to be inserted into the query.
|
|
159
|
+
"""
|
|
160
|
+
if self.debug and self.logger:
|
|
161
|
+
formatted_sql = sql
|
|
162
|
+
for value in values:
|
|
163
|
+
if isinstance(value, str):
|
|
164
|
+
formatted_sql = formatted_sql.replace("?", f"'{value}'", 1)
|
|
165
|
+
else:
|
|
166
|
+
formatted_sql = formatted_sql.replace("?", str(value), 1)
|
|
167
|
+
|
|
168
|
+
self.logger.debug("Executing SQL: %s", formatted_sql)
|
|
50
169
|
|
|
51
170
|
def connect(self) -> sqlite3.Connection:
|
|
52
|
-
"""
|
|
171
|
+
"""Establish a connection to the SQLite database.
|
|
172
|
+
|
|
173
|
+
Returns:
|
|
174
|
+
The SQLite connection object.
|
|
175
|
+
|
|
176
|
+
Raises:
|
|
177
|
+
DatabaseConnectionError: If unable to connect to the database.
|
|
178
|
+
"""
|
|
53
179
|
if not self.conn:
|
|
54
180
|
try:
|
|
55
181
|
self.conn = sqlite3.connect(self.db_filename)
|
|
@@ -58,41 +184,72 @@ class SqliterDB:
|
|
|
58
184
|
return self.conn
|
|
59
185
|
|
|
60
186
|
def close(self) -> None:
|
|
61
|
-
"""Close the connection
|
|
187
|
+
"""Close the database connection.
|
|
188
|
+
|
|
189
|
+
This method commits any pending changes if auto_commit is True,
|
|
190
|
+
then closes the connection. If the connection is already closed or does
|
|
191
|
+
not exist, this method silently does nothing.
|
|
192
|
+
"""
|
|
62
193
|
if self.conn:
|
|
63
194
|
self._maybe_commit()
|
|
64
195
|
self.conn.close()
|
|
65
196
|
self.conn = None
|
|
66
197
|
|
|
67
198
|
def commit(self) -> None:
|
|
68
|
-
"""Commit
|
|
199
|
+
"""Commit the current transaction.
|
|
200
|
+
|
|
201
|
+
This method explicitly commits any pending changes to the database.
|
|
202
|
+
"""
|
|
69
203
|
if self.conn:
|
|
70
204
|
self.conn.commit()
|
|
71
205
|
|
|
72
|
-
def create_table(
|
|
73
|
-
|
|
206
|
+
def create_table(
|
|
207
|
+
self,
|
|
208
|
+
model_class: type[BaseDBModel],
|
|
209
|
+
*,
|
|
210
|
+
exists_ok: bool = True,
|
|
211
|
+
force: bool = False,
|
|
212
|
+
) -> None:
|
|
213
|
+
"""Create a table in the database based on the given model class.
|
|
214
|
+
|
|
215
|
+
Args:
|
|
216
|
+
model_class: The Pydantic model class representing the table.
|
|
217
|
+
exists_ok: If True, do not raise an error if the table already
|
|
218
|
+
exists. Default is True which is the original behavior.
|
|
219
|
+
force: If True, drop the table if it exists before creating.
|
|
220
|
+
Defaults to False.
|
|
221
|
+
|
|
222
|
+
Raises:
|
|
223
|
+
TableCreationError: If there's an error creating the table.
|
|
224
|
+
ValueError: If the primary key field is not found in the model.
|
|
225
|
+
"""
|
|
74
226
|
table_name = model_class.get_table_name()
|
|
75
227
|
primary_key = model_class.get_primary_key()
|
|
76
|
-
create_pk = model_class.should_create_pk()
|
|
77
228
|
|
|
78
|
-
|
|
79
|
-
f"
|
|
229
|
+
if force:
|
|
230
|
+
drop_table_sql = f"DROP TABLE IF EXISTS {table_name}"
|
|
231
|
+
self._execute_sql(drop_table_sql)
|
|
232
|
+
|
|
233
|
+
fields = [f'"{primary_key}" INTEGER PRIMARY KEY AUTOINCREMENT']
|
|
234
|
+
|
|
235
|
+
# Add remaining fields
|
|
236
|
+
for field_name, field_info in model_class.model_fields.items():
|
|
237
|
+
if field_name != primary_key:
|
|
238
|
+
sqlite_type = infer_sqlite_type(field_info.annotation)
|
|
239
|
+
fields.append(f"{field_name} {sqlite_type}")
|
|
240
|
+
|
|
241
|
+
create_str = (
|
|
242
|
+
"CREATE TABLE IF NOT EXISTS" if exists_ok else "CREATE TABLE"
|
|
80
243
|
)
|
|
81
244
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
create_table_sql = f"""
|
|
91
|
-
CREATE TABLE IF NOT EXISTS {table_name} (
|
|
92
|
-
{fields},
|
|
93
|
-
PRIMARY KEY ({primary_key})
|
|
94
|
-
)
|
|
95
|
-
"""
|
|
245
|
+
create_table_sql = f"""
|
|
246
|
+
{create_str} {table_name} (
|
|
247
|
+
{", ".join(fields)}
|
|
248
|
+
)
|
|
249
|
+
"""
|
|
250
|
+
|
|
251
|
+
if self.debug:
|
|
252
|
+
self._log_sql(create_table_sql, [])
|
|
96
253
|
|
|
97
254
|
try:
|
|
98
255
|
with self.connect() as conn:
|
|
@@ -102,17 +259,80 @@ class SqliterDB:
|
|
|
102
259
|
except sqlite3.Error as exc:
|
|
103
260
|
raise TableCreationError(table_name) from exc
|
|
104
261
|
|
|
262
|
+
def _execute_sql(self, sql: str) -> None:
|
|
263
|
+
"""Execute an SQL statement.
|
|
264
|
+
|
|
265
|
+
Args:
|
|
266
|
+
sql: The SQL statement to execute.
|
|
267
|
+
|
|
268
|
+
Raises:
|
|
269
|
+
SqlExecutionError: If the SQL execution fails.
|
|
270
|
+
"""
|
|
271
|
+
if self.debug:
|
|
272
|
+
self._log_sql(sql, [])
|
|
273
|
+
|
|
274
|
+
try:
|
|
275
|
+
with self.connect() as conn:
|
|
276
|
+
cursor = conn.cursor()
|
|
277
|
+
cursor.execute(sql)
|
|
278
|
+
conn.commit()
|
|
279
|
+
except (sqlite3.Error, sqlite3.Warning) as exc:
|
|
280
|
+
raise SqlExecutionError(sql) from exc
|
|
281
|
+
|
|
282
|
+
def drop_table(self, model_class: type[BaseDBModel]) -> None:
|
|
283
|
+
"""Drop the table associated with the given model class.
|
|
284
|
+
|
|
285
|
+
Args:
|
|
286
|
+
model_class: The model class for which to drop the table.
|
|
287
|
+
|
|
288
|
+
Raises:
|
|
289
|
+
TableDeletionError: If there's an error dropping the table.
|
|
290
|
+
"""
|
|
291
|
+
table_name = model_class.get_table_name()
|
|
292
|
+
drop_table_sql = f"DROP TABLE IF EXISTS {table_name}"
|
|
293
|
+
|
|
294
|
+
if self.debug:
|
|
295
|
+
self._log_sql(drop_table_sql, [])
|
|
296
|
+
|
|
297
|
+
try:
|
|
298
|
+
with self.connect() as conn:
|
|
299
|
+
cursor = conn.cursor()
|
|
300
|
+
cursor.execute(drop_table_sql)
|
|
301
|
+
self.commit()
|
|
302
|
+
except sqlite3.Error as exc:
|
|
303
|
+
raise TableDeletionError(table_name) from exc
|
|
304
|
+
|
|
105
305
|
def _maybe_commit(self) -> None:
|
|
106
|
-
"""Commit changes if auto_commit is
|
|
306
|
+
"""Commit changes if auto_commit is enabled.
|
|
307
|
+
|
|
308
|
+
This method is called after operations that modify the database,
|
|
309
|
+
committing changes only if auto_commit is set to True.
|
|
310
|
+
"""
|
|
107
311
|
if self.auto_commit and self.conn:
|
|
108
312
|
self.conn.commit()
|
|
109
313
|
|
|
110
|
-
def insert(self, model_instance:
|
|
111
|
-
"""Insert a new record into the
|
|
314
|
+
def insert(self, model_instance: T) -> T:
|
|
315
|
+
"""Insert a new record into the database.
|
|
316
|
+
|
|
317
|
+
Args:
|
|
318
|
+
model_instance: The instance of the model class to insert.
|
|
319
|
+
|
|
320
|
+
Returns:
|
|
321
|
+
The updated model instance with the primary key (pk) set.
|
|
322
|
+
|
|
323
|
+
Raises:
|
|
324
|
+
RecordInsertionError: If an error occurs during the insertion.
|
|
325
|
+
"""
|
|
112
326
|
model_class = type(model_instance)
|
|
113
327
|
table_name = model_class.get_table_name()
|
|
114
328
|
|
|
329
|
+
# Get the data from the model
|
|
115
330
|
data = model_instance.model_dump()
|
|
331
|
+
# remove the primary key field if it exists, otherwise we'll get
|
|
332
|
+
# TypeErrors as multiple primary keys will exist
|
|
333
|
+
if data.get("pk", None) == 0:
|
|
334
|
+
data.pop("pk")
|
|
335
|
+
|
|
116
336
|
fields = ", ".join(data.keys())
|
|
117
337
|
placeholders = ", ".join(
|
|
118
338
|
["?" if value is not None else "NULL" for value in data.values()]
|
|
@@ -129,13 +349,28 @@ class SqliterDB:
|
|
|
129
349
|
cursor = conn.cursor()
|
|
130
350
|
cursor.execute(insert_sql, values)
|
|
131
351
|
self._maybe_commit()
|
|
352
|
+
|
|
132
353
|
except sqlite3.Error as exc:
|
|
133
354
|
raise RecordInsertionError(table_name) from exc
|
|
355
|
+
else:
|
|
356
|
+
data.pop("pk", None)
|
|
357
|
+
return model_class(pk=cursor.lastrowid, **data)
|
|
134
358
|
|
|
135
359
|
def get(
|
|
136
|
-
self, model_class: type[BaseDBModel], primary_key_value:
|
|
360
|
+
self, model_class: type[BaseDBModel], primary_key_value: int
|
|
137
361
|
) -> BaseDBModel | None:
|
|
138
|
-
"""Retrieve a record
|
|
362
|
+
"""Retrieve a single record from the database by its primary key.
|
|
363
|
+
|
|
364
|
+
Args:
|
|
365
|
+
model_class: The Pydantic model class representing the table.
|
|
366
|
+
primary_key_value: The value of the primary key to look up.
|
|
367
|
+
|
|
368
|
+
Returns:
|
|
369
|
+
An instance of the model class if found, None otherwise.
|
|
370
|
+
|
|
371
|
+
Raises:
|
|
372
|
+
RecordFetchError: If there's an error fetching the record.
|
|
373
|
+
"""
|
|
139
374
|
table_name = model_class.get_table_name()
|
|
140
375
|
primary_key = model_class.get_primary_key()
|
|
141
376
|
|
|
@@ -163,9 +398,18 @@ class SqliterDB:
|
|
|
163
398
|
return None
|
|
164
399
|
|
|
165
400
|
def update(self, model_instance: BaseDBModel) -> None:
|
|
166
|
-
"""Update an existing record
|
|
401
|
+
"""Update an existing record in the database.
|
|
402
|
+
|
|
403
|
+
Args:
|
|
404
|
+
model_instance: An instance of a Pydantic model to be updated.
|
|
405
|
+
|
|
406
|
+
Raises:
|
|
407
|
+
RecordUpdateError: If there's an error updating the record or if it
|
|
408
|
+
is not found.
|
|
409
|
+
"""
|
|
167
410
|
model_class = type(model_instance)
|
|
168
411
|
table_name = model_class.get_table_name()
|
|
412
|
+
|
|
169
413
|
primary_key = model_class.get_primary_key()
|
|
170
414
|
|
|
171
415
|
fields = ", ".join(
|
|
@@ -203,7 +447,17 @@ class SqliterDB:
|
|
|
203
447
|
def delete(
|
|
204
448
|
self, model_class: type[BaseDBModel], primary_key_value: str
|
|
205
449
|
) -> None:
|
|
206
|
-
"""Delete a record by its primary key.
|
|
450
|
+
"""Delete a record from the database by its primary key.
|
|
451
|
+
|
|
452
|
+
Args:
|
|
453
|
+
model_class: The Pydantic model class representing the table.
|
|
454
|
+
primary_key_value: The value of the primary key of the record to
|
|
455
|
+
delete.
|
|
456
|
+
|
|
457
|
+
Raises:
|
|
458
|
+
RecordDeletionError: If there's an error deleting the record.
|
|
459
|
+
RecordNotFoundError: If the record to delete is not found.
|
|
460
|
+
"""
|
|
207
461
|
table_name = model_class.get_table_name()
|
|
208
462
|
primary_key = model_class.get_primary_key()
|
|
209
463
|
|
|
@@ -228,18 +482,15 @@ class SqliterDB:
|
|
|
228
482
|
fields: Optional[list[str]] = None,
|
|
229
483
|
exclude: Optional[list[str]] = None,
|
|
230
484
|
) -> QueryBuilder:
|
|
231
|
-
"""
|
|
485
|
+
"""Create a QueryBuilder instance for selecting records.
|
|
232
486
|
|
|
233
487
|
Args:
|
|
234
|
-
model_class: The model class
|
|
235
|
-
fields: Optional list of
|
|
236
|
-
|
|
237
|
-
exclude: Optional list of field names to exclude from the query
|
|
238
|
-
output.
|
|
488
|
+
model_class: The Pydantic model class representing the table.
|
|
489
|
+
fields: Optional list of fields to include in the query.
|
|
490
|
+
exclude: Optional list of fields to exclude from the query.
|
|
239
491
|
|
|
240
492
|
Returns:
|
|
241
|
-
QueryBuilder
|
|
242
|
-
fields.
|
|
493
|
+
A QueryBuilder instance for further query construction.
|
|
243
494
|
"""
|
|
244
495
|
query_builder = QueryBuilder(self, model_class, fields)
|
|
245
496
|
|
|
@@ -251,7 +502,18 @@ class SqliterDB:
|
|
|
251
502
|
|
|
252
503
|
# --- Context manager methods ---
|
|
253
504
|
def __enter__(self) -> Self:
|
|
254
|
-
"""Enter the runtime context for the
|
|
505
|
+
"""Enter the runtime context for the SqliterDB instance.
|
|
506
|
+
|
|
507
|
+
This method is called when entering a 'with' statement. It ensures
|
|
508
|
+
that a database connection is established.
|
|
509
|
+
|
|
510
|
+
Note that this method should never be called explicitly, but will be
|
|
511
|
+
called by the 'with' statement when entering the context.
|
|
512
|
+
|
|
513
|
+
Returns:
|
|
514
|
+
The SqliterDB instance.
|
|
515
|
+
|
|
516
|
+
"""
|
|
255
517
|
self.connect()
|
|
256
518
|
return self
|
|
257
519
|
|
|
@@ -261,7 +523,24 @@ class SqliterDB:
|
|
|
261
523
|
exc_value: Optional[BaseException],
|
|
262
524
|
traceback: Optional[TracebackType],
|
|
263
525
|
) -> None:
|
|
264
|
-
"""Exit the runtime context
|
|
526
|
+
"""Exit the runtime context for the SqliterDB instance.
|
|
527
|
+
|
|
528
|
+
This method is called when exiting a 'with' statement. It handles
|
|
529
|
+
committing or rolling back transactions based on whether an exception
|
|
530
|
+
occurred, and closes the database connection.
|
|
531
|
+
|
|
532
|
+
Args:
|
|
533
|
+
exc_type: The type of the exception that caused the context to be
|
|
534
|
+
exited, or None if no exception was raised.
|
|
535
|
+
exc_value: The instance of the exception that caused the context
|
|
536
|
+
to be exited, or None if no exception was raised.
|
|
537
|
+
traceback: A traceback object encoding the stack trace, or None
|
|
538
|
+
if no exception was raised.
|
|
539
|
+
|
|
540
|
+
Note that this method should never be called explicitly, but will be
|
|
541
|
+
called by the 'with' statement when exiting the context.
|
|
542
|
+
|
|
543
|
+
"""
|
|
265
544
|
if self.conn:
|
|
266
545
|
try:
|
|
267
546
|
if exc_type:
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: sqliter-py
|
|
3
|
+
Version: 0.5.0
|
|
4
|
+
Summary: Interact with SQLite databases using Python and Pydantic
|
|
5
|
+
Project-URL: Pull Requests, https://github.com/seapagan/sqliter-py/pulls
|
|
6
|
+
Project-URL: Bug Tracker, https://github.com/seapagan/sqliter-py/issues
|
|
7
|
+
Project-URL: Changelog, https://github.com/seapagan/sqliter-py/blob/main/CHANGELOG.md
|
|
8
|
+
Project-URL: Repository, https://github.com/seapagan/sqliter-py
|
|
9
|
+
Author-email: Grant Ramsay <grant@gnramsay.com>
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE.txt
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Topic :: Software Development
|
|
22
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
23
|
+
Requires-Python: >=3.9
|
|
24
|
+
Requires-Dist: pydantic>=2.9.0
|
|
25
|
+
Provides-Extra: extras
|
|
26
|
+
Requires-Dist: inflect==7.0.0; extra == 'extras'
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
|
|
29
|
+
# SQLiter <!-- omit in toc -->
|
|
30
|
+
|
|
31
|
+
[](https://badge.fury.io/py/sqliter-py)
|
|
32
|
+
[](https://github.com/seapagan/sqliter-py/actions/workflows/testing.yml)
|
|
33
|
+
[](https://github.com/seapagan/sqliter-py/actions/workflows/linting.yml)
|
|
34
|
+
[](https://github.com/seapagan/sqliter-py/actions/workflows/mypy.yml)
|
|
35
|
+

|
|
36
|
+
|
|
37
|
+
SQLiter is a lightweight Object-Relational Mapping (ORM) library for SQLite
|
|
38
|
+
databases in Python. It provides a simplified interface for interacting with
|
|
39
|
+
SQLite databases using Pydantic models. The only external run-time dependency
|
|
40
|
+
is Pydantic itself.
|
|
41
|
+
|
|
42
|
+
It does not aim to be a full-fledged ORM like SQLAlchemy, but rather a simple
|
|
43
|
+
and easy-to-use library for basic database operations, especially for small
|
|
44
|
+
projects. It is NOT asynchronous and does not support complex queries (at this
|
|
45
|
+
time).
|
|
46
|
+
|
|
47
|
+
The ideal use case is more for Python CLI tools that need to store data in a
|
|
48
|
+
database-like format without needing to learn SQL or use a full ORM.
|
|
49
|
+
|
|
50
|
+
Full documentation is available on the [Documentation
|
|
51
|
+
Website](https://sqliter.grantramsay.dev)
|
|
52
|
+
|
|
53
|
+
> [!CAUTION]
|
|
54
|
+
> This project is still in the early stages of development and is lacking some
|
|
55
|
+
> planned functionality. Please use with caution - Classes and methods may
|
|
56
|
+
> change until a stable release is made. I'll try to keep this to an absolute
|
|
57
|
+
> minimum and the releases and documentation will be very clear about any
|
|
58
|
+
> breaking changes.
|
|
59
|
+
>
|
|
60
|
+
> Also, structures like `list`, `dict`, `set` etc are not supported **at this
|
|
61
|
+
> time** as field types, since SQLite does not have a native column type for
|
|
62
|
+
> these. This is the **next planned enhancement**. These will need to be
|
|
63
|
+
> `pickled` first then stored as a BLOB in the database . Also support `date`
|
|
64
|
+
> which can be stored as a Unix timestamp in an integer field.
|
|
65
|
+
>
|
|
66
|
+
> See the [TODO](TODO.md) for planned features and improvements.
|
|
67
|
+
|
|
68
|
+
- [Features](#features)
|
|
69
|
+
- [Installation](#installation)
|
|
70
|
+
- [Optional Dependencies](#optional-dependencies)
|
|
71
|
+
- [Quick Start](#quick-start)
|
|
72
|
+
- [Contributing](#contributing)
|
|
73
|
+
- [License](#license)
|
|
74
|
+
|
|
75
|
+
## Features
|
|
76
|
+
|
|
77
|
+
- Table creation based on Pydantic models
|
|
78
|
+
- CRUD operations (Create, Read, Update, Delete)
|
|
79
|
+
- Chained Query building with filtering, ordering, and pagination
|
|
80
|
+
- Transaction support
|
|
81
|
+
- Custom exceptions for better error handling
|
|
82
|
+
- Full type hinting and type checking
|
|
83
|
+
- Detailed documentation and examples
|
|
84
|
+
- No external dependencies other than Pydantic
|
|
85
|
+
- Full test coverage
|
|
86
|
+
- Can optionally output the raw SQL queries being executed for debugging
|
|
87
|
+
purposes.
|
|
88
|
+
|
|
89
|
+
## Installation
|
|
90
|
+
|
|
91
|
+
You can install SQLiter using whichever method you prefer or is compatible with
|
|
92
|
+
your project setup.
|
|
93
|
+
|
|
94
|
+
With `uv` which is rapidly becoming my favorite tool for managing projects and
|
|
95
|
+
virtual environments (`uv` is used for developing this project and in the CI):
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
uv add sqliter-py
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
With `pip`:
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
pip install sqliter-py
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Or with `Poetry`:
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
poetry add sqliter-py
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### Optional Dependencies
|
|
114
|
+
|
|
115
|
+
Currently by default, the only external dependency is Pydantic. However, there
|
|
116
|
+
are some optional dependencies that can be installed to enable additional
|
|
117
|
+
features:
|
|
118
|
+
|
|
119
|
+
- `inflect`: For pluralizing table names (if not specified). This just offers a
|
|
120
|
+
more-advanced pluralization than the default method used. In most cases you
|
|
121
|
+
will not need this.
|
|
122
|
+
|
|
123
|
+
See [Installing Optional
|
|
124
|
+
Dependencies](https://sqliter.grantramsay.dev/installation#optional-dependencies)
|
|
125
|
+
for more information.
|
|
126
|
+
|
|
127
|
+
## Quick Start
|
|
128
|
+
|
|
129
|
+
Here's a quick example of how to use SQLiter:
|
|
130
|
+
|
|
131
|
+
```python
|
|
132
|
+
from sqliter import SqliterDB
|
|
133
|
+
from sqliter.model import BaseDBModel
|
|
134
|
+
|
|
135
|
+
# Define your model
|
|
136
|
+
class User(BaseDBModel):
|
|
137
|
+
name: str
|
|
138
|
+
age: int
|
|
139
|
+
|
|
140
|
+
# Create a database connection
|
|
141
|
+
db = SqliterDB("example.db")
|
|
142
|
+
|
|
143
|
+
# Create the table
|
|
144
|
+
db.create_table(User)
|
|
145
|
+
|
|
146
|
+
# Insert a record
|
|
147
|
+
user = User(name="John Doe", age=30)
|
|
148
|
+
new_user = db.insert(user)
|
|
149
|
+
|
|
150
|
+
# Query records
|
|
151
|
+
results = db.select(User).filter(name="John Doe").fetch_all()
|
|
152
|
+
for user in results:
|
|
153
|
+
print(f"User: {user.name}, Age: {user.age}")
|
|
154
|
+
|
|
155
|
+
# Update a record
|
|
156
|
+
new_user.age = 31
|
|
157
|
+
db.update(new_user)
|
|
158
|
+
|
|
159
|
+
# Delete a record
|
|
160
|
+
db.delete(User, new_user.pk)
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
See the [Usage](https://sqliter.grantramsay.dev/usage) section of the documentation
|
|
164
|
+
for more detailed information on how to use SQLiter, and advanced features.
|
|
165
|
+
|
|
166
|
+
## Contributing
|
|
167
|
+
|
|
168
|
+
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
169
|
+
|
|
170
|
+
See the [CONTRIBUTING](CONTRIBUTING.md) guide for more information.
|
|
171
|
+
|
|
172
|
+
Please note that this project is released with a Contributor Code of Conduct,
|
|
173
|
+
which you can read in the [CODE_OF_CONDUCT](CODE_OF_CONDUCT.md) file.
|
|
174
|
+
|
|
175
|
+
## License
|
|
176
|
+
|
|
177
|
+
This project is licensed under the MIT License.
|
|
178
|
+
|
|
179
|
+
```pre
|
|
180
|
+
Copyright (c) 2024 Grant Ramsay
|
|
181
|
+
|
|
182
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
183
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
184
|
+
in the Software without restriction, including without limitation the rights
|
|
185
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
186
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
187
|
+
furnished to do so, subject to the following conditions:
|
|
188
|
+
|
|
189
|
+
The above copyright notice and this permission notice shall be included in all
|
|
190
|
+
copies or substantial portions of the Software.
|
|
191
|
+
|
|
192
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
193
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
194
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
|
195
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
|
196
|
+
DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
|
197
|
+
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
|
|
198
|
+
OR OTHER DEALINGS IN THE SOFTWARE.
|
|
199
|
+
```
|