TypeDAL 3.0.0b4__py3-none-any.whl → 3.1.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.

Potentially problematic release.


This version of TypeDAL might be problematic. Click here for more details.

typedal/__about__.py CHANGED
@@ -5,4 +5,4 @@ This file contains the Version info for this package.
5
5
  # SPDX-FileCopyrightText: 2023-present Robin van der Noord <robinvandernoord@gmail.com>
6
6
  #
7
7
  # SPDX-License-Identifier: MIT
8
- __version__ = "3.0.0-beta.4"
8
+ __version__ = "3.1.0"
typedal/cli.py CHANGED
@@ -2,7 +2,6 @@
2
2
  Typer CLI for TypeDAL.
3
3
  """
4
4
 
5
- import fnmatch
6
5
  import sys
7
6
  import typing
8
7
  import warnings
@@ -14,6 +13,7 @@ from configuraptor import asdict
14
13
  from configuraptor.alias import is_alias
15
14
  from configuraptor.helpers import is_optional
16
15
 
16
+ from .helpers import match_strings
17
17
  from .types import AnyDict
18
18
 
19
19
  try:
@@ -240,7 +240,7 @@ def generate_migrations(
240
240
  output_format: OutputFormat_Option = None,
241
241
  output_file: Optional[str] = None,
242
242
  dry_run: bool = False,
243
- ) -> bool:
243
+ ) -> bool: # pragma: no cover
244
244
  """
245
245
  Run pydal2sql based on the typedal config.
246
246
  """
@@ -313,7 +313,7 @@ def run_migrations(
313
313
  schema: Optional[str] = None,
314
314
  create_flag_location: Optional[bool] = None,
315
315
  dry_run: bool = False,
316
- ) -> bool:
316
+ ) -> bool: # pragma: no cover
317
317
  """
318
318
  Run edwh-migrate based on the typedal config.
319
319
  """
@@ -345,20 +345,6 @@ def run_migrations(
345
345
  return True
346
346
 
347
347
 
348
- def match_strings(patterns: list[str] | str, string_list: list[str]) -> list[str]:
349
- """
350
- Glob but on a list of strings.
351
- """
352
- if isinstance(patterns, str):
353
- patterns = [patterns]
354
-
355
- matches = []
356
- for pattern in patterns:
357
- matches.extend([s for s in string_list if fnmatch.fnmatch(s, pattern)])
358
-
359
- return matches
360
-
361
-
362
348
  @app.command(name="migrations.fake")
363
349
  @with_exit_code(hide_tb=IS_DEBUG)
364
350
  def fake_migrations(
@@ -370,7 +356,7 @@ def fake_migrations(
370
356
  db_folder: Optional[str] = None,
371
357
  migrate_table: Optional[str] = None,
372
358
  dry_run: bool = False,
373
- ) -> int:
359
+ ) -> int: # pragma: no cover
374
360
  """
375
361
  Mark one or more migrations as completed in the database, without executing the SQL code.
376
362
 
@@ -495,7 +481,7 @@ def cache_stats(
495
481
  fmt: typing.Annotated[
496
482
  str, typer.Option("--format", "--fmt", "-f", help="plaintext (default) or json")
497
483
  ] = "plaintext",
498
- ) -> None:
484
+ ) -> None: # pragma: no cover
499
485
  """
500
486
  Collect caching stats.
501
487
 
@@ -538,7 +524,7 @@ def cache_stats(
538
524
  def cache_clear(
539
525
  connection: typing.Annotated[str, typer.Option("--connection", "-c")] = None,
540
526
  purge: typing.Annotated[bool, typer.Option("--all", "--purge", "-p")] = False,
541
- ) -> None:
527
+ ) -> None: # pragma: no cover
542
528
  """
543
529
  Clear (expired) items from the cache.
544
530
 
typedal/config.py CHANGED
@@ -122,8 +122,7 @@ class TypeDALConfig(TypedConfig):
122
122
  )
123
123
 
124
124
 
