maxc-cli 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.
- maxc_cli/__init__.py +5 -0
- maxc_cli/__main__.py +6 -0
- maxc_cli/app.py +3406 -0
- maxc_cli/audit.py +18 -0
- maxc_cli/auth_providers.py +471 -0
- maxc_cli/backend/__init__.py +8 -0
- maxc_cli/backend/auth.py +144 -0
- maxc_cli/backend/data.py +87 -0
- maxc_cli/backend/job.py +304 -0
- maxc_cli/backend/meta.py +312 -0
- maxc_cli/backend/odps.py +130 -0
- maxc_cli/backend/query.py +148 -0
- maxc_cli/cache.py +662 -0
- maxc_cli/cli.py +1274 -0
- maxc_cli/config.py +406 -0
- maxc_cli/exceptions.py +99 -0
- maxc_cli/helpers.py +964 -0
- maxc_cli/models.py +533 -0
- maxc_cli/output.py +75 -0
- maxc_cli/store.py +123 -0
- maxc_cli/utils.py +136 -0
- maxc_cli-0.1.0.dist-info/METADATA +220 -0
- maxc_cli-0.1.0.dist-info/RECORD +26 -0
- maxc_cli-0.1.0.dist-info/WHEEL +5 -0
- maxc_cli-0.1.0.dist-info/entry_points.txt +2 -0
- maxc_cli-0.1.0.dist-info/top_level.txt +1 -0
maxc_cli/models.py
ADDED
|
@@ -0,0 +1,533 @@
|
|
|
1
|
+
"""Data models for MaxCompute CLI."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
import shlex
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class AgentHints:
|
|
11
|
+
next_actions: 'list[str]' = field(default_factory=list)
|
|
12
|
+
warnings: 'list[str]' = field(default_factory=list)
|
|
13
|
+
insights: 'list[str]' = field(default_factory=list)
|
|
14
|
+
|
|
15
|
+
def to_dict(self) -> 'dict[str, Any]':
|
|
16
|
+
payload: 'dict[str, Any]' = {}
|
|
17
|
+
if self.next_actions:
|
|
18
|
+
payload["next_actions"] = self.next_actions
|
|
19
|
+
if self.warnings:
|
|
20
|
+
payload["warnings"] = self.warnings
|
|
21
|
+
if self.insights:
|
|
22
|
+
payload["insights"] = self.insights
|
|
23
|
+
return payload
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class Envelope:
|
|
28
|
+
command: 'str'
|
|
29
|
+
status: 'str'
|
|
30
|
+
data: 'dict[str, Any]' = field(default_factory=dict)
|
|
31
|
+
metadata: 'dict[str, Any]' = field(default_factory=dict)
|
|
32
|
+
error: 'Any | None' = None
|
|
33
|
+
agent_hints: 'AgentHints | None' = None
|
|
34
|
+
version: 'str' = "2.0"
|
|
35
|
+
|
|
36
|
+
def to_dict(self, *, normalize: 'bool' = True) -> 'dict[str, Any]':
|
|
37
|
+
command = _format_command_path(self.command) if normalize else self.command
|
|
38
|
+
data = _normalize_data(self.command, self.data) if normalize else self.data
|
|
39
|
+
payload = {
|
|
40
|
+
"version": self.version,
|
|
41
|
+
"command": command,
|
|
42
|
+
"command_id": self.command,
|
|
43
|
+
"status": self.status,
|
|
44
|
+
"data": data,
|
|
45
|
+
"metadata": self.metadata,
|
|
46
|
+
}
|
|
47
|
+
payload["error"] = self.error.to_dict() if self.error else None
|
|
48
|
+
payload["agent_hints"] = _render_agent_hints(self)
|
|
49
|
+
return payload
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass
|
|
53
|
+
class QueryResult:
|
|
54
|
+
"""Result of a query execution."""
|
|
55
|
+
|
|
56
|
+
rows: 'list[dict[str, Any]]'
|
|
57
|
+
schema: 'list[dict[str, Any]]'
|
|
58
|
+
total_rows: 'int'
|
|
59
|
+
returned_rows: 'int'
|
|
60
|
+
has_more: 'bool'
|
|
61
|
+
next_cursor: 'str | None'
|
|
62
|
+
elapsed_ms: 'int'
|
|
63
|
+
bytes_scanned: 'int | None'
|
|
64
|
+
project: 'str'
|
|
65
|
+
sql_executed: 'str'
|
|
66
|
+
tables_used: 'list[str]'
|
|
67
|
+
warnings: 'list[str]' = field(default_factory=list)
|
|
68
|
+
job_id: 'str | None' = None
|
|
69
|
+
submitted_at: 'str | None' = None
|
|
70
|
+
completed_at: 'str | None' = None
|
|
71
|
+
extra_metadata: 'dict[str, Any]' = field(default_factory=dict)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@dataclass
|
|
75
|
+
class JobInfo:
|
|
76
|
+
"""Information about a job."""
|
|
77
|
+
|
|
78
|
+
job_id: 'str'
|
|
79
|
+
status: 'str'
|
|
80
|
+
project: 'str'
|
|
81
|
+
progress: 'int'
|
|
82
|
+
stage: 'str | None' = None
|
|
83
|
+
retryable: 'bool | None' = None
|
|
84
|
+
failure_reason: 'str | None' = None
|
|
85
|
+
task_summary: 'dict[str, Any]' = field(default_factory=dict)
|
|
86
|
+
sql: 'str | None' = None
|
|
87
|
+
submitted_at: 'str | None' = None
|
|
88
|
+
updated_at: 'str | None' = None
|
|
89
|
+
completed_at: 'str | None' = None
|
|
90
|
+
logview: 'str | None' = None
|
|
91
|
+
error_message: 'str | None' = None
|
|
92
|
+
warnings: 'list[str]' = field(default_factory=list)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _format_command_path(command: 'str') -> 'str':
|
|
96
|
+
if "." not in command:
|
|
97
|
+
return command
|
|
98
|
+
return command.replace(".", " ")
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _normalize_data(command: 'str', data: 'dict[str, Any]') -> 'dict[str, Any]':
|
|
102
|
+
if not isinstance(data, dict):
|
|
103
|
+
return {"value": data}
|
|
104
|
+
|
|
105
|
+
if _already_normalized(command, data):
|
|
106
|
+
return data
|
|
107
|
+
|
|
108
|
+
if command in {"query", "job.wait", "job.result"}:
|
|
109
|
+
if set(data) == {"job_id"}:
|
|
110
|
+
return {"job": {"job_id": data["job_id"]}}
|
|
111
|
+
return {
|
|
112
|
+
"result": {
|
|
113
|
+
"rows": data.get("rows", []),
|
|
114
|
+
"schema": data.get("schema", []),
|
|
115
|
+
"row_count": data.get("total_rows"),
|
|
116
|
+
"returned_rows": data.get("returned_rows"),
|
|
117
|
+
},
|
|
118
|
+
"pagination": {
|
|
119
|
+
"has_more": data.get("has_more", False),
|
|
120
|
+
"next_cursor": data.get("next_cursor"),
|
|
121
|
+
},
|
|
122
|
+
}
|
|
123
|
+
if command in {"query.cost", "query.explain"}:
|
|
124
|
+
return {"analysis": data}
|
|
125
|
+
if command == "auth.whoami":
|
|
126
|
+
options = data.get("auth_options")
|
|
127
|
+
identity = {key: value for key, value in data.items() if key != "auth_options"}
|
|
128
|
+
payload: 'dict[str, Any]' = {"identity": identity}
|
|
129
|
+
if options is not None:
|
|
130
|
+
payload["auth_options"] = options
|
|
131
|
+
return payload
|
|
132
|
+
if command == "auth.login":
|
|
133
|
+
identity = {
|
|
134
|
+
key: value
|
|
135
|
+
for key, value in data.items()
|
|
136
|
+
if key not in {"saved", "validated"}
|
|
137
|
+
}
|
|
138
|
+
return {
|
|
139
|
+
"identity": identity,
|
|
140
|
+
"persistence": {
|
|
141
|
+
"saved": data.get("saved"),
|
|
142
|
+
"validated": data.get("validated"),
|
|
143
|
+
},
|
|
144
|
+
}
|
|
145
|
+
if command == "auth.login-ncs":
|
|
146
|
+
if "raw_lines" in data or "raw_output" in data:
|
|
147
|
+
return {"accounts": data}
|
|
148
|
+
identity = {
|
|
149
|
+
key: value
|
|
150
|
+
for key, value in data.items()
|
|
151
|
+
if key not in {"saved", "validated"}
|
|
152
|
+
}
|
|
153
|
+
return {
|
|
154
|
+
"identity": identity,
|
|
155
|
+
"persistence": {
|
|
156
|
+
"saved": data.get("saved"),
|
|
157
|
+
"validated": data.get("validated"),
|
|
158
|
+
},
|
|
159
|
+
}
|
|
160
|
+
if command == "auth.can-i":
|
|
161
|
+
return {"authorization": data}
|
|
162
|
+
if command == "meta.list-tables":
|
|
163
|
+
return {
|
|
164
|
+
"tables": data.get("tables", []),
|
|
165
|
+
"pagination": {
|
|
166
|
+
"total": data.get("total"),
|
|
167
|
+
"has_more": False,
|
|
168
|
+
},
|
|
169
|
+
}
|
|
170
|
+
if command in {"meta.list-projects", "meta.list-schemas"}:
|
|
171
|
+
collection_key = "projects" if command == "meta.list-projects" else "schemas"
|
|
172
|
+
return {
|
|
173
|
+
collection_key: data.get(collection_key, []),
|
|
174
|
+
"pagination": {
|
|
175
|
+
"total": data.get("total"),
|
|
176
|
+
"has_more": False,
|
|
177
|
+
},
|
|
178
|
+
}
|
|
179
|
+
if command in {"meta.search", "meta.search-columns"}:
|
|
180
|
+
return {
|
|
181
|
+
"search": {
|
|
182
|
+
"keyword": data.get("keyword"),
|
|
183
|
+
"matches": data.get("matches", []),
|
|
184
|
+
},
|
|
185
|
+
"pagination": {
|
|
186
|
+
"total": data.get("total"),
|
|
187
|
+
"has_more": False,
|
|
188
|
+
},
|
|
189
|
+
}
|
|
190
|
+
if command == "meta.describe":
|
|
191
|
+
return {"table": data}
|
|
192
|
+
if command == "meta.partitions":
|
|
193
|
+
return {
|
|
194
|
+
"table": {"table_name": data.get("table_name")},
|
|
195
|
+
"partitions": data.get("partitions", []),
|
|
196
|
+
}
|
|
197
|
+
if command in {"meta.latest-partition", "meta.freshness", "meta.lineage"}:
|
|
198
|
+
key = {
|
|
199
|
+
"meta.latest-partition": "partition",
|
|
200
|
+
"meta.freshness": "freshness",
|
|
201
|
+
"meta.lineage": "lineage",
|
|
202
|
+
}[command]
|
|
203
|
+
return {key: data}
|
|
204
|
+
if command == "data.sample":
|
|
205
|
+
return {
|
|
206
|
+
"sample": {
|
|
207
|
+
"table_name": data.get("table_name"),
|
|
208
|
+
"applied_partition": data.get("applied_partition"),
|
|
209
|
+
"selected_columns": data.get("selected_columns"),
|
|
210
|
+
"rows": data.get("rows", []),
|
|
211
|
+
"schema": data.get("schema", []),
|
|
212
|
+
"returned_rows": data.get("returned_rows"),
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
if command == "data.profile":
|
|
216
|
+
return {"profile": data}
|
|
217
|
+
if command.startswith("project."):
|
|
218
|
+
return {"project": data}
|
|
219
|
+
if command.startswith("diff."):
|
|
220
|
+
return {"diff": data}
|
|
221
|
+
if command == "job.list":
|
|
222
|
+
return {
|
|
223
|
+
"jobs": data.get("jobs", []),
|
|
224
|
+
"pagination": {
|
|
225
|
+
"total": data.get("total"),
|
|
226
|
+
"has_more": False,
|
|
227
|
+
},
|
|
228
|
+
}
|
|
229
|
+
if command in {"job.status", "job.cancel"}:
|
|
230
|
+
return {"job": data}
|
|
231
|
+
if command == "job.diagnose":
|
|
232
|
+
return {"diagnosis": data}
|
|
233
|
+
if command in {"cache.get-semantic", "cache.save-semantic"}:
|
|
234
|
+
return {"semantic": data}
|
|
235
|
+
if command == "agent.context":
|
|
236
|
+
return {"context": data}
|
|
237
|
+
|
|
238
|
+
return data
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def _already_normalized(command: 'str', data: 'dict[str, Any]') -> 'bool':
|
|
242
|
+
if command in {"query", "job.wait", "job.result"}:
|
|
243
|
+
return _has_mapping(data, "job") or _has_mapping(data, "result", "pagination")
|
|
244
|
+
if command in {"query.cost", "query.explain"}:
|
|
245
|
+
return _has_mapping(data, "analysis")
|
|
246
|
+
if command == "auth.whoami":
|
|
247
|
+
return _has_mapping(data, "identity")
|
|
248
|
+
if command == "auth.login":
|
|
249
|
+
return _has_mapping(data, "identity", "persistence")
|
|
250
|
+
if command == "auth.login-ncs":
|
|
251
|
+
return _has_mapping(data, "identity", "persistence") or _has_mapping(data, "accounts")
|
|
252
|
+
if command == "auth.can-i":
|
|
253
|
+
return _has_mapping(data, "authorization")
|
|
254
|
+
if command == "meta.list-tables":
|
|
255
|
+
return _has_sequence(data, "tables") and _has_mapping(data, "pagination")
|
|
256
|
+
if command == "meta.list-projects":
|
|
257
|
+
return _has_sequence(data, "projects") and _has_mapping(data, "pagination")
|
|
258
|
+
if command == "meta.list-schemas":
|
|
259
|
+
return _has_sequence(data, "schemas") and _has_mapping(data, "pagination")
|
|
260
|
+
if command in {"meta.search", "meta.search-columns"}:
|
|
261
|
+
return _has_mapping(data, "search", "pagination")
|
|
262
|
+
if command == "meta.describe":
|
|
263
|
+
return _has_mapping(data, "table")
|
|
264
|
+
if command == "meta.partitions":
|
|
265
|
+
return _has_mapping(data, "table") and _has_sequence(data, "partitions")
|
|
266
|
+
if command == "meta.latest-partition":
|
|
267
|
+
return _has_mapping(data, "partition")
|
|
268
|
+
if command == "meta.freshness":
|
|
269
|
+
return _has_mapping(data, "freshness")
|
|
270
|
+
if command == "meta.lineage":
|
|
271
|
+
return _has_mapping(data, "lineage")
|
|
272
|
+
if command == "data.sample":
|
|
273
|
+
return _has_mapping(data, "sample")
|
|
274
|
+
if command == "data.profile":
|
|
275
|
+
return _has_mapping(data, "profile")
|
|
276
|
+
if command == "job.list":
|
|
277
|
+
return _has_sequence(data, "jobs") and _has_mapping(data, "pagination")
|
|
278
|
+
if command in {"job.status", "job.cancel"}:
|
|
279
|
+
return _has_mapping(data, "job")
|
|
280
|
+
if command == "job.diagnose":
|
|
281
|
+
return _has_mapping(data, "diagnosis")
|
|
282
|
+
if command in {"cache.get-semantic", "cache.save-semantic"}:
|
|
283
|
+
return _has_mapping(data, "semantic")
|
|
284
|
+
if command == "agent.context":
|
|
285
|
+
return _has_mapping(data, "context")
|
|
286
|
+
return False
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def _has_mapping(data: 'dict[str, Any]', *keys: 'str') -> 'bool':
|
|
290
|
+
return all(isinstance(data.get(key), dict) for key in keys)
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def _has_sequence(data: 'dict[str, Any]', key: 'str') -> 'bool':
|
|
294
|
+
return isinstance(data.get(key), list)
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def _render_agent_hints(envelope: 'Envelope') -> 'dict[str, Any] | None':
|
|
298
|
+
if envelope.agent_hints is None:
|
|
299
|
+
return None
|
|
300
|
+
|
|
301
|
+
payload = envelope.agent_hints.to_dict()
|
|
302
|
+
if envelope.agent_hints.next_actions:
|
|
303
|
+
payload["action_ids"] = list(envelope.agent_hints.next_actions)
|
|
304
|
+
payload["next_actions"] = [
|
|
305
|
+
_format_next_action(
|
|
306
|
+
action,
|
|
307
|
+
data=envelope.data,
|
|
308
|
+
metadata=envelope.metadata,
|
|
309
|
+
)
|
|
310
|
+
for action in envelope.agent_hints.next_actions
|
|
311
|
+
]
|
|
312
|
+
return payload
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def _format_next_action(
|
|
316
|
+
action: 'str',
|
|
317
|
+
*,
|
|
318
|
+
data: 'dict[str, Any]',
|
|
319
|
+
metadata: 'dict[str, Any]',
|
|
320
|
+
) -> 'str':
|
|
321
|
+
if " " in action:
|
|
322
|
+
return action
|
|
323
|
+
|
|
324
|
+
sql = _suggested_sql(data, metadata)
|
|
325
|
+
next_cursor = _string_value(data.get("next_cursor"))
|
|
326
|
+
job_id = _string_value(data.get("job_id")) or _string_value(metadata.get("job_id"))
|
|
327
|
+
build_id = _string_value(data.get("build_id")) or _string_value(metadata.get("build_id"))
|
|
328
|
+
table_name = _string_value(data.get("table_name")) or _single_list_value(metadata.get("tables_used"))
|
|
329
|
+
keyword = _string_value(data.get("keyword"))
|
|
330
|
+
operation = _string_value(data.get("operation")) or "SELECT"
|
|
331
|
+
project = _string_value(metadata.get("project")) or _string_value(data.get("project"))
|
|
332
|
+
project_name = _string_value(data.get("name")) or project
|
|
333
|
+
schema_name = _string_value(data.get("schema_name"))
|
|
334
|
+
left_table = _string_value(data.get("left_table"))
|
|
335
|
+
right_table = _string_value(data.get("right_table"))
|
|
336
|
+
|
|
337
|
+
if action in {"query.paginate", "query.next_page"}:
|
|
338
|
+
return _cli_command(
|
|
339
|
+
"query",
|
|
340
|
+
_shell_arg(sql, "<sql>"),
|
|
341
|
+
"--cursor",
|
|
342
|
+
_shell_arg(next_cursor, "<next_cursor>"),
|
|
343
|
+
"--json",
|
|
344
|
+
)
|
|
345
|
+
if action in {"query", "query.cost", "query.explain"}:
|
|
346
|
+
parts = ["query"]
|
|
347
|
+
if action != "query":
|
|
348
|
+
parts.append(action.split(".", 1)[1])
|
|
349
|
+
parts.extend([_shell_arg(sql, "<sql>"), "--json"])
|
|
350
|
+
return _cli_command(*parts)
|
|
351
|
+
if action == "job.submit":
|
|
352
|
+
return _cli_command("job", "submit", _shell_arg(sql, "<sql>"), "--json")
|
|
353
|
+
if action in {"job.status", "job.wait", "job.result", "job.cancel", "job.diagnose"}:
|
|
354
|
+
return _cli_command(
|
|
355
|
+
"job",
|
|
356
|
+
action.split(".", 1)[1],
|
|
357
|
+
_shell_arg(job_id, "<job_id>"),
|
|
358
|
+
"--json",
|
|
359
|
+
)
|
|
360
|
+
if action == "job.list":
|
|
361
|
+
return _cli_command("job", "list", "--json")
|
|
362
|
+
if action == "auth.can-i":
|
|
363
|
+
parts = [
|
|
364
|
+
"auth",
|
|
365
|
+
"can-i",
|
|
366
|
+
"--table",
|
|
367
|
+
_shell_arg(table_name, "<table_name>"),
|
|
368
|
+
"--operation",
|
|
369
|
+
_shell_arg(operation, "SELECT"),
|
|
370
|
+
]
|
|
371
|
+
if project:
|
|
372
|
+
parts.extend(["--project", _shell_arg(project, "<project>")])
|
|
373
|
+
parts.append("--json")
|
|
374
|
+
return _cli_command(*parts)
|
|
375
|
+
if action == "auth.whoami":
|
|
376
|
+
return _cli_command("auth", "whoami", "--json")
|
|
377
|
+
if action == "meta.list-tables":
|
|
378
|
+
return _cli_command("meta", "list-tables", "--json")
|
|
379
|
+
if action in {
|
|
380
|
+
"meta.describe",
|
|
381
|
+
"meta.partitions",
|
|
382
|
+
"meta.latest-partition",
|
|
383
|
+
"meta.freshness",
|
|
384
|
+
"meta.lineage",
|
|
385
|
+
"data.sample",
|
|
386
|
+
"data.profile",
|
|
387
|
+
}:
|
|
388
|
+
return _cli_command(
|
|
389
|
+
action.split(".", 1)[0],
|
|
390
|
+
action.split(".", 1)[1],
|
|
391
|
+
_shell_arg(table_name, "<table_name>"),
|
|
392
|
+
"--json",
|
|
393
|
+
)
|
|
394
|
+
if action in {"meta.search", "meta.search-columns"}:
|
|
395
|
+
return _cli_command(
|
|
396
|
+
action.split(".", 1)[0],
|
|
397
|
+
action.split(".", 1)[1],
|
|
398
|
+
_shell_arg(keyword, "<keyword>"),
|
|
399
|
+
"--json",
|
|
400
|
+
)
|
|
401
|
+
if action == "meta.list-projects":
|
|
402
|
+
return _cli_command("meta", "list-projects", "--json")
|
|
403
|
+
if action == "meta.list-schemas":
|
|
404
|
+
parts = ["meta", "list-schemas"]
|
|
405
|
+
if project:
|
|
406
|
+
parts.extend(["--project", _shell_arg(project, "<project>")])
|
|
407
|
+
parts.append("--json")
|
|
408
|
+
return _cli_command(*parts)
|
|
409
|
+
if action == "project.use":
|
|
410
|
+
parts = ["project", "use", _shell_arg(project_name, "<project_name>")]
|
|
411
|
+
if schema_name:
|
|
412
|
+
parts.extend(["--schema", _shell_arg(schema_name, "<schema_name>")])
|
|
413
|
+
parts.append("--json")
|
|
414
|
+
return _cli_command(*parts)
|
|
415
|
+
if action == "project.info":
|
|
416
|
+
parts = ["project", "info"]
|
|
417
|
+
if project_name:
|
|
418
|
+
parts.append(_shell_arg(project_name, "<project_name>"))
|
|
419
|
+
parts.append("--json")
|
|
420
|
+
return _cli_command(*parts)
|
|
421
|
+
if action in {"diff.schema", "diff.partition"}:
|
|
422
|
+
return _cli_command(
|
|
423
|
+
action.split(".", 1)[0],
|
|
424
|
+
action.split(".", 1)[1],
|
|
425
|
+
_shell_arg(left_table, "<left_table>"),
|
|
426
|
+
_shell_arg(right_table, "<right_table>"),
|
|
427
|
+
"--json",
|
|
428
|
+
)
|
|
429
|
+
if action == "diff.data":
|
|
430
|
+
return _cli_command(
|
|
431
|
+
"diff",
|
|
432
|
+
"data",
|
|
433
|
+
_shell_arg(left_table, "<left_table>"),
|
|
434
|
+
_shell_arg(right_table, "<right_table>"),
|
|
435
|
+
"--keys",
|
|
436
|
+
"<key_columns>",
|
|
437
|
+
"--json",
|
|
438
|
+
)
|
|
439
|
+
if action == "cache.build":
|
|
440
|
+
parts = ["cache", "build"]
|
|
441
|
+
if project:
|
|
442
|
+
parts.extend(["--project", _shell_arg(project, "<project>")])
|
|
443
|
+
parts.append("--json")
|
|
444
|
+
return _cli_command(*parts)
|
|
445
|
+
if action == "cache.build-status":
|
|
446
|
+
parts = ["cache", "build-status"]
|
|
447
|
+
if build_id:
|
|
448
|
+
parts.extend(["--build-id", _shell_arg(build_id, "<build_id>")])
|
|
449
|
+
if project:
|
|
450
|
+
parts.extend(["--project", _shell_arg(project, "<project>")])
|
|
451
|
+
parts.append("--json")
|
|
452
|
+
return _cli_command(*parts)
|
|
453
|
+
if action == "cache.status":
|
|
454
|
+
parts = ["cache", "status"]
|
|
455
|
+
if project:
|
|
456
|
+
parts.extend(["--project", _shell_arg(project, "<project>")])
|
|
457
|
+
parts.append("--json")
|
|
458
|
+
return _cli_command(*parts)
|
|
459
|
+
if action == "cache.clear":
|
|
460
|
+
parts = ["cache", "clear"]
|
|
461
|
+
if project:
|
|
462
|
+
parts.extend(["--project", _shell_arg(project, "<project>")])
|
|
463
|
+
parts.append("--json")
|
|
464
|
+
return _cli_command(*parts)
|
|
465
|
+
if action == "cache.get-semantic":
|
|
466
|
+
parts = [
|
|
467
|
+
"cache",
|
|
468
|
+
"get-semantic",
|
|
469
|
+
"--table",
|
|
470
|
+
_shell_arg(table_name, "<table_name>"),
|
|
471
|
+
]
|
|
472
|
+
if schema_name:
|
|
473
|
+
parts.extend(["--schema", _shell_arg(schema_name, "<schema_name>")])
|
|
474
|
+
if project:
|
|
475
|
+
parts.extend(["--project", _shell_arg(project, "<project>")])
|
|
476
|
+
parts.append("--json")
|
|
477
|
+
return _cli_command(*parts)
|
|
478
|
+
if action == "cache.save-semantic":
|
|
479
|
+
parts = [
|
|
480
|
+
"cache",
|
|
481
|
+
"save-semantic",
|
|
482
|
+
"--table",
|
|
483
|
+
_shell_arg(table_name, "<table_name>"),
|
|
484
|
+
"--semantic-desc",
|
|
485
|
+
"<semantic_desc>",
|
|
486
|
+
]
|
|
487
|
+
if schema_name:
|
|
488
|
+
parts.extend(["--schema", _shell_arg(schema_name, "<schema_name>")])
|
|
489
|
+
if project:
|
|
490
|
+
parts.extend(["--project", _shell_arg(project, "<project>")])
|
|
491
|
+
parts.append("--json")
|
|
492
|
+
return _cli_command(*parts)
|
|
493
|
+
if action == "agent.context":
|
|
494
|
+
return _cli_command("agent", "context", "--json")
|
|
495
|
+
|
|
496
|
+
if "." not in action:
|
|
497
|
+
return _cli_command(action, "--json")
|
|
498
|
+
group, subcommand = action.split(".", 1)
|
|
499
|
+
return _cli_command(group, subcommand, "--json")
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
def _suggested_sql(data: 'dict[str, Any]', metadata: 'dict[str, Any]') -> 'str | None':
|
|
503
|
+
sql = _string_value(metadata.get("sql_executed"))
|
|
504
|
+
if sql:
|
|
505
|
+
return sql
|
|
506
|
+
|
|
507
|
+
table_name = _string_value(data.get("table_name"))
|
|
508
|
+
if table_name:
|
|
509
|
+
return f"SELECT * FROM {table_name} LIMIT 20"
|
|
510
|
+
return None
|
|
511
|
+
|
|
512
|
+
|
|
513
|
+
def _string_value(value: 'Any') -> 'str | None':
|
|
514
|
+
if value is None:
|
|
515
|
+
return None
|
|
516
|
+
text = str(value).strip()
|
|
517
|
+
return text or None
|
|
518
|
+
|
|
519
|
+
|
|
520
|
+
def _single_list_value(value: 'Any') -> 'str | None':
|
|
521
|
+
if not isinstance(value, list) or len(value) != 1:
|
|
522
|
+
return None
|
|
523
|
+
return _string_value(value[0])
|
|
524
|
+
|
|
525
|
+
|
|
526
|
+
def _shell_arg(value: 'str | None', placeholder: 'str') -> 'str':
|
|
527
|
+
if value is None:
|
|
528
|
+
return placeholder
|
|
529
|
+
return shlex.quote(value)
|
|
530
|
+
|
|
531
|
+
|
|
532
|
+
def _cli_command(*parts: 'str') -> 'str':
|
|
533
|
+
return " ".join(part for part in parts if part)
|
maxc_cli/output.py
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
|
|
2
|
+
import json
|
|
3
|
+
from typing import Any, TextIO
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def emit_json(payload: 'dict[str, Any]', stdout: 'TextIO') -> 'None':
|
|
7
|
+
stdout.write(json.dumps(payload, ensure_ascii=False, indent=2) + "\n")
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def emit_ndjson(events: 'list[dict[str, Any]]', stdout: 'TextIO') -> 'None':
|
|
11
|
+
for event in events:
|
|
12
|
+
stdout.write(json.dumps(event, ensure_ascii=False) + "\n")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def render_table(rows: 'list[dict[str, Any]]') -> 'str':
|
|
16
|
+
if not rows:
|
|
17
|
+
return "(no rows)"
|
|
18
|
+
|
|
19
|
+
columns: 'list[str]' = []
|
|
20
|
+
for row in rows:
|
|
21
|
+
for key in row:
|
|
22
|
+
if key not in columns:
|
|
23
|
+
columns.append(key)
|
|
24
|
+
|
|
25
|
+
widths = {
|
|
26
|
+
column: max(
|
|
27
|
+
len(str(column)),
|
|
28
|
+
max(len(_stringify(row.get(column, ""))) for row in rows),
|
|
29
|
+
)
|
|
30
|
+
for column in columns
|
|
31
|
+
}
|
|
32
|
+
header = "| " + " | ".join(str(column).ljust(widths[column]) for column in columns) + " |"
|
|
33
|
+
separator = "|" + "|".join("-" * (widths[column] + 2) for column in columns) + "|"
|
|
34
|
+
lines = [header, separator]
|
|
35
|
+
for row in rows:
|
|
36
|
+
line = "| " + " | ".join(
|
|
37
|
+
_escape_md_cell(_stringify(row.get(column, ""))).ljust(widths[column]) for column in columns
|
|
38
|
+
) + " |"
|
|
39
|
+
lines.append(line)
|
|
40
|
+
return "\n".join(lines)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def render_key_values(mapping: 'dict[str, Any]') -> 'str':
|
|
44
|
+
if not mapping:
|
|
45
|
+
return ""
|
|
46
|
+
key_width = max(max(len(str(k)) for k in mapping), 3)
|
|
47
|
+
val_width = max(max(len(_stringify(v)) for v in mapping.values()), 5)
|
|
48
|
+
header = f"| {'Key'.ljust(key_width)} | {'Value'.ljust(val_width)} |"
|
|
49
|
+
separator = f"|{'-' * (key_width + 2)}|{'-' * (val_width + 2)}|"
|
|
50
|
+
lines = [header, separator]
|
|
51
|
+
for key, value in mapping.items():
|
|
52
|
+
lines.append(
|
|
53
|
+
f"| {str(key).ljust(key_width)} | {_escape_md_cell(_stringify(value)).ljust(val_width)} |"
|
|
54
|
+
)
|
|
55
|
+
return "\n".join(lines)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def render_error(code: 'str', message: 'str', suggestion: 'str | None' = None) -> 'str':
|
|
59
|
+
parts = [f"**Error** [`{code}`]: {message}"]
|
|
60
|
+
if suggestion:
|
|
61
|
+
parts.append("")
|
|
62
|
+
parts.append(f"> **Suggestion**: {suggestion}")
|
|
63
|
+
return "\n".join(parts)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _escape_md_cell(text: 'str') -> 'str':
|
|
67
|
+
return text.replace("|", "\\|")
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _stringify(value: 'Any') -> 'str':
|
|
71
|
+
if isinstance(value, float):
|
|
72
|
+
return f"{value:.2f}"
|
|
73
|
+
if isinstance(value, (list, dict)):
|
|
74
|
+
return json.dumps(value, ensure_ascii=False)
|
|
75
|
+
return str(value)
|