pipeloom-databricks 0.5.0__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.
@@ -0,0 +1,561 @@
1
+ """The `pipeloom databricks` command group, added through the `pipeloom.commands` entry point."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ import re
8
+ import time
9
+ from collections.abc import Callable
10
+ from dataclasses import dataclass
11
+ from pathlib import Path
12
+ from typing import Any, Final
13
+
14
+ import click
15
+
16
+ from pipeloom.api import Project, ProjectOptions
17
+ from pipeloom.cli._events import Invocation
18
+ from pipeloom.cli._options import (
19
+ log_format_option,
20
+ plugin_option,
21
+ project_options,
22
+ select_options,
23
+ target_option,
24
+ )
25
+ from pipeloom.config.bind import BoundProject
26
+ from pipeloom.diagnostics import Report
27
+ from pipeloom.engine.types import StateStoreCapabilities
28
+ from pipeloom.graph.selectors import Selection
29
+ from pipeloom.plan.model import Plan
30
+ from pipeloom.runtime.run import DEFAULT_CANCEL_GRACE
31
+ from pipeloom_databricks.bundle import Runtime, find_bundle_root, relative_to_root, render_bundle
32
+ from pipeloom_databricks.client import (
33
+ ConnectionNotConfigured,
34
+ DatabricksClient,
35
+ connect,
36
+ )
37
+ from pipeloom_databricks.deploy import api_errors, find_job, prepare
38
+ from pipeloom_databricks.deploy import deploy as deploy_pipeline
39
+ from pipeloom_databricks.jobs_model import to_json
40
+ from pipeloom_databricks.options import DatabricksDeployOptions
41
+ from pipeloom_databricks.render import deploy_options, job_name
42
+ from pipeloom_databricks.repair import repair as repair_run
43
+ from pipeloom_databricks.run import (
44
+ SelectionError,
45
+ WaitTimeout,
46
+ effective_run_id,
47
+ failure_lines,
48
+ find_run,
49
+ run_job,
50
+ submit_ephemeral,
51
+ wait_for_run,
52
+ )
53
+
54
+ CONNECTION_KEY: Final = "pipeloom_databricks.connection"
55
+ SLEEP_KEY: Final = "pipeloom_databricks.sleep"
56
+ DEFAULT_TIMEOUT: Final = 24 * 3600
57
+ _RUN_ID = re.compile(r"^[A-Za-z0-9._-]{1,64}$")
58
+
59
+
60
+ @dataclass
61
+ class Hooks:
62
+ """What a test hands to the command group as `obj`: a stand-in for `time.sleep`."""
63
+
64
+ sleep: Callable[[float], None] = time.sleep
65
+
66
+
67
+ def _load(
68
+ inv: Invocation,
69
+ project_dir: Path,
70
+ target: str | None,
71
+ vars_: dict[str, Any],
72
+ plugins: tuple[str, ...],
73
+ ) -> tuple[Plan, BoundProject, Runtime, Report]:
74
+ """Compile and bind the project; ends the command with exit 1 when that fails."""
75
+ project = Project(
76
+ ProjectOptions(
77
+ project_dir=project_dir, target=target, vars=vars_, plugins=plugins, env=os.environ
78
+ ),
79
+ invocation_id=inv.invocation_id,
80
+ )
81
+ compiled = project.compile()
82
+ bound = project.bind().value
83
+ if compiled.value is None or bound is None:
84
+ inv.finish(compiled.diagnostics, 1, bound=bound)
85
+ raise SystemExit(1)
86
+ store = bound.plugins.get("state_stores")
87
+ caps = None if store is None else store.spec.capabilities
88
+ grace = bound.runtime.cancel_grace
89
+ runtime = Runtime(
90
+ artifacts_root=bound.runtime.artifacts.root,
91
+ state_shared=isinstance(caps, StateStoreCapabilities) and caps.shared,
92
+ cancel_grace=int(DEFAULT_CANCEL_GRACE if grace is None else grace),
93
+ )
94
+ report = Report()
95
+ report.extend(compiled.diagnostics)
96
+ return compiled.value, bound, runtime, report
97
+
98
+
99
+ @click.group()
100
+ @click.option(
101
+ "--profile",
102
+ metavar="NAME",
103
+ default=None,
104
+ help="The Databricks profile to use. Without it, DATABRICKS_CONFIG_PROFILE, then "
105
+ "DATABRICKS_HOST, then the runtime; pipeloom never picks a profile by itself.",
106
+ )
107
+ @click.option("--client-factory", hidden=True, default=None, help="MODULE:ATTR of a test client.")
108
+ @click.pass_context
109
+ def databricks(ctx: click.Context, profile: str | None, client_factory: str | None) -> None:
110
+ """Databricks jobs for a pipeloom project."""
111
+ ctx.meta[CONNECTION_KEY] = (profile, client_factory)
112
+ ctx.meta[SLEEP_KEY] = ctx.obj.sleep if isinstance(ctx.obj, Hooks) else time.sleep
113
+
114
+
115
+ def _connect(
116
+ ctx: click.Context, inv: Invocation, report: Report, bound: BoundProject | None
117
+ ) -> DatabricksClient:
118
+ """The client the group options select; ends the command with PLD010 (exit 1) when none."""
119
+ profile, factory = ctx.meta.get(CONNECTION_KEY, (None, None))
120
+ try:
121
+ return connect(profile=profile, env=os.environ, factory=factory)
122
+ except ConnectionNotConfigured as err:
123
+ report.add("PLD010", str(err))
124
+ inv.finish(report.sorted(), 1, bound=bound)
125
+ raise SystemExit(1) from None
126
+
127
+
128
+ def _pipeline(name: str | None, opts: DatabricksDeployOptions) -> str:
129
+ """The pipeline to act on: `name`, or the only one of the deploy block. Exit 2 otherwise."""
130
+ if name is None:
131
+ if len(opts.pipelines) != 1:
132
+ raise click.UsageError(
133
+ f"--pipeline is required: the deploy block has {', '.join(sorted(opts.pipelines))}"
134
+ )
135
+ return next(iter(opts.pipelines))
136
+ if name not in opts.pipelines:
137
+ raise click.UsageError(
138
+ f"pipeline '{name}' is not in the deploy block ({', '.join(sorted(opts.pipelines))})"
139
+ )
140
+ return name
141
+
142
+
143
+ def _run_id(ctx: click.Context, param: click.Parameter, value: str | None) -> str | None:
144
+ if value is not None and not _RUN_ID.match(value):
145
+ raise click.BadParameter(
146
+ "use 1 to 64 letters, digits, dots, underscores or hyphens", ctx=ctx, param=param
147
+ )
148
+ return value
149
+
150
+
151
+ def _echo_started(job_id: int | None, job_run_id: int, run_id: str | None, url: str | None) -> None:
152
+ doc = {"job_id": job_id, "job_run_id": job_run_id, "run_id": run_id, "run_page_url": url}
153
+ click.echo(json.dumps(doc, ensure_ascii=True))
154
+
155
+
156
+ def _follow(
157
+ ctx: click.Context,
158
+ client: DatabricksClient,
159
+ job_run_id: int,
160
+ run_id: str | None,
161
+ runtime: Runtime,
162
+ report: Report,
163
+ *,
164
+ repair_id: int | None = None,
165
+ timeout: int = DEFAULT_TIMEOUT,
166
+ ) -> int:
167
+ """Wait for the run, say what happened to its tasks and units; the exit code of the command."""
168
+ say: Callable[[str], None] = lambda line: click.echo(line, err=True)
169
+ code = 1
170
+ with api_errors(report):
171
+ try:
172
+ run = wait_for_run(
173
+ client,
174
+ job_run_id,
175
+ sleep=ctx.meta[SLEEP_KEY],
176
+ say=say,
177
+ repair_id=repair_id,
178
+ timeout=timeout,
179
+ )
180
+ except WaitTimeout as err:
181
+ say(f"{err}; the run keeps going")
182
+ return 1
183
+ if run.result_state == "SUCCESS":
184
+ code = 0
185
+ else:
186
+ say(f"run {job_run_id} ended: {run.life_cycle_state}/{run.result_state}")
187
+ for line in failure_lines(
188
+ client, runtime.artifacts_root, run_id or effective_run_id(run)
189
+ ):
190
+ say(line)
191
+ return code
192
+
193
+
194
+ @databricks.command()
195
+ @project_options
196
+ @target_option
197
+ @plugin_option
198
+ @log_format_option
199
+ @click.option(
200
+ "--out",
201
+ type=click.Path(file_okay=False, path_type=Path),
202
+ required=True,
203
+ help="Folder for the generated files; it must lie inside the bundle.",
204
+ )
205
+ @click.option(
206
+ "--pipeline",
207
+ "pipelines",
208
+ multiple=True,
209
+ metavar="NAME",
210
+ help="A pipeline of the deploy block. Repeatable. Default: all of them.",
211
+ )
212
+ @click.option(
213
+ "--bundle-root",
214
+ type=click.Path(exists=True, file_okay=False, path_type=Path),
215
+ default=None,
216
+ help="The bundle's root folder. Default: the nearest folder above --out with databricks.yml.",
217
+ )
218
+ @click.option(
219
+ "--check",
220
+ is_flag=True,
221
+ help="Write nothing; exit 1 and list the files that differ from what would be written.",
222
+ )
223
+ def bundle(
224
+ project_dir: Path,
225
+ vars_: dict[str, Any],
226
+ target: str | None,
227
+ plugins: tuple[str, ...],
228
+ log_format: str,
229
+ out: Path,
230
+ pipelines: tuple[str, ...],
231
+ bundle_root: Path | None,
232
+ check: bool,
233
+ ) -> None:
234
+ """Write one job file and one manifest per pipeline into a bundle.
235
+
236
+ The job is <out>/pipeloom_<project>_<pipeline>_<target>.job.yml and refers to the project and
237
+ the manifest through ${workspace.file_path}. Exit codes: 0 ok, 1 a diagnostic or a changed
238
+ file with --check, 2 bad usage.
239
+ """
240
+ root = find_bundle_root(out, bundle_root)
241
+ if root is None:
242
+ raise click.UsageError("no databricks.yml above --out; pass --bundle-root")
243
+ project_rel = relative_to_root(project_dir, root)
244
+ out_rel = relative_to_root(out, root)
245
+ if project_rel is None or out_rel is None:
246
+ raise click.UsageError(f"the project dir and --out must lie inside the bundle {root}")
247
+
248
+ inv = Invocation("databricks bundle", log_format)
249
+ loaded = _load(inv, project_dir, target, vars_, plugins)
250
+ plan, bound, runtime, report = loaded
251
+ files = render_bundle(
252
+ plan,
253
+ runtime,
254
+ pipelines,
255
+ out=out,
256
+ project_rel=project_rel,
257
+ out_rel=out_rel,
258
+ report=report,
259
+ )
260
+ diagnostics = report.sorted()
261
+ if files is None:
262
+ inv.finish(diagnostics, 1, bound=bound)
263
+ return
264
+ changed = [f for f in files if not f.path.is_file() or f.path.read_bytes() != f.content]
265
+ if check:
266
+ for f in changed:
267
+ click.echo(f.path.as_posix())
268
+ inv.finish(diagnostics, 1 if changed else 0, bound=bound)
269
+ return
270
+ for f in files:
271
+ f.path.parent.mkdir(parents=True, exist_ok=True)
272
+ f.path.write_bytes(f.content)
273
+ click.echo(f.path.as_posix())
274
+ inv.finish(diagnostics, 0, bound=bound)
275
+
276
+
277
+ @databricks.command()
278
+ @project_options
279
+ @target_option
280
+ @plugin_option
281
+ @log_format_option
282
+ @click.option(
283
+ "--pipeline",
284
+ "pipelines",
285
+ multiple=True,
286
+ metavar="NAME",
287
+ help="A pipeline of the deploy block. Repeatable. Default: all of them.",
288
+ )
289
+ @click.option(
290
+ "--dry-run",
291
+ is_flag=True,
292
+ help="Print the job settings as JSON and connect to nothing.",
293
+ )
294
+ @click.pass_context
295
+ def deploy(
296
+ ctx: click.Context,
297
+ /,
298
+ project_dir: Path,
299
+ vars_: dict[str, Any],
300
+ target: str | None,
301
+ plugins: tuple[str, ...],
302
+ log_format: str,
303
+ pipelines: tuple[str, ...],
304
+ dry_run: bool,
305
+ ) -> None:
306
+ """Upload the project to the workspace and create or update one job per pipeline.
307
+
308
+ Prints one JSON line per pipeline: {pipeline, job_id, action, root}. With --dry-run it prints
309
+ {pipeline, root, settings} instead and creates no client. Exit codes: 0 ok, 1 a diagnostic.
310
+ """
311
+ inv = Invocation("databricks deploy", log_format)
312
+ plan, bound, runtime, report = _load(inv, project_dir, target, vars_, plugins)
313
+ opts = deploy_options(plan, report)
314
+ names = pipelines or (sorted(opts.pipelines) if opts else ())
315
+ for name in names:
316
+ if opts is not None and name not in opts.pipelines:
317
+ report.add("PLD004", f"pipeline '{name}' is not in the deploy block")
318
+ if opts is None or report.has_errors:
319
+ inv.finish(report.sorted(), 1, bound=bound)
320
+ return
321
+ if dry_run:
322
+ for name in names:
323
+ prepared = prepare(project_dir, plan, name, runtime, report, user=lambda: "<user>")
324
+ if prepared is not None:
325
+ doc: dict[str, Any] = {
326
+ "pipeline": name,
327
+ "root": prepared.root,
328
+ "settings": to_json(prepared.job.settings),
329
+ }
330
+ click.echo(json.dumps(doc, ensure_ascii=True))
331
+ else:
332
+ client = _connect(ctx, inv, report, bound)
333
+ for name in names:
334
+ result = deploy_pipeline(client, project_dir, plan, name, runtime, report=report)
335
+ if result is not None:
336
+ doc = {
337
+ "pipeline": name,
338
+ "job_id": result.job_id,
339
+ "action": result.action,
340
+ "root": result.root,
341
+ }
342
+ click.echo(json.dumps(doc, ensure_ascii=True))
343
+ inv.finish(report.sorted(), 1 if report.has_errors else 0, bound=bound)
344
+
345
+
346
+ _PIPELINE_OPTION = click.option(
347
+ "--pipeline",
348
+ metavar="NAME",
349
+ default=None,
350
+ help="A pipeline of the deploy block. Default: the only one.",
351
+ )
352
+
353
+
354
+ def _wait_options(func: Callable[..., Any]) -> Callable[..., Any]:
355
+ """--wait/--no-wait and --timeout, shared by run and repair."""
356
+ func = click.option(
357
+ "--timeout",
358
+ type=click.IntRange(min=1),
359
+ default=DEFAULT_TIMEOUT,
360
+ show_default=True,
361
+ metavar="SECONDS",
362
+ help="With --wait, stop waiting after this long and exit 1. The run keeps going.",
363
+ )(func)
364
+ return click.option(
365
+ "--wait/--no-wait",
366
+ default=True,
367
+ show_default=True,
368
+ help="Wait for the run to end (polling every 15 seconds) and exit 1 unless it succeeded.",
369
+ )(func)
370
+
371
+
372
+ @databricks.command()
373
+ @project_options
374
+ @target_option
375
+ @plugin_option
376
+ @log_format_option
377
+ @select_options
378
+ @_PIPELINE_OPTION
379
+ @click.option(
380
+ "--run-id",
381
+ callback=_run_id,
382
+ default=None,
383
+ metavar="RID",
384
+ help="Name the run (1 to 64 letters, digits, dots, underscores or hyphens) and make the "
385
+ "start idempotent. Default: dbx-<job run id>.",
386
+ )
387
+ @click.option("--full-refresh", is_flag=True, help="Rebuild append tables from scratch.")
388
+ @click.option(
389
+ "--ensure",
390
+ is_flag=True,
391
+ help="Deploy the job first when it is missing or out of date, instead of stopping.",
392
+ )
393
+ @click.option(
394
+ "--ephemeral",
395
+ is_flag=True,
396
+ help="Run once without a job: upload the project and submit the run. Serverless only.",
397
+ )
398
+ @_wait_options
399
+ @click.pass_context
400
+ def run(
401
+ ctx: click.Context,
402
+ /,
403
+ project_dir: Path,
404
+ vars_: dict[str, Any],
405
+ target: str | None,
406
+ plugins: tuple[str, ...],
407
+ log_format: str,
408
+ select: tuple[str, ...],
409
+ exclude: tuple[str, ...],
410
+ pipeline: str | None,
411
+ run_id: str | None,
412
+ full_refresh: bool,
413
+ ensure: bool,
414
+ ephemeral: bool,
415
+ wait: bool,
416
+ timeout: int,
417
+ ) -> None:
418
+ """Run the deployed job of a pipeline, or run it once without a job (--ephemeral).
419
+
420
+ Prints {job_id, job_run_id, run_id, run_page_url} as one JSON line. --select and --exclude
421
+ pick units: only their tasks (and finalize) run. Exit codes: 0 ok, 1 a diagnostic or a run
422
+ that did not succeed, 2 bad usage.
423
+ """
424
+ inv = Invocation("databricks run", log_format)
425
+ plan, bound, runtime, report = _load(inv, project_dir, target, vars_, plugins)
426
+ opts = deploy_options(plan, report)
427
+ if opts is None:
428
+ inv.finish(report.sorted(), 1, bound=bound)
429
+ return
430
+ name = _pipeline(pipeline, opts)
431
+ if ephemeral and ensure:
432
+ raise click.UsageError("--ensure deploys a job; --ephemeral runs without one")
433
+ client = _connect(ctx, inv, report, bound)
434
+ selection = Selection(select, exclude)
435
+ try:
436
+ if ephemeral:
437
+ started = submit_ephemeral(
438
+ client,
439
+ project_dir,
440
+ plan,
441
+ name,
442
+ runtime,
443
+ selection=selection,
444
+ full_refresh=full_refresh,
445
+ report=report,
446
+ run_id=run_id,
447
+ )
448
+ else:
449
+ started = run_job(
450
+ client,
451
+ project_dir,
452
+ plan,
453
+ name,
454
+ runtime,
455
+ selection=selection,
456
+ run_id=run_id,
457
+ full_refresh=full_refresh,
458
+ ensure=ensure,
459
+ report=report,
460
+ )
461
+ except SelectionError as err:
462
+ raise click.UsageError(str(err)) from err
463
+ code = 1
464
+ if started is not None:
465
+ shown = started.run_id or f"dbx-{started.job_run_id}"
466
+ _echo_started(started.job_id, started.job_run_id, shown, started.run_page_url)
467
+ code = 0
468
+ if wait:
469
+ code = _follow(
470
+ ctx, client, started.job_run_id, started.run_id, runtime, report, timeout=timeout
471
+ )
472
+ inv.finish(report.sorted(), 1 if report.has_errors else code, bound=bound)
473
+
474
+
475
+ @databricks.command()
476
+ @project_options
477
+ @target_option
478
+ @plugin_option
479
+ @log_format_option
480
+ @_PIPELINE_OPTION
481
+ @click.option("--run-id", callback=_run_id, default=None, metavar="RID", help="The run to repair.")
482
+ @click.option(
483
+ "--job-run-id",
484
+ type=click.IntRange(min=1),
485
+ default=None,
486
+ metavar="N",
487
+ help="The job run to repair.",
488
+ )
489
+ @_wait_options
490
+ @click.pass_context
491
+ def repair(
492
+ ctx: click.Context,
493
+ /,
494
+ project_dir: Path,
495
+ vars_: dict[str, Any],
496
+ target: str | None,
497
+ plugins: tuple[str, ...],
498
+ log_format: str,
499
+ pipeline: str | None,
500
+ run_id: str | None,
501
+ job_run_id: int | None,
502
+ wait: bool,
503
+ timeout: int,
504
+ ) -> None:
505
+ """Rerun the failed, cancelled and skipped tasks of a finished run, and finalize again.
506
+
507
+ Name the run with --run-id (the pipeloom run id) or --job-run-id (the Databricks run id),
508
+ exactly one. Prints {job_run_id, repair_id} as one JSON line. Exit codes: 0 ok (also when
509
+ there is nothing to repair), 1 a diagnostic or a repair that did not succeed, 2 bad usage.
510
+ """
511
+ if (run_id is None) == (job_run_id is None):
512
+ raise click.UsageError("use exactly one of --run-id and --job-run-id")
513
+ inv = Invocation("databricks repair", log_format)
514
+ plan, bound, runtime, report = _load(inv, project_dir, target, vars_, plugins)
515
+ opts = deploy_options(plan, report)
516
+ if opts is None:
517
+ inv.finish(report.sorted(), 1, bound=bound)
518
+ return
519
+ name = _pipeline(pipeline, opts)
520
+ client = _connect(ctx, inv, report, bound)
521
+ target_run = job_run_id
522
+ if run_id is not None:
523
+ with api_errors(report):
524
+ meta = plan.metadata
525
+ job = find_job(
526
+ client, f"{meta.project}.{name}.{meta.target}", name=job_name(plan, name)
527
+ )
528
+ found = None if job is None else find_run(client, job.job_id, run_id)
529
+ if found is None:
530
+ report.add(
531
+ "PLD007",
532
+ f"no run '{run_id}' of pipeline '{name}'"
533
+ if job is not None
534
+ else f"no job is deployed for pipeline '{name}'",
535
+ hint="check --run-id, --pipeline and --target",
536
+ )
537
+ else:
538
+ target_run = found.run_id
539
+ code = 1
540
+ if target_run is not None and not report.has_errors:
541
+ repair_id = repair_run(client, target_run, report)
542
+ if repair_id is not None:
543
+ click.echo(
544
+ json.dumps({"job_run_id": target_run, "repair_id": repair_id}, ensure_ascii=True)
545
+ )
546
+ code = 0
547
+ if wait:
548
+ code = _follow(
549
+ ctx,
550
+ client,
551
+ target_run,
552
+ run_id,
553
+ runtime,
554
+ report,
555
+ repair_id=repair_id,
556
+ timeout=timeout,
557
+ )
558
+ elif not report.has_errors:
559
+ click.echo(f"nothing to repair in run {target_run}", err=True)
560
+ code = 0
561
+ inv.finish(report.sorted(), 1 if report.has_errors else code, bound=bound)
@@ -0,0 +1,121 @@
1
+ """The Databricks client the plugin talks through (ADR-0036).
2
+
3
+ A small JSON-shaped protocol with records of our own, so nothing outside `client/sdk.py` and
4
+ `client/connect.py` depends on the SDK. Importing this module loads no SDK.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from collections.abc import Iterator, Mapping, Sequence
10
+ from dataclasses import dataclass, field
11
+ from typing import Protocol
12
+
13
+ from pydantic import JsonValue
14
+
15
+
16
+ @dataclass(frozen=True)
17
+ class JobSummary:
18
+ job_id: int
19
+ name: str
20
+ tags: Mapping[str, str]
21
+
22
+
23
+ @dataclass(frozen=True)
24
+ class TaskRunInfo:
25
+ task_key: str
26
+ run_id: int
27
+ attempt: int
28
+ life_cycle_state: str
29
+ result_state: str | None
30
+ termination_code: str | None
31
+ message: str | None
32
+
33
+
34
+ @dataclass(frozen=True)
35
+ class RunInfo:
36
+ run_id: int
37
+ job_id: int | None
38
+ life_cycle_state: str
39
+ result_state: str | None
40
+ job_parameters: Mapping[str, str] = field(default_factory=dict)
41
+ tasks: tuple[TaskRunInfo, ...] = ()
42
+ latest_repair_id: int | None = None
43
+ run_page_url: str | None = None
44
+ start_time_ms: int | None = None
45
+ repair_states: Mapping[int, str] = field(default_factory=dict)
46
+ """Repair id -> life cycle state of that repair's entry in the run's repair history."""
47
+
48
+
49
+ @dataclass(frozen=True)
50
+ class DirEntry:
51
+ path: str
52
+ is_dir: bool
53
+ modified_ms: int = 0
54
+ """Last change in epoch milliseconds; 0 when the workspace does not say."""
55
+
56
+
57
+ class ApiError(Exception):
58
+ """A failed API call. `status` is the HTTP status, 0 when the SDK did not say."""
59
+
60
+ def __init__(self, status: int, message: str, error_code: str | None = None) -> None:
61
+ super().__init__(message)
62
+ self.status = status
63
+ self.error_code = error_code
64
+ self.message = message
65
+
66
+
67
+ class ConnectionNotConfigured(Exception):
68
+ """No workspace connection could be set up (PLD010). The message says why."""
69
+
70
+
71
+ class DatabricksClient(Protocol):
72
+ """Every call the plugin makes. Failures raise `ApiError`."""
73
+
74
+ host: str
75
+
76
+ def list_jobs(self, *, name: str | None = None) -> Iterator[JobSummary]: ...
77
+ def create_job(self, settings: Mapping[str, JsonValue]) -> int: ...
78
+ def reset_job(self, job_id: int, settings: Mapping[str, JsonValue]) -> None: ...
79
+ def run_now(
80
+ self,
81
+ job_id: int,
82
+ *,
83
+ job_parameters: Mapping[str, str],
84
+ only: Sequence[str] | None,
85
+ idempotency_token: str | None,
86
+ ) -> int: ...
87
+ def submit_run(
88
+ self, spec: Mapping[str, JsonValue], *, idempotency_token: str | None
89
+ ) -> int: ...
90
+ def get_run(self, run_id: int) -> RunInfo: ...
91
+ def list_runs(self, job_id: int, *, active_only: bool = False) -> Iterator[RunInfo]: ...
92
+ def repair_run(
93
+ self, run_id: int, *, rerun_tasks: Sequence[str], latest_repair_id: int | None
94
+ ) -> int: ...
95
+ def current_user(self) -> str: ...
96
+ def mkdirs(self, path: str) -> None: ...
97
+ def upload(self, path: str, data: bytes) -> None:
98
+ """Store `data` as a plain workspace file, replacing what is there."""
99
+ ...
100
+
101
+ def list_dir(self, path: str) -> list[DirEntry]: ...
102
+ def delete(self, path: str, *, recursive: bool) -> None: ...
103
+ def read_file(self, path: str) -> bytes:
104
+ """The bytes of a file on a volume (Files API)."""
105
+ ...
106
+
107
+ def get_secret(self, scope: str, key: str) -> str:
108
+ """The `value` the API returns: the secret, base64 encoded."""
109
+ ...
110
+
111
+
112
+ def connect(
113
+ *, profile: str | None, env: Mapping[str, str], factory: str | None = None
114
+ ) -> DatabricksClient:
115
+ """A client for the workspace the arguments and `env` select; see `client.connect`.
116
+
117
+ Raises `ConnectionNotConfigured` when nothing is configured.
118
+ """
119
+ from pipeloom_databricks.client.connect import connect as _connect
120
+
121
+ return _connect(profile=profile, env=env, factory=factory)