onvif-python 0.1.2__py3-none-any.whl → 0.1.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.
onvif/cli/interactive.py CHANGED
@@ -3,6 +3,8 @@
3
3
  import cmd
4
4
  import sys
5
5
  import socket
6
+ import ssl
7
+ import os
6
8
  import threading
7
9
  import textwrap
8
10
  from datetime import datetime
@@ -21,6 +23,7 @@ from .utils import (
21
23
  format_capabilities_as_services,
22
24
  format_services_list,
23
25
  get_method_documentation,
26
+ get_operation_type_info,
24
27
  )
25
28
 
26
29
 
@@ -61,6 +64,7 @@ class InteractiveShell(cmd.Cmd):
61
64
  "pwd",
62
65
  "shortcuts",
63
66
  "desc",
67
+ "type",
64
68
  ]
65
69
 
66
70
  # For background health check
@@ -179,7 +183,7 @@ class InteractiveShell(cmd.Cmd):
179
183
  " / __ \\/ | / / | / / _/ ____/",
180
184
  " / / / / |/ /| | / // // /_ ",
181
185
  "/ /_/ / /| / | |/ // // __/ ",
182
- "\\____/_/ |_/ |___/___/_/ v0.1.2",
186
+ "\\____/_/ |_/ |___/___/_/ v0.1.4",
183
187
  " ",
184
188
  ]
185
189
 
@@ -218,22 +222,39 @@ class InteractiveShell(cmd.Cmd):
218
222
  self._health_check_thread.start()
219
223
 
220
224
  def _periodic_health_check(self):
221
- """Periodically checks device connection using a TCP socket ping."""
222
- # Wait a moment before the first check to allow the shell to display the intro
225
+ """Periodically checks device connection using TCP or TLS depending on mode."""
226
+ # Wait before first check to allow intro to finish
223
227
  self._stop_health_check.wait(10.0)
228
+
224
229
  while not self._stop_health_check.is_set():
225
230
  sock = None
226
231
  try:
227
- # Create a socket and set a short timeout
228
- sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
229
- sock.settimeout(3) # 3-second timeout for the connection attempt
232
+ sock = socket.create_connection(
233
+ (self.args.host, self.args.port), timeout=5.0
234
+ )
235
+
236
+ # If HTTPS, perform a short TLS handshake check
237
+ if getattr(self.args, "https", False):
238
+ context = ssl.create_default_context()
239
+ context.check_hostname = False
240
+ context.verify_mode = ssl.CERT_NONE
241
+
242
+ with context.wrap_socket(
243
+ sock, server_hostname=self.args.host
244
+ ) as ssock:
245
+ ssock.settimeout(5)
246
+ # Try a minimal handshake
247
+ ssock.do_handshake()
248
+
249
+ # If we reach here, the connection (and TLS if applicable) succeeded
250
+ pass
230
251
 
231
- # Try to connect to the host and port
232
- sock.connect((self.args.host, self.args.port))
233
252
  except (
234
253
  socket.timeout,
235
254
  ConnectionRefusedError,
236
255
  socket.gaierror,
256
+ ssl.SSLError,
257
+ ssl.SSLEOFError,
237
258
  OSError,
238
259
  ) as e:
239
260
  # Connection failed, trigger exit
@@ -242,7 +263,7 @@ class InteractiveShell(cmd.Cmd):
242
263
  file=sys.stderr,
243
264
  )
244
265
  print(
245
- f"{colorize('Error:', 'red')} TCP check failed - {e}",
266
+ f"{colorize('Error:', 'red')} Health check failed: {e}",
246
267
  file=sys.stderr,
247
268
  )
248
269
  print(
@@ -251,15 +272,13 @@ class InteractiveShell(cmd.Cmd):
251
272
  )
252
273
  # Forcibly exit the entire process. This is necessary to interrupt
253
274
  # the blocking input() call in the main thread.
254
- import os
255
-
256
275
  os._exit(1)
257
276
  finally:
258
277
  # Ensure the socket is always closed
259
278
  if sock:
260
279
  sock.close()
261
280
 
262
- # Wait for the next interval or until stopped
281
+ # Wait before next check or stop signal
263
282
  self._stop_health_check.wait(5.0) # Check every 5 seconds
264
283
 
265
284
  def _handle_connection_error(self, e):
@@ -766,8 +785,9 @@ class InteractiveShell(cmd.Cmd):
766
785
 
