axis 61__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.
Files changed (105) hide show
  1. {axis-61 → axis-63}/PKG-INFO +1 -1
  2. {axis-61 → axis-63}/axis/__main__.py +2 -2
  3. {axis-61 → axis-63}/axis/interfaces/pwdgrp_cgi.py +2 -4
  4. {axis-61 → axis-63}/axis/models/api.py +1 -1
  5. {axis-61 → axis-63}/axis/models/event.py +8 -3
  6. {axis-61 → axis-63}/axis/models/port_management.py +1 -1
  7. {axis-61 → axis-63}/axis/models/ptz_cgi.py +10 -9
  8. {axis-61 → axis-63}/axis/rtsp.py +15 -2
  9. {axis-61 → axis-63}/axis.egg-info/PKG-INFO +1 -1
  10. {axis-61 → axis-63}/axis.egg-info/requires.txt +10 -10
  11. {axis-61 → axis-63}/pyproject.toml +11 -11
  12. {axis-61 → axis-63}/tests/test_event.py +24 -0
  13. {axis-61 → axis-63}/tests/test_port_management.py +14 -12
  14. {axis-61 → axis-63}/tests/test_rtsp.py +27 -3
  15. {axis-61 → axis-63}/LICENSE +0 -0
  16. {axis-61 → axis-63}/README.md +0 -0
  17. {axis-61 → axis-63}/axis/__init__.py +0 -0
  18. {axis-61 → axis-63}/axis/device.py +0 -0
  19. {axis-61 → axis-63}/axis/errors.py +0 -0
  20. {axis-61 → axis-63}/axis/interfaces/__init__.py +0 -0
  21. {axis-61 → axis-63}/axis/interfaces/api_discovery.py +0 -0
  22. {axis-61 → axis-63}/axis/interfaces/api_handler.py +0 -0
  23. {axis-61 → axis-63}/axis/interfaces/applications/__init__.py +0 -0
  24. {axis-61 → axis-63}/axis/interfaces/applications/application_handler.py +0 -0
  25. {axis-61 → axis-63}/axis/interfaces/applications/applications.py +0 -0
  26. {axis-61 → axis-63}/axis/interfaces/applications/fence_guard.py +0 -0
  27. {axis-61 → axis-63}/axis/interfaces/applications/loitering_guard.py +0 -0
  28. {axis-61 → axis-63}/axis/interfaces/applications/motion_guard.py +0 -0
  29. {axis-61 → axis-63}/axis/interfaces/applications/object_analytics.py +0 -0
  30. {axis-61 → axis-63}/axis/interfaces/applications/vmd4.py +0 -0
  31. {axis-61 → axis-63}/axis/interfaces/basic_device_info.py +0 -0
  32. {axis-61 → axis-63}/axis/interfaces/event_instances.py +0 -0
  33. {axis-61 → axis-63}/axis/interfaces/event_manager.py +0 -0
  34. {axis-61 → axis-63}/axis/interfaces/light_control.py +0 -0
  35. {axis-61 → axis-63}/axis/interfaces/mqtt.py +0 -0
  36. {axis-61 → axis-63}/axis/interfaces/parameters/__init__.py +0 -0
  37. {axis-61 → axis-63}/axis/interfaces/parameters/brand.py +0 -0
  38. {axis-61 → axis-63}/axis/interfaces/parameters/image.py +0 -0
  39. {axis-61 → axis-63}/axis/interfaces/parameters/io_port.py +0 -0
  40. {axis-61 → axis-63}/axis/interfaces/parameters/param_cgi.py +0 -0
  41. {axis-61 → axis-63}/axis/interfaces/parameters/param_handler.py +0 -0
  42. {axis-61 → axis-63}/axis/interfaces/parameters/properties.py +0 -0
  43. {axis-61 → axis-63}/axis/interfaces/parameters/ptz.py +0 -0
  44. {axis-61 → axis-63}/axis/interfaces/parameters/stream_profile.py +0 -0
  45. {axis-61 → axis-63}/axis/interfaces/pir_sensor_configuration.py +0 -0
  46. {axis-61 → axis-63}/axis/interfaces/port_cgi.py +0 -0
  47. {axis-61 → axis-63}/axis/interfaces/port_management.py +0 -0
  48. {axis-61 → axis-63}/axis/interfaces/ptz.py +0 -0
  49. {axis-61 → axis-63}/axis/interfaces/stream_profiles.py +0 -0
  50. {axis-61 → axis-63}/axis/interfaces/user_groups.py +0 -0
  51. {axis-61 → axis-63}/axis/interfaces/vapix.py +0 -0
  52. {axis-61 → axis-63}/axis/interfaces/view_areas.py +0 -0
  53. {axis-61 → axis-63}/axis/models/__init__.py +0 -0
  54. {axis-61 → axis-63}/axis/models/api_discovery.py +0 -0
  55. {axis-61 → axis-63}/axis/models/applications/__init__.py +0 -0
  56. {axis-61 → axis-63}/axis/models/applications/application.py +0 -0
  57. {axis-61 → axis-63}/axis/models/applications/fence_guard.py +0 -0
  58. {axis-61 → axis-63}/axis/models/applications/loitering_guard.py +0 -0
  59. {axis-61 → axis-63}/axis/models/applications/motion_guard.py +0 -0
  60. {axis-61 → axis-63}/axis/models/applications/object_analytics.py +0 -0
  61. {axis-61 → axis-63}/axis/models/applications/vmd4.py +0 -0
  62. {axis-61 → axis-63}/axis/models/basic_device_info.py +0 -0
  63. {axis-61 → axis-63}/axis/models/configuration.py +0 -0
  64. {axis-61 → axis-63}/axis/models/event_instance.py +0 -0
  65. {axis-61 → axis-63}/axis/models/light_control.py +0 -0
  66. {axis-61 → axis-63}/axis/models/mqtt.py +0 -0
  67. {axis-61 → axis-63}/axis/models/parameters/__init__.py +0 -0
  68. {axis-61 → axis-63}/axis/models/parameters/brand.py +0 -0
  69. {axis-61 → axis-63}/axis/models/parameters/image.py +0 -0
  70. {axis-61 → axis-63}/axis/models/parameters/io_port.py +0 -0
  71. {axis-61 → axis-63}/axis/models/parameters/param_cgi.py +0 -0
  72. {axis-61 → axis-63}/axis/models/parameters/properties.py +0 -0
  73. {axis-61 → axis-63}/axis/models/parameters/ptz.py +0 -0
  74. {axis-61 → axis-63}/axis/models/parameters/stream_profile.py +0 -0
  75. {axis-61 → axis-63}/axis/models/pir_sensor_configuration.py +0 -0
  76. {axis-61 → axis-63}/axis/models/port_cgi.py +0 -0
  77. {axis-61 → axis-63}/axis/models/pwdgrp_cgi.py +0 -0
  78. {axis-61 → axis-63}/axis/models/stream_profile.py +0 -0
  79. {axis-61 → axis-63}/axis/models/user_group.py +0 -0
  80. {axis-61 → axis-63}/axis/models/view_area.py +0 -0
  81. {axis-61 → axis-63}/axis/py.typed +0 -0
  82. {axis-61 → axis-63}/axis/stream_manager.py +0 -0
  83. {axis-61 → axis-63}/axis.egg-info/SOURCES.txt +0 -0
  84. {axis-61 → axis-63}/axis.egg-info/dependency_links.txt +0 -0
  85. {axis-61 → axis-63}/axis.egg-info/entry_points.txt +0 -0
  86. {axis-61 → axis-63}/axis.egg-info/top_level.txt +0 -0
  87. {axis-61 → axis-63}/setup.cfg +0 -0
  88. {axis-61 → axis-63}/tests/test_api_discovery.py +0 -0
  89. {axis-61 → axis-63}/tests/test_api_handler.py +0 -0
  90. {axis-61 → axis-63}/tests/test_basic_device_info.py +0 -0
  91. {axis-61 → axis-63}/tests/test_configuration.py +0 -0
  92. {axis-61 → axis-63}/tests/test_device.py +0 -0
  93. {axis-61 → axis-63}/tests/test_event_instances.py +0 -0
  94. {axis-61 → axis-63}/tests/test_event_stream.py +0 -0
  95. {axis-61 → axis-63}/tests/test_light_control.py +0 -0
  96. {axis-61 → axis-63}/tests/test_mqtt.py +0 -0
  97. {axis-61 → axis-63}/tests/test_pir_sensor_configuration.py +0 -0
  98. {axis-61 → axis-63}/tests/test_port_cgi.py +0 -0
  99. {axis-61 → axis-63}/tests/test_ptz.py +0 -0
  100. {axis-61 → axis-63}/tests/test_pwdgrp_cgi.py +0 -0
  101. {axis-61 → axis-63}/tests/test_stream_manager.py +0 -0
  102. {axis-61 → axis-63}/tests/test_stream_profiles.py +0 -0
  103. {axis-61 → axis-63}/tests/test_user_groups.py +0 -0
  104. {axis-61 → axis-63}/tests/test_vapix.py +0 -0
  105. {axis-61 → axis-63}/tests/test_view_areas.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: axis
