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.
Files changed (74) hide show
  1. {typedal-4.4.5 → typedal-4.5.0}/CHANGELOG.md +16 -0
  2. {typedal-4.4.5 → typedal-4.5.0}/PKG-INFO +44 -11
  3. {typedal-4.4.5 → typedal-4.5.0}/README.md +43 -10
  4. typedal-4.5.0/docs/10_advanced_apis.md +76 -0
  5. {typedal-4.4.5 → typedal-4.5.0}/docs/2_defining_tables.md +2 -2
  6. {typedal-4.4.5 → typedal-4.5.0}/docs/3_building_queries.md +8 -3
  7. {typedal-4.4.5 → typedal-4.5.0}/docs/4_relationships.md +24 -1
  8. {typedal-4.4.5 → typedal-4.5.0}/docs/7_configuration.md +1 -1
  9. {typedal-4.4.5 → typedal-4.5.0}/docs/9_memoization.md +5 -0
  10. {typedal-4.4.5 → typedal-4.5.0}/docs/index.md +1 -0
  11. {typedal-4.4.5 → typedal-4.5.0}/mkdocs.yml +2 -1
  12. {typedal-4.4.5 → typedal-4.5.0}/src/typedal/__about__.py +1 -1
  13. {typedal-4.4.5 → typedal-4.5.0}/src/typedal/cli.py +2 -1
  14. {typedal-4.4.5 → typedal-4.5.0}/src/typedal/fields.py +2 -2
  15. {typedal-4.4.5 → typedal-4.5.0}/src/typedal/mixins.py +3 -6
  16. {typedal-4.4.5 → typedal-4.5.0}/src/typedal/query_builder.py +11 -6
  17. {typedal-4.4.5 → typedal-4.5.0}/src/typedal/relationships.py +1 -1
  18. {typedal-4.4.5 → typedal-4.5.0}/src/typedal/rows.py +17 -1
  19. {typedal-4.4.5 → typedal-4.5.0}/src/typedal/tables.py +63 -12
  20. {typedal-4.4.5 → typedal-4.5.0}/src/typedal/types.py +2 -2
  21. {typedal-4.4.5 → typedal-4.5.0}/tests/test_mypy.py +24 -0
  22. {typedal-4.4.5 → typedal-4.5.0}/tests/test_query_builder.py +8 -0
  23. typedal-4.4.5/.crush/.gitignore +0 -1
  24. typedal-4.4.5/.crush/crush.db-shm +0 -0
  25. typedal-4.4.5/.crush/crush.db-wal +0 -0
  26. typedal-4.4.5/.crush/init +0 -0
  27. typedal-4.4.5/.crush/logs/crush.log +0 -37
  28. {typedal-4.4.5 → typedal-4.5.0}/.github/workflows/su6.yml +0 -0
  29. {typedal-4.4.5 → typedal-4.5.0}/.gitignore +0 -0
  30. {typedal-4.4.5 → typedal-4.5.0}/.readthedocs.yml +0 -0
  31. {typedal-4.4.5 → typedal-4.5.0}/coverage.svg +0 -0
  32. {typedal-4.4.5 → typedal-4.5.0}/docs/1_getting_started.md +0 -0
  33. {typedal-4.4.5 → typedal-4.5.0}/docs/5_py4web.md +0 -0
  34. {typedal-4.4.5 → typedal-4.5.0}/docs/6_migrations.md +0 -0
  35. {typedal-4.4.5 → typedal-4.5.0}/docs/8_mixins.md +0 -0
  36. {typedal-4.4.5 → typedal-4.5.0}/docs/css/code_blocks.css +0 -0
  37. {typedal-4.4.5 → typedal-4.5.0}/docs/requirements.txt +0 -0
  38. {typedal-4.4.5 → typedal-4.5.0}/example_new.py +0 -0
  39. {typedal-4.4.5 → typedal-4.5.0}/example_old.py +0 -0
  40. {typedal-4.4.5 → typedal-4.5.0}/pyproject.toml +0 -0
  41. {typedal-4.4.5 → typedal-4.5.0}/src/typedal/__init__.py +0 -0
  42. {typedal-4.4.5 → typedal-4.5.0}/src/typedal/caching.py +0 -0
  43. {typedal-4.4.5 → typedal-4.5.0}/src/typedal/config.py +0 -0
  44. {typedal-4.4.5 → typedal-4.5.0}/src/typedal/constants.py +0 -0
  45. {typedal-4.4.5 → typedal-4.5.0}/src/typedal/core.py +0 -0
  46. {typedal-4.4.5 → typedal-4.5.0}/src/typedal/define.py +0 -0
  47. {typedal-4.4.5 → typedal-4.5.0}/src/typedal/for_py4web.py +0 -0
  48. {typedal-4.4.5 → typedal-4.5.0}/src/typedal/for_web2py.py +0 -0
  49. {typedal-4.4.5 → typedal-4.5.0}/src/typedal/helpers.py +0 -0
  50. {typedal-4.4.5 → typedal-4.5.0}/src/typedal/py.typed +0 -0
  51. {typedal-4.4.5 → typedal-4.5.0}/src/typedal/serializers/as_json.py +0 -0
  52. {typedal-4.4.5 → typedal-4.5.0}/src/typedal/web2py_py4web_shared.py +0 -0
  53. {typedal-4.4.5 → typedal-4.5.0}/tasks.py +0 -0
  54. {typedal-4.4.5 → typedal-4.5.0}/tests/__init__.py +0 -0
  55. {typedal-4.4.5 → typedal-4.5.0}/tests/configs/simple.toml +0 -0
  56. {typedal-4.4.5 → typedal-4.5.0}/tests/configs/valid.env +0 -0
  57. {typedal-4.4.5 → typedal-4.5.0}/tests/configs/valid.toml +0 -0
  58. {typedal-4.4.5 → typedal-4.5.0}/tests/py314_tests.py +0 -0
  59. {typedal-4.4.5 → typedal-4.5.0}/tests/test_cli.py +0 -0
  60. {typedal-4.4.5 → typedal-4.5.0}/tests/test_config.py +0 -0
  61. {typedal-4.4.5 → typedal-4.5.0}/tests/test_docs_examples.py +0 -0
  62. {typedal-4.4.5 → typedal-4.5.0}/tests/test_helpers.py +0 -0
  63. {typedal-4.4.5 → typedal-4.5.0}/tests/test_json.py +0 -0
  64. {typedal-4.4.5 → typedal-4.5.0}/tests/test_main.py +0 -0
  65. {typedal-4.4.5 → typedal-4.5.0}/tests/test_mixins.py +0 -0
  66. {typedal-4.4.5 → typedal-4.5.0}/tests/test_orm.py +0 -0
  67. {typedal-4.4.5 → typedal-4.5.0}/tests/test_py4web.py +0 -0
  68. {typedal-4.4.5 → typedal-4.5.0}/tests/test_relationships.py +0 -0
  69. {typedal-4.4.5 → typedal-4.5.0}/tests/test_row.py +0 -0
  70. {typedal-4.4.5 → typedal-4.5.0}/tests/test_stats.py +0 -0
  71. {typedal-4.4.5 → typedal-4.5.0}/tests/test_table.py +0 -0
  72. {typedal-4.4.5 → typedal-4.5.0}/tests/test_web2py.py +0 -0
  73. {typedal-4.4.5 → typedal-4.5.0}/tests/test_xx_others.py +0 -0
  74. {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.4.5
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 funcionality.
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 Typedal CLI provides a convenient interface for generating SQL migrations for [edwh-migrate](https://github.com/educationwarehouse/migrate#readme)
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 interchangable here
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
- - This package depends heavily on the current implementation of annotations (which are computed when the class is
334
- defined). PEP 563 (Postponed Evaluation of Annotations, accepted) aims to change this behavior (
335
- and `from __future__ import annotations` already does) in a way that this module currently can not handle: all
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 for the first caveat.
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 funcionality.
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 Typedal CLI provides a convenient interface for generating SQL migrations for [edwh-migrate](https://github.com/educationwarehouse/migrate#readme)
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 interchangable here
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
- - This package depends heavily on the current implementation of annotations (which are computed when the class is
277
- defined). PEP 563 (Postponed Evaluation of Annotations, accepted) aims to change this behavior (
278
- and `from __future__ import annotations` already does) in a way that this module currently can not handle: all
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 for the first caveat.
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)` | `name = UploadField()` |
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
- "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.
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 [Delete](#delete))
240
- - delete: instead of selecting rows, delete those matching the current query (see [Update](#update))
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.update_record(name="New Name")
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` fil
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).
@@ -9,3 +9,4 @@
9
9
  7. [Advanced Configuration](./7_configuration.md)
10
10
  8. [Mixins](./8_mixins.md)
11
11
  9. [Function Memoization](./9_memoization.md)
12
+ 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
@@ -5,4 +5,4 @@ This file contains the Version info for this package.
5
5
  # SPDX-FileCopyrightText: 2023-present Robin van der Noord <robinvandernoord@gmail.com>
6
6
  #
7
7
  # SPDX-License-Identifier: MIT
8
- __version__ = "4.4.5"
8
+ __version__ = "4.5.0"
@@ -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) & (db.ewh_implemented_features.installed == True) # noqa E712
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[TypedTable]") -> "TypedField[T_Value]": # pragma: no cover
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 have a base class in order to prevent MRO issues
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[TypedTable], key: str, parent_key: str = ""
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[TypedTable],
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
- if row := self.paginate(page=1, limit=1, verbose=verbose).first():
1137
- return self.model.from_row(row)
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
 
@@ -184,7 +184,7 @@ class Relationship(t.Generic[To_Type]):
184
184
  return db._config.lazy_policy
185
185
 
186
186
  # conservative fallback:
187
- return "warn"
187
+ return "warn" # pragma: no cover
188
188
 
189
189
  def get_table_name(self) -> str:
190
190
  """
@@ -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: int | None = None,
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
- @classproperty
685
- def _hooks(cls) -> dict[str, list[t.Callable[..., t.Optional[bool]]]]:
686
- return {
687
- "before_insert": cls._before_insert,
688
- "after_insert": cls._after_insert,
689
- "before_update": cls._before_update,
690
- "after_update": cls._after_update,
691
- "before_delete": cls._before_delete,
692
- "after_delete": cls._after_delete,
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.update(**new_row)
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="TypedTable")
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()
@@ -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