767
786
  if not self.current_service:
768
787
  print(
769
- f"{colorize('Error:', 'red')} This command can only be used within a service context."
788
+ f"{colorize('Error:', 'red')} You must be in a service mode to use 'desc'."
770
789
  )
790
+ print(f"Enter a service first (e.g., {colorize('devicemgmt', 'yellow')})")
771
791
  return
772
792
 
773
793
  if not method_name:
@@ -808,8 +828,175 @@ class InteractiveShell(cmd.Cmd):
808
828
  f"No documentation or parameter info found for method '{method_name}'."
809
829
  )
810
830
 
811
- def help_desc(self):
812
- print(self.do_desc.__doc__)
831
+ def complete_desc(self, text, line, begidx, endidx):
832
+ """Autocomplete method names for desc command"""
833
+ if not self.current_service:
834
+ return []
835
+ methods = get_service_methods(self.current_service)
836
+ return [m for m in methods if m.lower().startswith(text.lower())]
837
+
838
+ def do_type(self, line):
839
+ """Show input and output types for an operation. Usage: type <operation_name>"""
840
+ operation_name = line.strip()
841
+
842
+ if not self.current_service:
843
+ print(
844
+ f"{colorize('Error:', 'red')} You must be in a service mode to use 'type'."
845
+ )
846
+ print(f"Enter a service first (e.g., {colorize('d', 'yellow')})")
847
+ return
848
+
849
+ if not operation_name:
850
+ print(f"Usage: {colorize('type <operation_name>', 'yellow')}")
851
+ return
852
+
853
+ if not hasattr(self.current_service, operation_name):
854
+ print(
855
+ f"{colorize('Error:', 'red')} Operation '{operation_name}' not found in service '{self.current_service_name}'."
856
+ )
857
+ print(f"Use {colorize('ls', 'yellow')} to see available operations.")
858
+ return
859
+
860
+ type_info = get_operation_type_info(self.current_service, operation_name)
861
+
862
+ if type_info:
863
+ # Helper function to display parameters recursively with tree-style indentation
864
+ def display_params(params, prefix_lines=None, is_root_level=False):
865
+ """
866
+ Display parameters with tree-style formatting.
867
+
868
+ Args:
869
+ params: List of parameter dictionaries
870
+ prefix_lines: List of strings representing the tree prefix for each line
871
+ is_root_level: If True, don't show tree characters at this level
872
+ """
873
+ if prefix_lines is None:
874
+ prefix_lines = []
875
+
876
+ for idx, param in enumerate(params):
877
+ is_last = idx == len(params) - 1
878
+
879
+ # Determine tree characters (only if not root level)
880
+ if is_root_level:
881
+ tree_branch = ""
882
+ tree_continue = ""
883
+ else:
884
+ if is_last:
885
+ tree_branch = "└── "
886
+ tree_continue = " "
887
+ else:
888
+ tree_branch = "├── "
889
+ tree_continue = "│ "
890
+
891
+ # Determine prefix symbol: + for attributes, - for elements
892
+ prefix_symbol = "+" if param.get("is_attribute", False) else "-"
893
+
894
+ # Format occurrences based on type
895
+ occurs = ""
896
+ if param.get("is_attribute", False):
897
+ # For attributes: only show "required" if use="required"
898
+ # Default for attributes is optional (no label needed)
899
+ if param["minOccurs"] == "1": # This means use="required"
900
+ occurs = f" - {colorize('required', 'yellow')}"
901
+ # If minOccurs == "0", it's optional (default), so no label
902
+ else:
903
+ # For elements: show unbounded, optional, or required
904
+ if param["maxOccurs"] == "unbounded":
905
+ occurs = f" - {colorize('unbounded', 'magenta')}"
906
+ elif param["minOccurs"] == "0":
907
+ occurs = f" - {colorize('optional', 'green')}"
908
+ else:
909
+ occurs = f" - {colorize('required', 'yellow')}"
910
+
911
+ # Format type
912
+ type_str = f"[{param['type']}]" if param["type"] else ""
913
+
914
+ # Build the current line prefix from all previous levels
915
+ current_prefix = "".join(prefix_lines)
916
+
917
+ # Add spacing for root level items
918
+ if is_root_level:
919
+ current_prefix = " "
920
+
921
+ # Display parameter name with tree structure, prefix, occurrence, and type
922
+ # Only add semicolon separator if there's an occurrence label
923
+ separator = ";" if occurs else ""
924
+ param_line = f"{current_prefix}{tree_branch}{prefix_symbol}{colorize(param['name'], 'white')}{occurs}{separator} {colorize(type_str, 'cyan')}"
925
+ print(param_line)
926
+
927
+ # Display documentation if available
928
+ if param.get("documentation"):
929
+ doc_lines = param["documentation"].split("\n")
930
+ for doc_line in doc_lines:
931
+ if doc_line.strip():
932
+ # Add tree continuation for documentation
933
+ if is_root_level:
934
+ doc_prefix = " "
935
+ else:
936
+ doc_prefix = current_prefix + tree_continue
937
+ wrapped_doc = textwrap.fill(
938
+ doc_line.strip(),
939
+ width=96, # Slightly less to account for tree chars
940
+ initial_indent=doc_prefix + " ",
941
+ subsequent_indent=doc_prefix + " ",
942
+ )
943
+ print(colorize(wrapped_doc, "reset"))
944
+
945
+ # Display children recursively with updated prefix
946
+ if param.get("children"):
947
+ if is_root_level:
948
+ # Start tree structure from children
949
+ new_prefix_lines = [" "]
950
+ else:
951
+ new_prefix_lines = prefix_lines + [tree_continue]
952
+ display_params(
953
+ param["children"], new_prefix_lines, is_root_level=False
954
+ )
955
+
956
+ # Display Input
957
+ if type_info["input"]:
958
+ input_msg = type_info["input"]
959
+ print(f"\n{colorize('Input:', 'cyan')}")
960
+ msg_name = f"[{input_msg['name']}]"
961
+ print(f"{colorize(msg_name, 'yellow')}")
962
+
963
+ if input_msg["parameters"]:
964
+ display_params(input_msg["parameters"], is_root_level=True)
965
+ else:
966
+ print(f" {colorize('(no parameters)', 'reset')}")
967
+ else:
968
+ print(
969
+ f"\n{colorize('Input:', 'cyan')} {colorize('(not defined)', 'reset')}"
970
+ )
971
+
972
+ # Display Output
973
+ if type_info["output"]:
974
+ output_msg = type_info["output"]
975
+ print(f"\n{colorize('Output:', 'cyan')}")
976
+ msg_name = f"[{output_msg['name']}]"
977
+ print(f"{colorize(msg_name, 'yellow')}")
978
+
979
+ if output_msg["parameters"]:
980
+ display_params(output_msg["parameters"], is_root_level=True)
981
+ else:
982
+ print(f" {colorize('(no parameters)', 'reset')}")
983
+ else:
984
+ print(
985
+ f"\n{colorize('Output:', 'cyan')} {colorize('(not defined)', 'reset')}"
986
+ )
987
+
988
+ print() # Add newline for spacing
989
+ else:
990
+ print(
991
+ f"{colorize('Error:', 'red')} Could not retrieve type information for '{operation_name}'."
992
+ )
993
+
994
+ def complete_type(self, text, line, begidx, endidx):
995
+ """Autocomplete method names for type command"""
996
+ if not self.current_service:
997
+ return []
998
+ methods = get_service_methods(self.current_service)
999
+ return [m for m in methods if m.lower().startswith(text.lower())]
813
1000
 