125
-
126
- def _load_toml(path: str | bool | None = True) -> tuple[str, AnyDict]:
125
+ def _load_toml(path: str | bool | Path | None = True) -> tuple[str, AnyDict]:
127
126
  """
128
127
  Path can be a file, a directory, a bool or None.
129
128
 
@@ -136,10 +135,10 @@ def _load_toml(path: str | bool | None = True) -> tuple[str, AnyDict]:
136
135
  toml_path = None
137
136
  elif path in (True, None):
138
137
  toml_path = find_pyproject_toml()
139
- elif Path(str(path)).is_file():
140
- toml_path = str(path)
138
+ elif (_p := Path(str(path))) and _p.is_file():
139
+ toml_path = _p
141
140
  else:
142
- toml_path = find_pyproject_toml(path)
141
+ toml_path = find_pyproject_toml(str(path))
143
142
 
144
143
  if not toml_path:
145
144
  # nothing to load
typedal/core.py CHANGED
@@ -519,7 +519,7 @@ class TypeDAL(pydal.DAL): # type: ignore
519
519
  if k not in relationships and (new_relationship := to_relationship(cls, k, annotations[k]))
520
520
  }
521
521
 
522
- cache_dependency = kwargs.pop("cache_dependency", True)
522
+ cache_dependency = self._config.caching and kwargs.pop("cache_dependency", True)
523
523
 
524
524
  table: Table = self.define_table(tablename, *fields.values(), **kwargs)
525
525
 
@@ -1317,15 +1317,16 @@ class TypedField(typing.Generic[T_Value]): # pragma: no cover
1317
1317
  return typing.cast(Expression, ~self._field)
1318
1318
 
1319
1319
 
1320
- class TypedTable(metaclass=TableMeta):
1320
+ class _TypedTable:
1321
1321
  """
1322
- Enhanded modeling system on top of pydal's Table that adds typing and additional functionality.
1323
- """
1324
-
1325
- # set up by 'new':
1326
- _row: Row | None = None
1322
+ This class is a final shared parent between TypedTable and Mixins.
1327
1323
 
1328
- _with: list[str]
1324
+ This needs to exist because otherwise the __on_define__ of Mixins are not executed.
1325
+ Notably, this class exists at a level ABOVE the `metaclass=TableMeta`,
1326
+ because otherwise typing gets confused when Mixins are used and multiple types could satisfy
1327
+ generic 'T subclass of TypedTable'
1328
+ -> Setting 'TypedTable' as the parent for Mixin does not work at runtime (and works semi at type check time)
1329
+ """
1329
1330
 
1330
1331
  id: "TypedField[int]"
1331
1332
 
@@ -1336,6 +1337,26 @@ class TypedTable(metaclass=TableMeta):
1336
1337
  _before_delete: list[BeforeDeleteCallable]
1337
1338
  _after_delete: list[AfterDeleteCallable]
1338
1339
 
1340
+ @classmethod
1341
+ def __on_define__(cls, db: TypeDAL) -> None:
1342
+ """
1343
+ Method that can be implemented by tables to do an action after db.define is completed.
1344
+
1345
+ This can be useful if you need to add something like requires=IS_NOT_IN_DB(db, "table.field"),
1346
+ where you need a reference to the current database, which may not exist yet when defining the model.
1347
+ """
1348
+
1349
+
1350
+ class TypedTable(_TypedTable, metaclass=TableMeta):
1351
+ """
1352
+ Enhanded modeling system on top of pydal's Table that adds typing and additional functionality.
1353
+ """
1354
+
1355
+ # set up by 'new':
1356
+ _row: Row | None = None
1357
+
1358
+ _with: list[str]
1359
+
1339
1360
  def _setup_instance_methods(self) -> None:
1340
1361
  self.as_dict = self._as_dict # type: ignore
1341
1362
  self.__json__ = self.as_json = self._as_json # type: ignore
@@ -1382,15 +1403,6 @@ class TypedTable(metaclass=TableMeta):
1382
1403
  inst._setup_instance_methods()
1383
1404
  return inst
1384
1405
 
