TypeDAL 3.16.4__py3-none-any.whl → 4.2.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.
typedal/for_py4web.py CHANGED
@@ -14,7 +14,7 @@ from .types import AnyDict
14
14
  from .web2py_py4web_shared import AuthUser
15
15
 
16
16
 
17
- class Fixture(_Fixture): # type: ignore
17
+ class Fixture(_Fixture):
18
18
  """
19
19
  Make mypy happy.
20
20
  """
typedal/for_web2py.py CHANGED
@@ -6,7 +6,7 @@ import datetime as dt
6
6
 
7
7
  from pydal.validators import IS_NOT_IN_DB
8
8
 
9
- from .core import TypeDAL, TypedField, TypedTable
9
+ from . import TypeDAL, TypedField, TypedTable
10
10
  from .fields import TextField
11
11
  from .web2py_py4web_shared import AuthUser
12
12
 
typedal/helpers.py CHANGED
@@ -2,22 +2,30 @@
2
2
  Helpers that work independently of core.
3
3
  """
4
4
 
5
+ from __future__ import annotations
6
+
5
7
  import datetime as dt
6
8
  import fnmatch
7
9
  import io
10
+ import re
11
+ import sys
8
12
  import types
9
- import typing
13
+ import typing as t
10
14
  from collections import ChainMap
11
- from typing import Any
12
15
 
13
16
  from pydal import DAL
14
17
 
15
- from .types import AnyDict, Field, Table
18
+ from .types import AnyDict, Expression, Field, Row, T, Table, Template # type: ignore
19
+
20
+ try:
21
+ import annotationlib
22
+ except ImportError: # pragma: no cover
23
+ annotationlib = None
16
24
 
17
- if typing.TYPE_CHECKING:
18
- from . import TypeDAL, TypedField, TypedTable # noqa: F401
25
+ if t.TYPE_CHECKING:
26
+ from string.templatelib import Interpolation
19
27
 
20
- T = typing.TypeVar("T")
28
+ from . import TypeDAL, TypedField, TypedTable
21
29
 
22
30
 
23
31
  def is_union(some_type: type | types.UnionType) -> bool:
@@ -25,19 +33,34 @@ def is_union(some_type: type | types.UnionType) -> bool:
25
33
  Check if a type is some type of Union.
26
34
 
27
35
  Args:
28
- some_type: types.UnionType = type(int | str); typing.Union = typing.Union[int, str]
36
+ some_type: types.UnionType = type(int | str); t.Union = t.Union[int, str]
29
37
 
30
38
  """
31
- return typing.get_origin(some_type) in (types.UnionType, typing.Union)
39
+ return t.get_origin(some_type) in (types.UnionType, t.Union)
32
40
 
33
41
 
34
- def reversed_mro(cls: type) -> typing.Iterable[type]:
42
+ def reversed_mro(cls: type) -> t.Iterable[type]:
35
43
  """
36
44
  Get the Method Resolution Order (mro) for a class, in reverse order to be used with ChainMap.
37
45
  """
38
46
  return reversed(getattr(cls, "__mro__", []))
39
47
 
40
48
 
49
+ def _cls_annotations(c: type) -> dict[str, type]: # pragma: no cover
50
+ """
51
+ Functions to get the annotations of a class (excl inherited, use _all_annotations for that).
52
+
53
+ Uses `annotationlib` if available (since 3.14) and if so, resolves forward references immediately.
54
+ """
55
+ if annotationlib:
56
+ return t.cast(
57
+ dict[str, type],
58
+ annotationlib.get_annotations(c, format=annotationlib.Format.VALUE, eval_str=True),
59
+ )
60
+ else:
61
+ return getattr(c, "__annotations__", {})
62
+
63
+
41
64
  def _all_annotations(cls: type) -> ChainMap[str, type]:
42
65
  """
43
66
  Returns a dictionary-like ChainMap that includes annotations for all \
@@ -45,7 +68,7 @@ def _all_annotations(cls: type) -> ChainMap[str, type]:
45
68
  """
46
69
  # chainmap reverses the iterable, so reverse again beforehand to keep order normally:
47
70
 
48
- return ChainMap(*(c.__annotations__ for c in reversed_mro(cls) if "__annotations__" in c.__dict__))
71
+ return ChainMap(*(_cls_annotations(c) for c in reversed_mro(cls)))
49
72
 
50
73
 
51
74
  def all_dict(cls: type) -> AnyDict:
