sqliter-py 0.5.0__py3-none-any.whl → 0.9.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/constants.py +8 -0
- sqliter/exceptions.py +21 -0
- sqliter/helpers.py +65 -0
- sqliter/model/__init__.py +5 -3
- sqliter/model/model.py +111 -9
- sqliter/model/unique.py +19 -0
- sqliter/py.typed +0 -0
- sqliter/query/query.py +58 -8
- sqliter/sqliter.py +191 -21
- {sqliter_py-0.5.0.dist-info → sqliter_py-0.9.0.dist-info}/METADATA +38 -28
- sqliter_py-0.9.0.dist-info/RECORD +14 -0
- sqliter_py-0.9.0.dist-info/WHEEL +4 -0
- sqliter_py-0.5.0.dist-info/RECORD +0 -13
- sqliter_py-0.5.0.dist-info/WHEEL +0 -4
- sqliter_py-0.5.0.dist-info/licenses/LICENSE.txt +0 -20
sqliter/constants.py
CHANGED
|
@@ -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
|
}
|
sqliter/exceptions.py
CHANGED
|
@@ -145,3 +145,24 @@ class SqlExecutionError(SqliterError):
|
|
|
145
145
|
"""Raised when an SQL execution fails."""
|
|
146
146
|
|
|
147
147
|
message_template = "Failed to execute SQL: '{}'"
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
class InvalidIndexError(SqliterError):
|
|
151
|
+
"""Exception raised when an invalid index field is specified.
|
|
152
|
+
|
|
153
|
+
This error is triggered if one or more fields specified for an index
|
|
154
|
+
do not exist in the model's fields.
|
|
155
|
+
|
|
156
|
+
Attributes:
|
|
157
|
+
invalid_fields (list[str]): The list of fields that were invalid.
|
|
158
|
+
model_class (str): The name of the model where the error occurred.
|
|
159
|
+
"""
|
|
160
|
+
|
|
161
|
+
message_template = "Invalid fields for indexing in model '{}': {}"
|
|
162
|
+
|
|
163
|
+
def __init__(self, invalid_fields: list[str], model_class: str) -> None:
|
|
164
|
+
"""Tidy up the error message by joining the invalid fields."""
|
|
165
|
+
# Join invalid fields into a comma-separated string
|
|
166
|
+
invalid_fields_str = ", ".join(invalid_fields)
|
|
167
|
+
# Pass the formatted message to the parent class
|
|
168
|
+
super().__init__(model_class, invalid_fields_str)
|
sqliter/helpers.py
CHANGED
|
@@ -6,6 +6,9 @@ data types. These utilities support the core functionality of model
|
|
|
6
6
|
to database schema translation.
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import datetime
|
|
9
12
|
from typing import Union
|
|
10
13
|
|
|
11
14
|
from sqliter.constants import SQLITE_TYPE_MAPPING
|
|
@@ -33,3 +36,65 @@ def infer_sqlite_type(field_type: Union[type, None]) -> str:
|
|
|
33
36
|
|
|
34
37
|
# Map the simplified type to an SQLite type
|
|
35
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)
|
sqliter/model/__init__.py
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
"""This module provides the base model class for SQLiter database models.
|
|
2
2
|
|
|
3
3
|
It exports the BaseDBModel class, which is used to define database
|
|
4
|
-
models in SQLiter applications
|
|
4
|
+
models in SQLiter applications, and the Unique class, which is used to
|
|
5
|
+
define unique constraints on model fields.
|
|
5
6
|
"""
|
|
6
7
|
|
|
7
|
-
from .model import BaseDBModel
|
|
8
|
+
from .model import BaseDBModel, SerializableField
|
|
9
|
+
from .unique import Unique
|
|
8
10
|
|
|
9
|
-
__all__ = ["BaseDBModel"]
|
|
11
|
+
__all__ = ["BaseDBModel", "SerializableField", "Unique"]
|
sqliter/model/model.py
CHANGED
|
@@ -9,12 +9,28 @@ in SQLiter applications.
|
|
|
9
9
|
|
|
10
10
|
from __future__ import annotations
|
|
11
11
|
|
|
12
|
+
import datetime
|
|
13
|
+
import pickle
|
|
12
14
|
import re
|
|
13
|
-
from typing import
|
|
15
|
+
from typing import (
|
|
16
|
+
Any,
|
|
17
|
+
ClassVar,
|
|
18
|
+
Optional,
|
|
19
|
+
Protocol,
|
|
20
|
+
Union,
|
|
21
|
+
cast,
|
|
22
|
+
get_args,
|
|
23
|
+
get_origin,
|
|
24
|
+
)
|
|
14
25
|
|
|
15
26
|
from pydantic import BaseModel, ConfigDict, Field
|
|
27
|
+
from typing_extensions import Self
|
|
16
28
|
|
|
17
|
-
|
|
29
|
+
from sqliter.helpers import from_unix_timestamp, to_unix_timestamp
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class SerializableField(Protocol):
|
|
33
|
+
"""Protocol for fields that can be serialized or deserialized."""
|
|
18
34
|
|
|
19
35
|
|
|
20
36
|
class BaseDBModel(BaseModel):
|
|
@@ -29,11 +45,19 @@ class BaseDBModel(BaseModel):
|
|
|
29
45
|
"""
|
|
30
46
|
|
|
31
47
|
pk: int = Field(0, description="The mandatory primary key of the table.")
|
|
48
|
+
created_at: int = Field(
|
|
49
|
+
default=0,
|
|
50
|
+
description="Unix timestamp when the record was created.",
|
|
51
|
+
)
|
|
52
|
+
updated_at: int = Field(
|
|
53
|
+
default=0,
|
|
54
|
+
description="Unix timestamp when the record was last updated.",
|
|
55
|
+
)
|
|
32
56
|
|
|
33
57
|
model_config = ConfigDict(
|
|
34
58
|
extra="ignore",
|
|
35
59
|
populate_by_name=True,
|
|
36
|
-
validate_assignment=
|
|
60
|
+
validate_assignment=True,
|
|
37
61
|
from_attributes=True,
|
|
38
62
|
)
|
|
39
63
|
|
|
@@ -41,17 +65,27 @@ class BaseDBModel(BaseModel):
|
|
|
41
65
|
"""Metadata class for configuring database-specific attributes.
|
|
42
66
|
|
|
43
67
|
Attributes:
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
68
|
+
table_name (Optional[str]): The name of the database table. If not
|
|
69
|
+
specified, the table name will be inferred from the model class
|
|
70
|
+
name and converted to snake_case.
|
|
71
|
+
indexes (ClassVar[list[Union[str, tuple[str]]]]): A list of fields
|
|
72
|
+
or tuples of fields for which regular (non-unique) indexes
|
|
73
|
+
should be created. Indexes improve query performance on these
|
|
74
|
+
fields.
|
|
75
|
+
unique_indexes (ClassVar[list[Union[str, tuple[str]]]]): A list of
|
|
76
|
+
fields or tuples of fields for which unique indexes should be
|
|
77
|
+
created. Unique indexes enforce that all values in these fields
|
|
78
|
+
are distinct across the table.
|
|
47
79
|
"""
|
|
48
80
|
|
|
49
81
|
table_name: Optional[str] = (
|
|
50
82
|
None # Table name, defaults to class name if not set
|
|
51
83
|
)
|
|
84
|
+
indexes: ClassVar[list[Union[str, tuple[str]]]] = []
|
|
85
|
+
unique_indexes: ClassVar[list[Union[str, tuple[str]]]] = []
|
|
52
86
|
|
|
53
87
|
@classmethod
|
|
54
|
-
def model_validate_partial(cls
|
|
88
|
+
def model_validate_partial(cls, obj: dict[str, Any]) -> Self:
|
|
55
89
|
"""Validate and create a model instance from partial data.
|
|
56
90
|
|
|
57
91
|
This method allows for the creation of a model instance even when
|
|
@@ -87,7 +121,7 @@ class BaseDBModel(BaseModel):
|
|
|
87
121
|
else:
|
|
88
122
|
converted_obj[field_name] = field_type(value)
|
|
89
123
|
|
|
90
|
-
return cast(
|
|
124
|
+
return cast("Self", cls.model_construct(**converted_obj))
|
|
91
125
|
|
|
92
126
|
@classmethod
|
|
93
127
|
def get_table_name(cls) -> str:
|
|
@@ -111,7 +145,7 @@ class BaseDBModel(BaseModel):
|
|
|
111
145
|
|
|
112
146
|
# Pluralize the table name
|
|
113
147
|
try:
|
|
114
|
-
import inflect
|
|
148
|
+
import inflect # noqa: PLC0415
|
|
115
149
|
|
|
116
150
|
p = inflect.engine()
|
|
117
151
|
return p.plural(snake_case_name)
|
|
@@ -132,3 +166,71 @@ class BaseDBModel(BaseModel):
|
|
|
132
166
|
def should_create_pk(cls) -> bool:
|
|
133
167
|
"""Returns True since the primary key is always created."""
|
|
134
168
|
return True
|
|
169
|
+
|
|
170
|
+
@classmethod
|
|
171
|
+
def serialize_field(cls, value: SerializableField) -> SerializableField:
|
|
172
|
+
"""Serialize datetime or date fields to Unix timestamp.
|
|
173
|
+
|
|
174
|
+
Args:
|
|
175
|
+
field_name: The name of the field.
|
|
176
|
+
value: The value of the field.
|
|
177
|
+
|
|
178
|
+
Returns:
|
|
179
|
+
An integer Unix timestamp if the field is a datetime or date.
|
|
180
|
+
"""
|
|
181
|
+
if isinstance(value, (datetime.datetime, datetime.date)):
|
|
182
|
+
return to_unix_timestamp(value)
|
|
183
|
+
if isinstance(value, (list, dict, set, tuple)):
|
|
184
|
+
return pickle.dumps(value)
|
|
185
|
+
return value # Return value as-is for other fields
|
|
186
|
+
|
|
187
|
+
# Deserialization after fetching from the database
|
|
188
|
+
|
|
189
|
+
@classmethod
|
|
190
|
+
def deserialize_field(
|
|
191
|
+
cls,
|
|
192
|
+
field_name: str,
|
|
193
|
+
value: SerializableField,
|
|
194
|
+
*,
|
|
195
|
+
return_local_time: bool,
|
|
196
|
+
) -> object:
|
|
197
|
+
"""Deserialize fields from Unix timestamp to datetime or date.
|
|
198
|
+
|
|
199
|
+
Args:
|
|
200
|
+
field_name: The name of the field being deserialized.
|
|
201
|
+
value: The Unix timestamp value fetched from the database.
|
|
202
|
+
return_local_time: Flag to control whether the datetime is localized
|
|
203
|
+
to the user's timezone.
|
|
204
|
+
|
|
205
|
+
Returns:
|
|
206
|
+
A datetime or date object if the field type is datetime or date,
|
|
207
|
+
otherwise returns the value as-is.
|
|
208
|
+
"""
|
|
209
|
+
if value is None:
|
|
210
|
+
return None
|
|
211
|
+
|
|
212
|
+
# Get field type if it exists in model_fields
|
|
213
|
+
field_info = cls.model_fields.get(field_name)
|
|
214
|
+
if field_info is None:
|
|
215
|
+
# If field doesn't exist in model, return value as-is
|
|
216
|
+
return value
|
|
217
|
+
|
|
218
|
+
field_type = field_info.annotation
|
|
219
|
+
|
|
220
|
+
if (
|
|
221
|
+
isinstance(field_type, type)
|
|
222
|
+
and issubclass(field_type, (datetime.datetime, datetime.date))
|
|
223
|
+
and isinstance(value, int)
|
|
224
|
+
):
|
|
225
|
+
return from_unix_timestamp(
|
|
226
|
+
value, field_type, localize=return_local_time
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
origin_type = get_origin(field_type) or field_type
|
|
230
|
+
if origin_type in (list, dict, set, tuple) and isinstance(value, bytes):
|
|
231
|
+
try:
|
|
232
|
+
return pickle.loads(value)
|
|
233
|
+
except pickle.UnpicklingError:
|
|
234
|
+
return value
|
|
235
|
+
|
|
236
|
+
return value
|
sqliter/model/unique.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""Define a custom field type for unique constraints in SQLiter."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from pydantic.fields import FieldInfo
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Unique(FieldInfo):
|
|
9
|
+
"""A custom field type for unique constraints in SQLiter."""
|
|
10
|
+
|
|
11
|
+
def __init__(self, default: Any = ..., **kwargs: Any) -> None: # noqa: ANN401
|
|
12
|
+
"""Initialize a Unique field.
|
|
13
|
+
|
|
14
|
+
Args:
|
|
15
|
+
default: The default value for the field.
|
|
16
|
+
**kwargs: Additional keyword arguments to pass to FieldInfo.
|
|
17
|
+
"""
|
|
18
|
+
super().__init__(default=default, **kwargs)
|
|
19
|
+
self.unique = True
|
sqliter/py.typed
ADDED
|
File without changes
|
sqliter/query/query.py
CHANGED
|
@@ -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
|
sqliter/sqliter.py
CHANGED
|
@@ -10,12 +10,14 @@ from __future__ import annotations
|
|
|
10
10
|
|
|
11
11
|
import logging
|
|
12
12
|
import sqlite3
|
|
13
|
-
|
|
13
|
+
import time
|
|
14
|
+
from typing import TYPE_CHECKING, Any, Optional, TypeVar, Union
|
|
14
15
|
|
|
15
16
|
from typing_extensions import Self
|
|
16
17
|
|
|
17
18
|
from sqliter.exceptions import (
|
|
18
19
|
DatabaseConnectionError,
|
|
20
|
+
InvalidIndexError,
|
|
19
21
|
RecordDeletionError,
|
|
20
22
|
RecordFetchError,
|
|
21
23
|
RecordInsertionError,
|
|
@@ -26,6 +28,7 @@ from sqliter.exceptions import (
|
|
|
26
28
|
TableDeletionError,
|
|
27
29
|
)
|
|
28
30
|
from sqliter.helpers import infer_sqlite_type
|
|
31
|
+
from sqliter.model.unique import Unique
|
|
29
32
|
from sqliter.query.query import QueryBuilder
|
|
30
33
|
|
|
31
34
|
if TYPE_CHECKING: # pragma: no cover
|
|
@@ -49,7 +52,9 @@ class SqliterDB:
|
|
|
49
52
|
logger (Optional[logging.Logger]): Custom logger for debug output.
|
|
50
53
|
"""
|
|
51
54
|
|
|
52
|
-
|
|
55
|
+
MEMORY_DB = ":memory:"
|
|
56
|
+
|
|
57
|
+
def __init__( # noqa: PLR0913
|
|
53
58
|
self,
|
|
54
59
|
db_filename: Optional[str] = None,
|
|
55
60
|
*,
|
|
@@ -58,6 +63,7 @@ class SqliterDB:
|
|
|
58
63
|
debug: bool = False,
|
|
59
64
|
logger: Optional[logging.Logger] = None,
|
|
60
65
|
reset: bool = False,
|
|
66
|
+
return_local_time: bool = True,
|
|
61
67
|
) -> None:
|
|
62
68
|
"""Initialize a new SqliterDB instance.
|
|
63
69
|
|
|
@@ -69,12 +75,13 @@ class SqliterDB:
|
|
|
69
75
|
logger: Custom logger for debug output.
|
|
70
76
|
reset: Whether to reset the database on initialization. This will
|
|
71
77
|
basically drop all existing tables.
|
|
78
|
+
return_local_time: Whether to return local time for datetime fields.
|
|
72
79
|
|
|
73
80
|
Raises:
|
|
74
81
|
ValueError: If no filename is provided for a non-memory database.
|
|
75
82
|
"""
|
|
76
83
|
if memory:
|
|
77
|
-
self.db_filename =
|
|
84
|
+
self.db_filename = self.MEMORY_DB
|
|
78
85
|
elif db_filename:
|
|
79
86
|
self.db_filename = db_filename
|
|
80
87
|
else:
|
|
@@ -88,6 +95,9 @@ class SqliterDB:
|
|
|
88
95
|
self.logger = logger
|
|
89
96
|
self.conn: Optional[sqlite3.Connection] = None
|
|
90
97
|
self.reset = reset
|
|
98
|
+
self.return_local_time = return_local_time
|
|
99
|
+
|
|
100
|
+
self._in_transaction = False
|
|
91
101
|
|
|
92
102
|
if self.debug:
|
|
93
103
|
self._setup_logger()
|
|
@@ -95,6 +105,54 @@ class SqliterDB:
|
|
|
95
105
|
if self.reset:
|
|
96
106
|
self._reset_database()
|
|
97
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
|
+
|
|
98
156
|
def _reset_database(self) -> None:
|
|
99
157
|
"""Drop all user-created tables in the database."""
|
|
100
158
|
with self.connect() as conn:
|
|
@@ -236,7 +294,12 @@ class SqliterDB:
|
|
|
236
294
|
for field_name, field_info in model_class.model_fields.items():
|
|
237
295
|
if field_name != primary_key:
|
|
238
296
|
sqlite_type = infer_sqlite_type(field_info.annotation)
|
|
239
|
-
|
|
297
|
+
unique_constraint = (
|
|
298
|
+
"UNIQUE" if isinstance(field_info, Unique) else ""
|
|
299
|
+
)
|
|
300
|
+
fields.append(
|
|
301
|
+
f"{field_name} {sqlite_type} {unique_constraint}".strip()
|
|
302
|
+
)
|
|
240
303
|
|
|
241
304
|
create_str = (
|
|
242
305
|
"CREATE TABLE IF NOT EXISTS" if exists_ok else "CREATE TABLE"
|
|
@@ -259,6 +322,65 @@ class SqliterDB:
|
|
|
259
322
|
except sqlite3.Error as exc:
|
|
260
323
|
raise TableCreationError(table_name) from exc
|
|
261
324
|
|
|
325
|
+
# Create regular indexes
|
|
326
|
+
if hasattr(model_class.Meta, "indexes"):
|
|
327
|
+
self._create_indexes(
|
|
328
|
+
model_class, model_class.Meta.indexes, unique=False
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
# Create unique indexes
|
|
332
|
+
if hasattr(model_class.Meta, "unique_indexes"):
|
|
333
|
+
self._create_indexes(
|
|
334
|
+
model_class, model_class.Meta.unique_indexes, unique=True
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
def _create_indexes(
|
|
338
|
+
self,
|
|
339
|
+
model_class: type[BaseDBModel],
|
|
340
|
+
indexes: list[Union[str, tuple[str]]],
|
|
341
|
+
*,
|
|
342
|
+
unique: bool = False,
|
|
343
|
+
) -> None:
|
|
344
|
+
"""Helper method to create regular or unique indexes.
|
|
345
|
+
|
|
346
|
+
Args:
|
|
347
|
+
model_class: The model class defining the table.
|
|
348
|
+
indexes: List of fields or tuples of fields to create indexes for.
|
|
349
|
+
unique: If True, creates UNIQUE indexes; otherwise, creates regular
|
|
350
|
+
indexes.
|
|
351
|
+
|
|
352
|
+
Raises:
|
|
353
|
+
InvalidIndexError: If any fields specified for indexing do not exist
|
|
354
|
+
in the model.
|
|
355
|
+
"""
|
|
356
|
+
valid_fields = set(
|
|
357
|
+
model_class.model_fields.keys()
|
|
358
|
+
) # Get valid fields from the model
|
|
359
|
+
|
|
360
|
+
for index in indexes:
|
|
361
|
+
# Handle multiple fields in tuple form
|
|
362
|
+
fields = list(index) if isinstance(index, tuple) else [index]
|
|
363
|
+
|
|
364
|
+
# Check if all fields exist in the model
|
|
365
|
+
invalid_fields = [
|
|
366
|
+
field for field in fields if field not in valid_fields
|
|
367
|
+
]
|
|
368
|
+
if invalid_fields:
|
|
369
|
+
raise InvalidIndexError(invalid_fields, model_class.__name__)
|
|
370
|
+
|
|
371
|
+
# Build the SQL string
|
|
372
|
+
index_name = "_".join(fields)
|
|
373
|
+
index_postfix = "_unique" if unique else ""
|
|
374
|
+
index_type = " UNIQUE " if unique else " "
|
|
375
|
+
|
|
376
|
+
create_index_sql = (
|
|
377
|
+
f"CREATE{index_type}INDEX IF NOT EXISTS "
|
|
378
|
+
f"idx_{model_class.get_table_name()}"
|
|
379
|
+
f"_{index_name}{index_postfix} "
|
|
380
|
+
f"ON {model_class.get_table_name()} ({', '.join(fields)})"
|
|
381
|
+
)
|
|
382
|
+
self._execute_sql(create_index_sql)
|
|
383
|
+
|
|
262
384
|
def _execute_sql(self, sql: str) -> None:
|
|
263
385
|
"""Execute an SQL statement.
|
|
264
386
|
|
|
@@ -308,14 +430,21 @@ class SqliterDB:
|
|
|
308
430
|
This method is called after operations that modify the database,
|
|
309
431
|
committing changes only if auto_commit is set to True.
|
|
310
432
|
"""
|
|
311
|
-
if self.auto_commit and self.conn:
|
|
433
|
+
if not self._in_transaction and self.auto_commit and self.conn:
|
|
312
434
|
self.conn.commit()
|
|
313
435
|
|
|
314
|
-
def insert(
|
|
436
|
+
def insert(
|
|
437
|
+
self, model_instance: T, *, timestamp_override: bool = False
|
|
438
|
+
) -> T:
|
|
315
439
|
"""Insert a new record into the database.
|
|
316
440
|
|
|
317
441
|
Args:
|
|
318
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.
|
|
319
448
|
|
|
320
449
|
Returns:
|
|
321
450
|
The updated model instance with the primary key (pk) set.
|
|
@@ -326,8 +455,28 @@ class SqliterDB:
|
|
|
326
455
|
model_class = type(model_instance)
|
|
327
456
|
table_name = model_class.get_table_name()
|
|
328
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
|
+
|
|
329
473
|
# Get the data from the model
|
|
330
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
|
+
|
|
331
480
|
# remove the primary key field if it exists, otherwise we'll get
|
|
332
481
|
# TypeErrors as multiple primary keys will exist
|
|
333
482
|
if data.get("pk", None) == 0:
|
|
@@ -354,7 +503,13 @@ class SqliterDB:
|
|
|
354
503
|
raise RecordInsertionError(table_name) from exc
|
|
355
504
|
else:
|
|
356
505
|
data.pop("pk", None)
|
|
357
|
-
|
|
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)
|
|
358
513
|
|
|
359
514
|
def get(
|
|
360
515
|
self, model_class: type[BaseDBModel], primary_key_value: int
|
|
@@ -391,7 +546,17 @@ class SqliterDB:
|
|
|
391
546
|
field: result[idx]
|
|
392
547
|
for idx, field in enumerate(model_class.model_fields)
|
|
393
548
|
}
|
|
394
|
-
|
|
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)
|
|
395
560
|
except sqlite3.Error as exc:
|
|
396
561
|
raise RecordFetchError(table_name) from exc
|
|
397
562
|
else:
|
|
@@ -405,24 +570,27 @@ class SqliterDB:
|
|
|
405
570
|
|
|
406
571
|
Raises:
|
|
407
572
|
RecordUpdateError: If there's an error updating the record or if it
|
|
408
|
-
|
|
573
|
+
is not found.
|
|
409
574
|
"""
|
|
410
575
|
model_class = type(model_instance)
|
|
411
576
|
table_name = model_class.get_table_name()
|
|
412
|
-
|
|
413
577
|
primary_key = model_class.get_primary_key()
|
|
414
578
|
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
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())
|
|
426
594
|
|
|
427
595
|
update_sql = f"""
|
|
428
596
|
UPDATE {table_name}
|
|
@@ -515,6 +683,7 @@ class SqliterDB:
|
|
|
515
683
|
|
|
516
684
|
"""
|
|
517
685
|
self.connect()
|
|
686
|
+
self._in_transaction = True
|
|
518
687
|
return self
|
|
519
688
|
|
|
520
689
|
def __exit__(
|
|
@@ -552,3 +721,4 @@ class SqliterDB:
|
|
|
552
721
|
# Close the connection and reset the instance variable
|
|
553
722
|
self.conn.close()
|
|
554
723
|
self.conn = None
|
|
724
|
+
self._in_transaction = False
|
|
@@ -1,14 +1,10 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: sqliter-py
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.9.0
|
|
4
4
|
Summary: Interact with SQLite databases using Python and Pydantic
|
|
5
|
-
|
|
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
|
|
5
|
+
Author: Grant Ramsay
|
|
9
6
|
Author-email: Grant Ramsay <grant@gnramsay.com>
|
|
10
7
|
License-Expression: MIT
|
|
11
|
-
License-File: LICENSE.txt
|
|
12
8
|
Classifier: Development Status :: 4 - Beta
|
|
13
9
|
Classifier: Intended Audience :: Developers
|
|
14
10
|
Classifier: License :: OSI Approved :: MIT License
|
|
@@ -18,12 +14,19 @@ Classifier: Programming Language :: Python :: 3.9
|
|
|
18
14
|
Classifier: Programming Language :: Python :: 3.10
|
|
19
15
|
Classifier: Programming Language :: Python :: 3.11
|
|
20
16
|
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Topic :: Database :: Front-Ends
|
|
21
19
|
Classifier: Topic :: Software Development
|
|
22
20
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
21
|
+
Requires-Dist: pydantic==2.11.5
|
|
22
|
+
Requires-Dist: inflect==7.0.0 ; extra == 'extras'
|
|
23
23
|
Requires-Python: >=3.9
|
|
24
|
-
|
|
24
|
+
Project-URL: Bug Tracker, https://github.com/seapagan/sqliter-py/issues
|
|
25
|
+
Project-URL: Changelog, https://github.com/seapagan/sqliter-py/blob/main/CHANGELOG.md
|
|
26
|
+
Project-URL: Homepage, http://sqliter.grantramsay.dev
|
|
27
|
+
Project-URL: Pull Requests, https://github.com/seapagan/sqliter-py/pulls
|
|
28
|
+
Project-URL: Repository, https://github.com/seapagan/sqliter-py
|
|
25
29
|
Provides-Extra: extras
|
|
26
|
-
Requires-Dist: inflect==7.0.0; extra == 'extras'
|
|
27
30
|
Description-Content-Type: text/markdown
|
|
28
31
|
|
|
29
32
|
# SQLiter <!-- omit in toc -->
|
|
@@ -47,22 +50,19 @@ 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]
|
|
56
|
+
>
|
|
57
|
+
> Currently NOT compatible with Python 3.14 (I need to refactor some code d/t
|
|
58
|
+
> changes in the latest Pydantic versions, this is a priority.)
|
|
59
|
+
>
|
|
54
60
|
> This project is still in the early stages of development and is lacking some
|
|
55
61
|
> planned functionality. Please use with caution - Classes and methods may
|
|
56
62
|
> change until a stable release is made. I'll try to keep this to an absolute
|
|
57
63
|
> minimum and the releases and documentation will be very clear about any
|
|
58
64
|
> breaking changes.
|
|
59
65
|
>
|
|
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
66
|
> See the [TODO](TODO.md) for planned features and improvements.
|
|
67
67
|
|
|
68
68
|
- [Features](#features)
|
|
@@ -75,6 +75,12 @@ Website](https://sqliter.grantramsay.dev)
|
|
|
75
75
|
## Features
|
|
76
76
|
|
|
77
77
|
- Table creation based on Pydantic models
|
|
78
|
+
- Supports `date` and `datetime` fields
|
|
79
|
+
- Support for complex data types (`list`, `dict`, `set`, `tuple`) stored as
|
|
80
|
+
BLOBs
|
|
81
|
+
- Automatic primary key generation
|
|
82
|
+
- User defined indexes on any field
|
|
83
|
+
- Set any field as UNIQUE
|
|
78
84
|
- CRUD operations (Create, Read, Update, Delete)
|
|
79
85
|
- Chained Query building with filtering, ordering, and pagination
|
|
80
86
|
- Transaction support
|
|
@@ -98,16 +104,16 @@ virtual environments (`uv` is used for developing this project and in the CI):
|
|
|
98
104
|
uv add sqliter-py
|
|
99
105
|
```
|
|
100
106
|
|
|
101
|
-
With `
|
|
107
|
+
With `Poetry`:
|
|
102
108
|
|
|
103
109
|
```bash
|
|
104
|
-
|
|
110
|
+
poetry add sqliter-py
|
|
105
111
|
```
|
|
106
112
|
|
|
107
|
-
Or with `
|
|
113
|
+
Or with `pip`:
|
|
108
114
|
|
|
109
115
|
```bash
|
|
110
|
-
|
|
116
|
+
pip install sqliter-py
|
|
111
117
|
```
|
|
112
118
|
|
|
113
119
|
### Optional Dependencies
|
|
@@ -116,9 +122,9 @@ Currently by default, the only external dependency is Pydantic. However, there
|
|
|
116
122
|
are some optional dependencies that can be installed to enable additional
|
|
117
123
|
features:
|
|
118
124
|
|
|
119
|
-
- `inflect`: For pluralizing table names (if not
|
|
120
|
-
|
|
121
|
-
will not need this.
|
|
125
|
+
- `inflect`: For pluralizing the auto-generated table names (if not explicitly
|
|
126
|
+
set in the Model) This just offers a more-advanced pluralization than the
|
|
127
|
+
default method used. In most cases you will not need this.
|
|
122
128
|
|
|
123
129
|
See [Installing Optional
|
|
124
130
|
Dependencies](https://sqliter.grantramsay.dev/installation#optional-dependencies)
|
|
@@ -156,12 +162,16 @@ for user in results:
|
|
|
156
162
|
new_user.age = 31
|
|
157
163
|
db.update(new_user)
|
|
158
164
|
|
|
159
|
-
# Delete a record
|
|
165
|
+
# Delete a record by primary key
|
|
160
166
|
db.delete(User, new_user.pk)
|
|
167
|
+
|
|
168
|
+
# Delete all records returned from a query:
|
|
169
|
+
delete_count = db.select(User).filter(age__gt=30).delete()
|
|
161
170
|
```
|
|
162
171
|
|
|
163
|
-
See the [
|
|
164
|
-
for more detailed information on how to use SQLiter, and advanced
|
|
172
|
+
See the [Guide](https://sqliter.grantramsay.dev/guide/guide/) section of the
|
|
173
|
+
documentation for more detailed information on how to use SQLiter, and advanced
|
|
174
|
+
features.
|
|
165
175
|
|
|
166
176
|
## Contributing
|
|
167
177
|
|
|
@@ -177,7 +187,7 @@ which you can read in the [CODE_OF_CONDUCT](CODE_OF_CONDUCT.md) file.
|
|
|
177
187
|
This project is licensed under the MIT License.
|
|
178
188
|
|
|
179
189
|
```pre
|
|
180
|
-
Copyright (c) 2024 Grant Ramsay
|
|
190
|
+
Copyright (c) 2024-2025 Grant Ramsay
|
|
181
191
|
|
|
182
192
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
183
193
|
of this software and associated documentation files (the "Software"), to deal
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
sqliter/__init__.py,sha256=ECfn02OPmiMCQvRYbfizKFhVDk00xV-HV1s2cH9037I,244
|
|
2
|
+
sqliter/constants.py,sha256=pNKalajchG6YvXw0-lTMKOJ_OL2xPSs_3YuOuVsqkVk,1257
|
|
3
|
+
sqliter/exceptions.py,sha256=g-YZaPazBxlYxQ0lVP_MCWC-mQlX_qgyWip5LJCVLV4,5654
|
|
4
|
+
sqliter/helpers.py,sha256=d6k4oWl43Se_c8rAmX8Zj0_sf2O-bpaaeu33VN2IcsI,3442
|
|
5
|
+
sqliter/model/__init__.py,sha256=sBp6HcDZdespSYmA4sCXq9OikCFTNdFFkT-wM3JJ2Mw,396
|
|
6
|
+
sqliter/model/model.py,sha256=UOD5yO5CizHFWye_BlG28keSK6qWRdHQVI79Y7ovQ9o,8058
|
|
7
|
+
sqliter/model/unique.py,sha256=r4CXrW1GXB6OgNoOVDTpTEVZl8_zta3mYbbT5quMc-c,578
|
|
8
|
+
sqliter/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
9
|
+
sqliter/query/__init__.py,sha256=MRajhjTPJqjbmmrwndVKj8vqMbK5-XufpwoIswQf5z4,239
|
|
10
|
+
sqliter/query/query.py,sha256=yQjYtfSWxgEvbxQgQgwpjS6Z9tSFTQ2aISSKoQ9kaGM,25743
|
|
11
|
+
sqliter/sqliter.py,sha256=lPSvBrFrszCGT47CNPssmp4G7NiFccpdas4kpKZQmO0,25173
|
|
12
|
+
sqliter_py-0.9.0.dist-info/WHEEL,sha256=93kfTGt3a0Dykt_T-gsjtyS5_p8F_d6CE1NwmBOirzo,79
|
|
13
|
+
sqliter_py-0.9.0.dist-info/METADATA,sha256=2Rd3ABHO8IG82IZQOlryAOMoSzxYB-D_pPFmrR3SiwA,7566
|
|
14
|
+
sqliter_py-0.9.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=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,,
|
sqliter_py-0.5.0.dist-info/WHEEL
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
The MIT License (MIT)
|
|
2
|
-
Copyright (c) 2024 Grant Ramsay
|
|
3
|
-
|
|
4
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
5
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
6
|
-
in the Software without restriction, including without limitation the rights
|
|
7
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
8
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
9
|
-
furnished to do so, subject to the following conditions:
|
|
10
|
-
|
|
11
|
-
The above copyright notice and this permission notice shall be included in all
|
|
12
|
-
copies or substantial portions of the Software.
|
|
13
|
-
|
|
14
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
15
|
-
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
16
|
-
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
|
17
|
-
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
|
18
|
-
DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
|
19
|
-
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
|
|
20
|
-
OR OTHER DEALINGS IN THE SOFTWARE.
|