TypeDAL 5.0.6__tar.gz → 5.1.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.
Files changed (29) hide show
  1. {typedal-5.0.6 → typedal-5.1.1}/PKG-INFO +1 -1
  2. {typedal-5.0.6 → typedal-5.1.1}/pyproject.toml +3 -3
  3. {typedal-5.0.6 → typedal-5.1.1}/pyproject.toml.orig +3 -3
  4. {typedal-5.0.6 → typedal-5.1.1}/src/typedal/asynchronous.py +1 -0
  5. {typedal-5.0.6 → typedal-5.1.1}/src/typedal/cli.py +2 -1
  6. {typedal-5.0.6 → typedal-5.1.1}/src/typedal/config.py +1 -1
  7. {typedal-5.0.6 → typedal-5.1.1}/src/typedal/core.py +8 -3
  8. {typedal-5.0.6 → typedal-5.1.1}/src/typedal/define.py +4 -1
  9. {typedal-5.0.6 → typedal-5.1.1}/src/typedal/mixins.py +1 -0
  10. {typedal-5.0.6 → typedal-5.1.1}/src/typedal/query_builder.py +82 -5
  11. {typedal-5.0.6 → typedal-5.1.1}/src/typedal/relationships.py +6 -3
  12. {typedal-5.0.6 → typedal-5.1.1}/src/typedal/serializers/typescript.py +2 -1
  13. {typedal-5.0.6 → typedal-5.1.1}/src/typedal/tables.py +23 -0
  14. {typedal-5.0.6 → typedal-5.1.1}/src/typedal/types.py +2 -0
  15. typedal-5.1.1/src/typedal/warnings.py +9 -0
  16. {typedal-5.0.6 → typedal-5.1.1}/README.md +0 -0
  17. {typedal-5.0.6 → typedal-5.1.1}/src/typedal/__about__.py +0 -0
  18. {typedal-5.0.6 → typedal-5.1.1}/src/typedal/__init__.py +0 -0
  19. {typedal-5.0.6 → typedal-5.1.1}/src/typedal/caching.py +0 -0
  20. {typedal-5.0.6 → typedal-5.1.1}/src/typedal/constants.py +0 -0
  21. {typedal-5.0.6 → typedal-5.1.1}/src/typedal/enum_helpers.py +0 -0
  22. {typedal-5.0.6 → typedal-5.1.1}/src/typedal/fields.py +0 -0
  23. {typedal-5.0.6 → typedal-5.1.1}/src/typedal/for_py4web.py +0 -0
  24. {typedal-5.0.6 → typedal-5.1.1}/src/typedal/for_web2py.py +0 -0
  25. {typedal-5.0.6 → typedal-5.1.1}/src/typedal/helpers.py +0 -0
  26. {typedal-5.0.6 → typedal-5.1.1}/src/typedal/py.typed +0 -0
  27. {typedal-5.0.6 → typedal-5.1.1}/src/typedal/rows.py +0 -0
  28. {typedal-5.0.6 → typedal-5.1.1}/src/typedal/serializers/as_json.py +0 -0
  29. {typedal-5.0.6 → typedal-5.1.1}/src/typedal/web2py_py4web_shared.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: TypeDAL
3
- Version: 5.0.6
3
+ Version: 5.1.1
4
4
  Summary: Typing support for PyDAL
5
5
  Keywords:
6
6
  Author: Robin van der Noord
@@ -6,7 +6,7 @@ build-backend = "uv_build"
6
6
  name = "TypeDAL"
7
7
  description = "Typing support for PyDAL"
8
8
  readme = "README.md"
9
- version = "5.0.6"
9
+ version = "5.1.1"
10
10
  requires-python = ">=3.12"
11
11
  license-expression = "MIT"
12
12
  keywords = []
@@ -164,7 +164,7 @@ badge = true
164
164
  mypy = "--disable-error-code misc"
165
165
 
166
166
  [tool.black]
167
- target-version = ["py314"]
167
+ target-version = ["py312"]
168
168
  line-length = 120