@@ -55,9 +78,9 @@ def all_dict(cls: type) -> AnyDict:
55
78
  return dict(ChainMap(*(c.__dict__ for c in reversed_mro(cls)))) # type: ignore
56
79
 
57
80
 
58
- def all_annotations(cls: type, _except: typing.Optional[typing.Iterable[str]] = None) -> dict[str, type]:
81
+ def all_annotations(cls: type, _except: t.Optional[t.Iterable[str]] = None) -> dict[str, type]:
59
82
  """
60
- Wrapper around `_all_annotations` that filters away any keys in _except.
83
+ Wrapper around `_all_annotations` that filters away t.Any keys in _except.
61
84
 
62
85
  It also flattens the ChainMap to a regular dict.
63
86
  """
@@ -68,7 +91,7 @@ def all_annotations(cls: type, _except: typing.Optional[typing.Iterable[str]] =
68
91
  return {k: v for k, v in _all.items() if k not in _except}
69
92
 
70
93
 
71
- def instanciate(cls: typing.Type[T] | T, with_args: bool = False) -> T:
94
+ def instanciate(cls: t.Type[T] | T, with_args: bool = False) -> T:
72
95
  """
73
96
  Create an instance of T (if it is a class).
74
97
 
@@ -78,20 +101,20 @@ def instanciate(cls: typing.Type[T] | T, with_args: bool = False) -> T:
78
101
  If with_args: spread the generic args into the class creation
79
102
  (needed for e.g. TypedField(str), but not for list[str])
80
103
  """
81
- if inner_cls := typing.get_origin(cls):
104
+ if inner_cls := t.get_origin(cls):
82
105
  if not with_args:
83
- return typing.cast(T, inner_cls())
106
+ return t.cast(T, inner_cls())
84
107
 
85
- args = typing.get_args(cls)
86
- return typing.cast(T, inner_cls(*args))
108
+ args = t.get_args(cls)
109
+ return t.cast(T, inner_cls(*args))
87
110
 
88
111
  if isinstance(cls, type):
89
- return typing.cast(T, cls())
112
+ return t.cast(T, cls())
90
113
 
91
114
  return cls
92
115
 
93
116
 
94
- def origin_is_subclass(obj: Any, _type: type) -> bool:
117
+ def origin_is_subclass(obj: t.Any, _type: type) -> bool:
95
118
  """
96
119
  Check if the origin of a generic is a subclass of _type.
97
120
 
@@ -99,14 +122,14 @@ def origin_is_subclass(obj: Any, _type: type) -> bool:
99
122
  origin_is_subclass(list[str], list) -> True
100
123
  """
101
124
  return bool(
102
- typing.get_origin(obj)
103
- and isinstance(typing.get_origin(obj), type)
104
- and issubclass(typing.get_origin(obj), _type)
125
+ t.get_origin(obj) and isinstance(t.get_origin(obj), type) and issubclass(t.get_origin(obj), _type),
105
126
  )
106
127
 
107
128
 
108
129
  def mktable(
109
- data: dict[Any, Any], header: typing.Optional[typing.Iterable[str] | range] = None, skip_first: bool = True
130
+ data: dict[t.Any, t.Any],
131
+ header: t.Optional[t.Iterable[str] | range] = None,
132
+ skip_first: bool = True,
110
133
  ) -> str:
111
134
  """
112
135
  Display a table for 'data'.
@@ -150,11 +173,11 @@ def mktable(
150
173
  return output.getvalue()
151
174
 
152
175
 
153
- K = typing.TypeVar("K")
154
- V = typing.TypeVar("V")
176
+ K = t.TypeVar("K")
177
+ V = t.TypeVar("V")
155
178
 
156
179
 
157
- def looks_like(v: Any, _type: type[Any]) -> bool:
180
+ def looks_like(v: t.Any, _type: type[t.Any]) -> bool:
158
181
  """
159
182
  Returns true if v or v's class is of type _type, including if it is a generic.
160
183
 
@@ -166,7 +189,7 @@ def looks_like(v: Any, _type: type[Any]) -> bool:
166
189
  return isinstance(v, _type) or (isinstance(v, type) and issubclass(v, _type)) or origin_is_subclass(v, _type)
167
190
 
168
191
 
169
- def filter_out(mut_dict: dict[K, V], _type: type[T]) -> dict[K, type[T]]:
192
+ def filter_out(mut_dict: dict[K, V], _type: type[T]) -> dict[K, T]:
170
193
  """
171
194
  Split a dictionary into things matching _type and the rest.
172
195
 
@@ -182,19 +205,19 @@ def unwrap_type(_type: type) -> type:
182
205
  Example:
183
206
  list[list[str]] -> str
184
207
  """
185
- while args := typing.get_args(_type):
208
+ while args := t.get_args(_type):
186
209
  _type = args[0]
187
210
  return _type
188
211
 
189
212
 
190
- @typing.overload
213
+ @t.overload
191
214
  def extract_type_optional(annotation: T) -> tuple[T, bool]:
192
215
  """
193
216
  T -> T is not exactly right because you'll get the inner type, but mypy seems happy with this.
194
217
  """
195
218
 
196
219
 
197
- @typing.overload
220
+ @t.overload
198
221
  def extract_type_optional(annotation: None) -> tuple[None, bool]:
199
222
  """
200
223
  None leads to None, False.
@@ -208,10 +231,10 @@ def extract_type_optional(annotation: T | None) -> tuple[T | None, bool]:
208
231
  if annotation is None:
209
232
  return None, False
210
233
 
211
- if origin := typing.get_origin(annotation):
212
- args = typing.get_args(annotation)
234
+ if origin := t.get_origin(annotation):
235
+ args = t.get_args(annotation)
213
236
 
214
- if origin in (typing.Union, types.UnionType, typing.Optional) and args:
237
+ if origin in (t.Union, types.UnionType, t.Optional) and args:
215
238
  # remove None:
216
239
  return next(_ for _ in args if _ and _ != types.NoneType and not isinstance(_, types.NoneType)), True
217
240
 
@@ -252,7 +275,7 @@ class DummyQuery:
252
275
  return False
253
276
 
254
277
 
255
- def as_lambda(value: T) -> typing.Callable[..., T]:
278
+ def as_lambda(value: T) -> t.Callable[..., T]:
256
279
  """
257
280
  Wrap value in a callable.
258
281
  """
@@ -285,21 +308,21 @@ def get_db(table: "TypedTable | Table") -> "DAL":
285
308
  """
286
309
  Get the underlying DAL instance for a pydal or typedal table.
287
310
  """
288
- return typing.cast("DAL", table._db)
311
+ return t.cast("DAL", table._db)
289
312
 
290
313
 
291
314
  def get_table(table: "TypedTable | Table") -> "Table":
292
315
  """
293
316
  Get the underlying pydal table for a typedal table.
294
317
  """
295
- return typing.cast("Table", table._table)
318
+ return t.cast("Table", table._table)
296
319
 
297
320
 
298
- def get_field(field: "TypedField[typing.Any] | Field") -> "Field":
321
+ def get_field(field: "TypedField[t.Any] | Field") -> "Field":
299
322
  """
300
323
  Get the underlying pydal field from a typedal field.
301
324
  """
302
- return typing.cast(
325
+ return t.cast(
303
326
  "Field",
304
327
  field, # Table.field already is a Field, but cast to make sure the editor knows this too.
305
328
  )
@@ -310,7 +333,7 @@ class classproperty:
310
333
  Combination of @classmethod and @property.
311
334
  """
312
335
 
313
- def __init__(self, fget: typing.Callable[..., typing.Any]) -> None:
336
+ def __init__(self, fget: t.Callable[..., t.Any]) -> None:
314
337
  """
315
338
  Initialize the classproperty.
316
339
 
@@ -319,7 +342,7 @@ class classproperty:
319
342
  """
320
343
  self.fget = fget
321
344
 
322
- def __get__(self, obj: typing.Any, owner: typing.Type[T]) -> typing.Any:
345
+ def __get__(self, obj: t.Any, owner: t.Type[T]) -> t.Any:
323
346
  """
324
347
  Retrieve the property value.
325
348
 
@@ -331,3 +354,269 @@ class classproperty:
331
354
  The value returned by the function.
332
355
  """
333
356
  return self.fget(owner)
357
+
358
+
359
+ def smarter_adapt(db: TypeDAL, placeholder: t.Any) -> str:
360
+ """
361
+ Smarter adaptation of placeholder to quote if needed.
362
+
363
+ Args:
364
+ db: Database object.
365
+ placeholder: Placeholder object.
366
+
367
+ Returns:
368
+ Quoted placeholder if needed, except for numbers (smart_adapt logic)
369
+ or fields/tables (use already quoted rname).
370
+ """
371
+ return t.cast(
372
+ str,
373
+ getattr(placeholder, "sql_shortref", None) # for tables
374
+ or getattr(placeholder, "sqlsafe", None) # for fields
375
+ or db._adapter.smart_adapt(placeholder), # for others
376
+ )
377
+
378
+
379
+ # https://docs.python.org/3.14/library/string.templatelib.html
380
+ SYSTEM_SUPPORTS_TEMPLATES = sys.version_info > (3, 14)
381
+
382
+
383
+ def process_tstring(template: Template, operation: t.Callable[["Interpolation"], str]) -> str: # pragma: no cover
384
+ """
385
+ Process a Template string by applying an operation to each interpolation.
386
+
387
+ This function iterates through a Template object, which contains both string literals
388
+ and Interpolation objects. String literals are preserved as-is, while Interpolation
389
+ objects are transformed using the provided operation function.
390
+
391
+ Args:
392
+ template: A Template object containing mixed string literals and Interpolation objects.
393
+ operation: A callable that takes an Interpolation object and returns a string.
394
+ This function will be applied to each interpolated value in the template.
395
+
396
+ Returns:
397
+ str: The processed string with all interpolations replaced by the results of
398
+ applying the operation function.
399
+
400
+ Example:
401
+ Basic f-string functionality can be implemented as:
402
+
403
+ >>> def fstring_operation(interpolation):
404
+ ... return str(interpolation.value)
405
+ >>> value = "test"
406
+ >>> template = t"{value = }" # Template string literal
407
+ >>> result = process_tstring(template, fstring_operation)
408
+ >>> print(result) # "value = test"
409
+
410
+ Note:
411
+ This is a generic template processor. The specific behavior depends entirely
412
+ on the operation function provided.
413
+ """
414
+ return "".join(part if isinstance(part, str) else operation(part) for part in template)
415
+
416
+
417
+ def sql_escape_template(db: TypeDAL, sql_fragment: Template) -> str: # pragma: no cover
418
+ r"""
419
+ Safely escape a Template string for SQL execution using database-specific escaping.
420
+
421
+ This function processes a Template string (t-string) by escaping all interpolated
422
+ values using the database adapter's escape mechanism, preventing SQL injection
423
+ attacks while maintaining the structure of the SQL query.
424
+
425
+ Args:
426
+ db: TypeDAL database connection object that provides the adapter for escaping.
427
+ sql_fragment: A Template object (t-string) containing SQL with interpolated values.
428
+ The interpolated values will be automatically escaped.
429
+
430
+ Returns:
431
+ str: SQL string with all interpolated values properly escaped for safe execution.
432
+
433
+ Example:
434
+ >>> user_input = "'; DROP TABLE users; --"
435
+ >>> query = t"SELECT * FROM users WHERE name = {user_input}"
436
+ >>> safe_query = sql_escape_template(db, query)
437
+ >>> print(safe_query) # "SELECT * FROM users WHERE name = '\'; DROP TABLE users; --'"
438
+
439
+ Security:
440
+ This function is essential for preventing SQL injection attacks when using
441
+ user-provided data in SQL queries. All interpolated values are escaped
442
+ according to the database adapter's rules.
443
+
444
+ Note:
445
+ Only available in Python 3.14+ when SYSTEM_SUPPORTS_TEMPLATES is True.
446
+ For earlier Python versions, use sql_escape() with string formatting.
447
+ """
448
+ return process_tstring(sql_fragment, lambda part: smarter_adapt(db, part.value))
449
+
450
+
451
+ def sql_escape(db: TypeDAL, sql_fragment: str | Template, *raw_args: t.Any, **raw_kwargs: t.Any) -> str:
452
+ """
453
+ Generate escaped SQL fragments with safely substituted placeholders.
454
+
455
+ This function provides secure SQL string construction by escaping all provided
456
+ arguments using the database adapter's escaping mechanism. It supports both
457
+ traditional string formatting (Python < 3.14) and Template strings (Python 3.14+).
458
+
459
+ Args:
460
+ db: TypeDAL database connection object that provides the adapter for escaping.
461
+ sql_fragment: SQL fragment with placeholders (%s for positional, %(name)s for named).
462
+ In Python 3.14+, this can also be a Template (t-string) with
463
+ interpolated values that will be automatically escaped.
464
+ *raw_args: Positional arguments to be escaped and substituted for %s placeholders.
465
+ Only use with string fragments, not Template objects.
466
+ **raw_kwargs: Keyword arguments to be escaped and substituted for %(name)s placeholders.
467
+ Only use with string fragments, not Template objects.
468
+
469
+ Returns:
470
+ str: SQL fragment with all placeholders replaced by properly escaped values.
471
+
472
+ Raises:
473
+ ValueError: If both positional and keyword arguments are provided simultaneously.
474
+
475
+ Examples:
476
+ Positional arguments:
477
+ >>> safe_sql = sql_escape(db, "SELECT * FROM users WHERE id = %s", user_id)
478
+
479
+ Keyword arguments:
480
+ >>> safe_sql = sql_escape(db, "SELECT * FROM users WHERE name = %(name)s", name=username)
481
+
482
+ Template strings (Python 3.14+):
483
+ >>> safe_sql = sql_escape(db, t"SELECT * FROM users WHERE id = {user_id}")
484
+
485
+ Security:
486
+ All arguments are escaped using the database adapter's escaping rules to prevent
487
+ SQL injection attacks. Never concatenate user input directly into SQL strings.
488
+ """
489
+ if raw_args and raw_kwargs: # pragma: no cover
490
+ raise ValueError("Please provide either args or kwargs, not both.")
491
+
492
+ if SYSTEM_SUPPORTS_TEMPLATES and isinstance(sql_fragment, Template): # pragma: no cover
493
+ return sql_escape_template(db, sql_fragment)
494
+
495
+ if raw_args:
496
+ # list
497
+ return sql_fragment % tuple(smarter_adapt(db, placeholder) for placeholder in raw_args)
498
+ else:
499
+ # dict
500
+ return sql_fragment % {key: smarter_adapt(db, placeholder) for key, placeholder in raw_kwargs.items()}
501
+
502
+
503
+ def sql_expression(
504
+ db: TypeDAL,
505
+ sql_fragment: str | Template,
506
+ *raw_args: t.Any,
507
+ output_type: str | None = None,
508
+ **raw_kwargs: t.Any,
509
+ ) -> Expression:
510
+ """
511
+ Create a PyDAL Expression object from a raw SQL fragment with safe parameter substitution.
512
+
513
+ This function combines SQL escaping with PyDAL's Expression system, allowing you to
514
+ create database expressions from raw SQL while maintaining security through proper
515
+ parameter escaping.
516
+
517
+ Args:
518
+ db: The TypeDAL database connection object.
519
+ sql_fragment: Raw SQL fragment with placeholders (%s for positional, %(name)s for named).
520
+ In Python 3.14+, this can also be a Template (t-string) with
521
+ interpolated values that will be automatically escaped.
522
+ *raw_args: Positional arguments to be escaped and interpolated into the SQL fragment.
523
+ Only use with string fragments, not Template objects.
524
+ output_type: Optional type hint for the expected output type of the expression.
525
+ This can help with query analysis and optimization.
526
+ **raw_kwargs: Keyword arguments to be escaped and interpolated into the SQL fragment.
527
+ Only use with string fragments, not Template objects.
528
+
529
+ Returns:
530
+ Expression: A PyDAL Expression object wrapping the safely escaped SQL fragment.
531
+
532
+ Examples:
533
+ Creating a complex WHERE clause:
534
+ >>> expr = sql_expression(db,
535
+ ... "age > %s AND status = %s",
536
+ ... 18, "active",
537
+ ... output_type="boolean")
538
+ >>> query = db(expr).select()
539
+
540
+ Using keyword arguments:
541
+ >>> expr = sql_expression(db,
542
+ ... "EXTRACT(year FROM %(date_col)s) = %(year)s",
543
+ ... date_col="created_at", year=2023,
544
+ ... output_type="boolean")
545
+
546
+ Template strings (Python 3.14+):
547
+ >>> min_age = 21
548
+ >>> expr = sql_expression(db, t"age >= {min_age}", output_type="boolean")
549
+
550
+ Security:
551
+ All parameters are escaped using sql_escape() before being wrapped in the Expression,
552
+ ensuring protection against SQL injection attacks.
553
+
554
+ Note:
555
+ The returned Expression can be used anywhere PyDAL expects an expression,
556
+ such as in db().select(), .update(), or .delete() operations.
557
+ """
558
+ safe_sql = sql_escape(db, sql_fragment, *raw_args, **raw_kwargs)
559
+
560
+ # create a pydal Expression wrapping a raw SQL fragment + placeholders
561
+ return Expression(
562
+ db,
563
+ db._adapter.dialect.raw,
564
+ safe_sql,
565
+ type=output_type, # optional type hint
566
+ )
567
+
568
+
569
+ def normalize_table_keys(row: Row, pattern: re.Pattern[str] = re.compile(r"^([a-zA-Z_]+)_(\d{5,})$")) -> Row:
570
+ """
571
+ Normalize table keys in a PyDAL Row object by stripping numeric hash suffixes from table names, \
572
+ only if the suffix is 5 or more digits.
573
+
574
+ For example:
575
+ Row({'articles_12345': {...}}) -> Row({'articles': {...}})
576
+ Row({'articles_123': {...}}) -> unchanged
577
+
578
+ Returns:
579
+ Row: A new Row object with normalized keys.
580
+ """
581
+ new_data: dict[str, t.Any] = {}
582
+ for key, value in row.items():
583
+ if match := pattern.match(key):
584
+ base, _suffix = match.groups()
585
+ normalized_key = base
586
+ new_data[normalized_key] = value
587
+ else:
588
+ new_data[key] = value
589
+ return Row(new_data)
590
+
591
+
592
+ def default_representer(field: TypedField[T], value: T, table: t.Type[TypedTable]) -> str:
593
+ """
594
+ Simply call field.represent on the value.
595
+ """
596
+ if represent := getattr(field, "represent", None):
597
+ return str(represent(value, table))
598
+ else:
599
+ return repr(value)
600
+
601
+
602
+ def throw(exc: BaseException) -> t.Never:
603
+ """
604
+ Raise the given exception.
605
+
606
+ This function provides a functional way to raise exceptions, allowing
607
+ exception raising to be used in expressions where a statement wouldn't work.
608
+
609
+ Args:
610
+ exc: The exception to be raised.
611
+
612
+ Returns:
613
+ Never returns normally as an exception is always raised.
614
+
615
+ Raises:
616
+ BaseException: Always raises the provided exception.
617
+
618
+ Examples:
619
+ >>> value = get_value() or throw(ValueError("No value available"))
620
+ >>> result = data.get('key') if data else throw(KeyError("Missing data"))
621
+ """
622
+ raise exc
typedal/mixins.py CHANGED
@@ -5,26 +5,22 @@ Mixins can add reusable fields and behavior (optimally both, otherwise it doesn'
5
5
  """
6
6
 
7
7
  import base64
8
+ import datetime as dt
8
9
  import os
9
- import typing
10
+ import typing as t
10
11
  import warnings
11
- from datetime import datetime
12
- from typing import Any, Optional
13
12
 
14
13
  from pydal import DAL
15
14
  from pydal.validators import IS_NOT_IN_DB, ValidationError
16
15
  from slugify import slugify
17
16
 
18
- from .core import ( # noqa F401 - used by example in docstring
19
- QueryBuilder,
20
- T_MetaInstance,
21
- TableMeta,
22
- TypeDAL,
23
- TypedTable,
24
- _TypedTable,
25
- )
17
+ from .core import TypeDAL
26
18
  from .fields import DatetimeField, StringField
27
- from .types import OpRow, Set
19
+ from .tables import _TypedTable
20
+ from .types import OpRow, Set, T_MetaInstance
21
+
22
+ if t.TYPE_CHECKING:
23
+ from .tables import TypedTable # noqa: F401
28
24
 
29
25
 
30
26
  class Mixin(_TypedTable):
@@ -38,9 +34,9 @@ class Mixin(_TypedTable):
38
34
  ('inconsistent method resolution' or 'metaclass conflicts')
39
35
  """
40
36
 
41
- __settings__: typing.ClassVar[dict[str, Any]]
37
+ __settings__: t.ClassVar[dict[str, t.Any]]
42
38
 
43
- def __init_subclass__(cls, **kwargs: Any):
39
+ def __init_subclass__(cls, **kwargs: t.Any):
44
40
  """
45
41
  Ensures __settings__ exists for other mixins.
46
42
  """
@@ -52,8 +48,8 @@ class TimestampsMixin(Mixin):
52
48
  A Mixin class for adding timestamp fields to a model.
53
49
  """
54
50
 
55
- created_at = DatetimeField(default=datetime.now, writable=False)
56
- updated_at = DatetimeField(default=datetime.now, writable=False)
51
+ created_at = DatetimeField(default=dt.datetime.now, writable=False)
52
+ updated_at = DatetimeField(default=dt.datetime.now, writable=False)
57
53
 
58
54
  @classmethod
59
55
  def __on_define__(cls, db: TypeDAL) -> None:
@@ -73,7 +69,7 @@ class TimestampsMixin(Mixin):
73
69
  _: Set: Unused parameter.
74
70
  row (OpRow): The row to update.
75
71
  """
76
- row["updated_at"] = datetime.now()
72
+ row["updated_at"] = dt.datetime.now()
77
73
 
78
74
  cls._before_update.append(set_updated_at)
79
75
 
@@ -89,7 +85,7 @@ def slug_random_suffix(length: int = 8) -> str:
89
85
  return base64.urlsafe_b64encode(os.urandom(length)).rstrip(b"=").decode().strip("=")
90
86
 
91
87
 
92
- T = typing.TypeVar("T")
88
+ T = t.TypeVar("T")
93
89
 
94
90
 
95
91
  # noinspection PyPep8Naming
@@ -112,7 +108,7 @@ class HAS_UNIQUE_SLUG(IS_NOT_IN_DB):
112
108
  """
113
109
  super().__init__(db, field, error_message)
114
110
 
115
- def validate(self, original: T, record_id: Optional[int] = None) -> T:
111
+ def validate(self, original: T, record_id: t.Optional[int] = None) -> T:
116
112
  """
117
113
  Performs checks to see if the slug already exists for a different row.
118
114
  """
@@ -154,7 +150,7 @@ class SlugMixin(Mixin):
154
150
  # pub:
155
151
  slug = StringField(unique=True, writable=False)
156
152
  # priv:
157
- __settings__: typing.TypedDict( # type: ignore
153
+ __settings__: t.TypedDict( # type: ignore
158
154
  "SlugFieldSettings",
159
155
  {
160
156
  "slug_field": str,
@@ -164,10 +160,10 @@ class SlugMixin(Mixin):
164
160
 
165
161
  def __init_subclass__(
166
162
  cls,
167
- slug_field: typing.Optional[str] = None,
163
+ slug_field: t.Optional[str] = None,
168
164
  slug_suffix_length: int = 0,
169
- slug_suffix: Optional[int] = None,
170
- **kw: Any,
165
+ slug_suffix: t.Optional[int] = None,
166
+ **kw: t.Any,
171
167
  ) -> None:
172
168
  """
173
169
  Bind 'slug field' option to be used later (on_define).
@@ -180,7 +176,7 @@ class SlugMixin(Mixin):
180
176
  if slug_field is None:
181
177
  raise ValueError(
182
178
  "SlugMixin requires a valid slug_field setting: "
183
- "e.g. `class MyClass(TypedTable, SlugMixin, slug_field='title'): ...`"
179
+ "e.g. `class MyClass(TypedTable, SlugMixin, slug_field='title'): ...`",
184
180
  )
185
181
 
186
182
  if slug_suffix:
@@ -197,7 +193,7 @@ class SlugMixin(Mixin):
197
193
 
198
194
  @classmethod
199
195
  def __generate_slug_before_insert(cls, row: OpRow) -> None:
200
- if row.get("slug"):
196
+ if row.get("slug"): # type: ignore
201
197
  # manually set -> skip
202
198
  return None
203
199
 
@@ -235,7 +231,7 @@ class SlugMixin(Mixin):
235
231
  slug_field.requires = current_requires
236
232
 
237
233
  @classmethod
238
- def from_slug(cls: typing.Type[T_MetaInstance], slug: str, join: bool = True) -> Optional[T_MetaInstance]:
234
+ def from_slug(cls: t.Type[T_MetaInstance], slug: str, join: bool = True) -> t.Optional[T_MetaInstance]:
239
235
  """
240
236
  Find a row by its slug.
241
237
  """
@@ -246,7 +242,7 @@ class SlugMixin(Mixin):
246
242
  return builder.first()
247
243
 
248
244
  @classmethod
249
- def from_slug_or_fail(cls: typing.Type[T_MetaInstance], slug: str, join: bool = True) -> T_MetaInstance:
245
+ def from_slug_or_fail(cls: t.Type[T_MetaInstance], slug: str, join: bool = True) -> T_MetaInstance:
250
246
  """
251
247
  Find a row by its slug, or raise an error if it doesn't exist.
252
248
  """