deepflow-opendesk-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.
@@ -0,0 +1,696 @@
1
+ """Command-line interface for OpenDesk admin configuration APIs."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import getpass
7
+ import json
8
+ import sys
9
+ from pathlib import Path
10
+ from typing import Any
11
+
12
+ from .batch import load_jsonl, run_batch
13
+ from .client import (
14
+ CliError,
15
+ HttpResult,
16
+ OpenDeskClient,
17
+ host_from_env_or_profile,
18
+ load_json_payload,
19
+ parse_key_value,
20
+ print_json,
21
+ print_result,
22
+ token_from_env_or_profile,
23
+ )
24
+ from .profiles import (
25
+ DEFAULT_PROFILE,
26
+ delete_profile,
27
+ load_profile,
28
+ load_schema_cache,
29
+ save_profile,
30
+ save_schema_cache,
31
+ )
32
+ from .schema import (
33
+ EndpointDoc,
34
+ build_catalog,
35
+ fetch_openapi_schema,
36
+ find_matching_endpoint,
37
+ get_endpoint,
38
+ read_schema_file,
39
+ )
40
+ from .workflows import list_workflows, load_workflow_operations
41
+
42
+
43
+ def build_parser() -> argparse.ArgumentParser:
44
+ """Build the top-level argument parser."""
45
+ parser = argparse.ArgumentParser(description="OpenDesk admin configuration API CLI")
46
+ add_common_args(parser)
47
+ subparsers = parser.add_subparsers(dest="command", required=True)
48
+
49
+ schema = subparsers.add_parser("schema", help="Fetch and inspect OpenAPI schema.")
50
+ schema_sub = schema.add_subparsers(dest="schema_action", required=True)
51
+ schema_fetch = schema_sub.add_parser("fetch")
52
+ schema_fetch.add_argument("--output")
53
+ schema_export = schema_sub.add_parser("export")
54
+ schema_export.add_argument("--output")
55
+ add_schema_arg(schema_export)
56
+ schema_tags = schema_sub.add_parser("tags")
57
+ add_schema_arg(schema_tags)
58
+ schema.set_defaults(handler=handle_schema)
59
+
60
+ docs = subparsers.add_parser("docs", help="Query configuration API docs.")
61
+ docs_sub = docs.add_subparsers(dest="docs_action", required=True)
62
+ docs_list = docs_sub.add_parser("list")
63
+ add_schema_arg(docs_list)
64
+ docs_list.add_argument("--tag")
65
+ docs_list.add_argument("--method")
66
+ docs_list.add_argument("--query")
67
+ docs_show = docs_sub.add_parser("show")
68
+ add_schema_arg(docs_show)
69
+ docs_show.add_argument("endpoint")
70
+ docs_show.add_argument("--json", dest="json_doc", action="store_true")
71
+ docs.set_defaults(handler=handle_docs)
72
+
73
+ request = subparsers.add_parser("request", help="Call a configuration API by method and path.")
74
+ request.add_argument("method")
75
+ request.add_argument("path")
76
+ add_request_args(request)
77
+ add_schema_arg(request)
78
+ request.set_defaults(handler=handle_request)
79
+
80
+ call = subparsers.add_parser("call", help="Call a configuration API by endpoint key.")
81
+ call.add_argument("endpoint")
82
+ call.add_argument("--path", action="append", help="Path parameter as key=value. Repeatable.")
83
+ add_request_args(call)
84
+ add_schema_arg(call)
85
+ call.set_defaults(handler=handle_call)
86
+
87
+ auth = subparsers.add_parser("auth", help="Authentication commands.")
88
+ auth_sub = auth.add_subparsers(dest="auth_action", required=True)
89
+ login = auth_sub.add_parser("login")
90
+ login.add_argument("--tenant", required=True)
91
+ login.add_argument("--username", required=True)
92
+ login.add_argument("--password")
93
+ login.add_argument("--save-token", action="store_true")
94
+ auth_sub.add_parser("me")
95
+ auth_sub.add_parser("logout")
96
+ auth.set_defaults(handler=handle_auth)
97
+
98
+ add_system_parser(subparsers)
99
+ add_roles_parser(subparsers)
100
+ add_employees_parser(subparsers)
101
+ add_employee_groups_parser(subparsers)
102
+ add_fields_parser(subparsers)
103
+ add_forms_parser(subparsers)
104
+ add_views_parser(subparsers)
105
+ add_channels_parser(subparsers)
106
+ add_routing_parser(subparsers)
107
+ add_queue_parser(subparsers)
108
+ add_voice_flows_parser(subparsers)
109
+ add_ticket_workflows_parser(subparsers)
110
+
111
+ batch = subparsers.add_parser("batch", help="Run JSONL configuration operations.")
112
+ batch_sub = batch.add_subparsers(dest="batch_action", required=True)
113
+ batch_run = batch_sub.add_parser("run")
114
+ batch_run.add_argument("file")
115
+ batch_run.add_argument("--continue-on-error", action="store_true")
116
+ batch_run.add_argument("--output")
117
+ batch_run.add_argument("--dry-run", action="store_true")
118
+ batch_run.add_argument("--emit-curl", action="store_true")
119
+ batch_run.add_argument("--yes", action="store_true")
120
+ add_schema_arg(batch_run)
121
+ batch.set_defaults(handler=handle_batch)
122
+
123
+ workflow = subparsers.add_parser("workflow", help="Run configuration workflows.")
124
+ workflow_sub = workflow.add_subparsers(dest="workflow_action", required=True)
125
+ workflow_sub.add_parser("list")
126
+ workflow_run = workflow_sub.add_parser("run")
127
+ workflow_run.add_argument("name")
128
+ workflow_run.add_argument("--json-file")
129
+ workflow_run.add_argument("--continue-on-error", action="store_true")
130
+ workflow_run.add_argument("--dry-run", action="store_true")
131
+ workflow_run.add_argument("--emit-curl", action="store_true")
132
+ workflow_run.add_argument("--yes", action="store_true")
133
+ add_schema_arg(workflow_run)
134
+ workflow.set_defaults(handler=handle_workflow)
135
+
136
+ return parser
137
+
138
+
139
+ def add_common_args(parser: argparse.ArgumentParser) -> None:
140
+ """Add global arguments."""
141
+ parser.add_argument("--host", help="OpenDesk host, for example http://localhost:5001.")
142
+ parser.add_argument("--base-url", help="API base URL, for example http://localhost:5001/api/v1.")
143
+ parser.add_argument("--token", help="Bearer token. Defaults to OPENDESK_TOKEN or profile token.")
144
+ parser.add_argument("--profile", default=DEFAULT_PROFILE, help="Profile name for token and schema cache.")
145
+ parser.add_argument("--timeout", type=int, default=30)
146
+ parser.add_argument("--pretty", action=argparse.BooleanOptionalAction, default=True, help="Pretty-print JSON output.")
147
+ parser.add_argument("--raw", action="store_true", help="Print raw response text for HTTP requests.")
148
+
149
+
150
+ def add_schema_arg(parser: argparse.ArgumentParser) -> None:
151
+ """Add schema-file argument."""
152
+ parser.add_argument("--schema-file", help="Read OpenAPI schema from a local JSON file.")
153
+
154
+
155
+ def add_json_args(parser: argparse.ArgumentParser) -> None:
156
+ """Add JSON body arguments."""
157
+ parser.add_argument("--json", help="JSON object payload.")
158
+ parser.add_argument("--json-file", help="Read JSON object payload from file.")
159
+
160
+
161
+ def add_request_args(parser: argparse.ArgumentParser) -> None:
162
+ """Add common request arguments."""
163
+ parser.add_argument("--query", action="append", help="Query parameter as key=value. Repeatable.")
164
+ parser.add_argument("--header", action="append", help="Extra header as key=value. Repeatable.")
165
+ parser.add_argument("--form-file", action="append", help="Multipart file as field=path. Repeatable.")
166
+ parser.add_argument("--dry-run", action="store_true")
167
+ parser.add_argument("--emit-curl", action="store_true")
168
+ parser.add_argument("--yes", action="store_true", help="Allow destructive requests such as DELETE.")
169
+ add_json_args(parser)
170
+
171
+
172
+ def add_system_parser(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) -> None:
173
+ system = subparsers.add_parser("system", help="System configuration commands.")
174
+ system_sub = system.add_subparsers(dest="system_action", required=True)
175
+ system_sub.add_parser("info")
176
+ settings = system_sub.add_parser("settings")
177
+ settings_sub = settings.add_subparsers(dest="settings_action", required=False)
178
+ update = settings_sub.add_parser("update")
179
+ add_json_args(update)
180
+ system.set_defaults(handler=handle_system)
181
+
182
+
183
+ def add_roles_parser(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) -> None:
184
+ roles = subparsers.add_parser("roles", help="Role configuration commands.")
185
+ roles_sub = roles.add_subparsers(dest="roles_action", required=True)
186
+ roles_sub.add_parser("list")
187
+ roles_sub.add_parser("permission-tree")
188
+ create = roles_sub.add_parser("create")
189
+ add_json_args(create)
190
+ roles.set_defaults(handler=handle_roles)
191
+
192
+
193
+ def add_employees_parser(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) -> None:
194
+ employees = subparsers.add_parser("employees", help="Employee configuration commands.")
195
+ employees_sub = employees.add_subparsers(dest="employees_action", required=True)
196
+ employees_sub.add_parser("list")
197
+ create = employees_sub.add_parser("create")
198
+ add_json_args(create)
199
+ employees.set_defaults(handler=handle_employees)
200
+
201
+
202
+ def add_employee_groups_parser(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) -> None:
203
+ groups = subparsers.add_parser("employee-groups", help="Employee group configuration commands.")
204
+ groups_sub = groups.add_subparsers(dest="employee_groups_action", required=True)
205
+ groups_sub.add_parser("list")
206
+ groups.set_defaults(handler=handle_employee_groups)
207
+
208
+
209
+ def add_fields_parser(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) -> None:
210
+ fields = subparsers.add_parser("fields", help="Field definition commands.")
211
+ fields_sub = fields.add_subparsers(dest="fields_action", required=True)
212
+ list_cmd = fields_sub.add_parser("list")
213
+ list_cmd.add_argument("--domain")
214
+ create = fields_sub.add_parser("create")
215
+ add_json_args(create)
216
+ fields.set_defaults(handler=handle_fields)
217
+
218
+
219
+ def add_forms_parser(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) -> None:
220
+ forms = subparsers.add_parser("forms", help="Form layout commands.")
221
+ forms_sub = forms.add_subparsers(dest="forms_action", required=True)
222
+ by_scene = forms_sub.add_parser("get-by-scene")
223
+ by_scene.add_argument("scene")
224
+ forms.set_defaults(handler=handle_forms)
225
+
226
+
227
+ def add_views_parser(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) -> None:
228
+ views = subparsers.add_parser("views", help="Workspace view configuration commands.")
229
+ views_sub = views.add_subparsers(dest="views_action", required=True)
230
+ list_cmd = views_sub.add_parser("list")
231
+ list_cmd.add_argument("--resource", required=True, choices=["ticket", "user", "organization"])
232
+ views.set_defaults(handler=handle_views)
233
+
234
+
235
+ def add_channels_parser(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) -> None:
236
+ channels = subparsers.add_parser("channels", help="Channel configuration commands.")
237
+ channels_sub = channels.add_subparsers(dest="channels_action", required=True)
238
+ channels_sub.add_parser("list")
239
+ channels.set_defaults(handler=handle_channels)
240
+
241
+
242
+ def add_routing_parser(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) -> None:
243
+ routing = subparsers.add_parser("routing", help="Routing configuration commands.")
244
+ routing_sub = routing.add_subparsers(dest="routing_scope", required=True)
245
+ session = routing_sub.add_parser("session")
246
+ session_sub = session.add_subparsers(dest="routing_action", required=True)
247
+ session_sub.add_parser("list")
248
+ inbound = routing_sub.add_parser("inbound")
249
+ inbound_sub = inbound.add_subparsers(dest="routing_action", required=True)
250
+ inbound_sub.add_parser("list")
251
+ routing.set_defaults(handler=handle_routing)
252
+
253
+
254
+ def add_queue_parser(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) -> None:
255
+ queue = subparsers.add_parser("queue", help="Queue policy commands.")
256
+ queue_sub = queue.add_subparsers(dest="queue_action", required=True)
257
+ queue_sub.add_parser("policies")
258
+ queue.set_defaults(handler=handle_queue)
259
+
260
+
261
+ def add_voice_flows_parser(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) -> None:
262
+ voice = subparsers.add_parser("voice-flows", help="Voice flow configuration commands.")
263
+ voice_sub = voice.add_subparsers(dest="voice_flows_action", required=True)
264
+ voice_sub.add_parser("list")
265
+ voice.set_defaults(handler=handle_voice_flows)
266
+
267
+
268
+ def add_ticket_workflows_parser(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) -> None:
269
+ workflows = subparsers.add_parser("ticket-workflows", help="Ticket workflow configuration commands.")
270
+ workflows_sub = workflows.add_subparsers(dest="ticket_workflows_action", required=True)
271
+ workflows_sub.add_parser("list")
272
+ workflows.set_defaults(handler=handle_ticket_workflows)
273
+
274
+
275
+ def handle_schema(args: argparse.Namespace) -> int:
276
+ """Handle schema commands."""
277
+ if args.schema_action == "fetch":
278
+ schema = fetch_openapi_schema(resolved_host(args), resolved_token(args), args.timeout)
279
+ save_schema_cache(args.profile, schema)
280
+ if args.output:
281
+ Path(args.output).write_text(json.dumps(schema, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
282
+ print_json({"ok": True, "paths": len(schema.get("paths", {}))}, pretty=args.pretty)
283
+ return 0
284
+
285
+ schema = load_schema(args)
286
+ if args.schema_action == "export":
287
+ if args.output:
288
+ Path(args.output).write_text(json.dumps(schema, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
289
+ print_json({"ok": True, "output": args.output}, pretty=args.pretty)
290
+ else:
291
+ print_json(schema, pretty=args.pretty)
292
+ return 0
293
+
294
+ catalog = build_catalog(schema)
295
+ counts: dict[str, int] = {}
296
+ for endpoint in catalog:
297
+ counts[endpoint.tag] = counts.get(endpoint.tag, 0) + 1
298
+ items = [{"tag": tag, "count": count} for tag, count in sorted(counts.items())]
299
+ print_json(items, pretty=args.pretty)
300
+ return 0
301
+
302
+
303
+ def handle_docs(args: argparse.Namespace) -> int:
304
+ """Handle docs commands."""
305
+ catalog = build_catalog(load_schema(args))
306
+ if args.docs_action == "list":
307
+ items = [
308
+ {
309
+ "key": endpoint.key,
310
+ "method": endpoint.method,
311
+ "path": endpoint.path,
312
+ "tag": endpoint.tag,
313
+ "summary": endpoint.summary,
314
+ }
315
+ for endpoint in filter_catalog(catalog, tag=args.tag, method=args.method, query=args.query)
316
+ ]
317
+ print_json(items, pretty=args.pretty)
318
+ return 0
319
+
320
+ endpoint = get_endpoint(catalog, args.endpoint)
321
+ if args.json_doc:
322
+ print_json(endpoint.to_dict(), pretty=args.pretty)
323
+ return 0
324
+ print_endpoint_text(endpoint)
325
+ return 0
326
+
327
+
328
+ def handle_request(args: argparse.Namespace) -> int:
329
+ """Handle direct request command."""
330
+ endpoint = ensure_config_path(args, args.method, args.path)
331
+ result = execute_request(
332
+ args,
333
+ endpoint.method,
334
+ args.path,
335
+ query=parse_key_value(args.query),
336
+ body=optional_payload(args),
337
+ files=parse_key_value(args.form_file),
338
+ headers=parse_key_value(args.header),
339
+ dry_run=args.dry_run,
340
+ emit_curl=args.emit_curl,
341
+ yes=args.yes,
342
+ )
343
+ return finish_http(args, result)
344
+
345
+
346
+ def handle_call(args: argparse.Namespace) -> int:
347
+ """Handle endpoint-key calls."""
348
+ catalog = build_catalog(load_schema(args))
349
+ endpoint = get_endpoint(catalog, args.endpoint)
350
+ path = fill_path(endpoint.path, parse_key_value(args.path))
351
+ result = execute_request(
352
+ args,
353
+ endpoint.method,
354
+ path,
355
+ query=parse_key_value(args.query),
356
+ body=optional_payload(args),
357
+ files=parse_key_value(args.form_file),
358
+ headers=parse_key_value(args.header),
359
+ dry_run=args.dry_run,
360
+ emit_curl=args.emit_curl,
361
+ yes=args.yes,
362
+ )
363
+ return finish_http(args, result)
364
+
365
+
366
+ def handle_auth(args: argparse.Namespace) -> int:
367
+ """Handle auth commands."""
368
+ if args.auth_action == "logout":
369
+ removed = delete_profile(args.profile)
370
+ print_json({"ok": True, "removed": removed}, pretty=args.pretty)
371
+ return 0
372
+ if args.auth_action == "me":
373
+ return simple_request(args, "GET", "/api/v1/auth/me")
374
+
375
+ password = args.password or getpass.getpass("OpenDesk password: ")
376
+ body = {"tenant": args.tenant, "username": args.username, "password": password}
377
+ result = client_from_args(args, token=None).request("POST", "/api/v1/auth/login", body=body)
378
+ if result.ok and args.save_token and isinstance(result.data, dict):
379
+ access_token = result.data.get("access_token")
380
+ if isinstance(access_token, str):
381
+ save_profile(
382
+ args.profile,
383
+ {"host": resolved_host(args), "token": access_token, "user": result.data.get("user")},
384
+ )
385
+ return finish_http(args, result)
386
+
387
+
388
+ def handle_system(args: argparse.Namespace) -> int:
389
+ """Handle system commands."""
390
+ if args.system_action == "info":
391
+ return simple_request(args, "GET", "/api/v1/system/info")
392
+ if getattr(args, "settings_action", None) == "update":
393
+ return simple_request(args, "PUT", "/api/v1/system-settings", body=optional_payload(args))
394
+ return simple_request(args, "GET", "/api/v1/system-settings")
395
+
396
+
397
+ def handle_roles(args: argparse.Namespace) -> int:
398
+ """Handle role commands."""
399
+ if args.roles_action == "permission-tree":
400
+ return simple_request(args, "GET", "/api/v1/roles/permission-tree")
401
+ if args.roles_action == "create":
402
+ return simple_request(args, "POST", "/api/v1/roles", body=optional_payload(args))
403
+ return simple_request(args, "GET", "/api/v1/roles")
404
+
405
+
406
+ def handle_employees(args: argparse.Namespace) -> int:
407
+ """Handle employee commands."""
408
+ if args.employees_action == "create":
409
+ return simple_request(args, "POST", "/api/v1/employees", body=optional_payload(args))
410
+ return simple_request(args, "GET", "/api/v1/employees")
411
+
412
+
413
+ def handle_employee_groups(args: argparse.Namespace) -> int:
414
+ """Handle employee group commands."""
415
+ return simple_request(args, "GET", "/api/v1/employee-groups")
416
+
417
+
418
+ def handle_fields(args: argparse.Namespace) -> int:
419
+ """Handle field commands."""
420
+ if args.fields_action == "create":
421
+ return simple_request(args, "POST", "/api/v1/field-definitions", body=optional_payload(args))
422
+ query = {"domain": args.domain} if args.domain else None
423
+ return simple_request(args, "GET", "/api/v1/field-definitions", query=query)
424
+
425
+
426
+ def handle_forms(args: argparse.Namespace) -> int:
427
+ """Handle form commands."""
428
+ return simple_request(args, "GET", f"/api/v1/form-layouts/by-scene/{args.scene}")
429
+
430
+
431
+ def handle_views(args: argparse.Namespace) -> int:
432
+ """Handle view commands."""
433
+ path_by_resource = {
434
+ "ticket": "/api/v1/ticket-views",
435
+ "user": "/api/v1/user-views",
436
+ "organization": "/api/v1/organization-views",
437
+ }
438
+ return simple_request(args, "GET", path_by_resource[args.resource])
439
+
440
+
441
+ def handle_channels(args: argparse.Namespace) -> int:
442
+ """Handle channel commands."""
443
+ return simple_request(args, "GET", "/api/v1/channels")
444
+
445
+
446
+ def handle_routing(args: argparse.Namespace) -> int:
447
+ """Handle routing commands."""
448
+ if args.routing_scope == "inbound":
449
+ return simple_request(args, "GET", "/api/v1/inbound-routing-rules")
450
+ return simple_request(args, "GET", "/api/v1/session-routing-rules")
451
+
452
+
453
+ def handle_queue(args: argparse.Namespace) -> int:
454
+ """Handle queue commands."""
455
+ return simple_request(args, "GET", "/api/v1/queue/policies")
456
+
457
+
458
+ def handle_voice_flows(args: argparse.Namespace) -> int:
459
+ """Handle voice-flow commands."""
460
+ return simple_request(args, "GET", "/api/v1/voice-flows")
461
+
462
+
463
+ def handle_ticket_workflows(args: argparse.Namespace) -> int:
464
+ """Handle ticket-workflow commands."""
465
+ return simple_request(args, "GET", "/api/v1/ticket-workflows")
466
+
467
+
468
+ def handle_batch(args: argparse.Namespace) -> int:
469
+ """Handle batch commands."""
470
+ operations = load_jsonl(args.file)
471
+ results = run_batch(
472
+ operations,
473
+ lambda operation: run_operation(args, operation, dry_run=args.dry_run, emit_curl=args.emit_curl, yes=args.yes),
474
+ continue_on_error=args.continue_on_error,
475
+ )
476
+ text = "\n".join(json.dumps(item, ensure_ascii=False) for item in results) + "\n"
477
+ if args.output:
478
+ Path(args.output).write_text(text, encoding="utf-8")
479
+ print(text, end="")
480
+ return 0 if all(item.get("ok") for item in results) else 1
481
+
482
+
483
+ def handle_workflow(args: argparse.Namespace) -> int:
484
+ """Handle workflow commands."""
485
+ if args.workflow_action == "list":
486
+ print_json(list_workflows(), pretty=args.pretty)
487
+ return 0
488
+ operations = load_workflow_operations(args.name, args.json_file)
489
+ results = run_batch(
490
+ operations,
491
+ lambda operation: run_operation(args, operation, dry_run=args.dry_run, emit_curl=args.emit_curl, yes=args.yes),
492
+ continue_on_error=args.continue_on_error,
493
+ )
494
+ print("\n".join(json.dumps(item, ensure_ascii=False) for item in results))
495
+ return 0 if all(item.get("ok") for item in results) else 1
496
+
497
+
498
+ def simple_request(
499
+ args: argparse.Namespace,
500
+ method: str,
501
+ path: str,
502
+ *,
503
+ query: dict[str, Any] | None = None,
504
+ body: dict[str, Any] | None = None,
505
+ ) -> int:
506
+ """Run a known-safe configuration request."""
507
+ result = execute_request(args, method, path, query=query, body=body)
508
+ return finish_http(args, result)
509
+
510
+
511
+ def execute_request(
512
+ args: argparse.Namespace,
513
+ method: str,
514
+ path: str,
515
+ *,
516
+ query: dict[str, Any] | None = None,
517
+ body: dict[str, Any] | None = None,
518
+ files: dict[str, str] | None = None,
519
+ headers: dict[str, str] | None = None,
520
+ dry_run: bool = False,
521
+ emit_curl: bool = False,
522
+ yes: bool = False,
523
+ ) -> HttpResult:
524
+ """Execute a request with destructive-operation protection."""
525
+ if method.upper() == "DELETE" and not (yes or dry_run):
526
+ raise CliError("DELETE requires --yes or --dry-run")
527
+ return client_from_args(args).request(
528
+ method,
529
+ path,
530
+ query=query,
531
+ body=body,
532
+ files=files,
533
+ headers=headers,
534
+ dry_run=dry_run,
535
+ emit_curl=emit_curl,
536
+ )
537
+
538
+
539
+ def run_operation(
540
+ args: argparse.Namespace,
541
+ operation: dict[str, Any],
542
+ *,
543
+ dry_run: bool,
544
+ emit_curl: bool,
545
+ yes: bool,
546
+ ) -> HttpResult:
547
+ """Run a batch/workflow operation."""
548
+ if "key" in operation:
549
+ catalog = build_catalog(load_schema(args))
550
+ endpoint = get_endpoint(catalog, str(operation["key"]))
551
+ path = fill_path(endpoint.path, operation.get("path") or {})
552
+ method = endpoint.method
553
+ else:
554
+ method = str(operation.get("method") or "").upper()
555
+ path = str(operation.get("path") or "")
556
+ if not method or not path:
557
+ raise CliError("Batch operation requires either key or method/path")
558
+ ensure_config_path(args, method, path)
559
+ return execute_request(
560
+ args,
561
+ method,
562
+ path,
563
+ query=operation.get("query") if isinstance(operation.get("query"), dict) else None,
564
+ body=operation.get("body") if isinstance(operation.get("body"), dict) else None,
565
+ files=operation.get("files") if isinstance(operation.get("files"), dict) else None,
566
+ dry_run=dry_run,
567
+ emit_curl=emit_curl,
568
+ yes=yes,
569
+ )
570
+
571
+
572
+ def ensure_config_path(args: argparse.Namespace, method: str, path: str) -> EndpointDoc:
573
+ """Ensure a concrete path is in the configuration API catalog."""
574
+ catalog = build_catalog(load_schema(args))
575
+ endpoint = find_matching_endpoint(catalog, method, path)
576
+ if endpoint is None:
577
+ raise CliError(f"{method.upper()} {path} is not in the admin configuration API catalog")
578
+ return endpoint
579
+
580
+
581
+ def fill_path(template: str, params: dict[str, Any]) -> str:
582
+ """Fill a path template with path parameters."""
583
+ path = template
584
+ for name in [part[1:-1] for part in template.split("/") if part.startswith("{") and part.endswith("}")]:
585
+ if name not in params:
586
+ raise CliError(f"Missing path parameter: {name}")
587
+ path = path.replace("{" + name + "}", str(params[name]))
588
+ return path
589
+
590
+
591
+ def filter_catalog(
592
+ catalog: list[EndpointDoc],
593
+ *,
594
+ tag: str | None = None,
595
+ method: str | None = None,
596
+ query: str | None = None,
597
+ ) -> list[EndpointDoc]:
598
+ """Filter catalog rows for docs list."""
599
+ result = catalog
600
+ if tag:
601
+ result = [item for item in result if item.tag == tag]
602
+ if method:
603
+ result = [item for item in result if item.method == method.upper()]
604
+ if query:
605
+ needle = query.lower()
606
+ result = [
607
+ item
608
+ for item in result
609
+ if needle in item.key.lower()
610
+ or needle in item.path.lower()
611
+ or needle in item.summary.lower()
612
+ ]
613
+ return result
614
+
615
+
616
+ def print_endpoint_text(endpoint: EndpointDoc) -> None:
617
+ """Print a human-readable endpoint document."""
618
+ print(endpoint.key)
619
+ print(f" {endpoint.method} {endpoint.path}")
620
+ print(f" tag: {endpoint.tag}")
621
+ print(f" operation_id: {endpoint.operation_id}")
622
+ print(f" auth: {'required' if endpoint.requires_auth else 'not required'}")
623
+ print(f" summary: {endpoint.summary}")
624
+ if endpoint.path_params:
625
+ print(" path params:")
626
+ for item in endpoint.path_params:
627
+ print(f" - {item.name} ({item.type}, required={item.required}): {item.description}")
628
+ if endpoint.query_params:
629
+ print(" query params:")
630
+ for item in endpoint.query_params:
631
+ print(f" - {item.name} ({item.type}, required={item.required}): {item.description}")
632
+ if endpoint.body_schema:
633
+ print(f" body_schema: {endpoint.body_schema}")
634
+ if endpoint.response_schema:
635
+ print(f" response_schema: {endpoint.response_schema}")
636
+ for example in endpoint.examples:
637
+ print(f" example: {example}")
638
+
639
+
640
+ def optional_payload(args: argparse.Namespace) -> dict[str, Any] | None:
641
+ """Return JSON payload or None when no payload was provided."""
642
+ payload = load_json_payload(getattr(args, "json", None), getattr(args, "json_file", None))
643
+ return payload if payload else None
644
+
645
+
646
+ def client_from_args(args: argparse.Namespace, token: str | None | object = ... ) -> OpenDeskClient:
647
+ """Create an HTTP client from CLI args."""
648
+ profile_data = load_profile(args.profile)
649
+ resolved = resolved_token(args, profile_data) if token is ... else token
650
+ return OpenDeskClient(host=resolved_host(args, profile_data), token=resolved, timeout=args.timeout)
651
+
652
+
653
+ def resolved_host(args: argparse.Namespace, profile_data: dict[str, Any] | None = None) -> str:
654
+ """Resolve host from args, environment, or profile."""
655
+ profile_data = profile_data if profile_data is not None else load_profile(args.profile)
656
+ return host_from_env_or_profile(host=args.host, base_url=args.base_url, profile_data=profile_data)
657
+
658
+
659
+ def resolved_token(args: argparse.Namespace, profile_data: dict[str, Any] | None = None) -> str | None:
660
+ """Resolve token from args, environment, or profile."""
661
+ profile_data = profile_data if profile_data is not None else load_profile(args.profile)
662
+ return token_from_env_or_profile(token=args.token, profile_data=profile_data)
663
+
664
+
665
+ def load_schema(args: argparse.Namespace) -> dict[str, Any]:
666
+ """Load schema from file, cache, or remote host."""
667
+ schema_file = getattr(args, "schema_file", None)
668
+ if schema_file:
669
+ return read_schema_file(schema_file)
670
+ cached = load_schema_cache(args.profile)
671
+ if cached:
672
+ return cached
673
+ schema = fetch_openapi_schema(resolved_host(args), resolved_token(args), args.timeout)
674
+ save_schema_cache(args.profile, schema)
675
+ return schema
676
+
677
+
678
+ def finish_http(args: argparse.Namespace, result: HttpResult) -> int:
679
+ """Print an HTTP result and return a process code."""
680
+ print_result(result, pretty=args.pretty, raw=args.raw)
681
+ return 0 if result.ok else 1
682
+
683
+
684
+ def main(argv: list[str] | None = None) -> int:
685
+ """CLI entry point."""
686
+ parser = build_parser()
687
+ args = parser.parse_args(argv)
688
+ try:
689
+ return args.handler(args)
690
+ except (CliError, KeyError, RuntimeError, ValueError) as exc:
691
+ print(f"error: {exc}", file=sys.stderr)
692
+ return 1
693
+
694
+
695
+ if __name__ == "__main__":
696
+ raise SystemExit(main())