TypeDAL 3.17.3__py3-none-any.whl → 4.0.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/helpers.py CHANGED
@@ -7,19 +7,25 @@ from __future__ import annotations
7
7
  import datetime as dt
8
8
  import fnmatch
9
9
  import io
10
+ import re
11
+ import sys
10
12
  import types
11
- import typing
13
+ import typing as t
12
14
  from collections import ChainMap
13
- from typing import Any
14
15
 
15
16
  from pydal import DAL
16
17
 
17
- from .types import AnyDict, Expression, Field, Table
18
+ from .types import AnyDict, Expression, Field, Row, T, Table, Template # type: ignore
18
19
 
19
- if typing.TYPE_CHECKING:
20
- from . import TypeDAL, TypedField, TypedTable
20
+ try:
21
+ import annotationlib
22
+ except ImportError: # pragma: no cover
23
+ annotationlib = None
24
+
25
+ if t.TYPE_CHECKING:
26
+ from string.templatelib import Interpolation
21
27
 
22
- T = typing.TypeVar("T")
28
+ from . import TypeDAL, TypedField, TypedTable
23
29
 
24
30
 
25
31
  def is_union(some_type: type | types.UnionType) -> bool:
@@ -27,19 +33,34 @@ def is_union(some_type: type | types.UnionType) -> bool:
27
33
  Check if a type is some type of Union.
28
34
 
29
35
  Args:
30
- 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]
31
37
 
32
38
  """
33
- return typing.get_origin(some_type) in (types.UnionType, typing.Union)
39
+ return t.get_origin(some_type) in (types.UnionType, t.Union)
34
40
 
35
41
 
36
- def reversed_mro(cls: type) -> typing.Iterable[type]:
42
+ def reversed_mro(cls: type) -> t.Iterable[type]:
37
43
  """
38
44
  Get the Method Resolution Order (mro) for a class, in reverse order to be used with ChainMap.
39
45
  """
40
46
  return reversed(getattr(cls, "__mro__", []))
41
47
 
42
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
+
43
64
  def _all_annotations(cls: type) -> ChainMap[str, type]:
44
65
  """
45
66
  Returns a dictionary-like ChainMap that includes annotations for all \
@@ -47,7 +68,7 @@ def _all_annotations(cls: type) -> ChainMap[str, type]:
47
68
  """
48
69
  # chainmap reverses the iterable, so reverse again beforehand to keep order normally:
49
70
 
50
- 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)))
51
72
 
52
73
 
53
74
  def all_dict(cls: type) -> AnyDict:
@@ -57,9 +78,9 @@ def all_dict(cls: type) -> AnyDict:
57
78
  return dict(ChainMap(*(c.__dict__ for c in reversed_mro(cls)))) # type: ignore
58
79
 
59
80
 
60
- 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]:
61
82
  """
62
- Wrapper around `_all_annotations` that filters away any keys in _except.
83
+ Wrapper around `_all_annotations` that filters away t.Any keys in _except.
63
84
 
64
85
  It also flattens the ChainMap to a regular dict.
65
86
  """
@@ -70,7 +91,7 @@ def all_annotations(cls: type, _except: typing.Optional[typing.Iterable[str]] =
70
91
  return {k: v for k, v in _all.items() if k not in _except}
71
92
 
72
93
 
73
- 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:
74
95
  """
75
96
  Create an instance of T (if it is a class).
76
97
 
@@ -80,20 +101,20 @@ def instanciate(cls: typing.Type[T] | T, with_args: bool = False) -> T:
80
101
  If with_args: spread the generic args into the class creation
81
102
  (needed for e.g. TypedField(str), but not for list[str])
82
103
  """
83
- if inner_cls := typing.get_origin(cls):
104
+ if inner_cls := t.get_origin(cls):
84
105
  if not with_args:
85
- return typing.cast(T, inner_cls())
106
+ return t.cast(T, inner_cls())
86
107
 
87
- args = typing.get_args(cls)
88
- return typing.cast(T, inner_cls(*args))
108
+ args = t.get_args(cls)
109
+ return t.cast(T, inner_cls(*args))
89
110
 
90
111
  if isinstance(cls, type):
91
- return typing.cast(T, cls())
112
+ return t.cast(T, cls())
92
113
 
93
114
  return cls
94
115
 
95
116
 
96
- def origin_is_subclass(obj: Any, _type: type) -> bool:
117
+ def origin_is_subclass(obj: t.Any, _type: type) -> bool:
97
118
  """