169
169
  extend-exclude = '''
170
170
  # A regex preceded with ^/ will apply only to files and directories
@@ -199,7 +199,7 @@ exclude = [
199
199
  ]
200
200
 
201
201
  [tool.ruff]
202
- target-version = "py314"
202
+ target-version = "py312"
203
203
  line-length = 120
204
204
  extend-exclude = [
205
205
  "*.bak/",
@@ -6,7 +6,7 @@ build-backend = "uv_build"
6
6
  name = "TypeDAL"
7
7
  description = 'Typing support for PyDAL'
8
8
  readme = "README.md"
9
- version = "5.0.6"
9
+ version = "5.1.1"
10
10
  requires-python = ">=3.12"
11
11
  license-expression = "MIT"
12
12
  keywords = []
@@ -181,7 +181,7 @@ badge = true
181
181
  mypy = "--disable-error-code misc"
182
182
 
183
183
  [tool.black]
184
- target-version = ["py314"]
184
+ target-version = ["py312"]
185
185
  line-length = 120
186
186
  # 'extend-exclude' excludes files or directories in addition to the defaults
187
187
  extend-exclude = '''
@@ -229,7 +229,7 @@ strict = true
229
229
  exclude = ["venv", ".bak"]
230
230
 
231
231
  [tool.ruff]
232
- target-version = "py314"
232
+ target-version = "py312"
233
233
  line-length = 120
234
234
 
235
235
  extend-exclude = ["*.bak/", "venv*/", "tests/", "example_*.py"]
@@ -434,6 +434,7 @@ class BlockingAccessHandler(ExecutionHandler):
434
434
  f"the extra query. command: {command}",
435
435
  category=BlockingDatabaseAccessWarning,
436
436
  skip_file_prefixes=_SKIP_FRAMES,
437
+ stacklevel=2,
437
438
  )
438
439
 
439
440
 
@@ -30,6 +30,7 @@ except ImportError as e:
30
30
  "to fix this.",
31
31
  source=e,
32
32
  category=RuntimeWarning,
33
+ stacklevel=2,
33
34
  )
34
35
  exit(127) # command not found
35
36
 
