sqlite-database 0.7.4__tar.gz → 0.7.5__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.
- {sqlite_database-0.7.4/sqlite_database.egg-info → sqlite_database-0.7.5}/PKG-INFO +1 -1
- {sqlite_database-0.7.4 → sqlite_database-0.7.5}/docs/ModelAPI.md +9 -3
- {sqlite_database-0.7.4 → sqlite_database-0.7.5}/sqlite_database/__init__.py +6 -3
- {sqlite_database-0.7.4 → sqlite_database-0.7.5}/sqlite_database/_utils.py +1 -0
- {sqlite_database-0.7.4 → sqlite_database-0.7.5}/sqlite_database/models/__init__.py +60 -47
- sqlite_database-0.7.5/sqlite_database/models/helpers.py +215 -0
- sqlite_database-0.7.5/sqlite_database/models/type_checkers.py +27 -0
- {sqlite_database-0.7.4 → sqlite_database-0.7.5}/sqlite_database/query_builder.py +68 -37
- {sqlite_database-0.7.4 → sqlite_database-0.7.5/sqlite_database.egg-info}/PKG-INFO +1 -1
- {sqlite_database-0.7.4 → sqlite_database-0.7.5}/sqlite_database.egg-info/SOURCES.txt +1 -0
- {sqlite_database-0.7.4 → sqlite_database-0.7.5}/tests/database/model_api/test_model_api.py +49 -7
- {sqlite_database-0.7.4 → sqlite_database-0.7.5}/tests/database/setup.py +2 -2
- sqlite_database-0.7.4/sqlite_database/models/helpers.py +0 -117
- {sqlite_database-0.7.4 → sqlite_database-0.7.5}/.editorconfig +0 -0
- {sqlite_database-0.7.4 → sqlite_database-0.7.5}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {sqlite_database-0.7.4 → sqlite_database-0.7.5}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {sqlite_database-0.7.4 → sqlite_database-0.7.5}/.github/ISSUE_TEMPLATE/question.md +0 -0
- {sqlite_database-0.7.4 → sqlite_database-0.7.5}/.github/dependabot.yml +0 -0
- {sqlite_database-0.7.4 → sqlite_database-0.7.5}/.github/workflows/pylint.yml +0 -0
- {sqlite_database-0.7.4 → sqlite_database-0.7.5}/.github/workflows/pytest.yml +0 -0
- {sqlite_database-0.7.4 → sqlite_database-0.7.5}/.github/workflows/python-publish.yml +0 -0
- {sqlite_database-0.7.4 → sqlite_database-0.7.5}/.gitignore +0 -0
- {sqlite_database-0.7.4 → sqlite_database-0.7.5}/.readthedocs.yaml +0 -0
- {sqlite_database-0.7.4 → sqlite_database-0.7.5}/.vscode/settings.json +0 -0
- {sqlite_database-0.7.4 → sqlite_database-0.7.5}/Features.md +0 -0
- {sqlite_database-0.7.4 → sqlite_database-0.7.5}/History.md +0 -0
- {sqlite_database-0.7.4 → sqlite_database-0.7.5}/LICENSE +0 -0
- {sqlite_database-0.7.4 → sqlite_database-0.7.5}/README.md +0 -0
- {sqlite_database-0.7.4 → sqlite_database-0.7.5}/SimpleGuide.md +0 -0
- {sqlite_database-0.7.4 → sqlite_database-0.7.5}/TODO.md +0 -0
- {sqlite_database-0.7.4 → sqlite_database-0.7.5}/bin/activate +0 -0
- {sqlite_database-0.7.4 → sqlite_database-0.7.5}/bin/check.bat +0 -0
- {sqlite_database-0.7.4 → sqlite_database-0.7.5}/bin/check.sh +0 -0
- {sqlite_database-0.7.4 → sqlite_database-0.7.5}/bin/include/utility.bash +0 -0
- {sqlite_database-0.7.4 → sqlite_database-0.7.5}/bin/install.bash +0 -0
- {sqlite_database-0.7.4 → sqlite_database-0.7.5}/bin/need-installed/activate +0 -0
- {sqlite_database-0.7.4 → sqlite_database-0.7.5}/bin/need-installed/pre-commit +0 -0
- {sqlite_database-0.7.4 → sqlite_database-0.7.5}/bin/summarize-pylint.py +0 -0
- {sqlite_database-0.7.4 → sqlite_database-0.7.5}/dev-config/black.toml +0 -0
- {sqlite_database-0.7.4 → sqlite_database-0.7.5}/dev-config/pylint.toml +0 -0
- {sqlite_database-0.7.4 → sqlite_database-0.7.5}/dev-config/pytest.ini +0 -0
- {sqlite_database-0.7.4 → sqlite_database-0.7.5}/dev-requirements.txt +0 -0
- {sqlite_database-0.7.4 → sqlite_database-0.7.5}/docs/Makefile +0 -0
- {sqlite_database-0.7.4 → sqlite_database-0.7.5}/docs/SimpleGuide.md +0 -0
- {sqlite_database-0.7.4 → sqlite_database-0.7.5}/docs/_.md +0 -0
- {sqlite_database-0.7.4 → sqlite_database-0.7.5}/docs/api_reference.rst +0 -0
- {sqlite_database-0.7.4 → sqlite_database-0.7.5}/docs/conf.py +0 -0
- {sqlite_database-0.7.4 → sqlite_database-0.7.5}/docs/index.rst +0 -0
- {sqlite_database-0.7.4 → sqlite_database-0.7.5}/docs/make.bat +0 -0
- {sqlite_database-0.7.4 → sqlite_database-0.7.5}/docs/modules.rst +0 -0
- {sqlite_database-0.7.4 → sqlite_database-0.7.5}/docs/sqlite_database.column.rst +0 -0
- {sqlite_database-0.7.4 → sqlite_database-0.7.5}/docs/sqlite_database.config.rst +0 -0
- {sqlite_database-0.7.4 → sqlite_database-0.7.5}/docs/sqlite_database.csv.rst +0 -0
- {sqlite_database-0.7.4 → sqlite_database-0.7.5}/docs/sqlite_database.database.rst +0 -0
- {sqlite_database-0.7.4 → sqlite_database-0.7.5}/docs/sqlite_database.errors.rst +0 -0
- {sqlite_database-0.7.4 → sqlite_database-0.7.5}/docs/sqlite_database.functions.rst +0 -0
- {sqlite_database-0.7.4 → sqlite_database-0.7.5}/docs/sqlite_database.locals.rst +0 -0
- {sqlite_database-0.7.4 → sqlite_database-0.7.5}/docs/sqlite_database.model.errors.rst +0 -0
- {sqlite_database-0.7.4 → sqlite_database-0.7.5}/docs/sqlite_database.model.helpers.rst +0 -0
- {sqlite_database-0.7.4 → sqlite_database-0.7.5}/docs/sqlite_database.model.query_builder.rst +0 -0
- {sqlite_database-0.7.4 → sqlite_database-0.7.5}/docs/sqlite_database.model.rst +0 -0
- {sqlite_database-0.7.4 → sqlite_database-0.7.5}/docs/sqlite_database.operators.rst +0 -0
- {sqlite_database-0.7.4 → sqlite_database-0.7.5}/docs/sqlite_database.query_builder.rst +0 -0
- {sqlite_database-0.7.4 → sqlite_database-0.7.5}/docs/sqlite_database.rst +0 -0
- {sqlite_database-0.7.4 → sqlite_database-0.7.5}/docs/sqlite_database.signature.rst +0 -0
- {sqlite_database-0.7.4 → sqlite_database-0.7.5}/docs/sqlite_database.subexp.rst +0 -0
- {sqlite_database-0.7.4 → sqlite_database-0.7.5}/docs/sqlite_database.table.rst +0 -0
- {sqlite_database-0.7.4 → sqlite_database-0.7.5}/docs/sqlite_database.typings.rst +0 -0
- {sqlite_database-0.7.4 → sqlite_database-0.7.5}/docs/sqlite_database.utils.rst +0 -0
- {sqlite_database-0.7.4 → sqlite_database-0.7.5}/docs-requirements.txt +0 -0
- {sqlite_database-0.7.4 → sqlite_database-0.7.5}/project-init.bash +0 -0
- {sqlite_database-0.7.4 → sqlite_database-0.7.5}/pyproject.toml +0 -0
- {sqlite_database-0.7.4 → sqlite_database-0.7.5}/setup.cfg +0 -0
- {sqlite_database-0.7.4 → sqlite_database-0.7.5}/setup.py +0 -0
- {sqlite_database-0.7.4 → sqlite_database-0.7.5}/sqlite_database/_debug.py +0 -0
- {sqlite_database-0.7.4 → sqlite_database-0.7.5}/sqlite_database/column.py +0 -0
- {sqlite_database-0.7.4 → sqlite_database-0.7.5}/sqlite_database/csv.py +0 -0
- {sqlite_database-0.7.4 → sqlite_database-0.7.5}/sqlite_database/database.py +0 -0
- {sqlite_database-0.7.4 → sqlite_database-0.7.5}/sqlite_database/errors.py +0 -0
- {sqlite_database-0.7.4 → sqlite_database-0.7.5}/sqlite_database/functions.py +0 -0
- {sqlite_database-0.7.4 → sqlite_database-0.7.5}/sqlite_database/locals.py +0 -0
- {sqlite_database-0.7.4 → sqlite_database-0.7.5}/sqlite_database/models/errors.py +0 -0
- {sqlite_database-0.7.4 → sqlite_database-0.7.5}/sqlite_database/models/mixin.py +0 -0
- {sqlite_database-0.7.4 → sqlite_database-0.7.5}/sqlite_database/models/query_builder.py +0 -0
- {sqlite_database-0.7.4 → sqlite_database-0.7.5}/sqlite_database/operators.py +0 -0
- {sqlite_database-0.7.4 → sqlite_database-0.7.5}/sqlite_database/signature.py +0 -0
- {sqlite_database-0.7.4 → sqlite_database-0.7.5}/sqlite_database/subquery.py +0 -0
- {sqlite_database-0.7.4 → sqlite_database-0.7.5}/sqlite_database/table.py +0 -0
- {sqlite_database-0.7.4 → sqlite_database-0.7.5}/sqlite_database/typings.py +0 -0
- {sqlite_database-0.7.4 → sqlite_database-0.7.5}/sqlite_database/utils.py +0 -0
- {sqlite_database-0.7.4 → sqlite_database-0.7.5}/sqlite_database.egg-info/dependency_links.txt +0 -0
- {sqlite_database-0.7.4 → sqlite_database-0.7.5}/sqlite_database.egg-info/requires.txt +0 -0
- {sqlite_database-0.7.4 → sqlite_database-0.7.5}/sqlite_database.egg-info/top_level.txt +0 -0
- {sqlite_database-0.7.4 → sqlite_database-0.7.5}/sqlite_database.egg-info/zip-safe +0 -0
- {sqlite_database-0.7.4 → sqlite_database-0.7.5}/tests/__init__.py +0 -0
- {sqlite_database-0.7.4 → sqlite_database-0.7.5}/tests/database/__init__.py +0 -0
- {sqlite_database-0.7.4 → sqlite_database-0.7.5}/tests/database/model_api/__init__.py +0 -0
- {sqlite_database-0.7.4 → sqlite_database-0.7.5}/tests/database/table_api/__init__.py +0 -0
- {sqlite_database-0.7.4 → sqlite_database-0.7.5}/tests/database/table_api/test_csv.py +0 -0
- {sqlite_database-0.7.4 → sqlite_database-0.7.5}/tests/database/table_api/test_delete.py +0 -0
- {sqlite_database-0.7.4 → sqlite_database-0.7.5}/tests/database/table_api/test_insert.py +0 -0
- {sqlite_database-0.7.4 → sqlite_database-0.7.5}/tests/database/table_api/test_others.py +0 -0
- {sqlite_database-0.7.4 → sqlite_database-0.7.5}/tests/database/table_api/test_select.py +0 -0
- {sqlite_database-0.7.4 → sqlite_database-0.7.5}/tests/database/table_api/test_update.py +0 -0
- {sqlite_database-0.7.4 → sqlite_database-0.7.5}/tests/database/test_custom.py +0 -0
- {sqlite_database-0.7.4 → sqlite_database-0.7.5}/tests/manual_test_performances.py +0 -0
- {sqlite_database-0.7.4 → sqlite_database-0.7.5}/tests/test_internals.py +0 -0
- {sqlite_database-0.7.4 → sqlite_database-0.7.5}/tests/user_benchmark.py +0 -0
- {sqlite_database-0.7.4 → sqlite_database-0.7.5}/tests/user_helpers.py +0 -0
- {sqlite_database-0.7.4 → sqlite_database-0.7.5}/transient/README.md +0 -0
|
@@ -159,7 +159,7 @@ Here’s a small CLI app to tie it all together:
|
|
|
159
159
|
```py
|
|
160
160
|
from enum import IntEnum
|
|
161
161
|
from uuid import uuid4
|
|
162
|
-
from sqlite_database import Database, model, BaseModel, Primary
|
|
162
|
+
from sqlite_database import Database, model, BaseModel, Primary, Null
|
|
163
163
|
|
|
164
164
|
db = Database(":memory:")
|
|
165
165
|
|
|
@@ -180,6 +180,12 @@ def display():
|
|
|
180
180
|
print(f"Content : {note.content}")
|
|
181
181
|
print("-"*3)
|
|
182
182
|
|
|
183
|
+
def read(prompt: str):
|
|
184
|
+
try:
|
|
185
|
+
return input(prompt)
|
|
186
|
+
except KeyboardInterrupt:
|
|
187
|
+
return Null
|
|
188
|
+
|
|
183
189
|
def create():
|
|
184
190
|
title = input("Title: ")
|
|
185
191
|
content = input("Content: ")
|
|
@@ -189,8 +195,8 @@ def update():
|
|
|
189
195
|
note_id = input('ID: ')
|
|
190
196
|
note = Notes.first(id=note_id)
|
|
191
197
|
if note:
|
|
192
|
-
title =
|
|
193
|
-
content =
|
|
198
|
+
title = read("New title: ")
|
|
199
|
+
content = read("New content: ")
|
|
194
200
|
note.update(title=title, content=content)
|
|
195
201
|
else:
|
|
196
202
|
print("Note not found.")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""Database"""
|
|
2
2
|
|
|
3
|
-
from .models import BaseModel, model, Foreign, Primary, Unique
|
|
3
|
+
from .models import BaseModel, model, Foreign, Primary, Unique, hook, validate
|
|
4
4
|
from .database import Database
|
|
5
5
|
from ._utils import null, Row, Null
|
|
6
6
|
from .column import Column, text, integer, blob, real
|
|
@@ -14,7 +14,7 @@ def test_installed():
|
|
|
14
14
|
return True
|
|
15
15
|
|
|
16
16
|
|
|
17
|
-
__version__ = "0.7.
|
|
17
|
+
__version__ = "0.7.5"
|
|
18
18
|
__all__ = [
|
|
19
19
|
"Database",
|
|
20
20
|
"Table",
|
|
@@ -32,5 +32,8 @@ __all__ = [
|
|
|
32
32
|
"models",
|
|
33
33
|
"Foreign",
|
|
34
34
|
"Primary",
|
|
35
|
-
"Unique"
|
|
35
|
+
"Unique",
|
|
36
|
+
"model",
|
|
37
|
+
"hook",
|
|
38
|
+
"validate"
|
|
36
39
|
]
|
|
@@ -66,6 +66,7 @@ class Sentinel: # pylint: disable=too-few-public-methods
|
|
|
66
66
|
Pre-defined value:
|
|
67
67
|
- Null, use this one if you're unsure if the data you pulled exists or not.
|
|
68
68
|
The query builder will remove it if it detects Null sentinel."""
|
|
69
|
+
|
|
69
70
|
def __repr__(self) -> str:
|
|
70
71
|
return "<Sentinel>"
|
|
71
72
|
|
|
@@ -1,13 +1,28 @@
|
|
|
1
1
|
"""Models"""
|
|
2
2
|
|
|
3
|
-
# pylint: disable=unused-import,unused-argument,cyclic-import
|
|
3
|
+
# pylint: disable=unused-import,unused-argument,cyclic-import,protected-access
|
|
4
4
|
|
|
5
5
|
from contextlib import contextmanager
|
|
6
6
|
from typing import Any, Callable, Self, Type, TypeVar, overload
|
|
7
7
|
from dataclasses import asdict, dataclass, fields, is_dataclass, MISSING
|
|
8
8
|
|
|
9
|
-
from .
|
|
10
|
-
|
|
9
|
+
from sqlite_database.models.type_checkers import typecheck
|
|
10
|
+
|
|
11
|
+
from .helpers import (
|
|
12
|
+
Constraint,
|
|
13
|
+
Unique,
|
|
14
|
+
Primary,
|
|
15
|
+
Foreign,
|
|
16
|
+
TYPES,
|
|
17
|
+
Validators,
|
|
18
|
+
CASCADE,
|
|
19
|
+
DEFAULT,
|
|
20
|
+
NOACT,
|
|
21
|
+
RESTRICT,
|
|
22
|
+
SETNULL,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
from .helpers import VALID_HOOKS_NAME, hook, validate, initiate_hook, initiate_validators
|
|
11
26
|
from .query_builder import QueryBuilder
|
|
12
27
|
from .errors import ConstraintError
|
|
13
28
|
from ..errors import DatabaseExistsError
|
|
@@ -18,22 +33,15 @@ from ..operators import in_
|
|
|
18
33
|
NULL = object()
|
|
19
34
|
T = TypeVar("T", bound="BaseModel")
|
|
20
35
|
|
|
21
|
-
VALID_HOOKS_NAME = (
|
|
22
|
-
"before_create",
|
|
23
|
-
"after_create",
|
|
24
|
-
"before_update",
|
|
25
|
-
"after_update",
|
|
26
|
-
"before_delete",
|
|
27
|
-
"after_delete",
|
|
28
|
-
)
|
|
29
|
-
|
|
30
36
|
## Model functions
|
|
31
37
|
|
|
38
|
+
|
|
32
39
|
@staticmethod
|
|
33
40
|
def noop_autoid():
|
|
34
41
|
"""Default no-op function for BaseModel __auto_id__"""
|
|
35
42
|
return None
|
|
36
43
|
|
|
44
|
+
|
|
37
45
|
class BaseModel: # pylint: disable=too-few-public-methods,too-many-public-methods
|
|
38
46
|
"""Base class for all Models using Model API"""
|
|
39
47
|
|
|
@@ -42,7 +50,7 @@ class BaseModel: # pylint: disable=too-few-public-methods,too-many-public-metho
|
|
|
42
50
|
__validators__: dict[str, list[Validators]] = {}
|
|
43
51
|
__hooks__: "dict[str, list[Callable[[Self], None] | str]]" = {}
|
|
44
52
|
__hidden__: tuple[str, ...] = ()
|
|
45
|
-
__auto_id__ = noop_autoid
|
|
53
|
+
__auto_id__: Callable[[], Any] = noop_autoid
|
|
46
54
|
_tbl: Table
|
|
47
55
|
_primary: str | None
|
|
48
56
|
|
|
@@ -94,26 +102,27 @@ class BaseModel: # pylint: disable=too-few-public-methods,too-many-public-metho
|
|
|
94
102
|
|
|
95
103
|
@classmethod
|
|
96
104
|
def _execute_hooks(cls, name: str, instance: Self):
|
|
97
|
-
for
|
|
98
|
-
if isinstance(
|
|
99
|
-
getattr(cls,
|
|
105
|
+
for hook_fn in cls.__hooks__.get(name, ()):
|
|
106
|
+
if isinstance(hook_fn, str):
|
|
107
|
+
getattr(cls, hook_fn)(instance)
|
|
100
108
|
else:
|
|
101
|
-
|
|
109
|
+
hook_fn(instance)
|
|
102
110
|
|
|
103
111
|
@classmethod
|
|
104
112
|
def _execute_validators(cls, name: str, instance: Self):
|
|
105
|
-
for
|
|
106
|
-
|
|
113
|
+
for validator_fn in cls.__validators__.get(name, ()):
|
|
114
|
+
validator_fn.validate(instance)
|
|
107
115
|
|
|
108
116
|
@classmethod
|
|
109
|
-
def _register(cls, type_: str =
|
|
117
|
+
def _register(cls, type_: str = "hook", name: str = "", if_fail: str = ""):
|
|
110
118
|
"""Register a hook/validator under a name"""
|
|
111
119
|
if type_ not in ("hook", "validator"):
|
|
112
120
|
raise ValueError("Which do you want?")
|
|
121
|
+
|
|
113
122
|
def function(func):
|
|
114
|
-
if name ==
|
|
123
|
+
if name == "":
|
|
115
124
|
raise ValueError(f"{type_.title()} name needs to be declared.")
|
|
116
|
-
if type_ ==
|
|
125
|
+
if type_ == "hook":
|
|
117
126
|
if name not in VALID_HOOKS_NAME:
|
|
118
127
|
raise ValueError("Name of a hook doesn't match with expected value")
|
|
119
128
|
cls.__hooks__.setdefault(name, [])
|
|
@@ -128,26 +137,27 @@ class BaseModel: # pylint: disable=too-few-public-methods,too-many-public-metho
|
|
|
128
137
|
|
|
129
138
|
fields_ = tuple((field.name for field in fields(cls)))
|
|
130
139
|
if name not in fields_:
|
|
131
|
-
raise ValueError("Expected validator to has name as column field")
|
|
140
|
+
raise ValueError(f"Expected validator to has name as column field. Got {name!r}")
|
|
132
141
|
|
|
133
142
|
fail = if_fail or f"{name} fails certain validator"
|
|
134
143
|
|
|
135
144
|
cls.__validators__.setdefault(name, [])
|
|
136
|
-
|
|
145
|
+
validator_entry = Validators(func, fail)
|
|
137
146
|
if cls.__validators__[name]:
|
|
138
|
-
cls.__validators__[name] = [
|
|
147
|
+
cls.__validators__[name] = [validator_entry]
|
|
139
148
|
else:
|
|
140
|
-
cls.__validators__[name].append(
|
|
149
|
+
cls.__validators__[name].append(validator_entry)
|
|
141
150
|
return func
|
|
151
|
+
|
|
142
152
|
return function
|
|
143
153
|
|
|
144
154
|
@classmethod
|
|
145
155
|
def create(cls, **kwargs):
|
|
146
156
|
"""Create data based on kwargs"""
|
|
147
|
-
primary: str | None = cls._primary or kwargs.get(
|
|
148
|
-
id_present = bool(kwargs.get(
|
|
149
|
-
if primary and cls.__auto_id__ and not id_present:
|
|
150
|
-
kwargs[primary] = cls.__auto_id__()
|
|
157
|
+
primary: str | None = cls._primary or kwargs.get("id", None)
|
|
158
|
+
id_present = bool(kwargs.get("id", None))
|
|
159
|
+
if primary and cls.__auto_id__ and not id_present: # type: ignore
|
|
160
|
+
kwargs[primary] = cls.__auto_id__() # type: ignore
|
|
151
161
|
instance = cls(**kwargs)
|
|
152
162
|
|
|
153
163
|
cls._execute_hooks("before_create", instance)
|
|
@@ -283,7 +293,9 @@ class BaseModel: # pylint: disable=too-few-public-methods,too-many-public-metho
|
|
|
283
293
|
"""Wrap instance that complies with __hidden__."""
|
|
284
294
|
if is_dataclass(self):
|
|
285
295
|
dict_inst = asdict(self).items()
|
|
286
|
-
instance = {
|
|
296
|
+
instance = {
|
|
297
|
+
k: (v if not k in self.__hidden__ else None) for k, v in dict_inst
|
|
298
|
+
}
|
|
287
299
|
return type(self)(**instance)
|
|
288
300
|
raise TypeError("This class must be a dataclass")
|
|
289
301
|
|
|
@@ -304,7 +316,7 @@ class BaseModel: # pylint: disable=too-few-public-methods,too-many-public-metho
|
|
|
304
316
|
# Scan __schema__ of related model to find a Foreign key linking back
|
|
305
317
|
for constraint in related.__schema__:
|
|
306
318
|
if isinstance(constraint, Foreign):
|
|
307
|
-
table_ref, _ = constraint.target.split("/")
|
|
319
|
+
table_ref, _ = constraint.target.split("/") # type: ignore
|
|
308
320
|
if table_ref == self.__class__.__name__.lower():
|
|
309
321
|
foreign_key = constraint.column
|
|
310
322
|
break
|
|
@@ -322,11 +334,11 @@ class BaseModel: # pylint: disable=too-few-public-methods,too-many-public-metho
|
|
|
322
334
|
"""Retrieve the related model that this instance belongs to."""
|
|
323
335
|
# Find the Foreign() constraint that references `related_model`
|
|
324
336
|
for constraint in self.__schema__:
|
|
325
|
-
if isinstance(constraint, Foreign) and constraint.target.startswith(
|
|
337
|
+
if isinstance(constraint, Foreign) and constraint.target.startswith( # type: ignore
|
|
326
338
|
related_model.__table_name__ + "/"
|
|
327
339
|
):
|
|
328
340
|
foreign_key = constraint.column
|
|
329
|
-
referenced_column = constraint.target.split("/")[1]
|
|
341
|
+
referenced_column = constraint.target.split("/")[1] # type: ignore
|
|
330
342
|
return related_model.where(
|
|
331
343
|
**{referenced_column: getattr(self, foreign_key)}
|
|
332
344
|
).fetch_one()
|
|
@@ -351,16 +363,6 @@ class BaseModel: # pylint: disable=too-few-public-methods,too-many-public-metho
|
|
|
351
363
|
f"with {self.__class__.__name__}"
|
|
352
364
|
)
|
|
353
365
|
|
|
354
|
-
@classmethod
|
|
355
|
-
def hook(cls, name: str):
|
|
356
|
-
"""Register a hook"""
|
|
357
|
-
return cls._register('hook', name)
|
|
358
|
-
|
|
359
|
-
@classmethod
|
|
360
|
-
def validator(cls, column_name: str, if_fail: str):
|
|
361
|
-
"""Register a validator"""
|
|
362
|
-
return cls._register("validator", column_name, if_fail)
|
|
363
|
-
|
|
364
366
|
def get_table(self):
|
|
365
367
|
"""Return table instance"""
|
|
366
368
|
return self._tbl
|
|
@@ -375,9 +377,11 @@ class BaseModel: # pylint: disable=too-few-public-methods,too-many-public-metho
|
|
|
375
377
|
"""Return Query Builder related to this model"""
|
|
376
378
|
return QueryBuilder(cls)
|
|
377
379
|
|
|
378
|
-
def model(db: Database):
|
|
380
|
+
def model(db: Database, type_checking: bool = False):
|
|
379
381
|
"""Initiate Model API compatible classes. Requires target to be a dataclass,
|
|
380
|
-
the app automatically injects dataclass if this isn't a dataclass
|
|
382
|
+
the app automatically injects dataclass if this isn't a dataclass.
|
|
383
|
+
|
|
384
|
+
Use `type_checking` if you want automatic runtime type checker."""
|
|
381
385
|
|
|
382
386
|
def outer(cls: Type[T]) -> Type[T]:
|
|
383
387
|
if not issubclass(cls, BaseModel):
|
|
@@ -385,6 +389,13 @@ def model(db: Database):
|
|
|
385
389
|
if not is_dataclass(cls):
|
|
386
390
|
cls = dataclass(cls)
|
|
387
391
|
cls.create_table(db)
|
|
392
|
+
if type_checking:
|
|
393
|
+
for fn in typecheck(cls):
|
|
394
|
+
initiate_validators(cls, fn)
|
|
395
|
+
|
|
396
|
+
for member in cls.__dict__.values():
|
|
397
|
+
initiate_hook(cls, member)
|
|
398
|
+
initiate_validators(cls, member)
|
|
388
399
|
return cls
|
|
389
400
|
|
|
390
401
|
return outer
|
|
@@ -401,5 +412,7 @@ __all__ = [
|
|
|
401
412
|
"DEFAULT",
|
|
402
413
|
"NOACT",
|
|
403
414
|
"SETNULL",
|
|
404
|
-
"RESTRICT"
|
|
415
|
+
"RESTRICT",
|
|
416
|
+
"validate",
|
|
417
|
+
"hook"
|
|
405
418
|
]
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
"""Model helpers"""
|
|
2
|
+
|
|
3
|
+
# pylint: disable=invalid-name,too-few-public-methods,abstract-method,protected-access
|
|
4
|
+
|
|
5
|
+
from typing import Any, Callable, Type, TypeAlias, TypeVar, overload
|
|
6
|
+
from enum import StrEnum
|
|
7
|
+
|
|
8
|
+
import sqlite_database
|
|
9
|
+
from .errors import ValidationError
|
|
10
|
+
from ..column import BuilderColumn, text, integer, blob, boolean
|
|
11
|
+
|
|
12
|
+
TypeFunction: TypeAlias = Callable[[str], BuilderColumn]
|
|
13
|
+
Model = TypeVar("Model", bound="sqlite_database.BaseModel")
|
|
14
|
+
FuncT = TypeVar("FuncT", bound=Callable[..., bool])
|
|
15
|
+
BaseModel: TypeAlias = "sqlite_database.BaseModel"
|
|
16
|
+
|
|
17
|
+
TYPES: dict[Type[Any], TypeFunction] = (
|
|
18
|
+
{ # pylint: disable=possibly-used-before-assignment
|
|
19
|
+
int: integer,
|
|
20
|
+
str: text,
|
|
21
|
+
bytes: blob,
|
|
22
|
+
bool: boolean,
|
|
23
|
+
}
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
VALID_HOOKS_NAME = (
|
|
27
|
+
"before_create",
|
|
28
|
+
"after_create",
|
|
29
|
+
"before_update",
|
|
30
|
+
"after_update",
|
|
31
|
+
"before_delete",
|
|
32
|
+
"after_delete",
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class ConstraintEnum(StrEnum):
|
|
37
|
+
"""Constraints for update/delete"""
|
|
38
|
+
|
|
39
|
+
RESTRICT = "restrict"
|
|
40
|
+
SETNULL = "null"
|
|
41
|
+
CASCADE = "cascade"
|
|
42
|
+
NOACT = "no act"
|
|
43
|
+
DEFAULT = "default"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
RESTRICT = ConstraintEnum.RESTRICT
|
|
47
|
+
SETNULL = ConstraintEnum.SETNULL
|
|
48
|
+
CASCADE = ConstraintEnum.CASCADE
|
|
49
|
+
NOACT = ConstraintEnum.NOACT
|
|
50
|
+
DEFAULT = ConstraintEnum.DEFAULT
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class Constraint:
|
|
54
|
+
"""Base constraint class for models"""
|
|
55
|
+
|
|
56
|
+
def __init__(self, column: str) -> None:
|
|
57
|
+
self._column = column
|
|
58
|
+
|
|
59
|
+
@property
|
|
60
|
+
def column(self):
|
|
61
|
+
"""Columns"""
|
|
62
|
+
return self._column
|
|
63
|
+
|
|
64
|
+
def apply(self, type_: BuilderColumn):
|
|
65
|
+
"""Apply this constraint to an column"""
|
|
66
|
+
raise NotImplementedError()
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class Unique(Constraint):
|
|
70
|
+
"""Unique constraint"""
|
|
71
|
+
|
|
72
|
+
def apply(self, type_: BuilderColumn):
|
|
73
|
+
type_.unique()
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class Foreign(Constraint):
|
|
77
|
+
"""Foreign constraint"""
|
|
78
|
+
|
|
79
|
+
def __init__(self, column: str, target: str | Type[Model]) -> None:
|
|
80
|
+
super().__init__(column)
|
|
81
|
+
self._target = target
|
|
82
|
+
self.resolve()
|
|
83
|
+
self._base = target
|
|
84
|
+
self._on_delete = DEFAULT
|
|
85
|
+
self._on_update = DEFAULT
|
|
86
|
+
|
|
87
|
+
@property
|
|
88
|
+
def target(self):
|
|
89
|
+
"""Target foreign constraint"""
|
|
90
|
+
return self._target
|
|
91
|
+
|
|
92
|
+
def on_delete(self, constraint: ConstraintEnum):
|
|
93
|
+
"""On delete constraint"""
|
|
94
|
+
self._on_delete = constraint
|
|
95
|
+
return self
|
|
96
|
+
|
|
97
|
+
def on_update(self, constraint: ConstraintEnum):
|
|
98
|
+
"""On update constraint"""
|
|
99
|
+
self._on_update = constraint
|
|
100
|
+
return self
|
|
101
|
+
|
|
102
|
+
def resolve(self):
|
|
103
|
+
"""Resolve if current target is a Model"""
|
|
104
|
+
if issubclass(self._target, sqlite_database.BaseModel): # type: ignore
|
|
105
|
+
name = self._target.__table_name__
|
|
106
|
+
target = self._target._primary # pylint: disable=protected-access
|
|
107
|
+
if not target:
|
|
108
|
+
raise ValueError(f"{type(self._target)} does not have primary key")
|
|
109
|
+
self._target = f"{name}/{target}"
|
|
110
|
+
|
|
111
|
+
def apply(self, type_: BuilderColumn):
|
|
112
|
+
type_.foreign(self._target) # type: ignore
|
|
113
|
+
if self._on_delete != DEFAULT:
|
|
114
|
+
type_.on_delete(self._on_delete.value)
|
|
115
|
+
if self._on_update != DEFAULT:
|
|
116
|
+
type_.on_update(self._on_update.value)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class Primary(Constraint):
|
|
120
|
+
"""Primary constraint"""
|
|
121
|
+
|
|
122
|
+
def apply(self, type_: BuilderColumn):
|
|
123
|
+
"""Apply this constraint as primary"""
|
|
124
|
+
type_.primary()
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class Validators:
|
|
128
|
+
"""Base class to hold validators"""
|
|
129
|
+
|
|
130
|
+
def __init__(self, fn: Callable[[Any], bool], if_fail: str) -> None:
|
|
131
|
+
self._callable = fn
|
|
132
|
+
self._reason = if_fail
|
|
133
|
+
|
|
134
|
+
def validate(self, instance: BaseModel):
|
|
135
|
+
"""Validate a value"""
|
|
136
|
+
if not self._callable(instance):
|
|
137
|
+
err = ValidationError(self._reason)
|
|
138
|
+
err.add_note(str(instance))
|
|
139
|
+
raise err
|
|
140
|
+
return True
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
@overload
|
|
144
|
+
def hook(fn_or_name: Callable[[Model], None]) -> "staticmethod[[Callable[[Model], None]], None]":
|
|
145
|
+
pass
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
@overload
|
|
149
|
+
def hook(fn_or_name: str):
|
|
150
|
+
pass
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def hook(fn_or_name):
|
|
154
|
+
"""Register a hook"""
|
|
155
|
+
|
|
156
|
+
def decorator(func):
|
|
157
|
+
fn = staticmethod(func)
|
|
158
|
+
fn_name = func.__name__
|
|
159
|
+
final_name = fn_or_name if fn_name not in VALID_HOOKS_NAME else fn_name
|
|
160
|
+
|
|
161
|
+
if final_name is None:
|
|
162
|
+
raise ValueError("Hooks name is not valid. Provide with @hook(name)")
|
|
163
|
+
fn._hooks_info = (fn_or_name # type: ignore
|
|
164
|
+
if fn_name not in VALID_HOOKS_NAME else fn_name,)
|
|
165
|
+
return fn
|
|
166
|
+
|
|
167
|
+
return decorator(fn_or_name) if callable(fn_or_name) else decorator
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
@overload
|
|
171
|
+
def validate(fn_or_column: FuncT) -> "staticmethod[[FuncT], bool]":
|
|
172
|
+
pass
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
@overload
|
|
176
|
+
def validate(fn_or_column: str, reason: str | None = None): # type: ignore
|
|
177
|
+
pass
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def validate(fn_or_column, reason=None):
|
|
181
|
+
"""Register a validator"""
|
|
182
|
+
|
|
183
|
+
def decorator(func: Callable):
|
|
184
|
+
print(func, fn_or_column)
|
|
185
|
+
fn = staticmethod(func)
|
|
186
|
+
name = func.__name__
|
|
187
|
+
inferred_col = (
|
|
188
|
+
name[len("validate_") :] if name.startswith("validate_") else None
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
col = fn_or_column or inferred_col
|
|
192
|
+
if callable(col):
|
|
193
|
+
col = inferred_col
|
|
194
|
+
if col is None:
|
|
195
|
+
raise ValueError("Validator must have a column name.")
|
|
196
|
+
|
|
197
|
+
msg = reason or func.__doc__ or f"Validation failed for '{col}'"
|
|
198
|
+
fn._validators_info = (col, msg) # type: ignore
|
|
199
|
+
return fn
|
|
200
|
+
|
|
201
|
+
return decorator(fn_or_column) if callable(fn_or_column) else decorator
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def initiate_hook(cls: Type[BaseModel], member: Callable):
|
|
205
|
+
"""Initiate hooks"""
|
|
206
|
+
if hasattr(member, "_hooks_info"):
|
|
207
|
+
info = member._hooks_info
|
|
208
|
+
cls._register("hook", info[0])(member)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def initiate_validators(cls: Type[BaseModel], member: Callable):
|
|
212
|
+
"""Initiate validators"""
|
|
213
|
+
if hasattr(member, "_validators_info"):
|
|
214
|
+
info = member._validators_info
|
|
215
|
+
cls._register("validator", info[0], info[1])(member)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Type checking"""
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, Any, Type
|
|
4
|
+
from dataclasses import fields, is_dataclass
|
|
5
|
+
from .helpers import validate
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from . import BaseModel
|
|
9
|
+
|
|
10
|
+
def infer_type(name: str, type_: "Type[Any]"):
|
|
11
|
+
"""Infer type checking for specific columns"""
|
|
12
|
+
if not isinstance(type_, type):
|
|
13
|
+
return None
|
|
14
|
+
|
|
15
|
+
@validate(name, f"{name} type is not {type_.__name__}") # type: ignore
|
|
16
|
+
def function(instance: "Type[BaseModel]"):
|
|
17
|
+
return isinstance(getattr(instance, name), type_)
|
|
18
|
+
return function
|
|
19
|
+
|
|
20
|
+
def typecheck(cls: "Type[BaseModel]"):
|
|
21
|
+
"""Automatically pushed Runtime type checking"""
|
|
22
|
+
if not is_dataclass(cls):
|
|
23
|
+
raise TypeError(f"{cls.__name__} is not a dataclass")
|
|
24
|
+
|
|
25
|
+
for field in fields(cls):
|
|
26
|
+
name, type_ = field.name, field.type
|
|
27
|
+
yield infer_type(name, type_) # type: ignore
|