datavo-cli 0.22.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,50 @@
1
+ from __future__ import annotations
2
+
3
+ import sys
4
+
5
+ import yaml
6
+
7
+ from datavo_sdk import SampleStream, get_default_shard_cache
8
+ from datavo_sdk.jobpool import render_dataset_cache
9
+
10
+ from ..client_factory import build_client
11
+
12
+
13
+ def _parse_tier(spec: str) -> dict[str, object]:
14
+ """Parse a ``--tier TYPE[,max_bytes][,name]`` value into a tier mapping."""
15
+ parts = [p.strip() for p in spec.split(",") if p.strip()]
16
+ if not parts:
17
+ raise ValueError("empty --tier value")
18
+ tier: dict[str, object] = {"type": parts[0]}
19
+ if len(parts) > 1:
20
+ tier["max_bytes"] = parts[1]
21
+ if len(parts) > 2:
22
+ tier["name"] = parts[2]
23
+ return tier
24
+
25
+
26
+ def handle_render_dataset_cache_command(args) -> int:
27
+ tiers = [_parse_tier(spec) for spec in args.tiers]
28
+ rendered = render_dataset_cache(
29
+ disk=args.disk,
30
+ host_path=args.host_path,
31
+ mount=args.mount,
32
+ tiers=tiers,
33
+ token_alias=args.token_alias,
34
+ )
35
+ print(yaml.safe_dump(rendered, sort_keys=False).rstrip())
36
+ return 0
37
+
38
+
39
+ def handle_warm_command(args) -> int:
40
+ cache = get_default_shard_cache(args.cache_config)
41
+ with build_client() as client:
42
+ dataset = SampleStream.from_dataset(
43
+ args.dataset,
44
+ keys=list(args.keys) if args.keys else None,
45
+ client=client,
46
+ cache=cache,
47
+ )
48
+ count = sum(1 for _ in dataset)
49
+ print(f"warmed {count} samples into the cache for {args.dataset!r}", file=sys.stderr)
50
+ return 0
@@ -0,0 +1,29 @@
1
+ from __future__ import annotations
2
+
3
+ from datetime import datetime
4
+
5
+ from ..client_factory import build_client
6
+ from ..render import render_output
7
+
8
+
9
+ def _parse_datetime(value: str | None) -> datetime | None:
10
+ return datetime.fromisoformat(value) if value else None
11
+
12
+
13
+ def handle_sample_command(args) -> int:
14
+ output_format = getattr(args, "format", "plain")
15
+ with build_client() as client:
16
+ if args.sample_command == "get":
17
+ response = client.get_sample(args.sample_id)
18
+ render_output(response, output_format=output_format)
19
+ return 0
20
+ if args.sample_command == "history":
21
+ response = client.get_sample_history(
22
+ args.sample_id,
23
+ key=args.key,
24
+ as_of=_parse_datetime(args.as_of),
25
+ producer_id=args.producer_id,
26
+ )
27
+ render_output(response, output_format=output_format)
28
+ return 0
29
+ raise RuntimeError("Missing sample subcommand.")
@@ -0,0 +1,97 @@
1
+ from __future__ import annotations
2
+
3
+ from datavo_sdk import (
4
+ CopySamplesToNewStoreRequest,
5
+ HasKeys,
6
+ SampleStoreCreateRequest,
7
+ SampleStoreTransferRequest,
8
+ )
9
+
10
+ from ..client_factory import build_client
11
+ from ..render import render_output
12
+
13
+
14
+ def handle_sample_store_command(args) -> int:
15
+ output_format = getattr(args, "format", "plain")
16
+ with build_client() as client:
17
+ if args.sample_store_command == "create":
18
+ response = client.create_store(
19
+ SampleStoreCreateRequest(
20
+ name=args.name,
21
+ catalog_visibility="hidden" if args.hidden else "visible",
22
+ team=getattr(args, "team", None),
23
+ )
24
+ )
25
+ render_output(response, output_format=output_format)
26
+ return 0
27
+ if args.sample_store_command == "list":
28
+ response = client.list_stores(
29
+ query=args.query,
30
+ include_archived=args.include_archived,
31
+ include_hidden=args.include_hidden,
32
+ )
33
+ render_output(response, output_format=output_format)
34
+ return 0
35
+ if args.sample_store_command == "get":
36
+ response = client.get_store(args.name)
37
+ render_output(response, output_format=output_format)
38
+ return 0
39
+ if args.sample_store_command == "delete":
40
+ response = client.delete_store(args.name)
41
+ render_output(response, output_format=output_format)
42
+ return 0
43
+ if args.sample_store_command == "copy":
44
+ source_filter = None
45
+ if args.source_store and args.has_keys:
46
+ source_filter = [HasKeys(args.has_keys)]
47
+ response = client.copy_samples_to_new_store(
48
+ CopySamplesToNewStoreRequest(
49
+ target_sample_store=args.target,
50
+ catalog_visibility="hidden" if args.hidden else "visible",
51
+ source_sample_store=args.source_store,
52
+ filter=source_filter,
53
+ source_dataset=args.source_dataset,
54
+ keys=args.keys or None,
55
+ limit=args.limit,
56
+ randomize=args.randomize,
57
+ seed=args.seed,
58
+ reason=args.reason,
59
+ idempotency_key=args.idempotency_key,
60
+ )
61
+ )
62
+ render_output(response, output_format=output_format)
63
+ return 0
64
+ if args.sample_store_command == "transfer":
65
+ response = client.transfer_store(
66
+ args.source,
67
+ SampleStoreTransferRequest(
68
+ target_sample_store=args.target,
69
+ delete_source_store=not args.keep_source,
70
+ reason=args.reason,
71
+ created_event_id=args.created_event_id,
72
+ idempotency_key=args.idempotency_key,
73
+ ),
74
+ )
75
+ render_output(response, output_format=output_format)
76
+ return 0
77
+ if args.sample_store_command == "archive":
78
+ response = client.archive_store(args.name)
79
+ render_output(response, output_format=output_format)
80
+ return 0
81
+ if args.sample_store_command == "restore":
82
+ response = client.restore_store(args.name)
83
+ render_output(response, output_format=output_format)
84
+ return 0
85
+ if args.sample_store_command == "operations":
86
+ if args.operations_command == "list":
87
+ response = client.list_store_operations(
88
+ args.name, offset=args.offset, limit=args.limit, order=args.order
89
+ )
90
+ render_output(response, output_format=output_format)
91
+ return 0
92
+ if args.operations_command == "remove":
93
+ response = client.remove_store_operation(args.name, args.op_id)
94
+ render_output(response, output_format=output_format)
95
+ return 0
96
+ raise RuntimeError("Missing sample-store operations subcommand.")
97
+ raise RuntimeError("Missing sample-store subcommand.")
@@ -0,0 +1,38 @@
1
+ from __future__ import annotations
2
+
3
+ import sys
4
+
5
+ from ..client_factory import build_client
6
+ from ..render import render_output
7
+
8
+
9
+ def _resolve_source_import_id(client, args) -> str:
10
+ if args.source_import_id:
11
+ return args.source_import_id
12
+ operation_id = getattr(args, "operation_id", None)
13
+ source_ref = getattr(args, "source_ref", None)
14
+ if args.sample_store and operation_id:
15
+ return client.get_source_import_by_operation_id(args.sample_store, operation_id).source_import_id
16
+ if args.sample_store and source_ref:
17
+ # Deprecated spelling: reads both columns, so an import written before the
18
+ # cutover still resolves by the value its caller sent. The notice goes to stderr —
19
+ # stdout carries the command's output, which callers parse as JSON.
20
+ print("--source-ref is deprecated; use --operation-id.", file=sys.stderr)
21
+ return client.get_source_import_by_source_ref(args.sample_store, source_ref).source_import_id
22
+ raise RuntimeError("Provide --id, or both --sample-store and --operation-id, to identify the source import.")
23
+
24
+
25
+ def handle_source_import_command(args) -> int:
26
+ output_format = getattr(args, "format", "plain")
27
+ with build_client() as client:
28
+ if args.source_import_command == "delete":
29
+ source_import_id = _resolve_source_import_id(client, args)
30
+ # Default to a dry run unless --yes is given, so an accidental
31
+ # invocation reports the blast radius instead of deleting.
32
+ dry_run = args.dry_run or not args.yes
33
+ response = client.delete_source_import(source_import_id, dry_run=dry_run)
34
+ render_output(response, output_format=output_format)
35
+ if response.dry_run and not args.dry_run:
36
+ print("\nDry run only — re-run with --yes to actually delete.")
37
+ return 0
38
+ raise RuntimeError("Missing source-import subcommand.")
@@ -0,0 +1,79 @@
1
+ from __future__ import annotations
2
+
3
+ from datavo_sdk import (
4
+ DatasetSplitDraftRequest,
5
+ DatasetSplitSetCreateRequest,
6
+ )
7
+
8
+ from ..client_factory import build_client
9
+ from ..render import render_output
10
+ from ..utils import load_model
11
+
12
+
13
+ def handle_split_set_command(args) -> int:
14
+ output_format = getattr(args, "format", "plain")
15
+ with build_client() as client:
16
+ if args.split_set_command == "preview":
17
+ response = client.preview_dataset_split_set(
18
+ args.dataset, load_model(args.spec, DatasetSplitDraftRequest)
19
+ )
20
+ render_output(response, output_format=output_format)
21
+ return 0
22
+ if args.split_set_command == "create":
23
+ response = client.create_dataset_split_set(
24
+ args.dataset, load_model(args.spec, DatasetSplitSetCreateRequest)
25
+ )
26
+ render_output(response, output_format=output_format)
27
+ return 0
28
+ if args.split_set_command == "get":
29
+ response = client.get_dataset_split_set(args.dataset, args.split_set)
30
+ render_output(response, output_format=output_format)
31
+ return 0
32
+ if args.split_set_command == "archive":
33
+ response = client.archive_dataset_split_set(args.dataset, args.split_set)
34
+ render_output(response, output_format=output_format)
35
+ return 0
36
+ if args.split_set_command == "restore":
37
+ response = client.restore_dataset_split_set(args.dataset, args.split_set)
38
+ render_output(response, output_format=output_format)
39
+ return 0
40
+ if args.split_set_command == "delete":
41
+ response = client.delete_dataset_split_set(args.dataset, args.split_set)
42
+ render_output(response, output_format=output_format)
43
+ return 0
44
+ if args.split_set_command == "list":
45
+ response = client.list_dataset_split_sets(
46
+ args.dataset, include_archived=args.include_archived
47
+ )
48
+ render_output(response, output_format=output_format)
49
+ return 0
50
+ if args.split_set_command == "splits":
51
+ response = client.list_dataset_split_sets(
52
+ args.dataset, include_archived=args.include_archived
53
+ )
54
+ if args.split_set is not None and args.split_set not in {item.name for item in response.items}:
55
+ raise RuntimeError(f"dataset {args.dataset} has no split set {args.split_set}")
56
+ split_datasets = []
57
+ for item in response.items:
58
+ if args.split_set is not None and item.name != args.split_set:
59
+ continue
60
+ for dataset in item.datasets:
61
+ split_datasets.append(
62
+ {
63
+ "split_set": item.name,
64
+ "split_name": dataset.split_name,
65
+ "dataset_name": dataset.dataset_name,
66
+ "sample_count": dataset.sample_count,
67
+ "shard_count": dataset.shard_count,
68
+ }
69
+ )
70
+ render_output(
71
+ {
72
+ "parent_dataset": response.dataset_name,
73
+ "split_set": args.split_set,
74
+ "split_datasets": split_datasets,
75
+ },
76
+ output_format=output_format,
77
+ )
78
+ return 0
79
+ raise RuntimeError("Missing split-set subcommand.")
@@ -0,0 +1,22 @@
1
+ from __future__ import annotations
2
+
3
+ from datavo_sdk import StatsRebuildRequest
4
+
5
+ from ..client_factory import build_client
6
+ from ..render import render_output
7
+
8
+
9
+ def handle_stats_command(args) -> int:
10
+ output_format = getattr(args, "format", "plain")
11
+ with build_client() as client:
12
+ if args.stats_command == "rebuild":
13
+ response = client.rebuild_stats(
14
+ StatsRebuildRequest(
15
+ sample_store=args.sample_store,
16
+ dataset=args.dataset,
17
+ all=bool(args.all),
18
+ )
19
+ )
20
+ render_output(response, output_format=output_format)
21
+ return 0
22
+ raise RuntimeError("Missing stats subcommand.")
@@ -0,0 +1,191 @@
1
+ """``datavo teams``, ``datavo share`` / ``unshare``, and ``datavo transfer``.
2
+
3
+ Three things here are deliberate rather than incidental:
4
+
5
+ **Transfer refuses locally.** ``datavo transfer`` will not send the request without an
6
+ acknowledgement — it prints the cascade set first, then requires either
7
+ ``--yes-i-understand-this-is-irreversible`` or a typed confirmation of the resource name.
8
+ Letting the server 400 would technically work, but the point of the acknowledgement is
9
+ that the person typing the command has seen what it moves; a round trip that fails is not
10
+ the same as a prompt that informs.
11
+
12
+ **Capability check before send.** The team endpoints do not exist on an older server, so
13
+ without a check the command dies on a bare 404 from a path that was never there. Reading
14
+ ``/config`` first turns version skew into a sentence.
15
+
16
+ **Sharing is spelled share/unshare, ownership is spelled transfer.** They are different
17
+ verbs on purpose: one is reversible, the other is not.
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ from datavo_sdk import TeamCreateRequest, TeamUpdateRequest
23
+
24
+ from ..client_factory import build_client
25
+ from ..render import render_output
26
+
27
+ _CAPABILITY = "teams-and-sharing"
28
+
29
+ # `datavo share store NAME`, `... dataset NAME`, `... split-set NAME --dataset PARENT`.
30
+ _RESOURCE_KINDS = {
31
+ "store": "sample_store",
32
+ "dataset": "dataset",
33
+ "split-set": "split_set",
34
+ }
35
+
36
+
37
+ def _require_teams_capability(client) -> None:
38
+ """Refuse up front when the server predates this capability.
39
+
40
+ A missing route answers 404, which reads as "no such store" rather than "this server
41
+ is older than your CLI" — so the check happens here, once, before any command sends.
42
+ """
43
+ try:
44
+ config = client.get_public_config()
45
+ except Exception:
46
+ # If /config itself is unreachable the command's own error is the better message.
47
+ return
48
+ capabilities = list(getattr(config, "capabilities", None) or [])
49
+ if _CAPABILITY in capabilities:
50
+ return
51
+ raise RuntimeError(
52
+ f"this server does not support teams and sharing (missing the {_CAPABILITY!r} "
53
+ f"capability; server version {getattr(config, 'release_version', 'unknown')}). "
54
+ "Upgrade the server, or use a CLI matching it."
55
+ )
56
+
57
+
58
+ def _resource_target(args) -> tuple[str, str, str | None]:
59
+ """``(resource_kind, name, parent)`` from the shared positional shape."""
60
+ kind = _RESOURCE_KINDS.get(args.resource)
61
+ if kind is None:
62
+ raise RuntimeError(
63
+ f"unknown resource {args.resource!r}; expected one of {', '.join(_RESOURCE_KINDS)}"
64
+ )
65
+ parent = getattr(args, "dataset", None)
66
+ if kind == "split_set" and not parent:
67
+ raise RuntimeError("a split set is addressed under its parent dataset: pass --dataset")
68
+ return kind, args.name, parent
69
+
70
+
71
+ def handle_teams_command(args) -> int:
72
+ output_format = getattr(args, "format", "plain")
73
+ with build_client() as client:
74
+ _require_teams_capability(client)
75
+ command = args.teams_command
76
+ if command == "list":
77
+ render_output(
78
+ client.list_teams(include_archived=args.include_archived),
79
+ output_format=output_format,
80
+ )
81
+ return 0
82
+ if command == "show":
83
+ render_output(client.get_team(args.team), output_format=output_format)
84
+ return 0
85
+ if command == "create":
86
+ request = TeamCreateRequest(id=args.team, display_name=args.display_name)
87
+ render_output(client.create_team(request), output_format=output_format)
88
+ return 0
89
+ if command == "rename":
90
+ request = TeamUpdateRequest(display_name=args.display_name)
91
+ render_output(client.rename_team(args.team, request), output_format=output_format)
92
+ return 0
93
+ if command == "archive":
94
+ render_output(client.archive_team(args.team), output_format=output_format)
95
+ return 0
96
+ if command == "members":
97
+ if args.members_command == "add":
98
+ render_output(
99
+ client.add_team_member(args.team, args.user),
100
+ output_format=output_format,
101
+ )
102
+ return 0
103
+ if args.members_command == "remove":
104
+ render_output(
105
+ client.remove_team_member(args.team, args.user),
106
+ output_format=output_format,
107
+ )
108
+ return 0
109
+ raise RuntimeError(f"unknown members subcommand: {args.members_command}")
110
+ raise RuntimeError(f"unknown teams subcommand: {command}")
111
+
112
+
113
+ def handle_share_command(args) -> int:
114
+ output_format = getattr(args, "format", "plain")
115
+ with build_client() as client:
116
+ _require_teams_capability(client)
117
+ kind, name, parent = _resource_target(args)
118
+ if getattr(args, "list", False):
119
+ render_output(
120
+ client.list_resource_grants(kind, name, parent=parent),
121
+ output_format=output_format,
122
+ )
123
+ return 0
124
+ response = client.grant_resource_access(
125
+ kind, name, team=args.to, level=args.level, parent=parent
126
+ )
127
+ render_output(response, output_format=output_format)
128
+ return 0
129
+
130
+
131
+ def handle_unshare_command(args) -> int:
132
+ output_format = getattr(args, "format", "plain")
133
+ with build_client() as client:
134
+ _require_teams_capability(client)
135
+ kind, name, parent = _resource_target(args)
136
+ response = client.revoke_resource_access(kind, name, team=args.to, parent=parent)
137
+ render_output(response, output_format=output_format)
138
+ return 0
139
+
140
+
141
+ def handle_transfer_command(args) -> int:
142
+ """Hand a resource to another team, after showing what moves and confirming."""
143
+ output_format = getattr(args, "format", "plain")
144
+ with build_client() as client:
145
+ _require_teams_capability(client)
146
+ kind, name, parent = _resource_target(args)
147
+
148
+ preview = client.preview_resource_ownership_transfer(kind, name, parent=parent)
149
+ _print_transfer_preview(preview, destination=args.to)
150
+ if not _transfer_acknowledged(args, name=name):
151
+ # Refused locally rather than sent and 400'd: the acknowledgement exists so the
152
+ # person running this has seen the cascade, and a failed round trip does not
153
+ # achieve that.
154
+ print(
155
+ "Refusing to transfer without acknowledgement. Re-run with "
156
+ "--yes-i-understand-this-is-irreversible, or confirm the resource name "
157
+ "when prompted."
158
+ )
159
+ return 1
160
+
161
+ response = client.transfer_resource_ownership(
162
+ kind, name, team=args.to, acknowledge_irreversible=True, parent=parent
163
+ )
164
+ render_output(response, output_format=output_format)
165
+ return 0
166
+
167
+
168
+ def _print_transfer_preview(preview, *, destination: str) -> None:
169
+ moving = list(getattr(preview, "would_transfer", None) or [])
170
+ current = getattr(preview, "owner_team_id", None)
171
+ print(f"Transfer to team: {destination}")
172
+ print(f"Current owning team: {current or '(none)'}")
173
+ print(f"Resources that will move ({len(moving)}):")
174
+ for entry in moving:
175
+ label = entry.name or entry.resource_id
176
+ print(f" - {entry.resource_kind} {label}")
177
+ print(
178
+ "\nThis is irreversible. The current owning team keeps NOTHING — no ownership and "
179
+ "no grant. Only the destination team can grant access back."
180
+ )
181
+
182
+
183
+ def _transfer_acknowledged(args, *, name: str) -> bool:
184
+ if getattr(args, "yes_i_understand_this_is_irreversible", False):
185
+ return True
186
+ try:
187
+ typed = input(f"Type the resource name ({name}) to confirm: ").strip()
188
+ except (EOFError, KeyboardInterrupt):
189
+ print()
190
+ return False
191
+ return typed == name
@@ -0,0 +1,67 @@
1
+ """``datavo users`` — the admin surface over who exists and the role each holds.
2
+
3
+ Two things mirror ``datavo teams``:
4
+
5
+ **Capability check before send.** The user routes do not exist on an older server, so
6
+ without a check the command dies on a bare 404 from a path that was never there. Reading
7
+ ``/config`` first turns version skew into a sentence.
8
+
9
+ **A role is an override.** ``set-role`` replaces the Entra baseline everywhere, not only
10
+ here; ``clear-role`` returns the user to their directory role. The server refuses an
11
+ admin's attempt to remove their own admin (self-demotion) with a 409 — surfaced as the
12
+ request error rather than second-guessed locally, because unlike a transfer a role change
13
+ is reversible.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ from ..client_factory import build_client
19
+ from ..render import render_output
20
+
21
+ _CAPABILITY = "user-management"
22
+
23
+
24
+ def _require_users_capability(client) -> None:
25
+ """Refuse up front when the server predates this capability.
26
+
27
+ A missing route answers 404, which reads as "no such user" rather than "this server
28
+ is older than your CLI" — so the check happens here, once, before any command sends.
29
+ """
30
+ try:
31
+ config = client.get_public_config()
32
+ except Exception:
33
+ # If /config itself is unreachable the command's own error is the better message.
34
+ return
35
+ capabilities = list(getattr(config, "capabilities", None) or [])
36
+ if _CAPABILITY in capabilities:
37
+ return
38
+ raise RuntimeError(
39
+ f"this server does not support user management (missing the {_CAPABILITY!r} "
40
+ f"capability; server version {getattr(config, 'release_version', 'unknown')}). "
41
+ "Upgrade the server, or use a CLI matching it."
42
+ )
43
+
44
+
45
+ def handle_users_command(args) -> int:
46
+ output_format = getattr(args, "format", "plain")
47
+ with build_client() as client:
48
+ _require_users_capability(client)
49
+ command = args.users_command
50
+ if command == "list":
51
+ render_output(
52
+ client.list_users(query=args.query, offset=args.offset, limit=args.limit),
53
+ output_format=output_format,
54
+ )
55
+ return 0
56
+ if command == "show":
57
+ render_output(client.get_user(args.user), output_format=output_format)
58
+ return 0
59
+ if command == "set-role":
60
+ render_output(
61
+ client.set_user_role(args.user, args.role), output_format=output_format
62
+ )
63
+ return 0
64
+ if command == "clear-role":
65
+ render_output(client.clear_user_role(args.user), output_format=output_format)
66
+ return 0
67
+ raise RuntimeError(f"unknown users subcommand: {command}")