dbt-4ps-generator 0.1.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,10 @@
1
+ # Python-generated files
2
+ __pycache__/
3
+ *.py[oc]
4
+ build/
5
+ dist/
6
+ wheels/
7
+ *.egg-info
8
+
9
+ # Virtual environments
10
+ .venv
@@ -0,0 +1 @@
1
+ 3.13
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Misja Pronk
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,87 @@
1
+ Metadata-Version: 2.4
2
+ Name: dbt-4ps-generator
3
+ Version: 0.1.1
4
+ Summary: Generate dbt staging models for 4PS Construct (Business Central) from bc2adls CDM manifests
5
+ Project-URL: Repository, https://github.com/misja-pronk/dbt-4ps-staging
6
+ Project-URL: Documentation, https://misja-pronk.github.io/dbt-4ps-staging/
7
+ Project-URL: Changelog, https://github.com/misja-pronk/dbt-4ps-staging/blob/main/CHANGELOG.md
8
+ Author: Misja Pronk
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: 4ps,bc2adls,business-central,cdm,databricks,dbt
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Environment :: Console
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Programming Language :: Python :: 3.14
17
+ Classifier: Topic :: Database
18
+ Requires-Python: >=3.13
19
+ Requires-Dist: databricks-sdk>=0.50.0
20
+ Requires-Dist: humps>=0.2.2
21
+ Requires-Dist: pydantic>=2.11.3
22
+ Requires-Dist: pyyaml>=6.0.2
23
+ Requires-Dist: rich>=13.0.0
24
+ Requires-Dist: shandy-sqlfmt[jinjafmt]>=0.26.0
25
+ Requires-Dist: sqlglot>=26.15.0
26
+ Requires-Dist: typer>=0.15.2
27
+ Description-Content-Type: text/markdown
28
+
29
+ # dbt-4ps-generator
30
+
31
+ [![PyPI](https://img.shields.io/pypi/v/dbt-4ps-generator.svg)](https://pypi.org/project/dbt-4ps-generator/)
32
+
33
+ CLI that generates dbt staging models from a [bc2adls](https://github.com/Bertverbeek4PS/bc2adls) CDM export. For every entity in the manifest it writes:
34
+
35
+ - `stg_4ps__<table>.sql` — a `SELECT ... FROM STREAM READ_FILES(...)` model with the column types from the CDM metadata and columns renamed to snake_case (`CreditLimitLCY-20` → `credit_limit_lcy`)
36
+ - `_4ps__models.yaml` — model + column descriptions, and a `dbt_utils.unique_combination_of_columns` test on each table's primary key
37
+
38
+ Full documentation: <https://misja-pronk.github.io/dbt-4ps-staging/>
39
+
40
+ ## Install
41
+
42
+ ```bash
43
+ uvx dbt-4ps-generator --help # run once, ephemerally
44
+ uv tool install dbt-4ps-generator # install the command on PATH
45
+ ```
46
+
47
+ Or from a checkout: `uv run dbt-4ps-generator` in this directory.
48
+
49
+ ## Usage
50
+
51
+ Download the CDM metadata from the Unity Catalog volume bc2adls exports to (authenticates via `DATABRICKS_HOST`/`DATABRICKS_TOKEN` or a `~/.databrickscfg` profile):
52
+
53
+ ```bash
54
+ dbt-4ps-generator download \
55
+ --volume-path /Volumes/<catalog>/<schema>/<volume> \
56
+ --output-directory ../_cdm
57
+ ```
58
+
59
+ Generate the models:
60
+
61
+ ```bash
62
+ dbt-4ps-generator generate \
63
+ --manifest ../_cdm/deltas.manifest.cdm.json \
64
+ --output-directory ../dbt_4ps_staging_package/models/staging/4ps
65
+ ```
66
+
67
+ Options (see `--help`): `--model-prefix` (default `stg_4ps__`), `--schema-prefix` (default `_4ps__`), and `--no-file-metadata-column` to omit the `_metadata` source-file column.
68
+
69
+ ## Development
70
+
71
+ ```bash
72
+ uv sync
73
+ uv run pytest
74
+ uv run ruff check . && uv run ruff format --check .
75
+ uv run ty check
76
+ ```
77
+
78
+ ## Layout
79
+
80
+ | File (`src/dbt_4ps_generator/`) | Purpose |
81
+ |---|---|
82
+ | `cli.py` | Typer CLI (`generate`, `download`) |
83
+ | `cdm.py` | Pydantic models for the CDM manifest and entity files |
84
+ | `converters.py` | CDM → dbt/SQL conversions (types, names, READ_FILES options) |
85
+ | `dbt.py` | Builders that write the .sql models and schema yaml |
86
+ | `sql.py` | SQL primitives (columns, options, streaming-table DDL builder) |
87
+ | `databricks_helper.py` | Volume download via the Databricks SDK |
@@ -0,0 +1,59 @@
1
+ # dbt-4ps-generator
2
+
3
+ [![PyPI](https://img.shields.io/pypi/v/dbt-4ps-generator.svg)](https://pypi.org/project/dbt-4ps-generator/)
4
+
5
+ CLI that generates dbt staging models from a [bc2adls](https://github.com/Bertverbeek4PS/bc2adls) CDM export. For every entity in the manifest it writes:
6
+
7
+ - `stg_4ps__<table>.sql` — a `SELECT ... FROM STREAM READ_FILES(...)` model with the column types from the CDM metadata and columns renamed to snake_case (`CreditLimitLCY-20` → `credit_limit_lcy`)
8
+ - `_4ps__models.yaml` — model + column descriptions, and a `dbt_utils.unique_combination_of_columns` test on each table's primary key
9
+
10
+ Full documentation: <https://misja-pronk.github.io/dbt-4ps-staging/>
11
+
12
+ ## Install
13
+
14
+ ```bash
15
+ uvx dbt-4ps-generator --help # run once, ephemerally
16
+ uv tool install dbt-4ps-generator # install the command on PATH
17
+ ```
18
+
19
+ Or from a checkout: `uv run dbt-4ps-generator` in this directory.
20
+
21
+ ## Usage
22
+
23
+ Download the CDM metadata from the Unity Catalog volume bc2adls exports to (authenticates via `DATABRICKS_HOST`/`DATABRICKS_TOKEN` or a `~/.databrickscfg` profile):
24
+
25
+ ```bash
26
+ dbt-4ps-generator download \
27
+ --volume-path /Volumes/<catalog>/<schema>/<volume> \
28
+ --output-directory ../_cdm
29
+ ```
30
+
31
+ Generate the models:
32
+
33
+ ```bash
34
+ dbt-4ps-generator generate \
35
+ --manifest ../_cdm/deltas.manifest.cdm.json \
36
+ --output-directory ../dbt_4ps_staging_package/models/staging/4ps
37
+ ```
38
+
39
+ Options (see `--help`): `--model-prefix` (default `stg_4ps__`), `--schema-prefix` (default `_4ps__`), and `--no-file-metadata-column` to omit the `_metadata` source-file column.
40
+
41
+ ## Development
42
+
43
+ ```bash
44
+ uv sync
45
+ uv run pytest
46
+ uv run ruff check . && uv run ruff format --check .
47
+ uv run ty check
48
+ ```
49
+
50
+ ## Layout
51
+
52
+ | File (`src/dbt_4ps_generator/`) | Purpose |
53
+ |---|---|
54
+ | `cli.py` | Typer CLI (`generate`, `download`) |
55
+ | `cdm.py` | Pydantic models for the CDM manifest and entity files |
56
+ | `converters.py` | CDM → dbt/SQL conversions (types, names, READ_FILES options) |
57
+ | `dbt.py` | Builders that write the .sql models and schema yaml |
58
+ | `sql.py` | SQL primitives (columns, options, streaming-table DDL builder) |
59
+ | `databricks_helper.py` | Volume download via the Databricks SDK |
@@ -0,0 +1,74 @@
1
+ [project]
2
+ name = "dbt-4ps-generator"
3
+ version = "0.1.1"
4
+ description = "Generate dbt staging models for 4PS Construct (Business Central) from bc2adls CDM manifests"
5
+ readme = "README.md"
6
+ license = "MIT"
7
+ license-files = ["LICENSE"]
8
+ authors = [{ name = "Misja Pronk" }]
9
+ requires-python = ">=3.13"
10
+ keywords = ["dbt", "databricks", "bc2adls", "business-central", "4ps", "cdm"]
11
+ classifiers = [
12
+ "Development Status :: 4 - Beta",
13
+ "Environment :: Console",
14
+ "Intended Audience :: Developers",
15
+ "Programming Language :: Python :: 3.13",
16
+ "Programming Language :: Python :: 3.14",
17
+ "Topic :: Database",
18
+ ]
19
+ dependencies = [
20
+ "databricks-sdk>=0.50.0",
21
+ "humps>=0.2.2",
22
+ "pydantic>=2.11.3",
23
+ "pyyaml>=6.0.2",
24
+ "rich>=13.0.0",
25
+ "shandy-sqlfmt[jinjafmt]>=0.26.0",
26
+ "sqlglot>=26.15.0",
27
+ "typer>=0.15.2",
28
+ ]
29
+
30
+ [project.urls]
31
+ Repository = "https://github.com/misja-pronk/dbt-4ps-staging"
32
+ Documentation = "https://misja-pronk.github.io/dbt-4ps-staging/"
33
+ Changelog = "https://github.com/misja-pronk/dbt-4ps-staging/blob/main/CHANGELOG.md"
34
+
35
+ [project.scripts]
36
+ dbt-4ps-generator = "dbt_4ps_generator.cli:app"
37
+
38
+ [build-system]
39
+ requires = ["hatchling"]
40
+ build-backend = "hatchling.build"
41
+
42
+ [tool.hatch.build.targets.wheel]
43
+ packages = ["src/dbt_4ps_generator"]
44
+
45
+ [dependency-groups]
46
+ dev = [
47
+ "pytest>=8.0.0",
48
+ "ruff>=0.6",
49
+ "ty>=0.0.1a1",
50
+ ]
51
+ docs = [
52
+ "mkdocs-material>=9.7.5",
53
+ ]
54
+
55
+ [tool.pytest.ini_options]
56
+ testpaths = ["tests"]
57
+ addopts = "-q"
58
+
59
+ [tool.ruff]
60
+ line-length = 100
61
+ target-version = "py313"
62
+ src = ["src", "tests"]
63
+
64
+ [tool.ruff.lint]
65
+ select = ["E", "F", "I", "UP", "B", "SIM"]
66
+
67
+ [tool.ruff.lint.per-file-ignores]
68
+ # typer's idiom puts typer.Option(...) in argument defaults
69
+ "src/dbt_4ps_generator/cli.py" = ["B008"]
70
+
71
+ [tool.ty.environment]
72
+ python-version = "3.13"
73
+ root = ["src"]
74
+ extra-paths = ["tests"]
@@ -0,0 +1 @@
1
+ """Generate dbt staging models for 4PS Construct from bc2adls CDM manifests."""
@@ -0,0 +1,137 @@
1
+ import json
2
+ from pathlib import Path
3
+
4
+ from pydantic import BaseModel, Field
5
+
6
+
7
+ class Argument(BaseModel):
8
+ name: str
9
+ value: str
10
+
11
+
12
+ class PatternTraitReference(BaseModel):
13
+ trait_reference: str = Field(alias="traitReference")
14
+ arguments: list[Argument]
15
+
16
+
17
+ class DataPartitionPattern(BaseModel):
18
+ name: str
19
+ root_location: str = Field(alias="rootLocation")
20
+ glob_pattern: str = Field(alias="globPattern")
21
+ exhibits_traits: list[PatternTraitReference] = Field(alias="exhibitsTraits")
22
+
23
+
24
+ class ManifestEntity(BaseModel):
25
+ type: str
26
+ entity_name: str = Field(alias="entityName")
27
+ entity_path: str = Field(alias="entityPath")
28
+ data_partition_patterns: list[DataPartitionPattern] = Field(
29
+ default=[], alias="dataPartitionPatterns"
30
+ )
31
+
32
+ def get_entity_name(self) -> str:
33
+ return self.entity_name
34
+
35
+ def get_entity_path(self) -> str:
36
+ return self.entity_path
37
+
38
+ def get_root_location(self) -> str:
39
+ root_location = self.data_partition_patterns[0].root_location
40
+ return root_location
41
+
42
+ def get_glob_pattern(self) -> str:
43
+ glob_pattern = self.data_partition_patterns[0].glob_pattern
44
+ return glob_pattern
45
+
46
+ def get_trait_reference(self) -> str:
47
+ trait_reference = self.data_partition_patterns[0].exhibits_traits[0].trait_reference
48
+ return trait_reference
49
+
50
+ def get_arguments(self) -> list[Argument]:
51
+ arguments = self.data_partition_patterns[0].exhibits_traits[0].arguments
52
+ return arguments
53
+
54
+
55
+ class Manifest(BaseModel):
56
+ json_schema_semantic_version: str = Field(alias="jsonSchemaSemanticVersion")
57
+ imports: list[str]
58
+ manifest_name: str = Field(alias="manifestName")
59
+ explanation: str
60
+ entities: list[ManifestEntity] = Field(alias="entities")
61
+ relationships: list[str]
62
+
63
+
64
+ class AppliedTrait(BaseModel):
65
+ trait_reference: str = Field(alias="traitReference")
66
+ arguments: list[dict]
67
+
68
+
69
+ class Attribute(BaseModel):
70
+ name: str
71
+ data_format: str = Field(alias="dataFormat")
72
+ applied_traits: list[AppliedTrait] = Field(default=[], alias="appliedTraits")
73
+ display_name: str = Field(alias="displayName")
74
+ maximum_length: int | None = Field(alias="maximumLength")
75
+ is_primary_key: bool = Field(default=False, alias="isPrimaryKey")
76
+
77
+
78
+ class Definition(BaseModel):
79
+ entity_name: str = Field(alias="entityName")
80
+ exhibits_traits: list[str] = Field(default=[], alias="exhibitsTraits")
81
+ display_name: str = Field(alias="displayName")
82
+ description: str
83
+ has_attributes: list[Attribute] = Field(alias="hasAttributes")
84
+
85
+
86
+ class ModelEntity(BaseModel):
87
+ json_schema_semantic_version: str = Field(alias="jsonSchemaSemanticVersion")
88
+ imports: list[dict]
89
+ definitions: list[Definition]
90
+
91
+ def get_description(self) -> str:
92
+ description = self.definitions[0].description
93
+ return description
94
+
95
+ def get_attributes(self) -> list[Attribute]:
96
+ attributes = self.definitions[0].has_attributes
97
+ return attributes
98
+
99
+ def get_display_name(self) -> str:
100
+ display_name = self.definitions[0].display_name
101
+ return display_name
102
+
103
+
104
+ class Entity(BaseModel):
105
+ root_location: str
106
+ glob_pattern: str
107
+ description: str
108
+ attributes: list[Attribute]
109
+ entity_name: str
110
+ entity_path: str
111
+ trait_reference: str
112
+ arguments: list[Argument]
113
+ display_name: str
114
+
115
+
116
+ def build_entities(manifest_path: Path) -> list[Entity]:
117
+ entities = []
118
+ manifest = Manifest(**json.loads(manifest_path.read_text()))
119
+ for manifest_entity in manifest.entities:
120
+ model_entity_path = manifest_path.parent / Path(manifest_entity.entity_path.split("/")[0])
121
+ model_entity = ModelEntity(**json.loads(model_entity_path.read_text()))
122
+
123
+ entity = Entity(
124
+ trait_reference=manifest_entity.get_trait_reference(),
125
+ root_location=manifest_entity.get_root_location(),
126
+ description=model_entity.get_description(),
127
+ glob_pattern=manifest_entity.get_glob_pattern(),
128
+ entity_name=manifest_entity.get_entity_name(),
129
+ display_name=model_entity.get_display_name(),
130
+ entity_path=manifest_entity.get_entity_path(),
131
+ arguments=manifest_entity.get_arguments(),
132
+ attributes=model_entity.get_attributes(),
133
+ )
134
+
135
+ entities.append(entity)
136
+
137
+ return entities
@@ -0,0 +1,109 @@
1
+ from importlib.metadata import version as pkg_version
2
+ from pathlib import Path
3
+
4
+ import typer
5
+
6
+ from dbt_4ps_generator.cdm import build_entities
7
+ from dbt_4ps_generator.converters import (
8
+ argument_to_option,
9
+ attribute_to_column,
10
+ construct_path,
11
+ to_snake_case_with_column_number_removed,
12
+ trait_reference_to_format,
13
+ )
14
+ from dbt_4ps_generator.databricks_helper import download_cdm_files_from_databricks_volume
15
+ from dbt_4ps_generator.dbt import ModelBuilder, ModelsSchemaBuilder
16
+
17
+ app = typer.Typer(help="Generate dbt staging models from bc2adls CDM manifests.")
18
+
19
+
20
+ def _version_callback(value: bool):
21
+ if value:
22
+ typer.echo(pkg_version("dbt-4ps-generator"))
23
+ raise typer.Exit()
24
+
25
+
26
+ @app.callback()
27
+ def main(
28
+ version: bool = typer.Option(
29
+ False, "--version", callback=_version_callback, is_eager=True, help="Show the version."
30
+ ),
31
+ ):
32
+ pass
33
+
34
+
35
+ @app.command()
36
+ def generate(
37
+ manifest: Path = typer.Option(
38
+ ...,
39
+ help="Path to the CDM manifest, e.g. _cdm/deltas.manifest.cdm.json. "
40
+ "Entity .cdm.json files are resolved relative to this file.",
41
+ exists=True,
42
+ dir_okay=False,
43
+ ),
44
+ output_directory: Path = typer.Option(
45
+ ...,
46
+ help="Directory the .sql models and schema yaml are written to, "
47
+ "e.g. dbt_4ps_staging_package/models/staging/4ps.",
48
+ file_okay=False,
49
+ ),
50
+ model_prefix: str = typer.Option("stg_4ps__", help="Prefix for generated model names."),
51
+ schema_name: str = typer.Option("models", help="Base name of the generated schema yaml."),
52
+ schema_prefix: str = typer.Option("_4ps__", help="Prefix for the generated schema yaml."),
53
+ file_metadata_column: bool = typer.Option(
54
+ True, help="Add the _metadata column (source file info) to every model."
55
+ ),
56
+ ):
57
+ """Generate one staging model per entity in the manifest, plus a schema yaml."""
58
+ output_directory.mkdir(parents=True, exist_ok=True)
59
+
60
+ entities = build_entities(manifest)
61
+ configuration_builder = (
62
+ ModelsSchemaBuilder()
63
+ .name(name=schema_name, prefix=schema_prefix)
64
+ .output_directory(output_directory)
65
+ )
66
+
67
+ for entity in entities:
68
+ model_builder = (
69
+ ModelBuilder()
70
+ .name(name=entity.entity_name, prefix=model_prefix)
71
+ .description(entity.description)
72
+ .add_rename_function(to_snake_case_with_column_number_removed)
73
+ .add_options([argument_to_option(a) for a in entity.arguments])
74
+ .format(trait_reference_to_format(entity.trait_reference))
75
+ .add_columns([attribute_to_column(a) for a in entity.attributes])
76
+ .path(
77
+ construct_path(root_location=entity.root_location, glob_pattern=entity.glob_pattern)
78
+ )
79
+ .add_file_metadata_column(file_metadata_column)
80
+ .output_directory(output_directory)
81
+ .build()
82
+ )
83
+ configuration_builder.add_model_builder(model_builder)
84
+
85
+ configuration_builder.build()
86
+ typer.echo(f"Generated {len(entities)} models in {output_directory}")
87
+
88
+
89
+ @app.command()
90
+ def download(
91
+ volume_path: str = typer.Option(
92
+ ...,
93
+ help="Unity Catalog volume path holding the bc2adls export, "
94
+ "e.g. /Volumes/<catalog>/<schema>/<volume>.",
95
+ ),
96
+ output_directory: Path = typer.Option(
97
+ ..., help="Local directory to download the CDM json files into.", file_okay=False
98
+ ),
99
+ ):
100
+ """Download CDM manifest and entity files from a Databricks volume.
101
+
102
+ Authenticates via the Databricks SDK's default chain: set DATABRICKS_HOST and
103
+ DATABRICKS_TOKEN, or use a configured profile (DATABRICKS_CONFIG_PROFILE).
104
+ """
105
+ download_cdm_files_from_databricks_volume(volume_path, output_directory)
106
+
107
+
108
+ if __name__ == "__main__":
109
+ app()
@@ -0,0 +1,110 @@
1
+ import re
2
+
3
+ from dbt_4ps_generator.cdm import Argument, Attribute
4
+ from dbt_4ps_generator.sql import Column, Option
5
+
6
+
7
+ def to_snake_case_with_column_number_removed(input_string):
8
+ # Remove trailing hyphen + digits first
9
+ input_string = re.sub(r"[-_]\d+$", "", input_string)
10
+
11
+ # Find all words correctly
12
+ words = re.findall(
13
+ r"(?:[A-Z]+(?=[A-Z][a-z]))|(?:[A-Z]?[a-z]+)|(?:[A-Z]+)|(?:\d+)", input_string
14
+ )
15
+
16
+ # Join with underscores and lowercase
17
+ to_snake_case = "_".join(map(str.lower, words))
18
+ return to_snake_case
19
+
20
+
21
+ def trait_reference_to_format(trait_reference: str):
22
+ return trait_reference.split(".")[-1].lower()
23
+
24
+
25
+ def argument_to_option(argument: Argument) -> Option:
26
+ match argument.name:
27
+ case "columnHeaders":
28
+ return Option(key="header", value=argument.value)
29
+
30
+ case "delimiter":
31
+ return Option(key="sep", value=argument.value)
32
+
33
+ case "encoding":
34
+ return Option(key="encoding", value=argument.value.upper())
35
+
36
+ case "escape":
37
+ # double backslashes so the value survives SQL string-literal parsing
38
+ return Option(key="escape", value=argument.value.replace("\\", "\\\\"))
39
+ case _:
40
+ return Option(key=argument.name, value=argument.value)
41
+
42
+
43
+ def attribute_to_column(attribute: Attribute) -> Column:
44
+ match attribute.data_format.lower():
45
+ case "string":
46
+ type = "string"
47
+ case "int32":
48
+ type = "int"
49
+ case "decimal":
50
+ precision = None
51
+ scale = None
52
+
53
+ if attribute.maximum_length:
54
+ precision = attribute.maximum_length
55
+
56
+ for applied_trait in attribute.applied_traits:
57
+ if applied_trait.trait_reference == "is.dataFormat.numeric.shaped":
58
+ for argument in applied_trait.arguments:
59
+ if argument["name"] == "scale":
60
+ scale = argument["value"]
61
+ break
62
+
63
+ if precision and scale:
64
+ type = f"decimal({precision},{scale})"
65
+ elif precision:
66
+ type = f"decimal({precision})"
67
+ else:
68
+ type = "string"
69
+
70
+ case "double":
71
+ type = "double"
72
+ case "option":
73
+ type = "string"
74
+ case "int64":
75
+ type = "bigint"
76
+ case "boolean":
77
+ type = "boolean"
78
+ case "integer" | "interger": # tolerate the misspelled variant seen in some exports
79
+ type = "int"
80
+ case "time":
81
+ type = "string"
82
+ case "code":
83
+ type = "string"
84
+ case "date":
85
+ type = "date"
86
+ case "datetime":
87
+ type = "timestamp"
88
+ case "datetimeoffset":
89
+ type = "timestamp"
90
+ case "duration":
91
+ type = "string"
92
+ case "guid":
93
+ type = "string"
94
+ case _:
95
+ type = "string"
96
+
97
+ return Column(
98
+ name=attribute.name,
99
+ type=type,
100
+ display_name=attribute.display_name,
101
+ is_primary_key=attribute.is_primary_key,
102
+ )
103
+
104
+
105
+ def construct_path(root_location: str, glob_pattern: str) -> str:
106
+ volume = (
107
+ r'/Volumes/{{ var("source")["database"] }}'
108
+ r'/{{ var("source")["schema"] }}/{{ var("source")["volume"] }}'
109
+ )
110
+ return f"{volume}/{root_location}{glob_pattern}"
@@ -0,0 +1,25 @@
1
+ from pathlib import Path
2
+
3
+ from databricks.sdk import WorkspaceClient
4
+ from rich import print
5
+
6
+
7
+ def download_cdm_files_from_databricks_volume(volume_path: str, download_folder: Path) -> None:
8
+ """Download all .json files from a Unity Catalog volume to a local folder.
9
+
10
+ Uses the Databricks SDK default authentication chain (DATABRICKS_HOST /
11
+ DATABRICKS_TOKEN environment variables, or a profile from ~/.databrickscfg).
12
+ """
13
+ w = WorkspaceClient()
14
+ download_folder.mkdir(parents=True, exist_ok=True)
15
+
16
+ for directory_entry in w.files.list_directory_contents(volume_path):
17
+ name, path = directory_entry.name, directory_entry.path
18
+ if directory_entry.is_directory or not name or not path or not name.endswith(".json"):
19
+ continue
20
+ print(path)
21
+ resp = w.files.download(path)
22
+ if resp.contents is None:
23
+ raise RuntimeError(f"Empty response downloading {path}")
24
+ download_file = download_folder / Path(name)
25
+ download_file.write_text(data=str(resp.contents.read(), encoding="utf-8"))