814
1001
  def do_cd(self, line):
815
1002
  """Change to service directory (alias for entering service)"""
@@ -821,6 +1008,11 @@ class InteractiveShell(cmd.Cmd):
821
1008
  return
822
1009
  return self.do_enter_service(line)
823
1010
 
1011
+ def complete_cd(self, text, line, begidx, endidx):
1012
+ """Autocomplete service names for cd command"""
1013
+ services = get_device_available_services(self.client)
1014
+ return [s for s in services if s.lower().startswith(text.lower())]
1015
+
824
1016
  def do_pwd(self, line):
825
1017
  """Show current service context"""
826
1018
  if self.current_service_name:
@@ -844,7 +1036,11 @@ class InteractiveShell(cmd.Cmd):
844
1036
  up - Go up one level
845
1037
  pwd - Show current context
846
1038
  clear - Clear terminal screen
847
- desc <method> - Show method documentation in service mode
1039
+ help <command> - Show help for a command
1040
+
1041
+ {colorize('Service Mode Commands:', 'yellow')}
1042
+ desc <method> - Show method documentation
1043
+ type <method> - Show input/output types from WSDL
848
1044
 
849
1045
  {colorize('Quick Access:', 'yellow')}
850
1046
  caps - Show capabilities (same as 'capabilities')
