axis 64__tar.gz → 66__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-64 → axis-66}/PKG-INFO +19 -17
- {axis-64 → axis-66}/axis/__main__.py +14 -6
- {axis-64 → axis-66}/axis/interfaces/parameters/param_cgi.py +15 -2
- {axis-64 → axis-66}/axis/interfaces/vapix.py +7 -10
- {axis-64 → axis-66}/axis/models/configuration.py +1 -0
- {axis-64 → axis-66}/axis/models/event.py +7 -2
- {axis-64 → axis-66}/axis/models/event_instance.py +2 -2
- {axis-64 → axis-66}/axis/models/parameters/stream_profile.py +1 -1
- {axis-64 → axis-66}/axis/rtsp.py +2 -4
- {axis-64 → axis-66}/axis/stream_manager.py +4 -3
- {axis-64 → axis-66}/axis.egg-info/PKG-INFO +19 -17
- axis-66/axis.egg-info/requires.txt +24 -0
- {axis-64 → axis-66}/pyproject.toml +21 -21
- {axis-64 → axis-66}/tests/test_configuration.py +2 -0
- {axis-64 → axis-66}/tests/test_port_cgi.py +5 -5
- {axis-64 → axis-66}/tests/test_rtsp.py +1 -1
- {axis-64 → axis-66}/tests/test_stream_manager.py +24 -0
- {axis-64 → axis-66}/tests/test_vapix.py +58 -17
- axis-64/axis.egg-info/requires.txt +0 -23
- {axis-64 → axis-66}/LICENSE +0 -0
- {axis-64 → axis-66}/README.md +0 -0
- {axis-64 → axis-66}/axis/__init__.py +0 -0
- {axis-64 → axis-66}/axis/device.py +0 -0
- {axis-64 → axis-66}/axis/errors.py +0 -0
- {axis-64 → axis-66}/axis/interfaces/__init__.py +0 -0
- {axis-64 → axis-66}/axis/interfaces/api_discovery.py +0 -0
- {axis-64 → axis-66}/axis/interfaces/api_handler.py +0 -0
- {axis-64 → axis-66}/axis/interfaces/applications/__init__.py +0 -0
- {axis-64 → axis-66}/axis/interfaces/applications/application_handler.py +0 -0
- {axis-64 → axis-66}/axis/interfaces/applications/applications.py +0 -0
- {axis-64 → axis-66}/axis/interfaces/applications/fence_guard.py +0 -0
- {axis-64 → axis-66}/axis/interfaces/applications/loitering_guard.py +0 -0
- {axis-64 → axis-66}/axis/interfaces/applications/motion_guard.py +0 -0
- {axis-64 → axis-66}/axis/interfaces/applications/object_analytics.py +0 -0
- {axis-64 → axis-66}/axis/interfaces/applications/vmd4.py +0 -0
- {axis-64 → axis-66}/axis/interfaces/basic_device_info.py +0 -0
- {axis-64 → axis-66}/axis/interfaces/event_instances.py +0 -0
- {axis-64 → axis-66}/axis/interfaces/event_manager.py +0 -0
- {axis-64 → axis-66}/axis/interfaces/light_control.py +0 -0
- {axis-64 → axis-66}/axis/interfaces/mqtt.py +0 -0
- {axis-64 → axis-66}/axis/interfaces/parameters/__init__.py +0 -0
- {axis-64 → axis-66}/axis/interfaces/parameters/brand.py +0 -0
- {axis-64 → axis-66}/axis/interfaces/parameters/image.py +0 -0
- {axis-64 → axis-66}/axis/interfaces/parameters/io_port.py +0 -0
- {axis-64 → axis-66}/axis/interfaces/parameters/param_handler.py +0 -0
- {axis-64 → axis-66}/axis/interfaces/parameters/properties.py +0 -0
- {axis-64 → axis-66}/axis/interfaces/parameters/ptz.py +0 -0
- {axis-64 → axis-66}/axis/interfaces/parameters/stream_profile.py +0 -0
- {axis-64 → axis-66}/axis/interfaces/pir_sensor_configuration.py +0 -0
- {axis-64 → axis-66}/axis/interfaces/port_cgi.py +0 -0
- {axis-64 → axis-66}/axis/interfaces/port_management.py +0 -0
- {axis-64 → axis-66}/axis/interfaces/ptz.py +0 -0
- {axis-64 → axis-66}/axis/interfaces/pwdgrp_cgi.py +0 -0
- {axis-64 → axis-66}/axis/interfaces/stream_profiles.py +0 -0
- {axis-64 → axis-66}/axis/interfaces/user_groups.py +0 -0
- {axis-64 → axis-66}/axis/interfaces/view_areas.py +0 -0
- {axis-64 → axis-66}/axis/models/__init__.py +0 -0
- {axis-64 → axis-66}/axis/models/api.py +0 -0
- {axis-64 → axis-66}/axis/models/api_discovery.py +0 -0
- {axis-64 → axis-66}/axis/models/applications/__init__.py +0 -0
- {axis-64 → axis-66}/axis/models/applications/application.py +0 -0
- {axis-64 → axis-66}/axis/models/applications/fence_guard.py +0 -0
- {axis-64 → axis-66}/axis/models/applications/loitering_guard.py +0 -0
- {axis-64 → axis-66}/axis/models/applications/motion_guard.py +0 -0
- {axis-64 → axis-66}/axis/models/applications/object_analytics.py +0 -0
- {axis-64 → axis-66}/axis/models/applications/vmd4.py +0 -0
- {axis-64 → axis-66}/axis/models/basic_device_info.py +0 -0
- {axis-64 → axis-66}/axis/models/light_control.py +0 -0
- {axis-64 → axis-66}/axis/models/mqtt.py +0 -0
- {axis-64 → axis-66}/axis/models/parameters/__init__.py +0 -0
- {axis-64 → axis-66}/axis/models/parameters/brand.py +0 -0
- {axis-64 → axis-66}/axis/models/parameters/image.py +0 -0
- {axis-64 → axis-66}/axis/models/parameters/io_port.py +0 -0
- {axis-64 → axis-66}/axis/models/parameters/param_cgi.py +0 -0
- {axis-64 → axis-66}/axis/models/parameters/properties.py +0 -0
- {axis-64 → axis-66}/axis/models/parameters/ptz.py +0 -0
- {axis-64 → axis-66}/axis/models/pir_sensor_configuration.py +0 -0
- {axis-64 → axis-66}/axis/models/port_cgi.py +0 -0
- {axis-64 → axis-66}/axis/models/port_management.py +0 -0
- {axis-64 → axis-66}/axis/models/ptz_cgi.py +0 -0
- {axis-64 → axis-66}/axis/models/pwdgrp_cgi.py +0 -0
- {axis-64 → axis-66}/axis/models/stream_profile.py +0 -0
- {axis-64 → axis-66}/axis/models/user_group.py +0 -0
- {axis-64 → axis-66}/axis/models/view_area.py +0 -0
- {axis-64 → axis-66}/axis/py.typed +0 -0
- {axis-64 → axis-66}/axis.egg-info/SOURCES.txt +0 -0
- {axis-64 → axis-66}/axis.egg-info/dependency_links.txt +0 -0
- {axis-64 → axis-66}/axis.egg-info/entry_points.txt +0 -0
- {axis-64 → axis-66}/axis.egg-info/top_level.txt +0 -0
- {axis-64 → axis-66}/setup.cfg +0 -0
- {axis-64 → axis-66}/tests/test_api_discovery.py +0 -0
- {axis-64 → axis-66}/tests/test_api_handler.py +0 -0
- {axis-64 → axis-66}/tests/test_basic_device_info.py +0 -0
- {axis-64 → axis-66}/tests/test_device.py +0 -0
- {axis-64 → axis-66}/tests/test_event.py +0 -0
- {axis-64 → axis-66}/tests/test_event_instances.py +0 -0
- {axis-64 → axis-66}/tests/test_event_stream.py +0 -0
- {axis-64 → axis-66}/tests/test_light_control.py +0 -0
- {axis-64 → axis-66}/tests/test_mqtt.py +0 -0
- {axis-64 → axis-66}/tests/test_pir_sensor_configuration.py +0 -0
- {axis-64 → axis-66}/tests/test_port_management.py +0 -0
- {axis-64 → axis-66}/tests/test_ptz.py +0 -0
- {axis-64 → axis-66}/tests/test_pwdgrp_cgi.py +0 -0
- {axis-64 → axis-66}/tests/test_stream_profiles.py +0 -0
- {axis-64 → axis-66}/tests/test_user_groups.py +0 -0
- {axis-64 → axis-66}/tests/test_view_areas.py +0 -0
{axis-64 → axis-66}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: axis
|
|
3
|
-
Version:
|
|
3
|
+
Version: 66
|
|
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
|
|
@@ -12,30 +12,32 @@ Classifier: Development Status :: 5 - Production/Stable
|
|
|
12
12
|
Classifier: Intended Audience :: Developers
|
|
13
13
|
Classifier: License :: OSI Approved :: MIT License
|
|
14
14
|
Classifier: Operating System :: OS Independent
|
|
15
|
-
Classifier: Programming Language :: Python :: 3.
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
16
|
Classifier: Topic :: Home Automation
|
|
17
|
-
Requires-Python: >=3.
|
|
17
|
+
Requires-Python: >=3.13.0
|
|
18
18
|
Description-Content-Type: text/markdown
|
|
19
19
|
License-File: LICENSE
|
|
20
|
+
Requires-Dist: faust-cchardet>=2.1.18
|
|
20
21
|
Requires-Dist: httpx>=0.26
|
|
21
22
|
Requires-Dist: orjson>3.9
|
|
22
23
|
Requires-Dist: packaging>23
|
|
23
24
|
Requires-Dist: xmltodict>=0.13.0
|
|
24
25
|
Provides-Extra: requirements
|
|
25
|
-
Requires-Dist: httpx==0.
|
|
26
|
-
Requires-Dist: orjson==3.
|
|
27
|
-
Requires-Dist: packaging==
|
|
28
|
-
Requires-Dist: xmltodict==0.
|
|
26
|
+
Requires-Dist: httpx==0.28.1; extra == "requirements"
|
|
27
|
+
Requires-Dist: orjson==3.11.5; extra == "requirements"
|
|
28
|
+
Requires-Dist: packaging==25.0; extra == "requirements"
|
|
29
|
+
Requires-Dist: xmltodict==1.0.2; extra == "requirements"
|
|
29
30
|
Provides-Extra: requirements-test
|
|
30
|
-
Requires-Dist: mypy==1.
|
|
31
|
-
Requires-Dist: pytest==
|
|
32
|
-
Requires-Dist: pytest-aiohttp==1.0
|
|
33
|
-
Requires-Dist: pytest-asyncio==
|
|
34
|
-
Requires-Dist: pytest-cov==
|
|
35
|
-
Requires-Dist: respx==0.
|
|
36
|
-
Requires-Dist: ruff==0.
|
|
37
|
-
Requires-Dist: types-xmltodict==
|
|
31
|
+
Requires-Dist: mypy==1.19.1; extra == "requirements-test"
|
|
32
|
+
Requires-Dist: pytest==9.0.2; extra == "requirements-test"
|
|
33
|
+
Requires-Dist: pytest-aiohttp==1.1.0; extra == "requirements-test"
|
|
34
|
+
Requires-Dist: pytest-asyncio==1.3.0; extra == "requirements-test"
|
|
35
|
+
Requires-Dist: pytest-cov==7.0.0; extra == "requirements-test"
|
|
36
|
+
Requires-Dist: respx==0.22.0; extra == "requirements-test"
|
|
37
|
+
Requires-Dist: ruff==0.14.10; extra == "requirements-test"
|
|
38
|
+
Requires-Dist: types-xmltodict==v1.0.1.20250920; extra == "requirements-test"
|
|
38
39
|
Provides-Extra: requirements-dev
|
|
39
|
-
Requires-Dist: pre-commit==4.
|
|
40
|
+
Requires-Dist: pre-commit==4.5.1; extra == "requirements-dev"
|
|
41
|
+
Dynamic: license-file
|
|
40
42
|
|
|
41
43
|
Python project to set up a connection towards Axis Communications devices and to subscribe to specific events on the metadatastream.
|
|
@@ -7,23 +7,31 @@ import logging
|
|
|
7
7
|
from httpx import AsyncClient
|
|
8
8
|
|
|
9
9
|
import axis
|
|
10
|
+
from axis.device import AxisDevice
|
|
11
|
+
from axis.models.configuration import Configuration
|
|
12
|
+
from axis.models.event import Event
|
|
10
13
|
|
|
11
14
|
LOGGER = logging.getLogger(__name__)
|
|
12
15
|
|
|
13
16
|
|
|
14
|
-
def event_handler(event:
|
|
17
|
+
def event_handler(event: Event) -> None:
|
|
15
18
|
"""Receive and print events from RTSP stream."""
|
|
16
19
|
LOGGER.info(event)
|
|
17
20
|
|
|
18
21
|
|
|
19
22
|
async def axis_device(
|
|
20
|
-
host: str, port: int, username: str, password: str
|
|
21
|
-
) ->
|
|
23
|
+
host: str, port: int, username: str, password: str, is_companion: bool = False
|
|
24
|
+
) -> AxisDevice:
|
|
22
25
|
"""Create a Axis device."""
|
|
23
26
|
session = AsyncClient(verify=False) # noqa: S501
|
|
24
|
-
device =
|
|
25
|
-
|
|
26
|
-
session,
|
|
27
|
+
device = AxisDevice(
|
|
28
|
+
Configuration(
|
|
29
|
+
session,
|
|
30
|
+
host,
|
|
31
|
+
port=port,
|
|
32
|
+
username=username,
|
|
33
|
+
password=password,
|
|
34
|
+
is_companion=is_companion,
|
|
27
35
|
)
|
|
28
36
|
)
|
|
29
37
|
|
|
@@ -1,7 +1,19 @@
|
|
|
1
1
|
"""Axis Vapix parameter management."""
|
|
2
2
|
|
|
3
3
|
from collections.abc import Sequence
|
|
4
|
-
from typing import TYPE_CHECKING, Any
|
|
4
|
+
from typing import TYPE_CHECKING, Any, TypedDict
|
|
5
|
+
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
|
|
8
|
+
class _DetectResultType(TypedDict):
|
|
9
|
+
encoding: str
|
|
10
|
+
confidence: float
|
|
11
|
+
|
|
12
|
+
def detect(byte_str: bytes | bytearray) -> _DetectResultType:
|
|
13
|
+
"""Typed interface for chardet detect method."""
|
|
14
|
+
...
|
|
15
|
+
else:
|
|
16
|
+
from cchardet import detect
|
|
5
17
|
|
|
6
18
|
from ...models.api_discovery import ApiId
|
|
7
19
|
from ...models.parameters.param_cgi import ParameterGroup, ParamRequest, params_to_dict
|
|
@@ -36,7 +48,8 @@ class Params(ApiHandler[Any]):
|
|
|
36
48
|
async def _api_request(self, group: ParameterGroup | None = None) -> dict[str, Any]:
|
|
37
49
|
"""Fetch parameter data and convert it into a dictionary."""
|
|
38
50
|
bytes_data = await self.vapix.api_request(ParamRequest(group))
|
|
39
|
-
|
|
51
|
+
encoding = detect(bytes_data)["encoding"] or "utf-8"
|
|
52
|
+
return params_to_dict(bytes_data.decode(encoding=encoding)).get("root") or {}
|
|
40
53
|
|
|
41
54
|
async def _update(self, group: ParameterGroup | None = None) -> Sequence[str]:
|
|
42
55
|
"""Request parameter data and update items."""
|
|
@@ -11,17 +11,11 @@ import httpx
|
|
|
11
11
|
from ..errors import RequestError, raise_error
|
|
12
12
|
from ..models.pwdgrp_cgi import SecondaryGroup
|
|
13
13
|
from .api_discovery import ApiDiscoveryHandler
|
|
14
|
-
from .applications import
|
|
15
|
-
ApplicationsHandler,
|
|
16
|
-
)
|
|
14
|
+
from .applications import ApplicationsHandler
|
|
17
15
|
from .applications.fence_guard import FenceGuardHandler
|
|
18
|
-
from .applications.loitering_guard import
|
|
19
|
-
LoiteringGuardHandler,
|
|
20
|
-
)
|
|
16
|
+
from .applications.loitering_guard import LoiteringGuardHandler
|
|
21
17
|
from .applications.motion_guard import MotionGuardHandler
|
|
22
|
-
from .applications.object_analytics import
|
|
23
|
-
ObjectAnalyticsHandler,
|
|
24
|
-
)
|
|
18
|
+
from .applications.object_analytics import ObjectAnalyticsHandler
|
|
25
19
|
from .applications.vmd4 import Vmd4Handler
|
|
26
20
|
from .basic_device_info import BasicDeviceInfoHandler
|
|
27
21
|
from .event_instances import EventInstanceHandler
|
|
@@ -242,13 +236,16 @@ class Vapix:
|
|
|
242
236
|
|
|
243
237
|
async def api_request(self, api_request: ApiRequest) -> bytes:
|
|
244
238
|
"""Make a request to the device."""
|
|
239
|
+
params = api_request.params or {}
|
|
240
|
+
if self.device.config.is_companion:
|
|
241
|
+
params["Axis-Orig-Sw"] = "true"
|
|
245
242
|
return await self.request(
|
|
246
243
|
method=api_request.method,
|
|
247
244
|
path=api_request.path,
|
|
248
245
|
content=api_request.content,
|
|
249
246
|
data=api_request.data,
|
|
250
247
|
headers=api_request.headers,
|
|
251
|
-
params=
|
|
248
|
+
params=params,
|
|
252
249
|
)
|
|
253
250
|
|
|
254
251
|
async def request(
|
|
@@ -195,10 +195,15 @@ class Event:
|
|
|
195
195
|
data,
|
|
196
196
|
# attr_prefix="",
|
|
197
197
|
process_namespaces=True,
|
|
198
|
-
namespaces=XML_NAMESPACES,
|
|
198
|
+
namespaces=XML_NAMESPACES, # type: ignore[arg-type]
|
|
199
199
|
)
|
|
200
200
|
|
|
201
|
-
|
|
201
|
+
# Normalize the ONVIF metadata root: always use a dict, drop any stray
|
|
202
|
+
# XML namespace attribute ("@xmlns") added by xmltodict, and bail out
|
|
203
|
+
# early if the payload is empty.
|
|
204
|
+
stream = raw.get("MetadataStream") or {}
|
|
205
|
+
stream.pop("@xmlns", None)
|
|
206
|
+
if not stream:
|
|
202
207
|
return cls._decode_from_dict({})
|
|
203
208
|
|
|
204
209
|
topic = traverse(raw, TOPIC)
|
|
@@ -44,7 +44,7 @@ def get_events(data: dict[str, Any]) -> list[dict[str, Any]]:
|
|
|
44
44
|
event_list = get_events(value) # Recursive call
|
|
45
45
|
|
|
46
46
|
for event in event_list:
|
|
47
|
-
event["topic"] = f
|
|
47
|
+
event["topic"] = f"{key}/{event['topic']}" # Compose the topic
|
|
48
48
|
events.append(event)
|
|
49
49
|
|
|
50
50
|
return events
|
|
@@ -166,7 +166,7 @@ class ListEventInstancesResponse(ApiResponse[dict[str, Any]]):
|
|
|
166
166
|
bytes_data,
|
|
167
167
|
# attr_prefix="",
|
|
168
168
|
dict_constructor=dict, # Use dict rather than ordered_dict
|
|
169
|
-
namespaces=NAMESPACES, # Replace or remove defined namespaces
|
|
169
|
+
namespaces=NAMESPACES, # type: ignore[arg-type] # Replace or remove defined namespaces
|
|
170
170
|
process_namespaces=True,
|
|
171
171
|
)
|
|
172
172
|
raw_events = traverse(data, EVENT_INSTANCE) # Move past the irrelevant keys
|
|
@@ -51,7 +51,7 @@ class StreamProfileParam(ParamItem):
|
|
|
51
51
|
description=profile["Description"],
|
|
52
52
|
parameters=profile["Parameters"],
|
|
53
53
|
)
|
|
54
|
-
for profile in cast(dict[str, ProfileParamT], raw_profiles).values()
|
|
54
|
+
for profile in cast("dict[str, ProfileParamT]", raw_profiles).values()
|
|
55
55
|
]
|
|
56
56
|
|
|
57
57
|
return cls(
|
{axis-64 → axis-66}/axis/rtsp.py
RENAMED
|
@@ -5,9 +5,11 @@
|
|
|
5
5
|
# https://github.com/perexg/satip-axe/blob/master/tools/multicast-rtp
|
|
6
6
|
|
|
7
7
|
import asyncio
|
|
8
|
+
from base64 import b64encode
|
|
8
9
|
from collections import deque
|
|
9
10
|
from collections.abc import Callable
|
|
10
11
|
import enum
|
|
12
|
+
from hashlib import md5
|
|
11
13
|
import logging
|
|
12
14
|
import socket
|
|
13
15
|
from typing import Any
|
|
@@ -372,8 +374,6 @@ class RTSPSession:
|
|
|
372
374
|
|
|
373
375
|
def generate_digest(self) -> str:
|
|
374
376
|
"""RFC 2617."""
|
|
375
|
-
from hashlib import md5
|
|
376
|
-
|
|
377
377
|
_ha1 = f"{self.username}:{self.realm}:{self.password}"
|
|
378
378
|
ha1 = md5(_ha1.encode("UTF-8")).hexdigest()
|
|
379
379
|
_ha2 = f"{self.method}:{self.url}"
|
|
@@ -392,8 +392,6 @@ class RTSPSession:
|
|
|
392
392
|
|
|
393
393
|
def generate_basic(self) -> str:
|
|
394
394
|
"""RFC 2617."""
|
|
395
|
-
from base64 import b64encode
|
|
396
|
-
|
|
397
395
|
if not self._basic_auth:
|
|
398
396
|
creds = f"{self.username}:{self.password}"
|
|
399
397
|
self._basic_auth = "Basic "
|
|
@@ -13,9 +13,7 @@ if TYPE_CHECKING:
|
|
|
13
13
|
|
|
14
14
|
_LOGGER = logging.getLogger(__name__)
|
|
15
15
|
|
|
16
|
-
RTSP_URL =
|
|
17
|
-
"rtsp://{host}/axis-media/media.amp?video={video}&audio={audio}&event={event}"
|
|
18
|
-
)
|
|
16
|
+
RTSP_URL = "rtsp://{host}/axis-media/media.amp?video={video}&audio={audio}&event={event}{axis_orig_sw}"
|
|
19
17
|
|
|
20
18
|
RETRY_TIMER = 15
|
|
21
19
|
|
|
@@ -43,6 +41,9 @@ class StreamManager:
|
|
|
43
41
|
video=self.video_query,
|
|
44
42
|
audio=self.audio_query,
|
|
45
43
|
event=self.event_query,
|
|
44
|
+
axis_orig_sw="&Axis-Orig-Sw=true"
|
|
45
|
+
if self.device.config.is_companion
|
|
46
|
+
else "",
|
|
46
47
|
)
|
|
47
48
|
_LOGGER.debug(rtsp_url)
|
|
48
49
|
return rtsp_url
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: axis
|
|
3
|
-
Version:
|
|
3
|
+
Version: 66
|
|
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
|
|
@@ -12,30 +12,32 @@ Classifier: Development Status :: 5 - Production/Stable
|
|
|
12
12
|
Classifier: Intended Audience :: Developers
|
|
13
13
|
Classifier: License :: OSI Approved :: MIT License
|
|
14
14
|
Classifier: Operating System :: OS Independent
|
|
15
|
-
Classifier: Programming Language :: Python :: 3.
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
16
|
Classifier: Topic :: Home Automation
|
|
17
|
-
Requires-Python: >=3.
|
|
17
|
+
Requires-Python: >=3.13.0
|
|
18
18
|
Description-Content-Type: text/markdown
|
|
19
19
|
License-File: LICENSE
|
|
20
|
+
Requires-Dist: faust-cchardet>=2.1.18
|
|
20
21
|
Requires-Dist: httpx>=0.26
|
|
21
22
|
Requires-Dist: orjson>3.9
|
|
22
23
|
Requires-Dist: packaging>23
|
|
23
24
|
Requires-Dist: xmltodict>=0.13.0
|
|
24
25
|
Provides-Extra: requirements
|
|
25
|
-
Requires-Dist: httpx==0.
|
|
26
|
-
Requires-Dist: orjson==3.
|
|
27
|
-
Requires-Dist: packaging==
|
|
28
|
-
Requires-Dist: xmltodict==0.
|
|
26
|
+
Requires-Dist: httpx==0.28.1; extra == "requirements"
|
|
27
|
+
Requires-Dist: orjson==3.11.5; extra == "requirements"
|
|
28
|
+
Requires-Dist: packaging==25.0; extra == "requirements"
|
|
29
|
+
Requires-Dist: xmltodict==1.0.2; extra == "requirements"
|
|
29
30
|
Provides-Extra: requirements-test
|
|
30
|
-
Requires-Dist: mypy==1.
|
|
31
|
-
Requires-Dist: pytest==
|
|
32
|
-
Requires-Dist: pytest-aiohttp==1.0
|
|
33
|
-
Requires-Dist: pytest-asyncio==
|
|
34
|
-
Requires-Dist: pytest-cov==
|
|
35
|
-
Requires-Dist: respx==0.
|
|
36
|
-
Requires-Dist: ruff==0.
|
|
37
|
-
Requires-Dist: types-xmltodict==
|
|
31
|
+
Requires-Dist: mypy==1.19.1; extra == "requirements-test"
|
|
32
|
+
Requires-Dist: pytest==9.0.2; extra == "requirements-test"
|
|
33
|
+
Requires-Dist: pytest-aiohttp==1.1.0; extra == "requirements-test"
|
|
34
|
+
Requires-Dist: pytest-asyncio==1.3.0; extra == "requirements-test"
|
|
35
|
+
Requires-Dist: pytest-cov==7.0.0; extra == "requirements-test"
|
|
36
|
+
Requires-Dist: respx==0.22.0; extra == "requirements-test"
|
|
37
|
+
Requires-Dist: ruff==0.14.10; extra == "requirements-test"
|
|
38
|
+
Requires-Dist: types-xmltodict==v1.0.1.20250920; extra == "requirements-test"
|
|
38
39
|
Provides-Extra: requirements-dev
|
|
39
|
-
Requires-Dist: pre-commit==4.
|
|
40
|
+
Requires-Dist: pre-commit==4.5.1; extra == "requirements-dev"
|
|
41
|
+
Dynamic: license-file
|
|
40
42
|
|
|
41
43
|
Python project to set up a connection towards Axis Communications devices and to subscribe to specific events on the metadatastream.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
faust-cchardet>=2.1.18
|
|
2
|
+
httpx>=0.26
|
|
3
|
+
orjson>3.9
|
|
4
|
+
packaging>23
|
|
5
|
+
xmltodict>=0.13.0
|
|
6
|
+
|
|
7
|
+
[requirements]
|
|
8
|
+
httpx==0.28.1
|
|
9
|
+
orjson==3.11.5
|
|
10
|
+
packaging==25.0
|
|
11
|
+
xmltodict==1.0.2
|
|
12
|
+
|
|
13
|
+
[requirements-dev]
|
|
14
|
+
pre-commit==4.5.1
|
|
15
|
+
|
|
16
|
+
[requirements-test]
|
|
17
|
+
mypy==1.19.1
|
|
18
|
+
pytest==9.0.2
|
|
19
|
+
pytest-aiohttp==1.1.0
|
|
20
|
+
pytest-asyncio==1.3.0
|
|
21
|
+
pytest-cov==7.0.0
|
|
22
|
+
respx==0.22.0
|
|
23
|
+
ruff==0.14.10
|
|
24
|
+
types-xmltodict==v1.0.1.20250920
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
[build-system]
|
|
2
|
-
requires = ["setuptools==
|
|
2
|
+
requires = ["setuptools==80.9.0", "wheel==0.46.1"]
|
|
3
3
|
build-backend = "setuptools.build_meta"
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "axis"
|
|
7
|
-
version = "
|
|
7
|
+
version = "66"
|
|
8
8
|
license = {text = "MIT"}
|
|
9
9
|
description = "A Python library for communicating with devices from Axis Communications"
|
|
10
10
|
readme = "README.md"
|
|
@@ -15,11 +15,12 @@ classifiers = [
|
|
|
15
15
|
"Intended Audience :: Developers",
|
|
16
16
|
"License :: OSI Approved :: MIT License",
|
|
17
17
|
"Operating System :: OS Independent",
|
|
18
|
-
"Programming Language :: Python :: 3.
|
|
18
|
+
"Programming Language :: Python :: 3.13",
|
|
19
19
|
"Topic :: Home Automation",
|
|
20
20
|
]
|
|
21
|
-
requires-python = ">=3.
|
|
21
|
+
requires-python = ">=3.13.0"
|
|
22
22
|
dependencies = [
|
|
23
|
+
"faust-cchardet>=2.1.18",
|
|
23
24
|
"httpx>=0.26",
|
|
24
25
|
"orjson>3.9",
|
|
25
26
|
"packaging>23",
|
|
@@ -28,23 +29,23 @@ dependencies = [
|
|
|
28
29
|
|
|
29
30
|
[project.optional-dependencies]
|
|
30
31
|
requirements = [
|
|
31
|
-
"httpx==0.
|
|
32
|
-
"orjson==3.
|
|
33
|
-
"packaging==
|
|
34
|
-
"xmltodict==0.
|
|
32
|
+
"httpx==0.28.1",
|
|
33
|
+
"orjson==3.11.5",
|
|
34
|
+
"packaging==25.0",
|
|
35
|
+
"xmltodict==1.0.2",
|
|
35
36
|
]
|
|
36
37
|
requirements-test = [
|
|
37
|
-
"mypy==1.
|
|
38
|
-
"pytest==
|
|
39
|
-
"pytest-aiohttp==1.0
|
|
40
|
-
"pytest-asyncio==
|
|
41
|
-
"pytest-cov==
|
|
42
|
-
"respx==0.
|
|
43
|
-
"ruff==0.
|
|
44
|
-
"types-xmltodict==
|
|
38
|
+
"mypy==1.19.1",
|
|
39
|
+
"pytest==9.0.2",
|
|
40
|
+
"pytest-aiohttp==1.1.0",
|
|
41
|
+
"pytest-asyncio==1.3.0",
|
|
42
|
+
"pytest-cov==7.0.0",
|
|
43
|
+
"respx==0.22.0",
|
|
44
|
+
"ruff==0.14.10",
|
|
45
|
+
"types-xmltodict==v1.0.1.20250920",
|
|
45
46
|
]
|
|
46
47
|
requirements-dev = [
|
|
47
|
-
"pre-commit==4.
|
|
48
|
+
"pre-commit==4.5.1"
|
|
48
49
|
]
|
|
49
50
|
|
|
50
51
|
[project.urls]
|
|
@@ -68,7 +69,7 @@ include = ["axis*"]
|
|
|
68
69
|
"axis" = ["py.typed"]
|
|
69
70
|
|
|
70
71
|
[tool.mypy]
|
|
71
|
-
python_version = "3.
|
|
72
|
+
python_version = "3.13"
|
|
72
73
|
check_untyped_defs = true
|
|
73
74
|
disallow_any_generics = true
|
|
74
75
|
disallow_incomplete_defs = true
|
|
@@ -93,7 +94,7 @@ log_cli_level = "DEBUG"
|
|
|
93
94
|
testpaths = ["tests"]
|
|
94
95
|
|
|
95
96
|
[tool.ruff]
|
|
96
|
-
target-version = "
|
|
97
|
+
target-version = "py313"
|
|
97
98
|
lint.select = [
|
|
98
99
|
# "A", # flake8-builtins
|
|
99
100
|
"ANN", # flake8-annotations
|
|
@@ -147,8 +148,6 @@ lint.select = [
|
|
|
147
148
|
]
|
|
148
149
|
|
|
149
150
|
lint.ignore = [
|
|
150
|
-
"ANN101", # Missing type annotation for {name} in method
|
|
151
|
-
"ANN102", # Missing type annotation for {name} in classmethod
|
|
152
151
|
"ANN401", # Dynamically typed expressions (typing.Any) are disallowed in {name}
|
|
153
152
|
"COM812", # Trailing comma missing
|
|
154
153
|
"D203", # 1 blank line required before class docstring
|
|
@@ -161,6 +160,7 @@ lint.ignore = [
|
|
|
161
160
|
"PLR0915", # Too many statements ({statements} > {max_statements})
|
|
162
161
|
"PLR2004", # Magic value used in comparison, consider replacing {value} with a constant variable
|
|
163
162
|
"S324", # Probable use of insecure hash functions in {library}: {string}
|
|
163
|
+
"UP046", # 82 | class APIHandler(SubscriptionHandler, Generic[ApiItemT]):
|
|
164
164
|
]
|
|
165
165
|
|
|
166
166
|
[tool.ruff.lint.flake8-pytest-style]
|
|
@@ -28,6 +28,7 @@ def test_configuration():
|
|
|
28
28
|
assert config.web_proto == "https"
|
|
29
29
|
assert config.verify_ssl is True
|
|
30
30
|
assert config.url == "https://192.168.0.1:443"
|
|
31
|
+
assert config.is_companion is False
|
|
31
32
|
|
|
32
33
|
|
|
33
34
|
def test_minimal_configuration():
|
|
@@ -47,3 +48,4 @@ def test_minimal_configuration():
|
|
|
47
48
|
assert config.web_proto == "http"
|
|
48
49
|
assert config.verify_ssl is False
|
|
49
50
|
assert config.url == "http://192.168.1.1:80"
|
|
51
|
+
assert config.is_companion is False
|
|
@@ -20,7 +20,7 @@ def ports(axis_device) -> Ports:
|
|
|
20
20
|
async def test_ports(respx_mock, ports: Ports) -> None:
|
|
21
21
|
"""Test that different types of ports work."""
|
|
22
22
|
update_ports_route = respx_mock.post(f"http://{HOST}/axis-cgi/param.cgi").respond(
|
|
23
|
-
|
|
23
|
+
content="""root.Input.NbrOfInputs=3
|
|
24
24
|
root.IOPort.I0.Direction=input
|
|
25
25
|
root.IOPort.I0.Usage=Button
|
|
26
26
|
root.IOPort.I1.Configurable=no
|
|
@@ -44,8 +44,8 @@ root.IOPort.I3.Output.Mode=bistable
|
|
|
44
44
|
root.IOPort.I3.Output.Name=Tampering
|
|
45
45
|
root.IOPort.I3.Output.PulseTime=0
|
|
46
46
|
root.Output.NbrOfOutputs=1
|
|
47
|
-
""",
|
|
48
|
-
headers={"Content-Type": "text/plain"},
|
|
47
|
+
""".encode("iso-8859-1"),
|
|
48
|
+
headers={"Content-Type": "text/plain; charset=iso-8859-1"},
|
|
49
49
|
)
|
|
50
50
|
|
|
51
51
|
await ports.update()
|
|
@@ -99,8 +99,8 @@ root.Output.NbrOfOutputs=1
|
|
|
99
99
|
async def test_no_ports(respx_mock, ports: Ports) -> None:
|
|
100
100
|
"""Test that no ports also work."""
|
|
101
101
|
route = respx_mock.post(f"http://{HOST}/axis-cgi/param.cgi").respond(
|
|
102
|
-
|
|
103
|
-
headers={"Content-Type": "text/plain"},
|
|
102
|
+
content="".encode("iso-8859-1"),
|
|
103
|
+
headers={"Content-Type": "text/plain; charset=iso-8859-1"},
|
|
104
104
|
)
|
|
105
105
|
|
|
106
106
|
await ports.update()
|
|
@@ -674,7 +674,7 @@ def test_session_generate_digest_auth(rtsp_client):
|
|
|
674
674
|
)
|
|
675
675
|
|
|
676
676
|
|
|
677
|
-
def test_session_generate_basic_auth(
|
|
677
|
+
def test_session_generate_basic_auth(rtsp_client):
|
|
678
678
|
"""Verify generate basic auth method."""
|
|
679
679
|
session = rtsp_client.session
|
|
680
680
|
session.update('WWW-Authenticate: Basic realm="AXIS_ACCC8E012345"\r\n')
|
|
@@ -20,6 +20,12 @@ def stream_manager(axis_device) -> StreamManager:
|
|
|
20
20
|
return axis_device.stream
|
|
21
21
|
|
|
22
22
|
|
|
23
|
+
@pytest.fixture
|
|
24
|
+
def stream_manager_companion(axis_companion_device) -> StreamManager:
|
|
25
|
+
"""Return the StreamManager mock object."""
|
|
26
|
+
return axis_companion_device.stream
|
|
27
|
+
|
|
28
|
+
|
|
23
29
|
async def test_stream_url(stream_manager):
|
|
24
30
|
"""Verify stream url."""
|
|
25
31
|
assert stream_manager.video_query == 0
|
|
@@ -38,6 +44,24 @@ async def test_stream_url(stream_manager):
|
|
|
38
44
|
)
|
|
39
45
|
|
|
40
46
|
|
|
47
|
+
async def test_stream_url_companion(stream_manager_companion):
|
|
48
|
+
"""Verify stream url."""
|
|
49
|
+
assert stream_manager_companion.video_query == 0
|
|
50
|
+
assert stream_manager_companion.audio_query == 0
|
|
51
|
+
assert stream_manager_companion.event_query == "off"
|
|
52
|
+
assert (
|
|
53
|
+
stream_manager_companion.stream_url
|
|
54
|
+
== f"rtsp://{HOST}/axis-media/media.amp?video=0&audio=0&event=off&Axis-Orig-Sw=true"
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
stream_manager_companion.event = True
|
|
58
|
+
assert stream_manager_companion.event_query == "on"
|
|
59
|
+
assert (
|
|
60
|
+
stream_manager_companion.stream_url
|
|
61
|
+
== f"rtsp://{HOST}/axis-media/media.amp?video=0&audio=0&event=on&Axis-Orig-Sw=true"
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
|
|
41
65
|
@patch("axis.stream_manager.RTSPClient")
|
|
42
66
|
async def test_initialize_stream(rtsp_client, stream_manager):
|
|
43
67
|
"""Test stream commands."""
|
|
@@ -49,6 +49,12 @@ def vapix(axis_device: AxisDevice) -> Vapix:
|
|
|
49
49
|
return axis_device.vapix
|
|
50
50
|
|
|
51
51
|
|
|
52
|
+
@pytest.fixture
|
|
53
|
+
def vapix_companion_device(axis_companion_device: AxisDevice) -> Vapix:
|
|
54
|
+
"""Return the vapix object."""
|
|
55
|
+
return axis_companion_device.vapix
|
|
56
|
+
|
|
57
|
+
|
|
52
58
|
def test_vapix_not_initialized(vapix: Vapix) -> None:
|
|
53
59
|
"""Test Vapix class without initialising any data."""
|
|
54
60
|
assert dict(vapix.basic_device_info.items()) == {}
|
|
@@ -92,8 +98,10 @@ async def test_initialize(respx_mock, vapix: Vapix):
|
|
|
92
98
|
}
|
|
93
99
|
)
|
|
94
100
|
|
|
95
|
-
respx_mock.post("/axis-cgi/param.cgi").respond(
|
|
96
|
-
|
|
101
|
+
respx_mock.post("/axis-cgi/param.cgi").respond(
|
|
102
|
+
content=PARAM_CGI_RESPONSE.encode("iso-8859-1"),
|
|
103
|
+
headers={"Content-Type": "text/plain; charset=iso-8859-1"},
|
|
104
|
+
)
|
|
97
105
|
respx_mock.post("/axis-cgi/applications/list.cgi").respond(
|
|
98
106
|
text=APPLICATIONS_RESPONSE,
|
|
99
107
|
headers={"Content-Type": "text/xml"},
|
|
@@ -219,14 +227,15 @@ async def test_initialize_api_discovery_unsupported(respx_mock, vapix: Vapix):
|
|
|
219
227
|
async def test_initialize_param_cgi(respx_mock, vapix: Vapix):
|
|
220
228
|
"""Verify that you can list parameters."""
|
|
221
229
|
respx_mock.post("/axis-cgi/param.cgi").respond(
|
|
222
|
-
|
|
223
|
-
headers={"Content-Type": "text/plain"},
|
|
230
|
+
content=PARAM_CGI_RESPONSE.encode("iso-8859-1"),
|
|
231
|
+
headers={"Content-Type": "text/plain; charset=iso-8859-1"},
|
|
224
232
|
)
|
|
225
233
|
respx_mock.post("/axis-cgi/lightcontrol.cgi").respond(
|
|
226
234
|
json=LIGHT_CONTROL_RESPONSE,
|
|
227
235
|
)
|
|
228
236
|
await vapix.initialize_param_cgi()
|
|
229
237
|
|
|
238
|
+
assert "Axis-Orig-Sw" not in respx_mock.calls.last.request.url.params
|
|
230
239
|
assert vapix.firmware_version == "9.10.1"
|
|
231
240
|
assert vapix.product_number == "M1065-LW"
|
|
232
241
|
assert vapix.product_type == "Network Camera"
|
|
@@ -243,11 +252,43 @@ async def test_initialize_param_cgi(respx_mock, vapix: Vapix):
|
|
|
243
252
|
assert vapix.users.supported
|
|
244
253
|
|
|
245
254
|
|
|
255
|
+
async def test_initialize_param_cgi_for_companion_device(
|
|
256
|
+
respx_mock, vapix_companion_device: Vapix
|
|
257
|
+
):
|
|
258
|
+
"""Verify that you can list parameters."""
|
|
259
|
+
respx_mock.post("/axis-cgi/param.cgi").respond(
|
|
260
|
+
content=PARAM_CGI_RESPONSE.encode("iso-8859-1"),
|
|
261
|
+
headers={"Content-Type": "text/plain; charset=iso-8859-1"},
|
|
262
|
+
)
|
|
263
|
+
respx_mock.post("/axis-cgi/lightcontrol.cgi").respond(
|
|
264
|
+
json=LIGHT_CONTROL_RESPONSE,
|
|
265
|
+
)
|
|
266
|
+
await vapix_companion_device.initialize_param_cgi()
|
|
267
|
+
|
|
268
|
+
assert "Axis-Orig-Sw" in respx_mock.calls.last.request.url.params
|
|
269
|
+
|
|
270
|
+
assert vapix_companion_device.firmware_version == "9.10.1"
|
|
271
|
+
assert vapix_companion_device.product_number == "M1065-LW"
|
|
272
|
+
assert vapix_companion_device.product_type == "Network Camera"
|
|
273
|
+
assert vapix_companion_device.serial_number == "ACCC12345678"
|
|
274
|
+
assert len(vapix_companion_device.streaming_profiles) == 2
|
|
275
|
+
|
|
276
|
+
assert len(vapix_companion_device.basic_device_info) == 0
|
|
277
|
+
assert len(vapix_companion_device.ports.values()) == 1
|
|
278
|
+
assert len(vapix_companion_device.light_control.values()) == 1
|
|
279
|
+
assert len(vapix_companion_device.mqtt) == 0
|
|
280
|
+
assert len(vapix_companion_device.stream_profiles) == 0
|
|
281
|
+
assert len(vapix_companion_device.params.stream_profile_handler) == 1
|
|
282
|
+
|
|
283
|
+
assert vapix_companion_device.users.supported
|
|
284
|
+
|
|
285
|
+
|
|
246
286
|
async def test_initialize_params_no_data(respx_mock, vapix: Vapix):
|
|
247
287
|
"""Verify that you can list parameters."""
|
|
248
|
-
param_route = respx_mock.post(
|
|
249
|
-
"
|
|
250
|
-
|
|
288
|
+
param_route = respx_mock.post("/axis-cgi/param.cgi").respond(
|
|
289
|
+
content="".encode("iso-8859-1"),
|
|
290
|
+
headers={"Content-Type": "text/plain; charset=iso-8859-1"},
|
|
291
|
+
)
|
|
251
292
|
await vapix.initialize_param_cgi(preload_data=False)
|
|
252
293
|
|
|
253
294
|
assert param_route.call_count == 4
|
|
@@ -256,8 +297,8 @@ async def test_initialize_params_no_data(respx_mock, vapix: Vapix):
|
|
|
256
297
|
async def test_initialize_applications(respx_mock, vapix: Vapix):
|
|
257
298
|
"""Verify you can list and retrieve descriptions of applications."""
|
|
258
299
|
respx_mock.post("/axis-cgi/param.cgi").respond(
|
|
259
|
-
|
|
260
|
-
headers={"Content-Type": "text/plain"},
|
|
300
|
+
content=PARAM_CGI_RESPONSE.encode("iso-8859-1"),
|
|
301
|
+
headers={"Content-Type": "text/plain; charset=iso-8859-1"},
|
|
261
302
|
)
|
|
262
303
|
respx_mock.post("/axis-cgi/lightcontrol.cgi").respond(
|
|
263
304
|
json=LIGHT_CONTROL_RESPONSE,
|
|
@@ -296,8 +337,8 @@ async def test_initialize_applications(respx_mock, vapix: Vapix):
|
|
|
296
337
|
async def test_initialize_applications_unauthorized(respx_mock, vapix: Vapix, code):
|
|
297
338
|
"""Verify initialize applications doesnt break on too low credentials."""
|
|
298
339
|
respx_mock.post("/axis-cgi/param.cgi").respond(
|
|
299
|
-
|
|
300
|
-
headers={"Content-Type": "text/plain"},
|
|
340
|
+
content=PARAM_CGI_RESPONSE.encode("iso-8859-1"),
|
|
341
|
+
headers={"Content-Type": "text/plain; charset=iso-8859-1"},
|
|
301
342
|
)
|
|
302
343
|
respx_mock.post("/axis-cgi/lightcontrol.cgi").respond(
|
|
303
344
|
json=LIGHT_CONTROL_RESPONSE,
|
|
@@ -313,8 +354,8 @@ async def test_initialize_applications_unauthorized(respx_mock, vapix: Vapix, co
|
|
|
313
354
|
async def test_initialize_applications_not_running(respx_mock, vapix: Vapix):
|
|
314
355
|
"""Verify you can list and retrieve descriptions of applications."""
|
|
315
356
|
respx_mock.post("/axis-cgi/param.cgi").respond(
|
|
316
|
-
|
|
317
|
-
headers={"Content-Type": "text/plain"},
|
|
357
|
+
content=PARAM_CGI_RESPONSE.encode("iso-8859-1"),
|
|
358
|
+
headers={"Content-Type": "text/plain; charset=iso-8859-1"},
|
|
318
359
|
)
|
|
319
360
|
respx_mock.post("/axis-cgi/lightcontrol.cgi").respond(
|
|
320
361
|
json=LIGHT_CONTROL_RESPONSE,
|
|
@@ -351,10 +392,10 @@ async def test_initialize_event_instances(respx_mock, vapix: Vapix):
|
|
|
351
392
|
|
|
352
393
|
async def test_applications_dont_load_without_params(respx_mock, vapix: Vapix):
|
|
353
394
|
"""Applications depends on param cgi to be loaded first."""
|
|
354
|
-
param_route = respx_mock.post(
|
|
355
|
-
"
|
|
356
|
-
|
|
357
|
-
|
|
395
|
+
param_route = respx_mock.post("/axis-cgi/param.cgi").respond(
|
|
396
|
+
content="key=value".encode("iso-8859-1"),
|
|
397
|
+
headers={"Content-Type": "text/plain; charset=iso-8859-1"},
|
|
398
|
+
)
|
|
358
399
|
applications_route = respx_mock.post("/axis-cgi/applications/list.cgi")
|
|
359
400
|
|
|
360
401
|
await vapix.initialize_param_cgi(preload_data=False)
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
httpx>=0.26
|
|
2
|
-
orjson>3.9
|
|
3
|
-
packaging>23
|
|
4
|
-
xmltodict>=0.13.0
|
|
5
|
-
|
|
6
|
-
[requirements]
|
|
7
|
-
httpx==0.27.2
|
|
8
|
-
orjson==3.10.12
|
|
9
|
-
packaging==24.2
|
|
10
|
-
xmltodict==0.14.2
|
|
11
|
-
|
|
12
|
-
[requirements-dev]
|
|
13
|
-
pre-commit==4.0.1
|
|
14
|
-
|
|
15
|
-
[requirements-test]
|
|
16
|
-
mypy==1.13.0
|
|
17
|
-
pytest==8.3.4
|
|
18
|
-
pytest-aiohttp==1.0.5
|
|
19
|
-
pytest-asyncio==0.25.0
|
|
20
|
-
pytest-cov==6.0.0
|
|
21
|
-
respx==0.21.1
|
|
22
|
-
ruff==0.8.3
|
|
23
|
-
types-xmltodict==v0.14.0.20241009
|
{axis-64 → axis-66}/LICENSE
RENAMED
|
File without changes
|
{axis-64 → axis-66}/README.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{axis-64 → axis-66}/setup.cfg
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|