3
- Version: 61
3
+ Version: 63
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
@@ -71,8 +71,8 @@ async def main(
71
71
 
72
72
  try:
73
73
  if events:
74
- while True:
75
- await asyncio.sleep(1)
74
+ done = asyncio.Event()
75
+ await done.wait()
76
76
 
77
77
  except asyncio.CancelledError:
78
78
  device.stream.stop()
@@ -34,11 +34,9 @@ class Users(ApiHandler[User]):
34
34
  @property
35
35
  def listed_in_parameters(self) -> bool:
36
36
  """Is pwdgrp.cgi supported."""
37
- if self.vapix.params.property_handler.supported and (
37
+ return self.vapix.params.property_handler.supported and (
38
38
  self.vapix.params.property_handler["0"].api_http_version >= 3
39
- ):
40
- return True
41
- return False
39
+ )
42
40
 
43
41
  async def _api_request(self) -> dict[str, User]:
44
42
  """Get default data of basic device information."""
@@ -58,7 +58,7 @@ ApiResponseT = TypeVar("ApiResponseT", bound=ApiResponseSupportDecode)
58
58
 
59
59
 
60
60
  @dataclass
61
- class ApiRequest(ABC):
61
+ class ApiRequest:
62
62
  """Create API request body."""
63
63
 
64
64
  method: str = field(init=False)
@@ -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
- item = item[0]
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
  {
@@ -247,7 +247,7 @@ class SetPortsRequest(ApiRequest):
247
247
  "apiVersion": self.api_version,
248
248
  "context": self.context,
249
249
  "method": "setPorts",
250
- "params": ports,
250
+ "params": {"ports": ports},
251
251
  }
252
252
  )
