dwf-platform-cli 0.2.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.
Files changed (47) hide show
  1. dwf_cli/__init__.py +3 -0
  2. dwf_cli/__main__.py +4 -0
  3. dwf_cli/api/__init__.py +3 -0
  4. dwf_cli/api/auth.py +46 -0
  5. dwf_cli/api/client.py +113 -0
  6. dwf_cli/api/datamodel.py +458 -0
  7. dwf_cli/api/formmodel.py +197 -0
  8. dwf_cli/api/funcmodel.py +436 -0
  9. dwf_cli/cli/__init__.py +69 -0
  10. dwf_cli/cli/_common.py +152 -0
  11. dwf_cli/cli/auth.py +197 -0
  12. dwf_cli/cli/config.py +200 -0
  13. dwf_cli/cli/datamodel.py +2270 -0
  14. dwf_cli/cli/formmodel.py +1007 -0
  15. dwf_cli/cli/funcmodel.py +2055 -0
  16. dwf_cli/cli/schema.py +210 -0
  17. dwf_cli/core/__init__.py +0 -0
  18. dwf_cli/core/config.py +177 -0
  19. dwf_cli/core/crypto.py +22 -0
  20. dwf_cli/core/errors.py +63 -0
  21. dwf_cli/core/output.py +6 -0
  22. dwf_cli/core/validator.py +129 -0
  23. dwf_cli/mcp/__init__.py +3 -0
  24. dwf_cli/mcp/server.py +411 -0
  25. dwf_cli/schemas/__init__.py +37 -0
  26. dwf_cli/schemas/datamodel/attribute_bind.schema.json +21 -0
  27. dwf_cli/schemas/datamodel/attribute_create.schema.json +24 -0
  28. dwf_cli/schemas/datamodel/attribute_update.schema.json +21 -0
  29. dwf_cli/schemas/datamodel/create.schema.json +27 -0
  30. dwf_cli/schemas/datamodel/excel_confirm.schema.json +65 -0
  31. dwf_cli/schemas/datamodel/external_create.schema.json +47 -0
  32. dwf_cli/schemas/datamodel/external_update.schema.json +47 -0
  33. dwf_cli/schemas/datamodel/object_create.schema.json +24 -0
  34. dwf_cli/schemas/datamodel/object_update.schema.json +17 -0
  35. dwf_cli/schemas/datamodel/relation_create.schema.json +34 -0
  36. dwf_cli/schemas/datamodel/relation_update.schema.json +34 -0
  37. dwf_cli/schemas/datamodel/update.schema.json +26 -0
  38. dwf_cli/schemas/funcmodel/app_create.schema.json +150 -0
  39. dwf_cli/schemas/funcmodel/app_update.schema.json +153 -0
  40. dwf_cli/schemas/funcmodel/language-package_create.schema.json +15 -0
  41. dwf_cli/schemas/funcmodel/operations_create.schema.json +77 -0
  42. dwf_cli/schemas/funcmodel/operations_update.schema.json +76 -0
  43. dwf_platform_cli-0.2.0.dist-info/METADATA +347 -0
  44. dwf_platform_cli-0.2.0.dist-info/RECORD +47 -0
  45. dwf_platform_cli-0.2.0.dist-info/WHEEL +4 -0
  46. dwf_platform_cli-0.2.0.dist-info/entry_points.txt +3 -0
  47. dwf_platform_cli-0.2.0.dist-info/licenses/LICENSE +190 -0
