sqliter-py 0.6.0__tar.gz → 0.8.0__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.
Potentially problematic release.
This version of sqliter-py might be problematic. Click here for more details.
- {sqliter_py-0.6.0 → sqliter_py-0.8.0}/.gitignore +2 -0
- {sqliter_py-0.6.0 → sqliter_py-0.8.0}/LICENSE.txt +1 -1
- {sqliter_py-0.6.0 → sqliter_py-0.8.0}/PKG-INFO +13 -12
- {sqliter_py-0.6.0 → sqliter_py-0.8.0}/README.md +8 -10
- {sqliter_py-0.6.0 → sqliter_py-0.8.0}/pyproject.toml +5 -4
- {sqliter_py-0.6.0 → sqliter_py-0.8.0}/sqliter/constants.py +8 -0
- sqliter_py-0.8.0/sqliter/helpers.py +100 -0
- {sqliter_py-0.6.0 → sqliter_py-0.8.0}/sqliter/model/__init__.py +2 -2
- {sqliter_py-0.6.0 → sqliter_py-0.8.0}/sqliter/model/model.py +86 -1
- {sqliter_py-0.6.0 → sqliter_py-0.8.0}/sqliter/query/query.py +58 -8
- {sqliter_py-0.6.0 → sqliter_py-0.8.0}/sqliter/sqliter.py +118 -18
- sqliter_py-0.6.0/sqliter/helpers.py +0 -35
- {sqliter_py-0.6.0 → sqliter_py-0.8.0}/sqliter/__init__.py +0 -0
- {sqliter_py-0.6.0 → sqliter_py-0.8.0}/sqliter/exceptions.py +0 -0
- {sqliter_py-0.6.0 → sqliter_py-0.8.0}/sqliter/model/unique.py +0 -0
- {sqliter_py-0.6.0 → sqliter_py-0.8.0}/sqliter/query/__init__.py +0 -0
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: sqliter-py
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.8.0
|
|
4
4
|
Summary: Interact with SQLite databases using Python and Pydantic
|
|
5
|
+
Project-URL: Homepage, http://sqliter.grantramsay.dev
|
|
5
6
|
Project-URL: Pull Requests, https://github.com/seapagan/sqliter-py/pulls
|
|
6
7
|
Project-URL: Bug Tracker, https://github.com/seapagan/sqliter-py/issues
|
|
7
8
|
Project-URL: Changelog, https://github.com/seapagan/sqliter-py/blob/main/CHANGELOG.md
|
|
@@ -18,6 +19,8 @@ Classifier: Programming Language :: Python :: 3.9
|
|
|
18
19
|
Classifier: Programming Language :: Python :: 3.10
|
|
19
20
|
Classifier: Programming Language :: Python :: 3.11
|
|
20
21
|
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
23
|
+
Classifier: Topic :: Database :: Front-Ends
|
|
21
24
|
Classifier: Topic :: Software Development
|
|
22
25
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
23
26
|
Requires-Python: >=3.9
|
|
@@ -47,8 +50,7 @@ time).
|
|
|
47
50
|
The ideal use case is more for Python CLI tools that need to store data in a
|
|
48
51
|
database-like format without needing to learn SQL or use a full ORM.
|
|
49
52
|
|
|
50
|
-
Full documentation is available on the [
|
|
51
|
-
Website](https://sqliter.grantramsay.dev)
|
|
53
|
+
Full documentation is available on the [Website](https://sqliter.grantramsay.dev)
|
|
52
54
|
|
|
53
55
|
> [!CAUTION]
|
|
54
56
|
> This project is still in the early stages of development and is lacking some
|
|
@@ -57,12 +59,6 @@ Website](https://sqliter.grantramsay.dev)
|
|
|
57
59
|
> minimum and the releases and documentation will be very clear about any
|
|
58
60
|
> breaking changes.
|
|
59
61
|
>
|
|
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
62
|
> See the [TODO](TODO.md) for planned features and improvements.
|
|
67
63
|
|
|
68
64
|
- [Features](#features)
|
|
@@ -75,6 +71,8 @@ Website](https://sqliter.grantramsay.dev)
|
|
|
75
71
|
## Features
|
|
76
72
|
|
|
77
73
|
- Table creation based on Pydantic models
|
|
74
|
+
- Supports `date` and `datetime` fields
|
|
75
|
+
- Support for complex data types (`list`, `dict`, `set`, `tuple`) stored as BLOBs
|
|
78
76
|
- Automatic primary key generation
|
|
79
77
|
- User defined indexes on any field
|
|
80
78
|
- Set any field as UNIQUE
|
|
@@ -159,8 +157,11 @@ for user in results:
|
|
|
159
157
|
new_user.age = 31
|
|
160
158
|
db.update(new_user)
|
|
161
159
|
|
|
162
|
-
# Delete a record
|
|
160
|
+
# Delete a record by primary key
|
|
163
161
|
db.delete(User, new_user.pk)
|
|
162
|
+
|
|
163
|
+
# Delete all records returned from a query:
|
|
164
|
+
delete_count = db.select(User).filter(age__gt=30).delete()
|
|
164
165
|
```
|
|
165
166
|
|
|
166
167
|
See the [Usage](https://sqliter.grantramsay.dev/usage) section of the documentation
|
|
@@ -180,7 +181,7 @@ which you can read in the [CODE_OF_CONDUCT](CODE_OF_CONDUCT.md) file.
|
|
|
180
181
|
This project is licensed under the MIT License.
|
|
181
182
|
|
|
182
183
|
```pre
|
|
183
|
-
Copyright (c) 2024 Grant Ramsay
|
|
184
|
+
Copyright (c) 2024-2025 Grant Ramsay
|
|
184
185
|
|
|
185
186
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
186
187
|
of this software and associated documentation files (the "Software"), to deal
|
|
@@ -19,8 +19,7 @@ time).
|
|
|
19
19
|
The ideal use case is more for Python CLI tools that need to store data in a
|
|
20
20
|
database-like format without needing to learn SQL or use a full ORM.
|
|
21
21
|
|
|
22
|
-
Full documentation is available on the [
|
|
23
|
-
Website](https://sqliter.grantramsay.dev)
|
|
22
|
+
Full documentation is available on the [Website](https://sqliter.grantramsay.dev)
|
|
24
23
|
|
|
25
24
|
> [!CAUTION]
|
|
26
25
|
> This project is still in the early stages of development and is lacking some
|
|
@@ -29,12 +28,6 @@ Website](https://sqliter.grantramsay.dev)
|
|
|
29
28
|
> minimum and the releases and documentation will be very clear about any
|
|
30
29
|
> breaking changes.
|
|
31
30
|
>
|
|
32
|
-
> Also, structures like `list`, `dict`, `set` etc are not supported **at this
|
|
33
|
-
> time** as field types, since SQLite does not have a native column type for
|
|
34
|
-
> these. This is the **next planned enhancement**. These will need to be
|
|
35
|
-
> `pickled` first then stored as a BLOB in the database . Also support `date`
|
|
36
|
-
> which can be stored as a Unix timestamp in an integer field.
|
|
37
|
-
>
|
|
38
31
|
> See the [TODO](TODO.md) for planned features and improvements.
|
|
39
32
|
|
|
40
33
|
- [Features](#features)
|
|
@@ -47,6 +40,8 @@ Website](https://sqliter.grantramsay.dev)
|
|
|
47
40
|
## Features
|
|
48
41
|
|
|
49
42
|
- Table creation based on Pydantic models
|
|
43
|
+
- Supports `date` and `datetime` fields
|
|
44
|
+
- Support for complex data types (`list`, `dict`, `set`, `tuple`) stored as BLOBs
|
|
50
45
|
- Automatic primary key generation
|
|
51
46
|
- User defined indexes on any field
|
|
52
47
|
- Set any field as UNIQUE
|
|
@@ -131,8 +126,11 @@ for user in results:
|
|
|
131
126
|
new_user.age = 31
|
|
132
127
|
db.update(new_user)
|
|
133
128
|
|
|
134
|
-
# Delete a record
|
|
129
|
+
# Delete a record by primary key
|
|
135
130
|
db.delete(User, new_user.pk)
|
|
131
|
+
|
|
132
|
+
# Delete all records returned from a query:
|
|
133
|
+
delete_count = db.select(User).filter(age__gt=30).delete()
|
|
136
134
|
```
|
|
137
135
|
|
|
138
136
|
See the [Usage](https://sqliter.grantramsay.dev/usage) section of the documentation
|
|
@@ -152,7 +150,7 @@ which you can read in the [CODE_OF_CONDUCT](CODE_OF_CONDUCT.md) file.
|
|
|
152
150
|
This project is licensed under the MIT License.
|
|
153
151
|
|
|
154
152
|
```pre
|
|
155
|
-
Copyright (c) 2024 Grant Ramsay
|
|
153
|
+
Copyright (c) 2024-2025 Grant Ramsay
|
|
156
154
|
|
|
157
155
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
158
156
|
of this software and associated documentation files (the "Software"), to deal
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
[project]
|
|
5
5
|
name = "sqliter-py"
|
|
6
|
-
version = "0.
|
|
6
|
+
version = "0.8.0"
|
|
7
7
|
description = "Interact with SQLite databases using Python and Pydantic"
|
|
8
8
|
readme = "README.md"
|
|
9
9
|
requires-python = ">=3.9"
|
|
@@ -21,6 +21,8 @@ classifiers = [
|
|
|
21
21
|
"Programming Language :: Python :: 3.10",
|
|
22
22
|
"Programming Language :: Python :: 3.11",
|
|
23
23
|
"Programming Language :: Python :: 3.12",
|
|
24
|
+
"Programming Language :: Python :: 3.13",
|
|
25
|
+
"Topic :: Database :: Front-Ends",
|
|
24
26
|
"Topic :: Software Development",
|
|
25
27
|
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
26
28
|
]
|
|
@@ -29,7 +31,7 @@ classifiers = [
|
|
|
29
31
|
extras = ["inflect==7.0.0"]
|
|
30
32
|
|
|
31
33
|
[project.urls]
|
|
32
|
-
|
|
34
|
+
"Homepage" = "http://sqliter.grantramsay.dev"
|
|
33
35
|
"Pull Requests" = "https://github.com/seapagan/sqliter-py/pulls"
|
|
34
36
|
"Bug Tracker" = "https://github.com/seapagan/sqliter-py/issues"
|
|
35
37
|
"Changelog" = "https://github.com/seapagan/sqliter-py/blob/main/CHANGELOG.md"
|
|
@@ -100,12 +102,11 @@ changelog.help = "Generate a changelog"
|
|
|
100
102
|
line-length = 80
|
|
101
103
|
lint.select = ["ALL"] # we are being very strict!
|
|
102
104
|
lint.ignore = [
|
|
103
|
-
"ANN101",
|
|
104
|
-
"ANN102",
|
|
105
105
|
"PGH003",
|
|
106
106
|
"FBT002",
|
|
107
107
|
"FBT003",
|
|
108
108
|
"B006",
|
|
109
|
+
"S301", # in this library we use 'pickle' for saving and loading list etc
|
|
109
110
|
] # These rules are too strict even for us 😝
|
|
110
111
|
lint.extend-ignore = [
|
|
111
112
|
"COM812",
|
|
@@ -6,6 +6,8 @@ operators and data types, which are crucial for translating between
|
|
|
6
6
|
Pydantic models and SQLite database operations.
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
|
+
import datetime
|
|
10
|
+
|
|
9
11
|
# A dictionary mapping SQLiter filter operators to their corresponding SQL
|
|
10
12
|
# operators.
|
|
11
13
|
OPERATOR_MAPPING = {
|
|
@@ -34,4 +36,10 @@ SQLITE_TYPE_MAPPING = {
|
|
|
34
36
|
str: "TEXT",
|
|
35
37
|
bool: "INTEGER", # SQLite stores booleans as integers (0 or 1)
|
|
36
38
|
bytes: "BLOB",
|
|
39
|
+
datetime.datetime: "INTEGER", # Store as Unix timestamp
|
|
40
|
+
datetime.date: "INTEGER", # Store as Unix timestamp
|
|
41
|
+
list: "BLOB",
|
|
42
|
+
dict: "BLOB",
|
|
43
|
+
set: "BLOB",
|
|
44
|
+
tuple: "BLOB",
|
|
37
45
|
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""Utility functions for SQLiter internal operations.
|
|
2
|
+
|
|
3
|
+
This module provides helper functions used across the SQLiter library,
|
|
4
|
+
primarily for type inference and mapping between Python and SQLite
|
|
5
|
+
data types. These utilities support the core functionality of model
|
|
6
|
+
to database schema translation.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import datetime
|
|
12
|
+
from typing import Union
|
|
13
|
+
|
|
14
|
+
from sqliter.constants import SQLITE_TYPE_MAPPING
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def infer_sqlite_type(field_type: Union[type, None]) -> str:
|
|
18
|
+
"""Infer the SQLite column type based on the Python type.
|
|
19
|
+
|
|
20
|
+
This function maps Python types to their corresponding SQLite column
|
|
21
|
+
types. It's used when creating database tables to ensure that the
|
|
22
|
+
correct SQLite types are used for each field.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
field_type: The Python type of the field, or None.
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
A string representing the corresponding SQLite column type.
|
|
29
|
+
|
|
30
|
+
Note:
|
|
31
|
+
If the input type is None or not recognized, it defaults to 'TEXT'.
|
|
32
|
+
"""
|
|
33
|
+
# If field_type is None, default to TEXT
|
|
34
|
+
if field_type is None:
|
|
35
|
+
return "TEXT"
|
|
36
|
+
|
|
37
|
+
# Map the simplified type to an SQLite type
|
|
38
|
+
return SQLITE_TYPE_MAPPING.get(field_type, "TEXT")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def to_unix_timestamp(value: datetime.date | datetime.datetime) -> int:
|
|
42
|
+
"""Convert datetime or date to a Unix timestamp in UTC.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
value: The datetime or date object to convert.
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
An integer Unix timestamp.
|
|
49
|
+
|
|
50
|
+
Raises:
|
|
51
|
+
TypeError: If the value is not a datetime or date object.
|
|
52
|
+
"""
|
|
53
|
+
if isinstance(value, datetime.datetime):
|
|
54
|
+
# If no timezone is provided, assume local time and convert to UTC
|
|
55
|
+
if value.tzinfo is None:
|
|
56
|
+
value = value.astimezone() # Convert to user's local timezone
|
|
57
|
+
# Convert to UTC before storing
|
|
58
|
+
value = value.astimezone(datetime.timezone.utc)
|
|
59
|
+
return int(value.timestamp())
|
|
60
|
+
if isinstance(value, datetime.date):
|
|
61
|
+
# Convert date to datetime at midnight in UTC
|
|
62
|
+
dt = datetime.datetime.combine(
|
|
63
|
+
value, datetime.time(0, 0), tzinfo=datetime.timezone.utc
|
|
64
|
+
)
|
|
65
|
+
return int(dt.timestamp())
|
|
66
|
+
|
|
67
|
+
err_msg = "Expected datetime or date object."
|
|
68
|
+
raise TypeError(err_msg)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def from_unix_timestamp(
|
|
72
|
+
value: int, to_type: type, *, localize: bool = True
|
|
73
|
+
) -> datetime.date | datetime.datetime:
|
|
74
|
+
"""Convert a Unix timestamp to datetime or date, optionally to local time.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
value: The Unix timestamp as an integer.
|
|
78
|
+
to_type: The expected output type, either datetime or date.
|
|
79
|
+
localize: If True, convert the datetime to the user's local timezone.
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
The corresponding datetime or date object.
|
|
83
|
+
|
|
84
|
+
Raises:
|
|
85
|
+
TypeError: If to_type is not datetime or date.
|
|
86
|
+
"""
|
|
87
|
+
if to_type is datetime.datetime:
|
|
88
|
+
# Convert the Unix timestamp to UTC datetime
|
|
89
|
+
dt = datetime.datetime.fromtimestamp(value, tz=datetime.timezone.utc)
|
|
90
|
+
# Convert to local time if requested
|
|
91
|
+
return dt.astimezone() if localize else dt
|
|
92
|
+
if to_type is datetime.date:
|
|
93
|
+
# Convert to UTC datetime first
|
|
94
|
+
dt = datetime.datetime.fromtimestamp(value, tz=datetime.timezone.utc)
|
|
95
|
+
# Convert to local time if requested, then return the date part
|
|
96
|
+
dt_local = dt.astimezone() if localize else dt
|
|
97
|
+
return dt_local.date() # Extract the date part
|
|
98
|
+
|
|
99
|
+
err_msg = "Expected datetime or date type."
|
|
100
|
+
raise TypeError(err_msg)
|
|
@@ -5,7 +5,7 @@ models in SQLiter applications, and the Unique class, which is used to
|
|
|
5
5
|
define unique constraints on model fields.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
-
from .model import BaseDBModel
|
|
8
|
+
from .model import BaseDBModel, SerializableField
|
|
9
9
|
from .unique import Unique
|
|
10
10
|
|
|
11
|
-
__all__ = ["BaseDBModel", "Unique"]
|
|
11
|
+
__all__ = ["BaseDBModel", "SerializableField", "Unique"]
|
|
@@ -9,11 +9,14 @@ in SQLiter applications.
|
|
|
9
9
|
|
|
10
10
|
from __future__ import annotations
|
|
11
11
|
|
|
12
|
+
import datetime
|
|
13
|
+
import pickle
|
|
12
14
|
import re
|
|
13
15
|
from typing import (
|
|
14
16
|
Any,
|
|
15
17
|
ClassVar,
|
|
16
18
|
Optional,
|
|
19
|
+
Protocol,
|
|
17
20
|
TypeVar,
|
|
18
21
|
Union,
|
|
19
22
|
cast,
|
|
@@ -23,9 +26,15 @@ from typing import (
|
|
|
23
26
|
|
|
24
27
|
from pydantic import BaseModel, ConfigDict, Field
|
|
25
28
|
|
|
29
|
+
from sqliter.helpers import from_unix_timestamp, to_unix_timestamp
|
|
30
|
+
|
|
26
31
|
T = TypeVar("T", bound="BaseDBModel")
|
|
27
32
|
|
|
28
33
|
|
|
34
|
+
class SerializableField(Protocol):
|
|
35
|
+
"""Protocol for fields that can be serialized or deserialized."""
|
|
36
|
+
|
|
37
|
+
|
|
29
38
|
class BaseDBModel(BaseModel):
|
|
30
39
|
"""Base model class for SQLiter database models.
|
|
31
40
|
|
|
@@ -38,11 +47,19 @@ class BaseDBModel(BaseModel):
|
|
|
38
47
|
"""
|
|
39
48
|
|
|
40
49
|
pk: int = Field(0, description="The mandatory primary key of the table.")
|
|
50
|
+
created_at: int = Field(
|
|
51
|
+
default=0,
|
|
52
|
+
description="Unix timestamp when the record was created.",
|
|
53
|
+
)
|
|
54
|
+
updated_at: int = Field(
|
|
55
|
+
default=0,
|
|
56
|
+
description="Unix timestamp when the record was last updated.",
|
|
57
|
+
)
|
|
41
58
|
|
|
42
59
|
model_config = ConfigDict(
|
|
43
60
|
extra="ignore",
|
|
44
61
|
populate_by_name=True,
|
|
45
|
-
validate_assignment=
|
|
62
|
+
validate_assignment=True,
|
|
46
63
|
from_attributes=True,
|
|
47
64
|
)
|
|
48
65
|
|
|
@@ -151,3 +168,71 @@ class BaseDBModel(BaseModel):
|
|
|
151
168
|
def should_create_pk(cls) -> bool:
|
|
152
169
|
"""Returns True since the primary key is always created."""
|
|
153
170
|
return True
|
|
171
|
+
|
|
172
|
+
@classmethod
|
|
173
|
+
def serialize_field(cls, value: SerializableField) -> SerializableField:
|
|
174
|
+
"""Serialize datetime or date fields to Unix timestamp.
|
|
175
|
+
|
|
176
|
+
Args:
|
|
177
|
+
field_name: The name of the field.
|
|
178
|
+
value: The value of the field.
|
|
179
|
+
|
|
180
|
+
Returns:
|
|
181
|
+
An integer Unix timestamp if the field is a datetime or date.
|
|
182
|
+
"""
|
|
183
|
+
if isinstance(value, (datetime.datetime, datetime.date)):
|
|
184
|
+
return to_unix_timestamp(value)
|
|
185
|
+
if isinstance(value, (list, dict, set, tuple)):
|
|
186
|
+
return pickle.dumps(value)
|
|
187
|
+
return value # Return value as-is for other fields
|
|
188
|
+
|
|
189
|
+
# Deserialization after fetching from the database
|
|
190
|
+
|
|
191
|
+
@classmethod
|
|
192
|
+
def deserialize_field(
|
|
193
|
+
cls,
|
|
194
|
+
field_name: str,
|
|
195
|
+
value: SerializableField,
|
|
196
|
+
*,
|
|
197
|
+
return_local_time: bool,
|
|
198
|
+
) -> object:
|
|
199
|
+
"""Deserialize fields from Unix timestamp to datetime or date.
|
|
200
|
+
|
|
201
|
+
Args:
|
|
202
|
+
field_name: The name of the field being deserialized.
|
|
203
|
+
value: The Unix timestamp value fetched from the database.
|
|
204
|
+
return_local_time: Flag to control whether the datetime is localized
|
|
205
|
+
to the user's timezone.
|
|
206
|
+
|
|
207
|
+
Returns:
|
|
208
|
+
A datetime or date object if the field type is datetime or date,
|
|
209
|
+
otherwise returns the value as-is.
|
|
210
|
+
"""
|
|
211
|
+
if value is None:
|
|
212
|
+
return None
|
|
213
|
+
|
|
214
|
+
# Get field type if it exists in model_fields
|
|
215
|
+
field_info = cls.model_fields.get(field_name)
|
|
216
|
+
if field_info is None:
|
|
217
|
+
# If field doesn't exist in model, return value as-is
|
|
218
|
+
return value
|
|
219
|
+
|
|
220
|
+
field_type = field_info.annotation
|
|
221
|
+
|
|
222
|
+
if (
|
|
223
|
+
isinstance(field_type, type)
|
|
224
|
+
and issubclass(field_type, (datetime.datetime, datetime.date))
|
|
225
|
+
and isinstance(value, int)
|
|
226
|
+
):
|
|
227
|
+
return from_unix_timestamp(
|
|
228
|
+
value, field_type, localize=return_local_time
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
origin_type = get_origin(field_type) or field_type
|
|
232
|
+
if origin_type in (list, dict, set, tuple) and isinstance(value, bytes):
|
|
233
|
+
try:
|
|
234
|
+
return pickle.loads(value)
|
|
235
|
+
except pickle.UnpicklingError:
|
|
236
|
+
return value
|
|
237
|
+
|
|
238
|
+
return value
|
|
@@ -28,6 +28,7 @@ from sqliter.exceptions import (
|
|
|
28
28
|
InvalidFilterError,
|
|
29
29
|
InvalidOffsetError,
|
|
30
30
|
InvalidOrderError,
|
|
31
|
+
RecordDeletionError,
|
|
31
32
|
RecordFetchError,
|
|
32
33
|
)
|
|
33
34
|
|
|
@@ -35,7 +36,7 @@ if TYPE_CHECKING: # pragma: no cover
|
|
|
35
36
|
from pydantic.fields import FieldInfo
|
|
36
37
|
|
|
37
38
|
from sqliter import SqliterDB
|
|
38
|
-
from sqliter.model import BaseDBModel
|
|
39
|
+
from sqliter.model import BaseDBModel, SerializableField
|
|
39
40
|
|
|
40
41
|
# Define a type alias for the possible value types
|
|
41
42
|
FilterValue = Union[
|
|
@@ -609,14 +610,32 @@ class QueryBuilder:
|
|
|
609
610
|
An instance of the model class populated with the row data.
|
|
610
611
|
"""
|
|
611
612
|
if self._fields:
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
return self.model_class(
|
|
616
|
-
**{
|
|
617
|
-
field: row[idx]
|
|
618
|
-
for idx, field in enumerate(self.model_class.model_fields)
|
|
613
|
+
data = {
|
|
614
|
+
field: self._deserialize(field, row[idx])
|
|
615
|
+
for idx, field in enumerate(self._fields)
|
|
619
616
|
}
|
|
617
|
+
return self.model_class.model_validate_partial(data)
|
|
618
|
+
|
|
619
|
+
data = {
|
|
620
|
+
field: self._deserialize(field, row[idx])
|
|
621
|
+
for idx, field in enumerate(self.model_class.model_fields)
|
|
622
|
+
}
|
|
623
|
+
return self.model_class(**data)
|
|
624
|
+
|
|
625
|
+
def _deserialize(
|
|
626
|
+
self, field_name: str, value: SerializableField
|
|
627
|
+
) -> SerializableField:
|
|
628
|
+
"""Deserialize a field value if needed.
|
|
629
|
+
|
|
630
|
+
Args:
|
|
631
|
+
field_name: Name of the field being deserialized.
|
|
632
|
+
value: Value from the database.
|
|
633
|
+
|
|
634
|
+
Returns:
|
|
635
|
+
The deserialized value.
|
|
636
|
+
"""
|
|
637
|
+
return self.model_class.deserialize_field(
|
|
638
|
+
field_name, value, return_local_time=self.db.return_local_time
|
|
620
639
|
)
|
|
621
640
|
|
|
622
641
|
@overload
|
|
@@ -710,3 +729,34 @@ class QueryBuilder:
|
|
|
710
729
|
True if at least one result exists, False otherwise.
|
|
711
730
|
"""
|
|
712
731
|
return self.count() > 0
|
|
732
|
+
|
|
733
|
+
def delete(self) -> int:
|
|
734
|
+
"""Delete records that match the current query conditions.
|
|
735
|
+
|
|
736
|
+
Returns:
|
|
737
|
+
The number of records deleted.
|
|
738
|
+
|
|
739
|
+
Raises:
|
|
740
|
+
RecordDeletionError: If there's an error deleting the records.
|
|
741
|
+
"""
|
|
742
|
+
sql = f'DELETE FROM "{self.table_name}"' # noqa: S608 # nosec
|
|
743
|
+
|
|
744
|
+
# Build the WHERE clause with special handling for None (NULL in SQL)
|
|
745
|
+
values, where_clause = self._parse_filter()
|
|
746
|
+
|
|
747
|
+
if self.filters:
|
|
748
|
+
sql += f" WHERE {where_clause}"
|
|
749
|
+
|
|
750
|
+
# Print the raw SQL and values if debug is enabled
|
|
751
|
+
if self.db.debug:
|
|
752
|
+
self.db._log_sql(sql, values) # noqa: SLF001
|
|
753
|
+
|
|
754
|
+
try:
|
|
755
|
+
with self.db.connect() as conn:
|
|
756
|
+
cursor = conn.cursor()
|
|
757
|
+
cursor.execute(sql, values)
|
|
758
|
+
deleted_count = cursor.rowcount
|
|
759
|
+
self.db._maybe_commit() # noqa: SLF001
|
|
760
|
+
return deleted_count
|
|
761
|
+
except sqlite3.Error as exc:
|
|
762
|
+
raise RecordDeletionError(self.table_name) from exc
|
|
@@ -10,6 +10,7 @@ from __future__ import annotations
|
|
|
10
10
|
|
|
11
11
|
import logging
|
|
12
12
|
import sqlite3
|
|
13
|
+
import time
|
|
13
14
|
from typing import TYPE_CHECKING, Any, Optional, TypeVar, Union
|
|
14
15
|
|
|
15
16
|
from typing_extensions import Self
|
|
@@ -51,7 +52,9 @@ class SqliterDB:
|
|
|
51
52
|
logger (Optional[logging.Logger]): Custom logger for debug output.
|
|
52
53
|
"""
|
|
53
54
|
|
|
54
|
-
|
|
55
|
+
MEMORY_DB = ":memory:"
|
|
56
|
+
|
|
57
|
+
def __init__( # noqa: PLR0913
|
|
55
58
|
self,
|
|
56
59
|
db_filename: Optional[str] = None,
|
|
57
60
|
*,
|
|
@@ -60,6 +63,7 @@ class SqliterDB:
|
|
|
60
63
|
debug: bool = False,
|
|
61
64
|
logger: Optional[logging.Logger] = None,
|
|
62
65
|
reset: bool = False,
|
|
66
|
+
return_local_time: bool = True,
|
|
63
67
|
) -> None:
|
|
64
68
|
"""Initialize a new SqliterDB instance.
|
|
65
69
|
|
|
@@ -71,12 +75,13 @@ class SqliterDB:
|
|
|
71
75
|
logger: Custom logger for debug output.
|
|
72
76
|
reset: Whether to reset the database on initialization. This will
|
|
73
77
|
basically drop all existing tables.
|
|
78
|
+
return_local_time: Whether to return local time for datetime fields.
|
|
74
79
|
|
|
75
80
|
Raises:
|
|
76
81
|
ValueError: If no filename is provided for a non-memory database.
|
|
77
82
|
"""
|
|
78
83
|
if memory:
|
|
79
|
-
self.db_filename =
|
|
84
|
+
self.db_filename = self.MEMORY_DB
|
|
80
85
|
elif db_filename:
|
|
81
86
|
self.db_filename = db_filename
|
|
82
87
|
else:
|
|
@@ -90,6 +95,7 @@ class SqliterDB:
|
|
|
90
95
|
self.logger = logger
|
|
91
96
|
self.conn: Optional[sqlite3.Connection] = None
|
|
92
97
|
self.reset = reset
|
|
98
|
+
self.return_local_time = return_local_time
|
|
93
99
|
|
|
94
100
|
self._in_transaction = False
|
|
95
101
|
|
|
@@ -99,6 +105,54 @@ class SqliterDB:
|
|
|
99
105
|
if self.reset:
|
|
100
106
|
self._reset_database()
|
|
101
107
|
|
|
108
|
+
@property
|
|
109
|
+
def filename(self) -> Optional[str]:
|
|
110
|
+
"""Returns the filename of the current database or None if in-memory."""
|
|
111
|
+
return None if self.db_filename == self.MEMORY_DB else self.db_filename
|
|
112
|
+
|
|
113
|
+
@property
|
|
114
|
+
def is_memory(self) -> bool:
|
|
115
|
+
"""Returns True if the database is in-memory."""
|
|
116
|
+
return self.db_filename == self.MEMORY_DB
|
|
117
|
+
|
|
118
|
+
@property
|
|
119
|
+
def is_autocommit(self) -> bool:
|
|
120
|
+
"""Returns True if auto-commit is enabled."""
|
|
121
|
+
return self.auto_commit
|
|
122
|
+
|
|
123
|
+
@property
|
|
124
|
+
def is_connected(self) -> bool:
|
|
125
|
+
"""Returns True if the database is connected, False otherwise."""
|
|
126
|
+
return self.conn is not None
|
|
127
|
+
|
|
128
|
+
@property
|
|
129
|
+
def table_names(self) -> list[str]:
|
|
130
|
+
"""Returns a list of all table names in the database.
|
|
131
|
+
|
|
132
|
+
Temporarily connects to the database if not connected and restores
|
|
133
|
+
the connection state afterward.
|
|
134
|
+
"""
|
|
135
|
+
was_connected = self.is_connected
|
|
136
|
+
if not was_connected:
|
|
137
|
+
self.connect()
|
|
138
|
+
|
|
139
|
+
if self.conn is None:
|
|
140
|
+
err_msg = "Failed to establish a database connection."
|
|
141
|
+
raise DatabaseConnectionError(err_msg)
|
|
142
|
+
|
|
143
|
+
cursor = self.conn.cursor()
|
|
144
|
+
cursor.execute(
|
|
145
|
+
"SELECT name FROM sqlite_master WHERE type='table' "
|
|
146
|
+
"AND name NOT LIKE 'sqlite_%';"
|
|
147
|
+
)
|
|
148
|
+
tables = [row[0] for row in cursor.fetchall()]
|
|
149
|
+
|
|
150
|
+
# Restore the connection state
|
|
151
|
+
if not was_connected:
|
|
152
|
+
self.close()
|
|
153
|
+
|
|
154
|
+
return tables
|
|
155
|
+
|
|
102
156
|
def _reset_database(self) -> None:
|
|
103
157
|
"""Drop all user-created tables in the database."""
|
|
104
158
|
with self.connect() as conn:
|
|
@@ -379,11 +433,18 @@ class SqliterDB:
|
|
|
379
433
|
if not self._in_transaction and self.auto_commit and self.conn:
|
|
380
434
|
self.conn.commit()
|
|
381
435
|
|
|
382
|
-
def insert(
|
|
436
|
+
def insert(
|
|
437
|
+
self, model_instance: T, *, timestamp_override: bool = False
|
|
438
|
+
) -> T:
|
|
383
439
|
"""Insert a new record into the database.
|
|
384
440
|
|
|
385
441
|
Args:
|
|
386
442
|
model_instance: The instance of the model class to insert.
|
|
443
|
+
timestamp_override: If True, override the created_at and updated_at
|
|
444
|
+
timestamps with provided values. Default is False. If the values
|
|
445
|
+
are not provided, they will be set to the current time as
|
|
446
|
+
normal. Without this flag, the timestamps will always be set to
|
|
447
|
+
the current time, even if provided.
|
|
387
448
|
|
|
388
449
|
Returns:
|
|
389
450
|
The updated model instance with the primary key (pk) set.
|
|
@@ -394,8 +455,28 @@ class SqliterDB:
|
|
|
394
455
|
model_class = type(model_instance)
|
|
395
456
|
table_name = model_class.get_table_name()
|
|
396
457
|
|
|
458
|
+
# Always set created_at and updated_at timestamps
|
|
459
|
+
current_timestamp = int(time.time())
|
|
460
|
+
|
|
461
|
+
# Handle the case where timestamp_override is False
|
|
462
|
+
if not timestamp_override:
|
|
463
|
+
# Always override both timestamps with the current time
|
|
464
|
+
model_instance.created_at = current_timestamp
|
|
465
|
+
model_instance.updated_at = current_timestamp
|
|
466
|
+
else:
|
|
467
|
+
# Respect provided values, but set to current time if they are 0
|
|
468
|
+
if model_instance.created_at == 0:
|
|
469
|
+
model_instance.created_at = current_timestamp
|
|
470
|
+
if model_instance.updated_at == 0:
|
|
471
|
+
model_instance.updated_at = current_timestamp
|
|
472
|
+
|
|
397
473
|
# Get the data from the model
|
|
398
474
|
data = model_instance.model_dump()
|
|
475
|
+
|
|
476
|
+
# Serialize the data
|
|
477
|
+
for field_name, value in list(data.items()):
|
|
478
|
+
data[field_name] = model_instance.serialize_field(value)
|
|
479
|
+
|
|
399
480
|
# remove the primary key field if it exists, otherwise we'll get
|
|
400
481
|
# TypeErrors as multiple primary keys will exist
|
|
401
482
|
if data.get("pk", None) == 0:
|
|
@@ -422,7 +503,13 @@ class SqliterDB:
|
|
|
422
503
|
raise RecordInsertionError(table_name) from exc
|
|
423
504
|
else:
|
|
424
505
|
data.pop("pk", None)
|
|
425
|
-
|
|
506
|
+
# Deserialize each field before creating the model instance
|
|
507
|
+
deserialized_data = {}
|
|
508
|
+
for field_name, value in data.items():
|
|
509
|
+
deserialized_data[field_name] = model_class.deserialize_field(
|
|
510
|
+
field_name, value, return_local_time=self.return_local_time
|
|
511
|
+
)
|
|
512
|
+
return model_class(pk=cursor.lastrowid, **deserialized_data)
|
|
426
513
|
|
|
427
514
|
def get(
|
|
428
515
|
self, model_class: type[BaseDBModel], primary_key_value: int
|
|
@@ -459,7 +546,17 @@ class SqliterDB:
|
|
|
459
546
|
field: result[idx]
|
|
460
547
|
for idx, field in enumerate(model_class.model_fields)
|
|
461
548
|
}
|
|
462
|
-
|
|
549
|
+
# Deserialize each field before creating the model instance
|
|
550
|
+
deserialized_data = {}
|
|
551
|
+
for field_name, value in result_dict.items():
|
|
552
|
+
deserialized_data[field_name] = (
|
|
553
|
+
model_class.deserialize_field(
|
|
554
|
+
field_name,
|
|
555
|
+
value,
|
|
556
|
+
return_local_time=self.return_local_time,
|
|
557
|
+
)
|
|
558
|
+
)
|
|
559
|
+
return model_class(**deserialized_data)
|
|
463
560
|
except sqlite3.Error as exc:
|
|
464
561
|
raise RecordFetchError(table_name) from exc
|
|
465
562
|
else:
|
|
@@ -473,24 +570,27 @@ class SqliterDB:
|
|
|
473
570
|
|
|
474
571
|
Raises:
|
|
475
572
|
RecordUpdateError: If there's an error updating the record or if it
|
|
476
|
-
|
|
573
|
+
is not found.
|
|
477
574
|
"""
|
|
478
575
|
model_class = type(model_instance)
|
|
479
576
|
table_name = model_class.get_table_name()
|
|
480
|
-
|
|
481
577
|
primary_key = model_class.get_primary_key()
|
|
482
578
|
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
primary_key_value =
|
|
579
|
+
# Set updated_at timestamp
|
|
580
|
+
current_timestamp = int(time.time())
|
|
581
|
+
model_instance.updated_at = current_timestamp
|
|
582
|
+
|
|
583
|
+
# Get the data and serialize any datetime/date fields
|
|
584
|
+
data = model_instance.model_dump()
|
|
585
|
+
for field_name, value in list(data.items()):
|
|
586
|
+
data[field_name] = model_instance.serialize_field(value)
|
|
587
|
+
|
|
588
|
+
# Remove the primary key from the update data
|
|
589
|
+
primary_key_value = data.pop(primary_key)
|
|
590
|
+
|
|
591
|
+
# Create the SQL using the processed data
|
|
592
|
+
fields = ", ".join(f"{field} = ?" for field in data)
|
|
593
|
+
values = tuple(data.values())
|
|
494
594
|
|
|
495
595
|
update_sql = f"""
|
|
496
596
|
UPDATE {table_name}
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
"""Utility functions for SQLiter internal operations.
|
|
2
|
-
|
|
3
|
-
This module provides helper functions used across the SQLiter library,
|
|
4
|
-
primarily for type inference and mapping between Python and SQLite
|
|
5
|
-
data types. These utilities support the core functionality of model
|
|
6
|
-
to database schema translation.
|
|
7
|
-
"""
|
|
8
|
-
|
|
9
|
-
from typing import Union
|
|
10
|
-
|
|
11
|
-
from sqliter.constants import SQLITE_TYPE_MAPPING
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
def infer_sqlite_type(field_type: Union[type, None]) -> str:
|
|
15
|
-
"""Infer the SQLite column type based on the Python type.
|
|
16
|
-
|
|
17
|
-
This function maps Python types to their corresponding SQLite column
|
|
18
|
-
types. It's used when creating database tables to ensure that the
|
|
19
|
-
correct SQLite types are used for each field.
|
|
20
|
-
|
|
21
|
-
Args:
|
|
22
|
-
field_type: The Python type of the field, or None.
|
|
23
|
-
|
|
24
|
-
Returns:
|
|
25
|
-
A string representing the corresponding SQLite column type.
|
|
26
|
-
|
|
27
|
-
Note:
|
|
28
|
-
If the input type is None or not recognized, it defaults to 'TEXT'.
|
|
29
|
-
"""
|
|
30
|
-
# If field_type is None, default to TEXT
|
|
31
|
-
if field_type is None:
|
|
32
|
-
return "TEXT"
|
|
33
|
-
|
|
34
|
-
# Map the simplified type to an SQLite type
|
|
35
|
-
return SQLITE_TYPE_MAPPING.get(field_type, "TEXT")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|