sqliter-py 0.5.0__tar.gz → 0.6.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.5.0 → sqliter_py-0.6.0}/PKG-INFO +11 -8
- {sqliter_py-0.5.0 → sqliter_py-0.6.0}/README.md +10 -7
- {sqliter_py-0.5.0 → sqliter_py-0.6.0}/pyproject.toml +1 -1
- {sqliter_py-0.5.0 → sqliter_py-0.6.0}/sqliter/exceptions.py +21 -0
- {sqliter_py-0.5.0 → sqliter_py-0.6.0}/sqliter/model/__init__.py +4 -2
- {sqliter_py-0.5.0 → sqliter_py-0.6.0}/sqliter/model/model.py +23 -4
- sqliter_py-0.6.0/sqliter/model/unique.py +19 -0
- {sqliter_py-0.5.0 → sqliter_py-0.6.0}/sqliter/sqliter.py +73 -3
- {sqliter_py-0.5.0 → sqliter_py-0.6.0}/.gitignore +0 -0
- {sqliter_py-0.5.0 → sqliter_py-0.6.0}/LICENSE.txt +0 -0
- {sqliter_py-0.5.0 → sqliter_py-0.6.0}/sqliter/__init__.py +0 -0
- {sqliter_py-0.5.0 → sqliter_py-0.6.0}/sqliter/constants.py +0 -0
- {sqliter_py-0.5.0 → sqliter_py-0.6.0}/sqliter/helpers.py +0 -0
- {sqliter_py-0.5.0 → sqliter_py-0.6.0}/sqliter/query/__init__.py +0 -0
- {sqliter_py-0.5.0 → sqliter_py-0.6.0}/sqliter/query/query.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: sqliter-py
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.0
|
|
4
4
|
Summary: Interact with SQLite databases using Python and Pydantic
|
|
5
5
|
Project-URL: Pull Requests, https://github.com/seapagan/sqliter-py/pulls
|
|
6
6
|
Project-URL: Bug Tracker, https://github.com/seapagan/sqliter-py/issues
|
|
@@ -75,6 +75,9 @@ Website](https://sqliter.grantramsay.dev)
|
|
|
75
75
|
## Features
|
|
76
76
|
|
|
77
77
|
- Table creation based on Pydantic models
|
|
78
|
+
- Automatic primary key generation
|
|
79
|
+
- User defined indexes on any field
|
|
80
|
+
- Set any field as UNIQUE
|
|
78
81
|
- CRUD operations (Create, Read, Update, Delete)
|
|
79
82
|
- Chained Query building with filtering, ordering, and pagination
|
|
80
83
|
- Transaction support
|
|
@@ -98,16 +101,16 @@ virtual environments (`uv` is used for developing this project and in the CI):
|
|
|
98
101
|
uv add sqliter-py
|
|
99
102
|
```
|
|
100
103
|
|
|
101
|
-
With `
|
|
104
|
+
With `Poetry`:
|
|
102
105
|
|
|
103
106
|
```bash
|
|
104
|
-
|
|
107
|
+
poetry add sqliter-py
|
|
105
108
|
```
|
|
106
109
|
|
|
107
|
-
Or with `
|
|
110
|
+
Or with `pip`:
|
|
108
111
|
|
|
109
112
|
```bash
|
|
110
|
-
|
|
113
|
+
pip install sqliter-py
|
|
111
114
|
```
|
|
112
115
|
|
|
113
116
|
### Optional Dependencies
|
|
@@ -116,9 +119,9 @@ Currently by default, the only external dependency is Pydantic. However, there
|
|
|
116
119
|
are some optional dependencies that can be installed to enable additional
|
|
117
120
|
features:
|
|
118
121
|
|
|
119
|
-
- `inflect`: For pluralizing table names (if not
|
|
120
|
-
|
|
121
|
-
will not need this.
|
|
122
|
+
- `inflect`: For pluralizing the auto-generated table names (if not explicitly
|
|
123
|
+
set in the Model) This just offers a more-advanced pluralization than the
|
|
124
|
+
default method used. In most cases you will not need this.
|
|
122
125
|
|
|
123
126
|
See [Installing Optional
|
|
124
127
|
Dependencies](https://sqliter.grantramsay.dev/installation#optional-dependencies)
|
|
@@ -47,6 +47,9 @@ Website](https://sqliter.grantramsay.dev)
|
|
|
47
47
|
## Features
|
|
48
48
|
|
|
49
49
|
- Table creation based on Pydantic models
|
|
50
|
+
- Automatic primary key generation
|
|
51
|
+
- User defined indexes on any field
|
|
52
|
+
- Set any field as UNIQUE
|
|
50
53
|
- CRUD operations (Create, Read, Update, Delete)
|
|
51
54
|
- Chained Query building with filtering, ordering, and pagination
|
|
52
55
|
- Transaction support
|
|
@@ -70,16 +73,16 @@ virtual environments (`uv` is used for developing this project and in the CI):
|
|
|
70
73
|
uv add sqliter-py
|
|
71
74
|
```
|
|
72
75
|
|
|
73
|
-
With `
|
|
76
|
+
With `Poetry`:
|
|
74
77
|
|
|
75
78
|
```bash
|
|
76
|
-
|
|
79
|
+
poetry add sqliter-py
|
|
77
80
|
```
|
|
78
81
|
|
|
79
|
-
Or with `
|
|
82
|
+
Or with `pip`:
|
|
80
83
|
|
|
81
84
|
```bash
|
|
82
|
-
|
|
85
|
+
pip install sqliter-py
|
|
83
86
|
```
|
|
84
87
|
|
|
85
88
|
### Optional Dependencies
|
|
@@ -88,9 +91,9 @@ Currently by default, the only external dependency is Pydantic. However, there
|
|
|
88
91
|
are some optional dependencies that can be installed to enable additional
|
|
89
92
|
features:
|
|
90
93
|
|
|
91
|
-
- `inflect`: For pluralizing table names (if not
|
|
92
|
-
|
|
93
|
-
will not need this.
|
|
94
|
+
- `inflect`: For pluralizing the auto-generated table names (if not explicitly
|
|
95
|
+
set in the Model) This just offers a more-advanced pluralization than the
|
|
96
|
+
default method used. In most cases you will not need this.
|
|
94
97
|
|
|
95
98
|
See [Installing Optional
|
|
96
99
|
Dependencies](https://sqliter.grantramsay.dev/installation#optional-dependencies)
|
|
@@ -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)
|
|
@@ -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
8
|
from .model import BaseDBModel
|
|
9
|
+
from .unique import Unique
|
|
8
10
|
|
|
9
|
-
__all__ = ["BaseDBModel"]
|
|
11
|
+
__all__ = ["BaseDBModel", "Unique"]
|
|
@@ -10,7 +10,16 @@ in SQLiter applications.
|
|
|
10
10
|
from __future__ import annotations
|
|
11
11
|
|
|
12
12
|
import re
|
|
13
|
-
from typing import
|
|
13
|
+
from typing import (
|
|
14
|
+
Any,
|
|
15
|
+
ClassVar,
|
|
16
|
+
Optional,
|
|
17
|
+
TypeVar,
|
|
18
|
+
Union,
|
|
19
|
+
cast,
|
|
20
|
+
get_args,
|
|
21
|
+
get_origin,
|
|
22
|
+
)
|
|
14
23
|
|
|
15
24
|
from pydantic import BaseModel, ConfigDict, Field
|
|
16
25
|
|
|
@@ -41,14 +50,24 @@ class BaseDBModel(BaseModel):
|
|
|
41
50
|
"""Metadata class for configuring database-specific attributes.
|
|
42
51
|
|
|
43
52
|
Attributes:
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
53
|
+
table_name (Optional[str]): The name of the database table. If not
|
|
54
|
+
specified, the table name will be inferred from the model class
|
|
55
|
+
name and converted to snake_case.
|
|
56
|
+
indexes (ClassVar[list[Union[str, tuple[str]]]]): A list of fields
|
|
57
|
+
or tuples of fields for which regular (non-unique) indexes
|
|
58
|
+
should be created. Indexes improve query performance on these
|
|
59
|
+
fields.
|
|
60
|
+
unique_indexes (ClassVar[list[Union[str, tuple[str]]]]): A list of
|
|
61
|
+
fields or tuples of fields for which unique indexes should be
|
|
62
|
+
created. Unique indexes enforce that all values in these fields
|
|
63
|
+
are distinct across the table.
|
|
47
64
|
"""
|
|
48
65
|
|
|
49
66
|
table_name: Optional[str] = (
|
|
50
67
|
None # Table name, defaults to class name if not set
|
|
51
68
|
)
|
|
69
|
+
indexes: ClassVar[list[Union[str, tuple[str]]]] = []
|
|
70
|
+
unique_indexes: ClassVar[list[Union[str, tuple[str]]]] = []
|
|
52
71
|
|
|
53
72
|
@classmethod
|
|
54
73
|
def model_validate_partial(cls: type[T], obj: dict[str, Any]) -> T:
|
|
@@ -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
|
|
@@ -10,12 +10,13 @@ from __future__ import annotations
|
|
|
10
10
|
|
|
11
11
|
import logging
|
|
12
12
|
import sqlite3
|
|
13
|
-
from typing import TYPE_CHECKING, Any, Optional, TypeVar
|
|
13
|
+
from typing import TYPE_CHECKING, Any, Optional, TypeVar, Union
|
|
14
14
|
|
|
15
15
|
from typing_extensions import Self
|
|
16
16
|
|
|
17
17
|
from sqliter.exceptions import (
|
|
18
18
|
DatabaseConnectionError,
|
|
19
|
+
InvalidIndexError,
|
|
19
20
|
RecordDeletionError,
|
|
20
21
|
RecordFetchError,
|
|
21
22
|
RecordInsertionError,
|
|
@@ -26,6 +27,7 @@ from sqliter.exceptions import (
|
|
|
26
27
|
TableDeletionError,
|
|
27
28
|
)
|
|
28
29
|
from sqliter.helpers import infer_sqlite_type
|
|
30
|
+
from sqliter.model.unique import Unique
|
|
29
31
|
from sqliter.query.query import QueryBuilder
|
|
30
32
|
|
|
31
33
|
if TYPE_CHECKING: # pragma: no cover
|
|
@@ -89,6 +91,8 @@ class SqliterDB:
|
|
|
89
91
|
self.conn: Optional[sqlite3.Connection] = None
|
|
90
92
|
self.reset = reset
|
|
91
93
|
|
|
94
|
+
self._in_transaction = False
|
|
95
|
+
|
|
92
96
|
if self.debug:
|
|
93
97
|
self._setup_logger()
|
|
94
98
|
|
|
@@ -236,7 +240,12 @@ class SqliterDB:
|
|
|
236
240
|
for field_name, field_info in model_class.model_fields.items():
|
|
237
241
|
if field_name != primary_key:
|
|
238
242
|
sqlite_type = infer_sqlite_type(field_info.annotation)
|
|
239
|
-
|
|
243
|
+
unique_constraint = (
|
|
244
|
+
"UNIQUE" if isinstance(field_info, Unique) else ""
|
|
245
|
+
)
|
|
246
|
+
fields.append(
|
|
247
|
+
f"{field_name} {sqlite_type} {unique_constraint}".strip()
|
|
248
|
+
)
|
|
240
249
|
|
|
241
250
|
create_str = (
|
|
242
251
|
"CREATE TABLE IF NOT EXISTS" if exists_ok else "CREATE TABLE"
|
|
@@ -259,6 +268,65 @@ class SqliterDB:
|
|
|
259
268
|
except sqlite3.Error as exc:
|
|
260
269
|
raise TableCreationError(table_name) from exc
|
|
261
270
|
|
|
271
|
+
# Create regular indexes
|
|
272
|
+
if hasattr(model_class.Meta, "indexes"):
|
|
273
|
+
self._create_indexes(
|
|
274
|
+
model_class, model_class.Meta.indexes, unique=False
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
# Create unique indexes
|
|
278
|
+
if hasattr(model_class.Meta, "unique_indexes"):
|
|
279
|
+
self._create_indexes(
|
|
280
|
+
model_class, model_class.Meta.unique_indexes, unique=True
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
def _create_indexes(
|
|
284
|
+
self,
|
|
285
|
+
model_class: type[BaseDBModel],
|
|
286
|
+
indexes: list[Union[str, tuple[str]]],
|
|
287
|
+
*,
|
|
288
|
+
unique: bool = False,
|
|
289
|
+
) -> None:
|
|
290
|
+
"""Helper method to create regular or unique indexes.
|
|
291
|
+
|
|
292
|
+
Args:
|
|
293
|
+
model_class: The model class defining the table.
|
|
294
|
+
indexes: List of fields or tuples of fields to create indexes for.
|
|
295
|
+
unique: If True, creates UNIQUE indexes; otherwise, creates regular
|
|
296
|
+
indexes.
|
|
297
|
+
|
|
298
|
+
Raises:
|
|
299
|
+
InvalidIndexError: If any fields specified for indexing do not exist
|
|
300
|
+
in the model.
|
|
301
|
+
"""
|
|
302
|
+
valid_fields = set(
|
|
303
|
+
model_class.model_fields.keys()
|
|
304
|
+
) # Get valid fields from the model
|
|
305
|
+
|
|
306
|
+
for index in indexes:
|
|
307
|
+
# Handle multiple fields in tuple form
|
|
308
|
+
fields = list(index) if isinstance(index, tuple) else [index]
|
|
309
|
+
|
|
310
|
+
# Check if all fields exist in the model
|
|
311
|
+
invalid_fields = [
|
|
312
|
+
field for field in fields if field not in valid_fields
|
|
313
|
+
]
|
|
314
|
+
if invalid_fields:
|
|
315
|
+
raise InvalidIndexError(invalid_fields, model_class.__name__)
|
|
316
|
+
|
|
317
|
+
# Build the SQL string
|
|
318
|
+
index_name = "_".join(fields)
|
|
319
|
+
index_postfix = "_unique" if unique else ""
|
|
320
|
+
index_type = " UNIQUE " if unique else " "
|
|
321
|
+
|
|
322
|
+
create_index_sql = (
|
|
323
|
+
f"CREATE{index_type}INDEX IF NOT EXISTS "
|
|
324
|
+
f"idx_{model_class.get_table_name()}"
|
|
325
|
+
f"_{index_name}{index_postfix} "
|
|
326
|
+
f"ON {model_class.get_table_name()} ({', '.join(fields)})"
|
|
327
|
+
)
|
|
328
|
+
self._execute_sql(create_index_sql)
|
|
329
|
+
|
|
262
330
|
def _execute_sql(self, sql: str) -> None:
|
|
263
331
|
"""Execute an SQL statement.
|
|
264
332
|
|
|
@@ -308,7 +376,7 @@ class SqliterDB:
|
|
|
308
376
|
This method is called after operations that modify the database,
|
|
309
377
|
committing changes only if auto_commit is set to True.
|
|
310
378
|
"""
|
|
311
|
-
if self.auto_commit and self.conn:
|
|
379
|
+
if not self._in_transaction and self.auto_commit and self.conn:
|
|
312
380
|
self.conn.commit()
|
|
313
381
|
|
|
314
382
|
def insert(self, model_instance: T) -> T:
|
|
@@ -515,6 +583,7 @@ class SqliterDB:
|
|
|
515
583
|
|
|
516
584
|
"""
|
|
517
585
|
self.connect()
|
|
586
|
+
self._in_transaction = True
|
|
518
587
|
return self
|
|
519
588
|
|
|
520
589
|
def __exit__(
|
|
@@ -552,3 +621,4 @@ class SqliterDB:
|
|
|
552
621
|
# Close the connection and reset the instance variable
|
|
553
622
|
self.conn.close()
|
|
554
623
|
self.conn = None
|
|
624
|
+
self._in_transaction = False
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|