@@ -925,6 +1121,10 @@ class InteractiveShell(cmd.Cmd):
925
1121
  else:
926
1122
  print(f"{colorize('Error:', 'red')} No stored data named '{line}'")
927
1123
 
1124
+ def complete_rm(self, text, line, begidx, endidx):
1125
+ """Autocomplete stored variable names for rm command"""
1126
+ return [name for name in self.stored_data.keys() if name.startswith(text)]
1127
+
928
1128
  def do_show(self, line):
929
1129
  """Show stored data: show <name> or show <name>.<attribute> or show <name>[index]"""
930
1130
  if not line:
@@ -947,10 +1147,17 @@ class InteractiveShell(cmd.Cmd):
947
1147
  else:
948
1148
  print(f"{colorize('Error:', 'red')} Cannot resolve '{line}'")
949
1149
 
1150
+ def complete_show(self, text, line, begidx, endidx):
1151
+ """Autocomplete stored variable names for show command"""
1152
+ # Get the part being completed
1153
+ parts = line.split()
1154
+ if len(parts) <= 1 or (len(parts) == 2 and not line.endswith(" ")):
1155
+ # Completing the variable name
1156
+ return [name for name in self.stored_data.keys() if name.startswith(text)]
1157
+ return []
1158
+
950
1159
  def do_clear(self, line):
951
1160
  """Clear terminal screen"""
952
- import os
953
-
954
1161
  # Clear screen for both Windows and Unix-like systems
955
1162
  os.system("cls" if os.name == "nt" else "clear")
956
1163
 
@@ -1093,7 +1300,7 @@ class InteractiveShell(cmd.Cmd):
1093
1300
  super().do_help(line)
1094
1301
  else:
1095
1302
  help_text = f"""
1096
- {colorize('ONVIF Interactive Shell Commands -', 'cyan')}\n{colorize('https://github.com/nirsimetri/onvif-python', 'white')}
1303
+ {colorize('ONVIF Interactive Shell Commands - v0.1.4', 'cyan')}\n{colorize('https://github.com/nirsimetri/onvif-python', 'white')}
1097
1304
 
1098
1305
  {colorize('Basic Commands:', 'yellow')}
1099
1306
  capabilities, caps - Show device capabilities
@@ -1109,7 +1316,11 @@ class InteractiveShell(cmd.Cmd):
1109
1316
  up - Exit current service mode (go up one level)
1110
1317
  pwd - Show current service context
1111
1318
  clear - Clear terminal screen
1112
- desc <method> - Show method documentation in service mode
1319
+ help <command> - Show help for a specific command
1320
+
1321
+ {colorize('Service Mode Commands:', 'yellow')}
1322
+ desc <method> - Show method documentation
1323
+ type <method> - Show input/output types from WSDL
1113
1324
 
1114
1325
  {colorize('Method Execution:', 'yellow')}
1115
1326
  <method> - Execute method without parameters
onvif/cli/main.py CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  import argparse
4
4
  import sys
5
+ import warnings
5
6
  import getpass
6
7
  from typing import Any, Optional, Tuple
7
8
 
@@ -15,7 +16,7 @@ def create_parser():
15
16
  """Create argument parser for ONVIF CLI"""
16
17
  parser = argparse.ArgumentParser(
17
18
  prog="onvif",
18
- description=f"{colorize('ONVIF Terminal Client', 'yellow')} —\nhttps://github.com/nirsimetri/onvif-python",
19
+ description=f"{colorize('ONVIF Terminal Client', 'yellow')} — v0.1.4\nhttps://github.com/nirsimetri/onvif-python",
19
20
  formatter_class=argparse.RawDescriptionHelpFormatter,
20
21
  epilog=f"""
21
22
  Examples:
@@ -106,11 +107,19 @@ Examples:
106
107
  "params", nargs="*", help="Method parameters as Simple Parameter or JSON string"
107
108
  )
108
109
 
110
+ # ONVI CLI
111
+ parser.add_argument(
112
+ "--version", "-v", action="store_true", help="Show ONVIF CLI version and exit"
113
+ )
114
+
109
115
  return parser
110
116
 
111
117
 
112
118
  def main():
113
119
  """Main CLI entry point"""
