hivekit 0.3.0__tar.gz → 0.4.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.
- {hivekit-0.3.0 → hivekit-0.4.0}/PKG-INFO +2 -1
- {hivekit-0.3.0 → hivekit-0.4.0}/pyproject.toml +2 -1
- {hivekit-0.3.0 → hivekit-0.4.0}/src/cli/config.py +80 -38
- {hivekit-0.3.0 → hivekit-0.4.0}/src/cli/http_client.py +69 -0
- {hivekit-0.3.0 → hivekit-0.4.0}/src/cli/main.py +362 -28
- hivekit-0.4.0/src/cli/utils/terminal.py +347 -0
- {hivekit-0.3.0 → hivekit-0.4.0}/src/cli/utils/time.py +5 -6
- {hivekit-0.3.0 → hivekit-0.4.0}/src/hivekit.egg-info/PKG-INFO +2 -1
- {hivekit-0.3.0 → hivekit-0.4.0}/src/hivekit.egg-info/SOURCES.txt +1 -0
- {hivekit-0.3.0 → hivekit-0.4.0}/src/hivekit.egg-info/requires.txt +1 -0
- {hivekit-0.3.0 → hivekit-0.4.0}/tests/test_config.py +97 -35
- {hivekit-0.3.0 → hivekit-0.4.0}/tests/test_experiment.py +1 -2
- {hivekit-0.3.0 → hivekit-0.4.0}/tests/test_http_client.py +165 -0
- {hivekit-0.3.0 → hivekit-0.4.0}/tests/test_main.py +235 -0
- {hivekit-0.3.0 → hivekit-0.4.0}/tests/test_overrides.py +2 -2
- {hivekit-0.3.0 → hivekit-0.4.0}/LICENSE +0 -0
- {hivekit-0.3.0 → hivekit-0.4.0}/README.md +0 -0
- {hivekit-0.3.0 → hivekit-0.4.0}/setup.cfg +0 -0
- {hivekit-0.3.0 → hivekit-0.4.0}/src/cli/__init__.py +0 -0
- {hivekit-0.3.0 → hivekit-0.4.0}/src/cli/auth/__init__.py +0 -0
- {hivekit-0.3.0 → hivekit-0.4.0}/src/cli/auth/auth_utils.py +0 -0
- {hivekit-0.3.0 → hivekit-0.4.0}/src/cli/auth/credential_store.py +0 -0
- {hivekit-0.3.0 → hivekit-0.4.0}/src/cli/auth/login_page.py +0 -0
- {hivekit-0.3.0 → hivekit-0.4.0}/src/cli/auth/logo.svg +0 -0
- {hivekit-0.3.0 → hivekit-0.4.0}/src/cli/auth/oidc_flow.py +0 -0
- {hivekit-0.3.0 → hivekit-0.4.0}/src/cli/auth/session_manager.py +0 -0
- {hivekit-0.3.0 → hivekit-0.4.0}/src/cli/auth/token_revoker.py +0 -0
- {hivekit-0.3.0 → hivekit-0.4.0}/src/cli/completers.py +0 -0
- {hivekit-0.3.0 → hivekit-0.4.0}/src/cli/experiment.py +0 -0
- {hivekit-0.3.0 → hivekit-0.4.0}/src/cli/utils/__init__.py +0 -0
- {hivekit-0.3.0 → hivekit-0.4.0}/src/cli/utils/archive.py +0 -0
- {hivekit-0.3.0 → hivekit-0.4.0}/src/cli/utils/config_paths.py +0 -0
- {hivekit-0.3.0 → hivekit-0.4.0}/src/cli/utils/config_sync.py +0 -0
- {hivekit-0.3.0 → hivekit-0.4.0}/src/cli/utils/docker.py +0 -0
- {hivekit-0.3.0 → hivekit-0.4.0}/src/cli/utils/logger.py +0 -0
- {hivekit-0.3.0 → hivekit-0.4.0}/src/cli/utils/upload.py +0 -0
- {hivekit-0.3.0 → hivekit-0.4.0}/src/cli/utils/url_utils.py +0 -0
- {hivekit-0.3.0 → hivekit-0.4.0}/src/cli/version.py +0 -0
- {hivekit-0.3.0 → hivekit-0.4.0}/src/hivekit.egg-info/dependency_links.txt +0 -0
- {hivekit-0.3.0 → hivekit-0.4.0}/src/hivekit.egg-info/entry_points.txt +0 -0
- {hivekit-0.3.0 → hivekit-0.4.0}/src/hivekit.egg-info/top_level.txt +0 -0
- {hivekit-0.3.0 → hivekit-0.4.0}/tests/test_completers.py +0 -0
- {hivekit-0.3.0 → hivekit-0.4.0}/tests/test_config_sync.py +0 -0
- {hivekit-0.3.0 → hivekit-0.4.0}/tests/test_logout.py +0 -0
- {hivekit-0.3.0 → hivekit-0.4.0}/tests/test_push_image.py +0 -0
- {hivekit-0.3.0 → hivekit-0.4.0}/tests/test_time_utils.py +0 -0
- {hivekit-0.3.0 → hivekit-0.4.0}/tests/test_upload.py +0 -0
- {hivekit-0.3.0 → hivekit-0.4.0}/tests/test_version.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: hivekit
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Toolkits for using hive agents, including CLI and SDK.
|
|
5
5
|
License:
|
|
6
6
|
Apache License
|
|
@@ -223,6 +223,7 @@ Requires-Dist: keyring>=25.0.0
|
|
|
223
223
|
Requires-Dist: py-machineid>=0.6.0
|
|
224
224
|
Requires-Dist: cryptography>=42.0.0
|
|
225
225
|
Requires-Dist: ruamel.yaml>=0.18
|
|
226
|
+
Requires-Dist: websocket-client>=1.7.0
|
|
226
227
|
Dynamic: license-file
|
|
227
228
|
|
|
228
229
|
```
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "hivekit"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.4.0"
|
|
8
8
|
description = "Toolkits for using hive agents, including CLI and SDK."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.12"
|
|
@@ -25,6 +25,7 @@ dependencies = [
|
|
|
25
25
|
"py-machineid>=0.6.0",
|
|
26
26
|
"cryptography>=42.0.0",
|
|
27
27
|
"ruamel.yaml>=0.18",
|
|
28
|
+
"websocket-client>=1.7.0",
|
|
28
29
|
]
|
|
29
30
|
|
|
30
31
|
[dependency-groups]
|
|
@@ -20,7 +20,7 @@ from pydantic import (
|
|
|
20
20
|
)
|
|
21
21
|
|
|
22
22
|
from cli.utils.logger import set_log_level
|
|
23
|
-
from cli.utils.time import
|
|
23
|
+
from cli.utils.time import unique_suffix
|
|
24
24
|
|
|
25
25
|
logger = logging.getLogger("hivekit")
|
|
26
26
|
|
|
@@ -29,11 +29,35 @@ class BaseModel(pydantic.BaseModel):
|
|
|
29
29
|
model_config = ConfigDict(extra="forbid", coerce_numbers_to_str=True)
|
|
30
30
|
|
|
31
31
|
|
|
32
|
-
#
|
|
33
|
-
#
|
|
34
|
-
#
|
|
35
|
-
|
|
36
|
-
|
|
32
|
+
# Kubernetes caps DNS labels and label values at 63 bytes. The operator derives
|
|
33
|
+
# object names/labels by appending suffixes to the experiment name (the longest
|
|
34
|
+
# is "-coordinator"), so reserve that headroom to keep the derived names valid.
|
|
35
|
+
K8S_LABEL_MAX_LENGTH = 63
|
|
36
|
+
RESERVED_NAME_SUFFIX_LENGTH = len("-coordinator")
|
|
37
|
+
MAX_EXPERIMENT_NAME_LENGTH = K8S_LABEL_MAX_LENGTH - RESERVED_NAME_SUFFIX_LENGTH
|
|
38
|
+
# A name ending in '-' gets a random uniqueness suffix appended (see
|
|
39
|
+
# unique_suffix), which eats into the budget the user actually controls (their
|
|
40
|
+
# typed base name).
|
|
41
|
+
AUTO_HASH_LENGTH = 7
|
|
42
|
+
MAX_EXPERIMENT_BASE_NAME_LENGTH = MAX_EXPERIMENT_NAME_LENGTH - AUTO_HASH_LENGTH
|
|
43
|
+
|
|
44
|
+
# Accelerator (GPU) types the cloud providers know how to schedule. Mirrors the
|
|
45
|
+
# controller's LabelsByAcceleratorType switch. Unknown types are not rejected —
|
|
46
|
+
# a provider may support more than we list here — but they trigger a warning.
|
|
47
|
+
KNOWN_ACCELERATOR_TYPES = frozenset(
|
|
48
|
+
{
|
|
49
|
+
"a10",
|
|
50
|
+
"a100",
|
|
51
|
+
"a100-40gb",
|
|
52
|
+
"a100-80gb",
|
|
53
|
+
"b200",
|
|
54
|
+
"h100",
|
|
55
|
+
"h200",
|
|
56
|
+
"l4",
|
|
57
|
+
"l40s",
|
|
58
|
+
"t4",
|
|
59
|
+
}
|
|
60
|
+
)
|
|
37
61
|
|
|
38
62
|
|
|
39
63
|
class ResourceConfig(BaseModel):
|
|
@@ -62,8 +86,6 @@ class ResourceConfig(BaseModel):
|
|
|
62
86
|
raise ValueError(f"cpu is not a valid quantity (e.g. '1', '500m'), got: '{v}'")
|
|
63
87
|
if cores <= 0:
|
|
64
88
|
raise ValueError(f"cpu must be positive, got: '{v}'")
|
|
65
|
-
if cores > MAX_CPU_CORES:
|
|
66
|
-
raise ValueError(f"cpu must be at most {MAX_CPU_CORES} cores, got: '{v}'")
|
|
67
89
|
return v
|
|
68
90
|
|
|
69
91
|
@field_validator("memory")
|
|
@@ -75,10 +97,40 @@ class ResourceConfig(BaseModel):
|
|
|
75
97
|
raise ValueError(f"memory is not a valid quantity (e.g. '2Gi', '512Mi'), got: '{v}'")
|
|
76
98
|
if memory_bytes <= 0:
|
|
77
99
|
raise ValueError(f"memory must be positive, got: '{v}'")
|
|
78
|
-
if memory_bytes > MAX_MEMORY_BYTES:
|
|
79
|
-
raise ValueError(f"memory must be at most 128Gi, got: '{v}'")
|
|
80
100
|
return v
|
|
81
101
|
|
|
102
|
+
@field_validator("accelerators")
|
|
103
|
+
@classmethod
|
|
104
|
+
def validate_accelerators(cls, v):
|
|
105
|
+
# NOTE: KNOWN_ACCELERATOR_TYPES is a hardcoded mirror of the controller's
|
|
106
|
+
# switch, so it can drift out of sync. A better approach would be a new
|
|
107
|
+
# backend endpoint that lists the currently available GPU types, letting
|
|
108
|
+
# us validate against the live source of truth instead of this static set.
|
|
109
|
+
if v is None:
|
|
110
|
+
return v
|
|
111
|
+
parts = v.split(":")
|
|
112
|
+
if len(parts) != 2:
|
|
113
|
+
raise ValueError(
|
|
114
|
+
f"accelerators must be in '<type>:<count>' format (e.g. 'a100-80gb:8'), got: '{v}'"
|
|
115
|
+
)
|
|
116
|
+
gpu_type, count = parts[0].strip(), parts[1].strip()
|
|
117
|
+
if not gpu_type:
|
|
118
|
+
raise ValueError(f"accelerators type must not be empty, got: '{v}'")
|
|
119
|
+
try:
|
|
120
|
+
n = int(count)
|
|
121
|
+
except ValueError:
|
|
122
|
+
raise ValueError(f"accelerators count must be an integer, got: '{count}'")
|
|
123
|
+
if n <= 0:
|
|
124
|
+
raise ValueError(f"accelerators count must be positive, got: '{count}'")
|
|
125
|
+
if gpu_type not in KNOWN_ACCELERATOR_TYPES:
|
|
126
|
+
logger.warning(
|
|
127
|
+
"accelerators type '%s' is not among the known GPU types (%s); "
|
|
128
|
+
"any available GPUs may be used. Please confirm with the Hiverge team.",
|
|
129
|
+
gpu_type,
|
|
130
|
+
", ".join(sorted(KNOWN_ACCELERATOR_TYPES)),
|
|
131
|
+
)
|
|
132
|
+
return f"{gpu_type}:{n}"
|
|
133
|
+
|
|
82
134
|
|
|
83
135
|
class KeyValueSet(BaseModel):
|
|
84
136
|
name: str
|
|
@@ -225,24 +277,14 @@ class RepoConfig(BaseModel):
|
|
|
225
277
|
return v
|
|
226
278
|
|
|
227
279
|
|
|
228
|
-
# Platform caps. One sandbox is provisioned per agent, so num_agents is bounded
|
|
229
|
-
# by the max number of sandboxes. max_runtime_seconds is bounded by the maximum
|
|
230
|
-
# runtime of an experiment (6 hours).
|
|
231
|
-
MAX_SANDBOXES = 32
|
|
232
|
-
MAX_RUNTIME_SECONDS = 6 * 60 * 60
|
|
233
|
-
|
|
234
|
-
|
|
235
280
|
class RuntimeConfig(BaseModel):
|
|
236
281
|
num_agents: int = Field(
|
|
237
282
|
default=1,
|
|
238
283
|
description="Number of agents to use in the experiment. Default to 1.",
|
|
239
284
|
)
|
|
240
285
|
max_runtime_seconds: int = Field(
|
|
241
|
-
default
|
|
242
|
-
description=
|
|
243
|
-
"Maximum runtime for the experiment in seconds. -1 means no limit. "
|
|
244
|
-
f"Defaults to the platform maximum of {MAX_RUNTIME_SECONDS} (6 hours)."
|
|
245
|
-
),
|
|
286
|
+
default=-1,
|
|
287
|
+
description="Maximum runtime for the experiment in seconds. -1 means no limit.",
|
|
246
288
|
)
|
|
247
289
|
max_iterations: int = Field(
|
|
248
290
|
default=-1,
|
|
@@ -256,30 +298,21 @@ class RuntimeConfig(BaseModel):
|
|
|
256
298
|
@field_validator("num_agents")
|
|
257
299
|
@classmethod
|
|
258
300
|
def validate_num_agents(cls, v):
|
|
259
|
-
# One sandbox per agent
|
|
301
|
+
# One sandbox is provisioned per agent.
|
|
260
302
|
if v < 1:
|
|
261
303
|
raise ValueError(f"num_agents must be at least 1, got {v}.")
|
|
262
|
-
if v > MAX_SANDBOXES:
|
|
263
|
-
raise ValueError(
|
|
264
|
-
f"num_agents must be at most {MAX_SANDBOXES} (one sandbox per agent), got {v}."
|
|
265
|
-
)
|
|
266
304
|
return v
|
|
267
305
|
|
|
268
306
|
@field_validator("max_runtime_seconds")
|
|
269
307
|
@classmethod
|
|
270
308
|
def validate_max_runtime_seconds(cls, v):
|
|
271
|
-
# -1 is the sentinel for "no limit"
|
|
272
|
-
# platform's maximum experiment runtime.
|
|
309
|
+
# -1 is the sentinel for "no limit"
|
|
273
310
|
if v == -1:
|
|
274
311
|
return v
|
|
275
312
|
if v < 1:
|
|
276
313
|
raise ValueError(
|
|
277
314
|
f"max_runtime_seconds must be -1 (no limit) or a positive integer, got {v}."
|
|
278
315
|
)
|
|
279
|
-
if v > MAX_RUNTIME_SECONDS:
|
|
280
|
-
raise ValueError(
|
|
281
|
-
f"max_runtime_seconds must be at most {MAX_RUNTIME_SECONDS} (6 hours), got {v}."
|
|
282
|
-
)
|
|
283
316
|
return v
|
|
284
317
|
|
|
285
318
|
|
|
@@ -295,15 +328,24 @@ class HiveConfig(BaseModel):
|
|
|
295
328
|
@field_validator("experiment_name")
|
|
296
329
|
@classmethod
|
|
297
330
|
def validate_experiment_name(cls, v):
|
|
298
|
-
|
|
299
|
-
|
|
331
|
+
base_name = v
|
|
332
|
+
auto_hashed = v.endswith("-")
|
|
333
|
+
if auto_hashed:
|
|
334
|
+
v = f"{v}{unique_suffix()}"
|
|
300
335
|
if not re.fullmatch(r"[a-z0-9]([a-z0-9-]*[a-z0-9])?", v):
|
|
301
336
|
raise ValueError(
|
|
302
|
-
f"Experiment name must be a valid DNS label (RFC 1123): [a-z0-9] separated by '-', got: '{
|
|
337
|
+
f"Experiment name must be a valid DNS label (RFC 1123): [a-z0-9] separated by '-', got: '{base_name}'"
|
|
303
338
|
)
|
|
304
|
-
if len(v) >
|
|
339
|
+
if len(v) > MAX_EXPERIMENT_NAME_LENGTH:
|
|
340
|
+
if auto_hashed:
|
|
341
|
+
raise ValueError(
|
|
342
|
+
f"Experiment name must be no more than {MAX_EXPERIMENT_BASE_NAME_LENGTH} "
|
|
343
|
+
f"characters when it ends in '-', got {len(base_name)}: '{base_name}'. "
|
|
344
|
+
f"Shorten the name or drop the trailing '-'."
|
|
345
|
+
)
|
|
305
346
|
raise ValueError(
|
|
306
|
-
f"Experiment name must be no more than
|
|
347
|
+
f"Experiment name must be no more than {MAX_EXPERIMENT_NAME_LENGTH} "
|
|
348
|
+
f"characters, got {len(v)}: '{v}'. Shorten the name."
|
|
307
349
|
)
|
|
308
350
|
return v
|
|
309
351
|
|
|
@@ -49,6 +49,14 @@ class StreamNotReadyError(Exception):
|
|
|
49
49
|
"""Raised when the log source is not yet available for streaming."""
|
|
50
50
|
|
|
51
51
|
|
|
52
|
+
class ShellNotFoundError(Exception):
|
|
53
|
+
"""Raised when a shell no longer exists server-side (HTTP 404).
|
|
54
|
+
|
|
55
|
+
Distinct from a transient failure: the shell's CR is gone (deleted, reaped,
|
|
56
|
+
or never existed), so the caller must give up rather than retry.
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
|
|
52
60
|
class HttpClient:
|
|
53
61
|
"""
|
|
54
62
|
HTTP client for communicating with the Hive backend server.
|
|
@@ -89,6 +97,67 @@ class HttpClient:
|
|
|
89
97
|
response = self._send("post", "/experiments", "create experiment", json=experiment_data)
|
|
90
98
|
return response.json()
|
|
91
99
|
|
|
100
|
+
def create_shell(self, shell_data: Dict[str, Any]) -> Dict[str, Any]:
|
|
101
|
+
"""
|
|
102
|
+
Create a new interactive shell.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
shell_data: Body of the form
|
|
106
|
+
{"sandbox": <SandboxSpec>, "ttlSeconds": <int, optional>}.
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
Server payload {"shellId": <str>, "name": <k8s Shell CR name>}.
|
|
110
|
+
"""
|
|
111
|
+
response = self._send("post", "/shells", "create shell", json=shell_data)
|
|
112
|
+
return response.json()
|
|
113
|
+
|
|
114
|
+
def get_shell(self, shell_id: str) -> Dict[str, Any]:
|
|
115
|
+
"""
|
|
116
|
+
Get a shell's readiness status.
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
Server payload {"shellId", "phase", "podName", "ready": bool}.
|
|
120
|
+
|
|
121
|
+
Raises:
|
|
122
|
+
ShellNotFoundError: the shell is gone (HTTP 404) -- terminal, do not
|
|
123
|
+
retry.
|
|
124
|
+
Exception: any other failure (5xx, connection error) -- an "unknown"
|
|
125
|
+
status the caller may retry within a bounded window.
|
|
126
|
+
"""
|
|
127
|
+
url = f"{self.base_url}/shells/{shell_id}"
|
|
128
|
+
response = self._request(method="get", url=url, headers=self._get_headers())
|
|
129
|
+
# 404 is the terminal "gone" signal, distinct from a retryable failure.
|
|
130
|
+
if response.status_code == HTTPStatus.NOT_FOUND:
|
|
131
|
+
raise ShellNotFoundError(shell_id)
|
|
132
|
+
# Any other non-2xx is "unknown": reuse _send()'s formatting so the detail
|
|
133
|
+
# comes from the server's JSON `error` field, not the raw body.
|
|
134
|
+
try:
|
|
135
|
+
response.raise_for_status()
|
|
136
|
+
except requests.exceptions.HTTPError as e:
|
|
137
|
+
raise Exception(f"Failed to get shell: {_http_error_detail(e)}") from e
|
|
138
|
+
return response.json()
|
|
139
|
+
|
|
140
|
+
def bearer_token(self) -> str:
|
|
141
|
+
"""Return a currently-valid bearer access token for out-of-band use.
|
|
142
|
+
|
|
143
|
+
The WebSocket handshake attaches the token by hand and bypasses both the
|
|
144
|
+
session's automatic ``Authorization`` injection and the 401 re-auth
|
|
145
|
+
retry, so the token is refreshed here first (if expired/near expiry) to
|
|
146
|
+
avoid handing a stale credential to the ALB/Tyk edge.
|
|
147
|
+
"""
|
|
148
|
+
token = getattr(self._session, "token", None)
|
|
149
|
+
if not token:
|
|
150
|
+
raise AuthenticationError(
|
|
151
|
+
"No access token available. Please run 'hive login' to authenticate."
|
|
152
|
+
)
|
|
153
|
+
try:
|
|
154
|
+
self._session.ensure_active_token(token)
|
|
155
|
+
except OAuthError as e:
|
|
156
|
+
raise AuthenticationError(
|
|
157
|
+
"Your credentials have expired. Please run 'hive login' to re-authenticate."
|
|
158
|
+
) from e
|
|
159
|
+
return self._session.token["access_token"]
|
|
160
|
+
|
|
92
161
|
def get_push_credentials(self, reference: str) -> Dict[str, Any]:
|
|
93
162
|
"""
|
|
94
163
|
Request short-lived registry credentials for pushing an image.
|