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.
Files changed (113) hide show
  1. {axis-68 → axis-69}/PKG-INFO +8 -8
  2. axis-69/axis/interfaces/aiohttp_digest.py +266 -0
  3. {axis-68 → axis-69}/axis/interfaces/vapix.py +12 -0
  4. {axis-68 → axis-69}/axis/models/api_discovery.py +15 -0
  5. {axis-68 → axis-69}/axis.egg-info/PKG-INFO +8 -8
  6. {axis-68 → axis-69}/axis.egg-info/SOURCES.txt +1 -0
  7. {axis-68 → axis-69}/axis.egg-info/requires.txt +7 -7
  8. {axis-68 → axis-69}/pyproject.toml +9 -9
  9. axis-69/tests/test_http_client_compat.py +393 -0
  10. axis-68/tests/test_http_client_compat.py +0 -165
  11. {axis-68 → axis-69}/LICENSE +0 -0
  12. {axis-68 → axis-69}/README.md +0 -0
  13. {axis-68 → axis-69}/axis/__init__.py +0 -0
  14. {axis-68 → axis-69}/axis/__main__.py +0 -0
  15. {axis-68 → axis-69}/axis/device.py +0 -0
  16. {axis-68 → axis-69}/axis/errors.py +0 -0
  17. {axis-68 → axis-69}/axis/interfaces/__init__.py +0 -0
  18. {axis-68 → axis-69}/axis/interfaces/api_discovery.py +0 -0
  19. {axis-68 → axis-69}/axis/interfaces/api_handler.py +0 -0
  20. {axis-68 → axis-69}/axis/interfaces/applications/__init__.py +0 -0
  21. {axis-68 → axis-69}/axis/interfaces/applications/application_handler.py +0 -0
  22. {axis-68 → axis-69}/axis/interfaces/applications/applications.py +0 -0
  23. {axis-68 → axis-69}/axis/interfaces/applications/fence_guard.py +0 -0
  24. {axis-68 → axis-69}/axis/interfaces/applications/loitering_guard.py +0 -0
  25. {axis-68 → axis-69}/axis/interfaces/applications/motion_guard.py +0 -0
  26. {axis-68 → axis-69}/axis/interfaces/applications/object_analytics.py +0 -0
  27. {axis-68 → axis-69}/axis/interfaces/applications/vmd4.py +0 -0
  28. {axis-68 → axis-69}/axis/interfaces/basic_device_info.py +0 -0
  29. {axis-68 → axis-69}/axis/interfaces/event_instances.py +0 -0
  30. {axis-68 → axis-69}/axis/interfaces/event_manager.py +0 -0
  31. {axis-68 → axis-69}/axis/interfaces/light_control.py +0 -0
  32. {axis-68 → axis-69}/axis/interfaces/mqtt.py +0 -0
  33. {axis-68 → axis-69}/axis/interfaces/parameters/__init__.py +0 -0
  34. {axis-68 → axis-69}/axis/interfaces/parameters/brand.py +0 -0
  35. {axis-68 → axis-69}/axis/interfaces/parameters/image.py +0 -0
  36. {axis-68 → axis-69}/axis/interfaces/parameters/io_port.py +0 -0
  37. {axis-68 → axis-69}/axis/interfaces/parameters/param_cgi.py +0 -0
  38. {axis-68 → axis-69}/axis/interfaces/parameters/param_handler.py +0 -0
  39. {axis-68 → axis-69}/axis/interfaces/parameters/properties.py +0 -0
  40. {axis-68 → axis-69}/axis/interfaces/parameters/ptz.py +0 -0
  41. {axis-68 → axis-69}/axis/interfaces/parameters/stream_profile.py +0 -0
  42. {axis-68 → axis-69}/axis/interfaces/pir_sensor_configuration.py +0 -0
  43. {axis-68 → axis-69}/axis/interfaces/port_cgi.py +0 -0
  44. {axis-68 → axis-69}/axis/interfaces/port_management.py +0 -0
  45. {axis-68 → axis-69}/axis/interfaces/ptz.py +0 -0
  46. {axis-68 → axis-69}/axis/interfaces/pwdgrp_cgi.py +0 -0
  47. {axis-68 → axis-69}/axis/interfaces/stream_profiles.py +0 -0
  48. {axis-68 → axis-69}/axis/interfaces/user_groups.py +0 -0
  49. {axis-68 → axis-69}/axis/interfaces/view_areas.py +0 -0
  50. {axis-68 → axis-69}/axis/models/__init__.py +0 -0
  51. {axis-68 → axis-69}/axis/models/api.py +0 -0
  52. {axis-68 → axis-69}/axis/models/applications/__init__.py +0 -0
  53. {axis-68 → axis-69}/axis/models/applications/application.py +0 -0
  54. {axis-68 → axis-69}/axis/models/applications/fence_guard.py +0 -0
  55. {axis-68 → axis-69}/axis/models/applications/loitering_guard.py +0 -0
  56. {axis-68 → axis-69}/axis/models/applications/motion_guard.py +0 -0
  57. {axis-68 → axis-69}/axis/models/applications/object_analytics.py +0 -0
  58. {axis-68 → axis-69}/axis/models/applications/vmd4.py +0 -0
  59. {axis-68 → axis-69}/axis/models/basic_device_info.py +0 -0
  60. {axis-68 → axis-69}/axis/models/configuration.py +0 -0
  61. {axis-68 → axis-69}/axis/models/event.py +0 -0
  62. {axis-68 → axis-69}/axis/models/event_instance.py +0 -0
  63. {axis-68 → axis-69}/axis/models/light_control.py +0 -0
  64. {axis-68 → axis-69}/axis/models/mqtt.py +0 -0
  65. {axis-68 → axis-69}/axis/models/parameters/__init__.py +0 -0
  66. {axis-68 → axis-69}/axis/models/parameters/brand.py +0 -0
  67. {axis-68 → axis-69}/axis/models/parameters/image.py +0 -0
  68. {axis-68 → axis-69}/axis/models/parameters/io_port.py +0 -0
  69. {axis-68 → axis-69}/axis/models/parameters/param_cgi.py +0 -0
  70. {axis-68 → axis-69}/axis/models/parameters/properties.py +0 -0
  71. {axis-68 → axis-69}/axis/models/parameters/ptz.py +0 -0
  72. {axis-68 → axis-69}/axis/models/parameters/stream_profile.py +0 -0
  73. {axis-68 → axis-69}/axis/models/pir_sensor_configuration.py +0 -0
  74. {axis-68 → axis-69}/axis/models/port_cgi.py +0 -0
  75. {axis-68 → axis-69}/axis/models/port_management.py +0 -0
  76. {axis-68 → axis-69}/axis/models/ptz_cgi.py +0 -0
  77. {axis-68 → axis-69}/axis/models/pwdgrp_cgi.py +0 -0
  78. {axis-68 → axis-69}/axis/models/stream_profile.py +0 -0
  79. {axis-68 → axis-69}/axis/models/user_group.py +0 -0
  80. {axis-68 → axis-69}/axis/models/view_area.py +0 -0
  81. {axis-68 → axis-69}/axis/py.typed +0 -0
  82. {axis-68 → axis-69}/axis/rtsp.py +0 -0
  83. {axis-68 → axis-69}/axis/stream_manager.py +0 -0
  84. {axis-68 → axis-69}/axis/stream_transport.py +0 -0
  85. {axis-68 → axis-69}/axis/websocket.py +0 -0
  86. {axis-68 → axis-69}/axis.egg-info/dependency_links.txt +0 -0
  87. {axis-68 → axis-69}/axis.egg-info/entry_points.txt +0 -0
  88. {axis-68 → axis-69}/axis.egg-info/top_level.txt +0 -0
  89. {axis-68 → axis-69}/setup.cfg +0 -0
  90. {axis-68 → axis-69}/tests/test_api_discovery.py +0 -0
  91. {axis-68 → axis-69}/tests/test_api_handler.py +0 -0
  92. {axis-68 → axis-69}/tests/test_auth_scheme.py +0 -0
  93. {axis-68 → axis-69}/tests/test_basic_device_info.py +0 -0
  94. {axis-68 → axis-69}/tests/test_configuration.py +0 -0
  95. {axis-68 → axis-69}/tests/test_device.py +0 -0
  96. {axis-68 → axis-69}/tests/test_event.py +0 -0
  97. {axis-68 → axis-69}/tests/test_event_instances.py +0 -0
  98. {axis-68 → axis-69}/tests/test_event_stream.py +0 -0
  99. {axis-68 → axis-69}/tests/test_light_control.py +0 -0
  100. {axis-68 → axis-69}/tests/test_main_http_client.py +0 -0
  101. {axis-68 → axis-69}/tests/test_mqtt.py +0 -0
  102. {axis-68 → axis-69}/tests/test_pir_sensor_configuration.py +0 -0
  103. {axis-68 → axis-69}/tests/test_port_cgi.py +0 -0
  104. {axis-68 → axis-69}/tests/test_port_management.py +0 -0
  105. {axis-68 → axis-69}/tests/test_ptz.py +0 -0
  106. {axis-68 → axis-69}/tests/test_pwdgrp_cgi.py +0 -0
  107. {axis-68 → axis-69}/tests/test_rtsp.py +0 -0
  108. {axis-68 → axis-69}/tests/test_stream_manager.py +0 -0
  109. {axis-68 → axis-69}/tests/test_stream_profiles.py +0 -0
  110. {axis-68 → axis-69}/tests/test_user_groups.py +0 -0
  111. {axis-68 → axis-69}/tests/test_vapix.py +0 -0
  112. {axis-68 → axis-69}/tests/test_view_areas.py +0 -0
  113. {axis-68 → axis-69}/tests/test_websocket.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: axis
