ubunye-engine 0.1.1__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 (63) hide show
  1. ubunye/__init__.py +10 -0
  2. ubunye/backends/__init__.py +1 -0
  3. ubunye/backends/spark_backend.py +127 -0
  4. ubunye/cli/__init__.py +0 -0
  5. ubunye/cli/lineage.py +304 -0
  6. ubunye/cli/main.py +444 -0
  7. ubunye/cli/models.py +301 -0
  8. ubunye/cli/test_cmd.py +220 -0
  9. ubunye/compat/__init__.py +0 -0
  10. ubunye/compat/analytics_engine_shim.py +1 -0
  11. ubunye/config/__init__.py +6 -0
  12. ubunye/config/loader.py +136 -0
  13. ubunye/config/resolver.py +121 -0
  14. ubunye/config/schema.py +246 -0
  15. ubunye/core/__init__.py +0 -0
  16. ubunye/core/interfaces.py +67 -0
  17. ubunye/core/runtime.py +373 -0
  18. ubunye/lineage/__init__.py +23 -0
  19. ubunye/lineage/context.py +154 -0
  20. ubunye/lineage/hasher.py +149 -0
  21. ubunye/lineage/recorder.py +175 -0
  22. ubunye/lineage/storage.py +210 -0
  23. ubunye/models/__init__.py +17 -0
  24. ubunye/models/base.py +140 -0
  25. ubunye/models/gates.py +161 -0
  26. ubunye/models/loader.py +130 -0
  27. ubunye/models/registry.py +457 -0
  28. ubunye/orchestration/__init__.py +0 -0
  29. ubunye/orchestration/airflow_exporter.py +74 -0
  30. ubunye/orchestration/base.py +17 -0
  31. ubunye/orchestration/databricks_expoter.py +69 -0
  32. ubunye/plugins/__init__.py +0 -0
  33. ubunye/plugins/ml/__init__.py +0 -0
  34. ubunye/plugins/ml/adapters.py +35 -0
  35. ubunye/plugins/ml/base.py +231 -0
  36. ubunye/plugins/ml/pysparkml.py +47 -0
  37. ubunye/plugins/ml/sklearn.py +58 -0
  38. ubunye/plugins/readers/__init__.py +0 -0
  39. ubunye/plugins/readers/hive.py +25 -0
  40. ubunye/plugins/readers/jdbc.py +101 -0
  41. ubunye/plugins/readers/rest_api.py +444 -0
  42. ubunye/plugins/readers/s3.py +43 -0
  43. ubunye/plugins/readers/unity.py +46 -0
  44. ubunye/plugins/transforms/__init__.py +0 -0
  45. ubunye/plugins/transforms/model_transform.py +188 -0
  46. ubunye/plugins/transforms/noop.py +14 -0
  47. ubunye/plugins/writers/__init__.py +0 -0
  48. ubunye/plugins/writers/jdbc.py +76 -0
  49. ubunye/plugins/writers/rest_api.py +244 -0
  50. ubunye/plugins/writers/s3.py +22 -0
  51. ubunye/plugins/writers/unity.py +98 -0
  52. ubunye/telemetry/__init__.py +0 -0
  53. ubunye/telemetry/events.py +131 -0
  54. ubunye/telemetry/mlflow.py +75 -0
  55. ubunye/telemetry/monitors.py +54 -0
  56. ubunye/telemetry/otel.py +106 -0
  57. ubunye/telemetry/prometheus.py +128 -0
  58. ubunye_engine-0.1.1.dist-info/METADATA +289 -0
  59. ubunye_engine-0.1.1.dist-info/RECORD +63 -0
  60. ubunye_engine-0.1.1.dist-info/WHEEL +5 -0
  61. ubunye_engine-0.1.1.dist-info/entry_points.txt +27 -0
  62. ubunye_engine-0.1.1.dist-info/licenses/LICENSE +1 -0
  63. ubunye_engine-0.1.1.dist-info/top_level.txt +1 -0
