axis 62__tar.gz → 63__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-62 → axis-63}/PKG-INFO +1 -1
- {axis-62 → axis-63}/axis/__main__.py +2 -2
- {axis-62 → axis-63}/axis/models/event.py +8 -3
- {axis-62 → axis-63}/axis/models/ptz_cgi.py +1 -2
- {axis-62 → axis-63}/axis/rtsp.py +15 -2
- {axis-62 → axis-63}/axis.egg-info/PKG-INFO +1 -1
- {axis-62 → axis-63}/axis.egg-info/requires.txt +9 -9
- {axis-62 → axis-63}/pyproject.toml +10 -10
- {axis-62 → axis-63}/tests/test_event.py +24 -0
- {axis-62 → axis-63}/tests/test_rtsp.py +27 -3
- {axis-62 → axis-63}/LICENSE +0 -0
- {axis-62 → axis-63}/README.md +0 -0
- {axis-62 → axis-63}/axis/__init__.py +0 -0
- {axis-62 → axis-63}/axis/device.py +0 -0
- {axis-62 → axis-63}/axis/errors.py +0 -0
- {axis-62 → axis-63}/axis/interfaces/__init__.py +0 -0
- {axis-62 → axis-63}/axis/interfaces/api_discovery.py +0 -0
- {axis-62 → axis-63}/axis/interfaces/api_handler.py +0 -0
- {axis-62 → axis-63}/axis/interfaces/applications/__init__.py +0 -0
- {axis-62 → axis-63}/axis/interfaces/applications/application_handler.py +0 -0
- {axis-62 → axis-63}/axis/interfaces/applications/applications.py +0 -0
- {axis-62 → axis-63}/axis/interfaces/applications/fence_guard.py +0 -0
- {axis-62 → axis-63}/axis/interfaces/applications/loitering_guard.py +0 -0
- {axis-62 → axis-63}/axis/interfaces/applications/motion_guard.py +0 -0
- {axis-62 → axis-63}/axis/interfaces/applications/object_analytics.py +0 -0
- {axis-62 → axis-63}/axis/interfaces/applications/vmd4.py +0 -0
- {axis-62 → axis-63}/axis/interfaces/basic_device_info.py +0 -0
- {axis-62 → axis-63}/axis/interfaces/event_instances.py +0 -0
- {axis-62 → axis-63}/axis/interfaces/event_manager.py +0 -0
- {axis-62 → axis-63}/axis/interfaces/light_control.py +0 -0
- {axis-62 → axis-63}/axis/interfaces/mqtt.py +0 -0
- {axis-62 → axis-63}/axis/interfaces/parameters/__init__.py +0 -0
- {axis-62 → axis-63}/axis/interfaces/parameters/brand.py +0 -0
- {axis-62 → axis-63}/axis/interfaces/parameters/image.py +0 -0
- {axis-62 → axis-63}/axis/interfaces/parameters/io_port.py +0 -0
- {axis-62 → axis-63}/axis/interfaces/parameters/param_cgi.py +0 -0
- {axis-62 → axis-63}/axis/interfaces/parameters/param_handler.py +0 -0
- {axis-62 → axis-63}/axis/interfaces/parameters/properties.py +0 -0
- {axis-62 → axis-63}/axis/interfaces/parameters/ptz.py +0 -0
- {axis-62 → axis-63}/axis/interfaces/parameters/stream_profile.py +0 -0
- {axis-62 → axis-63}/axis/interfaces/pir_sensor_configuration.py +0 -0
- {axis-62 → axis-63}/axis/interfaces/port_cgi.py +0 -0
- {axis-62 → axis-63}/axis/interfaces/port_management.py +0 -0
- {axis-62 → axis-63}/axis/interfaces/ptz.py +0 -0
- {axis-62 → axis-63}/axis/interfaces/pwdgrp_cgi.py +0 -0
- {axis-62 → axis-63}/axis/interfaces/stream_profiles.py +0 -0
- {axis-62 → axis-63}/axis/interfaces/user_groups.py +0 -0
- {axis-62 → axis-63}/axis/interfaces/vapix.py +0 -0
- {axis-62 → axis-63}/axis/interfaces/view_areas.py +0 -0
- {axis-62 → axis-63}/axis/models/__init__.py +0 -0
- {axis-62 → axis-63}/axis/models/api.py +0 -0
- {axis-62 → axis-63}/axis/models/api_discovery.py +0 -0
- {axis-62 → axis-63}/axis/models/applications/__init__.py +0 -0
- {axis-62 → axis-63}/axis/models/applications/application.py +0 -0
- {axis-62 → axis-63}/axis/models/applications/fence_guard.py +0 -0
- {axis-62 → axis-63}/axis/models/applications/loitering_guard.py +0 -0
- {axis-62 → axis-63}/axis/models/applications/motion_guard.py +0 -0
- {axis-62 → axis-63}/axis/models/applications/object_analytics.py +0 -0
- {axis-62 → axis-63}/axis/models/applications/vmd4.py +0 -0
- {axis-62 → axis-63}/axis/models/basic_device_info.py +0 -0
- {axis-62 → axis-63}/axis/models/configuration.py +0 -0
- {axis-62 → axis-63}/axis/models/event_instance.py +0 -0
- {axis-62 → axis-63}/axis/models/light_control.py +0 -0
- {axis-62 → axis-63}/axis/models/mqtt.py +0 -0
- {axis-62 → axis-63}/axis/models/parameters/__init__.py +0 -0
- {axis-62 → axis-63}/axis/models/parameters/brand.py +0 -0
- {axis-62 → axis-63}/axis/models/parameters/image.py +0 -0
- {axis-62 → axis-63}/axis/models/parameters/io_port.py +0 -0
- {axis-62 → axis-63}/axis/models/parameters/param_cgi.py +0 -0
- {axis-62 → axis-63}/axis/models/parameters/properties.py +0 -0
- {axis-62 → axis-63}/axis/models/parameters/ptz.py +0 -0
- {axis-62 → axis-63}/axis/models/parameters/stream_profile.py +0 -0
- {axis-62 → axis-63}/axis/models/pir_sensor_configuration.py +0 -0
- {axis-62 → axis-63}/axis/models/port_cgi.py +0 -0
- {axis-62 → axis-63}/axis/models/port_management.py +0 -0
- {axis-62 → axis-63}/axis/models/pwdgrp_cgi.py +0 -0
- {axis-62 → axis-63}/axis/models/stream_profile.py +0 -0
- {axis-62 → axis-63}/axis/models/user_group.py +0 -0
- {axis-62 → axis-63}/axis/models/view_area.py +0 -0
- {axis-62 → axis-63}/axis/py.typed +0 -0
- {axis-62 → axis-63}/axis/stream_manager.py +0 -0
- {axis-62 → axis-63}/axis.egg-info/SOURCES.txt +0 -0
- {axis-62 → axis-63}/axis.egg-info/dependency_links.txt +0 -0
- {axis-62 → axis-63}/axis.egg-info/entry_points.txt +0 -0
- {axis-62 → axis-63}/axis.egg-info/top_level.txt +0 -0
- {axis-62 → axis-63}/setup.cfg +0 -0
- {axis-62 → axis-63}/tests/test_api_discovery.py +0 -0
- {axis-62 → axis-63}/tests/test_api_handler.py +0 -0
- {axis-62 → axis-63}/tests/test_basic_device_info.py +0 -0
- {axis-62 → axis-63}/tests/test_configuration.py +0 -0
- {axis-62 → axis-63}/tests/test_device.py +0 -0
- {axis-62 → axis-63}/tests/test_event_instances.py +0 -0
- {axis-62 → axis-63}/tests/test_event_stream.py +0 -0
- {axis-62 → axis-63}/tests/test_light_control.py +0 -0
- {axis-62 → axis-63}/tests/test_mqtt.py +0 -0
- {axis-62 → axis-63}/tests/test_pir_sensor_configuration.py +0 -0
- {axis-62 → axis-63}/tests/test_port_cgi.py +0 -0
- {axis-62 → axis-63}/tests/test_port_management.py +0 -0
- {axis-62 → axis-63}/tests/test_ptz.py +0 -0
- {axis-62 → axis-63}/tests/test_pwdgrp_cgi.py +0 -0
- {axis-62 → axis-63}/tests/test_stream_manager.py +0 -0
- {axis-62 → axis-63}/tests/test_stream_profiles.py +0 -0
- {axis-62 → axis-63}/tests/test_user_groups.py +0 -0
- {axis-62 → axis-63}/tests/test_vapix.py +0 -0
- {axis-62 → axis-63}/tests/test_view_areas.py +0 -0
{axis-62 → axis-63}/PKG-INFO
RENAMED
|
@@ -122,12 +122,17 @@ def traverse(
|
|
|
122
122
|
|
|
123
123
|
|
|
124
124
|
def extract_name_value(
|
|
125
|
-
data: dict[str, list[dict[str, str]] | dict[str, str]],
|
|
125
|
+
data: dict[str, list[dict[str, str]] | dict[str, str]], prefer: str | None = None
|
|
126
126
|
) -> tuple[str, str]:
|
|
127
127
|
"""Extract name and value from a simple item, take first dictionary if it is a list."""
|
|
128
128
|
item = data.get("SimpleItem", {})
|
|
129
129
|
if isinstance(item, list):
|
|
130
|
-
|
|
130
|
+
if prefer is None:
|
|
131
|
+
item = item[0]
|
|
132
|
+
else:
|
|
133
|
+
item = next(
|
|
134
|
+
(item for item in item if item.get("@Name", "") == prefer), item[0]
|
|
135
|
+
)
|
|
131
136
|
return item.get("@Name", ""), item.get("@Value", "")
|
|
132
137
|
# return item.get("Name", ""), item.get("Value", "")
|
|
133
138
|
|
|
@@ -206,7 +211,7 @@ class Event:
|
|
|
206
211
|
|
|
207
212
|
data_type = data_value = ""
|
|
208
213
|
if match := traverse(raw, DATA):
|
|
209
|
-
data_type, data_value = extract_name_value(match)
|
|
214
|
+
data_type, data_value = extract_name_value(match, "active")
|
|
210
215
|
|
|
211
216
|
return cls._decode_from_dict(
|
|
212
217
|
{
|
|
@@ -269,8 +269,7 @@ class PtzControlRequest(ApiRequest):
|
|
|
269
269
|
data["center"] = f"{x},{y}"
|
|
270
270
|
if self.area_zoom:
|
|
271
271
|
x, y, z = self.area_zoom
|
|
272
|
-
|
|
273
|
-
z = 1
|
|
272
|
+
z = max(z, 1)
|
|
274
273
|
data["areazoom"] = f"{x},{y},{z}"
|
|
275
274
|
if self.center or self.area_zoom:
|
|
276
275
|
if self.image_width:
|
{axis-62 → axis-63}/axis/rtsp.py
RENAMED
|
@@ -35,6 +35,7 @@ class State(enum.StrEnum):
|
|
|
35
35
|
|
|
36
36
|
|
|
37
37
|
TIME_OUT_LIMIT = 5
|
|
38
|
+
RTP_HEADER_SIZE = 12
|
|
38
39
|
|
|
39
40
|
|
|
40
41
|
class RTSPClient(asyncio.Protocol):
|
|
@@ -191,6 +192,7 @@ class RTPClient:
|
|
|
191
192
|
self.callback = callback
|
|
192
193
|
self.data: deque[bytes] = deque()
|
|
193
194
|
self.transport: asyncio.BaseTransport | None = None
|
|
195
|
+
self.fragment: bool = False
|
|
194
196
|
|
|
195
197
|
def connection_made(self, transport: asyncio.BaseTransport) -> None:
|
|
196
198
|
"""Execute when port is up and listening.
|
|
@@ -207,8 +209,19 @@ class RTPClient:
|
|
|
207
209
|
def datagram_received(self, data: bytes, addr: Any) -> None:
|
|
208
210
|
"""Signals when new data is available."""
|
|
209
211
|
if self.callback:
|
|
210
|
-
|
|
211
|
-
|
|
212
|
+
payload = data[RTP_HEADER_SIZE:]
|
|
213
|
+
|
|
214
|
+
# if the previous packet was a fragment, then merge it
|
|
215
|
+
if self.fragment:
|
|
216
|
+
previous = self.data.pop()
|
|
217
|
+
self.data.append(previous + payload)
|
|
218
|
+
else:
|
|
219
|
+
self.data.append(payload)
|
|
220
|
+
|
|
221
|
+
# check whether the RTP marker bit is set, if not it is a fragment
|
|
222
|
+
self.fragment = (data[1] & 0b1 << 7) == 0
|
|
223
|
+
if not self.fragment:
|
|
224
|
+
self.callback(Signal.DATA)
|
|
212
225
|
|
|
213
226
|
|
|
214
227
|
class RTSPSession:
|
|
@@ -4,21 +4,21 @@ packaging>23
|
|
|
4
4
|
xmltodict>=0.13.0
|
|
5
5
|
|
|
6
6
|
[requirements]
|
|
7
|
-
httpx==0.27.
|
|
8
|
-
orjson==3.10.
|
|
7
|
+
httpx==0.27.2
|
|
8
|
+
orjson==3.10.9
|
|
9
9
|
packaging==24.1
|
|
10
|
-
xmltodict==0.
|
|
10
|
+
xmltodict==0.14.2
|
|
11
11
|
|
|
12
12
|
[requirements_dev]
|
|
13
|
-
pre-commit==
|
|
13
|
+
pre-commit==4.0.1
|
|
14
14
|
|
|
15
15
|
[requirements_test]
|
|
16
|
-
mypy==1.
|
|
17
|
-
pytest==8.
|
|
16
|
+
mypy==1.12.1
|
|
17
|
+
pytest==8.3.3
|
|
18
18
|
pytest-aiohttp==1.0.5
|
|
19
|
-
pytest-asyncio==0.
|
|
19
|
+
pytest-asyncio==0.24.0
|
|
20
20
|
pytest-cov==5.0.0
|
|
21
21
|
respx==0.21.1
|
|
22
|
-
ruff==0.
|
|
22
|
+
ruff==0.7.0
|
|
23
23
|
types-orjson==3.6.2
|
|
24
|
-
types-xmltodict==v0.
|
|
24
|
+
types-xmltodict==v0.14.0.20241009
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "axis"
|
|
7
|
-
version = "
|
|
7
|
+
version = "63"
|
|
8
8
|
license = {text = "MIT"}
|
|
9
9
|
description = "A Python library for communicating with devices from Axis Communications"
|
|
10
10
|
readme = "README.md"
|
|
@@ -28,24 +28,24 @@ dependencies = [
|
|
|
28
28
|
|
|
29
29
|
[project.optional-dependencies]
|
|
30
30
|
requirements = [
|
|
31
|
-
"httpx==0.27.
|
|
32
|
-
"orjson==3.10.
|
|
31
|
+
"httpx==0.27.2",
|
|
32
|
+
"orjson==3.10.9",
|
|
33
33
|
"packaging==24.1",
|
|
34
|
-
"xmltodict==0.
|
|
34
|
+
"xmltodict==0.14.2",
|
|
35
35
|
]
|
|
36
36
|
requirements_test = [
|
|
37
|
-
"mypy==1.
|
|
38
|
-
"pytest==8.
|
|
37
|
+
"mypy==1.12.1",
|
|
38
|
+
"pytest==8.3.3",
|
|
39
39
|
"pytest-aiohttp==1.0.5",
|
|
40
|
-
"pytest-asyncio==0.
|
|
40
|
+
"pytest-asyncio==0.24.0",
|
|
41
41
|
"pytest-cov==5.0.0",
|
|
42
42
|
"respx==0.21.1",
|
|
43
|
-
"ruff==0.
|
|
43
|
+
"ruff==0.7.0",
|
|
44
44
|
"types-orjson==3.6.2",
|
|
45
|
-
"types-xmltodict==v0.
|
|
45
|
+
"types-xmltodict==v0.14.0.20241009",
|
|
46
46
|
]
|
|
47
47
|
requirements_dev = [
|
|
48
|
-
"pre-commit==
|
|
48
|
+
"pre-commit==4.0.1"
|
|
49
49
|
]
|
|
50
50
|
|
|
51
51
|
[project.urls]
|
|
@@ -18,6 +18,7 @@ from .event_fixtures import (
|
|
|
18
18
|
LIGHT_STATUS_INIT,
|
|
19
19
|
LOITERING_GUARD_INIT,
|
|
20
20
|
MOTION_GUARD_INIT,
|
|
21
|
+
OBJECT_ANALYTICS_ANY_CHANGE,
|
|
21
22
|
OBJECT_ANALYTICS_INIT,
|
|
22
23
|
PIR_CHANGE,
|
|
23
24
|
PIR_INIT,
|
|
@@ -133,6 +134,18 @@ from .event_fixtures import (
|
|
|
133
134
|
"tripped": False,
|
|
134
135
|
},
|
|
135
136
|
),
|
|
137
|
+
(
|
|
138
|
+
OBJECT_ANALYTICS_ANY_CHANGE,
|
|
139
|
+
{
|
|
140
|
+
"topic": "tnsaxis:CameraApplicationPlatform/ObjectAnalytics/Device1Scenario1",
|
|
141
|
+
"source": "",
|
|
142
|
+
"source_idx": "Device1Scenario1",
|
|
143
|
+
"group": EventGroup.MOTION,
|
|
144
|
+
"type": "Object Analytics",
|
|
145
|
+
"state": "1",
|
|
146
|
+
"tripped": True,
|
|
147
|
+
},
|
|
148
|
+
),
|
|
136
149
|
(
|
|
137
150
|
PIR_INIT,
|
|
138
151
|
{
|
|
@@ -329,6 +342,17 @@ def test_create_event(input: bytes, expected: tuple) -> None:
|
|
|
329
342
|
"value": "1",
|
|
330
343
|
},
|
|
331
344
|
),
|
|
345
|
+
(
|
|
346
|
+
OBJECT_ANALYTICS_ANY_CHANGE,
|
|
347
|
+
{
|
|
348
|
+
"operation": "Changed",
|
|
349
|
+
"topic": "tnsaxis:CameraApplicationPlatform/ObjectAnalytics/Device1Scenario1",
|
|
350
|
+
"source": "",
|
|
351
|
+
"source_idx": "",
|
|
352
|
+
"type": "active",
|
|
353
|
+
"value": "1",
|
|
354
|
+
},
|
|
355
|
+
),
|
|
332
356
|
],
|
|
333
357
|
)
|
|
334
358
|
def test_parse_event_xml(input: bytes, expected: dict):
|
|
@@ -9,9 +9,14 @@ from unittest.mock import Mock, patch
|
|
|
9
9
|
|
|
10
10
|
import pytest
|
|
11
11
|
|
|
12
|
-
from axis.rtsp import RTSPClient, Signal, State
|
|
12
|
+
from axis.rtsp import RTP_HEADER_SIZE, RTSPClient, Signal, State
|
|
13
13
|
|
|
14
14
|
from .conftest import HOST, RTSP_PORT
|
|
15
|
+
from .packet_fixtures import (
|
|
16
|
+
RTP_PACKET1_FULL,
|
|
17
|
+
RTP_PACKET2_FRAGMENT1,
|
|
18
|
+
RTP_PACKET2_FRAGMENT2,
|
|
19
|
+
)
|
|
15
20
|
|
|
16
21
|
LOGGER = logging.getLogger(__name__)
|
|
17
22
|
|
|
@@ -514,14 +519,33 @@ def test_rtp_client(rtsp_client, caplog):
|
|
|
514
519
|
assert "Stream recepient offline" in caplog.text
|
|
515
520
|
|
|
516
521
|
with patch.object(rtp_client.client, "callback") as mock_callback:
|
|
517
|
-
rtp_client.client.datagram_received(
|
|
522
|
+
rtp_client.client.datagram_received(
|
|
523
|
+
bytes.fromhex("008000000000000000000000AABBCCDD"), "addr"
|
|
524
|
+
)
|
|
518
525
|
mock_callback.assert_called_with(Signal.DATA)
|
|
519
|
-
assert rtp_client.data == "
|
|
526
|
+
assert rtp_client.data == bytes.fromhex("AABBCCDD")
|
|
520
527
|
|
|
521
528
|
rtsp_client.stop()
|
|
522
529
|
mock_transport.close.assert_called()
|
|
523
530
|
|
|
524
531
|
|
|
532
|
+
@pytest.mark.parametrize(
|
|
533
|
+
("packets"),
|
|
534
|
+
[([RTP_PACKET1_FULL]), ([RTP_PACKET2_FRAGMENT1, RTP_PACKET2_FRAGMENT2])],
|
|
535
|
+
)
|
|
536
|
+
def test_rtp_fragment(rtsp_client, packets: list[bytes]):
|
|
537
|
+
"""Verify RTP fragment handling."""
|
|
538
|
+
rtp_client = rtsp_client.rtp
|
|
539
|
+
|
|
540
|
+
with patch.object(rtp_client.client, "callback") as mock_callback:
|
|
541
|
+
payload = b""
|
|
542
|
+
for packet in packets:
|
|
543
|
+
rtp_client.client.datagram_received(packet, "addr")
|
|
544
|
+
payload += packet[RTP_HEADER_SIZE:]
|
|
545
|
+
mock_callback.assert_called_with(Signal.DATA)
|
|
546
|
+
assert rtp_client.data == payload
|
|
547
|
+
|
|
548
|
+
|
|
525
549
|
def test_methods(rtsp_client):
|
|
526
550
|
"""Verify method attributes."""
|
|
527
551
|
method = rtsp_client.method
|
{axis-62 → axis-63}/LICENSE
RENAMED
|
File without changes
|
{axis-62 → axis-63}/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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{axis-62 → axis-63}/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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|