120
+ # Setup custom warning format for cleaner output
121
+ setup_warning_format()
122
+
114
123
  parser = create_parser()
115
124
 
116
125
  # Check if no arguments provided at all
@@ -120,6 +129,17 @@ def main():
120
129
 
121
130
  args = parser.parse_intermixed_args()
122
131
 
132
+ # Show ONVIF CLI version
133
+ if args.version:
134
+ print(colorize("0.1.4", "yellow"))
135
+ sys.exit(0)
136
+
137
+ # Validate arguments early (before discovery)
138
+ if not args.interactive and (not args.service or not args.method):
139
+ parser.error(
140
+ f"Either {colorize('--interactive', 'white')}/{colorize('-i', 'white')} mode or {colorize('service/method', 'white')} must be specified"
141
+ )
142
+
123
143
  # Handle discovery mode
124
144
  if args.discover:
125
145
  if args.host:
@@ -127,8 +147,8 @@ def main():
127
147
  f"{colorize('--discover', 'white')} cannot be used with {colorize('--host', 'white')}"
128
148
  )
129
149
 
130
- # Discover devices
131
- devices = discover_devices(timeout=4)
150
+ # Discover devices (pass --https flag to prioritize HTTPS XAddrs)
151
+ devices = discover_devices(timeout=4, prefer_https=args.https)
132
152
 
133
153
  if not devices:
134
154
  print(f"\n{colorize('No ONVIF devices discovered. Exiting.', 'red')}")
@@ -141,8 +161,11 @@ def main():
141
161
  print(f"{colorize('Device selection cancelled.', 'cyan')}")
142
162
  sys.exit(0)
143
163
 
144
- # Set host and port from selected device
145
- args.host, args.port = selected
164
+ # Set host, port, and HTTPS from selected device
165
+ args.host, args.port, device_use_https = selected
166
+
167
+ # Use device's detected protocol (already filtered by prefer_https in discover_devices)
168
+ # No need to override - device info already has correct protocol based on --https flag
146
169
 
147
170
  # Validate that host is provided (either via --host or --discover)
148
171
  if not args.host:
@@ -168,12 +191,6 @@ def main():
168
191
  print("\nPassword entry cancelled.")
169
192
  sys.exit(1)
170
193
 
171
- # Validate arguments
172
- if not args.interactive and (not args.service or not args.method):
173
- parser.error(
174
- f"Either {colorize('--interactive', 'white')}/{colorize('-i', 'white')} mode or {colorize('service/method', 'white')} must be specified"
175
- )
176
-
177
194
  try:
178
195
  # Create ONVIF client
