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.
Files changed (110) hide show
  1. {sqlite_database-0.7.4/sqlite_database.egg-info → sqlite_database-0.7.5}/PKG-INFO +1 -1
  2. {sqlite_database-0.7.4 → sqlite_database-0.7.5}/docs/ModelAPI.md +9 -3
  3. {sqlite_database-0.7.4 → sqlite_database-0.7.5}/sqlite_database/__init__.py +6 -3
  4. {sqlite_database-0.7.4 → sqlite_database-0.7.5}/sqlite_database/_utils.py +1 -0
  5. {sqlite_database-0.7.4 → sqlite_database-0.7.5}/sqlite_database/models/__init__.py +60 -47
  6. sqlite_database-0.7.5/sqlite_database/models/helpers.py +215 -0
  7. sqlite_database-0.7.5/sqlite_database/models/type_checkers.py +27 -0
  8. {sqlite_database-0.7.4 → sqlite_database-0.7.5}/sqlite_database/query_builder.py +68 -37
  9. {sqlite_database-0.7.4 → sqlite_database-0.7.5/sqlite_database.egg-info}/PKG-INFO +1 -1
  10. {sqlite_database-0.7.4 → sqlite_database-0.7.5}/sqlite_database.egg-info/SOURCES.txt +1 -0
  11. {sqlite_database-0.7.4 → sqlite_database-0.7.5}/tests/database/model_api/test_model_api.py +49 -7
  12. {sqlite_database-0.7.4 → sqlite_database-0.7.5}/tests/database/setup.py +2 -2
  13. sqlite_database-0.7.4/sqlite_database/models/helpers.py +0 -117
  14. {sqlite_database-0.7.4 → sqlite_database-0.7.5}/.editorconfig +0 -0
  15. {sqlite_database-0.7.4 → sqlite_database-0.7.5}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  16. {sqlite_database-0.7.4 → sqlite_database-0.7.5}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  17. {sqlite_database-0.7.4 → sqlite_database-0.7.5}/.github/ISSUE_TEMPLATE/question.md +0 -0
  18. {sqlite_database-0.7.4 → sqlite_database-0.7.5}/.github/dependabot.yml +0 -0
  19. {sqlite_database-0.7.4 → sqlite_database-0.7.5}/.github/workflows/pylint.yml +0 -0
  20. {sqlite_database-0.7.4 → sqlite_database-0.7.5}/.github/workflows/pytest.yml +0 -0
  21. {sqlite_database-0.7.4 → sqlite_database-0.7.5}/.github/workflows/python-publish.yml +0 -0
  22. {sqlite_database-0.7.4 → sqlite_database-0.7.5}/.gitignore +0 -0
  23. {sqlite_database-0.7.4 → sqlite_database-0.7.5}/.readthedocs.yaml +0 -0
  24. {sqlite_database-0.7.4 → sqlite_database-0.7.5}/.vscode/settings.json +0 -0
  25. {sqlite_database-0.7.4 → sqlite_database-0.7.5}/Features.md +0 -0
  26. {sqlite_database-0.7.4 → sqlite_database-0.7.5}/History.md +0 -0
  27. {sqlite_database-0.7.4 → sqlite_database-0.7.5}/LICENSE +0 -0
  28. {sqlite_database-0.7.4 → sqlite_database-0.7.5}/README.md +0 -0
  29. {sqlite_database-0.7.4 → sqlite_database-0.7.5}/SimpleGuide.md +0 -0
  30. {sqlite_database-0.7.4 → sqlite_database-0.7.5}/TODO.md +0 -0
  31. {sqlite_database-0.7.4 → sqlite_database-0.7.5}/bin/activate +0 -0
  32. {sqlite_database-0.7.4 → sqlite_database-0.7.5}/bin/check.bat +0 -0
  33. {sqlite_database-0.7.4 → sqlite_database-0.7.5}/bin/check.sh +0 -0
  34. {sqlite_database-0.7.4 → sqlite_database-0.7.5}/bin/include/utility.bash +0 -0
  35. {sqlite_database-0.7.4 → sqlite_database-0.7.5}/bin/install.bash +0 -0
  36. {sqlite_database-0.7.4 → sqlite_database-0.7.5}/bin/need-installed/activate +0 -0
  37. {sqlite_database-0.7.4 → sqlite_database-0.7.5}/bin/need-installed/pre-commit +0 -0
  38. {sqlite_database-0.7.4 → sqlite_database-0.7.5}/bin/summarize-pylint.py +0 -0
  39. {sqlite_database-0.7.4 → sqlite_database-0.7.5}/dev-config/black.toml +0 -0
  40. {sqlite_database-0.7.4 → sqlite_database-0.7.5}/dev-config/pylint.toml +0 -0
  41. {sqlite_database-0.7.4 → sqlite_database-0.7.5}/dev-config/pytest.ini +0 -0
  42. {sqlite_database-0.7.4 → sqlite_database-0.7.5}/dev-requirements.txt +0 -0
  43. {sqlite_database-0.7.4 → sqlite_database-0.7.5}/docs/Makefile +0 -0
  44. {sqlite_database-0.7.4 → sqlite_database-0.7.5}/docs/SimpleGuide.md +0 -0
  45. {sqlite_database-0.7.4 → sqlite_database-0.7.5}/docs/_.md +0 -0
  46. {sqlite_database-0.7.4 → sqlite_database-0.7.5}/docs/api_reference.rst +0 -0
  47. {sqlite_database-0.7.4 → sqlite_database-0.7.5}/docs/conf.py +0 -0
  48. {sqlite_database-0.7.4 → sqlite_database-0.7.5}/docs/index.rst +0 -0
  49. {sqlite_database-0.7.4 → sqlite_database-0.7.5}/docs/make.bat +0 -0
  50. {sqlite_database-0.7.4 → sqlite_database-0.7.5}/docs/modules.rst +0 -0
  51. {sqlite_database-0.7.4 → sqlite_database-0.7.5}/docs/sqlite_database.column.rst +0 -0
  52. {sqlite_database-0.7.4 → sqlite_database-0.7.5}/docs/sqlite_database.config.rst +0 -0
  53. {sqlite_database-0.7.4 → sqlite_database-0.7.5}/docs/sqlite_database.csv.rst +0 -0
  54. {sqlite_database-0.7.4 → sqlite_database-0.7.5}/docs/sqlite_database.database.rst +0 -0
  55. {sqlite_database-0.7.4 → sqlite_database-0.7.5}/docs/sqlite_database.errors.rst +0 -0
  56. {sqlite_database-0.7.4 → sqlite_database-0.7.5}/docs/sqlite_database.functions.rst +0 -0
  57. {sqlite_database-0.7.4 → sqlite_database-0.7.5}/docs/sqlite_database.locals.rst +0 -0
  58. {sqlite_database-0.7.4 → sqlite_database-0.7.5}/docs/sqlite_database.model.errors.rst +0 -0
  59. {sqlite_database-0.7.4 → sqlite_database-0.7.5}/docs/sqlite_database.model.helpers.rst +0 -0
  60. {sqlite_database-0.7.4 → sqlite_database-0.7.5}/docs/sqlite_database.model.query_builder.rst +0 -0
  61. {sqlite_database-0.7.4 → sqlite_database-0.7.5}/docs/sqlite_database.model.rst +0 -0
  62. {sqlite_database-0.7.4 → sqlite_database-0.7.5}/docs/sqlite_database.operators.rst +0 -0
  63. {sqlite_database-0.7.4 → sqlite_database-0.7.5}/docs/sqlite_database.query_builder.rst +0 -0
  64. {sqlite_database-0.7.4 → sqlite_database-0.7.5}/docs/sqlite_database.rst +0 -0
  65. {sqlite_database-0.7.4 → sqlite_database-0.7.5}/docs/sqlite_database.signature.rst +0 -0
  66. {sqlite_database-0.7.4 → sqlite_database-0.7.5}/docs/sqlite_database.subexp.rst +0 -0
  67. {sqlite_database-0.7.4 → sqlite_database-0.7.5}/docs/sqlite_database.table.rst +0 -0
  68. {sqlite_database-0.7.4 → sqlite_database-0.7.5}/docs/sqlite_database.typings.rst +0 -0
  69. {sqlite_database-0.7.4 → sqlite_database-0.7.5}/docs/sqlite_database.utils.rst +0 -0
  70. {sqlite_database-0.7.4 → sqlite_database-0.7.5}/docs-requirements.txt +0 -0
  71. {sqlite_database-0.7.4 → sqlite_database-0.7.5}/project-init.bash +0 -0
  72. {sqlite_database-0.7.4 → sqlite_database-0.7.5}/pyproject.toml +0 -0
  73. {sqlite_database-0.7.4 → sqlite_database-0.7.5}/setup.cfg +0 -0
  74. {sqlite_database-0.7.4 → sqlite_database-0.7.5}/setup.py +0 -0
  75. {sqlite_database-0.7.4 → sqlite_database-0.7.5}/sqlite_database/_debug.py +0 -0
  76. {sqlite_database-0.7.4 → sqlite_database-0.7.5}/sqlite_database/column.py +0 -0
  77. {sqlite_database-0.7.4 → sqlite_database-0.7.5}/sqlite_database/csv.py +0 -0
  78. {sqlite_database-0.7.4 → sqlite_database-0.7.5}/sqlite_database/database.py +0 -0
  79. {sqlite_database-0.7.4 → sqlite_database-0.7.5}/sqlite_database/errors.py +0 -0
  80. {sqlite_database-0.7.4 → sqlite_database-0.7.5}/sqlite_database/functions.py +0 -0
  81. {sqlite_database-0.7.4 → sqlite_database-0.7.5}/sqlite_database/locals.py +0 -0
  82. {sqlite_database-0.7.4 → sqlite_database-0.7.5}/sqlite_database/models/errors.py +0 -0
  83. {sqlite_database-0.7.4 → sqlite_database-0.7.5}/sqlite_database/models/mixin.py +0 -0
  84. {sqlite_database-0.7.4 → sqlite_database-0.7.5}/sqlite_database/models/query_builder.py +0 -0
  85. {sqlite_database-0.7.4 → sqlite_database-0.7.5}/sqlite_database/operators.py +0 -0
  86. {sqlite_database-0.7.4 → sqlite_database-0.7.5}/sqlite_database/signature.py +0 -0
  87. {sqlite_database-0.7.4 → sqlite_database-0.7.5}/sqlite_database/subquery.py +0 -0
  88. {sqlite_database-0.7.4 → sqlite_database-0.7.5}/sqlite_database/table.py +0 -0
  89. {sqlite_database-0.7.4 → sqlite_database-0.7.5}/sqlite_database/typings.py +0 -0
  90. {sqlite_database-0.7.4 → sqlite_database-0.7.5}/sqlite_database/utils.py +0 -0
  91. {sqlite_database-0.7.4 → sqlite_database-0.7.5}/sqlite_database.egg-info/dependency_links.txt +0 -0
  92. {sqlite_database-0.7.4 → sqlite_database-0.7.5}/sqlite_database.egg-info/requires.txt +0 -0
  93. {sqlite_database-0.7.4 → sqlite_database-0.7.5}/sqlite_database.egg-info/top_level.txt +0 -0
  94. {sqlite_database-0.7.4 → sqlite_database-0.7.5}/sqlite_database.egg-info/zip-safe +0 -0
  95. {sqlite_database-0.7.4 → sqlite_database-0.7.5}/tests/__init__.py +0 -0
  96. {sqlite_database-0.7.4 → sqlite_database-0.7.5}/tests/database/__init__.py +0 -0
  97. {sqlite_database-0.7.4 → sqlite_database-0.7.5}/tests/database/model_api/__init__.py +0 -0
  98. {sqlite_database-0.7.4 → sqlite_database-0.7.5}/tests/database/table_api/__init__.py +0 -0
  99. {sqlite_database-0.7.4 → sqlite_database-0.7.5}/tests/database/table_api/test_csv.py +0 -0
  100. {sqlite_database-0.7.4 → sqlite_database-0.7.5}/tests/database/table_api/test_delete.py +0 -0
  101. {sqlite_database-0.7.4 → sqlite_database-0.7.5}/tests/database/table_api/test_insert.py +0 -0
  102. {sqlite_database-0.7.4 → sqlite_database-0.7.5}/tests/database/table_api/test_others.py +0 -0
  103. {sqlite_database-0.7.4 → sqlite_database-0.7.5}/tests/database/table_api/test_select.py +0 -0
  104. {sqlite_database-0.7.4 → sqlite_database-0.7.5}/tests/database/table_api/test_update.py +0 -0
  105. {sqlite_database-0.7.4 → sqlite_database-0.7.5}/tests/database/test_custom.py +0 -0
  106. {sqlite_database-0.7.4 → sqlite_database-0.7.5}/tests/manual_test_performances.py +0 -0
  107. {sqlite_database-0.7.4 → sqlite_database-0.7.5}/tests/test_internals.py +0 -0
  108. {sqlite_database-0.7.4 → sqlite_database-0.7.5}/tests/user_benchmark.py +0 -0
  109. {sqlite_database-0.7.4 → sqlite_database-0.7.5}/tests/user_helpers.py +0 -0
  110. {sqlite_database-0.7.4 → sqlite_database-0.7.5}/transient/README.md +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sqlite_database