253
253
 
@@ -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
- if z < 1:
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:
@@ -308,13 +307,15 @@ class PtzControlRequest(ApiRequest):
308
307
  if command_bool is not None:
309
308
  data[key] = "on" if command_bool else "off"
310
309
 
311
- for key, command_enum in (
312
- ("imagerotation", self.image_rotation),
313
- ("ircutfilter", self.ir_cut_filter),
314
- ("move", self.move),
315
- ):
316
- if command_enum is not None:
317
- data[key] = command_enum
310
+ data |= {
311
+ key: command_enum
312
+ for key, command_enum in (
313
+ ("imagerotation", self.image_rotation),
314
+ ("ircutfilter", self.ir_cut_filter),
315
+ ("move", self.move),
316
+ )
317
+ if command_enum is not None
318
+ }
318
319
 
319
320
  if self.continuous_pantilt_move:
320
321
  pan_speed, tilt_speed = self.continuous_pantilt_move
@@ -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
- self.data.append(data[12:])
211
- self.callback(Signal.DATA)
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:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: axis
3
- Version: 61
3
+ Version: 63
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
@@ -4,21 +4,21 @@ packaging>23
4
4
  xmltodict>=0.13.0
5
5
 
6
6
  [requirements]
7
- httpx==0.27.0
8
- orjson==3.10.0
9
- packaging==24.0
10
- xmltodict==0.13.0
7
+ httpx==0.27.2
8
+ orjson==3.10.9
9
+ packaging==24.1
10
+ xmltodict==0.14.2
11
11
 
