onvif-python 0.3.1__py3-none-any.whl → 0.4.0__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 (67) hide show
  1. onvif/__init__.py +6 -2
  2. onvif/cli/helpers/discovery.py +38 -6
  3. onvif/cli/helpers/files.py +17 -15
  4. onvif/cli/helpers/messages.py +3 -4
  5. onvif/cli/helpers/products.py +2 -0
  6. onvif/cli/helpers/wsdl_parser.py +9 -4
  7. onvif/cli/interactive.py +7 -13
  8. onvif/cli/main.py +10 -4
  9. onvif/cli/utils.py +2 -0
  10. onvif/client.py +35 -25
  11. onvif/meta.py +2 -2
  12. onvif/operator.py +73 -41
  13. onvif/services/accesscontrol.py +2 -1
  14. onvif/services/accessrules.py +2 -1
  15. onvif/services/actionengine.py +2 -1
  16. onvif/services/analytics/__init__.py +1 -0
  17. onvif/services/analytics/analytics.py +2 -1
  18. onvif/services/analytics/ruleengine.py +2 -1
  19. onvif/services/analyticsdevice.py +2 -1
  20. onvif/services/appmgmt.py +2 -1
  21. onvif/services/authenticationbehavior.py +3 -2
  22. onvif/services/credential.py +2 -1
  23. onvif/services/deviceio.py +3 -1
  24. onvif/services/devicemgmt.py +5 -1
  25. onvif/services/display.py +2 -1
  26. onvif/services/doorcontrol.py +2 -1
  27. onvif/services/events/__init__.py +1 -0
  28. onvif/services/events/events.py +2 -1
  29. onvif/services/events/notification.py +5 -19
  30. onvif/services/events/pausable_subscription.py +9 -5
  31. onvif/services/events/pullpoint.py +2 -1
  32. onvif/services/events/subscription.py +6 -5
  33. onvif/services/imaging.py +6 -3
  34. onvif/services/media.py +2 -1
  35. onvif/services/media2.py +3 -1
  36. onvif/services/provisioning.py +3 -2
  37. onvif/services/ptz.py +2 -1
  38. onvif/services/receiver.py +2 -1
  39. onvif/services/recording.py +2 -1
  40. onvif/services/replay.py +2 -1
  41. onvif/services/schedule.py +2 -1
  42. onvif/services/search.py +9 -1
  43. onvif/services/security/__init__.py +1 -0
  44. onvif/services/security/advancedsecurity.py +3 -2
  45. onvif/services/security/authorizationserver.py +3 -2
  46. onvif/services/security/dot1x.py +3 -2
  47. onvif/services/security/jwt.py +3 -2
  48. onvif/services/security/keystore.py +3 -2
  49. onvif/services/security/mediasigning.py +3 -2
  50. onvif/services/security/tlsserver.py +3 -2
  51. onvif/services/thermal.py +2 -1
  52. onvif/services/uplink.py +2 -1
  53. onvif/utils/__init__.py +9 -4
  54. onvif/utils/discovery.py +132 -18
  55. onvif/utils/error_handlers.py +206 -99
  56. onvif/utils/exceptions.py +56 -41
  57. onvif/utils/plugins.py +407 -79
  58. onvif/utils/service.py +103 -84
  59. onvif/utils/wsdl.py +61 -32
  60. onvif/utils/zeep.py +262 -274
  61. {onvif_python-0.3.1.dist-info → onvif_python-0.4.0.dist-info}/METADATA +18 -8
  62. {onvif_python-0.3.1.dist-info → onvif_python-0.4.0.dist-info}/RECORD +66 -67
  63. onvif/utils/xml_capture.py +0 -219
  64. {onvif_python-0.3.1.dist-info → onvif_python-0.4.0.dist-info}/WHEEL +0 -0
  65. {onvif_python-0.3.1.dist-info → onvif_python-0.4.0.dist-info}/entry_points.txt +0 -0
  66. {onvif_python-0.3.1.dist-info → onvif_python-0.4.0.dist-info}/licenses/LICENSE.md +0 -0
  67. {onvif_python-0.3.1.dist-info → onvif_python-0.4.0.dist-info}/top_level.txt +0 -0
