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
dwf_cli/cli/_common.py ADDED
@@ -0,0 +1,152 @@
1
+ from __future__ import annotations
2
+
3
+ import json as json_mod
4
+ import os
5
+ import sys
6
+ from dataclasses import dataclass
7
+ from pathlib import Path
8
+ from typing import Any
9
+
10
+ import typer
11
+ from rich.console import Console
12
+
13
+ from dwf_cli.api.client import APIClient
14
+ from dwf_cli.core.config import Config
15
+ from dwf_cli.core.errors import AuthError, DWFError
16
+ from dwf_cli.core.output import OutputFormat
17
+
18
+ _SENSITIVE_DIRS = {".ssh", ".gnupg", ".aws", ".kube"}
19
+
20
+
21
+ @dataclass
22
+ class ContextObj:
23
+ config: Config
24
+ client: APIClient | None
25
+ app_client: APIClient | None = None
26
+
27
+
28
+ @dataclass
29
+ class SuccessResult:
30
+ action: str
31
+ resource: str
32
+ detail: str | None = None
33
+ data: Any = None
34
+
35
+
36
+ def report_success(
37
+ result: SuccessResult, fmt: OutputFormat = OutputFormat.table
38
+ ) -> None:
39
+ fmt = resolve_format(fmt)
40
+ if fmt == OutputFormat.json:
41
+ obj: dict[str, Any] = {
42
+ "success": True,
43
+ "action": result.action,
44
+ "resource": result.resource,
45
+ }
46
+ if result.detail:
47
+ obj["detail"] = result.detail
48
+ if result.data is not None:
49
+ obj["data"] = result.data
50
+ typer.echo(json_mod.dumps(obj, ensure_ascii=False, indent=2))
51
+ else:
52
+ msg = f"{result.action} {result.resource}"
53
+ if result.detail:
54
+ msg += f": {result.detail}"
55
+ Console().print(f"[green]{msg}[/green]")
56
+
57
+
58
+ def is_tty() -> bool:
59
+ return sys.stdout.isatty()
60
+
61
+
62
+ def _should_disable_color() -> bool:
63
+ if os.environ.get("NO_COLOR") is not None:
64
+ return True
65
+ if os.environ.get("TERM") == "dumb":
66
+ return True
67
+ if not is_tty():
68
+ return True
69
+ return False
70
+
71
+
72
+ _console: Console | None = None
73
+
74
+
75
+ def get_console() -> Console:
76
+ global _console
77
+ if _console is None:
78
+ _console = Console(no_color=_should_disable_color())
79
+ return _console
80
+
81
+
82
+ def resolve_format(fmt: OutputFormat) -> OutputFormat:
83
+ if fmt == OutputFormat.table and not is_tty():
84
+ return OutputFormat.json
85
+ return fmt
86
+
87
+
88
+ def validate_path(path: Path) -> Path:
89
+ resolved = path.resolve()
90
+ for part in resolved.parts:
91
+ if part in _SENSITIVE_DIRS:
92
+ raise DWFError(f"Path contains sensitive directory: {part}")
93
+ if ".." in str(path):
94
+ raise DWFError("Path traversal not allowed")
95
+ return resolved
96
+
97
+
98
+ def get_client(ctx: typer.Context) -> APIClient:
99
+ obj: ContextObj = ctx.obj
100
+ if obj.client is None:
101
+ raise AuthError("Not logged in")
102
+ return obj.client
103
+
104
+
105
+ def get_app_client(ctx: typer.Context) -> APIClient:
106
+ obj: ContextObj = ctx.obj
107
+ if obj.app_client is not None:
108
+ return obj.app_client
109
+ if obj.client is not None:
110
+ return obj.client
111
+ raise AuthError("Not logged in")
112
+
113
+
114
+ def handle_error(exc: DWFError) -> None:
115
+ if is_tty():
116
+ parts = [f"Error: {exc}"]
117
+ if exc.suggestion:
118
+ parts.append(f" 建议: {exc.suggestion}")
119
+ if exc.detail:
120
+ parts.append(f" 详情: {exc.detail}")
121
+ typer.echo("\n".join(parts), err=True)
122
+ else:
123
+ _print_error_json(
124
+ type(exc).__name__,
125
+ str(exc),
126
+ exc.suggestion,
127
+ retryable=exc.retryable,
128
+ detail=exc.detail,
129
+ )
130
+ raise typer.Exit(code=exc.exit_code)
131
+
132
+
133
+ def _print_error_json(
134
+ error_type: str,
135
+ message: str,
136
+ suggestion: str | None,
137
+ retryable: bool = False,
138
+ *,
139
+ detail: str | None = None,
140
+ ) -> None:
141
+ import json as json_mod
142
+
143
+ error_obj: dict[str, Any] = {
144
+ "error": error_type,
145
+ "message": message,
146
+ "retryable": retryable,
147
+ }
148
+ if suggestion:
149
+ error_obj["suggestion"] = suggestion
150
+ if detail:
151
+ error_obj["detail"] = detail
152
+ typer.echo(json_mod.dumps(error_obj, ensure_ascii=False), err=True)
dwf_cli/cli/auth.py ADDED
@@ -0,0 +1,197 @@
1
+ from __future__ import annotations
2
+
3
+ import json as json_mod
4
+ from pathlib import Path
5
+ from typing import Annotated
6
+
7
+ import typer
8
+
9
+ from dwf_cli.api import auth as auth_api
10
+ from dwf_cli.api.client import APIClient
11
+ from dwf_cli.cli._common import (
12
+ ContextObj,
13
+ handle_error,
14
+ is_tty,
15
+ resolve_format,
16
+ validate_path,
17
+ )
18
+ from dwf_cli.core.errors import AuthError, ConfigError, DWFError
19
+ from dwf_cli.core.output import OutputFormat
20
+
21
+ _HELP_CONTEXT_SETTINGS = {"help_option_names": ["-h", "--help"]}
22
+
23
+ DRY_RUN_EXIT_CODE = 10
24
+
25
+ app = typer.Typer(
26
+ help="User authentication.\n\n"
27
+ "Examples:\n"
28
+ " dwf-cli auth login --username admin --password-file /path/to/password\n"
29
+ " dwf-cli auth login --username admin (prompts for password in TTY)\n"
30
+ " dwf-cli auth status\n"
31
+ " dwf-cli auth logout",
32
+ context_settings=_HELP_CONTEXT_SETTINGS,
33
+ )
34
+
35
+
36
+ @app.command()
37
+ def login(
38
+ ctx: typer.Context,
39
+ username: Annotated[
40
+ str,
41
+ typer.Option(
42
+ prompt=is_tty(),
43
+ help="Username (required; will prompt in TTY mode)",
44
+ ),
45
+ ],
46
+ password_file: Annotated[
47
+ Path | None,
48
+ typer.Option(
49
+ "--password-file",
50
+ help="Read password from file (first line, trailing newline stripped)",
51
+ ),
52
+ ] = None,
53
+ dry_run: Annotated[
54
+ bool,
55
+ typer.Option("--dry-run", help="Preview without executing"),
56
+ ] = False,
57
+ ) -> None:
58
+ try:
59
+ password = ""
60
+ if password_file:
61
+ password_file = validate_path(password_file)
62
+ password = password_file.read_text(encoding="utf-8").splitlines()[0]
63
+ elif is_tty():
64
+ password = typer.prompt("Password", hide_input=True)
65
+ else:
66
+ raise AuthError(
67
+ "Password required in non-TTY mode",
68
+ detail="Use --password-file or run in TTY mode",
69
+ )
70
+
71
+ obj: ContextObj = ctx.obj
72
+ server = obj.config.server
73
+ if not server:
74
+ raise ConfigError(
75
+ "Server not configured",
76
+ detail="Run 'dwf-cli config set server <url>' first",
77
+ )
78
+
79
+ if dry_run:
80
+ typer.echo(
81
+ json_mod.dumps(
82
+ {
83
+ "action": "login",
84
+ "resource": "auth",
85
+ "server": server,
86
+ "username": username,
87
+ "reversible": True,
88
+ },
89
+ ensure_ascii=False,
90
+ indent=2,
91
+ )
92
+ )
93
+ raise typer.Exit(code=DRY_RUN_EXIT_CODE)
94
+
95
+ temp_client = APIClient(server)
96
+ token = auth_api.login(
97
+ temp_client,
98
+ username,
99
+ password,
100
+ aes_key=obj.config.aes_key,
101
+ aes_iv=obj.config.aes_iv,
102
+ )
103
+ obj.config.save_token(token)
104
+ typer.echo(f"Logged in to {server}", err=True)
105
+ except DWFError as exc:
106
+ handle_error(exc)
107
+
108
+
109
+ @app.command()
110
+ def logout(
111
+ ctx: typer.Context,
112
+ dry_run: Annotated[
113
+ bool,
114
+ typer.Option("--dry-run", help="Preview without executing"),
115
+ ] = False,
116
+ ) -> None:
117
+ try:
118
+ obj: ContextObj = ctx.obj
119
+ if dry_run:
120
+ typer.echo(
121
+ json_mod.dumps(
122
+ {
123
+ "action": "logout",
124
+ "resource": "auth",
125
+ "has_token": bool(obj.config.token),
126
+ "reversible": True,
127
+ },
128
+ ensure_ascii=False,
129
+ indent=2,
130
+ )
131
+ )
132
+ raise typer.Exit(code=DRY_RUN_EXIT_CODE)
133
+
134
+ obj.config.clear_token()
135
+ typer.echo("Logged out.", err=True)
136
+ except DWFError as exc:
137
+ handle_error(exc)
138
+
139
+
140
+ @app.command()
141
+ def status(
142
+ ctx: typer.Context,
143
+ fmt: Annotated[
144
+ OutputFormat,
145
+ typer.Option("--format", "-f", help="Output format: table/json"),
146
+ ] = OutputFormat.table,
147
+ ) -> None:
148
+ obj: ContextObj = ctx.obj
149
+ fmt = resolve_format(fmt)
150
+
151
+ if not obj.config.token:
152
+ if fmt == OutputFormat.json:
153
+ typer.echo(json_mod.dumps({"logged_in": False}))
154
+ else:
155
+ typer.echo("Not logged in.")
156
+ return
157
+
158
+ if obj.client is None:
159
+ if fmt == OutputFormat.json:
160
+ typer.echo(json_mod.dumps({"logged_in": False, "reason": "no_server"}))
161
+ else:
162
+ typer.echo("Not logged in (no server configured).")
163
+ return
164
+
165
+ try:
166
+ valid = auth_api.validate_token(obj.client)
167
+ if valid:
168
+ if fmt == OutputFormat.json:
169
+ typer.echo(
170
+ json_mod.dumps(
171
+ {"logged_in": True, "server": obj.config.server},
172
+ ensure_ascii=False,
173
+ )
174
+ )
175
+ else:
176
+ typer.echo(f"Logged in ({obj.config.server})")
177
+ else:
178
+ if fmt == OutputFormat.json:
179
+ typer.echo(
180
+ json_mod.dumps(
181
+ {
182
+ "logged_in": False,
183
+ "server": obj.config.server,
184
+ "reason": "token_expired",
185
+ },
186
+ ensure_ascii=False,
187
+ )
188
+ )
189
+ else:
190
+ typer.echo("Token expired. Run 'dwf-cli auth login' again.")
191
+ except DWFError:
192
+ if fmt == OutputFormat.json:
193
+ typer.echo(
194
+ json_mod.dumps({"logged_in": False, "reason": "server_unreachable"})
195
+ )
196
+ else:
197
+ typer.echo("Token expired or server unreachable.")
dwf_cli/cli/config.py ADDED
@@ -0,0 +1,200 @@
1
+ from __future__ import annotations
2
+
3
+ import json as json_mod
4
+ from pathlib import Path
5
+ from typing import Annotated, Any
6
+
7
+ import httpx
8
+ import typer
9
+
10
+ from dwf_cli.cli._common import ContextObj, handle_error, resolve_format
11
+ from dwf_cli.core.config import parse_config_js, resolve_config_js_url
12
+ from dwf_cli.core.errors import ConfigError, DWFError
13
+ from dwf_cli.core.output import OutputFormat
14
+
15
+ _HELP_CONTEXT_SETTINGS = {"help_option_names": ["-h", "--help"]}
16
+
17
+ DRY_RUN_EXIT_CODE = 10
18
+
19
+ app = typer.Typer(
20
+ help="CLI configuration.\n\n"
21
+ "Examples:\n"
22
+ " dwf-cli config set server https://your-dwf-server.com\n"
23
+ " dwf-cli config get server\n"
24
+ " dwf-cli config list\n"
25
+ " dwf-cli config detect http://host:6060 --save",
26
+ context_settings=_HELP_CONTEXT_SETTINGS,
27
+ )
28
+
29
+
30
+ @app.command(name="list")
31
+ def list_config(
32
+ ctx: typer.Context,
33
+ fmt: Annotated[
34
+ OutputFormat,
35
+ typer.Option("--format", "-f", help="Output format: table/json"),
36
+ ] = OutputFormat.table,
37
+ ) -> None:
38
+ try:
39
+ obj: ContextObj = ctx.obj
40
+ data = obj.config.list_all()
41
+ fmt = resolve_format(fmt)
42
+
43
+ if fmt == OutputFormat.json:
44
+ typer.echo(json_mod.dumps(data, ensure_ascii=False, indent=2))
45
+ else:
46
+ for key, value in data.items():
47
+ typer.echo(f" {key} = {value}")
48
+ except DWFError as exc:
49
+ handle_error(exc)
50
+
51
+
52
+ @app.command()
53
+ def get(
54
+ ctx: typer.Context,
55
+ key: Annotated[str, typer.Argument(help="Config key")],
56
+ fmt: Annotated[
57
+ OutputFormat,
58
+ typer.Option("--format", "-f", help="Output format: table/json"),
59
+ ] = OutputFormat.table,
60
+ ) -> None:
61
+ try:
62
+ obj: ContextObj = ctx.obj
63
+ value = obj.config.get(key)
64
+ if value is None:
65
+ raise ConfigError(f"Key not found: {key}")
66
+
67
+ fmt = resolve_format(fmt)
68
+
69
+ if fmt == OutputFormat.json:
70
+ typer.echo(json_mod.dumps({key: value}, ensure_ascii=False, indent=2))
71
+ else:
72
+ typer.echo(f"{key} = {value}")
73
+ except DWFError as exc:
74
+ handle_error(exc)
75
+
76
+
77
+ @app.command()
78
+ def set(
79
+ ctx: typer.Context,
80
+ key: Annotated[str, typer.Argument(help="Config key")],
81
+ value: Annotated[str, typer.Argument(help="Config value")],
82
+ dry_run: Annotated[
83
+ bool,
84
+ typer.Option("--dry-run", help="Preview without executing"),
85
+ ] = False,
86
+ ) -> None:
87
+ try:
88
+ if dry_run:
89
+ typer.echo(
90
+ json_mod.dumps(
91
+ {
92
+ "action": "set",
93
+ "resource": "config",
94
+ "key": key,
95
+ "value": value,
96
+ "reversible": True,
97
+ },
98
+ ensure_ascii=False,
99
+ indent=2,
100
+ )
101
+ )
102
+ raise typer.Exit(code=DRY_RUN_EXIT_CODE)
103
+
104
+ obj: ContextObj = ctx.obj
105
+ obj.config.set(key, value)
106
+ typer.echo(f"Set {key} = {value}", err=True)
107
+ except DWFError as exc:
108
+ handle_error(exc)
109
+
110
+
111
+ @app.command()
112
+ def detect(
113
+ ctx: typer.Context,
114
+ source: Annotated[
115
+ str,
116
+ typer.Argument(
117
+ help="DWF server URL or local config.js path. "
118
+ "Examples: http://host:6060, http://host/modeler-web, ./config.js"
119
+ ),
120
+ ],
121
+ save: Annotated[
122
+ bool,
123
+ typer.Option("--save", help="Save detected config to config file"),
124
+ ] = False,
125
+ dry_run: Annotated[
126
+ bool,
127
+ typer.Option("--dry-run", help="Preview without saving"),
128
+ ] = False,
129
+ fmt: Annotated[
130
+ OutputFormat,
131
+ typer.Option("--format", "-f", help="Output format: table/json"),
132
+ ] = OutputFormat.table,
133
+ ) -> None:
134
+ try:
135
+ resolved = resolve_config_js_url(source)
136
+ content: str | None = None
137
+ actual_url: str | None = None
138
+
139
+ p = Path(resolved)
140
+ if p.exists() and p.is_file():
141
+ content = p.read_text(encoding="utf-8")
142
+ else:
143
+ actual_url = resolved
144
+ try:
145
+ resp = httpx.get(resolved, timeout=15.0, follow_redirects=True)
146
+ resp.raise_for_status()
147
+ content = resp.text
148
+ except httpx.HTTPError as exc:
149
+ err = ConfigError(f"Failed to fetch config.js from {resolved}: {exc}")
150
+ err.suggestion = "Check the URL or provide a local config.js file path"
151
+ raise err
152
+
153
+ detected = parse_config_js(content, source_url=actual_url)
154
+ if not detected:
155
+ err = ConfigError(f"No DWF config fields found in {resolved}")
156
+ err.suggestion = (
157
+ "Ensure the file contains modelerApiBase or appApiBase fields"
158
+ )
159
+ raise err
160
+
161
+ if dry_run or not save:
162
+ fmt = resolve_format(fmt)
163
+ if fmt == OutputFormat.json:
164
+ output: dict[str, Any] = {
165
+ "source": resolved,
166
+ "detected": detected,
167
+ "saved": False,
168
+ }
169
+ typer.echo(json_mod.dumps(output, ensure_ascii=False, indent=2))
170
+ else:
171
+ typer.echo(f"Detected from {resolved}:", err=True)
172
+ for key, value in detected.items():
173
+ typer.echo(f" {key} = {value}", err=True)
174
+ if not save and not dry_run:
175
+ typer.echo(" (use --save to write to config)", err=True)
176
+ if dry_run:
177
+ raise typer.Exit(code=DRY_RUN_EXIT_CODE)
178
+ else:
179
+ obj: ContextObj = ctx.obj
180
+ updated = obj.config.bulk_set(detected)
181
+ fmt = resolve_format(fmt)
182
+ if fmt == OutputFormat.json:
183
+ typer.echo(
184
+ json_mod.dumps(
185
+ {
186
+ "source": resolved,
187
+ "detected": detected,
188
+ "saved": True,
189
+ "updated_keys": updated,
190
+ },
191
+ ensure_ascii=False,
192
+ indent=2,
193
+ )
194
+ )
195
+ else:
196
+ typer.echo(f"Detected and saved from {resolved}:", err=True)
197
+ for key in updated:
198
+ typer.echo(f" {key} = {detected[key]}", err=True)
199
+ except DWFError as exc:
200
+ handle_error(exc)