TypeDAL 4.4.0__tar.gz → 4.4.1__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.0 → typedal-4.4.1}/.crush/crush.db-shm +0 -0
- typedal-4.4.1/.crush/crush.db-wal +0 -0
- typedal-4.4.1/.crush/logs/crush.log +25 -0
- {typedal-4.4.0 → typedal-4.4.1}/CHANGELOG.md +10 -0
- {typedal-4.4.0 → typedal-4.4.1}/PKG-INFO +1 -1
- {typedal-4.4.0 → typedal-4.4.1}/docs/3_building_queries.md +2 -0
- {typedal-4.4.0 → typedal-4.4.1}/docs/8_mixins.md +4 -0
- typedal-4.4.1/docs/9_memoization.md +150 -0
- {typedal-4.4.0 → typedal-4.4.1}/docs/index.md +1 -0
- {typedal-4.4.0 → typedal-4.4.1}/src/typedal/__about__.py +1 -1
- {typedal-4.4.0 → typedal-4.4.1}/src/typedal/caching.py +30 -1
- {typedal-4.4.0 → typedal-4.4.1}/src/typedal/cli.py +1 -2
- {typedal-4.4.0 → typedal-4.4.1}/src/typedal/core.py +5 -1
- {typedal-4.4.0 → typedal-4.4.1}/src/typedal/query_builder.py +9 -1
- {typedal-4.4.0 → typedal-4.4.1}/tests/test_query_builder.py +59 -25
- {typedal-4.4.0 → typedal-4.4.1}/tests/test_relationships.py +45 -0
- typedal-4.4.0/.crush/crush.db-wal +0 -0
- typedal-4.4.0/.crush/logs/crush.log +0 -37
- {typedal-4.4.0 → typedal-4.4.1}/.crush/.gitignore +0 -0
- {typedal-4.4.0 → typedal-4.4.1}/.crush/init +0 -0
- {typedal-4.4.0 → typedal-4.4.1}/.github/workflows/su6.yml +0 -0
- {typedal-4.4.0 → typedal-4.4.1}/.gitignore +0 -0
- {typedal-4.4.0 → typedal-4.4.1}/.readthedocs.yml +0 -0
- {typedal-4.4.0 → typedal-4.4.1}/README.md +0 -0
- {typedal-4.4.0 → typedal-4.4.1}/coverage.svg +0 -0
- {typedal-4.4.0 → typedal-4.4.1}/docs/1_getting_started.md +0 -0
- {typedal-4.4.0 → typedal-4.4.1}/docs/2_defining_tables.md +0 -0
- {typedal-4.4.0 → typedal-4.4.1}/docs/4_relationships.md +0 -0
- {typedal-4.4.0 → typedal-4.4.1}/docs/5_py4web.md +0 -0
- {typedal-4.4.0 → typedal-4.4.1}/docs/6_migrations.md +0 -0
- {typedal-4.4.0 → typedal-4.4.1}/docs/7_configuration.md +0 -0
- {typedal-4.4.0 → typedal-4.4.1}/docs/css/code_blocks.css +0 -0
- {typedal-4.4.0 → typedal-4.4.1}/docs/requirements.txt +0 -0
- {typedal-4.4.0 → typedal-4.4.1}/example_new.py +0 -0
- {typedal-4.4.0 → typedal-4.4.1}/example_old.py +0 -0
- {typedal-4.4.0 → typedal-4.4.1}/mkdocs.yml +0 -0
- {typedal-4.4.0 → typedal-4.4.1}/pyproject.toml +0 -0
- {typedal-4.4.0 → typedal-4.4.1}/src/typedal/__init__.py +0 -0
- {typedal-4.4.0 → typedal-4.4.1}/src/typedal/config.py +0 -0
- {typedal-4.4.0 → typedal-4.4.1}/src/typedal/constants.py +0 -0
- {typedal-4.4.0 → typedal-4.4.1}/src/typedal/define.py +0 -0
- {typedal-4.4.0 → typedal-4.4.1}/src/typedal/fields.py +0 -0
- {typedal-4.4.0 → typedal-4.4.1}/src/typedal/for_py4web.py +0 -0
- {typedal-4.4.0 → typedal-4.4.1}/src/typedal/for_web2py.py +0 -0
- {typedal-4.4.0 → typedal-4.4.1}/src/typedal/helpers.py +0 -0
- {typedal-4.4.0 → typedal-4.4.1}/src/typedal/mixins.py +0 -0
- {typedal-4.4.0 → typedal-4.4.1}/src/typedal/py.typed +0 -0
- {typedal-4.4.0 → typedal-4.4.1}/src/typedal/relationships.py +0 -0
- {typedal-4.4.0 → typedal-4.4.1}/src/typedal/rows.py +0 -0
- {typedal-4.4.0 → typedal-4.4.1}/src/typedal/serializers/as_json.py +0 -0
- {typedal-4.4.0 → typedal-4.4.1}/src/typedal/tables.py +0 -0
- {typedal-4.4.0 → typedal-4.4.1}/src/typedal/types.py +0 -0
- {typedal-4.4.0 → typedal-4.4.1}/src/typedal/web2py_py4web_shared.py +0 -0
- {typedal-4.4.0 → typedal-4.4.1}/tests/__init__.py +0 -0
- {typedal-4.4.0 → typedal-4.4.1}/tests/configs/simple.toml +0 -0
- {typedal-4.4.0 → typedal-4.4.1}/tests/configs/valid.env +0 -0
- {typedal-4.4.0 → typedal-4.4.1}/tests/configs/valid.toml +0 -0
- {typedal-4.4.0 → typedal-4.4.1}/tests/py314_tests.py +0 -0
- {typedal-4.4.0 → typedal-4.4.1}/tests/test_cli.py +0 -0
- {typedal-4.4.0 → typedal-4.4.1}/tests/test_config.py +0 -0
- {typedal-4.4.0 → typedal-4.4.1}/tests/test_docs_examples.py +0 -0
- {typedal-4.4.0 → typedal-4.4.1}/tests/test_helpers.py +0 -0
- {typedal-4.4.0 → typedal-4.4.1}/tests/test_json.py +0 -0
- {typedal-4.4.0 → typedal-4.4.1}/tests/test_main.py +0 -0
- {typedal-4.4.0 → typedal-4.4.1}/tests/test_mixins.py +0 -0
- {typedal-4.4.0 → typedal-4.4.1}/tests/test_mypy.py +0 -0
- {typedal-4.4.0 → typedal-4.4.1}/tests/test_orm.py +0 -0
- {typedal-4.4.0 → typedal-4.4.1}/tests/test_py4web.py +0 -0
- {typedal-4.4.0 → typedal-4.4.1}/tests/test_row.py +0 -0
- {typedal-4.4.0 → typedal-4.4.1}/tests/test_stats.py +0 -0
- {typedal-4.4.0 → typedal-4.4.1}/tests/test_table.py +0 -0
- {typedal-4.4.0 → typedal-4.4.1}/tests/test_web2py.py +0 -0
- {typedal-4.4.0 → typedal-4.4.1}/tests/test_xx_others.py +0 -0
- {typedal-4.4.0 → typedal-4.4.1}/tests/timings.py +0 -0
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{"time":"2026-01-26T17:01:48.91963488+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-26T17:01:49.11634171+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-26T17:01:49.116589785+01:00","level":"WARN","source":{"function":"github.com/charmbracelet/crush/internal/config.(*Config).configureProviders","file":"github.com/charmbracelet/crush/internal/config/load.go","line":259},"msg":"Skipping provider due to missing API key","provider":"anthropic"}
|
|
4
|
+
{"time":"2026-01-26T17:01:49.128053484+01:00","level":"INFO","msg":"OK 20250424200609_initial.sql (1.26ms)"}
|
|
5
|
+
{"time":"2026-01-26T17:01:49.128338994+01:00","level":"INFO","msg":"OK 20250515105448_add_summary_message_id.sql (263.8µs)"}
|
|
6
|
+
{"time":"2026-01-26T17:01:49.128678839+01:00","level":"INFO","msg":"OK 20250624000000_add_created_at_indexes.sql (325.2µs)"}
|
|
7
|
+
{"time":"2026-01-26T17:01:49.128984204+01:00","level":"INFO","msg":"OK 20250627000000_add_provider_to_messages.sql (291.73µs)"}
|
|
8
|
+
{"time":"2026-01-26T17:01:49.12929983+01:00","level":"INFO","msg":"OK 20250810000000_add_is_summary_message.sql (265.62µs)"}
|
|
9
|
+
{"time":"2026-01-26T17:01:49.129580779+01:00","level":"INFO","msg":"OK 20250812000000_add_todos_to_sessions.sql (268.09µs)"}
|
|
10
|
+
{"time":"2026-01-26T17:01:49.129585964+01:00","level":"INFO","msg":"goose: successfully migrated database to version: 20250812000000"}
|
|
11
|
+
{"time":"2026-01-26T17:01:49.129633405+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"}
|
|
12
|
+
{"time":"2026-01-26T17:01:49.129745943+01:00","level":"INFO","source":{"function":"github.com/charmbracelet/crush/internal/app.New.func1","file":"github.com/charmbracelet/crush/internal/app/app.go","line":110},"msg":"Initializing MCP clients"}
|
|
13
|
+
{"time":"2026-01-26T17:01:49.73470854+01:00","level":"INFO","source":{"function":"github.com/charmbracelet/crush/internal/app.(*App).createAndStartLSPClient","file":"github.com/charmbracelet/crush/internal/app/lsp.go","line":76},"msg":"LSP client initialized","name":"python"}
|
|
14
|
+
{"time":"2026-01-26T17:02:52.962674187+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"}
|
|
15
|
+
{"time":"2026-01-26T17:02:57.3247847+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."}
|
|
16
|
+
{"time":"2026-01-26T17:11:33.432259554+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":507},"msg":"Shutdown took 6.286852ms"}
|
|
17
|
+
{"time":"2026-01-26T17:11:33.982816329+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"}
|
|
18
|
+
{"time":"2026-01-26T17:11:34.178260248+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"}
|
|
19
|
+
{"time":"2026-01-26T17:11:34.178494988+01:00","level":"WARN","source":{"function":"github.com/charmbracelet/crush/internal/config.(*Config).configureProviders","file":"github.com/charmbracelet/crush/internal/config/load.go","line":259},"msg":"Skipping provider due to missing API key","provider":"anthropic"}
|
|
20
|
+
{"time":"2026-01-26T17:11:34.18216688+01:00","level":"INFO","msg":"goose: no migrations to run. current version: 20250812000000"}
|
|
21
|
+
{"time":"2026-01-26T17:11:34.182194111+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"}
|
|
22
|
+
{"time":"2026-01-26T17:11:34.182335616+01:00","level":"INFO","source":{"function":"github.com/charmbracelet/crush/internal/app.New.func1","file":"github.com/charmbracelet/crush/internal/app/app.go","line":110},"msg":"Initializing MCP clients"}
|
|
23
|
+
{"time":"2026-01-26T17:11:34.786142525+01:00","level":"INFO","source":{"function":"github.com/charmbracelet/crush/internal/app.(*App).createAndStartLSPClient","file":"github.com/charmbracelet/crush/internal/app/lsp.go","line":76},"msg":"LSP client initialized","name":"python"}
|
|
24
|
+
{"time":"2026-01-26T17:11:43.042930337+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"}
|
|
25
|
+
{"time":"2026-01-26T17:11:44.911909276+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."}
|
|
@@ -2,6 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
<!--next-version-placeholder-->
|
|
4
4
|
|
|
5
|
+
## v4.4.1 (2026-01-26)
|
|
6
|
+
|
|
7
|
+
### Fix
|
|
8
|
+
|
|
9
|
+
* Test and support memoize with empty table and .execute() ([`76ea39b`](https://github.com/trialandsuccess/TypeDAL/commit/76ea39bf4f8beb2ad303e66b69ad01780359ad14))
|
|
10
|
+
|
|
11
|
+
### Documentation
|
|
12
|
+
|
|
13
|
+
* Add chapter 9 about `memoize` ([`e23a8c8`](https://github.com/trialandsuccess/TypeDAL/commit/e23a8c80ee60a5adb2d32c6d05fb398d5d703116))
|
|
14
|
+
|
|
5
15
|
## v4.4.0 (2026-01-20)
|
|
6
16
|
|
|
7
17
|
### Feature
|
|
@@ -217,6 +217,8 @@ In order to enable this functionality, TypeDAL adds a `before update` and `befor
|
|
|
217
217
|
which manages the dependencies. You can disable this behavior by passing `cache_dependency=False` to `db.define`.
|
|
218
218
|
Be aware doing this might break some caching functionality!
|
|
219
219
|
|
|
220
|
+
**Note:** For caching function results (instead of just query results), see [9. Function Memoization](./9_memoization.md).
|
|
221
|
+
|
|
220
222
|
### Collecting
|
|
221
223
|
|
|
222
224
|
The Query Builder has a few operations that don't return a new builder instance:
|
|
@@ -121,3 +121,7 @@ recent_articles = (
|
|
|
121
121
|
|
|
122
122
|
By using these mixins, you can enhance the functionality of your models in a modular and reusable manner, saving you
|
|
123
123
|
time and effort in your development process.
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
Looking to cache expensive function results? Head to [9. Function Memoization](./9_memoization.md) to learn about `db.memoize()`.
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# 9. Function Memoization
|
|
2
|
+
|
|
3
|
+
TypeDAL provides database-aware function memoization via `db.memoize()`. This allows you to cache the result of expensive functions and automatically invalidate the cache when the underlying database rows change.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
While `.cache()` on query builders (see [3. Building Queries](./3_building_queries.md#cache)) caches query results, `db.memoize()` caches entire function results while tracking all database operations that happen inside the function.
|
|
8
|
+
|
|
9
|
+
This is designed for cases where the database query itself is fast, but the application logic that follows is expensive.
|
|
10
|
+
|
|
11
|
+
## Basic Usage
|
|
12
|
+
|
|
13
|
+
Wrap any function call with `db.memoize()` to cache its result:
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
def process_articles(articles: TypedRows[Article]) -> dict:
|
|
17
|
+
result = {}
|
|
18
|
+
# dummy example, normally you'd use .join() of course
|
|
19
|
+
for article in articles:
|
|
20
|
+
comments = Comment.where(article=article).collect()
|
|
21
|
+
result[article.id] = comments
|
|
22
|
+
return result
|
|
23
|
+
|
|
24
|
+
articles = Article.where(published=True).collect()
|
|
25
|
+
|
|
26
|
+
result, status = db.memoize(process_articles, articles)
|
|
27
|
+
assert status == "fresh"
|
|
28
|
+
|
|
29
|
+
result, status = db.memoize(process_articles, articles)
|
|
30
|
+
assert status == "cached"
|
|
31
|
+
|
|
32
|
+
Comment.first().update_record(text="Updated")
|
|
33
|
+
|
|
34
|
+
result, status = db.memoize(process_articles, articles)
|
|
35
|
+
assert status == "fresh" # cache invalidated!
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
The return value is a tuple of `(result, status)`:
|
|
39
|
+
- `result`: The actual return value of the function
|
|
40
|
+
- `status`: Either `"fresh"` (newly computed) or `"cached"` (retrieved from cache)
|
|
41
|
+
|
|
42
|
+
## Automatic Dependency Tracking
|
|
43
|
+
|
|
44
|
+
TypeDAL automatically tracks all database rows loaded during function execution. This includes:
|
|
45
|
+
|
|
46
|
+
- Direct queries (`Table.where(...).collect()`)
|
|
47
|
+
- Joins (`Table.join().collect()`)
|
|
48
|
+
- Nested queries inside loops
|
|
49
|
+
|
|
50
|
+
When any tracked row is updated, inserted, or deleted, the cached result is invalidated:
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
def something_slow():
|
|
54
|
+
return list(User.join())
|
|
55
|
+
|
|
56
|
+
result, status = db.memoize(something_slow)
|
|
57
|
+
assert status == "fresh"
|
|
58
|
+
|
|
59
|
+
User.first().update_record(name="Changed")
|
|
60
|
+
|
|
61
|
+
result, status = db.memoize(something_slow)
|
|
62
|
+
assert status == "fresh" # cache was invalidated
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## TTL (Time To Live)
|
|
66
|
+
|
|
67
|
+
Control cache lifetime using the `ttl` parameter. It accepts seconds (int), a `timedelta`, or an absolute `datetime`:
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
from datetime import datetime, timedelta
|
|
71
|
+
|
|
72
|
+
# Expire after 1 hour (3600 seconds)
|
|
73
|
+
db.memoize(func, data, ttl=3600)
|
|
74
|
+
|
|
75
|
+
# Expire after 1 hour (timedelta)
|
|
76
|
+
db.memoize(func, data, ttl=timedelta(hours=1))
|
|
77
|
+
|
|
78
|
+
# Expire at specific datetime
|
|
79
|
+
db.memoize(func, data, ttl=datetime(2026, 1, 7))
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Cache Maintenance
|
|
83
|
+
|
|
84
|
+
The `typedal.caching` module provides utilities for cache management:
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
from typedal.caching import clear_cache, remove_cache_for_table, clear_expired
|
|
88
|
+
|
|
89
|
+
# Remove all cache entries
|
|
90
|
+
clear_cache()
|
|
91
|
+
|
|
92
|
+
# Invalidate all cache entries related to a specific table
|
|
93
|
+
remove_cache_for_table(User)
|
|
94
|
+
|
|
95
|
+
# Clean up expired entries only
|
|
96
|
+
clear_expired()
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
You can also use the CLI:
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
# Clean up expired entries
|
|
103
|
+
typedal cache.clear
|
|
104
|
+
|
|
105
|
+
# Show cache statistics
|
|
106
|
+
typedal cache.stats
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Debugging & Profiling
|
|
110
|
+
|
|
111
|
+
TypeDAL provides `before_collect`/`before_execute` and `after_collect`/`after_execute` hooks on the database instance for debugging and profiling queries:
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
def print_query(qb: QueryBuilder):
|
|
115
|
+
print("going to run", qb.to_sql())
|
|
116
|
+
|
|
117
|
+
def print_duration(_qb: QueryBuilder, rows, _raw):
|
|
118
|
+
print("took", rows.metadata["select_duration"])
|
|
119
|
+
|
|
120
|
+
db.before_collect.append(print_query)
|
|
121
|
+
db.after_collect.append(print_duration)
|
|
122
|
+
|
|
123
|
+
TestQueryTable.all() # will trigger both hooks
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
These hooks are used internally for dependency tracking but are exposed for debugging and observability.
|
|
127
|
+
|
|
128
|
+
## How It Works
|
|
129
|
+
|
|
130
|
+
When you call `db.memoize(func, *args, **kwargs)`:
|
|
131
|
+
|
|
132
|
+
1. TypeDAL computes a cache key based on the function and its arguments
|
|
133
|
+
2. If a valid cached result exists, it's returned with `status="cached"`
|
|
134
|
+
3. Otherwise, the function is executed while tracking all database operations
|
|
135
|
+
4. The result is cached along with the IDs of all rows accessed
|
|
136
|
+
5. When any tracked row changes, the cache entry is invalidated
|
|
137
|
+
|
|
138
|
+
The cache is stored in the `typedal_cache` (and `typedal_cache_dependency`) table (same as query-level caching).
|
|
139
|
+
|
|
140
|
+
## Disabling Dependency Tracking
|
|
141
|
+
|
|
142
|
+
If you need to disable cache invalidation hooks for a specific table:
|
|
143
|
+
|
|
144
|
+
```python
|
|
145
|
+
@db.define(cache_dependency=False)
|
|
146
|
+
class SpecialTable(TypedTable):
|
|
147
|
+
...
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
**Warning:** Disabling this may break caching functionality for queries involving this table.
|
|
@@ -577,16 +577,45 @@ def memoize(
|
|
|
577
577
|
return cached, "cached"
|
|
578
578
|
# Cache miss - compute result
|
|
579
579
|
|
|
580
|
-
def
|
|
580
|
+
def track_execute(qb: "QueryBuilder[t.Any]", raw: Rows):
|
|
581
581
|
# find dependant table+id combinations, includes relationships:
|
|
582
582
|
deps.update(_determine_dependencies_auto(raw))
|
|
583
583
|
|
|
584
|
+
# tables: qb.model;
|
|
585
|
+
# something with qb.relationships
|
|
586
|
+
# something with qb.select_args
|
|
587
|
+
|
|
588
|
+
related_tables = (
|
|
589
|
+
{
|
|
590
|
+
# original table
|
|
591
|
+
str(qb.model)
|
|
592
|
+
}
|
|
593
|
+
| {
|
|
594
|
+
# other tables in select()
|
|
595
|
+
_get_table_name(arg)
|
|
596
|
+
for arg in qb.select_args
|
|
597
|
+
}
|
|
598
|
+
| {
|
|
599
|
+
# other tables in relationships
|
|
600
|
+
str(r.table)
|
|
601
|
+
for r in qb.relationships.values()
|
|
602
|
+
}
|
|
603
|
+
)
|
|
604
|
+
|
|
605
|
+
# mark dependency for every relevant table in this query without id:
|
|
606
|
+
deps.update({(table, 0) for table in related_tables})
|
|
607
|
+
|
|
608
|
+
def track_collect(qb: "QueryBuilder[t.Any]", _: TypedRows[t.Any], raw: Rows) -> None:
|
|
609
|
+
return track_execute(qb, raw)
|
|
610
|
+
|
|
584
611
|
# hooks every .collect() to track extra dependencies
|
|
585
612
|
db._after_collect.append(track_collect)
|
|
613
|
+
db._after_execute.append(track_execute)
|
|
586
614
|
try:
|
|
587
615
|
result = func(*args, **kwargs)
|
|
588
616
|
finally:
|
|
589
617
|
db._after_collect.remove(track_collect)
|
|
618
|
+
db._after_execute.remove(track_execute)
|
|
590
619
|
|
|
591
620
|
# Save to cache
|
|
592
621
|
if isinstance(ttl, dt.datetime):
|
|
@@ -392,8 +392,7 @@ def fake_migrations(
|
|
|
392
392
|
|
|
393
393
|
previously_migrated = (
|
|
394
394
|
db(
|
|
395
|
-
db.ewh_implemented_features.name.belongs(to_fake)
|
|
396
|
-
& (db.ewh_implemented_features.installed == True) # noqa E712
|
|
395
|
+
db.ewh_implemented_features.name.belongs(to_fake) & (db.ewh_implemented_features.installed == True) # noqa E712
|
|
397
396
|
)
|
|
398
397
|
.select(db.ewh_implemented_features.name)
|
|
399
398
|
.column("name")
|
|
@@ -148,10 +148,12 @@ class TypeDAL(pydal.DAL):
|
|
|
148
148
|
_config: TypeDALConfig
|
|
149
149
|
_builder: TableDefinitionBuilder
|
|
150
150
|
|
|
151
|
-
# similar to the insert/update/delete hooks at table-level but for .collect:
|
|
151
|
+
# similar to the insert/update/delete hooks at table-level but for .collect/.execute:
|
|
152
152
|
# note: return values are ignored!
|
|
153
153
|
_before_collect: list[t.Callable[["QueryBuilder[t.Any]"], None]]
|
|
154
154
|
_after_collect: list[t.Callable[["QueryBuilder[t.Any]", "TypedRows[t.Any]", "Rows"], None]]
|
|
155
|
+
_before_execute: list[t.Callable[["QueryBuilder[t.Any]"], None]]
|
|
156
|
+
_after_execute: list[t.Callable[["QueryBuilder[t.Any]", "Rows"], None]]
|
|
155
157
|
|
|
156
158
|
def __init__(
|
|
157
159
|
self,
|
|
@@ -208,6 +210,8 @@ class TypeDAL(pydal.DAL):
|
|
|
208
210
|
|
|
209
211
|
self._before_collect = []
|
|
210
212
|
self._after_collect = []
|
|
213
|
+
self._before_execute = []
|
|
214
|
+
self._after_execute = []
|
|
211
215
|
|
|
212
216
|
if config.folder:
|
|
213
217
|
Path(config.folder).mkdir(exist_ok=True)
|
|
@@ -552,7 +552,15 @@ class QueryBuilder(t.Generic[T_MetaInstance]):
|
|
|
552
552
|
|
|
553
553
|
query, select_args, select_kwargs = self._before_query(metadata, add_id=add_id)
|
|
554
554
|
|
|
555
|
-
|
|
555
|
+
for fn_before in db._before_execute:
|
|
556
|
+
fn_before(self)
|
|
557
|
+
|
|
558
|
+
rows = db(query).select(*select_args, **select_kwargs)
|
|
559
|
+
|
|
560
|
+
for fn_after in db._after_execute:
|
|
561
|
+
fn_after(self, rows)
|
|
562
|
+
|
|
563
|
+
return rows
|
|
556
564
|
|
|
557
565
|
def collect(
|
|
558
566
|
self,
|
|
@@ -527,7 +527,7 @@ def test_minimal_functionality_on_pydal_style_tables():
|
|
|
527
527
|
assert len(qb2) == 1
|
|
528
528
|
|
|
529
529
|
|
|
530
|
-
def test_before_after_collect():
|
|
530
|
+
def test_before_after_collect(capsys):
|
|
531
531
|
_setup_data()
|
|
532
532
|
|
|
533
533
|
def print_query(qb: QueryBuilder):
|
|
@@ -543,16 +543,30 @@ def test_before_after_collect():
|
|
|
543
543
|
db._after_collect.append(print_duration)
|
|
544
544
|
|
|
545
545
|
TestQueryTable.all()
|
|
546
|
+
captured = capsys.readouterr()
|
|
547
|
+
assert "going to run" in captured.out
|
|
548
|
+
assert "took" in captured.out
|
|
549
|
+
|
|
550
|
+
db._before_execute.append(lambda *_: print("BEFORE EXECUTE"))
|
|
551
|
+
db._after_execute.append(lambda *_: print("AFTER EXECUTE"))
|
|
552
|
+
TestQueryTable.select().execute()
|
|
553
|
+
captured = capsys.readouterr()
|
|
554
|
+
assert "BEFORE EXECUTE" in captured.out
|
|
555
|
+
assert "AFTER EXECUTE" in captured.out
|
|
546
556
|
|
|
547
557
|
|
|
548
558
|
def test_groupby_basic():
|
|
549
559
|
"""Test basic groupby with count aggregation."""
|
|
550
560
|
_setup_data()
|
|
551
561
|
|
|
552
|
-
result =
|
|
553
|
-
TestRelationship.
|
|
554
|
-
|
|
555
|
-
|
|
562
|
+
result = (
|
|
563
|
+
TestRelationship.select(
|
|
564
|
+
TestRelationship.querytable.with_alias("query_table"),
|
|
565
|
+
TestRelationship.querytable.count().with_alias("count"),
|
|
566
|
+
)
|
|
567
|
+
.groupby(TestRelationship.querytable)
|
|
568
|
+
.execute()
|
|
569
|
+
)
|
|
556
570
|
|
|
557
571
|
assert len(result) == 2
|
|
558
572
|
for row in result:
|
|
@@ -563,11 +577,15 @@ def test_groupby_multiple_fields():
|
|
|
563
577
|
"""Test grouping by multiple fields."""
|
|
564
578
|
_setup_data()
|
|
565
579
|
|
|
566
|
-
result =
|
|
567
|
-
TestRelationship.
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
580
|
+
result = (
|
|
581
|
+
TestRelationship.select(
|
|
582
|
+
TestRelationship.querytable,
|
|
583
|
+
TestRelationship.value,
|
|
584
|
+
TestRelationship.id.count().with_alias("count"),
|
|
585
|
+
)
|
|
586
|
+
.groupby(TestRelationship.querytable, TestRelationship.value)
|
|
587
|
+
.execute()
|
|
588
|
+
)
|
|
571
589
|
|
|
572
590
|
# Should group by combination of querytable and value
|
|
573
591
|
assert len(result) > 0
|
|
@@ -577,10 +595,15 @@ def test_groupby_with_having():
|
|
|
577
595
|
"""Test groupby with having to filter groups."""
|
|
578
596
|
_setup_data()
|
|
579
597
|
|
|
580
|
-
result =
|
|
581
|
-
TestRelationship.
|
|
582
|
-
|
|
583
|
-
|
|
598
|
+
result = (
|
|
599
|
+
TestRelationship.select(
|
|
600
|
+
TestRelationship.querytable.with_alias("query_table"),
|
|
601
|
+
TestRelationship.querytable.count().with_alias("count"),
|
|
602
|
+
)
|
|
603
|
+
.groupby(TestRelationship.querytable)
|
|
604
|
+
.having(TestRelationship.querytable.count() > 3)
|
|
605
|
+
.execute()
|
|
606
|
+
)
|
|
584
607
|
|
|
585
608
|
# Only groups with count > 3
|
|
586
609
|
assert len(result) == 2
|
|
@@ -593,16 +616,25 @@ def test_having_filters_aggregates():
|
|
|
593
616
|
_setup_data()
|
|
594
617
|
|
|
595
618
|
# Get all groups
|
|
596
|
-
all_groups =
|
|
597
|
-
TestRelationship.
|
|
598
|
-
|
|
599
|
-
|
|
619
|
+
all_groups = (
|
|
620
|
+
TestRelationship.select(
|
|
621
|
+
TestRelationship.querytable,
|
|
622
|
+
TestRelationship.querytable.count().with_alias("count"),
|
|
623
|
+
)
|
|
624
|
+
.groupby(TestRelationship.querytable)
|
|
625
|
+
.execute()
|
|
626
|
+
)
|
|
600
627
|
|
|
601
628
|
# Filter with having
|
|
602
|
-
filtered =
|
|
603
|
-
TestRelationship.
|
|
604
|
-
|
|
605
|
-
|
|
629
|
+
filtered = (
|
|
630
|
+
TestRelationship.select(
|
|
631
|
+
TestRelationship.querytable,
|
|
632
|
+
TestRelationship.querytable.count().with_alias("count"),
|
|
633
|
+
)
|
|
634
|
+
.groupby(TestRelationship.querytable)
|
|
635
|
+
.having(TestRelationship.querytable.count() > 10)
|
|
636
|
+
.execute()
|
|
637
|
+
)
|
|
606
638
|
|
|
607
639
|
# Should have fewer results (or zero if no groups have count > 10)
|
|
608
640
|
assert len(filtered) <= len(all_groups)
|
|
@@ -610,9 +642,11 @@ def test_having_filters_aggregates():
|
|
|
610
642
|
|
|
611
643
|
def test_groupby_to_sql():
|
|
612
644
|
"""Verify SQL generation includes GROUP BY."""
|
|
613
|
-
sql =
|
|
614
|
-
TestRelationship.querytable, TestRelationship.querytable.count()
|
|
615
|
-
|
|
645
|
+
sql = (
|
|
646
|
+
TestRelationship.select(TestRelationship.querytable, TestRelationship.querytable.count())
|
|
647
|
+
.groupby(TestRelationship.querytable)
|
|
648
|
+
.to_sql()
|
|
649
|
+
)
|
|
616
650
|
|
|
617
651
|
assert "GROUP BY" in sql
|
|
618
652
|
|
|
@@ -774,6 +774,51 @@ def test_memoize_nested_dependencies2():
|
|
|
774
774
|
assert status == "cached"
|
|
775
775
|
|
|
776
776
|
|
|
777
|
+
def test_memoize_with_empty_table():
|
|
778
|
+
"""
|
|
779
|
+
Test memoization when the table has no data yet.
|
|
780
|
+
Ensures cache invalidation works correctly when data is added later.
|
|
781
|
+
"""
|
|
782
|
+
# (no setup data needed since we'll truncate anyway)
|
|
783
|
+
|
|
784
|
+
# Clean up - ensure table is empty
|
|
785
|
+
for table in db.tables:
|
|
786
|
+
db[table].truncate()
|
|
787
|
+
|
|
788
|
+
db.commit()
|
|
789
|
+
|
|
790
|
+
# Memoize a function with empty table
|
|
791
|
+
def get_all_users() -> list[str]:
|
|
792
|
+
users = User.join().select(User.name).execute() # also tests execute instead of collect
|
|
793
|
+
return [user[User.name] for user in users]
|
|
794
|
+
|
|
795
|
+
# First call with empty table
|
|
796
|
+
result1, status1 = db.memoize(get_all_users)
|
|
797
|
+
assert status1 == "fresh"
|
|
798
|
+
assert result1 == []
|
|
799
|
+
|
|
800
|
+
# Second call - should be cached
|
|
801
|
+
result2, status2 = db.memoize(get_all_users)
|
|
802
|
+
assert status2 == "cached"
|
|
803
|
+
assert result2 == []
|
|
804
|
+
|
|
805
|
+
# Now insert data into the empty table
|
|
806
|
+
role = Role.insert(name="admin")
|
|
807
|
+
User.insert(name="First User", roles=[role], main_role=role, extra_roles=[])
|
|
808
|
+
db.commit()
|
|
809
|
+
|
|
810
|
+
# Third call - cache should be invalidated due to insert
|
|
811
|
+
result3, status3 = db.memoize(get_all_users)
|
|
812
|
+
assert status3 == "fresh", "Cache should be invalidated after insert into previously empty table"
|
|
813
|
+
assert len(result3) == 1
|
|
814
|
+
assert "First User" in result3
|
|
815
|
+
|
|
816
|
+
# Fourth call - should be cached again
|
|
817
|
+
result4, status4 = db.memoize(get_all_users)
|
|
818
|
+
assert status4 == "cached"
|
|
819
|
+
assert result3 == result4
|
|
820
|
+
|
|
821
|
+
|
|
777
822
|
def test_illegal():
|
|
778
823
|
with pytest.raises(ValueError), pytest.warns(UserWarning):
|
|
779
824
|
|
|
Binary file
|
|
@@ -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
|
|
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
|