qwenpaw-data-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,5 @@
1
+ """Standalone command-line interface for QwenPaw Data."""
2
+
3
+ from .main import build_parser, main
4
+
5
+ __all__ = ["build_parser", "main"]
@@ -0,0 +1,9 @@
1
+ """QwenPaw Data CLI command registry."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from . import chat, datasource, doctor, execute, plan, run, semantic
6
+
7
+ COMMANDS = [plan, execute, run, chat, datasource, semantic, doctor]
8
+
9
+ __all__ = ["COMMANDS"]
@@ -0,0 +1,56 @@
1
+ """Chat command."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import sys
7
+
8
+ from qwenpaw_data.cli.util import (
9
+ add_datasource_id_arg,
10
+ add_permission_mode_arg,
11
+ add_workspace_arg,
12
+ build_cli_confirmation_handler,
13
+ create_qwenpaw_data,
14
+ print_execution_summary,
15
+ print_msg,
16
+ request_context_from_args,
17
+ resolve_permission_mode,
18
+ resolve_workspace_type,
19
+ )
20
+
21
+
22
+ def register(subparsers: argparse._SubParsersAction) -> None:
23
+ parser = subparsers.add_parser("chat", help="Start an interactive chat")
24
+ add_datasource_id_arg(parser)
25
+ add_workspace_arg(parser)
26
+ add_permission_mode_arg(parser)
27
+ parser.set_defaults(handler=handle)
28
+
29
+
30
+ async def handle(args: argparse.Namespace) -> int:
31
+ workspace_type = resolve_workspace_type(args)
32
+ dp = create_qwenpaw_data(
33
+ request_context=request_context_from_args(args),
34
+ workspace_type=workspace_type,
35
+ permission_mode=resolve_permission_mode(args, workspace_type),
36
+ confirmation_handler=build_cli_confirmation_handler(),
37
+ )
38
+ print("QwenPaw Data chat. Type 'exit' or 'quit' to quit.", file=sys.stderr)
39
+ try:
40
+ while True:
41
+ try:
42
+ text = input("> ").strip()
43
+ except EOFError:
44
+ print()
45
+ return 0
46
+
47
+ if not text:
48
+ continue
49
+ if text.lower() in {"exit", "quit"}:
50
+ return 0
51
+
52
+ msg = await dp.run(text)
53
+ print_msg(msg)
54
+ print_execution_summary(msg)
55
+ finally:
56
+ await dp.close()
@@ -0,0 +1,281 @@
1
+ """Context Manager datasource discovery and management commands."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ from qwenpaw_data.host.core.cm_client import CMDatasource, ContextManagerClient
10
+ from qwenpaw_data.host.core.semantic_config_client import (
11
+ SEMANTIC_CONFIG_PREFIX,
12
+ SemanticConfigClient,
13
+ )
14
+
15
+ from qwenpaw_data.cli.util import (
16
+ confirm_deletion,
17
+ load_json_object,
18
+ parse_json_object,
19
+ print_json,
20
+ )
21
+
22
+ MASKED_SECRET = "******"
23
+ SENSITIVE_CONFIG_FIELDS = frozenset(
24
+ {
25
+ "password",
26
+ "access_key_id",
27
+ "access_key_secret",
28
+ "sts_token",
29
+ },
30
+ )
31
+
32
+ _DATASOURCE_PATH = f"{SEMANTIC_CONFIG_PREFIX}/datasource"
33
+
34
+
35
+ def register(subparsers: argparse._SubParsersAction) -> None:
36
+ parser = subparsers.add_parser(
37
+ "datasource",
38
+ help="Manage datasources configured in DataBridge",
39
+ )
40
+ datasource_subparsers = parser.add_subparsers(
41
+ dest="datasource_command",
42
+ required=True,
43
+ )
44
+
45
+ list_parser = datasource_subparsers.add_parser(
46
+ "list",
47
+ help="List all DataBridge datasources",
48
+ )
49
+ list_parser.set_defaults(handler=handle_list)
50
+
51
+ get_parser = datasource_subparsers.add_parser(
52
+ "get",
53
+ help="Show one datasource",
54
+ )
55
+ get_parser.add_argument("datasource_id", help="Datasource id")
56
+ get_parser.add_argument(
57
+ "--show-config",
58
+ action="store_true",
59
+ help="Include the connection config with sensitive fields masked",
60
+ )
61
+ get_parser.set_defaults(handler=handle_get)
62
+
63
+ create_parser = datasource_subparsers.add_parser(
64
+ "create",
65
+ help="Create a datasource (requires a credentials:manage API key)",
66
+ )
67
+ create_parser.add_argument("--name", required=True, help="Datasource name")
68
+ create_parser.add_argument(
69
+ "--type",
70
+ required=True,
71
+ dest="datasource_type",
72
+ help="Datasource type, e.g. postgresql / mysql / odps",
73
+ )
74
+ _add_config_args(create_parser, required=True)
75
+ create_parser.add_argument(
76
+ "--test",
77
+ action="store_true",
78
+ help="Test the connection first and abort the create on failure",
79
+ )
80
+ create_parser.set_defaults(handler=handle_create)
81
+
82
+ update_parser = datasource_subparsers.add_parser(
83
+ "update",
84
+ help="Update a datasource; a provided config replaces the stored one",
85
+ )
86
+ update_parser.add_argument("datasource_id", help="Datasource id")
87
+ update_parser.add_argument("--name", help="New datasource name")
88
+ update_parser.add_argument(
89
+ "--type",
90
+ dest="datasource_type",
91
+ help="New datasource type",
92
+ )
93
+ _add_config_args(update_parser, required=False)
94
+ update_parser.set_defaults(handler=handle_update)
95
+
96
+ delete_parser = datasource_subparsers.add_parser(
97
+ "delete",
98
+ help="Delete a datasource",
99
+ )
100
+ delete_parser.add_argument("datasource_id", help="Datasource id")
101
+ delete_parser.add_argument(
102
+ "--yes",
103
+ action="store_true",
104
+ help="Skip the interactive confirmation",
105
+ )
106
+ delete_parser.set_defaults(handler=handle_delete)
107
+
108
+ test_parser = datasource_subparsers.add_parser(
109
+ "test",
110
+ help="Test connectivity of a saved datasource or an ad-hoc config",
111
+ )
112
+ test_parser.add_argument(
113
+ "datasource_id",
114
+ nargs="?",
115
+ help="Saved datasource id (omit when testing an ad-hoc config)",
116
+ )
117
+ test_parser.add_argument(
118
+ "--type",
119
+ dest="datasource_type",
120
+ help="Datasource type for an ad-hoc connection test",
121
+ )
122
+ _add_config_args(test_parser, required=False)
123
+ test_parser.set_defaults(handler=handle_test)
124
+
125
+
126
+ def _add_config_args(parser: argparse.ArgumentParser, *, required: bool) -> None:
127
+ group = parser.add_mutually_exclusive_group(required=required)
128
+ group.add_argument(
129
+ "--config-file",
130
+ type=Path,
131
+ help="Path to a JSON file with the connection config",
132
+ )
133
+ group.add_argument(
134
+ "--config",
135
+ dest="config_inline",
136
+ help="Inline JSON object with the connection config",
137
+ )
138
+
139
+
140
+ def _load_config(args: argparse.Namespace) -> dict[str, Any] | None:
141
+ if getattr(args, "config_file", None) is not None:
142
+ return load_json_object(args.config_file)
143
+ if getattr(args, "config_inline", None) is not None:
144
+ return parse_json_object(args.config_inline, flag="--config")
145
+ return None
146
+
147
+
148
+ def _mask_config(config: dict[str, Any] | None) -> dict[str, Any] | None:
149
+ if config is None:
150
+ return None
151
+ return {
152
+ key: MASKED_SECRET
153
+ if key in SENSITIVE_CONFIG_FIELDS and value not in (None, "")
154
+ else value
155
+ for key, value in config.items()
156
+ }
157
+
158
+
159
+ def _masked_record(record: dict[str, Any], *, include_config: bool) -> dict[str, Any]:
160
+ payload = {
161
+ "datasource_id": record.get("datasource_id"),
162
+ "datasource_name": record.get("datasource_name"),
163
+ "datasource_type": record.get("datasource_type"),
164
+ }
165
+ if include_config:
166
+ config = record.get("config")
167
+ payload["config"] = _mask_config(config if isinstance(config, dict) else None)
168
+ return payload
169
+
170
+
171
+ def _output_item(item: CMDatasource) -> dict[str, Any]:
172
+ return {
173
+ "datasource_id": item.datasource_id,
174
+ "datasource_name": item.datasource_name,
175
+ "datasource_type": item.datasource_type,
176
+ "config": _mask_config(item.config),
177
+ }
178
+
179
+
180
+ def handle_list(_: argparse.Namespace) -> int:
181
+ result = ContextManagerClient().list_datasources()
182
+ payload = {
183
+ "items": [_output_item(item) for item in result.items],
184
+ "total": result.total,
185
+ }
186
+ print_json(payload)
187
+ return 0
188
+
189
+
190
+ def handle_get(args: argparse.Namespace) -> int:
191
+ record = SemanticConfigClient().get(
192
+ f"{_DATASOURCE_PATH}/{args.datasource_id}",
193
+ )
194
+ print_json(_masked_record(record, include_config=args.show_config))
195
+ return 0
196
+
197
+
198
+ def handle_create(args: argparse.Namespace) -> int:
199
+ config = _load_config(args)
200
+ client = SemanticConfigClient()
201
+ if args.test:
202
+ result = client.post(
203
+ f"{_DATASOURCE_PATH}/test-connection",
204
+ json={"datasource_type": args.datasource_type, "config": config},
205
+ )
206
+ if not result.get("success"):
207
+ raise ValueError(
208
+ f"connection test failed, datasource not created: {result.get('message')}",
209
+ )
210
+ record = client.post(
211
+ _DATASOURCE_PATH,
212
+ json={
213
+ "datasource_name": args.name,
214
+ "datasource_type": args.datasource_type,
215
+ "config": config,
216
+ },
217
+ )
218
+ print_json(_masked_record(record, include_config=True))
219
+ return 0
220
+
221
+
222
+ def handle_update(args: argparse.Namespace) -> int:
223
+ config = _load_config(args)
224
+ payload: dict[str, Any] = {}
225
+ if args.name is not None:
226
+ payload["datasource_name"] = args.name
227
+ if args.datasource_type is not None:
228
+ payload["datasource_type"] = args.datasource_type
229
+ if config is not None:
230
+ payload["config"] = config
231
+ if not payload:
232
+ raise ValueError("nothing to update: pass --name, --type, or a config")
233
+ record = SemanticConfigClient().put(
234
+ f"{_DATASOURCE_PATH}/{args.datasource_id}",
235
+ json=payload,
236
+ )
237
+ print_json(_masked_record(record, include_config=True))
238
+ return 0
239
+
240
+
241
+ def handle_delete(args: argparse.Namespace) -> int:
242
+ if not confirm_deletion(f"datasource {args.datasource_id}", assume_yes=args.yes):
243
+ print("aborted")
244
+ return 1
245
+ result = SemanticConfigClient().delete(
246
+ f"{_DATASOURCE_PATH}/{args.datasource_id}",
247
+ )
248
+ print_json(result if result else {"deleted": args.datasource_id})
249
+ return 0
250
+
251
+
252
+ def handle_test(args: argparse.Namespace) -> int:
253
+ config = _load_config(args)
254
+ ad_hoc = args.datasource_type is not None or config is not None
255
+ if args.datasource_id and ad_hoc:
256
+ raise ValueError("pass either a datasource id or --type with a config, not both")
257
+ client = SemanticConfigClient()
258
+ if args.datasource_id:
259
+ result = client.post(
260
+ f"{_DATASOURCE_PATH}/{args.datasource_id}/test-connection",
261
+ )
262
+ elif args.datasource_type is not None and config is not None:
263
+ result = client.post(
264
+ f"{_DATASOURCE_PATH}/test-connection",
265
+ json={"datasource_type": args.datasource_type, "config": config},
266
+ )
267
+ else:
268
+ raise ValueError("pass a datasource id, or --type together with a config")
269
+ print_json(result)
270
+ return 0 if result.get("success") else 1
271
+
272
+
273
+ __all__ = [
274
+ "handle_create",
275
+ "handle_delete",
276
+ "handle_get",
277
+ "handle_list",
278
+ "handle_test",
279
+ "handle_update",
280
+ "register",
281
+ ]