1385
- @classmethod
1386
- def __on_define__(cls, db: TypeDAL) -> None:
1387
- """
1388
- Method that can be implemented by tables to do an action after db.define is completed.
1389
-
1390
- This can be useful if you need to add something like requires=IS_NOT_IN_DB(db, "table.field"),
1391
- where you need a reference to the current database, which may not exist yet when defining the model.
1392
- """
1393
-
1394
1406
  def __iter__(self) -> typing.Generator[Any, None, None]:
1395
1407
  """
1396
1408
  Allows looping through the columns.
typedal/for_web2py.py CHANGED
@@ -24,6 +24,8 @@ class AuthGroup(TypedTable):
24
24
  """
25
25
  When we have access to 'db', set the NOT IN DB requirement to make the role unique.
26
26
  """
27
+ super().__on_define__(db)
28
+
27
29
  cls.role.requires = IS_NOT_IN_DB(db, "w2p_auth_group.role")
28
30
 
29
31
 
typedal/helpers.py CHANGED
@@ -2,6 +2,7 @@
2
2
  Helpers that work independently of core.
3
3
  """
4
4
 
5
+ import fnmatch
5
6
  import io
6
7
  import types
7
8
  import typing
@@ -24,19 +25,28 @@ def is_union(some_type: type | types.UnionType) -> bool:
24
25
  return typing.get_origin(some_type) in (types.UnionType, typing.Union)
25
26
 
26
27
 
28
+ def reversed_mro(cls: type) -> typing.Iterable[type]:
29
+ """
30
+ Get the Method Resolution Order (mro) for a class, in reverse order to be used with ChainMap.
31
+ """
32
+ return reversed(getattr(cls, "__mro__", []))
33
+
34
+
27
35
  def _all_annotations(cls: type) -> ChainMap[str, type]:
28
36
  """
29
37
  Returns a dictionary-like ChainMap that includes annotations for all \
30
38
  attributes defined in cls or inherited from superclasses.
31
39
  """
32
- return ChainMap(*(c.__annotations__ for c in getattr(cls, "__mro__", []) if "__annotations__" in c.__dict__))
40
+ # chainmap reverses the iterable, so reverse again beforehand to keep order normally:
41
+
42
+ return ChainMap(*(c.__annotations__ for c in reversed_mro(cls) if "__annotations__" in c.__dict__))
33
43
 
34
44
 
35
45
  def all_dict(cls: type) -> AnyDict:
36
46
  """
37
47
  Get the internal data of a class and all it's parents.
38
48
  """
39
- return dict(ChainMap(*(c.__dict__ for c in getattr(cls, "__mro__", []))))
49
+ return dict(ChainMap(*(c.__dict__ for c in reversed_mro(cls)))) # type: ignore
40
50
 
41
51
 
42
52
  def all_annotations(cls: type, _except: typing.Iterable[str] = None) -> dict[str, type]:
@@ -241,3 +251,17 @@ def as_lambda(value: T) -> typing.Callable[..., T]:
241
251
  Wrap value in a callable.
