licos-platform-cli 0.4.0__tar.gz → 0.4.1__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.4.0 → licos_platform_cli-0.4.1}/PKG-INFO +1 -1
- {licos_platform_cli-0.4.0 → licos_platform_cli-0.4.1}/pyproject.toml +2 -2
- {licos_platform_cli-0.4.0 → licos_platform_cli-0.4.1}/src/licos_platform_cli/help_commands.py +163 -150
- {licos_platform_cli-0.4.0 → licos_platform_cli-0.4.1}/src/licos_platform_cli/industrial_commands.py +335 -335
- {licos_platform_cli-0.4.0 → licos_platform_cli-0.4.1}/src/licos_platform_cli/main.py +5 -5
- {licos_platform_cli-0.4.0 → licos_platform_cli-0.4.1}/tests/test_cli.py +56 -56
- {licos_platform_cli-0.4.0 → licos_platform_cli-0.4.1}/.gitignore +0 -0
- {licos_platform_cli-0.4.0 → licos_platform_cli-0.4.1}/src/licos_platform_cli/__init__.py +0 -0
- {licos_platform_cli-0.4.0 → licos_platform_cli-0.4.1}/src/licos_platform_cli/database_commands.py +0 -0
- {licos_platform_cli-0.4.0 → licos_platform_cli-0.4.1}/src/licos_platform_cli/knowledge_commands.py +0 -0
- {licos_platform_cli-0.4.0 → licos_platform_cli-0.4.1}/src/licos_platform_cli/oauth_commands.py +0 -0
- {licos_platform_cli-0.4.0 → licos_platform_cli-0.4.1}/src/licos_platform_cli/storage_commands.py +0 -0
- {licos_platform_cli-0.4.0 → licos_platform_cli-0.4.1}/src/licos_platform_cli/uns_commands.py +0 -0
|
@@ -4,11 +4,11 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "licos-platform-cli"
|
|
7
|
-
version = "0.4.
|
|
7
|
+
version = "0.4.1"
|
|
8
8
|
description = "LICOS platform CLI"
|
|
9
9
|
requires-python = ">=3.10"
|
|
10
10
|
dependencies = [
|
|
11
|
-
"licos-platform-sdk>=0.5.0",
|
|
11
|
+
"licos-platform-sdk>=0.5.0",
|
|
12
12
|
]
|
|
13
13
|
|
|
14
14
|
[project.scripts]
|
{licos_platform_cli-0.4.0 → licos_platform_cli-0.4.1}/src/licos_platform_cli/help_commands.py
RENAMED
|
@@ -31,92 +31,105 @@ STORAGE_HELP = {
|
|
|
31
31
|
"node": "import { storage } from '@kmlckj/licos-platform-sdk';\nconst file = await storage.uploadFile('/workspace/projects/report.pdf');\nconst link = await storage.shareUrl(file.id);",
|
|
32
32
|
},
|
|
33
33
|
"commands": {
|
|
34
|
-
"summary": {
|
|
35
|
-
"description": "Return project storage summary.",
|
|
36
|
-
"parameters": [],
|
|
37
|
-
"
|
|
38
|
-
|
|
34
|
+
"summary": {
|
|
35
|
+
"description": "Return project storage summary.",
|
|
36
|
+
"parameters": [],
|
|
37
|
+
"returns": "Storage summary object: IDs/scope strings, usedSizeBytes integer, fileCount/folderCount integers, serviceEnabled boolean.",
|
|
38
|
+
"example": "licos-platform storage summary",
|
|
39
|
+
},
|
|
39
40
|
"folders": {
|
|
40
|
-
"description": "List all folders in the current project storage scope.",
|
|
41
|
-
"parameters": [],
|
|
42
|
-
"
|
|
43
|
-
|
|
41
|
+
"description": "List all folders in the current project storage scope.",
|
|
42
|
+
"parameters": [],
|
|
43
|
+
"returns": "Array of folder objects. Folder IDs are durable operation identifiers; parentId is null for root.",
|
|
44
|
+
"example": "licos-platform storage folders",
|
|
45
|
+
},
|
|
44
46
|
"entries": {
|
|
45
47
|
"description": "List folders and files under a folder; omit folder-id for root.",
|
|
46
|
-
"parameters": [{"name": "--folder-id", "required": False, "description": "Folder ID. Omit for root."}],
|
|
47
|
-
"
|
|
48
|
-
|
|
48
|
+
"parameters": [{"name": "--folder-id", "type": "string", "required": False, "description": "Folder ID. Omit for root."}],
|
|
49
|
+
"returns": "Directory entries object with currentFolderId, currentFolderName, currentFolderPath, breadcrumbs, folders, and files.",
|
|
50
|
+
"example": "licos-platform storage entries --folder-id folder-id",
|
|
51
|
+
},
|
|
49
52
|
"files": {
|
|
50
|
-
"description": "List all files in the current project storage scope.",
|
|
51
|
-
"parameters": [],
|
|
52
|
-
"
|
|
53
|
-
|
|
53
|
+
"description": "List all files in the current project storage scope.",
|
|
54
|
+
"parameters": [],
|
|
55
|
+
"returns": "Array of file objects. downloadUrl is temporary; use id/storageKey as durable identity.",
|
|
56
|
+
"example": "licos-platform storage files",
|
|
57
|
+
},
|
|
54
58
|
"create-folder": {
|
|
55
59
|
"description": "Create a folder under root or a parent folder.",
|
|
56
60
|
"parameters": [
|
|
57
|
-
{"name": "folder_name", "required": True, "description": "
|
|
58
|
-
{"name": "--parent-id", "required": False, "description": "Parent folder ID. Omit for root."},
|
|
59
|
-
],
|
|
61
|
+
{"name": "folder_name", "type": "string", "required": True, "description": "Non-empty new folder name."},
|
|
62
|
+
{"name": "--parent-id", "type": "string", "required": False, "description": "Parent folder ID. Omit for root."},
|
|
63
|
+
],
|
|
64
|
+
"returns": "Created folder object.",
|
|
60
65
|
"example": "licos-platform storage create-folder reports --parent-id folder-id",
|
|
61
66
|
},
|
|
62
67
|
"rename-folder": {
|
|
63
68
|
"description": "Rename a folder by folder ID.",
|
|
64
69
|
"parameters": [
|
|
65
|
-
{"name": "folder_id", "required": True, "description": "
|
|
66
|
-
{"name": "folder_name", "required": True, "description": "
|
|
67
|
-
],
|
|
70
|
+
{"name": "folder_id", "type": "string", "required": True, "description": "Exact folder ID."},
|
|
71
|
+
{"name": "folder_name", "type": "string", "required": True, "description": "Non-empty new folder name."},
|
|
72
|
+
],
|
|
73
|
+
"returns": "Updated folder object.",
|
|
68
74
|
"example": "licos-platform storage rename-folder folder-id reports",
|
|
69
75
|
},
|
|
70
76
|
"delete-folder": {
|
|
71
77
|
"description": "Delete a folder by folder ID.",
|
|
72
|
-
"parameters": [{"name": "folder_id", "required": True, "description": "
|
|
78
|
+
"parameters": [{"name": "folder_id", "type": "string", "required": True, "description": "Exact folder ID."}],
|
|
79
|
+
"returns": "{\"success\": true} on success.",
|
|
73
80
|
"example": "licos-platform storage delete-folder folder-id",
|
|
74
81
|
},
|
|
75
82
|
"upload-file": {
|
|
76
|
-
"description": "Upload a local file up to
|
|
77
|
-
"parameters": [
|
|
78
|
-
{"name": "file", "required": True, "description": "
|
|
79
|
-
{"name": "--folder-id", "required": False, "description": "Target folder ID. Omit for root."},
|
|
80
|
-
{"name": "--file-name", "required": False, "description": "Override uploaded display file name."},
|
|
81
|
-
{"name": "--content-type", "required": False, "description": "MIME type, for example application/pdf."},
|
|
82
|
-
],
|
|
83
|
+
"description": "Upload a local file up to 100 MiB. Files over 8 MiB automatically use multipart upload; always use the returned file object.",
|
|
84
|
+
"parameters": [
|
|
85
|
+
{"name": "file", "type": "path", "required": True, "description": "Readable local file path; maximum size 100 MiB."},
|
|
86
|
+
{"name": "--folder-id", "type": "string", "required": False, "description": "Target folder ID. Omit for root."},
|
|
87
|
+
{"name": "--file-name", "type": "string", "required": False, "description": "Override uploaded display file name."},
|
|
88
|
+
{"name": "--content-type", "type": "MIME string", "required": False, "description": "MIME type, for example application/pdf."},
|
|
89
|
+
],
|
|
90
|
+
"returns": "Uploaded file object with durable id/storageKey metadata and a temporary downloadUrl.",
|
|
83
91
|
"example": "licos-platform storage upload-file ./report.pdf --folder-id folder-id --file-name report.pdf --content-type application/pdf",
|
|
84
92
|
},
|
|
85
93
|
"rename-file": {
|
|
86
94
|
"description": "Rename a file by file ID.",
|
|
87
95
|
"parameters": [
|
|
88
|
-
{"name": "file_id", "required": True, "description": "
|
|
89
|
-
{"name": "file_name", "required": True, "description": "
|
|
90
|
-
],
|
|
96
|
+
{"name": "file_id", "type": "string", "required": True, "description": "Exact file ID."},
|
|
97
|
+
{"name": "file_name", "type": "string", "required": True, "description": "Non-empty new file name."},
|
|
98
|
+
],
|
|
99
|
+
"returns": "Updated file object.",
|
|
91
100
|
"example": "licos-platform storage rename-file file-id final.pdf",
|
|
92
101
|
},
|
|
93
102
|
"move-file": {
|
|
94
103
|
"description": "Move a file to a folder; omit folder-id to move to root.",
|
|
95
104
|
"parameters": [
|
|
96
|
-
{"name": "file_id", "required": True, "description": "
|
|
97
|
-
{"name": "--folder-id", "required": False, "description": "Target folder ID. Omit
|
|
98
|
-
],
|
|
105
|
+
{"name": "file_id", "type": "string", "required": True, "description": "Exact file ID."},
|
|
106
|
+
{"name": "--folder-id", "type": "string", "required": False, "description": "Target folder ID. Omit to move to root."},
|
|
107
|
+
],
|
|
108
|
+
"returns": "Updated file object.",
|
|
99
109
|
"example": "licos-platform storage move-file file-id --folder-id folder-id",
|
|
100
110
|
},
|
|
101
111
|
"share-url": {
|
|
102
112
|
"description": "Generate a temporary share URL for a file.",
|
|
103
113
|
"parameters": [
|
|
104
|
-
{"name": "file_id", "required": True, "description": "
|
|
105
|
-
{"name": "--expiry-seconds", "required": False, "description": "URL lifetime in seconds."},
|
|
106
|
-
],
|
|
114
|
+
{"name": "file_id", "type": "string", "required": True, "description": "Exact file ID."},
|
|
115
|
+
{"name": "--expiry-seconds", "type": "integer", "required": False, "description": "Positive URL lifetime in seconds. Omit for the platform default of 7 days."},
|
|
116
|
+
],
|
|
117
|
+
"returns": "Object with temporary url string and expiresAt datetime string.",
|
|
107
118
|
"example": "licos-platform storage share-url file-id --expiry-seconds 3600",
|
|
108
119
|
},
|
|
109
120
|
"object-exists": {
|
|
110
121
|
"description": "Check whether an object exists by file ID or object key.",
|
|
111
122
|
"parameters": [
|
|
112
|
-
{"name": "--file-id", "required": False, "description": "File ID."},
|
|
113
|
-
{"name": "--object-key", "required": False, "description": "Storage object key."},
|
|
114
|
-
],
|
|
123
|
+
{"name": "--file-id", "type": "string", "required": False, "description": "File ID. Takes precedence when both selectors are provided."},
|
|
124
|
+
{"name": "--object-key", "type": "string", "required": False, "description": "Storage object key. At least one selector is required."},
|
|
125
|
+
],
|
|
126
|
+
"returns": "Object with fileId, bucketName, objectKey, and boolean exists.",
|
|
115
127
|
"example": "licos-platform storage object-exists --file-id file-id",
|
|
116
128
|
},
|
|
117
129
|
"delete-file": {
|
|
118
130
|
"description": "Delete a file by file ID.",
|
|
119
|
-
"parameters": [{"name": "file_id", "required": True, "description": "
|
|
131
|
+
"parameters": [{"name": "file_id", "type": "string", "required": True, "description": "Exact file ID."}],
|
|
132
|
+
"returns": "{\"success\": true} on success.",
|
|
120
133
|
"example": "licos-platform storage delete-file file-id",
|
|
121
134
|
},
|
|
122
135
|
},
|
|
@@ -767,116 +780,116 @@ UNS_HELP = {
|
|
|
767
780
|
}
|
|
768
781
|
|
|
769
782
|
|
|
770
|
-
INDUSTRIAL_HELP = {
|
|
771
|
-
"topic": "industrial",
|
|
772
|
-
"description": "Project-scoped direct collection and video integration without creating UNS nodes.",
|
|
773
|
-
"runtime": RUNTIME_ENV,
|
|
774
|
-
"rules": [
|
|
775
|
-
"Use only when the user explicitly requests direct collector access or explicitly says not to create UNS.",
|
|
776
|
-
"CLI and SDK call platform APIs; never request or expose IOTDATA API keys or MQTT credentials.",
|
|
777
|
-
"Search before create. Update and delete require the versionHash returned by resource-get/search.",
|
|
778
|
-
"Apply requires reviewed documentHash and planHash. Delete also requires --confirm-impact.",
|
|
779
|
-
"Realtime defaults to WebSocket; MQTT must be selected explicitly. Point control defaults to HTTP; MQTT must be selected explicitly.",
|
|
780
|
-
"Transport failures do not silently switch to another transport.",
|
|
781
|
-
],
|
|
782
|
-
"resourceTypes": [
|
|
783
|
-
"edgeAgent (read-only)",
|
|
784
|
-
"connection",
|
|
785
|
-
"collectionTask",
|
|
786
|
-
"collectionPoint",
|
|
787
|
-
"videoEdge",
|
|
788
|
-
"videoGateway",
|
|
789
|
-
"videoDevice",
|
|
790
|
-
"videoChannel",
|
|
791
|
-
],
|
|
792
|
-
"commands": {
|
|
793
|
-
"protocol-schema": {
|
|
794
|
-
"description": "Get required and default fields for one collection protocol.",
|
|
795
|
-
"parameters": [{"name": "protocol", "required": True, "description": "Protocol code."}],
|
|
796
|
-
"example": "licos-platform industrial protocol-schema modbus-tcp",
|
|
797
|
-
},
|
|
798
|
-
"resource-search": {
|
|
799
|
-
"description": "Search project-managed or enterprise collection/video resources.",
|
|
800
|
-
"parameters": [
|
|
801
|
-
{"name": "--resource-type", "required": True, "description": "Allowlisted resource type."},
|
|
802
|
-
{"name": "--scope", "required": False, "description": "project or enterprise."},
|
|
803
|
-
{"name": "--protocol", "required": False, "description": "Collection protocol filter."},
|
|
804
|
-
{"name": "--keyword", "required": False, "description": "Name/code/address keyword."},
|
|
805
|
-
{"name": "--selector-json", "required": False, "description": "Exact field selector object."},
|
|
806
|
-
],
|
|
807
|
-
"example": "licos-platform industrial resource-search --resource-type collectionPoint --scope enterprise --keyword TEMP",
|
|
808
|
-
},
|
|
809
|
-
"resource-get": {
|
|
810
|
-
"description": "Get one resource, writable state, and concurrency versionHash.",
|
|
811
|
-
"parameters": [
|
|
812
|
-
{"name": "--resource-type", "required": True, "description": "Allowlisted resource type."},
|
|
813
|
-
{"name": "--resource-id", "required": True, "description": "Resource ID."},
|
|
814
|
-
],
|
|
815
|
-
"example": "licos-platform industrial resource-get --resource-type collectionPoint --resource-id point-id",
|
|
816
|
-
},
|
|
817
|
-
"changes-validate / changes-plan / changes-apply": {
|
|
818
|
-
"description": "Validate, review, and apply an IndustrialChangeSet JSON file.",
|
|
819
|
-
"parameters": [
|
|
820
|
-
{"name": "--file", "required": True, "description": "IndustrialChangeSet JSON file."},
|
|
821
|
-
{"name": "--document-hash / --plan-hash", "required": True, "description": "Required only by apply."},
|
|
822
|
-
{"name": "--confirm", "required": True, "description": "Required only by apply."},
|
|
823
|
-
{"name": "--confirm-impact", "required": False, "description": "Required when the plan contains delete impact."},
|
|
824
|
-
],
|
|
825
|
-
"example": "licos-platform industrial changes-plan --file ./industrial.change.json",
|
|
826
|
-
},
|
|
827
|
-
"resource-create / resource-update / resource-delete": {
|
|
828
|
-
"description": "Generate and plan one operation; these commands do not apply writes.",
|
|
829
|
-
"parameters": [
|
|
830
|
-
{"name": "--resource-type", "required": True, "description": "Allowlisted resource type."},
|
|
831
|
-
{"name": "--spec-json / --set-json", "required": False, "description": "Create or update fields."},
|
|
832
|
-
{"name": "--resource-id / --version-hash", "required": False, "description": "Required by update/delete."},
|
|
833
|
-
{"name": "--output", "required": False, "description": "Write the generated ChangeSet under LICOS_PROJECT_PATH."},
|
|
834
|
-
],
|
|
835
|
-
"example": "licos-platform industrial resource-update --resource-type collectionPoint --resource-id point-id --version-hash HASH --set-json '{\"name\":\"炉温\"}'",
|
|
836
|
-
},
|
|
837
|
-
"realtime-config": {
|
|
838
|
-
"description": "Build the platform WebSocket URL and subscribe message for application code.",
|
|
839
|
-
"parameters": [
|
|
840
|
-
{"name": "--point-id / --tag-code", "required": True, "description": "At least one selector; repeatable."},
|
|
841
|
-
{"name": "--transport", "required": False, "description": "auto/websocket defaults to WebSocket; mqtt is explicit."},
|
|
842
|
-
],
|
|
843
|
-
"example": "licos-platform industrial realtime-config --point-id point-id --transport websocket",
|
|
844
|
-
},
|
|
845
|
-
"point-current / point-history": {
|
|
846
|
-
"description": "Read current or historical point values.",
|
|
847
|
-
"parameters": [{"name": "point_id", "required": True, "description": "Collection point ID."}],
|
|
848
|
-
"example": "licos-platform industrial point-current point-id",
|
|
849
|
-
},
|
|
850
|
-
"point-write": {
|
|
851
|
-
"description": "Dry-run or control a writable project-managed point.",
|
|
852
|
-
"parameters": [
|
|
853
|
-
{"name": "point_id", "required": True, "description": "Collection point ID."},
|
|
854
|
-
{"name": "--value-json", "required": True, "description": "JSON scalar/object value."},
|
|
855
|
-
{"name": "--reason / --ticket", "required": True, "description": "Audit reason and ticket."},
|
|
856
|
-
{"name": "--transport", "required": False, "description": "auto/http defaults to HTTP; mqtt is explicit."},
|
|
857
|
-
{"name": "--dry-run / --confirm", "required": True, "description": "One safety mode is required."},
|
|
858
|
-
],
|
|
859
|
-
"example": "licos-platform industrial point-write point-id --value-json 80 --reason 调试 --ticket T-1 --confirm",
|
|
860
|
-
},
|
|
861
|
-
"video-stream / video-start / video-stop": {
|
|
862
|
-
"description": "Get playback options or explicitly start/stop a project-managed video preview.",
|
|
863
|
-
"parameters": [
|
|
864
|
-
{"name": "channel_id", "required": True, "description": "Video channel ID."},
|
|
865
|
-
{"name": "--protocol", "required": False, "description": "Start defaults to http-flv."},
|
|
866
|
-
{"name": "--confirm", "required": True, "description": "Required by start/stop."},
|
|
867
|
-
],
|
|
868
|
-
"example": "licos-platform industrial video-start channel-id --protocol http-flv --confirm",
|
|
869
|
-
},
|
|
870
|
-
},
|
|
871
|
-
}
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
HELP_TOPICS = {
|
|
783
|
+
INDUSTRIAL_HELP = {
|
|
784
|
+
"topic": "industrial",
|
|
785
|
+
"description": "Project-scoped direct collection and video integration without creating UNS nodes.",
|
|
786
|
+
"runtime": RUNTIME_ENV,
|
|
787
|
+
"rules": [
|
|
788
|
+
"Use only when the user explicitly requests direct collector access or explicitly says not to create UNS.",
|
|
789
|
+
"CLI and SDK call platform APIs; never request or expose IOTDATA API keys or MQTT credentials.",
|
|
790
|
+
"Search before create. Update and delete require the versionHash returned by resource-get/search.",
|
|
791
|
+
"Apply requires reviewed documentHash and planHash. Delete also requires --confirm-impact.",
|
|
792
|
+
"Realtime defaults to WebSocket; MQTT must be selected explicitly. Point control defaults to HTTP; MQTT must be selected explicitly.",
|
|
793
|
+
"Transport failures do not silently switch to another transport.",
|
|
794
|
+
],
|
|
795
|
+
"resourceTypes": [
|
|
796
|
+
"edgeAgent (read-only)",
|
|
797
|
+
"connection",
|
|
798
|
+
"collectionTask",
|
|
799
|
+
"collectionPoint",
|
|
800
|
+
"videoEdge",
|
|
801
|
+
"videoGateway",
|
|
802
|
+
"videoDevice",
|
|
803
|
+
"videoChannel",
|
|
804
|
+
],
|
|
805
|
+
"commands": {
|
|
806
|
+
"protocol-schema": {
|
|
807
|
+
"description": "Get required and default fields for one collection protocol.",
|
|
808
|
+
"parameters": [{"name": "protocol", "required": True, "description": "Protocol code."}],
|
|
809
|
+
"example": "licos-platform industrial protocol-schema modbus-tcp",
|
|
810
|
+
},
|
|
811
|
+
"resource-search": {
|
|
812
|
+
"description": "Search project-managed or enterprise collection/video resources.",
|
|
813
|
+
"parameters": [
|
|
814
|
+
{"name": "--resource-type", "required": True, "description": "Allowlisted resource type."},
|
|
815
|
+
{"name": "--scope", "required": False, "description": "project or enterprise."},
|
|
816
|
+
{"name": "--protocol", "required": False, "description": "Collection protocol filter."},
|
|
817
|
+
{"name": "--keyword", "required": False, "description": "Name/code/address keyword."},
|
|
818
|
+
{"name": "--selector-json", "required": False, "description": "Exact field selector object."},
|
|
819
|
+
],
|
|
820
|
+
"example": "licos-platform industrial resource-search --resource-type collectionPoint --scope enterprise --keyword TEMP",
|
|
821
|
+
},
|
|
822
|
+
"resource-get": {
|
|
823
|
+
"description": "Get one resource, writable state, and concurrency versionHash.",
|
|
824
|
+
"parameters": [
|
|
825
|
+
{"name": "--resource-type", "required": True, "description": "Allowlisted resource type."},
|
|
826
|
+
{"name": "--resource-id", "required": True, "description": "Resource ID."},
|
|
827
|
+
],
|
|
828
|
+
"example": "licos-platform industrial resource-get --resource-type collectionPoint --resource-id point-id",
|
|
829
|
+
},
|
|
830
|
+
"changes-validate / changes-plan / changes-apply": {
|
|
831
|
+
"description": "Validate, review, and apply an IndustrialChangeSet JSON file.",
|
|
832
|
+
"parameters": [
|
|
833
|
+
{"name": "--file", "required": True, "description": "IndustrialChangeSet JSON file."},
|
|
834
|
+
{"name": "--document-hash / --plan-hash", "required": True, "description": "Required only by apply."},
|
|
835
|
+
{"name": "--confirm", "required": True, "description": "Required only by apply."},
|
|
836
|
+
{"name": "--confirm-impact", "required": False, "description": "Required when the plan contains delete impact."},
|
|
837
|
+
],
|
|
838
|
+
"example": "licos-platform industrial changes-plan --file ./industrial.change.json",
|
|
839
|
+
},
|
|
840
|
+
"resource-create / resource-update / resource-delete": {
|
|
841
|
+
"description": "Generate and plan one operation; these commands do not apply writes.",
|
|
842
|
+
"parameters": [
|
|
843
|
+
{"name": "--resource-type", "required": True, "description": "Allowlisted resource type."},
|
|
844
|
+
{"name": "--spec-json / --set-json", "required": False, "description": "Create or update fields."},
|
|
845
|
+
{"name": "--resource-id / --version-hash", "required": False, "description": "Required by update/delete."},
|
|
846
|
+
{"name": "--output", "required": False, "description": "Write the generated ChangeSet under LICOS_PROJECT_PATH."},
|
|
847
|
+
],
|
|
848
|
+
"example": "licos-platform industrial resource-update --resource-type collectionPoint --resource-id point-id --version-hash HASH --set-json '{\"name\":\"炉温\"}'",
|
|
849
|
+
},
|
|
850
|
+
"realtime-config": {
|
|
851
|
+
"description": "Build the platform WebSocket URL and subscribe message for application code.",
|
|
852
|
+
"parameters": [
|
|
853
|
+
{"name": "--point-id / --tag-code", "required": True, "description": "At least one selector; repeatable."},
|
|
854
|
+
{"name": "--transport", "required": False, "description": "auto/websocket defaults to WebSocket; mqtt is explicit."},
|
|
855
|
+
],
|
|
856
|
+
"example": "licos-platform industrial realtime-config --point-id point-id --transport websocket",
|
|
857
|
+
},
|
|
858
|
+
"point-current / point-history": {
|
|
859
|
+
"description": "Read current or historical point values.",
|
|
860
|
+
"parameters": [{"name": "point_id", "required": True, "description": "Collection point ID."}],
|
|
861
|
+
"example": "licos-platform industrial point-current point-id",
|
|
862
|
+
},
|
|
863
|
+
"point-write": {
|
|
864
|
+
"description": "Dry-run or control a writable project-managed point.",
|
|
865
|
+
"parameters": [
|
|
866
|
+
{"name": "point_id", "required": True, "description": "Collection point ID."},
|
|
867
|
+
{"name": "--value-json", "required": True, "description": "JSON scalar/object value."},
|
|
868
|
+
{"name": "--reason / --ticket", "required": True, "description": "Audit reason and ticket."},
|
|
869
|
+
{"name": "--transport", "required": False, "description": "auto/http defaults to HTTP; mqtt is explicit."},
|
|
870
|
+
{"name": "--dry-run / --confirm", "required": True, "description": "One safety mode is required."},
|
|
871
|
+
],
|
|
872
|
+
"example": "licos-platform industrial point-write point-id --value-json 80 --reason 调试 --ticket T-1 --confirm",
|
|
873
|
+
},
|
|
874
|
+
"video-stream / video-start / video-stop": {
|
|
875
|
+
"description": "Get playback options or explicitly start/stop a project-managed video preview.",
|
|
876
|
+
"parameters": [
|
|
877
|
+
{"name": "channel_id", "required": True, "description": "Video channel ID."},
|
|
878
|
+
{"name": "--protocol", "required": False, "description": "Start defaults to http-flv."},
|
|
879
|
+
{"name": "--confirm", "required": True, "description": "Required by start/stop."},
|
|
880
|
+
],
|
|
881
|
+
"example": "licos-platform industrial video-start channel-id --protocol http-flv --confirm",
|
|
882
|
+
},
|
|
883
|
+
},
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
|
|
887
|
+
HELP_TOPICS = {
|
|
875
888
|
"storage": STORAGE_HELP,
|
|
876
889
|
"database": DATABASE_HELP,
|
|
877
890
|
"knowledge": KNOWLEDGE_HELP,
|
|
878
|
-
"oauth": OAUTH_HELP,
|
|
879
|
-
"industrial": INDUSTRIAL_HELP,
|
|
891
|
+
"oauth": OAUTH_HELP,
|
|
892
|
+
"industrial": INDUSTRIAL_HELP,
|
|
880
893
|
"uns": UNS_HELP,
|
|
881
894
|
}
|
|
882
895
|
|
{licos_platform_cli-0.4.0 → licos_platform_cli-0.4.1}/src/licos_platform_cli/industrial_commands.py
RENAMED
|
@@ -1,335 +1,335 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import argparse
|
|
4
|
-
import json
|
|
5
|
-
import os
|
|
6
|
-
import tempfile
|
|
7
|
-
from pathlib import Path
|
|
8
|
-
from typing import Any, Callable
|
|
9
|
-
|
|
10
|
-
from licos_platform_sdk import industrial
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
Handler = Callable[[argparse.Namespace], Any]
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
def _set_handler(parser: argparse.ArgumentParser, handler: Handler) -> None:
|
|
17
|
-
parser.set_defaults(handler=handler)
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
def _json(value: str, label: str) -> Any:
|
|
21
|
-
try:
|
|
22
|
-
return json.loads(value)
|
|
23
|
-
except json.JSONDecodeError as exc:
|
|
24
|
-
raise argparse.ArgumentTypeError(f"{label} must be valid JSON: {exc.msg}") from exc
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
def _json_object(value: str | None, label: str) -> dict[str, Any]:
|
|
28
|
-
if value is None:
|
|
29
|
-
return {}
|
|
30
|
-
payload = _json(value, label)
|
|
31
|
-
if not isinstance(payload, dict):
|
|
32
|
-
raise argparse.ArgumentTypeError(f"{label} must be a JSON object")
|
|
33
|
-
return payload
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
def _document(path: str) -> dict[str, Any]:
|
|
37
|
-
try:
|
|
38
|
-
payload = json.loads(Path(path).read_text(encoding="utf-8"))
|
|
39
|
-
except OSError as exc:
|
|
40
|
-
raise argparse.ArgumentTypeError(f"cannot read industrial change file: {exc}") from exc
|
|
41
|
-
except json.JSONDecodeError as exc:
|
|
42
|
-
raise argparse.ArgumentTypeError(
|
|
43
|
-
f"invalid JSON at line {exc.lineno}, column {exc.colno}: {exc.msg}"
|
|
44
|
-
) from exc
|
|
45
|
-
if not isinstance(payload, dict):
|
|
46
|
-
raise argparse.ArgumentTypeError("industrial change document root must be an object")
|
|
47
|
-
return payload
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
def _change_set(operation: dict[str, Any]) -> dict[str, Any]:
|
|
51
|
-
return {
|
|
52
|
-
"schemaVersion": "1.0",
|
|
53
|
-
"kind": "IndustrialChangeSet",
|
|
54
|
-
"operations": [operation],
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
def _plan_operation(operation: dict[str, Any], output: str | None) -> Any:
|
|
59
|
-
document = _change_set(operation)
|
|
60
|
-
plan = industrial.plan_changes(document)
|
|
61
|
-
if output is None:
|
|
62
|
-
return plan
|
|
63
|
-
output_path = _write_document(output, document)
|
|
64
|
-
return {"changeSetFile": str(output_path), "plan": plan}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
def _write_document(path: str, document: dict[str, Any]) -> Path:
|
|
68
|
-
project_root = Path(os.environ.get("LICOS_PROJECT_PATH", "/workspace/projects")).resolve()
|
|
69
|
-
output = Path(path).resolve()
|
|
70
|
-
if not output.is_relative_to(project_root):
|
|
71
|
-
raise argparse.ArgumentTypeError("change-set output must be inside LICOS_PROJECT_PATH")
|
|
72
|
-
output.parent.mkdir(parents=True, exist_ok=True)
|
|
73
|
-
payload = (json.dumps(document, ensure_ascii=False, indent=2) + "\n").encode("utf-8")
|
|
74
|
-
with tempfile.NamedTemporaryFile(
|
|
75
|
-
mode="wb", prefix=".industrial.change.", suffix=".tmp", dir=output.parent, delete=False
|
|
76
|
-
) as staging:
|
|
77
|
-
staging.write(payload)
|
|
78
|
-
staging.flush()
|
|
79
|
-
os.fsync(staging.fileno())
|
|
80
|
-
staging_path = Path(staging.name)
|
|
81
|
-
os.replace(staging_path, output)
|
|
82
|
-
return output
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
def _cmd_protocol_schema(args: argparse.Namespace) -> Any:
|
|
86
|
-
return industrial.protocol_schema(args.protocol)
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
def _cmd_resource_search(args: argparse.Namespace) -> Any:
|
|
90
|
-
return industrial.search_resources(
|
|
91
|
-
args.resource_type,
|
|
92
|
-
protocol=args.protocol,
|
|
93
|
-
keyword=args.keyword,
|
|
94
|
-
scope=args.scope,
|
|
95
|
-
selector=_json_object(args.selector_json, "selector"),
|
|
96
|
-
page=args.page,
|
|
97
|
-
page_size=args.page_size,
|
|
98
|
-
)
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
def _cmd_resource_get(args: argparse.Namespace) -> Any:
|
|
102
|
-
return industrial.resource(args.resource_type, args.resource_id)
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
def _cmd_changes_validate(args: argparse.Namespace) -> Any:
|
|
106
|
-
return industrial.validate_changes(_document(args.file))
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
def _cmd_changes_plan(args: argparse.Namespace) -> Any:
|
|
110
|
-
return industrial.plan_changes(_document(args.file))
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
def _cmd_changes_apply(args: argparse.Namespace) -> Any:
|
|
114
|
-
if not args.confirm:
|
|
115
|
-
raise argparse.ArgumentTypeError("industrial changes apply requires --confirm")
|
|
116
|
-
return industrial.apply_changes(
|
|
117
|
-
_document(args.file),
|
|
118
|
-
expected_document_hash=args.document_hash,
|
|
119
|
-
expected_plan_hash=args.plan_hash,
|
|
120
|
-
confirmed=True,
|
|
121
|
-
impact_confirmed=args.confirm_impact,
|
|
122
|
-
)
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
def _cmd_resource_create(args: argparse.Namespace) -> Any:
|
|
126
|
-
return _plan_operation(
|
|
127
|
-
{
|
|
128
|
-
"op": "create",
|
|
129
|
-
"resourceType": args.resource_type,
|
|
130
|
-
"spec": _json_object(args.spec_json, "resource spec"),
|
|
131
|
-
},
|
|
132
|
-
args.output,
|
|
133
|
-
)
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
def _cmd_resource_update(args: argparse.Namespace) -> Any:
|
|
137
|
-
return _plan_operation(
|
|
138
|
-
{
|
|
139
|
-
"op": "update",
|
|
140
|
-
"resourceType": args.resource_type,
|
|
141
|
-
"target": {"id": args.resource_id, "versionHash": args.version_hash},
|
|
142
|
-
"set": _json_object(args.set_json, "resource update"),
|
|
143
|
-
},
|
|
144
|
-
args.output,
|
|
145
|
-
)
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
def _cmd_resource_delete(args: argparse.Namespace) -> Any:
|
|
149
|
-
return _plan_operation(
|
|
150
|
-
{
|
|
151
|
-
"op": "delete",
|
|
152
|
-
"resourceType": args.resource_type,
|
|
153
|
-
"target": {"id": args.resource_id, "versionHash": args.version_hash},
|
|
154
|
-
},
|
|
155
|
-
args.output,
|
|
156
|
-
)
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
def _cmd_runtime_capabilities(_args: argparse.Namespace) -> Any:
|
|
160
|
-
return industrial.runtime_capabilities()
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
def _cmd_realtime_config(args: argparse.Namespace) -> Any:
|
|
164
|
-
return industrial.realtime_connection(
|
|
165
|
-
point_ids=args.point_ids,
|
|
166
|
-
tag_codes=args.tag_codes,
|
|
167
|
-
transport=args.transport,
|
|
168
|
-
)
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
def _cmd_point_current(args: argparse.Namespace) -> Any:
|
|
172
|
-
return industrial.point_current(args.point_id)
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
def _cmd_point_history(args: argparse.Namespace) -> Any:
|
|
176
|
-
return industrial.point_history(
|
|
177
|
-
args.point_id,
|
|
178
|
-
start_time=args.start_time,
|
|
179
|
-
end_time=args.end_time,
|
|
180
|
-
page=args.page,
|
|
181
|
-
page_size=args.page_size,
|
|
182
|
-
)
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
def _cmd_point_write(args: argparse.Namespace) -> Any:
|
|
186
|
-
if not args.dry_run and not args.confirm:
|
|
187
|
-
raise argparse.ArgumentTypeError(
|
|
188
|
-
"industrial point write requires --confirm unless --dry-run is used"
|
|
189
|
-
)
|
|
190
|
-
return industrial.write_point(
|
|
191
|
-
args.point_id,
|
|
192
|
-
_json(args.value_json, "point value"),
|
|
193
|
-
reason=args.reason,
|
|
194
|
-
ticket=args.ticket,
|
|
195
|
-
transport=args.transport,
|
|
196
|
-
request_id=args.request_id,
|
|
197
|
-
dry_run=args.dry_run,
|
|
198
|
-
confirmed=args.confirm,
|
|
199
|
-
)
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
def _cmd_video_stream(args: argparse.Namespace) -> Any:
|
|
203
|
-
return industrial.video_stream_options(args.channel_id)
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
def _cmd_video_start(args: argparse.Namespace) -> Any:
|
|
207
|
-
if not args.confirm:
|
|
208
|
-
raise argparse.ArgumentTypeError("industrial video preview start requires --confirm")
|
|
209
|
-
return industrial.start_video_preview(
|
|
210
|
-
args.channel_id,
|
|
211
|
-
preferred_protocol=args.protocol,
|
|
212
|
-
preferred_protocol_order=args.protocol_order,
|
|
213
|
-
confirmed=True,
|
|
214
|
-
)
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
def _cmd_video_stop(args: argparse.Namespace) -> Any:
|
|
218
|
-
if not args.confirm:
|
|
219
|
-
raise argparse.ArgumentTypeError("industrial video preview stop requires --confirm")
|
|
220
|
-
return industrial.stop_video_preview(args.channel_id, confirmed=True)
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
def add_industrial_parser(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) -> None:
|
|
224
|
-
parser = subparsers.add_parser(
|
|
225
|
-
"industrial", help="Manage collection and video resources without creating UNS nodes"
|
|
226
|
-
)
|
|
227
|
-
commands = parser.add_subparsers(dest="industrial_command", required=True)
|
|
228
|
-
|
|
229
|
-
protocol = commands.add_parser("protocol-schema", help="Get one collection protocol Schema")
|
|
230
|
-
protocol.add_argument("protocol")
|
|
231
|
-
_set_handler(protocol, _cmd_protocol_schema)
|
|
232
|
-
|
|
233
|
-
search = commands.add_parser("resource-search", help="Search collection or video resources")
|
|
234
|
-
search.add_argument("--resource-type", required=True)
|
|
235
|
-
search.add_argument("--protocol")
|
|
236
|
-
search.add_argument("--keyword")
|
|
237
|
-
search.add_argument("--scope", choices=("project", "enterprise"), default="project")
|
|
238
|
-
search.add_argument("--selector-json")
|
|
239
|
-
search.add_argument("--page", type=int, default=1)
|
|
240
|
-
search.add_argument("--page-size", type=int, default=20)
|
|
241
|
-
_set_handler(search, _cmd_resource_search)
|
|
242
|
-
|
|
243
|
-
resource_get = commands.add_parser("resource-get", help="Get a resource and its version hash")
|
|
244
|
-
resource_get.add_argument("--resource-type", required=True)
|
|
245
|
-
resource_get.add_argument("--resource-id", required=True)
|
|
246
|
-
_set_handler(resource_get, _cmd_resource_get)
|
|
247
|
-
|
|
248
|
-
for name, handler, help_text in (
|
|
249
|
-
("changes-validate", _cmd_changes_validate, "Validate an IndustrialChangeSet file"),
|
|
250
|
-
("changes-plan", _cmd_changes_plan, "Create a read-only IndustrialChangeSet plan"),
|
|
251
|
-
):
|
|
252
|
-
command = commands.add_parser(name, help=help_text)
|
|
253
|
-
command.add_argument("--file", required=True)
|
|
254
|
-
_set_handler(command, handler)
|
|
255
|
-
|
|
256
|
-
apply_command = commands.add_parser("changes-apply", help="Apply a reviewed IndustrialChangeSet")
|
|
257
|
-
apply_command.add_argument("--file", required=True)
|
|
258
|
-
apply_command.add_argument("--document-hash", required=True)
|
|
259
|
-
apply_command.add_argument("--plan-hash", required=True)
|
|
260
|
-
apply_command.add_argument("--confirm", action="store_true")
|
|
261
|
-
apply_command.add_argument("--confirm-impact", action="store_true")
|
|
262
|
-
_set_handler(apply_command, _cmd_changes_apply)
|
|
263
|
-
|
|
264
|
-
create = commands.add_parser("resource-create", help="Generate and plan one resource creation")
|
|
265
|
-
create.add_argument("--resource-type", required=True)
|
|
266
|
-
create.add_argument("--spec-json", required=True)
|
|
267
|
-
create.add_argument("--output")
|
|
268
|
-
_set_handler(create, _cmd_resource_create)
|
|
269
|
-
|
|
270
|
-
update = commands.add_parser("resource-update", help="Generate and plan one resource update")
|
|
271
|
-
update.add_argument("--resource-type", required=True)
|
|
272
|
-
update.add_argument("--resource-id", required=True)
|
|
273
|
-
update.add_argument("--version-hash", required=True)
|
|
274
|
-
update.add_argument("--set-json", required=True)
|
|
275
|
-
update.add_argument("--output")
|
|
276
|
-
_set_handler(update, _cmd_resource_update)
|
|
277
|
-
|
|
278
|
-
delete = commands.add_parser("resource-delete", help="Generate and plan one resource deletion")
|
|
279
|
-
delete.add_argument("--resource-type", required=True)
|
|
280
|
-
delete.add_argument("--resource-id", required=True)
|
|
281
|
-
delete.add_argument("--version-hash", required=True)
|
|
282
|
-
delete.add_argument("--output")
|
|
283
|
-
_set_handler(delete, _cmd_resource_delete)
|
|
284
|
-
|
|
285
|
-
_set_handler(
|
|
286
|
-
commands.add_parser("runtime-capabilities", help="Get subscription and control transports"),
|
|
287
|
-
_cmd_runtime_capabilities,
|
|
288
|
-
)
|
|
289
|
-
|
|
290
|
-
realtime = commands.add_parser(
|
|
291
|
-
"realtime-config", help="Build a platform WebSocket URL and subscribe message"
|
|
292
|
-
)
|
|
293
|
-
realtime.add_argument("--point-id", action="append", dest="point_ids")
|
|
294
|
-
realtime.add_argument("--tag-code", action="append", dest="tag_codes")
|
|
295
|
-
realtime.add_argument("--transport", choices=("auto", "websocket", "mqtt"), default="auto")
|
|
296
|
-
_set_handler(realtime, _cmd_realtime_config)
|
|
297
|
-
|
|
298
|
-
current = commands.add_parser("point-current", help="Read a collection point current value")
|
|
299
|
-
current.add_argument("point_id")
|
|
300
|
-
_set_handler(current, _cmd_point_current)
|
|
301
|
-
|
|
302
|
-
history = commands.add_parser("point-history", help="Read collection point history")
|
|
303
|
-
history.add_argument("point_id")
|
|
304
|
-
history.add_argument("--start-time", required=True)
|
|
305
|
-
history.add_argument("--end-time", required=True)
|
|
306
|
-
history.add_argument("--page", type=int, default=1)
|
|
307
|
-
history.add_argument("--page-size", type=int, default=100)
|
|
308
|
-
_set_handler(history, _cmd_point_history)
|
|
309
|
-
|
|
310
|
-
point_write = commands.add_parser("point-write", help="Control a collection point")
|
|
311
|
-
point_write.add_argument("point_id")
|
|
312
|
-
point_write.add_argument("--value-json", required=True)
|
|
313
|
-
point_write.add_argument("--reason", required=True)
|
|
314
|
-
point_write.add_argument("--ticket", required=True)
|
|
315
|
-
point_write.add_argument("--transport", choices=("auto", "http", "mqtt"), default="auto")
|
|
316
|
-
point_write.add_argument("--request-id")
|
|
317
|
-
point_write.add_argument("--dry-run", action="store_true")
|
|
318
|
-
point_write.add_argument("--confirm", action="store_true")
|
|
319
|
-
_set_handler(point_write, _cmd_point_write)
|
|
320
|
-
|
|
321
|
-
stream = commands.add_parser("video-stream", help="Get video channel playback options")
|
|
322
|
-
stream.add_argument("channel_id")
|
|
323
|
-
_set_handler(stream, _cmd_video_stream)
|
|
324
|
-
|
|
325
|
-
video_start = commands.add_parser("video-start", help="Start a video channel preview")
|
|
326
|
-
video_start.add_argument("channel_id")
|
|
327
|
-
video_start.add_argument("--protocol", default="http-flv")
|
|
328
|
-
video_start.add_argument("--protocol-order", action="append")
|
|
329
|
-
video_start.add_argument("--confirm", action="store_true")
|
|
330
|
-
_set_handler(video_start, _cmd_video_start)
|
|
331
|
-
|
|
332
|
-
video_stop = commands.add_parser("video-stop", help="Stop a video channel preview")
|
|
333
|
-
video_stop.add_argument("channel_id")
|
|
334
|
-
video_stop.add_argument("--confirm", action="store_true")
|
|
335
|
-
_set_handler(video_stop, _cmd_video_stop)
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import tempfile
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any, Callable
|
|
9
|
+
|
|
10
|
+
from licos_platform_sdk import industrial
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
Handler = Callable[[argparse.Namespace], Any]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _set_handler(parser: argparse.ArgumentParser, handler: Handler) -> None:
|
|
17
|
+
parser.set_defaults(handler=handler)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _json(value: str, label: str) -> Any:
|
|
21
|
+
try:
|
|
22
|
+
return json.loads(value)
|
|
23
|
+
except json.JSONDecodeError as exc:
|
|
24
|
+
raise argparse.ArgumentTypeError(f"{label} must be valid JSON: {exc.msg}") from exc
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _json_object(value: str | None, label: str) -> dict[str, Any]:
|
|
28
|
+
if value is None:
|
|
29
|
+
return {}
|
|
30
|
+
payload = _json(value, label)
|
|
31
|
+
if not isinstance(payload, dict):
|
|
32
|
+
raise argparse.ArgumentTypeError(f"{label} must be a JSON object")
|
|
33
|
+
return payload
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _document(path: str) -> dict[str, Any]:
|
|
37
|
+
try:
|
|
38
|
+
payload = json.loads(Path(path).read_text(encoding="utf-8"))
|
|
39
|
+
except OSError as exc:
|
|
40
|
+
raise argparse.ArgumentTypeError(f"cannot read industrial change file: {exc}") from exc
|
|
41
|
+
except json.JSONDecodeError as exc:
|
|
42
|
+
raise argparse.ArgumentTypeError(
|
|
43
|
+
f"invalid JSON at line {exc.lineno}, column {exc.colno}: {exc.msg}"
|
|
44
|
+
) from exc
|
|
45
|
+
if not isinstance(payload, dict):
|
|
46
|
+
raise argparse.ArgumentTypeError("industrial change document root must be an object")
|
|
47
|
+
return payload
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _change_set(operation: dict[str, Any]) -> dict[str, Any]:
|
|
51
|
+
return {
|
|
52
|
+
"schemaVersion": "1.0",
|
|
53
|
+
"kind": "IndustrialChangeSet",
|
|
54
|
+
"operations": [operation],
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _plan_operation(operation: dict[str, Any], output: str | None) -> Any:
|
|
59
|
+
document = _change_set(operation)
|
|
60
|
+
plan = industrial.plan_changes(document)
|
|
61
|
+
if output is None:
|
|
62
|
+
return plan
|
|
63
|
+
output_path = _write_document(output, document)
|
|
64
|
+
return {"changeSetFile": str(output_path), "plan": plan}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _write_document(path: str, document: dict[str, Any]) -> Path:
|
|
68
|
+
project_root = Path(os.environ.get("LICOS_PROJECT_PATH", "/workspace/projects")).resolve()
|
|
69
|
+
output = Path(path).resolve()
|
|
70
|
+
if not output.is_relative_to(project_root):
|
|
71
|
+
raise argparse.ArgumentTypeError("change-set output must be inside LICOS_PROJECT_PATH")
|
|
72
|
+
output.parent.mkdir(parents=True, exist_ok=True)
|
|
73
|
+
payload = (json.dumps(document, ensure_ascii=False, indent=2) + "\n").encode("utf-8")
|
|
74
|
+
with tempfile.NamedTemporaryFile(
|
|
75
|
+
mode="wb", prefix=".industrial.change.", suffix=".tmp", dir=output.parent, delete=False
|
|
76
|
+
) as staging:
|
|
77
|
+
staging.write(payload)
|
|
78
|
+
staging.flush()
|
|
79
|
+
os.fsync(staging.fileno())
|
|
80
|
+
staging_path = Path(staging.name)
|
|
81
|
+
os.replace(staging_path, output)
|
|
82
|
+
return output
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _cmd_protocol_schema(args: argparse.Namespace) -> Any:
|
|
86
|
+
return industrial.protocol_schema(args.protocol)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _cmd_resource_search(args: argparse.Namespace) -> Any:
|
|
90
|
+
return industrial.search_resources(
|
|
91
|
+
args.resource_type,
|
|
92
|
+
protocol=args.protocol,
|
|
93
|
+
keyword=args.keyword,
|
|
94
|
+
scope=args.scope,
|
|
95
|
+
selector=_json_object(args.selector_json, "selector"),
|
|
96
|
+
page=args.page,
|
|
97
|
+
page_size=args.page_size,
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _cmd_resource_get(args: argparse.Namespace) -> Any:
|
|
102
|
+
return industrial.resource(args.resource_type, args.resource_id)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _cmd_changes_validate(args: argparse.Namespace) -> Any:
|
|
106
|
+
return industrial.validate_changes(_document(args.file))
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _cmd_changes_plan(args: argparse.Namespace) -> Any:
|
|
110
|
+
return industrial.plan_changes(_document(args.file))
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _cmd_changes_apply(args: argparse.Namespace) -> Any:
|
|
114
|
+
if not args.confirm:
|
|
115
|
+
raise argparse.ArgumentTypeError("industrial changes apply requires --confirm")
|
|
116
|
+
return industrial.apply_changes(
|
|
117
|
+
_document(args.file),
|
|
118
|
+
expected_document_hash=args.document_hash,
|
|
119
|
+
expected_plan_hash=args.plan_hash,
|
|
120
|
+
confirmed=True,
|
|
121
|
+
impact_confirmed=args.confirm_impact,
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _cmd_resource_create(args: argparse.Namespace) -> Any:
|
|
126
|
+
return _plan_operation(
|
|
127
|
+
{
|
|
128
|
+
"op": "create",
|
|
129
|
+
"resourceType": args.resource_type,
|
|
130
|
+
"spec": _json_object(args.spec_json, "resource spec"),
|
|
131
|
+
},
|
|
132
|
+
args.output,
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _cmd_resource_update(args: argparse.Namespace) -> Any:
|
|
137
|
+
return _plan_operation(
|
|
138
|
+
{
|
|
139
|
+
"op": "update",
|
|
140
|
+
"resourceType": args.resource_type,
|
|
141
|
+
"target": {"id": args.resource_id, "versionHash": args.version_hash},
|
|
142
|
+
"set": _json_object(args.set_json, "resource update"),
|
|
143
|
+
},
|
|
144
|
+
args.output,
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _cmd_resource_delete(args: argparse.Namespace) -> Any:
|
|
149
|
+
return _plan_operation(
|
|
150
|
+
{
|
|
151
|
+
"op": "delete",
|
|
152
|
+
"resourceType": args.resource_type,
|
|
153
|
+
"target": {"id": args.resource_id, "versionHash": args.version_hash},
|
|
154
|
+
},
|
|
155
|
+
args.output,
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _cmd_runtime_capabilities(_args: argparse.Namespace) -> Any:
|
|
160
|
+
return industrial.runtime_capabilities()
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _cmd_realtime_config(args: argparse.Namespace) -> Any:
|
|
164
|
+
return industrial.realtime_connection(
|
|
165
|
+
point_ids=args.point_ids,
|
|
166
|
+
tag_codes=args.tag_codes,
|
|
167
|
+
transport=args.transport,
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _cmd_point_current(args: argparse.Namespace) -> Any:
|
|
172
|
+
return industrial.point_current(args.point_id)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _cmd_point_history(args: argparse.Namespace) -> Any:
|
|
176
|
+
return industrial.point_history(
|
|
177
|
+
args.point_id,
|
|
178
|
+
start_time=args.start_time,
|
|
179
|
+
end_time=args.end_time,
|
|
180
|
+
page=args.page,
|
|
181
|
+
page_size=args.page_size,
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _cmd_point_write(args: argparse.Namespace) -> Any:
|
|
186
|
+
if not args.dry_run and not args.confirm:
|
|
187
|
+
raise argparse.ArgumentTypeError(
|
|
188
|
+
"industrial point write requires --confirm unless --dry-run is used"
|
|
189
|
+
)
|
|
190
|
+
return industrial.write_point(
|
|
191
|
+
args.point_id,
|
|
192
|
+
_json(args.value_json, "point value"),
|
|
193
|
+
reason=args.reason,
|
|
194
|
+
ticket=args.ticket,
|
|
195
|
+
transport=args.transport,
|
|
196
|
+
request_id=args.request_id,
|
|
197
|
+
dry_run=args.dry_run,
|
|
198
|
+
confirmed=args.confirm,
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _cmd_video_stream(args: argparse.Namespace) -> Any:
|
|
203
|
+
return industrial.video_stream_options(args.channel_id)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _cmd_video_start(args: argparse.Namespace) -> Any:
|
|
207
|
+
if not args.confirm:
|
|
208
|
+
raise argparse.ArgumentTypeError("industrial video preview start requires --confirm")
|
|
209
|
+
return industrial.start_video_preview(
|
|
210
|
+
args.channel_id,
|
|
211
|
+
preferred_protocol=args.protocol,
|
|
212
|
+
preferred_protocol_order=args.protocol_order,
|
|
213
|
+
confirmed=True,
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def _cmd_video_stop(args: argparse.Namespace) -> Any:
|
|
218
|
+
if not args.confirm:
|
|
219
|
+
raise argparse.ArgumentTypeError("industrial video preview stop requires --confirm")
|
|
220
|
+
return industrial.stop_video_preview(args.channel_id, confirmed=True)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def add_industrial_parser(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) -> None:
|
|
224
|
+
parser = subparsers.add_parser(
|
|
225
|
+
"industrial", help="Manage collection and video resources without creating UNS nodes"
|
|
226
|
+
)
|
|
227
|
+
commands = parser.add_subparsers(dest="industrial_command", required=True)
|
|
228
|
+
|
|
229
|
+
protocol = commands.add_parser("protocol-schema", help="Get one collection protocol Schema")
|
|
230
|
+
protocol.add_argument("protocol")
|
|
231
|
+
_set_handler(protocol, _cmd_protocol_schema)
|
|
232
|
+
|
|
233
|
+
search = commands.add_parser("resource-search", help="Search collection or video resources")
|
|
234
|
+
search.add_argument("--resource-type", required=True)
|
|
235
|
+
search.add_argument("--protocol")
|
|
236
|
+
search.add_argument("--keyword")
|
|
237
|
+
search.add_argument("--scope", choices=("project", "enterprise"), default="project")
|
|
238
|
+
search.add_argument("--selector-json")
|
|
239
|
+
search.add_argument("--page", type=int, default=1)
|
|
240
|
+
search.add_argument("--page-size", type=int, default=20)
|
|
241
|
+
_set_handler(search, _cmd_resource_search)
|
|
242
|
+
|
|
243
|
+
resource_get = commands.add_parser("resource-get", help="Get a resource and its version hash")
|
|
244
|
+
resource_get.add_argument("--resource-type", required=True)
|
|
245
|
+
resource_get.add_argument("--resource-id", required=True)
|
|
246
|
+
_set_handler(resource_get, _cmd_resource_get)
|
|
247
|
+
|
|
248
|
+
for name, handler, help_text in (
|
|
249
|
+
("changes-validate", _cmd_changes_validate, "Validate an IndustrialChangeSet file"),
|
|
250
|
+
("changes-plan", _cmd_changes_plan, "Create a read-only IndustrialChangeSet plan"),
|
|
251
|
+
):
|
|
252
|
+
command = commands.add_parser(name, help=help_text)
|
|
253
|
+
command.add_argument("--file", required=True)
|
|
254
|
+
_set_handler(command, handler)
|
|
255
|
+
|
|
256
|
+
apply_command = commands.add_parser("changes-apply", help="Apply a reviewed IndustrialChangeSet")
|
|
257
|
+
apply_command.add_argument("--file", required=True)
|
|
258
|
+
apply_command.add_argument("--document-hash", required=True)
|
|
259
|
+
apply_command.add_argument("--plan-hash", required=True)
|
|
260
|
+
apply_command.add_argument("--confirm", action="store_true")
|
|
261
|
+
apply_command.add_argument("--confirm-impact", action="store_true")
|
|
262
|
+
_set_handler(apply_command, _cmd_changes_apply)
|
|
263
|
+
|
|
264
|
+
create = commands.add_parser("resource-create", help="Generate and plan one resource creation")
|
|
265
|
+
create.add_argument("--resource-type", required=True)
|
|
266
|
+
create.add_argument("--spec-json", required=True)
|
|
267
|
+
create.add_argument("--output")
|
|
268
|
+
_set_handler(create, _cmd_resource_create)
|
|
269
|
+
|
|
270
|
+
update = commands.add_parser("resource-update", help="Generate and plan one resource update")
|
|
271
|
+
update.add_argument("--resource-type", required=True)
|
|
272
|
+
update.add_argument("--resource-id", required=True)
|
|
273
|
+
update.add_argument("--version-hash", required=True)
|
|
274
|
+
update.add_argument("--set-json", required=True)
|
|
275
|
+
update.add_argument("--output")
|
|
276
|
+
_set_handler(update, _cmd_resource_update)
|
|
277
|
+
|
|
278
|
+
delete = commands.add_parser("resource-delete", help="Generate and plan one resource deletion")
|
|
279
|
+
delete.add_argument("--resource-type", required=True)
|
|
280
|
+
delete.add_argument("--resource-id", required=True)
|
|
281
|
+
delete.add_argument("--version-hash", required=True)
|
|
282
|
+
delete.add_argument("--output")
|
|
283
|
+
_set_handler(delete, _cmd_resource_delete)
|
|
284
|
+
|
|
285
|
+
_set_handler(
|
|
286
|
+
commands.add_parser("runtime-capabilities", help="Get subscription and control transports"),
|
|
287
|
+
_cmd_runtime_capabilities,
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
realtime = commands.add_parser(
|
|
291
|
+
"realtime-config", help="Build a platform WebSocket URL and subscribe message"
|
|
292
|
+
)
|
|
293
|
+
realtime.add_argument("--point-id", action="append", dest="point_ids")
|
|
294
|
+
realtime.add_argument("--tag-code", action="append", dest="tag_codes")
|
|
295
|
+
realtime.add_argument("--transport", choices=("auto", "websocket", "mqtt"), default="auto")
|
|
296
|
+
_set_handler(realtime, _cmd_realtime_config)
|
|
297
|
+
|
|
298
|
+
current = commands.add_parser("point-current", help="Read a collection point current value")
|
|
299
|
+
current.add_argument("point_id")
|
|
300
|
+
_set_handler(current, _cmd_point_current)
|
|
301
|
+
|
|
302
|
+
history = commands.add_parser("point-history", help="Read collection point history")
|
|
303
|
+
history.add_argument("point_id")
|
|
304
|
+
history.add_argument("--start-time", required=True)
|
|
305
|
+
history.add_argument("--end-time", required=True)
|
|
306
|
+
history.add_argument("--page", type=int, default=1)
|
|
307
|
+
history.add_argument("--page-size", type=int, default=100)
|
|
308
|
+
_set_handler(history, _cmd_point_history)
|
|
309
|
+
|
|
310
|
+
point_write = commands.add_parser("point-write", help="Control a collection point")
|
|
311
|
+
point_write.add_argument("point_id")
|
|
312
|
+
point_write.add_argument("--value-json", required=True)
|
|
313
|
+
point_write.add_argument("--reason", required=True)
|
|
314
|
+
point_write.add_argument("--ticket", required=True)
|
|
315
|
+
point_write.add_argument("--transport", choices=("auto", "http", "mqtt"), default="auto")
|
|
316
|
+
point_write.add_argument("--request-id")
|
|
317
|
+
point_write.add_argument("--dry-run", action="store_true")
|
|
318
|
+
point_write.add_argument("--confirm", action="store_true")
|
|
319
|
+
_set_handler(point_write, _cmd_point_write)
|
|
320
|
+
|
|
321
|
+
stream = commands.add_parser("video-stream", help="Get video channel playback options")
|
|
322
|
+
stream.add_argument("channel_id")
|
|
323
|
+
_set_handler(stream, _cmd_video_stream)
|
|
324
|
+
|
|
325
|
+
video_start = commands.add_parser("video-start", help="Start a video channel preview")
|
|
326
|
+
video_start.add_argument("channel_id")
|
|
327
|
+
video_start.add_argument("--protocol", default="http-flv")
|
|
328
|
+
video_start.add_argument("--protocol-order", action="append")
|
|
329
|
+
video_start.add_argument("--confirm", action="store_true")
|
|
330
|
+
_set_handler(video_start, _cmd_video_start)
|
|
331
|
+
|
|
332
|
+
video_stop = commands.add_parser("video-stop", help="Stop a video channel preview")
|
|
333
|
+
video_stop.add_argument("channel_id")
|
|
334
|
+
video_stop.add_argument("--confirm", action="store_true")
|
|
335
|
+
_set_handler(video_stop, _cmd_video_stop)
|
|
@@ -8,8 +8,8 @@ from typing import Any, Sequence
|
|
|
8
8
|
from licos_platform_sdk import PlatformSdkError
|
|
9
9
|
|
|
10
10
|
from .database_commands import add_database_parser
|
|
11
|
-
from .help_commands import add_help_parser
|
|
12
|
-
from .industrial_commands import add_industrial_parser
|
|
11
|
+
from .help_commands import add_help_parser
|
|
12
|
+
from .industrial_commands import add_industrial_parser
|
|
13
13
|
from .knowledge_commands import add_knowledge_parser
|
|
14
14
|
from .oauth_commands import add_oauth_parser
|
|
15
15
|
from .storage_commands import add_storage_parser
|
|
@@ -35,9 +35,9 @@ def _build_parser() -> argparse.ArgumentParser:
|
|
|
35
35
|
add_storage_parser(subparsers)
|
|
36
36
|
add_database_parser(subparsers)
|
|
37
37
|
add_knowledge_parser(subparsers)
|
|
38
|
-
add_oauth_parser(subparsers)
|
|
39
|
-
add_uns_parser(subparsers)
|
|
40
|
-
add_industrial_parser(subparsers)
|
|
38
|
+
add_oauth_parser(subparsers)
|
|
39
|
+
add_uns_parser(subparsers)
|
|
40
|
+
add_industrial_parser(subparsers)
|
|
41
41
|
return parser
|
|
42
42
|
|
|
43
43
|
|
|
@@ -14,14 +14,14 @@ sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "licos-platform-sdk
|
|
|
14
14
|
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
|
|
15
15
|
|
|
16
16
|
from licos_platform_cli import main as cli_main
|
|
17
|
-
from licos_platform_cli import (
|
|
18
|
-
database_commands,
|
|
19
|
-
industrial_commands,
|
|
20
|
-
knowledge_commands,
|
|
21
|
-
oauth_commands,
|
|
22
|
-
storage_commands,
|
|
23
|
-
uns_commands,
|
|
24
|
-
)
|
|
17
|
+
from licos_platform_cli import (
|
|
18
|
+
database_commands,
|
|
19
|
+
industrial_commands,
|
|
20
|
+
knowledge_commands,
|
|
21
|
+
oauth_commands,
|
|
22
|
+
storage_commands,
|
|
23
|
+
uns_commands,
|
|
24
|
+
)
|
|
25
25
|
from licos_platform_sdk import ApiError, PlatformSdkError
|
|
26
26
|
|
|
27
27
|
|
|
@@ -53,10 +53,10 @@ class PlatformCliTests(unittest.TestCase):
|
|
|
53
53
|
self.assertEqual(code, 0)
|
|
54
54
|
self.assertEqual(stderr, "")
|
|
55
55
|
payload = json.loads(stdout)
|
|
56
|
-
self.assertEqual(
|
|
57
|
-
payload["topics"],
|
|
58
|
-
["database", "industrial", "knowledge", "oauth", "storage", "uns"],
|
|
59
|
-
)
|
|
56
|
+
self.assertEqual(
|
|
57
|
+
payload["topics"],
|
|
58
|
+
["database", "industrial", "knowledge", "oauth", "storage", "uns"],
|
|
59
|
+
)
|
|
60
60
|
self.assertIn("licos-platform help storage", payload["usage"])
|
|
61
61
|
|
|
62
62
|
def test_structured_help_storage_command_describes_parameters(self) -> None:
|
|
@@ -94,7 +94,7 @@ class PlatformCliTests(unittest.TestCase):
|
|
|
94
94
|
self.assertIn("search", payload["commands"])
|
|
95
95
|
self.assertTrue(any("Search all accessible datasets by default" in rule for rule in payload["rules"]))
|
|
96
96
|
|
|
97
|
-
def test_structured_help_uns_exposes_only_confirmed_apply(self) -> None:
|
|
97
|
+
def test_structured_help_uns_exposes_only_confirmed_apply(self) -> None:
|
|
98
98
|
code, stdout, stderr = run_cli("help", "uns")
|
|
99
99
|
|
|
100
100
|
self.assertEqual(code, 0)
|
|
@@ -102,49 +102,49 @@ class PlatformCliTests(unittest.TestCase):
|
|
|
102
102
|
payload = json.loads(stdout)
|
|
103
103
|
self.assertIn("apply", payload["commands"])
|
|
104
104
|
parameters = {item["name"] for item in payload["commands"]["apply"]["parameters"]}
|
|
105
|
-
self.assertEqual(parameters, {"--file", "--document-hash", "--plan-hash", "--confirm"})
|
|
106
|
-
|
|
107
|
-
def test_structured_help_industrial_declares_transport_defaults(self) -> None:
|
|
108
|
-
code, stdout, stderr = run_cli("help", "industrial")
|
|
109
|
-
|
|
110
|
-
self.assertEqual(code, 0)
|
|
111
|
-
self.assertEqual(stderr, "")
|
|
112
|
-
payload = json.loads(stdout)
|
|
113
|
-
self.assertTrue(any("Realtime defaults to WebSocket" in rule for rule in payload["rules"]))
|
|
114
|
-
self.assertTrue(any("Point control defaults to HTTP" in rule for rule in payload["rules"]))
|
|
115
|
-
|
|
116
|
-
def test_industrial_point_write_forwards_explicit_mqtt_transport(self) -> None:
|
|
117
|
-
with mock.patch.object(
|
|
118
|
-
industrial_commands.industrial, "write_point", return_value={"transport": "mqtt"}
|
|
119
|
-
) as write_point:
|
|
120
|
-
code, stdout, stderr = run_cli(
|
|
121
|
-
"industrial",
|
|
122
|
-
"point-write",
|
|
123
|
-
"point-1",
|
|
124
|
-
"--value-json",
|
|
125
|
-
"80",
|
|
126
|
-
"--reason",
|
|
127
|
-
"test",
|
|
128
|
-
"--ticket",
|
|
129
|
-
"T-1",
|
|
130
|
-
"--transport",
|
|
131
|
-
"mqtt",
|
|
132
|
-
"--confirm",
|
|
133
|
-
)
|
|
134
|
-
|
|
135
|
-
self.assertEqual(code, 0)
|
|
136
|
-
self.assertEqual(stderr, "")
|
|
137
|
-
self.assertEqual(json.loads(stdout), {"transport": "mqtt"})
|
|
138
|
-
write_point.assert_called_once_with(
|
|
139
|
-
"point-1",
|
|
140
|
-
80,
|
|
141
|
-
reason="test",
|
|
142
|
-
ticket="T-1",
|
|
143
|
-
transport="mqtt",
|
|
144
|
-
request_id=None,
|
|
145
|
-
dry_run=False,
|
|
146
|
-
confirmed=True,
|
|
147
|
-
)
|
|
105
|
+
self.assertEqual(parameters, {"--file", "--document-hash", "--plan-hash", "--confirm"})
|
|
106
|
+
|
|
107
|
+
def test_structured_help_industrial_declares_transport_defaults(self) -> None:
|
|
108
|
+
code, stdout, stderr = run_cli("help", "industrial")
|
|
109
|
+
|
|
110
|
+
self.assertEqual(code, 0)
|
|
111
|
+
self.assertEqual(stderr, "")
|
|
112
|
+
payload = json.loads(stdout)
|
|
113
|
+
self.assertTrue(any("Realtime defaults to WebSocket" in rule for rule in payload["rules"]))
|
|
114
|
+
self.assertTrue(any("Point control defaults to HTTP" in rule for rule in payload["rules"]))
|
|
115
|
+
|
|
116
|
+
def test_industrial_point_write_forwards_explicit_mqtt_transport(self) -> None:
|
|
117
|
+
with mock.patch.object(
|
|
118
|
+
industrial_commands.industrial, "write_point", return_value={"transport": "mqtt"}
|
|
119
|
+
) as write_point:
|
|
120
|
+
code, stdout, stderr = run_cli(
|
|
121
|
+
"industrial",
|
|
122
|
+
"point-write",
|
|
123
|
+
"point-1",
|
|
124
|
+
"--value-json",
|
|
125
|
+
"80",
|
|
126
|
+
"--reason",
|
|
127
|
+
"test",
|
|
128
|
+
"--ticket",
|
|
129
|
+
"T-1",
|
|
130
|
+
"--transport",
|
|
131
|
+
"mqtt",
|
|
132
|
+
"--confirm",
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
self.assertEqual(code, 0)
|
|
136
|
+
self.assertEqual(stderr, "")
|
|
137
|
+
self.assertEqual(json.loads(stdout), {"transport": "mqtt"})
|
|
138
|
+
write_point.assert_called_once_with(
|
|
139
|
+
"point-1",
|
|
140
|
+
80,
|
|
141
|
+
reason="test",
|
|
142
|
+
ticket="T-1",
|
|
143
|
+
transport="mqtt",
|
|
144
|
+
request_id=None,
|
|
145
|
+
dry_run=False,
|
|
146
|
+
confirmed=True,
|
|
147
|
+
)
|
|
148
148
|
|
|
149
149
|
def test_uns_validate_loads_json_and_calls_sdk(self) -> None:
|
|
150
150
|
document = {"schemaVersion": "1.0", "resources": {}, "uns": {"nodes": []}}
|
|
File without changes
|
|
File without changes
|
{licos_platform_cli-0.4.0 → licos_platform_cli-0.4.1}/src/licos_platform_cli/database_commands.py
RENAMED
|
File without changes
|
{licos_platform_cli-0.4.0 → licos_platform_cli-0.4.1}/src/licos_platform_cli/knowledge_commands.py
RENAMED
|
File without changes
|
{licos_platform_cli-0.4.0 → licos_platform_cli-0.4.1}/src/licos_platform_cli/oauth_commands.py
RENAMED
|
File without changes
|
{licos_platform_cli-0.4.0 → licos_platform_cli-0.4.1}/src/licos_platform_cli/storage_commands.py
RENAMED
|
File without changes
|
{licos_platform_cli-0.4.0 → licos_platform_cli-0.4.1}/src/licos_platform_cli/uns_commands.py
RENAMED
|
File without changes
|