onvif/__init__.py CHANGED
@@ -6,10 +6,12 @@ from onvif.operator import CacheMode
6
6
  from onvif.utils import (
7
7
  ONVIFWSDL,
8
8
  ONVIFDiscovery,
9
- ONVIFErrorHandler,
10
9
  ONVIFOperationException,
11
10
  ONVIFParser,
12
11
  ZeepPatcher,
12
+ ignore_unsupported,
13
+ is_action_not_supported,
14
+ safe_call,
13
15
  )
14
16
 
15
17
  __all__ = [
@@ -17,9 +19,11 @@ __all__ = [
17
19
  "CacheMode",
18
20
  "ONVIFWSDL",
19
21
  "ONVIFOperationException",
20
- "ONVIFErrorHandler",
21
22
  "ZeepPatcher",
22
23
  "ONVIFCLI",
23
24
  "ONVIFDiscovery",
24
25
  "ONVIFParser",
26
+ "ignore_unsupported",
27
+ "is_action_not_supported",
28
+ "safe_call",
25
29
  ]
@@ -1,5 +1,7 @@
1
1
  """ONVIF discovery helpers."""
2
2
 
3
+ from datetime import datetime
4
+
3
5
  from onvif.cli.utils import colorize
4
6
  from onvif.utils import ONVIFDiscovery
5
7
 
@@ -16,7 +18,7 @@ def discover_devices(
16
18
  timeout (int): Discovery timeout in seconds
17
19
  interface (str | None): Network interface to use for discovery
18
20
  prefer_https (bool): If True, prioritize HTTPS XAddrs when available
19
- filter_term (str | None): Optional search term to filter devices by types or scopes
21
+ filter_term (str | None): Optional search term to filter devices by types or scopes (case-insensitive)
20
22
 
21
23
  Returns:
22
24
  List of discovered devices with connection info
@@ -36,6 +38,7 @@ def discover_devices(
36
38
  return devices
37
39
 
38
40
 
41
+ # pylint: disable=too-many-locals,too-many-statements,too-many-branches
39
42
  def select_device_interactive(devices: list) -> tuple[str, int, bool] | None:
40
43
  """Display devices and allow user to select one interactively.
41
44
 
@@ -70,15 +73,43 @@ def select_device_interactive(devices: list) -> tuple[str, int, bool] | None:
70
73
  epr_display = epr_display.replace("uuid:", "")
71
74
  print(f" [uuid] {epr_display}")
72
75
 
73
- if device["xaddrs"]:
76
+ if device.get("hostname"):
77
+ print(f" [hostname] {device["hostname"]}")
78
+
79
+ if device.get("xaddrs"):
74
80
  xaddrs_parts = [f"[{xaddr}]" for xaddr in device["xaddrs"]]
75
81
  print(f" [xaddrs] {' '.join(xaddrs_parts)}")
76
82
 
77
- if device["types"]:
83
+ if device.get("date_time"):
84
+ utc = (
85
+ datetime.fromisoformat(device["date_time"].get("utc")).strftime(
86
+ "%H:%M:%S %d-%m-%Y"
87
+ )
88
+ if device["date_time"].get("utc")
89
+ else ""
90
+ )
91
+ local = (
92
+ datetime.fromisoformat(device["date_time"].get("local")).strftime(
93
+ "%H:%M:%S %d-%m-%Y"
94
+ )
95
+ if device["date_time"].get("local")
96
+ else ""
97
+ )
98
+ print(
99
+ f" [date_time]"
100
+ f'{" [UTC: " + utc + "]" if utc else ""}'
101
+ f'{" [Local: " + local + "]" if local else ""}'
102
+ )
103
+
104
+ if device.get("types"):
78
105
  types_parts = [f"[{t}]" for t in device["types"]]
79
106
  print(f" [types] {' '.join(types_parts)}")
80
107
 
81
- if device["scopes"]:
108
+ if device.get("services"):
109
+ service = [colorize(f"[{t}]", "cyan") for t in device["services"]]
110
+ print(f" [services] {' '.join(service)}")
111
+
112
+ if device.get("scopes"):
82
113
  scope_parts = []
83
114
  for scope in device["scopes"]:
84
115
  # Remove the prefix "onvif://www.onvif.org/" if present
@@ -110,13 +141,14 @@ def select_device_interactive(devices: list) -> tuple[str, int, bool] | None:
110
141
  print(
111
142
  f"\n{colorize('Selected:', 'green')} {colorize(protocol, 'cyan')}://{colorize(host_port, 'yellow')}"
112
143
  )
144
+
113
145
  return (
114
146
  selected["host"],
115
147
  selected["port"],
116
148
  selected.get("use_https", False),
117
149
  )
118
- else:
119
- print(colorize("Invalid selection. Please try again.", "red"))
150
+
151
+ print(colorize("Invalid selection. Please try again.", "red"))
120
152
 
121
153
  except ValueError:
122
154
  print(colorize("Invalid input. Please enter a number.", "red"))
@@ -102,6 +102,7 @@ def save_output_to_file(
102
102
  print(str(result))
103
103
 
104
104
 
105
+ # pylint: disable=too-many-return-statements,too-many-branches,too-many-nested-blocks
105
106
  def _serialize_for_json(obj: Any) -> Any:
106
107
  """Recursively serialize ONVIF objects for JSON output.
107
108
 
@@ -113,17 +114,17 @@ def _serialize_for_json(obj: Any) -> Any:
113
114
  """
114
115
  if obj is None:
115
116
  return None
116
- elif isinstance(obj, (str, int, float, bool)):
117
+ if isinstance(obj, (str, int, float, bool)):
117
118
  return obj
118
- elif isinstance(obj, datetime):
119
+ if isinstance(obj, datetime):
119
120
  return obj.isoformat()
120
- elif isinstance(obj, (list, tuple)):
121
+ if isinstance(obj, (list, tuple)):
121
122
  return [_serialize_for_json(item) for item in obj]
122
- elif isinstance(obj, dict):
123
+ if isinstance(obj, dict):
123
124
  return {key: _serialize_for_json(value) for key, value in obj.items()}
124
125
 
125
126
  # Check if this is a Zeep object (has _xsd_type attribute)
126
- elif hasattr(obj, "_xsd_type"):
127
+ if hasattr(obj, "_xsd_type"):
127
128
  result = {}
128
129
  # Try to get all elements from XSD type
129
130
  if hasattr(obj._xsd_type, "elements"): # pylint: disable=protected-access
@@ -154,7 +155,7 @@ def _serialize_for_json(obj: Any) -> Any:
154
155
 
155
156
  return result
156
157
 
157
- elif hasattr(obj, "__dict__"):
158
+ if hasattr(obj, "__dict__"):
158
159
  # Handle regular objects with attributes
159
160
  result = {}
160
161
  for key, value in obj.__dict__.items():
@@ -176,14 +177,15 @@ def _serialize_for_json(obj: Any) -> Any:
176
177
  pass
177
178
 
178
179
  return result
179
- elif hasattr(obj, "_value_1"):
180
+
181
+ if hasattr(obj, "_value_1"):
180
182
  # Handle zeep objects with special structure
181
183
  return _serialize_for_json(obj._value_1) # pylint: disable=protected-access
182
- else:
183
- # Try to convert to dict using vars() if available
184
- try:
185
- obj_dict = vars(obj)
186
- return _serialize_for_json(obj_dict)
187
- except TypeError:
188
- # Fallback to string representation
189
- return str(obj)
184
+
185
+ # Try to convert to dict using vars() if available
186
+ try:
187
+ obj_dict = vars(obj)
188
+ return _serialize_for_json(obj_dict)
189
+ except TypeError:
190
+ # Fallback to string representation
191
+ return str(obj)
@@ -69,7 +69,8 @@ def print_interactive_intro(args, device_info_text) -> str:
69
69
  f"{banner}\n"
70
70
  f"{repo_info}\n"
71
71
  f"{terminal_header}\n"
72
- f" Connected to : {colorize(f'{args.host}:{args.port}', 'yellow')}"
72
+ f" Connected to : {colorize(f'{args.host}:{args.port}', 'yellow')}\n"
73
+ f" Auth Method : {colorize(f'{'HTTP Digest' if args.digest else 'WS-UsernameToken'}', 'yellow')}"
73
74
  f"{options_display}{device_info_text}\n\n"
74
75
  f"{colorize('[Quick Start]', 'green')}\n"
75
76
  f" - Type {colorize('dev', 'yellow')} + {colorize('TAB', 'yellow')} to see `devicemgmt` suggestion\n"
@@ -105,7 +106,6 @@ INTERACTIVE_HELP = f"""
105
106
  cd <service> - Enter service mode (alias)
106
107
  ls - List commands/services/methods in grid format
107
108
  up - Exit current service mode (go up one level)
108
- pwd - Show current service context
109
109
  clear - Clear terminal screen
110
110
  help <command> - Show help for a specific command
111
111
 
@@ -165,7 +165,6 @@ INTERACTIVE_SHORTCUTS = f"""
165
165
  cd <service> - Enter service (same as '<service>')
166
166
  ls - List commands/services in grid format (like TAB)
167
167
  up - Go up one level
168
- pwd - Show current context
169
168
  clear - Clear terminal screen
170
169
  help <command> - Show help for a command
171
170
 
@@ -193,7 +192,7 @@ Examples:
193
192
  # Discover ONVIF devices on network
194
193
  {colorize('onvif', 'yellow')} --discover --username admin --password admin123 --interactive
195
194
  {colorize('onvif', 'yellow')} media GetProfiles --discover --username admin
196
- {colorize('onvif', 'yellow')} -d -i
195
+ {colorize('onvif', 'yellow')} -d --interface 192.168.1.77 -i
197
196
 
198
197
  # Discover with filtering
199
198
  {colorize('onvif', 'yellow')} --discover --filter ptz --interactive
@@ -10,6 +10,8 @@ from pathlib import Path
10
10
  from onvif.cli.utils import colorize
11
11
 
12
12
 
13
+ # pylint: disable=too-many-locals,too-many-nested-blocks
14
+ # pylint: disable=too-many-statements,too-many-branches
13
15
  def search_products(search_term: str, page: int = 1, per_page: int = 20) -> None:
14
16
  """Search ONVIF products database and display results in table format with
15
17
  pagination.
@@ -36,6 +36,7 @@ class OperationTypeInfo(TypedDict):
36
36
  output: MessageInfo | None
37
37
 
38
38
 
39
+ # pylint: disable=too-many-locals,too-many-statements,too-many-branches
39
40
  def get_method_documentation(service_obj, method_name: str) -> dict[str, Any] | None:
40
41
  """Extracts documentation from WSDL and parameters from the Python method signature.
41
42
 
@@ -45,7 +46,7 @@ def get_method_documentation(service_obj, method_name: str) -> dict[str, Any] |
45
46
  required_args: list[str] = []
46
47
  optional_args: list[str] = []
47
48
 
48
- try:
49
+ try: # pylint: disable=too-many-nested-blocks
49
50
  # 1. Get documentation from WSDL (existing logic)
50
51
  wsdl_path = service_obj.operator.wsdl_path
51
52
 
@@ -308,8 +309,7 @@ def clean_documentation_html(doc_text: str) -> str:
308
309
  text = match.group(2).strip() # Strip whitespace from link text
309
310
  if text:
310
311
  return f"{text} ({url})"
311
- else:
312
- return url # If no text, just return URL
312
+ return url # If no text, just return URL
313
313
 
314
314
  # Replace anchor tags - handle newlines and whitespace
315
315
  doc_text = re.sub(
@@ -361,7 +361,9 @@ def extract_documentation_text(doc_elem) -> str:
361
361
  def _load_imported_schemas(root, schema_context: dict, namespaces: dict):
362
362
  """Recursively load all imported and included schemas into the schema context."""
363
363
  # Find all xs:import and xs:include in xs:schema elements
364
- for schema in root.findall(".//xs:schema", namespaces):
364
+ for schema in root.findall( # pylint: disable=too-many-nested-blocks
365
+ ".//xs:schema", namespaces
366
+ ):
365
367
  # Process imports
366
368
  for import_elem in schema.findall("xs:import", namespaces):
367
369
  schema_location = import_elem.get("schemaLocation")
@@ -443,6 +445,7 @@ def _load_imported_schemas(root, schema_context: dict, namespaces: dict):
443
445
  continue
444
446
 
445
447
 
448
+ # pylint: disable=too-many-locals,too-many-statements,too-many-branches
446
449
  def resolve_element_type(
447
450
  element_name: str,
448
451
  namespaces: dict,
@@ -605,6 +608,7 @@ def resolve_element_type(
605
608
  return parameters
606
609
 
607
610
 
611
+ # pylint: disable=too-many-locals,too-many-statements,too-many-branches
608
612
  def resolve_complex_type(
609
613
  type_name: str,
610
614
  namespaces: dict,
@@ -794,6 +798,7 @@ def resolve_complex_type(
794
798
  return children
795
799
 
796
800
 
801
+ # pylint: disable=too-many-locals,too-many-statements,too-many-branches
797
802
  def parse_inline_complex_type(
798
803
  complex_type,
799
804
  namespaces: dict,
onvif/cli/interactive.py CHANGED
@@ -1,3 +1,5 @@
1
+ # pylint: disable=too-many-lines,too-many-instance-attributes,too-many-locals
2
+ # pylint: disable=too-many-statements,too-many-public-methods,too-many-branches
1
3
  """ONVIF CLI interactive shell implementation."""
2
4
 
3
5
  import cmd
@@ -70,7 +72,6 @@ class InteractiveShell(cmd.Cmd):
70
72
  "debug",
71
73
  "ls",
72
74
  "cd",
73
- "pwd",
74
75
  "shortcuts",
75
76
  "desc",
76
77
  "type",
@@ -274,6 +275,7 @@ class InteractiveShell(cmd.Cmd):
274
275
  depth = max(0, depth - 1)
275
276
 
276
277
  # Detect top-level &&
278
+ # pylint: disable=too-many-boolean-expressions
277
279
  if (
278
280
  not in_single
279
281
  and not in_double
@@ -522,6 +524,8 @@ class InteractiveShell(cmd.Cmd):
522
524
  print(f"{colorize('Unknown command:', 'red')} {line}")
523
525
  print(f"Type {colorize('help', 'white')} for available commands")
524
526
 
527
+ return None
528
+
525
529
  def get_suggestions(self, partial_cmd: str) -> list[str]:
526
530
  """Get command suggestions based on partial input."""
527
531
  suggestions = []
@@ -1026,7 +1030,7 @@ class InteractiveShell(cmd.Cmd):
1026
1030
  available_services = get_device_available_services(self.client)
1027
1031
  colored_services = [colorize(svc, "cyan") for svc in available_services]
1028
1032
  print(f"Available services: {', '.join(colored_services)}")
1029
- return
1033
+ return None
1030
1034
  return self.do_enter_service(line)
1031
1035
 
1032
1036
  def complete_cd(
@@ -1036,17 +1040,6 @@ class InteractiveShell(cmd.Cmd):
1036
1040
  services = get_device_available_services(self.client)
1037
1041
  return [s for s in services if s.lower().startswith(text.lower())]
1038
1042
 
1039
- def do_pwd(self, line): # pylint: disable=unused-argument
1040
- """Show current service context."""
1041
- if self.current_service_name:
1042
- print(
1043
- f"{colorize('Current service:', 'yellow')} {colorize(self.current_service_name, 'cyan')}"
1044
- )
1045
- else:
1046
- print(
1047
- f"{colorize('Current context:', 'yellow')} {colorize('root', 'blue')}"
1048
- )
1049
-
1050
1043
  def do_shortcuts(self, line): # pylint: disable=unused-argument
1051
1044
  """Show available shortcuts."""
1052
1045
  print(INTERACTIVE_SHORTCUTS)
@@ -1221,6 +1214,7 @@ class InteractiveShell(cmd.Cmd):
1221
1214
  print(
1222
1215
  f"\n{colorize('[ONVIF Terminal Client]', 'yellow')}"
1223
1216
  f"\n Connected to : {colorize(f'{self.args.host}:{self.args.port}', 'yellow')}"
1217
+ f"\n Auth Method : {colorize(f'{'HTTP Digest' if self.args.digest else 'WS-UsernameToken'}', 'yellow')}"
1224
1218
  f"{options_display}{self.device_info_text}"
1225
1219
  )
1226
1220
  print() # Extra newline for spacing
onvif/cli/main.py CHANGED
@@ -24,7 +24,6 @@ from onvif.operator import CacheMode
24
24
  from onvif.utils import ONVIFOperationException
25
25
 
26
26
 
27
- # pylint: disable=line-too-long
28
27
  def create_parser() -> ArgumentParser:
29
28
  """Create argument parser for ONVIF CLI."""
30
29
  parser = ArgumentParser(
@@ -97,6 +96,11 @@ def create_parser() -> ArgumentParser:
97
96
  default=10,
98
97
  help="ONVIF connection timeout in seconds (default: 10)",
99
98
  )
99
+ parser.add_argument(
100
+ "--digest",
101
+ action="store_true",
102
+ help="Use HTTP Digest instead of WS-UsernameToken",
103
+ )
100
104
  parser.add_argument(
101
105
  "--https", action="store_true", help="Use HTTPS instead of HTTP"
102
106
  )
@@ -118,9 +122,9 @@ def create_parser() -> ArgumentParser:
118
122
  parser.add_argument(
119
123
  "--cache",
120
124
  choices=[mode.value for mode in CacheMode],
121
- default=CacheMode.ALL.value,
122
- help="Caching mode for ONVIFClient (default: all). "
123
- "'all': memory+disk, 'db': disk-only, 'mem': memory-only, 'none': disabled.",
125
+ default=CacheMode.DB.value,
126
+ help="Caching mode for ONVIFClient (default: db). "
127
+ "'db': disk-only, 'mem': memory-only, 'none': disabled.",
124
128
  )
125
129
  parser.add_argument(
126
130
  "--health-check-interval",
@@ -159,6 +163,7 @@ def create_parser() -> ArgumentParser:
159
163
  return parser
160
164
 
161
165
 
166
+ # pylint: disable=too-many-statements,too-many-branches
162
167
  def main() -> None:
163
168
  """Main CLI entry point."""
164
169
  # Setup custom warning format for cleaner output
@@ -278,6 +283,7 @@ def main() -> None:
278
283
  port=args.port,
279
284
  username=args.username,
280
285
  password=args.password,
286
+ http_digest=args.digest,
281
287
  timeout=args.timeout,
282
288
  cache=CacheMode(args.cache),
283
289
  use_https=args.https,
onvif/cli/utils.py CHANGED
@@ -80,6 +80,7 @@ def _is_valid_json(s: str) -> bool:
80
80
  return True
81
81
 
82
82
 
83
+ # pylint: disable=too-many-statements,too-many-branches
83
84
  def parse_json_params(params_str: str) -> dict[str, Any]:
84
85
  """Parse parameters from a JSON string or key=value pairs into a dict.
85
86
 
@@ -406,6 +407,7 @@ def format_services_list(services_list) -> str:
406
407
  return result
407
408
 
408
409
 
410
+ # pylint: disable=too-many-statements,too-many-branches
409
411
  def get_device_available_services(client) -> list:
410
412
  """Get list of services actually available on the connected device.
411
413
 
onvif/client.py CHANGED
@@ -9,6 +9,7 @@ from typing import ParamSpec, TypeVar
9
9
  from urllib.parse import urlparse, urlunparse
10
10
 
11
11
  from lxml import etree
12
+ from zeep import Plugin
12
13
 
13
14
  from onvif.operator import CacheMode
14
15
  from onvif.services import (
@@ -50,13 +51,10 @@ from onvif.services import (
50
51
  TLSServer,
51
52
  Uplink,
52
53
  )
53
- from onvif.utils import (
54
- ONVIFWSDL,
55
- ONVIFOperationException,
56
- XMLCapturePlugin,
57
- ZeepPatcher,
58
- )
59
- from onvif.utils.plugins import ReferenceParametersPlugin
54
+ from onvif.utils.exceptions import ONVIFOperationException
55
+ from onvif.utils.plugins import ReferenceParametersPlugin, XMLCapturePlugin
56
+ from onvif.utils.wsdl import ONVIFWSDL
57
+ from onvif.utils.zeep import ZeepPatcher
60
58
 
61
59
  logger = logging.getLogger(__name__)
62
60
  logger.addHandler(logging.NullHandler())
@@ -66,7 +64,8 @@ R = TypeVar("R")
66
64
 
67
65
 
68
66
  def service(func: Callable[P, R]) -> Callable[P, R]:
69
- """Decorator to wrap service accessor methods with `ONVIFOperationException` handling.
67
+ """Decorator to wrap service accessor methods with `ONVIFOperationException`
68
+ handling.
70
69
 
71
70
  This decorator catches any exception raised during service initialization and
72
71
  wraps it in `ONVIFOperationException` for consistent error handling across all
@@ -104,21 +103,25 @@ def service(func: Callable[P, R]) -> Callable[P, R]:
104
103
  return wrapper
105
104
 
106
105
 
107
- # pylint: disable=too-many-instance-attributes,too-many-locals,too-many-public-methods,too-many-statements
106
+ # pylint: disable=too-many-instance-attributes,too-many-locals
107
+ # pylint: disable=too-many-public-methods,too-many-statements
108
108
  class ONVIFClient:
109
- """ONVIF Client for communicating with ONVIF-compliant devices.
109
+ """High-level ONVIF client for interacting with ONVIF-compliant devices.
110
110
 
111
111
  This is the main class for interacting with ONVIF devices. It provides access to
112
112
  all ONVIF services including Device Management, Media, PTZ, Events, Analytics, and more.
113
113
 
114
- The client automatically discovers available services on the device using GetServices
115
- or GetCapabilities, and provides lazy initialization for service endpoints.
114
+ The client automatically discovers available services on the device using `GetServices`
115
+ or `GetCapabilities`, and provides lazy initialization for service endpoints.
116
116
 
117
117
  Attributes:
118
- services (list): List of available services from GetServices response
119
- capabilities (CompoundValue): Device capabilities from GetCapabilities response (fallback)
120
- xml_plugin (XMLCapturePlugin): XML capture plugin for debugging (if capture_xml=True)
118
+ services (list): List of available services from `GetServices` response
119
+ capabilities (CompoundValue): Device capabilities from `GetCapabilities` response (fallback)
120
+ xml_plugin (XMLCapturePlugin): XML capture plugin for debugging (if `capture_xml=True`)
121
121
  wsdl_dir (str | None): Custom WSDL directory path (if provided)
122
+
123
+ !!! tip "Version History"
124
+ - Available since [`>=v0.0.1`](/onvif-python/releases/#v0.0.1) (first release).
122
125
  """
123
126
 
124
127
  def __init__(
@@ -127,15 +130,15 @@ class ONVIFClient:
127
130
  port: int,
128
131
  username: str | None = None,
129
132
  password: str | None = None,
130
- http_digest: bool = False, # will use WS-Usernametoken by default (recommended! trust me)
133
+ http_digest: bool = False, # will use WS-UsernameToken by default
131
134
  timeout: int = 10,
132
- cache: CacheMode = CacheMode.ALL,
135
+ cache: CacheMode = CacheMode.DB,
133
136
  use_https: bool = False,
134
137
  verify_ssl: bool = False,
135
138
  apply_patch: bool = True,
136
139
  capture_xml: bool = False,
137
140
  wsdl_dir: str | None = None,
138
- plugins: list | None = None,
141
+ plugins: list[Plugin] | None = None,
139
142
  ):
140
143
  """Initialize the ONVIF client.
141
144
 
@@ -144,7 +147,7 @@ class ONVIFClient:
144
147
  port (int): Device port number
145
148
  username (str | None): ONVIF username
146
149
  password (str | None): ONVIF password
147
- http_digest (bool): Whether to use **HTTP Digest** or **WS-Usernametoken** for auth
150
+ http_digest (bool): Whether to use **HTTP Digest** or **WS-UsernameToken** for auth
148
151
  timeout (int): Request timeout in seconds
149
152
  cache (CacheMode): WSDL caching strategy
150
153
  use_https (bool): Use HTTPS instead of HTTP for secure communication
@@ -152,9 +155,16 @@ class ONVIFClient:
152
155
  apply_patch (bool): Whether to apply ``xsd:any`` flattening patch
153
156
  capture_xml (bool): Whether to use XML capture plugin for debugging SOAP requests/responses
154
157
  wsdl_dir (str | None): Custom WSDL directory path for using external WSDL files instead of built-in ones
155
- plugins (list | None): List of enabled Zeep plugins
158
+ plugins (list[Plugin] | None): List of user-provided Zeep plugins (zeep.plugins)
159
+
160
+ !!! tip "Version History"
161
+ - Added in [`>=v0.0.4`](/onvif-python/releases/#v0.0.4): `apply_patch`
162
+ - Added in [`>=v0.0.6`](/onvif-python/releases/#v0.0.6): `capture_xml`
163
+ - Added in [`>=v0.1.0`](/onvif-python/releases/#v0.1.0): `wsdl_dir`
164
+ - Added in [`>=v0.2.2`](/onvif-python/releases/#v0.2.2): `plugins`
165
+ - Added in [`>=v0.3.0`](/onvif-python/releases/#v0.3.0): `http_digest`
166
+ - Changed in [`>=v0.3.0`](/onvif-python/releases/#v0.3.0): `username` → `str | None`, `password` → `str | None`
156
167
  """
157
-
158
168
  logger.info("Initializing ONVIF client for %s:%d", host, port)
159
169
  logger.debug(
160
170
  "Connection settings: HTTPS=%s, SSL_verify=%s, cache=%s, timeout=%ds",
@@ -219,14 +229,14 @@ class ONVIFClient:
219
229
  if namespace and xaddr:
220
230
  self._service_map[namespace] = xaddr
221
231
  logger.debug("Mapped service: %s -> %s", namespace, xaddr)
222
- except Exception as e: # pylint: disable=broad-except
232
+ except (ValueError, AttributeError, ONVIFOperationException) as e:
223
233
  logger.warning("GetServices failed: %s", e)
224
234
  # Fallback to GetCapabilities if GetServices is not supported on device
225
235
  try:
226
236
  logger.debug("Falling back to GetCapabilities")
227
237
  self.capabilities = self._devicemgmt.GetCapabilities(Category="All")
228
238
  logger.info("Successfully retrieved device capabilities")
229
- except Exception as e2: # pylint: disable=broad-except
239
+ except ONVIFOperationException as e2:
230
240
  # If both fail, we'll use default URLs
231
241
  logger.error("Both GetServices and GetCapabilities failed: %s", e2)
232
242
  logger.warning("Using default URLs for services")
@@ -293,9 +303,9 @@ class ONVIFClient:
293
303
  self._authorizationserver: AuthorizationServer | None = None
294
304
  self._mediasigning: MediaSigning | None = None
295
305
 
296
- def _get_xaddr(
306
+ def _get_xaddr( # pylint: disable=too-many-branches
297
307
  self, service_name: str, service_path: str
298
- ) -> str: # pylint: disable=too-many-branches
308
+ ) -> str:
299
309
  """Resolve XAddr for ONVIF services using a comprehensive 3-tier discovery
300
310
  approach.
301
311
 
onvif/meta.py CHANGED
@@ -1,6 +1,6 @@
1
- """onvif-python: A Python library for interacting with ONVIF-compliant devices."""
1
+ """ONVIF Python: A Python library for interacting with ONVIF-compliant devices."""
2
2
 
3
3
  from typing import Final
4
4
 
5
- __version__: Final[str] = "0.3.1"
5
+ __version__: Final[str] = "0.4.0"
6
6
  __repository__: Final[str] = "https://github.com/nirsimetri/onvif-python"