axis 68__tar.gz → 69__tar.gz
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.
- {axis-68 → axis-69}/PKG-INFO +8 -8
- axis-69/axis/interfaces/aiohttp_digest.py +266 -0
- {axis-68 → axis-69}/axis/interfaces/vapix.py +12 -0
- {axis-68 → axis-69}/axis/models/api_discovery.py +15 -0
- {axis-68 → axis-69}/axis.egg-info/PKG-INFO +8 -8
- {axis-68 → axis-69}/axis.egg-info/SOURCES.txt +1 -0
- {axis-68 → axis-69}/axis.egg-info/requires.txt +7 -7
- {axis-68 → axis-69}/pyproject.toml +9 -9
- axis-69/tests/test_http_client_compat.py +393 -0
- axis-68/tests/test_http_client_compat.py +0 -165
- {axis-68 → axis-69}/LICENSE +0 -0
- {axis-68 → axis-69}/README.md +0 -0
- {axis-68 → axis-69}/axis/__init__.py +0 -0
- {axis-68 → axis-69}/axis/__main__.py +0 -0
- {axis-68 → axis-69}/axis/device.py +0 -0
- {axis-68 → axis-69}/axis/errors.py +0 -0
- {axis-68 → axis-69}/axis/interfaces/__init__.py +0 -0
- {axis-68 → axis-69}/axis/interfaces/api_discovery.py +0 -0
- {axis-68 → axis-69}/axis/interfaces/api_handler.py +0 -0
- {axis-68 → axis-69}/axis/interfaces/applications/__init__.py +0 -0
- {axis-68 → axis-69}/axis/interfaces/applications/application_handler.py +0 -0
- {axis-68 → axis-69}/axis/interfaces/applications/applications.py +0 -0
- {axis-68 → axis-69}/axis/interfaces/applications/fence_guard.py +0 -0
- {axis-68 → axis-69}/axis/interfaces/applications/loitering_guard.py +0 -0
- {axis-68 → axis-69}/axis/interfaces/applications/motion_guard.py +0 -0
- {axis-68 → axis-69}/axis/interfaces/applications/object_analytics.py +0 -0
- {axis-68 → axis-69}/axis/interfaces/applications/vmd4.py +0 -0
- {axis-68 → axis-69}/axis/interfaces/basic_device_info.py +0 -0
- {axis-68 → axis-69}/axis/interfaces/event_instances.py +0 -0
- {axis-68 → axis-69}/axis/interfaces/event_manager.py +0 -0
- {axis-68 → axis-69}/axis/interfaces/light_control.py +0 -0
- {axis-68 → axis-69}/axis/interfaces/mqtt.py +0 -0
- {axis-68 → axis-69}/axis/interfaces/parameters/__init__.py +0 -0
- {axis-68 → axis-69}/axis/interfaces/parameters/brand.py +0 -0
- {axis-68 → axis-69}/axis/interfaces/parameters/image.py +0 -0
- {axis-68 → axis-69}/axis/interfaces/parameters/io_port.py +0 -0
- {axis-68 → axis-69}/axis/interfaces/parameters/param_cgi.py +0 -0
- {axis-68 → axis-69}/axis/interfaces/parameters/param_handler.py +0 -0
- {axis-68 → axis-69}/axis/interfaces/parameters/properties.py +0 -0
- {axis-68 → axis-69}/axis/interfaces/parameters/ptz.py +0 -0
- {axis-68 → axis-69}/axis/interfaces/parameters/stream_profile.py +0 -0
- {axis-68 → axis-69}/axis/interfaces/pir_sensor_configuration.py +0 -0
- {axis-68 → axis-69}/axis/interfaces/port_cgi.py +0 -0
- {axis-68 → axis-69}/axis/interfaces/port_management.py +0 -0
- {axis-68 → axis-69}/axis/interfaces/ptz.py +0 -0
- {axis-68 → axis-69}/axis/interfaces/pwdgrp_cgi.py +0 -0
- {axis-68 → axis-69}/axis/interfaces/stream_profiles.py +0 -0
- {axis-68 → axis-69}/axis/interfaces/user_groups.py +0 -0
- {axis-68 → axis-69}/axis/interfaces/view_areas.py +0 -0
- {axis-68 → axis-69}/axis/models/__init__.py +0 -0
- {axis-68 → axis-69}/axis/models/api.py +0 -0
- {axis-68 → axis-69}/axis/models/applications/__init__.py +0 -0
- {axis-68 → axis-69}/axis/models/applications/application.py +0 -0
- {axis-68 → axis-69}/axis/models/applications/fence_guard.py +0 -0
- {axis-68 → axis-69}/axis/models/applications/loitering_guard.py +0 -0
- {axis-68 → axis-69}/axis/models/applications/motion_guard.py +0 -0
- {axis-68 → axis-69}/axis/models/applications/object_analytics.py +0 -0
- {axis-68 → axis-69}/axis/models/applications/vmd4.py +0 -0
- {axis-68 → axis-69}/axis/models/basic_device_info.py +0 -0
- {axis-68 → axis-69}/axis/models/configuration.py +0 -0
- {axis-68 → axis-69}/axis/models/event.py +0 -0
- {axis-68 → axis-69}/axis/models/event_instance.py +0 -0
- {axis-68 → axis-69}/axis/models/light_control.py +0 -0
- {axis-68 → axis-69}/axis/models/mqtt.py +0 -0
- {axis-68 → axis-69}/axis/models/parameters/__init__.py +0 -0
- {axis-68 → axis-69}/axis/models/parameters/brand.py +0 -0
- {axis-68 → axis-69}/axis/models/parameters/image.py +0 -0
- {axis-68 → axis-69}/axis/models/parameters/io_port.py +0 -0
- {axis-68 → axis-69}/axis/models/parameters/param_cgi.py +0 -0
- {axis-68 → axis-69}/axis/models/parameters/properties.py +0 -0
- {axis-68 → axis-69}/axis/models/parameters/ptz.py +0 -0
- {axis-68 → axis-69}/axis/models/parameters/stream_profile.py +0 -0
- {axis-68 → axis-69}/axis/models/pir_sensor_configuration.py +0 -0
- {axis-68 → axis-69}/axis/models/port_cgi.py +0 -0
- {axis-68 → axis-69}/axis/models/port_management.py +0 -0
- {axis-68 → axis-69}/axis/models/ptz_cgi.py +0 -0
- {axis-68 → axis-69}/axis/models/pwdgrp_cgi.py +0 -0
- {axis-68 → axis-69}/axis/models/stream_profile.py +0 -0
- {axis-68 → axis-69}/axis/models/user_group.py +0 -0
- {axis-68 → axis-69}/axis/models/view_area.py +0 -0
- {axis-68 → axis-69}/axis/py.typed +0 -0
- {axis-68 → axis-69}/axis/rtsp.py +0 -0
- {axis-68 → axis-69}/axis/stream_manager.py +0 -0
- {axis-68 → axis-69}/axis/stream_transport.py +0 -0
- {axis-68 → axis-69}/axis/websocket.py +0 -0
- {axis-68 → axis-69}/axis.egg-info/dependency_links.txt +0 -0
- {axis-68 → axis-69}/axis.egg-info/entry_points.txt +0 -0
- {axis-68 → axis-69}/axis.egg-info/top_level.txt +0 -0
- {axis-68 → axis-69}/setup.cfg +0 -0
- {axis-68 → axis-69}/tests/test_api_discovery.py +0 -0
- {axis-68 → axis-69}/tests/test_api_handler.py +0 -0
- {axis-68 → axis-69}/tests/test_auth_scheme.py +0 -0
- {axis-68 → axis-69}/tests/test_basic_device_info.py +0 -0
- {axis-68 → axis-69}/tests/test_configuration.py +0 -0
- {axis-68 → axis-69}/tests/test_device.py +0 -0
- {axis-68 → axis-69}/tests/test_event.py +0 -0
- {axis-68 → axis-69}/tests/test_event_instances.py +0 -0
- {axis-68 → axis-69}/tests/test_event_stream.py +0 -0
- {axis-68 → axis-69}/tests/test_light_control.py +0 -0
- {axis-68 → axis-69}/tests/test_main_http_client.py +0 -0
- {axis-68 → axis-69}/tests/test_mqtt.py +0 -0
- {axis-68 → axis-69}/tests/test_pir_sensor_configuration.py +0 -0
- {axis-68 → axis-69}/tests/test_port_cgi.py +0 -0
- {axis-68 → axis-69}/tests/test_port_management.py +0 -0
- {axis-68 → axis-69}/tests/test_ptz.py +0 -0
- {axis-68 → axis-69}/tests/test_pwdgrp_cgi.py +0 -0
- {axis-68 → axis-69}/tests/test_rtsp.py +0 -0
- {axis-68 → axis-69}/tests/test_stream_manager.py +0 -0
- {axis-68 → axis-69}/tests/test_stream_profiles.py +0 -0
- {axis-68 → axis-69}/tests/test_user_groups.py +0 -0
- {axis-68 → axis-69}/tests/test_vapix.py +0 -0
- {axis-68 → axis-69}/tests/test_view_areas.py +0 -0
- {axis-68 → axis-69}/tests/test_websocket.py +0 -0
{axis-68 → axis-69}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: axis
|
|
3
|
-
Version:
|
|
3
|
+
Version: 69
|
|
4
4
|
Summary: A Python library for communicating with devices from Axis Communications
|
|
5
5
|
Author-email: Robert Svensson <Kane610@users.noreply.github.com>
|
|
6
6
|
License: MIT
|
|
@@ -27,19 +27,19 @@ Provides-Extra: requirements
|
|
|
27
27
|
Requires-Dist: aiohttp==3.13.5; extra == "requirements"
|
|
28
28
|
Requires-Dist: httpx==0.28.1; extra == "requirements"
|
|
29
29
|
Requires-Dist: orjson==3.11.8; extra == "requirements"
|
|
30
|
-
Requires-Dist: packaging==26.
|
|
30
|
+
Requires-Dist: packaging==26.2; extra == "requirements"
|
|
31
31
|
Requires-Dist: xmltodict==1.0.4; extra == "requirements"
|
|
32
32
|
Provides-Extra: requirements-test
|
|
33
|
-
Requires-Dist: mypy==1.20.
|
|
34
|
-
Requires-Dist: pytest==9.0.
|
|
33
|
+
Requires-Dist: mypy==1.20.2; extra == "requirements-test"
|
|
34
|
+
Requires-Dist: pytest==9.0.3; extra == "requirements-test"
|
|
35
35
|
Requires-Dist: pytest-aiohttp==1.1.0; extra == "requirements-test"
|
|
36
36
|
Requires-Dist: pytest-asyncio==1.3.0; extra == "requirements-test"
|
|
37
37
|
Requires-Dist: pytest-cov==7.1.0; extra == "requirements-test"
|
|
38
|
-
Requires-Dist: respx==0.
|
|
39
|
-
Requires-Dist: ruff==0.15.
|
|
40
|
-
Requires-Dist: types-xmltodict==v1.0.1.
|
|
38
|
+
Requires-Dist: respx==0.23.1; extra == "requirements-test"
|
|
39
|
+
Requires-Dist: ruff==0.15.12; extra == "requirements-test"
|
|
40
|
+
Requires-Dist: types-xmltodict==v1.0.1.20260408; extra == "requirements-test"
|
|
41
41
|
Provides-Extra: requirements-dev
|
|
42
|
-
Requires-Dist: pre-commit==4.
|
|
42
|
+
Requires-Dist: pre-commit==4.6.0; extra == "requirements-dev"
|
|
43
43
|
Dynamic: license-file
|
|
44
44
|
|
|
45
45
|
# axis
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
"""Aiohttp digest authentication handler.
|
|
2
|
+
|
|
3
|
+
Implements library-managed RFC 2617 digest authentication for aiohttp requests
|
|
4
|
+
to handle special characters in request parameters that break middleware-based auth.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import hashlib
|
|
10
|
+
import logging
|
|
11
|
+
import re
|
|
12
|
+
import secrets
|
|
13
|
+
from typing import TYPE_CHECKING, Any, cast
|
|
14
|
+
from urllib.parse import quote, urlsplit
|
|
15
|
+
|
|
16
|
+
from ..models.configuration import AuthScheme
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from ..device import AxisDevice
|
|
20
|
+
|
|
21
|
+
LOGGER = logging.getLogger(__name__)
|
|
22
|
+
TIME_OUT = 15
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class AiohttpDigestAuth:
|
|
26
|
+
"""Manages digest authentication for aiohttp requests."""
|
|
27
|
+
|
|
28
|
+
def __init__(self, device: AxisDevice) -> None:
|
|
29
|
+
"""Initialize digest auth handler."""
|
|
30
|
+
self.device = device
|
|
31
|
+
self._nonce: str | None = None
|
|
32
|
+
self._nonce_count = 0
|
|
33
|
+
|
|
34
|
+
def should_use_library_digest(self, http_client: str, has_basic_auth: bool) -> bool:
|
|
35
|
+
"""Return if aiohttp requests should use library-managed digest auth.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
http_client: Name of HTTP client ("aiohttp" or "httpx").
|
|
39
|
+
has_basic_auth: Whether basic auth is configured.
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
True if library-managed digest should be used.
|
|
43
|
+
|
|
44
|
+
"""
|
|
45
|
+
return (
|
|
46
|
+
http_client == "aiohttp"
|
|
47
|
+
and not has_basic_auth
|
|
48
|
+
and self.device.config.auth_scheme != AuthScheme.BASIC
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
def request_target(
|
|
52
|
+
self, url: str, params: dict[str, str] | None, should_encode: bool
|
|
53
|
+
) -> tuple[str, dict[str, str] | None]:
|
|
54
|
+
"""Return request URL and params for aiohttp request.
|
|
55
|
+
|
|
56
|
+
With library-managed digest auth, pre-encode params into the URL so the
|
|
57
|
+
signed URI exactly matches the request-target on the wire.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
url: Base request URL.
|
|
61
|
+
params: Optional query parameters.
|
|
62
|
+
should_encode: Whether to pre-encode params for digest signing.
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
Tuple of (request_url, request_params) to use in actual request.
|
|
66
|
+
|
|
67
|
+
"""
|
|
68
|
+
if params is None or not should_encode:
|
|
69
|
+
return url, params
|
|
70
|
+
|
|
71
|
+
separator = "&" if "?" in url else "?"
|
|
72
|
+
encoded_parts = [
|
|
73
|
+
f"{quote(k, safe='')}={quote(v, safe='')}" for k, v in params.items()
|
|
74
|
+
]
|
|
75
|
+
encoded_query = "&".join(encoded_parts)
|
|
76
|
+
encoded_url = f"{url}{separator}{encoded_query}"
|
|
77
|
+
return encoded_url, None
|
|
78
|
+
|
|
79
|
+
def extract_challenge(self, headers: Any) -> str | None:
|
|
80
|
+
"""Return digest challenge header when present.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
headers: Response headers (dict-like or aiohttp MultiDictProxy).
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
Digest challenge string if present, None otherwise.
|
|
87
|
+
|
|
88
|
+
"""
|
|
89
|
+
candidates: list[str] = []
|
|
90
|
+
if hasattr(headers, "getall"):
|
|
91
|
+
candidates.extend(cast("list[str]", headers.getall("WWW-Authenticate", [])))
|
|
92
|
+
else:
|
|
93
|
+
for name, value in cast("dict[str, str]", headers).items():
|
|
94
|
+
if name.lower() == "www-authenticate":
|
|
95
|
+
candidates.append(value)
|
|
96
|
+
|
|
97
|
+
for value in candidates:
|
|
98
|
+
if value.lower().startswith("digest "):
|
|
99
|
+
return value
|
|
100
|
+
return None
|
|
101
|
+
|
|
102
|
+
def build_authorization(
|
|
103
|
+
self,
|
|
104
|
+
method: str,
|
|
105
|
+
request_url: str,
|
|
106
|
+
digest_challenge: str,
|
|
107
|
+
) -> str | None:
|
|
108
|
+
"""Build digest authorization header from challenge and request URI.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
method: HTTP method (GET, POST, etc.).
|
|
112
|
+
request_url: Full request URL (will extract path + query).
|
|
113
|
+
digest_challenge: Digest challenge string from WWW-Authenticate header.
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
Authorization header value or None if digest cannot be built.
|
|
117
|
+
|
|
118
|
+
"""
|
|
119
|
+
challenge_values = {
|
|
120
|
+
key.lower(): value.strip('"')
|
|
121
|
+
for key, value in re.findall(
|
|
122
|
+
r"(\w+)=((?:\"[^\"]*\")|(?:[^,]+))", digest_challenge
|
|
123
|
+
)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
realm = challenge_values.get("realm")
|
|
127
|
+
nonce = challenge_values.get("nonce")
|
|
128
|
+
if realm is None or nonce is None:
|
|
129
|
+
return None
|
|
130
|
+
|
|
131
|
+
algorithm = challenge_values.get("algorithm", "MD5").upper()
|
|
132
|
+
if algorithm != "MD5":
|
|
133
|
+
LOGGER.debug("Unsupported digest algorithm for aiohttp path: %s", algorithm)
|
|
134
|
+
return None
|
|
135
|
+
|
|
136
|
+
uri = self._digest_uri(request_url)
|
|
137
|
+
qop = None
|
|
138
|
+
if qop_header := challenge_values.get("qop"):
|
|
139
|
+
qop_values = [value.strip() for value in qop_header.split(",")]
|
|
140
|
+
if "auth" in qop_values:
|
|
141
|
+
qop = "auth"
|
|
142
|
+
|
|
143
|
+
username = self.device.config.username
|
|
144
|
+
password = self.device.config.password
|
|
145
|
+
ha1 = hashlib.md5(f"{username}:{realm}:{password}".encode()).hexdigest()
|
|
146
|
+
ha2 = hashlib.md5(f"{method.upper()}:{uri}".encode()).hexdigest()
|
|
147
|
+
|
|
148
|
+
parts = [
|
|
149
|
+
f'username="{username}"',
|
|
150
|
+
f'realm="{realm}"',
|
|
151
|
+
f'nonce="{nonce}"',
|
|
152
|
+
f'uri="{uri}"',
|
|
153
|
+
'algorithm="MD5"',
|
|
154
|
+
]
|
|
155
|
+
|
|
156
|
+
if opaque := challenge_values.get("opaque"):
|
|
157
|
+
parts.append(f'opaque="{opaque}"')
|
|
158
|
+
|
|
159
|
+
if qop == "auth":
|
|
160
|
+
if nonce != self._nonce:
|
|
161
|
+
self._nonce = nonce
|
|
162
|
+
self._nonce_count = 0
|
|
163
|
+
|
|
164
|
+
self._nonce_count += 1
|
|
165
|
+
nc = f"{self._nonce_count:08x}"
|
|
166
|
+
cnonce = secrets.token_hex(8)
|
|
167
|
+
response = hashlib.md5(
|
|
168
|
+
f"{ha1}:{nonce}:{nc}:{cnonce}:{qop}:{ha2}".encode()
|
|
169
|
+
).hexdigest()
|
|
170
|
+
parts.extend(
|
|
171
|
+
[
|
|
172
|
+
f'response="{response}"',
|
|
173
|
+
f"qop={qop}",
|
|
174
|
+
f"nc={nc}",
|
|
175
|
+
f'cnonce="{cnonce}"',
|
|
176
|
+
]
|
|
177
|
+
)
|
|
178
|
+
else:
|
|
179
|
+
response = hashlib.md5(f"{ha1}:{nonce}:{ha2}".encode()).hexdigest()
|
|
180
|
+
parts.append(f'response="{response}"')
|
|
181
|
+
|
|
182
|
+
return f"Digest {', '.join(parts)}"
|
|
183
|
+
|
|
184
|
+
def _digest_uri(self, request_url: str) -> str:
|
|
185
|
+
"""Return path + query request-target URI for digest signing.
|
|
186
|
+
|
|
187
|
+
Args:
|
|
188
|
+
request_url: Full request URL.
|
|
189
|
+
|
|
190
|
+
Returns:
|
|
191
|
+
Path and query string for digest signature.
|
|
192
|
+
|
|
193
|
+
"""
|
|
194
|
+
split_result = urlsplit(request_url)
|
|
195
|
+
if split_result.query:
|
|
196
|
+
return f"{split_result.path}?{split_result.query}"
|
|
197
|
+
return split_result.path
|
|
198
|
+
|
|
199
|
+
async def perform_request(
|
|
200
|
+
self,
|
|
201
|
+
session: Any,
|
|
202
|
+
method: str,
|
|
203
|
+
url: str,
|
|
204
|
+
request_data: bytes | dict[str, str] | None,
|
|
205
|
+
headers: dict[str, str] | None,
|
|
206
|
+
params: dict[str, str] | None,
|
|
207
|
+
) -> tuple[int, dict[str, str], bytes]:
|
|
208
|
+
"""Execute aiohttp request with digest auth handling.
|
|
209
|
+
|
|
210
|
+
Args:
|
|
211
|
+
session: aiohttp ClientSession.
|
|
212
|
+
method: HTTP method.
|
|
213
|
+
url: Request URL.
|
|
214
|
+
request_data: Request body (bytes or form data).
|
|
215
|
+
headers: Request headers.
|
|
216
|
+
params: Query parameters.
|
|
217
|
+
|
|
218
|
+
Returns:
|
|
219
|
+
Tuple of (status_code, response_headers, response_content).
|
|
220
|
+
|
|
221
|
+
"""
|
|
222
|
+
request_url, request_params = self.request_target(url, params, True)
|
|
223
|
+
request_headers = dict(headers) if headers is not None else {}
|
|
224
|
+
|
|
225
|
+
# First attempt without auth to get challenge
|
|
226
|
+
async with session.request(
|
|
227
|
+
method,
|
|
228
|
+
request_url,
|
|
229
|
+
data=request_data,
|
|
230
|
+
headers=request_headers,
|
|
231
|
+
params=request_params,
|
|
232
|
+
auth=None,
|
|
233
|
+
timeout=TIME_OUT,
|
|
234
|
+
) as response:
|
|
235
|
+
response_content = await response.read()
|
|
236
|
+
response_headers = dict(response.headers)
|
|
237
|
+
if response.status != 401:
|
|
238
|
+
return response.status, response_headers, response_content
|
|
239
|
+
|
|
240
|
+
digest_challenge = self.extract_challenge(response.headers)
|
|
241
|
+
if digest_challenge is None:
|
|
242
|
+
return response.status, response_headers, response_content
|
|
243
|
+
|
|
244
|
+
# Build digest auth and retry
|
|
245
|
+
digest_authorization = self.build_authorization(
|
|
246
|
+
method=method,
|
|
247
|
+
request_url=request_url,
|
|
248
|
+
digest_challenge=digest_challenge,
|
|
249
|
+
)
|
|
250
|
+
if digest_authorization is None:
|
|
251
|
+
return 401, {}, b""
|
|
252
|
+
|
|
253
|
+
retry_headers = dict(request_headers)
|
|
254
|
+
retry_headers["Authorization"] = digest_authorization
|
|
255
|
+
|
|
256
|
+
async with session.request(
|
|
257
|
+
method,
|
|
258
|
+
request_url,
|
|
259
|
+
data=request_data,
|
|
260
|
+
headers=retry_headers,
|
|
261
|
+
params=request_params,
|
|
262
|
+
auth=None,
|
|
263
|
+
timeout=TIME_OUT,
|
|
264
|
+
) as response:
|
|
265
|
+
response_content = await response.read()
|
|
266
|
+
return response.status, dict(response.headers), response_content
|
|
@@ -12,6 +12,7 @@ import httpx
|
|
|
12
12
|
from ..errors import RequestError, raise_error
|
|
13
13
|
from ..models.configuration import AuthScheme
|
|
14
14
|
from ..models.pwdgrp_cgi import SecondaryGroup
|
|
15
|
+
from .aiohttp_digest import AiohttpDigestAuth
|
|
15
16
|
from .api_discovery import ApiDiscoveryHandler
|
|
16
17
|
from .api_handler import ApiHandler, HandlerGroup
|
|
17
18
|
from .applications import ApplicationsHandler
|
|
@@ -56,6 +57,7 @@ class Vapix:
|
|
|
56
57
|
self.device = device
|
|
57
58
|
self._http_client = self._client_name()
|
|
58
59
|
self._aiohttp_digest_middleware: Any | None = None
|
|
60
|
+
self._aiohttp_digest_auth = AiohttpDigestAuth(device)
|
|
59
61
|
|
|
60
62
|
if self._http_client == "aiohttp":
|
|
61
63
|
if device.config.auth_scheme == AuthScheme.BASIC:
|
|
@@ -445,6 +447,16 @@ class Vapix:
|
|
|
445
447
|
content if content is not None else data
|
|
446
448
|
)
|
|
447
449
|
session = self._aiohttp_session()
|
|
450
|
+
|
|
451
|
+
if (
|
|
452
|
+
self._http_client == "aiohttp"
|
|
453
|
+
and not self._aiohttp_auth()
|
|
454
|
+
and self.device.config.auth_scheme != AuthScheme.BASIC
|
|
455
|
+
):
|
|
456
|
+
return await self._aiohttp_digest_auth.perform_request(
|
|
457
|
+
session, method, url, request_data, headers, params
|
|
458
|
+
)
|
|
459
|
+
|
|
448
460
|
request_kwargs: dict[str, Any] = {
|
|
449
461
|
"data": request_data,
|
|
450
462
|
"headers": headers,
|
|
@@ -18,16 +18,22 @@ LOGGER = logging.getLogger(__name__)
|
|
|
18
18
|
class ApiId(enum.StrEnum):
|
|
19
19
|
"""The API discovery ID."""
|
|
20
20
|
|
|
21
|
+
AIR_QUALITY = "airquality"
|
|
21
22
|
ANALYTICS_METADATA_CONFIG = "analytics-metadata-config"
|
|
22
23
|
API_DISCOVERY = "api-discovery"
|
|
24
|
+
APPLICATION = "application"
|
|
23
25
|
AUDIO_ANALYTICS = "audio-analytics"
|
|
24
26
|
AUDIO_DEVICE_CONTROL = "audio-device-control"
|
|
25
27
|
AUDIO_MIXER = "audio-mixer"
|
|
26
28
|
AUDIO_STREAMING_CAPABILITIES = "audio-streaming-capabilities"
|
|
29
|
+
AUDIT_LOG = "audit-log"
|
|
27
30
|
BASIC_DEVICE_INFO = "basic-device-info"
|
|
28
31
|
CAPTURE_MODE = "capture-mode"
|
|
29
32
|
CUSTOM_HTTP_HEADER = "customhttpheader"
|
|
30
33
|
CUSTOM_FIRMWARE_CERTIFICATE = "custom-firmware-certificate"
|
|
34
|
+
DAY_NIGHT = "daynight"
|
|
35
|
+
DEVICE_CONFIG = "device-config"
|
|
36
|
+
DEVICE_SELF_TEST = "device-self-test"
|
|
31
37
|
DISK_MANAGEMENT = "disk-management"
|
|
32
38
|
DISK_NETWORK_SHARE = "disk-network-share"
|
|
33
39
|
DISK_PROPERTIES = "disk-properties"
|
|
@@ -40,6 +46,7 @@ class ApiId(enum.StrEnum):
|
|
|
40
46
|
IMAGE_SIZE = "image-size"
|
|
41
47
|
IO_PORT_MANAGEMENT = "io-port-management"
|
|
42
48
|
LIGHT_CONTROL = "light-control"
|
|
49
|
+
MEDIA_CGI = "media-cgi"
|
|
43
50
|
MEDIA_CLIP = "mediaclip"
|
|
44
51
|
MDNS_SD = "mdnssd"
|
|
45
52
|
MQTT_CLIENT = "mqtt-client"
|
|
@@ -48,10 +55,12 @@ class ApiId(enum.StrEnum):
|
|
|
48
55
|
NTP = "ntp"
|
|
49
56
|
OAK = "oak"
|
|
50
57
|
ON_SCREEN_CONTROLS = "onscreencontrols"
|
|
58
|
+
OPTICS_CONTROL = "optics-control"
|
|
51
59
|
OVERLAY_IMAGE = "overlayimage"
|
|
52
60
|
PACKAGE_MANAGER = "packagemanager"
|
|
53
61
|
PARAM_CGI = "param-cgi"
|
|
54
62
|
PIR_SENSOR_CONFIGURATION = "pir-sensor-configuration"
|
|
63
|
+
POWER_SETTINGS = "power-settings"
|
|
55
64
|
PRIVACY_MASK = "privacy-mask"
|
|
56
65
|
PTZ_CONTROL = "ptz-control"
|
|
57
66
|
RECORDING = "recording"
|
|
@@ -61,17 +70,23 @@ class ApiId(enum.StrEnum):
|
|
|
61
70
|
REMOTE_SERVICE = "remoteservice"
|
|
62
71
|
REMOTE_SYSLOG = "remote-syslog"
|
|
63
72
|
RTSP_OVER_WEBSOCKET = "rtsp-over-websocket"
|
|
73
|
+
SERIAL_PORT = "serial-port"
|
|
64
74
|
SHUTTERGAIN_CGI = "shuttergain-cgi"
|
|
65
75
|
SIGNED_VIDEO = "signed-video"
|
|
66
76
|
SIP = "sip"
|
|
67
77
|
SSH = "ssh"
|
|
78
|
+
STRAIGHTEN_IMAGE = "straightenimage"
|
|
68
79
|
STREAM_PROFILES = "stream-profiles"
|
|
80
|
+
STREAM_STATUS = "streamstatus"
|
|
81
|
+
SUPERVISED_IO = "supervised-io"
|
|
69
82
|
SYSTEM_READY = "systemready"
|
|
70
83
|
TEMPERATURE_CONTROL = "temperaturecontrol"
|
|
71
84
|
TIME_SERVICE = "time-service"
|
|
72
85
|
UPNP = "upnp"
|
|
73
86
|
USER_MANAGEMENT = "user-management"
|
|
87
|
+
VIDEO_STREAMING_INDICATOR = "video-streaming-indicator"
|
|
74
88
|
VIEW_AREA = "view-area"
|
|
89
|
+
WIDGET_OVERLAY = "widget-overlay"
|
|
75
90
|
ZIP_STREAM = "zipstream"
|
|
76
91
|
|
|
77
92
|
UNKNOWN = "unknown"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: axis
|
|
3
|
-
Version:
|
|
3
|
+
Version: 69
|
|
4
4
|
Summary: A Python library for communicating with devices from Axis Communications
|
|
5
5
|
Author-email: Robert Svensson <Kane610@users.noreply.github.com>
|
|
6
6
|
License: MIT
|
|
@@ -27,19 +27,19 @@ Provides-Extra: requirements
|
|
|
27
27
|
Requires-Dist: aiohttp==3.13.5; extra == "requirements"
|
|
28
28
|
Requires-Dist: httpx==0.28.1; extra == "requirements"
|
|
29
29
|
Requires-Dist: orjson==3.11.8; extra == "requirements"
|
|
30
|
-
Requires-Dist: packaging==26.
|
|
30
|
+
Requires-Dist: packaging==26.2; extra == "requirements"
|
|
31
31
|
Requires-Dist: xmltodict==1.0.4; extra == "requirements"
|
|
32
32
|
Provides-Extra: requirements-test
|
|
33
|
-
Requires-Dist: mypy==1.20.
|
|
34
|
-
Requires-Dist: pytest==9.0.
|
|
33
|
+
Requires-Dist: mypy==1.20.2; extra == "requirements-test"
|
|
34
|
+
Requires-Dist: pytest==9.0.3; extra == "requirements-test"
|
|
35
35
|
Requires-Dist: pytest-aiohttp==1.1.0; extra == "requirements-test"
|
|
36
36
|
Requires-Dist: pytest-asyncio==1.3.0; extra == "requirements-test"
|
|
37
37
|
Requires-Dist: pytest-cov==7.1.0; extra == "requirements-test"
|
|
38
|
-
Requires-Dist: respx==0.
|
|
39
|
-
Requires-Dist: ruff==0.15.
|
|
40
|
-
Requires-Dist: types-xmltodict==v1.0.1.
|
|
38
|
+
Requires-Dist: respx==0.23.1; extra == "requirements-test"
|
|
39
|
+
Requires-Dist: ruff==0.15.12; extra == "requirements-test"
|
|
40
|
+
Requires-Dist: types-xmltodict==v1.0.1.20260408; extra == "requirements-test"
|
|
41
41
|
Provides-Extra: requirements-dev
|
|
42
|
-
Requires-Dist: pre-commit==4.
|
|
42
|
+
Requires-Dist: pre-commit==4.6.0; extra == "requirements-dev"
|
|
43
43
|
Dynamic: license-file
|
|
44
44
|
|
|
45
45
|
# axis
|
|
@@ -17,6 +17,7 @@ axis.egg-info/entry_points.txt
|
|
|
17
17
|
axis.egg-info/requires.txt
|
|
18
18
|
axis.egg-info/top_level.txt
|
|
19
19
|
axis/interfaces/__init__.py
|
|
20
|
+
axis/interfaces/aiohttp_digest.py
|
|
20
21
|
axis/interfaces/api_discovery.py
|
|
21
22
|
axis/interfaces/api_handler.py
|
|
22
23
|
axis/interfaces/basic_device_info.py
|
|
@@ -9,18 +9,18 @@ xmltodict>=0.13.0
|
|
|
9
9
|
aiohttp==3.13.5
|
|
10
10
|
httpx==0.28.1
|
|
11
11
|
orjson==3.11.8
|
|
12
|
-
packaging==26.
|
|
12
|
+
packaging==26.2
|
|
13
13
|
xmltodict==1.0.4
|
|
14
14
|
|
|
15
15
|
[requirements-dev]
|
|
16
|
-
pre-commit==4.
|
|
16
|
+
pre-commit==4.6.0
|
|
17
17
|
|
|
18
18
|
[requirements-test]
|
|
19
|
-
mypy==1.20.
|
|
20
|
-
pytest==9.0.
|
|
19
|
+
mypy==1.20.2
|
|
20
|
+
pytest==9.0.3
|
|
21
21
|
pytest-aiohttp==1.1.0
|
|
22
22
|
pytest-asyncio==1.3.0
|
|
23
23
|
pytest-cov==7.1.0
|
|
24
|
-
respx==0.
|
|
25
|
-
ruff==0.15.
|
|
26
|
-
types-xmltodict==v1.0.1.
|
|
24
|
+
respx==0.23.1
|
|
25
|
+
ruff==0.15.12
|
|
26
|
+
types-xmltodict==v1.0.1.20260408
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
[build-system]
|
|
2
|
-
requires = ["setuptools==82.0.1", "wheel==0.
|
|
2
|
+
requires = ["setuptools==82.0.1", "wheel==0.47.0"]
|
|
3
3
|
build-backend = "setuptools.build_meta"
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "axis"
|
|
7
|
-
version = "
|
|
7
|
+
version = "69"
|
|
8
8
|
license = {text = "MIT"}
|
|
9
9
|
description = "A Python library for communicating with devices from Axis Communications"
|
|
10
10
|
readme = "README.md"
|
|
@@ -33,21 +33,21 @@ requirements = [
|
|
|
33
33
|
"aiohttp==3.13.5",
|
|
34
34
|
"httpx==0.28.1",
|
|
35
35
|
"orjson==3.11.8",
|
|
36
|
-
"packaging==26.
|
|
36
|
+
"packaging==26.2",
|
|
37
37
|
"xmltodict==1.0.4",
|
|
38
38
|
]
|
|
39
39
|
requirements-test = [
|
|
40
|
-
"mypy==1.20.
|
|
41
|
-
"pytest==9.0.
|
|
40
|
+
"mypy==1.20.2",
|
|
41
|
+
"pytest==9.0.3",
|
|
42
42
|
"pytest-aiohttp==1.1.0",
|
|
43
43
|
"pytest-asyncio==1.3.0",
|
|
44
44
|
"pytest-cov==7.1.0",
|
|
45
|
-
"respx==0.
|
|
46
|
-
"ruff==0.15.
|
|
47
|
-
"types-xmltodict==v1.0.1.
|
|
45
|
+
"respx==0.23.1",
|
|
46
|
+
"ruff==0.15.12",
|
|
47
|
+
"types-xmltodict==v1.0.1.20260408",
|
|
48
48
|
]
|
|
49
49
|
requirements-dev = [
|
|
50
|
-
"pre-commit==4.
|
|
50
|
+
"pre-commit==4.6.0"
|
|
51
51
|
]
|
|
52
52
|
|
|
53
53
|
[project.urls]
|