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.
@@ -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,9 @@
1
+ __pycache__/
2
+ *.pyc
3
+ *.egg-info/
4
+ dist/
5
+ build/
6
+ .pytest_cache/
7
+ .venv/
8
+ venv/
9
+ .DS_Store
@@ -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")