nullspace-sdk 0.1.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.
nullspace/__init__.py ADDED
@@ -0,0 +1,354 @@
1
+ from importlib.metadata import PackageNotFoundError, version as _pkg_version
2
+
3
+ try:
4
+ __version__ = _pkg_version("nullspace-sdk")
5
+ except PackageNotFoundError:
6
+ __version__ = "0.1.0"
7
+
8
+ from .sandbox import Sandbox, AsyncSandbox, SandboxPaginator, AsyncSandboxPaginator
9
+ from .git import GitHttpsAuth
10
+ from .snapshot import Snapshot, AsyncSnapshot
11
+ from .template import (
12
+ CommandReadinessProbe,
13
+ HttpReadinessProbe,
14
+ PortReadinessProbe,
15
+ Template,
16
+ TemplateBuild,
17
+ TemplateBuildStatusSnapshot,
18
+ TimeoutReadinessProbe,
19
+ default_build_logger,
20
+ wait_for_file,
21
+ wait_for_port,
22
+ wait_for_process,
23
+ wait_for_timeout,
24
+ wait_for_url,
25
+ )
26
+ from .monitor import Monitor, AsyncMonitor
27
+ from .lifecycle import (
28
+ Lifecycle,
29
+ AsyncLifecycle,
30
+ LifecycleSubscription,
31
+ AsyncLifecycleSubscription,
32
+ )
33
+ from .code_interpreter import CodeInterpreter, AsyncCodeInterpreter
34
+ from .volume import Volume, AsyncVolume
35
+ from .types import (
36
+ SandboxInfo,
37
+ SandboxHostInfo,
38
+ SandboxConfig,
39
+ SandboxStatus,
40
+ SandboxListState,
41
+ SandboxTimeoutAction,
42
+ SandboxQuery,
43
+ SandboxState,
44
+ VolumeStatus,
45
+ VolumeInfo,
46
+ VolumeMount,
47
+ VolumeAttachment,
48
+ TemplateInfo,
49
+ TemplateTagInfo,
50
+ TemplateNameClaim,
51
+ TemplateAliasInfo,
52
+ TemplateVisibility,
53
+ TemplateNamespaceInfo,
54
+ TemplateNamespaceKind,
55
+ LogEntry,
56
+ LogEntryEnd,
57
+ LogEntryStart,
58
+ TemplateBuildCacheKind,
59
+ TemplateBuildCacheReason,
60
+ TemplateBuildCacheStatus,
61
+ TemplateBuildLogEntry,
62
+ TemplateBuildLogKind,
63
+ TemplateBuildLogLevel,
64
+ TemplateBuildStatus,
65
+ TemplateFailureDetail,
66
+ TemplateFailurePhase,
67
+ UploadKind,
68
+ UploadSourceKind,
69
+ UploadConflictPolicy,
70
+ UploadArchiveFormat,
71
+ UploadChecksumAlgorithm,
72
+ UploadSessionStatus,
73
+ UploadFailureReason,
74
+ UploadFailureDetail,
75
+ UploadLimits,
76
+ UploadSessionInfo,
77
+ CompleteUploadResponse,
78
+ VolumeUploadLimits,
79
+ VolumeUploadSessionInfo,
80
+ VolumeCompleteUploadResponse,
81
+ UploadTransport,
82
+ UploadResult,
83
+ DownloadResult,
84
+ UploadProgressPhase,
85
+ UploadProgress,
86
+ AsyncBackgroundCommand,
87
+ BackgroundCommand,
88
+ CommandResult,
89
+ GitBranches,
90
+ CommandLogs,
91
+ FileInfo,
92
+ BatchWriteFailure,
93
+ FileEvent,
94
+ UploadUrl,
95
+ SearchMatch,
96
+ VolumeReplaceResult,
97
+ ProcessInfo,
98
+ SnapshotInfo,
99
+ SandboxMetrics,
100
+ LifecycleAudience,
101
+ LifecycleStatus,
102
+ LifecycleOperation,
103
+ LifecycleActorKind,
104
+ LifecycleEvent,
105
+ LifecycleEventListResponse,
106
+ LifecycleWebhookDeliveryAttemptInfo,
107
+ LifecycleWebhookDeliveryDetail,
108
+ LifecycleWebhookDeliveryInfo,
109
+ LifecycleWebhookDeliveryStatus,
110
+ LifecycleWebhookInfo,
111
+ LifecycleStreamError,
112
+ MonitorSnapshotEvent,
113
+ MonitorUpdateEvent,
114
+ MonitorMetricsEvent,
115
+ MonitorProcessesEvent,
116
+ MonitorErrorEvent,
117
+ DisplayVirtualScreen,
118
+ DisplayMonitor,
119
+ DisplayInfo,
120
+ DisplayEntry,
121
+ WindowInfo,
122
+ DesktopCaptureCapabilities,
123
+ DesktopInputCapabilities,
124
+ DesktopDisplayCapabilities,
125
+ DesktopWindowCapabilities,
126
+ DesktopLaunchCapabilities,
127
+ DesktopOpenCapabilities,
128
+ DesktopClipboardCapabilities,
129
+ DesktopClipboardData,
130
+ DesktopStreamingCapabilities,
131
+ DesktopRecordingCapabilities,
132
+ DesktopCapabilities,
133
+ DesktopStreamInfo,
134
+ ManagedDesktopStreamInfo,
135
+ RecordingInfo,
136
+ PtySessionInfo,
137
+ PtyResult,
138
+ Execution,
139
+ CodeArtifact,
140
+ Result,
141
+ Chart,
142
+ BarChart,
143
+ LineChart,
144
+ PieChart,
145
+ ScatterChart,
146
+ BoxAndWhiskerChart,
147
+ BarChartElement,
148
+ LineChartElement,
149
+ PieChartElement,
150
+ ScatterChartElement,
151
+ BoxAndWhiskerChartElement,
152
+ Logs,
153
+ ExecutionError,
154
+ OutputMessage,
155
+ CodeContext,
156
+ CodeRun,
157
+ serialize_results,
158
+ )
159
+ from .errors import (
160
+ AuthError,
161
+ BuildError,
162
+ FileUploadError,
163
+ UploadChecksumError,
164
+ UploadConflictError,
165
+ UploadExpiredError,
166
+ UploadLimitError,
167
+ UploadPathError,
168
+ UploadStateError,
169
+ SandboxError,
170
+ TimeoutError,
171
+ NotFoundError,
172
+ PermissionError,
173
+ ConflictError,
174
+ QuotaExceededError,
175
+ RateLimitError,
176
+ NotImplementedError,
177
+ GitError,
178
+ BatchWriteError,
179
+ )
180
+
181
+ __all__ = [
182
+ "__version__",
183
+ "Sandbox",
184
+ "AsyncSandbox",
185
+ "GitHttpsAuth",
186
+ "Volume",
187
+ "AsyncVolume",
188
+ "Snapshot",
189
+ "AsyncSnapshot",
190
+ "Template",
191
+ "TemplateBuild",
192
+ "TemplateBuildStatusSnapshot",
193
+ "CommandReadinessProbe",
194
+ "HttpReadinessProbe",
195
+ "PortReadinessProbe",
196
+ "TimeoutReadinessProbe",
197
+ "default_build_logger",
198
+ "wait_for_file",
199
+ "wait_for_port",
200
+ "wait_for_process",
201
+ "wait_for_timeout",
202
+ "wait_for_url",
203
+ "Monitor",
204
+ "AsyncMonitor",
205
+ "Lifecycle",
206
+ "AsyncLifecycle",
207
+ "LifecycleSubscription",
208
+ "AsyncLifecycleSubscription",
209
+ "CodeInterpreter",
210
+ "AsyncCodeInterpreter",
211
+ "SandboxInfo",
212
+ "SandboxHostInfo",
213
+ "SandboxConfig",
214
+ "SandboxStatus",
215
+ "SandboxListState",
216
+ "SandboxTimeoutAction",
217
+ "SandboxQuery",
218
+ "SandboxState",
219
+ "SandboxPaginator",
220
+ "AsyncSandboxPaginator",
221
+ "VolumeStatus",
222
+ "VolumeInfo",
223
+ "VolumeMount",
224
+ "VolumeAttachment",
225
+ "TemplateInfo",
226
+ "TemplateTagInfo",
227
+ "TemplateNameClaim",
228
+ "TemplateAliasInfo",
229
+ "TemplateVisibility",
230
+ "TemplateNamespaceInfo",
231
+ "TemplateNamespaceKind",
232
+ "LogEntry",
233
+ "LogEntryEnd",
234
+ "LogEntryStart",
235
+ "TemplateBuildCacheKind",
236
+ "TemplateBuildCacheReason",
237
+ "TemplateBuildCacheStatus",
238
+ "TemplateBuildLogEntry",
239
+ "TemplateBuildLogKind",
240
+ "TemplateBuildLogLevel",
241
+ "TemplateBuildStatus",
242
+ "TemplateFailureDetail",
243
+ "TemplateFailurePhase",
244
+ "UploadKind",
245
+ "UploadSourceKind",
246
+ "UploadConflictPolicy",
247
+ "UploadArchiveFormat",
248
+ "UploadChecksumAlgorithm",
249
+ "UploadSessionStatus",
250
+ "UploadFailureReason",
251
+ "UploadFailureDetail",
252
+ "UploadLimits",
253
+ "UploadSessionInfo",
254
+ "CompleteUploadResponse",
255
+ "VolumeUploadLimits",
256
+ "VolumeUploadSessionInfo",
257
+ "VolumeCompleteUploadResponse",
258
+ "UploadTransport",
259
+ "UploadResult",
260
+ "DownloadResult",
261
+ "UploadProgressPhase",
262
+ "UploadProgress",
263
+ "AsyncBackgroundCommand",
264
+ "BackgroundCommand",
265
+ "CommandResult",
266
+ "GitBranches",
267
+ "CommandLogs",
268
+ "FileInfo",
269
+ "BatchWriteFailure",
270
+ "FileEvent",
271
+ "UploadUrl",
272
+ "SearchMatch",
273
+ "VolumeReplaceResult",
274
+ "ProcessInfo",
275
+ "SnapshotInfo",
276
+ "SandboxMetrics",
277
+ "LifecycleAudience",
278
+ "LifecycleStatus",
279
+ "LifecycleOperation",
280
+ "LifecycleActorKind",
281
+ "LifecycleEvent",
282
+ "LifecycleEventListResponse",
283
+ "LifecycleWebhookDeliveryAttemptInfo",
284
+ "LifecycleWebhookDeliveryDetail",
285
+ "LifecycleWebhookDeliveryInfo",
286
+ "LifecycleWebhookDeliveryStatus",
287
+ "LifecycleWebhookInfo",
288
+ "LifecycleStreamError",
289
+ "MonitorSnapshotEvent",
290
+ "MonitorUpdateEvent",
291
+ "MonitorMetricsEvent",
292
+ "MonitorProcessesEvent",
293
+ "MonitorErrorEvent",
294
+ "DisplayVirtualScreen",
295
+ "DisplayMonitor",
296
+ "DisplayInfo",
297
+ "DisplayEntry",
298
+ "WindowInfo",
299
+ "DesktopCaptureCapabilities",
300
+ "DesktopInputCapabilities",
301
+ "DesktopDisplayCapabilities",
302
+ "DesktopWindowCapabilities",
303
+ "DesktopLaunchCapabilities",
304
+ "DesktopOpenCapabilities",
305
+ "DesktopClipboardCapabilities",
306
+ "DesktopClipboardData",
307
+ "DesktopStreamingCapabilities",
308
+ "DesktopRecordingCapabilities",
309
+ "DesktopCapabilities",
310
+ "DesktopStreamInfo",
311
+ "ManagedDesktopStreamInfo",
312
+ "RecordingInfo",
313
+ "PtySessionInfo",
314
+ "PtyResult",
315
+ "Execution",
316
+ "CodeArtifact",
317
+ "Result",
318
+ "Chart",
319
+ "BarChart",
320
+ "LineChart",
321
+ "PieChart",
322
+ "ScatterChart",
323
+ "BoxAndWhiskerChart",
324
+ "BarChartElement",
325
+ "LineChartElement",
326
+ "PieChartElement",
327
+ "ScatterChartElement",
328
+ "BoxAndWhiskerChartElement",
329
+ "Logs",
330
+ "ExecutionError",
331
+ "OutputMessage",
332
+ "CodeContext",
333
+ "CodeRun",
334
+ "serialize_results",
335
+ "AuthError",
336
+ "BuildError",
337
+ "FileUploadError",
338
+ "UploadLimitError",
339
+ "UploadChecksumError",
340
+ "UploadConflictError",
341
+ "UploadExpiredError",
342
+ "UploadPathError",
343
+ "UploadStateError",
344
+ "SandboxError",
345
+ "TimeoutError",
346
+ "NotFoundError",
347
+ "PermissionError",
348
+ "ConflictError",
349
+ "QuotaExceededError",
350
+ "RateLimitError",
351
+ "NotImplementedError",
352
+ "GitError",
353
+ "BatchWriteError",
354
+ ]
@@ -0,0 +1,230 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ import click
6
+
7
+ from ._exit_codes import EXIT_CODE_DESCRIPTIONS
8
+
9
+ SCHEMA_VERSION = "v1"
10
+
11
+
12
+ def build_cli_schema(root: click.Command, *, root_name: str = "nullspace") -> dict[str, Any]:
13
+ """Return the versioned machine-readable schema for a Click command tree."""
14
+ return {
15
+ "schema_version": SCHEMA_VERSION,
16
+ "program": root_name,
17
+ "output_versions": ["v1"],
18
+ "commands": {
19
+ entry["id"]: entry
20
+ for entry in _iter_command_entries(root, root_name=root_name)
21
+ if entry["id"]
22
+ },
23
+ "exit_codes": [
24
+ {"code": code, "description": description}
25
+ for code, description in sorted(EXIT_CODE_DESCRIPTIONS.items())
26
+ ],
27
+ "error_envelope": {
28
+ "type": "object",
29
+ "required": ["error"],
30
+ "properties": {
31
+ "error": {
32
+ "type": "object",
33
+ "required": ["code", "message", "doc_url"],
34
+ "properties": {
35
+ "code": {"type": "string"},
36
+ "message": {"type": "string"},
37
+ "hint": {"type": ["string", "null"]},
38
+ "doc_url": {"type": "string"},
39
+ "request_id": {"type": ["string", "null"]},
40
+ "retryable": {"type": ["boolean", "null"]},
41
+ },
42
+ }
43
+ },
44
+ },
45
+ }
46
+
47
+
48
+ def command_schema(
49
+ root: click.Command,
50
+ command_path: list[str] | tuple[str, ...],
51
+ *,
52
+ root_name: str = "nullspace",
53
+ ) -> dict[str, Any]:
54
+ """Return a single command entry selected by command path."""
55
+ command = _resolve_command(root, command_path)
56
+ path = list(command_path)
57
+ return _command_entry(command, path=path, root_name=root_name)
58
+
59
+
60
+ def command_ids(root: click.Command) -> list[str]:
61
+ return [entry["id"] for entry in _iter_command_entries(root, root_name="nullspace") if entry["id"]]
62
+
63
+
64
+ def _iter_command_entries(
65
+ command: click.Command,
66
+ *,
67
+ root_name: str,
68
+ path: list[str] | None = None,
69
+ ) -> list[dict[str, Any]]:
70
+ path = path or []
71
+ entries = [_command_entry(command, path=path, root_name=root_name)]
72
+ if isinstance(command, click.Group):
73
+ for name, child in sorted(command.commands.items()):
74
+ if getattr(child, "hidden", False):
75
+ continue
76
+ entries.extend(
77
+ _iter_command_entries(
78
+ child,
79
+ root_name=root_name,
80
+ path=[*path, name],
81
+ )
82
+ )
83
+ return entries
84
+
85
+
86
+ def _command_entry(command: click.Command, *, path: list[str], root_name: str) -> dict[str, Any]:
87
+ params = [_param_entry(param) for param in command.params]
88
+ return {
89
+ "id": ".".join(path),
90
+ "name": path[-1] if path else root_name,
91
+ "kind": "group" if isinstance(command, click.Group) else "command",
92
+ "path": path,
93
+ "full_path": " ".join([root_name, *path]),
94
+ "summary": _first_sentence(command.short_help or command.help or ""),
95
+ "help": command.help or "",
96
+ "examples": [],
97
+ "params": params,
98
+ "flags": [param for param in params if param["kind"] == "option"],
99
+ "arguments": [param for param in params if param["kind"] == "argument"],
100
+ "error_codes": _default_error_codes(path),
101
+ "output": {
102
+ "version": "v1",
103
+ "success": "command-specific JSON when --json is supported; otherwise human text",
104
+ "failure": "error_envelope",
105
+ },
106
+ "json_schema": _input_json_schema(params),
107
+ }
108
+
109
+
110
+ def _resolve_command(root: click.Command, command_path: list[str] | tuple[str, ...]) -> click.Command:
111
+ current = root
112
+ for part in command_path:
113
+ if not isinstance(current, click.Group):
114
+ raise click.UsageError(f"{' '.join(command_path)!r} is not a command group")
115
+ next_command = current.commands.get(part)
116
+ if next_command is None:
117
+ raise click.UsageError(f"unknown command path: {' '.join(command_path)}")
118
+ current = next_command
119
+ return current
120
+
121
+
122
+ def _param_entry(param: click.Parameter) -> dict[str, Any]:
123
+ if isinstance(param, click.Option):
124
+ names = [*param.opts, *param.secondary_opts]
125
+ return {
126
+ "kind": "option",
127
+ "name": param.name,
128
+ "flags": names,
129
+ "primary_flag": param.opts[0] if param.opts else None,
130
+ "type": _click_type_name(param.type),
131
+ "json_schema": _param_json_schema(param),
132
+ "default": _json_default(param.default),
133
+ "required": param.required,
134
+ "multiple": bool(param.multiple),
135
+ "is_flag": bool(param.is_flag),
136
+ "description": param.help or "",
137
+ }
138
+ return {
139
+ "kind": "argument",
140
+ "name": param.name,
141
+ "type": _click_type_name(param.type),
142
+ "json_schema": _param_json_schema(param),
143
+ "default": None,
144
+ "required": param.required,
145
+ "multiple": bool(getattr(param, "nargs", 1) == -1),
146
+ "description": "",
147
+ }
148
+
149
+
150
+ def _input_json_schema(params: list[dict[str, Any]]) -> dict[str, Any]:
151
+ properties = {}
152
+ required = []
153
+ for param in params:
154
+ properties[param["name"]] = param["json_schema"]
155
+ if param["required"]:
156
+ required.append(param["name"])
157
+ schema: dict[str, Any] = {
158
+ "type": "object",
159
+ "properties": properties,
160
+ "additionalProperties": False,
161
+ }
162
+ if required:
163
+ schema["required"] = required
164
+ return schema
165
+
166
+
167
+ def _param_json_schema(param: click.Parameter) -> dict[str, Any]:
168
+ schema = _type_json_schema(param.type)
169
+ if isinstance(param, click.Option) and param.is_flag:
170
+ schema = {"type": "boolean"}
171
+ if getattr(param, "multiple", False) or getattr(param, "nargs", 1) == -1:
172
+ schema = {"type": "array", "items": schema}
173
+ return schema
174
+
175
+
176
+ def _type_json_schema(param_type: click.ParamType) -> dict[str, Any]:
177
+ if isinstance(param_type, click.Choice):
178
+ return {"type": "string", "enum": list(param_type.choices)}
179
+ if isinstance(param_type, click.IntRange):
180
+ schema: dict[str, Any] = {"type": "integer"}
181
+ if param_type.min is not None:
182
+ schema["minimum"] = param_type.min
183
+ if param_type.max is not None:
184
+ schema["maximum"] = param_type.max
185
+ return schema
186
+ if isinstance(param_type, click.types.FloatRange):
187
+ schema = {"type": "number"}
188
+ if param_type.min is not None:
189
+ schema["minimum"] = param_type.min
190
+ if param_type.max is not None:
191
+ schema["maximum"] = param_type.max
192
+ return schema
193
+ name = _click_type_name(param_type)
194
+ if name in {"integer", "int"}:
195
+ return {"type": "integer"}
196
+ if name in {"float"}:
197
+ return {"type": "number"}
198
+ if name in {"boolean", "bool"}:
199
+ return {"type": "boolean"}
200
+ return {"type": "string"}
201
+
202
+
203
+ def _click_type_name(param_type: click.ParamType) -> str:
204
+ return getattr(param_type, "name", None) or param_type.__class__.__name__.lower()
205
+
206
+
207
+ def _json_default(value: Any) -> Any:
208
+ if callable(value):
209
+ return None
210
+ if value is None or isinstance(value, (str, int, float, bool)):
211
+ return value
212
+ if isinstance(value, tuple):
213
+ return [_json_default(item) for item in value]
214
+ if isinstance(value, list):
215
+ return [_json_default(item) for item in value]
216
+ if isinstance(value, dict):
217
+ return {str(key): _json_default(item) for key, item in value.items()}
218
+ return None
219
+
220
+
221
+ def _first_sentence(value: str) -> str:
222
+ return " ".join(value.strip().split())
223
+
224
+
225
+ def _default_error_codes(path: list[str]) -> list[str]:
226
+ codes = ["invalid_request", "missing_api_key", "unauthorized", "rate_limit_exceeded"]
227
+ if path:
228
+ resource = path[0].replace("-", "_")
229
+ codes.append(f"{resource}_not_found")
230
+ return list(dict.fromkeys(codes))
nullspace/_config.py ADDED
@@ -0,0 +1,90 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import os
5
+ from pathlib import Path
6
+
7
+ DEFAULT_BASE_URL = "http://localhost:3000"
8
+
9
+
10
+ def resolve_api_key(explicit: str | None = None) -> str | None:
11
+ if explicit is not None:
12
+ return explicit.strip()
13
+ return (
14
+ _env_value("NULLSPACE_API_KEY")
15
+ or _project_env().get("NULLSPACE_API_KEY")
16
+ or _config_value("api_key")
17
+ )
18
+
19
+
20
+ def resolve_base_url(explicit: str | None = None) -> str:
21
+ if explicit is not None:
22
+ return explicit.strip() or DEFAULT_BASE_URL
23
+ return (
24
+ _env_value("NULLSPACE_API_URL")
25
+ or _env_value("NULLSPACE_BASE_URL")
26
+ or _project_env().get("NULLSPACE_API_URL")
27
+ or _project_env().get("NULLSPACE_BASE_URL")
28
+ or _config_value("base_url")
29
+ or _config_value("api_url")
30
+ or DEFAULT_BASE_URL
31
+ )
32
+
33
+
34
+ def _env_value(name: str) -> str | None:
35
+ value = os.environ.get(name)
36
+ if value is None:
37
+ return None
38
+ value = value.strip()
39
+ return value or None
40
+
41
+
42
+ def _project_env() -> dict[str, str]:
43
+ path = Path.cwd() / ".env"
44
+ if not path.is_file():
45
+ return {}
46
+ values: dict[str, str] = {}
47
+ try:
48
+ for line in path.read_text(encoding="utf-8").splitlines():
49
+ line = line.strip()
50
+ if not line or line.startswith("#") or "=" not in line:
51
+ continue
52
+ key, value = line.split("=", 1)
53
+ key = key.strip()
54
+ value = value.strip().strip("'\"")
55
+ if key:
56
+ values[key] = value
57
+ except OSError:
58
+ return {}
59
+ return values
60
+
61
+
62
+ def _config_value(name: str) -> str | None:
63
+ for path in _config_paths():
64
+ value = _read_config_value(path, name)
65
+ if value:
66
+ return value
67
+ return None
68
+
69
+
70
+ def _config_paths() -> list[Path]:
71
+ home = Path(os.path.expanduser("~"))
72
+ xdg = Path(os.environ.get("XDG_CONFIG_HOME", home / ".config"))
73
+ return [
74
+ xdg / "nullspace" / "config.json",
75
+ home / ".nullspace" / "config.json",
76
+ ]
77
+
78
+
79
+ def _read_config_value(path: Path, name: str) -> str | None:
80
+ if not path.is_file():
81
+ return None
82
+ try:
83
+ data = json.loads(path.read_text(encoding="utf-8"))
84
+ except (json.JSONDecodeError, OSError):
85
+ return None
86
+ value = data.get(name)
87
+ if not isinstance(value, str):
88
+ return None
89
+ value = value.strip()
90
+ return value or None