sqliter-py 0.9.0__py3-none-any.whl → 0.16.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 +4 -3
- sqliter/exceptions.py +43 -0
- sqliter/model/__init__.py +38 -3
- sqliter/model/foreign_key.py +153 -0
- sqliter/model/model.py +42 -3
- sqliter/model/unique.py +20 -11
- sqliter/orm/__init__.py +16 -0
- sqliter/orm/fields.py +412 -0
- sqliter/orm/foreign_key.py +8 -0
- sqliter/orm/model.py +243 -0
- sqliter/orm/query.py +221 -0
- sqliter/orm/registry.py +169 -0
- sqliter/query/query.py +720 -69
- sqliter/sqliter.py +533 -76
- sqliter/tui/__init__.py +62 -0
- sqliter/tui/__main__.py +6 -0
- sqliter/tui/app.py +179 -0
- sqliter/tui/demos/__init__.py +96 -0
- sqliter/tui/demos/base.py +114 -0
- sqliter/tui/demos/caching.py +283 -0
- sqliter/tui/demos/connection.py +150 -0
- sqliter/tui/demos/constraints.py +211 -0
- sqliter/tui/demos/crud.py +154 -0
- sqliter/tui/demos/errors.py +231 -0
- sqliter/tui/demos/field_selection.py +150 -0
- sqliter/tui/demos/filters.py +389 -0
- sqliter/tui/demos/models.py +248 -0
- sqliter/tui/demos/ordering.py +156 -0
- sqliter/tui/demos/orm.py +460 -0
- sqliter/tui/demos/results.py +241 -0
- sqliter/tui/demos/string_filters.py +210 -0
- sqliter/tui/demos/timestamps.py +126 -0
- sqliter/tui/demos/transactions.py +177 -0
- sqliter/tui/runner.py +116 -0
- sqliter/tui/styles/app.tcss +130 -0
- sqliter/tui/widgets/__init__.py +7 -0
- sqliter/tui/widgets/code_display.py +81 -0
- sqliter/tui/widgets/demo_list.py +65 -0
- sqliter/tui/widgets/output_display.py +92 -0
- {sqliter_py-0.9.0.dist-info → sqliter_py-0.16.0.dist-info}/METADATA +27 -11
- sqliter_py-0.16.0.dist-info/RECORD +47 -0
- {sqliter_py-0.9.0.dist-info → sqliter_py-0.16.0.dist-info}/WHEEL +2 -2
- sqliter_py-0.16.0.dist-info/entry_points.txt +3 -0
- sqliter_py-0.9.0.dist-info/RECORD +0 -14
sqliter/constants.py
CHANGED
|
@@ -21,9 +21,10 @@ OPERATOR_MAPPING = {
|
|
|
21
21
|
"__not_in": "NOT IN",
|
|
22
22
|
"__isnull": "IS NULL",
|
|
23
23
|
"__notnull": "IS NOT NULL",
|
|
24
|
-
"
|
|
25
|
-
"
|
|
26
|
-
"
|
|
24
|
+
"__like": "LIKE",
|
|
25
|
+
"__startswith": "GLOB",
|
|
26
|
+
"__endswith": "GLOB",
|
|
27
|
+
"__contains": "GLOB",
|
|
27
28
|
"__istartswith": "LIKE",
|
|
28
29
|
"__iendswith": "LIKE",
|
|
29
30
|
"__icontains": "LIKE",
|
sqliter/exceptions.py
CHANGED
|
@@ -166,3 +166,46 @@ class InvalidIndexError(SqliterError):
|
|
|
166
166
|
invalid_fields_str = ", ".join(invalid_fields)
|
|
167
167
|
# Pass the formatted message to the parent class
|
|
168
168
|
super().__init__(model_class, invalid_fields_str)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
class ForeignKeyError(SqliterError):
|
|
172
|
+
"""Base exception for foreign key related errors."""
|
|
173
|
+
|
|
174
|
+
message_template = "Foreign key error: {}"
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
class ForeignKeyConstraintError(ForeignKeyError):
|
|
178
|
+
"""Raised when a foreign key constraint is violated.
|
|
179
|
+
|
|
180
|
+
This error occurs when attempting to insert/update a record with a
|
|
181
|
+
foreign key value that doesn't exist in the referenced table, or
|
|
182
|
+
when attempting to delete a record that is still referenced.
|
|
183
|
+
"""
|
|
184
|
+
|
|
185
|
+
message_template = (
|
|
186
|
+
"Foreign key constraint violation: Cannot {} record - "
|
|
187
|
+
"referenced record {}"
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
class InvalidForeignKeyError(ForeignKeyError):
|
|
192
|
+
"""Raised when an invalid foreign key configuration is detected.
|
|
193
|
+
|
|
194
|
+
This error occurs when defining a foreign key with invalid parameters,
|
|
195
|
+
such as using SET NULL without null=True.
|
|
196
|
+
"""
|
|
197
|
+
|
|
198
|
+
message_template = "Invalid foreign key configuration: {}"
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
class InvalidRelationshipError(SqliterError):
|
|
202
|
+
"""Raised when an invalid relationship path is specified.
|
|
203
|
+
|
|
204
|
+
This error occurs when using select_related() or relationship filter
|
|
205
|
+
traversal with a non-existent relationship field or invalid path.
|
|
206
|
+
"""
|
|
207
|
+
|
|
208
|
+
message_template = (
|
|
209
|
+
"Invalid relationship path '{}': field '{}' is not a valid "
|
|
210
|
+
"foreign key relationship on model {}"
|
|
211
|
+
)
|
sqliter/model/__init__.py
CHANGED
|
@@ -1,11 +1,46 @@
|
|
|
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, and the
|
|
4
|
+
models in SQLiter applications, and the unique function, which is used to
|
|
5
5
|
define unique constraints on model fields.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
+
import warnings
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from typing_extensions import deprecated
|
|
12
|
+
|
|
13
|
+
from .foreign_key import ForeignKey, ForeignKeyInfo, get_foreign_key_info
|
|
8
14
|
from .model import BaseDBModel, SerializableField
|
|
9
|
-
from .unique import
|
|
15
|
+
from .unique import unique
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@deprecated("Use 'unique' instead. Will be removed in a future version.")
|
|
19
|
+
def Unique(default: Any = ..., **kwargs: Any) -> Any: # noqa: ANN401, N802
|
|
20
|
+
"""Deprecated: Use 'unique' instead. Will be removed in a future version.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
default: The default value for the field.
|
|
24
|
+
**kwargs: Additional keyword arguments to pass to Field.
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
A Field with unique metadata attached.
|
|
28
|
+
"""
|
|
29
|
+
warnings.warn(
|
|
30
|
+
"Unique is deprecated and will be removed in a future version. "
|
|
31
|
+
"Use 'unique' instead.",
|
|
32
|
+
DeprecationWarning,
|
|
33
|
+
stacklevel=2,
|
|
34
|
+
)
|
|
35
|
+
return unique(default=default, **kwargs)
|
|
36
|
+
|
|
10
37
|
|
|
11
|
-
__all__ = [
|
|
38
|
+
__all__ = [
|
|
39
|
+
"BaseDBModel",
|
|
40
|
+
"ForeignKey",
|
|
41
|
+
"ForeignKeyInfo",
|
|
42
|
+
"SerializableField",
|
|
43
|
+
"Unique",
|
|
44
|
+
"get_foreign_key_info",
|
|
45
|
+
"unique",
|
|
46
|
+
]
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"""Foreign key support for SQLiter ORM.
|
|
2
|
+
|
|
3
|
+
This module provides the ForeignKey factory function and ForeignKeyInfo
|
|
4
|
+
dataclass for defining foreign key relationships between models.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from typing import TYPE_CHECKING, Any, Literal, Optional
|
|
11
|
+
|
|
12
|
+
from pydantic import Field
|
|
13
|
+
|
|
14
|
+
from sqliter.exceptions import InvalidForeignKeyError
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING: # pragma: no cover
|
|
17
|
+
from pydantic.fields import FieldInfo
|
|
18
|
+
|
|
19
|
+
from sqliter.model.model import BaseDBModel
|
|
20
|
+
|
|
21
|
+
# Type alias for foreign key actions
|
|
22
|
+
FKAction = Literal["CASCADE", "SET NULL", "RESTRICT", "NO ACTION"]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class ForeignKeyInfo:
|
|
27
|
+
"""Metadata about a foreign key relationship.
|
|
28
|
+
|
|
29
|
+
Attributes:
|
|
30
|
+
to_model: The target model class that this foreign key references.
|
|
31
|
+
on_delete: Action to take when the referenced record is deleted.
|
|
32
|
+
on_update: Action to take when the referenced record's PK is updated.
|
|
33
|
+
null: Whether the foreign key field can be NULL.
|
|
34
|
+
unique: Whether the foreign key field must be unique (one-to-one).
|
|
35
|
+
related_name: Optional name for the reverse relationship (Phase 2).
|
|
36
|
+
db_column: Optional custom column name in the database.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
to_model: type[BaseDBModel]
|
|
40
|
+
on_delete: FKAction
|
|
41
|
+
on_update: FKAction
|
|
42
|
+
null: bool
|
|
43
|
+
unique: bool
|
|
44
|
+
related_name: Optional[str]
|
|
45
|
+
db_column: Optional[str]
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def ForeignKey( # noqa: N802, PLR0913
|
|
49
|
+
to: type[BaseDBModel],
|
|
50
|
+
*,
|
|
51
|
+
on_delete: FKAction = "RESTRICT",
|
|
52
|
+
on_update: FKAction = "RESTRICT",
|
|
53
|
+
null: bool = False,
|
|
54
|
+
unique: bool = False,
|
|
55
|
+
related_name: Optional[str] = None,
|
|
56
|
+
db_column: Optional[str] = None,
|
|
57
|
+
default: Any = ..., # noqa: ANN401
|
|
58
|
+
**kwargs: Any, # noqa: ANN401
|
|
59
|
+
) -> Any: # noqa: ANN401
|
|
60
|
+
"""Create a foreign key field.
|
|
61
|
+
|
|
62
|
+
This function creates a Pydantic Field with foreign key metadata stored
|
|
63
|
+
in json_schema_extra. In Phase 1, this provides FK constraint support.
|
|
64
|
+
Phase 2 will add descriptor support for `book.author` style access.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
to: The target model class that this foreign key references.
|
|
68
|
+
on_delete: Action when referenced record is deleted.
|
|
69
|
+
- CASCADE: Delete this record too.
|
|
70
|
+
- SET NULL: Set this field to NULL (requires null=True).
|
|
71
|
+
- RESTRICT: Prevent deletion if references exist (default).
|
|
72
|
+
- NO ACTION: Similar to RESTRICT in SQLite.
|
|
73
|
+
on_update: Action when referenced record's PK is updated.
|
|
74
|
+
- CASCADE: Update this field to the new PK value.
|
|
75
|
+
- SET NULL: Set this field to NULL (requires null=True).
|
|
76
|
+
- RESTRICT: Prevent update if references exist (default).
|
|
77
|
+
- NO ACTION: Similar to RESTRICT in SQLite.
|
|
78
|
+
null: Whether the foreign key field can be NULL. Default is False.
|
|
79
|
+
unique: Whether the field must be unique (creates one-to-one
|
|
80
|
+
relationship). Default is False.
|
|
81
|
+
related_name: Optional name for the reverse relationship. Reserved
|
|
82
|
+
for Phase 2 implementation.
|
|
83
|
+
db_column: Optional custom column name. If not specified, defaults
|
|
84
|
+
to `{field_name}_id`.
|
|
85
|
+
default: Default value for the field. If null=True and no default
|
|
86
|
+
is provided, defaults to None.
|
|
87
|
+
**kwargs: Additional keyword arguments passed to Pydantic Field.
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
A Pydantic Field with foreign key metadata.
|
|
91
|
+
|
|
92
|
+
Raises:
|
|
93
|
+
InvalidForeignKeyError: If SET NULL action is used without null=True.
|
|
94
|
+
|
|
95
|
+
Example:
|
|
96
|
+
>>> class Book(BaseDBModel):
|
|
97
|
+
... title: str
|
|
98
|
+
... author_id: int = ForeignKey(Author, on_delete="CASCADE")
|
|
99
|
+
"""
|
|
100
|
+
# Validate SET NULL requires null=True
|
|
101
|
+
if on_delete == "SET NULL" and not null:
|
|
102
|
+
msg = "on_delete='SET NULL' requires null=True"
|
|
103
|
+
raise InvalidForeignKeyError(msg)
|
|
104
|
+
if on_update == "SET NULL" and not null:
|
|
105
|
+
msg = "on_update='SET NULL' requires null=True"
|
|
106
|
+
raise InvalidForeignKeyError(msg)
|
|
107
|
+
|
|
108
|
+
# Handle existing json_schema_extra
|
|
109
|
+
existing_extra = kwargs.pop("json_schema_extra", {})
|
|
110
|
+
if not isinstance(existing_extra, dict):
|
|
111
|
+
existing_extra = {}
|
|
112
|
+
|
|
113
|
+
# Create ForeignKeyInfo metadata
|
|
114
|
+
fk_info = ForeignKeyInfo(
|
|
115
|
+
to_model=to,
|
|
116
|
+
on_delete=on_delete,
|
|
117
|
+
on_update=on_update,
|
|
118
|
+
null=null,
|
|
119
|
+
unique=unique,
|
|
120
|
+
related_name=related_name,
|
|
121
|
+
db_column=db_column,
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
# Store FK metadata in json_schema_extra
|
|
125
|
+
existing_extra["foreign_key"] = fk_info
|
|
126
|
+
if unique:
|
|
127
|
+
existing_extra["unique"] = True
|
|
128
|
+
|
|
129
|
+
# Set default value
|
|
130
|
+
if default is ... and "default_factory" not in kwargs:
|
|
131
|
+
default = None if null else ...
|
|
132
|
+
|
|
133
|
+
return Field(default=default, json_schema_extra=existing_extra, **kwargs)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def get_foreign_key_info(field_info: FieldInfo) -> Optional[ForeignKeyInfo]:
|
|
137
|
+
"""Extract ForeignKeyInfo from a field if it's a foreign key.
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
field_info: The Pydantic FieldInfo to examine.
|
|
141
|
+
|
|
142
|
+
Returns:
|
|
143
|
+
The ForeignKeyInfo if the field is a foreign key, None otherwise.
|
|
144
|
+
"""
|
|
145
|
+
if not hasattr(field_info, "json_schema_extra"):
|
|
146
|
+
return None
|
|
147
|
+
extra = field_info.json_schema_extra
|
|
148
|
+
if not isinstance(extra, dict):
|
|
149
|
+
return None
|
|
150
|
+
fk_info = extra.get("foreign_key")
|
|
151
|
+
if isinstance(fk_info, ForeignKeyInfo):
|
|
152
|
+
return fk_info
|
|
153
|
+
return None
|
sqliter/model/model.py
CHANGED
|
@@ -123,6 +123,32 @@ class BaseDBModel(BaseModel):
|
|
|
123
123
|
|
|
124
124
|
return cast("Self", cls.model_construct(**converted_obj))
|
|
125
125
|
|
|
126
|
+
@staticmethod
|
|
127
|
+
def _validate_table_name(table_name: str) -> str:
|
|
128
|
+
"""Validate that a table name contains only safe characters.
|
|
129
|
+
|
|
130
|
+
Table names must contain only alphanumeric characters and underscores,
|
|
131
|
+
and must start with a letter or underscore. This prevents SQL injection
|
|
132
|
+
through malicious table names.
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
table_name: The table name to validate.
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
The validated table name.
|
|
139
|
+
|
|
140
|
+
Raises:
|
|
141
|
+
ValueError: If the table name contains invalid characters.
|
|
142
|
+
"""
|
|
143
|
+
if not re.match(r"^[a-zA-Z_][a-zA-Z0-9_]*$", table_name):
|
|
144
|
+
msg = (
|
|
145
|
+
f"Invalid table name '{table_name}'. "
|
|
146
|
+
"Table names must start with a letter or underscore and "
|
|
147
|
+
"contain only letters, numbers, and underscores."
|
|
148
|
+
)
|
|
149
|
+
raise ValueError(msg)
|
|
150
|
+
return table_name
|
|
151
|
+
|
|
126
152
|
@classmethod
|
|
127
153
|
def get_table_name(cls) -> str:
|
|
128
154
|
"""Get the database table name for the model.
|
|
@@ -130,12 +156,22 @@ class BaseDBModel(BaseModel):
|
|
|
130
156
|
This method determines the table name based on the Meta configuration
|
|
131
157
|
or derives it from the class name if not explicitly set.
|
|
132
158
|
|
|
159
|
+
When deriving the table name automatically, the class name is converted
|
|
160
|
+
to snake_case and pluralized. If the `inflect` library is installed,
|
|
161
|
+
it provides grammatically correct pluralization (e.g., "person" becomes
|
|
162
|
+
"people", "category" becomes "categories"). Otherwise, a simple "s"
|
|
163
|
+
suffix is added if the name doesn't already end in "s".
|
|
164
|
+
|
|
133
165
|
Returns:
|
|
134
166
|
The name of the database table for this model.
|
|
167
|
+
|
|
168
|
+
Raises:
|
|
169
|
+
ValueError: If the table name contains invalid characters.
|
|
135
170
|
"""
|
|
136
171
|
table_name: str | None = getattr(cls.Meta, "table_name", None)
|
|
137
172
|
if table_name is not None:
|
|
138
|
-
|
|
173
|
+
# Validate custom table names
|
|
174
|
+
return cls._validate_table_name(table_name)
|
|
139
175
|
|
|
140
176
|
# Get class name and remove 'Model' suffix if present
|
|
141
177
|
class_name = cls.__name__.removesuffix("Model")
|
|
@@ -148,15 +184,18 @@ class BaseDBModel(BaseModel):
|
|
|
148
184
|
import inflect # noqa: PLC0415
|
|
149
185
|
|
|
150
186
|
p = inflect.engine()
|
|
151
|
-
|
|
187
|
+
table_name = p.plural(snake_case_name)
|
|
152
188
|
except ImportError:
|
|
153
189
|
# Fallback to simple pluralization by adding 's'
|
|
154
|
-
|
|
190
|
+
table_name = (
|
|
155
191
|
snake_case_name
|
|
156
192
|
if snake_case_name.endswith("s")
|
|
157
193
|
else snake_case_name + "s"
|
|
158
194
|
)
|
|
159
195
|
|
|
196
|
+
# Validate auto-generated table names (should always pass)
|
|
197
|
+
return cls._validate_table_name(table_name)
|
|
198
|
+
|
|
160
199
|
@classmethod
|
|
161
200
|
def get_primary_key(cls) -> str:
|
|
162
201
|
"""Returns the mandatory primary key, always 'pk'."""
|
sqliter/model/unique.py
CHANGED
|
@@ -2,18 +2,27 @@
|
|
|
2
2
|
|
|
3
3
|
from typing import Any
|
|
4
4
|
|
|
5
|
-
from pydantic
|
|
5
|
+
from pydantic import Field
|
|
6
6
|
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
"""A custom field type for unique constraints in SQLiter.
|
|
8
|
+
def unique(default: Any = ..., **kwargs: Any) -> Any: # noqa: ANN401
|
|
9
|
+
"""A custom field type for unique constraints in SQLiter.
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
Args:
|
|
12
|
+
default: The default value for the field.
|
|
13
|
+
**kwargs: Additional keyword arguments to pass to Field.
|
|
13
14
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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/orm/__init__.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""ORM submodule for SQLiter.
|
|
2
|
+
|
|
3
|
+
This module provides ORM functionality including lazy loading and reverse
|
|
4
|
+
relationships. It extends the BaseDBModel from sqliter.model without breaking
|
|
5
|
+
changes to the existing code.
|
|
6
|
+
|
|
7
|
+
Users can choose between modes via import:
|
|
8
|
+
- Legacy mode: from sqliter.model import BaseDBModel
|
|
9
|
+
- ORM mode: from sqliter.orm import BaseDBModel
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from sqliter.orm.foreign_key import ForeignKey
|
|
13
|
+
from sqliter.orm.model import BaseDBModel
|
|
14
|
+
from sqliter.orm.registry import ModelRegistry
|
|
15
|
+
|
|
16
|
+
__all__ = ["BaseDBModel", "ForeignKey", "ModelRegistry"]
|