sqliter-py 0.4.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/exceptions.py +1 -1
- sqliter/model/model.py +9 -19
- sqliter/query/query.py +19 -10
- sqliter/sqliter.py +24 -24
- {sqliter_py-0.4.0.dist-info → sqliter_py-0.5.0.dist-info}/METADATA +14 -11
- sqliter_py-0.5.0.dist-info/RECORD +13 -0
- sqliter_py-0.4.0.dist-info/RECORD +0 -13
- {sqliter_py-0.4.0.dist-info → sqliter_py-0.5.0.dist-info}/WHEEL +0 -0
- {sqliter_py-0.4.0.dist-info → sqliter_py-0.5.0.dist-info}/licenses/LICENSE.txt +0 -0
sqliter/exceptions.py
CHANGED
|
@@ -114,7 +114,7 @@ class RecordUpdateError(SqliterError):
|
|
|
114
114
|
class RecordNotFoundError(SqliterError):
|
|
115
115
|
"""Exception raised when a requested record is not found in the database."""
|
|
116
116
|
|
|
117
|
-
message_template = "Failed to find
|
|
117
|
+
message_template = "Failed to find that record in the table (key '{}') "
|
|
118
118
|
|
|
119
119
|
|
|
120
120
|
class RecordFetchError(SqliterError):
|
sqliter/model/model.py
CHANGED
|
@@ -10,9 +10,9 @@ in SQLiter applications.
|
|
|
10
10
|
from __future__ import annotations
|
|
11
11
|
|
|
12
12
|
import re
|
|
13
|
-
from typing import Any, Optional, TypeVar, Union, get_args, get_origin
|
|
13
|
+
from typing import Any, Optional, TypeVar, Union, cast, get_args, get_origin
|
|
14
14
|
|
|
15
|
-
from pydantic import BaseModel, ConfigDict
|
|
15
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
16
16
|
|
|
17
17
|
T = TypeVar("T", bound="BaseDBModel")
|
|
18
18
|
|
|
@@ -28,6 +28,8 @@ class BaseDBModel(BaseModel):
|
|
|
28
28
|
representing database models.
|
|
29
29
|
"""
|
|
30
30
|
|
|
31
|
+
pk: int = Field(0, description="The mandatory primary key of the table.")
|
|
32
|
+
|
|
31
33
|
model_config = ConfigDict(
|
|
32
34
|
extra="ignore",
|
|
33
35
|
populate_by_name=True,
|
|
@@ -44,10 +46,6 @@ class BaseDBModel(BaseModel):
|
|
|
44
46
|
table_name (Optional[str]): The name of the database table.
|
|
45
47
|
"""
|
|
46
48
|
|
|
47
|
-
create_pk: bool = (
|
|
48
|
-
True # Whether to create an auto-increment primary key
|
|
49
|
-
)
|
|
50
|
-
primary_key: str = "id" # Default primary key name
|
|
51
49
|
table_name: Optional[str] = (
|
|
52
50
|
None # Table name, defaults to class name if not set
|
|
53
51
|
)
|
|
@@ -89,7 +87,7 @@ class BaseDBModel(BaseModel):
|
|
|
89
87
|
else:
|
|
90
88
|
converted_obj[field_name] = field_type(value)
|
|
91
89
|
|
|
92
|
-
return cls.model_construct(**converted_obj)
|
|
90
|
+
return cast(T, cls.model_construct(**converted_obj))
|
|
93
91
|
|
|
94
92
|
@classmethod
|
|
95
93
|
def get_table_name(cls) -> str:
|
|
@@ -127,18 +125,10 @@ class BaseDBModel(BaseModel):
|
|
|
127
125
|
|
|
128
126
|
@classmethod
|
|
129
127
|
def get_primary_key(cls) -> str:
|
|
130
|
-
"""
|
|
131
|
-
|
|
132
|
-
Returns:
|
|
133
|
-
The name of the primary key field.
|
|
134
|
-
"""
|
|
135
|
-
return getattr(cls.Meta, "primary_key", "id")
|
|
128
|
+
"""Returns the mandatory primary key, always 'pk'."""
|
|
129
|
+
return "pk"
|
|
136
130
|
|
|
137
131
|
@classmethod
|
|
138
132
|
def should_create_pk(cls) -> bool:
|
|
139
|
-
"""
|
|
140
|
-
|
|
141
|
-
Returns:
|
|
142
|
-
True if a primary key should be created, False otherwise.
|
|
143
|
-
"""
|
|
144
|
-
return getattr(cls.Meta, "create_pk", True)
|
|
133
|
+
"""Returns True since the primary key is always created."""
|
|
134
|
+
return True
|
sqliter/query/query.py
CHANGED
|
@@ -129,8 +129,11 @@ class QueryBuilder:
|
|
|
129
129
|
field_name, operator = self._parse_field_operator(field)
|
|
130
130
|
self._validate_field(field_name, valid_fields)
|
|
131
131
|
|
|
132
|
-
|
|
133
|
-
|
|
132
|
+
if operator in ["__isnull", "__notnull"]:
|
|
133
|
+
self._handle_null(field_name, value, operator)
|
|
134
|
+
else:
|
|
135
|
+
handler = self._get_operator_handler(operator)
|
|
136
|
+
handler(field_name, value, operator)
|
|
134
137
|
|
|
135
138
|
return self
|
|
136
139
|
|
|
@@ -145,6 +148,8 @@ class QueryBuilder:
|
|
|
145
148
|
The QueryBuilder instance for method chaining.
|
|
146
149
|
"""
|
|
147
150
|
if fields:
|
|
151
|
+
if "pk" not in fields:
|
|
152
|
+
fields.append("pk")
|
|
148
153
|
self._fields = fields
|
|
149
154
|
self._validate_fields()
|
|
150
155
|
return self
|
|
@@ -164,6 +169,9 @@ class QueryBuilder:
|
|
|
164
169
|
invalid fields are specified.
|
|
165
170
|
"""
|
|
166
171
|
if fields:
|
|
172
|
+
if "pk" in fields:
|
|
173
|
+
err = "The primary key 'pk' cannot be excluded."
|
|
174
|
+
raise ValueError(err)
|
|
167
175
|
all_fields = set(self.model_class.model_fields.keys())
|
|
168
176
|
|
|
169
177
|
# Check for invalid fields before subtraction
|
|
@@ -179,7 +187,7 @@ class QueryBuilder:
|
|
|
179
187
|
self._fields = list(all_fields - set(fields))
|
|
180
188
|
|
|
181
189
|
# Explicit check: raise an error if no fields remain
|
|
182
|
-
if
|
|
190
|
+
if self._fields == ["pk"]:
|
|
183
191
|
err = "Exclusion results in no fields being selected."
|
|
184
192
|
raise ValueError(err)
|
|
185
193
|
|
|
@@ -208,7 +216,7 @@ class QueryBuilder:
|
|
|
208
216
|
raise ValueError(err)
|
|
209
217
|
|
|
210
218
|
# Set self._fields to just the single field
|
|
211
|
-
self._fields = [field]
|
|
219
|
+
self._fields = [field, "pk"]
|
|
212
220
|
return self
|
|
213
221
|
|
|
214
222
|
def _get_operator_handler(
|
|
@@ -275,7 +283,7 @@ class QueryBuilder:
|
|
|
275
283
|
self.filters.append((field_name, value, operator))
|
|
276
284
|
|
|
277
285
|
def _handle_null(
|
|
278
|
-
self, field_name: str,
|
|
286
|
+
self, field_name: str, value: Union[str, float, None], operator: str
|
|
279
287
|
) -> None:
|
|
280
288
|
"""Handle IS NULL and IS NOT NULL filter conditions.
|
|
281
289
|
|
|
@@ -283,15 +291,14 @@ class QueryBuilder:
|
|
|
283
291
|
field_name: The name of the field to filter on. _: Placeholder for
|
|
284
292
|
unused value parameter.
|
|
285
293
|
operator: The operator string ('__isnull' or '__notnull').
|
|
294
|
+
value: The value to check for.
|
|
286
295
|
|
|
287
296
|
This method adds an IS NULL or IS NOT NULL condition to the filters
|
|
288
297
|
list.
|
|
289
298
|
"""
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
else f"{field_name} IS NULL"
|
|
294
|
-
)
|
|
299
|
+
is_null = operator == "__isnull"
|
|
300
|
+
check_null = bool(value) if is_null else not bool(value)
|
|
301
|
+
condition = f"{field_name} IS {'NOT ' if not check_null else ''}NULL"
|
|
295
302
|
self.filters.append((condition, None, operator))
|
|
296
303
|
|
|
297
304
|
def _handle_in(
|
|
@@ -527,6 +534,8 @@ class QueryBuilder:
|
|
|
527
534
|
if count_only:
|
|
528
535
|
fields = "COUNT(*)"
|
|
529
536
|
elif self._fields:
|
|
537
|
+
if "pk" not in self._fields:
|
|
538
|
+
self._fields.append("pk")
|
|
530
539
|
fields = ", ".join(f'"{field}"' for field in self._fields)
|
|
531
540
|
else:
|
|
532
541
|
fields = ", ".join(
|
sqliter/sqliter.py
CHANGED
|
@@ -10,7 +10,7 @@ from __future__ import annotations
|
|
|
10
10
|
|
|
11
11
|
import logging
|
|
12
12
|
import sqlite3
|
|
13
|
-
from typing import TYPE_CHECKING, Any, Optional
|
|
13
|
+
from typing import TYPE_CHECKING, Any, Optional, TypeVar
|
|
14
14
|
|
|
15
15
|
from typing_extensions import Self
|
|
16
16
|
|
|
@@ -33,6 +33,8 @@ if TYPE_CHECKING: # pragma: no cover
|
|
|
33
33
|
|
|
34
34
|
from sqliter.model.model import BaseDBModel
|
|
35
35
|
|
|
36
|
+
T = TypeVar("T", bound="BaseDBModel")
|
|
37
|
+
|
|
36
38
|
|
|
37
39
|
class SqliterDB:
|
|
38
40
|
"""Main class for interacting with SQLite databases.
|
|
@@ -223,28 +225,12 @@ class SqliterDB:
|
|
|
223
225
|
"""
|
|
224
226
|
table_name = model_class.get_table_name()
|
|
225
227
|
primary_key = model_class.get_primary_key()
|
|
226
|
-
create_pk = model_class.should_create_pk()
|
|
227
228
|
|
|
228
229
|
if force:
|
|
229
230
|
drop_table_sql = f"DROP TABLE IF EXISTS {table_name}"
|
|
230
231
|
self._execute_sql(drop_table_sql)
|
|
231
232
|
|
|
232
|
-
fields = []
|
|
233
|
-
|
|
234
|
-
# Always add the primary key field first
|
|
235
|
-
if create_pk:
|
|
236
|
-
fields.append(f"{primary_key} INTEGER PRIMARY KEY AUTOINCREMENT")
|
|
237
|
-
else:
|
|
238
|
-
field_info = model_class.model_fields.get(primary_key)
|
|
239
|
-
if field_info is not None:
|
|
240
|
-
sqlite_type = infer_sqlite_type(field_info.annotation)
|
|
241
|
-
fields.append(f"{primary_key} {sqlite_type} PRIMARY KEY")
|
|
242
|
-
else:
|
|
243
|
-
err = (
|
|
244
|
-
f"Primary key field '{primary_key}' not found in model "
|
|
245
|
-
"fields."
|
|
246
|
-
)
|
|
247
|
-
raise ValueError(err)
|
|
233
|
+
fields = [f'"{primary_key}" INTEGER PRIMARY KEY AUTOINCREMENT']
|
|
248
234
|
|
|
249
235
|
# Add remaining fields
|
|
250
236
|
for field_name, field_info in model_class.model_fields.items():
|
|
@@ -325,19 +311,28 @@ class SqliterDB:
|
|
|
325
311
|
if self.auto_commit and self.conn:
|
|
326
312
|
self.conn.commit()
|
|
327
313
|
|
|
328
|
-
def insert(self, model_instance:
|
|
314
|
+
def insert(self, model_instance: T) -> T:
|
|
329
315
|
"""Insert a new record into the database.
|
|
330
316
|
|
|
331
317
|
Args:
|
|
332
|
-
model_instance:
|
|
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.
|
|
333
322
|
|
|
334
323
|
Raises:
|
|
335
|
-
RecordInsertionError: If
|
|
324
|
+
RecordInsertionError: If an error occurs during the insertion.
|
|
336
325
|
"""
|
|
337
326
|
model_class = type(model_instance)
|
|
338
327
|
table_name = model_class.get_table_name()
|
|
339
328
|
|
|
329
|
+
# Get the data from the model
|
|
340
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
|
+
|
|
341
336
|
fields = ", ".join(data.keys())
|
|
342
337
|
placeholders = ", ".join(
|
|
343
338
|
["?" if value is not None else "NULL" for value in data.values()]
|
|
@@ -354,11 +349,15 @@ class SqliterDB:
|
|
|
354
349
|
cursor = conn.cursor()
|
|
355
350
|
cursor.execute(insert_sql, values)
|
|
356
351
|
self._maybe_commit()
|
|
352
|
+
|
|
357
353
|
except sqlite3.Error as exc:
|
|
358
354
|
raise RecordInsertionError(table_name) from exc
|
|
355
|
+
else:
|
|
356
|
+
data.pop("pk", None)
|
|
357
|
+
return model_class(pk=cursor.lastrowid, **data)
|
|
359
358
|
|
|
360
359
|
def get(
|
|
361
|
-
self, model_class: type[BaseDBModel], primary_key_value:
|
|
360
|
+
self, model_class: type[BaseDBModel], primary_key_value: int
|
|
362
361
|
) -> BaseDBModel | None:
|
|
363
362
|
"""Retrieve a single record from the database by its primary key.
|
|
364
363
|
|
|
@@ -405,11 +404,12 @@ class SqliterDB:
|
|
|
405
404
|
model_instance: An instance of a Pydantic model to be updated.
|
|
406
405
|
|
|
407
406
|
Raises:
|
|
408
|
-
RecordUpdateError: If there's an error updating the record
|
|
409
|
-
|
|
407
|
+
RecordUpdateError: If there's an error updating the record or if it
|
|
408
|
+
is not found.
|
|
410
409
|
"""
|
|
411
410
|
model_class = type(model_instance)
|
|
412
411
|
table_name = model_class.get_table_name()
|
|
412
|
+
|
|
413
413
|
primary_key = model_class.get_primary_key()
|
|
414
414
|
|
|
415
415
|
fields = ", ".join(
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: sqliter-py
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
4
4
|
Summary: Interact with SQLite databases using Python and Pydantic
|
|
5
5
|
Project-URL: Pull Requests, https://github.com/seapagan/sqliter-py/pulls
|
|
6
6
|
Project-URL: Bug Tracker, https://github.com/seapagan/sqliter-py/issues
|
|
@@ -52,14 +52,16 @@ Website](https://sqliter.grantramsay.dev)
|
|
|
52
52
|
|
|
53
53
|
> [!CAUTION]
|
|
54
54
|
> This project is still in the early stages of development and is lacking some
|
|
55
|
-
> planned functionality. Please use with caution
|
|
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.
|
|
56
59
|
>
|
|
57
60
|
> Also, structures like `list`, `dict`, `set` etc are not supported **at this
|
|
58
61
|
> time** as field types, since SQLite does not have a native column type for
|
|
59
|
-
> these.
|
|
60
|
-
>
|
|
61
|
-
>
|
|
62
|
-
> `BLOB` fields), then serialize before saving after and retrieving data.
|
|
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.
|
|
63
65
|
>
|
|
64
66
|
> See the [TODO](TODO.md) for planned features and improvements.
|
|
65
67
|
|
|
@@ -74,10 +76,11 @@ Website](https://sqliter.grantramsay.dev)
|
|
|
74
76
|
|
|
75
77
|
- Table creation based on Pydantic models
|
|
76
78
|
- CRUD operations (Create, Read, Update, Delete)
|
|
77
|
-
-
|
|
79
|
+
- Chained Query building with filtering, ordering, and pagination
|
|
78
80
|
- Transaction support
|
|
79
81
|
- Custom exceptions for better error handling
|
|
80
82
|
- Full type hinting and type checking
|
|
83
|
+
- Detailed documentation and examples
|
|
81
84
|
- No external dependencies other than Pydantic
|
|
82
85
|
- Full test coverage
|
|
83
86
|
- Can optionally output the raw SQL queries being executed for debugging
|
|
@@ -142,7 +145,7 @@ db.create_table(User)
|
|
|
142
145
|
|
|
143
146
|
# Insert a record
|
|
144
147
|
user = User(name="John Doe", age=30)
|
|
145
|
-
db.insert(user)
|
|
148
|
+
new_user = db.insert(user)
|
|
146
149
|
|
|
147
150
|
# Query records
|
|
148
151
|
results = db.select(User).filter(name="John Doe").fetch_all()
|
|
@@ -150,11 +153,11 @@ for user in results:
|
|
|
150
153
|
print(f"User: {user.name}, Age: {user.age}")
|
|
151
154
|
|
|
152
155
|
# Update a record
|
|
153
|
-
|
|
154
|
-
db.update(
|
|
156
|
+
new_user.age = 31
|
|
157
|
+
db.update(new_user)
|
|
155
158
|
|
|
156
159
|
# Delete a record
|
|
157
|
-
db.delete(User,
|
|
160
|
+
db.delete(User, new_user.pk)
|
|
158
161
|
```
|
|
159
162
|
|
|
160
163
|
See the [Usage](https://sqliter.grantramsay.dev/usage) section of the documentation
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
sqliter/__init__.py,sha256=ECfn02OPmiMCQvRYbfizKFhVDk00xV-HV1s2cH9037I,244
|
|
2
|
+
sqliter/constants.py,sha256=j0lE2cB1Uj8cNo40GGtCPdOR5asm-dHRDmGG0oyindA,1050
|
|
3
|
+
sqliter/exceptions.py,sha256=g6jjcBnU2wnd_Sz90PCARl8PRayhU-D88W0tIUh548A,4808
|
|
4
|
+
sqliter/helpers.py,sha256=75r4zMmGztVPm9_Bz3L1cSvBdx17uPEAnaggVhD70Pg,1138
|
|
5
|
+
sqliter/sqliter.py,sha256=XLMIZZfMdRFv7iyGLH6N9uHE_0bAIxgvxgLxADYAjv8,18545
|
|
6
|
+
sqliter/model/__init__.py,sha256=GgULmKRn0Dq0Jz6LbHGPndS6GP3vd1uxI1KrEevofLs,237
|
|
7
|
+
sqliter/model/model.py,sha256=1aLe0QX5HEovOb9U6F9i_-fs4JqpPpewafd8KWINAkQ,4595
|
|
8
|
+
sqliter/query/__init__.py,sha256=MRajhjTPJqjbmmrwndVKj8vqMbK5-XufpwoIswQf5z4,239
|
|
9
|
+
sqliter/query/query.py,sha256=g9MbF3AGqNNha7QStxNv1qtIXv0A9FUgerur_0qhu8U,24081
|
|
10
|
+
sqliter_py-0.5.0.dist-info/METADATA,sha256=-kRDsXK1GrxUoL3B9uNEMwnsNUwhGsUjYGuNsjSxZ2g,7271
|
|
11
|
+
sqliter_py-0.5.0.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
|
|
12
|
+
sqliter_py-0.5.0.dist-info/licenses/LICENSE.txt,sha256=-r4mvgoEWzkl1hPO5k8I_iMwJate7zDj8p_Fmn7dhVg,1078
|
|
13
|
+
sqliter_py-0.5.0.dist-info/RECORD,,
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
sqliter/__init__.py,sha256=ECfn02OPmiMCQvRYbfizKFhVDk00xV-HV1s2cH9037I,244
|
|
2
|
-
sqliter/constants.py,sha256=j0lE2cB1Uj8cNo40GGtCPdOR5asm-dHRDmGG0oyindA,1050
|
|
3
|
-
sqliter/exceptions.py,sha256=56yDixGEkfC-OHPG-8t067xzJhspXXoxQWFU5jkU-1M,4794
|
|
4
|
-
sqliter/helpers.py,sha256=75r4zMmGztVPm9_Bz3L1cSvBdx17uPEAnaggVhD70Pg,1138
|
|
5
|
-
sqliter/sqliter.py,sha256=n3EtU0Lj9tJaIZicapz1l6L5ofpcpmozmQ7O3QQ977A,18741
|
|
6
|
-
sqliter/model/__init__.py,sha256=GgULmKRn0Dq0Jz6LbHGPndS6GP3vd1uxI1KrEevofLs,237
|
|
7
|
-
sqliter/model/model.py,sha256=rzmYEpv28fXnLV9yuApjHyUj7bL4lkHWUzv8GXkk3kQ,4901
|
|
8
|
-
sqliter/query/__init__.py,sha256=MRajhjTPJqjbmmrwndVKj8vqMbK5-XufpwoIswQf5z4,239
|
|
9
|
-
sqliter/query/query.py,sha256=iUJ1jdp1vWp5n77by2LKjxdl0-SWEA7T01Uk2_gHvZ8,23547
|
|
10
|
-
sqliter_py-0.4.0.dist-info/METADATA,sha256=fmkuZ-W_ZBN7ycWuxnnxZ6FPcMe1DYRtdIBatG9otrI,7102
|
|
11
|
-
sqliter_py-0.4.0.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
|
|
12
|
-
sqliter_py-0.4.0.dist-info/licenses/LICENSE.txt,sha256=-r4mvgoEWzkl1hPO5k8I_iMwJate7zDj8p_Fmn7dhVg,1078
|
|
13
|
-
sqliter_py-0.4.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|