3
- Version: 68
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.0; extra == "requirements"
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.0; extra == "requirements-test"
34
- Requires-Dist: pytest==9.0.2; extra == "requirements-test"
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.22.0; extra == "requirements-test"
39
- Requires-Dist: ruff==0.15.9; extra == "requirements-test"
40
- Requires-Dist: types-xmltodict==v1.0.1.20260113; extra == "requirements-test"
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.5.1; extra == "requirements-dev"
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: 68
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.0; extra == "requirements"
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.0; extra == "requirements-test"
34
- Requires-Dist: pytest==9.0.2; extra == "requirements-test"
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.22.0; extra == "requirements-test"
39
- Requires-Dist: ruff==0.15.9; extra == "requirements-test"
40
- Requires-Dist: types-xmltodict==v1.0.1.20260113; extra == "requirements-test"
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.5.1; extra == "requirements-dev"
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.0
12
+ packaging==26.2
13
13
  xmltodict==1.0.4
14
14
 
15
15
  [requirements-dev]
16
- pre-commit==4.5.1
16
+ pre-commit==4.6.0
17
17
 
18
18
  [requirements-test]
19
- mypy==1.20.0
20
- pytest==9.0.2
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.22.0
25
- ruff==0.15.9
26
- types-xmltodict==v1.0.1.20260113
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.46.3"]
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 = "68"
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.0",
36
+ "packaging==26.2",
37
37
  "xmltodict==1.0.4",
38
38
  ]
39
39
  requirements-test = [
40
- "mypy==1.20.0",
41
- "pytest==9.0.2",
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.22.0",
46
- "ruff==0.15.9",
47
- "types-xmltodict==v1.0.1.20260113",
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.5.1"
50
+ "pre-commit==4.6.0"
51
51
  ]
52
52
 
53
53
  [project.urls]