onvif-python 0.4.2__py3-none-any.whl → 0.4.3__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.
- onvif/cli/__init__.py +0 -4
- onvif/cli/interactive/__init__.py +5 -0
- onvif/cli/interactive/context.py +55 -0
- onvif/cli/interactive/information.py +175 -0
- onvif/cli/interactive/navigation.py +185 -0
- onvif/cli/interactive/reference.py +204 -0
- onvif/cli/interactive/service.py +325 -0
- onvif/cli/interactive/shell.py +246 -0
- onvif/cli/interactive/utils.py +313 -0
- onvif/cli/main.py +1 -1
- onvif/client.py +2 -0
- onvif/meta.py +1 -1
- onvif/operator.py +92 -2
- onvif/services/deviceio.py +3 -3
- onvif/services/devicemgmt.py +3 -3
- onvif/services/media2.py +25 -6
- onvif/services/ptz.py +1 -1
- onvif/services/recording.py +5 -4
- onvif/services/security/tlsserver.py +9 -12
- onvif/utils/error_handlers.py +2 -0
- onvif/utils/exceptions.py +3 -1
- onvif/utils/service.py +39 -1
- onvif/utils/zeep.py +2 -0
- {onvif_python-0.4.2.dist-info → onvif_python-0.4.3.dist-info}/METADATA +34 -34
- onvif_python-0.4.3.dist-info/RECORD +82 -0
- onvif_python-0.4.2.dist-info/licenses/LICENSE.md → onvif_python-0.4.3.dist-info/licenses/LICENSE +1 -1
- onvif_python-0.4.3.dist-info/licenses/NOTICE +11 -0
- onvif/cli/interactive.py +0 -1335
- onvif/wsdl/schema/common/b-2.xsd +0 -580
- onvif/wsdl/schema/common/bf-2.xsd +0 -84
- onvif/wsdl/schema/common/bw-2.wsdl +0 -448
- onvif/wsdl/schema/common/include +0 -13
- onvif/wsdl/schema/common/mpqf.xsd +0 -1109
- onvif/wsdl/schema/common/r-2.xsd +0 -50
- onvif/wsdl/schema/common/rw-2.wsdl +0 -54
- onvif/wsdl/schema/common/soap-envelope +0 -165
- onvif/wsdl/schema/common/t-1.xsd +0 -185
- onvif/wsdl/schema/common/ws-addr.xsd +0 -144
- onvif/wsdl/schema/common/xml.xsd +0 -287
- onvif/wsdl/schema/common/xmlmime +0 -49
- onvif/wsdl/ver10/accesscontrol/wsdl/index.html +0 -7
- onvif/wsdl/ver10/accessrules/wsdl/accessrules.wsdl +0 -702
- onvif/wsdl/ver10/accessrules/wsdl/index.html +0 -7
- onvif/wsdl/ver10/actionengine.wsdl +0 -1265
- onvif/wsdl/ver10/advancedsecurity/wsdl/advancedsecurity.wsdl +0 -4205
- onvif/wsdl/ver10/advancedsecurity/wsdl/index.html +0 -7
- onvif/wsdl/ver10/analyticsdevice.wsdl +0 -709
- onvif/wsdl/ver10/appmgmt/wsdl/appmgmt.wsdl +0 -502
- onvif/wsdl/ver10/appmgmt/wsdl/index.html +0 -7
- onvif/wsdl/ver10/authenticationbehavior/wsdl/authenticationbehavior.wsdl +0 -1382
- onvif/wsdl/ver10/authenticationbehavior/wsdl/index.html +0 -7
- onvif/wsdl/ver10/credential/wsdl/credential.wsdl +0 -2018
- onvif/wsdl/ver10/credential/wsdl/index.html +0 -7
- onvif/wsdl/ver10/device/wsdl/devicemgmt.wsdl +0 -4786
- onvif/wsdl/ver10/deviceio.wsdl +0 -1414
- onvif/wsdl/ver10/display/display.wsdl +0 -383
- onvif/wsdl/ver10/display/wsdl/index.htm +0 -383
- onvif/wsdl/ver10/display.wsdl +0 -521
- onvif/wsdl/ver10/doorcontrol/wsdl/index.html +0 -7
- onvif/wsdl/ver10/events/wsdl/bw-2-vs-mod.wsdl +0 -449
- onvif/wsdl/ver10/events/wsdl/event-vs.wsdl +0 -947
- onvif/wsdl/ver10/events/wsdl/event.wsdl +0 -947
- onvif/wsdl/ver10/events/wsdl/index.html +0 -7
- onvif/wsdl/ver10/federatedsearch.wsdl +0 -344
- onvif/wsdl/ver10/media/wsdl/media.wsdl +0 -3684
- onvif/wsdl/ver10/pacs/accesscontrol.wsdl +0 -1942
- onvif/wsdl/ver10/pacs/doorcontrol.wsdl +0 -1792
- onvif/wsdl/ver10/pacs/types.xsd +0 -181
- onvif/wsdl/ver10/provisioning/wsdl/provisioning.wsdl +0 -588
- onvif/wsdl/ver10/receiver.wsdl +0 -398
- onvif/wsdl/ver10/recording.wsdl +0 -1442
- onvif/wsdl/ver10/replay.wsdl +0 -221
- onvif/wsdl/ver10/schedule/wsdl/index.html +0 -7
- onvif/wsdl/ver10/schedule/wsdl/schedule.wsdl +0 -1383
- onvif/wsdl/ver10/schema/common.xsd +0 -431
- onvif/wsdl/ver10/schema/metadatastream.xsd +0 -534
- onvif/wsdl/ver10/schema/onvif.xsd +0 -9743
- onvif/wsdl/ver10/search.wsdl +0 -1114
- onvif/wsdl/ver10/thermal/wsdl/index.html +0 -7
- onvif/wsdl/ver10/thermal/wsdl/thermal.wsdl +0 -777
- onvif/wsdl/ver10/topics/topicns.xml +0 -13
- onvif/wsdl/ver10/uplink/wsdl/index.html +0 -7
- onvif/wsdl/ver10/uplink/wsdl/uplink.wsdl +0 -256
- onvif/wsdl/ver20/analytics/humanbody.xsd +0 -490
- onvif/wsdl/ver20/analytics/humanface.xsd +0 -501
- onvif/wsdl/ver20/analytics/index.html +0 -7
- onvif/wsdl/ver20/analytics/radiometry.xsd +0 -389
- onvif/wsdl/ver20/analytics/rules.xsd +0 -127
- onvif/wsdl/ver20/analytics/wsdl/analytics.wsdl +0 -757
- onvif/wsdl/ver20/analytics/wsdl/index.html +0 -7
- onvif/wsdl/ver20/imaging/wsdl/imaging.wsdl +0 -603
- onvif/wsdl/ver20/media/wsdl/index.html +0 -7
- onvif/wsdl/ver20/media/wsdl/media.wsdl +0 -3126
- onvif/wsdl/ver20/ptz/wsdl/ptz.wsdl +0 -1450
- onvif/wsdl/ver20/util/federated_search_documentation.html +0 -1647
- onvif/wsdl/ver20/util/fedsearch-docu-viewer.xsl +0 -8
- onvif/wsdl/ver20/util/onvif-wsdl-viewer.xsl +0 -2580
- onvif/wsdl/ver20/util/operationIndex.html +0 -97
- onvif_python-0.4.2.dist-info/RECORD +0 -144
- /onvif/wsdl/{LICENSE.md → LICENSE} +0 -0
- {onvif_python-0.4.2.dist-info → onvif_python-0.4.3.dist-info}/WHEEL +0 -0
- {onvif_python-0.4.2.dist-info → onvif_python-0.4.3.dist-info}/entry_points.txt +0 -0
- {onvif_python-0.4.2.dist-info → onvif_python-0.4.3.dist-info}/top_level.txt +0 -0
onvif/cli/__init__.py
CHANGED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""ONVIF Interactive Shell context."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from onvif.client import ONVIFClient
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class ShellContext: # pylint: disable=too-many-instance-attributes
|
|
12
|
+
"""Store shared state for the interactive shell."""
|
|
13
|
+
|
|
14
|
+
UNSET = object()
|
|
15
|
+
|
|
16
|
+
client: ONVIFClient
|
|
17
|
+
args: object
|
|
18
|
+
|
|
19
|
+
current_service: object | None = None
|
|
20
|
+
current_service_name: str | None = None
|
|
21
|
+
stored_data: dict[str, Any] = field(
|
|
22
|
+
default_factory=dict
|
|
23
|
+
) # For storing command results
|
|
24
|
+
stored_metadata: dict[str, dict[str, Any]] = field(
|
|
25
|
+
default_factory=dict
|
|
26
|
+
) # For storing metadata about stored data (service, method
|
|
27
|
+
|
|
28
|
+
last_result: Any = field(default=UNSET, repr=False)
|
|
29
|
+
last_method: str | None = None
|
|
30
|
+
last_service_name: str | None = None
|
|
31
|
+
last_operation_timestamp: datetime | None = None
|
|
32
|
+
|
|
33
|
+
base_commands: list[str] = field(
|
|
34
|
+
default_factory=lambda: [
|
|
35
|
+
"capabilities",
|
|
36
|
+
"caps",
|
|
37
|
+
"services",
|
|
38
|
+
"help",
|
|
39
|
+
"exit",
|
|
40
|
+
"store",
|
|
41
|
+
"rm",
|
|
42
|
+
"show",
|
|
43
|
+
"cls",
|
|
44
|
+
"clear",
|
|
45
|
+
"info",
|
|
46
|
+
"debug",
|
|
47
|
+
"ls",
|
|
48
|
+
"cd",
|
|
49
|
+
"shortcuts",
|
|
50
|
+
"desc",
|
|
51
|
+
"type",
|
|
52
|
+
]
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
device_info_text: str | None = None
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
"""Information commands implementation."""
|
|
2
|
+
|
|
3
|
+
import cmd
|
|
4
|
+
from typing import Callable
|
|
5
|
+
|
|
6
|
+
from requests.exceptions import RequestException
|
|
7
|
+
from zeep.exceptions import TransportError
|
|
8
|
+
|
|
9
|
+
from onvif.cli.helpers.messages import INTERACTIVE_HELP, INTERACTIVE_SHORTCUTS
|
|
10
|
+
from onvif.cli.utils import (
|
|
11
|
+
colorize,
|
|
12
|
+
format_capabilities_as_services,
|
|
13
|
+
format_services_list,
|
|
14
|
+
)
|
|
15
|
+
from onvif.utils.exceptions import ONVIFOperationException
|
|
16
|
+
|
|
17
|
+
from .context import ShellContext
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class InformationCommands:
|
|
21
|
+
"""Provide information commands."""
|
|
22
|
+
|
|
23
|
+
context: ShellContext
|
|
24
|
+
_handle_connection_error: Callable[..., None]
|
|
25
|
+
|
|
26
|
+
def do_help(self, arg):
|
|
27
|
+
"""Show help information."""
|
|
28
|
+
if arg:
|
|
29
|
+
cmd.Cmd.do_help(self, arg)
|
|
30
|
+
else:
|
|
31
|
+
print(INTERACTIVE_HELP)
|
|
32
|
+
|
|
33
|
+
def do_shortcuts(self, _line):
|
|
34
|
+
"""Show available shortcuts."""
|
|
35
|
+
print(INTERACTIVE_SHORTCUTS)
|
|
36
|
+
|
|
37
|
+
def do_info(self, _line):
|
|
38
|
+
"""Show connection and device information."""
|
|
39
|
+
# Build connection and CLI options info
|
|
40
|
+
options_info = []
|
|
41
|
+
|
|
42
|
+
# Connection options
|
|
43
|
+
if hasattr(self.context.args, "https") and self.context.args.https:
|
|
44
|
+
options_info.append(f" Use HTTPS : {colorize('True', 'green')}")
|
|
45
|
+
|
|
46
|
+
if (
|
|
47
|
+
hasattr(self.context.args, "no_verify_ssl")
|
|
48
|
+
and self.context.args.no_verify_ssl
|
|
49
|
+
):
|
|
50
|
+
options_info.append(f" Verify SSL : {colorize('False', 'red')}")
|
|
51
|
+
|
|
52
|
+
if (
|
|
53
|
+
hasattr(self.context.args, "timeout") and self.context.args.timeout != 10
|
|
54
|
+
): # 10 is default
|
|
55
|
+
options_info.append(
|
|
56
|
+
f" Timeout : {colorize(f'{self.context.args.timeout}s', 'yellow')}"
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
# CLI options
|
|
60
|
+
if hasattr(self.context.args, "debug") and self.context.args.debug:
|
|
61
|
+
options_info.append(f" Debug Mode : {colorize('True', 'green')}")
|
|
62
|
+
|
|
63
|
+
if hasattr(self.context.args, "no_patch") and self.context.args.no_patch:
|
|
64
|
+
options_info.append(f" ZeepPatcher : {colorize('Disabled', 'red')}")
|
|
65
|
+
|
|
66
|
+
if hasattr(self.context.args, "wsdl") and self.context.args.wsdl:
|
|
67
|
+
options_info.append(
|
|
68
|
+
f" Custom WSDL : {colorize(self.context.args.wsdl, 'yellow')}"
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
if (
|
|
72
|
+
hasattr(self.context.args, "health_check_interval")
|
|
73
|
+
and self.context.args.health_check_interval != 10
|
|
74
|
+
): # 10 is default
|
|
75
|
+
options_info.append(
|
|
76
|
+
f" Health Check : every {colorize(f'{self.context.args.health_check_interval}s', 'yellow')}"
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
# Format options info
|
|
80
|
+
options_display = ""
|
|
81
|
+
if options_info:
|
|
82
|
+
options_display = "\n" + "\n".join(options_info)
|
|
83
|
+
|
|
84
|
+
# Display connection and device information using stored data
|
|
85
|
+
print(
|
|
86
|
+
f"\n{colorize('[ONVIF Terminal Client]', 'yellow')}"
|
|
87
|
+
f"\n Connected to : "
|
|
88
|
+
f"{colorize(f'{self.context.args.host}:{self.context.args.port}', 'yellow')}"
|
|
89
|
+
f"\n Auth Method : "
|
|
90
|
+
f"{colorize('HTTP Digest' if self.context.args.digest else 'WS-UsernameToken', 'yellow')}"
|
|
91
|
+
f"{options_display}{self.context.device_info_text}"
|
|
92
|
+
)
|
|
93
|
+
print() # Extra newline for spacing
|
|
94
|
+
|
|
95
|
+
def do_capabilities(self, _line):
|
|
96
|
+
"""Show device capabilities in service format."""
|
|
97
|
+
try:
|
|
98
|
+
if (
|
|
99
|
+
hasattr(self.context.client, "capabilities")
|
|
100
|
+
and self.context.client.capabilities
|
|
101
|
+
):
|
|
102
|
+
result = self.context.client.capabilities
|
|
103
|
+
else:
|
|
104
|
+
result = self.context.client.devicemgmt().GetCapabilities(
|
|
105
|
+
Category="All"
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
self.context.stored_data["capabilities"] = result
|
|
109
|
+
self.context.stored_metadata["capabilities"] = {
|
|
110
|
+
"service": "devicemgmt",
|
|
111
|
+
"method": "GetCapabilities",
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
# Use special formatting for capabilities
|
|
115
|
+
output = format_capabilities_as_services(result)
|
|
116
|
+
print(output)
|
|
117
|
+
except ONVIFOperationException as e:
|
|
118
|
+
if isinstance(e.original_exception, (RequestException, TransportError)):
|
|
119
|
+
self._handle_connection_error()
|
|
120
|
+
else:
|
|
121
|
+
print(f"{colorize('Error:', 'red')} {e}")
|
|
122
|
+
|
|
123
|
+
def do_caps(self, line):
|
|
124
|
+
"""Show device capabilities (alias for 'capabilities')"""
|
|
125
|
+
return self.do_capabilities(line)
|
|
126
|
+
|
|
127
|
+
def do_services(self, _line):
|
|
128
|
+
"""Show available ONVIF services in service format."""
|
|
129
|
+
try:
|
|
130
|
+
if (
|
|
131
|
+
hasattr(self.context.client, "services")
|
|
132
|
+
and self.context.client.services
|
|
133
|
+
):
|
|
134
|
+
result = self.context.client.services
|
|
135
|
+
else:
|
|
136
|
+
result = self.context.client.devicemgmt().GetServices(
|
|
137
|
+
IncludeCapability=False
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
self.context.stored_data["services"] = result
|
|
141
|
+
self.context.stored_metadata["services"] = {
|
|
142
|
+
"service": "devicemgmt",
|
|
143
|
+
"method": "GetServices",
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
# Use special formatting for services
|
|
147
|
+
output = format_services_list(result)
|
|
148
|
+
print(output)
|
|
149
|
+
except ONVIFOperationException as e:
|
|
150
|
+
if isinstance(e.original_exception, (RequestException, TransportError)):
|
|
151
|
+
self._handle_connection_error()
|
|
152
|
+
else:
|
|
153
|
+
print(f"{colorize('Error:', 'red')} {e}")
|
|
154
|
+
|
|
155
|
+
def do_debug(self, _line):
|
|
156
|
+
"""Show debug information."""
|
|
157
|
+
if self.context.client.xml_plugin:
|
|
158
|
+
if self.context.last_method and self.context.last_operation_timestamp:
|
|
159
|
+
print(f"{colorize('Last Operation:', 'cyan')}")
|
|
160
|
+
print(
|
|
161
|
+
f" operation: {self.context.last_service_name}.{self.context.last_method}()"
|
|
162
|
+
)
|
|
163
|
+
print(
|
|
164
|
+
f" timestamp: {self.context.last_operation_timestamp.strftime('%Y-%m-%d %H:%M:%S')}\n"
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
print(f"{colorize('Last SOAP Request:', 'cyan')}")
|
|
168
|
+
print(self.context.client.xml_plugin.last_sent_xml or "None")
|
|
169
|
+
print(f"\n{colorize('Last SOAP Response:', 'cyan')}")
|
|
170
|
+
print(self.context.client.xml_plugin.last_received_xml or "None")
|
|
171
|
+
else:
|
|
172
|
+
print(f"{colorize('Debug mode not enabled', 'yellow')}")
|
|
173
|
+
print(
|
|
174
|
+
f"Start CLI with {colorize('--debug', 'white')} flag to enable XML capture"
|
|
175
|
+
)
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
"""Navigation commands implementation."""
|
|
2
|
+
|
|
3
|
+
import traceback
|
|
4
|
+
from typing import Callable
|
|
5
|
+
|
|
6
|
+
from onvif.cli.utils import (
|
|
7
|
+
colorize,
|
|
8
|
+
get_device_available_services,
|
|
9
|
+
get_service_methods,
|
|
10
|
+
get_service_required_args,
|
|
11
|
+
parse_json_params,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
from .context import ShellContext
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class NavigationCommands:
|
|
18
|
+
"""Provide navigation commands."""
|
|
19
|
+
|
|
20
|
+
context: ShellContext
|
|
21
|
+
_display_grid: Callable[..., None]
|
|
22
|
+
update_prompt: Callable[[], None]
|
|
23
|
+
|
|
24
|
+
def do_ls(self, _line):
|
|
25
|
+
"""List available commands/services like TAB completion."""
|
|
26
|
+
if self.context.current_service:
|
|
27
|
+
# In service mode - show available methods
|
|
28
|
+
methods = get_service_methods(self.context.current_service)
|
|
29
|
+
# Add helper commands in service mode
|
|
30
|
+
methods.extend([colorize("type", "yellow"), colorize("desc", "yellow")])
|
|
31
|
+
if methods:
|
|
32
|
+
# Use the same display format as TAB completion
|
|
33
|
+
self._display_grid(methods)
|
|
34
|
+
else:
|
|
35
|
+
print("No methods available")
|
|
36
|
+
else:
|
|
37
|
+
# In root mode - show device-specific services and commands
|
|
38
|
+
services = get_device_available_services(self.context.client)
|
|
39
|
+
commands = self.context.base_commands
|
|
40
|
+
all_items = services + commands
|
|
41
|
+
|
|
42
|
+
if all_items:
|
|
43
|
+
# Use the same display format as TAB completion
|
|
44
|
+
self._display_grid(all_items)
|
|
45
|
+
else:
|
|
46
|
+
print("No commands available")
|
|
47
|
+
|
|
48
|
+
def do_cd(self, line):
|
|
49
|
+
"""Change to service directory (alias for entering service)"""
|
|
50
|
+
if not line:
|
|
51
|
+
print("Usage: cd <service_name>")
|
|
52
|
+
available_services = get_device_available_services(self.context.client)
|
|
53
|
+
colored_services = [colorize(svc, "cyan") for svc in available_services]
|
|
54
|
+
print(f"Available services: {', '.join(colored_services)}")
|
|
55
|
+
return None
|
|
56
|
+
return self.do_enter_service(line)
|
|
57
|
+
|
|
58
|
+
def complete_cd(self, text, _line, _begidx, _endidx):
|
|
59
|
+
"""Autocomplete service names for cd command."""
|
|
60
|
+
services = get_device_available_services(self.context.client)
|
|
61
|
+
return [s for s in services if s.lower().startswith(text.lower())]
|
|
62
|
+
|
|
63
|
+
def do_up(self, _line):
|
|
64
|
+
"""Exit current service mode (go up one level)"""
|
|
65
|
+
if self.context.current_service:
|
|
66
|
+
print(
|
|
67
|
+
f"{colorize('Exited service:', 'yellow')} "
|
|
68
|
+
f"{colorize(f'{self.context.current_service_name}', 'cyan')}"
|
|
69
|
+
)
|
|
70
|
+
self.context.current_service = None
|
|
71
|
+
self.context.current_service_name = None
|
|
72
|
+
self.update_prompt()
|
|
73
|
+
else:
|
|
74
|
+
print("Not in service mode")
|
|
75
|
+
|
|
76
|
+
# pylint: disable=too-many-locals,too-many-branches,too-many-statements
|
|
77
|
+
def do_enter_service(self, line):
|
|
78
|
+
"""Enter service mode with optional arguments for services that require them."""
|
|
79
|
+
# Parse service name and arguments
|
|
80
|
+
parts = line.split(None, 1)
|
|
81
|
+
service_name = parts[0] if parts else ""
|
|
82
|
+
args_str = parts[1] if len(parts) > 1 else ""
|
|
83
|
+
|
|
84
|
+
available_services = get_device_available_services(self.context.client)
|
|
85
|
+
if service_name not in available_services:
|
|
86
|
+
print(f"{colorize('Error:', 'red')} Unknown service '{service_name}'")
|
|
87
|
+
colored_services = [colorize(svc, "cyan") for svc in available_services]
|
|
88
|
+
print(f"Available services: {', '.join(colored_services)}")
|
|
89
|
+
return
|
|
90
|
+
|
|
91
|
+
try:
|
|
92
|
+
# Check if this service requires arguments
|
|
93
|
+
required_args = get_service_required_args(service_name)
|
|
94
|
+
|
|
95
|
+
if required_args:
|
|
96
|
+
# Service requires arguments
|
|
97
|
+
if not args_str:
|
|
98
|
+
# No arguments provided, show help
|
|
99
|
+
print(
|
|
100
|
+
f"{colorize('Error:', 'red')} Service '{service_name}' requires arguments"
|
|
101
|
+
)
|
|
102
|
+
print(
|
|
103
|
+
f"{colorize('Usage:', 'yellow')} {service_name} "
|
|
104
|
+
f"{' '.join([f'{arg}=<value>' for arg in required_args])}"
|
|
105
|
+
)
|
|
106
|
+
print(
|
|
107
|
+
f"{colorize('Example:', 'yellow')} "
|
|
108
|
+
f"{service_name} {required_args[0]}=$subscription"
|
|
109
|
+
)
|
|
110
|
+
return
|
|
111
|
+
|
|
112
|
+
# Parse arguments
|
|
113
|
+
try:
|
|
114
|
+
parsed_args = parse_json_params(args_str)
|
|
115
|
+
except (OSError, AttributeError, ValueError, TypeError) as e:
|
|
116
|
+
print(f"{colorize('Error parsing arguments:', 'red')} {e}")
|
|
117
|
+
return
|
|
118
|
+
|
|
119
|
+
# Check if all required arguments are provided
|
|
120
|
+
missing_args = [arg for arg in required_args if arg not in parsed_args]
|
|
121
|
+
if missing_args:
|
|
122
|
+
print(
|
|
123
|
+
f"{colorize('Error:', 'red')} "
|
|
124
|
+
f"Missing required arguments: {', '.join(missing_args)}"
|
|
125
|
+
)
|
|
126
|
+
print(
|
|
127
|
+
f"{colorize('Usage:', 'yellow')} {service_name} "
|
|
128
|
+
f"{' '.join([f'{arg}=<value>' for arg in required_args])}"
|
|
129
|
+
)
|
|
130
|
+
return
|
|
131
|
+
|
|
132
|
+
# Resolve stored references in arguments
|
|
133
|
+
for arg_name in required_args:
|
|
134
|
+
arg_value = parsed_args[arg_name]
|
|
135
|
+
if isinstance(arg_value, str) and arg_value.startswith("$"):
|
|
136
|
+
# Resolve stored reference
|
|
137
|
+
reference = arg_value[1:] # Remove $ prefix
|
|
138
|
+
resolved_value = self._resolve_stored_reference(reference)
|
|
139
|
+
if resolved_value is None:
|
|
140
|
+
print(
|
|
141
|
+
f"{colorize('Error:', 'red')} "
|
|
142
|
+
f"Could not resolve reference '{arg_value}'"
|
|
143
|
+
)
|
|
144
|
+
return
|
|
145
|
+
parsed_args[arg_name] = resolved_value
|
|
146
|
+
|
|
147
|
+
# Call service method with arguments
|
|
148
|
+
service_method = getattr(self.context.client, service_name)
|
|
149
|
+
service = service_method(**parsed_args)
|
|
150
|
+
else:
|
|
151
|
+
# Service doesn't require arguments
|
|
152
|
+
service = getattr(self.context.client, service_name)()
|
|
153
|
+
|
|
154
|
+
self.context.current_service = service
|
|
155
|
+
self.context.current_service_name = service_name
|
|
156
|
+
self.update_prompt()
|
|
157
|
+
|
|
158
|
+
print(
|
|
159
|
+
f"{colorize('Entered service:', 'yellow')} {colorize(service_name, 'cyan')}"
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
# Show available methods
|
|
163
|
+
methods = get_service_methods(service)
|
|
164
|
+
methods_preview = ", ".join(methods[:10])
|
|
165
|
+
if len(methods) > 10:
|
|
166
|
+
print(
|
|
167
|
+
f"{colorize('Available methods:', 'yellow')} {methods_preview} ... "
|
|
168
|
+
f"and {colorize(f'{len(methods) - 10} more.', 'yellow')}"
|
|
169
|
+
)
|
|
170
|
+
print(
|
|
171
|
+
f"Type {colorize('ls', 'cyan')}/press "
|
|
172
|
+
f"<{colorize('TAB', 'yellow')}> to see all."
|
|
173
|
+
)
|
|
174
|
+
else:
|
|
175
|
+
print(f"{colorize('Available methods:', 'yellow')} {methods_preview}")
|
|
176
|
+
print(f"Type {colorize('up', 'cyan')} to exit service mode.")
|
|
177
|
+
|
|
178
|
+
except (OSError, RuntimeError, ValueError, AttributeError) as e:
|
|
179
|
+
print(f"{colorize('Error entering service:', 'red')} {e}")
|
|
180
|
+
if self.context.args.debug:
|
|
181
|
+
traceback.print_exc()
|
|
182
|
+
|
|
183
|
+
def do_exit_service(self, line):
|
|
184
|
+
"""Exit current service mode (alias for 'up')"""
|
|
185
|
+
return self.do_up(line)
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
"""Reference commands implementation."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import re
|
|
5
|
+
|
|
6
|
+
from onvif.cli.utils import colorize
|
|
7
|
+
|
|
8
|
+
from .context import ShellContext
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ReferenceCommands:
|
|
12
|
+
"""Provide stored-reference commands."""
|
|
13
|
+
|
|
14
|
+
context: ShellContext
|
|
15
|
+
|
|
16
|
+
def do_store(self, line):
|
|
17
|
+
"""Store last result with a name: store <name>."""
|
|
18
|
+
if not line:
|
|
19
|
+
print("Usage: store <name>")
|
|
20
|
+
return
|
|
21
|
+
|
|
22
|
+
if not re.match(r"^[a-zA-Z_][a-zA-Z0-9_]*$", line):
|
|
23
|
+
print(
|
|
24
|
+
f"{colorize('Error:', 'red')} Invalid name '{line}'. "
|
|
25
|
+
"Only letters, numbers, and underscores allowed, "
|
|
26
|
+
"and must not start with a number."
|
|
27
|
+
)
|
|
28
|
+
return
|
|
29
|
+
|
|
30
|
+
if line in self.context.stored_data:
|
|
31
|
+
print(
|
|
32
|
+
f"{colorize('Error:', 'red')} Name '{line}' already exists. "
|
|
33
|
+
"Use a different name or remove it first."
|
|
34
|
+
)
|
|
35
|
+
return
|
|
36
|
+
|
|
37
|
+
UNSET = object() # pylint: disable=invalid-name
|
|
38
|
+
|
|
39
|
+
if self.context.last_result is UNSET:
|
|
40
|
+
print(f"{colorize('Error:', 'red')} No result to store")
|
|
41
|
+
return
|
|
42
|
+
|
|
43
|
+
self.context.stored_data[line] = self.context.last_result
|
|
44
|
+
|
|
45
|
+
metadata = {}
|
|
46
|
+
|
|
47
|
+
if self.context.last_service_name:
|
|
48
|
+
metadata["service"] = self.context.last_service_name
|
|
49
|
+
|
|
50
|
+
if self.context.last_method:
|
|
51
|
+
metadata["method"] = self.context.last_method
|
|
52
|
+
|
|
53
|
+
self.context.stored_metadata[line] = metadata
|
|
54
|
+
|
|
55
|
+
service = metadata.get("service", "unknown")
|
|
56
|
+
method = metadata.get("method", "unknown")
|
|
57
|
+
|
|
58
|
+
print(
|
|
59
|
+
f"{colorize('Stored result as:', 'green')} "
|
|
60
|
+
f"{colorize('$' + line, 'yellow')} - "
|
|
61
|
+
f"{colorize(service, 'cyan')}.{colorize(method, 'white')}()"
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
def do_show(self, line):
|
|
65
|
+
"""Show stored data: show <name> or show <name>.<attribute> or show
|
|
66
|
+
<name>[index]"""
|
|
67
|
+
if not line:
|
|
68
|
+
print(colorize("Stored data:", "green"))
|
|
69
|
+
for name in self.context.stored_data:
|
|
70
|
+
# Get metadata if available
|
|
71
|
+
metadata = self.context.stored_metadata.get(name, {})
|
|
72
|
+
service = metadata.get("service", "unknown")
|
|
73
|
+
method = metadata.get("method", "unknown")
|
|
74
|
+
|
|
75
|
+
# Format: name - service.method()
|
|
76
|
+
info = (
|
|
77
|
+
f"{colorize('$'+name, 'yellow')} - "
|
|
78
|
+
f"{colorize(service, 'cyan')}.{colorize(method, 'white')}()"
|
|
79
|
+
)
|
|
80
|
+
print(f" {info}")
|
|
81
|
+
return
|
|
82
|
+
|
|
83
|
+
# Parse accessor expression (e.g., profiles[0].token or profiles.Token)
|
|
84
|
+
result = self._resolve_stored_reference(line)
|
|
85
|
+
if result is not None:
|
|
86
|
+
print(str(result))
|
|
87
|
+
else:
|
|
88
|
+
print(f"{colorize('Error:', 'red')} Cannot resolve '{line}'")
|
|
89
|
+
|
|
90
|
+
def _substitute_stored_references(self, params_str: str) -> str:
|
|
91
|
+
"""Substitute $variable references in parameter string with stored data.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
params_str: Parameter string that may contain $variable references
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
Parameter string with substituted values
|
|
98
|
+
"""
|
|
99
|
+
# Find all $variable references (e.g., $profiles[0].token)
|
|
100
|
+
pattern = (
|
|
101
|
+
r"\$([a-zA-Z_][a-zA-Z0-9_]*(?:\[[0-9]+\])?(?:\.[a-zA-Z_][a-zA-Z0-9_]*)*)"
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
def replace_reference(match):
|
|
105
|
+
reference = match.group(1)
|
|
106
|
+
value = self._resolve_stored_reference(reference)
|
|
107
|
+
|
|
108
|
+
if value is None:
|
|
109
|
+
print(f"{colorize('Warning:', 'yellow')} Cannot resolve ${reference}")
|
|
110
|
+
return match.group(0) # Keep original if not found
|
|
111
|
+
|
|
112
|
+
# Convert value to string representation suitable for JSON
|
|
113
|
+
# Use json.dumps to serialize values correctly for JSON parsing.
|
|
114
|
+
# This preserves numbers, booleans, nulls, arrays and objects.
|
|
115
|
+
try:
|
|
116
|
+
return json.dumps(value)
|
|
117
|
+
except (TypeError, KeyError, ValueError, RuntimeError):
|
|
118
|
+
# Fall back to string-quoting for anything not serializable
|
|
119
|
+
return json.dumps(str(value))
|
|
120
|
+
|
|
121
|
+
return re.sub(pattern, replace_reference, params_str)
|
|
122
|
+
|
|
123
|
+
def _resolve_stored_reference(self, reference: str):
|
|
124
|
+
"""Resolve stored data reference like 'profiles[0].token' or 'profiles.Token'.
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
reference: String reference like 'profiles[0]' or 'profiles.Token'
|
|
128
|
+
|
|
129
|
+
Returns:
|
|
130
|
+
Resolved value or None if not found
|
|
131
|
+
"""
|
|
132
|
+
# Parse the reference - e.g., "profiles[0].token" or "services.Namespace"
|
|
133
|
+
# Split by dots and brackets
|
|
134
|
+
parts = re.split(r"\.|\[|\]", reference)
|
|
135
|
+
parts = [p for p in parts if p] # Remove empty strings
|
|
136
|
+
|
|
137
|
+
if not parts:
|
|
138
|
+
return None
|
|
139
|
+
|
|
140
|
+
# Start with the stored variable name
|
|
141
|
+
var_name = parts[0]
|
|
142
|
+
if var_name not in self.context.stored_data:
|
|
143
|
+
return None
|
|
144
|
+
|
|
145
|
+
current = self.context.stored_data[var_name]
|
|
146
|
+
|
|
147
|
+
# Navigate through the rest of the path
|
|
148
|
+
for part in parts[1:]:
|
|
149
|
+
try:
|
|
150
|
+
# Check if it's an integer index
|
|
151
|
+
if part.isdigit():
|
|
152
|
+
index = int(part)
|
|
153
|
+
if isinstance(current, (list, tuple)):
|
|
154
|
+
current = current[index]
|
|
155
|
+
else:
|
|
156
|
+
# Try to convert to list if it's iterable
|
|
157
|
+
try:
|
|
158
|
+
current = list(current)[index]
|
|
159
|
+
except (TypeError, IndexError):
|
|
160
|
+
return None
|
|
161
|
+
else:
|
|
162
|
+
# It's an attribute name
|
|
163
|
+
if hasattr(current, part):
|
|
164
|
+
current = getattr(current, part)
|
|
165
|
+
elif isinstance(current, dict) and part in current:
|
|
166
|
+
current = current[part]
|
|
167
|
+
else:
|
|
168
|
+
return None
|
|
169
|
+
except (IndexError, AttributeError, KeyError, TypeError):
|
|
170
|
+
return None
|
|
171
|
+
|
|
172
|
+
return current
|
|
173
|
+
|
|
174
|
+
def complete_show(self, text, line, _begidx, _endidx):
|
|
175
|
+
"""Autocomplete stored variable names for show command."""
|
|
176
|
+
# Get the part being completed
|
|
177
|
+
parts = line.split()
|
|
178
|
+
if len(parts) <= 1 or (len(parts) == 2 and not line.endswith(" ")):
|
|
179
|
+
# Completing the variable name
|
|
180
|
+
return [name for name in self.context.stored_data if name.startswith(text)]
|
|
181
|
+
return []
|
|
182
|
+
|
|
183
|
+
def do_rm(self, line):
|
|
184
|
+
"""Remove stored data: rm <name>"""
|
|
185
|
+
if not line:
|
|
186
|
+
print("Usage: rm <name>")
|
|
187
|
+
return
|
|
188
|
+
if line in self.context.stored_data:
|
|
189
|
+
del self.context.stored_data[line]
|
|
190
|
+
if line in self.context.stored_metadata:
|
|
191
|
+
del self.context.stored_metadata[line]
|
|
192
|
+
print(f"{colorize('Removed:', 'yellow')} {colorize('$'+line, 'cyan')}")
|
|
193
|
+
else:
|
|
194
|
+
print(f"{colorize('Error:', 'red')} No stored data named '{line}'")
|
|
195
|
+
|
|
196
|
+
def complete_rm(self, text, _line, _begidx, _endidx):
|
|
197
|
+
"""Autocomplete stored variable names for rm command."""
|
|
198
|
+
return [name for name in self.context.stored_data if name.startswith(text)]
|
|
199
|
+
|
|
200
|
+
def do_cls(self, _line):
|
|
201
|
+
"""Clear stored data."""
|
|
202
|
+
self.context.stored_data.clear()
|
|
203
|
+
self.context.stored_metadata.clear()
|
|
204
|
+
print(f"{colorize('Cleared all stored data', 'yellow')}")
|