179
196
  client = ONVIFClient(
@@ -253,8 +270,16 @@ def execute_command(
253
270
  return method(**params)
254
271
 
255
272
 
256
- def discover_devices(timeout: int = 4) -> list:
257
- """Discover ONVIF devices on the network using WS-Discovery."""
273
+ def discover_devices(timeout: int = 4, prefer_https: bool = False) -> list:
274
+ """Discover ONVIF devices on the network using WS-Discovery.
275
+
276
+ Args:
277
+ timeout: Discovery timeout in seconds
278
+ prefer_https: If True, prioritize HTTPS XAddrs when available
279
+
280
+ Returns:
281
+ List of discovered devices with connection info
282
+ """
258
283
  import socket
259
284
  import uuid
260
285
  import xml.etree.ElementTree as ET
@@ -360,6 +385,7 @@ def discover_devices(timeout: int = 4) -> list:
360
385
  "xaddrs": [],
361
386
  "host": None,
362
387
  "port": 80,
388
+ "use_https": False, # Auto-detect HTTPS from XAddrs
363
389
  }
364
390
 
365
391
  epr = probe_match.find(".//wsa:EndpointReference/wsa:Address", namespaces)
@@ -384,17 +410,39 @@ def discover_devices(timeout: int = 4) -> list:
384
410
  if xaddrs_elem is not None and xaddrs_elem.text:
385
411
  device_info["xaddrs"] = xaddrs_elem.text.split()
386
412
 
387
- # Extract host and port from first XAddr
413
+ # Extract host, port, and protocol from XAddrs
414
+ # Use prefer_https parameter to decide which XAddr to use
388
415
  if device_info["xaddrs"]:
389
- xaddr = device_info["xaddrs"][0]
416
+ # Select XAddr based on prefer_https flag
417
+ if prefer_https:
418
+ # Try to find HTTPS XAddr first if prefer_https=True
419
+ https_xaddr = next(
420
+ (
421
+ x
422
+ for x in device_info["xaddrs"]
423
+ if x.startswith("https://")
424
+ ),
425
+ None,
426
+ )
427
+ xaddr = https_xaddr or device_info["xaddrs"][0]
428
+ else:
429
+ # Use first XAddr (usually HTTP) if prefer_https=False
430
+ xaddr = device_info["xaddrs"][0]
431
+
390
432
  if "://" in xaddr:
433
+ # Detect protocol
434
+ protocol = xaddr.split("://")[0]
435
+ device_info["use_https"] = protocol == "https"
436
+
437
+ # Extract host and port
391
438
  parts = xaddr.split("://")[1].split("/")[0]
392
439
  if ":" in parts:
393
440
  device_info["host"] = parts.split(":")[0]
394
441
  device_info["port"] = int(parts.split(":")[1])
395
442
  else:
396
443
  device_info["host"] = parts
397
- device_info["port"] = 80
444
+ # Set default port based on protocol
445
+ device_info["port"] = 443 if protocol == "https" else 80
398
446
 
399
447
  if device_info["host"]:
400
448
  devices.append(device_info)
@@ -405,8 +453,12 @@ def discover_devices(timeout: int = 4) -> list:
405
453
  return devices
406
454
 
407
455
 
408
- def select_device_interactive(devices: list) -> Optional[Tuple[str, int]]:
409
- """Display devices and allow user to select one interactively."""
456
+ def select_device_interactive(devices: list) -> Optional[Tuple[str, int, bool]]:
457
+ """Display devices and allow user to select one interactively.
458
+
459
+ Returns:
460
+ Tuple of (host, port, use_https) or None if cancelled
461
+ """
410
462
  if not devices:
411
463
  print(f"\n{colorize('No ONVIF devices found.', 'red')}")
412
464
  return None
@@ -415,8 +467,14 @@ def select_device_interactive(devices: list) -> Optional[Tuple[str, int]]:
415
467
 
416
468
  for idx, device in enumerate(devices, 1):
417
469
  idx_str = colorize(f"[{idx}]", "yellow")
470
+ protocol = "https" if device.get("use_https", False) else "http"
418
471
  host_port = f"{device['host']}:{device['port']}"
419
- print(f"\n{idx_str} {colorize(host_port, 'yellow')}")
472
+ protocol_indicator = (
473
+ colorize("🔒 HTTPS", "green")
474
+ if device.get("use_https", False)
475
+ else colorize("HTTP", "white")
476
+ )
477
+ print(f"\n{idx_str} {colorize(host_port, 'yellow')} ({protocol_indicator})")
420
478
  print(f" [id] {device['epr']}")
421
479
 
422
480
  if device["xaddrs"]:
@@ -454,11 +512,16 @@ def select_device_interactive(devices: list) -> Optional[Tuple[str, int]]:
454
512
  idx = int(selection)
455
513
  if 1 <= idx <= len(devices):
456
514
  selected = devices[idx - 1]
515
+ protocol = "https" if selected.get("use_https", False) else "http"
457
516
  host_port = f"{selected['host']}:{selected['port']}"
458
517
  print(
459
- f"\n{colorize('Selected:', 'green')} {colorize(host_port, 'yellow')}"
518
+ f"\n{colorize('Selected:', 'green')} {colorize(protocol, 'cyan')}://{colorize(host_port, 'yellow')}"
519
+ )
520
+ return (
521
+ selected["host"],
522
+ selected["port"],
523
+ selected.get("use_https", False),
460
524
  )
461
- return (selected["host"], selected["port"])
462
525
  else:
463
526
  print(f"{colorize('Invalid selection. Please try again.', 'red')}")
464
527
 
@@ -468,5 +531,15 @@ def select_device_interactive(devices: list) -> Optional[Tuple[str, int]]:
468
531
  return None
469
532
 
470
533
 
534
+ def setup_warning_format():
535
+ """Setup custom warning format to show clean, concise warnings"""
536
+
537
+ def custom_warning_format(message, category, filename, lineno, line=None):
538
+ # Show only the warning message without file path and line number
539
+ return f"{category.__name__}: {message}\n"
540
+
541
+ warnings.formatwarning = custom_warning_format
542
+
543
+
471
544
  if __name__ == "__main__":
472
545
  main()