ubunye/__init__.py ADDED
@@ -0,0 +1,10 @@
1
+ """Ubunye Engine package."""
2
+
3
+ from importlib.metadata import PackageNotFoundError, version
4
+
5
+ try:
6
+ __version__ = version("ubunye-engine")
7
+ except PackageNotFoundError:
8
+ __version__ = "unknown"
9
+
10
+ __all__ = ["core", "config", "cli", "plugins", "backends"]
@@ -0,0 +1 @@
1
+ from .spark_backend import SparkBackend as SparkBackend
@@ -0,0 +1,127 @@
1
+ """
2
+ Spark backend implementation for Ubunye.
3
+
4
+ - Lazily imports pyspark so users can install the package without Spark.
5
+ - Provides a simple lifecycle: start() / stop().
6
+ - Supports context-manager usage: `with SparkBackend(...) as backend: ...`
7
+ - Guards against double-starts and exposes the effective Spark conf for debugging.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from typing import TYPE_CHECKING, Dict, Optional
13
+
14
+ from ubunye.core.interfaces import Backend
15
+
16
+ if TYPE_CHECKING: # only for type checkers; no runtime dependency on pyspark
17
+ from pyspark.sql import SparkSession
18
+
19
+
20
+ class SparkBackend(Backend):
21
+ """Creates and manages a SparkSession for a Ubunye run.
22
+
23
+ Parameters
24
+ ----------
25
+ app_name : str
26
+ Spark application name (appears in Spark UI/history server).
27
+ conf : Optional[Dict[str, str]]
28
+ Spark configuration key-values (e.g., {"spark.master": "yarn"}).
29
+
30
+ Notes
31
+ -----
32
+ - pyspark is imported lazily inside `start()` to keep installation lightweight.
33
+ - `spark` property is only valid after `start()` (or inside the context manager).
34
+ """
35
+
36
+ def __init__(self, app_name: str = "ubunye", conf: Optional[Dict[str, str]] = None) -> None:
37
+ self._spark: Optional["SparkSession"] = None
38
+ self._app_name = app_name
39
+ self._conf = dict(conf or {})
40
+
41
+ # -------------------------
42
+ # Lifecycle
43
+ # -------------------------
44
+ def start(self) -> None:
45
+ """Create (or reuse) a SparkSession.
46
+
47
+ Safe to call multiple times; a second call is a no-op if a session already exists.
48
+ """
49
+ if self._spark is not None:
50
+ return # already started
51
+
52
+ # Lazy import to avoid hard dependency during pip install
53
+ from pyspark.sql import SparkSession # type: ignore
54
+
55
+ builder = SparkSession.builder.appName(self._app_name)
56
+ for k, v in self._conf.items():
57
+ builder = builder.config(k, v)
58
+ self._spark = builder.getOrCreate()
59
+
60
+ def stop(self) -> None:
61
+ """Stop the SparkSession if running."""
62
+ if self._spark is not None:
63
+ try:
64
+ self._spark.stop()
65
+ finally:
66
+ self._spark = None
67
+
68
+ # Context manager support
69
+ def __enter__(self) -> "SparkBackend":
70
+ self.start()
71
+ return self
72
+
73
+ def __exit__(self, exc_type, exc, tb) -> None:
74
+ self.stop()
75
+
76
+ def __del__(self) -> None:
77
+ # Best-effort cleanup; not guaranteed to run in all interpreter shutdown scenarios.
78
+ try:
79
+ self.stop()
80
+ except Exception:
81
+ # Avoid noisy destructor exceptions at process teardown.
82
+ pass
83
+
84
+ # -------------------------
85
+ # Properties
86
+ # -------------------------
87
+ @property
88
+ def spark(self) -> "SparkSession":
89
+ """Return the active SparkSession (after `start()`).
90
+
91
+ Raises
92
+ ------
93
+ RuntimeError
94
+ If `start()` has not been called.
95
+ """
96
+ if self._spark is None:
97
+ raise RuntimeError(
98
+ "Spark session not started. Call start() first or use context manager."
99
+ )
100
+ return self._spark
101
+
102
+ @property
103
+ def is_spark(self) -> bool:
104
+ """Whether this backend is Spark-based (always True here)."""
105
+ return True
106
+
107
+ @property
108
+ def app_name(self) -> str:
109
+ """Configured Spark app name."""
110
+ return self._app_name
111
+
112
+ @property
113
+ def conf_input(self) -> Dict[str, str]:
114
+ """The configuration dict passed into this backend at construction time."""
115
+ return dict(self._conf)
116
+
117
+ @property
118
+ def conf_effective(self) -> Dict[str, str]:
119
+ """The *effective* Spark configuration currently applied to the session.
120
+
121
+ Returns an empty dict if the session hasn't been started yet.
122
+ """
123
+ if self._spark is None:
124
+ return {}
125
+ sc = self._spark.sparkContext
126
+ # Convert list[tuple[str,str]] to dict[str,str]
127
+ return {k: v for (k, v) in sc.getConf().getAll()}
ubunye/cli/__init__.py ADDED
File without changes
ubunye/cli/lineage.py ADDED
@@ -0,0 +1,304 @@
1
+ """CLI commands for inspecting run lineage records.
2
+
3
+ Mounted on the main app as a sub-command group:
4
+
5
+ ubunye lineage show -d DIR -u USECASE -p PKG -t TASK [--run-id ID]
6
+ ubunye lineage list -d DIR -u USECASE -p PKG -t TASK [-n 10]
7
+ ubunye lineage compare -d DIR -u USECASE -p PKG -t TASK --run-id1 ID1 --run-id2 ID2
8
+ ubunye lineage search -d DIR [--status error] [--since 2025-01-01]
9
+ ubunye lineage trace -d DIR -u USECASE -p PKG -t TASK
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import json
15
+ from pathlib import Path
16
+ from typing import Optional
17
+
18
+ import typer
19
+
20
+ from ubunye.lineage.context import RunContext
21
+ from ubunye.lineage.storage import FileSystemLineageStore
22
+
23
+ lineage_app = typer.Typer(name="lineage", help="Inspect run lineage records.", add_completion=False)
24
+
25
+ # ---------------------------------------------------------------------------
26
+ # Shared helpers
27
+ # ---------------------------------------------------------------------------
28
+
29
+ _LINE = "-" * 60
30
+
31
+
32
+ def _store(lineage_dir: str) -> FileSystemLineageStore:
33
+ return FileSystemLineageStore(lineage_dir)
34
+
35
+
36
+ def _task_path(usecase: str, package: str, task: str) -> str:
37
+ return f"{usecase}/{package}/{task}"
38
+
39
+
40
+ def _fmt_row(ctx: RunContext) -> str:
41
+ duration = f"{ctx.duration_sec:.1f}s" if ctx.duration_sec is not None else "-"
42
+ in_rows = sum(s.row_count or 0 for s in ctx.inputs)
43
+ out_rows = sum(s.row_count or 0 for s in ctx.outputs)
44
+ return (
45
+ f"{ctx.run_id[:8]} {ctx.started_at[:19]} "
46
+ f"{ctx.status:<8} {duration:>7} "
47
+ f"in:{in_rows} out:{out_rows}"
48
+ )
49
+
50
+
51
+ # ---------------------------------------------------------------------------
52
+ # show
53
+ # ---------------------------------------------------------------------------
54
+
55
+
56
+ @lineage_app.command("show")
57
+ def show(
58
+ usecase_dir: Path = typer.Option(
59
+ ..., "-d", "--usecase-dir", help="Root directory of pipelines."
60
+ ),
61
+ usecase: str = typer.Option(..., "-u", "--usecase"),
62
+ package: str = typer.Option(..., "-p", "--package"),
63
+ task: str = typer.Option(..., "-t", "--task"),
64
+ run_id: Optional[str] = typer.Option(
65
+ None, "--run-id", help="Specific run ID (default: latest)."
66
+ ),
67
+ lineage_dir: str = typer.Option(".ubunye/lineage", "--lineage-dir"),
68
+ ):
69
+ """Show a run record as formatted JSON (latest or specific run)."""
70
+ store = _store(str(usecase_dir / lineage_dir))
71
+ tp = _task_path(usecase, package, task)
72
+
73
+ try:
74
+ if run_id:
75
+ ctx = store.load(tp, run_id)
76
+ else:
77
+ runs = store.list_runs(tp, n=1)
78
+ if not runs:
79
+ typer.secho(f"No lineage records found for task '{tp}'.", fg=typer.colors.YELLOW)
80
+ raise typer.Exit(code=1)
81
+ ctx = runs[0]
82
+ except FileNotFoundError as e:
83
+ typer.secho(str(e), fg=typer.colors.RED, err=True)
84
+ raise typer.Exit(code=1)
85
+
86
+ typer.echo(json.dumps(ctx.to_dict(), indent=2))
87
+
88
+
89
+ # ---------------------------------------------------------------------------
90
+ # list
91
+ # ---------------------------------------------------------------------------
92
+
93
+
94
+ @lineage_app.command("list")
95
+ def list_runs(
96
+ usecase_dir: Path = typer.Option(..., "-d", "--usecase-dir"),
97
+ usecase: str = typer.Option(..., "-u", "--usecase"),
98
+ package: str = typer.Option(..., "-p", "--package"),
99
+ task: str = typer.Option(..., "-t", "--task"),
100
+ n: int = typer.Option(10, "-n", "--n", help="Number of recent runs to show."),
101
+ lineage_dir: str = typer.Option(".ubunye/lineage", "--lineage-dir"),
102
+ ):
103
+ """List recent runs for a task (newest first)."""
104
+ store = _store(str(usecase_dir / lineage_dir))
105
+ tp = _task_path(usecase, package, task)
106
+ runs = store.list_runs(tp, n=n)
107
+
108
+ if not runs:
109
+ typer.secho(f"No lineage records found for '{tp}'.", fg=typer.colors.YELLOW)
110
+ return
111
+
112
+ typer.echo(_LINE)
113
+ typer.echo(f"Task: {tp}")
114
+ typer.echo(_LINE)
115
+ typer.echo(f"{'RUN_ID':9} {'STARTED':19} {'STATUS':<8} {'DUR':>7} ROWS")
116
+ typer.echo(_LINE)
117
+ for ctx in runs:
118
+ color = (
119
+ typer.colors.GREEN
120
+ if ctx.status == "success"
121
+ else (typer.colors.RED if ctx.status == "error" else typer.colors.YELLOW)
122
+ )
123
+ typer.secho(_fmt_row(ctx), fg=color)
124
+
125
+
126
+ # ---------------------------------------------------------------------------
127
+ # compare
128
+ # ---------------------------------------------------------------------------
129
+
130
+
131
+ @lineage_app.command("compare")
132
+ def compare(
133
+ usecase_dir: Path = typer.Option(..., "-d", "--usecase-dir"),
134
+ usecase: str = typer.Option(..., "-u", "--usecase"),
135
+ package: str = typer.Option(..., "-p", "--package"),
136
+ task: str = typer.Option(..., "-t", "--task"),
137
+ run_id1: str = typer.Option(..., "--run-id1", help="First run ID."),
138
+ run_id2: str = typer.Option(..., "--run-id2", help="Second run ID."),
139
+ lineage_dir: str = typer.Option(".ubunye/lineage", "--lineage-dir"),
140
+ ):
141
+ """Diff two run records — highlight changes in hashes, row counts, and status."""
142
+ store = _store(str(usecase_dir / lineage_dir))
143
+ tp = _task_path(usecase, package, task)
144
+
145
+ try:
146
+ a = store.load(tp, run_id1)
147
+ b = store.load(tp, run_id2)
148
+ except FileNotFoundError as e:
149
+ typer.secho(str(e), fg=typer.colors.RED, err=True)
150
+ raise typer.Exit(code=1)
151
+
152
+ typer.echo(_LINE)
153
+ typer.echo(f"Comparing runs for '{tp}'")
154
+ typer.echo(f" A: {a.run_id[:8]} {a.started_at[:19]} {a.status}")
155
+ typer.echo(f" B: {b.run_id[:8]} {b.started_at[:19]} {b.status}")
156
+ typer.echo(_LINE)
157
+
158
+ def _cmp(label: str, va: object, vb: object) -> None:
159
+ if va == vb:
160
+ typer.echo(f" {label}: {va} (unchanged)")
161
+ else:
162
+ typer.secho(f" {label}: {va} → {vb} CHANGED", fg=typer.colors.YELLOW)
163
+
164
+ _cmp("status", a.status, b.status)
165
+ _cmp("duration_sec", a.duration_sec, b.duration_sec)
166
+ _cmp("config_hash", a.config_hash, b.config_hash)
167
+
168
+ # Compare outputs step by step
169
+ a_out = {s.name: s for s in a.outputs}
170
+ b_out = {s.name: s for s in b.outputs}
171
+ all_names = sorted(set(a_out) | set(b_out))
172
+ for name in all_names:
173
+ sa = a_out.get(name)
174
+ sb = b_out.get(name)
175
+ typer.echo(f" Output '{name}':")
176
+ _cmp(" row_count", sa.row_count if sa else None, sb.row_count if sb else None)
177
+ _cmp(" schema_hash", sa.schema_hash if sa else None, sb.schema_hash if sb else None)
178
+ _cmp(" data_hash", sa.data_hash if sa else None, sb.data_hash if sb else None)
179
+
180
+
181
+ # ---------------------------------------------------------------------------
182
+ # search
183
+ # ---------------------------------------------------------------------------
184
+
185
+
186
+ @lineage_app.command("search")
187
+ def search(
188
+ usecase_dir: Path = typer.Option(..., "-d", "--usecase-dir"),
189
+ task: Optional[str] = typer.Option(None, "-t", "--task", help="Restrict to this task name."),
190
+ usecase: Optional[str] = typer.Option(
191
+ None, "-u", "--usecase", help="Restrict to this usecase."
192
+ ),
193
+ package: Optional[str] = typer.Option(
194
+ None, "-p", "--package", help="Restrict to this package."
195
+ ),
196
+ status: Optional[str] = typer.Option(
197
+ None, "--status", help="Filter by status: success|error|running."
198
+ ),
199
+ since: Optional[str] = typer.Option(
200
+ None, "--since", help="Only runs started on or after this ISO date."
201
+ ),
202
+ lineage_dir: str = typer.Option(".ubunye/lineage", "--lineage-dir"),
203
+ ):
204
+ """Search all recorded runs across tasks with optional filters."""
205
+ store = _store(str(usecase_dir / lineage_dir))
206
+
207
+ tp = None
208
+ if usecase and package and task:
209
+ tp = _task_path(usecase, package, task)
210
+ elif usecase and package:
211
+ tp = f"{usecase}/{package}"
212
+ elif usecase:
213
+ tp = usecase
214
+
215
+ runs = store.search(task_path=tp, status=status, since=since)
216
+
217
+ if not runs:
218
+ typer.secho("No matching lineage records found.", fg=typer.colors.YELLOW)
219
+ return
220
+
221
+ typer.echo(_LINE)
222
+ typer.echo(f"{'RUN_ID':9} {'TASK PATH':40} {'STARTED':19} {'STATUS':<8} DUR")
223
+ typer.echo(_LINE)
224
+ for ctx in runs:
225
+ color = (
226
+ typer.colors.GREEN
227
+ if ctx.status == "success"
228
+ else (typer.colors.RED if ctx.status == "error" else typer.colors.YELLOW)
229
+ )
230
+ duration = f"{ctx.duration_sec:.1f}s" if ctx.duration_sec is not None else "-"
231
+ typer.secho(
232
+ f"{ctx.run_id[:8]} {ctx.task_path:<40} {ctx.started_at[:19]} "
233
+ f"{ctx.status:<8} {duration}",
234
+ fg=color,
235
+ )
236
+
237
+
238
+ # ---------------------------------------------------------------------------
239
+ # trace
240
+ # ---------------------------------------------------------------------------
241
+
242
+
243
+ @lineage_app.command("trace")
244
+ def trace(
245
+ usecase_dir: Path = typer.Option(..., "-d", "--usecase-dir"),
246
+ usecase: str = typer.Option(..., "-u", "--usecase"),
247
+ package: str = typer.Option(..., "-p", "--package"),
248
+ task: str = typer.Option(..., "-t", "--task"),
249
+ run_id: Optional[str] = typer.Option(None, "--run-id", help="Specific run (default: latest)."),
250
+ lineage_dir: str = typer.Option(".ubunye/lineage", "--lineage-dir"),
251
+ ):
252
+ """Print the input → transform → output data flow graph for a run."""
253
+ store = _store(str(usecase_dir / lineage_dir))
254
+ tp = _task_path(usecase, package, task)
255
+
256
+ try:
257
+ if run_id:
258
+ ctx = store.load(tp, run_id)
259
+ else:
260
+ runs = store.list_runs(tp, n=1)
261
+ if not runs:
262
+ typer.secho(f"No lineage records for '{tp}'.", fg=typer.colors.YELLOW)
263
+ raise typer.Exit(code=1)
264
+ ctx = runs[0]
265
+ except FileNotFoundError as e:
266
+ typer.secho(str(e), fg=typer.colors.RED, err=True)
267
+ raise typer.Exit(code=1)
268
+
269
+ typer.echo()
270
+ typer.secho(f"Lineage trace: {ctx.task_path}", bold=True)
271
+ typer.echo(f"Run: {ctx.run_id} [{ctx.status}] {ctx.started_at[:19]}")
272
+ typer.echo(f"Version: {ctx.model} v{ctx.version}")
273
+ typer.echo()
274
+
275
+ # INPUTS
276
+ typer.secho(" INPUTS", fg=typer.colors.CYAN)
277
+ for step in ctx.inputs:
278
+ typer.echo(f" [{step.format}] {step.name}")
279
+ typer.echo(f" location : {step.location}")
280
+ if step.row_count is not None:
281
+ typer.echo(f" rows : {step.row_count:,}")
282
+ if step.schema_hash:
283
+ typer.echo(f" schema : {step.schema_hash}")
284
+ if step.data_hash:
285
+ typer.echo(f" data : {step.data_hash}")
286
+
287
+ # TRANSFORM
288
+ typer.echo()
289
+ typer.secho(" TRANSFORM", fg=typer.colors.CYAN)
290
+ typer.echo(" (transformations.py)")
291
+
292
+ # OUTPUTS
293
+ typer.echo()
294
+ typer.secho(" OUTPUTS", fg=typer.colors.CYAN)
295
+ for step in ctx.outputs:
296
+ typer.echo(f" [{step.format}] {step.name}")
297
+ typer.echo(f" location : {step.location}")
298
+ if step.row_count is not None:
299
+ typer.echo(f" rows : {step.row_count:,}")
300
+ if step.schema_hash:
301
+ typer.echo(f" schema : {step.schema_hash}")
302
+ if step.data_hash:
303
+ typer.echo(f" data : {step.data_hash}")
304
+ typer.echo()