seizu-cli 2.0.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,506 @@
1
+ """CLI commands for managing toolsets and tools."""
2
+
3
+ import json
4
+ import sys
5
+ from typing import Any
6
+
7
+ import typer
8
+ from rich.console import Console
9
+ from rich.table import Table
10
+
11
+ from seizu_cli import state
12
+ from seizu_cli.client import APIError
13
+
14
+ app = typer.Typer(help="Manage toolsets and tools.", no_args_is_help=True)
15
+ tools_app = typer.Typer(help="Manage tools within a toolset.", no_args_is_help=True)
16
+ app.add_typer(tools_app, name="tools")
17
+
18
+ console = Console()
19
+ err_console = Console(stderr=True)
20
+
21
+
22
+ def _die(exc: Exception) -> None:
23
+ if isinstance(exc, APIError):
24
+ err_console.print(f"[red]Error {exc.status_code}[/red]: {exc}")
25
+ else:
26
+ err_console.print(f"[red]Error[/red]: {exc}")
27
+ sys.exit(1)
28
+
29
+
30
+ def _print_toolset_detail(data: dict[str, Any], as_json: bool) -> None:
31
+ if as_json:
32
+ console.print_json(json.dumps(data))
33
+ return
34
+ console.print(f"[bold]ID[/bold]: {data['toolset_id']}")
35
+ console.print(f"[bold]Name[/bold]: {data['name']}")
36
+ console.print(f"[bold]Description[/bold]: {data.get('description', '')}")
37
+ console.print(f"[bold]Enabled[/bold]: {data.get('enabled', True)}")
38
+ console.print(f"[bold]Version[/bold]: {data.get('current_version', data.get('version'))}")
39
+ console.print(f"[bold]Created By[/bold]: {data['created_by']}")
40
+ console.print(f"[bold]Updated By[/bold]: {data.get('updated_by', '')}")
41
+
42
+
43
+ def _print_tool_detail(data: dict[str, Any], as_json: bool) -> None:
44
+ if as_json:
45
+ console.print_json(json.dumps(data))
46
+ return
47
+ console.print(f"[bold]ID[/bold]: {data['tool_id']}")
48
+ console.print(f"[bold]Toolset ID[/bold]: {data['toolset_id']}")
49
+ console.print(f"[bold]Name[/bold]: {data['name']}")
50
+ console.print(f"[bold]Description[/bold]: {data.get('description', '')}")
51
+ console.print(f"[bold]Enabled[/bold]: {data.get('enabled', True)}")
52
+ console.print(f"[bold]Version[/bold]: {data.get('current_version', data.get('version'))}")
53
+ console.print(f"[bold]Created By[/bold]: {data['created_by']}")
54
+ console.print(f"[bold]Updated By[/bold]: {data.get('updated_by', '')}")
55
+ if data.get("parameters"):
56
+ console.print(f"[bold]Parameters[/bold]: {json.dumps(data['parameters'])}")
57
+ console.print(f"[bold]Cypher[/bold]\n{data['cypher']}")
58
+
59
+
60
+ # ---------------------------------------------------------------------------
61
+ # Toolset commands
62
+ # ---------------------------------------------------------------------------
63
+
64
+
65
+ @app.command("list")
66
+ def list_toolsets(
67
+ output: str = typer.Option("table", "--output", "-o", help="Output format: table or json."),
68
+ ) -> None:
69
+ """List all toolsets."""
70
+ try:
71
+ data = state.get_client().get("/api/v1/toolsets")
72
+ except Exception as exc:
73
+ _die(exc)
74
+ return
75
+
76
+ if output == "json":
77
+ console.print_json(json.dumps(data))
78
+ return
79
+
80
+ items = data.get("toolsets", [])
81
+ if not items:
82
+ console.print("[dim]No toolsets found.[/dim]")
83
+ return
84
+
85
+ table = Table(show_header=True, header_style="bold")
86
+ table.add_column("ID")
87
+ table.add_column("Name")
88
+ table.add_column("Description")
89
+ table.add_column("Enabled")
90
+ table.add_column("Version", justify="right")
91
+ table.add_column("Updated At")
92
+
93
+ for ts in items:
94
+ table.add_row(
95
+ ts["toolset_id"],
96
+ ts["name"],
97
+ ts.get("description", ""),
98
+ "yes" if ts.get("enabled", True) else "no",
99
+ str(ts.get("current_version", "")),
100
+ ts.get("updated_at", ""),
101
+ )
102
+
103
+ console.print(table)
104
+
105
+
106
+ @app.command("get")
107
+ def get_toolset(
108
+ toolset_id: str = typer.Argument(help="Toolset ID."),
109
+ output: str = typer.Option("table", "--output", "-o", help="Output format: table or json."),
110
+ ) -> None:
111
+ """Get a toolset by ID."""
112
+ try:
113
+ data = state.get_client().get(f"/api/v1/toolsets/{toolset_id}")
114
+ except Exception as exc:
115
+ _die(exc)
116
+ return
117
+ _print_toolset_detail(data, as_json=(output == "json"))
118
+
119
+
120
+ @app.command("create")
121
+ def create_toolset(
122
+ toolset_id: str = typer.Argument(help="Immutable lower_snake_case toolset ID."),
123
+ name: str = typer.Argument(help="Toolset name."),
124
+ description: str = typer.Option("", "--description", "-d", help="Description."),
125
+ disabled: bool = typer.Option(False, "--disabled", help="Create as disabled."),
126
+ ) -> None:
127
+ """Create a new toolset."""
128
+ try:
129
+ data = state.get_client().post(
130
+ "/api/v1/toolsets",
131
+ json={"toolset_id": toolset_id, "name": name, "description": description, "enabled": not disabled},
132
+ )
133
+ except Exception as exc:
134
+ _die(exc)
135
+ return
136
+ console.print(f"[green]Created[/green]: {data['toolset_id']} name={data['name']!r}")
137
+
138
+
139
+ @app.command("update")
140
+ def update_toolset(
141
+ toolset_id: str = typer.Argument(help="Toolset ID."),
142
+ name: str = typer.Option(..., "--name", "-n", help="New name."),
143
+ description: str = typer.Option("", "--description", "-d", help="Description."),
144
+ enabled: bool = typer.Option(True, "--enabled/--disabled", help="Enable or disable."),
145
+ comment: str | None = typer.Option(None, "--comment", "-c", help="Version comment."),
146
+ ) -> None:
147
+ """Update a toolset (creates a new version)."""
148
+ try:
149
+ data = state.get_client().put(
150
+ f"/api/v1/toolsets/{toolset_id}",
151
+ json={
152
+ "name": name,
153
+ "description": description,
154
+ "enabled": enabled,
155
+ "comment": comment,
156
+ },
157
+ )
158
+ except Exception as exc:
159
+ _die(exc)
160
+ return
161
+ _print_toolset_detail(data, as_json=False)
162
+
163
+
164
+ @app.command("delete")
165
+ def delete_toolset(
166
+ toolset_id: str = typer.Argument(help="Toolset ID."),
167
+ yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation prompt."),
168
+ ) -> None:
169
+ """Delete a toolset and all its tools."""
170
+ if not yes:
171
+ typer.confirm(f"Delete toolset {toolset_id!r} and all its tools?", abort=True)
172
+ try:
173
+ state.get_client().delete(f"/api/v1/toolsets/{toolset_id}")
174
+ except Exception as exc:
175
+ _die(exc)
176
+ return
177
+ console.print(f"[red]Deleted[/red]: {toolset_id}")
178
+
179
+
180
+ @app.command("versions")
181
+ def list_toolset_versions(
182
+ toolset_id: str = typer.Argument(help="Toolset ID."),
183
+ output: str = typer.Option("table", "--output", "-o", help="Output format: table or json."),
184
+ ) -> None:
185
+ """List all versions of a toolset."""
186
+ try:
187
+ data = state.get_client().get(f"/api/v1/toolsets/{toolset_id}/versions")
188
+ except Exception as exc:
189
+ _die(exc)
190
+ return
191
+
192
+ if output == "json":
193
+ console.print_json(json.dumps(data))
194
+ return
195
+
196
+ versions = data.get("versions", [])
197
+ table = Table(show_header=True, header_style="bold")
198
+ table.add_column("Version", justify="right")
199
+ table.add_column("Created By")
200
+ table.add_column("Created At")
201
+ table.add_column("Comment")
202
+
203
+ for v in versions:
204
+ table.add_row(
205
+ str(v["version"]),
206
+ v["created_by"],
207
+ v["created_at"],
208
+ v.get("comment") or "",
209
+ )
210
+ console.print(table)
211
+
212
+
213
+ @app.command("version-get")
214
+ def get_toolset_version(
215
+ toolset_id: str = typer.Argument(help="Toolset ID."),
216
+ version: int = typer.Argument(help="Version number."),
217
+ output: str = typer.Option("table", "--output", "-o", help="Output format: table or json."),
218
+ ) -> None:
219
+ """Get a specific version of a toolset."""
220
+ try:
221
+ data = state.get_client().get(f"/api/v1/toolsets/{toolset_id}/versions/{version}")
222
+ except Exception as exc:
223
+ _die(exc)
224
+ return
225
+ _print_toolset_detail(data, as_json=(output == "json"))
226
+
227
+
228
+ # ---------------------------------------------------------------------------
229
+ # Tool commands (nested under toolsets tools)
230
+ # ---------------------------------------------------------------------------
231
+
232
+
233
+ @tools_app.command("list")
234
+ def list_tools(
235
+ toolset_id: str = typer.Argument(help="Toolset ID."),
236
+ output: str = typer.Option("table", "--output", "-o", help="Output format: table or json."),
237
+ ) -> None:
238
+ """List all tools in a toolset."""
239
+ try:
240
+ data = state.get_client().get(f"/api/v1/toolsets/{toolset_id}/tools")
241
+ except Exception as exc:
242
+ _die(exc)
243
+ return
244
+
245
+ if output == "json":
246
+ console.print_json(json.dumps(data))
247
+ return
248
+
249
+ items = data.get("tools", [])
250
+ if not items:
251
+ console.print("[dim]No tools found.[/dim]")
252
+ return
253
+
254
+ table = Table(show_header=True, header_style="bold")
255
+ table.add_column("ID")
256
+ table.add_column("Name")
257
+ table.add_column("Description")
258
+ table.add_column("Enabled")
259
+ table.add_column("Version", justify="right")
260
+ table.add_column("Updated At")
261
+
262
+ for t in items:
263
+ table.add_row(
264
+ t["tool_id"],
265
+ t["name"],
266
+ t.get("description", ""),
267
+ "yes" if t.get("enabled", True) else "no",
268
+ str(t.get("current_version", "")),
269
+ t.get("updated_at", ""),
270
+ )
271
+
272
+ console.print(table)
273
+
274
+
275
+ @tools_app.command("get")
276
+ def get_tool(
277
+ toolset_id: str = typer.Argument(help="Toolset ID."),
278
+ tool_id: str = typer.Argument(help="Tool ID."),
279
+ output: str = typer.Option("table", "--output", "-o", help="Output format: table or json."),
280
+ ) -> None:
281
+ """Get a tool by ID."""
282
+ try:
283
+ data = state.get_client().get(f"/api/v1/toolsets/{toolset_id}/tools/{tool_id}")
284
+ except Exception as exc:
285
+ _die(exc)
286
+ return
287
+ _print_tool_detail(data, as_json=(output == "json"))
288
+
289
+
290
+ @tools_app.command("create")
291
+ def create_tool(
292
+ toolset_id: str = typer.Argument(help="Toolset ID."),
293
+ tool_id: str = typer.Argument(help="Immutable lower_snake_case tool ID."),
294
+ name: str = typer.Option(..., "--name", "-n", help="Tool name."),
295
+ cypher: str = typer.Option(..., "--cypher", help="Cypher query."),
296
+ description: str = typer.Option("", "--description", "-d", help="Description."),
297
+ parameters: str | None = typer.Option(
298
+ None,
299
+ "--parameters",
300
+ help="Parameters as a JSON array of ToolParamDef objects.",
301
+ ),
302
+ disabled: bool = typer.Option(False, "--disabled", help="Create as disabled."),
303
+ ) -> None:
304
+ """Create a new tool within a toolset."""
305
+ params: list[dict[str, Any]] = []
306
+ if parameters:
307
+ try:
308
+ params = json.loads(parameters)
309
+ except json.JSONDecodeError as exc:
310
+ err_console.print(f"[red]Error[/red]: --parameters is not valid JSON: {exc}")
311
+ sys.exit(1)
312
+ try:
313
+ data = state.get_client().post(
314
+ f"/api/v1/toolsets/{toolset_id}/tools",
315
+ json={
316
+ "tool_id": tool_id,
317
+ "name": name,
318
+ "description": description,
319
+ "cypher": cypher,
320
+ "parameters": params,
321
+ "enabled": not disabled,
322
+ },
323
+ )
324
+ except Exception as exc:
325
+ _die(exc)
326
+ return
327
+ console.print(f"[green]Created[/green]: {data['tool_id']} name={data['name']!r}")
328
+
329
+
330
+ @tools_app.command("update")
331
+ def update_tool(
332
+ toolset_id: str = typer.Argument(help="Toolset ID."),
333
+ tool_id: str = typer.Argument(help="Tool ID."),
334
+ name: str = typer.Option(..., "--name", "-n", help="New name."),
335
+ cypher: str = typer.Option(..., "--cypher", help="Cypher query."),
336
+ description: str = typer.Option("", "--description", "-d", help="Description."),
337
+ parameters: str | None = typer.Option(
338
+ None,
339
+ "--parameters",
340
+ help="Parameters as a JSON array of ToolParamDef objects.",
341
+ ),
342
+ enabled: bool = typer.Option(True, "--enabled/--disabled", help="Enable or disable."),
343
+ comment: str | None = typer.Option(None, "--comment", "-c", help="Version comment."),
344
+ ) -> None:
345
+ """Update a tool (creates a new version)."""
346
+ params: list[dict[str, Any]] = []
347
+ if parameters:
348
+ try:
349
+ params = json.loads(parameters)
350
+ except json.JSONDecodeError as exc:
351
+ err_console.print(f"[red]Error[/red]: --parameters is not valid JSON: {exc}")
352
+ sys.exit(1)
353
+ try:
354
+ data = state.get_client().put(
355
+ f"/api/v1/toolsets/{toolset_id}/tools/{tool_id}",
356
+ json={
357
+ "name": name,
358
+ "description": description,
359
+ "cypher": cypher,
360
+ "parameters": params,
361
+ "enabled": enabled,
362
+ "comment": comment,
363
+ },
364
+ )
365
+ except Exception as exc:
366
+ _die(exc)
367
+ return
368
+ _print_tool_detail(data, as_json=False)
369
+
370
+
371
+ @tools_app.command("delete")
372
+ def delete_tool(
373
+ toolset_id: str = typer.Argument(help="Toolset ID."),
374
+ tool_id: str = typer.Argument(help="Tool ID."),
375
+ yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation prompt."),
376
+ ) -> None:
377
+ """Delete a tool."""
378
+ if not yes:
379
+ typer.confirm(f"Delete tool {tool_id!r}?", abort=True)
380
+ try:
381
+ state.get_client().delete(f"/api/v1/toolsets/{toolset_id}/tools/{tool_id}")
382
+ except Exception as exc:
383
+ _die(exc)
384
+ return
385
+ console.print(f"[red]Deleted[/red]: {tool_id}")
386
+
387
+
388
+ @tools_app.command("call")
389
+ def call_tool(
390
+ toolset_id: str = typer.Argument(help="Toolset ID."),
391
+ tool_id: str = typer.Argument(help="Tool ID."),
392
+ arg: list[str] = typer.Option(
393
+ [],
394
+ "--arg",
395
+ help=(
396
+ "Argument as KEY=VALUE where VALUE is a JSON literal "
397
+ "(e.g. --arg limit=10 --arg name='\"foo\"'). "
398
+ "Ignored if --args-json is provided."
399
+ ),
400
+ ),
401
+ args_json: str | None = typer.Option(
402
+ None,
403
+ "--args-json",
404
+ help="All arguments as a JSON object (overrides --arg).",
405
+ ),
406
+ output: str = typer.Option("table", "--output", "-o", help="Output format: table or json."),
407
+ ) -> None:
408
+ """Execute a tool's Cypher query with the provided arguments."""
409
+ arguments: dict[str, Any] = {}
410
+
411
+ if args_json is not None:
412
+ try:
413
+ arguments = json.loads(args_json)
414
+ except json.JSONDecodeError as exc:
415
+ err_console.print(f"[red]Error[/red]: --args-json is not valid JSON: {exc}")
416
+ sys.exit(1)
417
+ elif arg:
418
+ for pair in arg:
419
+ if "=" not in pair:
420
+ err_console.print(f"[red]Error[/red]: --arg {pair!r} must be KEY=VALUE")
421
+ sys.exit(1)
422
+ key, _, raw_value = pair.partition("=")
423
+ try:
424
+ arguments[key] = json.loads(raw_value)
425
+ except json.JSONDecodeError:
426
+ # Treat as plain string if not valid JSON
427
+ arguments[key] = raw_value
428
+
429
+ try:
430
+ data = state.get_client().post(
431
+ f"/api/v1/toolsets/{toolset_id}/tools/{tool_id}/call",
432
+ json={"arguments": arguments},
433
+ )
434
+ except Exception as exc:
435
+ _die(exc)
436
+ return
437
+
438
+ if output == "json":
439
+ console.print_json(json.dumps(data))
440
+ return
441
+
442
+ results = data.get("results", [])
443
+ if not results:
444
+ console.print("[dim]No results.[/dim]")
445
+ return
446
+
447
+ # Build table from first result's keys
448
+ columns = list(results[0].keys()) if results else []
449
+ table = Table(show_header=True, header_style="bold")
450
+ for col in columns:
451
+ table.add_column(col)
452
+
453
+ for row in results:
454
+ table.add_row(*[str(row.get(c, "")) for c in columns])
455
+
456
+ console.print(table)
457
+
458
+
459
+ @tools_app.command("versions")
460
+ def list_tool_versions(
461
+ toolset_id: str = typer.Argument(help="Toolset ID."),
462
+ tool_id: str = typer.Argument(help="Tool ID."),
463
+ output: str = typer.Option("table", "--output", "-o", help="Output format: table or json."),
464
+ ) -> None:
465
+ """List all versions of a tool."""
466
+ try:
467
+ data = state.get_client().get(f"/api/v1/toolsets/{toolset_id}/tools/{tool_id}/versions")
468
+ except Exception as exc:
469
+ _die(exc)
470
+ return
471
+
472
+ if output == "json":
473
+ console.print_json(json.dumps(data))
474
+ return
475
+
476
+ versions = data.get("versions", [])
477
+ table = Table(show_header=True, header_style="bold")
478
+ table.add_column("Version", justify="right")
479
+ table.add_column("Created By")
480
+ table.add_column("Created At")
481
+ table.add_column("Comment")
482
+
483
+ for v in versions:
484
+ table.add_row(
485
+ str(v["version"]),
486
+ v["created_by"],
487
+ v["created_at"],
488
+ v.get("comment") or "",
489
+ )
490
+ console.print(table)
491
+
492
+
493
+ @tools_app.command("version-get")
494
+ def get_tool_version(
495
+ toolset_id: str = typer.Argument(help="Toolset ID."),
496
+ tool_id: str = typer.Argument(help="Tool ID."),
497
+ version: int = typer.Argument(help="Version number."),
498
+ output: str = typer.Option("table", "--output", "-o", help="Output format: table or json."),
499
+ ) -> None:
500
+ """Get a specific version of a tool."""
501
+ try:
502
+ data = state.get_client().get(f"/api/v1/toolsets/{toolset_id}/tools/{tool_id}/versions/{version}")
503
+ except Exception as exc:
504
+ _die(exc)
505
+ return
506
+ _print_tool_detail(data, as_json=(output == "json"))
seizu_cli/config.py ADDED
@@ -0,0 +1,38 @@
1
+ """CLI configuration loaded from ~/.config/seizu/seizu.conf."""
2
+
3
+ from pathlib import Path
4
+
5
+ import yaml
6
+ from pydantic import BaseModel
7
+
8
+ _DEFAULT_CONFIG_DIR = Path.home() / ".config" / "seizu"
9
+ _DEFAULT_CONFIG_FILE = _DEFAULT_CONFIG_DIR / "seizu.conf"
10
+ _DEFAULT_SEED_FILE = _DEFAULT_CONFIG_DIR / "reporting-dashboard.yaml"
11
+
12
+
13
+ class SeizuConfig(BaseModel):
14
+ """Parsed contents of ~/.config/seizu/seizu.conf."""
15
+
16
+ api_url: str | None = None
17
+ seed_file: str | None = None
18
+
19
+
20
+ def load_config(config_file: Path | None = None) -> SeizuConfig:
21
+ """Load the CLI config file.
22
+
23
+ Reads *config_file* if provided, otherwise falls back to
24
+ ``~/.config/seizu/seizu.conf``. Returns an empty :class:`SeizuConfig`
25
+ when the file does not exist rather than raising an error.
26
+ """
27
+ path = config_file or _DEFAULT_CONFIG_FILE
28
+ try:
29
+ with open(path) as f:
30
+ data = yaml.safe_load(f) or {}
31
+ return SeizuConfig.model_validate(data)
32
+ except FileNotFoundError:
33
+ return SeizuConfig()
34
+
35
+
36
+ def default_seed_file() -> str:
37
+ """Return the default seed-file path as a string."""
38
+ return str(_DEFAULT_SEED_FILE)