sqliter-py 0.3.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/__init__.py CHANGED
@@ -1,4 +1,8 @@
1
- """The 'sqliter' package provides an minimal ORM for the SQLite library."""
1
+ """SQLiter: A lightweight ORM-like library for SQLite databases in Python.
2
+
3
+ This module provides the main SqliterDB class for interacting with
4
+ SQLite databases using Pydantic models.
5
+ """
2
6
 
3
7
  from .sqliter import SqliterDB
4
8
 
sqliter/constants.py CHANGED
@@ -1,5 +1,15 @@
1
- """Define constants used in the library."""
1
+ """Constant values and mappings used throughout SQLiter.
2
2
 
3
+ This module defines constant dictionaries that map SQLiter-specific
4
+ concepts to their SQLite equivalents. It includes mappings for query
5
+ operators and data types, which are crucial for translating between
6
+ Pydantic models and SQLite database operations.
7
+ """
8
+
9
+ import datetime
10
+
11
+ # A dictionary mapping SQLiter filter operators to their corresponding SQL
12
+ # operators.
3
13
  OPERATOR_MAPPING = {
4
14
  "__lt": "<",
5
15
  "__lte": "<=",
@@ -18,3 +28,18 @@ OPERATOR_MAPPING = {
18
28
  "__iendswith": "LIKE",
19
29
  "__icontains": "LIKE",
20
30
  }
31
+
32
+ # A dictionary mapping Python types to their corresponding SQLite column types.
33
+ SQLITE_TYPE_MAPPING = {
34
+ int: "INTEGER",
35
+ float: "REAL",
36
+ str: "TEXT",
37
+ bool: "INTEGER", # SQLite stores booleans as integers (0 or 1)
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",
45
+ }
sqliter/exceptions.py CHANGED
@@ -1,4 +1,11 @@
1
- """Define custom exceptions for the sqliter package."""
1
+ """Custom exception classes for SQLiter error handling.
2
+
3
+ This module defines a hierarchy of exception classes specific to
4
+ SQLiter operations. These exceptions provide detailed error information
5
+ for various scenarios such as connection issues, invalid queries,
6
+ and CRUD operation failures, enabling more precise error handling
7
+ in applications using SQLiter.
8
+ """
2
9
 
3
10
  import os
4
11
  import sys
@@ -6,7 +13,15 @@ import traceback
6
13
 
7
14
 
8
15
  class SqliterError(Exception):
9
- """Base class for all exceptions raised by the sqliter package."""
16
+ """Base exception class for all SQLiter-specific errors.
17
+
18
+ This class serves as the parent for all custom exceptions in SQLiter,
19
+ providing a consistent interface and message formatting.
20
+
21
+ Attributes:
22
+ message_template (str): A template string for the error message.
23
+ original_exception (Exception): The original exception that was caught.
24
+ """
10
25
 
11
26
  message_template: str = "An error occurred in the SQLiter package."
12
27
 
@@ -59,13 +74,13 @@ class SqliterError(Exception):
59
74
 
60
75
 
61
76
  class DatabaseConnectionError(SqliterError):
62
- """Raised when the SQLite database connection fails."""
77
+ """Exception raised when a database connection cannot be established."""
63
78
 
64
79
  message_template = "Failed to connect to the database: '{}'"
65
80
 
66
81
 
67
82
  class InvalidOffsetError(SqliterError):
68
- """Raised when an invalid offset value (0 or negative) is used."""
83
+ """Exception raised when an invalid offset value is provided."""
69
84
 
70
85
  message_template = (
71
86
  "Invalid offset value: '{}'. Offset must be a positive integer."
@@ -73,48 +88,81 @@ class InvalidOffsetError(SqliterError):
73
88
 
74
89
 
75
90
  class InvalidOrderError(SqliterError):
76
- """Raised when an invalid order value is used."""
91
+ """Exception raised when an invalid order specification is provided."""
77
92
 
78
93
  message_template = "Invalid order value - {}"
79
94
 
80
95
 
81
96
  class TableCreationError(SqliterError):
82
- """Raised when a table cannot be created in the database."""
97
+ """Exception raised when a table cannot be created in the database."""
83
98
 
84
99
  message_template = "Failed to create the table: '{}'"
85
100
 
86
101
 
87
102
  class RecordInsertionError(SqliterError):
88
- """Raised when an error occurs during record insertion."""
103
+ """Exception raised when a record cannot be inserted into the database."""
89
104
 
90
105
  message_template = "Failed to insert record into table: '{}'"
91
106
 
92
107
 
93
108
  class RecordUpdateError(SqliterError):
94
- """Raised when an error occurs during record update."""
109
+ """Exception raised when a record cannot be updated in the database."""
95
110
 
96
111
  message_template = "Failed to update record in table: '{}'"
97
112
 
98
113
 
99
114
  class RecordNotFoundError(SqliterError):
100
- """Raised when a record with the specified primary key is not found."""
115
+ """Exception raised when a requested record is not found in the database."""
101
116
 
102
- message_template = "Failed to find a record for key '{}' "
117
+ message_template = "Failed to find that record in the table (key '{}') "
103
118
 
104
119
 
105
120
  class RecordFetchError(SqliterError):
106
- """Raised when an error occurs during record fetching."""
121
+ """Exception raised on an error fetching records from the database."""
107
122
 
108
123
  message_template = "Failed to fetch record from table: '{}'"
109
124
 
110
125
 
111
126
  class RecordDeletionError(SqliterError):
112
- """Raised when an error occurs during record deletion."""
127
+ """Exception raised when a record cannot be deleted from the database."""
113
128
 
114
129
  message_template = "Failed to delete record from table: '{}'"
115
130
 
116
131
 
117
132
  class InvalidFilterError(SqliterError):
118
- """Raised when an invalid filter field is used in a query."""
133
+ """Exception raised when an invalid filter is applied to a query."""
119
134
 
120
135
  message_template = "Failed to apply filter: invalid field '{}'"
136
+
137
+
138
+ class TableDeletionError(SqliterError):
139
+ """Raised when a table cannot be deleted from the database."""
140
+
141
+ message_template = "Failed to delete the table: '{}'"
142
+
143
+
144
+ class SqlExecutionError(SqliterError):
145
+ """Raised when an SQL execution fails."""
146
+
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 ADDED
@@ -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)
sqliter/model/__init__.py CHANGED
@@ -1,8 +1,11 @@
1
- """This module defines the BaseDBModel class.
1
+ """This module provides the base model class for SQLiter database models.
2
2
 
3
- This should be subclassed by the user to interact with the database.
3
+ It exports the BaseDBModel class, which is used to define database
4
+ models in SQLiter applications, and the Unique class, which is used to
5
+ define unique constraints on model fields.
4
6
  """
5
7
 
6
- from .model import BaseDBModel
8
+ from .model import BaseDBModel, SerializableField
9
+ from .unique import Unique
7
10
 
8
- __all__ = ["BaseDBModel"]
11
+ __all__ = ["BaseDBModel", "SerializableField", "Unique"]
sqliter/model/model.py CHANGED
@@ -1,39 +1,102 @@
1
- """Define the Base model class."""
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
+ """
2
9
 
3
10
  from __future__ import annotations
4
11
 
12
+ import datetime
13
+ import pickle
5
14
  import re
6
- from typing import Any, Optional, TypeVar, Union, get_args, get_origin
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
7
28
 
8
- from pydantic import BaseModel, ConfigDict
29
+ from sqliter.helpers import from_unix_timestamp, to_unix_timestamp
9
30
 
10
- T = TypeVar("T", bound="BaseDBModel")
31
+
32
+ class SerializableField(Protocol):
33
+ """Protocol for fields that can be serialized or deserialized."""
11
34
 
12
35
 
13
36
  class BaseDBModel(BaseModel):
14
- """Custom base model for database models."""
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
+ )
15
56
 
