databricks360 0.1.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.
- databricks360-0.1.0/.github/workflows/publish.yml +67 -0
- databricks360-0.1.0/.github/workflows/test.yml +37 -0
- databricks360-0.1.0/.gitignore +9 -0
- databricks360-0.1.0/LICENSE +21 -0
- databricks360-0.1.0/PKG-INFO +176 -0
- databricks360-0.1.0/README.md +155 -0
- databricks360-0.1.0/databricks360/__init__.py +67 -0
- databricks360-0.1.0/databricks360/_catalog.py +99 -0
- databricks360-0.1.0/databricks360/_install.py +162 -0
- databricks360-0.1.0/databricks360/_notebook.py +107 -0
- databricks360-0.1.0/databricks360/courses/__init__.py +0 -0
- databricks360-0.1.0/databricks360/courses/genie_agents/01_catalog_and_schemas.sql +31 -0
- databricks360-0.1.0/databricks360/courses/genie_agents/02_dimensions.sql +291 -0
- databricks360-0.1.0/databricks360/courses/genie_agents/03_facts.sql +295 -0
- databricks360-0.1.0/databricks360/courses/genie_agents/__init__.py +0 -0
- databricks360-0.1.0/databricks360/courses/genie_agents/manifest.json +41 -0
- databricks360-0.1.0/docs/genie-agents-dataset.md +62 -0
- databricks360-0.1.0/pyproject.toml +31 -0
- databricks360-0.1.0/run_tests.py +37 -0
- databricks360-0.1.0/tests/test_notebooks.py +113 -0
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# Publishes to PyPI on a version tag, using Trusted Publishing (OIDC).
|
|
2
|
+
#
|
|
3
|
+
# No API token exists anywhere — not in repo secrets, not on a laptop. PyPI
|
|
4
|
+
# verifies the workflow's short-lived OIDC identity instead. A leaked token is
|
|
5
|
+
# the usual way a package supply chain gets compromised, so the best token is
|
|
6
|
+
# one that was never created.
|
|
7
|
+
#
|
|
8
|
+
# One-time PyPI setup is described in README.md → Publishing.
|
|
9
|
+
|
|
10
|
+
name: Publish
|
|
11
|
+
|
|
12
|
+
on:
|
|
13
|
+
push:
|
|
14
|
+
tags: ["v*"]
|
|
15
|
+
workflow_dispatch:
|
|
16
|
+
|
|
17
|
+
permissions: {}
|
|
18
|
+
|
|
19
|
+
jobs:
|
|
20
|
+
build:
|
|
21
|
+
name: Build distributions
|
|
22
|
+
runs-on: ubuntu-latest
|
|
23
|
+
steps:
|
|
24
|
+
- uses: actions/checkout@v4
|
|
25
|
+
|
|
26
|
+
- uses: actions/setup-python@v5
|
|
27
|
+
with:
|
|
28
|
+
python-version: "3.12"
|
|
29
|
+
|
|
30
|
+
- name: Run tests
|
|
31
|
+
run: python run_tests.py
|
|
32
|
+
|
|
33
|
+
- name: Build sdist and wheel
|
|
34
|
+
run: |
|
|
35
|
+
python -m pip install --upgrade build twine
|
|
36
|
+
python -m build
|
|
37
|
+
twine check dist/*
|
|
38
|
+
|
|
39
|
+
- name: Fail if the tag does not match the package version
|
|
40
|
+
run: |
|
|
41
|
+
TAG="${GITHUB_REF_NAME#v}"
|
|
42
|
+
VER=$(python -c "import tomllib,pathlib;print(tomllib.loads(pathlib.Path('pyproject.toml').read_text())['project']['version'])")
|
|
43
|
+
echo "tag=$TAG pyproject=$VER"
|
|
44
|
+
test "$TAG" = "$VER" || { echo "::error::Tag v$TAG does not match pyproject version $VER"; exit 1; }
|
|
45
|
+
if: startsWith(github.ref, 'refs/tags/v')
|
|
46
|
+
|
|
47
|
+
- uses: actions/upload-artifact@v4
|
|
48
|
+
with:
|
|
49
|
+
name: dist
|
|
50
|
+
path: dist/
|
|
51
|
+
|
|
52
|
+
publish:
|
|
53
|
+
name: Publish to PyPI
|
|
54
|
+
needs: build
|
|
55
|
+
runs-on: ubuntu-latest
|
|
56
|
+
# Requires a GitHub environment named "pypi". Add required reviewers to it
|
|
57
|
+
# if you want a human approval gate before anything reaches the index.
|
|
58
|
+
environment: pypi
|
|
59
|
+
permissions:
|
|
60
|
+
id-token: write
|
|
61
|
+
steps:
|
|
62
|
+
- uses: actions/download-artifact@v4
|
|
63
|
+
with:
|
|
64
|
+
name: dist
|
|
65
|
+
path: dist/
|
|
66
|
+
|
|
67
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
name: Tests
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
permissions: {}
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
test:
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
strategy:
|
|
14
|
+
fail-fast: false
|
|
15
|
+
matrix:
|
|
16
|
+
python-version: ["3.9", "3.10", "3.11", "3.12"]
|
|
17
|
+
steps:
|
|
18
|
+
- uses: actions/checkout@v4
|
|
19
|
+
- uses: actions/setup-python@v5
|
|
20
|
+
with:
|
|
21
|
+
python-version: ${{ matrix.python-version }}
|
|
22
|
+
- name: Install
|
|
23
|
+
run: python -m pip install -e .
|
|
24
|
+
- name: Run tests
|
|
25
|
+
run: python run_tests.py
|
|
26
|
+
- name: Verify the built wheel ships its SQL
|
|
27
|
+
run: |
|
|
28
|
+
python -m pip install --upgrade build
|
|
29
|
+
python -m build --wheel
|
|
30
|
+
python - <<'PY'
|
|
31
|
+
import glob, zipfile
|
|
32
|
+
names = zipfile.ZipFile(glob.glob("dist/*.whl")[0]).namelist()
|
|
33
|
+
sql = [n for n in names if n.endswith(".sql")]
|
|
34
|
+
assert len(sql) >= 3, f"expected the lab SQL in the wheel, found {sql}"
|
|
35
|
+
assert len(names) == len(set(names)), "duplicate entries in the wheel"
|
|
36
|
+
print(f"ok: {len(sql)} SQL files, no duplicates")
|
|
37
|
+
PY
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Lakehouse Academy
|
|
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,176 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: databricks360
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Install Databricks course lab environments: notebooks, catalogs, datasets and governance objects.
|
|
5
|
+
Project-URL: Homepage, https://github.com/databrickslms/dbxdemos
|
|
6
|
+
Project-URL: Source, https://github.com/databrickslms/dbxdemos
|
|
7
|
+
Project-URL: Issues, https://github.com/databrickslms/dbxdemos/issues
|
|
8
|
+
Author: Lakehouse Academy
|
|
9
|
+
License: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: databricks,genie,lakehouse,training,unity-catalog
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Education
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Topic :: Education
|
|
16
|
+
Requires-Python: >=3.9
|
|
17
|
+
Requires-Dist: databricks-sdk>=0.38.0
|
|
18
|
+
Provides-Extra: dev
|
|
19
|
+
Requires-Dist: pytest>=7; extra == 'dev'
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
|
|
22
|
+
# databricks360
|
|
23
|
+
|
|
24
|
+
Installs Databricks course lab environments — notebooks, catalogs, datasets and
|
|
25
|
+
governance objects — into your own workspace.
|
|
26
|
+
|
|
27
|
+
Modelled on `dbdemos`: you run it **inside a Databricks notebook**, so
|
|
28
|
+
`databricks-sdk` picks up the notebook's own identity. There is no host, token or
|
|
29
|
+
profile to configure.
|
|
30
|
+
|
|
31
|
+
## Install
|
|
32
|
+
|
|
33
|
+
Not on PyPI yet, so install from the repo. In a **Databricks notebook**:
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
%pip install git+https://github.com/databrickslms/dbxdemos.git
|
|
37
|
+
dbutils.library.restartPython()
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Once published, that becomes `%pip install databricks360`.
|
|
41
|
+
|
|
42
|
+
To pin a version, append a tag or commit:
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
%pip install git+https://github.com/databrickslms/dbxdemos.git@v0.1.0
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Usage
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
import databricks360 as academy
|
|
52
|
+
|
|
53
|
+
academy.list_courses()
|
|
54
|
+
academy.install('genie-agents')
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
`install` writes the lab notebooks into your workspace and prints the run order.
|
|
58
|
+
Then you open them and run each in turn.
|
|
59
|
+
|
|
60
|
+
```
|
|
61
|
+
Installed 'genie-agents' → /Workspace/Users/you@corp.com/databricks360/genie-agents
|
|
62
|
+
catalog: mfg tier: small
|
|
63
|
+
|
|
64
|
+
Run these in order:
|
|
65
|
+
1. 01_catalog_and_schemas
|
|
66
|
+
2. 02_dimensions
|
|
67
|
+
3. 03_facts (slow)
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Options
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
academy.install(
|
|
74
|
+
'genie-agents',
|
|
75
|
+
path='/Workspace/Shared/labs', # default: your home folder
|
|
76
|
+
catalog='training_v2', # default: the course's own catalog
|
|
77
|
+
tier='large', # default: 'small'
|
|
78
|
+
overwrite=True, # replace existing notebooks
|
|
79
|
+
)
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Why it does not run the notebooks for you
|
|
83
|
+
|
|
84
|
+
`dbdemos` starts a job and loads the data on your behalf. This deliberately does
|
|
85
|
+
not. Generating the data is the substance of Module 0 — the point is to watch a
|
|
86
|
+
warehouse chew through 20M rows and see the flaws appear, not to have a finished
|
|
87
|
+
catalog materialise. It also means nothing consumes your DBUs without you asking.
|
|
88
|
+
|
|
89
|
+
## Tiers
|
|
90
|
+
|
|
91
|
+
| Tier | Transactions | Use |
|
|
92
|
+
|---|---|---|
|
|
93
|
+
| `small` | 20M | default. Every module except 13 |
|
|
94
|
+
| `large` | 900M | Module 13 (latency) only. Left unclustered on purpose |
|
|
95
|
+
|
|
96
|
+
Start small. The large tier exists because you cannot measure query latency on a
|
|
97
|
+
toy dataset, and nowhere else needs it.
|
|
98
|
+
|
|
99
|
+
## Adding a course
|
|
100
|
+
|
|
101
|
+
Each course is a subpackage under `databricks360/courses/`:
|
|
102
|
+
|
|
103
|
+
```
|
|
104
|
+
databricks360/courses/<course_id>/
|
|
105
|
+
__init__.py
|
|
106
|
+
manifest.json # title, default catalog, tiers, notebooks in run order
|
|
107
|
+
*.sql # synced from content/courses/<id>/assets/lab/
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Placeholders available in the SQL: `{{CATALOG}}`, plus anything declared under a
|
|
111
|
+
tier's `values` (currently `{{TXN_COUNT}}`). An unresolved placeholder raises rather
|
|
112
|
+
than silently rendering empty.
|
|
113
|
+
|
|
114
|
+
Dataset documentation lives in [`docs/`](docs/).
|
|
115
|
+
|
|
116
|
+
## Publishing
|
|
117
|
+
|
|
118
|
+
Releases go to PyPI via **Trusted Publishing** — GitHub Actions authenticates to
|
|
119
|
+
PyPI with a short-lived OIDC identity, so no API token exists in repo secrets or on
|
|
120
|
+
anyone's laptop. A leaked token is the usual way a package supply chain gets
|
|
121
|
+
compromised; the safest token is one that was never created.
|
|
122
|
+
|
|
123
|
+
### One-time PyPI setup
|
|
124
|
+
|
|
125
|
+
1. Sign in at [pypi.org](https://pypi.org) → **Your account → Publishing**
|
|
126
|
+
2. Under *Add a new pending publisher*, choose **GitHub** and enter exactly:
|
|
127
|
+
|
|
128
|
+
| Field | Value |
|
|
129
|
+
|---|---|
|
|
130
|
+
| PyPI Project Name | `databricks360` |
|
|
131
|
+
| Owner | `databrickslms` |
|
|
132
|
+
| Repository name | `dbxdemos` |
|
|
133
|
+
| Workflow name | `publish.yml` |
|
|
134
|
+
| Environment name | `pypi` |
|
|
135
|
+
|
|
136
|
+
3. In GitHub → **Settings → Environments → New environment** → name it `pypi`.
|
|
137
|
+
Add yourself as a required reviewer if you want to approve each release.
|
|
138
|
+
|
|
139
|
+
"Pending" publisher is correct — the project does not exist on PyPI yet, and the
|
|
140
|
+
first successful run creates it.
|
|
141
|
+
|
|
142
|
+
### Cutting a release
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
# bump version in pyproject.toml, commit, then:
|
|
146
|
+
git tag v0.1.0
|
|
147
|
+
git push origin v0.1.0
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
The workflow runs the tests, builds, checks the tag matches `pyproject.toml`, and
|
|
151
|
+
publishes. A mismatched tag fails before anything reaches the index — versions on
|
|
152
|
+
PyPI are immutable, so a wrong number cannot be taken back, only yanked.
|
|
153
|
+
|
|
154
|
+
### Publishing by hand instead
|
|
155
|
+
|
|
156
|
+
```bash
|
|
157
|
+
python -m pip install build twine
|
|
158
|
+
python -m build
|
|
159
|
+
twine check dist/*
|
|
160
|
+
twine upload dist/* # prompts for an API token
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Test it against TestPyPI first if you want a dry run:
|
|
164
|
+
`twine upload --repository testpypi dist/*`.
|
|
165
|
+
|
|
166
|
+
## Tests
|
|
167
|
+
|
|
168
|
+
```bash
|
|
169
|
+
python3 run_tests.py
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Eleven tests, no workspace required: manifest loading, notebook cell structure,
|
|
173
|
+
catalog substitution, tier switching, unresolved-placeholder detection, and a
|
|
174
|
+
`dry_run` install. It also asserts the flaw-teaching column comments survive into
|
|
175
|
+
the generated notebooks — those comments are the curriculum, so losing them in
|
|
176
|
+
rendering would be a silent failure.
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# databricks360
|
|
2
|
+
|
|
3
|
+
Installs Databricks course lab environments — notebooks, catalogs, datasets and
|
|
4
|
+
governance objects — into your own workspace.
|
|
5
|
+
|
|
6
|
+
Modelled on `dbdemos`: you run it **inside a Databricks notebook**, so
|
|
7
|
+
`databricks-sdk` picks up the notebook's own identity. There is no host, token or
|
|
8
|
+
profile to configure.
|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
Not on PyPI yet, so install from the repo. In a **Databricks notebook**:
|
|
13
|
+
|
|
14
|
+
```python
|
|
15
|
+
%pip install git+https://github.com/databrickslms/dbxdemos.git
|
|
16
|
+
dbutils.library.restartPython()
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Once published, that becomes `%pip install databricks360`.
|
|
20
|
+
|
|
21
|
+
To pin a version, append a tag or commit:
|
|
22
|
+
|
|
23
|
+
```python
|
|
24
|
+
%pip install git+https://github.com/databrickslms/dbxdemos.git@v0.1.0
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Usage
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
import databricks360 as academy
|
|
31
|
+
|
|
32
|
+
academy.list_courses()
|
|
33
|
+
academy.install('genie-agents')
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
`install` writes the lab notebooks into your workspace and prints the run order.
|
|
37
|
+
Then you open them and run each in turn.
|
|
38
|
+
|
|
39
|
+
```
|
|
40
|
+
Installed 'genie-agents' → /Workspace/Users/you@corp.com/databricks360/genie-agents
|
|
41
|
+
catalog: mfg tier: small
|
|
42
|
+
|
|
43
|
+
Run these in order:
|
|
44
|
+
1. 01_catalog_and_schemas
|
|
45
|
+
2. 02_dimensions
|
|
46
|
+
3. 03_facts (slow)
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### Options
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
academy.install(
|
|
53
|
+
'genie-agents',
|
|
54
|
+
path='/Workspace/Shared/labs', # default: your home folder
|
|
55
|
+
catalog='training_v2', # default: the course's own catalog
|
|
56
|
+
tier='large', # default: 'small'
|
|
57
|
+
overwrite=True, # replace existing notebooks
|
|
58
|
+
)
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Why it does not run the notebooks for you
|
|
62
|
+
|
|
63
|
+
`dbdemos` starts a job and loads the data on your behalf. This deliberately does
|
|
64
|
+
not. Generating the data is the substance of Module 0 — the point is to watch a
|
|
65
|
+
warehouse chew through 20M rows and see the flaws appear, not to have a finished
|
|
66
|
+
catalog materialise. It also means nothing consumes your DBUs without you asking.
|
|
67
|
+
|
|
68
|
+
## Tiers
|
|
69
|
+
|
|
70
|
+
| Tier | Transactions | Use |
|
|
71
|
+
|---|---|---|
|
|
72
|
+
| `small` | 20M | default. Every module except 13 |
|
|
73
|
+
| `large` | 900M | Module 13 (latency) only. Left unclustered on purpose |
|
|
74
|
+
|
|
75
|
+
Start small. The large tier exists because you cannot measure query latency on a
|
|
76
|
+
toy dataset, and nowhere else needs it.
|
|
77
|
+
|
|
78
|
+
## Adding a course
|
|
79
|
+
|
|
80
|
+
Each course is a subpackage under `databricks360/courses/`:
|
|
81
|
+
|
|
82
|
+
```
|
|
83
|
+
databricks360/courses/<course_id>/
|
|
84
|
+
__init__.py
|
|
85
|
+
manifest.json # title, default catalog, tiers, notebooks in run order
|
|
86
|
+
*.sql # synced from content/courses/<id>/assets/lab/
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Placeholders available in the SQL: `{{CATALOG}}`, plus anything declared under a
|
|
90
|
+
tier's `values` (currently `{{TXN_COUNT}}`). An unresolved placeholder raises rather
|
|
91
|
+
than silently rendering empty.
|
|
92
|
+
|
|
93
|
+
Dataset documentation lives in [`docs/`](docs/).
|
|
94
|
+
|
|
95
|
+
## Publishing
|
|
96
|
+
|
|
97
|
+
Releases go to PyPI via **Trusted Publishing** — GitHub Actions authenticates to
|
|
98
|
+
PyPI with a short-lived OIDC identity, so no API token exists in repo secrets or on
|
|
99
|
+
anyone's laptop. A leaked token is the usual way a package supply chain gets
|
|
100
|
+
compromised; the safest token is one that was never created.
|
|
101
|
+
|
|
102
|
+
### One-time PyPI setup
|
|
103
|
+
|
|
104
|
+
1. Sign in at [pypi.org](https://pypi.org) → **Your account → Publishing**
|
|
105
|
+
2. Under *Add a new pending publisher*, choose **GitHub** and enter exactly:
|
|
106
|
+
|
|
107
|
+
| Field | Value |
|
|
108
|
+
|---|---|
|
|
109
|
+
| PyPI Project Name | `databricks360` |
|
|
110
|
+
| Owner | `databrickslms` |
|
|
111
|
+
| Repository name | `dbxdemos` |
|
|
112
|
+
| Workflow name | `publish.yml` |
|
|
113
|
+
| Environment name | `pypi` |
|
|
114
|
+
|
|
115
|
+
3. In GitHub → **Settings → Environments → New environment** → name it `pypi`.
|
|
116
|
+
Add yourself as a required reviewer if you want to approve each release.
|
|
117
|
+
|
|
118
|
+
"Pending" publisher is correct — the project does not exist on PyPI yet, and the
|
|
119
|
+
first successful run creates it.
|
|
120
|
+
|
|
121
|
+
### Cutting a release
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
# bump version in pyproject.toml, commit, then:
|
|
125
|
+
git tag v0.1.0
|
|
126
|
+
git push origin v0.1.0
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
The workflow runs the tests, builds, checks the tag matches `pyproject.toml`, and
|
|
130
|
+
publishes. A mismatched tag fails before anything reaches the index — versions on
|
|
131
|
+
PyPI are immutable, so a wrong number cannot be taken back, only yanked.
|
|
132
|
+
|
|
133
|
+
### Publishing by hand instead
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
python -m pip install build twine
|
|
137
|
+
python -m build
|
|
138
|
+
twine check dist/*
|
|
139
|
+
twine upload dist/* # prompts for an API token
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Test it against TestPyPI first if you want a dry run:
|
|
143
|
+
`twine upload --repository testpypi dist/*`.
|
|
144
|
+
|
|
145
|
+
## Tests
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
python3 run_tests.py
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Eleven tests, no workspace required: manifest loading, notebook cell structure,
|
|
152
|
+
catalog substitution, tier switching, unresolved-placeholder detection, and a
|
|
153
|
+
`dry_run` install. It also asserts the flaw-teaching column comments survive into
|
|
154
|
+
the generated notebooks — those comments are the curriculum, so losing them in
|
|
155
|
+
rendering would be a silent failure.
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""Lakehouse Academy — install Databricks course lab environments.
|
|
2
|
+
|
|
3
|
+
Run inside a Databricks notebook:
|
|
4
|
+
|
|
5
|
+
%pip install databricks360
|
|
6
|
+
dbutils.library.restartPython()
|
|
7
|
+
|
|
8
|
+
import databricks360 as academy
|
|
9
|
+
academy.list_courses()
|
|
10
|
+
academy.install('genie-agents')
|
|
11
|
+
|
|
12
|
+
`install` writes the lab notebooks into your workspace. You then run them in
|
|
13
|
+
order. Nothing is executed for you: generating the data is real work on your
|
|
14
|
+
warehouse, and watching it happen is part of the lesson.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from ._catalog import Course, available_courses, get_course
|
|
20
|
+
from ._install import Installation, build_notebook_source, install
|
|
21
|
+
|
|
22
|
+
__version__ = "0.1.0"
|
|
23
|
+
__all__ = [
|
|
24
|
+
"install",
|
|
25
|
+
"list_courses",
|
|
26
|
+
"get_course",
|
|
27
|
+
"available_courses",
|
|
28
|
+
"build_notebook_source",
|
|
29
|
+
"Course",
|
|
30
|
+
"Installation",
|
|
31
|
+
"__version__",
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def list_courses() -> None:
|
|
36
|
+
"""Print the available course labs."""
|
|
37
|
+
courses = available_courses()
|
|
38
|
+
if not courses:
|
|
39
|
+
print("No courses bundled in this build.")
|
|
40
|
+
return
|
|
41
|
+
|
|
42
|
+
for course in courses:
|
|
43
|
+
print(f"\n{course.id}")
|
|
44
|
+
print(f" {course.title}")
|
|
45
|
+
if course.description:
|
|
46
|
+
for line in _wrap(course.description, 76):
|
|
47
|
+
print(f" {line}")
|
|
48
|
+
print(f" catalog: {course.default_catalog} notebooks: {len(course.notebooks)}")
|
|
49
|
+
if course.tiers:
|
|
50
|
+
print(" tiers:")
|
|
51
|
+
for name, tier in course.tiers.items():
|
|
52
|
+
marker = " (default)" if name == course.default_tier else ""
|
|
53
|
+
print(f" {name}{marker} — {tier.description}")
|
|
54
|
+
print(f"\nInstall with: academy.install('{courses[0].id}')\n")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _wrap(text: str, width: int) -> list[str]:
|
|
58
|
+
words, lines, current = text.split(), [], ""
|
|
59
|
+
for word in words:
|
|
60
|
+
if len(current) + len(word) + 1 > width:
|
|
61
|
+
lines.append(current)
|
|
62
|
+
current = word
|
|
63
|
+
else:
|
|
64
|
+
current = f"{current} {word}".strip()
|
|
65
|
+
if current:
|
|
66
|
+
lines.append(current)
|
|
67
|
+
return lines
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""Course discovery: reads the manifests bundled with the package."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from importlib import resources
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass(frozen=True)
|
|
12
|
+
class Notebook:
|
|
13
|
+
"""One notebook in a course's lab, in run order."""
|
|
14
|
+
|
|
15
|
+
order: int
|
|
16
|
+
name: str
|
|
17
|
+
sql: str
|
|
18
|
+
title: str
|
|
19
|
+
intro: str = ""
|
|
20
|
+
requires_admin: bool = False
|
|
21
|
+
slow: bool = False
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass(frozen=True)
|
|
25
|
+
class Tier:
|
|
26
|
+
name: str
|
|
27
|
+
values: dict
|
|
28
|
+
description: str = ""
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass(frozen=True)
|
|
32
|
+
class Course:
|
|
33
|
+
id: str
|
|
34
|
+
title: str
|
|
35
|
+
description: str
|
|
36
|
+
default_catalog: str
|
|
37
|
+
notebooks: list = field(default_factory=list)
|
|
38
|
+
tiers: dict = field(default_factory=dict)
|
|
39
|
+
default_tier: str = "small"
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def package(self) -> str:
|
|
43
|
+
return f"databricks360.courses.{self.id.replace('-', '_')}"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _course_packages() -> list:
|
|
47
|
+
root = resources.files("databricks360.courses")
|
|
48
|
+
return sorted(
|
|
49
|
+
p.name
|
|
50
|
+
for p in root.iterdir()
|
|
51
|
+
if p.is_dir() and not p.name.startswith("_") and (p / "manifest.json").is_file()
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _load(pkg_name: str) -> Course:
|
|
56
|
+
raw: dict = json.loads(
|
|
57
|
+
(resources.files(f"databricks360.courses.{pkg_name}") / "manifest.json")
|
|
58
|
+
.read_text(encoding="utf-8")
|
|
59
|
+
)
|
|
60
|
+
return Course(
|
|
61
|
+
id=raw["id"],
|
|
62
|
+
title=raw["title"],
|
|
63
|
+
description=raw.get("description", ""),
|
|
64
|
+
default_catalog=raw.get("default_catalog", "main"),
|
|
65
|
+
default_tier=raw.get("default_tier", "small"),
|
|
66
|
+
notebooks=[
|
|
67
|
+
Notebook(
|
|
68
|
+
order=n["order"],
|
|
69
|
+
name=n["name"],
|
|
70
|
+
sql=n["sql"],
|
|
71
|
+
title=n["title"],
|
|
72
|
+
intro=n.get("intro", ""),
|
|
73
|
+
requires_admin=n.get("requires_admin", False),
|
|
74
|
+
slow=n.get("slow", False),
|
|
75
|
+
)
|
|
76
|
+
for n in sorted(raw["notebooks"], key=lambda n: n["order"])
|
|
77
|
+
],
|
|
78
|
+
tiers={
|
|
79
|
+
name: Tier(name=name, values=t["values"], description=t.get("description", ""))
|
|
80
|
+
for name, t in raw.get("tiers", {}).items()
|
|
81
|
+
},
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def available_courses() -> list:
|
|
86
|
+
return [_load(p) for p in _course_packages()]
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def get_course(course_id: str) -> Course:
|
|
90
|
+
wanted = course_id.replace("-", "_")
|
|
91
|
+
for pkg in _course_packages():
|
|
92
|
+
if pkg == wanted:
|
|
93
|
+
return _load(pkg)
|
|
94
|
+
known = ", ".join(c.id for c in available_courses()) or "none"
|
|
95
|
+
raise ValueError(f"Unknown course {course_id!r}. Available: {known}")
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def read_sql(course: Course, filename: str) -> str:
|
|
99
|
+
return (resources.files(course.package) / filename).read_text(encoding="utf-8")
|