view-scheduler 0.4.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.
- view_scheduler-0.4.0/LICENSE +21 -0
- view_scheduler-0.4.0/PKG-INFO +230 -0
- view_scheduler-0.4.0/README.md +210 -0
- view_scheduler-0.4.0/pyproject.toml +41 -0
- view_scheduler-0.4.0/setup.cfg +4 -0
- view_scheduler-0.4.0/src/view_scheduler/__init__.py +34 -0
- view_scheduler-0.4.0/src/view_scheduler/advice.py +162 -0
- view_scheduler-0.4.0/src/view_scheduler/cli.py +71 -0
- view_scheduler-0.4.0/src/view_scheduler/client.py +222 -0
- view_scheduler-0.4.0/src/view_scheduler/config.py +29 -0
- view_scheduler-0.4.0/src/view_scheduler/gate.py +278 -0
- view_scheduler-0.4.0/src/view_scheduler/history.py +145 -0
- view_scheduler-0.4.0/src/view_scheduler/models.py +36 -0
- view_scheduler-0.4.0/src/view_scheduler/obs.py +38 -0
- view_scheduler-0.4.0/src/view_scheduler/recommend.py +174 -0
- view_scheduler-0.4.0/src/view_scheduler/report.py +159 -0
- view_scheduler-0.4.0/src/view_scheduler/run.py +51 -0
- view_scheduler-0.4.0/src/view_scheduler/walker.py +140 -0
- view_scheduler-0.4.0/src/view_scheduler.egg-info/PKG-INFO +230 -0
- view_scheduler-0.4.0/src/view_scheduler.egg-info/SOURCES.txt +37 -0
- view_scheduler-0.4.0/src/view_scheduler.egg-info/dependency_links.txt +1 -0
- view_scheduler-0.4.0/src/view_scheduler.egg-info/entry_points.txt +2 -0
- view_scheduler-0.4.0/src/view_scheduler.egg-info/requires.txt +5 -0
- view_scheduler-0.4.0/src/view_scheduler.egg-info/top_level.txt +1 -0
- view_scheduler-0.4.0/tests/test_advice.py +111 -0
- view_scheduler-0.4.0/tests/test_bundle.py +50 -0
- view_scheduler-0.4.0/tests/test_cli.py +101 -0
- view_scheduler-0.4.0/tests/test_client.py +83 -0
- view_scheduler-0.4.0/tests/test_fixtures.py +30 -0
- view_scheduler-0.4.0/tests/test_gate.py +398 -0
- view_scheduler-0.4.0/tests/test_history.py +189 -0
- view_scheduler-0.4.0/tests/test_oracle.py +149 -0
- view_scheduler-0.4.0/tests/test_plan.py +118 -0
- view_scheduler-0.4.0/tests/test_real_shapes.py +79 -0
- view_scheduler-0.4.0/tests/test_recommend.py +136 -0
- view_scheduler-0.4.0/tests/test_report.py +187 -0
- view_scheduler-0.4.0/tests/test_run.py +92 -0
- view_scheduler-0.4.0/tests/test_walker.py +94 -0
- view_scheduler-0.4.0/tests/test_write_slot.py +88 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Kiran B
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: view-scheduler
|
|
3
|
+
Version: 0.4.0
|
|
4
|
+
Summary: Recommend when a Unity Catalog view can be materialized with every dependency already refreshed
|
|
5
|
+
Author: Kiran B
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/kiranb-1508/view-scheduler-docs
|
|
8
|
+
Keywords: databricks,unity-catalog,delta,scheduling
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
12
|
+
Requires-Python: >=3.10
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
License-File: LICENSE
|
|
15
|
+
Requires-Dist: databricks-sdk>=0.30
|
|
16
|
+
Provides-Extra: dev
|
|
17
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
18
|
+
Requires-Dist: build>=1.0; extra == "dev"
|
|
19
|
+
Dynamic: license-file
|
|
20
|
+
|
|
21
|
+
# View Materialization Scheduler
|
|
22
|
+
|
|
23
|
+
Two questions, two tools.
|
|
24
|
+
|
|
25
|
+
- **When** should this view run? → `advise`. It reads history and suggests a time.
|
|
26
|
+
- **Can** it run right now? → `can_refresh`, the gate. It checks today's state.
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
$ view-scheduler sales_gold.customer_360
|
|
30
|
+
|
|
31
|
+
VIEW sales_gold.customer_360
|
|
32
|
+
8 dependencies · last 30 days · times Asia/Kolkata
|
|
33
|
+
|
|
34
|
+
SCHEDULE AT 06:25 safe — every job had finished, on all 30 days
|
|
35
|
+
EARLIER 05:45 one job had not finished yet, on 1 of 30 days
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Read this before you rely on it
|
|
39
|
+
|
|
40
|
+
**The recommendation is advisory.** It says when a view *usually* can run; it
|
|
41
|
+
does not check today. If a dependency runs later than it ever has, the job
|
|
42
|
+
starts anyway and publishes stale data, silently. Pair it with the gate —
|
|
43
|
+
`can_refresh` — which checks every dependency's actual last write before the
|
|
44
|
+
refresh runs. Set the schedule from history; guard the run with the gate.
|
|
45
|
+
|
|
46
|
+
**The arithmetic is still being validated.** A 30-day fixture rig
|
|
47
|
+
(`rig/`) is running now to check the day-spread logic against real multi-day
|
|
48
|
+
commit history. Until that completes, treat output as indicative. Hence `0.4.0`.
|
|
49
|
+
|
|
50
|
+
## Install
|
|
51
|
+
|
|
52
|
+
### Databricks (recommended)
|
|
53
|
+
|
|
54
|
+
Ship the wheel through a Unity Catalog Volume, so access is governed by UC
|
|
55
|
+
grants rather than by GitHub accounts.
|
|
56
|
+
|
|
57
|
+
Once, to create the shared location:
|
|
58
|
+
|
|
59
|
+
```sql
|
|
60
|
+
CREATE VOLUME IF NOT EXISTS <catalog>.<schema>.libs;
|
|
61
|
+
GRANT READ VOLUME ON VOLUME <catalog>.<schema>.libs TO `<group>`;
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Build and upload a release:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
python -m build --wheel
|
|
68
|
+
databricks fs cp dist/view_scheduler-0.4.0-py3-none-any.whl \
|
|
69
|
+
dbfs:/Volumes/<catalog>/<schema>/libs/
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Keep old versions in the volume so running jobs can stay pinned.
|
|
73
|
+
|
|
74
|
+
Then, in a notebook:
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
%pip install /Volumes/<catalog>/<schema>/libs/view_scheduler-0.4.0-py3-none-any.whl
|
|
78
|
+
dbutils.library.restartPython()
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Or as a job library, including from a DAB:
|
|
82
|
+
|
|
83
|
+
```yaml
|
|
84
|
+
tasks:
|
|
85
|
+
- task_key: recommend
|
|
86
|
+
libraries:
|
|
87
|
+
- whl: /Volumes/<catalog>/<schema>/libs/view_scheduler-0.4.0-py3-none-any.whl
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### From source
|
|
91
|
+
|
|
92
|
+
For working on the package itself:
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
pip install -e ".[dev]" # a checkout
|
|
96
|
+
pip install git+https://github.com/kiranb-1508/view-scheduler-docs.git
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Use
|
|
100
|
+
|
|
101
|
+
As a command:
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
export WAREHOUSE_ID=<sql warehouse id>
|
|
105
|
+
view-scheduler catalog.schema.my_view
|
|
106
|
+
view-scheduler catalog.schema.my_view --days 60 --buffer 30 --quiet
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
As a library:
|
|
110
|
+
|
|
111
|
+
```python
|
|
112
|
+
from view_scheduler import advise
|
|
113
|
+
|
|
114
|
+
advice = advise("sales.gold.customer_360") # WAREHOUSE_ID from the env
|
|
115
|
+
advice.safe_time # datetime.time(6, 25) or None
|
|
116
|
+
advice.earlier_time # datetime.time(5, 45) or None
|
|
117
|
+
advice.next_day # True when the (+1 day) marker applies
|
|
118
|
+
advice.dependencies # one row per upstream job
|
|
119
|
+
advice.to_dict() # JSON, for a job log
|
|
120
|
+
print(advice) # the report
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
`advise` prints the report and returns an `Advice`. Nothing is dropped
|
|
124
|
+
silently — anything unreadable comes back in `advice.unreadable` with a reason.
|
|
125
|
+
|
|
126
|
+
`analyze` is the low-level call. It returns a tuple
|
|
127
|
+
`(text, histories, skipped)` for code that needs the raw per-table history.
|
|
128
|
+
|
|
129
|
+
Four settings: `days_history` (30), `buffer_min` (15), `timezone`
|
|
130
|
+
(`Asia/Kolkata`), and `fresh_hours` (24) — the gate's window. There is a
|
|
131
|
+
standing rule to ask before adding a fifth.
|
|
132
|
+
|
|
133
|
+
### Gate: is my view ready to refresh?
|
|
134
|
+
|
|
135
|
+
Before you materialize a view, ask whether its inputs are actually fresh. The
|
|
136
|
+
gate walks every dependency and checks that its last real write is inside a
|
|
137
|
+
rolling window (24 hours by default).
|
|
138
|
+
|
|
139
|
+
```python
|
|
140
|
+
from view_scheduler import can_refresh
|
|
141
|
+
|
|
142
|
+
result = can_refresh("sales.gold.v_orders") # WAREHOUSE_ID from $env
|
|
143
|
+
if result.ready:
|
|
144
|
+
...run the refresh...
|
|
145
|
+
else:
|
|
146
|
+
print(result) # says exactly what to fix
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
A list works the same way. You get one verdict per view, and one overall:
|
|
150
|
+
|
|
151
|
+
```python
|
|
152
|
+
result = can_refresh(["sales.gold.v_orders", "sales.gold.v_revenue"])
|
|
153
|
+
print(result.ready) # True only when every view is ready
|
|
154
|
+
print(result.blockers()) # ('sales.silver.daily_sales',)
|
|
155
|
+
print(result.to_dict()) # JSON, for a job log
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
```
|
|
159
|
+
GATE BLOCKED 2 views, window 24h, times Asia/Kolkata
|
|
160
|
+
|
|
161
|
+
READY sales.gold.v_orders
|
|
162
|
+
fresh sales.silver.daily_orders (MANAGED) last refresh 2026-10-07 04:00 IST (11h 48m ago)
|
|
163
|
+
|
|
164
|
+
BLOCKED sales.gold.v_revenue
|
|
165
|
+
fresh sales.silver.orders (MANAGED) last refresh 2026-10-07 04:00 IST (11h 48m ago)
|
|
166
|
+
stale sales.silver.daily_sales (MANAGED) last refresh 2026-10-05 04:00 IST (2d 11h ago) — older than the 24h window
|
|
167
|
+
|
|
168
|
+
To unblock, refresh:
|
|
169
|
+
sales.silver.daily_sales (needed by sales.gold.v_revenue)
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
The gate decides; it never refreshes. It is read-only, like `analyze`. The rules
|
|
173
|
+
are the whole table:
|
|
174
|
+
|
|
175
|
+
| Case | Verdict |
|
|
176
|
+
|---|---|
|
|
177
|
+
| Wrote inside the window | fresh |
|
|
178
|
+
| Wrote before the window | stale — **blocks** |
|
|
179
|
+
| No qualifying write in its history | **blocks** |
|
|
180
|
+
| Cannot be read (missing, no permission) | **blocks** — the gate fails closed |
|
|
181
|
+
| Streaming table | checked from its history, like any table — one on a schedule is stale between runs |
|
|
182
|
+
| Materialized view | read from its pipeline event log |
|
|
183
|
+
| Function dependency (a UDF) | a note, not a blocker |
|
|
184
|
+
|
|
185
|
+
`Workspace(warehouse_id=...)` and `Config.fresh_hours` are optional. Without a
|
|
186
|
+
`Workspace`, the gate reads `WAREHOUSE_ID` from the environment, so the
|
|
187
|
+
one-liner above is enough inside a job.
|
|
188
|
+
|
|
189
|
+
**Permissions.** The caller needs `SELECT` on each dependency (for
|
|
190
|
+
`DESCRIBE HISTORY`) and `USE` on the catalogs and schemas involved. Materialized
|
|
191
|
+
views are read through their pipeline event log, which by default only the
|
|
192
|
+
pipeline's run-as user can read. Without that access the gate **blocks** the MV:
|
|
193
|
+
it reports `history unreadable (permission)`. The call itself does not raise.
|
|
194
|
+
|
|
195
|
+
## Design docs
|
|
196
|
+
|
|
197
|
+
| File | What it is |
|
|
198
|
+
|---|---|
|
|
199
|
+
| [docs/01-plan.html](docs/01-plan.html) | The plan. Start here — output shape, what's built, what was deliberately cut |
|
|
200
|
+
| [docs/02-scheduler-design.html](docs/02-scheduler-design.html) | Scheduler design: architecture, code flow, four steps, worked example |
|
|
201
|
+
| [docs/03-impact-design.html](docs/03-impact-design.html) | Impact analysis: given a table, what depends on it. Separate tool, separate data source |
|
|
202
|
+
| [docs/04-library-flow.html](docs/04-library-flow.html) | Visual end-to-end flow: input, functions, branches, recommendation, and report |
|
|
203
|
+
| [docs/05-interactive-guide.html](docs/05-interactive-guide.html) | Interactive guide: clickable flow, a gate simulator, the design decisions, and the open findings |
|
|
204
|
+
| [docs/06-code-walkthrough.html](docs/06-code-walkthrough.html) | Code walkthrough: the module map, call traces, a class tour, and a searchable index of all 66 functions |
|
|
205
|
+
| [rig/README.md](rig/README.md) | The 30-day fixture rig and its checkpoints |
|
|
206
|
+
| [GENIE-PROMPT.md](GENIE-PROMPT.md) | Build prompt for Databricks Genie Code, phased with verification gates |
|
|
207
|
+
|
|
208
|
+
Section 08 of the scheduler design lists what the docs describe but nobody
|
|
209
|
+
built. The runtime gate is no longer on that list — it ships as `can_refresh`.
|
|
210
|
+
|
|
211
|
+
## The two things most likely to bite
|
|
212
|
+
|
|
213
|
+
1. **Operation class comes from `operationParameters.mode`, not the operation
|
|
214
|
+
name.** The commonest Delta operation name is plain `WRITE`; only its mode
|
|
215
|
+
says whether it replaced or appended. Classify by name and most tables read
|
|
216
|
+
as dormant — a confident wrong answer, not an error.
|
|
217
|
+
2. **`DESCRIBE HISTORY` carries no partition values.** Verified against the docs.
|
|
218
|
+
Any design that attributes commits to business dates via partitions is built
|
|
219
|
+
on an input that does not exist.
|
|
220
|
+
|
|
221
|
+
## Development
|
|
222
|
+
|
|
223
|
+
```bash
|
|
224
|
+
pip install -e ".[dev]"
|
|
225
|
+
pytest
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
176 tests, all offline — no Spark session, no network, no credentials.
|
|
229
|
+
`tools/` holds diagnostic scripts that run against a live workspace;
|
|
230
|
+
`rig/` builds and checks the 30-day fixture. Neither ships in the wheel.
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
# View Materialization Scheduler
|
|
2
|
+
|
|
3
|
+
Two questions, two tools.
|
|
4
|
+
|
|
5
|
+
- **When** should this view run? → `advise`. It reads history and suggests a time.
|
|
6
|
+
- **Can** it run right now? → `can_refresh`, the gate. It checks today's state.
|
|
7
|
+
|
|
8
|
+
```
|
|
9
|
+
$ view-scheduler sales_gold.customer_360
|
|
10
|
+
|
|
11
|
+
VIEW sales_gold.customer_360
|
|
12
|
+
8 dependencies · last 30 days · times Asia/Kolkata
|
|
13
|
+
|
|
14
|
+
SCHEDULE AT 06:25 safe — every job had finished, on all 30 days
|
|
15
|
+
EARLIER 05:45 one job had not finished yet, on 1 of 30 days
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Read this before you rely on it
|
|
19
|
+
|
|
20
|
+
**The recommendation is advisory.** It says when a view *usually* can run; it
|
|
21
|
+
does not check today. If a dependency runs later than it ever has, the job
|
|
22
|
+
starts anyway and publishes stale data, silently. Pair it with the gate —
|
|
23
|
+
`can_refresh` — which checks every dependency's actual last write before the
|
|
24
|
+
refresh runs. Set the schedule from history; guard the run with the gate.
|
|
25
|
+
|
|
26
|
+
**The arithmetic is still being validated.** A 30-day fixture rig
|
|
27
|
+
(`rig/`) is running now to check the day-spread logic against real multi-day
|
|
28
|
+
commit history. Until that completes, treat output as indicative. Hence `0.4.0`.
|
|
29
|
+
|
|
30
|
+
## Install
|
|
31
|
+
|
|
32
|
+
### Databricks (recommended)
|
|
33
|
+
|
|
34
|
+
Ship the wheel through a Unity Catalog Volume, so access is governed by UC
|
|
35
|
+
grants rather than by GitHub accounts.
|
|
36
|
+
|
|
37
|
+
Once, to create the shared location:
|
|
38
|
+
|
|
39
|
+
```sql
|
|
40
|
+
CREATE VOLUME IF NOT EXISTS <catalog>.<schema>.libs;
|
|
41
|
+
GRANT READ VOLUME ON VOLUME <catalog>.<schema>.libs TO `<group>`;
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Build and upload a release:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
python -m build --wheel
|
|
48
|
+
databricks fs cp dist/view_scheduler-0.4.0-py3-none-any.whl \
|
|
49
|
+
dbfs:/Volumes/<catalog>/<schema>/libs/
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Keep old versions in the volume so running jobs can stay pinned.
|
|
53
|
+
|
|
54
|
+
Then, in a notebook:
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
%pip install /Volumes/<catalog>/<schema>/libs/view_scheduler-0.4.0-py3-none-any.whl
|
|
58
|
+
dbutils.library.restartPython()
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Or as a job library, including from a DAB:
|
|
62
|
+
|
|
63
|
+
```yaml
|
|
64
|
+
tasks:
|
|
65
|
+
- task_key: recommend
|
|
66
|
+
libraries:
|
|
67
|
+
- whl: /Volumes/<catalog>/<schema>/libs/view_scheduler-0.4.0-py3-none-any.whl
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### From source
|
|
71
|
+
|
|
72
|
+
For working on the package itself:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
pip install -e ".[dev]" # a checkout
|
|
76
|
+
pip install git+https://github.com/kiranb-1508/view-scheduler-docs.git
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Use
|
|
80
|
+
|
|
81
|
+
As a command:
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
export WAREHOUSE_ID=<sql warehouse id>
|
|
85
|
+
view-scheduler catalog.schema.my_view
|
|
86
|
+
view-scheduler catalog.schema.my_view --days 60 --buffer 30 --quiet
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
As a library:
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
from view_scheduler import advise
|
|
93
|
+
|
|
94
|
+
advice = advise("sales.gold.customer_360") # WAREHOUSE_ID from the env
|
|
95
|
+
advice.safe_time # datetime.time(6, 25) or None
|
|
96
|
+
advice.earlier_time # datetime.time(5, 45) or None
|
|
97
|
+
advice.next_day # True when the (+1 day) marker applies
|
|
98
|
+
advice.dependencies # one row per upstream job
|
|
99
|
+
advice.to_dict() # JSON, for a job log
|
|
100
|
+
print(advice) # the report
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
`advise` prints the report and returns an `Advice`. Nothing is dropped
|
|
104
|
+
silently — anything unreadable comes back in `advice.unreadable` with a reason.
|
|
105
|
+
|
|
106
|
+
`analyze` is the low-level call. It returns a tuple
|
|
107
|
+
`(text, histories, skipped)` for code that needs the raw per-table history.
|
|
108
|
+
|
|
109
|
+
Four settings: `days_history` (30), `buffer_min` (15), `timezone`
|
|
110
|
+
(`Asia/Kolkata`), and `fresh_hours` (24) — the gate's window. There is a
|
|
111
|
+
standing rule to ask before adding a fifth.
|
|
112
|
+
|
|
113
|
+
### Gate: is my view ready to refresh?
|
|
114
|
+
|
|
115
|
+
Before you materialize a view, ask whether its inputs are actually fresh. The
|
|
116
|
+
gate walks every dependency and checks that its last real write is inside a
|
|
117
|
+
rolling window (24 hours by default).
|
|
118
|
+
|
|
119
|
+
```python
|
|
120
|
+
from view_scheduler import can_refresh
|
|
121
|
+
|
|
122
|
+
result = can_refresh("sales.gold.v_orders") # WAREHOUSE_ID from $env
|
|
123
|
+
if result.ready:
|
|
124
|
+
...run the refresh...
|
|
125
|
+
else:
|
|
126
|
+
print(result) # says exactly what to fix
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
A list works the same way. You get one verdict per view, and one overall:
|
|
130
|
+
|
|
131
|
+
```python
|
|
132
|
+
result = can_refresh(["sales.gold.v_orders", "sales.gold.v_revenue"])
|
|
133
|
+
print(result.ready) # True only when every view is ready
|
|
134
|
+
print(result.blockers()) # ('sales.silver.daily_sales',)
|
|
135
|
+
print(result.to_dict()) # JSON, for a job log
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
```
|
|
139
|
+
GATE BLOCKED 2 views, window 24h, times Asia/Kolkata
|
|
140
|
+
|
|
141
|
+
READY sales.gold.v_orders
|
|
142
|
+
fresh sales.silver.daily_orders (MANAGED) last refresh 2026-10-07 04:00 IST (11h 48m ago)
|
|
143
|
+
|
|
144
|
+
BLOCKED sales.gold.v_revenue
|
|
145
|
+
fresh sales.silver.orders (MANAGED) last refresh 2026-10-07 04:00 IST (11h 48m ago)
|
|
146
|
+
stale sales.silver.daily_sales (MANAGED) last refresh 2026-10-05 04:00 IST (2d 11h ago) — older than the 24h window
|
|
147
|
+
|
|
148
|
+
To unblock, refresh:
|
|
149
|
+
sales.silver.daily_sales (needed by sales.gold.v_revenue)
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
The gate decides; it never refreshes. It is read-only, like `analyze`. The rules
|
|
153
|
+
are the whole table:
|
|
154
|
+
|
|
155
|
+
| Case | Verdict |
|
|
156
|
+
|---|---|
|
|
157
|
+
| Wrote inside the window | fresh |
|
|
158
|
+
| Wrote before the window | stale — **blocks** |
|
|
159
|
+
| No qualifying write in its history | **blocks** |
|
|
160
|
+
| Cannot be read (missing, no permission) | **blocks** — the gate fails closed |
|
|
161
|
+
| Streaming table | checked from its history, like any table — one on a schedule is stale between runs |
|
|
162
|
+
| Materialized view | read from its pipeline event log |
|
|
163
|
+
| Function dependency (a UDF) | a note, not a blocker |
|
|
164
|
+
|
|
165
|
+
`Workspace(warehouse_id=...)` and `Config.fresh_hours` are optional. Without a
|
|
166
|
+
`Workspace`, the gate reads `WAREHOUSE_ID` from the environment, so the
|
|
167
|
+
one-liner above is enough inside a job.
|
|
168
|
+
|
|
169
|
+
**Permissions.** The caller needs `SELECT` on each dependency (for
|
|
170
|
+
`DESCRIBE HISTORY`) and `USE` on the catalogs and schemas involved. Materialized
|
|
171
|
+
views are read through their pipeline event log, which by default only the
|
|
172
|
+
pipeline's run-as user can read. Without that access the gate **blocks** the MV:
|
|
173
|
+
it reports `history unreadable (permission)`. The call itself does not raise.
|
|
174
|
+
|
|
175
|
+
## Design docs
|
|
176
|
+
|
|
177
|
+
| File | What it is |
|
|
178
|
+
|---|---|
|
|
179
|
+
| [docs/01-plan.html](docs/01-plan.html) | The plan. Start here — output shape, what's built, what was deliberately cut |
|
|
180
|
+
| [docs/02-scheduler-design.html](docs/02-scheduler-design.html) | Scheduler design: architecture, code flow, four steps, worked example |
|
|
181
|
+
| [docs/03-impact-design.html](docs/03-impact-design.html) | Impact analysis: given a table, what depends on it. Separate tool, separate data source |
|
|
182
|
+
| [docs/04-library-flow.html](docs/04-library-flow.html) | Visual end-to-end flow: input, functions, branches, recommendation, and report |
|
|
183
|
+
| [docs/05-interactive-guide.html](docs/05-interactive-guide.html) | Interactive guide: clickable flow, a gate simulator, the design decisions, and the open findings |
|
|
184
|
+
| [docs/06-code-walkthrough.html](docs/06-code-walkthrough.html) | Code walkthrough: the module map, call traces, a class tour, and a searchable index of all 66 functions |
|
|
185
|
+
| [rig/README.md](rig/README.md) | The 30-day fixture rig and its checkpoints |
|
|
186
|
+
| [GENIE-PROMPT.md](GENIE-PROMPT.md) | Build prompt for Databricks Genie Code, phased with verification gates |
|
|
187
|
+
|
|
188
|
+
Section 08 of the scheduler design lists what the docs describe but nobody
|
|
189
|
+
built. The runtime gate is no longer on that list — it ships as `can_refresh`.
|
|
190
|
+
|
|
191
|
+
## The two things most likely to bite
|
|
192
|
+
|
|
193
|
+
1. **Operation class comes from `operationParameters.mode`, not the operation
|
|
194
|
+
name.** The commonest Delta operation name is plain `WRITE`; only its mode
|
|
195
|
+
says whether it replaced or appended. Classify by name and most tables read
|
|
196
|
+
as dormant — a confident wrong answer, not an error.
|
|
197
|
+
2. **`DESCRIBE HISTORY` carries no partition values.** Verified against the docs.
|
|
198
|
+
Any design that attributes commits to business dates via partitions is built
|
|
199
|
+
on an input that does not exist.
|
|
200
|
+
|
|
201
|
+
## Development
|
|
202
|
+
|
|
203
|
+
```bash
|
|
204
|
+
pip install -e ".[dev]"
|
|
205
|
+
pytest
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
176 tests, all offline — no Spark session, no network, no credentials.
|
|
209
|
+
`tools/` holds diagnostic scripts that run against a live workspace;
|
|
210
|
+
`rig/` builds and checks the 30-day fixture. Neither ships in the wheel.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "view-scheduler"
|
|
7
|
+
version = "0.4.0"
|
|
8
|
+
description = "Recommend when a Unity Catalog view can be materialized with every dependency already refreshed"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
authors = [{ name = "Kiran B" }]
|
|
13
|
+
keywords = ["databricks", "unity-catalog", "delta", "scheduling"]
|
|
14
|
+
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Programming Language :: Python :: 3.10",
|
|
17
|
+
"Programming Language :: Python :: 3.11",
|
|
18
|
+
"Programming Language :: Python :: 3.12",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
dependencies = [
|
|
22
|
+
"databricks-sdk>=0.30",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
[project.urls]
|
|
26
|
+
Homepage = "https://github.com/kiranb-1508/view-scheduler-docs"
|
|
27
|
+
|
|
28
|
+
[project.optional-dependencies]
|
|
29
|
+
dev = [
|
|
30
|
+
"pytest>=7.0",
|
|
31
|
+
"build>=1.0",
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
[project.scripts]
|
|
35
|
+
view-scheduler = "view_scheduler.cli:main"
|
|
36
|
+
|
|
37
|
+
[tool.setuptools.packages.find]
|
|
38
|
+
where = ["src"]
|
|
39
|
+
|
|
40
|
+
# src layout: only view_scheduler ships. rig/, tests/ and tools/ are repo-only
|
|
41
|
+
# and stay out of the wheel — `unzip -l dist/*.whl` is the check.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""View materialization scheduler.
|
|
2
|
+
|
|
3
|
+
Two tools for a Unity Catalog view:
|
|
4
|
+
|
|
5
|
+
* `advise` — suggest the time each day the view can run with every upstream
|
|
6
|
+
job already finished. Computed from history. ADVISORY.
|
|
7
|
+
* `can_refresh` — a runtime GATE. Right now, has every dependency of this
|
|
8
|
+
view refreshed recently enough? READY or BLOCKED.
|
|
9
|
+
|
|
10
|
+
READ ONLY. This package never writes, alters, optimizes, vacuums or drops a
|
|
11
|
+
user table. Metadata and history reads only.
|
|
12
|
+
|
|
13
|
+
`advise` predicts. `can_refresh` checks. Use the first to set the schedule,
|
|
14
|
+
the second to guard the run. See README.md.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from .advice import Advice, DependencyRun, advise
|
|
18
|
+
from .client import Workspace
|
|
19
|
+
from .config import Config
|
|
20
|
+
from .gate import Dependency, GateResult, ViewResult, can_refresh
|
|
21
|
+
from .run import analyze
|
|
22
|
+
|
|
23
|
+
__all__ = [
|
|
24
|
+
"advise",
|
|
25
|
+
"Advice",
|
|
26
|
+
"analyze",
|
|
27
|
+
"can_refresh",
|
|
28
|
+
"Config",
|
|
29
|
+
"Dependency",
|
|
30
|
+
"DependencyRun",
|
|
31
|
+
"GateResult",
|
|
32
|
+
"ViewResult",
|
|
33
|
+
"Workspace",
|
|
34
|
+
]
|