From f2e46f78ec41a0e09cc9ca0594156cdaa63e9244 Mon Sep 17 00:00:00 2001 From: Andre Date: Mon, 2 Feb 2026 10:39:05 -0300 Subject: [PATCH 1/9] update DBR and force cli version --- .gitignore | 13 +++++-------- Makefile | 4 ---- README.md | 15 ++++++++------- databricks.yml | 1 + pyproject.toml | 8 ++++---- resources/wf_template.yml | 4 ++-- 6 files changed, 20 insertions(+), 25 deletions(-) diff --git a/.gitignore b/.gitignore index ffe59e1..fb2a185 100644 --- a/.gitignore +++ b/.gitignore @@ -2,17 +2,14 @@ notes/ .databricks/ .vscode/ .venv/ -*.pyc -*.lock -__pycache__/ +.claude/ .pytest_cache/ dist/ build/ -output/ coverage_reports/ -.claude/ src/template.egg-info/ +CLAUDE.md +*.pyc +*.lock +.coverage* resources/workflow.yml -.coverage -.coverage.* -CLAUDE.md \ No newline at end of file diff --git a/Makefile b/Makefile index 4312ada..490a572 100644 --- a/Makefile +++ b/Makefile @@ -10,10 +10,6 @@ pre-commit: pre-commit autoupdate pre-commit run --all-files -deploy: - uv run python ./scripts/generate_template_workflow.py $(env) - uv run databricks bundle deploy --target $(env) - deploy-serverless: uv run python ./scripts/generate_template_workflow.py $(env) --serverless uv run databricks bundle deploy --target $(env) diff --git a/README.md b/README.md index 5907811..5a0639c 100644 --- a/README.md +++ b/README.md @@ -50,17 +50,14 @@ This project template demonstrates how to: - utilize [Databricks Asset Bundles](https://docs.databricks.com/en/dev-tools/bundles/index.html) to package/deploy/run a Python wheel package on Databricks. - utilize [Databricks DQX](https://databrickslabs.github.io/dqx/) to define and enforce data quality rules, such as null checks, uniqueness, thresholds, and schema validation. -- utilize [Databricks SDK for Python](https://docs.databricks.com/en/dev-tools/sdk-python.html) to manage workspaces and accounts. The sample script enables metastore system tables with [relevant data about billing, usage, lineage, prices, and access](https://www.youtube.com/watch?v=LcRWHzk8Wm4). +- utilize [Databricks SDK for Python](https://docs.databricks.com/en/dev-tools/sdk-python.html) to manage workspaces and accounts and analyse costs. - utilize [Databricks Unity Catalog](https://www.databricks.com/product/unity-catalog) and get data lineage for your tables and columns and a simplified permission model for your data. - utilize [Databricks Lakeflow Jobs](https://docs.databricks.com/en/workflows/index.html) to execute a DAG and [task parameters](https://docs.databricks.com/en/workflows/jobs/parameter-value-references.html) to share context information between tasks (see [Task Parameters section](#task-parameters)). Yes, you don't need Airflow to manage your DAGs here!!! -- **utilize serverless clusters on Databricks Free Edition to deploy your pipelines.** -- utilize [Databricks job clusters](https://docs.databricks.com/en/workflows/jobs/use-compute.html#use-databricks-compute-with-your-jobs) to reduce costs. +- **utilize serverless job clusters on Databricks Free Edition to deploy your pipelines.** - define Databricks clusters on AWS and Azure. ## 🧠 Resources -- [Goodbye Pip and Poetry. Why UV Might Be All You Need](https://codecut.ai/why-uv-might-all-you-need/) - For a debate on the use of notebooks vs. Python packaging, please refer to: - [The Rise of The Notebook Engineer](https://dataengineeringcentral.substack.com/p/the-rise-of-the-notebook-engineer) - [Please don’t make me use Databricks notebooks](https://medium.com/@seade03/please-dont-make-me-use-databricks-notebooks-3d07a4a332ae) @@ -73,7 +70,11 @@ Sessions on Databricks Asset Bundles, CI/CD, and Software Development Life Cycle - [Deploying Databricks Asset Bundles (DABs) at Scale](https://www.youtube.com/watch?v=mMwprgB-sIU) - [A Prescription for Success: Leveraging DABs for Faster Deployment and Better Patient Outcomes](https://www.youtube.com/watch?v=01JHTM2UP-U) -## Jobs (former Workflows) +Other: +- [Goodbye Pip and Poetry. Why UV Might Be All You Need](https://codecut.ai/why-uv-might-all-you-need/) + + +## Jobs
@@ -89,7 +90,7 @@ Sessions on Databricks Asset Bundles, CI/CD, and Software Development Life Cycle
-## Data Lineage (Catalog Explorer) +## Data Lineage
diff --git a/databricks.yml b/databricks.yml index 6bc5d95..2edb70a 100644 --- a/databricks.yml +++ b/databricks.yml @@ -2,6 +2,7 @@ # See https://docs.databricks.com/dev-tools/bundles/index.html for documentation. bundle: name: default_python + databricks_cli_version: ">=0.236.0" artifacts: default: diff --git a/pyproject.toml b/pyproject.toml index e2bfde1..689e77c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,23 +6,23 @@ authors = [ {name = "user@company.com"} ] readme = "README.md" -requires-python = ">=3.12" +requires-python = "==3.12" dependencies = [ "funcy==2.0", - "databricks-labs-dqx==0.9.3", + "databricks-labs-dqx==0.12.0", ] [project.optional-dependencies] dev = [ "numpy==2.1.3", "pandas==2.2.3", - "pyarrow==19.0.1", + "pyarrow==21.0.0", "pydantic==2.10.6", "coverage==7.6.1", "pre-commit==4.0.1", "pytest==8.3.5", "pytest-cov==5.0.0", - "pyspark==4.0.0", + "pyspark==4.1.0", "jinja2==3.1.5", ] diff --git a/resources/wf_template.yml b/resources/wf_template.yml index 8f02ff1..720a904 100644 --- a/resources/wf_template.yml +++ b/resources/wf_template.yml @@ -72,7 +72,7 @@ resources: job_clusters: # - job_cluster_key: cluster-dev-azure # new_cluster: - # spark_version: 15.3.x-scala2.12 + # spark_version: 18.0.x-scala2.13 # node_type_id: Standard_D8as_v5 # num_workers: 1 # azure_attributes: @@ -82,7 +82,7 @@ resources: - job_cluster_key: cluster-dev-aws new_cluster: - spark_version: 14.2.x-scala2.12 + spark_version: 18.0.x-scala2.13 node_type_id: c5d.xlarge num_workers: 1 aws_attributes: From 8788de57736faf79b7b13802dde4e9499a14962f Mon Sep 17 00:00:00 2001 From: Andre Date: Mon, 2 Feb 2026 10:43:33 -0300 Subject: [PATCH 2/9] fix python version for ci/cd pipe --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 689e77c..409af2f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ authors = [ {name = "user@company.com"} ] readme = "README.md" -requires-python = "==3.12" +requires-python = "==3.12.*" dependencies = [ "funcy==2.0", "databricks-labs-dqx==0.12.0", From c413c7f389ed54949472062fb34286509fe45a33 Mon Sep 17 00:00:00 2001 From: Andre Date: Mon, 2 Feb 2026 16:52:06 -0300 Subject: [PATCH 3/9] update Databricks CLI --- databricks.yml | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/databricks.yml b/databricks.yml index 2edb70a..e3ced86 100644 --- a/databricks.yml +++ b/databricks.yml @@ -2,7 +2,7 @@ # See https://docs.databricks.com/dev-tools/bundles/index.html for documentation. bundle: name: default_python - databricks_cli_version: ">=0.236.0" + databricks_cli_version: ">=0.286.0" artifacts: default: diff --git a/pyproject.toml b/pyproject.toml index 409af2f..0162d41 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,7 +9,7 @@ readme = "README.md" requires-python = "==3.12.*" dependencies = [ "funcy==2.0", - "databricks-labs-dqx==0.12.0", + "databricks-labs-dqx==0.12.0", ] [project.optional-dependencies] From 7af1c5f3578f0181a4d2b87abe6a5e9c0c5ef961 Mon Sep 17 00:00:00 2001 From: Andre Date: Tue, 3 Feb 2026 11:06:42 -0300 Subject: [PATCH 4/9] sdk demos --- README.md | 94 +++++++++++++++++++++++++----------- scripts/sdk_system_tables.py | 77 ----------------------------- 2 files changed, 66 insertions(+), 105 deletions(-) delete mode 100644 scripts/sdk_system_tables.py diff --git a/README.md b/README.md index 5a0639c..fc72323 100644 --- a/README.md +++ b/README.md @@ -18,12 +18,14 @@ Interested in bringing these principles in your own project? Let’s [connect o ## πŸ§ͺ Technologies - Databricks Free Edition (Serverless) -- Databricks Runtime 17.3 LTS -- PySpark 4.0 -- Python 3.12+ -- Unity Catalog +- Databricks Runtime 18 LTS - Databricks Asset Bundles - Databricks DQX +- Databricks CLI +- Databricks Python SDK +- PySpark 4.1 +- Python 3.12+ +- Unity Catalog - GitHub Actions - Pytest @@ -32,8 +34,9 @@ Interested in bringing these principles in your own project? Let’s [connect o This project template demonstrates how to: - structure PySpark code inside classes/packages. -- structure unit tests for the data transformations and set up VSCode to run them on your local machine. +- run unit tests on transformations with [pytest package](https://pypi.org/project/pytest/) - set up VSCode to run unit tests on your local machine. - structure integration tests to be executed on different environments / catalogs. +- utilize [coverage package](https://pypi.org/project/coverage/) to generate test coverage reports. - package and deploy code to different environments (dev, staging, prod) using a CI/CD pipeline with [Github Actions](https://docs.github.com/en/actions). - isolate "dev" environments / catalogs to avoid concurrency issues between developers testing jobs. - utilize [uv](https://docs.astral.sh/uv/) as a project/package manager. @@ -42,19 +45,16 @@ This project template demonstrates how to: - use [medallion architecture](https://www.databricks.com/glossary/medallion-architecture) pattern. - lint and format code with [ruff](https://docs.astral.sh/ruff/) and [pre-commit](https://pre-commit.com/). - use a Make file to automate repetitive tasks. -- utilize [pytest package](https://pypi.org/project/pytest/) to run unit tests on transformations and generate test coverage reports. - utilize [argparse package](https://pypi.org/project/argparse/) to build a flexible command line interface to start the jobs. -- utilize [funcy package](https://pypi.org/project/funcy/) to log the execution time of each transformation.
- utilize [Databricks Asset Bundles](https://docs.databricks.com/en/dev-tools/bundles/index.html) to package/deploy/run a Python wheel package on Databricks. - utilize [Databricks DQX](https://databrickslabs.github.io/dqx/) to define and enforce data quality rules, such as null checks, uniqueness, thresholds, and schema validation. -- utilize [Databricks SDK for Python](https://docs.databricks.com/en/dev-tools/sdk-python.html) to manage workspaces and accounts and analyse costs. +- utilize [Databricks SDK for Python](https://docs.databricks.com/en/dev-tools/sdk-python.html) to manage workspaces and accounts and analyse costs. Refer to 'scripts' folder for some examples. - utilize [Databricks Unity Catalog](https://www.databricks.com/product/unity-catalog) and get data lineage for your tables and columns and a simplified permission model for your data. - utilize [Databricks Lakeflow Jobs](https://docs.databricks.com/en/workflows/index.html) to execute a DAG and [task parameters](https://docs.databricks.com/en/workflows/jobs/parameter-value-references.html) to share context information between tasks (see [Task Parameters section](#task-parameters)). Yes, you don't need Airflow to manage your DAGs here!!! -- **utilize serverless job clusters on Databricks Free Edition to deploy your pipelines.** -- define Databricks clusters on AWS and Azure. +- utilize serverless job clusters on [Databricks Free Edition](https://docs.databricks.com/aws/en/getting-started/free-edition ) to deploy your pipelines. ## 🧠 Resources @@ -73,6 +73,58 @@ Sessions on Databricks Asset Bundles, CI/CD, and Software Development Life Cycle Other: - [Goodbye Pip and Poetry. Why UV Might Be All You Need](https://codecut.ai/why-uv-might-all-you-need/) +## πŸ“ Folder Structure + +databricks-template/ +β”‚ +β”œβ”€β”€ .github/ # CI/CD automation +β”‚ └── workflows/ +β”‚ └── onpush.yml # GitHub Actions pipeline +β”‚ +β”œβ”€β”€ src/ # Main source code +β”‚ └── template/ # Python package +β”‚ β”œβ”€β”€ main.py # Entry point with CLI (argparse) +β”‚ β”œβ”€β”€ config.py # Configuration management +β”‚ β”œβ”€β”€ baseTask.py # Base class for all tasks +β”‚ β”œβ”€β”€ commonSchemas.py # Shared PySpark schemas +β”‚ └── job1/ # Job-specific tasks +β”‚ β”œβ”€β”€ extract_source1.py +β”‚ β”œβ”€β”€ extract_source2.py +β”‚ β”œβ”€β”€ generate_orders.py +β”‚ β”œβ”€β”€ generate_orders_agg.py +β”‚ β”œβ”€β”€ integration_setup.py +β”‚ └── integration_validate.py +β”‚ +β”œβ”€β”€ tests/ # Unit tests +β”‚ └── job1/ +β”‚ └── unit_test.py # Pytest unit tests +β”‚ +β”œβ”€β”€ resources/ # Databricks workflow templates +β”‚ β”œβ”€β”€ wf_template_serverless.yml # Jinja2 template for serverless +β”‚ β”œβ”€β”€ wf_template.yml # Jinja2 template for job clusters +β”‚ └── workflow.yml # Generated workflow (auto-created) +β”‚ +β”œβ”€β”€ scripts/ # Helper scripts +β”‚ β”œβ”€β”€ generate_template_workflow.py # Workflow generator (Jinja2) +β”‚ β”œβ”€β”€ sdk_analyze_job_costs.py # Cost analysis script +β”‚ └── sdk_workspace_and_account.py # Workspace management +β”‚ +β”œβ”€β”€ docs/ # Documentation assets +β”‚ β”œβ”€β”€ dag.png +β”‚ β”œβ”€β”€ task_output.png +β”‚ β”œβ”€β”€ data_lineage.png +β”‚ β”œβ”€β”€ data_quality.png +β”‚ └── ci_cd.png +β”‚ +β”œβ”€β”€ dist/ # Build artifacts (Python wheel) +β”œβ”€β”€ coverage_reports/ # Test coverage reports +β”‚ +β”œβ”€β”€ databricks.yml # Databricks Asset Bundle config +β”œβ”€β”€ pyproject.toml # Python project configuration (uv) +β”œβ”€β”€ Makefile # Build automation +β”œβ”€β”€ .pre-commit-config.yaml # Pre-commit hooks (ruff) +└── README.md # This file +``` ## Jobs @@ -92,7 +144,8 @@ Other: ## Data Lineage -
+
print("SUMMARY") + @@ -118,16 +171,10 @@ Other: ## Instructions -### 1) Create a Databricks Workspace - -option 1) utilize a [Databricks Free Edition](https://docs.databricks.com/aws/en/getting-started/free-edition) workspace. +### 1) Create a Databricks Workspace - utilize a [Databricks Free Edition](https://docs.databricks.com/aws/en/getting-started/free-edition) workspace. -option 2) create a Premium workspace. Follow instructions [here](https://github.com/databricks/terraform-databricks-examples) - -### 2) Install and configure Databricks CLI on your local machine - -Follow the instructions [here](https://docs.databricks.com/en/dev-tools/cli/install.html) +### 2) Install and configure Databricks CLI on your local machine - Follow instructions [here](https://docs.databricks.com/en/dev-tools/cli/install.html) ### 3) Build Python env and execute unit tests on your local machine @@ -147,15 +194,6 @@ option 1) for Databricks Free Edition use: make deploy-serverless env=prod -option 2) for Premium workspace: - - Update "job_clusters" properties on wf_template.yml file. There are different properties for AWS and Azure. - - make deploy env=dev - make deploy env=staging - make deploy env=prod - - ### 5) configure CI/CD automation Configure [Github Actions repository secrets](https://docs.github.com/en/actions/security-guides/using-secrets-in-github-actions) DATABRICKS_HOST and DATABRICKS_TOKEN. diff --git a/scripts/sdk_system_tables.py b/scripts/sdk_system_tables.py deleted file mode 100644 index 4f9b835..0000000 --- a/scripts/sdk_system_tables.py +++ /dev/null @@ -1,77 +0,0 @@ -from databricks.sdk import WorkspaceClient -from databricks.sdk import AccountClient -import requests - -host = None -token = None - - -def demoWorkspaceApi(w): - for j in w.jobs.list(): - print("Job: " + str(j.job_id)) - - for c in w.catalogs.list(): - print("Catalog: " + c.name) - schemas = w.schemas.list(catalog_name=c.name) - for s in schemas: - print(" Schema: " + s.name) - - -def demoAccountApi(): - a = AccountClient(profile="account") - - print(a) - - for m in a.metastores.list(): - print("Metastore: " + m.metastore_id) - metastore = m - - return metastore - - -def enable(system_tables, metastore): - print("Enabling " + system_tables + " tables for " + metastore.name + " ...") - - update = f"{host}/api/2.0/unity-catalog/metastores/{metastore.metastore_id}/systemschemas/{system_tables}" - response = requests.put(update, headers=token) - - if response.status_code == 200: - print("OK") - else: - print("Failed") - print(response.text) - - -def enableSystemTables(metastore): - enable("billing", metastore) - - enable("access", metastore) - - enable("storage", metastore) - - enable("compute", metastore) - - enable("marketplace", metastore) - - enable("lineage", metastore) - - list = f"{host}/api/2.0/unity-catalog/metastores/a238eb20-95d3-4a62-91ea-629992514227/systemschemas" - response = requests.get(list, headers=token) - print(response.text) - - -if __name__ == "__main__": - workspace = WorkspaceClient( - profile="dev" # as configured in .databrickscfg - ) - - print(workspace) - - token = workspace.config.authenticate() - host = workspace.config.host - - demoWorkspaceApi(workspace) - - metastore = demoAccountApi() - - enableSystemTables(metastore) From e4523a218ca127ee699242782ad2db8d955626a9 Mon Sep 17 00:00:00 2001 From: Andre Date: Tue, 3 Feb 2026 11:11:45 -0300 Subject: [PATCH 5/9] fix readme --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index fc72323..51efcd9 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,7 @@ This project template demonstrates how to:
- utilize [Databricks Asset Bundles](https://docs.databricks.com/en/dev-tools/bundles/index.html) to package/deploy/run a Python wheel package on Databricks. -- utilize [Databricks DQX](https://databrickslabs.github.io/dqx/) to define and enforce data quality rules, such as null checks, uniqueness, thresholds, and schema validation. +- utilize [Databricks DQX](https://databrickslabs.github.io/dqx/) to define and enforce data quality rules, such as null checks, uniqueness, thresholds, and schema validation, and filter bad data on quarantine tables. - utilize [Databricks SDK for Python](https://docs.databricks.com/en/dev-tools/sdk-python.html) to manage workspaces and accounts and analyse costs. Refer to 'scripts' folder for some examples. - utilize [Databricks Unity Catalog](https://www.databricks.com/product/unity-catalog) and get data lineage for your tables and columns and a simplified permission model for your data. - utilize [Databricks Lakeflow Jobs](https://docs.databricks.com/en/workflows/index.html) to execute a DAG and [task parameters](https://docs.databricks.com/en/workflows/jobs/parameter-value-references.html) to share context information between tasks (see [Task Parameters section](#task-parameters)). Yes, you don't need Airflow to manage your DAGs here!!! @@ -75,6 +75,7 @@ Other: ## πŸ“ Folder Structure +``` databricks-template/ β”‚ β”œβ”€β”€ .github/ # CI/CD automation @@ -144,7 +145,7 @@ databricks-template/ ## Data Lineage -
print("SUMMARY") +
From 66f08bed912ef67566e4d60878f8f1776dc72ca9 Mon Sep 17 00:00:00 2001 From: Andre Date: Tue, 3 Feb 2026 11:13:57 -0300 Subject: [PATCH 6/9] sdk demos --- scripts/sdk_analyze_job_costs.py | 231 +++++++++++++++++++++++++++ scripts/sdk_workspace_and_account.py | 34 ++++ 2 files changed, 265 insertions(+) create mode 100644 scripts/sdk_analyze_job_costs.py create mode 100644 scripts/sdk_workspace_and_account.py diff --git a/scripts/sdk_analyze_job_costs.py b/scripts/sdk_analyze_job_costs.py new file mode 100644 index 0000000..e39b735 --- /dev/null +++ b/scripts/sdk_analyze_job_costs.py @@ -0,0 +1,231 @@ +""" +Analyze costs of Databricks jobs run today. + +This script queries Databricks system tables to calculate costs for jobs +executed today, providing insights into compute usage and billing. + +Usage: + python scripts/analyze_job_costs.py [--profile PROFILE] [--date DATE] + +Examples: + python scripts/analyze_job_costs.py + python scripts/analyze_job_costs.py --profile dev + python scripts/analyze_job_costs.py --date 2024-01-15 +""" + +import argparse +from datetime import datetime, timedelta +from databricks.sdk import WorkspaceClient +from databricks.sdk.service.sql import StatementState +import time + + +def get_todays_date(): + """Get today's date in YYYY-MM-DD format.""" + return datetime.now().strftime("%Y-%m-%d") + + +def analyze_job_costs(workspace: WorkspaceClient, target_date: str): + """ + Analyze job costs for a specific date using system tables. + + Args: + workspace: Databricks WorkspaceClient instance + target_date: Date to analyze in YYYY-MM-DD format + """ + print(f"\n{'=' * 80}") + print(f"Job Cost Analysis for {target_date}") + print(f"{'=' * 80}\n") + + # Query to get usage data from system tables + # Note: This assumes system tables are enabled for the metastore + query = f""" + SELECT + u.usage_metadata.job_id, + u.usage_metadata.job_name, + u.sku_name, + u.cloud, + SUM(usage_quantity) as total_dbu, + COUNT(DISTINCT u.usage_metadata.job_run_id) as num_runs, + ROUND(SUM(usage_quantity * list_prices.pricing.default), 2) as estimated_cost_usd + FROM + system.billing.usage u + LEFT JOIN + system.billing.list_prices ON u.sku_name = list_prices.sku_name + AND u.cloud = list_prices.cloud + WHERE + usage_date = '{target_date}' + AND + u.usage_metadata.job_id IS NOT NULL + GROUP BY + u.usage_metadata.job_id, + u.usage_metadata.job_name, + u.sku_name, + u.cloud + ORDER BY + estimated_cost_usd DESC + """ + + try: + print("Querying system.billing.usage table...\n") + result = workspace.statement_execution.execute_statement( + warehouse_id=get_warehouse_id(workspace), + statement=query, + wait_timeout="0s", # async + ) + + # Poll for result completion + while True: + result = workspace.statement_execution.get_statement(result.statement_id) + print("Waiting for query to complete...") + state = result.status.state + print(state) + if state in [StatementState.SUCCEEDED, StatementState.FAILED, StatementState.CANCELED]: + break + time.sleep(1) + + # print(result.result) + # print("-----") + # print(result.result.data_array) + + # Process and display results + if result.result and result.result.data_array: + display_job_costs(result.result.data_array) + display_summary(result.result.data_array) + else: + print(f"No job runs found for {target_date}") + + except Exception as e: + print(f"Error querying system tables: {e}") + fallback_analysis(workspace, target_date) + + +def get_warehouse_id(workspace: WorkspaceClient) -> str: + """Get the first available SQL warehouse ID.""" + warehouses = list(workspace.warehouses.list()) + if not warehouses: + raise ValueError("No SQL warehouse found. Please create one to query system tables.") + return warehouses[0].id + + +def display_job_costs(data_array): + print(f"{'Job ID':<15} {'Job Name':<40} {'SKU':<40} {'Runs':>8} {'DBUs':>10} {'Cost (USD)':>12}") + print("-" * 110) + + for row in data_array: + job_id = row[0] or "N/A" + job_name = row[1] or "Unnamed" + sku = row[2] or "Unnamed" + total_dbu = float(row[4] or 0) + num_runs = int(row[5] or 0) + cost = float(row[6] or 0) + + print(f"{str(job_id):<15} {job_name:<40.40} {sku:<40.40} {num_runs:>8} {total_dbu:>10.2f} ${cost:>11.2f}") + + +def display_summary(data_array): + print("\nSUMMARY") + print("=" * 80) + + total_jobs = len(set([row[0] for row in data_array if row[0]])) + total_dbu = sum([float(row[4] or 0) for row in data_array]) + # total_runs = sum([int(row[5] or 0) for row in data_array]) + total_cost = sum([float(row[6] or 0) for row in data_array]) + + print(f"Total Jobs: {total_jobs}") + # print(f"Total Runs: {total_runs}") + print(f"Total DBUs: {total_dbu:.2f}") + print(f"Total Cost: ${total_cost:.2f}") + print("=" * 80 + "\n") + + +def fallback_analysis(workspace: WorkspaceClient, target_date: str): + """ + Fallback method using Jobs API when system tables are unavailable. + Note: This doesn't provide cost data, only run information. + """ + print("\nFallback: Using Jobs API for run information (costs not available)\n") + + target_datetime = datetime.strptime(target_date, "%Y-%m-%d") + start_time_ms = int(target_datetime.timestamp() * 1000) + end_time_ms = int((target_datetime + timedelta(days=1)).timestamp() * 1000) + + print(f"{'Job ID':<15} {'Job Name':<40} {'Status':<15} {'Start Time':<20}") + print("-" * 95) + + job_count = 0 + run_count = 0 + + try: + # List all jobs + for job in workspace.jobs.list(): + # Get runs for this job + runs = workspace.jobs.list_runs(job_id=job.job_id, start_time_from=start_time_ms, start_time_to=end_time_ms) + + for run in runs: + if run.start_time and start_time_ms <= run.start_time < end_time_ms: + job_name = job.settings.name if job.settings and job.settings.name else "Unnamed" + if len(job_name) > 38: + job_name = job_name[:35] + "..." + + start_time = datetime.fromtimestamp(run.start_time / 1000).strftime("%Y-%m-%d %H:%M:%S") + status = run.state.life_cycle_state if run.state else "UNKNOWN" + + print(f"{job.job_id:<15} {job_name:<40} {status:<15} {start_time:<20}") + run_count += 1 + + if runs: + job_count += 1 + + print(f"\nFound {run_count} runs across {job_count} jobs") + print("Note: Cost information requires system.billing.usage table access\n") + + except Exception as e: + print(f"Error accessing Jobs API: {e}\n") + + +def main(): + """Main entry point for the script.""" + parser = argparse.ArgumentParser( + description="Analyze Databricks job costs for today", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" +Examples: + python scripts/analyze_job_costs.py + python scripts/analyze_job_costs.py --profile dev + python scripts/analyze_job_costs.py --date 2024-01-15 + """, + ) + + parser.add_argument("--profile", default="dev", help="Databricks CLI profile to use (default: dev)") + + parser.add_argument( + "--date", default=get_todays_date(), help="Date to analyze in YYYY-MM-DD format (default: today)" + ) + + args = parser.parse_args() + + # Validate date format + try: + datetime.strptime(args.date, "%Y-%m-%d") + except ValueError: + print(f"Error: Invalid date format '{args.date}'. Use YYYY-MM-DD") + return 1 + + # Initialize Databricks workspace client + try: + workspace = WorkspaceClient(profile=args.profile) + print(f"Connected to: {workspace.config.host}") + except Exception as e: + print(f"Error connecting to Databricks: {e}") + print(f"Ensure profile '{args.profile}' exists in ~/.databrickscfg") + return 1 + + # Run cost analysis + analyze_job_costs(workspace, args.date) + + return 0 + + +if __name__ == "__main__": + exit(main()) diff --git a/scripts/sdk_workspace_and_account.py b/scripts/sdk_workspace_and_account.py new file mode 100644 index 0000000..e1fc25b --- /dev/null +++ b/scripts/sdk_workspace_and_account.py @@ -0,0 +1,34 @@ +from databricks.sdk import WorkspaceClient +from databricks.sdk import AccountClient +import requests + + +def demoWorkspaceApi(): + workspace = WorkspaceClient( + profile="dev" # as configured in .databrickscfg + ) + + for j in workspace.jobs.list(): + print("Job: " + str(j.job_id)) + + for c in workspace.catalogs.list(): + print("Catalog: " + c.name) + schemas = workspace.schemas.list(catalog_name=c.name) + for s in schemas: + print(" Schema: " + s.name) + + +def demoAccountApi(): + # you need to run "databricks auth login -p " first... + a = AccountClient(profile="account1") + + print(a) + + for w in a.workspaces.list(): + print("Workspace: " + w.workspace_name) + + +if __name__ == "__main__": + demoWorkspaceApi() + + demoAccountApi() From 9ff8abffe8cf19945bb4326e9b8e3e5c4ea85c46 Mon Sep 17 00:00:00 2001 From: Andre Date: Tue, 3 Feb 2026 11:18:44 -0300 Subject: [PATCH 7/9] readme --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 51efcd9..744c9b1 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@ # databricks-template -> A production-ready PySpark project template with medallion architecture, Python packaging, unit tests, integration tests, CI/CD automation, Databricks Asset Bundles, and DQX data quality framework. +> A production-ready PySpark project template with medallion architecture, Python packaging, unit tests, integration tests, coverage tests, CI/CD automation, Databricks Asset Bundles, and DQX data quality framework. ![Databricks](https://img.shields.io/badge/platform-Databricks-orange?logo=databricks) -![PySpark](https://img.shields.io/badge/pyspark-4.0+-brightgreen?logo=apache-spark) +![PySpark](https://img.shields.io/badge/pyspark-4.1+-brightgreen?logo=apache-spark) ![CI/CD](https://img.shields.io/github/actions/workflow/status/andre-salvati/databricks-template/.github/workflows/onpush.yml) ![Stars](https://img.shields.io/github/stars/andre-salvati/databricks-template?style=social) @@ -18,7 +18,7 @@ Interested in bringing these principles in your own project? Let’s [connect o ## πŸ§ͺ Technologies - Databricks Free Edition (Serverless) -- Databricks Runtime 18 LTS +- Databricks Runtime 18.0 LTS - Databricks Asset Bundles - Databricks DQX - Databricks CLI @@ -101,7 +101,7 @@ databricks-template/ β”‚ └── unit_test.py # Pytest unit tests β”‚ β”œβ”€β”€ resources/ # Databricks workflow templates -β”‚ β”œβ”€β”€ wf_template_serverless.yml # Jinja2 template for serverless +β”‚ β”œβ”€β”€ wf_template_serverless.yml # Jinja2 template print("SUMMARY")for serverless β”‚ β”œβ”€β”€ wf_template.yml # Jinja2 template for job clusters β”‚ └── workflow.yml # Generated workflow (auto-created) β”‚ @@ -109,7 +109,7 @@ databricks-template/ β”‚ β”œβ”€β”€ generate_template_workflow.py # Workflow generator (Jinja2) β”‚ β”œβ”€β”€ sdk_analyze_job_costs.py # Cost analysis script β”‚ └── sdk_workspace_and_account.py # Workspace management -β”‚ +β”‚ print("SUMMARY") β”œβ”€β”€ docs/ # Documentation assets β”‚ β”œβ”€β”€ dag.png β”‚ β”œβ”€β”€ task_output.png From bdcbe2add667106559acaf636b053cd36ffd9ef6 Mon Sep 17 00:00:00 2001 From: Andre Date: Tue, 3 Feb 2026 11:44:27 -0300 Subject: [PATCH 8/9] docs --- README.md | 21 ++++++++------------- docs/ci_cd.drawio | 42 ++++++++++++++++++------------------------ docs/ci_cd.png | Bin 46998 -> 31413 bytes 3 files changed, 26 insertions(+), 37 deletions(-) diff --git a/README.md b/README.md index 744c9b1..a63c0a1 100644 --- a/README.md +++ b/README.md @@ -172,32 +172,27 @@ databricks-template/ ## Instructions -### 1) Create a Databricks Workspace - utilize a [Databricks Free Edition](https://docs.databricks.com/aws/en/getting-started/free-edition) workspace. +1) Create a workspace. Use a [Databricks Free Edition](https://docs.databricks.com/aws/en/getting-started/free-edition) workspace. -### 2) Install and configure Databricks CLI on your local machine - Follow instructions [here](https://docs.databricks.com/en/dev-tools/cli/install.html) +2) Install and configure Databricks CLI on your local machine. Follow instructions [here](https://docs.databricks.com/en/dev-tools/cli/install.html). Check the current version on databricks.yaml. -### 3) Build Python env and execute unit tests on your local machine +3) Build Python env and execute unit tests on your local machine make sync & make test -You can also execute unit tests from your preferred IDE. Here's a screenshot from [VS Code](https://code.visualstudio.com/) with [Microsoft's Python extension](https://marketplace.visualstudio.com/items?itemName=ms-python.python) installed. - - -### 4) Deploy and execute on the dev workspace. - -option 1) for Databricks Free Edition use: +4) Deploy and execute on the dev workspace. make deploy-serverless env=dev - make deploy-serverless env=staging - make deploy-serverless env=prod -### 5) configure CI/CD automation +5) configure CI/CD automation. Configure [Github Actions repository secrets](https://docs.github.com/en/actions/security-guides/using-secrets-in-github-actions) (DATABRICKS_HOST and DATABRICKS_TOKEN). + +6) You can also execute unit tests from your preferred IDE. Here's a screenshot from [VS Code](https://code.visualstudio.com/) with [Microsoft's Python extension](https://marketplace.visualstudio.com/items?itemName=ms-python.python) installed. -Configure [Github Actions repository secrets](https://docs.github.com/en/actions/security-guides/using-secrets-in-github-actions) DATABRICKS_HOST and DATABRICKS_TOKEN. +- ## Task parameters diff --git a/docs/ci_cd.drawio b/docs/ci_cd.drawio index bb83077..6fe2533 100755 --- a/docs/ci_cd.drawio +++ b/docs/ci_cd.drawio @@ -1,17 +1,17 @@ - + - + - + - + @@ -21,30 +21,30 @@ - - + + - - + + - - + + - + - - + + - + @@ -55,17 +55,11 @@ - - + + - - - - - - - - + + diff --git a/docs/ci_cd.png b/docs/ci_cd.png index 82514978ccac5dda93daabbf8b147b671f6b4fce..7d63661cd38546d30323922b19349d40e336ef71 100644 GIT binary patch literal 31413 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

n4q_U;O+rO3UuoqqgaN5CtFhb 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 From d4b6290a8a8174471327e52d1928bfa8895596fc Mon Sep 17 00:00:00 2001 From: Andre Date: Thu, 5 Feb 2026 14:47:41 -0300 Subject: [PATCH 9/9] add tags for job observability --- .github/workflows/onpush.yml | 22 +++++- README.md | 4 +- resources/wf_template.yml | 95 -------------------------- resources/wf_template_serverless.yml | 12 ++++ scripts/generate_template_workflow.py | 96 ++++++++++++++++++++------- 5 files changed, 107 insertions(+), 122 deletions(-) delete mode 100644 resources/wf_template.yml diff --git a/.github/workflows/onpush.yml b/.github/workflows/onpush.yml index 85ca194..98cf18d 100644 --- a/.github/workflows/onpush.yml +++ b/.github/workflows/onpush.yml @@ -47,7 +47,16 @@ jobs: - name: Deploy on staging run: | - make deploy-serverless env=staging + BRANCH_NAME="${{ github.head_ref || github.ref_name }}" + PR_NUMBER="${{ github.event.pull_request.number }}" + DEVELOPER="${{ github.actor }}" + + uv run python ./scripts/generate_template_workflow.py staging --serverless \ + --branch "$BRANCH_NAME" \ + --developer "$DEVELOPER" \ + $(if [ -n "$PR_NUMBER" ]; then echo "--pr-number $PR_NUMBER"; fi) + + uv run databricks bundle deploy --target staging - name: Run on staging (integration tests) run: | @@ -55,4 +64,13 @@ jobs: - name: Deploy on prod run: | - make deploy-serverless env=prod + BRANCH_NAME="${{ github.head_ref || github.ref_name }}" + PR_NUMBER="${{ github.event.pull_request.number }}" + DEVELOPER="${{ github.actor }}" + + uv run python ./scripts/generate_template_workflow.py prod --serverless \ + --branch "$BRANCH_NAME" \ + --developer "$DEVELOPER" \ + $(if [ -n "$PR_NUMBER" ]; then echo "--pr-number $PR_NUMBER"; fi) + + uv run databricks bundle deploy --target prod diff --git a/README.md b/README.md index a63c0a1..4a77e42 100644 --- a/README.md +++ b/README.md @@ -101,14 +101,14 @@ databricks-template/ β”‚ └── unit_test.py # Pytest unit tests β”‚ β”œβ”€β”€ resources/ # Databricks workflow templates -β”‚ β”œβ”€β”€ wf_template_serverless.yml # Jinja2 template print("SUMMARY")for serverless +β”‚ β”œβ”€β”€ wf_template_serverless.yml # Jinja2 template for serverless β”‚ β”œβ”€β”€ wf_template.yml # Jinja2 template for job clusters β”‚ └── workflow.yml # Generated workflow (auto-created) β”‚ β”œβ”€β”€ scripts/ # Helper scripts β”‚ β”œβ”€β”€ generate_template_workflow.py # Workflow generator (Jinja2) β”‚ β”œβ”€β”€ sdk_analyze_job_costs.py # Cost analysis script -β”‚ └── sdk_workspace_and_account.py # Workspace management +β”‚ └── sdk_workspace_and_account.py # Workspace and account management β”‚ print("SUMMARY") β”œβ”€β”€ docs/ # Documentation assets β”‚ β”œβ”€β”€ dag.png diff --git a/resources/wf_template.yml b/resources/wf_template.yml deleted file mode 100644 index 720a904..0000000 --- a/resources/wf_template.yml +++ /dev/null @@ -1,95 +0,0 @@ -# The main job for default_python -resources: - jobs: - - job1: - name: template_${bundle.target} - timeout_seconds: 3600 - - {% if environment == 'prod'%} - schedule: - quartz_cron_expression: "0 0 5 * * ?" - timezone_id: "UTC" - - no_alert_for_skipped_runs: false - {% endif %} - - tasks: - - - task_key: extract_source1 - job_cluster_key: cluster-dev-aws - max_retries: 0 - python_wheel_task: - package_name: template - entry_point: main - parameters: ["--task={{'{{task.name}}'}}", - "--env=${bundle.target}", - "${var.debug}"] - libraries: - - whl: ../dist/*.whl - - - task_key: extract_source2 - job_cluster_key: cluster-dev-aws - max_retries: 0 - python_wheel_task: - package_name: template - entry_point: main - parameters: ["--task={{'{{task.name}}'}}", - "--env=${bundle.target}", - "${var.debug}"] - libraries: - - whl: ../dist/*.whl - - - task_key: generate_orders - depends_on: - - task_key: extract_source1 - - task_key: extract_source2 - job_cluster_key: cluster-dev-aws - max_retries: 0 - python_wheel_task: - package_name: template - entry_point: main - parameters: ["--task={{'{{task.name}}'}}", - "--env=${bundle.target}", - "${var.debug}"] - libraries: - - whl: ../dist/*.whl - - - task_key: generate_orders_agg - depends_on: - - task_key: generate_orders - job_cluster_key: cluster-dev-aws - max_retries: 0 - python_wheel_task: - package_name: template - entry_point: main - parameters: ["--task={{'{{task.name}}'}}", - "--env=${bundle.target}", - "${var.debug}"] - libraries: - - whl: ../dist/*.whl - - job_clusters: - # - job_cluster_key: cluster-dev-azure - # new_cluster: - # spark_version: 18.0.x-scala2.13 - # node_type_id: Standard_D8as_v5 - # num_workers: 1 - # azure_attributes: - # first_on_demand: 1 - # availability: SPOT_AZURE - # data_security_mode: SINGLE_USER - - - job_cluster_key: cluster-dev-aws - new_cluster: - spark_version: 18.0.x-scala2.13 - node_type_id: c5d.xlarge - num_workers: 1 - aws_attributes: - first_on_demand: 1 - availability: SPOT_WITH_FALLBACK - zone_id: auto - spot_bid_price_percent: 100 - ebs_volume_count: 0 - policy_id: 001934F3ABD02D4A - data_security_mode: SINGLE_USER diff --git a/resources/wf_template_serverless.yml b/resources/wf_template_serverless.yml index d30c6d3..372a7a5 100644 --- a/resources/wf_template_serverless.yml +++ b/resources/wf_template_serverless.yml @@ -6,6 +6,12 @@ resources: name: template_${bundle.target} timeout_seconds: 3600 + # Git metadata tags for traceability + tags: + git_branch: "{{ branch }}" + deployed_by: "{{ developer }}" + {% if pr_number %}pr_number: "{{ pr_number }}"{% endif %} + # A list of task execution environment specifications that can be referenced by tasks of this job. deployment: kind: BUNDLE @@ -85,6 +91,12 @@ resources: name: template_${bundle.target}_integration_test timeout_seconds: 3600 + # Git metadata tags for traceability + tags: + git_branch: "{{ branch }}" + deployed_by: "{{ developer }}" + {% if pr_number %}pr_number: "{{ pr_number }}"{% endif %} + environments: - environment_key: default spec: diff --git a/scripts/generate_template_workflow.py b/scripts/generate_template_workflow.py index 1f23555..6623dd8 100644 --- a/scripts/generate_template_workflow.py +++ b/scripts/generate_template_workflow.py @@ -1,32 +1,82 @@ import sys - +import subprocess +import argparse from jinja2 import Environment, FileSystemLoader -if len(sys.argv) not in [2, 3]: - print("Usage: python generate_workflow.py [serverless]") - print("Example: python generate_workflow.py prod") - print("Example: python generate_workflow.py prod serverless") - sys.exit(1) -serverless = len(sys.argv) == 3 and sys.argv[2].lower() == "--serverless" -print(sys.argv[2].lower()) -print(f"Serverless mode: {serverless}") +def get_git_branch(): + """Get current git branch name, fallback to 'unknown' if not available.""" + try: + branch = ( + subprocess.check_output(["git", "rev-parse", "--abbrev-ref", "HEAD"], stderr=subprocess.DEVNULL) + .decode("utf-8") + .strip() + ) + return branch + except Exception: + return "unknown" + + +def get_git_user(): + """Get git user name, fallback to 'unknown' if not available.""" + try: + user = ( + subprocess.check_output(["git", "config", "user.name"], stderr=subprocess.DEVNULL).decode("utf-8").strip() + ) + return user + except Exception: + return "unknown" + + +def main(): + parser = argparse.ArgumentParser( + description="Generate Databricks workflow YAML from Jinja2 template", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" +Examples: + python generate_template_workflow.py dev --serverless + python generate_template_workflow.py staging --serverless --branch main --developer john --pr-number 123 + """, + ) + + parser.add_argument("environment", help="Target environment (dev, staging, prod)") + parser.add_argument("--serverless", action="store_true", help="Use serverless workflow template") + parser.add_argument("--branch", help="Git branch name (auto-detected if not provided)") + parser.add_argument("--developer", help="Developer/deployer name (auto-detected if not provided)") + parser.add_argument("--pr-number", help="Pull request number (optional)") + + args = parser.parse_args() + + # Get or auto-detect git metadata + branch = args.branch if args.branch else get_git_branch() + developer = args.developer if args.developer else get_git_user() + pr_number = args.pr_number if args.pr_number else "" + + print(f"Environment: {args.environment}") + print(f"Serverless mode: {args.serverless}") + print(f"Git branch: {branch}") + print(f"Developer: {developer}") + print(f"PR number: {pr_number if pr_number else 'N/A'}") + + # Load and render template + file_loader = FileSystemLoader(".") + env = Environment(loader=file_loader) + + if args.serverless: + template = env.get_template("/resources/wf_template_serverless.yml") + else: + template = env.get_template("/resources/wf_template.yml") -environment = sys.argv[1] + # Render the template with all variables + output = template.render(environment=args.environment, branch=branch, developer=developer, pr_number=pr_number) -file_loader = FileSystemLoader(".") -env = Environment(loader=file_loader) -if serverless: - template = env.get_template("/resources/wf_template_serverless.yml") -else: - template = env.get_template("/resources/wf_template.yml") + # Save the rendered YAML to a file + output_file = "./resources/workflow.yml" + with open(output_file, "w") as f: + f.write(output) -# Render the template with the environment variable -output = template.render(environment=environment) + print(f"\nGenerated {output_file}") -# Save the rendered YAML to a file -output_file = "./resources/workflow.yml" -with open(output_file, "w") as f: - f.write(output) -print(f"Generated {output_file}") +if __name__ == "__main__": + main()