duckhaven-cli 0.2.0__tar.gz → 0.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- duckhaven_cli-0.3.0/CHANGELOG.md +88 -0
- {duckhaven_cli-0.2.0 → duckhaven_cli-0.3.0}/PKG-INFO +23 -3
- {duckhaven_cli-0.2.0 → duckhaven_cli-0.3.0}/README.md +20 -0
- {duckhaven_cli-0.2.0 → duckhaven_cli-0.3.0}/pyproject.toml +2 -2
- {duckhaven_cli-0.2.0 → duckhaven_cli-0.3.0}/src/dh/_version.py +2 -2
- {duckhaven_cli-0.2.0 → duckhaven_cli-0.3.0}/src/dh/commands/admin.py +118 -5
- {duckhaven_cli-0.2.0 → duckhaven_cli-0.3.0}/src/dh/commands/catalog.py +107 -3
- {duckhaven_cli-0.2.0 → duckhaven_cli-0.3.0}/src/dh/commands/query.py +14 -1
- duckhaven_cli-0.3.0/src/dh/commands/saved.py +371 -0
- {duckhaven_cli-0.2.0 → duckhaven_cli-0.3.0}/src/dh/commands/session.py +21 -3
- {duckhaven_cli-0.2.0 → duckhaven_cli-0.3.0}/src/dh/execute.py +37 -20
- {duckhaven_cli-0.2.0 → duckhaven_cli-0.3.0}/src/dh/main.py +1 -0
- duckhaven_cli-0.2.0/CHANGELOG.md +0 -48
- duckhaven_cli-0.2.0/src/dh/commands/saved.py +0 -213
- {duckhaven_cli-0.2.0 → duckhaven_cli-0.3.0}/.gitignore +0 -0
- {duckhaven_cli-0.2.0 → duckhaven_cli-0.3.0}/LICENSE +0 -0
- {duckhaven_cli-0.2.0 → duckhaven_cli-0.3.0}/src/dh/__init__.py +0 -0
- {duckhaven_cli-0.2.0 → duckhaven_cli-0.3.0}/src/dh/commands/__init__.py +0 -0
- {duckhaven_cli-0.2.0 → duckhaven_cli-0.3.0}/src/dh/commands/api.py +0 -0
- {duckhaven_cli-0.2.0 → duckhaven_cli-0.3.0}/src/dh/commands/auth.py +0 -0
- {duckhaven_cli-0.2.0 → duckhaven_cli-0.3.0}/src/dh/commands/dbt.py +0 -0
- {duckhaven_cli-0.2.0 → duckhaven_cli-0.3.0}/src/dh/commands/grant.py +0 -0
- {duckhaven_cli-0.2.0 → duckhaven_cli-0.3.0}/src/dh/commands/health.py +0 -0
- {duckhaven_cli-0.2.0 → duckhaven_cli-0.3.0}/src/dh/commands/profile.py +0 -0
- {duckhaven_cli-0.2.0 → duckhaven_cli-0.3.0}/src/dh/config.py +0 -0
- {duckhaven_cli-0.2.0 → duckhaven_cli-0.3.0}/src/dh/context.py +0 -0
- {duckhaven_cli-0.2.0 → duckhaven_cli-0.3.0}/src/dh/errors.py +0 -0
- {duckhaven_cli-0.2.0 → duckhaven_cli-0.3.0}/src/dh/introspect.py +0 -0
- {duckhaven_cli-0.2.0 → duckhaven_cli-0.3.0}/src/dh/output/__init__.py +0 -0
- {duckhaven_cli-0.2.0 → duckhaven_cli-0.3.0}/src/dh/repl.py +0 -0
- {duckhaven_cli-0.2.0 → duckhaven_cli-0.3.0}/src/dh/resolve.py +0 -0
- {duckhaven_cli-0.2.0 → duckhaven_cli-0.3.0}/src/dh/rest.py +0 -0
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `duckhaven-cli` are documented here. The format follows
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versions come from git tags
|
|
5
|
+
(`duckhaven-cli-vX.Y.Z`).
|
|
6
|
+
|
|
7
|
+
## [Unreleased]
|
|
8
|
+
|
|
9
|
+
## [0.3.0] - 2026-10-13
|
|
10
|
+
|
|
11
|
+
Brings the CLI up to date with the DuckHaven server's result cache, DuckLake catalogs,
|
|
12
|
+
agent runtimes, worksheets and reshaped monitoring. Everything new needs a server that
|
|
13
|
+
offers it; against an older one the command fails with that server's own error.
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
|
|
17
|
+
- **Result cache.** `--no-cache` on `dh sql`, `dh query run`, `dh session open` and
|
|
18
|
+
`dh session exec`, and `dh workspace update --result-cache/--no-result-cache` (owner
|
|
19
|
+
only). When a query was answered from the cache, `dh sql` and `dh session exec` say so on
|
|
20
|
+
stderr. The REPL is not cache-aware.
|
|
21
|
+
- **Catalogs.** `dh catalog create --kind` (`iceberg_polaris` or `ducklake`),
|
|
22
|
+
`dh catalog kinds`, `dh catalog rotate-role`, and `dh catalog export list|create|cancel`
|
|
23
|
+
for copying a DuckLake catalog out as Iceberg. `dh admin catalog reconcile-roles` creates
|
|
24
|
+
any DuckLake database role that is missing (idempotent, deployment-wide).
|
|
25
|
+
- **Runtimes.** `dh agent runtimes`, and `--runtime` / `--allow-beta` on
|
|
26
|
+
`dh admin agent elastic-create`.
|
|
27
|
+
- **Monitoring.** `dh admin agent monitoring` takes `--window` or `--start`/`--end`, and
|
|
28
|
+
`dh admin agent queries` lists the runs behind a time range, sortable by wait, duration,
|
|
29
|
+
peak memory, CPU time, spill or bytes read.
|
|
30
|
+
- **Maintenance.** `dh admin maintenance recommendations` and `dh admin maintenance apply`
|
|
31
|
+
(asks first; needs `maintenance:manage`).
|
|
32
|
+
- **Worksheets.** `dh worksheet list|get|create|update|delete`. Changing a worksheet's
|
|
33
|
+
title or SQL requires `--base-version`; a concurrent edit is refused with exit `5` and
|
|
34
|
+
the current worksheet in the error's details.
|
|
35
|
+
- `dh saved-query create --on-conflict error` fails with exit `5` instead of overwriting
|
|
36
|
+
a query with the same name, and `dh search --type` limits the kinds of object searched.
|
|
37
|
+
|
|
38
|
+
### Changed
|
|
39
|
+
|
|
40
|
+
- With no agent named, `dh sql` and `dh session open` no longer list agents first and leave
|
|
41
|
+
the choice to the server, which picks a connected agent on the default runtime. Before,
|
|
42
|
+
an account that could see several agents sent no choice and a server without elastic
|
|
43
|
+
compute refused with `agent_required`.
|
|
44
|
+
- Status polls ask the server to hold the call until the query finishes, and a query the
|
|
45
|
+
submit response already reports as finished is not polled at all.
|
|
46
|
+
- The pinned API contract is refreshed, and now also checks the query parameters the new
|
|
47
|
+
commands send.
|
|
48
|
+
|
|
49
|
+
## [0.2.0] - 2026-09-09
|
|
50
|
+
|
|
51
|
+
### Changed
|
|
52
|
+
|
|
53
|
+
- Drop support for Python 3.10; the minimum supported version is now 3.11.
|
|
54
|
+
|
|
55
|
+
## [0.1.1] - 2026-09-09
|
|
56
|
+
|
|
57
|
+
### Fixed
|
|
58
|
+
|
|
59
|
+
- Declare `click` as a direct dependency. `dh` failed to start with
|
|
60
|
+
`ModuleNotFoundError: No module named 'click'` when installed standalone:
|
|
61
|
+
`context.py`/`repl.py` catch `click.Abort` directly, but `typer>=0.27` no longer
|
|
62
|
+
depends on `click` itself, and the workspace dev environment masked this because
|
|
63
|
+
other packages pulled `click` in transitively.
|
|
64
|
+
|
|
65
|
+
## [0.1.0] - 2026-09-09
|
|
66
|
+
|
|
67
|
+
### Added
|
|
68
|
+
|
|
69
|
+
- `dh`, the DuckHaven command-line interface.
|
|
70
|
+
- **Running SQL.** `dh sql` submits, polls to a terminal state and fetches every page
|
|
71
|
+
of results; `--no-wait` returns the id instead. Ctrl-C cancels the query on the
|
|
72
|
+
server rather than orphaning it, and a query that runs and fails exits `6` so a
|
|
73
|
+
pipeline can tell bad SQL from a broken CLI. `dh sql` with no arguments on a
|
|
74
|
+
terminal opens a REPL, session-backed where the server allows it.
|
|
75
|
+
- **The dbt path.** `dh lineage import` and `dh semantic import` publish build
|
|
76
|
+
artifacts, including the `manifest.json` + `catalog.json` pair that unlocks
|
|
77
|
+
column-level lineage, plus `validate`/`publish` to promote imported drafts.
|
|
78
|
+
- **Catalog and access.** `dh workspace`, `dh catalog`, `dh schema`, `dh table` and
|
|
79
|
+
`dh grant`, with catalog resolution and `catalog.schema.table` references.
|
|
80
|
+
- **History.** `dh query list` with the full server-side filter set, `dh saved-query`,
|
|
81
|
+
`dh schedule` and `dh search`.
|
|
82
|
+
- **Operators.** A thin `dh admin` over service accounts, tokens, users and agents,
|
|
83
|
+
and `dh api` for anything without a hand-written command.
|
|
84
|
+
- **Auth.** `dh auth login` signs in and mints a personal access token, storing only
|
|
85
|
+
the token at mode 0600 and refusing a config other users can read.
|
|
86
|
+
- Output as `--format json|table|csv` behind one stable envelope
|
|
87
|
+
(`{data, cursor, has_more}`), defaulting to a table on a terminal and JSON when
|
|
88
|
+
piped. Errors are JSON too when JSON was asked for.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: duckhaven-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: dh — the command-line interface for DuckHaven
|
|
5
5
|
Project-URL: Homepage, https://github.com/tamasmrtn/duckhaven-clients
|
|
6
6
|
Project-URL: Source, https://github.com/tamasmrtn/duckhaven-clients
|
|
@@ -21,10 +21,10 @@ Classifier: Topic :: Database
|
|
|
21
21
|
Classifier: Topic :: Utilities
|
|
22
22
|
Classifier: Typing :: Typed
|
|
23
23
|
Requires-Python: >=3.11
|
|
24
|
-
Requires-Dist: click>=8.0
|
|
24
|
+
Requires-Dist: click>=8.5.0
|
|
25
25
|
Requires-Dist: duckhaven-sql-connector>=0.4.1
|
|
26
26
|
Requires-Dist: httpx>=0.28
|
|
27
|
-
Requires-Dist: typer>=0.
|
|
27
|
+
Requires-Dist: typer>=0.27.3
|
|
28
28
|
Description-Content-Type: text/markdown
|
|
29
29
|
|
|
30
30
|
# duckhaven-cli
|
|
@@ -57,6 +57,26 @@ dh auth login --host https://duckhaven.example.com
|
|
|
57
57
|
dh sql -q "select 1"
|
|
58
58
|
```
|
|
59
59
|
|
|
60
|
+
## The result cache
|
|
61
|
+
|
|
62
|
+
DuckHaven answers a repeated read from its result cache when nothing the query reads has
|
|
63
|
+
changed. `dh sql` says so on stderr when it happens; pass `--no-cache` to run the query
|
|
64
|
+
anyway (also on `dh session open` and `dh session exec`). An owner can switch the cache off
|
|
65
|
+
for a whole workspace with `dh workspace update --no-result-cache`. The interactive REPL
|
|
66
|
+
does not take `--no-cache`: it holds its session through the connector, which has its own
|
|
67
|
+
`use_cache` option, and the shell does not expose it yet.
|
|
68
|
+
|
|
69
|
+
## Newer server features
|
|
70
|
+
|
|
71
|
+
A server that supports them offers: DuckLake catalogs (`dh catalog kinds`,
|
|
72
|
+
`dh catalog create --kind ducklake`, `dh catalog rotate-role`, `dh catalog export`), the
|
|
73
|
+
DuckDB runtimes agents run (`dh agent runtimes`, `--runtime` on
|
|
74
|
+
`dh admin agent elastic-create`), per-agent monitoring over a window or range and the
|
|
75
|
+
queries behind it (`dh admin agent monitoring`, `dh admin agent queries`), applying
|
|
76
|
+
maintenance recommendations (`dh admin maintenance recommendations|apply`), and your
|
|
77
|
+
private editor tabs (`dh worksheet`). Against an older server these commands answer with the
|
|
78
|
+
server's own not-found or validation error.
|
|
79
|
+
|
|
60
80
|
## Documentation
|
|
61
81
|
|
|
62
82
|
- [Command-line quickstart](https://tamasmrtn.github.io/duckhaven/getting-started/cli-quickstart/) — install, sign in,
|
|
@@ -28,6 +28,26 @@ dh auth login --host https://duckhaven.example.com
|
|
|
28
28
|
dh sql -q "select 1"
|
|
29
29
|
```
|
|
30
30
|
|
|
31
|
+
## The result cache
|
|
32
|
+
|
|
33
|
+
DuckHaven answers a repeated read from its result cache when nothing the query reads has
|
|
34
|
+
changed. `dh sql` says so on stderr when it happens; pass `--no-cache` to run the query
|
|
35
|
+
anyway (also on `dh session open` and `dh session exec`). An owner can switch the cache off
|
|
36
|
+
for a whole workspace with `dh workspace update --no-result-cache`. The interactive REPL
|
|
37
|
+
does not take `--no-cache`: it holds its session through the connector, which has its own
|
|
38
|
+
`use_cache` option, and the shell does not expose it yet.
|
|
39
|
+
|
|
40
|
+
## Newer server features
|
|
41
|
+
|
|
42
|
+
A server that supports them offers: DuckLake catalogs (`dh catalog kinds`,
|
|
43
|
+
`dh catalog create --kind ducklake`, `dh catalog rotate-role`, `dh catalog export`), the
|
|
44
|
+
DuckDB runtimes agents run (`dh agent runtimes`, `--runtime` on
|
|
45
|
+
`dh admin agent elastic-create`), per-agent monitoring over a window or range and the
|
|
46
|
+
queries behind it (`dh admin agent monitoring`, `dh admin agent queries`), applying
|
|
47
|
+
maintenance recommendations (`dh admin maintenance recommendations|apply`), and your
|
|
48
|
+
private editor tabs (`dh worksheet`). Against an older server these commands answer with the
|
|
49
|
+
server's own not-found or validation error.
|
|
50
|
+
|
|
31
51
|
## Documentation
|
|
32
52
|
|
|
33
53
|
- [Command-line quickstart](https://tamasmrtn.github.io/duckhaven/getting-started/cli-quickstart/) — install, sign in,
|
|
@@ -22,10 +22,10 @@ classifiers = [
|
|
|
22
22
|
"Typing :: Typed",
|
|
23
23
|
]
|
|
24
24
|
dependencies = [
|
|
25
|
-
"typer>=0.
|
|
25
|
+
"typer>=0.27.3",
|
|
26
26
|
# click.Abort is caught directly (Ctrl-C/Ctrl-D handling); typer>=0.27 no longer
|
|
27
27
|
# pulls click in as its own dependency, so it must be declared here too.
|
|
28
|
-
"click>=8.0",
|
|
28
|
+
"click>=8.5.0",
|
|
29
29
|
"httpx>=0.28",
|
|
30
30
|
# The SQL execution path (sessions, statements, polling, row pagination) is the
|
|
31
31
|
# connector's, not a second implementation of it.
|
|
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
|
|
|
18
18
|
commit_id: str | None
|
|
19
19
|
__commit_id__: str | None
|
|
20
20
|
|
|
21
|
-
__version__ = version = '0.
|
|
22
|
-
__version_tuple__ = version_tuple = (0,
|
|
21
|
+
__version__ = version = '0.3.0'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 3, 0)
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -12,6 +12,7 @@ from __future__ import annotations
|
|
|
12
12
|
import typer
|
|
13
13
|
|
|
14
14
|
from dh import context
|
|
15
|
+
from dh.errors import ConflictError
|
|
15
16
|
|
|
16
17
|
app = typer.Typer(name="admin", help="Operator tasks: accounts, users, agents.")
|
|
17
18
|
|
|
@@ -20,9 +21,12 @@ pat_app = typer.Typer(name="pat", help="Tokens issued to a service account.")
|
|
|
20
21
|
user_app = typer.Typer(name="user", help="People and their workspace roles.")
|
|
21
22
|
agent_app = typer.Typer(name="agent", help="Compute agents.")
|
|
22
23
|
storage_app = typer.Typer(name="storage", help="Storage backends.")
|
|
23
|
-
maintenance_app = typer.Typer(
|
|
24
|
+
maintenance_app = typer.Typer(
|
|
25
|
+
name="maintenance", help="Maintenance policy, scans and recommendations."
|
|
26
|
+
)
|
|
27
|
+
catalog_app = typer.Typer(name="catalog", help="Catalog-wide operations.")
|
|
24
28
|
|
|
25
|
-
for sub in (sa_app, pat_app, user_app, agent_app, storage_app, maintenance_app):
|
|
29
|
+
for sub in (sa_app, pat_app, user_app, agent_app, storage_app, maintenance_app, catalog_app):
|
|
26
30
|
app.add_typer(sub)
|
|
27
31
|
|
|
28
32
|
|
|
@@ -239,12 +243,24 @@ def create_elastic_agent(
|
|
|
239
243
|
cpu: float = typer.Option(..., "--cpu"),
|
|
240
244
|
memory_gb: float = typer.Option(..., "--memory-gb"),
|
|
241
245
|
idle_timeout_minutes: int = typer.Option(None, "--idle-timeout-minutes"),
|
|
246
|
+
runtime: str = typer.Option(
|
|
247
|
+
None,
|
|
248
|
+
"--runtime",
|
|
249
|
+
help="DuckDB runtime id, from `dh agent runtimes`. Omitted: the deployment's default.",
|
|
250
|
+
),
|
|
251
|
+
allow_beta: bool = typer.Option(
|
|
252
|
+
False, "--allow-beta", help="Permit a beta runtime, which is otherwise refused."
|
|
253
|
+
),
|
|
242
254
|
):
|
|
243
255
|
"""Provision an elastic agent. Accepted asynchronously; poll `admin agent get`."""
|
|
244
256
|
cli = context.of(ctx)
|
|
245
257
|
body: dict[str, object] = {"cpu": cpu, "memory_gb": memory_gb}
|
|
246
258
|
if idle_timeout_minutes is not None:
|
|
247
259
|
body["idle_timeout_minutes"] = idle_timeout_minutes
|
|
260
|
+
if runtime:
|
|
261
|
+
body["runtime_id"] = runtime
|
|
262
|
+
if allow_beta:
|
|
263
|
+
body["allow_beta"] = True
|
|
248
264
|
with cli.client() as client:
|
|
249
265
|
cli.emit(client.post("admin/agents/elastic", json=body))
|
|
250
266
|
|
|
@@ -266,11 +282,60 @@ def agent_metrics(ctx: typer.Context):
|
|
|
266
282
|
|
|
267
283
|
|
|
268
284
|
@agent_app.command("monitoring")
|
|
269
|
-
def agent_monitoring(
|
|
270
|
-
|
|
285
|
+
def agent_monitoring(
|
|
286
|
+
ctx: typer.Context,
|
|
287
|
+
agent_id: str,
|
|
288
|
+
window: str = typer.Option(
|
|
289
|
+
None, "--window", help="A preset ending now: 1h, 3h, 8h (the default), 12h, 24h, 3d or 7d."
|
|
290
|
+
),
|
|
291
|
+
start: str = typer.Option(None, "--start", help="ISO-8601 start of a custom range."),
|
|
292
|
+
end: str = typer.Option(None, "--end", help="ISO-8601 end of a custom range."),
|
|
293
|
+
):
|
|
294
|
+
"""One agent's monitoring detail over a window or a custom range.
|
|
295
|
+
|
|
296
|
+
Returns flat time buckets (busy, idle, concurrency, outcomes, CPU and memory)
|
|
297
|
+
on one shared grid, the lifecycle spans and a summary for the range.
|
|
298
|
+
"""
|
|
271
299
|
cli = context.of(ctx)
|
|
300
|
+
if (start is None) != (end is None):
|
|
301
|
+
raise ConflictError("bad_range", "Give both --start and --end, or neither.")
|
|
302
|
+
if window and start:
|
|
303
|
+
raise ConflictError("bad_range", "Give --window or --start/--end, not both.")
|
|
304
|
+
params = {"window": window, "start": start, "end": end}
|
|
272
305
|
with cli.client() as client:
|
|
273
|
-
cli.emit(client.get(f"admin/agents/{agent_id}/monitoring"))
|
|
306
|
+
cli.emit(client.get(f"admin/agents/{agent_id}/monitoring", params=params))
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
@agent_app.command("queries")
|
|
310
|
+
def agent_queries(
|
|
311
|
+
ctx: typer.Context,
|
|
312
|
+
agent_id: str,
|
|
313
|
+
start: str = typer.Option(..., "--start", help="ISO-8601 start of the range."),
|
|
314
|
+
end: str = typer.Option(..., "--end", help="ISO-8601 end of the range."),
|
|
315
|
+
status: list[str] = typer.Option(None, "--status", help="Repeatable."),
|
|
316
|
+
sort: str = typer.Option(
|
|
317
|
+
None,
|
|
318
|
+
"--sort",
|
|
319
|
+
help="started_at (default), duration, wait, peak_memory, cpu_time, spill or bytes_read.",
|
|
320
|
+
),
|
|
321
|
+
direction: str = typer.Option(None, "--dir", help="asc or desc (default)."),
|
|
322
|
+
limit: int = typer.Option(None, "--limit"),
|
|
323
|
+
fetch_all: bool = typer.Option(False, "--all"),
|
|
324
|
+
):
|
|
325
|
+
"""The runs that were running or waiting on an agent during a range, with what they cost.
|
|
326
|
+
|
|
327
|
+
Needs the permission to see queries across workspaces as well as `use` on the agent.
|
|
328
|
+
"""
|
|
329
|
+
cli = context.of(ctx)
|
|
330
|
+
params = {"start": start, "end": end, "status": status, "sort": sort, "dir": direction}
|
|
331
|
+
with cli.client() as client:
|
|
332
|
+
cli.page(
|
|
333
|
+
client,
|
|
334
|
+
f"admin/agents/{agent_id}/queries",
|
|
335
|
+
params=params,
|
|
336
|
+
limit=limit,
|
|
337
|
+
fetch_all=fetch_all,
|
|
338
|
+
)
|
|
274
339
|
|
|
275
340
|
|
|
276
341
|
@agent_app.command("access")
|
|
@@ -351,3 +416,51 @@ def trigger_scan(ctx: typer.Context):
|
|
|
351
416
|
cli = context.of(ctx)
|
|
352
417
|
with cli.client() as client:
|
|
353
418
|
cli.emit(client.post("admin/maintenance/scan"))
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
@maintenance_app.command("recommendations")
|
|
422
|
+
def list_recommendations(
|
|
423
|
+
ctx: typer.Context,
|
|
424
|
+
status: list[str] = typer.Option(None, "--status", help="Repeatable."),
|
|
425
|
+
severity: list[str] = typer.Option(None, "--severity", help="Repeatable."),
|
|
426
|
+
limit: int = typer.Option(None, "--limit"),
|
|
427
|
+
fetch_all: bool = typer.Option(False, "--all"),
|
|
428
|
+
):
|
|
429
|
+
"""What the maintenance scan recommends, with the outcome of any apply."""
|
|
430
|
+
cli = context.of(ctx)
|
|
431
|
+
params = {"status": status, "severity": severity}
|
|
432
|
+
with cli.client() as client:
|
|
433
|
+
cli.page(
|
|
434
|
+
client, "maintenance/recommendations", params=params, limit=limit, fetch_all=fetch_all
|
|
435
|
+
)
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
@maintenance_app.command("apply")
|
|
439
|
+
def apply_recommendation(ctx: typer.Context, recommendation_id: str, yes: bool = context.YES):
|
|
440
|
+
"""Run the maintenance a recommendation asks for.
|
|
441
|
+
|
|
442
|
+
Rewrites or deletes data files, so it asks first. Needs `maintenance:manage` and
|
|
443
|
+
only works for catalog kinds that support applying (see `dh catalog kinds`).
|
|
444
|
+
Accepted asynchronously: the recommendation's `apply_status` reports the outcome,
|
|
445
|
+
so follow with `dh admin maintenance recommendations`.
|
|
446
|
+
"""
|
|
447
|
+
cli = context.of(ctx)
|
|
448
|
+
cli.confirm("Apply maintenance recommendation", recommendation_id, yes=yes)
|
|
449
|
+
with cli.client() as client:
|
|
450
|
+
cli.emit(client.post(f"maintenance/recommendations/{recommendation_id}/apply"))
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
# --- Catalogs ----------------------------------------------------------------
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
@catalog_app.command("reconcile-roles")
|
|
457
|
+
def reconcile_ducklake_roles(ctx: typer.Context):
|
|
458
|
+
"""Create any DuckLake database role that does not exist yet.
|
|
459
|
+
|
|
460
|
+
A post-upgrade step for operators: a migration can only write the credentials,
|
|
461
|
+
not the roles themselves in the catalog database. Idempotent, and it also repairs
|
|
462
|
+
a role that was dropped. Prints how many catalogs it saw and how many it reconciled.
|
|
463
|
+
"""
|
|
464
|
+
cli = context.of(ctx)
|
|
465
|
+
with cli.client() as client:
|
|
466
|
+
cli.emit(client.post("admin/catalogs/ducklake/reconcile-roles"))
|
|
@@ -102,13 +102,21 @@ def update_workspace(
|
|
|
102
102
|
workspace: str = typer.Argument(None),
|
|
103
103
|
name: str = typer.Option(None, "--name"),
|
|
104
104
|
description: str = typer.Option(None, "--description"),
|
|
105
|
+
result_cache: bool = typer.Option(
|
|
106
|
+
None,
|
|
107
|
+
"--result-cache/--no-result-cache",
|
|
108
|
+
help="Whether repeated queries may be answered from the result cache. Owner only.",
|
|
109
|
+
),
|
|
105
110
|
) -> None:
|
|
106
|
-
"""Rename or re-describe a workspace."""
|
|
111
|
+
"""Rename or re-describe a workspace, or switch its result cache on or off."""
|
|
107
112
|
cli = context.of(ctx)
|
|
108
113
|
settings = cli.settings()
|
|
109
|
-
|
|
114
|
+
fields = {"name": name, "description": description, "result_cache_enabled": result_cache}
|
|
115
|
+
body = {k: v for k, v in fields.items() if v is not None}
|
|
110
116
|
if not body:
|
|
111
|
-
raise ConflictError(
|
|
117
|
+
raise ConflictError(
|
|
118
|
+
"nothing_to_update", "Give --name, --description or --result-cache/--no-result-cache."
|
|
119
|
+
)
|
|
112
120
|
with cli.client(settings) as client:
|
|
113
121
|
target = workspace or settings.require("workspace")
|
|
114
122
|
cli.emit(client.patch(f"workspaces/{target}", json=body))
|
|
@@ -171,6 +179,19 @@ def list_agents(ctx: typer.Context) -> None:
|
|
|
171
179
|
cli.emit(client.get("agents"))
|
|
172
180
|
|
|
173
181
|
|
|
182
|
+
@agent_app.command("runtimes")
|
|
183
|
+
def list_runtimes(ctx: typer.Context) -> None:
|
|
184
|
+
"""The DuckDB runtimes agents can run, and which one is the default.
|
|
185
|
+
|
|
186
|
+
Each is a DuckDB line with its baked-in extensions and a lifecycle status
|
|
187
|
+
(`beta`, `ga`, `deprecated`, `retired`). Retired runtimes are listed so an agent
|
|
188
|
+
still running one can be recognised, but the server will not send them work.
|
|
189
|
+
"""
|
|
190
|
+
cli = context.of(ctx)
|
|
191
|
+
with cli.client() as client:
|
|
192
|
+
cli.emit(client.get("runtimes"))
|
|
193
|
+
|
|
194
|
+
|
|
174
195
|
# --- Catalogs --------------------------------------------------------------
|
|
175
196
|
|
|
176
197
|
|
|
@@ -191,6 +212,11 @@ def list_catalogs(
|
|
|
191
212
|
def create_catalog(
|
|
192
213
|
ctx: typer.Context,
|
|
193
214
|
name: str = typer.Argument(..., help="Identifier-safe name."),
|
|
215
|
+
kind: str = typer.Option(
|
|
216
|
+
None,
|
|
217
|
+
"--kind",
|
|
218
|
+
help="iceberg_polaris (the server default) or ducklake. See `dh catalog kinds`.",
|
|
219
|
+
),
|
|
194
220
|
storage_backend: str = typer.Option(None, "--storage-backend", help="Backend id."),
|
|
195
221
|
access_mode: str = typer.Option(None, "--access-mode", help="open or scoped."),
|
|
196
222
|
) -> None:
|
|
@@ -198,6 +224,8 @@ def create_catalog(
|
|
|
198
224
|
cli = context.of(ctx)
|
|
199
225
|
settings = cli.settings()
|
|
200
226
|
body = {"name": name}
|
|
227
|
+
if kind:
|
|
228
|
+
body["kind"] = kind
|
|
201
229
|
if storage_backend:
|
|
202
230
|
body["storage_backend_id"] = storage_backend
|
|
203
231
|
if access_mode:
|
|
@@ -206,6 +234,82 @@ def create_catalog(
|
|
|
206
234
|
cli.emit(client.post(f"workspaces/{settings.require('workspace')}/catalogs", json=body))
|
|
207
235
|
|
|
208
236
|
|
|
237
|
+
@catalog_app.command("kinds")
|
|
238
|
+
def list_catalog_kinds(ctx: typer.Context) -> None:
|
|
239
|
+
"""The catalog kinds this deployment offers, and what each can do.
|
|
240
|
+
|
|
241
|
+
A kind that is known but switched off is listed with `available: false` and the
|
|
242
|
+
reason, so `dh catalog create --kind` failing is explicable.
|
|
243
|
+
"""
|
|
244
|
+
cli = context.of(ctx)
|
|
245
|
+
with cli.client() as client:
|
|
246
|
+
cli.emit(client.get("catalog-kinds"))
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
@catalog_app.command("rotate-role")
|
|
250
|
+
def rotate_role(ctx: typer.Context, catalog_id: str, yes: bool = context.YES) -> None:
|
|
251
|
+
"""Rotate the database credential of a DuckLake catalog.
|
|
252
|
+
|
|
253
|
+
Queries already running keep their connection; ones that start afterwards use
|
|
254
|
+
the new credential.
|
|
255
|
+
"""
|
|
256
|
+
cli = context.of(ctx)
|
|
257
|
+
cli.confirm("Rotate the database role of catalog", catalog_id, yes=yes)
|
|
258
|
+
with cli.client() as client:
|
|
259
|
+
cli.emit(client.post(f"catalogs/{catalog_id}/ducklake/rotate-role"))
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
export_app = typer.Typer(name="export", help="Copy a DuckLake catalog out as Iceberg.")
|
|
263
|
+
catalog_app.add_typer(export_app)
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
@export_app.command("list")
|
|
267
|
+
def list_exports(
|
|
268
|
+
ctx: typer.Context, catalog_id: str, fetch_all: bool = typer.Option(False, "--all")
|
|
269
|
+
) -> None:
|
|
270
|
+
"""A catalog's exports, newest first."""
|
|
271
|
+
cli = context.of(ctx)
|
|
272
|
+
with cli.client() as client:
|
|
273
|
+
cli.page(client, f"catalogs/{catalog_id}/exports", fetch_all=fetch_all)
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
@export_app.command("create")
|
|
277
|
+
def create_export(
|
|
278
|
+
ctx: typer.Context,
|
|
279
|
+
catalog_id: str,
|
|
280
|
+
target_name: str = typer.Option(..., "--target-name", help="Name of the new Iceberg catalog."),
|
|
281
|
+
storage_backend: str = typer.Option(
|
|
282
|
+
..., "--storage-backend", help="Storage backend id for the new catalog."
|
|
283
|
+
),
|
|
284
|
+
) -> None:
|
|
285
|
+
"""Start exporting a DuckLake catalog to a new Iceberg catalog.
|
|
286
|
+
|
|
287
|
+
The new catalog is attached to the current workspace. This only starts the
|
|
288
|
+
export; follow it with `dh catalog export list`.
|
|
289
|
+
"""
|
|
290
|
+
cli = context.of(ctx)
|
|
291
|
+
settings = cli.settings()
|
|
292
|
+
with cli.client(settings) as client:
|
|
293
|
+
cli.emit(
|
|
294
|
+
client.post(
|
|
295
|
+
f"catalogs/{catalog_id}/exports",
|
|
296
|
+
params={"workspace": settings.require("workspace")},
|
|
297
|
+
json={
|
|
298
|
+
"target_name": target_name,
|
|
299
|
+
"target_storage_backend_id": storage_backend,
|
|
300
|
+
},
|
|
301
|
+
)
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
@export_app.command("cancel")
|
|
306
|
+
def cancel_export(ctx: typer.Context, catalog_id: str, export_id: str) -> None:
|
|
307
|
+
"""Stop an export that has not finished."""
|
|
308
|
+
cli = context.of(ctx)
|
|
309
|
+
with cli.client() as client:
|
|
310
|
+
cli.emit(client.post(f"catalogs/{catalog_id}/exports/{export_id}/cancel"))
|
|
311
|
+
|
|
312
|
+
|
|
209
313
|
@catalog_app.command("attach")
|
|
210
314
|
def attach_catalog(
|
|
211
315
|
ctx: typer.Context,
|
|
@@ -41,6 +41,7 @@ def _run(
|
|
|
41
41
|
limit: int | None,
|
|
42
42
|
fetch_all: bool,
|
|
43
43
|
agent: str | None,
|
|
44
|
+
no_cache: bool,
|
|
44
45
|
) -> None:
|
|
45
46
|
cli = context.of(ctx)
|
|
46
47
|
settings = cli.settings()
|
|
@@ -62,16 +63,19 @@ def _run(
|
|
|
62
63
|
# The same budget bounds the client's wait and the server's execution.
|
|
63
64
|
# Two timeouts would be a contract people get wrong; one cannot drift.
|
|
64
65
|
timeout_s=budget,
|
|
66
|
+
use_cache=False if no_cache else None,
|
|
65
67
|
)
|
|
66
68
|
if no_wait:
|
|
67
69
|
cli.note(f"Submitted {submitted['id']}.")
|
|
68
70
|
cli.emit(submitted)
|
|
69
71
|
return
|
|
70
72
|
|
|
71
|
-
finished = execute.wait(client, submitted["id"], timeout=budget)
|
|
73
|
+
finished = execute.wait(client, submitted["id"], timeout=budget, known=submitted)
|
|
72
74
|
execute.raise_for_status(finished)
|
|
73
75
|
page = execute.fetch_rows(client, finished["id"], limit=None if fetch_all else limit)
|
|
74
76
|
|
|
77
|
+
if finished.get("cache_status") == "hit":
|
|
78
|
+
cli.note("Answered from the result cache. Use --no-cache to run it again.")
|
|
75
79
|
if page["truncated"]:
|
|
76
80
|
cli.note(f"Showing {len(page['rows'])} of {page['total']} rows. Use --all for the rest.")
|
|
77
81
|
cli.emit({"columns": page["columns"], "rows": page["rows"], "total": page["total"]})
|
|
@@ -85,6 +89,11 @@ _TIMEOUT = typer.Option("20m", "--timeout", help="Give up after this long, e.g.
|
|
|
85
89
|
_LIMIT = typer.Option(None, "--limit", help="Stop after this many rows.")
|
|
86
90
|
_ALL = typer.Option(False, "--all", help="Fetch every row, ignoring --limit.")
|
|
87
91
|
_AGENT = typer.Option(None, "--agent", help="Agent name or id to run on.")
|
|
92
|
+
_NO_CACHE = typer.Option(
|
|
93
|
+
False,
|
|
94
|
+
"--no-cache",
|
|
95
|
+
help="Always run the query instead of answering it from the result cache.",
|
|
96
|
+
)
|
|
88
97
|
|
|
89
98
|
|
|
90
99
|
def sql(
|
|
@@ -97,6 +106,7 @@ def sql(
|
|
|
97
106
|
limit: int = _LIMIT,
|
|
98
107
|
fetch_all: bool = _ALL,
|
|
99
108
|
agent: str = _AGENT,
|
|
109
|
+
no_cache: bool = _NO_CACHE,
|
|
100
110
|
) -> None:
|
|
101
111
|
"""Run SQL and print the results.
|
|
102
112
|
|
|
@@ -117,6 +127,7 @@ def sql(
|
|
|
117
127
|
limit=limit,
|
|
118
128
|
fetch_all=fetch_all,
|
|
119
129
|
agent=agent,
|
|
130
|
+
no_cache=no_cache,
|
|
120
131
|
)
|
|
121
132
|
|
|
122
133
|
|
|
@@ -131,6 +142,7 @@ def run_query(
|
|
|
131
142
|
limit: int = _LIMIT,
|
|
132
143
|
fetch_all: bool = _ALL,
|
|
133
144
|
agent: str = _AGENT,
|
|
145
|
+
no_cache: bool = _NO_CACHE,
|
|
134
146
|
) -> None:
|
|
135
147
|
"""Run SQL and print the results. The noun-first spelling of `dh sql`."""
|
|
136
148
|
_run(
|
|
@@ -143,6 +155,7 @@ def run_query(
|
|
|
143
155
|
limit=limit,
|
|
144
156
|
fetch_all=fetch_all,
|
|
145
157
|
agent=agent,
|
|
158
|
+
no_cache=no_cache,
|
|
146
159
|
)
|
|
147
160
|
|
|
148
161
|
|