16
57
  model_config = ConfigDict(
17
58
  extra="ignore",
18
59
  populate_by_name=True,
19
- validate_assignment=False,
60
+ validate_assignment=True,
20
61
  from_attributes=True,
21
62
  )
22
63
 
23
64
  class Meta:
24
- """Configure the base model with default options."""
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
+ """
25
80
 
26
- create_pk: bool = (
27
- True # Whether to create an auto-increment primary key
28
- )
29
- primary_key: str = "id" # Default primary key name
30
81
  table_name: Optional[str] = (
31
82
  None # Table name, defaults to class name if not set
32
83
  )
84
+ indexes: ClassVar[list[Union[str, tuple[str]]]] = []
85
+ unique_indexes: ClassVar[list[Union[str, tuple[str]]]] = []
33
86
 
34
87
  @classmethod
35
- def model_validate_partial(cls: type[T], obj: dict[str, Any]) -> T:
36
- """Validate a partial model object."""
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
+ """
37
100
  converted_obj: dict[str, Any] = {}
38
101
  for field_name, value in obj.items():
39
102
  field = cls.model_fields[field_name]
@@ -58,16 +121,17 @@ class BaseDBModel(BaseModel):
58
121
  else:
59
122
  converted_obj[field_name] = field_type(value)
60
123
 
61
- return cls.model_construct(**converted_obj)
124
+ return cast("Self", cls.model_construct(**converted_obj))
62
125
 
63
126
  @classmethod
64
127
  def get_table_name(cls) -> str:
65
- """Get the table name from the Meta, or generate one.
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.
66
132
 
67
133
  Returns:
68
- str: The table name, either specified in the Meta class or
69
- generated by converting the class name to pluralized snake_case
70
- and removing any 'Model' suffix.
134
+ The name of the database table for this model.
71
135
  """
72
136
  table_name: str | None = getattr(cls.Meta, "table_name", None)
73
137
  if table_name is not None:
@@ -81,7 +145,7 @@ class BaseDBModel(BaseModel):
81
145
 
82
146
  # Pluralize the table name
83
147
  try:
84
- import inflect
148
+ import inflect # noqa: PLC0415
85
149
 
86
150
  p = inflect.engine()
87
151
  return p.plural(snake_case_name)
@@ -95,10 +159,78 @@ class BaseDBModel(BaseModel):
95
159
 
96
160
  @classmethod
97
161
  def get_primary_key(cls) -> str:
98
- """Get the primary key from the Meta class or default to 'id'."""
99
- return getattr(cls.Meta, "primary_key", "id")
162
+ """Returns the mandatory primary key, always 'pk'."""
163
+ return "pk"
100
164
 
101
165
  @classmethod
102
166
  def should_create_pk(cls) -> bool:
103
- """Check whether the model should create an auto-increment ID."""
104
- return getattr(cls.Meta, "create_pk", True)
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,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/__init__.py CHANGED
@@ -1,4 +1,8 @@
1
- """Define the 'QueryBuilder' class for building SQL queries."""
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
+ """
2
6
 
3
7
  from .query import QueryBuilder
4
8