98
119
  Check if the origin of a generic is a subclass of _type.
99
120
 
@@ -101,15 +122,13 @@ def origin_is_subclass(obj: Any, _type: type) -> bool:
101
122
  origin_is_subclass(list[str], list) -> True
102
123
  """
103
124
  return bool(
104
- typing.get_origin(obj)
105
- and isinstance(typing.get_origin(obj), type)
106
- 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),
107
126
  )
108
127
 
109
128
 
110
129
  def mktable(
111
- data: dict[Any, Any],
112
- header: typing.Optional[typing.Iterable[str] | range] = None,
130
+ data: dict[t.Any, t.Any],
131
+ header: t.Optional[t.Iterable[str] | range] = None,
113
132
  skip_first: bool = True,
114
133
  ) -> str:
115
134
  """
@@ -154,11 +173,11 @@ def mktable(
154
173
  return output.getvalue()
155
174
 
156
175
 
157
- K = typing.TypeVar("K")
158
- V = typing.TypeVar("V")
176
+ K = t.TypeVar("K")
177
+ V = t.TypeVar("V")
159
178
 
160
179
 
161
- def looks_like(v: Any, _type: type[Any]) -> bool:
180
+ def looks_like(v: t.Any, _type: type[t.Any]) -> bool:
162
181
  """
163
182
  Returns true if v or v's class is of type _type, including if it is a generic.
164
183
 
@@ -186,19 +205,19 @@ def unwrap_type(_type: type) -> type:
186
205
  Example:
187
206
  list[list[str]] -> str
188
207
  """
189
- while args := typing.get_args(_type):
208
+ while args := t.get_args(_type):
190
209
  _type = args[0]
191
210
  return _type
192
211
 
193
212
 
194
- @typing.overload
213
+ @t.overload
195
214
  def extract_type_optional(annotation: T) -> tuple[T, bool]:
196
215
  """
197
216
  T -> T is not exactly right because you'll get the inner type, but mypy seems happy with this.
198
217
  """
199
218
 
200
219
 
201
- @typing.overload
220
+ @t.overload
202
221
  def extract_type_optional(annotation: None) -> tuple[None, bool]:
203
222
  """
204
223
  None leads to None, False.
@@ -212,10 +231,10 @@ def extract_type_optional(annotation: T | None) -> tuple[T | None, bool]:
212
231
  if annotation is None:
213
232
  return None, False
214
233
 
215
- if origin := typing.get_origin(annotation):
216
- args = typing.get_args(annotation)
234
+ if origin := t.get_origin(annotation):
235
+ args = t.get_args(annotation)
217
236
 
218
- if origin in (typing.Union, types.UnionType, typing.Optional) and args:
237
+ if origin in (t.Union, types.UnionType, t.Optional) and args:
219
238
  # remove None:
220
239
  return next(_ for _ in args if _ and _ != types.NoneType and not isinstance(_, types.NoneType)), True
221
240
 
@@ -256,7 +275,7 @@ class DummyQuery:
256
275
  return False
257
276
 
258
277
 
259
- def as_lambda(value: T) -> typing.Callable[..., T]:
278
+ def as_lambda(value: T) -> t.Callable[..., T]:
260
279
  """
261
280
  Wrap value in a callable.
262
281
  """
@@ -289,21 +308,21 @@ def get_db(table: "TypedTable | Table") -> "DAL":
289
308
  """
290
309
  Get the underlying DAL instance for a pydal or typedal table.
291
310
  """
292
- return typing.cast("DAL", table._db)
311
+ return t.cast("DAL", table._db)
293
312
 
294
313
 
295
314
  def get_table(table: "TypedTable | Table") -> "Table":
296
315
  """
297
316
  Get the underlying pydal table for a typedal table.
298
317
  """
299
- return typing.cast("Table", table._table)
318
+ return t.cast("Table", table._table)
300
319
 
301
320
 
302
- def get_field(field: "TypedField[typing.Any] | Field") -> "Field":
321
+ def get_field(field: "TypedField[t.Any] | Field") -> "Field":
303
322
  """
304
323
  Get the underlying pydal field from a typedal field.
305
324
  """
306
- return typing.cast(
325
+ return t.cast(
307
326
  "Field",
308
327
  field, # Table.field already is a Field, but cast to make sure the editor knows this too.
309
328
  )
@@ -314,7 +333,7 @@ class classproperty:
314
333
  Combination of @classmethod and @property.
315
334
  """
316
335
 
317
- def __init__(self, fget: typing.Callable[..., typing.Any]) -> None:
336
+ def __init__(self, fget: t.Callable[..., t.Any]) -> None:
318
337
  """
319
338
  Initialize the classproperty.
320
339
 
@@ -323,7 +342,7 @@ class classproperty:
323
342
  """
324
343
  self.fget = fget
325
344
 
326
- 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:
327
346
  """
328
347
  Retrieve the property value.
329
348
 
@@ -337,7 +356,7 @@ class classproperty:
337
356
  return self.fget(owner)
338
357
 
339
358
 
340
- def smarter_adapt(db: TypeDAL, placeholder: Any) -> str:
359
+ def smarter_adapt(db: TypeDAL, placeholder: t.Any) -> str:
341
360
  """
342
361
  Smarter adaptation of placeholder to quote if needed.
343
362
 
@@ -349,7 +368,7 @@ def smarter_adapt(db: TypeDAL, placeholder: Any) -> str:
349
368
  Quoted placeholder if needed, except for numbers (smart_adapt logic)
350
369
  or fields/tables (use already quoted rname).
351
370
  """
352
- return typing.cast(
371
+ return t.cast(
353
372
  str,
354
373
  getattr(placeholder, "sql_shortref", None) # for tables
355
374
  or getattr(placeholder, "sqlsafe", None) # for fields
@@ -357,26 +376,123 @@ def smarter_adapt(db: TypeDAL, placeholder: Any) -> str:
357
376
  )
358
377
 
359
378
 
360
- def sql_escape(db: TypeDAL, sql_fragment: str, *raw_args: Any, **raw_kwargs: Any) -> str:
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
361
384
  """
362
- Generates escaped SQL fragments with placeholders.
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.
363
390
 
364
391
  Args:
365
- db: Database object.
366
- sql_fragment: SQL fragment with placeholders.
367
- *raw_args: Positional arguments to be escaped.
368
- **raw_kwargs: Keyword arguments to be escaped.
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.
369
468
 
370
469
  Returns:
371
- Escaped SQL fragment with placeholders replaced with escaped values.
470
+ str: SQL fragment with all placeholders replaced by properly escaped values.
372
471
 
373
472
  Raises:
374
- ValueError: If both args and kwargs are provided.
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.
375
488
  """
376
489
  if raw_args and raw_kwargs: # pragma: no cover
377
490
  raise ValueError("Please provide either args or kwargs, not both.")
378
491
 
379
- elif raw_args:
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:
380
496
  # list
381
497
  return sql_fragment % tuple(smarter_adapt(db, placeholder) for placeholder in raw_args)
382
498
  else:
@@ -386,23 +502,58 @@ def sql_escape(db: TypeDAL, sql_fragment: str, *raw_args: Any, **raw_kwargs: Any
386
502
 
387
503
  def sql_expression(
388
504
  db: TypeDAL,
389
- sql_fragment: str,
390
- *raw_args: Any,
505
+ sql_fragment: str | Template,
506
+ *raw_args: t.Any,
391
507
  output_type: str | None = None,
392
- **raw_kwargs: Any,
508
+ **raw_kwargs: t.Any,
393
509
  ) -> Expression:
394
510
  """
395
- Creates a pydal Expression object representing a raw SQL fragment.
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.
396
516
 
397
517
  Args:
398
- db: The TypeDAL object.
399
- sql_fragment: The raw SQL fragment.
400
- *raw_args: Arguments to be interpolated into the SQL fragment.
401
- output_type: The expected output type of the expression.
402
- **raw_kwargs: Keyword arguments to be interpolated into the SQL fragment.
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.
403
528
 
404
529
  Returns:
405
- A pydal Expression object.
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.
406
557
  """
407
558
  safe_sql = sql_escape(db, sql_fragment, *raw_args, **raw_kwargs)
408
559
 
@@ -413,3 +564,59 @@ def sql_expression(
413
564
  safe_sql,
414
565
  type=output_type, # optional type hint
415
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).
@@ -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
  """