onvif-python 0.4.2__py3-none-any.whl → 0.4.4__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. onvif/cli/__init__.py +0 -4
  2. onvif/cli/interactive/__init__.py +5 -0
  3. onvif/cli/interactive/context.py +55 -0
  4. onvif/cli/interactive/information.py +175 -0
  5. onvif/cli/interactive/navigation.py +185 -0
  6. onvif/cli/interactive/reference.py +204 -0
  7. onvif/cli/interactive/service.py +325 -0
  8. onvif/cli/interactive/shell.py +246 -0
  9. onvif/cli/interactive/utils.py +313 -0
  10. onvif/cli/main.py +1 -1
  11. onvif/client.py +2 -0
  12. onvif/meta.py +1 -1
  13. onvif/operator.py +92 -2
  14. onvif/services/deviceio.py +3 -3
  15. onvif/services/devicemgmt.py +3 -3
  16. onvif/services/media2.py +25 -6
  17. onvif/services/ptz.py +1 -1
  18. onvif/services/recording.py +5 -4
  19. onvif/services/security/tlsserver.py +9 -12
  20. onvif/utils/error_handlers.py +2 -0
  21. onvif/utils/exceptions.py +3 -1
  22. onvif/utils/service.py +39 -1
  23. onvif/utils/zeep.py +2 -0
  24. onvif/wsdl/ver10/advancedsecurity/wsdl/advancedsecurity.wsdl +10 -5
  25. onvif/wsdl/ver10/appmgmt/wsdl/appmgmt.wsdl +1 -1
  26. onvif/wsdl/ver10/events/wsdl/event-vs.wsdl +2 -2
  27. onvif/wsdl/ver10/events/wsdl/event.wsdl +2 -2
  28. onvif/wsdl/ver10/pacs/types.xsd +2 -2
  29. onvif/wsdl/ver10/recording.wsdl +3 -2
  30. onvif/wsdl/ver10/schema/common.xsd +1 -1
  31. onvif/wsdl/ver10/schema/metadatastream.xsd +21 -15
  32. onvif/wsdl/ver10/schema/onvif.xsd +26 -26
  33. onvif/wsdl/ver20/analytics/humanbody.xsd +18 -18
  34. onvif/wsdl/ver20/analytics/humanface.xsd +18 -18
  35. onvif/wsdl/ver20/analytics/radiometry.xsd +2 -2
  36. onvif/wsdl/ver20/analytics/rules.xsd +1 -1
  37. onvif/wsdl/ver20/media/wsdl/media.wsdl +7 -7
  38. {onvif_python-0.4.2.dist-info → onvif_python-0.4.4.dist-info}/METADATA +34 -34
  39. {onvif_python-0.4.2.dist-info → onvif_python-0.4.4.dist-info}/RECORD +45 -37
  40. onvif_python-0.4.2.dist-info/licenses/LICENSE.md → onvif_python-0.4.4.dist-info/licenses/LICENSE +1 -1
  41. onvif_python-0.4.4.dist-info/licenses/NOTICE +11 -0
  42. onvif/cli/interactive.py +0 -1335
  43. /onvif/wsdl/{LICENSE.md → LICENSE} +0 -0
  44. {onvif_python-0.4.2.dist-info → onvif_python-0.4.4.dist-info}/WHEEL +0 -0
  45. {onvif_python-0.4.2.dist-info → onvif_python-0.4.4.dist-info}/entry_points.txt +0 -0
  46. {onvif_python-0.4.2.dist-info → onvif_python-0.4.4.dist-info}/top_level.txt +0 -0
onvif/cli/__init__.py CHANGED
@@ -1,5 +1 @@
1
1
  """ONVIF CLI."""
2
-
3
- from onvif.cli.main import main
4
-
5
- __all__ = ["main"]
@@ -0,0 +1,5 @@
1
+ """ONVIF Interactive Shell."""
2
+
3
+ from onvif.cli.interactive.shell import InteractiveShell
4
+
5
+ __all__ = ["InteractiveShell"]
@@ -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')}")