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 +354 -0
- nullspace/_cli_schema.py +230 -0
- nullspace/_config.py +90 -0
- nullspace/_exit_codes.py +108 -0
- nullspace/_fields.py +16 -0
- nullspace/_filesystem_core.py +4553 -0
- nullspace/_mcp_server.py +215 -0
- nullspace/cli.py +6339 -0
- nullspace/client.py +1184 -0
- nullspace/code_interpreter.py +583 -0
- nullspace/commands.py +393 -0
- nullspace/desktop.py +572 -0
- nullspace/errors.py +300 -0
- nullspace/files.py +568 -0
- nullspace/git.py +1034 -0
- nullspace/lifecycle.py +1048 -0
- nullspace/monitor.py +268 -0
- nullspace/process.py +37 -0
- nullspace/pty.py +483 -0
- nullspace/sandbox.py +1479 -0
- nullspace/snapshot.py +109 -0
- nullspace/template.py +3702 -0
- nullspace/types.py +1936 -0
- nullspace/volume.py +419 -0
- nullspace/volume_files.py +936 -0
- nullspace_sdk-0.1.0.dist-info/METADATA +416 -0
- nullspace_sdk-0.1.0.dist-info/RECORD +29 -0
- nullspace_sdk-0.1.0.dist-info/WHEEL +4 -0
- nullspace_sdk-0.1.0.dist-info/entry_points.txt +2 -0
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
|
+
]
|
nullspace/_cli_schema.py
ADDED
|
@@ -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
|