TypeDAL 3.16.5__py3-none-any.whl → 3.17.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.
Potentially problematic release.
This version of TypeDAL might be problematic. Click here for more details.
- typedal/__about__.py +1 -1
- typedal/__init__.py +19 -2
- typedal/cli.py +2 -1
- typedal/core.py +71 -33
- typedal/fields.py +40 -6
- typedal/helpers.py +66 -4
- typedal/mixins.py +2 -2
- {typedal-3.16.5.dist-info → typedal-3.17.0.dist-info}/METADATA +1 -1
- typedal-3.17.0.dist-info/RECORD +19 -0
- typedal-3.16.5.dist-info/RECORD +0 -19
- {typedal-3.16.5.dist-info → typedal-3.17.0.dist-info}/WHEEL +0 -0
- {typedal-3.16.5.dist-info → typedal-3.17.0.dist-info}/entry_points.txt +0 -0
typedal/__about__.py
CHANGED
typedal/__init__.py
CHANGED
|
@@ -3,11 +3,28 @@ TypeDAL Library.
|
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
5
|
from . import fields
|
|
6
|
-
from .core import
|
|
6
|
+
from .core import (
|
|
7
|
+
Relationship,
|
|
8
|
+
TypeDAL,
|
|
9
|
+
TypedField,
|
|
10
|
+
TypedRows,
|
|
11
|
+
TypedTable,
|
|
12
|
+
relationship,
|
|
13
|
+
)
|
|
14
|
+
from .helpers import sql_expression
|
|
7
15
|
|
|
8
16
|
try:
|
|
9
17
|
from .for_py4web import DAL as P4W_DAL
|
|
10
18
|
except ImportError: # pragma: no cover
|
|
11
19
|
P4W_DAL = None # type: ignore
|
|
12
20
|
|
|
13
|
-
__all__ = [
|
|
21
|
+
__all__ = [
|
|
22
|
+
"Relationship",
|
|
23
|
+
"TypeDAL",
|
|
24
|
+
"TypedField",
|
|
25
|
+
"TypedRows",
|
|
26
|
+
"TypedTable",
|
|
27
|
+
"fields",
|
|
28
|
+
"relationship",
|
|
29
|
+
"sql_expression",
|
|
30
|
+
]
|
typedal/cli.py
CHANGED
|
@@ -392,7 +392,8 @@ def fake_migrations(
|
|
|
392
392
|
|
|
393
393
|
previously_migrated = (
|
|
394
394
|
db(
|
|
395
|
-
db.ewh_implemented_features.name.belongs(to_fake)
|
|
395
|
+
db.ewh_implemented_features.name.belongs(to_fake)
|
|
396
|
+
& (db.ewh_implemented_features.installed == True) # noqa E712
|
|
396
397
|
)
|
|
397
398
|
.select(db.ewh_implemented_features.name)
|
|
398
399
|
.column("name")
|
typedal/core.py
CHANGED
|
@@ -25,10 +25,12 @@ from typing import Any, Optional, Type
|
|
|
25
25
|
|
|
26
26
|
import pydal
|
|
27
27
|
from pydal._globals import DEFAULT
|
|
28
|
-
|
|
29
|
-
from pydal.objects import
|
|
28
|
+
|
|
29
|
+
# from pydal.objects import Field as _Field
|
|
30
|
+
# from pydal.objects import Query as _Query
|
|
30
31
|
from pydal.objects import Row
|
|
31
|
-
|
|
32
|
+
|
|
33
|
+
# from pydal.objects import Table as _Table
|
|
32
34
|
from typing_extensions import Self, Unpack
|
|
33
35
|
|
|
34
36
|
from .config import TypeDALConfig, load_config
|
|
@@ -45,6 +47,7 @@ from .helpers import (
|
|
|
45
47
|
looks_like,
|
|
46
48
|
mktable,
|
|
47
49
|
origin_is_subclass,
|
|
50
|
+
sql_expression,
|
|
48
51
|
to_snake,
|
|
49
52
|
unwrap_type,
|
|
50
53
|
)
|
|
@@ -71,7 +74,7 @@ from .types import (
|
|
|
71
74
|
|
|
72
75
|
# use typing.cast(type, ...) to make mypy happy with unions
|
|
73
76
|
T_annotation = Type[Any] | types.UnionType
|
|
74
|
-
T_Query = typing.Union["Table", Query, bool, None, "TypedTable", Type["TypedTable"]]
|
|
77
|
+
T_Query = typing.Union["Table", Query, bool, None, "TypedTable", Type["TypedTable"], Expression]
|
|
75
78
|
T_Value = typing.TypeVar("T_Value") # actual type of the Field (via Generic)
|
|
76
79
|
T_MetaInstance = typing.TypeVar("T_MetaInstance", bound="TypedTable") # bound="TypedTable"; bound="TableMeta"
|
|
77
80
|
T = typing.TypeVar("T")
|
|
@@ -132,7 +135,7 @@ class Relationship(typing.Generic[To_Type]):
|
|
|
132
135
|
Define a relationship to another table.
|
|
133
136
|
"""
|
|
134
137
|
|
|
135
|
-
_type: To_Type
|
|
138
|
+
_type: Type[To_Type]
|
|
136
139
|
table: Type["TypedTable"] | type | str
|
|
137
140
|
condition: Condition
|
|
138
141
|
condition_and: Condition
|
|
@@ -142,7 +145,7 @@ class Relationship(typing.Generic[To_Type]):
|
|
|
142
145
|
|
|
143
146
|
def __init__(
|
|
144
147
|
self,
|
|
145
|
-
_type: To_Type,
|
|
148
|
+
_type: Type[To_Type],
|
|
146
149
|
condition: Condition = None,
|
|
147
150
|
join: JOIN_OPTIONS = None,
|
|
148
151
|
on: OnQuery = None,
|
|
@@ -165,7 +168,7 @@ class Relationship(typing.Generic[To_Type]):
|
|
|
165
168
|
self.table = unwrap_type(args[0])
|
|
166
169
|
self.multiple = True
|
|
167
170
|
else:
|
|
168
|
-
self.table = _type
|
|
171
|
+
self.table = typing.cast(type[TypedTable], _type)
|
|
169
172
|
self.multiple = False
|
|
170
173
|
|
|
171
174
|
if isinstance(self.table, str):
|
|
@@ -234,7 +237,7 @@ class Relationship(typing.Generic[To_Type]):
|
|
|
234
237
|
|
|
235
238
|
return str(table)
|
|
236
239
|
|
|
237
|
-
def __get__(self, instance: Any, owner: Any) -> typing.Optional[list[Any]] |
|
|
240
|
+
def __get__(self, instance: Any, owner: Any) -> "typing.Optional[list[Any]] | Relationship[To_Type]":
|
|
238
241
|
"""
|
|
239
242
|
Relationship is a descriptor class, which can be returned from a class but not an instance.
|
|
240
243
|
|
|
@@ -756,7 +759,7 @@ class TypeDAL(pydal.DAL): # type: ignore
|
|
|
756
759
|
if mapping := BASIC_MAPPINGS.get(ftype):
|
|
757
760
|
# basi types
|
|
758
761
|
return mapping
|
|
759
|
-
elif isinstance(ftype,
|
|
762
|
+
elif isinstance(ftype, pydal.objects.Table):
|
|
760
763
|
# db.table
|
|
761
764
|
return f"reference {ftype._tablename}"
|
|
762
765
|
elif issubclass(type(ftype), type) and issubclass(ftype, TypedTable):
|
|
@@ -829,13 +832,34 @@ class TypeDAL(pydal.DAL): # type: ignore
|
|
|
829
832
|
"""
|
|
830
833
|
return to_snake(camel)
|
|
831
834
|
|
|
835
|
+
def sql_expression(
|
|
836
|
+
self,
|
|
837
|
+
sql_fragment: str,
|
|
838
|
+
*raw_args: str,
|
|
839
|
+
output_type: str | None = None,
|
|
840
|
+
**raw_kwargs: str,
|
|
841
|
+
) -> str:
|
|
842
|
+
"""
|
|
843
|
+
Creates a pydal Expression object representing a raw SQL fragment.
|
|
844
|
+
|
|
845
|
+
Args:
|
|
846
|
+
sql_fragment: The raw SQL fragment.
|
|
847
|
+
*raw_args: Arguments to be interpolated into the SQL fragment.
|
|
848
|
+
output_type: The expected output type of the expression.
|
|
849
|
+
**raw_kwargs: Keyword arguments to be interpolated into the SQL fragment.
|
|
850
|
+
|
|
851
|
+
Returns:
|
|
852
|
+
A pydal Expression object.
|
|
853
|
+
"""
|
|
854
|
+
return sql_expression(self, sql_fragment, *raw_args, output_type=output_type, **raw_kwargs)
|
|
855
|
+
|
|
832
856
|
|
|
833
857
|
def default_representer(field: TypedField[T], value: T, table: Type[TypedTable]) -> str:
|
|
834
858
|
"""
|
|
835
859
|
Simply call field.represent on the value.
|
|
836
860
|
"""
|
|
837
861
|
if represent := getattr(field, "represent", None):
|
|
838
|
-
return
|
|
862
|
+
return str(represent(value, table))
|
|
839
863
|
else:
|
|
840
864
|
return repr(value)
|
|
841
865
|
|
|
@@ -848,7 +872,7 @@ R = typing.TypeVar("R")
|
|
|
848
872
|
|
|
849
873
|
def reorder_fields(
|
|
850
874
|
table: pydal.objects.Table,
|
|
851
|
-
fields: typing.Iterable[str | Field | TypedField],
|
|
875
|
+
fields: typing.Iterable[str | Field | TypedField[Any]],
|
|
852
876
|
keep_others: bool = True,
|
|
853
877
|
) -> None:
|
|
854
878
|
"""
|
|
@@ -1423,7 +1447,7 @@ class TableMeta(type):
|
|
|
1423
1447
|
"""
|
|
1424
1448
|
return cls._hook_once(cls._after_delete, fn)
|
|
1425
1449
|
|
|
1426
|
-
def reorder_fields(cls, *fields: str | Field | TypedField, keep_others: bool = True):
|
|
1450
|
+
def reorder_fields(cls, *fields: str | Field | TypedField[Any], keep_others: bool = True) -> None:
|
|
1427
1451
|
"""
|
|
1428
1452
|
Reorder fields of a typedal table.
|
|
1429
1453
|
|
|
@@ -1433,7 +1457,6 @@ class TableMeta(type):
|
|
|
1433
1457
|
- True (default): keep other fields at the end, in their original order.
|
|
1434
1458
|
- False: remove other fields (only keep what's specified).
|
|
1435
1459
|
"""
|
|
1436
|
-
|
|
1437
1460
|
return reorder_fields(cls._table, fields, keep_others=keep_others)
|
|
1438
1461
|
|
|
1439
1462
|
|
|
@@ -1770,13 +1793,13 @@ class TypedTable(_TypedTable, metaclass=TableMeta):
|
|
|
1770
1793
|
|
|
1771
1794
|
raise AttributeError(item)
|
|
1772
1795
|
|
|
1773
|
-
def keys(self):
|
|
1796
|
+
def keys(self) -> list[str]:
|
|
1774
1797
|
"""
|
|
1775
1798
|
Return the combination of row + relationship keys.
|
|
1776
1799
|
|
|
1777
1800
|
Used by dict(row).
|
|
1778
1801
|
"""
|
|
1779
|
-
return list(self._row.keys()) + getattr(self, "_with", [])
|
|
1802
|
+
return list(self._row.keys() if self._row else ()) + getattr(self, "_with", [])
|
|
1780
1803
|
|
|
1781
1804
|
def get(self, item: str, default: Any = None) -> Any:
|
|
1782
1805
|
"""
|
|
@@ -2037,7 +2060,17 @@ class TypedTable(_TypedTable, metaclass=TableMeta):
|
|
|
2037
2060
|
|
|
2038
2061
|
return pydal2sql.generate_sql(cls)
|
|
2039
2062
|
|
|
2040
|
-
def render(self, fields=None, compact=False) -> Self:
|
|
2063
|
+
def render(self, fields: list[Field] = None, compact: bool = False) -> Self:
|
|
2064
|
+
"""
|
|
2065
|
+
Renders a copy of the object with potentially modified values.
|
|
2066
|
+
|
|
2067
|
+
Args:
|
|
2068
|
+
fields: A list of fields to render. Defaults to all representable fields in the table.
|
|
2069
|
+
compact: Whether to return only the value of the first field if there is only one field.
|
|
2070
|
+
|
|
2071
|
+
Returns:
|
|
2072
|
+
A copy of the object with potentially modified values.
|
|
2073
|
+
"""
|
|
2041
2074
|
row = copy.deepcopy(self)
|
|
2042
2075
|
keys = list(row)
|
|
2043
2076
|
if not fields:
|
|
@@ -2091,7 +2124,7 @@ class TypedTable(_TypedTable, metaclass=TableMeta):
|
|
|
2091
2124
|
)
|
|
2092
2125
|
|
|
2093
2126
|
if compact and len(keys) == 1 and keys[0] != "_extra": # pragma: no cover
|
|
2094
|
-
return row[keys[0]]
|
|
2127
|
+
return typing.cast(Self, row[keys[0]])
|
|
2095
2128
|
return row
|
|
2096
2129
|
|
|
2097
2130
|
|
|
@@ -2290,11 +2323,11 @@ class TypedRows(typing.Collection[T_MetaInstance], Rows):
|
|
|
2290
2323
|
|
|
2291
2324
|
def as_dict(
|
|
2292
2325
|
self,
|
|
2293
|
-
key: str | Field = None,
|
|
2326
|
+
key: str | Field | None = None,
|
|
2294
2327
|
compact: bool = False,
|
|
2295
2328
|
storage_to_dict: bool = False,
|
|
2296
2329
|
datetime_to_str: bool = False,
|
|
2297
|
-
custom_types: list[type] = None,
|
|
2330
|
+
custom_types: list[type] | None = None,
|
|
2298
2331
|
) -> dict[int, AnyDict]:
|
|
2299
2332
|
"""
|
|
2300
2333
|
Get the data in a dict of dicts.
|
|
@@ -2468,10 +2501,12 @@ class TypedRows(typing.Collection[T_MetaInstance], Rows):
|
|
|
2468
2501
|
self.__dict__.update(state)
|
|
2469
2502
|
# db etc. set after undill by caching.py
|
|
2470
2503
|
|
|
2471
|
-
def render(
|
|
2504
|
+
def render(
|
|
2505
|
+
self, i: int | None = None, fields: list[Field] | None = None
|
|
2506
|
+
) -> typing.Generator[T_MetaInstance, None, None]:
|
|
2472
2507
|
"""
|
|
2473
|
-
Takes an index and returns a copy of the indexed row with values
|
|
2474
|
-
|
|
2508
|
+
Takes an index and returns a copy of the indexed row with values \
|
|
2509
|
+
transformed via the "represent" attributes of the associated fields.
|
|
2475
2510
|
|
|
2476
2511
|
Args:
|
|
2477
2512
|
i: index. If not specified, a generator is returned for iteration
|
|
@@ -2481,7 +2516,7 @@ class TypedRows(typing.Collection[T_MetaInstance], Rows):
|
|
|
2481
2516
|
"""
|
|
2482
2517
|
if i is None:
|
|
2483
2518
|
# difference: uses .keys() instead of index
|
|
2484
|
-
return (self.render(i, fields=fields) for i in self.records
|
|
2519
|
+
return (self.render(i, fields=fields) for i in self.records)
|
|
2485
2520
|
|
|
2486
2521
|
if not self.db.has_representer("rows_render"): # pragma: no cover
|
|
2487
2522
|
raise RuntimeError(
|
|
@@ -2503,10 +2538,10 @@ from .caching import ( # noqa: E402
|
|
|
2503
2538
|
)
|
|
2504
2539
|
|
|
2505
2540
|
|
|
2506
|
-
def normalize_table_keys(row: Row, pattern: re.Pattern = re.compile(r"^([a-zA-Z_]+)_(\d{5,})$")) -> Row:
|
|
2541
|
+
def normalize_table_keys(row: Row, pattern: re.Pattern[str] = re.compile(r"^([a-zA-Z_]+)_(\d{5,})$")) -> Row:
|
|
2507
2542
|
"""
|
|
2508
|
-
Normalize table keys in a PyDAL Row object by stripping numeric hash suffixes
|
|
2509
|
-
|
|
2543
|
+
Normalize table keys in a PyDAL Row object by stripping numeric hash suffixes from table names, \
|
|
2544
|
+
only if the suffix is 5 or more digits.
|
|
2510
2545
|
|
|
2511
2546
|
For example:
|
|
2512
2547
|
Row({'articles_12345': {...}}) -> Row({'articles': {...}})
|
|
@@ -2645,7 +2680,7 @@ class QueryBuilder(typing.Generic[T_MetaInstance]):
|
|
|
2645
2680
|
|
|
2646
2681
|
def where(
|
|
2647
2682
|
self,
|
|
2648
|
-
*queries_or_lambdas: Query | typing.Callable[[Type[T_MetaInstance]], Query] | dict,
|
|
2683
|
+
*queries_or_lambdas: Query | typing.Callable[[Type[T_MetaInstance]], Query] | dict[str, Any],
|
|
2649
2684
|
**filters: Any,
|
|
2650
2685
|
) -> "QueryBuilder[T_MetaInstance]":
|
|
2651
2686
|
"""
|
|
@@ -2669,15 +2704,15 @@ class QueryBuilder(typing.Generic[T_MetaInstance]):
|
|
|
2669
2704
|
filters,
|
|
2670
2705
|
)
|
|
2671
2706
|
|
|
2672
|
-
subquery
|
|
2707
|
+
subquery = typing.cast(Query, DummyQuery())
|
|
2673
2708
|
for query_part in queries_or_lambdas:
|
|
2674
|
-
if isinstance(query_part,
|
|
2709
|
+
if isinstance(query_part, (Field, pydal.objects.Field)) or is_typed_field(query_part):
|
|
2710
|
+
subquery |= typing.cast(Query, query_part != None)
|
|
2711
|
+
elif isinstance(query_part, (pydal.objects.Query, Expression, pydal.objects.Expression)):
|
|
2675
2712
|
subquery |= typing.cast(Query, query_part)
|
|
2676
2713
|
elif callable(query_part):
|
|
2677
2714
|
if result := query_part(self.model):
|
|
2678
2715
|
subquery |= result
|
|
2679
|
-
elif isinstance(query_part, (Field, _Field)) or is_typed_field(query_part):
|
|
2680
|
-
subquery |= typing.cast(Query, query_part != None)
|
|
2681
2716
|
elif isinstance(query_part, dict):
|
|
2682
2717
|
subsubquery = DummyQuery()
|
|
2683
2718
|
for field, value in query_part.items():
|
|
@@ -2727,8 +2762,9 @@ class QueryBuilder(typing.Generic[T_MetaInstance]):
|
|
|
2727
2762
|
if isinstance(condition, pydal.objects.Query):
|
|
2728
2763
|
condition = as_lambda(condition)
|
|
2729
2764
|
|
|
2765
|
+
to_field = typing.cast(Type[TypedTable], fields[0])
|
|
2730
2766
|
relationships = {
|
|
2731
|
-
str(
|
|
2767
|
+
str(to_field): Relationship(to_field, condition=condition, join=method, condition_and=condition_and)
|
|
2732
2768
|
}
|
|
2733
2769
|
elif on:
|
|
2734
2770
|
if len(fields) != 1:
|
|
@@ -2739,7 +2775,9 @@ class QueryBuilder(typing.Generic[T_MetaInstance]):
|
|
|
2739
2775
|
|
|
2740
2776
|
if isinstance(on, list):
|
|
2741
2777
|
on = as_lambda(on)
|
|
2742
|
-
|
|
2778
|
+
|
|
2779
|
+
to_field = typing.cast(Type[TypedTable], fields[0])
|
|
2780
|
+
relationships = {str(to_field): Relationship(to_field, on=on, join=method, condition_and=condition_and)}
|
|
2743
2781
|
|
|
2744
2782
|
else:
|
|
2745
2783
|
if fields:
|
typedal/fields.py
CHANGED
|
@@ -256,18 +256,50 @@ def TimestampField(**kw: Unpack[FieldSettings]) -> TypedField[dt.datetime]:
|
|
|
256
256
|
)
|
|
257
257
|
|
|
258
258
|
|
|
259
|
-
def safe_decode_native_point(value: str | None):
|
|
259
|
+
def safe_decode_native_point(value: str | None) -> tuple[float, ...]:
|
|
260
|
+
"""
|
|
261
|
+
Safely decode a string into a tuple of floats.
|
|
262
|
+
|
|
263
|
+
The function attempts to parse the input string using `ast.literal_eval`.
|
|
264
|
+
If the parsing is successful, the function casts the parsed value to a tuple of floats and returns it.
|
|
265
|
+
Otherwise, the function returns an empty tuple.
|
|
266
|
+
|
|
267
|
+
Args:
|
|
268
|
+
value: The string to decode.
|
|
269
|
+
|
|
270
|
+
Returns:
|
|
271
|
+
A tuple of floats.
|
|
272
|
+
"""
|
|
260
273
|
if not value:
|
|
261
274
|
return ()
|
|
262
275
|
|
|
263
276
|
try:
|
|
264
|
-
|
|
277
|
+
parsed = ast.literal_eval(value)
|
|
278
|
+
return typing.cast(tuple[float, ...], parsed)
|
|
265
279
|
except ValueError: # pragma: no cover
|
|
266
280
|
# should not happen when inserted with `safe_encode_native_point` but you never know
|
|
267
281
|
return ()
|
|
268
282
|
|
|
269
283
|
|
|
270
|
-
def safe_encode_native_point(value: tuple[str, str] | str) -> str:
|
|
284
|
+
def safe_encode_native_point(value: tuple[str, str] | tuple[float, float] | str) -> str:
|
|
285
|
+
"""
|
|
286
|
+
|
|
287
|
+
Safe encodes a point value.
|
|
288
|
+
|
|
289
|
+
The function takes a point value as input.
|
|
290
|
+
It can be a string in the format "x,y" or a tuple of two numbers.
|
|
291
|
+
The function converts the string to a tuple if necessary, validates the tuple,
|
|
292
|
+
and formats it into the expected string format.
|
|
293
|
+
|
|
294
|
+
Args:
|
|
295
|
+
value: The point value to be encoded.
|
|
296
|
+
|
|
297
|
+
Returns:
|
|
298
|
+
The encoded point value as a string in the format "x,y".
|
|
299
|
+
|
|
300
|
+
Raises:
|
|
301
|
+
ValueError: If the input value is not a valid point.
|
|
302
|
+
"""
|
|
271
303
|
if not value:
|
|
272
304
|
return ""
|
|
273
305
|
|
|
@@ -276,13 +308,15 @@ def safe_encode_native_point(value: tuple[str, str] | str) -> str:
|
|
|
276
308
|
value = value.strip("() ")
|
|
277
309
|
if not value:
|
|
278
310
|
return ""
|
|
279
|
-
|
|
311
|
+
value_tup = tuple(float(x.strip()) for x in value.split(","))
|
|
312
|
+
else:
|
|
313
|
+
value_tup = value # type: ignore
|
|
280
314
|
|
|
281
315
|
# Validate and format
|
|
282
|
-
if len(
|
|
316
|
+
if len(value_tup) != 2:
|
|
283
317
|
raise ValueError("Point must have exactly 2 coordinates")
|
|
284
318
|
|
|
285
|
-
x, y =
|
|
319
|
+
x, y = value_tup
|
|
286
320
|
return f"({x},{y})"
|
|
287
321
|
|
|
288
322
|
|
typedal/helpers.py
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
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
|
|
@@ -12,10 +14,10 @@ from typing import Any
|
|
|
12
14
|
|
|
13
15
|
from pydal import DAL
|
|
14
16
|
|
|
15
|
-
from .types import AnyDict, Field, Table
|
|
17
|
+
from .types import AnyDict, Expression, Field, Table
|
|
16
18
|
|
|
17
19
|
if typing.TYPE_CHECKING:
|
|
18
|
-
from . import TypeDAL, TypedField, TypedTable
|
|
20
|
+
from . import TypeDAL, TypedField, TypedTable
|
|
19
21
|
|
|
20
22
|
T = typing.TypeVar("T")
|
|
21
23
|
|
|
@@ -101,12 +103,14 @@ def origin_is_subclass(obj: Any, _type: type) -> bool:
|
|
|
101
103
|
return bool(
|
|
102
104
|
typing.get_origin(obj)
|
|
103
105
|
and isinstance(typing.get_origin(obj), type)
|
|
104
|
-
and issubclass(typing.get_origin(obj), _type)
|
|
106
|
+
and issubclass(typing.get_origin(obj), _type),
|
|
105
107
|
)
|
|
106
108
|
|
|
107
109
|
|
|
108
110
|
def mktable(
|
|
109
|
-
data: dict[Any, Any],
|
|
111
|
+
data: dict[Any, Any],
|
|
112
|
+
header: typing.Optional[typing.Iterable[str] | range] = None,
|
|
113
|
+
skip_first: bool = True,
|
|
110
114
|
) -> str:
|
|
111
115
|
"""
|
|
112
116
|
Display a table for 'data'.
|
|
@@ -331,3 +335,61 @@ class classproperty:
|
|
|
331
335
|
The value returned by the function.
|
|
332
336
|
"""
|
|
333
337
|
return self.fget(owner)
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def sql_escape(db: TypeDAL, sql_fragment: str, *raw_args: Any, **raw_kwargs: Any) -> str:
|
|
341
|
+
"""
|
|
342
|
+
Generates escaped SQL fragments with placeholders.
|
|
343
|
+
|
|
344
|
+
Args:
|
|
345
|
+
db: Database object.
|
|
346
|
+
sql_fragment: SQL fragment with placeholders.
|
|
347
|
+
*raw_args: Positional arguments to be escaped.
|
|
348
|
+
**raw_kwargs: Keyword arguments to be escaped.
|
|
349
|
+
|
|
350
|
+
Returns:
|
|
351
|
+
Escaped SQL fragment with placeholders replaced with escaped values.
|
|
352
|
+
|
|
353
|
+
Raises:
|
|
354
|
+
ValueError: If both args and kwargs are provided.
|
|
355
|
+
"""
|
|
356
|
+
if raw_args and raw_kwargs: # pragma: no cover
|
|
357
|
+
raise ValueError("Please provide either args or kwargs, not both.")
|
|
358
|
+
|
|
359
|
+
elif raw_args:
|
|
360
|
+
# list
|
|
361
|
+
return sql_fragment % tuple(db._adapter.adapt(placeholder) for placeholder in raw_args)
|
|
362
|
+
else:
|
|
363
|
+
# dict
|
|
364
|
+
return sql_fragment % {key: db._adapter.adapt(placeholder) for key, placeholder in raw_kwargs.items()}
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
def sql_expression(
|
|
368
|
+
db: TypeDAL,
|
|
369
|
+
sql_fragment: str,
|
|
370
|
+
*raw_args: str,
|
|
371
|
+
output_type: str | None = None,
|
|
372
|
+
**raw_kwargs: str,
|
|
373
|
+
) -> Expression:
|
|
374
|
+
"""
|
|
375
|
+
Creates a pydal Expression object representing a raw SQL fragment.
|
|
376
|
+
|
|
377
|
+
Args:
|
|
378
|
+
db: The TypeDAL object.
|
|
379
|
+
sql_fragment: The raw SQL fragment.
|
|
380
|
+
*raw_args: Arguments to be interpolated into the SQL fragment.
|
|
381
|
+
output_type: The expected output type of the expression.
|
|
382
|
+
**raw_kwargs: Keyword arguments to be interpolated into the SQL fragment.
|
|
383
|
+
|
|
384
|
+
Returns:
|
|
385
|
+
A pydal Expression object.
|
|
386
|
+
"""
|
|
387
|
+
safe_sql = sql_escape(db, sql_fragment, *raw_args, **raw_kwargs)
|
|
388
|
+
|
|
389
|
+
# create a pydal Expression wrapping a raw SQL fragment + placeholders
|
|
390
|
+
return Expression(
|
|
391
|
+
db,
|
|
392
|
+
db._adapter.dialect.raw,
|
|
393
|
+
safe_sql,
|
|
394
|
+
type=output_type, # optional type hint
|
|
395
|
+
)
|
typedal/mixins.py
CHANGED
|
@@ -180,7 +180,7 @@ class SlugMixin(Mixin):
|
|
|
180
180
|
if slug_field is None:
|
|
181
181
|
raise ValueError(
|
|
182
182
|
"SlugMixin requires a valid slug_field setting: "
|
|
183
|
-
"e.g. `class MyClass(TypedTable, SlugMixin, slug_field='title'): ...`"
|
|
183
|
+
"e.g. `class MyClass(TypedTable, SlugMixin, slug_field='title'): ...`",
|
|
184
184
|
)
|
|
185
185
|
|
|
186
186
|
if slug_suffix:
|
|
@@ -197,7 +197,7 @@ class SlugMixin(Mixin):
|
|
|
197
197
|
|
|
198
198
|
@classmethod
|
|
199
199
|
def __generate_slug_before_insert(cls, row: OpRow) -> None:
|
|
200
|
-
if row.get("slug"):
|
|
200
|
+
if row.get("slug"): # type: ignore
|
|
201
201
|
# manually set -> skip
|
|
202
202
|
return None
|
|
203
203
|
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
typedal/__about__.py,sha256=6xOJ9nMOnP3X9kALmHamZ6MhTwflJ564cHVnjqBINXg,207
|
|
2
|
+
typedal/__init__.py,sha256=mCU8C3xPYiGcai4PYIoZNJSkFa1jb0MHkV8s2XXqEeY,484
|
|
3
|
+
typedal/caching.py,sha256=6YUzUMpan56nSy3D-Jl0FS-8V4LbTnpRSoDJHj6yPYo,11782
|
|
4
|
+
typedal/cli.py,sha256=e08L8k6q1NGSzpKs7ywin0uwkK7Kz07I4REVjHdbyyE,19267
|
|
5
|
+
typedal/config.py,sha256=0qy1zrTUdtmXPM9jHzFnSR1DJsqGJqcdG6pvhzKQHe0,11625
|
|
6
|
+
typedal/core.py,sha256=eFcm3wyjydYpP3cEfoGz1jUVzaNBzRSoYSScYweZRmE,117701
|
|
7
|
+
typedal/fields.py,sha256=bZIgjl3Lj7eMqFCyt-bogsS_BqIs3cETEUH4W59qiXw,8425
|
|
8
|
+
typedal/for_py4web.py,sha256=KIIu8XgnAfRQCJfZCra79k8SInOHiFuLDKUv3hzTJng,1908
|
|
9
|
+
typedal/for_web2py.py,sha256=xn7zo6ImsmTkH6LacbjLQl2oqyBvP0zLqRxEJvMQk1w,1929
|
|
10
|
+
typedal/helpers.py,sha256=eQjH3JpL1MPqMCcaFtj01tj0LxdwdT2zbnvKlDnXo5E,10702
|
|
11
|
+
typedal/mixins.py,sha256=NiGp-3Lr1lllWjP-whUhc_2HXwCS_ROB0bs5N_s9wk4,7976
|
|
12
|
+
typedal/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
13
|
+
typedal/types.py,sha256=1FIgv1s0be0E8r5Wd9E1nvDvK4ETV8u2NlfI7_P6UUY,6752
|
|
14
|
+
typedal/web2py_py4web_shared.py,sha256=UYmD0_aK1bSVBt_f3j59Mxq-zOmQNkYkb8sPDUibq68,1539
|
|
15
|
+
typedal/serializers/as_json.py,sha256=3JZlFhPrdvZVFAmH7P5DUAz8-TIk-br0F1CjKG3PFDM,2246
|
|
16
|
+
typedal-3.17.0.dist-info/METADATA,sha256=SdNlSCbh3PyB8TUqVf_lE-x8r57BW4zUDijqcyLCR-w,10461
|
|
17
|
+
typedal-3.17.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
18
|
+
typedal-3.17.0.dist-info/entry_points.txt,sha256=m1wqcc_10rHWPdlQ71zEkmJDADUAnZtn7Jac_6mbyUc,44
|
|
19
|
+
typedal-3.17.0.dist-info/RECORD,,
|
typedal-3.16.5.dist-info/RECORD
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
typedal/__about__.py,sha256=e0fosPOvZo8qNSX4V8VRAtwzAGVMRPU4yC9FAx0jjfk,207
|
|
2
|
-
typedal/__init__.py,sha256=Y6LT5UE3HrfWND_drJddYFbDjfnvqQER8MxZiEREFGw,366
|
|
3
|
-
typedal/caching.py,sha256=6YUzUMpan56nSy3D-Jl0FS-8V4LbTnpRSoDJHj6yPYo,11782
|
|
4
|
-
typedal/cli.py,sha256=SnWceLPDd-t90VPHAV9O3RR7JZtpVniT55TqtrRv3VM,19255
|
|
5
|
-
typedal/config.py,sha256=0qy1zrTUdtmXPM9jHzFnSR1DJsqGJqcdG6pvhzKQHe0,11625
|
|
6
|
-
typedal/core.py,sha256=z0EsNEnUmfoHA2UWIUpRB_F3ptvLoKNG1SXX-cxh6L0,116127
|
|
7
|
-
typedal/fields.py,sha256=oOmTonXG-g4Lpj5_gSr8GJ-EZIEqO435Fm8-MS_cmoc,7356
|
|
8
|
-
typedal/for_py4web.py,sha256=KIIu8XgnAfRQCJfZCra79k8SInOHiFuLDKUv3hzTJng,1908
|
|
9
|
-
typedal/for_web2py.py,sha256=xn7zo6ImsmTkH6LacbjLQl2oqyBvP0zLqRxEJvMQk1w,1929
|
|
10
|
-
typedal/helpers.py,sha256=LpBgTwKmt9f1b4Mz98mxusvIHGiCpW_abDMZLP81g6Y,8850
|
|
11
|
-
typedal/mixins.py,sha256=p8OAx1N9MVEF4bhdxB_cC-GTczXzt76OMl_KBKtMYAU,7959
|
|
12
|
-
typedal/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
13
|
-
typedal/types.py,sha256=1FIgv1s0be0E8r5Wd9E1nvDvK4ETV8u2NlfI7_P6UUY,6752
|
|
14
|
-
typedal/web2py_py4web_shared.py,sha256=UYmD0_aK1bSVBt_f3j59Mxq-zOmQNkYkb8sPDUibq68,1539
|
|
15
|
-
typedal/serializers/as_json.py,sha256=3JZlFhPrdvZVFAmH7P5DUAz8-TIk-br0F1CjKG3PFDM,2246
|
|
16
|
-
typedal-3.16.5.dist-info/METADATA,sha256=Uv3w_mVpZkVEKeM3hl8ROUuOoCJFZuIcx2F7fJipzb0,10461
|
|
17
|
-
typedal-3.16.5.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
18
|
-
typedal-3.16.5.dist-info/entry_points.txt,sha256=m1wqcc_10rHWPdlQ71zEkmJDADUAnZtn7Jac_6mbyUc,44
|
|
19
|
-
typedal-3.16.5.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|