@@ -226,7 +227,7 @@ def setup(
226
227
  data.pop("connection", None)
227
228
 
228
229
  # ignore any None:
229
- old_contents["tool"]["typedal"] = {k: v for k, v in data.items() if v is not None}
230
+ old_contents["tool"]["typedal"] = {k: v for k, v in data.items() if v is not None} # type: ignore
230
231
 
231
232
  with toml_path.open("w") as f:
232
233
  tomlkit.dump(old_contents, f)
@@ -159,7 +159,7 @@ def _load_toml(path: str | bool | Path | None = True) -> tuple[str, AnyDict]:
159
159
 
160
160
  return str(toml_path) or "", t.cast(AnyDict, data["tool"]["typedal"])
161
161
  except Exception as e:
162
- warnings.warn(f"Could not load typedal config toml: {e}", source=e)
162
+ warnings.warn(f"Could not load typedal config toml: {e}", source=e, stacklevel=2)
163
163
  return str(toml_path) or "", {}
164
164
 
165
165
 
@@ -53,8 +53,8 @@ def _expression_subclasses() -> t.Iterator[type]:
53
53
  """
54
54
  Yield Expression and every (nested) subclass currently loaded, e.g. Field and TypedField.
55
55
  """
56
- seen = {Expression}
57
- stack = [Expression]
56
+ seen: set[type[Expression]] = {Expression}
57
+ stack: list[type[Expression]] = [Expression]
58
58
  while stack:
59
59
  for subclass in stack.pop().__subclasses__():
60
60
  if subclass not in seen:
@@ -476,7 +476,12 @@ class TypeDAL(_TypeDALBase):
476
476
  delattr(self, tablename)
477
477
 
478
478
  if verbose:
479
- warnings.warn(f"{model} could not be migrated, try faking", source=e, category=RuntimeWarning)
479
+ warnings.warn(
480
+ f"{model} could not be migrated, try faking",
481
+ source=e,
482
+ category=RuntimeWarning,
483
+ stacklevel=2,
484
+ )
480
485
 
481
486
  # try again:
482
487
  return self.define(model, migrate=self._migrate, fake_migrate=self._migrate, redefine=True)
@@ -123,7 +123,10 @@ class TableDefinitionBuilder:
123
123
  self.class_map[table._rname] = cls # table_model - sql name
124
124
  cls.__on_define__(self.db)
125
125
  else:
126
- warnings.warn("db.define used without inheriting TypedTable. This could lead to strange problems!")
126
+ warnings.warn(
127
+ "db.define used without inheriting TypedTable. This could lead to strange problems!",
128
+ stacklevel=2,
129
+ )
127
130
 
128
131
  if not tablename.startswith("typedal_") and cache_dependency:
129
132
  from .caching import _remove_cache, remove_cache_for_table
@@ -180,6 +180,7 @@ class SlugMixin(Mixin):
180
180
  warnings.warn(
181
181
  "The 'slug_suffix' option is deprecated, use 'slug_suffix_length' instead.",
182
182
  DeprecationWarning,
183
+ stacklevel=2,
183
184
  )
184
185
 
185
186
  slug_suffix = slug_suffix_length or kw.get("slug_suffix", 0)
@@ -8,6 +8,7 @@ import datetime as dt
8
8
  import math
9
9
  import time
10
10
  import typing as t
11
+ import warnings
11
12
  from collections import defaultdict
12
13
 
13
14
  from pydal.helpers.classes import SQLALL
@@ -45,6 +46,19 @@ from .types import (
45
46
  merge_permissions,
46
47
  require_permission,
47
48
  )
49
+ from .warnings import NoopQueryWarning, UnusedWindowWarning
50
+
51
+
52
+ def warn_noop(method: str) -> None:
53
+ """
54
+ Warn that `method` was called without arguments, which has no effect.
55
+ """
56
+ # stacklevel: 1 = here, 2 = the query builder method, 3 = the caller.
57
+ warnings.warn(
58
+ f"`.{method}()` without arguments does nothing. You can remove this call.",
59
+ stacklevel=3,
60
+ category=NoopQueryWarning,
61
+ )
48
62
 
49
63
 
50
64
  class QueryBuilder[T_MetaInstance: _TypedTable](Select):
@@ -173,6 +187,10 @@ class QueryBuilder[T_MetaInstance: _TypedTable](Select):
173
187
  """
174
188
  Return a clone of this builder with permission overrides merged in.
175
189
  """
190
+ if not permissions and self:
191
+ warn_noop("permissions")
192
+ return self
193
+
176
194
  return self._extend(permissions=permissions)
177
195
 
178
196
  def _normalize_select_option(
@@ -214,8 +232,15 @@ class QueryBuilder[T_MetaInstance: _TypedTable](Select):
214
232
  join: othertable.on(query) - do an INNER JOIN. Using TypeDAL relationships with .join() is recommended!
215
233
  left: othertable.on(query) - do a LEFT JOIN. Using TypeDAL relationships with .join() is recommended!
216
234
  cache: cache the query result to speed up repeated queries; e.g. (cache=(cache.ram, 3600), cacheable=True)
235
+
236
+ Calling this without any fields or options on a builder that already has settings
237
+ does nothing and emits a NoopQueryWarning.
217
238
  """
218
239
 
240
+ if not fields and not options and self:
241
+ warn_noop("select")
242
+ return self
243
+
219
244
  for key in ("distinct",):
220
245
  if options.get(key):
221
246
  options[key] = self._normalize_select_option(options[key])
@@ -280,7 +305,15 @@ class QueryBuilder[T_MetaInstance: _TypedTable](Select):
280
305
  .where(lambda table: table.id == 5).where(lambda table: table.id == 6) == (table.id == 5) & (table.id=6)
281
306
  When passing multiple queries to a single .where, they will be ORed:
282
307
  .where(lambda table: table.id == 5, lambda table: table.id == 6) == (table.id == 5) | (table.id=6)
308
+
309
+ Calling this without any arguments on a builder that already has settings
310
+ does nothing and emits a NoopQueryWarning.
311
+ Starting an empty builder (e.g. `Model.where()`) is allowed and stays silent.
283
312
  """
313
+ if not queries_or_lambdas and not filters and self:
314
+ warn_noop("where")
315
+ return self
316
+
284
317
  new_query = self.query
285
318
  table = self._ensure_table_defined()
286
319
 
@@ -296,7 +329,7 @@ class QueryBuilder[T_MetaInstance: _TypedTable](Select):
296
329
  elif isinstance(query_part, (Query, Expression)):
297
330
  subquery |= t.cast(Query, query_part)
298
331
  elif callable(query_part):
299
- if result := query_part(self.model): # ty: ignore[call-top-callable]
332
+ if result := query_part(self.model):
300
333
  subquery |= result
301
334
  elif isinstance(query_part, dict):
302
335
  subsubquery = DummyQuery()
@@ -662,6 +695,17 @@ class QueryBuilder[T_MetaInstance: _TypedTable](Select):
662
695
 
663
696
  query, select_args, select_kwargs = self._before_query(metadata, add_id=add_id)
664
697
 
698
+ window_size = metadata.get("window_size", None)
699
+ is_iterating = metadata.get("iterating", False)
700
+
701
+ if window_size is not None and is_iterating is not True:
702
+ warnings.warn(
703
+ "A window size was configured, but collect() was called directly. "
704
+ "Use iteration over the query builder to fetch rows in windows.",
705
+ stacklevel=2,
706
+ category=UnusedWindowWarning,
707
+ )
708
+
665
709
  metadata["sql"] = db(query)._select(*select_args, **select_kwargs)
666
710
 
667
711
  if verbose: # pragma: no cover
@@ -1222,13 +1266,40 @@ class QueryBuilder[T_MetaInstance: _TypedTable](Select):
1222
1266
  """
1223
1267
  You can start iterating a Query Builder object before calling collect, for ease of use.
1224
1268
  """
1225
- yield from self.collect()
1269
+ builder = self._extend(metadata={"iterating": True})
1270
+ window_size = self.metadata.get("window_size", None)
1271
+
1272
+ if not window_size:
1273
+ yield from builder.collect()
1274
+ return
1275
+
1276
+ for chunk in builder.chunk(window_size):
1277
+ yield from chunk
1278
+
1279
+ def __await__(self):
1280
+ return self.collect_async().__await__()
1281
+
1282
+ async def __aiter__(self):
1283
+ builder = self._extend(metadata={"iterating": True})
1284
+
1285
+ window_size = self.metadata.get("window_size", None)
1286
+
1287
+ if window_size:
1288
+ async for chunk in builder.chunk_async(window_size):
1289
+ for row in chunk:
1290
+ yield row
1291
+ else:
1292
+ rows = await builder.collect_async()
1293
+ for row in rows:
1294
+ yield row
1295
+
1296
+ def window(self, window_size: int) -> QueryBuilder[T_MetaInstance]:
1297
+ return self._extend(metadata={"window_size": window_size})
1226
1298
 
1227
1299
  def __count(
1228
1300
  self,
1229
1301
  db: TypeDAL,
1230
1302
  distinct: t.Optional[bool] = None,
1231
- *,
1232
1303
  include_left_for_distinct: bool = True,
1233
1304
  ) -> Query:
1234
1305
  # internal, shared logic between .count and ._count
@@ -1351,7 +1422,10 @@ class QueryBuilder[T_MetaInstance: _TypedTable](Select):
1351
1422
  pass
1352
1423
  ```
1353
1424
  """
1354
- require_permission(self._permissions, "read")
1425
+ # require_permission checked in .collect()
1426
+ if "limitby" in self.select_kwargs:
1427
+ raise ValueError("chunk() cannot be combined with an existing limitby")
1428
+
1355
1429
  page = 1
1356
1430
 
1357
1431
  while rows := self.__paginate(chunk_size, page).collect():
@@ -1506,7 +1580,10 @@ class QueryBuilder[T_MetaInstance: _TypedTable](Select):
1506
1580
  writer can be visible halfway through the iteration; wrap it in `db.session()` if you
1507
1581
  need every page to come from the same snapshot.
1508
1582
  """
1509
- require_permission(self._permissions, "read")
1583
+ # require_permission checked in .collect()
1584
+ if "limitby" in self.select_kwargs:
1585
+ raise ValueError("chunk_async() cannot be combined with an existing limitby")
1586
+
1510
1587
  db = self._get_db()
1511
1588
  page = 1
1512
1589
 
@@ -110,7 +110,7 @@ class Relationship[To_Type]:
110
110
 
111
111
  @staticmethod
112
112
  def _error_duplicate_condition(condition: Condition, on: OnQuery) -> t.Never:
113
- warnings.warn(f"Relation | Both specified! {condition=} {on=}")
113
+ warnings.warn(f"Relation | Both specified! {condition=} {on=}", stacklevel=2)
114
114
  raise ValueError("Please specify either a condition or an 'on' statement for this relationship!")
115
115
 
116
116
  def __repr__(self) -> str:
@@ -278,6 +278,7 @@ class Relationship[To_Type]:
278
278
  f"Trying to access relationship '{self.name}' without joining. "
279
279
  f"Did you forget to use .join('{self.name}')? Returning empty value.",
280
280
  category=RuntimeWarning,
281
+ stacklevel=2,
281
282
  )
282
283
  return fallback_value
283
284
 
@@ -296,6 +297,7 @@ class Relationship[To_Type]:
296
297
  "This performs an extra database query. "
297
298
  f"Consider using .join('{self.name}') for better performance.",
298
299
  category=RuntimeWarning,
300
+ stacklevel=2,
299
301
  )
300
302
 
301
303
  return builder.first()[self.name] # type: ignore
@@ -304,6 +306,7 @@ class Relationship[To_Type]:
304
306
  f"Failed to lazy load relationship '{self.name}': {e}",
305
307
  category=RuntimeWarning,
306
308
  source=e,
309
+ stacklevel=2,
307
310
  )
308
311
 
309
312
  return fallback_value
@@ -501,12 +504,12 @@ def to_relationship(
501
504
  try:
502
505
  condition = _generate_relationship_condition(cls, key, field)
503
506
  except Exception as e: # pragma: no cover
504
- warnings.warn("Could not generate Relationship condition", source=e)
507
+ warnings.warn("Could not generate Relationship condition", source=e, stacklevel=2)
505
508
  condition = None
506
509
 
507
510
  if not condition: # pragma: no cover
508
511
  # something went wrong, not a valid relationship
509
- warnings.warn(f"Invalid relationship for {cls.__name__}.{key}: {field}")
512
+ warnings.warn(f"Invalid relationship for {cls.__name__}.{key}: {field}", stacklevel=2)
510
513
  return None
511
514
 
512
515
  join = "left" if optional or t.get_origin(field) is list else "inner"
@@ -31,7 +31,7 @@ class TypedDictRegistry(Singleton):
31
31
  def __init__(self) -> None:
32
32
  """Initialize the singleton registry and optional shared typtyp world."""
33
33
  self._types: dict[type, type[dict[str, t.Any]]] = {}
34
- self._world = typtyp.World() if typtyp else None
34
+ self._world = typtyp.World() if typtyp else None # type: ignore
35
35
  self._names: set[str] = set()
36
36
 
37
37
  @property
@@ -78,6 +78,7 @@ class TypedDictRegistry(Singleton):
78
78
  if world is None: # pragma: ignore
79
79
  warnings.warn(
80
80
  f"`{caller_name}` can not be used without the typescript extra. Please install `typedal[typescript]`",
81
+ stacklevel=2,
81
82
  )
82
83
  return ""
83
84
 
@@ -138,6 +138,23 @@ class TableMeta(type):
138
138
  self._relationships = None
139
139
  self._permissions = None
140
140
 
141
+ def __getattribute__(self, col: str) -> t.Any:
142
+ """
143
+ Resolve bound table fields before metaclass convenience methods.
144
+
145
+ Annotation-only fields are not stored in the model class dictionary, so
146
+ normal class lookup would otherwise return a same-named method defined
147
+ on this metaclass. For example, a model may have a 'window' column
148
+ while this metaclass also provides 'window()'. The column takes
149
+ precedence on the model class; use 'QueryBuilder(Model).window()' to
150
+ call the query helper in that case.
151
+ """
152
+ table = type.__getattribute__(self, "_table")
153
+ if table is not None and col in table._fields:
154
+ return table[col]
155
+
156
+ return type.__getattribute__(self, col)
157
+
141
158
  def __getattr__(self, col: str) -> t.Optional[Field]:
142
159
  """
143
160
  Magic method used by TypedTableMeta to get a database field with dot notation on a class.
@@ -465,6 +482,12 @@ class TableMeta(type):
465
482
  """
466
483
  return QueryBuilder(self).join(*fields, on=on, condition=condition, method=method, condition_and=condition_and)
467
484
 
485
+ def window(self: t.Type[T_MetaInstance], window_size: int) -> "QueryBuilder[T_MetaInstance]":
486
+ """
487
+ See QueryBuilder.window!
488
+ """
489
+ return QueryBuilder(self).window(window_size)
490
+
468
491
  def collect(self: t.Type[T_MetaInstance], verbose: bool = False) -> "TypedRows[T_MetaInstance]":
469
492
  """
470
493
  See QueryBuilder.collect!
@@ -313,6 +313,8 @@ class Metadata(t.TypedDict):
313
313
  select_duration: t.NotRequired[float]
314
314
  relationships: t.NotRequired[set[str]]
315
315
  sql: t.NotRequired[str]
316
+ window_size: t.NotRequired[int]
317
+ iterating: t.NotRequired[bool]
316
318
 
317
319
 
318
320
  class FieldSettings(t.TypedDict, total=False):
@@ -0,0 +1,9 @@
1
+ """Warning types emitted by TypeDAL."""
2
+
3
+
4
+ class UnusedWindowWarning(RuntimeWarning):
5
+ """Warn when a window size is configured but the query is collected directly."""
6
+
7
+
8
+ class NoopQueryWarning(RuntimeWarning):
9
+ """Warn when a query builder method is called without arguments, so it does nothing."""
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes