TypeDAL 3.12.2__tar.gz → 3.13.1__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.

Potentially problematic release.


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

Files changed (58) hide show
  1. {typedal-3.12.2 → typedal-3.13.1}/CHANGELOG.md +12 -0
  2. {typedal-3.12.2 → typedal-3.13.1}/PKG-INFO +2 -2
  3. {typedal-3.12.2 → typedal-3.13.1}/src/typedal/__about__.py +1 -1
  4. {typedal-3.12.2 → typedal-3.13.1}/src/typedal/core.py +101 -4
  5. {typedal-3.12.2 → typedal-3.13.1}/src/typedal/helpers.py +24 -0
  6. {typedal-3.12.2 → typedal-3.13.1}/tests/test_main.py +39 -0
  7. {typedal-3.12.2 → typedal-3.13.1}/tests/test_query_builder.py +2 -0
  8. {typedal-3.12.2 → typedal-3.13.1}/.github/workflows/su6.yml +0 -0
  9. {typedal-3.12.2 → typedal-3.13.1}/.gitignore +0 -0
  10. {typedal-3.12.2 → typedal-3.13.1}/.readthedocs.yml +0 -0
  11. {typedal-3.12.2 → typedal-3.13.1}/README.md +0 -0
  12. {typedal-3.12.2 → typedal-3.13.1}/coverage.svg +0 -0
  13. {typedal-3.12.2 → typedal-3.13.1}/docs/1_getting_started.md +0 -0
  14. {typedal-3.12.2 → typedal-3.13.1}/docs/2_defining_tables.md +0 -0
  15. {typedal-3.12.2 → typedal-3.13.1}/docs/3_building_queries.md +0 -0
  16. {typedal-3.12.2 → typedal-3.13.1}/docs/4_relationships.md +0 -0
  17. {typedal-3.12.2 → typedal-3.13.1}/docs/5_py4web.md +0 -0
  18. {typedal-3.12.2 → typedal-3.13.1}/docs/6_migrations.md +0 -0
  19. {typedal-3.12.2 → typedal-3.13.1}/docs/7_mixins.md +0 -0
  20. {typedal-3.12.2 → typedal-3.13.1}/docs/css/code_blocks.css +0 -0
  21. {typedal-3.12.2 → typedal-3.13.1}/docs/index.md +0 -0
  22. {typedal-3.12.2 → typedal-3.13.1}/docs/requirements.txt +0 -0
  23. {typedal-3.12.2 → typedal-3.13.1}/example_new.py +0 -0
  24. {typedal-3.12.2 → typedal-3.13.1}/example_old.py +0 -0
  25. {typedal-3.12.2 → typedal-3.13.1}/mkdocs.yml +0 -0
  26. {typedal-3.12.2 → typedal-3.13.1}/pyproject.toml +0 -0
  27. {typedal-3.12.2 → typedal-3.13.1}/src/typedal/__init__.py +0 -0
  28. {typedal-3.12.2 → typedal-3.13.1}/src/typedal/caching.py +0 -0
  29. {typedal-3.12.2 → typedal-3.13.1}/src/typedal/cli.py +0 -0
  30. {typedal-3.12.2 → typedal-3.13.1}/src/typedal/config.py +0 -0
  31. {typedal-3.12.2 → typedal-3.13.1}/src/typedal/fields.py +0 -0
  32. {typedal-3.12.2 → typedal-3.13.1}/src/typedal/for_py4web.py +0 -0
  33. {typedal-3.12.2 → typedal-3.13.1}/src/typedal/for_web2py.py +0 -0
  34. {typedal-3.12.2 → typedal-3.13.1}/src/typedal/mixins.py +0 -0
  35. {typedal-3.12.2 → typedal-3.13.1}/src/typedal/py.typed +0 -0
  36. {typedal-3.12.2 → typedal-3.13.1}/src/typedal/serializers/as_json.py +0 -0
  37. {typedal-3.12.2 → typedal-3.13.1}/src/typedal/types.py +0 -0
  38. {typedal-3.12.2 → typedal-3.13.1}/src/typedal/web2py_py4web_shared.py +0 -0
  39. {typedal-3.12.2 → typedal-3.13.1}/tests/__init__.py +0 -0
  40. {typedal-3.12.2 → typedal-3.13.1}/tests/configs/simple.toml +0 -0
  41. {typedal-3.12.2 → typedal-3.13.1}/tests/configs/valid.env +0 -0
  42. {typedal-3.12.2 → typedal-3.13.1}/tests/configs/valid.toml +0 -0
  43. {typedal-3.12.2 → typedal-3.13.1}/tests/test_cli.py +0 -0
  44. {typedal-3.12.2 → typedal-3.13.1}/tests/test_config.py +0 -0
  45. {typedal-3.12.2 → typedal-3.13.1}/tests/test_docs_examples.py +0 -0
  46. {typedal-3.12.2 → typedal-3.13.1}/tests/test_helpers.py +0 -0
  47. {typedal-3.12.2 → typedal-3.13.1}/tests/test_json.py +0 -0
  48. {typedal-3.12.2 → typedal-3.13.1}/tests/test_mixins.py +0 -0
  49. {typedal-3.12.2 → typedal-3.13.1}/tests/test_mypy.py +0 -0
  50. {typedal-3.12.2 → typedal-3.13.1}/tests/test_orm.py +0 -0
  51. {typedal-3.12.2 → typedal-3.13.1}/tests/test_py4web.py +0 -0
  52. {typedal-3.12.2 → typedal-3.13.1}/tests/test_relationships.py +0 -0
  53. {typedal-3.12.2 → typedal-3.13.1}/tests/test_row.py +0 -0
  54. {typedal-3.12.2 → typedal-3.13.1}/tests/test_stats.py +0 -0
  55. {typedal-3.12.2 → typedal-3.13.1}/tests/test_table.py +0 -0
  56. {typedal-3.12.2 → typedal-3.13.1}/tests/test_web2py.py +0 -0
  57. {typedal-3.12.2 → typedal-3.13.1}/tests/test_xx_others.py +0 -0
  58. {typedal-3.12.2 → typedal-3.13.1}/tests/timings.py +0 -0
@@ -2,6 +2,18 @@
2
2
 
3
3
  <!--next-version-placeholder-->
4
4
 
5
+ ## v3.13.1 (2025-04-28)
6
+
7
+ ### Fix
8
+
9
+ * Pass select kwargs via `.column()` - so you can do e.g. `distinct=True` ([`e5bc168`](https://github.com/trialandsuccess/TypeDAL/commit/e5bc168b90d6a5d214f049de0ef31e544214cc23))
10
+
11
+ ## v3.13.0 (2025-04-28)
12
+
13
+ ### Feature
14
+
15
+ * Adding `_once` hooks ([`a69fbb3`](https://github.com/trialandsuccess/TypeDAL/commit/a69fbb361cdfec9352fca503206299c5bbb940d2))
16
+
5
17
  ## v3.12.2 (2025-04-25)
6
18
 
7
19
  ### Fix
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.3
2
2
  Name: TypeDAL
3
- Version: 3.12.2
3
+ Version: 3.13.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
@@ -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.12.2"
8
+ __version__ = "3.13.1"
@@ -5,6 +5,7 @@ Core functionality of TypeDAL.
5
5
  import contextlib
6
6
  import csv
7
7
  import datetime as dt
8
+ import functools
8
9
  import inspect
9
10
  import json
10
11
  import math
@@ -33,6 +34,7 @@ from .helpers import (
33
34
  all_annotations,
34
35
  all_dict,
35
36
  as_lambda,
37
+ classproperty,
36
38
  extract_type_optional,
37
39
  filter_out,
38
40
  instanciate,
@@ -796,6 +798,10 @@ class TypeDAL(pydal.DAL): # type: ignore
796
798
  return to_snake(camel)
797
799
 
798
800
 
801
+ P = typing.ParamSpec("P")
802
+ R = typing.TypeVar("R")
803
+
804
+
799
805
  class TableMeta(type):
800
806
  """
801
807
  This metaclass contains functionality on table classes, that doesn't exist on its instances.
@@ -1012,6 +1018,15 @@ class TableMeta(type):
1012
1018
  """
1013
1019
  return QueryBuilder(self).select(*a, **kw)
1014
1020
 
1021
+ def column(self: Type[T_MetaInstance], field: "TypedField[T] | T", **options: Unpack[SelectKwargs]) -> list[T]:
1022
+ """
1023
+ Get all values in a specific column.
1024
+
1025
+ Shortcut for `.select(field).execute().column(field)`.
1026
+ """
1027
+
1028
+ return QueryBuilder(self).select(field, **options).execute().column(field)
1029
+
1015
1030
  def paginate(self: Type[T_MetaInstance], limit: int, page: int = 1) -> "PaginatedRows[T_MetaInstance]":
1016
1031
  """
1017
1032
  See QueryBuilder.paginate!
@@ -1196,6 +1211,19 @@ class TableMeta(type):
1196
1211
  return self.with_alias(key)
1197
1212
 
1198
1213
  # hooks:
1214
+ def _hook_once(
1215
+ cls: Type[T_MetaInstance], hooks: list[typing.Callable[P, R]], fn: typing.Callable[P, R]
1216
+ ) -> Type[T_MetaInstance]:
1217
+ @functools.wraps(fn)
1218
+ def wraps(*a: P.args, **kw: P.kwargs) -> R:
1219
+ try:
1220
+ return fn(*a, **kw)
1221
+ finally:
1222
+ hooks.remove(wraps)
1223
+
1224
+ hooks.append(wraps)
1225
+ return cls
1226
+
1199
1227
  def before_insert(
1200
1228
  cls: Type[T_MetaInstance],
1201
1229
  fn: typing.Callable[[T_MetaInstance], Optional[bool]] | typing.Callable[[OpRow], Optional[bool]],
@@ -1207,6 +1235,15 @@ class TableMeta(type):
1207
1235
  cls._before_insert.append(fn)
1208
1236
  return cls
1209
1237
 
1238
+ def before_insert_once(
1239
+ cls: Type[T_MetaInstance],
1240
+ fn: typing.Callable[[T_MetaInstance], Optional[bool]] | typing.Callable[[OpRow], Optional[bool]],
1241
+ ) -> Type[T_MetaInstance]:
1242
+ """
1243
+ Add a before insert hook that only fires once and then removes itself.
1244
+ """
1245
+ return cls._hook_once(cls._before_insert, fn) # type: ignore
1246
+
1210
1247
  def after_insert(
1211
1248
  cls: Type[T_MetaInstance],
1212
1249
  fn: (
@@ -1221,6 +1258,18 @@ class TableMeta(type):
1221
1258
  cls._after_insert.append(fn)
1222
1259
  return cls
1223
1260
 
1261
+ def after_insert_once(
1262
+ cls: Type[T_MetaInstance],
1263
+ fn: (
1264
+ typing.Callable[[T_MetaInstance, Reference], Optional[bool]]
1265
+ | typing.Callable[[OpRow, Reference], Optional[bool]]
1266
+ ),
1267
+ ) -> Type[T_MetaInstance]:
1268
+ """
1269
+ Add an after insert hook that only fires once and then removes itself.
1270
+ """
1271
+ return cls._hook_once(cls._after_insert, fn) # type: ignore
1272
+
1224
1273
  def before_update(
1225
1274
  cls: Type[T_MetaInstance],
1226
1275
  fn: typing.Callable[[Set, T_MetaInstance], Optional[bool]] | typing.Callable[[Set, OpRow], Optional[bool]],
@@ -1232,6 +1281,15 @@ class TableMeta(type):
1232
1281
  cls._before_update.append(fn)
1233
1282
  return cls
1234
1283
 
1284
+ def before_update_once(
1285
+ cls,
1286
+ fn: typing.Callable[[Set, T_MetaInstance], Optional[bool]] | typing.Callable[[Set, OpRow], Optional[bool]],
1287
+ ) -> Type[T_MetaInstance]:
1288
+ """
1289
+ Add a before update hook that only fires once and then removes itself.
1290
+ """
1291
+ return cls._hook_once(cls._before_update, fn) # type: ignore
1292
+
1235
1293
  def after_update(
1236
1294
  cls: Type[T_MetaInstance],
1237
1295
  fn: typing.Callable[[Set, T_MetaInstance], Optional[bool]] | typing.Callable[[Set, OpRow], Optional[bool]],
@@ -1243,6 +1301,15 @@ class TableMeta(type):
1243
1301
  cls._after_update.append(fn)
1244
1302
  return cls
1245
1303
 
1304
+ def after_update_once(
1305
+ cls: Type[T_MetaInstance],
1306
+ fn: typing.Callable[[Set, T_MetaInstance], Optional[bool]] | typing.Callable[[Set, OpRow], Optional[bool]],
1307
+ ) -> Type[T_MetaInstance]:
1308
+ """
1309
+ Add an after update hook that only fires once and then removes itself.
1310
+ """
1311
+ return cls._hook_once(cls._after_update, fn) # type: ignore
1312
+
1246
1313
  def before_delete(cls: Type[T_MetaInstance], fn: typing.Callable[[Set], Optional[bool]]) -> Type[T_MetaInstance]:
1247
1314
  """
1248
1315
  Add a before delete hook.
@@ -1251,6 +1318,15 @@ class TableMeta(type):
1251
1318
  cls._before_delete.append(fn)
1252
1319
  return cls
1253
1320
 
1321
+ def before_delete_once(
1322
+ cls: Type[T_MetaInstance],
1323
+ fn: typing.Callable[[Set], Optional[bool]],
1324
+ ) -> Type[T_MetaInstance]:
1325
+ """
1326
+ Add a before delete hook that only fires once and then removes itself.
1327
+ """
1328
+ return cls._hook_once(cls._before_delete, fn)
1329
+
1254
1330
  def after_delete(cls: Type[T_MetaInstance], fn: typing.Callable[[Set], Optional[bool]]) -> Type[T_MetaInstance]:
1255
1331
  """
1256
1332
  Add an after delete hook.
@@ -1259,6 +1335,15 @@ class TableMeta(type):
1259
1335
  cls._after_delete.append(fn)
1260
1336
  return cls
1261
1337
 
1338
+ def after_delete_once(
1339
+ cls: Type[T_MetaInstance],
1340
+ fn: typing.Callable[[Set], Optional[bool]],
1341
+ ) -> Type[T_MetaInstance]:
1342
+ """
1343
+ Add an after delete hook that only fires once and then removes itself.
1344
+ """
1345
+ return cls._hook_once(cls._after_delete, fn)
1346
+
1262
1347
 
1263
1348
  class TypedField(Expression, typing.Generic[T_Value]): # pragma: no cover
1264
1349
  """
@@ -1482,6 +1567,17 @@ class _TypedTable:
1482
1567
  where you need a reference to the current database, which may not exist yet when defining the model.
1483
1568
  """
1484
1569
 
1570
+ @classproperty
1571
+ def _hooks(cls) -> dict[str, list[typing.Callable[..., Optional[bool]]]]:
1572
+ return {
1573
+ "before_insert": cls._before_insert,
1574
+ "after_insert": cls._after_insert,
1575
+ "before_update": cls._before_update,
1576
+ "after_update": cls._after_update,
1577
+ "before_delete": cls._before_delete,
1578
+ "after_delete": cls._after_delete,
1579
+ }
1580
+
1485
1581
 
1486
1582
  class TypedTable(_TypedTable, metaclass=TableMeta):
1487
1583
  """
@@ -2608,24 +2704,25 @@ class QueryBuilder(typing.Generic[T_MetaInstance]):
2608
2704
  return save_to_cache(typed_rows, rows)
2609
2705
 
2610
2706
  @typing.overload
2611
- def column(self, field: TypedField[T]) -> list[T]:
2707
+ def column(self, field: TypedField[T], **options: Unpack[SelectKwargs]) -> list[T]:
2612
2708
  """
2613
2709
  If a typedfield is passed, the output type can be safely determined.
2614
2710
  """
2615
2711
 
2616
2712
  @typing.overload
2617
- def column(self, field: T) -> list[T]:
2713
+ def column(self, field: T, **options: Unpack[SelectKwargs]) -> list[T]:
2618
2714
  """
2619
2715
  Otherwise, the output type is loosely determined (assumes `field: type` or Any).
2620
2716
  """
2621
2717
 
2622
- def column(self, field: TypedField[T] | T) -> list[T]:
2718
+ def column(self, field: TypedField[T] | T, **options: Unpack[SelectKwargs]) -> list[T]:
2623
2719
  """
2624
2720
  Get all values in a specific column.
2625
2721
 
2626
2722
  Shortcut for `.select(field).execute().column(field)`.
2627
2723
  """
2628
- return self.select(field).execute().column(field)
2724
+
2725
+ return self.select(field, **options).execute().column(field)
2629
2726
 
2630
2727
  def _handle_relationships_pre_select(
2631
2728
  self,
@@ -303,3 +303,27 @@ def get_field(field: "TypedField[typing.Any] | Field") -> "Field":
303
303
  "Field",
304
304
  field, # Table.field already is a Field, but cast to make sure the editor knows this too.
305
305
  )
306
+
307
+
308
+ class classproperty:
309
+ def __init__(self, fget: typing.Callable[..., typing.Any]) -> None:
310
+ """
311
+ Initialize the classproperty.
312
+
313
+ Args:
314
+ fget: A function that takes the class as an argument and returns a value.
315
+ """
316
+ self.fget = fget
317
+
318
+ def __get__(self, obj: typing.Any, owner: typing.Type[T]) -> typing.Any:
319
+ """
320
+ Retrieve the property value.
321
+
322
+ Args:
323
+ obj: The instance of the class (unused).
324
+ owner: The class that owns the property.
325
+
326
+ Returns:
327
+ The value returned by the function.
328
+ """
329
+ return self.fget(owner)
@@ -510,10 +510,49 @@ def test_hooks_duplicates():
510
510
  HookedTableV3.after_insert(increase_counter_v2) # other hash
511
511
 
512
512
  HookedTableV3.insert(name="Should increase counter twice")
513
+ assert counter == 3
514
+
515
+ for hook in HookedTableV3._hooks.values():
516
+ hook.clear()
513
517
 
518
+ HookedTableV3.insert(name="Should NOT increase counter")
514
519
  assert counter == 3
515
520
 
516
521
 
522
+ def test_hooks_once():
523
+ @db.define()
524
+ class HookedTableV4(TypedTable):
525
+ name: str
526
+
527
+ counter = 0
528
+
529
+ def increase_counter_v2(_, __=None):
530
+ nonlocal counter
531
+ counter += 1
532
+
533
+ HookedTableV4.before_insert_once(increase_counter_v2)
534
+ HookedTableV4.after_insert_once(increase_counter_v2)
535
+ HookedTableV4.before_update_once(increase_counter_v2)
536
+ HookedTableV4.after_update_once(increase_counter_v2)
537
+ HookedTableV4.before_delete_once(increase_counter_v2)
538
+ HookedTableV4.after_delete_once(increase_counter_v2)
539
+
540
+ assert counter == 0
541
+
542
+ HookedTableV4.insert(name="1")
543
+ assert counter == 2
544
+ row = HookedTableV4.insert(name="2")
545
+ assert counter == 2
546
+
547
+ row.update_record(name="3")
548
+ assert counter == 4
549
+ row.update_record(name="4")
550
+ assert counter == 4
551
+
552
+ row.delete_record()
553
+ assert counter == 6
554
+
555
+
517
556
  def test_try():
518
557
  class SomeTableToRetry(TypedTable):
519
558
  key: int
@@ -483,6 +483,8 @@ def test_column():
483
483
  assert len(rows) == 4
484
484
  assert set(rows) == {33}
485
485
 
486
+ assert TestRelationship.column(TestRelationship.value, distinct=True, orderby=~TestRelationship.value) == [33, 3]
487
+
486
488
 
487
489
  def test_collect_with_extra_fields():
488
490
  _setup_data()
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes