licos-platform-cli 0.3.2__tar.gz → 0.3.3__tar.gz
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.
- {licos_platform_cli-0.3.2 → licos_platform_cli-0.3.3}/PKG-INFO +1 -1
- {licos_platform_cli-0.3.2 → licos_platform_cli-0.3.3}/pyproject.toml +1 -1
- {licos_platform_cli-0.3.2 → licos_platform_cli-0.3.3}/src/licos_platform_cli/help_commands.py +28 -1
- {licos_platform_cli-0.3.2 → licos_platform_cli-0.3.3}/src/licos_platform_cli/uns_commands.py +79 -2
- {licos_platform_cli-0.3.2 → licos_platform_cli-0.3.3}/tests/test_cli.py +53 -0
- {licos_platform_cli-0.3.2 → licos_platform_cli-0.3.3}/.gitignore +0 -0
- {licos_platform_cli-0.3.2 → licos_platform_cli-0.3.3}/src/licos_platform_cli/__init__.py +0 -0
- {licos_platform_cli-0.3.2 → licos_platform_cli-0.3.3}/src/licos_platform_cli/database_commands.py +0 -0
- {licos_platform_cli-0.3.2 → licos_platform_cli-0.3.3}/src/licos_platform_cli/knowledge_commands.py +0 -0
- {licos_platform_cli-0.3.2 → licos_platform_cli-0.3.3}/src/licos_platform_cli/main.py +0 -0
- {licos_platform_cli-0.3.2 → licos_platform_cli-0.3.3}/src/licos_platform_cli/oauth_commands.py +0 -0
- {licos_platform_cli-0.3.2 → licos_platform_cli-0.3.3}/src/licos_platform_cli/storage_commands.py +0 -0
{licos_platform_cli-0.3.2 → licos_platform_cli-0.3.3}/src/licos_platform_cli/help_commands.py
RENAMED
|
@@ -647,7 +647,34 @@ UNS_HELP = {
|
|
|
647
647
|
],
|
|
648
648
|
"example": "licos-platform uns directory-create --parent-path project:/ --node-json '{\"name\":\"一号产线\",\"code\":\"LINE_1\"}'",
|
|
649
649
|
},
|
|
650
|
-
"
|
|
650
|
+
"connection-update": {
|
|
651
|
+
"description": "生成并规划一个当前项目托管采集连接的增量修改,不直接执行写入。",
|
|
652
|
+
"parameters": [
|
|
653
|
+
{"name": "--selector-json", "required": True, "description": "精确的 name、protocol 选择器。"},
|
|
654
|
+
{"name": "--set-json", "required": True, "description": "name、credentialRef 或 configPatch。"},
|
|
655
|
+
{"name": "--output", "required": False, "description": "在项目目录内保存生成的 UNSChangeSet。"},
|
|
656
|
+
],
|
|
657
|
+
"example": "licos-platform uns connection-update --selector-json '{\"name\":\"一号PLC\",\"protocol\":\"modbus-tcp\"}' --set-json '{\"configPatch\":{\"host\":\"10.0.0.2\"}}' --output UNS.change.json",
|
|
658
|
+
},
|
|
659
|
+
"collection-task-update": {
|
|
660
|
+
"description": "生成并规划一个当前项目托管采集任务的增量修改,不直接执行写入。",
|
|
661
|
+
"parameters": [
|
|
662
|
+
{"name": "--selector-json", "required": True, "description": "精确的 name、protocol、connectionName 选择器。"},
|
|
663
|
+
{"name": "--set-json", "required": True, "description": "name、pollIntervalMs、enabled 或 configPatch。"},
|
|
664
|
+
{"name": "--output", "required": False, "description": "在项目目录内保存生成的 UNSChangeSet。"},
|
|
665
|
+
],
|
|
666
|
+
"example": "licos-platform uns collection-task-update --selector-json '{\"name\":\"温度采集\",\"protocol\":\"modbus-tcp\",\"connectionName\":\"一号PLC\"}' --set-json '{\"pollIntervalMs\":2000}' --output UNS.change.json",
|
|
667
|
+
},
|
|
668
|
+
"point-config-update": {
|
|
669
|
+
"description": "生成并规划一个当前项目托管采集点位配置的增量修改,不改变点位编码和所属任务。",
|
|
670
|
+
"parameters": [
|
|
671
|
+
{"name": "--selector-json", "required": True, "description": "精确的 tagCode、taskName、protocol 选择器。"},
|
|
672
|
+
{"name": "--set-json", "required": True, "description": "点位通用字段或协议 addressPatch。"},
|
|
673
|
+
{"name": "--output", "required": False, "description": "在项目目录内保存生成的 UNSChangeSet。"},
|
|
674
|
+
],
|
|
675
|
+
"example": "licos-platform uns point-config-update --selector-json '{\"tagCode\":\"TEMP_01\",\"taskName\":\"温度采集\",\"protocol\":\"modbus-tcp\"}' --set-json '{\"addressPatch\":{\"address\":\"40002\"}}' --output UNS.change.json",
|
|
676
|
+
},
|
|
677
|
+
"finalize": {
|
|
651
678
|
"description": "Validate and atomically install a candidate as project-root UNS.json, revalidate it, and return a read-only plan.",
|
|
652
679
|
"parameters": [{"name": "--file", "required": True, "description": "Candidate JSON file inside LICOS_PROJECT_PATH."}],
|
|
653
680
|
"example": "licos-platform uns finalize --file .UNS.json.candidate",
|
{licos_platform_cli-0.3.2 → licos_platform_cli-0.3.3}/src/licos_platform_cli/uns_commands.py
RENAMED
|
@@ -77,6 +77,35 @@ def _change_set(operation: dict[str, Any]) -> dict[str, Any]:
|
|
|
77
77
|
}
|
|
78
78
|
|
|
79
79
|
|
|
80
|
+
def _plan_generated_change(operation: dict[str, Any], output: str | None) -> Any:
|
|
81
|
+
document = _change_set(operation)
|
|
82
|
+
if output is None:
|
|
83
|
+
return uns.plan_changes(document)
|
|
84
|
+
output_path = _write_change_set(output, document)
|
|
85
|
+
return {
|
|
86
|
+
"changeSetFile": str(output_path),
|
|
87
|
+
"plan": uns.plan_changes(document),
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _write_change_set(path: str, document: dict[str, Any]) -> Path:
|
|
92
|
+
project_root = Path(os.environ.get("LICOS_PROJECT_PATH", "/workspace/projects")).resolve()
|
|
93
|
+
output = Path(path).resolve()
|
|
94
|
+
if not output.is_relative_to(project_root):
|
|
95
|
+
raise argparse.ArgumentTypeError("change-set output must be inside LICOS_PROJECT_PATH")
|
|
96
|
+
output.parent.mkdir(parents=True, exist_ok=True)
|
|
97
|
+
payload = (json.dumps(document, ensure_ascii=False, indent=2) + "\n").encode("utf-8")
|
|
98
|
+
with tempfile.NamedTemporaryFile(
|
|
99
|
+
mode="wb", prefix=".UNS.change.", suffix=".tmp", dir=output.parent, delete=False
|
|
100
|
+
) as staging:
|
|
101
|
+
staging.write(payload)
|
|
102
|
+
staging.flush()
|
|
103
|
+
os.fsync(staging.fileno())
|
|
104
|
+
staging_path = Path(staging.name)
|
|
105
|
+
os.replace(staging_path, output)
|
|
106
|
+
return output
|
|
107
|
+
|
|
108
|
+
|
|
80
109
|
def _locator(path: str | None, node_code: str | None, label: str) -> dict[str, str]:
|
|
81
110
|
if bool(path) == bool(node_code):
|
|
82
111
|
raise argparse.ArgumentTypeError(f"provide exactly one {label} path or node code")
|
|
@@ -210,6 +239,30 @@ def _cmd_directory_create(args: argparse.Namespace) -> Any:
|
|
|
210
239
|
"parent": _locator(args.parent_path, args.parent_node_code, "parent"),
|
|
211
240
|
"node": _json_object(args.node_json, "directory node"),
|
|
212
241
|
}))
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def _cmd_connection_update(args: argparse.Namespace) -> Any:
|
|
245
|
+
return _plan_generated_change({
|
|
246
|
+
"op": "connectionUpdate",
|
|
247
|
+
"selector": _json_object(args.selector_json, "connection selector"),
|
|
248
|
+
"set": _json_object(args.set_json, "connection update"),
|
|
249
|
+
}, args.output)
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def _cmd_collection_task_update(args: argparse.Namespace) -> Any:
|
|
253
|
+
return _plan_generated_change({
|
|
254
|
+
"op": "collectionTaskUpdate",
|
|
255
|
+
"selector": _json_object(args.selector_json, "collection task selector"),
|
|
256
|
+
"set": _json_object(args.set_json, "collection task update"),
|
|
257
|
+
}, args.output)
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def _cmd_point_config_update(args: argparse.Namespace) -> Any:
|
|
261
|
+
return _plan_generated_change({
|
|
262
|
+
"op": "collectionPointUpdate",
|
|
263
|
+
"selector": _json_object(args.selector_json, "collection point selector"),
|
|
264
|
+
"set": _json_object(args.set_json, "collection point update"),
|
|
265
|
+
}, args.output)
|
|
213
266
|
|
|
214
267
|
|
|
215
268
|
def _cmd_data_query(args: argparse.Namespace) -> Any:
|
|
@@ -484,8 +537,32 @@ def add_uns_parser(subparsers: argparse._SubParsersAction[argparse.ArgumentParse
|
|
|
484
537
|
directory_parent.add_argument("--parent-node-code")
|
|
485
538
|
directory_create.add_argument("--node-json", required=True)
|
|
486
539
|
_set_handler(directory_create, _cmd_directory_create)
|
|
487
|
-
|
|
488
|
-
|
|
540
|
+
|
|
541
|
+
connection_update = commands.add_parser(
|
|
542
|
+
"connection-update", help="Plan updating one project-managed collection connection"
|
|
543
|
+
)
|
|
544
|
+
connection_update.add_argument("--selector-json", required=True)
|
|
545
|
+
connection_update.add_argument("--set-json", required=True)
|
|
546
|
+
connection_update.add_argument("--output")
|
|
547
|
+
_set_handler(connection_update, _cmd_connection_update)
|
|
548
|
+
|
|
549
|
+
task_update = commands.add_parser(
|
|
550
|
+
"collection-task-update", help="Plan updating one project-managed collection task"
|
|
551
|
+
)
|
|
552
|
+
task_update.add_argument("--selector-json", required=True)
|
|
553
|
+
task_update.add_argument("--set-json", required=True)
|
|
554
|
+
task_update.add_argument("--output")
|
|
555
|
+
_set_handler(task_update, _cmd_collection_task_update)
|
|
556
|
+
|
|
557
|
+
point_config_update = commands.add_parser(
|
|
558
|
+
"point-config-update", help="Plan updating one project-managed collection point configuration"
|
|
559
|
+
)
|
|
560
|
+
point_config_update.add_argument("--selector-json", required=True)
|
|
561
|
+
point_config_update.add_argument("--set-json", required=True)
|
|
562
|
+
point_config_update.add_argument("--output")
|
|
563
|
+
_set_handler(point_config_update, _cmd_point_config_update)
|
|
564
|
+
|
|
565
|
+
finalize = commands.add_parser(
|
|
489
566
|
"finalize", help="Validate and atomically install a candidate as project UNS.json, then create a plan"
|
|
490
567
|
)
|
|
491
568
|
finalize.add_argument("--file", required=True)
|
|
@@ -345,6 +345,59 @@ class PlatformCliTests(unittest.TestCase):
|
|
|
345
345
|
self.assertEqual(operation["target"], {"nodeCode": "TEMP_1"})
|
|
346
346
|
self.assertEqual(operation["selector"]["tagCode"], "T1")
|
|
347
347
|
self.assertNotIn("fieldMappings", operation)
|
|
348
|
+
|
|
349
|
+
def test_uns_collection_update_commands_build_strict_changesets(self) -> None:
|
|
350
|
+
cases = [
|
|
351
|
+
(
|
|
352
|
+
"connection-update",
|
|
353
|
+
'{"name":"PLC","protocol":"modbus-tcp"}',
|
|
354
|
+
'{"configPatch":{"host":"10.0.0.2"}}',
|
|
355
|
+
"connectionUpdate",
|
|
356
|
+
),
|
|
357
|
+
(
|
|
358
|
+
"collection-task-update",
|
|
359
|
+
'{"name":"采集任务","protocol":"modbus-tcp","connectionName":"PLC"}',
|
|
360
|
+
'{"pollIntervalMs":2000}',
|
|
361
|
+
"collectionTaskUpdate",
|
|
362
|
+
),
|
|
363
|
+
(
|
|
364
|
+
"point-config-update",
|
|
365
|
+
'{"tagCode":"T1","taskName":"采集任务","protocol":"modbus-tcp"}',
|
|
366
|
+
'{"addressPatch":{"address":"40002"}}',
|
|
367
|
+
"collectionPointUpdate",
|
|
368
|
+
),
|
|
369
|
+
]
|
|
370
|
+
for command, selector, update, expected_op in cases:
|
|
371
|
+
with self.subTest(command=command):
|
|
372
|
+
with mock.patch.object(uns_commands.uns, "plan_changes", return_value={}) as plan_changes:
|
|
373
|
+
code, _stdout, stderr = run_cli(
|
|
374
|
+
"uns", command, "--selector-json", selector, "--set-json", update
|
|
375
|
+
)
|
|
376
|
+
|
|
377
|
+
self.assertEqual(code, 0)
|
|
378
|
+
self.assertEqual(stderr, "")
|
|
379
|
+
operation = plan_changes.call_args.args[0]["operations"][0]
|
|
380
|
+
self.assertEqual(operation["op"], expected_op)
|
|
381
|
+
|
|
382
|
+
def test_uns_collection_update_can_persist_the_exact_planned_changeset(self) -> None:
|
|
383
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
384
|
+
output = Path(tmp) / "UNS.change.json"
|
|
385
|
+
with mock.patch.dict(os.environ, {"LICOS_PROJECT_PATH": tmp}, clear=False):
|
|
386
|
+
with mock.patch.object(
|
|
387
|
+
uns_commands.uns, "plan_changes", return_value={"planHash": "plan-hash"}
|
|
388
|
+
) as plan_changes:
|
|
389
|
+
code, stdout, stderr = run_cli(
|
|
390
|
+
"uns", "connection-update",
|
|
391
|
+
"--selector-json", '{"name":"PLC","protocol":"modbus-tcp"}',
|
|
392
|
+
"--set-json", '{"configPatch":{"host":"10.0.0.2"}}',
|
|
393
|
+
"--output", str(output),
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
self.assertEqual(code, 0)
|
|
397
|
+
self.assertEqual(stderr, "")
|
|
398
|
+
saved = json.loads(output.read_text(encoding="utf-8"))
|
|
399
|
+
self.assertEqual(saved, plan_changes.call_args.args[0])
|
|
400
|
+
self.assertEqual(json.loads(stdout)["plan"]["planHash"], "plan-hash")
|
|
348
401
|
|
|
349
402
|
def test_uns_finalize_atomically_replaces_target_after_validation(self) -> None:
|
|
350
403
|
document = {"schemaVersion": "1.0", "resources": {}, "uns": {"nodes": []}}
|
|
File without changes
|
|
File without changes
|
{licos_platform_cli-0.3.2 → licos_platform_cli-0.3.3}/src/licos_platform_cli/database_commands.py
RENAMED
|
File without changes
|
{licos_platform_cli-0.3.2 → licos_platform_cli-0.3.3}/src/licos_platform_cli/knowledge_commands.py
RENAMED
|
File without changes
|
|
File without changes
|
{licos_platform_cli-0.3.2 → licos_platform_cli-0.3.3}/src/licos_platform_cli/oauth_commands.py
RENAMED
|
File without changes
|
{licos_platform_cli-0.3.2 → licos_platform_cli-0.3.3}/src/licos_platform_cli/storage_commands.py
RENAMED
|
File without changes
|