onvif-python 0.2.1__py3-none-any.whl → 0.2.2__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/__init__.py +2 -0
- onvif/cli/interactive.py +2 -2
- onvif/cli/main.py +2 -2
- onvif/cli/utils.py +95 -1
- onvif/client.py +21 -5
- onvif/services/security/advancedsecurity.py +1 -1
- onvif/utils/__init__.py +2 -0
- onvif/utils/parser.py +136 -0
- onvif/utils/xml_capture.py +0 -9
- onvif/utils/zeep.py +38 -3
- {onvif_python-0.2.1.dist-info → onvif_python-0.2.2.dist-info}/METADATA +6 -6
- {onvif_python-0.2.1.dist-info → onvif_python-0.2.2.dist-info}/RECORD +16 -15
- {onvif_python-0.2.1.dist-info → onvif_python-0.2.2.dist-info}/WHEEL +0 -0
- {onvif_python-0.2.1.dist-info → onvif_python-0.2.2.dist-info}/entry_points.txt +0 -0
- {onvif_python-0.2.1.dist-info → onvif_python-0.2.2.dist-info}/licenses/LICENSE.md +0 -0
- {onvif_python-0.2.1.dist-info → onvif_python-0.2.2.dist-info}/top_level.txt +0 -0
onvif/__init__.py
CHANGED
onvif/cli/interactive.py
CHANGED
|
@@ -194,7 +194,7 @@ class InteractiveShell(cmd.Cmd):
|
|
|
194
194
|
" / __ \\/ | / / | / / _/ ____/",
|
|
195
195
|
" / / / / |/ /| | / // // /_ ",
|
|
196
196
|
"/ /_/ / /| / | |/ // // __/ ",
|
|
197
|
-
"\\____/_/ |_/ |___/___/_/ v0.2.
|
|
197
|
+
"\\____/_/ |_/ |___/___/_/ v0.2.2",
|
|
198
198
|
" ",
|
|
199
199
|
]
|
|
200
200
|
|
|
@@ -1400,7 +1400,7 @@ class InteractiveShell(cmd.Cmd):
|
|
|
1400
1400
|
super().do_help(line)
|
|
1401
1401
|
else:
|
|
1402
1402
|
help_text = f"""
|
|
1403
|
-
{colorize('ONVIF Interactive Shell — v0.2.
|
|
1403
|
+
{colorize('ONVIF Interactive Shell — v0.2.2', 'cyan')}\n{colorize('https://github.com/nirsimetri/onvif-python', 'white')}
|
|
1404
1404
|
|
|
1405
1405
|
{colorize('Basic Commands:', 'yellow')}
|
|
1406
1406
|
capabilities, caps - Show device capabilities
|
onvif/cli/main.py
CHANGED
|
@@ -21,7 +21,7 @@ def create_parser():
|
|
|
21
21
|
"""Create argument parser for ONVIF CLI"""
|
|
22
22
|
parser = argparse.ArgumentParser(
|
|
23
23
|
prog="onvif",
|
|
24
|
-
description=f"{colorize('ONVIF Terminal Client', 'yellow')} — v0.2.
|
|
24
|
+
description=f"{colorize('ONVIF Terminal Client', 'yellow')} — v0.2.2\nhttps://github.com/nirsimetri/onvif-python",
|
|
25
25
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
26
26
|
epilog=f"""
|
|
27
27
|
Examples:
|
|
@@ -461,7 +461,7 @@ def main():
|
|
|
461
461
|
|
|
462
462
|
# Show ONVIF CLI version
|
|
463
463
|
if args.version:
|
|
464
|
-
print(colorize("0.2.
|
|
464
|
+
print(colorize("0.2.2", "yellow"))
|
|
465
465
|
sys.exit(0)
|
|
466
466
|
|
|
467
467
|
# Handle product search
|
onvif/cli/utils.py
CHANGED
|
@@ -618,7 +618,7 @@ def get_device_available_services(client) -> list:
|
|
|
618
618
|
# Security namespace has multiple bindings
|
|
619
619
|
available_services.extend(
|
|
620
620
|
[
|
|
621
|
-
"
|
|
621
|
+
"security",
|
|
622
622
|
"jwt",
|
|
623
623
|
"keystore",
|
|
624
624
|
"tlsserver",
|
|
@@ -628,6 +628,100 @@ def get_device_available_services(client) -> list:
|
|
|
628
628
|
]
|
|
629
629
|
)
|
|
630
630
|
|
|
631
|
+
# Additional check: Try to call security.GetServiceCapabilities() to verify availability
|
|
632
|
+
# This is necessary because Security service might not be reported in GetServices/GetCapabilities
|
|
633
|
+
# Use caching to avoid calling GetServiceCapabilities repeatedly
|
|
634
|
+
if "security" not in available_services:
|
|
635
|
+
# Check if we've already tried to get security capabilities
|
|
636
|
+
if not hasattr(client, "_security_capabilities_checked"):
|
|
637
|
+
client._security_capabilities_checked = False
|
|
638
|
+
client._security_capabilities = None
|
|
639
|
+
|
|
640
|
+
if not client._security_capabilities_checked:
|
|
641
|
+
# First time check - call GetServiceCapabilities and cache result
|
|
642
|
+
try:
|
|
643
|
+
security_service = client.security()
|
|
644
|
+
# Try to call GetServiceCapabilities to verify the service is actually available
|
|
645
|
+
caps = security_service.GetServiceCapabilities()
|
|
646
|
+
|
|
647
|
+
# Cache the capabilities for future use
|
|
648
|
+
client._security_capabilities = caps
|
|
649
|
+
client._security_capabilities_checked = True
|
|
650
|
+
|
|
651
|
+
except Exception:
|
|
652
|
+
# Security service not available, mark as checked
|
|
653
|
+
client._security_capabilities_checked = True
|
|
654
|
+
client._security_capabilities = None
|
|
655
|
+
|
|
656
|
+
# Use cached capabilities
|
|
657
|
+
if client._security_capabilities is not None:
|
|
658
|
+
caps = client._security_capabilities
|
|
659
|
+
|
|
660
|
+
# If successful, add main security service
|
|
661
|
+
available_services.append("security")
|
|
662
|
+
|
|
663
|
+
# Check each sub-service capability to determine availability
|
|
664
|
+
# Only add sub-services if their corresponding capability is not None
|
|
665
|
+
if (
|
|
666
|
+
hasattr(caps, "KeystoreCapabilities")
|
|
667
|
+
and caps.KeystoreCapabilities is not None
|
|
668
|
+
):
|
|
669
|
+
available_services.append("keystore")
|
|
670
|
+
|
|
671
|
+
if (
|
|
672
|
+
hasattr(caps, "TLSServerCapabilities")
|
|
673
|
+
and caps.TLSServerCapabilities is not None
|
|
674
|
+
):
|
|
675
|
+
available_services.append("tlsserver")
|
|
676
|
+
|
|
677
|
+
if (
|
|
678
|
+
hasattr(caps, "Dot1XCapabilities")
|
|
679
|
+
and caps.Dot1XCapabilities is not None
|
|
680
|
+
):
|
|
681
|
+
available_services.append("dot1x")
|
|
682
|
+
|
|
683
|
+
if (
|
|
684
|
+
hasattr(caps, "AuthorizationServer")
|
|
685
|
+
and caps.AuthorizationServer is not None
|
|
686
|
+
):
|
|
687
|
+
available_services.append("authorizationserver")
|
|
688
|
+
|
|
689
|
+
if hasattr(caps, "MediaSigning") and caps.MediaSigning is not None:
|
|
690
|
+
available_services.append("mediasigning")
|
|
691
|
+
|
|
692
|
+
# Additional check for JWT service: Try to call jwt.GetJWTConfiguration()
|
|
693
|
+
# JWT doesn't have a capability in GetServiceCapabilities, so we need to test it directly
|
|
694
|
+
if "jwt" not in available_services:
|
|
695
|
+
# Check if we've already tried to get JWT availability
|
|
696
|
+
if not hasattr(client, "_jwt_checked"):
|
|
697
|
+
client._jwt_checked = False
|
|
698
|
+
client._jwt_available = None
|
|
699
|
+
|
|
700
|
+
if not client._jwt_checked:
|
|
701
|
+
# First time check - try to call GetJWTConfiguration and cache result
|
|
702
|
+
try:
|
|
703
|
+
# JWT service requires xaddr from security service
|
|
704
|
+
# Try to construct xaddr from security service endpoint
|
|
705
|
+
protocol = "https" if client.common_args["use_https"] else "http"
|
|
706
|
+
default_xaddr = f"{protocol}://{client.common_args['host']}:{client.common_args['port']}/onvif/AdvancedSecurity"
|
|
707
|
+
|
|
708
|
+
# Try to call GetJWTConfiguration to verify JWT is available
|
|
709
|
+
jwt_service = client.jwt(xaddr=default_xaddr)
|
|
710
|
+
jwt_service.GetJWTConfiguration()
|
|
711
|
+
|
|
712
|
+
# If successful, JWT is available
|
|
713
|
+
client._jwt_available = True
|
|
714
|
+
client._jwt_checked = True
|
|
715
|
+
|
|
716
|
+
except Exception:
|
|
717
|
+
# JWT service not available, mark as checked
|
|
718
|
+
client._jwt_checked = True
|
|
719
|
+
client._jwt_available = False
|
|
720
|
+
|
|
721
|
+
# Use cached JWT availability
|
|
722
|
+
if client._jwt_available:
|
|
723
|
+
available_services.append("jwt")
|
|
724
|
+
|
|
631
725
|
return sorted(list(set(available_services))) # Remove duplicates and sort
|
|
632
726
|
|
|
633
727
|
|
onvif/client.py
CHANGED
|
@@ -111,6 +111,7 @@ class ONVIFClient:
|
|
|
111
111
|
apply_patch: bool = True,
|
|
112
112
|
capture_xml: bool = False,
|
|
113
113
|
wsdl_dir: str = None,
|
|
114
|
+
plugins: list = None,
|
|
114
115
|
):
|
|
115
116
|
logger.info(f"Initializing ONVIF client for {host}:{port}")
|
|
116
117
|
logger.debug(
|
|
@@ -131,6 +132,15 @@ class ONVIFClient:
|
|
|
131
132
|
logger.debug("Enabling XML capture plugin")
|
|
132
133
|
self.xml_plugin = XMLCapturePlugin()
|
|
133
134
|
|
|
135
|
+
# Merge user plugins with xml_plugin
|
|
136
|
+
all_plugins = []
|
|
137
|
+
if plugins:
|
|
138
|
+
logger.debug(f"Adding {len(plugins)} user-provided plugins")
|
|
139
|
+
all_plugins.extend(plugins)
|
|
140
|
+
if self.xml_plugin:
|
|
141
|
+
logger.debug("Adding XML capture plugin")
|
|
142
|
+
all_plugins.append(self.xml_plugin)
|
|
143
|
+
|
|
134
144
|
# Store custom WSDL directory if provided
|
|
135
145
|
self.wsdl_dir = wsdl_dir
|
|
136
146
|
if wsdl_dir:
|
|
@@ -148,7 +158,7 @@ class ONVIFClient:
|
|
|
148
158
|
"use_https": use_https,
|
|
149
159
|
"verify_ssl": verify_ssl,
|
|
150
160
|
"apply_patch": apply_patch,
|
|
151
|
-
"plugins":
|
|
161
|
+
"plugins": all_plugins if all_plugins else None,
|
|
152
162
|
}
|
|
153
163
|
|
|
154
164
|
# Device Management (Core) service is always available
|
|
@@ -162,6 +172,14 @@ class ONVIFClient:
|
|
|
162
172
|
# Temporary variable to hold capabilities
|
|
163
173
|
self.capabilities = None
|
|
164
174
|
|
|
175
|
+
# Cache for security service capabilities (lazy loaded)
|
|
176
|
+
self._security_capabilities = None
|
|
177
|
+
self._security_capabilities_checked = False
|
|
178
|
+
|
|
179
|
+
# Cache for JWT service availability (lazy loaded)
|
|
180
|
+
self._jwt_available = None
|
|
181
|
+
self._jwt_checked = False
|
|
182
|
+
|
|
165
183
|
try:
|
|
166
184
|
# Try GetServices first (preferred method)
|
|
167
185
|
logger.debug("Attempting GetServices call for service discovery")
|
|
@@ -722,17 +740,15 @@ class ONVIFClient:
|
|
|
722
740
|
if self._security is None:
|
|
723
741
|
logger.debug("Initializing Security service")
|
|
724
742
|
self._security = AdvancedSecurity(
|
|
725
|
-
xaddr=self._get_xaddr("advancedsecurity", "Security"),
|
|
726
743
|
**self.common_args,
|
|
727
744
|
)
|
|
728
745
|
return self._security
|
|
729
746
|
|
|
730
747
|
@service
|
|
731
|
-
def jwt(self
|
|
748
|
+
def jwt(self):
|
|
732
749
|
if self._jwt is None:
|
|
733
750
|
logger.debug("Initializing JWT service")
|
|
734
|
-
|
|
735
|
-
self._jwt = JWT(xaddr=xaddr, **self.common_args)
|
|
751
|
+
self._jwt = JWT(**self.common_args)
|
|
736
752
|
return self._jwt
|
|
737
753
|
|
|
738
754
|
@service
|
|
@@ -15,7 +15,7 @@ class AdvancedSecurity(ONVIFService):
|
|
|
15
15
|
self.operator = ONVIFOperator(
|
|
16
16
|
definition["path"],
|
|
17
17
|
binding=f"{{{definition['namespace']}}}{definition['binding']}",
|
|
18
|
-
service_path="
|
|
18
|
+
service_path="device_service", # fallback
|
|
19
19
|
xaddr=xaddr,
|
|
20
20
|
**kwargs,
|
|
21
21
|
)
|
onvif/utils/__init__.py
CHANGED
|
@@ -7,6 +7,7 @@ from .xml_capture import XMLCapturePlugin
|
|
|
7
7
|
from .error_handlers import ONVIFErrorHandler
|
|
8
8
|
from .discovery import ONVIFDiscovery
|
|
9
9
|
from .service import ONVIFService
|
|
10
|
+
from .parser import ONVIFParser
|
|
10
11
|
|
|
11
12
|
|
|
12
13
|
__all__ = [
|
|
@@ -17,4 +18,5 @@ __all__ = [
|
|
|
17
18
|
"ONVIFErrorHandler",
|
|
18
19
|
"ONVIFDiscovery",
|
|
19
20
|
"ONVIFService",
|
|
21
|
+
"ONVIFParser",
|
|
20
22
|
]
|
onvif/utils/parser.py
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# onvif/utils/parser.py
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from typing import List, Dict, Optional
|
|
5
|
+
from zeep import Plugin
|
|
6
|
+
|
|
7
|
+
logger = logging.getLogger(__name__)
|
|
8
|
+
logger.addHandler(logging.NullHandler())
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ONVIFParser(Plugin):
|
|
12
|
+
"""
|
|
13
|
+
Lightweight Zeep plugin to extract XML elements from SOAP responses using XPath.
|
|
14
|
+
|
|
15
|
+
This plugin extracts specific XML elements from raw SOAP responses before zeep
|
|
16
|
+
parses them into Python objects. This is useful for extracting data that zeep
|
|
17
|
+
doesn't parse correctly, such as simpleContent elements with attributes (e.g., Topic).
|
|
18
|
+
|
|
19
|
+
Unlike XMLCapturePlugin which stores full XML history in memory, ONVIFParser only
|
|
20
|
+
extracts and caches specified element texts from the last response.
|
|
21
|
+
|
|
22
|
+
Usage Example 1 - Extract Topic from event notifications:
|
|
23
|
+
>>> from onvif import ONVIFClient
|
|
24
|
+
>>> from onvif.utils import ONVIFParser
|
|
25
|
+
>>>
|
|
26
|
+
>>> # Create parser to extract Topic elements
|
|
27
|
+
>>> parser = ONVIFParser(extract_xpaths={
|
|
28
|
+
... 'topic': './/{http://docs.oasis-open.org/wsn/b-2}Topic'
|
|
29
|
+
... })
|
|
30
|
+
>>>
|
|
31
|
+
>>> # Pass parser to client as plugin
|
|
32
|
+
>>> client = ONVIFClient(host, port, user, pass, plugins=[parser])
|
|
33
|
+
>>>
|
|
34
|
+
>>> # Make SOAP call
|
|
35
|
+
>>> pullpoint = client.pullpoint(subscription)
|
|
36
|
+
>>> msgs = pullpoint.PullMessages(Timeout="PT5S", MessageLimit=10)
|
|
37
|
+
>>>
|
|
38
|
+
>>> # Extract topic texts (cache auto-cleared on next SOAP call)
|
|
39
|
+
>>> topics = parser.get_extracted_texts('topic', count=10)
|
|
40
|
+
>>> for topic in topics:
|
|
41
|
+
... print(f"Topic: {topic}")
|
|
42
|
+
|
|
43
|
+
Usage Example 2 - Extract multiple elements:
|
|
44
|
+
>>> parser = ONVIFParser(extract_xpaths={
|
|
45
|
+
... 'topic': './/{http://docs.oasis-open.org/wsn/b-2}Topic',
|
|
46
|
+
... 'custom': './/ns:CustomElement'
|
|
47
|
+
... })
|
|
48
|
+
>>> client = ONVIFClient(host, port, user, pass, plugins=[parser])
|
|
49
|
+
>>>
|
|
50
|
+
>>> # After SOAP call
|
|
51
|
+
>>> topics = parser.get_extracted_texts('topic', count=5)
|
|
52
|
+
>>> customs = parser.get_extracted_texts('custom', count=5)
|
|
53
|
+
|
|
54
|
+
Notes:
|
|
55
|
+
- Uses ingress() hook to access raw XML before zeep parsing
|
|
56
|
+
- Cache automatically cleared on each SOAP response
|
|
57
|
+
- Thread-safe for single client usage
|
|
58
|
+
- Works with any XPath expression
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
def __init__(self, extract_xpaths: Dict[str, str]):
|
|
62
|
+
"""
|
|
63
|
+
Initialize XML element parser.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
extract_xpaths: Dictionary mapping names to XPath expressions. XPath expressions will be used to find elements in SOAP response.
|
|
67
|
+
Example: {
|
|
68
|
+
'topic': './/{http://docs.oasis-open.org/wsn/b-2}Topic',
|
|
69
|
+
'custom': './/ns:CustomElement'
|
|
70
|
+
}
|
|
71
|
+
"""
|
|
72
|
+
self.extract_xpaths = extract_xpaths
|
|
73
|
+
self._extracted_elements = {}
|
|
74
|
+
|
|
75
|
+
logger.debug(
|
|
76
|
+
f"ONVIFParser initialized with XPaths: {list(extract_xpaths.keys())}"
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
def ingress(self, envelope, http_headers, operation):
|
|
80
|
+
"""
|
|
81
|
+
Zeep plugin hook - called when SOAP response is received.
|
|
82
|
+
|
|
83
|
+
Extracts element texts from raw XML envelope using configured XPath expressions.
|
|
84
|
+
The envelope at this stage is an lxml Element tree, allowing XPath queries.
|
|
85
|
+
|
|
86
|
+
Cache is automatically cleared before extracting new elements to prevent memory accumulation.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
envelope: lxml Element representing SOAP envelope
|
|
90
|
+
http_headers: HTTP response headers
|
|
91
|
+
operation: Zeep operation being executed
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
Tuple of (envelope, http_headers) to pass to next plugin
|
|
95
|
+
"""
|
|
96
|
+
# Auto-clear cache from previous response
|
|
97
|
+
self._extracted_elements = {}
|
|
98
|
+
|
|
99
|
+
try:
|
|
100
|
+
# Extract elements using XPath from raw XML envelope
|
|
101
|
+
for name, xpath in self.extract_xpaths.items():
|
|
102
|
+
elements = envelope.findall(xpath)
|
|
103
|
+
|
|
104
|
+
# Extract text content from found elements
|
|
105
|
+
texts = [elem.text for elem in elements if elem.text]
|
|
106
|
+
|
|
107
|
+
if texts:
|
|
108
|
+
self._extracted_elements[name] = texts
|
|
109
|
+
logger.debug(
|
|
110
|
+
f"ONVIFParser: Extracted {len(texts)} '{name}' elements"
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
except Exception as e:
|
|
114
|
+
logger.warning(f"ONVIFParser: Failed to extract elements: {e}")
|
|
115
|
+
|
|
116
|
+
return envelope, http_headers
|
|
117
|
+
|
|
118
|
+
def get_extracted_texts(self, name: str, count: int) -> List[Optional[str]]:
|
|
119
|
+
"""
|
|
120
|
+
Get extracted element texts by name.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
name: Name of the extracted elements (key from extract_xpaths dict)
|
|
124
|
+
count: Number of elements to return
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
List of element text values, padded with None if fewer elements were found.
|
|
128
|
+
Example: If 3 elements found but count=5, returns [text1, text2, text3, None, None]
|
|
129
|
+
"""
|
|
130
|
+
texts = self._extracted_elements.get(name, [])[:count]
|
|
131
|
+
|
|
132
|
+
# Pad with None if not enough elements
|
|
133
|
+
while len(texts) < count:
|
|
134
|
+
texts.append(None)
|
|
135
|
+
|
|
136
|
+
return texts
|
onvif/utils/xml_capture.py
CHANGED
|
@@ -22,15 +22,6 @@ class XMLCapturePlugin(Plugin):
|
|
|
22
22
|
- Operation names for context
|
|
23
23
|
- Complete history of all transactions
|
|
24
24
|
|
|
25
|
-
Key Features:
|
|
26
|
-
- Pretty-printed XML with proper indentation
|
|
27
|
-
- Last request/response quick access
|
|
28
|
-
- Full transaction history storage
|
|
29
|
-
- HTTP header capture
|
|
30
|
-
- Operation name tracking
|
|
31
|
-
- File export capability
|
|
32
|
-
- Secure XML parsing (XXE protection)
|
|
33
|
-
|
|
34
25
|
Use Cases:
|
|
35
26
|
1. **Debugging**: See exact SOAP messages being sent/received
|
|
36
27
|
2. **Learning**: Understand ONVIF protocol structure
|
onvif/utils/zeep.py
CHANGED
|
@@ -360,8 +360,7 @@ class ZeepPatcher:
|
|
|
360
360
|
)
|
|
361
361
|
|
|
362
362
|
if should_flatten:
|
|
363
|
-
# This is truly a wrapper - flatten by
|
|
364
|
-
# But still set the tag_name field to the inner_content
|
|
363
|
+
# This is truly a wrapper - flatten by extracting children to parent
|
|
365
364
|
# Convert zeep object to dict to preserve manually added attributes
|
|
366
365
|
if hasattr(inner_content, "__values__") or hasattr(
|
|
367
366
|
inner_content, "__dict__"
|
|
@@ -369,8 +368,23 @@ class ZeepPatcher:
|
|
|
369
368
|
inner_content = ZeepPatcher._zeep_object_to_dict(
|
|
370
369
|
inner_content
|
|
371
370
|
)
|
|
371
|
+
|
|
372
|
+
# Set the wrapper field itself
|
|
372
373
|
values[tag_name] = inner_content
|
|
373
|
-
|
|
374
|
+
|
|
375
|
+
# If inner_content is a dict with children, copy them to parent too
|
|
376
|
+
if isinstance(inner_content, dict):
|
|
377
|
+
for child_key, child_val in inner_content.items():
|
|
378
|
+
if child_key.startswith("_"):
|
|
379
|
+
continue
|
|
380
|
+
# Copy children to parent level if field exists and is None
|
|
381
|
+
if (
|
|
382
|
+
child_key in values
|
|
383
|
+
and values[child_key] is None
|
|
384
|
+
):
|
|
385
|
+
values[child_key] = child_val
|
|
386
|
+
elif child_key not in values:
|
|
387
|
+
values[child_key] = child_val
|
|
374
388
|
else:
|
|
375
389
|
# Not a wrapper - just set the field directly
|
|
376
390
|
# Convert zeep object to dict to preserve manually added attributes
|
|
@@ -448,6 +462,20 @@ class ZeepPatcher:
|
|
|
448
462
|
inner_content
|
|
449
463
|
)
|
|
450
464
|
setattr(obj, tag_name, inner_content)
|
|
465
|
+
|
|
466
|
+
# If inner_content is a dict with children, copy them to parent too
|
|
467
|
+
if isinstance(inner_content, dict):
|
|
468
|
+
for child_key, child_val in inner_content.items():
|
|
469
|
+
if child_key.startswith("_"):
|
|
470
|
+
continue
|
|
471
|
+
# Copy children to parent level if field exists and is None
|
|
472
|
+
if (
|
|
473
|
+
hasattr(obj, child_key)
|
|
474
|
+
and getattr(obj, child_key) is None
|
|
475
|
+
):
|
|
476
|
+
setattr(obj, child_key, child_val)
|
|
477
|
+
elif not hasattr(obj, child_key):
|
|
478
|
+
setattr(obj, child_key, child_val)
|
|
451
479
|
else:
|
|
452
480
|
# Not a wrapper - just set the field directly
|
|
453
481
|
# Convert zeep object to dict to preserve manually added attributes
|
|
@@ -611,7 +639,14 @@ class ZeepPatcher:
|
|
|
611
639
|
child_result[child_qname.localname] = parsed
|
|
612
640
|
|
|
613
641
|
parsed_result[tag_name] = child_result
|
|
642
|
+
elif xmlelement.attrib:
|
|
643
|
+
# Element has attributes but no children - parse attributes
|
|
644
|
+
parsed_result[tag_name] = {
|
|
645
|
+
k: ZeepPatcher.parse_text_value(v)
|
|
646
|
+
for k, v in xmlelement.attrib.items()
|
|
647
|
+
}
|
|
614
648
|
else:
|
|
649
|
+
# Element has no attributes and no children - just text content
|
|
615
650
|
parsed_result[tag_name] = ZeepPatcher.parse_text_value(xmlelement.text)
|
|
616
651
|
|
|
617
652
|
# Store original elements in a special key for later restoration
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: onvif-python
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: A modern Python library for ONVIF-compliant devices
|
|
5
5
|
Author-email: Nirsimetri Technologies® <open@nirsimetri.com>
|
|
6
6
|
Maintainer: Kaburagi
|
|
@@ -47,7 +47,7 @@ Dynamic: license-file
|
|
|
47
47
|
|
|
48
48
|
[](https://app.codacy.com/gh/nirsimetri/onvif-python/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade)
|
|
49
49
|
[](https://deepwiki.com/nirsimetri/onvif-python)
|
|
50
|
-
[](https://pypi.org/project/onvif-python/)
|
|
51
51
|
[](https://clickpy.clickhouse.com/dashboard/onvif-python)
|
|
52
52
|
<br>
|
|
53
53
|
[](https://github.com/nirsimetri/onvif-python/actions/workflows/python-app.yml)
|
|
@@ -356,7 +356,7 @@ usage: onvif [-h] [--host HOST] [--port PORT] [--username USERNAME] [--password
|
|
|
356
356
|
[--no-verify] [--no-patch] [--interactive] [--debug] [--wsdl WSDL] [--cache {all,db,mem,none}] [--health-check-interval HEALTH_CHECK_INTERVAL] [--version]
|
|
357
357
|
[service] [method] [params ...]
|
|
358
358
|
|
|
359
|
-
ONVIF Terminal Client — v0.2.
|
|
359
|
+
ONVIF Terminal Client — v0.2.2
|
|
360
360
|
https://github.com/nirsimetri/onvif-python
|
|
361
361
|
|
|
362
362
|
positional arguments:
|
|
@@ -429,7 +429,7 @@ Examples:
|
|
|
429
429
|
<summary><b>2. Interactive Shell</b></summary>
|
|
430
430
|
|
|
431
431
|
```bash
|
|
432
|
-
ONVIF Interactive Shell — v0.2.
|
|
432
|
+
ONVIF Interactive Shell — v0.2.2
|
|
433
433
|
https://github.com/nirsimetri/onvif-python
|
|
434
434
|
|
|
435
435
|
Basic Commands:
|
|
@@ -1108,9 +1108,9 @@ Some ONVIF services have multiple bindings in the same WSDL. These typically inc
|
|
|
1108
1108
|
2. **Security (Advanced Security)**
|
|
1109
1109
|
- **Root:** `AdvancedSecurityServiceBinding`
|
|
1110
1110
|
- **Sub-bindings:**
|
|
1111
|
+
- `JWTBinding`
|
|
1111
1112
|
- `AuthorizationServerBinding`
|
|
1112
1113
|
- `KeystoreBinding`
|
|
1113
|
-
- `JWTBinding`
|
|
1114
1114
|
- `Dot1XBinding`
|
|
1115
1115
|
- `TLSServerBinding`
|
|
1116
1116
|
- `MediaSigningBinding`
|
|
@@ -1118,9 +1118,9 @@ Some ONVIF services have multiple bindings in the same WSDL. These typically inc
|
|
|
1118
1118
|
Usage in library:
|
|
1119
1119
|
```python
|
|
1120
1120
|
client.security() # root binding
|
|
1121
|
+
client.jwt() # sub-binding accessor
|
|
1121
1122
|
client.authorizationserver(xaddr) # sub-binding accessor (requires xAddr)
|
|
1122
1123
|
client.keystore(xaddr) # ..
|
|
1123
|
-
client.jwt(xaddr)
|
|
1124
1124
|
client.dot1x(xaddr)
|
|
1125
1125
|
client.tlsserver(xaddr)
|
|
1126
1126
|
client.mediasigning(xaddr)
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
onvif/__init__.py,sha256=
|
|
2
|
-
onvif/client.py,sha256=
|
|
1
|
+
onvif/__init__.py,sha256=JYjHyKme5ou0I63dBnArAwN1rAIFalHXv_AgIgLgT-Y,499
|
|
2
|
+
onvif/client.py,sha256=0G4AG-IJyaCheR2jXbZDWL_p5tx-wGRnXelMRKNS2bc,26693
|
|
3
3
|
onvif/operator.py,sha256=fBaoWYC3ZVFdgd2S_cmztWiLAA-HaWwSflq278YNlTs,13970
|
|
4
4
|
onvif/cli/__init__.py,sha256=y3hc-mo-Iwwxf8m5vo9P_0m2ISzGuOy-jaRPkdRY05A,68
|
|
5
5
|
onvif/cli/__main__.py,sha256=IZyZcevWVKE1BzdFY53a3MjOOfybncGVVXQ_7lZ3TOQ,165
|
|
6
|
-
onvif/cli/interactive.py,sha256=
|
|
7
|
-
onvif/cli/main.py,sha256=
|
|
8
|
-
onvif/cli/utils.py,sha256=
|
|
6
|
+
onvif/cli/interactive.py,sha256=vlcCbmCyB5nWv0LwG4DloR5n4Q9PTdXkTfZBk6jsNf4,59756
|
|
7
|
+
onvif/cli/main.py,sha256=G82DSIogd6Wc6KTIPmU6dS-mJna6z-0KYYPZYxSQGuE,27312
|
|
8
|
+
onvif/cli/utils.py,sha256=t84r0NkqqaaV0xWvBSgP9FYZIFYpJwPINAcrDVZ9Eq8,59109
|
|
9
9
|
onvif/db/companies.db,sha256=yJ8bSHfGpxNnA5NRBhYOFS_1Pix-KgH8GnUFV3ytzPo,270336
|
|
10
10
|
onvif/db/products.db,sha256=4BVPfy6i6pLMLQyfue238uRp6_NIDodu4QXd9iCoooY,81559552
|
|
11
11
|
onvif/services/__init__.py,sha256=1CJDDq656ag8VR19Mnm_0N-1Avd0t2832Jze-30wnMY,2005
|
|
@@ -38,21 +38,22 @@ onvif/services/events/events.py,sha256=Km8fOEOLpWvNMDI4biAqSTPVEbs0FvXBGsMp_sAOA
|
|
|
38
38
|
onvif/services/events/notification.py,sha256=l0RVuxpdGxss9CRcI8hOMP0E1gYD9e3nVISF_-hctwk,1263
|
|
39
39
|
onvif/services/events/pullpoint.py,sha256=mSegFY3AJ4rLkO3nWJ-G37opEAiSKCj7TGh3RDWXKyU,1145
|
|
40
40
|
onvif/services/events/subscription.py,sha256=a-rQgW4eSRlOCuitxnwJ7krTXyKfCHg4mn9S0_rTT8g,883
|
|
41
|
-
onvif/services/security/advancedsecurity.py,sha256=
|
|
41
|
+
onvif/services/security/advancedsecurity.py,sha256=vA5l1894H2Kw_ZI-h3_prahWdmHvNeL0bJgMmtbvhVg,955
|
|
42
42
|
onvif/services/security/authorizationserver.py,sha256=1eKd_wjGE1Ues7egLL54VLvJ7zUGTcBIZNbZxiPSdLc,1501
|
|
43
43
|
onvif/services/security/dot1x.py,sha256=biQrWH6aCXwxK7Hy_BcH1SHMyp8J0BHQ5MHKai36sXE,1808
|
|
44
44
|
onvif/services/security/jwt.py,sha256=_jiFliLK5pjErgAED5z6S2DTLEP8eezikq6KGJ96g08,976
|
|
45
45
|
onvif/services/security/keystore.py,sha256=P9QYieCAZ3yf5Gq6DZKpG832XD-rNVateFxlZBiV0bk,7285
|
|
46
46
|
onvif/services/security/mediasigning.py,sha256=7ia_NIdLM3kwZwwOM95WfR0TTXkZSYCt6j8wL5Vl7zI,1361
|
|
47
47
|
onvif/services/security/tlsserver.py,sha256=UO_0VHtbu0Zx1cOsJePtEwPbmYL0RHyRhoInCWIzx_M,3412
|
|
48
|
-
onvif/utils/__init__.py,sha256=
|
|
48
|
+
onvif/utils/__init__.py,sha256=5bXw0F0jOhBSYwF8lsy9GIA1JTRFKLeaB5UYc2MX-Bg,518
|
|
49
49
|
onvif/utils/discovery.py,sha256=YO44KiQhfcyUECNrRe4qe03DSh14GGVByu1W5ekbAXU,14765
|
|
50
50
|
onvif/utils/error_handlers.py,sha256=MwPNfUeDA6d_nNZ01P0Ut67ERBYVvjsYrRsQhIo28yc,4851
|
|
51
51
|
onvif/utils/exceptions.py,sha256=5DxvqKAL7w038qPFeWmtkR5F3jWK0nz0qerzsOQPh0k,7788
|
|
52
|
+
onvif/utils/parser.py,sha256=QYzzIqsP1RuSIdx0SYPnighomYkLsfroRPMcjGOmgeo,5125
|
|
52
53
|
onvif/utils/service.py,sha256=VTCOY5PMjaY9KgCznDomTcPLVIorIRnqBPd7VCoLxAM,12722
|
|
53
54
|
onvif/utils/wsdl.py,sha256=9VqJwxjyrz_gtNQ5IHPQ0pjek6Y0aOOkencWXkUGKp4,27250
|
|
54
|
-
onvif/utils/xml_capture.py,sha256=
|
|
55
|
-
onvif/utils/zeep.py,sha256=
|
|
55
|
+
onvif/utils/xml_capture.py,sha256=r8N2_D1hbKywMdftlTTMnxPhzxOsJDLkEJDKgudfpbo,7984
|
|
56
|
+
onvif/utils/zeep.py,sha256=a5naCxLg7PRrKFB59TOlJbGyFpxUh9HZ1zAs-r14ojs,30609
|
|
56
57
|
onvif/wsdl/LICENSE.md,sha256=O0J5vlcNSuEulS5rESOZWYy-IEnH4QeJkQ4ebZZNkSQ,16439
|
|
57
58
|
onvif/wsdl/ver10/actionengine.wsdl,sha256=mQCRdRMdh517EUkDCKEfBHl_GvX9dPABqtgwU9K8FWE,55762
|
|
58
59
|
onvif/wsdl/ver10/analyticsdevice.wsdl,sha256=JCd2pab5RAgoPm_26Q3l6sda8cGkam4dsXeluJ4euyA,30999
|
|
@@ -112,9 +113,9 @@ onvif/wsdl/ver20/util/federated_search_documentation.html,sha256=GR6a4SPBsRDisaW
|
|
|
112
113
|
onvif/wsdl/ver20/util/fedsearch-docu-viewer.xsl,sha256=8eP-uMZgsTtuU5WaVyCzs_DJZqWIFD7iRz_5-3ComPg,325
|
|
113
114
|
onvif/wsdl/ver20/util/onvif-wsdl-viewer.xsl,sha256=Emauxs6rh69IxlxlU3k_LEH_i3SsRbwIKUk-VcMlVgk,94525
|
|
114
115
|
onvif/wsdl/ver20/util/operationIndex.html,sha256=mUsAGgNUFCIQjVV9Rx_O5YUNW3EdWNhUkAbBY-xVMhQ,39290
|
|
115
|
-
onvif_python-0.2.
|
|
116
|
-
onvif_python-0.2.
|
|
117
|
-
onvif_python-0.2.
|
|
118
|
-
onvif_python-0.2.
|
|
119
|
-
onvif_python-0.2.
|
|
120
|
-
onvif_python-0.2.
|
|
116
|
+
onvif_python-0.2.2.dist-info/licenses/LICENSE.md,sha256=ya8FumJeUkFWwCMkG9KdMxHWRWMNhtI3vHzzAWPQS8s,1091
|
|
117
|
+
onvif_python-0.2.2.dist-info/METADATA,sha256=CN636idmdC-V40qde6INqJGTyONKJXBSWvmj6n9LxA4,60827
|
|
118
|
+
onvif_python-0.2.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
119
|
+
onvif_python-0.2.2.dist-info/entry_points.txt,sha256=7xUdI64yzWh7YfNPQDKCW9cT8XIgWuNCloikP9-yjqY,46
|
|
120
|
+
onvif_python-0.2.2.dist-info/top_level.txt,sha256=MqCi1uJmIvtVFJDz2IxLbpwq6FwDtWhqrWQtKwLaQg4,6
|
|
121
|
+
onvif_python-0.2.2.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|