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/__about__.py +1 -1
- typedal/__init__.py +21 -3
- typedal/caching.py +37 -34
- typedal/config.py +18 -16
- typedal/constants.py +25 -0
- typedal/core.py +188 -3115
- typedal/define.py +188 -0
- typedal/fields.py +293 -34
- typedal/for_py4web.py +1 -1
- typedal/for_web2py.py +1 -1
- typedal/helpers.py +329 -40
- typedal/mixins.py +23 -27
- typedal/query_builder.py +1119 -0
- typedal/relationships.py +390 -0
- typedal/rows.py +524 -0
- typedal/serializers/as_json.py +9 -10
- typedal/tables.py +1131 -0
- typedal/types.py +187 -179
- typedal/web2py_py4web_shared.py +1 -1
- {typedal-3.16.4.dist-info → typedal-4.2.0.dist-info}/METADATA +8 -7
- typedal-4.2.0.dist-info/RECORD +25 -0
- {typedal-3.16.4.dist-info → typedal-4.2.0.dist-info}/WHEEL +1 -1
- typedal-3.16.4.dist-info/RECORD +0 -19
- {typedal-3.16.4.dist-info → typedal-4.2.0.dist-info}/entry_points.txt +0 -0
typedal/__about__.py
CHANGED
typedal/__init__.py
CHANGED
|
@@ -2,12 +2,30 @@
|
|
|
2
2
|
TypeDAL Library.
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
|
-
from . import
|
|
6
|
-
from .
|
|
5
|
+
from .core import TypeDAL
|
|
6
|
+
from .fields import TypedField
|
|
7
|
+
from .helpers import sql_expression
|
|
8
|
+
from .query_builder import QueryBuilder
|
|
9
|
+
from .relationships import Relationship, relationship
|
|
10
|
+
from .rows import PaginatedRows, TypedRows
|
|
11
|
+
from .tables import TypedTable
|
|
12
|
+
|
|
13
|
+
from . import fields # isort: skip
|
|
7
14
|
|
|
8
15
|
try:
|
|
9
16
|
from .for_py4web import DAL as P4W_DAL
|
|
10
17
|
except ImportError: # pragma: no cover
|
|
11
18
|
P4W_DAL = None # type: ignore
|
|
12
19
|
|
|
13
|
-
__all__ = [
|
|
20
|
+
__all__ = [
|
|
21
|
+
"PaginatedRows",
|
|
22
|
+
"QueryBuilder",
|
|
23
|
+
"Relationship",
|
|
24
|
+
"TypeDAL",
|
|
25
|
+
"TypedField",
|
|
26
|
+
"TypedRows",
|
|
27
|
+
"TypedTable",
|
|
28
|
+
"fields",
|
|
29
|
+
"relationship",
|
|
30
|
+
"sql_expression",
|
|
31
|
+
]
|
typedal/caching.py
CHANGED
|
@@ -3,27 +3,28 @@ Helpers to facilitate db-based caching.
|
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
5
|
import contextlib
|
|
6
|
+
import datetime as dt
|
|
6
7
|
import hashlib
|
|
7
8
|
import json
|
|
8
|
-
import typing
|
|
9
|
-
from datetime import datetime, timedelta, timezone
|
|
10
|
-
from typing import Any, Iterable, Mapping, Optional, TypeVar
|
|
9
|
+
import typing as t
|
|
11
10
|
|
|
12
11
|
import dill # nosec
|
|
13
12
|
from pydal.objects import Field, Rows, Set
|
|
14
13
|
|
|
15
|
-
from .
|
|
14
|
+
from .fields import TypedField
|
|
15
|
+
from .rows import TypedRows
|
|
16
|
+
from .tables import TypedTable
|
|
16
17
|
from .types import Query
|
|
17
18
|
|
|
18
|
-
if
|
|
19
|
+
if t.TYPE_CHECKING:
|
|
19
20
|
from .core import TypeDAL
|
|
20
21
|
|
|
21
22
|
|
|
22
|
-
def get_now(tz: timezone = timezone.utc) -> datetime:
|
|
23
|
+
def get_now(tz: dt.timezone = dt.timezone.utc) -> dt.datetime:
|
|
23
24
|
"""
|
|
24
25
|
Get the default datetime, optionally in a specific timezone.
|
|
25
26
|
"""
|
|
26
|
-
return datetime.now(tz)
|
|
27
|
+
return dt.datetime.now(tz)
|
|
27
28
|
|
|
28
29
|
|
|
29
30
|
class _TypedalCache(TypedTable):
|
|
@@ -33,8 +34,8 @@ class _TypedalCache(TypedTable):
|
|
|
33
34
|
|
|
34
35
|
key: TypedField[str]
|
|
35
36
|
data: TypedField[bytes]
|
|
36
|
-
cached_at = TypedField(datetime, default=get_now)
|
|
37
|
-
expires_at: TypedField[datetime | None]
|
|
37
|
+
cached_at = TypedField(dt.datetime, default=get_now)
|
|
38
|
+
expires_at: TypedField[dt.datetime | None]
|
|
38
39
|
|
|
39
40
|
|
|
40
41
|
class _TypedalCacheDependency(TypedTable):
|
|
@@ -47,7 +48,7 @@ class _TypedalCacheDependency(TypedTable):
|
|
|
47
48
|
idx: TypedField[int]
|
|
48
49
|
|
|
49
50
|
|
|
50
|
-
def prepare(field: Any) -> str:
|
|
51
|
+
def prepare(field: t.Any) -> str:
|
|
51
52
|
"""
|
|
52
53
|
Prepare data to be used in a cache key.
|
|
53
54
|
|
|
@@ -56,10 +57,10 @@ def prepare(field: Any) -> str:
|
|
|
56
57
|
"""
|
|
57
58
|
if isinstance(field, str):
|
|
58
59
|
return field
|
|
59
|
-
elif isinstance(field, (dict, Mapping)):
|
|
60
|
+
elif isinstance(field, (dict, t.Mapping)):
|
|
60
61
|
data = {str(k): prepare(v) for k, v in field.items()}
|
|
61
62
|
return json.dumps(data, sort_keys=True)
|
|
62
|
-
elif isinstance(field, Iterable):
|
|
63
|
+
elif isinstance(field, t.Iterable):
|
|
63
64
|
return ",".join(sorted([prepare(_) for _ in field]))
|
|
64
65
|
elif isinstance(field, bool):
|
|
65
66
|
return str(int(field))
|
|
@@ -67,7 +68,7 @@ def prepare(field: Any) -> str:
|
|
|
67
68
|
return str(field)
|
|
68
69
|
|
|
69
70
|
|
|
70
|
-
def create_cache_key(*fields: Any) -> str:
|
|
71
|
+
def create_cache_key(*fields: t.Any) -> str:
|
|
71
72
|
"""
|
|
72
73
|
Turn any fields of data into a string.
|
|
73
74
|
"""
|
|
@@ -83,7 +84,7 @@ def hash_cache_key(cache_key: str | bytes) -> str:
|
|
|
83
84
|
return h.hexdigest()
|
|
84
85
|
|
|
85
86
|
|
|
86
|
-
def create_and_hash_cache_key(*fields: Any) -> tuple[str, str]:
|
|
87
|
+
def create_and_hash_cache_key(*fields: t.Any) -> tuple[str, str]:
|
|
87
88
|
"""
|
|
88
89
|
Combine the input fields into one key and hash it with SHA 256.
|
|
89
90
|
"""
|
|
@@ -112,7 +113,7 @@ def _get_dependency_ids(rows: Rows, dependency_keys: list[tuple[Field, str]]) ->
|
|
|
112
113
|
return dependencies
|
|
113
114
|
|
|
114
115
|
|
|
115
|
-
def _determine_dependencies_auto(_: TypedRows[Any], rows: Rows) -> DependencyTupleSet:
|
|
116
|
+
def _determine_dependencies_auto(_: TypedRows[t.Any], rows: Rows) -> DependencyTupleSet:
|
|
116
117
|
dependency_keys = []
|
|
117
118
|
for field in rows.fields:
|
|
118
119
|
if str(field).endswith(".id"):
|
|
@@ -123,7 +124,7 @@ def _determine_dependencies_auto(_: TypedRows[Any], rows: Rows) -> DependencyTup
|
|
|
123
124
|
return _get_dependency_ids(rows, dependency_keys)
|
|
124
125
|
|
|
125
126
|
|
|
126
|
-
def _determine_dependencies(instance: TypedRows[Any], rows: Rows, depends_on: list[Any]) -> DependencyTupleSet:
|
|
127
|
+
def _determine_dependencies(instance: TypedRows[t.Any], rows: Rows, depends_on: list[t.Any]) -> DependencyTupleSet:
|
|
127
128
|
if not depends_on:
|
|
128
129
|
return _determine_dependencies_auto(instance, rows)
|
|
129
130
|
|
|
@@ -144,11 +145,11 @@ def _determine_dependencies(instance: TypedRows[Any], rows: Rows, depends_on: li
|
|
|
144
145
|
return _get_dependency_ids(rows, dependency_keys)
|
|
145
146
|
|
|
146
147
|
|
|
147
|
-
def remove_cache(idx: int | Iterable[int], table: str) -> None:
|
|
148
|
+
def remove_cache(idx: int | t.Iterable[int], table: str) -> None:
|
|
148
149
|
"""
|
|
149
150
|
Remove any cache entries that are dependant on one or multiple indices of a table.
|
|
150
151
|
"""
|
|
151
|
-
if not isinstance(idx, Iterable):
|
|
152
|
+
if not isinstance(idx, t.Iterable):
|
|
152
153
|
idx = [idx]
|
|
153
154
|
|
|
154
155
|
related = (
|
|
@@ -184,12 +185,14 @@ def _remove_cache(s: Set, tablename: str) -> None:
|
|
|
184
185
|
remove_cache(indeces, tablename)
|
|
185
186
|
|
|
186
187
|
|
|
187
|
-
T_TypedTable = TypeVar("T_TypedTable", bound=TypedTable)
|
|
188
|
+
T_TypedTable = t.TypeVar("T_TypedTable", bound=TypedTable)
|
|
188
189
|
|
|
189
190
|
|
|
190
191
|
def get_expire(
|
|
191
|
-
expires_at: Optional[datetime] = None,
|
|
192
|
-
|
|
192
|
+
expires_at: t.Optional[dt.datetime] = None,
|
|
193
|
+
ttl: t.Optional[int | dt.timedelta] = None,
|
|
194
|
+
now: t.Optional[dt.datetime] = None,
|
|
195
|
+
) -> dt.datetime | None:
|
|
193
196
|
"""
|
|
194
197
|
Based on an expires_at date or a ttl (in seconds or a time delta), determine the expire date.
|
|
195
198
|
"""
|
|
@@ -197,10 +200,10 @@ def get_expire(
|
|
|
197
200
|
|
|
198
201
|
if expires_at and ttl:
|
|
199
202
|
raise ValueError("Please only supply an `expired at` date or a `ttl` in seconds!")
|
|
200
|
-
elif isinstance(ttl, timedelta):
|
|
203
|
+
elif isinstance(ttl, dt.timedelta):
|
|
201
204
|
return now + ttl
|
|
202
205
|
elif ttl:
|
|
203
|
-
return now + timedelta(seconds=ttl)
|
|
206
|
+
return now + dt.timedelta(seconds=ttl)
|
|
204
207
|
elif expires_at:
|
|
205
208
|
return expires_at
|
|
206
209
|
|
|
@@ -210,8 +213,8 @@ def get_expire(
|
|
|
210
213
|
def save_to_cache(
|
|
211
214
|
instance: TypedRows[T_TypedTable],
|
|
212
215
|
rows: Rows,
|
|
213
|
-
expires_at: Optional[datetime] = None,
|
|
214
|
-
ttl: Optional[int | timedelta] = None,
|
|
216
|
+
expires_at: t.Optional[dt.datetime] = None,
|
|
217
|
+
ttl: t.Optional[int | dt.timedelta] = None,
|
|
215
218
|
) -> TypedRows[T_TypedTable]:
|
|
216
219
|
"""
|
|
217
220
|
Save a typedrows result to the database, and save dependencies from rows.
|
|
@@ -237,13 +240,13 @@ def save_to_cache(
|
|
|
237
240
|
return instance
|
|
238
241
|
|
|
239
242
|
|
|
240
|
-
def _load_from_cache(key: str, db: "TypeDAL") -> Any | None:
|
|
243
|
+
def _load_from_cache(key: str, db: "TypeDAL") -> t.Any | None:
|
|
241
244
|
if not (row := _TypedalCache.where(key=key).first()):
|
|
242
245
|
return None
|
|
243
246
|
|
|
244
247
|
now = get_now()
|
|
245
248
|
|
|
246
|
-
expires = row.expires_at.replace(tzinfo=timezone.utc) if row.expires_at else None
|
|
249
|
+
expires = row.expires_at.replace(tzinfo=dt.timezone.utc) if row.expires_at else None
|
|
247
250
|
|
|
248
251
|
if expires and now >= expires:
|
|
249
252
|
row.delete_record()
|
|
@@ -257,11 +260,11 @@ def _load_from_cache(key: str, db: "TypeDAL") -> Any | None:
|
|
|
257
260
|
|
|
258
261
|
inst.db = db
|
|
259
262
|
inst.model = db._class_map[inst.model]
|
|
260
|
-
inst.model._setup_instance_methods(inst.model)
|
|
263
|
+
inst.model._setup_instance_methods(inst.model)
|
|
261
264
|
return inst
|
|
262
265
|
|
|
263
266
|
|
|
264
|
-
def load_from_cache(key: str, db: "TypeDAL") -> Any | None:
|
|
267
|
+
def load_from_cache(key: str, db: "TypeDAL") -> t.Any | None:
|
|
265
268
|
"""
|
|
266
269
|
If 'key' matches a non-expired row in the database, try to load the dill.
|
|
267
270
|
|
|
@@ -302,10 +305,10 @@ def _expired_and_valid_query() -> tuple[str, str]:
|
|
|
302
305
|
return expired_items, valid_items
|
|
303
306
|
|
|
304
307
|
|
|
305
|
-
T =
|
|
306
|
-
Stats =
|
|
308
|
+
T = t.TypeVar("T")
|
|
309
|
+
Stats = t.TypedDict("Stats", {"total": T, "valid": T, "expired": T})
|
|
307
310
|
|
|
308
|
-
RowStats =
|
|
311
|
+
RowStats = t.TypedDict(
|
|
309
312
|
"RowStats",
|
|
310
313
|
{
|
|
311
314
|
"Dependent Cache Entries": int,
|
|
@@ -338,7 +341,7 @@ def row_stats(db: "TypeDAL", table: str, row_id: str) -> Stats[RowStats]:
|
|
|
338
341
|
}
|
|
339
342
|
|
|
340
343
|
|
|
341
|
-
TableStats =
|
|
344
|
+
TableStats = t.TypedDict(
|
|
342
345
|
"TableStats",
|
|
343
346
|
{
|
|
344
347
|
"Dependent Cache Entries": int,
|
|
@@ -371,7 +374,7 @@ def table_stats(db: "TypeDAL", table: str) -> Stats[TableStats]:
|
|
|
371
374
|
}
|
|
372
375
|
|
|
373
376
|
|
|
374
|
-
GenericStats =
|
|
377
|
+
GenericStats = t.TypedDict(
|
|
375
378
|
"GenericStats",
|
|
376
379
|
{
|
|
377
380
|
"entries": int,
|
typedal/config.py
CHANGED
|
@@ -4,11 +4,10 @@ TypeDAL can be configured by a combination of pyproject.toml (static), env (dyna
|
|
|
4
4
|
|
|
5
5
|
import os
|
|
6
6
|
import re
|
|
7
|
-
import typing
|
|
7
|
+
import typing as t
|
|
8
8
|
import warnings
|
|
9
9
|
from collections import defaultdict
|
|
10
10
|
from pathlib import Path
|
|
11
|
-
from typing import Any, Optional
|
|
12
11
|
|
|
13
12
|
import tomli
|
|
14
13
|
from configuraptor import TypedConfig, alias
|
|
@@ -17,10 +16,12 @@ from dotenv import dotenv_values, find_dotenv
|
|
|
17
16
|
|
|
18
17
|
from .types import AnyDict
|
|
19
18
|
|
|
20
|
-
if
|
|
19
|
+
if t.TYPE_CHECKING:
|
|
21
20
|
from edwh_migrate import Config as MigrateConfig
|
|
22
21
|
from pydal2sql.typer_support import Config as P2SConfig
|
|
23
22
|
|
|
23
|
+
LazyPolicy = t.Literal["forbid", "warn", "ignore", "tolerate", "allow"]
|
|
24
|
+
|
|
24
25
|
|
|
25
26
|
class TypeDALConfig(TypedConfig):
|
|
26
27
|
"""
|
|
@@ -35,21 +36,22 @@ class TypeDALConfig(TypedConfig):
|
|
|
35
36
|
pool_size: int = 0
|
|
36
37
|
pyproject: str
|
|
37
38
|
connection: str = "default"
|
|
39
|
+
lazy_policy: LazyPolicy = "tolerate"
|
|
38
40
|
|
|
39
41
|
# pydal2sql:
|
|
40
42
|
input: str = ""
|
|
41
43
|
output: str = ""
|
|
42
44
|
noop: bool = False
|
|
43
45
|
magic: bool = True
|
|
44
|
-
tables: Optional[list[str]] = None
|
|
46
|
+
tables: t.Optional[list[str]] = None
|
|
45
47
|
function: str = "define_tables"
|
|
46
48
|
|
|
47
49
|
# edwh-migrate:
|
|
48
50
|
# migrate uri = database
|
|
49
|
-
database_to_restore: Optional[str]
|
|
50
|
-
migrate_cat_command: Optional[str]
|
|
51
|
-
schema_version: Optional[str]
|
|
52
|
-
redis_host: Optional[str]
|
|
51
|
+
database_to_restore: t.Optional[str]
|
|
52
|
+
migrate_cat_command: t.Optional[str]
|
|
53
|
+
schema_version: t.Optional[str]
|
|
54
|
+
redis_host: t.Optional[str]
|
|
53
55
|
migrate_table: str = "typedal_implemented_features"
|
|
54
56
|
flag_location: str
|
|
55
57
|
create_flag_location: bool = True
|
|
@@ -148,7 +150,7 @@ def _load_toml(path: str | bool | Path | None = True) -> tuple[str, AnyDict]:
|
|
|
148
150
|
with open(toml_path, "rb") as f:
|
|
149
151
|
data = tomli.load(f)
|
|
150
152
|
|
|
151
|
-
return str(toml_path) or "",
|
|
153
|
+
return str(toml_path) or "", t.cast(AnyDict, data["tool"]["typedal"])
|
|
152
154
|
except Exception as e:
|
|
153
155
|
warnings.warn(f"Could not load typedal config toml: {e}", source=e)
|
|
154
156
|
return str(toml_path) or "", {}
|
|
@@ -194,7 +196,7 @@ def get_db_for_alias(db_name: str) -> str:
|
|
|
194
196
|
return DB_ALIASES.get(db_name, db_name)
|
|
195
197
|
|
|
196
198
|
|
|
197
|
-
DEFAULTS: dict[str, Any |
|
|
199
|
+
DEFAULTS: dict[str, t.Any | t.Callable[[AnyDict], t.Any]] = {
|
|
198
200
|
"database": lambda data: data.get("db_uri") or "sqlite:memory",
|
|
199
201
|
"dialect": lambda data: (
|
|
200
202
|
get_db_for_alias(data["database"].split(":")[0]) if ":" in data["database"] else data.get("db_type")
|
|
@@ -208,7 +210,7 @@ DEFAULTS: dict[str, Any | typing.Callable[[AnyDict], Any]] = {
|
|
|
208
210
|
}
|
|
209
211
|
|
|
210
212
|
|
|
211
|
-
def _fill_defaults(data: AnyDict, prop: str, fallback: Any = None) -> None:
|
|
213
|
+
def _fill_defaults(data: AnyDict, prop: str, fallback: t.Any = None) -> None:
|
|
212
214
|
default = DEFAULTS.get(prop, fallback)
|
|
213
215
|
if callable(default):
|
|
214
216
|
default = default(data)
|
|
@@ -223,7 +225,7 @@ def fill_defaults(data: AnyDict, prop: str) -> None:
|
|
|
223
225
|
_fill_defaults(data, prop)
|
|
224
226
|
|
|
225
227
|
|
|
226
|
-
TRANSFORMS: dict[str,
|
|
228
|
+
TRANSFORMS: dict[str, t.Callable[[AnyDict], t.Any]] = {
|
|
227
229
|
"database": lambda data: (
|
|
228
230
|
data["database"]
|
|
229
231
|
if (":" in data["database"] or not data.get("dialect"))
|
|
@@ -264,7 +266,7 @@ def expand_posix_vars(posix_expr: str, context: dict[str, str]) -> str:
|
|
|
264
266
|
# Regular expression to match "${VAR:default}" pattern
|
|
265
267
|
pattern = r"\$\{([^}]+)\}"
|
|
266
268
|
|
|
267
|
-
def replace_var(match: re.Match[Any]) -> str:
|
|
269
|
+
def replace_var(match: re.Match[t.Any]) -> str:
|
|
268
270
|
var_with_default = match.group(1)
|
|
269
271
|
var_name, default_value = var_with_default.split(":") if ":" in var_with_default else (var_with_default, "")
|
|
270
272
|
return env.get(var_name.lower(), default_value)
|
|
@@ -325,10 +327,10 @@ def expand_env_vars_into_toml_values(toml: AnyDict, env: AnyDict) -> None:
|
|
|
325
327
|
|
|
326
328
|
|
|
327
329
|
def load_config(
|
|
328
|
-
connection_name: Optional[str] = None,
|
|
330
|
+
connection_name: t.Optional[str] = None,
|
|
329
331
|
_use_pyproject: bool | str | None = True,
|
|
330
332
|
_use_env: bool | str | None = True,
|
|
331
|
-
**fallback: Any,
|
|
333
|
+
**fallback: t.Any,
|
|
332
334
|
) -> TypeDALConfig:
|
|
333
335
|
"""
|
|
334
336
|
Combines multiple sources of config into one config instance.
|
|
@@ -338,7 +340,7 @@ def load_config(
|
|
|
338
340
|
# combine and fill with fallback values
|
|
339
341
|
# load typedal config or fail
|
|
340
342
|
toml_path, toml = _load_toml(_use_pyproject)
|
|
341
|
-
|
|
343
|
+
_dotenv_path, dotenv = _load_dotenv(_use_env)
|
|
342
344
|
|
|
343
345
|
expand_env_vars_into_toml_values(toml, dotenv)
|
|
344
346
|
|
typedal/constants.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Constants values.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import datetime as dt
|
|
6
|
+
import typing as t
|
|
7
|
+
from decimal import Decimal
|
|
8
|
+
|
|
9
|
+
from .types import T_annotation
|
|
10
|
+
|
|
11
|
+
JOIN_OPTIONS = t.Literal["left", "inner", None]
|
|
12
|
+
DEFAULT_JOIN_OPTION: JOIN_OPTIONS = "left"
|
|
13
|
+
|
|
14
|
+
BASIC_MAPPINGS: dict[T_annotation, str] = {
|
|
15
|
+
str: "string",
|
|
16
|
+
int: "integer",
|
|
17
|
+
bool: "boolean",
|
|
18
|
+
bytes: "blob",
|
|
19
|
+
float: "double",
|
|
20
|
+
object: "json",
|
|
21
|
+
Decimal: "decimal(10,2)",
|
|
22
|
+
dt.date: "date",
|
|
23
|
+
dt.time: "time",
|
|
24
|
+
dt.datetime: "datetime",
|
|
25
|
+
}
|