TypeDAL 4.4.5__tar.gz → 4.5.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.4.5 → typedal-4.5.0}/CHANGELOG.md +16 -0
- {typedal-4.4.5 → typedal-4.5.0}/PKG-INFO +44 -11
- {typedal-4.4.5 → typedal-4.5.0}/README.md +43 -10
- typedal-4.5.0/docs/10_advanced_apis.md +76 -0
- {typedal-4.4.5 → typedal-4.5.0}/docs/2_defining_tables.md +2 -2
- {typedal-4.4.5 → typedal-4.5.0}/docs/3_building_queries.md +8 -3
- {typedal-4.4.5 → typedal-4.5.0}/docs/4_relationships.md +24 -1
- {typedal-4.4.5 → typedal-4.5.0}/docs/7_configuration.md +1 -1
- {typedal-4.4.5 → typedal-4.5.0}/docs/9_memoization.md +5 -0
- {typedal-4.4.5 → typedal-4.5.0}/docs/index.md +1 -0
- {typedal-4.4.5 → typedal-4.5.0}/mkdocs.yml +2 -1
- {typedal-4.4.5 → typedal-4.5.0}/src/typedal/__about__.py +1 -1
- {typedal-4.4.5 → typedal-4.5.0}/src/typedal/cli.py +2 -1
- {typedal-4.4.5 → typedal-4.5.0}/src/typedal/fields.py +2 -2
- {typedal-4.4.5 → typedal-4.5.0}/src/typedal/mixins.py +3 -6
- {typedal-4.4.5 → typedal-4.5.0}/src/typedal/query_builder.py +11 -6
- {typedal-4.4.5 → typedal-4.5.0}/src/typedal/relationships.py +1 -1
- {typedal-4.4.5 → typedal-4.5.0}/src/typedal/rows.py +17 -1
- {typedal-4.4.5 → typedal-4.5.0}/src/typedal/tables.py +63 -12
- {typedal-4.4.5 → typedal-4.5.0}/src/typedal/types.py +2 -2
- {typedal-4.4.5 → typedal-4.5.0}/tests/test_mypy.py +24 -0
- {typedal-4.4.5 → typedal-4.5.0}/tests/test_query_builder.py +8 -0
- typedal-4.4.5/.crush/.gitignore +0 -1
- typedal-4.4.5/.crush/crush.db-shm +0 -0
- typedal-4.4.5/.crush/crush.db-wal +0 -0
- typedal-4.4.5/.crush/init +0 -0
- typedal-4.4.5/.crush/logs/crush.log +0 -37
- {typedal-4.4.5 → typedal-4.5.0}/.github/workflows/su6.yml +0 -0
- {typedal-4.4.5 → typedal-4.5.0}/.gitignore +0 -0
- {typedal-4.4.5 → typedal-4.5.0}/.readthedocs.yml +0 -0
- {typedal-4.4.5 → typedal-4.5.0}/coverage.svg +0 -0
- {typedal-4.4.5 → typedal-4.5.0}/docs/1_getting_started.md +0 -0
- {typedal-4.4.5 → typedal-4.5.0}/docs/5_py4web.md +0 -0
- {typedal-4.4.5 → typedal-4.5.0}/docs/6_migrations.md +0 -0
- {typedal-4.4.5 → typedal-4.5.0}/docs/8_mixins.md +0 -0
- {typedal-4.4.5 → typedal-4.5.0}/docs/css/code_blocks.css +0 -0
- {typedal-4.4.5 → typedal-4.5.0}/docs/requirements.txt +0 -0
- {typedal-4.4.5 → typedal-4.5.0}/example_new.py +0 -0
- {typedal-4.4.5 → typedal-4.5.0}/example_old.py +0 -0
- {typedal-4.4.5 → typedal-4.5.0}/pyproject.toml +0 -0
- {typedal-4.4.5 → typedal-4.5.0}/src/typedal/__init__.py +0 -0
- {typedal-4.4.5 → typedal-4.5.0}/src/typedal/caching.py +0 -0
- {typedal-4.4.5 → typedal-4.5.0}/src/typedal/config.py +0 -0
- {typedal-4.4.5 → typedal-4.5.0}/src/typedal/constants.py +0 -0
- {typedal-4.4.5 → typedal-4.5.0}/src/typedal/core.py +0 -0
- {typedal-4.4.5 → typedal-4.5.0}/src/typedal/define.py +0 -0
- {typedal-4.4.5 → typedal-4.5.0}/src/typedal/for_py4web.py +0 -0
- {typedal-4.4.5 → typedal-4.5.0}/src/typedal/for_web2py.py +0 -0
- {typedal-4.4.5 → typedal-4.5.0}/src/typedal/helpers.py +0 -0
- {typedal-4.4.5 → typedal-4.5.0}/src/typedal/py.typed +0 -0
- {typedal-4.4.5 → typedal-4.5.0}/src/typedal/serializers/as_json.py +0 -0
- {typedal-4.4.5 → typedal-4.5.0}/src/typedal/web2py_py4web_shared.py +0 -0
- {typedal-4.4.5 → typedal-4.5.0}/tasks.py +0 -0
- {typedal-4.4.5 → typedal-4.5.0}/tests/__init__.py +0 -0
- {typedal-4.4.5 → typedal-4.5.0}/tests/configs/simple.toml +0 -0
- {typedal-4.4.5 → typedal-4.5.0}/tests/configs/valid.env +0 -0
- {typedal-4.4.5 → typedal-4.5.0}/tests/configs/valid.toml +0 -0
- {typedal-4.4.5 → typedal-4.5.0}/tests/py314_tests.py +0 -0
- {typedal-4.4.5 → typedal-4.5.0}/tests/test_cli.py +0 -0
- {typedal-4.4.5 → typedal-4.5.0}/tests/test_config.py +0 -0
- {typedal-4.4.5 → typedal-4.5.0}/tests/test_docs_examples.py +0 -0
- {typedal-4.4.5 → typedal-4.5.0}/tests/test_helpers.py +0 -0
- {typedal-4.4.5 → typedal-4.5.0}/tests/test_json.py +0 -0
- {typedal-4.4.5 → typedal-4.5.0}/tests/test_main.py +0 -0
- {typedal-4.4.5 → typedal-4.5.0}/tests/test_mixins.py +0 -0
- {typedal-4.4.5 → typedal-4.5.0}/tests/test_orm.py +0 -0
- {typedal-4.4.5 → typedal-4.5.0}/tests/test_py4web.py +0 -0
- {typedal-4.4.5 → typedal-4.5.0}/tests/test_relationships.py +0 -0
- {typedal-4.4.5 → typedal-4.5.0}/tests/test_row.py +0 -0
- {typedal-4.4.5 → typedal-4.5.0}/tests/test_stats.py +0 -0
- {typedal-4.4.5 → typedal-4.5.0}/tests/test_table.py +0 -0
- {typedal-4.4.5 → typedal-4.5.0}/tests/test_web2py.py +0 -0
- {typedal-4.4.5 → typedal-4.5.0}/tests/test_xx_others.py +0 -0
- {typedal-4.4.5 → typedal-4.5.0}/tests/timings.py +0 -0
|
@@ -2,6 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
<!--next-version-placeholder-->
|
|
4
4
|
|
|
5
|
+
## v4.5.0 (2026-03-06)
|
|
6
|
+
|
|
7
|
+
### Feature
|
|
8
|
+
|
|
9
|
+
* **typing:** Support mixin-typed table class APIs without MRO conflicts ([`fe0c99e`](https://github.com/trialandsuccess/TypeDAL/commit/fe0c99eb2790baff8ddf4de3ec1f23c53e4b3a11))
|
|
10
|
+
|
|
11
|
+
## v4.4.6 (2026-03-05)
|
|
12
|
+
|
|
13
|
+
### Fix
|
|
14
|
+
|
|
15
|
+
* Support .first() when using querybuilder on pydal-style tables. ([`d75f254`](https://github.com/trialandsuccess/TypeDAL/commit/d75f25440f0dcca51991bc047bf7ce8d479f942b))
|
|
16
|
+
|
|
17
|
+
### Documentation
|
|
18
|
+
|
|
19
|
+
* Make docs up-to-date with recently introduced features, improve reading flow for new users ([`64bb7be`](https://github.com/trialandsuccess/TypeDAL/commit/64bb7be220c5f1a5998940b320565b5a3dbca988))
|
|
20
|
+
|
|
5
21
|
## v4.4.5 (2026-02-27)
|
|
6
22
|
|
|
7
23
|
### Fix
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: TypeDAL
|
|
3
|
-
Version: 4.
|
|
3
|
+
Version: 4.5.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
|
|
@@ -81,13 +81,49 @@ the underlying `db.define_table` pydal Tables.
|
|
|
81
81
|
e.g. `rows: TypedRows[SomeTable] = db(...).select()`. When using the QueryBuilder, a `TypedRows` instance is returned
|
|
82
82
|
by `.collect()`.
|
|
83
83
|
|
|
84
|
-
Version 2.0 also introduces more ORM-like
|
|
84
|
+
Version 2.0 also introduces more ORM-like functionality.
|
|
85
85
|
Most notably, a Typed Query Builder that sees your table classes as models with relationships to each other.
|
|
86
86
|
See [3. Building Queries](https://typedal.readthedocs.io/en/stable/3_building_queries/) for more
|
|
87
87
|
details.
|
|
88
88
|
|
|
89
|
+
## Quickstart
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
uv pip install typedal
|
|
93
|
+
# alternative:
|
|
94
|
+
pip install typedal
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
from typedal import TypeDAL, TypedTable
|
|
99
|
+
|
|
100
|
+
db = TypeDAL("sqlite:memory")
|
|
101
|
+
# Alternatives:
|
|
102
|
+
# db = TypeDAL("sqlite://storage.sqlite")
|
|
103
|
+
# db = TypeDAL("postgres://user:password@localhost:5432/mydb")
|
|
104
|
+
# db = TypeDAL("mysql://user:password@localhost:3306/mydb")
|
|
105
|
+
# ...
|
|
106
|
+
|
|
107
|
+
@db.define()
|
|
108
|
+
class User(TypedTable):
|
|
109
|
+
name: str
|
|
110
|
+
age: int | None
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
User.insert(name="Alice", age=30)
|
|
114
|
+
adults = User.where(User.age >= 18).collect()
|
|
115
|
+
print(adults.column("name")) # ['Alice']
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
If you are new to TypeDAL, start with:
|
|
119
|
+
|
|
120
|
+
1. [Getting Started](https://typedal.readthedocs.io/en/stable/1_getting_started/)
|
|
121
|
+
2. [Defining Tables](https://typedal.readthedocs.io/en/stable/2_defining_tables/)
|
|
122
|
+
3. [Building Queries](https://typedal.readthedocs.io/en/stable/3_building_queries/)
|
|
123
|
+
4. [Relationships](https://typedal.readthedocs.io/en/stable/4_relationships/)
|
|
124
|
+
|
|
89
125
|
## CLI
|
|
90
|
-
The
|
|
126
|
+
The TypeDAL CLI provides a convenient interface for generating SQL migrations for [edwh-migrate](https://github.com/educationwarehouse/migrate#readme)
|
|
91
127
|
from PyDAL or TypeDAL configurations using [pydal2sql](https://github.com/robinvandernoord/pydal2sql).
|
|
92
128
|
It offers various commands to streamline database management tasks.
|
|
93
129
|
|
|
@@ -252,7 +288,7 @@ row = db.table_name(id=1) # -> Any (Row)
|
|
|
252
288
|
# all:
|
|
253
289
|
all_rows = TableName.collect() # or .all()
|
|
254
290
|
# some:
|
|
255
|
-
# order of select and where is
|
|
291
|
+
# order of select and where is interchangeable here
|
|
256
292
|
rows = TableName.select(Tablename.id).where(TableName.id > 5).where(TableName.id < 50).collect()
|
|
257
293
|
# one:
|
|
258
294
|
row = TableName(id=1) # or .where(...).first()
|
|
@@ -330,13 +366,10 @@ db.commit() # this is usually done automatically but sometimes you want to manua
|
|
|
330
366
|
|
|
331
367
|
## Caveats
|
|
332
368
|
|
|
333
|
-
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
annotations are converted to string representations. This makes it very hard to re-evaluate the annotation into the
|
|
337
|
-
original type, since the variable scope is lost (and thus references to variables or other classes are ambiguous or
|
|
338
|
-
simply impossible to find).
|
|
369
|
+
- Some editors (notably PyCharm) cannot always distinguish class-level and instance-level access on the same symbol.
|
|
370
|
+
For example, `Model.somefield` is a field descriptor (query operations like `.belongs()`), while
|
|
371
|
+
`model.somefield` is the runtime value (for example `list[str]`).
|
|
339
372
|
- `TypedField` limitations; Since pydal implements some magic methods to perform queries, some features of typing will
|
|
340
373
|
not work on a typed field: `typing.Optional` or a union (`Field() | None`) will result in errors. The only way to make
|
|
341
374
|
a typedfield optional right now, would be to set `required=False` as an argument yourself. This is also a reason
|
|
342
|
-
why `typing.get_type_hints` is not a solution
|
|
375
|
+
why `typing.get_type_hints` is not a complete solution.
|
|
@@ -24,13 +24,49 @@ the underlying `db.define_table` pydal Tables.
|
|
|
24
24
|
e.g. `rows: TypedRows[SomeTable] = db(...).select()`. When using the QueryBuilder, a `TypedRows` instance is returned
|
|
25
25
|
by `.collect()`.
|
|
26
26
|
|
|
27
|
-
Version 2.0 also introduces more ORM-like
|
|
27
|
+
Version 2.0 also introduces more ORM-like functionality.
|
|
28
28
|
Most notably, a Typed Query Builder that sees your table classes as models with relationships to each other.
|
|
29
29
|
See [3. Building Queries](https://typedal.readthedocs.io/en/stable/3_building_queries/) for more
|
|
30
30
|
details.
|
|
31
31
|
|
|
32
|
+
## Quickstart
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
uv pip install typedal
|
|
36
|
+
# alternative:
|
|
37
|
+
pip install typedal
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
from typedal import TypeDAL, TypedTable
|
|
42
|
+
|
|
43
|
+
db = TypeDAL("sqlite:memory")
|
|
44
|
+
# Alternatives:
|
|
45
|
+
# db = TypeDAL("sqlite://storage.sqlite")
|
|
46
|
+
# db = TypeDAL("postgres://user:password@localhost:5432/mydb")
|
|
47
|
+
# db = TypeDAL("mysql://user:password@localhost:3306/mydb")
|
|
48
|
+
# ...
|
|
49
|
+
|
|
50
|
+
@db.define()
|
|
51
|
+
class User(TypedTable):
|
|
52
|
+
name: str
|
|
53
|
+
age: int | None
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
User.insert(name="Alice", age=30)
|
|
57
|
+
adults = User.where(User.age >= 18).collect()
|
|
58
|
+
print(adults.column("name")) # ['Alice']
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
If you are new to TypeDAL, start with:
|
|
62
|
+
|
|
63
|
+
1. [Getting Started](https://typedal.readthedocs.io/en/stable/1_getting_started/)
|
|
64
|
+
2. [Defining Tables](https://typedal.readthedocs.io/en/stable/2_defining_tables/)
|
|
65
|
+
3. [Building Queries](https://typedal.readthedocs.io/en/stable/3_building_queries/)
|
|
66
|
+
4. [Relationships](https://typedal.readthedocs.io/en/stable/4_relationships/)
|
|
67
|
+
|
|
32
68
|
## CLI
|
|
33
|
-
The
|
|
69
|
+
The TypeDAL CLI provides a convenient interface for generating SQL migrations for [edwh-migrate](https://github.com/educationwarehouse/migrate#readme)
|
|
34
70
|
from PyDAL or TypeDAL configurations using [pydal2sql](https://github.com/robinvandernoord/pydal2sql).
|
|
35
71
|
It offers various commands to streamline database management tasks.
|
|
36
72
|
|
|
@@ -195,7 +231,7 @@ row = db.table_name(id=1) # -> Any (Row)
|
|
|
195
231
|
# all:
|
|
196
232
|
all_rows = TableName.collect() # or .all()
|
|
197
233
|
# some:
|
|
198
|
-
# order of select and where is
|
|
234
|
+
# order of select and where is interchangeable here
|
|
199
235
|
rows = TableName.select(Tablename.id).where(TableName.id > 5).where(TableName.id < 50).collect()
|
|
200
236
|
# one:
|
|
201
237
|
row = TableName(id=1) # or .where(...).first()
|
|
@@ -273,13 +309,10 @@ db.commit() # this is usually done automatically but sometimes you want to manua
|
|
|
273
309
|
|
|
274
310
|
## Caveats
|
|
275
311
|
|
|
276
|
-
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
annotations are converted to string representations. This makes it very hard to re-evaluate the annotation into the
|
|
280
|
-
original type, since the variable scope is lost (and thus references to variables or other classes are ambiguous or
|
|
281
|
-
simply impossible to find).
|
|
312
|
+
- Some editors (notably PyCharm) cannot always distinguish class-level and instance-level access on the same symbol.
|
|
313
|
+
For example, `Model.somefield` is a field descriptor (query operations like `.belongs()`), while
|
|
314
|
+
`model.somefield` is the runtime value (for example `list[str]`).
|
|
282
315
|
- `TypedField` limitations; Since pydal implements some magic methods to perform queries, some features of typing will
|
|
283
316
|
not work on a typed field: `typing.Optional` or a union (`Field() | None`) will result in errors. The only way to make
|
|
284
317
|
a typedfield optional right now, would be to set `required=False` as an argument yourself. This is also a reason
|
|
285
|
-
why `typing.get_type_hints` is not a solution
|
|
318
|
+
why `typing.get_type_hints` is not a complete solution.
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# 10. Advanced APIs
|
|
2
|
+
|
|
3
|
+
This chapter documents a few public APIs that are useful in specific cases, but are not part of the default onboarding flow.
|
|
4
|
+
|
|
5
|
+
## QueryBuilder on old-style pyDAL tables
|
|
6
|
+
|
|
7
|
+
If you are migrating incrementally, you can still use TypeDAL's query builder on existing pyDAL tables:
|
|
8
|
+
|
|
9
|
+
```python
|
|
10
|
+
from typedal import QueryBuilder
|
|
11
|
+
|
|
12
|
+
rows = QueryBuilder(db.some_table).where(id=2).collect()
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
This gives you partial builder ergonomics while keeping your existing table definitions.
|
|
16
|
+
|
|
17
|
+
> **Important:** support is intentionally limited for old-style tables.
|
|
18
|
+
> Internally, `.collect()` is effectively a passthrough to `.execute()` and returns regular pyDAL `Rows`.
|
|
19
|
+
> `.first()`/`.first_or_fail()` return a regular pyDAL `Row` in this mode.
|
|
20
|
+
|
|
21
|
+
### Verified working methods
|
|
22
|
+
|
|
23
|
+
- Query composition: `.where(...)`, `.select(...)`, `.orderby(...)`, `.groupby(...)`, `.having(...)`
|
|
24
|
+
- Execution/introspection: `.execute()`, `.collect()`, `.to_sql()`
|
|
25
|
+
- Row access: `.first()`, `.first_or_fail()`
|
|
26
|
+
- Pagination helpers: `.paginate()`, `.chunk()`
|
|
27
|
+
- Basic set operations: `.count()`, `.exists()`, `.update(...)`, `.delete(...)`, `.collect_or_fail()`
|
|
28
|
+
- `.cache(...).collect()` runs (returns pyDAL `Rows`)
|
|
29
|
+
|
|
30
|
+
### Verified unsupported methods
|
|
31
|
+
|
|
32
|
+
- `.join(...)`: **not supported** for old-style tables (depends on TypeDAL model relationship internals)
|
|
33
|
+
|
|
34
|
+
### Behavioral caveat
|
|
35
|
+
|
|
36
|
+
Legacy mode does not perform typed model mapping. Expect pyDAL `Rows`/`Row` outputs rather than typed entities.
|
|
37
|
+
|
|
38
|
+
If you need full QueryBuilder behavior (typed entities, relationships, typed joins, cache integration),
|
|
39
|
+
migrate that table to `TypedTable`.
|
|
40
|
+
|
|
41
|
+
## Upsert and validation helpers
|
|
42
|
+
|
|
43
|
+
`TypedTable` exposes convenience methods for common upsert/validation flows:
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
# update if found, otherwise insert
|
|
47
|
+
user = User.update_or_insert(User.email == "a@example.com", email="a@example.com", name="Alice")
|
|
48
|
+
|
|
49
|
+
# validate before insert
|
|
50
|
+
created, errors = User.validate_and_insert(email="a@example.com")
|
|
51
|
+
|
|
52
|
+
# validate before update
|
|
53
|
+
updated, errors = User.validate_and_update(User.id == 1, name="Alice Updated")
|
|
54
|
+
|
|
55
|
+
# validate before update-or-insert
|
|
56
|
+
row, errors = User.validate_and_update_or_insert(User.email == "a@example.com", name="Alice")
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Behavior notes:
|
|
60
|
+
|
|
61
|
+
- `update_or_insert(...)` returns the resulting instance.
|
|
62
|
+
- `validate_and_*` methods return `(instance_or_none, errors_or_none)`.
|
|
63
|
+
|
|
64
|
+
## Reordering table fields
|
|
65
|
+
|
|
66
|
+
You can reorder fields on a defined table with `reorder_fields`:
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
# Keep listed fields first, keep all other fields after them
|
|
70
|
+
MyTable.reorder_fields(MyTable.id, MyTable.name)
|
|
71
|
+
|
|
72
|
+
# Keep only the listed fields
|
|
73
|
+
MyTable.reorder_fields(MyTable.id, MyTable.name, keep_others=False)
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
This is useful when you want deterministic field order for SQL generation, inspection, or exports.
|
|
@@ -40,7 +40,7 @@ Any keyword arguments you would pass to `db.define_table`, you can also pass to
|
|
|
40
40
|
| `Field('name', 'time')` | `name: datetime.time` | `name: TypedField[datetime.time]` | `name = TypedField(datetime.time)` | `name = TimeField()` |
|
|
41
41
|
| `Field('name', 'datetime')` | `name: datetime.datetime` | `name: TypedField[datetime.datetime]` | `name = TypedField(datetime.datetime)` | `name = DatetimeField()` |
|
|
42
42
|
| `Field('name', 'password')` | × | × | `name = TypedField(str, type="password")` | `name = PasswordField()` |
|
|
43
|
-
| `Field('name', 'upload')` | × | × | `name = TypedField(str, type="upload)`
|
|
43
|
+
| `Field('name', 'upload')` | × | × | `name = TypedField(str, type="upload")` | `name = UploadField()` |
|
|
44
44
|
| `Field('name', 'reference <table>')` | `name: Table` | `name: TypedField[Table]` | `name = TypedField(Table)` | `name = ReferenceField('table')` |
|
|
45
45
|
| `Field('name', 'list:string')` | `name: list[str]` | `name: TypedField[list[str]]` | `name = TypedField(list[str])` | `name = ListStringField()` |
|
|
46
46
|
| `Field('name', 'list:integer')` | `name: list[int]` | `name: TypedField[list[int]]` | `name = TypedField()` | `name = ListIntegerField()` |
|
|
@@ -104,4 +104,4 @@ row.delete_record() # to trigger
|
|
|
104
104
|
MyTable.where(...).delete() # to trigger
|
|
105
105
|
```
|
|
106
106
|
|
|
107
|
-
|
|
107
|
+
Now that we have some tables, it's time to actually query them! Let's go to [3. Building Queries](./3_building_queries.md) to learn how.
|
|
@@ -236,8 +236,8 @@ The Query Builder has a few operations that don't return a new builder instance:
|
|
|
236
236
|
- first: get the first entity matching your query, possibly with relationships loaded (if .join was used)
|
|
237
237
|
- first_or_fail: where `first` may return an empty result, this variant will raise an error if there are no results.
|
|
238
238
|
- to_sql: get the SQL string that would run, useful for debugging, subqueries and other advanced SQL operations.
|
|
239
|
-
- update: instead of selecting rows, update those matching the current query (see [
|
|
240
|
-
- delete: instead of selecting rows, delete those matching the current query (see [
|
|
239
|
+
- update: instead of selecting rows, update those matching the current query (see [Update](#update))
|
|
240
|
+
- delete: instead of selecting rows, delete those matching the current query (see [Delete](#delete))
|
|
241
241
|
|
|
242
242
|
Additionally, you can directly call `.all()`, `.collect()`, `.count()`, `.first()` on a model (e.g. `User.all()`).
|
|
243
243
|
|
|
@@ -264,7 +264,7 @@ person.update_record(name="New Name")
|
|
|
264
264
|
db(db.person.name == "Old Name").delete()
|
|
265
265
|
|
|
266
266
|
row = db.person(4)
|
|
267
|
-
row.
|
|
267
|
+
row.delete_record()
|
|
268
268
|
|
|
269
269
|
# typedal:
|
|
270
270
|
Person.where(id="Old Name").delete() # via query builder
|
|
@@ -272,3 +272,8 @@ Person.where(id="Old Name").delete() # via query builder
|
|
|
272
272
|
person = Person(4)
|
|
273
273
|
person.delete_record()
|
|
274
274
|
```
|
|
275
|
+
|
|
276
|
+
---
|
|
277
|
+
|
|
278
|
+
Need less-common query patterns (for example, using `QueryBuilder` on old-style pyDAL tables)?
|
|
279
|
+
See [10. Advanced APIs](./10_advanced_apis.md).
|
|
@@ -98,6 +98,30 @@ In this example, `Relationship["Sidekick"]` is added as an extra type hint, sinc
|
|
|
98
98
|
after the superhero class.
|
|
99
99
|
Adding the `Relationship["Sidekick"]` hint is optional, but recommended to improve editor support.
|
|
100
100
|
|
|
101
|
+
### Forward References with `Ref[...]` (typing helper)
|
|
102
|
+
|
|
103
|
+
For relationship targets, you can use `Ref[...]` as a typed replacement for the string form:
|
|
104
|
+
|
|
105
|
+
```python
|
|
106
|
+
from typedal.relationships import Ref
|
|
107
|
+
|
|
108
|
+
bestie = relationship(Ref["BestFriend"], lambda user, bestie: user.id == bestie.friend)
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
This is equivalent at runtime to:
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
bestie = relationship("BestFriend", lambda user, bestie: user.id == bestie.friend)
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
`Ref[...]` is primarily for type-checking/editor support. It does not change runtime behavior.
|
|
118
|
+
|
|
119
|
+
For normal table fields, `Ref[...]` is not needed. Use standard forward-reference annotations, for example:
|
|
120
|
+
|
|
121
|
+
```python
|
|
122
|
+
owner: "User"
|
|
123
|
+
```
|
|
124
|
+
|
|
101
125
|
## Many-to-Many
|
|
102
126
|
|
|
103
127
|
Setting up a relationship that uses a junction/pivot table is slightly harder.
|
|
@@ -199,4 +223,3 @@ Depending on your setup:
|
|
|
199
223
|
- **Using py4web or web2py?** → [5. py4web & web2py](./5_py4web.md)
|
|
200
224
|
- **Ready to manage your database?** → [6. Migrations](./6_migrations.md)
|
|
201
225
|
- **Dive deeper into functionality?** → [8.: Mixins](./8_mixins.md)
|
|
202
|
-
|
|
@@ -55,7 +55,7 @@ noop = false
|
|
|
55
55
|
### Generating Migrations (pydal2sql)
|
|
56
56
|
|
|
57
57
|
- **`input`**: Path to your TypeDAL table definitions file
|
|
58
|
-
- **`output`**: Path to the generated migration `.py`
|
|
58
|
+
- **`output`**: Path to the generated migration `.py` file
|
|
59
59
|
- **`dialect`**: Database type: `sqlite`, `postgres`, `mysql`, etc. (if unclear from database uri)
|
|
60
60
|
- **`magic`**: Insert missing variables to prevent crashes (default: `true`).
|
|
61
61
|
See [pydal2sql docs](https://github.com/robinvandernoord/pydal2sql#configuration).
|
|
@@ -148,3 +148,8 @@ class SpecialTable(TypedTable):
|
|
|
148
148
|
```
|
|
149
149
|
|
|
150
150
|
**Warning:** Disabling this may break caching functionality for queries involving this table.
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
Want to explore less common but useful APIs (like old-style pyDAL `QueryBuilder`, validation/upsert helpers, and field reordering)?
|
|
155
|
+
Continue with [10. Advanced APIs](./10_advanced_apis.md).
|
|
@@ -11,6 +11,7 @@ nav:
|
|
|
11
11
|
- 7. Configuration: 7_configuration.md
|
|
12
12
|
- 8. Mixins: 8_mixins.md
|
|
13
13
|
- 9. Function Memoization: 9_memoization.md
|
|
14
|
+
- 10. Advanced APIs: 10_advanced_apis.md
|
|
14
15
|
extra:
|
|
15
16
|
version:
|
|
16
17
|
default: stable
|
|
@@ -40,4 +41,4 @@ theme:
|
|
|
40
41
|
|
|
41
42
|
extra_css:
|
|
42
43
|
- https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/styles/tokyo-night-dark.min.css
|
|
43
|
-
- css/code_blocks.css
|
|
44
|
+
- css/code_blocks.css
|
|
@@ -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")
|
|
@@ -31,7 +31,7 @@ from .types import (
|
|
|
31
31
|
|
|
32
32
|
if t.TYPE_CHECKING:
|
|
33
33
|
# will be imported for real later:
|
|
34
|
-
from .tables import TypedTable
|
|
34
|
+
from .tables import TypedTable, _TypedTable
|
|
35
35
|
|
|
36
36
|
|
|
37
37
|
## general
|
|
@@ -79,7 +79,7 @@ class TypedField(Expression, t.Generic[T_Value]): # pragma: no cover
|
|
|
79
79
|
"""
|
|
80
80
|
|
|
81
81
|
@t.overload
|
|
82
|
-
def __get__(self, instance: None, owner: "t.Type[
|
|
82
|
+
def __get__(self, instance: None, owner: "t.Type[_TypedTable]") -> "TypedField[T_Value]": # pragma: no cover
|
|
83
83
|
"""
|
|
84
84
|
Table.field -> Field.
|
|
85
85
|
"""
|
|
@@ -16,21 +16,18 @@ from slugify import slugify
|
|
|
16
16
|
|
|
17
17
|
from .core import TypeDAL
|
|
18
18
|
from .fields import DatetimeField, StringField
|
|
19
|
-
from .tables import _TypedTable
|
|
19
|
+
from .tables import TableMeta, _TypedTable
|
|
20
20
|
from .types import OpRow, Set, T_MetaInstance
|
|
21
21
|
|
|
22
|
-
if t.TYPE_CHECKING:
|
|
23
|
-
from .tables import TypedTable # noqa: F401
|
|
24
22
|
|
|
25
|
-
|
|
26
|
-
class Mixin(_TypedTable):
|
|
23
|
+
class Mixin(_TypedTable, metaclass=TableMeta):
|
|
27
24
|
"""
|
|
28
25
|
A mixin should be derived from this class.
|
|
29
26
|
|
|
30
27
|
The mixin base class itself doesn't do anything,
|
|
31
28
|
but using it makes sure the mixin fields are placed AFTER the table's normal fields (instead of before)
|
|
32
29
|
|
|
33
|
-
During runtime, mixin should not
|
|
30
|
+
During runtime, mixin should not inherit from TypedTable to prevent MRO issues
|
|
34
31
|
('inconsistent method resolution' or 'metaclass conflicts')
|
|
35
32
|
"""
|
|
36
33
|
|
|
@@ -23,7 +23,7 @@ from .helpers import (
|
|
|
23
23
|
normalize_table_keys,
|
|
24
24
|
throw,
|
|
25
25
|
)
|
|
26
|
-
from .tables import TableMeta, TypedTable
|
|
26
|
+
from .tables import TableMeta, TypedTable, _TypedTable
|
|
27
27
|
from .types import (
|
|
28
28
|
CacheMetadata,
|
|
29
29
|
Condition,
|
|
@@ -698,7 +698,7 @@ class QueryBuilder(t.Generic[T_MetaInstance]):
|
|
|
698
698
|
return joins
|
|
699
699
|
|
|
700
700
|
def _build_inner_joins_recursive(
|
|
701
|
-
self, relation: Relationship[t.Any], parent_table: t.Type[
|
|
701
|
+
self, relation: Relationship[t.Any], parent_table: t.Type[_TypedTable], key: str, parent_key: str = ""
|
|
702
702
|
) -> list[t.Any]:
|
|
703
703
|
"""Recursively build inner joins for a relationship and its nested relationships."""
|
|
704
704
|
db = self._get_db()
|
|
@@ -764,7 +764,7 @@ class QueryBuilder(t.Generic[T_MetaInstance]):
|
|
|
764
764
|
key: str,
|
|
765
765
|
select_args: list[t.Any],
|
|
766
766
|
left_joins: list[Expression],
|
|
767
|
-
parent_table: t.Type[
|
|
767
|
+
parent_table: t.Type[_TypedTable],
|
|
768
768
|
parent_key: str = "",
|
|
769
769
|
) -> list[t.Any]:
|
|
770
770
|
"""Process a single relationship for left join and field selection."""
|
|
@@ -1133,11 +1133,16 @@ class QueryBuilder(t.Generic[T_MetaInstance]):
|
|
|
1133
1133
|
|
|
1134
1134
|
Also adds paginate, since it would be a waste to select more rows than needed.
|
|
1135
1135
|
"""
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
else:
|
|
1136
|
+
row = self.paginate(page=1, limit=1, verbose=verbose).first()
|
|
1137
|
+
if not row:
|
|
1139
1138
|
return None
|
|
1140
1139
|
|
|
1140
|
+
if not isinstance(self.model, TableMeta):
|
|
1141
|
+
# old-style pydal table: keep pydal semantics and return raw Row
|
|
1142
|
+
return row
|
|
1143
|
+
|
|
1144
|
+
return self.model.from_row(row)
|
|
1145
|
+
|
|
1141
1146
|
def _first(self) -> str:
|
|
1142
1147
|
return self._paginate(page=1, limit=1)
|
|
1143
1148
|
|
|
@@ -400,11 +400,27 @@ class TypedRows(t.Collection[T_MetaInstance], Rows):
|
|
|
400
400
|
self.__dict__.update(state)
|
|
401
401
|
# db etc. set after undill by caching.py
|
|
402
402
|
|
|
403
|
+
@t.overload
|
|
403
404
|
def render(
|
|
404
405
|
self,
|
|
405
|
-
i:
|
|
406
|
+
i: None = None,
|
|
406
407
|
fields: list[Field] | None = None,
|
|
407
408
|
) -> t.Generator[T_MetaInstance, None, None]:
|
|
409
|
+
"""With no index, yield rendered rows as a generator."""
|
|
410
|
+
|
|
411
|
+
@t.overload
|
|
412
|
+
def render(
|
|
413
|
+
self,
|
|
414
|
+
i: int,
|
|
415
|
+
fields: list[Field] | None = None,
|
|
416
|
+
) -> T_MetaInstance:
|
|
417
|
+
"""With an index, return one rendered row instance."""
|
|
418
|
+
|
|
419
|
+
def render(
|
|
420
|
+
self,
|
|
421
|
+
i: int | None = None,
|
|
422
|
+
fields: list[Field] | None = None,
|
|
423
|
+
) -> t.Generator[T_MetaInstance, None, None] | T_MetaInstance:
|
|
408
424
|
"""
|
|
409
425
|
Takes an index and returns a copy of the indexed row with values \
|
|
410
426
|
transformed via the "represent" attributes of the associated fields.
|
|
@@ -650,7 +650,7 @@ class TableMeta(type):
|
|
|
650
650
|
return reorder_fields(cls._table, fields, keep_others=keep_others)
|
|
651
651
|
|
|
652
652
|
|
|
653
|
-
class _TypedTable:
|
|
653
|
+
class _TypedTable(metaclass=TableMeta):
|
|
654
654
|
"""
|
|
655
655
|
This class is a final shared parent between TypedTable and Mixins.
|
|
656
656
|
|
|
@@ -661,6 +661,9 @@ class _TypedTable:
|
|
|
661
661
|
-> Setting 'TypedTable' as the parent for Mixin does not work at runtime (and works semi at type check time)
|
|
662
662
|
"""
|
|
663
663
|
|
|
664
|
+
# This class contains weird typing glue to dodge MRO headaches without losing editor/mypy table methods
|
|
665
|
+
# you can safely ignore it when changing runtime behavior; touch it only for typing/mypy issues
|
|
666
|
+
|
|
664
667
|
id: "TypedField[int]"
|
|
665
668
|
|
|
666
669
|
_before_insert: list[t.Callable[[t.Self], t.Optional[bool]] | t.Callable[[OpRow], t.Optional[bool]]]
|
|
@@ -671,6 +674,8 @@ class _TypedTable:
|
|
|
671
674
|
_after_update: list[t.Callable[[Set, t.Self], t.Optional[bool]] | t.Callable[[Set, OpRow], t.Optional[bool]]]
|
|
672
675
|
_before_delete: list[t.Callable[[Set], t.Optional[bool]]]
|
|
673
676
|
_after_delete: list[t.Callable[[Set], t.Optional[bool]]]
|
|
677
|
+
_rows: tuple[Row, ...]
|
|
678
|
+
_with: list[str]
|
|
674
679
|
|
|
675
680
|
@classmethod
|
|
676
681
|
def __on_define__(cls, db: TypeDAL) -> None:
|
|
@@ -681,16 +686,49 @@ class _TypedTable:
|
|
|
681
686
|
where you need a reference to the current database, which may not exist yet when defining the model.
|
|
682
687
|
"""
|
|
683
688
|
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
689
|
+
def __new__(cls, *_args: t.Any, **_kwargs: t.Any) -> t.Self:
|
|
690
|
+
"""
|
|
691
|
+
Shared constructor signature for typing.
|
|
692
|
+
|
|
693
|
+
TypedTable provides the concrete behavior; this base only keeps static typing happy
|
|
694
|
+
for generic classmethod flows that instantiate `self(...)`.
|
|
695
|
+
"""
|
|
696
|
+
return super().__new__(cls)
|
|
697
|
+
|
|
698
|
+
# Add an abstract placeholder here only when generic code is typed against
|
|
699
|
+
# `T_MetaInstance` (bound to `_TypedTable`) and directly calls/accesses that member.
|
|
700
|
+
# If a member is only used on concrete `TypedTable` paths, it should stay on `TypedTable`.
|
|
701
|
+
def _ensure_matching_row(self) -> Row:
|
|
702
|
+
# Typed on the shared base so generic instance helpers can call into row access safely.
|
|
703
|
+
raise NotImplementedError # pragma: no cover
|
|
704
|
+
|
|
705
|
+
def _update(self: t.Self, **fields: t.Any) -> t.Self:
|
|
706
|
+
# Declared here for generic update flows; real behavior is implemented in TypedTable.
|
|
707
|
+
raise NotImplementedError # pragma: no cover
|
|
708
|
+
|
|
709
|
+
def _update_record(self: t.Self, **fields: t.Any) -> t.Self:
|
|
710
|
+
# Declared here for generic update flows; real behavior is implemented in TypedTable.
|
|
711
|
+
raise NotImplementedError # pragma: no cover
|
|
712
|
+
|
|
713
|
+
def update_record(self: t.Self, **fields: t.Any) -> t.Self:
|
|
714
|
+
# Declared here for generic update flows; real behavior is implemented in TypedTable.
|
|
715
|
+
raise NotImplementedError # pragma: no cover
|
|
716
|
+
|
|
717
|
+
def as_dict(self, *args: t.Any, **kwargs: t.Any) -> AnyDict:
|
|
718
|
+
# Broad signature keeps class/instance serialization overrides LSP-compatible.
|
|
719
|
+
raise NotImplementedError # pragma: no cover
|
|
720
|
+
|
|
721
|
+
def render(self: t.Self, *fields: t.Any, **kwargs: t.Any) -> t.Self:
|
|
722
|
+
# Rows/QueryBuilder treat render as model-preserving, so this returns Self for typing.
|
|
723
|
+
raise NotImplementedError # pragma: no cover
|
|
724
|
+
|
|
725
|
+
def __getitem__(self, key: str) -> t.Any:
|
|
726
|
+
# Relationship collection writes into model instances via dict-style access.
|
|
727
|
+
raise NotImplementedError # pragma: no cover
|
|
728
|
+
|
|
729
|
+
def __setitem__(self, key: str, value: t.Any) -> None:
|
|
730
|
+
# Relationship collection writes into model instances via dict-style access.
|
|
731
|
+
raise NotImplementedError # pragma: no cover
|
|
694
732
|
|
|
695
733
|
|
|
696
734
|
class TypedTable(_TypedTable, metaclass=TableMeta):
|
|
@@ -702,8 +740,21 @@ class TypedTable(_TypedTable, metaclass=TableMeta):
|
|
|
702
740
|
_row: Row | None = None
|
|
703
741
|
_rows: tuple[Row, ...] = ()
|
|
704
742
|
|
|
743
|
+
id: "TypedField[int]"
|
|
744
|
+
|
|
705
745
|
_with: list[str]
|
|
706
746
|
|
|
747
|
+
@classproperty
|
|
748
|
+
def _hooks(cls) -> dict[str, list[t.Callable[..., t.Optional[bool]]]]:
|
|
749
|
+
return {
|
|
750
|
+
"before_insert": cls._before_insert,
|
|
751
|
+
"after_insert": cls._after_insert,
|
|
752
|
+
"before_update": cls._before_update,
|
|
753
|
+
"after_update": cls._after_update,
|
|
754
|
+
"before_delete": cls._before_delete,
|
|
755
|
+
"after_delete": cls._after_delete,
|
|
756
|
+
}
|
|
757
|
+
|
|
707
758
|
def _setup_instance_methods(self) -> None:
|
|
708
759
|
self.as_dict = self._as_dict # type: ignore
|
|
709
760
|
self.__json__ = self.as_json = self._as_json # type: ignore
|
|
@@ -990,7 +1041,7 @@ class TypedTable(_TypedTable, metaclass=TableMeta):
|
|
|
990
1041
|
def _update_record(self: T_MetaInstance, **fields: t.Any) -> T_MetaInstance:
|
|
991
1042
|
row = self._ensure_matching_row()
|
|
992
1043
|
new_row = row.update_record(**fields)
|
|
993
|
-
self.
|
|
1044
|
+
self._update(**new_row)
|
|
994
1045
|
return self
|
|
995
1046
|
|
|
996
1047
|
def update_record(self: T_MetaInstance, **fields: t.Any) -> T_MetaInstance: # pragma: no cover
|
|
@@ -35,7 +35,7 @@ except ImportError:
|
|
|
35
35
|
# Internal references
|
|
36
36
|
if t.TYPE_CHECKING:
|
|
37
37
|
from .fields import TypedField
|
|
38
|
-
from .tables import TypedTable
|
|
38
|
+
from .tables import TypedTable, _TypedTable
|
|
39
39
|
|
|
40
40
|
# ---------------------------------------------------------------------------
|
|
41
41
|
# Aliases
|
|
@@ -297,7 +297,7 @@ T = t.TypeVar("T", bound=t.Any)
|
|
|
297
297
|
P = t.ParamSpec("P")
|
|
298
298
|
R = t.TypeVar("R")
|
|
299
299
|
|
|
300
|
-
T_MetaInstance = t.TypeVar("T_MetaInstance", bound="
|
|
300
|
+
T_MetaInstance = t.TypeVar("T_MetaInstance", bound="_TypedTable")
|
|
301
301
|
T_Query = t.Union[
|
|
302
302
|
"Table",
|
|
303
303
|
Query,
|
|
@@ -10,6 +10,7 @@ from typedal import ( # todo: why does src.typedal not work anymore?
|
|
|
10
10
|
TypedRows,
|
|
11
11
|
TypedTable,
|
|
12
12
|
)
|
|
13
|
+
from typedal.mixins import Mixin
|
|
13
14
|
from typedal.types import CacheFn, CacheTuple, OpRow, Reference, Rows
|
|
14
15
|
|
|
15
16
|
db = TypeDAL("sqlite:memory")
|
|
@@ -29,6 +30,14 @@ class OtherTable(TypedTable): ...
|
|
|
29
30
|
class LaterDefine(TypedTable): ...
|
|
30
31
|
|
|
31
32
|
|
|
33
|
+
class SearchMixin(Mixin): ...
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@db.define
|
|
37
|
+
class SearchableTable(TypedTable, SearchMixin):
|
|
38
|
+
title: str
|
|
39
|
+
|
|
40
|
+
|
|
32
41
|
old_style = db.define_table("old_table")
|
|
33
42
|
|
|
34
43
|
|
|
@@ -152,6 +161,21 @@ def mypy_test_query() -> None:
|
|
|
152
161
|
reveal_type(MyTable.where().column(MyTable.fancy)) # R: builtins.list[builtins.str]
|
|
153
162
|
|
|
154
163
|
|
|
164
|
+
@pytest.mark.mypy_testing
|
|
165
|
+
def mypy_test_rows_render_overload() -> None:
|
|
166
|
+
rows = MyTable.where().collect()
|
|
167
|
+
reveal_type(rows.render()) # R: typing.Generator[tests.test_mypy.MyTable, None, None]
|
|
168
|
+
reveal_type(rows.render(1)) # R: tests.test_mypy.MyTable
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
@pytest.mark.mypy_testing
|
|
172
|
+
def mypy_test_mixin_typed_table_argument() -> None:
|
|
173
|
+
def using_mixin(table: type[SearchMixin]) -> None:
|
|
174
|
+
reveal_type(table.where()) # R: typedal.query_builder.QueryBuilder[tests.test_mypy.SearchMixin]
|
|
175
|
+
|
|
176
|
+
using_mixin(SearchableTable)
|
|
177
|
+
|
|
178
|
+
|
|
155
179
|
@pytest.mark.mypy_testing
|
|
156
180
|
def mypy_test_cachefn() -> None:
|
|
157
181
|
def cache_model(key: str, fn: CacheFn, expire: int) -> Rows:
|
|
@@ -526,6 +526,14 @@ def test_minimal_functionality_on_pydal_style_tables():
|
|
|
526
526
|
assert qb2
|
|
527
527
|
assert len(qb2) == 1
|
|
528
528
|
|
|
529
|
+
first = QueryBuilder(db.test_query_table).where(number=2).first()
|
|
530
|
+
assert first
|
|
531
|
+
assert first.id == qb1.first().id
|
|
532
|
+
|
|
533
|
+
first_or_fail = QueryBuilder(db.test_query_table).where(number=2).first_or_fail()
|
|
534
|
+
assert first_or_fail
|
|
535
|
+
assert first_or_fail.id == qb1.first().id
|
|
536
|
+
|
|
529
537
|
|
|
530
538
|
def test_before_after_collect(capsys):
|
|
531
539
|
_setup_data()
|
typedal-4.4.5/.crush/.gitignore
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
*
|
|
Binary file
|
|
Binary file
|
typedal-4.4.5/.crush/init
DELETED
|
File without changes
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
{"time":"2026-01-20T14:19:21.359487106+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-01-20T14:19:21.518115889+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-01-20T14:19:21.52626179+01:00","level":"INFO","msg":"OK 20250424200609_initial.sql (1.52ms)"}
|
|
4
|
-
{"time":"2026-01-20T14:19:21.526651475+01:00","level":"INFO","msg":"OK 20250515105448_add_summary_message_id.sql (330.71µs)"}
|
|
5
|
-
{"time":"2026-01-20T14:19:21.526995425+01:00","level":"INFO","msg":"OK 20250624000000_add_created_at_indexes.sql (325.29µs)"}
|
|
6
|
-
{"time":"2026-01-20T14:19:21.527303588+01:00","level":"INFO","msg":"OK 20250627000000_add_provider_to_messages.sql (293.15µs)"}
|
|
7
|
-
{"time":"2026-01-20T14:19:21.52790648+01:00","level":"INFO","msg":"OK 20250810000000_add_is_summary_message.sql (495.71µs)"}
|
|
8
|
-
{"time":"2026-01-20T14:19:21.528316924+01:00","level":"INFO","msg":"OK 20250812000000_add_todos_to_sessions.sql (389.38µs)"}
|
|
9
|
-
{"time":"2026-01-20T14:19:21.528324939+01:00","level":"INFO","msg":"goose: successfully migrated database to version: 20250812000000"}
|
|
10
|
-
{"time":"2026-01-20T14:19:21.528356959+01:00","level":"INFO","source":{"function":"github.com/charmbracelet/crush/internal/app.(*App).initLSPClients","file":"github.com/charmbracelet/crush/internal/app/lsp.go","line":21},"msg":"LSP clients initialization started in background"}
|
|
11
|
-
{"time":"2026-01-20T14:19:21.52843831+01:00","level":"INFO","source":{"function":"github.com/charmbracelet/crush/internal/app.New.func1","file":"github.com/charmbracelet/crush/internal/app/app.go","line":102},"msg":"Initializing MCP clients"}
|
|
12
|
-
{"time":"2026-01-20T14:21:06.413595152+01:00","level":"INFO","source":{"function":"github.com/charmbracelet/crush/internal/agent.(*sessionAgent).generateTitle","file":"github.com/charmbracelet/crush/internal/agent/agent.go","line":788},"msg":"generated title with small model"}
|
|
13
|
-
{"time":"2026-01-20T14:21:14.539289381+01:00","level":"INFO","source":{"function":"github.com/charmbracelet/crush/internal/agent.(*sessionAgent).generateTitle","file":"github.com/charmbracelet/crush/internal/agent/agent.go","line":788},"msg":"generated title with small model"}
|
|
14
|
-
{"time":"2026-01-20T14:21:15.710688424+01:00","level":"WARN","source":{"function":"github.com/charmbracelet/crush/internal/agent/tools.init.func1","file":"github.com/charmbracelet/crush/internal/agent/tools/rg.go","line":18},"msg":"Ripgrep (rg) not found in $PATH. Some grep features might be limited or slower."}
|
|
15
|
-
{"time":"2026-01-20T14:21:17.08089755+01:00","level":"INFO","source":{"function":"github.com/charmbracelet/crush/internal/agent.(*sessionAgent).generateTitle","file":"github.com/charmbracelet/crush/internal/agent/agent.go","line":788},"msg":"generated title with small model"}
|
|
16
|
-
{"time":"2026-01-20T14:21:17.151106375+01:00","level":"INFO","source":{"function":"github.com/charmbracelet/crush/internal/agent.(*sessionAgent).generateTitle","file":"github.com/charmbracelet/crush/internal/agent/agent.go","line":788},"msg":"generated title with small model"}
|
|
17
|
-
{"time":"2026-01-20T14:22:05.503761296+01:00","level":"INFO","source":{"function":"github.com/charmbracelet/crush/internal/agent.(*sessionAgent).generateTitle","file":"github.com/charmbracelet/crush/internal/agent/agent.go","line":788},"msg":"generated title with small model"}
|
|
18
|
-
{"time":"2026-01-20T14:26:05.030425805+01:00","level":"INFO","source":{"function":"github.com/charmbracelet/crush/internal/agent.(*sessionAgent).generateTitle","file":"github.com/charmbracelet/crush/internal/agent/agent.go","line":788},"msg":"generated title with small model"}
|
|
19
|
-
{"time":"2026-01-20T14:36:26.778665189+01:00","level":"INFO","source":{"function":"github.com/charmbracelet/crush/internal/agent.(*sessionAgent).generateTitle","file":"github.com/charmbracelet/crush/internal/agent/agent.go","line":788},"msg":"generated title with small model"}
|
|
20
|
-
{"time":"2026-01-20T14:38:51.314473736+01:00","level":"INFO","source":{"function":"github.com/charmbracelet/crush/internal/agent.(*sessionAgent).generateTitle","file":"github.com/charmbracelet/crush/internal/agent/agent.go","line":788},"msg":"generated title with small model"}
|
|
21
|
-
{"time":"2026-01-20T15:02:22.602405814+01:00","level":"INFO","source":{"function":"github.com/charmbracelet/crush/internal/app.(*App).Shutdown.func1","file":"github.com/charmbracelet/crush/internal/app/app.go","line":403},"msg":"Shutdown took 6.62949ms"}
|
|
22
|
-
{"time":"2026-01-20T15:02:23.719585648+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"}
|
|
23
|
-
{"time":"2026-01-20T15:02:23.881789126+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"}
|
|
24
|
-
{"time":"2026-01-20T15:02:23.884044691+01:00","level":"INFO","msg":"goose: no migrations to run. current version: 20250812000000"}
|
|
25
|
-
{"time":"2026-01-20T15:02:23.884082792+01:00","level":"INFO","source":{"function":"github.com/charmbracelet/crush/internal/app.(*App).initLSPClients","file":"github.com/charmbracelet/crush/internal/app/lsp.go","line":21},"msg":"LSP clients initialization started in background"}
|
|
26
|
-
{"time":"2026-01-20T15:02:23.884273487+01:00","level":"INFO","source":{"function":"github.com/charmbracelet/crush/internal/app.New.func1","file":"github.com/charmbracelet/crush/internal/app/app.go","line":102},"msg":"Initializing MCP clients"}
|
|
27
|
-
{"time":"2026-01-20T15:04:13.024457515+01:00","level":"INFO","source":{"function":"github.com/charmbracelet/crush/internal/agent.(*sessionAgent).generateTitle","file":"github.com/charmbracelet/crush/internal/agent/agent.go","line":788},"msg":"generated title with small model"}
|
|
28
|
-
{"time":"2026-01-20T15:04:14.232072885+01:00","level":"WARN","source":{"function":"github.com/charmbracelet/crush/internal/agent/tools.init.func1","file":"github.com/charmbracelet/crush/internal/agent/tools/rg.go","line":18},"msg":"Ripgrep (rg) not found in $PATH. Some grep features might be limited or slower."}
|
|
29
|
-
{"time":"2026-01-20T15:11:24.612161916+01:00","level":"INFO","source":{"function":"github.com/charmbracelet/crush/internal/app.(*App).Shutdown.func1","file":"github.com/charmbracelet/crush/internal/app/app.go","line":403},"msg":"Shutdown took 9.537141ms"}
|
|
30
|
-
{"time":"2026-01-20T15:11:25.263960575+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"}
|
|
31
|
-
{"time":"2026-01-20T15:11:25.431990051+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"}
|
|
32
|
-
{"time":"2026-01-20T15:11:25.434282035+01:00","level":"INFO","msg":"goose: no migrations to run. current version: 20250812000000"}
|
|
33
|
-
{"time":"2026-01-20T15:11:25.434366662+01:00","level":"INFO","source":{"function":"github.com/charmbracelet/crush/internal/app.(*App).initLSPClients","file":"github.com/charmbracelet/crush/internal/app/lsp.go","line":21},"msg":"LSP clients initialization started in background"}
|
|
34
|
-
{"time":"2026-01-20T15:11:25.434579879+01:00","level":"INFO","source":{"function":"github.com/charmbracelet/crush/internal/app.New.func1","file":"github.com/charmbracelet/crush/internal/app/app.go","line":102},"msg":"Initializing MCP clients"}
|
|
35
|
-
{"time":"2026-01-20T15:24:40.456010578+01:00","level":"INFO","source":{"function":"github.com/charmbracelet/crush/internal/agent.(*sessionAgent).generateTitle","file":"github.com/charmbracelet/crush/internal/agent/agent.go","line":788},"msg":"generated title with small model"}
|
|
36
|
-
{"time":"2026-01-20T15:24:51.673808433+01:00","level":"WARN","source":{"function":"github.com/charmbracelet/crush/internal/agent/tools.init.func1","file":"github.com/charmbracelet/crush/internal/agent/tools/rg.go","line":18},"msg":"Ripgrep (rg) not found in $PATH. Some grep features might be limited or slower."}
|
|
37
|
-
{"time":"2026-01-20T15:26:03.868420566+01:00","level":"INFO","source":{"function":"github.com/charmbracelet/crush/internal/agent.(*sessionAgent).Cancel","file":"github.com/charmbracelet/crush/internal/agent/agent.go","line":907},"msg":"Request cancellation initiated","session_id":"4c2d1684-8db2-4ffd-83bb-9c08fe850f29"}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|