12
12
  [requirements_dev]
13
- pre-commit==3.7.0
13
+ pre-commit==4.0.1
14
14
 
15
15
  [requirements_test]
16
- mypy==1.9.0
17
- pytest==8.1.1
16
+ mypy==1.12.1
17
+ pytest==8.3.3
18
18
  pytest-aiohttp==1.0.5
19
- pytest-asyncio==0.23.6
19
+ pytest-asyncio==0.24.0
20
20
  pytest-cov==5.0.0
21
21
  respx==0.21.1
22
- ruff==0.3.5
22
+ ruff==0.7.0
23
23
  types-orjson==3.6.2
24
- types-xmltodict==v0.13.0.3
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 = "61"
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.0",
32
- "orjson==3.10.0",
33
- "packaging==24.0",
34
- "xmltodict==0.13.0",
31
+ "httpx==0.27.2",
32
+ "orjson==3.10.9",
33
+ "packaging==24.1",
34
+ "xmltodict==0.14.2",
35
35
  ]
36
36
  requirements_test = [
37
- "mypy==1.9.0",
38
- "pytest==8.1.1",
37
+ "mypy==1.12.1",
38
+ "pytest==8.3.3",
39
39
  "pytest-aiohttp==1.0.5",
40
- "pytest-asyncio==0.23.6",
40
+ "pytest-asyncio==0.24.0",
41
41
  "pytest-cov==5.0.0",
42
42
  "respx==0.21.1",
43
- "ruff==0.3.5",
43
+ "ruff==0.7.0",
44
44
  "types-orjson==3.6.2",
45
- "types-xmltodict==v0.13.0.3",
45
+ "types-xmltodict==v0.14.0.20241009",
46
46
  ]
47
47
  requirements_dev = [
48
- "pre-commit==3.7.0"
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):
@@ -55,7 +55,7 @@ async def test_get_ports(respx_mock, io_port_management):
55
55
  "method": "setPorts",
56
56
  "apiVersion": "1.0",
57
57
  "context": "Axis library",
58
- "params": [{"port": "0", "state": "open"}],
58
+ "params": {"ports": [{"port": "0", "state": "open"}]},
59
59
  }
60
60
 
61
61
  await io_port_management.close("0")
@@ -67,7 +67,7 @@ async def test_get_ports(respx_mock, io_port_management):
67
67
  "method": "setPorts",
68
68
  "apiVersion": "1.0",
69
69
  "context": "Axis library",
70
- "params": [{"port": "0", "state": "closed"}],
70
+ "params": {"ports": [{"port": "0", "state": "closed"}]},
71
71
  }
72
72
 
73
73
 
@@ -105,16 +105,18 @@ async def test_set_ports(respx_mock, io_port_management):
105
105
  "method": "setPorts",
106
106
  "apiVersion": "1.0",
107
107
  "context": "Axis library",
108
- "params": [
109
- {
110
- "port": "0",
111
- "usage": "",
112
- "direction": "",
113
- "name": "",
114
- "normalState": "",
115
- "state": "closed",
116
- }
117
- ],
108
+ "params": {
109
+ "ports": [
110
+ {
111
+ "port": "0",
112
+ "usage": "",
113
+ "direction": "",
114
+ "name": "",
115
+ "normalState": "",
116
+ "state": "closed",
117
+ }
118
+ ]
119
+ },
118
120
  }
119
121
 
120
122
 
@@ -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("0123456789ABCDEF", "addr")
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 == "CDEF"
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
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