TypeDAL 4.5.0__tar.gz → 4.6.0__tar.gz
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-4.6.0/.crush/.gitignore +1 -0
- typedal-4.6.0/.crush/crush.db-shm +0 -0
- typedal-4.6.0/.crush/crush.db-wal +0 -0
- typedal-4.6.0/.crush/logs/crush.log +19 -0
- {typedal-4.5.0 → typedal-4.6.0}/.github/workflows/su6.yml +1 -1
- {typedal-4.5.0 → typedal-4.6.0}/.readthedocs.yml +1 -1
- {typedal-4.5.0 → typedal-4.6.0}/CHANGELOG.md +6 -0
- {typedal-4.5.0 → typedal-4.6.0}/PKG-INFO +4 -3
- {typedal-4.5.0 → typedal-4.6.0}/docs/8_mixins.md +53 -0
- {typedal-4.5.0 → typedal-4.6.0}/pyproject.toml +4 -3
- {typedal-4.5.0 → typedal-4.6.0}/src/typedal/__about__.py +1 -1
- {typedal-4.5.0 → typedal-4.6.0}/src/typedal/caching.py +6 -8
- {typedal-4.5.0 → typedal-4.6.0}/src/typedal/cli.py +4 -6
- {typedal-4.5.0 → typedal-4.6.0}/src/typedal/core.py +14 -6
- {typedal-4.5.0 → typedal-4.6.0}/src/typedal/define.py +2 -3
- {typedal-4.5.0 → typedal-4.6.0}/src/typedal/fields.py +4 -4
- {typedal-4.5.0 → typedal-4.6.0}/src/typedal/helpers.py +11 -15
- typedal-4.6.0/src/typedal/mixins.py +599 -0
- {typedal-4.5.0 → typedal-4.6.0}/src/typedal/query_builder.py +4 -5
- {typedal-4.5.0 → typedal-4.6.0}/src/typedal/relationships.py +64 -13
- {typedal-4.5.0 → typedal-4.6.0}/src/typedal/rows.py +2 -3
- {typedal-4.5.0 → typedal-4.6.0}/src/typedal/tables.py +4 -5
- {typedal-4.5.0 → typedal-4.6.0}/src/typedal/types.py +19 -24
- typedal-4.6.0/tests/__init__.py +0 -0
- typedal-4.6.0/tests/test_mixins.py +468 -0
- {typedal-4.5.0 → typedal-4.6.0}/tests/test_orm.py +1 -7
- {typedal-4.5.0 → typedal-4.6.0}/tests/test_relationships.py +5 -1
- typedal-4.5.0/src/typedal/mixins.py +0 -250
- typedal-4.5.0/tests/test_mixins.py +0 -166
- /typedal-4.5.0/src/typedal/py.typed → /typedal-4.6.0/.crush/init +0 -0
- {typedal-4.5.0 → typedal-4.6.0}/.gitignore +0 -0
- {typedal-4.5.0 → typedal-4.6.0}/README.md +0 -0
- {typedal-4.5.0 → typedal-4.6.0}/coverage.svg +0 -0
- {typedal-4.5.0 → typedal-4.6.0}/docs/10_advanced_apis.md +0 -0
- {typedal-4.5.0 → typedal-4.6.0}/docs/1_getting_started.md +0 -0
- {typedal-4.5.0 → typedal-4.6.0}/docs/2_defining_tables.md +0 -0
- {typedal-4.5.0 → typedal-4.6.0}/docs/3_building_queries.md +0 -0
- {typedal-4.5.0 → typedal-4.6.0}/docs/4_relationships.md +0 -0
- {typedal-4.5.0 → typedal-4.6.0}/docs/5_py4web.md +0 -0
- {typedal-4.5.0 → typedal-4.6.0}/docs/6_migrations.md +0 -0
- {typedal-4.5.0 → typedal-4.6.0}/docs/7_configuration.md +0 -0
- {typedal-4.5.0 → typedal-4.6.0}/docs/9_memoization.md +0 -0
- {typedal-4.5.0 → typedal-4.6.0}/docs/css/code_blocks.css +0 -0
- {typedal-4.5.0 → typedal-4.6.0}/docs/index.md +0 -0
- {typedal-4.5.0 → typedal-4.6.0}/docs/requirements.txt +0 -0
- {typedal-4.5.0 → typedal-4.6.0}/example_new.py +0 -0
- {typedal-4.5.0 → typedal-4.6.0}/example_old.py +0 -0
- {typedal-4.5.0 → typedal-4.6.0}/mkdocs.yml +0 -0
- {typedal-4.5.0 → typedal-4.6.0}/src/typedal/__init__.py +0 -0
- {typedal-4.5.0 → typedal-4.6.0}/src/typedal/config.py +0 -0
- {typedal-4.5.0 → typedal-4.6.0}/src/typedal/constants.py +0 -0
- {typedal-4.5.0 → typedal-4.6.0}/src/typedal/for_py4web.py +0 -0
- {typedal-4.5.0 → typedal-4.6.0}/src/typedal/for_web2py.py +0 -0
- /typedal-4.5.0/tests/__init__.py → /typedal-4.6.0/src/typedal/py.typed +0 -0
- {typedal-4.5.0 → typedal-4.6.0}/src/typedal/serializers/as_json.py +0 -0
- {typedal-4.5.0 → typedal-4.6.0}/src/typedal/web2py_py4web_shared.py +0 -0
- {typedal-4.5.0 → typedal-4.6.0}/tasks.py +0 -0
- {typedal-4.5.0 → typedal-4.6.0}/tests/configs/simple.toml +0 -0
- {typedal-4.5.0 → typedal-4.6.0}/tests/configs/valid.env +0 -0
- {typedal-4.5.0 → typedal-4.6.0}/tests/configs/valid.toml +0 -0
- {typedal-4.5.0 → typedal-4.6.0}/tests/py314_tests.py +0 -0
- {typedal-4.5.0 → typedal-4.6.0}/tests/test_cli.py +0 -0
- {typedal-4.5.0 → typedal-4.6.0}/tests/test_config.py +0 -0
- {typedal-4.5.0 → typedal-4.6.0}/tests/test_docs_examples.py +0 -0
- {typedal-4.5.0 → typedal-4.6.0}/tests/test_helpers.py +0 -0
- {typedal-4.5.0 → typedal-4.6.0}/tests/test_json.py +0 -0
- {typedal-4.5.0 → typedal-4.6.0}/tests/test_main.py +0 -0
- {typedal-4.5.0 → typedal-4.6.0}/tests/test_mypy.py +0 -0
- {typedal-4.5.0 → typedal-4.6.0}/tests/test_py4web.py +0 -0
- {typedal-4.5.0 → typedal-4.6.0}/tests/test_query_builder.py +0 -0
- {typedal-4.5.0 → typedal-4.6.0}/tests/test_row.py +0 -0
- {typedal-4.5.0 → typedal-4.6.0}/tests/test_stats.py +0 -0
- {typedal-4.5.0 → typedal-4.6.0}/tests/test_table.py +0 -0
- {typedal-4.5.0 → typedal-4.6.0}/tests/test_web2py.py +0 -0
- {typedal-4.5.0 → typedal-4.6.0}/tests/test_xx_others.py +0 -0
- {typedal-4.5.0 → typedal-4.6.0}/tests/timings.py +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
*
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{"time":"2026-03-13T20:12:28.275088774+01:00","level":"INFO","source":{"function":"github.com/charmbracelet/crush/internal/config.(*catwalkSync).Get.func1","file":"github.com/charmbracelet/crush/internal/config/catwalk.go","line":55},"msg":"Fetching providers from Catwalk"}
|
|
2
|
+
{"time":"2026-03-13T20:12:28.486572096+01:00","level":"INFO","source":{"function":"github.com/charmbracelet/crush/internal/config.(*catwalkSync).Get.func1","file":"github.com/charmbracelet/crush/internal/config/catwalk.go","line":63},"msg":"Catwalk providers not modified"}
|
|
3
|
+
{"time":"2026-03-13T20:12:28.486667656+01:00","level":"WARN","source":{"function":"github.com/charmbracelet/crush/internal/config.(*Config).configureProviders","file":"github.com/charmbracelet/crush/internal/config/load.go","line":296},"msg":"Skipping provider due to missing API key","provider":"anthropic"}
|
|
4
|
+
{"time":"2026-03-13T20:12:28.497862335+01:00","level":"INFO","msg":"OK 20250424200609_initial.sql (1.55ms)"}
|
|
5
|
+
{"time":"2026-03-13T20:12:28.498184106+01:00","level":"INFO","msg":"OK 20250515105448_add_summary_message_id.sql (300.3µs)"}
|
|
6
|
+
{"time":"2026-03-13T20:12:28.498510046+01:00","level":"INFO","msg":"OK 20250624000000_add_created_at_indexes.sql (298.08µs)"}
|
|
7
|
+
{"time":"2026-03-13T20:12:28.498811912+01:00","level":"INFO","msg":"OK 20250627000000_add_provider_to_messages.sql (289.51µs)"}
|
|
8
|
+
{"time":"2026-03-13T20:12:28.499108738+01:00","level":"INFO","msg":"OK 20250810000000_add_is_summary_message.sql (245.48µs)"}
|
|
9
|
+
{"time":"2026-03-13T20:12:28.499362065+01:00","level":"INFO","msg":"OK 20250812000000_add_todos_to_sessions.sql (241.95µs)"}
|
|
10
|
+
{"time":"2026-03-13T20:12:28.499726903+01:00","level":"INFO","msg":"OK 20260127000000_add_read_files_table.sql (351.05µs)"}
|
|
11
|
+
{"time":"2026-03-13T20:12:28.499731494+01:00","level":"INFO","msg":"goose: successfully migrated database to version: 20260127000000"}
|
|
12
|
+
{"time":"2026-03-13T20:12:28.50106094+01:00","level":"INFO","source":{"function":"github.com/charmbracelet/crush/internal/agent/tools/mcp.Initialize","file":"github.com/charmbracelet/crush/internal/agent/tools/mcp/init.go","line":167},"msg":"Initializing MCP clients"}
|
|
13
|
+
{"time":"2026-03-13T20:12:31.915239786+01:00","level":"INFO","source":{"function":"github.com/charmbracelet/crush/internal/config.(*catwalkSync).Get.func1","file":"github.com/charmbracelet/crush/internal/config/catwalk.go","line":55},"msg":"Fetching providers from Catwalk"}
|
|
14
|
+
{"time":"2026-03-13T20:12:32.150078123+01:00","level":"INFO","source":{"function":"github.com/charmbracelet/crush/internal/config.(*catwalkSync).Get.func1","file":"github.com/charmbracelet/crush/internal/config/catwalk.go","line":63},"msg":"Catwalk providers not modified"}
|
|
15
|
+
{"time":"2026-03-13T20:12:32.150303604+01:00","level":"WARN","source":{"function":"github.com/charmbracelet/crush/internal/config.(*Config).configureProviders","file":"github.com/charmbracelet/crush/internal/config/load.go","line":296},"msg":"Skipping provider due to missing API key","provider":"anthropic"}
|
|
16
|
+
{"time":"2026-03-13T20:12:32.15507598+01:00","level":"INFO","msg":"goose: no migrations to run. current version: 20260127000000"}
|
|
17
|
+
{"time":"2026-03-13T20:12:32.161646966+01:00","level":"INFO","source":{"function":"github.com/charmbracelet/crush/internal/agent/tools/mcp.Initialize","file":"github.com/charmbracelet/crush/internal/agent/tools/mcp/init.go","line":167},"msg":"Initializing MCP clients"}
|
|
18
|
+
{"time":"2026-03-13T20:15:20.65020982+01:00","level":"ERROR","source":{"function":"github.com/charmbracelet/x/powernap/pkg/lsp.startServerProcess.func1","file":"github.com/charmbracelet/x/powernap@v0.1.3/pkg/lsp/client.go","line":650},"msg":"Language server stderr","command":"ty","output":"2026-03-13 20:15:20.650149482 INFO Version: 0.0.20\n"}
|
|
19
|
+
{"time":"2026-03-13T20:15:20.651255948+01:00","level":"ERROR","source":{"function":"github.com/charmbracelet/x/powernap/pkg/lsp.startServerProcess.func1","file":"github.com/charmbracelet/x/powernap@v0.1.3/pkg/lsp/client.go","line":650},"msg":"Language server stderr","command":"ty","output":"2026-03-13 20:15:20.651183033 ERROR Invalid client response: did not contain a result or error (method=workspace/configuration)\n"}
|
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
<!--next-version-placeholder-->
|
|
4
4
|
|
|
5
|
+
## v4.6.0 (2026-03-13)
|
|
6
|
+
|
|
7
|
+
### Documentation
|
|
8
|
+
|
|
9
|
+
* Add section about PydanticMixin ([`d91c504`](https://github.com/trialandsuccess/TypeDAL/commit/d91c50485f7f7dd7a1e04ae93998475a05bb3742))
|
|
10
|
+
|
|
5
11
|
## v4.5.0 (2026-03-06)
|
|
6
12
|
|
|
7
13
|
### Feature
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: TypeDAL
|
|
3
|
-
Version: 4.
|
|
3
|
+
Version: 4.6.0
|
|
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
|
|
@@ -8,13 +8,12 @@ Project-URL: Source, https://github.com/trialandsuccess/TypeDAL
|
|
|
8
8
|
Author-email: Robin van der Noord <contact@trialandsuccess.nl>
|
|
9
9
|
Classifier: Development Status :: 4 - Beta
|
|
10
10
|
Classifier: Programming Language :: Python
|
|
11
|
-
Classifier: Programming Language :: Python :: 3.11
|
|
12
11
|
Classifier: Programming Language :: Python :: 3.12
|
|
13
12
|
Classifier: Programming Language :: Python :: 3.13
|
|
14
13
|
Classifier: Programming Language :: Python :: 3.14
|
|
15
14
|
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
16
15
|
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
|
17
|
-
Requires-Python: >=3.
|
|
16
|
+
Requires-Python: >=3.12
|
|
18
17
|
Requires-Dist: configurable-json<2
|
|
19
18
|
Requires-Dist: configuraptor<3,>=2.0.1
|
|
20
19
|
Requires-Dist: dill<1
|
|
@@ -26,6 +25,7 @@ Provides-Extra: all
|
|
|
26
25
|
Requires-Dist: edwh-migrate[full]>=0.8.0; extra == 'all'
|
|
27
26
|
Requires-Dist: py4web; extra == 'all'
|
|
28
27
|
Requires-Dist: pydal2sql[all]>=1.2.0; extra == 'all'
|
|
28
|
+
Requires-Dist: pydantic<3; extra == 'all'
|
|
29
29
|
Requires-Dist: questionary; extra == 'all'
|
|
30
30
|
Requires-Dist: tabulate; extra == 'all'
|
|
31
31
|
Requires-Dist: tomlkit; extra == 'all'
|
|
@@ -36,6 +36,7 @@ Requires-Dist: ewok; extra == 'dev'
|
|
|
36
36
|
Requires-Dist: hatch; extra == 'dev'
|
|
37
37
|
Requires-Dist: mkdocs; extra == 'dev'
|
|
38
38
|
Requires-Dist: mkdocs-dracula-theme; extra == 'dev'
|
|
39
|
+
Requires-Dist: pydantic<3; extra == 'dev'
|
|
39
40
|
Requires-Dist: pytest-mypy-testing; extra == 'dev'
|
|
40
41
|
Requires-Dist: python-semantic-release<8; extra == 'dev'
|
|
41
42
|
Requires-Dist: requests<2.32; extra == 'dev'
|
|
@@ -47,6 +47,58 @@ class MyTable(TypedTable, SlugMixin, slug_field="title"):
|
|
|
47
47
|
# Now, whenever you insert a record into MyTable, the 'slug' field will be automatically generated based on the 'title' field.
|
|
48
48
|
```
|
|
49
49
|
|
|
50
|
+
## Using `PydanticMixin`
|
|
51
|
+
|
|
52
|
+
The `PydanticMixin` enables seamless integration with Pydantic-based frameworks (like FastAPI) by adding schema generation
|
|
53
|
+
capabilities to your models. Without this mixin, you cannot return TypedTable instances directly as FastAPI responses
|
|
54
|
+
or use them with `pydantic.TypeAdapter`.
|
|
55
|
+
|
|
56
|
+
Add the mixin to enable `model_dump()` for serialization, including support for relationships and computed properties:
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
from typedal import TypedTable
|
|
60
|
+
from typedal.mixins import PydanticMixin
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class Author(TypedTable, PydanticMixin):
|
|
64
|
+
name: str
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class Book(TypedTable, PydanticMixin):
|
|
68
|
+
title: str
|
|
69
|
+
author: Author
|
|
70
|
+
|
|
71
|
+
@property
|
|
72
|
+
def display_title(self) -> str:
|
|
73
|
+
return f"{self.title} by {self.author.name}"
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
# After inserting records and joining relationships:
|
|
77
|
+
book = Book.where(id=1).join("author").first()
|
|
78
|
+
|
|
79
|
+
# model_dump() serializes the full object graph
|
|
80
|
+
data = book.model_dump()
|
|
81
|
+
# -> {"id": 1, "title": "...", "author": {"id": 1, "name": "..."}, "display_title": "..."}
|
|
82
|
+
|
|
83
|
+
# Use mode="json" for JSON-serializable output (dates as ISO strings, etc.)
|
|
84
|
+
data = book.model_dump(mode="json")
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
> **Note:** When referencing other TypedTable models from a PydanticMixin class, those models must also include
|
|
88
|
+
> `PydanticMixin`. This ensures the entire object graph can be serialized consistently.
|
|
89
|
+
|
|
90
|
+
With this mixin, TypedTable instances work seamlessly as FastAPI response models:
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
from fastapi import FastAPI
|
|
94
|
+
|
|
95
|
+
app = FastAPI()
|
|
96
|
+
|
|
97
|
+
@app.get("/books/{book_id}")
|
|
98
|
+
def get_book(book_id: int) -> Book:
|
|
99
|
+
return Book.where(id=book_id).join("author").first()
|
|
100
|
+
```
|
|
101
|
+
|
|
50
102
|
## Creating Custom Mixins
|
|
51
103
|
|
|
52
104
|
To create your own mixins for additional functionality, follow these steps:
|
|
@@ -122,6 +174,7 @@ recent_articles = (
|
|
|
122
174
|
By using these mixins, you can enhance the functionality of your models in a modular and reusable manner, saving you
|
|
123
175
|
time and effort in your development process.
|
|
124
176
|
|
|
177
|
+
|
|
125
178
|
---
|
|
126
179
|
|
|
127
180
|
Looking to cache expensive function results? Head to [9. Function Memoization](./9_memoization.md) to learn about `db.memoize()`.
|
|
@@ -10,7 +10,7 @@ name = "TypeDAL"
|
|
|
10
10
|
dynamic = ["version"]
|
|
11
11
|
description = 'Typing support for PyDAL'
|
|
12
12
|
readme = "README.md"
|
|
13
|
-
requires-python = ">=3.
|
|
13
|
+
requires-python = ">=3.12"
|
|
14
14
|
license-expression = "MIT"
|
|
15
15
|
keywords = []
|
|
16
16
|
authors = [
|
|
@@ -19,7 +19,6 @@ authors = [
|
|
|
19
19
|
classifiers = [
|
|
20
20
|
"Development Status :: 4 - Beta",
|
|
21
21
|
"Programming Language :: Python",
|
|
22
|
-
"Programming Language :: Python :: 3.11",
|
|
23
22
|
"Programming Language :: Python :: 3.12",
|
|
24
23
|
"Programming Language :: Python :: 3.13",
|
|
25
24
|
"Programming Language :: Python :: 3.14",
|
|
@@ -56,6 +55,7 @@ all = [
|
|
|
56
55
|
"tabulate",
|
|
57
56
|
"pydal2sql[all]>=1.2.0",
|
|
58
57
|
"edwh-migrate[full]>=0.8.0",
|
|
58
|
+
"pydantic < 3",
|
|
59
59
|
"questionary",
|
|
60
60
|
"tomlkit",
|
|
61
61
|
]
|
|
@@ -70,6 +70,7 @@ dev = [
|
|
|
70
70
|
"pytest-mypy-testing",
|
|
71
71
|
"contextlib-chdir",
|
|
72
72
|
"testcontainers",
|
|
73
|
+
"pydantic < 3",
|
|
73
74
|
# depends on ->
|
|
74
75
|
"requests<2.32",
|
|
75
76
|
# mypy:
|
|
@@ -142,7 +143,7 @@ exclude_also = [
|
|
|
142
143
|
]
|
|
143
144
|
|
|
144
145
|
[tool.mypy]
|
|
145
|
-
python_version = "3.
|
|
146
|
+
python_version = "3.13"
|
|
146
147
|
|
|
147
148
|
# `some: int = None` looks nicer than `some: int | None = None` and pycharm still understands it
|
|
148
149
|
no_implicit_optional = false # I guess 'strict_optional' should be true, but disable this one because it's double!
|
|
@@ -203,10 +203,6 @@ def _remove_cache(s: Set, tablename: str) -> None:
|
|
|
203
203
|
indeces = s.select("id").column("id")
|
|
204
204
|
remove_cache(indeces, tablename)
|
|
205
205
|
|
|
206
|
-
|
|
207
|
-
T_TypedTable = t.TypeVar("T_TypedTable", bound=TypedTable)
|
|
208
|
-
|
|
209
|
-
|
|
210
206
|
def get_expire(
|
|
211
207
|
expires_at: t.Optional[dt.datetime] = None,
|
|
212
208
|
ttl: t.Optional[int | dt.timedelta] = None,
|
|
@@ -250,7 +246,7 @@ def _insert_cache_entry(
|
|
|
250
246
|
db.commit()
|
|
251
247
|
|
|
252
248
|
|
|
253
|
-
def save_to_cache(
|
|
249
|
+
def save_to_cache[T_TypedTable: TypedTable](
|
|
254
250
|
instance: TypedRows[T_TypedTable],
|
|
255
251
|
rows: Rows,
|
|
256
252
|
expires_at: t.Optional[dt.datetime] = None,
|
|
@@ -418,8 +414,10 @@ def _expired_and_valid_query() -> tuple[str, str]:
|
|
|
418
414
|
return expired_items, valid_items
|
|
419
415
|
|
|
420
416
|
|
|
421
|
-
T
|
|
422
|
-
|
|
417
|
+
class Stats[T](t.TypedDict):
|
|
418
|
+
total: T
|
|
419
|
+
valid: T
|
|
420
|
+
expired: T
|
|
423
421
|
|
|
424
422
|
RowStats = t.TypedDict(
|
|
425
423
|
"RowStats",
|
|
@@ -527,7 +525,7 @@ def calculate_stats(db: "TypeDAL") -> Stats[GenericStats]:
|
|
|
527
525
|
}
|
|
528
526
|
|
|
529
527
|
|
|
530
|
-
def memoize(
|
|
528
|
+
def memoize[T: t.Any](
|
|
531
529
|
db: "TypeDAL",
|
|
532
530
|
func: t.Callable[..., T],
|
|
533
531
|
*args: TypedRows[t.Any] | TypedTable,
|
|
@@ -94,12 +94,10 @@ questionary_types: dict[typing.Hashable, Optional[AnyDict]] = {
|
|
|
94
94
|
"fake_migrate": None, # only enable via config if required
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
-
T = typing.TypeVar("T")
|
|
98
|
-
|
|
99
97
|
notfound = object()
|
|
100
98
|
|
|
101
99
|
|
|
102
|
-
def _get_question(prop: str, annotation: typing.Type[T]) -> Optional[AnyDict]: # pragma: no cover
|
|
100
|
+
def _get_question[T](prop: str, annotation: typing.Type[T]) -> Optional[AnyDict]: # pragma: no cover
|
|
103
101
|
question = questionary_types.get(prop, notfound)
|
|
104
102
|
if question is notfound:
|
|
105
103
|
# None means skip the question, notfound means use the type default!
|
|
@@ -111,7 +109,7 @@ def _get_question(prop: str, annotation: typing.Type[T]) -> Optional[AnyDict]:
|
|
|
111
109
|
return question.copy() # type: ignore
|
|
112
110
|
|
|
113
111
|
|
|
114
|
-
def get_question(prop: str, annotation: typing.Type[T], default: T | None) -> Optional[T]: # pragma: no cover
|
|
112
|
+
def get_question[T](prop: str, annotation: typing.Type[T], default: T | None) -> Optional[T]: # pragma: no cover
|
|
115
113
|
"""
|
|
116
114
|
Generate a question based on a config property and prompt the user for it.
|
|
117
115
|
"""
|
|
@@ -449,7 +447,7 @@ def migrations_stub(
|
|
|
449
447
|
return 0
|
|
450
448
|
|
|
451
449
|
|
|
452
|
-
AnyNestedDict
|
|
450
|
+
type AnyNestedDict = dict[str, AnyDict]
|
|
453
451
|
|
|
454
452
|
|
|
455
453
|
def tabulate_data(data: AnyNestedDict) -> None:
|
|
@@ -466,7 +464,7 @@ def tabulate_data(data: AnyNestedDict) -> None:
|
|
|
466
464
|
print(tabulate(flattened_data, headers="keys"))
|
|
467
465
|
|
|
468
466
|
|
|
469
|
-
FormatOptions
|
|
467
|
+
type FormatOptions = typing.Literal["plaintext", "json", "yaml", "toml"]
|
|
470
468
|
|
|
471
469
|
|
|
472
470
|
def get_output_format(fmt: FormatOptions) -> typing.Callable[[AnyNestedDict], None]:
|
|
@@ -21,7 +21,7 @@ from .helpers import (
|
|
|
21
21
|
sql_expression,
|
|
22
22
|
to_snake,
|
|
23
23
|
)
|
|
24
|
-
from .types import CacheStatus, Field,
|
|
24
|
+
from .types import CacheStatus, Field, Template # noqa: F401
|
|
25
25
|
|
|
26
26
|
try:
|
|
27
27
|
# python 3.14+
|
|
@@ -246,7 +246,7 @@ class TypeDAL(pydal.DAL):
|
|
|
246
246
|
self.try_define(_TypedalCache)
|
|
247
247
|
self.try_define(_TypedalCacheDependency)
|
|
248
248
|
|
|
249
|
-
def try_define(self, model: t.Type[T], verbose: bool = False) -> t.Type[T]:
|
|
249
|
+
def try_define[T: t.Any](self, model: t.Type[T], verbose: bool = False) -> t.Type[T]:
|
|
250
250
|
"""
|
|
251
251
|
Try to define a model with migrate or fall back to fake migrate.
|
|
252
252
|
"""
|
|
@@ -270,7 +270,7 @@ class TypeDAL(pydal.DAL):
|
|
|
270
270
|
}
|
|
271
271
|
|
|
272
272
|
@t.overload
|
|
273
|
-
def define(self, maybe_cls: None = None, **kwargs: t.Any) -> t.Callable[[t.Type[T]], t.Type[T]]:
|
|
273
|
+
def define[T: t.Any](self, maybe_cls: None = None, **kwargs: t.Any) -> t.Callable[[t.Type[T]], t.Type[T]]:
|
|
274
274
|
"""
|
|
275
275
|
Typing Overload for define without a class.
|
|
276
276
|
|
|
@@ -279,7 +279,7 @@ class TypeDAL(pydal.DAL):
|
|
|
279
279
|
"""
|
|
280
280
|
|
|
281
281
|
@t.overload
|
|
282
|
-
def define(self, maybe_cls: t.Type[T], **kwargs: t.Any) -> t.Type[T]:
|
|
282
|
+
def define[T: t.Any](self, maybe_cls: t.Type[T], **kwargs: t.Any) -> t.Type[T]:
|
|
283
283
|
"""
|
|
284
284
|
Typing Overload for define with a class.
|
|
285
285
|
|
|
@@ -287,7 +287,7 @@ class TypeDAL(pydal.DAL):
|
|
|
287
287
|
class MyTable(TypedTable): ...
|
|
288
288
|
"""
|
|
289
289
|
|
|
290
|
-
def define(
|
|
290
|
+
def define[T: t.Any](
|
|
291
291
|
self,
|
|
292
292
|
maybe_cls: t.Type[T] | None = None,
|
|
293
293
|
**kwargs: t.Any,
|
|
@@ -379,6 +379,14 @@ class TypeDAL(pydal.DAL):
|
|
|
379
379
|
# alias for backward-compatibility
|
|
380
380
|
return self._builder.class_map
|
|
381
381
|
|
|
382
|
+
def _known_classes(self) -> dict[str, t.Type["TypedTable"]]:
|
|
383
|
+
"""
|
|
384
|
+
Return currently defined TypedTable classes keyed by class name.
|
|
385
|
+
|
|
386
|
+
Useful when resolving forward references in annotations/relationships.
|
|
387
|
+
"""
|
|
388
|
+
return {table.__name__: table for table in self._class_map.values()}
|
|
389
|
+
|
|
382
390
|
@staticmethod
|
|
383
391
|
def to_snake(camel: str) -> str:
|
|
384
392
|
"""
|
|
@@ -460,7 +468,7 @@ class TypeDAL(pydal.DAL):
|
|
|
460
468
|
"""
|
|
461
469
|
return sql_expression(self, sql_fragment, *raw_args, output_type=output_type, **raw_kwargs)
|
|
462
470
|
|
|
463
|
-
def memoize(
|
|
471
|
+
def memoize[T: t.Any](
|
|
464
472
|
self,
|
|
465
473
|
func: t.Callable[..., T],
|
|
466
474
|
# should be TypedRows[TypedTable] or TypedTable but for some reason that breaks
|
|
@@ -29,7 +29,6 @@ from .relationships import Relationship, to_relationship
|
|
|
29
29
|
from .tables import TypedTable
|
|
30
30
|
from .types import (
|
|
31
31
|
Field,
|
|
32
|
-
T,
|
|
33
32
|
T_annotation,
|
|
34
33
|
Table,
|
|
35
34
|
_Types,
|
|
@@ -53,7 +52,7 @@ class TableDefinitionBuilder:
|
|
|
53
52
|
self.db = db
|
|
54
53
|
self.class_map: dict[str, t.Type["TypedTable"]] = {}
|
|
55
54
|
|
|
56
|
-
def define(self, cls: t.Type[T], **kwargs: t.Any) -> t.Type[T]:
|
|
55
|
+
def define[T: t.Any](self, cls: t.Type[T], **kwargs: t.Any) -> t.Type[T]:
|
|
57
56
|
"""Build and register a table from a TypedTable class."""
|
|
58
57
|
full_dict = all_dict(cls)
|
|
59
58
|
tablename = to_snake(cls.__name__)
|
|
@@ -133,7 +132,7 @@ class TableDefinitionBuilder:
|
|
|
133
132
|
"""Convert Python type annotation to pydal field type string."""
|
|
134
133
|
ftype = t.cast(type, ftype_annotation) # cast from Type to type to make mypy happy)
|
|
135
134
|
|
|
136
|
-
known_classes =
|
|
135
|
+
known_classes = self.db._known_classes()
|
|
137
136
|
|
|
138
137
|
if isinstance(ftype, str):
|
|
139
138
|
# extract type from string
|
|
@@ -24,8 +24,6 @@ from .types import (
|
|
|
24
24
|
Query,
|
|
25
25
|
T_annotation,
|
|
26
26
|
T_MetaInstance,
|
|
27
|
-
T_subclass,
|
|
28
|
-
T_Value,
|
|
29
27
|
Validator,
|
|
30
28
|
)
|
|
31
29
|
|
|
@@ -37,7 +35,7 @@ if t.TYPE_CHECKING:
|
|
|
37
35
|
## general
|
|
38
36
|
|
|
39
37
|
|
|
40
|
-
class TypedField
|
|
38
|
+
class TypedField[T_Value](Expression): # pragma: no cover
|
|
41
39
|
"""
|
|
42
40
|
Typed version of pydal.Field, which will be converted to a normal Field in the background.
|
|
43
41
|
"""
|
|
@@ -375,7 +373,9 @@ def UploadField(**kw: t.Unpack[FieldSettings]) -> TypedField[str]:
|
|
|
375
373
|
Upload = UploadField
|
|
376
374
|
|
|
377
375
|
|
|
378
|
-
def ReferenceField
|
|
376
|
+
def ReferenceField[
|
|
377
|
+
T_subclass: (TypedTable, Table)
|
|
378
|
+
](
|
|
379
379
|
other_table: str | t.Type[TypedTable] | TypedTable | Table | T_subclass,
|
|
380
380
|
**kw: t.Unpack[FieldSettings],
|
|
381
381
|
) -> TypedField[int]:
|
|
@@ -15,7 +15,7 @@ from collections import ChainMap
|
|
|
15
15
|
|
|
16
16
|
from pydal import DAL
|
|
17
17
|
|
|
18
|
-
from .types import AnyDict, Expression, Field, Row,
|
|
18
|
+
from .types import AnyDict, Expression, Field, Row, Table, Template
|
|
19
19
|
|
|
20
20
|
try:
|
|
21
21
|
import annotationlib
|
|
@@ -91,7 +91,7 @@ def all_annotations(cls: type, _except: t.Optional[t.Iterable[str]] = None) -> d
|
|
|
91
91
|
return {k: v for k, v in _all.items() if k not in _except}
|
|
92
92
|
|
|
93
93
|
|
|
94
|
-
def instanciate(cls: t.Type[T] | T, with_args: bool = False) -> T:
|
|
94
|
+
def instanciate[T: t.Any](cls: t.Type[T] | T, with_args: bool = False) -> T:
|
|
95
95
|
"""
|
|
96
96
|
Create an instance of T (if it is a class).
|
|
97
97
|
|
|
@@ -173,10 +173,6 @@ def mktable(
|
|
|
173
173
|
return output.getvalue()
|
|
174
174
|
|
|
175
175
|
|
|
176
|
-
K = t.TypeVar("K")
|
|
177
|
-
V = t.TypeVar("V")
|
|
178
|
-
|
|
179
|
-
|
|
180
176
|
def looks_like(v: t.Any, _type: type[t.Any]) -> bool:
|
|
181
177
|
"""
|
|
182
178
|
Returns true if v or v's class is of type _type, including if it is a generic.
|
|
@@ -189,7 +185,7 @@ def looks_like(v: t.Any, _type: type[t.Any]) -> bool:
|
|
|
189
185
|
return isinstance(v, _type) or (isinstance(v, type) and issubclass(v, _type)) or origin_is_subclass(v, _type)
|
|
190
186
|
|
|
191
187
|
|
|
192
|
-
def filter_out(mut_dict: dict[K, V], _type: type[T]) -> dict[K, T]:
|
|
188
|
+
def filter_out[K, V, T](mut_dict: dict[K, V], _type: type[T]) -> dict[K, T]:
|
|
193
189
|
"""
|
|
194
190
|
Split a dictionary into things matching _type and the rest.
|
|
195
191
|
|
|
@@ -211,20 +207,20 @@ def unwrap_type(_type: type) -> type:
|
|
|
211
207
|
|
|
212
208
|
|
|
213
209
|
@t.overload
|
|
214
|
-
def extract_type_optional(annotation: T) -> tuple[T, bool]:
|
|
210
|
+
def extract_type_optional[T](annotation: T) -> tuple[T, bool]:
|
|
215
211
|
"""
|
|
216
212
|
T -> T is not exactly right because you'll get the inner type, but mypy seems happy with this.
|
|
217
213
|
"""
|
|
218
214
|
|
|
219
215
|
|
|
220
216
|
@t.overload
|
|
221
|
-
def extract_type_optional(annotation: None) -> tuple[None, bool]:
|
|
217
|
+
def extract_type_optional[T](annotation: None) -> tuple[None, bool]:
|
|
222
218
|
"""
|
|
223
219
|
None leads to None, False.
|
|
224
220
|
"""
|
|
225
221
|
|
|
226
222
|
|
|
227
|
-
def extract_type_optional(annotation: T | None) -> tuple[T | None, bool]:
|
|
223
|
+
def extract_type_optional[T](annotation: T | None) -> tuple[T | None, bool]:
|
|
228
224
|
"""
|
|
229
225
|
Given an annotation, extract the actual type and whether it is optional.
|
|
230
226
|
"""
|
|
@@ -256,13 +252,13 @@ class DummyQuery:
|
|
|
256
252
|
Placeholder to &= and |= actual query parts.
|
|
257
253
|
"""
|
|
258
254
|
|
|
259
|
-
def __or__(self, other: T) -> T:
|
|
255
|
+
def __or__[T](self, other: T) -> T:
|
|
260
256
|
"""
|
|
261
257
|
For 'or': DummyQuery | Other == Other.
|
|
262
258
|
"""
|
|
263
259
|
return other
|
|
264
260
|
|
|
265
|
-
def __and__(self, other: T) -> T:
|
|
261
|
+
def __and__[T](self, other: T) -> T:
|
|
266
262
|
"""
|
|
267
263
|
For 'and': DummyQuery & Other == Other.
|
|
268
264
|
"""
|
|
@@ -275,7 +271,7 @@ class DummyQuery:
|
|
|
275
271
|
return False
|
|
276
272
|
|
|
277
273
|
|
|
278
|
-
def as_lambda(value: T) -> t.Callable[..., T]:
|
|
274
|
+
def as_lambda[T](value: T) -> t.Callable[..., T]:
|
|
279
275
|
"""
|
|
280
276
|
Wrap value in a callable.
|
|
281
277
|
"""
|
|
@@ -342,7 +338,7 @@ class classproperty:
|
|
|
342
338
|
"""
|
|
343
339
|
self.fget = fget
|
|
344
340
|
|
|
345
|
-
def __get__(self, obj: t.Any, owner: t.Type[T]) -> t.Any:
|
|
341
|
+
def __get__[T](self, obj: t.Any, owner: t.Type[T]) -> t.Any:
|
|
346
342
|
"""
|
|
347
343
|
Retrieve the property value.
|
|
348
344
|
|
|
@@ -589,7 +585,7 @@ def normalize_table_keys(row: Row, pattern: re.Pattern[str] = re.compile(r"^([a-
|
|
|
589
585
|
return Row(new_data)
|
|
590
586
|
|
|
591
587
|
|
|
592
|
-
def default_representer(field: TypedField[T], value: T, table: t.Type[TypedTable]) -> str:
|
|
588
|
+
def default_representer[T: t.Any](field: TypedField[T], value: T, table: t.Type[TypedTable]) -> str:
|
|
593
589
|
"""
|
|
594
590
|
Simply call field.represent on the value.
|
|
595
591
|
"""
|