datamuru 0.1.0a0__py3-none-any.whl
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.
- datamuru/__init__.py +4 -0
- datamuru/api.py +50 -0
- datamuru/bootstrap.py +141 -0
- datamuru/cli/__init__.py +1 -0
- datamuru/cli/commands/__init__.py +19 -0
- datamuru/cli/commands/apply.py +45 -0
- datamuru/cli/commands/destroy.py +23 -0
- datamuru/cli/commands/doctor.py +31 -0
- datamuru/cli/commands/edition.py +32 -0
- datamuru/cli/commands/import_.py +83 -0
- datamuru/cli/commands/init.py +25 -0
- datamuru/cli/commands/plan.py +32 -0
- datamuru/cli/commands/validate.py +26 -0
- datamuru/cli/guard.py +19 -0
- datamuru/cli/main.py +22 -0
- datamuru/cli/output.py +51 -0
- datamuru/core/__init__.py +1 -0
- datamuru/core/apply/__init__.py +5 -0
- datamuru/core/apply/engine.py +10 -0
- datamuru/core/apply/executor.py +88 -0
- datamuru/core/apply/models.py +16 -0
- datamuru/core/apply.py +31 -0
- datamuru/core/config/__init__.py +31 -0
- datamuru/core/config/models.py +75 -0
- datamuru/core/config/parser.py +44 -0
- datamuru/core/config/resolver.py +79 -0
- datamuru/core/config/validator.py +370 -0
- datamuru/core/config.py +123 -0
- datamuru/core/engine.py +123 -0
- datamuru/core/importer/__init__.py +5 -0
- datamuru/core/importer/discovery.py +9 -0
- datamuru/core/importer/engine.py +76 -0
- datamuru/core/importer/generator.py +20 -0
- datamuru/core/importer/models.py +43 -0
- datamuru/core/models.py +80 -0
- datamuru/core/plan/__init__.py +5 -0
- datamuru/core/plan/engine.py +122 -0
- datamuru/core/plan/models.py +60 -0
- datamuru/core/plan/renderer.py +15 -0
- datamuru/core/plan.py +66 -0
- datamuru/core/schema.py +91 -0
- datamuru/core/state/__init__.py +5 -0
- datamuru/core/state/backends/__init__.py +4 -0
- datamuru/core/state/backends/base.py +13 -0
- datamuru/core/state/backends/local.py +33 -0
- datamuru/core/state/manager.py +20 -0
- datamuru/core/state/models.py +28 -0
- datamuru/core/state.py +22 -0
- datamuru/edition.py +74 -0
- datamuru/errors.py +92 -0
- datamuru/governance/__init__.py +1 -0
- datamuru/governance/masking.py +19 -0
- datamuru/governance/rbac.py +50 -0
- datamuru/governance/taxonomy.py +29 -0
- datamuru/modeling.py +7 -0
- datamuru/providers/__init__.py +1 -0
- datamuru/providers/base.py +32 -0
- datamuru/providers/databricks/__init__.py +1 -0
- datamuru/providers/databricks/auth.py +64 -0
- datamuru/providers/databricks/client.py +837 -0
- datamuru/providers/databricks/execution.py +38 -0
- datamuru/providers/databricks/provider.py +1061 -0
- datamuru/providers/factory.py +14 -0
- datamuru/py.typed +1 -0
- datamuru/types.py +74 -0
- datamuru-0.1.0a0.dist-info/METADATA +168 -0
- datamuru-0.1.0a0.dist-info/RECORD +71 -0
- datamuru-0.1.0a0.dist-info/WHEEL +5 -0
- datamuru-0.1.0a0.dist-info/entry_points.txt +2 -0
- datamuru-0.1.0a0.dist-info/licenses/LICENSE +201 -0
- datamuru-0.1.0a0.dist-info/top_level.txt +1 -0
datamuru/__init__.py
ADDED
datamuru/api.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from datamuru.core.engine import DataMuruEngine
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class DataMuru:
|
|
9
|
+
def __init__(self, config_path: str | Path, environment: str | None = None) -> None:
|
|
10
|
+
self.engine = DataMuruEngine(config_path=config_path, environment=environment)
|
|
11
|
+
|
|
12
|
+
def validate(self):
|
|
13
|
+
return self.engine.validate()
|
|
14
|
+
|
|
15
|
+
def plan(self, target: str | None = None):
|
|
16
|
+
return self.engine.plan(target=target)
|
|
17
|
+
|
|
18
|
+
def apply(self, target: str | None = None):
|
|
19
|
+
return self.engine.apply(target=target)
|
|
20
|
+
|
|
21
|
+
def apply_saved_plan(self, plan_path: str | Path):
|
|
22
|
+
return self.engine.apply_saved_plan(plan_path)
|
|
23
|
+
|
|
24
|
+
def destroy(self, target: str | None = None):
|
|
25
|
+
return self.engine.destroy(target=target)
|
|
26
|
+
|
|
27
|
+
def save_plan(self, output_path: str | Path, target: str | None = None):
|
|
28
|
+
return self.engine.save_plan(output_path=output_path, target=target)
|
|
29
|
+
|
|
30
|
+
def edition_summary(self):
|
|
31
|
+
return self.engine.edition_summary()
|
|
32
|
+
|
|
33
|
+
def doctor(self):
|
|
34
|
+
return self.engine.doctor()
|
|
35
|
+
|
|
36
|
+
def import_discover(self, include_system: bool = False):
|
|
37
|
+
return self.engine.import_discover(include_system=include_system)
|
|
38
|
+
|
|
39
|
+
def import_generate(
|
|
40
|
+
self,
|
|
41
|
+
*,
|
|
42
|
+
catalogs: list[str] | None = None,
|
|
43
|
+
include_groups: bool = False,
|
|
44
|
+
include_system: bool = False,
|
|
45
|
+
):
|
|
46
|
+
return self.engine.import_generate(
|
|
47
|
+
catalogs=catalogs,
|
|
48
|
+
include_groups=include_groups,
|
|
49
|
+
include_system=include_system,
|
|
50
|
+
)
|
datamuru/bootstrap.py
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import textwrap
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class ProjectScaffolder:
|
|
8
|
+
def scaffold(
|
|
9
|
+
self,
|
|
10
|
+
output_dir: str | Path,
|
|
11
|
+
*,
|
|
12
|
+
name: str,
|
|
13
|
+
provider: str = "databricks",
|
|
14
|
+
cloud: str = "azure",
|
|
15
|
+
edition: str = "open-source",
|
|
16
|
+
) -> list[Path]:
|
|
17
|
+
root = Path(output_dir)
|
|
18
|
+
created: list[Path] = []
|
|
19
|
+
for relative in [
|
|
20
|
+
Path("environments"),
|
|
21
|
+
Path("providers"),
|
|
22
|
+
Path("workspaces"),
|
|
23
|
+
Path("governance"),
|
|
24
|
+
]:
|
|
25
|
+
target = root / relative
|
|
26
|
+
target.mkdir(parents=True, exist_ok=True)
|
|
27
|
+
created.append(target)
|
|
28
|
+
|
|
29
|
+
content = {
|
|
30
|
+
root / "datamuru.yml": textwrap.dedent(
|
|
31
|
+
f"""
|
|
32
|
+
project:
|
|
33
|
+
name: {name}
|
|
34
|
+
version: "0.1.0"
|
|
35
|
+
description: "Bootstrap DataMuru project"
|
|
36
|
+
edition: {edition}
|
|
37
|
+
provider: {provider}
|
|
38
|
+
|
|
39
|
+
environments:
|
|
40
|
+
- name: dev
|
|
41
|
+
config: ./environments/dev.yml
|
|
42
|
+
|
|
43
|
+
default_environment: dev
|
|
44
|
+
|
|
45
|
+
features:
|
|
46
|
+
governance: true
|
|
47
|
+
data_mesh: false
|
|
48
|
+
ingestion: false
|
|
49
|
+
modeling: false
|
|
50
|
+
observability: false
|
|
51
|
+
compliance_reporting: false
|
|
52
|
+
multi_workspace: false
|
|
53
|
+
hosted_control_plane: false
|
|
54
|
+
identity_management: false
|
|
55
|
+
|
|
56
|
+
state:
|
|
57
|
+
backend: local
|
|
58
|
+
path: ./.datamuru/state-dev.json
|
|
59
|
+
|
|
60
|
+
provider:
|
|
61
|
+
name: {provider}
|
|
62
|
+
cloud: {cloud}
|
|
63
|
+
config: ./providers/{provider}.yml
|
|
64
|
+
"""
|
|
65
|
+
).strip() + "\n",
|
|
66
|
+
root / "environments" / "dev.yml": "environment:\n name: dev\n",
|
|
67
|
+
root / "providers" / f"{provider}.yml": textwrap.dedent(
|
|
68
|
+
f"""
|
|
69
|
+
provider:
|
|
70
|
+
cloud: {cloud}
|
|
71
|
+
connect_timeout_seconds: 10
|
|
72
|
+
credential_mode: personal-access-token
|
|
73
|
+
execution_mode: state-only
|
|
74
|
+
auth_type: pat
|
|
75
|
+
token_env: DATABRICKS_TOKEN
|
|
76
|
+
host: https://adb-placeholder.{cloud}.databricks.example
|
|
77
|
+
"""
|
|
78
|
+
).strip() + "\n",
|
|
79
|
+
root / "workspaces" / "alpha-dev.yml": textwrap.dedent(
|
|
80
|
+
f"""
|
|
81
|
+
workspace:
|
|
82
|
+
name: alpha-dev
|
|
83
|
+
cloud: {cloud}
|
|
84
|
+
region: eastus2
|
|
85
|
+
catalogs:
|
|
86
|
+
- name: alpha_marketing
|
|
87
|
+
schemas:
|
|
88
|
+
- raw
|
|
89
|
+
- bronze
|
|
90
|
+
- silver
|
|
91
|
+
- gold
|
|
92
|
+
"""
|
|
93
|
+
).strip() + "\n",
|
|
94
|
+
root / "governance" / "taxonomy.yml": textwrap.dedent(
|
|
95
|
+
"""
|
|
96
|
+
taxonomy:
|
|
97
|
+
name: bootstrap
|
|
98
|
+
version: "0.1"
|
|
99
|
+
categories:
|
|
100
|
+
- id: internal
|
|
101
|
+
label: Internal
|
|
102
|
+
description: "Internal data"
|
|
103
|
+
handling:
|
|
104
|
+
retention_years: 3
|
|
105
|
+
encryption: at_rest
|
|
106
|
+
"""
|
|
107
|
+
).strip() + "\n",
|
|
108
|
+
root / "governance" / "rbac.yml": textwrap.dedent(
|
|
109
|
+
"""
|
|
110
|
+
rbac:
|
|
111
|
+
roles:
|
|
112
|
+
- id: data_consumer
|
|
113
|
+
name: Data Consumer
|
|
114
|
+
permissions:
|
|
115
|
+
- resource_type: schema
|
|
116
|
+
resource_pattern: "*.gold"
|
|
117
|
+
privilege: SELECT
|
|
118
|
+
assignments:
|
|
119
|
+
- principal: sample-consumers
|
|
120
|
+
type: group
|
|
121
|
+
roles:
|
|
122
|
+
- data_consumer
|
|
123
|
+
domains:
|
|
124
|
+
- alpha_marketing
|
|
125
|
+
"""
|
|
126
|
+
).strip() + "\n",
|
|
127
|
+
root / "governance" / "masking.yml": textwrap.dedent(
|
|
128
|
+
"""
|
|
129
|
+
masking:
|
|
130
|
+
builtins:
|
|
131
|
+
- id: partial_mask
|
|
132
|
+
description: Show last four characters for strings.
|
|
133
|
+
strategy: partial_mask
|
|
134
|
+
"""
|
|
135
|
+
).strip() + "\n",
|
|
136
|
+
}
|
|
137
|
+
for path, value in content.items():
|
|
138
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
139
|
+
path.write_text(value, encoding="utf-8")
|
|
140
|
+
created.append(path)
|
|
141
|
+
return created
|
datamuru/cli/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from .apply import apply_command
|
|
2
|
+
from .destroy import destroy_command
|
|
3
|
+
from .doctor import doctor_command
|
|
4
|
+
from .edition import edition_group
|
|
5
|
+
from .init import init_command
|
|
6
|
+
from .import_ import import_group
|
|
7
|
+
from .plan import plan_command
|
|
8
|
+
from .validate import validate_command
|
|
9
|
+
|
|
10
|
+
COMMANDS = [
|
|
11
|
+
init_command,
|
|
12
|
+
validate_command,
|
|
13
|
+
plan_command,
|
|
14
|
+
apply_command,
|
|
15
|
+
destroy_command,
|
|
16
|
+
doctor_command,
|
|
17
|
+
import_group,
|
|
18
|
+
edition_group,
|
|
19
|
+
]
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import click
|
|
7
|
+
|
|
8
|
+
from datamuru.api import DataMuru
|
|
9
|
+
from datamuru.types import Plan
|
|
10
|
+
|
|
11
|
+
from ..guard import with_cli_errors
|
|
12
|
+
from ..output import console
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@click.command("apply")
|
|
16
|
+
@click.option("--config", "config_path", default="datamuru.yml", show_default=True)
|
|
17
|
+
@click.option("--target", default=None)
|
|
18
|
+
@click.option("--plan", "plan_path", default=None)
|
|
19
|
+
@click.option("--auto-approve", is_flag=True, default=False)
|
|
20
|
+
@with_cli_errors
|
|
21
|
+
def apply_command(config_path: str, target: str | None, plan_path: str | None, auto_approve: bool) -> None:
|
|
22
|
+
if not auto_approve:
|
|
23
|
+
raise click.ClickException("Alpha bootstrap requires --auto-approve for non-interactive apply.")
|
|
24
|
+
dm = DataMuru(config_path=config_path)
|
|
25
|
+
preview_plan: Plan | None = None
|
|
26
|
+
if plan_path:
|
|
27
|
+
preview_plan = Plan.from_dict(json.loads(Path(plan_path).read_text(encoding="utf-8")))
|
|
28
|
+
result = dm.apply_saved_plan(plan_path)
|
|
29
|
+
else:
|
|
30
|
+
preview_plan = dm.plan(target=target)
|
|
31
|
+
if target and not preview_plan.changes:
|
|
32
|
+
console.print(f"[warning]Target '{target}' matched nothing; apply skipped.[/warning]")
|
|
33
|
+
return
|
|
34
|
+
result = dm.apply(target=target)
|
|
35
|
+
if not result.success:
|
|
36
|
+
for failure in result.failures:
|
|
37
|
+
console.print(f"[error]FAILED[/error] {failure.resource}: {failure.reason}")
|
|
38
|
+
raise SystemExit(1)
|
|
39
|
+
noop_count = 0
|
|
40
|
+
if preview_plan is not None:
|
|
41
|
+
noop_count = sum(1 for change in preview_plan.changes if change.action == "noop")
|
|
42
|
+
message = f"[success]Applied[/success] {len(result.applied)} changes."
|
|
43
|
+
if noop_count:
|
|
44
|
+
message += f" [muted]{noop_count} resources already matched.[/muted]"
|
|
45
|
+
console.print(message)
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
|
|
5
|
+
from datamuru.api import DataMuru
|
|
6
|
+
|
|
7
|
+
from ..guard import with_cli_errors
|
|
8
|
+
from ..output import console
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@click.command("destroy")
|
|
12
|
+
@click.option("--config", "config_path", default="datamuru.yml", show_default=True)
|
|
13
|
+
@click.option("--target", default=None)
|
|
14
|
+
@click.option("--confirm-destroy", is_flag=True, default=False)
|
|
15
|
+
@with_cli_errors
|
|
16
|
+
def destroy_command(config_path: str, target: str | None, confirm_destroy: bool) -> None:
|
|
17
|
+
if not confirm_destroy:
|
|
18
|
+
raise click.ClickException("Destroy requires --confirm-destroy.")
|
|
19
|
+
dm = DataMuru(config_path=config_path)
|
|
20
|
+
result = dm.destroy(target=target)
|
|
21
|
+
if not result.success:
|
|
22
|
+
raise SystemExit(1)
|
|
23
|
+
console.print(f"[success]Destroyed[/success] {len(result.applied)} resources.")
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
|
|
7
|
+
from datamuru.api import DataMuru
|
|
8
|
+
|
|
9
|
+
from ..guard import with_cli_errors
|
|
10
|
+
from ..output import console
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@click.command("doctor")
|
|
14
|
+
@click.option("--config", "config_path", default="datamuru.yml", show_default=True)
|
|
15
|
+
@click.option("--output", "output_format", default="text", type=click.Choice(["text", "json"]))
|
|
16
|
+
@with_cli_errors
|
|
17
|
+
def doctor_command(config_path: str, output_format: str) -> None:
|
|
18
|
+
dm = DataMuru(config_path=config_path)
|
|
19
|
+
report = dm.doctor()
|
|
20
|
+
if output_format == "json":
|
|
21
|
+
console.print_json(json.dumps(report.to_dict(), indent=2))
|
|
22
|
+
if not report.success:
|
|
23
|
+
raise SystemExit(1)
|
|
24
|
+
return
|
|
25
|
+
console.print(f"[primary]Provider[/primary]: [code]{report.provider}[/code]")
|
|
26
|
+
console.print(f"[primary]Environment[/primary]: [code]{report.environment}[/code]")
|
|
27
|
+
for check in report.checks:
|
|
28
|
+
style = "success" if check.level == "ok" else "warning" if check.level == "warning" else "error"
|
|
29
|
+
console.print(f"[{style}][{check.level}][/{style}] {check.code}: {check.message}")
|
|
30
|
+
if not report.success:
|
|
31
|
+
raise SystemExit(1)
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
|
|
7
|
+
from datamuru.api import DataMuru
|
|
8
|
+
|
|
9
|
+
from ..guard import with_cli_errors
|
|
10
|
+
from ..output import console
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@click.group("edition")
|
|
14
|
+
def edition_group() -> None:
|
|
15
|
+
"""Inspect edition-aware product metadata."""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@edition_group.command("show")
|
|
19
|
+
@click.option("--config", "config_path", default="datamuru.yml", show_default=True)
|
|
20
|
+
@click.option("--output", "output_format", default="text", type=click.Choice(["text", "json"]))
|
|
21
|
+
@with_cli_errors
|
|
22
|
+
def edition_show(config_path: str, output_format: str) -> None:
|
|
23
|
+
dm = DataMuru(config_path=config_path)
|
|
24
|
+
summary = dm.edition_summary()
|
|
25
|
+
if output_format == "json":
|
|
26
|
+
console.print_json(json.dumps(summary.to_dict(), indent=2))
|
|
27
|
+
return
|
|
28
|
+
console.print(f"[primary]Edition[/primary]: [code]{summary.edition}[/code]")
|
|
29
|
+
console.print(f"Enabled features: {', '.join(summary.enabled_features) if summary.enabled_features else 'none'}")
|
|
30
|
+
console.print(
|
|
31
|
+
f"Restricted features: {', '.join(summary.restricted_features) if summary.restricted_features else 'none'}"
|
|
32
|
+
)
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import click
|
|
7
|
+
|
|
8
|
+
from datamuru.api import DataMuru
|
|
9
|
+
|
|
10
|
+
from ..guard import with_cli_errors
|
|
11
|
+
from ..output import console
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@click.group("import")
|
|
15
|
+
def import_group() -> None:
|
|
16
|
+
"""Discover and generate starter YAML from an existing live workspace."""
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@import_group.command("discover")
|
|
20
|
+
@click.option("--config", "config_path", default="datamuru.yml", show_default=True)
|
|
21
|
+
@click.option("--include-system", is_flag=True, default=False, help="Include system catalogs, schemas, and groups.")
|
|
22
|
+
@click.option("--output", "output_format", default="text", type=click.Choice(["text", "json"]))
|
|
23
|
+
@with_cli_errors
|
|
24
|
+
def import_discover_command(config_path: str, include_system: bool, output_format: str) -> None:
|
|
25
|
+
dm = DataMuru(config_path=config_path)
|
|
26
|
+
report = dm.import_discover(include_system=include_system)
|
|
27
|
+
if output_format == "json":
|
|
28
|
+
console.print_json(json.dumps(report.to_dict(), indent=2))
|
|
29
|
+
return
|
|
30
|
+
|
|
31
|
+
console.print(f"[primary]Import Discovery[/primary] - provider: [code]{report.provider}[/code]")
|
|
32
|
+
console.print(f"[primary]Environment[/primary]: [code]{report.environment}[/code]")
|
|
33
|
+
console.print(f"[primary]Workspace[/primary]: [code]{report.workspace.name}[/code]")
|
|
34
|
+
console.print(f"[primary]Cloud[/primary]: [code]{report.workspace.cloud}[/code]")
|
|
35
|
+
console.print(f"[primary]Region[/primary]: [code]{report.workspace.region}[/code]")
|
|
36
|
+
if report.workspace.groups:
|
|
37
|
+
console.print("[primary]Groups[/primary]:")
|
|
38
|
+
for group_name in report.workspace.groups:
|
|
39
|
+
console.print(f" - [code]{group_name}[/code]")
|
|
40
|
+
if report.workspace.catalogs:
|
|
41
|
+
console.print("[primary]Catalogs[/primary]:")
|
|
42
|
+
for catalog in report.workspace.catalogs:
|
|
43
|
+
console.print(f" - [code]{catalog.name}[/code]")
|
|
44
|
+
for schema in catalog.schemas:
|
|
45
|
+
console.print(f" - schema [code]{schema.name}[/code]")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@import_group.command("generate")
|
|
49
|
+
@click.option("--config", "config_path", default="datamuru.yml", show_default=True)
|
|
50
|
+
@click.option("--catalog", "catalogs", multiple=True, help="Catalog name to include. Repeat to select multiple.")
|
|
51
|
+
@click.option("--include-groups", is_flag=True, default=False, help="Include discovered groups in principals.")
|
|
52
|
+
@click.option("--include-system", is_flag=True, default=False, help="Include system catalogs, schemas, and groups.")
|
|
53
|
+
@click.option("--out", "out_path", default=None, help="Write generated workspace YAML to a file.")
|
|
54
|
+
@click.option("--output", "output_format", default="text", type=click.Choice(["text", "json"]))
|
|
55
|
+
@with_cli_errors
|
|
56
|
+
def import_generate_command(
|
|
57
|
+
config_path: str,
|
|
58
|
+
catalogs: tuple[str, ...],
|
|
59
|
+
include_groups: bool,
|
|
60
|
+
include_system: bool,
|
|
61
|
+
out_path: str | None,
|
|
62
|
+
output_format: str,
|
|
63
|
+
) -> None:
|
|
64
|
+
dm = DataMuru(config_path=config_path)
|
|
65
|
+
result = dm.import_generate(
|
|
66
|
+
catalogs=list(catalogs) or None,
|
|
67
|
+
include_groups=include_groups,
|
|
68
|
+
include_system=include_system,
|
|
69
|
+
)
|
|
70
|
+
if out_path:
|
|
71
|
+
resolved = Path(out_path).resolve()
|
|
72
|
+
resolved.write_text(result.workspace_file_text, encoding="utf-8")
|
|
73
|
+
if output_format == "json":
|
|
74
|
+
payload = result.to_dict()
|
|
75
|
+
if out_path:
|
|
76
|
+
payload["written_to"] = str(Path(out_path).resolve())
|
|
77
|
+
console.print_json(json.dumps(payload, indent=2))
|
|
78
|
+
return
|
|
79
|
+
|
|
80
|
+
console.print(f"[primary]Import Generate[/primary] - environment: [code]{result.environment}[/code]")
|
|
81
|
+
if out_path:
|
|
82
|
+
console.print(f"[success]Wrote[/success] starter workspace YAML to [code]{Path(out_path).resolve()}[/code]")
|
|
83
|
+
console.print(result.workspace_file_text)
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
|
|
7
|
+
from datamuru.bootstrap import ProjectScaffolder
|
|
8
|
+
|
|
9
|
+
from ..guard import with_cli_errors
|
|
10
|
+
from ..output import console
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@click.command("init")
|
|
14
|
+
@click.option("--name", default="datamuru-project", show_default=True)
|
|
15
|
+
@click.option("--provider", default="databricks", show_default=True)
|
|
16
|
+
@click.option("--cloud", default="azure", show_default=True)
|
|
17
|
+
@click.option("--edition", default="open-source", show_default=True)
|
|
18
|
+
@click.option("--output-dir", default=".", show_default=True)
|
|
19
|
+
@with_cli_errors
|
|
20
|
+
def init_command(name: str, provider: str, cloud: str, edition: str, output_dir: str) -> None:
|
|
21
|
+
scaffolder = ProjectScaffolder()
|
|
22
|
+
created = scaffolder.scaffold(output_dir, name=name, provider=provider, cloud=cloud, edition=edition)
|
|
23
|
+
console.print(
|
|
24
|
+
f"[success]Created[/success] {len(created)} bootstrap artifacts in [code]{Path(output_dir).resolve()}[/code]"
|
|
25
|
+
)
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
|
|
7
|
+
from datamuru.api import DataMuru
|
|
8
|
+
|
|
9
|
+
from ..guard import with_cli_errors
|
|
10
|
+
from ..output import console, render_plan_symbol
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@click.command("plan")
|
|
14
|
+
@click.option("--config", "config_path", default="datamuru.yml", show_default=True)
|
|
15
|
+
@click.option("--target", default=None)
|
|
16
|
+
@click.option("--out", "out_path", default=None)
|
|
17
|
+
@click.option("--output", "output_format", default="text", type=click.Choice(["text", "json"]))
|
|
18
|
+
@with_cli_errors
|
|
19
|
+
def plan_command(config_path: str, target: str | None, out_path: str | None, output_format: str) -> None:
|
|
20
|
+
dm = DataMuru(config_path=config_path)
|
|
21
|
+
result = dm.plan(target=target)
|
|
22
|
+
if out_path:
|
|
23
|
+
dm.save_plan(output_path=out_path, target=target)
|
|
24
|
+
if output_format == "json":
|
|
25
|
+
console.print_json(json.dumps(result.to_dict(), indent=2))
|
|
26
|
+
return
|
|
27
|
+
console.print(f"[primary]DataMuru Plan[/primary] - environment: [code]{result.environment}[/code]")
|
|
28
|
+
if target and not result.changes:
|
|
29
|
+
console.print(f"[warning]No resources matched target '{target}'.[/warning]")
|
|
30
|
+
return
|
|
31
|
+
for change in result.changes:
|
|
32
|
+
console.print(f" {render_plan_symbol(change.action)} {change.resource.address} [muted]({change.reason})[/muted]")
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
|
|
5
|
+
from datamuru.api import DataMuru
|
|
6
|
+
|
|
7
|
+
from ..guard import with_cli_errors
|
|
8
|
+
from ..output import console
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@click.command("validate")
|
|
12
|
+
@click.option("--config", "config_path", default="datamuru.yml", show_default=True)
|
|
13
|
+
@click.option("--strict", is_flag=True, default=False)
|
|
14
|
+
@with_cli_errors
|
|
15
|
+
def validate_command(config_path: str, strict: bool) -> None:
|
|
16
|
+
dm = DataMuru(config_path=config_path)
|
|
17
|
+
issues = dm.validate()
|
|
18
|
+
errors = [issue for issue in issues if issue.level == "error"]
|
|
19
|
+
for issue in issues:
|
|
20
|
+
style = "error" if issue.level == "error" else "warning"
|
|
21
|
+
console.print(f"[{style}][{issue.level}][/{style}] {issue.path}: {issue.message}")
|
|
22
|
+
if errors:
|
|
23
|
+
raise SystemExit(1)
|
|
24
|
+
if strict and any(issue.level == "warning" for issue in issues):
|
|
25
|
+
raise SystemExit(1)
|
|
26
|
+
console.print("[success]Configuration is valid.[/success]")
|
datamuru/cli/guard.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from functools import wraps
|
|
4
|
+
|
|
5
|
+
from datamuru.errors import DataMuruError
|
|
6
|
+
|
|
7
|
+
from .output import render_error
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def with_cli_errors(fn):
|
|
11
|
+
@wraps(fn)
|
|
12
|
+
def wrapper(*args, **kwargs):
|
|
13
|
+
try:
|
|
14
|
+
return fn(*args, **kwargs)
|
|
15
|
+
except DataMuruError as exc:
|
|
16
|
+
render_error(exc)
|
|
17
|
+
raise SystemExit(exc.exit_code) from exc
|
|
18
|
+
|
|
19
|
+
return wrapper
|
datamuru/cli/main.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
|
|
5
|
+
from .commands import COMMANDS
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@click.group()
|
|
9
|
+
def cli() -> None:
|
|
10
|
+
"""DataMuru alpha CLI."""
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
for command in COMMANDS:
|
|
14
|
+
cli.add_command(command)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def main() -> None:
|
|
18
|
+
cli()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
if __name__ == "__main__":
|
|
22
|
+
main()
|
datamuru/cli/output.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""Shared rich console and formatting helpers for the DataMuru CLI."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from rich.panel import Panel
|
|
6
|
+
from rich.console import Console
|
|
7
|
+
from rich.theme import Theme
|
|
8
|
+
from rich.traceback import install
|
|
9
|
+
|
|
10
|
+
from datamuru.errors import DataMuruError
|
|
11
|
+
|
|
12
|
+
DATAMURU_THEME = Theme(
|
|
13
|
+
{
|
|
14
|
+
"primary": "bold #0D7377",
|
|
15
|
+
"secondary": "bold #14539A",
|
|
16
|
+
"accent": "#C8962A",
|
|
17
|
+
"success": "bold green",
|
|
18
|
+
"warning": "bold yellow",
|
|
19
|
+
"error": "bold red",
|
|
20
|
+
"create": "bold green",
|
|
21
|
+
"update": "bold yellow",
|
|
22
|
+
"destroy": "bold red",
|
|
23
|
+
"nochange": "dim",
|
|
24
|
+
"code": "#9CDCFE",
|
|
25
|
+
"muted": "dim white",
|
|
26
|
+
}
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
console = Console(theme=DATAMURU_THEME)
|
|
30
|
+
install(show_locals=False)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def render_plan_symbol(action: str) -> str:
|
|
34
|
+
"""Return the rich-styled symbol for a plan action."""
|
|
35
|
+
|
|
36
|
+
mapping = {
|
|
37
|
+
"create": "[create]+[/create]",
|
|
38
|
+
"update": "[update]~[/update]",
|
|
39
|
+
"destroy": "[destroy]-[/destroy]",
|
|
40
|
+
"noop": "[nochange]=[/nochange]",
|
|
41
|
+
}
|
|
42
|
+
return mapping[action]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def render_error(error: DataMuruError) -> None:
|
|
46
|
+
lines = [f"[error]{error.code}[/error] [primary]{error.title}[/primary]", error.description]
|
|
47
|
+
for key, value in error.context.items():
|
|
48
|
+
lines.append(f"[muted]{key}[/muted]: {value}")
|
|
49
|
+
if error.suggestion:
|
|
50
|
+
lines.append(f"[accent]Suggestion[/accent]: {error.suggestion}")
|
|
51
|
+
console.print(Panel.fit("\n".join(lines), border_style="error", title="DataMuru"))
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from datamuru.core.plan.models import Plan
|
|
4
|
+
|
|
5
|
+
from .executor import PlanExecutor
|
|
6
|
+
from .models import ApplyResult
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def apply_plan(plan: Plan, provider, state_backend) -> ApplyResult:
|
|
10
|
+
return PlanExecutor().execute(plan=plan, provider=provider, state_backend=state_backend)
|