TypeDAL 2.1.5__py3-none-any.whl → 2.2.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/__about__.py +1 -1
- typedal/cli.py +375 -0
- typedal/config.py +271 -0
- typedal/core.py +50 -15
- typedal/for_py4web.py +30 -9
- typedal/types.py +22 -4
- {typedal-2.1.5.dist-info → typedal-2.2.1.dist-info}/METADATA +15 -15
- typedal-2.2.1.dist-info/RECORD +15 -0
- typedal-2.2.1.dist-info/entry_points.txt +2 -0
- typedal-2.1.5.dist-info/RECORD +0 -12
- {typedal-2.1.5.dist-info → typedal-2.2.1.dist-info}/WHEEL +0 -0
typedal/__about__.py
CHANGED
typedal/cli.py
ADDED
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Typer CLI for TypeDAL.
|
|
3
|
+
"""
|
|
4
|
+
import sys
|
|
5
|
+
import typing
|
|
6
|
+
import warnings
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Optional
|
|
9
|
+
|
|
10
|
+
import tomli
|
|
11
|
+
from configuraptor import asdict
|
|
12
|
+
from configuraptor.alias import is_alias
|
|
13
|
+
from configuraptor.helpers import is_optional
|
|
14
|
+
|
|
15
|
+
try:
|
|
16
|
+
import edwh_migrate
|
|
17
|
+
import pydal2sql # noqa: F401
|
|
18
|
+
import questionary
|
|
19
|
+
import rich
|
|
20
|
+
import tomlkit
|
|
21
|
+
import typer
|
|
22
|
+
except ImportError as e: # pragma: no cover
|
|
23
|
+
# ImportWarning is hidden by default
|
|
24
|
+
warnings.warn(
|
|
25
|
+
"`migrations` extra not installed. Please run `pip install typedal[migrations]` to fix this.",
|
|
26
|
+
source=e,
|
|
27
|
+
category=RuntimeWarning,
|
|
28
|
+
)
|
|
29
|
+
exit(127) # command not found
|
|
30
|
+
|
|
31
|
+
from pydal2sql.typer_support import IS_DEBUG, with_exit_code
|
|
32
|
+
from pydal2sql.types import (
|
|
33
|
+
DBType_Option,
|
|
34
|
+
OptionalArgument,
|
|
35
|
+
OutputFormat_Option,
|
|
36
|
+
Tables_Option,
|
|
37
|
+
)
|
|
38
|
+
from pydal2sql_core import core_alter, core_create
|
|
39
|
+
from typing_extensions import Never
|
|
40
|
+
|
|
41
|
+
from .__about__ import __version__
|
|
42
|
+
from .config import TypeDALConfig, fill_defaults, load_config, transform
|
|
43
|
+
|
|
44
|
+
app = typer.Typer(
|
|
45
|
+
no_args_is_help=True,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
questionary_types: dict[typing.Hashable, Optional[dict[str, typing.Any]]] = {
|
|
49
|
+
str: {
|
|
50
|
+
"type": "text",
|
|
51
|
+
"validate": lambda text: True if len(text) > 0 else "Please enter a value",
|
|
52
|
+
},
|
|
53
|
+
Optional[str]: {
|
|
54
|
+
"type": "text",
|
|
55
|
+
# no validate because it's optional
|
|
56
|
+
},
|
|
57
|
+
bool: {
|
|
58
|
+
"type": "confirm",
|
|
59
|
+
},
|
|
60
|
+
int: {"type": "text", "validate": lambda text: True if text.isdigit() else "Please enter a number"},
|
|
61
|
+
# specific props:
|
|
62
|
+
"dialect": {
|
|
63
|
+
"type": "select",
|
|
64
|
+
"choices": ["sqlite", "postgres", "mysql"],
|
|
65
|
+
},
|
|
66
|
+
"folder": {
|
|
67
|
+
"type": "path",
|
|
68
|
+
"message": "Database directory:",
|
|
69
|
+
"only_directories": True,
|
|
70
|
+
"default": "",
|
|
71
|
+
},
|
|
72
|
+
"input": {
|
|
73
|
+
"type": "path",
|
|
74
|
+
"message": "Python file containing table definitions.",
|
|
75
|
+
"file_filter": lambda file: "." not in file or file.endswith(".py"),
|
|
76
|
+
},
|
|
77
|
+
"output": {
|
|
78
|
+
"type": "path",
|
|
79
|
+
"message": "Python file where migrations will be written to.",
|
|
80
|
+
"file_filter": lambda file: "." not in file or file.endswith(".py"),
|
|
81
|
+
},
|
|
82
|
+
# disabled props:
|
|
83
|
+
"pyproject": None,
|
|
84
|
+
"noop": None,
|
|
85
|
+
# bool: questionary.confirm,
|
|
86
|
+
# int: questionary.text,
|
|
87
|
+
# 'pyproject': None,
|
|
88
|
+
# 'input': questionary.text,
|
|
89
|
+
# 'output': questionary.text,
|
|
90
|
+
# 'tables': questionary.print,
|
|
91
|
+
# 'flag_location': questionary.path, # directory
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
T = typing.TypeVar("T")
|
|
95
|
+
|
|
96
|
+
notfound = object()
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _get_question(prop: str, annotation: typing.Type[T]) -> Optional[dict[str, typing.Any]]: # pragma: no cover
|
|
100
|
+
question = questionary_types.get(prop, notfound)
|
|
101
|
+
if question is notfound:
|
|
102
|
+
# None means skip the question, notfound means use the type default!
|
|
103
|
+
question = questionary_types.get(annotation) # type: ignore
|
|
104
|
+
|
|
105
|
+
if not question:
|
|
106
|
+
return None
|
|
107
|
+
# make a copy so the original is not overwritten:
|
|
108
|
+
return question.copy() # type: ignore
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def get_question(prop: str, annotation: typing.Type[T], default: T | None) -> Optional[T]: # pragma: no cover
|
|
112
|
+
"""
|
|
113
|
+
Generate a question based on a config property and prompt the user for it.
|
|
114
|
+
"""
|
|
115
|
+
if not (question := _get_question(prop, annotation)):
|
|
116
|
+
return default
|
|
117
|
+
|
|
118
|
+
question["name"] = prop
|
|
119
|
+
question["message"] = question.get("message", f"{prop}? ")
|
|
120
|
+
default = typing.cast(T, default or question.get("default") or "")
|
|
121
|
+
|
|
122
|
+
if annotation == int:
|
|
123
|
+
default = typing.cast(T, str(default))
|
|
124
|
+
|
|
125
|
+
response = questionary.unsafe_prompt([question], default=default)[prop]
|
|
126
|
+
return typing.cast(T, response)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
@app.command()
|
|
130
|
+
@with_exit_code(hide_tb=IS_DEBUG)
|
|
131
|
+
def setup(
|
|
132
|
+
config_file: typing.Annotated[Optional[str], typer.Option("--config", "-c")] = None,
|
|
133
|
+
minimal: bool = False,
|
|
134
|
+
) -> None: # pragma: no cover
|
|
135
|
+
"""
|
|
136
|
+
Setup a [tool.typedal] entry in the local pyproject.toml.
|
|
137
|
+
"""
|
|
138
|
+
# 1. check if [tool.typedal] in pyproject.toml and ask missing questions (excl .env vars)
|
|
139
|
+
# 2. else if [tool.migrate] and/or [tool.pydal2sql] exist in the config, ask the user with copied defaults
|
|
140
|
+
# 3. else: ask the user every question or minimal questions based on cli arg
|
|
141
|
+
|
|
142
|
+
config = load_config(config_file)
|
|
143
|
+
|
|
144
|
+
toml_path = Path(config.pyproject)
|
|
145
|
+
|
|
146
|
+
if not (config.pyproject and toml_path.exists()):
|
|
147
|
+
# no pyproject.toml found!
|
|
148
|
+
toml_path = toml_path if config.pyproject else Path("pyproject.toml")
|
|
149
|
+
rich.print(f"[blue]Config toml doesn't exist yet, creating {toml_path}[/blue]", file=sys.stderr)
|
|
150
|
+
toml_path.touch()
|
|
151
|
+
|
|
152
|
+
toml_contents = toml_path.read_text()
|
|
153
|
+
# tomli has native Python types, tomlkit doesn't but preserves comments
|
|
154
|
+
toml_obj: dict[str, typing.Any] = tomli.loads(toml_contents)
|
|
155
|
+
|
|
156
|
+
if "[tool.typedal]" in toml_contents:
|
|
157
|
+
section = toml_obj["tool"]["typedal"]
|
|
158
|
+
config.update(**section, _overwrite=True)
|
|
159
|
+
|
|
160
|
+
if "[tool.pydal2sql]" in toml_contents:
|
|
161
|
+
mapping = {"": ""} # <- placeholder
|
|
162
|
+
|
|
163
|
+
extra_config = toml_obj["tool"]["pydal2sql"]
|
|
164
|
+
extra_config = {mapping.get(k, k): v for k, v in extra_config.items()}
|
|
165
|
+
extra_config.pop("format", None) # always edwh-migrate
|
|
166
|
+
config.update(**extra_config)
|
|
167
|
+
|
|
168
|
+
if "[tool.migrate]" in toml_contents:
|
|
169
|
+
mapping = {"migrate_uri": "database"}
|
|
170
|
+
|
|
171
|
+
extra_config = toml_obj["tool"]["migrate"]
|
|
172
|
+
extra_config = {mapping.get(k, k): v for k, v in extra_config.items()}
|
|
173
|
+
|
|
174
|
+
config.update(**extra_config)
|
|
175
|
+
|
|
176
|
+
data = asdict(config, with_top_level_key=False)
|
|
177
|
+
data["migrate"] = None # determined based on existence of input/output file.
|
|
178
|
+
for prop, annotation in TypeDALConfig.__annotations__.items():
|
|
179
|
+
if is_alias(config.__class__, prop):
|
|
180
|
+
# don't store aliases!
|
|
181
|
+
data.pop(prop, None)
|
|
182
|
+
continue
|
|
183
|
+
|
|
184
|
+
if minimal and getattr(config, prop, None) not in (None, "") or is_optional(annotation):
|
|
185
|
+
# property already present or not required, SKIP!
|
|
186
|
+
data[prop] = getattr(config, prop, None)
|
|
187
|
+
continue
|
|
188
|
+
|
|
189
|
+
fill_defaults(data, prop)
|
|
190
|
+
# default_value = getattr(config, prop, None)
|
|
191
|
+
default_value = data.get(prop, None)
|
|
192
|
+
answer: typing.Any = get_question(prop, annotation, default_value)
|
|
193
|
+
|
|
194
|
+
if annotation == bool:
|
|
195
|
+
answer = bool(answer)
|
|
196
|
+
elif annotation == int:
|
|
197
|
+
answer = int(answer)
|
|
198
|
+
|
|
199
|
+
config.update(**{prop: answer})
|
|
200
|
+
data[prop] = answer
|
|
201
|
+
|
|
202
|
+
for prop in TypeDALConfig.__annotations__:
|
|
203
|
+
transform(data, prop)
|
|
204
|
+
|
|
205
|
+
with toml_path.open("r") as f:
|
|
206
|
+
old_contents: dict[str, typing.Any] = tomlkit.load(f)
|
|
207
|
+
|
|
208
|
+
if "tool" not in old_contents:
|
|
209
|
+
old_contents["tool"] = {}
|
|
210
|
+
|
|
211
|
+
data.pop("pyproject", None)
|
|
212
|
+
|
|
213
|
+
# ignore any None:
|
|
214
|
+
old_contents["tool"]["typedal"] = {k: v for k, v in data.items() if v is not None}
|
|
215
|
+
|
|
216
|
+
with toml_path.open("w") as f:
|
|
217
|
+
tomlkit.dump(old_contents, f)
|
|
218
|
+
|
|
219
|
+
rich.print(f"[green]Wrote updated config to {toml_path}![/green]")
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
@app.command()
|
|
223
|
+
@with_exit_code(hide_tb=IS_DEBUG)
|
|
224
|
+
def generate_migrations(
|
|
225
|
+
filename_before: OptionalArgument[str] = None,
|
|
226
|
+
filename_after: OptionalArgument[str] = None,
|
|
227
|
+
dialect: DBType_Option = None,
|
|
228
|
+
tables: Tables_Option = None,
|
|
229
|
+
magic: Optional[bool] = None,
|
|
230
|
+
noop: Optional[bool] = None,
|
|
231
|
+
function: Optional[str] = None,
|
|
232
|
+
output_format: OutputFormat_Option = None,
|
|
233
|
+
output_file: Optional[str] = None,
|
|
234
|
+
dry_run: bool = False,
|
|
235
|
+
) -> bool:
|
|
236
|
+
"""
|
|
237
|
+
Run pydal2sql based on the typedal config.
|
|
238
|
+
"""
|
|
239
|
+
# 1. choose CREATE or ALTER based on whether 'output' exists?
|
|
240
|
+
# 2. pass right args based on 'config' to function chosen in 1.
|
|
241
|
+
generic_config = load_config()
|
|
242
|
+
pydal2sql_config = generic_config.to_pydal2sql()
|
|
243
|
+
pydal2sql_config.update(
|
|
244
|
+
magic=magic,
|
|
245
|
+
noop=noop,
|
|
246
|
+
tables=tables,
|
|
247
|
+
db_type=dialect.value if dialect else None,
|
|
248
|
+
function=function,
|
|
249
|
+
format=output_format,
|
|
250
|
+
input=filename_before,
|
|
251
|
+
output=output_file,
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
if pydal2sql_config.output and Path(pydal2sql_config.output).exists():
|
|
255
|
+
if dry_run:
|
|
256
|
+
print("Would run `pyda2sql alter` with config", asdict(pydal2sql_config), file=sys.stderr)
|
|
257
|
+
sys.stderr.flush()
|
|
258
|
+
|
|
259
|
+
return True
|
|
260
|
+
else: # pragma: no cover
|
|
261
|
+
return core_alter(
|
|
262
|
+
pydal2sql_config.input,
|
|
263
|
+
filename_after or pydal2sql_config.input,
|
|
264
|
+
db_type=pydal2sql_config.db_type,
|
|
265
|
+
tables=pydal2sql_config.tables,
|
|
266
|
+
noop=pydal2sql_config.noop,
|
|
267
|
+
magic=pydal2sql_config.magic,
|
|
268
|
+
function=pydal2sql_config.function,
|
|
269
|
+
output_format=pydal2sql_config.format,
|
|
270
|
+
output_file=pydal2sql_config.output,
|
|
271
|
+
)
|
|
272
|
+
else:
|
|
273
|
+
if dry_run:
|
|
274
|
+
print("Would run `pyda2sql create` with config", asdict(pydal2sql_config), file=sys.stderr)
|
|
275
|
+
sys.stderr.flush()
|
|
276
|
+
|
|
277
|
+
return True
|
|
278
|
+
else: # pragma: no cover
|
|
279
|
+
return core_create(
|
|
280
|
+
filename=pydal2sql_config.input,
|
|
281
|
+
db_type=pydal2sql_config.db_type,
|
|
282
|
+
tables=pydal2sql_config.tables,
|
|
283
|
+
noop=pydal2sql_config.noop,
|
|
284
|
+
magic=pydal2sql_config.magic,
|
|
285
|
+
function=pydal2sql_config.function,
|
|
286
|
+
output_format=pydal2sql_config.format,
|
|
287
|
+
output_file=pydal2sql_config.output,
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
@app.command()
|
|
292
|
+
@with_exit_code(hide_tb=IS_DEBUG)
|
|
293
|
+
def run_migrations(
|
|
294
|
+
migrations_file: OptionalArgument[str] = None,
|
|
295
|
+
db_uri: Optional[str] = None,
|
|
296
|
+
db_folder: Optional[str] = None,
|
|
297
|
+
schema_version: Optional[str] = None,
|
|
298
|
+
redis_host: Optional[str] = None,
|
|
299
|
+
migrate_cat_command: Optional[str] = None,
|
|
300
|
+
database_to_restore: Optional[str] = None,
|
|
301
|
+
migrate_table: Optional[str] = None,
|
|
302
|
+
flag_location: Optional[str] = None,
|
|
303
|
+
schema: Optional[str] = None,
|
|
304
|
+
create_flag_location: Optional[bool] = None,
|
|
305
|
+
dry_run: bool = False,
|
|
306
|
+
) -> bool:
|
|
307
|
+
"""
|
|
308
|
+
Run edwh-migrate based on the typedal config.
|
|
309
|
+
"""
|
|
310
|
+
# 1. build migrate Config from TypeDAL config
|
|
311
|
+
# 2. import right file
|
|
312
|
+
# 3. `activate_migrations`
|
|
313
|
+
generic_config = load_config()
|
|
314
|
+
migrate_config = generic_config.to_migrate()
|
|
315
|
+
|
|
316
|
+
migrate_config.update(
|
|
317
|
+
migrate_uri=db_uri,
|
|
318
|
+
schema_version=schema_version,
|
|
319
|
+
redis_host=redis_host,
|
|
320
|
+
migrate_cat_command=migrate_cat_command,
|
|
321
|
+
database_to_restore=database_to_restore,
|
|
322
|
+
migrate_table=migrate_table,
|
|
323
|
+
flag_location=flag_location,
|
|
324
|
+
schema=schema,
|
|
325
|
+
create_flag_location=create_flag_location,
|
|
326
|
+
db_folder=db_folder,
|
|
327
|
+
migrations_file=migrations_file,
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
if dry_run:
|
|
331
|
+
print("Would run `migrate` with config", asdict(migrate_config), file=sys.stderr)
|
|
332
|
+
else: # pragma: no cover
|
|
333
|
+
edwh_migrate.console_hook([], config=migrate_config)
|
|
334
|
+
return True
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def version_callback() -> Never:
|
|
338
|
+
"""
|
|
339
|
+
--version requested!
|
|
340
|
+
"""
|
|
341
|
+
print(f"pydal2sql Version: {__version__}")
|
|
342
|
+
|
|
343
|
+
raise typer.Exit(0)
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def config_callback() -> Never:
|
|
347
|
+
"""
|
|
348
|
+
--show-config requested.
|
|
349
|
+
"""
|
|
350
|
+
config = load_config()
|
|
351
|
+
|
|
352
|
+
print(repr(config))
|
|
353
|
+
|
|
354
|
+
raise typer.Exit(0)
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
@app.callback(invoke_without_command=True)
|
|
358
|
+
def main(
|
|
359
|
+
_: typer.Context,
|
|
360
|
+
# stops the program:
|
|
361
|
+
show_config: bool = False,
|
|
362
|
+
version: bool = False,
|
|
363
|
+
) -> None:
|
|
364
|
+
"""
|
|
365
|
+
This script can be used to generate the create or alter sql from pydal or typedal.
|
|
366
|
+
"""
|
|
367
|
+
if show_config:
|
|
368
|
+
config_callback()
|
|
369
|
+
elif version:
|
|
370
|
+
version_callback()
|
|
371
|
+
# else: just continue
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
if __name__ == "__main__": # pragma: no cover
|
|
375
|
+
app()
|
typedal/config.py
ADDED
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
"""
|
|
2
|
+
TypeDAL can be configured by a combination of pyproject.toml (static), env (dynamic) and code (programmic).
|
|
3
|
+
"""
|
|
4
|
+
import os
|
|
5
|
+
import typing
|
|
6
|
+
import warnings
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any, Optional
|
|
9
|
+
|
|
10
|
+
import black.files
|
|
11
|
+
import tomli
|
|
12
|
+
from configuraptor import TypedConfig, alias
|
|
13
|
+
from dotenv import dotenv_values, find_dotenv
|
|
14
|
+
|
|
15
|
+
if typing.TYPE_CHECKING: # pragma: no cover
|
|
16
|
+
from edwh_migrate import Config as MigrateConfig
|
|
17
|
+
from pydal2sql.typer_support import Config as P2SConfig
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class TypeDALConfig(TypedConfig):
|
|
21
|
+
"""
|
|
22
|
+
Unified config for TypeDAL runtime behavior and migration utilities.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
# typedal:
|
|
26
|
+
database: str
|
|
27
|
+
dialect: str
|
|
28
|
+
folder: str = "databases"
|
|
29
|
+
caching: bool = True
|
|
30
|
+
pool_size: int = 0
|
|
31
|
+
pyproject: str
|
|
32
|
+
connection: str = "default"
|
|
33
|
+
|
|
34
|
+
# pydal2sql:
|
|
35
|
+
input: str = "" # noqa: A003
|
|
36
|
+
output: str = ""
|
|
37
|
+
noop: bool = False
|
|
38
|
+
magic: bool = True
|
|
39
|
+
tables: Optional[list[str]] = None
|
|
40
|
+
function: str = "define_tables"
|
|
41
|
+
|
|
42
|
+
# edwh-migrate:
|
|
43
|
+
# migrate uri = database
|
|
44
|
+
database_to_restore: Optional[str]
|
|
45
|
+
migrate_cat_command: Optional[str]
|
|
46
|
+
schema_version: Optional[str]
|
|
47
|
+
redis_host: Optional[str]
|
|
48
|
+
migrate_table: str = "typedal_implemented_features"
|
|
49
|
+
flag_location: str
|
|
50
|
+
create_flag_location: bool = True
|
|
51
|
+
schema: str = "public"
|
|
52
|
+
|
|
53
|
+
# typedal (depends on properties above)
|
|
54
|
+
migrate: bool = True
|
|
55
|
+
fake_migrate: bool = False
|
|
56
|
+
|
|
57
|
+
# aliases:
|
|
58
|
+
db_uri: str = alias("database")
|
|
59
|
+
db_type: str = alias("dialect")
|
|
60
|
+
db_folder: str = alias("folder")
|
|
61
|
+
|
|
62
|
+
def __repr__(self) -> str:
|
|
63
|
+
"""
|
|
64
|
+
Dump the config to a (fancy) string.
|
|
65
|
+
"""
|
|
66
|
+
return f"<TypeDAL {self.__dict__}>"
|
|
67
|
+
|
|
68
|
+
def to_pydal2sql(self) -> "P2SConfig":
|
|
69
|
+
"""
|
|
70
|
+
Convert the config to the format required by pydal2sql.
|
|
71
|
+
"""
|
|
72
|
+
from pydal2sql.typer_support import Config, get_pydal2sql_config
|
|
73
|
+
|
|
74
|
+
if self.pyproject: # pragma: no cover
|
|
75
|
+
project = Path(self.pyproject).read_text()
|
|
76
|
+
|
|
77
|
+
if "[tool.typedal]" not in project and "[tool.pydal2sql]" in project:
|
|
78
|
+
# no typedal config, but existing p2s config:
|
|
79
|
+
return get_pydal2sql_config(self.pyproject)
|
|
80
|
+
|
|
81
|
+
return Config.load(
|
|
82
|
+
{
|
|
83
|
+
"db_type": self.dialect,
|
|
84
|
+
"format": "edwh-migrate",
|
|
85
|
+
"tables": self.tables,
|
|
86
|
+
"magic": self.magic,
|
|
87
|
+
"function": self.function,
|
|
88
|
+
"input": self.input,
|
|
89
|
+
"output": self.output,
|
|
90
|
+
"pyproject": self.pyproject,
|
|
91
|
+
}
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
def to_migrate(self) -> "MigrateConfig":
|
|
95
|
+
"""
|
|
96
|
+
Convert the config to the format required by edwh-migrate.
|
|
97
|
+
"""
|
|
98
|
+
from edwh_migrate import Config, get_config
|
|
99
|
+
|
|
100
|
+
if self.pyproject: # pragma: no cover
|
|
101
|
+
project = Path(self.pyproject).read_text()
|
|
102
|
+
|
|
103
|
+
if "[tool.typedal]" not in project and "[tool.migrate]" in project:
|
|
104
|
+
# no typedal config, but existing p2s config:
|
|
105
|
+
return get_config()
|
|
106
|
+
|
|
107
|
+
return Config.load(
|
|
108
|
+
{
|
|
109
|
+
"migrate_uri": self.database,
|
|
110
|
+
"schema_version": self.schema_version,
|
|
111
|
+
"redis_host": self.redis_host,
|
|
112
|
+
"migrate_cat_command": self.migrate_cat_command,
|
|
113
|
+
"database_to_restore": self.database_to_restore,
|
|
114
|
+
"migrate_table": self.migrate_table,
|
|
115
|
+
"flag_location": self.flag_location,
|
|
116
|
+
"create_flag_location": self.create_flag_location,
|
|
117
|
+
"schema": self.schema,
|
|
118
|
+
"db_folder": self.folder,
|
|
119
|
+
"migrations_file": self.output,
|
|
120
|
+
}
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def find_pyproject_toml(directory: str | None = None) -> typing.Optional[str]:
|
|
125
|
+
"""
|
|
126
|
+
Find the project's config toml, looks up until it finds the project root (black's logic).
|
|
127
|
+
"""
|
|
128
|
+
return black.files.find_pyproject_toml((directory or os.getcwd(),))
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _load_toml(path: str | bool | None = True) -> tuple[str, dict[str, Any]]:
|
|
132
|
+
"""
|
|
133
|
+
Path can be a file, a directory, a bool or None.
|
|
134
|
+
|
|
135
|
+
If it is True or None, the default logic is used.
|
|
136
|
+
If it is False, no data is loaded.
|
|
137
|
+
if it is a directory, the pyproject.toml will be searched there.
|
|
138
|
+
If it is a path, that file will be used.
|
|
139
|
+
"""
|
|
140
|
+
if path is False:
|
|
141
|
+
toml_path = None
|
|
142
|
+
elif path in (True, None):
|
|
143
|
+
toml_path = find_pyproject_toml()
|
|
144
|
+
elif Path(str(path)).is_file():
|
|
145
|
+
toml_path = str(path)
|
|
146
|
+
else:
|
|
147
|
+
toml_path = find_pyproject_toml(str(path))
|
|
148
|
+
|
|
149
|
+
if not toml_path:
|
|
150
|
+
# nothing to load
|
|
151
|
+
return "", {}
|
|
152
|
+
|
|
153
|
+
try:
|
|
154
|
+
with open(toml_path, "rb") as f:
|
|
155
|
+
data = tomli.load(f)
|
|
156
|
+
|
|
157
|
+
return toml_path or "", typing.cast(dict[str, Any], data["tool"]["typedal"])
|
|
158
|
+
except Exception as e:
|
|
159
|
+
warnings.warn(f"Could not load typedal config toml: {e}", source=e)
|
|
160
|
+
return toml_path or "", {}
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _load_dotenv(path: str | bool | None = True) -> tuple[str, dict[str, Any]]:
|
|
164
|
+
if path is False:
|
|
165
|
+
dotenv_path = None
|
|
166
|
+
elif path in (True, None):
|
|
167
|
+
dotenv_path = find_dotenv(usecwd=True)
|
|
168
|
+
elif Path(str(path)).is_file():
|
|
169
|
+
dotenv_path = str(path)
|
|
170
|
+
else:
|
|
171
|
+
dotenv_path = str(Path(str(path)) / ".env")
|
|
172
|
+
|
|
173
|
+
if not dotenv_path:
|
|
174
|
+
return "", {}
|
|
175
|
+
|
|
176
|
+
# 1. find everything with TYPEDAL_ prefix
|
|
177
|
+
# 2. remove that prefix
|
|
178
|
+
# 3. format values if possible
|
|
179
|
+
data = dotenv_values(dotenv_path)
|
|
180
|
+
data |= os.environ # higher prio than .env
|
|
181
|
+
|
|
182
|
+
typedal_data = {k.lower().removeprefix("typedal_"): v for k, v in data.items()}
|
|
183
|
+
|
|
184
|
+
return dotenv_path, typedal_data
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
DB_ALIASES = {
|
|
188
|
+
"postgresql": "postgres",
|
|
189
|
+
"psql": "postgres",
|
|
190
|
+
"sqlite3": "sqlite",
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def get_db_for_alias(db_name: str) -> str:
|
|
195
|
+
"""
|
|
196
|
+
Convert a db dialect alias to the standard name.
|
|
197
|
+
"""
|
|
198
|
+
return DB_ALIASES.get(db_name, db_name)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
DEFAULTS: dict[str, Any | typing.Callable[[dict[str, Any]], Any]] = {
|
|
202
|
+
"database": lambda data: data.get("db_uri") or "sqlite:memory",
|
|
203
|
+
"dialect": lambda data: get_db_for_alias(data["database"].split(":")[0])
|
|
204
|
+
if ":" in data["database"]
|
|
205
|
+
else data.get("db_type"),
|
|
206
|
+
"migrate": lambda data: not (data.get("input") or data.get("output")),
|
|
207
|
+
"folder": lambda data: data.get("db_folder"),
|
|
208
|
+
"flag_location": lambda data: f"{db_folder}/flags"
|
|
209
|
+
if (db_folder := (data.get("folder") or data.get("db_folder")))
|
|
210
|
+
else "/flags",
|
|
211
|
+
"pool_size": lambda data: 1 if data.get("dialect", "sqlite") == "sqlite" else 3,
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def fill_defaults(data: dict[str, Any], prop: str) -> None:
|
|
216
|
+
"""
|
|
217
|
+
Fill missing property defaults with (calculated) sane defaults.
|
|
218
|
+
"""
|
|
219
|
+
if data.get(prop, None) is None:
|
|
220
|
+
default = DEFAULTS.get(prop)
|
|
221
|
+
if callable(default):
|
|
222
|
+
default = default(data)
|
|
223
|
+
data[prop] = default
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
TRANSFORMS: dict[str, typing.Callable[[dict[str, Any]], Any]] = {
|
|
227
|
+
"database": lambda data: data["database"]
|
|
228
|
+
if (":" in data["database"] or not data.get("dialect"))
|
|
229
|
+
else (data["dialect"] + "://" + data["database"])
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def transform(data: dict[str, Any], prop: str) -> bool:
|
|
234
|
+
"""
|
|
235
|
+
After the user has chosen a value, possibly transform it.
|
|
236
|
+
"""
|
|
237
|
+
if fn := TRANSFORMS.get(prop):
|
|
238
|
+
data[prop] = fn(data)
|
|
239
|
+
return True
|
|
240
|
+
return False
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def load_config(
|
|
244
|
+
_use_pyproject: bool | str | None = True, _use_env: bool | str | None = True, **fallback: Any
|
|
245
|
+
) -> TypeDALConfig:
|
|
246
|
+
"""
|
|
247
|
+
Combines multiple sources of config into one config instance.
|
|
248
|
+
"""
|
|
249
|
+
# load toml data
|
|
250
|
+
# load .env data
|
|
251
|
+
# combine and fill with fallback values
|
|
252
|
+
# load typedal config or fail
|
|
253
|
+
toml_path, toml = _load_toml(_use_pyproject)
|
|
254
|
+
dotenv_path, dotenv = _load_dotenv(_use_env)
|
|
255
|
+
|
|
256
|
+
connection_name = dotenv.get("connection", "") or toml.get("default", "")
|
|
257
|
+
connection: dict[str, Any] = (toml.get(connection_name) if connection_name else toml) or {}
|
|
258
|
+
|
|
259
|
+
combined = connection | dotenv | fallback
|
|
260
|
+
combined = {k.replace("-", "_"): v for k, v in combined.items()}
|
|
261
|
+
|
|
262
|
+
combined["pyproject"] = toml_path
|
|
263
|
+
combined["connection"] = connection_name
|
|
264
|
+
|
|
265
|
+
for prop in TypeDALConfig.__annotations__:
|
|
266
|
+
fill_defaults(combined, prop)
|
|
267
|
+
|
|
268
|
+
for prop in TypeDALConfig.__annotations__:
|
|
269
|
+
transform(combined, prop)
|
|
270
|
+
|
|
271
|
+
return TypeDALConfig.load(combined, convert_types=True)
|
typedal/core.py
CHANGED
|
@@ -19,10 +19,11 @@ import pydal
|
|
|
19
19
|
from pydal._globals import DEFAULT
|
|
20
20
|
from pydal.objects import Field as _Field
|
|
21
21
|
from pydal.objects import Query as _Query
|
|
22
|
-
from pydal.objects import Row
|
|
22
|
+
from pydal.objects import Row
|
|
23
23
|
from pydal.objects import Table as _Table
|
|
24
24
|
from typing_extensions import Self
|
|
25
25
|
|
|
26
|
+
from .config import load_config
|
|
26
27
|
from .helpers import (
|
|
27
28
|
DummyQuery,
|
|
28
29
|
all_annotations,
|
|
@@ -52,6 +53,8 @@ from .types import (
|
|
|
52
53
|
PaginateDict,
|
|
53
54
|
Pagination,
|
|
54
55
|
Query,
|
|
56
|
+
Rows,
|
|
57
|
+
Validator,
|
|
55
58
|
_Types,
|
|
56
59
|
)
|
|
57
60
|
|
|
@@ -336,13 +339,13 @@ class TypeDAL(pydal.DAL): # type: ignore
|
|
|
336
339
|
|
|
337
340
|
def __init__(
|
|
338
341
|
self,
|
|
339
|
-
uri: str =
|
|
340
|
-
pool_size: int =
|
|
341
|
-
folder: Optional[str | Path] = None,
|
|
342
|
+
uri: Optional[str] = None, # default from config or 'sqlite:memory'
|
|
343
|
+
pool_size: int = None, # default 1 if sqlite else 3
|
|
344
|
+
folder: Optional[str | Path] = None, # default 'databases' in config
|
|
342
345
|
db_codec: str = "UTF-8",
|
|
343
346
|
check_reserved: Optional[list[str]] = None,
|
|
344
|
-
migrate: bool = True
|
|
345
|
-
fake_migrate: bool = False
|
|
347
|
+
migrate: Optional[bool] = None, # default True by config
|
|
348
|
+
fake_migrate: Optional[bool] = None, # default False by config
|
|
346
349
|
migrate_enabled: bool = True,
|
|
347
350
|
fake_migrate_all: bool = False,
|
|
348
351
|
decode_credentials: bool = False,
|
|
@@ -359,24 +362,37 @@ class TypeDAL(pydal.DAL): # type: ignore
|
|
|
359
362
|
ignore_field_case: bool = True,
|
|
360
363
|
entity_quoting: bool = True,
|
|
361
364
|
table_hash: Optional[str] = None,
|
|
362
|
-
enable_typedal_caching: bool =
|
|
365
|
+
enable_typedal_caching: bool = None,
|
|
366
|
+
use_pyproject: bool | str = True,
|
|
367
|
+
use_env: bool | str = True,
|
|
363
368
|
) -> None:
|
|
364
369
|
"""
|
|
365
370
|
Adds some internal tables after calling pydal's default init.
|
|
366
371
|
|
|
367
372
|
Set enable_typedal_caching to False to disable this behavior.
|
|
368
373
|
"""
|
|
369
|
-
|
|
370
|
-
|
|
374
|
+
config = load_config(_use_pyproject=use_pyproject, _use_env=use_env)
|
|
375
|
+
config.update(
|
|
376
|
+
database=uri,
|
|
377
|
+
dialect=uri.split(":")[0] if uri and ":" in uri else None,
|
|
378
|
+
folder=folder,
|
|
379
|
+
migrate=migrate,
|
|
380
|
+
fake_migrate=fake_migrate,
|
|
381
|
+
caching=enable_typedal_caching,
|
|
382
|
+
pool_size=pool_size,
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
if config.folder:
|
|
386
|
+
Path(config.folder).mkdir(exist_ok=True)
|
|
371
387
|
|
|
372
388
|
super().__init__(
|
|
373
|
-
|
|
374
|
-
pool_size,
|
|
375
|
-
|
|
389
|
+
config.database,
|
|
390
|
+
config.pool_size,
|
|
391
|
+
config.folder,
|
|
376
392
|
db_codec,
|
|
377
393
|
check_reserved,
|
|
378
|
-
migrate,
|
|
379
|
-
fake_migrate,
|
|
394
|
+
config.migrate,
|
|
395
|
+
config.fake_migrate,
|
|
380
396
|
migrate_enabled,
|
|
381
397
|
fake_migrate_all,
|
|
382
398
|
decode_credentials,
|
|
@@ -395,7 +411,7 @@ class TypeDAL(pydal.DAL): # type: ignore
|
|
|
395
411
|
table_hash,
|
|
396
412
|
)
|
|
397
413
|
|
|
398
|
-
if
|
|
414
|
+
if config.caching:
|
|
399
415
|
self.try_define(_TypedalCache)
|
|
400
416
|
self.try_define(_TypedalCacheDependency)
|
|
401
417
|
|
|
@@ -508,6 +524,7 @@ class TypeDAL(pydal.DAL): # type: ignore
|
|
|
508
524
|
relationships=typing.cast(dict[str, Relationship[Any]], relationships),
|
|
509
525
|
)
|
|
510
526
|
self._class_map[str(table)] = cls
|
|
527
|
+
cls.__on_define__(self)
|
|
511
528
|
else:
|
|
512
529
|
warnings.warn("db.define used without inheriting TypedTable. This could lead to strange problems!")
|
|
513
530
|
|
|
@@ -1042,6 +1059,13 @@ class TableMeta(type):
|
|
|
1042
1059
|
|
|
1043
1060
|
# other table methods:
|
|
1044
1061
|
|
|
1062
|
+
def truncate(self, mode: str = "") -> None:
|
|
1063
|
+
"""
|
|
1064
|
+
Remove all data and reset index.
|
|
1065
|
+
"""
|
|
1066
|
+
table = self._ensure_table_defined()
|
|
1067
|
+
table.truncate(mode)
|
|
1068
|
+
|
|
1045
1069
|
def drop(self, mode: str = "") -> None:
|
|
1046
1070
|
"""
|
|
1047
1071
|
Remove the underlying table.
|
|
@@ -1143,6 +1167,8 @@ class TypedField(typing.Generic[T_Value]): # pragma: no cover
|
|
|
1143
1167
|
_type: T_annotation
|
|
1144
1168
|
kwargs: Any
|
|
1145
1169
|
|
|
1170
|
+
requires: Validator | typing.Iterable[Validator]
|
|
1171
|
+
|
|
1146
1172
|
def __init__(self, _type: typing.Type[T_Value] | types.UnionType = str, /, **settings: Any) -> None: # type: ignore
|
|
1147
1173
|
"""
|
|
1148
1174
|
A TypedFieldType should not be inited manually, but TypedField (from `fields.py`) should be used!
|
|
@@ -1359,6 +1385,15 @@ class TypedTable(metaclass=TableMeta):
|
|
|
1359
1385
|
inst._setup_instance_methods()
|
|
1360
1386
|
return inst
|
|
1361
1387
|
|
|
1388
|
+
@classmethod
|
|
1389
|
+
def __on_define__(cls, db: TypeDAL) -> None:
|
|
1390
|
+
"""
|
|
1391
|
+
Method that can be implemented by tables to do an action after db.define is completed.
|
|
1392
|
+
|
|
1393
|
+
This can be useful if you need to add something like requires=IS_NOT_IN_DB(db, "table.field"),
|
|
1394
|
+
where you need a reference to the current database, which may not exist yet when defining the model.
|
|
1395
|
+
"""
|
|
1396
|
+
|
|
1362
1397
|
def __iter__(self) -> typing.Generator[Any, None, None]:
|
|
1363
1398
|
"""
|
|
1364
1399
|
Allows looping through the columns.
|
typedal/for_py4web.py
CHANGED
|
@@ -1,14 +1,25 @@
|
|
|
1
1
|
"""
|
|
2
2
|
ONLY USE IN COMBINATION WITH PY4WEB!
|
|
3
3
|
"""
|
|
4
|
+
import typing
|
|
4
5
|
from datetime import datetime
|
|
5
6
|
from typing import Any, Optional
|
|
6
7
|
|
|
7
8
|
import json_fix # noqa: F401
|
|
8
9
|
import threadsafevariable
|
|
9
|
-
from py4web.core import ICECUBE
|
|
10
|
+
from py4web.core import ICECUBE
|
|
11
|
+
from py4web.core import Fixture as _Fixture
|
|
12
|
+
from pydal.validators import CRYPT, IS_EMAIL, IS_NOT_EMPTY, IS_NOT_IN_DB, IS_STRONG
|
|
10
13
|
|
|
11
|
-
from .core import TypeDAL, TypedTable
|
|
14
|
+
from .core import TypeDAL, TypedField, TypedTable
|
|
15
|
+
from .fields import PasswordField
|
|
16
|
+
from .types import Validator
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class Fixture(_Fixture): # type: ignore
|
|
20
|
+
"""
|
|
21
|
+
Make mypy happy.
|
|
22
|
+
"""
|
|
12
23
|
|
|
13
24
|
|
|
14
25
|
class DAL(TypeDAL, Fixture): # pragma: no cover
|
|
@@ -46,13 +57,23 @@ class AuthUser(TypedTable):
|
|
|
46
57
|
|
|
47
58
|
# call db.define on this when ready
|
|
48
59
|
|
|
49
|
-
email: str
|
|
50
|
-
password
|
|
51
|
-
first_name: Optional[str]
|
|
52
|
-
last_name: Optional[str]
|
|
53
|
-
sso_id: Optional[str]
|
|
54
|
-
action_token: Optional[str]
|
|
55
|
-
last_password_change: Optional[datetime]
|
|
60
|
+
email: TypedField[str]
|
|
61
|
+
password = PasswordField(requires=[IS_STRONG(entropy=45), CRYPT()])
|
|
62
|
+
first_name: TypedField[Optional[str]]
|
|
63
|
+
last_name: TypedField[Optional[str]]
|
|
64
|
+
sso_id: TypedField[Optional[str]]
|
|
65
|
+
action_token: TypedField[Optional[str]]
|
|
66
|
+
last_password_change: TypedField[Optional[datetime]]
|
|
67
|
+
|
|
56
68
|
# past_passwords_hash: Optional[str]
|
|
57
69
|
# username: Optional[str]
|
|
58
70
|
# phone_number: Optional[str]
|
|
71
|
+
|
|
72
|
+
@classmethod
|
|
73
|
+
def __on_define__(cls, db: TypeDAL) -> None:
|
|
74
|
+
"""
|
|
75
|
+
Add some requires= to the auth_user fields.
|
|
76
|
+
"""
|
|
77
|
+
cls.email.requires = typing.cast(tuple[Validator, ...], (IS_EMAIL(), IS_NOT_IN_DB(db, "auth_user.email")))
|
|
78
|
+
cls.first_name.requires = IS_NOT_EMPTY()
|
|
79
|
+
cls.last_name.requires = IS_NOT_EMPTY()
|
typedal/types.py
CHANGED
|
@@ -10,7 +10,9 @@ from pydal.helpers.classes import Reference as _Reference
|
|
|
10
10
|
from pydal.objects import Expression as _Expression
|
|
11
11
|
from pydal.objects import Field as _Field
|
|
12
12
|
from pydal.objects import Query as _Query
|
|
13
|
+
from pydal.objects import Rows as _Rows
|
|
13
14
|
from pydal.objects import Set as _Set
|
|
15
|
+
from pydal.validators import Validator as _Validator
|
|
14
16
|
from typing_extensions import NotRequired
|
|
15
17
|
|
|
16
18
|
|
|
@@ -30,7 +32,7 @@ class Expression(_Expression): # type: ignore
|
|
|
30
32
|
"""
|
|
31
33
|
|
|
32
34
|
|
|
33
|
-
class Set(_Set):
|
|
35
|
+
class Set(_Set): # type: ignore
|
|
34
36
|
"""
|
|
35
37
|
Pydal Set object.
|
|
36
38
|
|
|
@@ -38,7 +40,7 @@ class Set(_Set):
|
|
|
38
40
|
"""
|
|
39
41
|
|
|
40
42
|
|
|
41
|
-
class OpRow(_OpRow):
|
|
43
|
+
class OpRow(_OpRow): # type: ignore
|
|
42
44
|
"""
|
|
43
45
|
Pydal OpRow object.
|
|
44
46
|
|
|
@@ -46,7 +48,7 @@ class OpRow(_OpRow):
|
|
|
46
48
|
"""
|
|
47
49
|
|
|
48
50
|
|
|
49
|
-
class Reference(_Reference):
|
|
51
|
+
class Reference(_Reference): # type: ignore
|
|
50
52
|
"""
|
|
51
53
|
Pydal Reference object.
|
|
52
54
|
|
|
@@ -54,7 +56,7 @@ class Reference(_Reference):
|
|
|
54
56
|
"""
|
|
55
57
|
|
|
56
58
|
|
|
57
|
-
class Field(_Field):
|
|
59
|
+
class Field(_Field): # type: ignore
|
|
58
60
|
"""
|
|
59
61
|
Pydal Field object.
|
|
60
62
|
|
|
@@ -62,6 +64,22 @@ class Field(_Field):
|
|
|
62
64
|
"""
|
|
63
65
|
|
|
64
66
|
|
|
67
|
+
class Rows(_Rows): # type: ignore
|
|
68
|
+
"""
|
|
69
|
+
Pydal Rows object.
|
|
70
|
+
|
|
71
|
+
Make mypy happy.
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class Validator(_Validator): # type: ignore
|
|
76
|
+
"""
|
|
77
|
+
Pydal Validator object.
|
|
78
|
+
|
|
79
|
+
Make mypy happy.
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
|
|
65
83
|
class _Types:
|
|
66
84
|
"""
|
|
67
85
|
Internal type storage for stuff that mypy otherwise won't understand.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: TypeDAL
|
|
3
|
-
Version: 2.1
|
|
3
|
+
Version: 2.2.1
|
|
4
4
|
Summary: Typing support for PyDAL
|
|
5
5
|
Project-URL: Documentation, https://typedal.readthedocs.io/
|
|
6
6
|
Project-URL: Issues, https://github.com/trialandsuccess/TypeDAL/issues
|
|
@@ -15,6 +15,7 @@ Classifier: Programming Language :: Python :: 3.12
|
|
|
15
15
|
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
16
16
|
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
|
17
17
|
Requires-Python: >=3.10
|
|
18
|
+
Requires-Dist: configuraptor>=1.24.0
|
|
18
19
|
Requires-Dist: dill
|
|
19
20
|
Requires-Dist: pydal
|
|
20
21
|
Provides-Extra: all
|
|
@@ -22,6 +23,9 @@ Requires-Dist: edwh-migrate; extra == 'all'
|
|
|
22
23
|
Requires-Dist: json-fix; extra == 'all'
|
|
23
24
|
Requires-Dist: py4web; extra == 'all'
|
|
24
25
|
Requires-Dist: pydal2sql[all]; extra == 'all'
|
|
26
|
+
Requires-Dist: questionary; extra == 'all'
|
|
27
|
+
Requires-Dist: tomlkit; extra == 'all'
|
|
28
|
+
Requires-Dist: typer; extra == 'all'
|
|
25
29
|
Provides-Extra: dev
|
|
26
30
|
Requires-Dist: hatch; extra == 'dev'
|
|
27
31
|
Requires-Dist: mkdocs; extra == 'dev'
|
|
@@ -32,6 +36,9 @@ Requires-Dist: su6[all]; extra == 'dev'
|
|
|
32
36
|
Provides-Extra: migrations
|
|
33
37
|
Requires-Dist: edwh-migrate; extra == 'migrations'
|
|
34
38
|
Requires-Dist: pydal2sql; extra == 'migrations'
|
|
39
|
+
Requires-Dist: questionary; extra == 'migrations'
|
|
40
|
+
Requires-Dist: tomlkit; extra == 'migrations'
|
|
41
|
+
Requires-Dist: typer; extra == 'migrations'
|
|
35
42
|
Provides-Extra: py4web
|
|
36
43
|
Requires-Dist: json-fix; extra == 'py4web'
|
|
37
44
|
Requires-Dist: py4web; extra == 'py4web'
|
|
@@ -55,10 +62,13 @@ the underlying `db.define_table` pydal Tables.
|
|
|
55
62
|
- `TypeDAL` is the replacement class for DAL that manages the code on top of DAL.
|
|
56
63
|
- `TypedTable` must be the parent class of any custom Tables you define (e.g. `class SomeTable(TypedTable)`)
|
|
57
64
|
- `TypedField` can be used instead of Python native types when extra settings (such as `default`) are required (
|
|
58
|
-
e.g. `name = TypedField(str, default="John Doe")`). It can also be used in an annotation (`name: TypedField[str]`) to
|
|
65
|
+
e.g. `name = TypedField(str, default="John Doe")`). It can also be used in an annotation (`name: TypedField[str]`) to
|
|
66
|
+
improve
|
|
59
67
|
editor support over only annotating with `str`.
|
|
60
|
-
- `TypedRows`: can be used as the return type annotation of pydal's `.select()` and subscribed with the actual table
|
|
61
|
-
|
|
68
|
+
- `TypedRows`: can be used as the return type annotation of pydal's `.select()` and subscribed with the actual table
|
|
69
|
+
class, so
|
|
70
|
+
e.g. `rows: TypedRows[SomeTable] = db(...).select()`. When using the QueryBuilder, a `TypedRows` instance is returned
|
|
71
|
+
by `.collect()`.
|
|
62
72
|
|
|
63
73
|
Version 2.0 also introduces more ORM-like funcionality.
|
|
64
74
|
Most notably, a Typed Query Builder that sees your table classes as models with relationships to each other.
|
|
@@ -68,7 +78,7 @@ details.
|
|
|
68
78
|
## Quick Overview
|
|
69
79
|
|
|
70
80
|
Below you'll find a quick overview of translation from pydal to TypeDAL. For more info,
|
|
71
|
-
see [the docs](https://
|
|
81
|
+
see [the docs](https://typedal.readthedocs.io/en/latest/).
|
|
72
82
|
|
|
73
83
|
### Translations from pydal to typedal
|
|
74
84
|
|
|
@@ -247,16 +257,6 @@ row: TableName = db.table_name(id=1)
|
|
|
247
257
|
|
|
248
258
|
See [2. Defining Tables](docs/2_defining_tables.md)
|
|
249
259
|
|
|
250
|
-
## Roadmap
|
|
251
|
-
|
|
252
|
-
This section contains a non-exhaustive list of planned features for future feature releases:
|
|
253
|
-
|
|
254
|
-
- 2.2
|
|
255
|
-
- Migrations: currently, you can use pydal's automatic migrations or disable those and manage them yourself, but
|
|
256
|
-
adding something like [`edwh-migrate`](https://github.com/educationwarehouse/migrate#readme)
|
|
257
|
-
with [`pydal2sql`](https://github.com/robinvandernoord/pydal2sql-core) as an option could make this project more
|
|
258
|
-
production-friendly.
|
|
259
|
-
|
|
260
260
|
## Caveats
|
|
261
261
|
|
|
262
262
|
- This package depends heavily on the current implementation of annotations (which are computed when the class is
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
typedal/__about__.py,sha256=h_6wVhT5zBpkimIzSHhAe8DZCR_Nn-sqaS8GZXghPko,206
|
|
2
|
+
typedal/__init__.py,sha256=QQpLiVl9w9hm2LBxey49Y_tCF_VB2bScVaS_mCjYy54,366
|
|
3
|
+
typedal/caching.py,sha256=53WU5J_yRLygkKoDfD0rV_dkh5oqqT--64R9Mvz050Y,7457
|
|
4
|
+
typedal/cli.py,sha256=MF2bGwL4SOaIqGzr1JRSvGD6CSujy88hKGgVIehlFNk,11789
|
|
5
|
+
typedal/config.py,sha256=9ORQqJJ_3tDmFiL2Py_9PiceXLihpMR69t6OKZ_qpe8,8387
|
|
6
|
+
typedal/core.py,sha256=LV0Fszbmh0pojAiARD1GKj5ooxakJmmj_4ebpvZZkcs,93644
|
|
7
|
+
typedal/fields.py,sha256=z2PD9vLWqBR_zXtiY0DthqTG4AeF3yxKoeuVfGXnSdg,5197
|
|
8
|
+
typedal/for_py4web.py,sha256=kw44_55-dBWlYmepB7YEev5sj7tbYcgzvp-Ecc7K9_I,2230
|
|
9
|
+
typedal/helpers.py,sha256=ZpHdwBMSANw-P9I5gs56Vf6GUbxGzFsIwbBvASKXX8s,6487
|
|
10
|
+
typedal/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
11
|
+
typedal/types.py,sha256=5qm3PgS8DXGCu9ZTUWQiIi2XXD8gz4_4Csg_vZlu_yo,3379
|
|
12
|
+
typedal-2.2.1.dist-info/METADATA,sha256=-4kp0PN1msDhP9C07HqMh_4VCigOu0AhkN6yNrRPDkM,7623
|
|
13
|
+
typedal-2.2.1.dist-info/WHEEL,sha256=KGYbc1zXlYddvwxnNty23BeaKzh7YuoSIvIMO4jEhvw,87
|
|
14
|
+
typedal-2.2.1.dist-info/entry_points.txt,sha256=m1wqcc_10rHWPdlQ71zEkmJDADUAnZtn7Jac_6mbyUc,44
|
|
15
|
+
typedal-2.2.1.dist-info/RECORD,,
|
typedal-2.1.5.dist-info/RECORD
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
typedal/__about__.py,sha256=z5coRWunglB2VeEfC8bQlGvTzl_8V8VDJJvYOu95z3A,206
|
|
2
|
-
typedal/__init__.py,sha256=QQpLiVl9w9hm2LBxey49Y_tCF_VB2bScVaS_mCjYy54,366
|
|
3
|
-
typedal/caching.py,sha256=53WU5J_yRLygkKoDfD0rV_dkh5oqqT--64R9Mvz050Y,7457
|
|
4
|
-
typedal/core.py,sha256=OVLs-1xb3i-FsHEcVXeSgyCNhzcHu-WCAlX7YX95ulg,92238
|
|
5
|
-
typedal/fields.py,sha256=z2PD9vLWqBR_zXtiY0DthqTG4AeF3yxKoeuVfGXnSdg,5197
|
|
6
|
-
typedal/for_py4web.py,sha256=NN6GxZpp971HFxqiI5QZaNQYydsEvJh1TctxKuZE5pw,1460
|
|
7
|
-
typedal/helpers.py,sha256=ZpHdwBMSANw-P9I5gs56Vf6GUbxGzFsIwbBvASKXX8s,6487
|
|
8
|
-
typedal/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
9
|
-
typedal/types.py,sha256=AADKhLbBotpDm5FM__fhvaRNGoHPjG-LcxflnjvhOz0,3011
|
|
10
|
-
typedal-2.1.5.dist-info/METADATA,sha256=x9hqsQdodHt8tcU5v2W-6J78Kx2YJDI0azqicUNHE1w,7814
|
|
11
|
-
typedal-2.1.5.dist-info/WHEEL,sha256=KGYbc1zXlYddvwxnNty23BeaKzh7YuoSIvIMO4jEhvw,87
|
|
12
|
-
typedal-2.1.5.dist-info/RECORD,,
|
|
File without changes
|