sqliter-py 0.12.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 +9 -0
- sqliter/constants.py +45 -0
- sqliter/exceptions.py +198 -0
- sqliter/helpers.py +100 -0
- sqliter/model/__init__.py +46 -0
- sqliter/model/foreign_key.py +153 -0
- sqliter/model/model.py +236 -0
- sqliter/model/unique.py +28 -0
- sqliter/py.typed +0 -0
- sqliter/query/__init__.py +9 -0
- sqliter/query/query.py +891 -0
- sqliter/sqliter.py +1087 -0
- sqliter_py-0.12.0.dist-info/METADATA +209 -0
- sqliter_py-0.12.0.dist-info/RECORD +15 -0
- sqliter_py-0.12.0.dist-info/WHEEL +4 -0
sqliter/model/model.py
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
"""Defines the base model class for SQLiter ORM functionality.
|
|
2
|
+
|
|
3
|
+
This module provides the BaseDBModel class, which extends Pydantic's
|
|
4
|
+
BaseModel to add SQLiter-specific functionality. It includes methods
|
|
5
|
+
for table name inference, primary key management, and partial model
|
|
6
|
+
validation, forming the foundation for defining database-mapped models
|
|
7
|
+
in SQLiter applications.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import datetime
|
|
13
|
+
import pickle
|
|
14
|
+
import re
|
|
15
|
+
from typing import (
|
|
16
|
+
Any,
|
|
17
|
+
ClassVar,
|
|
18
|
+
Optional,
|
|
19
|
+
Protocol,
|
|
20
|
+
Union,
|
|
21
|
+
cast,
|
|
22
|
+
get_args,
|
|
23
|
+
get_origin,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
27
|
+
from typing_extensions import Self
|
|
28
|
+
|
|
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."""
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class BaseDBModel(BaseModel):
|
|
37
|
+
"""Base model class for SQLiter database models.
|
|
38
|
+
|
|
39
|
+
This class extends Pydantic's BaseModel to provide additional functionality
|
|
40
|
+
for database operations. It includes configuration options and methods
|
|
41
|
+
specific to SQLiter's ORM-like functionality.
|
|
42
|
+
|
|
43
|
+
This should not be used directly, but should be inherited by subclasses
|
|
44
|
+
representing database models.
|
|
45
|
+
"""
|
|
46
|
+
|
|
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
|
+
)
|
|
56
|
+
|
|
57
|
+
model_config = ConfigDict(
|
|
58
|
+
extra="ignore",
|
|
59
|
+
populate_by_name=True,
|
|
60
|
+
validate_assignment=True,
|
|
61
|
+
from_attributes=True,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
class Meta:
|
|
65
|
+
"""Metadata class for configuring database-specific attributes.
|
|
66
|
+
|
|
67
|
+
Attributes:
|
|
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.
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
table_name: Optional[str] = (
|
|
82
|
+
None # Table name, defaults to class name if not set
|
|
83
|
+
)
|
|
84
|
+
indexes: ClassVar[list[Union[str, tuple[str]]]] = []
|
|
85
|
+
unique_indexes: ClassVar[list[Union[str, tuple[str]]]] = []
|
|
86
|
+
|
|
87
|
+
@classmethod
|
|
88
|
+
def model_validate_partial(cls, obj: dict[str, Any]) -> Self:
|
|
89
|
+
"""Validate and create a model instance from partial data.
|
|
90
|
+
|
|
91
|
+
This method allows for the creation of a model instance even when
|
|
92
|
+
not all fields are present in the input data.
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
obj: A dictionary of field names and values.
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
An instance of the model class with the provided data.
|
|
99
|
+
"""
|
|
100
|
+
converted_obj: dict[str, Any] = {}
|
|
101
|
+
for field_name, value in obj.items():
|
|
102
|
+
field = cls.model_fields[field_name]
|
|
103
|
+
field_type: Optional[type] = field.annotation
|
|
104
|
+
if (
|
|
105
|
+
field_type is None or value is None
|
|
106
|
+
): # Direct check for None values here
|
|
107
|
+
converted_obj[field_name] = None
|
|
108
|
+
else:
|
|
109
|
+
origin = get_origin(field_type)
|
|
110
|
+
if origin is Union:
|
|
111
|
+
args = get_args(field_type)
|
|
112
|
+
for arg in args:
|
|
113
|
+
try:
|
|
114
|
+
# Try converting the value to the type
|
|
115
|
+
converted_obj[field_name] = arg(value)
|
|
116
|
+
break
|
|
117
|
+
except (ValueError, TypeError):
|
|
118
|
+
pass
|
|
119
|
+
else:
|
|
120
|
+
converted_obj[field_name] = value
|
|
121
|
+
else:
|
|
122
|
+
converted_obj[field_name] = field_type(value)
|
|
123
|
+
|
|
124
|
+
return cast("Self", cls.model_construct(**converted_obj))
|
|
125
|
+
|
|
126
|
+
@classmethod
|
|
127
|
+
def get_table_name(cls) -> str:
|
|
128
|
+
"""Get the database table name for the model.
|
|
129
|
+
|
|
130
|
+
This method determines the table name based on the Meta configuration
|
|
131
|
+
or derives it from the class name if not explicitly set.
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
The name of the database table for this model.
|
|
135
|
+
"""
|
|
136
|
+
table_name: str | None = getattr(cls.Meta, "table_name", None)
|
|
137
|
+
if table_name is not None:
|
|
138
|
+
return table_name
|
|
139
|
+
|
|
140
|
+
# Get class name and remove 'Model' suffix if present
|
|
141
|
+
class_name = cls.__name__.removesuffix("Model")
|
|
142
|
+
|
|
143
|
+
# Convert to snake_case
|
|
144
|
+
snake_case_name = re.sub(r"(?<!^)(?=[A-Z])", "_", class_name).lower()
|
|
145
|
+
|
|
146
|
+
# Pluralize the table name
|
|
147
|
+
try:
|
|
148
|
+
import inflect # noqa: PLC0415
|
|
149
|
+
|
|
150
|
+
p = inflect.engine()
|
|
151
|
+
return p.plural(snake_case_name)
|
|
152
|
+
except ImportError:
|
|
153
|
+
# Fallback to simple pluralization by adding 's'
|
|
154
|
+
return (
|
|
155
|
+
snake_case_name
|
|
156
|
+
if snake_case_name.endswith("s")
|
|
157
|
+
else snake_case_name + "s"
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
@classmethod
|
|
161
|
+
def get_primary_key(cls) -> str:
|
|
162
|
+
"""Returns the mandatory primary key, always 'pk'."""
|
|
163
|
+
return "pk"
|
|
164
|
+
|
|
165
|
+
@classmethod
|
|
166
|
+
def should_create_pk(cls) -> bool:
|
|
167
|
+
"""Returns True since the primary key is always created."""
|
|
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,28 @@
|
|
|
1
|
+
"""Define a custom field type for unique constraints in SQLiter."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from pydantic import Field
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def unique(default: Any = ..., **kwargs: Any) -> Any: # noqa: ANN401
|
|
9
|
+
"""A custom field type for unique constraints in SQLiter.
|
|
10
|
+
|
|
11
|
+
Args:
|
|
12
|
+
default: The default value for the field.
|
|
13
|
+
**kwargs: Additional keyword arguments to pass to Field.
|
|
14
|
+
|
|
15
|
+
Returns:
|
|
16
|
+
A Field with unique metadata attached.
|
|
17
|
+
"""
|
|
18
|
+
# Extract any existing json_schema_extra from kwargs
|
|
19
|
+
existing_extra = kwargs.pop("json_schema_extra", {})
|
|
20
|
+
|
|
21
|
+
# Ensure it's a dict
|
|
22
|
+
if not isinstance(existing_extra, dict):
|
|
23
|
+
existing_extra = {}
|
|
24
|
+
|
|
25
|
+
# Add our unique marker to json_schema_extra
|
|
26
|
+
existing_extra["unique"] = True
|
|
27
|
+
|
|
28
|
+
return Field(default=default, json_schema_extra=existing_extra, **kwargs)
|
sqliter/py.typed
ADDED
|
File without changes
|