@@ -0,0 +1,1007 @@
1
+ from __future__ import annotations
2
+
3
+ import json as json_mod
4
+ from enum import Enum
5
+ from pathlib import Path
6
+ from typing import Annotated, Any
7
+
8
+ import typer
9
+ from rich.table import Table
10
+
11
+ from dwf_cli.api import formmodel as formmodel_api
12
+ from dwf_cli.cli._common import (
13
+ get_client,
14
+ get_console,
15
+ handle_error,
16
+ is_tty,
17
+ resolve_format,
18
+ validate_path,
19
+ )
20
+ from dwf_cli.core.errors import ConflictError, DWFError
21
+ from dwf_cli.core.output import OutputFormat
22
+
23
+ _HELP_CONTEXT_SETTINGS = {"help_option_names": ["-h", "--help"]}
24
+
25
+ app = typer.Typer(
26
+ help="Form model management.\n\n"
27
+ "Examples:\n"
28
+ " dwf-cli formmodel view list -c User\n"
29
+ " dwf-cli formmodel view list -t standalone\n"
30
+ " dwf-cli formmodel view create -c User -n create --v3-content '{}'",
31
+ context_settings=_HELP_CONTEXT_SETTINGS,
32
+ )
33
+
34
+ _view_sub = typer.Typer(
35
+ help="View management.", context_settings=_HELP_CONTEXT_SETTINGS
36
+ )
37
+
38
+ app.add_typer(_view_sub, name="view")
39
+
40
+ DRY_RUN_EXIT_CODE = 10
41
+
42
+
43
+ class ViewType(str, Enum):
44
+ class_ = "class"
45
+ standalone = "standalone"
46
+
47
+
48
+ class FormDeviceType(str, Enum):
49
+ pc = "actPc"
50
+ ipad = "actIpad"
51
+ ipad_h = "actIpadH"
52
+ phone = "actPhone"
53
+ phone_h = "actPhoneH"
54
+ mobile = "actMobile"
55
+
56
+
57
+ class FormType(str, Enum):
58
+ PC = "PC"
59
+ Mobile = "Mobile"
60
+
61
+
62
+ _STANDALONE_CLASS_NAME = "DWFDesign"
63
+
64
+
65
+ def _extract_items(result: Any) -> list[dict[str, Any]]:
66
+ if isinstance(result, list):
67
+ return result
68
+ if isinstance(result, dict):
69
+ payload = result.get("data", result)
70
+ if isinstance(payload, dict):
71
+ items = payload.get("data", payload.get("items", []))
72
+ return items if isinstance(items, list) else []
73
+ if isinstance(payload, list):
74
+ return payload
75
+ return []
76
+
77
+
78
+ def _print_views_table(
79
+ items: list[dict[str, Any]],
80
+ class_name: str,
81
+ ) -> None:
82
+ console = get_console()
83
+ cols = list(items[0].keys()) if items else []
84
+ table = Table(
85
+ title=f"Views of [bold]{class_name}[/bold]",
86
+ show_header=True,
87
+ header_style="bold cyan",
88
+ )
89
+ table.add_column("#", style="dim", width=4)
90
+ for col in cols:
91
+ table.add_column(col)
92
+ for idx, item in enumerate(items, 1):
93
+ row = [str(idx)] + [str(item.get(col, "")) for col in cols]
94
+ table.add_row(*row)
95
+ console.print(table)
96
+ console.print(f"[dim]Total {len(items)} views[/dim]")
97
+
98
+
99
+ def _read_payload(data: str, file: Path | None) -> Any:
100
+ if file:
101
+ file = validate_path(file)
102
+ return json_mod.loads(file.read_text(encoding="utf-8"))
103
+ if data:
104
+ return json_mod.loads(data)
105
+ raise DWFError("Provide --data or --file")
106
+
107
+
108
+ def _build_view_payload(
109
+ class_name: str,
110
+ view_name: str,
111
+ is_relation: bool,
112
+ v3_content: str,
113
+ display_name: str,
114
+ note: str,
115
+ device_type: str,
116
+ form_type: str,
117
+ basic_info: str,
118
+ has_thumbnail: bool,
119
+ widget_annotation: str,
120
+ ) -> dict[str, Any]:
121
+ return {
122
+ "className": class_name,
123
+ "viewName": view_name,
124
+ "isRelation": is_relation,
125
+ "v3Content": v3_content,
126
+ "displayName": display_name,
127
+ "note": note,
128
+ "deviceType": device_type,
129
+ "formType": form_type,
130
+ "basicInfo": basic_info,
131
+ "hasThumbnail": has_thumbnail,
132
+ "widgetAnnotation": widget_annotation,
133
+ }
134
+
135
+
136
+ @_view_sub.command(name="list")
137
+ def list_views(
138
+ ctx: typer.Context,
139
+ view_type: Annotated[
140
+ ViewType,
141
+ typer.Option(
142
+ "--type",
143
+ "-t",
144
+ help="View type: class (entity/relation), standalone",
145
+ case_sensitive=False,
146
+ ),
147
+ ] = ViewType.class_,
148
+ class_name: Annotated[
149
+ str | None,
150
+ typer.Option(
151
+ "--class-name", "-c", help="Class name (required when type is 'class')"
152
+ ),
153
+ ] = None,
154
+ need_v3_content: Annotated[
155
+ bool,
156
+ typer.Option("--need-v3-content", help="Return form JSON content"),
157
+ ] = False,
158
+ fmt: Annotated[
159
+ OutputFormat,
160
+ typer.Option("--format", "-f", help="Output format: table/json"),
161
+ ] = OutputFormat.table,
162
+ ) -> None:
163
+ try:
164
+ if view_type == ViewType.standalone:
165
+ target_class = _STANDALONE_CLASS_NAME
166
+ elif class_name:
167
+ target_class = class_name
168
+ else:
169
+ raise DWFError("--class-name is required when --type is 'class'")
170
+
171
+ client = get_client(ctx)
172
+ result = formmodel_api.list_views(
173
+ client,
174
+ target_class,
175
+ need_v3_content=need_v3_content,
176
+ )
177
+ items = _extract_items(result)
178
+
179
+ if not items:
180
+ if is_tty():
181
+ get_console().print(f"[dim]No views found for '{target_class}'.[/dim]")
182
+ else:
183
+ typer.echo("[]")
184
+ return
185
+
186
+ fmt = resolve_format(fmt)
187
+
188
+ if fmt == OutputFormat.json:
189
+ typer.echo(json_mod.dumps(result, ensure_ascii=False, indent=2))
190
+ return
191
+
192
+ _print_views_table(items, target_class)
193
+ except DWFError as exc:
194
+ handle_error(exc)
195
+
196
+
197
+ @_view_sub.command(name="create")
198
+ def create_view(
199
+ ctx: typer.Context,
200
+ class_name: Annotated[str, typer.Option("--class-name", "-c", help="Class name")],
201
+ view_name: Annotated[
202
+ str, typer.Option("--view-name", "-n", help="View name (English)")
203
+ ],
204
+ v3_content: Annotated[
205
+ str | None,
206
+ typer.Option(
207
+ "--v3-content",
208
+ help="Form JSON content (string)",
209
+ ),
210
+ ] = None,
211
+ v3_content_file: Annotated[
212
+ Path | None,
213
+ typer.Option(
214
+ "--v3-content-file",
215
+ help="Path to file containing form JSON content",
216
+ ),
217
+ ] = None,
218
+ display_name: Annotated[
219
+ str, typer.Option("--display-name", help="Display name (Chinese)")
220
+ ] = "",
221
+ note: Annotated[str, typer.Option("--note", help="Note")] = "",
222
+ is_relation: Annotated[
223
+ bool,
224
+ typer.Option("--is-relation", help="Whether this is a relation form"),
225
+ ] = False,
226
+ device_type: Annotated[
227
+ FormDeviceType,
228
+ typer.Option("--device-type", help="Device type"),
229
+ ] = FormDeviceType.pc,
230
+ form_type: Annotated[
231
+ FormType,
232
+ typer.Option("--form-type", help="Form type"),
233
+ ] = FormType.PC,
234
+ basic_info: Annotated[str, typer.Option("--basic-info", help="Basic info")] = "",
235
+ has_thumbnail: Annotated[
236
+ bool,
237
+ typer.Option("--has-thumbnail", help="Has thumbnail"),
238
+ ] = False,
239
+ widget_annotation: Annotated[
240
+ str, typer.Option("--widget-annotation", help="Widget annotation")
241
+ ] = "",
242
+ if_not_exists: Annotated[
243
+ bool,
244
+ typer.Option("--if-not-exists", help="Skip if view already exists"),
245
+ ] = False,
246
+ dry_run: Annotated[
247
+ bool,
248
+ typer.Option("--dry-run", help="Preview without executing"),
249
+ ] = False,
250
+ ) -> None:
251
+ try:
252
+ content = ""
253
+ if v3_content_file:
254
+ v3_content_file = validate_path(v3_content_file)
255
+ content = v3_content_file.read_text(encoding="utf-8")
256
+ elif v3_content:
257
+ content = v3_content
258
+ else:
259
+ if is_tty():
260
+ content = typer.prompt("v3Content (form JSON)")
261
+ else:
262
+ raise DWFError("--v3-content or --v3-content-file is required")
263
+
264
+ payload = _build_view_payload(
265
+ class_name=class_name,
266
+ view_name=view_name,
267
+ is_relation=is_relation,
268
+ v3_content=content,
269
+ display_name=display_name,
270
+ note=note,
271
+ device_type=device_type.value,
272
+ form_type=form_type.value,
273
+ basic_info=basic_info,
274
+ has_thumbnail=has_thumbnail,
275
+ widget_annotation=widget_annotation,
276
+ )
277
+
278
+ if dry_run:
279
+ typer.echo(
280
+ json_mod.dumps(
281
+ {
282
+ "action": "create",
283
+ "resource": "view",
284
+ "data": [payload],
285
+ "if_not_exists": if_not_exists,
286
+ "reversible": False,
287
+ },
288
+ ensure_ascii=False,
289
+ indent=2,
290
+ )
291
+ )
292
+ raise typer.Exit(code=DRY_RUN_EXIT_CODE)
293
+
294
+ client = get_client(ctx)
295
+ result = None
296
+ try:
297
+ result = formmodel_api.create_views(client, [payload])
298
+ typer.echo(f"Created view: {view_name}", err=True)
299
+ except ConflictError:
300
+ if if_not_exists:
301
+ typer.echo(
302
+ f"View already exists, skipped (--if-not-exists): {view_name}",
303
+ err=True,
304
+ )
305
+ else:
306
+ raise
307
+ if isinstance(result, dict):
308
+ typer.echo(json_mod.dumps(result, ensure_ascii=False, indent=2))
309
+ except DWFError as exc:
310
+ handle_error(exc)
311
+
312
+
313
+ @_view_sub.command(name="update")
314
+ def update_view(
315
+ ctx: typer.Context,
316
+ oid: Annotated[str, typer.Argument(help="View OID to update")],
317
+ class_name: Annotated[str, typer.Option("--class-name", "-c", help="Class name")],
318
+ view_name: Annotated[
319
+ str, typer.Option("--view-name", "-n", help="View name (English)")
320
+ ],
321
+ is_relation: Annotated[
322
+ bool,
323
+ typer.Option("--is-relation", help="Whether this is a relation form"),
324
+ ] = False,
325
+ v3_content: Annotated[
326
+ str | None,
327
+ typer.Option("--v3-content", help="Form JSON content (string)"),
328
+ ] = None,
329
+ v3_content_file: Annotated[
330
+ Path | None,
331
+ typer.Option("--v3-content-file", help="Path to form JSON content file"),
332
+ ] = None,
333
+ display_name: Annotated[
334
+ str, typer.Option("--display-name", help="Display name (Chinese)")
335
+ ] = "",
336
+ note: Annotated[str, typer.Option("--note", help="Note")] = "",
337
+ device_type: Annotated[
338
+ FormDeviceType,
339
+ typer.Option("--device-type", help="Device type"),
340
+ ] = FormDeviceType.pc,
341
+ form_type: Annotated[
342
+ FormType,
343
+ typer.Option("--form-type", help="Form type"),
344
+ ] = FormType.PC,
345
+ basic_info: Annotated[str, typer.Option("--basic-info", help="Basic info")] = "",
346
+ has_thumbnail: Annotated[
347
+ bool,
348
+ typer.Option("--has-thumbnail", help="Has thumbnail"),
349
+ ] = False,
350
+ widget_annotation: Annotated[
351
+ str, typer.Option("--widget-annotation", help="Widget annotation")
352
+ ] = "",
353
+ addin_hide_rule: Annotated[
354
+ str, typer.Option("--addin-hide-rule", help="Addin hide rule (JSON)")
355
+ ] = "[]",
356
+ force_update: Annotated[
357
+ bool,
358
+ typer.Option("--force-update", help="Force overwrite"),
359
+ ] = True,
360
+ dry_run: Annotated[
361
+ bool,
362
+ typer.Option("--dry-run", help="Preview without executing"),
363
+ ] = False,
364
+ ) -> None:
365
+ try:
366
+ content = ""
367
+ if v3_content_file:
368
+ v3_content_file = validate_path(v3_content_file)
369
+ content = v3_content_file.read_text(encoding="utf-8")
370
+ elif v3_content:
371
+ content = v3_content
372
+ else:
373
+ if is_tty():
374
+ content = typer.prompt("v3Content (form JSON)")
375
+ else:
376
+ raise DWFError("--v3-content or --v3-content-file is required")
377
+
378
+ import time
379
+
380
+ payload = _build_view_payload(
381
+ class_name=class_name,
382
+ view_name=view_name,
383
+ is_relation=is_relation,
384
+ v3_content=content,
385
+ display_name=display_name,
386
+ note=note,
387
+ device_type=device_type.value,
388
+ form_type=form_type.value,
389
+ basic_info=basic_info,
390
+ has_thumbnail=has_thumbnail,
391
+ widget_annotation=widget_annotation,
392
+ )
393
+ payload["oid"] = oid
394
+ payload["addinHideRule"] = addin_hide_rule
395
+ payload["lastModifyTime"] = int(time.time() * 1000)
396
+
397
+ if dry_run:
398
+ typer.echo(
399
+ json_mod.dumps(
400
+ {
401
+ "action": "update",
402
+ "resource": "view",
403
+ "target": oid,
404
+ "data": payload,
405
+ "reversible": True,
406
+ },
407
+ ensure_ascii=False,
408
+ indent=2,
409
+ )
410
+ )
411
+ raise typer.Exit(code=DRY_RUN_EXIT_CODE)
412
+
413
+ client = get_client(ctx)
414
+ result = formmodel_api.update_view(
415
+ client,
416
+ payload,
417
+ force_update=force_update,
418
+ )
419
+ typer.echo(f"Updated view: {oid}", err=True)
420
+ if isinstance(result, dict):
421
+ typer.echo(json_mod.dumps(result, ensure_ascii=False, indent=2))
422
+ except DWFError as exc:
423
+ handle_error(exc)
424
+
425
+
426
+ @_view_sub.command(name="copy")
427
+ def copy_view(
428
+ ctx: typer.Context,
429
+ oid: Annotated[str, typer.Argument(help="View OID to copy")],
430
+ new_class_name: Annotated[
431
+ str,
432
+ typer.Option("--new-class-name", help="Class name for the copied view"),
433
+ ] = "",
434
+ new_view_name: Annotated[
435
+ str,
436
+ typer.Option("--new-view-name", "-n", help="View name for the copy"),
437
+ ] = "",
438
+ new_device_type: Annotated[
439
+ FormDeviceType,
440
+ typer.Option("--new-device-type", help="Device type for the copy"),
441
+ ] = FormDeviceType.pc,
442
+ new_display_name: Annotated[
443
+ str,
444
+ typer.Option("--new-display-name", help="Display name for the copy"),
445
+ ] = "",
446
+ new_note: Annotated[
447
+ str,
448
+ typer.Option("--new-note", help="Note for the copy"),
449
+ ] = "",
450
+ new_form_type: Annotated[
451
+ FormType,
452
+ typer.Option("--new-form-type", help="Form type: PC or Mobile"),
453
+ ] = FormType.PC,
454
+ dry_run: Annotated[
455
+ bool,
456
+ typer.Option("--dry-run", help="Preview without executing"),
457
+ ] = False,
458
+ ) -> None:
459
+ try:
460
+ if dry_run:
461
+ typer.echo(
462
+ json_mod.dumps(
463
+ {
464
+ "action": "copy",
465
+ "resource": "view",
466
+ "target": oid,
467
+ "data": {
468
+ "newClassName": new_class_name,
469
+ "newViewName": new_view_name,
470
+ "newDeviceType": new_device_type.value,
471
+ "newDisplayName": new_display_name,
472
+ "newNote": new_note,
473
+ "newFormType": new_form_type.value,
474
+ },
475
+ "reversible": False,
476
+ },
477
+ ensure_ascii=False,
478
+ indent=2,
479
+ )
480
+ )
481
+ raise typer.Exit(code=DRY_RUN_EXIT_CODE)
482
+
483
+ client = get_client(ctx)
484
+ result = formmodel_api.copy_view(
485
+ client,
486
+ oid,
487
+ new_class_name=new_class_name,
488
+ new_view_name=new_view_name,
489
+ new_device_type=new_device_type.value,
490
+ new_display_name=new_display_name,
491
+ new_note=new_note,
492
+ new_form_type=new_form_type.value,
493
+ )
494
+ typer.echo(f"Copied view: {oid}", err=True)
495
+ if isinstance(result, dict):
496
+ typer.echo(json_mod.dumps(result, ensure_ascii=False, indent=2))
497
+ except DWFError as exc:
498
+ handle_error(exc)
499
+
500
+
501
+ @_view_sub.command(name="delete")
502
+ def delete_view(
503
+ ctx: typer.Context,
504
+ oid: Annotated[str, typer.Argument(help="View OID to delete")],
505
+ dry_run: Annotated[
506
+ bool,
507
+ typer.Option("--dry-run", help="Preview without executing"),
508
+ ] = False,
509
+ yes: Annotated[
510
+ bool,
511
+ typer.Option("--force", help="Skip confirmation prompt"),
512
+ ] = False,
513
+ ) -> None:
514
+ try:
515
+ if dry_run:
516
+ typer.echo(
517
+ json_mod.dumps(
518
+ {
519
+ "action": "delete",
520
+ "resource": "view",
521
+ "target": oid,
522
+ "reversible": False,
523
+ },
524
+ ensure_ascii=False,
525
+ indent=2,
526
+ )
527
+ )
528
+ raise typer.Exit(code=DRY_RUN_EXIT_CODE)
529
+
530
+ if is_tty() and not yes:
531
+ confirm = typer.confirm(f"Delete view {oid}?", default=False)
532
+ if not confirm:
533
+ typer.echo("Cancelled.", err=True)
534
+ raise typer.Exit(code=0)
535
+
536
+ client = get_client(ctx)
537
+ formmodel_api.delete_view(client, oid)
538
+ typer.echo(f"Deleted view: {oid}", err=True)
539
+ except DWFError as exc:
540
+ handle_error(exc)
541
+
542
+
543
+ _component_sub = typer.Typer(
544
+ help="Component management.", context_settings=_HELP_CONTEXT_SETTINGS
545
+ )
546
+
547
+ app.add_typer(_component_sub, name="component")
548
+
549
+
550
+ @_component_sub.command(name="create-group")
551
+ def create_component_group(
552
+ ctx: typer.Context,
553
+ name: Annotated[str, typer.Option("--name", "-n", help="Group name")],
554
+ display_name: Annotated[
555
+ str,
556
+ typer.Option("--display-name", "-d", help="Display name"),
557
+ ] = "",
558
+ dry_run: Annotated[
559
+ bool,
560
+ typer.Option("--dry-run", help="Preview without executing"),
561
+ ] = False,
562
+ ) -> None:
563
+ try:
564
+ if dry_run:
565
+ typer.echo(
566
+ json_mod.dumps(
567
+ {
568
+ "action": "create",
569
+ "resource": "component-group",
570
+ "data": {
571
+ "name": name,
572
+ "displayName": display_name,
573
+ },
574
+ "reversible": False,
575
+ },
576
+ ensure_ascii=False,
577
+ indent=2,
578
+ )
579
+ )
580
+ raise typer.Exit(code=DRY_RUN_EXIT_CODE)
581
+
582
+ client = get_client(ctx)
583
+ result = formmodel_api.create_component_group(client, name, display_name)
584
+ typer.echo(f"Created component group: {name}", err=True)
585
+ if isinstance(result, dict):
586
+ typer.echo(json_mod.dumps(result, ensure_ascii=False, indent=2))
587
+ except DWFError as exc:
588
+ handle_error(exc)
589
+
590
+
591
+ @_component_sub.command(name="tree")
592
+ def get_component_tree(
593
+ ctx: typer.Context,
594
+ need_v3_content: Annotated[
595
+ bool,
596
+ typer.Option(
597
+ "--need-v3-content",
598
+ help="Return v3Content and basicInfo",
599
+ ),
600
+ ] = False,
601
+ form_type: Annotated[
602
+ str,
603
+ typer.Option("--form-type", help="Form type filter: PC or Mobile"),
604
+ ] = "",
605
+ fmt: Annotated[
606
+ OutputFormat,
607
+ typer.Option("--format", "-f", help="Output format: table/json"),
608
+ ] = OutputFormat.json,
609
+ ) -> None:
610
+ try:
611
+ client = get_client(ctx)
612
+ result = formmodel_api.get_component_tree(
613
+ client,
614
+ need_v3_content=need_v3_content,
615
+ form_type=form_type,
616
+ )
617
+ fmt = resolve_format(fmt)
618
+ typer.echo(json_mod.dumps(result, ensure_ascii=False, indent=2))
619
+ except DWFError as exc:
620
+ handle_error(exc)
621
+
622
+
623
+ @_component_sub.command(name="create")
624
+ def create_component(
625
+ ctx: typer.Context,
626
+ view_name: Annotated[
627
+ str, typer.Option("--view-name", "-n", help="Component name (English)")
628
+ ],
629
+ display_name: Annotated[
630
+ str, typer.Option("--display-name", "-d", help="Component display name")
631
+ ],
632
+ form_type: Annotated[
633
+ FormType,
634
+ typer.Option("--form-type", help="Form type: PC or Mobile"),
635
+ ],
636
+ icon: Annotated[str, typer.Option("--icon", help="Icon")],
637
+ owner: Annotated[str, typer.Option("--owner", help="Owner component group OID")],
638
+ basic_info: Annotated[str, typer.Option("--basic-info", help="Basic info")] = "",
639
+ note: Annotated[str, typer.Option("--note", help="Note")] = "",
640
+ v3_content: Annotated[
641
+ str | None,
642
+ typer.Option("--v3-content", help="Form JSON content (string)"),
643
+ ] = None,
644
+ v3_content_file: Annotated[
645
+ Path | None,
646
+ typer.Option("--v3-content-file", help="Path to form JSON content file"),
647
+ ] = None,
648
+ widget_annotation: Annotated[
649
+ str, typer.Option("--widget-annotation", help="Widget annotation (JSON)")
650
+ ] = "{}",
651
+ dry_run: Annotated[
652
+ bool,
653
+ typer.Option("--dry-run", help="Preview without executing"),
654
+ ] = False,
655
+ ) -> None:
656
+ try:
657
+ content = ""
658
+ if v3_content_file:
659
+ v3_content_file = validate_path(v3_content_file)
660
+ content = v3_content_file.read_text(encoding="utf-8")
661
+ elif v3_content:
662
+ content = v3_content
663
+ else:
664
+ if is_tty():
665
+ content = typer.prompt("v3Content (form JSON)")
666
+ else:
667
+ raise DWFError("--v3-content or --v3-content-file is required")
668
+
669
+ payload = {
670
+ "basicInfo": basic_info,
671
+ "displayName": display_name,
672
+ "formType": form_type.value,
673
+ "icon": icon,
674
+ "note": note,
675
+ "owner": owner,
676
+ "v3Content": content,
677
+ "viewName": view_name,
678
+ "widgetAnnotation": widget_annotation,
679
+ }
680
+
681
+ if dry_run:
682
+ typer.echo(
683
+ json_mod.dumps(
684
+ {
685
+ "action": "create",
686
+ "resource": "component",
687
+ "data": [payload],
688
+ "reversible": False,
689
+ },
690
+ ensure_ascii=False,
691
+ indent=2,
692
+ )
693
+ )
694
+ raise typer.Exit(code=DRY_RUN_EXIT_CODE)
695
+
696
+ client = get_client(ctx)
697
+ result = formmodel_api.create_component(client, [payload])
698
+ typer.echo(f"Created component: {view_name}", err=True)
699
+ if isinstance(result, dict):
700
+ typer.echo(json_mod.dumps(result, ensure_ascii=False, indent=2))
701
+ except DWFError as exc:
702
+ handle_error(exc)
703
+
704
+
705
+ @_component_sub.command(name="update-group-rel")
706
+ def update_comp2compgroup(
707
+ ctx: typer.Context,
708
+ oid: Annotated[str, typer.Option("--oid", help="Relation OID of the component")],
709
+ component_oid: Annotated[
710
+ str, typer.Option("--component-oid", help="Component OID")
711
+ ],
712
+ component_group_oid: Annotated[
713
+ str,
714
+ typer.Option("--component-group-oid", help="Target component group OID"),
715
+ ],
716
+ component_display_name: Annotated[
717
+ str,
718
+ typer.Option("--display-name", "-d", help="Component display name"),
719
+ ] = "",
720
+ component_icon: Annotated[
721
+ str,
722
+ typer.Option("--icon", help="Component icon"),
723
+ ] = "",
724
+ component_note: Annotated[
725
+ str,
726
+ typer.Option("--note", help="Component note"),
727
+ ] = "",
728
+ dry_run: Annotated[
729
+ bool,
730
+ typer.Option("--dry-run", help="Preview without executing"),
731
+ ] = False,
732
+ ) -> None:
733
+ try:
734
+ if dry_run:
735
+ typer.echo(
736
+ json_mod.dumps(
737
+ {
738
+ "action": "update",
739
+ "resource": "comp2compgroup",
740
+ "data": {
741
+ "componentDisplayName": component_display_name,
742
+ "componentGroupOid": component_group_oid,
743
+ "componentIcon": component_icon,
744
+ "componentNote": component_note,
745
+ "componentOid": component_oid,
746
+ "oid": oid,
747
+ },
748
+ "reversible": True,
749
+ },
750
+ ensure_ascii=False,
751
+ indent=2,
752
+ )
753
+ )
754
+ raise typer.Exit(code=DRY_RUN_EXIT_CODE)
755
+
756
+ client = get_client(ctx)
757
+ result = formmodel_api.update_comp2compgroup(
758
+ client,
759
+ component_display_name=component_display_name,
760
+ component_group_oid=component_group_oid,
761
+ component_icon=component_icon,
762
+ component_note=component_note,
763
+ component_oid=component_oid,
764
+ oid=oid,
765
+ )
766
+ typer.echo(f"Updated comp2compgroup: {oid}", err=True)
767
+ if isinstance(result, dict):
768
+ typer.echo(json_mod.dumps(result, ensure_ascii=False, indent=2))
769
+ except DWFError as exc:
770
+ handle_error(exc)
771
+
772
+
773
+ @_component_sub.command(name="delete")
774
+ def delete_component(
775
+ ctx: typer.Context,
776
+ oid: Annotated[str, typer.Argument(help="Component OID to delete")],
777
+ dry_run: Annotated[
778
+ bool,
779
+ typer.Option("--dry-run", help="Preview without executing"),
780
+ ] = False,
781
+ yes: Annotated[
782
+ bool,
783
+ typer.Option("--force", help="Skip confirmation prompt"),
784
+ ] = False,
785
+ ) -> None:
786
+ try:
787
+ if dry_run:
788
+ typer.echo(
789
+ json_mod.dumps(
790
+ {
791
+ "action": "delete",
792
+ "resource": "component",
793
+ "target": oid,
794
+ "reversible": False,
795
+ },
796
+ ensure_ascii=False,
797
+ indent=2,
798
+ )
799
+ )
800
+ raise typer.Exit(code=DRY_RUN_EXIT_CODE)
801
+
802
+ if is_tty() and not yes:
803
+ confirm = typer.confirm(f"Delete component {oid}?", default=False)
804
+ if not confirm:
805
+ typer.echo("Cancelled.", err=True)
806
+ raise typer.Exit(code=0)
807
+
808
+ client = get_client(ctx)
809
+ formmodel_api.delete_component(client, oid)
810
+ typer.echo(f"Deleted component: {oid}", err=True)
811
+ except DWFError as exc:
812
+ handle_error(exc)
813
+
814
+
815
+ @_component_sub.command(name="delete-group")
816
+ def delete_component_group(
817
+ ctx: typer.Context,
818
+ oid: Annotated[str, typer.Argument(help="Component group OID to delete")],
819
+ dry_run: Annotated[
820
+ bool,
821
+ typer.Option("--dry-run", help="Preview without executing"),
822
+ ] = False,
823
+ yes: Annotated[
824
+ bool,
825
+ typer.Option("--force", help="Skip confirmation prompt"),
826
+ ] = False,
827
+ ) -> None:
828
+ try:
829
+ if dry_run:
830
+ typer.echo(
831
+ json_mod.dumps(
832
+ {
833
+ "action": "delete",
834
+ "resource": "component-group",
835
+ "target": oid,
836
+ "reversible": False,
837
+ },
838
+ ensure_ascii=False,
839
+ indent=2,
840
+ )
841
+ )
842
+ raise typer.Exit(code=DRY_RUN_EXIT_CODE)
843
+
844
+ if is_tty() and not yes:
845
+ confirm = typer.confirm(f"Delete component group {oid}?", default=False)
846
+ if not confirm:
847
+ typer.echo("Cancelled.", err=True)
848
+ raise typer.Exit(code=0)
849
+
850
+ client = get_client(ctx)
851
+ formmodel_api.delete_component_group(client, oid)
852
+ typer.echo(f"Deleted component group: {oid}", err=True)
853
+ except DWFError as exc:
854
+ handle_error(exc)
855
+
856
+
857
+ @_view_sub.command(name="get-by-device")
858
+ def get_view_by_device(
859
+ ctx: typer.Context,
860
+ class_name: Annotated[str, typer.Option("--class-name", "-c", help="Class name")],
861
+ view_name: Annotated[str, typer.Option("--view-name", "-n", help="View name")],
862
+ device_type: Annotated[
863
+ FormDeviceType,
864
+ typer.Option("--device-type", help="Device type"),
865
+ ],
866
+ form_type: Annotated[
867
+ FormType,
868
+ typer.Option("--form-type", help="Form type: PC or Mobile"),
869
+ ],
870
+ fmt: Annotated[
871
+ OutputFormat,
872
+ typer.Option("--format", "-f", help="Output format: table/json"),
873
+ ] = OutputFormat.json,
874
+ ) -> None:
875
+ try:
876
+ client = get_client(ctx)
877
+ result = formmodel_api.get_view_by_device_type(
878
+ client,
879
+ class_name=class_name,
880
+ view_name=view_name,
881
+ device_type=device_type.value,
882
+ form_type=form_type.value,
883
+ )
884
+ resolve_format(fmt)
885
+ typer.echo(json_mod.dumps(result, ensure_ascii=False, indent=2))
886
+ except DWFError as exc:
887
+ handle_error(exc)
888
+
889
+
890
+ _library_sub = typer.Typer(
891
+ help="Library management.", context_settings=_HELP_CONTEXT_SETTINGS
892
+ )
893
+
894
+ app.add_typer(_library_sub, name="library")
895
+
896
+
897
+ @_library_sub.command(name="files")
898
+ def list_library_files(
899
+ ctx: typer.Context,
900
+ library_id: Annotated[
901
+ str,
902
+ typer.Option("--library-id", "-l", help="Library ID"),
903
+ ] = "picture_management",
904
+ fmt: Annotated[
905
+ OutputFormat,
906
+ typer.Option("--format", "-f", help="Output format: table/json"),
907
+ ] = OutputFormat.json,
908
+ ) -> None:
909
+ try:
910
+ client = get_client(ctx)
911
+ result = formmodel_api.list_library_files(client, library_id)
912
+ resolve_format(fmt)
913
+ typer.echo(json_mod.dumps(result, ensure_ascii=False, indent=2))
914
+ except DWFError as exc:
915
+ handle_error(exc)
916
+
917
+
918
+ @_library_sub.command(name="upload")
919
+ def upload_file(
920
+ ctx: typer.Context,
921
+ file_path: Annotated[
922
+ Path,
923
+ typer.Option("--file-path", "-p", help="Path to the file to upload"),
924
+ ],
925
+ library_id: Annotated[
926
+ str,
927
+ typer.Option("--library-id", "-l", help="Library ID"),
928
+ ] = "",
929
+ dry_run: Annotated[
930
+ bool,
931
+ typer.Option("--dry-run", help="Preview without executing"),
932
+ ] = False,
933
+ ) -> None:
934
+ try:
935
+ file_path = validate_path(file_path)
936
+
937
+ if dry_run:
938
+ typer.echo(
939
+ json_mod.dumps(
940
+ {
941
+ "action": "upload",
942
+ "resource": "file",
943
+ "file": str(file_path),
944
+ "library_id": library_id or None,
945
+ "reversible": False,
946
+ },
947
+ ensure_ascii=False,
948
+ indent=2,
949
+ )
950
+ )
951
+ raise typer.Exit(code=DRY_RUN_EXIT_CODE)
952
+
953
+ file_content = file_path.read_bytes()
954
+ client = get_client(ctx)
955
+ result = formmodel_api.upload_file(
956
+ client,
957
+ file_content,
958
+ file_path.name,
959
+ library_id=library_id,
960
+ )
961
+ typer.echo(f"Uploaded file: {file_path.name}", err=True)
962
+ if isinstance(result, dict):
963
+ typer.echo(json_mod.dumps(result, ensure_ascii=False, indent=2))
964
+ except DWFError as exc:
965
+ handle_error(exc)
966
+
967
+
968
+ @_library_sub.command(name="delete")
969
+ def delete_file(
970
+ ctx: typer.Context,
971
+ file_oid: Annotated[str, typer.Argument(help="File OID to delete")],
972
+ dry_run: Annotated[
973
+ bool,
974
+ typer.Option("--dry-run", help="Preview without executing"),
975
+ ] = False,
976
+ yes: Annotated[
977
+ bool,
978
+ typer.Option("--force", help="Skip confirmation prompt"),
979
+ ] = False,
980
+ ) -> None:
981
+ try:
982
+ if dry_run:
983
+ typer.echo(
984
+ json_mod.dumps(
985
+ {
986
+ "action": "delete",
987
+ "resource": "file",
988
+ "target": file_oid,
989
+ "reversible": False,
990
+ },
991
+ ensure_ascii=False,
992
+ indent=2,
993
+ )
994
+ )
995
+ raise typer.Exit(code=DRY_RUN_EXIT_CODE)
996
+
997
+ if is_tty() and not yes:
998
+ confirm = typer.confirm(f"Delete file {file_oid}?", default=False)
999
+ if not confirm:
1000
+ typer.echo("Cancelled.", err=True)
1001
+ raise typer.Exit(code=0)
1002
+
1003
+ client = get_client(ctx)
1004
+ formmodel_api.delete_file(client, file_oid)
1005
+ typer.echo(f"Deleted file: {file_oid}", err=True)
1006
+ except DWFError as exc:
1007
+ handle_error(exc)