TypeDAL 3.0.1__py3-none-any.whl → 3.1.1__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.1"
8
+ __version__ = "3.1.1"
typedal/core.py CHANGED
@@ -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
@@ -25,19 +25,28 @@ def is_union(some_type: type | types.UnionType) -> bool:
25
25
  return typing.get_origin(some_type) in (types.UnionType, typing.Union)
26
26
 
27
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
+
28
35
  def _all_annotations(cls: type) -> ChainMap[str, type]:
29
36
  """
30
37
  Returns a dictionary-like ChainMap that includes annotations for all \
31
38
  attributes defined in cls or inherited from superclasses.
32
39
  """
33
- 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__))
34
43
 
35
44
 
36
45
  def all_dict(cls: type) -> AnyDict:
37
46
  """
38
47
  Get the internal data of a class and all it's parents.
39
48
  """
40
- 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
41
50
 
42
51
 
43
52
  def all_annotations(cls: type, _except: typing.Iterable[str] = None) -> dict[str, type]:
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
- Metadata-Version: 2.3
1
+ Metadata-Version: 2.1
2
2
  Name: TypeDAL
3
- Version: 3.0.1
3
+ Version: 3.1.1
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'
@@ -59,7 +60,7 @@ Description-Content-Type: text/markdown
59
60
  Typing support for [PyDAL](http://web2py.com/books/default/chapter/29/6).
60
61
  This package aims to improve the typing support for PyDAL. By using classes instead of the define_table method,
61
62
  type hinting the result of queries can improve the experience while developing. In the background, the queries are still
62
- generated and executed by pydal itself, this package only proves some logic to properly pass calls from class methods to
63
+ generated and executed by pydal itself, this package only provides some logic to properly pass calls from class methods to
63
64
  the underlying `db.define_table` pydal Tables.
64
65
 
65
66
  - `TypeDAL` is the replacement class for DAL that manages the code on top of DAL.
@@ -75,13 +76,49 @@ the underlying `db.define_table` pydal Tables.
75
76
 
76
77
  Version 2.0 also introduces more ORM-like funcionality.
77
78
  Most notably, a Typed Query Builder that sees your table classes as models with relationships to each other.
78
- See [3. Building Queries](https://github.com/trialandsuccess/TypeDAL/blob/master/docs/3_building_queries.md) for more
79
+ See [3. Building Queries](https://typedal.readthedocs.io/en/stable/3_building_queries/) for more
79
80
  details.
80
81
 
81
- ## Quick Overview
82
+ ## CLI
83
+ The Typedal CLI provides a convenient interface for generating SQL migrations for [edwh-migrate](https://github.com/educationwarehouse/migrate#readme)
84
+ from PyDAL or TypeDAL configurations using [pydal2sql](https://github.com/robinvandernoord/pydal2sql).
85
+ It offers various commands to streamline database management tasks.
82
86
 
83
- Below you'll find a quick overview of translation from pydal to TypeDAL. For more info,
84
- see [the docs](https://typedal.readthedocs.io/en/latest/).
87
+ ### Usage
88
+
89
+ ```bash
90
+ typedal --help
91
+ ```
92
+
93
+ ## Options
94
+
95
+ - `--show-config`: Toggle to show configuration details. Default is `no-show-config`.
96
+ - `--version`: Toggle to display version information. Default is `no-version`.
97
+ - `--install-completion`: Install completion for the current shell.
98
+ - `--show-completion`: Show completion for the current shell, for copying or customization.
99
+ - `--help`: Display help message and exit.
100
+
101
+ ## Commands
102
+
103
+ - `cache.clear`: Clear expired items from the cache.
104
+ - `cache.stats`: Show caching statistics.
105
+ - `migrations.fake`: Mark one or more migrations as completed in the database without executing the SQL code.
106
+ - `migrations.generate`: Run `pydal2sql` based on the TypeDAL configuration.
107
+ - `migrations.run`: Run `edwh-migrate` based on the TypeDAL configuration.
108
+ - `setup`: Interactively setup a `[tool.typedal]` entry in the local `pyproject.toml`.
109
+
110
+ ### Configuration
111
+
112
+ TypeDAL and its CLI can be configured via `pyproject.toml`.
113
+ See [6. Migrations](https://typedal.readthedocs.io/en/stable/6_migrations/) for more information about configuration.
114
+
115
+
116
+ ## TypeDAL for PyDAL users - Quick Overview
117
+
118
+ Below you'll find a quick overview of translation from pydal to TypeDAL.
119
+ For more info, see **[the docs](https://typedal.readthedocs.io/en/latest/)**.
120
+
121
+ ---
85
122
 
86
123
  ### Translations from pydal to typedal
87
124
 
@@ -258,7 +295,7 @@ row: TableName = db.table_name(id=1)
258
295
 
259
296
  ### All Types
260
297
 
261
- See [2. Defining Tables](docs/2_defining_tables.md)
298
+ See [2. Defining Tables](https://typedal.readthedocs.io/en/stable/2_defining_tables/)
262
299
 
263
300
  ## Caveats
264
301
 
@@ -0,0 +1,19 @@
1
+ typedal/__about__.py,sha256=OTBqn2XOHv_4-hOGbfVC29HOywMZOEFQS_8L6KP94Y4,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.1.dist-info/METADATA,sha256=TlBsIu6-i1xGFDGHPw76tFtE8I4oambuIGOVEbt2dyE,9270
17
+ typedal-3.1.1.dist-info/WHEEL,sha256=KGYbc1zXlYddvwxnNty23BeaKzh7YuoSIvIMO4jEhvw,87
18
+ typedal-3.1.1.dist-info/entry_points.txt,sha256=m1wqcc_10rHWPdlQ71zEkmJDADUAnZtn7Jac_6mbyUc,44
19
+ typedal-3.1.1.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: hatchling 1.22.4
2
+ Generator: hatchling 1.17.1
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
@@ -1,18 +0,0 @@
1
- typedal/__about__.py,sha256=8UfBCir8PsjPHcCRJyA0FLS1a3orteNLkZd6vHHlXjc,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=nHJ2Iq67rFIB4j43KCCD-tLIjvvvf4oJhT9T6tjXqxU,95063
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=n9dpIjXIjPpVFQnLBQreTWqRDR6hIsoNt8vGdEHGo_s,6871
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.1.dist-info/METADATA,sha256=GTmtFnSnOhgJI-P_hriMxRSZJf1EkEu9cT6eV2APM0s,7782
16
- typedal-3.0.1.dist-info/WHEEL,sha256=uNdcs2TADwSd5pVaP0Z_kcjcvvTUklh2S7bxZMF8Uj0,87
17
- typedal-3.0.1.dist-info/entry_points.txt,sha256=m1wqcc_10rHWPdlQ71zEkmJDADUAnZtn7Jac_6mbyUc,44
18
- typedal-3.0.1.dist-info/RECORD,,