242
252
  """
243
253
  return lambda *_, **__: value
254
+
255
+
256
+ def match_strings(patterns: list[str] | str, string_list: list[str]) -> list[str]:
257
+ """
258
+ Glob but on a list of strings.
259
+ """
260
+ if isinstance(patterns, str):
261
+ patterns = [patterns]
262
+
263
+ matches = []
264
+ for pattern in patterns:
265
+ matches.extend([s for s in string_list if fnmatch.fnmatch(s, pattern)])
266
+
267
+ return matches
typedal/mixins.py ADDED
@@ -0,0 +1,135 @@
1
+ """
2
+ This file contains example Mixins.
3
+
4
+ Mixins can add reusable fields and behavior (optimally both, otherwise it doesn't add much).
5
+ """
6
+
7
+ import base64
8
+ import os
9
+ import typing
10
+ from datetime import datetime
11
+ from typing import Any
12
+
13
+ from slugify import slugify
14
+
15
+ from .core import TypedTable # noqa F401 - used by example in docstring
16
+ from .core import TypeDAL, _TypedTable
17
+ from .fields import DatetimeField, StringField
18
+ from .types import OpRow, Set
19
+
20
+
21
+ class Mixin(_TypedTable):
22
+ """
23
+ A mixin should be derived from this class.
24
+
25
+ The mixin base class itself doesn't do anything,
26
+ but using it makes sure the mixin fields are placed AFTER the table's normal fields (instead of before)
27
+
28
+ During runtime, mixin should not have a base class in order to prevent MRO issues
29
+ ('inconsistent method resolution' or 'metaclass conflicts')
30
+ """
31
+
32
+
33
+ class TimestampsMixin(Mixin):
34
+ """
35
+ A Mixin class for adding timestamp fields to a model.
36
+ """
37
+
38
+ created_at = DatetimeField(default=datetime.now, writable=False)
39
+ updated_at = DatetimeField(default=datetime.now, writable=False)
40
+
41
+ @classmethod
42
+ def __on_define__(cls, db: TypeDAL) -> None:
43
+ """
44
+ Hook called when defining the model to initialize timestamps.
45
+
46
+ Args:
47
+ db (TypeDAL): The database layer.
48
+ """
49
+ super().__on_define__(db)
50
+
51
+ def set_updated_at(_: Set, row: OpRow) -> None:
52
+ """
53
+ Callback function to update the 'updated_at' field before saving changes.
54
+
55
+ Args:
56
+ _: Set: Unused parameter.
57
+ row (OpRow): The row to update.
58
+ """
59
+ row["updated_at"] = datetime.now()
60
+
61
+ cls._before_update.append(set_updated_at)
62
+
63
+
64
+ def slug_random_suffix(length: int = 8) -> str:
65
+ """
66
+ Generate a random suffix to make slugs unique, even when titles are the same.
67
+
68
+ UUID4 uses 16 bytes, but 8 is probably more than enough given you probably don't have THAT much duplicate titles.
69
+ Strip away '=' to make it URL-safe
70
+ (even though 'urlsafe_b64encode' sounds like it should already be url-safe - it is not)
71
+ """
72
+ return base64.urlsafe_b64encode(os.urandom(length)).rstrip(b"=").decode().strip("=")
73
+
74
+
75
+ class SlugMixin(Mixin):
76
+ """
77
+ (Opinionated) example mixin to add a 'slug' field, which depends on a user-provided other field.
78
+
79
+ Some random bytes are added at the end to prevent duplicates.
80
+
81
+ Example:
82
+ >>> class MyTable(TypedTable, SlugMixin, slug_field="some_name"):
83
+ >>> some_name: str
84
+ >>> ...
85
+ """
86
+
87
+ # pub:
88
+ slug = StringField(unique=True)
89
+ # priv:
90
+ __settings__: typing.TypedDict( # type: ignore
91
+ "SlugFieldSettings",
92
+ {
93
+ "slug_field": str,
94
+ "slug_suffix": int,
95
+ },
96
+ ) # set via init subclass
97
+
98
+ def __init_subclass__(cls, slug_field: str = None, slug_suffix: int = 8, **kw: Any) -> None:
99
+ """
100
+ Bind 'slug field' option to be used later (on_define).
101
+
102
+ You can control the length of the random suffix with the `slug_suffix` option (0 is no suffix).
103
+ """
104
+ # unfortunately, PyCharm and mypy do not recognize/autocomplete/typecheck init subclass (keyword) arguments.
105
+ if slug_field is None:
106
+ raise ValueError(
107
+ "SlugMixin requires a valid slug_field setting: "
108
+ "e.g. `class MyClass(TypedTable, SlugMixin, slug_field='title'): ...`"
109
+ )
110
+
111
+ cls.__settings__ = {
112
+ "slug_field": slug_field,
113
+ "slug_suffix": slug_suffix,
114
+ }
115
+
116
+ @classmethod
117
+ def __on_define__(cls, db: TypeDAL) -> None:
118
+ """
119
+ When db is available, include a before_insert hook to generate and include a slug.
120
+ """
121
+ super().__on_define__(db)
122
+
123
+ # slugs should not be editable (for SEO reasons), so there is only a before insert hook:
124
+ def generate_slug_before_insert(row: OpRow) -> None:
125
+ settings = cls.__settings__
126
+
127
+ text_input = row[settings["slug_field"]]
128
+ generated_slug = slugify(text_input)
129
+
130
+ if suffix_len := settings["slug_suffix"]:
131
+ generated_slug += f"-{slug_random_suffix(suffix_len)}"
132
+
133
+ row["slug"] = slugify(generated_slug)
134
+
135
+ cls._before_insert.append(generate_slug_before_insert)
@@ -33,6 +33,8 @@ class AuthUser(TypedTable):
33
33
  """
34
34
  When we have access to 'db', set the IS_NOT_IN_DB requirement.
35
35
  """
36
+ super().__on_define__(db)
37
+
36
38
  cls.email.requires = (
37
39
  IS_EMAIL(),
38
40
  IS_NOT_IN_DB(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: TypeDAL
3
- Version: 3.0.0b4
3
+ Version: 3.1.0
4
4
  Summary: Typing support for PyDAL
5
5
  Project-URL: Documentation, https://typedal.readthedocs.io/
6
6
  Project-URL: Issues, https://github.com/trialandsuccess/TypeDAL/issues
@@ -19,8 +19,9 @@ Requires-Dist: configurable-json
19
19
  Requires-Dist: configuraptor>=1.26.2
20
20
  Requires-Dist: dill
21
21
  Requires-Dist: pydal
22
+ Requires-Dist: python-slugify
22
23
  Provides-Extra: all
23
- Requires-Dist: edwh-migrate>=0.8.0b1; extra == 'all'
24
+ Requires-Dist: edwh-migrate>=0.8.0; extra == 'all'
24
25
  Requires-Dist: py4web; extra == 'all'
25
26
  Requires-Dist: pydal2sql[all]>=1.1.3; extra == 'all'
26
27
  Requires-Dist: questionary; extra == 'all'
@@ -37,7 +38,7 @@ Requires-Dist: su6[all]; extra == 'dev'
37
38
  Requires-Dist: types-pyyaml; extra == 'dev'
38
39
  Requires-Dist: types-tabulate; extra == 'dev'
39
40
  Provides-Extra: migrations
40
- Requires-Dist: edwh-migrate>=0.8.0b1; extra == 'migrations'
41
+ Requires-Dist: edwh-migrate>=0.8.0; extra == 'migrations'
41
42
  Requires-Dist: pydal2sql>=1.1.3; extra == 'migrations'
42
43
  Requires-Dist: questionary; extra == 'migrations'
43
44
  Requires-Dist: tabulate; extra == 'migrations'
@@ -0,0 +1,19 @@
1
+ typedal/__about__.py,sha256=CN-1Orlz3CG7QijS-1qeKVPqHlDwyfxEQpBei5EcMRU,206
2
+ typedal/__init__.py,sha256=QQpLiVl9w9hm2LBxey49Y_tCF_VB2bScVaS_mCjYy54,366
3
+ typedal/caching.py,sha256=8UABVAhOlBpL96ykmqhxLaFYOe-XeAh7JoGh57OkxP8,11818
4
+ typedal/cli.py,sha256=3tge8B-YjgjMC6425-RMczmWvpOTfWV5QYPXRY23IWA,18200
5
+ typedal/config.py,sha256=jS1K0_1F5rwJtvwTZ-qR29ZCX7WlyORGEIFvfSnusko,11645
6
+ typedal/core.py,sha256=zkjuSmfkDAagahiOE43__dpd3cYNwqlIXnX-qGz1VDQ,95613
7
+ typedal/fields.py,sha256=z2PD9vLWqBR_zXtiY0DthqTG4AeF3yxKoeuVfGXnSdg,5197
8
+ typedal/for_py4web.py,sha256=d07b8hL_PvNDUS26Z5fDH2OxWb-IETBuAFPSzrRwm04,1285
9
+ typedal/for_web2py.py,sha256=4RHgzGXgKIO_BYB-7adC5e35u52rX-p1t4tPEz-NK24,1867
10
+ typedal/helpers.py,sha256=mtRYPFlS0dx2wK8kYSJ4vm1wsTXRkdPumFRlOAjF_xU,7177
11
+ typedal/mixins.py,sha256=stGjFBWPtRHteGJa3F6c-z9SPcYzFa2ToEbS9qlXEB0,4345
12
+ typedal/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
13
+ typedal/types.py,sha256=1kGkNX6vfGg6ln84AG558C4Zx5ACRz-emrUTnuy-rRY,3410
14
+ typedal/web2py_py4web_shared.py,sha256=VK9T8P5UwVLvfNBsY4q79ANcABv-jX76YKADt1Zz_co,1539
15
+ typedal/serializers/as_json.py,sha256=ffo152W-sARYXym4BzwX709rrO2-QwKk2KunWY8RNl4,2229
16
+ typedal-3.1.0.dist-info/METADATA,sha256=gCefk7iKuT5OVtj6uj8MpfVJlyu_xTSYF2oEim3ZxJg,7808
17
+ typedal-3.1.0.dist-info/WHEEL,sha256=KGYbc1zXlYddvwxnNty23BeaKzh7YuoSIvIMO4jEhvw,87
18
+ typedal-3.1.0.dist-info/entry_points.txt,sha256=m1wqcc_10rHWPdlQ71zEkmJDADUAnZtn7Jac_6mbyUc,44
19
+ typedal-3.1.0.dist-info/RECORD,,
@@ -1,18 +0,0 @@
1
- typedal/__about__.py,sha256=SUF9TxfBN1DcUhWrX9OEqf6k25fMzFQ0el_VEvjx_Mw,213
2
- typedal/__init__.py,sha256=QQpLiVl9w9hm2LBxey49Y_tCF_VB2bScVaS_mCjYy54,366
3
- typedal/caching.py,sha256=8UABVAhOlBpL96ykmqhxLaFYOe-XeAh7JoGh57OkxP8,11818
4
- typedal/cli.py,sha256=5-2U_pQOZNKHmhefiYtkd7g6B0DAXzjf4A1Jh7D37io,18427
5
- typedal/config.py,sha256=KDJXRsIQuFpSZy5XpSJiC_9WGLlmaOexACW0sWdCw54,11626
6
- typedal/core.py,sha256=qgJPvlcQYCujsjiiD6SOhWbIr1lxoUDpZUkMnK-mcDQ,95038
7
- typedal/fields.py,sha256=z2PD9vLWqBR_zXtiY0DthqTG4AeF3yxKoeuVfGXnSdg,5197
8
- typedal/for_py4web.py,sha256=d07b8hL_PvNDUS26Z5fDH2OxWb-IETBuAFPSzrRwm04,1285
9
- typedal/for_web2py.py,sha256=zvd5xC-SmuKc0JLDqT3hMIs6COaYnwTFXD_BIeC1vug,1832
10
- typedal/helpers.py,sha256=BFuGd-1tBA1-QS91C9PEvNY5z5KFHd3gTplxxDWdwSo,6509
11
- typedal/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
- typedal/types.py,sha256=1kGkNX6vfGg6ln84AG558C4Zx5ACRz-emrUTnuy-rRY,3410
13
- typedal/web2py_py4web_shared.py,sha256=cEbjkK0WOS9Q0nTyZuQaJWffeP4bjrL79Bx0xGy_UOs,1504
14
- typedal/serializers/as_json.py,sha256=ffo152W-sARYXym4BzwX709rrO2-QwKk2KunWY8RNl4,2229
15
- typedal-3.0.0b4.dist-info/METADATA,sha256=5XYmt8o4m4_atdsIPn2hSAQwKgHRIF_qunAe93lsaII,7784
16
- typedal-3.0.0b4.dist-info/WHEEL,sha256=KGYbc1zXlYddvwxnNty23BeaKzh7YuoSIvIMO4jEhvw,87
17
- typedal-3.0.0b4.dist-info/entry_points.txt,sha256=m1wqcc_10rHWPdlQ71zEkmJDADUAnZtn7Jac_6mbyUc,44
18
- typedal-3.0.0b4.dist-info/RECORD,,