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.
Files changed (71) hide show
  1. datamuru/__init__.py +4 -0
  2. datamuru/api.py +50 -0
  3. datamuru/bootstrap.py +141 -0
  4. datamuru/cli/__init__.py +1 -0
  5. datamuru/cli/commands/__init__.py +19 -0
  6. datamuru/cli/commands/apply.py +45 -0
  7. datamuru/cli/commands/destroy.py +23 -0
  8. datamuru/cli/commands/doctor.py +31 -0
  9. datamuru/cli/commands/edition.py +32 -0
  10. datamuru/cli/commands/import_.py +83 -0
  11. datamuru/cli/commands/init.py +25 -0
  12. datamuru/cli/commands/plan.py +32 -0
  13. datamuru/cli/commands/validate.py +26 -0
  14. datamuru/cli/guard.py +19 -0
  15. datamuru/cli/main.py +22 -0
  16. datamuru/cli/output.py +51 -0
  17. datamuru/core/__init__.py +1 -0
  18. datamuru/core/apply/__init__.py +5 -0
  19. datamuru/core/apply/engine.py +10 -0
  20. datamuru/core/apply/executor.py +88 -0
  21. datamuru/core/apply/models.py +16 -0
  22. datamuru/core/apply.py +31 -0
  23. datamuru/core/config/__init__.py +31 -0
  24. datamuru/core/config/models.py +75 -0
  25. datamuru/core/config/parser.py +44 -0
  26. datamuru/core/config/resolver.py +79 -0
  27. datamuru/core/config/validator.py +370 -0
  28. datamuru/core/config.py +123 -0
  29. datamuru/core/engine.py +123 -0
  30. datamuru/core/importer/__init__.py +5 -0
  31. datamuru/core/importer/discovery.py +9 -0
  32. datamuru/core/importer/engine.py +76 -0
  33. datamuru/core/importer/generator.py +20 -0
  34. datamuru/core/importer/models.py +43 -0
  35. datamuru/core/models.py +80 -0
  36. datamuru/core/plan/__init__.py +5 -0
  37. datamuru/core/plan/engine.py +122 -0
  38. datamuru/core/plan/models.py +60 -0
  39. datamuru/core/plan/renderer.py +15 -0
  40. datamuru/core/plan.py +66 -0
  41. datamuru/core/schema.py +91 -0
  42. datamuru/core/state/__init__.py +5 -0
  43. datamuru/core/state/backends/__init__.py +4 -0
  44. datamuru/core/state/backends/base.py +13 -0
  45. datamuru/core/state/backends/local.py +33 -0
  46. datamuru/core/state/manager.py +20 -0
  47. datamuru/core/state/models.py +28 -0
  48. datamuru/core/state.py +22 -0
  49. datamuru/edition.py +74 -0
  50. datamuru/errors.py +92 -0
  51. datamuru/governance/__init__.py +1 -0
  52. datamuru/governance/masking.py +19 -0
  53. datamuru/governance/rbac.py +50 -0
  54. datamuru/governance/taxonomy.py +29 -0
  55. datamuru/modeling.py +7 -0
  56. datamuru/providers/__init__.py +1 -0
  57. datamuru/providers/base.py +32 -0
  58. datamuru/providers/databricks/__init__.py +1 -0
  59. datamuru/providers/databricks/auth.py +64 -0
  60. datamuru/providers/databricks/client.py +837 -0
  61. datamuru/providers/databricks/execution.py +38 -0
  62. datamuru/providers/databricks/provider.py +1061 -0
  63. datamuru/providers/factory.py +14 -0
  64. datamuru/py.typed +1 -0
  65. datamuru/types.py +74 -0
  66. datamuru-0.1.0a0.dist-info/METADATA +168 -0
  67. datamuru-0.1.0a0.dist-info/RECORD +71 -0
  68. datamuru-0.1.0a0.dist-info/WHEEL +5 -0
  69. datamuru-0.1.0a0.dist-info/entry_points.txt +2 -0
  70. datamuru-0.1.0a0.dist-info/licenses/LICENSE +201 -0
  71. datamuru-0.1.0a0.dist-info/top_level.txt +1 -0
datamuru/__init__.py ADDED
@@ -0,0 +1,4 @@
1
+ from .api import DataMuru
2
+
3
+ __all__ = ["DataMuru"]
4
+ __version__ = "0.1.0a0"
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
@@ -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,5 @@
1
+ from .engine import apply_plan
2
+ from .executor import PlanExecutor
3
+ from .models import ApplyFailure, ApplyResult
4
+
5
+ __all__ = ["ApplyFailure", "ApplyResult", "PlanExecutor", "apply_plan"]
@@ -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)