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/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
@@ -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
@@ -0,0 +1,9 @@
1
+ """This module provides the query building functionality for SQLiter.
2
+
3
+ It exports the QueryBuilder class, which is used to construct and
4
+ execute database queries in SQLiter.
5
+ """
6
+
7
+ from .query import QueryBuilder
8
+
9
+ __all__ = ["QueryBuilder"]