3
- Version: 0.7.4
3
+ Version: 0.7.5
4
4
  Summary: A weird wrapper for SQLite Connection
5
5
  Home-page: https://github.com/RimuEirnarn/sqlite_database
6
6
  Author: RimuEirnarn
@@ -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 = input("New title: ")
193
- content = input("New 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.4"
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 .helpers import (Constraint, Unique, Primary, Foreign, TYPES, Validators,
10
- CASCADE, DEFAULT, NOACT, RESTRICT, SETNULL)
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 hook in cls.__hooks__.get(name, ()):
98
- if isinstance(hook, str):
99
- getattr(cls, hook)(instance)
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
- hook(instance)
109
+ hook_fn(instance)
102
110
 
103
111
  @classmethod
104
112
  def _execute_validators(cls, name: str, instance: Self):
105
- for validator in cls.__validators__.get(name, ()):
106
- validator.validate(instance)
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 = 'hook', name: str = "", if_fail: 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_ == 'hook':
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
- validator = Validators(func, fail)
145
+ validator_entry = Validators(func, fail)
137
146
  if cls.__validators__[name]:
138
- cls.__validators__[name] = [validator]
147
+ cls.__validators__[name] = [validator_entry]
139
148
  else:
140
- cls.__validators__[name].append(validator)
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('id', None)
148
- id_present = bool(kwargs.get('id', None))
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 = {k: (v if not k in self.__hidden__ else None) for k, v in dict_inst}
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("/") # type: ignore
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( # type: ignore
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] # type: ignore
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