OpenDahua 1.0.0__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 (111) hide show
  1. opendahua-1.0.0/PKG-INFO +8 -0
  2. opendahua-1.0.0/pyproject.toml +22 -0
  3. opendahua-1.0.0/src/opendahua/__init__.py +0 -0
  4. opendahua-1.0.0/src/opendahua/api/__init__.py +0 -0
  5. opendahua-1.0.0/src/opendahua/api/api_client.py +5 -0
  6. opendahua-1.0.0/src/opendahua/api/api_request.py +44 -0
  7. opendahua-1.0.0/src/opendahua/api/api_response.py +9 -0
  8. opendahua-1.0.0/src/opendahua/api_dahua/__init__.py +0 -0
  9. opendahua-1.0.0/src/opendahua/api_dahua/api_client_dahua.py +108 -0
  10. opendahua-1.0.0/src/opendahua/api_dahua/api_dahua_authentication_util.py +129 -0
  11. opendahua-1.0.0/src/opendahua/api_dahua/api_dahua_body_parser.py +150 -0
  12. opendahua-1.0.0/src/opendahua/api_dahua/api_dahua_util.py +9 -0
  13. opendahua-1.0.0/src/opendahua/api_dahua/object/__init__.py +0 -0
  14. opendahua-1.0.0/src/opendahua/api_dahua/object/api_dahua_media_file_find_result_item.py +37 -0
  15. opendahua-1.0.0/src/opendahua/api_dahua/object/api_dahua_media_file_finder_identifier.py +13 -0
  16. opendahua-1.0.0/src/opendahua/api_dahua/object/api_dahua_video_stream.py +8 -0
  17. opendahua-1.0.0/src/opendahua/api_dahua/object/datetime_dahua.py +8 -0
  18. opendahua-1.0.0/src/opendahua/api_dahua/request/__init__.py +0 -0
  19. opendahua-1.0.0/src/opendahua/api_dahua/request/api_request_dahua.py +9 -0
  20. opendahua-1.0.0/src/opendahua/api_dahua/request/api_request_dahua_machine_name_read.py +28 -0
  21. opendahua-1.0.0/src/opendahua/api_dahua/request/api_request_dahua_media_file_download.py +36 -0
  22. opendahua-1.0.0/src/opendahua/api_dahua/request/api_request_dahua_media_file_finder_close.py +38 -0
  23. opendahua-1.0.0/src/opendahua/api_dahua/request/api_request_dahua_media_file_finder_create.py +29 -0
  24. opendahua-1.0.0/src/opendahua/api_dahua/request/api_request_dahua_media_file_finder_find.py +50 -0
  25. opendahua-1.0.0/src/opendahua/api_dahua/request/api_request_dahua_media_file_finder_read.py +39 -0
  26. opendahua-1.0.0/src/opendahua/api_dahua/request/api_request_dahua_time_current_read.py +32 -0
  27. opendahua-1.0.0/src/opendahua/api_dahua/response/__init__.py +0 -0
  28. opendahua-1.0.0/src/opendahua/api_dahua/response/api_response_dahua.py +7 -0
  29. opendahua-1.0.0/src/opendahua/api_dahua/response/api_response_dahua_machine_name_read.py +22 -0
  30. opendahua-1.0.0/src/opendahua/api_dahua/response/api_response_dahua_media_file_download.py +22 -0
  31. opendahua-1.0.0/src/opendahua/api_dahua/response/api_response_dahua_media_file_finder_close.py +12 -0
  32. opendahua-1.0.0/src/opendahua/api_dahua/response/api_response_dahua_media_file_finder_create.py +23 -0
  33. opendahua-1.0.0/src/opendahua/api_dahua/response/api_response_dahua_media_file_finder_find.py +11 -0
  34. opendahua-1.0.0/src/opendahua/api_dahua/response/api_response_dahua_media_file_finder_read.py +45 -0
  35. opendahua-1.0.0/src/opendahua/api_dahua/response/api_response_dahua_time_current_read.py +23 -0
  36. opendahua-1.0.0/src/opendahua/api_peer_to_peer/__init__.py +0 -0
  37. opendahua-1.0.0/src/opendahua/api_peer_to_peer/api_client_peer_to_peer.py +72 -0
  38. opendahua-1.0.0/src/opendahua/api_peer_to_peer/api_peer_to_peer_authentication_util.py +123 -0
  39. opendahua-1.0.0/src/opendahua/api_peer_to_peer/api_peer_to_peer_body_parser.py +18 -0
  40. opendahua-1.0.0/src/opendahua/api_peer_to_peer/object/__init__.py +0 -0
  41. opendahua-1.0.0/src/opendahua/api_peer_to_peer/object/api_peer_to_peer_random_salt.py +8 -0
  42. opendahua-1.0.0/src/opendahua/api_peer_to_peer/request/__init__.py +0 -0
  43. opendahua-1.0.0/src/opendahua/api_peer_to_peer/request/api_peer_to_peer_encryption_util.py +74 -0
  44. opendahua-1.0.0/src/opendahua/api_peer_to_peer/request/api_request_peer_to_peer.py +9 -0
  45. opendahua-1.0.0/src/opendahua/api_peer_to_peer/request/api_request_peer_to_peer_channel_create.py +75 -0
  46. opendahua-1.0.0/src/opendahua/api_peer_to_peer/request/api_request_peer_to_peer_device_probe.py +31 -0
  47. opendahua-1.0.0/src/opendahua/api_peer_to_peer/request/api_request_peer_to_peer_device_read.py +33 -0
  48. opendahua-1.0.0/src/opendahua/api_peer_to_peer/request/api_request_peer_to_peer_server_info_read.py +32 -0
  49. opendahua-1.0.0/src/opendahua/api_peer_to_peer/request/api_request_peer_to_peer_server_probe.py +25 -0
  50. opendahua-1.0.0/src/opendahua/api_peer_to_peer/response/__init__.py +0 -0
  51. opendahua-1.0.0/src/opendahua/api_peer_to_peer/response/api_response_peer_to_peer.py +7 -0
  52. opendahua-1.0.0/src/opendahua/api_peer_to_peer/response/api_response_peer_to_peer_channel_create.py +38 -0
  53. opendahua-1.0.0/src/opendahua/api_peer_to_peer/response/api_response_peer_to_peer_device_probe.py +8 -0
  54. opendahua-1.0.0/src/opendahua/api_peer_to_peer/response/api_response_peer_to_peer_device_read.py +27 -0
  55. opendahua-1.0.0/src/opendahua/api_peer_to_peer/response/api_response_peer_to_peer_server_info_read.py +22 -0
  56. opendahua-1.0.0/src/opendahua/api_peer_to_peer/response/api_response_peer_to_peer_server_probe.py +8 -0
  57. opendahua-1.0.0/src/opendahua/common_object/__init__.py +0 -0
  58. opendahua-1.0.0/src/opendahua/common_object/dahua_error.py +4 -0
  59. opendahua-1.0.0/src/opendahua/common_object/dahua_error_bad_request.py +5 -0
  60. opendahua-1.0.0/src/opendahua/common_object/key.py +8 -0
  61. opendahua-1.0.0/src/opendahua/common_object/nonce.py +16 -0
  62. opendahua-1.0.0/src/opendahua/common_util/__init__.py +0 -0
  63. opendahua-1.0.0/src/opendahua/common_util/error_util.py +17 -0
  64. opendahua-1.0.0/src/opendahua/dahua/__init__.py +0 -0
  65. opendahua-1.0.0/src/opendahua/dahua/dahua_device.py +15 -0
  66. opendahua-1.0.0/src/opendahua/dahua/dahua_nvr.py +141 -0
  67. opendahua-1.0.0/src/opendahua/dahua/dahua_peer_to_peer_connection_error.py +4 -0
  68. opendahua-1.0.0/src/opendahua/dahua/object/__init__.py +0 -0
  69. opendahua-1.0.0/src/opendahua/dahua/object/dahua_video.py +44 -0
  70. opendahua-1.0.0/src/opendahua/dahua/object/dahua_video_file_type.py +9 -0
  71. opendahua-1.0.0/src/opendahua/http/__init__.py +0 -0
  72. opendahua-1.0.0/src/opendahua/http/http_header.py +31 -0
  73. opendahua-1.0.0/src/opendahua/http/http_header_parser.py +28 -0
  74. opendahua-1.0.0/src/opendahua/http/http_request.py +122 -0
  75. opendahua-1.0.0/src/opendahua/http/http_request_body.py +25 -0
  76. opendahua-1.0.0/src/opendahua/http/http_request_method.py +7 -0
  77. opendahua-1.0.0/src/opendahua/http/http_response.py +51 -0
  78. opendahua-1.0.0/src/opendahua/http/http_response_body.py +38 -0
  79. opendahua-1.0.0/src/opendahua/http/http_response_parser.py +121 -0
  80. opendahua-1.0.0/src/opendahua/http/http_status_code.py +12 -0
  81. opendahua-1.0.0/src/opendahua/logger.py +74 -0
  82. opendahua-1.0.0/src/opendahua/main.py +25 -0
  83. opendahua-1.0.0/src/opendahua/object/__init__.py +0 -0
  84. opendahua-1.0.0/src/opendahua/object/address.py +42 -0
  85. opendahua-1.0.0/src/opendahua/object/authentication_identifier.py +26 -0
  86. opendahua-1.0.0/src/opendahua/object/cookie.py +17 -0
  87. opendahua-1.0.0/src/opendahua/object/log_level.py +8 -0
  88. opendahua-1.0.0/src/opendahua/object/transaction_identifier.py +18 -0
  89. opendahua-1.0.0/src/opendahua/object/url.py +48 -0
  90. opendahua-1.0.0/src/opendahua/ptcp/__init__.py +0 -0
  91. opendahua-1.0.0/src/opendahua/ptcp/ptcp_http_client.py +56 -0
  92. opendahua-1.0.0/src/opendahua/ptcp/ptcp_packet.py +77 -0
  93. opendahua-1.0.0/src/opendahua/ptcp/ptcp_packet_body.py +19 -0
  94. opendahua-1.0.0/src/opendahua/ptcp/ptcp_packet_body_bind.py +42 -0
  95. opendahua-1.0.0/src/opendahua/ptcp/ptcp_packet_body_connection_status.py +5 -0
  96. opendahua-1.0.0/src/opendahua/ptcp/ptcp_packet_body_data.py +38 -0
  97. opendahua-1.0.0/src/opendahua/ptcp/ptcp_packet_body_empty.py +9 -0
  98. opendahua-1.0.0/src/opendahua/ptcp/ptcp_packet_body_heartbeat.py +9 -0
  99. opendahua-1.0.0/src/opendahua/ptcp/ptcp_packet_body_parser.py +43 -0
  100. opendahua-1.0.0/src/opendahua/ptcp/ptcp_packet_body_syn.py +9 -0
  101. opendahua-1.0.0/src/opendahua/ptcp/ptcp_packet_identifier.py +39 -0
  102. opendahua-1.0.0/src/opendahua/ptcp/ptcp_packet_parser.py +60 -0
  103. opendahua-1.0.0/src/opendahua/ptcp/ptcp_packet_type.py +8 -0
  104. opendahua-1.0.0/src/opendahua/ptcp/ptcp_realm_identifier.py +20 -0
  105. opendahua-1.0.0/src/opendahua/ptcp/ptcp_socket.py +272 -0
  106. opendahua-1.0.0/src/opendahua/signaling_client.py +205 -0
  107. opendahua-1.0.0/src/opendahua/udp/__init__.py +0 -0
  108. opendahua-1.0.0/src/opendahua/udp/udp_http_client.py +45 -0
  109. opendahua-1.0.0/src/opendahua/udp/udp_protocol.py +33 -0
  110. opendahua-1.0.0/src/opendahua/udp/udp_socket.py +80 -0
  111. opendahua-1.0.0/src/opendahua/udp/udp_socket_closed_error.py +3 -0
@@ -0,0 +1,8 @@
1
+ Metadata-Version: 2.3
2
+ Name: OpenDahua
3
+ Version: 1.0.0
4
+ Summary: SDK for interacting with Dahua NVRs over their peer-to-peer protocol.
5
+ Requires-Dist: aiofiles>=25.1.0
6
+ Requires-Dist: cryptography>=48.0.0
7
+ Requires-Dist: xmltodict>=1.0.4
8
+ Requires-Python: >=3.14
@@ -0,0 +1,22 @@
1
+ [project]
2
+ name = "OpenDahua"
3
+ version = "1.0.0"
4
+ description = "SDK for interacting with Dahua NVRs over their peer-to-peer protocol."
5
+ requires-python = ">=3.14"
6
+ dependencies = [
7
+ "aiofiles>=25.1.0",
8
+ "cryptography>=48.0.0",
9
+ "xmltodict>=1.0.4",
10
+ ]
11
+
12
+ [dependency-groups]
13
+ dev = [
14
+ "pytest==9.0.3"
15
+ ]
16
+
17
+ [tool.uv]
18
+ package = true
19
+
20
+ [build-system]
21
+ requires = ["uv_build>=0.11.17,<0.12"]
22
+ build-backend = "uv_build"
File without changes
File without changes
@@ -0,0 +1,5 @@
1
+ from abc import ABC
2
+
3
+
4
+ class ApiClient(ABC):
5
+ pass
@@ -0,0 +1,44 @@
1
+ from abc import ABC, abstractmethod
2
+ from typing import Type, TypeVar, Generic
3
+
4
+ from opendahua.api.api_response import ApiResponse
5
+ from opendahua.http.http_header import HttpHeader
6
+ from opendahua.http.http_request import HttpRequest
7
+ from opendahua.http.http_request_body import HttpRequestBody
8
+ from opendahua.http.http_request_method import HttpRequestMethod
9
+ from opendahua.object.url import Url
10
+
11
+ T = TypeVar("T", bound=ApiResponse)
12
+
13
+ class ApiRequest(ABC, Generic[T]):
14
+ @abstractmethod
15
+ def determine_endpoint(self) -> Url:
16
+ ...
17
+
18
+
19
+ @abstractmethod
20
+ def get_request_method(self) -> HttpRequestMethod:
21
+ ...
22
+
23
+
24
+ @abstractmethod
25
+ def determine_body_or_none(self) -> HttpRequestBody|None:
26
+ ...
27
+
28
+
29
+ def determine_all_header(self) -> list[HttpHeader]:
30
+ return []
31
+
32
+
33
+ def generate_http_request(self) -> HttpRequest:
34
+ return HttpRequest(
35
+ self.get_request_method(),
36
+ self.determine_endpoint(),
37
+ self.determine_all_header(),
38
+ self.determine_body_or_none(),
39
+ )
40
+
41
+
42
+ @abstractmethod
43
+ def get_response_class(self) -> Type[T]:
44
+ ...
@@ -0,0 +1,9 @@
1
+ from abc import ABC, abstractmethod
2
+
3
+
4
+ class ApiResponse(ABC):
5
+ @classmethod
6
+ @abstractmethod
7
+ def parse(cls, value: dict) -> ApiResponse:
8
+ ...
9
+
File without changes
@@ -0,0 +1,108 @@
1
+ from opendahua.api.api_request import T
2
+ from opendahua.api_dahua.api_dahua_authentication_util import ApiDahuaAuthenticationUtil
3
+ from opendahua.api_dahua.api_dahua_body_parser import ApiDahuaBodyParser
4
+ from opendahua.api_dahua.request.api_request_dahua import ApiRequestDahua
5
+ from opendahua.common_object.dahua_error import DahuaError
6
+ from opendahua.common_object.dahua_error_bad_request import DahuaErrorBadRequest
7
+ from opendahua.common_object.nonce import Nonce
8
+ from opendahua.dahua.dahua_device import DahuaDevice
9
+ from opendahua.http.http_request import HttpRequest
10
+ from opendahua.http.http_response import HttpResponse
11
+ from opendahua.http.http_status_code import HttpStatusCode
12
+ from opendahua.ptcp.ptcp_http_client import PtcpHttpClient
13
+ from opendahua.signaling_client import SignalingClient
14
+
15
+ class ApiClientDahua:
16
+ # Error constants.
17
+ ERROR_DAHUA_API = "Received error response from Dahua API: \"{http_response}\"."
18
+ ERROR_ALREADY_CONNECTED = "Already connected."
19
+ ERROR_ALREADY_DISCONNECTED = "Already disconnected."
20
+ ERROR_EXPECTED_DIGEST_CHALLENGE = "Received response with status code \"{status_code}\" instead of digest authentication challenge."
21
+ ERROR_NOT_CONNECTED = "You have to use connect() to connect to the device before making API requests."
22
+
23
+
24
+ def __init__(self, device: DahuaDevice):
25
+ self._device: DahuaDevice = device
26
+ self._signaling_client: SignalingClient = SignalingClient(device)
27
+ self._http_client: PtcpHttpClient|None = None
28
+
29
+
30
+ async def connect(self) -> None:
31
+ if self._http_client is None:
32
+ ptcp_socket = await self._signaling_client.connect()
33
+
34
+ http_client = PtcpHttpClient(ptcp_socket)
35
+
36
+ await http_client.connect()
37
+ self._http_client = http_client
38
+ else:
39
+ raise DahuaError(self.ERROR_ALREADY_CONNECTED)
40
+
41
+
42
+ async def disconnect(self) -> None:
43
+ if self._http_client is None:
44
+ raise DahuaError(self.ERROR_ALREADY_DISCONNECTED)
45
+ else:
46
+ await self._http_client.disconnect()
47
+
48
+
49
+ async def send_request(self, api_request: ApiRequestDahua[T]) -> T:
50
+ client = await self._get_http_client()
51
+
52
+ http_request = api_request.generate_http_request()
53
+ http_response = await client.send_request(http_request)
54
+
55
+ if http_response.get_status_code() == HttpStatusCode.UNAUTHORIZED:
56
+ nonce = ApiDahuaAuthenticationUtil.determine_nonce_from_http_response_unauthorized(http_response)
57
+ realm = ApiDahuaAuthenticationUtil.determine_realm_from_http_response_unauthorized(http_response)
58
+
59
+ http_request = self._add_header_authentication(http_request, nonce, realm)
60
+
61
+ http_response = await client.send_request(http_request)
62
+
63
+ if self._is_http_response_success(http_response):
64
+ return self._parse_api_response(api_request, http_response)
65
+ elif http_response.get_status_code() == HttpStatusCode.BAD_REQUEST:
66
+ raise DahuaErrorBadRequest(self.ERROR_DAHUA_API.format(http_response=http_response))
67
+ else:
68
+ raise DahuaError(self.ERROR_DAHUA_API.format(http_response=http_response))
69
+ else:
70
+ raise DahuaError(self.ERROR_EXPECTED_DIGEST_CHALLENGE.format(status_code=http_response.get_status_code()))
71
+
72
+
73
+ async def _get_http_client(self) -> PtcpHttpClient:
74
+ if self._http_client is None:
75
+ raise DahuaError(self.ERROR_NOT_CONNECTED)
76
+ else:
77
+ return self._http_client
78
+
79
+
80
+ def _add_header_authentication(self, http_request: HttpRequest, nonce: Nonce, realm: str) -> HttpRequest:
81
+ header_authentication = ApiDahuaAuthenticationUtil.generate_header_authentication(
82
+ http_request,
83
+ self._device,
84
+ nonce,
85
+ realm,
86
+ )
87
+ http_request.add_header(header_authentication)
88
+
89
+ return http_request
90
+
91
+
92
+ def _is_http_response_success(self, http_response: HttpResponse) -> bool:
93
+ return http_response.get_status_code() in self._get_all_http_status_code_success()
94
+
95
+
96
+ @staticmethod
97
+ def _get_all_http_status_code_success() -> list[HttpStatusCode]:
98
+ return [
99
+ HttpStatusCode.OK,
100
+ ]
101
+
102
+
103
+ @staticmethod
104
+ def _parse_api_response(api_request: ApiRequestDahua[T], http_response: HttpResponse) -> T:
105
+ response_class = api_request.get_response_class()
106
+ response_body_dict = ApiDahuaBodyParser.determine_dict(http_response)
107
+
108
+ return response_class.parse(response_body_dict)
@@ -0,0 +1,129 @@
1
+ import hashlib
2
+ import re
3
+
4
+ from opendahua.common_object.nonce import Nonce
5
+ from opendahua.common_util.error_util import ErrorUtil
6
+ from opendahua.dahua.dahua_device import DahuaDevice
7
+ from opendahua.http.http_header import HttpHeader
8
+ from opendahua.http.http_request import HttpRequest
9
+ from opendahua.http.http_response import HttpResponse
10
+
11
+
12
+ class ApiDahuaAuthenticationUtil:
13
+ # Header constants.
14
+ HEADER_KEY_AUTHORIZATION = "Authorization"
15
+ HEADER_KEY_DIGEST_AUTHENTICATION_REQUIRED = "WWW-Authenticate"
16
+
17
+ # Format constants.
18
+ FORMAT_HEADER_DIGEST = (
19
+ "Digest "
20
+ "username=\"{username}\", "
21
+ "realm=\"{realm}\", "
22
+ "nonce=\"{nonce}\", "
23
+ "uri=\"{url_request}\", "
24
+ "qop=\"{qop}\", "
25
+ "nc={number_of_time_nonce_used:08x}, "
26
+ "cnonce=\"{nonce_client}\", "
27
+ "response=\"{response}\", "
28
+ "opaque=\"{opaque}\""
29
+ )
30
+ FORMAT_HA1 = "{username}:{realm}:{password}"
31
+ FORMAT_HA2 = "{http_request_method}:{url_request}"
32
+ FORMAT_RESPONSE = "{ha1}:{nonce}:{number_of_time_nonce_used:08x}:{nonce_client}:{qop}:{ha2}"
33
+
34
+ # Digest constants.
35
+ DIGEST_QUALITY_OF_PROTECTION_AUTHENTICATION = "auth"
36
+ DIGEST_NUMBER_OF_TIME_NONCE_USED = 1
37
+
38
+ # Regex constants.
39
+ REGEX_PATTERN_NONCE = re.compile(r'nonce="([^"]*)"')
40
+ REGEX_PATTERN_REALM = re.compile(r'realm="([^"]*)"')
41
+
42
+ @classmethod
43
+ def determine_nonce_from_http_response_unauthorized(
44
+ cls,
45
+ http_response_unauthorized: HttpResponse,
46
+ ) -> Nonce:
47
+ header_digest_authentication_required = cls._get_header_digest_authentication_required(
48
+ http_response_unauthorized,
49
+ )
50
+
51
+ match = cls.REGEX_PATTERN_NONCE.search(header_digest_authentication_required.get_header_value_string())
52
+
53
+ return Nonce(match.group(1))
54
+
55
+ @classmethod
56
+ def determine_realm_from_http_response_unauthorized(
57
+ cls,
58
+ http_response_unauthorized: HttpResponse,
59
+ ) -> str:
60
+ header_digest_authentication_required = cls._get_header_digest_authentication_required(
61
+ http_response_unauthorized,
62
+ )
63
+
64
+ match = cls.REGEX_PATTERN_REALM.search(header_digest_authentication_required.get_header_value_string())
65
+
66
+ return match.group(1)
67
+
68
+ @classmethod
69
+ def _get_header_digest_authentication_required(cls, http_response_unauthorized: HttpResponse) -> HttpHeader:
70
+ header_digest_authentication_required = http_response_unauthorized.get_header_or_none(
71
+ cls.HEADER_KEY_DIGEST_AUTHENTICATION_REQUIRED,
72
+ )
73
+
74
+ if header_digest_authentication_required is None:
75
+ raise ErrorUtil.create_error_unexpected_none_value(HttpHeader)
76
+ else:
77
+ return header_digest_authentication_required
78
+
79
+
80
+ @classmethod
81
+ def generate_header_authentication(
82
+ cls,
83
+ request: HttpRequest,
84
+ device: DahuaDevice,
85
+ nonce: Nonce,
86
+ realm: str,
87
+ ) -> HttpHeader:
88
+ quality_of_protection = cls.DIGEST_QUALITY_OF_PROTECTION_AUTHENTICATION
89
+ nonce_client = Nonce.create_random()
90
+
91
+ ha1 = cls._hash_md5(
92
+ cls.FORMAT_HA1.format(username=device.get_username(), realm=realm, password=device.get_password()),
93
+ )
94
+ ha2 = cls._hash_md5(
95
+ cls.FORMAT_HA2.format(
96
+ http_request_method=request.get_method().value,
97
+ url_request=request.get_url().get_url_string(),
98
+ ),
99
+ )
100
+
101
+ response = cls._hash_md5(
102
+ cls.FORMAT_RESPONSE.format(
103
+ ha1=ha1,
104
+ nonce=nonce.get_nonce_string(),
105
+ number_of_time_nonce_used=cls.DIGEST_NUMBER_OF_TIME_NONCE_USED,
106
+ nonce_client=nonce_client.get_nonce_string(),
107
+ qop=quality_of_protection,
108
+ ha2=ha2,
109
+ )
110
+ )
111
+
112
+ return HttpHeader(
113
+ key=cls.HEADER_KEY_AUTHORIZATION,
114
+ value=cls.FORMAT_HEADER_DIGEST.format(
115
+ username=device.get_username(),
116
+ realm=realm,
117
+ nonce=nonce.get_nonce_string(),
118
+ url_request=request.get_url().get_url_string(),
119
+ qop=quality_of_protection,
120
+ number_of_time_nonce_used=cls.DIGEST_NUMBER_OF_TIME_NONCE_USED,
121
+ nonce_client=nonce_client.get_nonce_string(),
122
+ response=response,
123
+ opaque="",
124
+ )
125
+ )
126
+
127
+ @staticmethod
128
+ def _hash_md5(value: str) -> str:
129
+ return hashlib.md5(value.encode()).hexdigest()
@@ -0,0 +1,150 @@
1
+ import re
2
+ from datetime import datetime
3
+ from typing import Any
4
+
5
+ from opendahua.common_object.dahua_error import DahuaError
6
+ from opendahua.common_util.error_util import ErrorUtil
7
+ from opendahua.http.http_header import HttpHeader
8
+ from opendahua.http.http_response import HttpResponse
9
+
10
+
11
+ class ApiDahuaBodyParser:
12
+ # Error constants.
13
+ ERROR_UNEXPECTED_CONTENT_TYPE = "Unexpected content type \"{content_type}\"."
14
+
15
+ # Separator constants.
16
+ SEPARATOR_LINE_BODY = "\r\n"
17
+ SEPARATOR_KEY_VALUE = "="
18
+ SEPARATOR_PART_KEY = "."
19
+
20
+ # TODO: verplaats http logica naar http response zelf
21
+ # Header constants.
22
+ HEADER_CONTENT_TYPE = "Content-type"
23
+
24
+ # Content constants.
25
+ CONTENT_TYPE_TEXT_PLAIN = "text/plain"
26
+ CONTENT_TYPE_TEXT_PLAIN_CHARSET_UTF_8 = "text/plain;charset=utf-8"
27
+ CONTENT_TYPE_APPLICATION_HTTP = "application/http"
28
+
29
+ # Field constants.
30
+ FIELD_DATA = "data"
31
+
32
+ # Regex constants.
33
+ REGEX_PART_KEY_LIST = r"^(?P<key>[a-zA-Z]+)\[(?P<index>\d+)\]$"
34
+ REGEX_MATCH_GROUP_KEY = "key"
35
+ REGEX_MATCH_GROUP_INDEX = "index"
36
+
37
+ # Index constants.
38
+ INDEX_FIRST = 0
39
+
40
+
41
+ @classmethod
42
+ def determine_dict(cls, http_response: HttpResponse) -> dict:
43
+ content_type_response = cls.determine_content_type_response(http_response)
44
+
45
+ # TODO: Iets van should_use_key_value_parsing.
46
+ if content_type_response == cls.CONTENT_TYPE_TEXT_PLAIN or content_type_response == cls.CONTENT_TYPE_TEXT_PLAIN_CHARSET_UTF_8:
47
+ return cls.parse_body_key_value(http_response.get_body().get_http_response_body_string())
48
+ elif content_type_response == cls.CONTENT_TYPE_APPLICATION_HTTP:
49
+ return {cls.FIELD_DATA: http_response.get_body().get_http_response_body_bytes()}
50
+ else:
51
+ raise DahuaError(cls.ERROR_UNEXPECTED_CONTENT_TYPE.format(content_type=content_type_response))
52
+
53
+
54
+ @classmethod
55
+ def determine_content_type_response(cls, http_response: HttpResponse) -> str:
56
+ header_content_type = http_response.get_header_or_none(cls.HEADER_CONTENT_TYPE)
57
+
58
+ if header_content_type is None:
59
+ raise ErrorUtil.create_error_unexpected_none_value(HttpHeader)
60
+ else:
61
+ return header_content_type.get_header_value_string()
62
+
63
+
64
+ @classmethod
65
+ def parse_body_key_value(cls, body_string: str) -> dict:
66
+ response_dict = {}
67
+ body_string_excluding_trailing_carriage_return = body_string.strip()
68
+
69
+ all_line_body = body_string_excluding_trailing_carriage_return.split(cls.SEPARATOR_LINE_BODY)
70
+
71
+ for line_body in all_line_body:
72
+ cls._parse_key_value(line_body, response_dict)
73
+
74
+ return response_dict
75
+
76
+ @classmethod
77
+ def _parse_key_value(cls, key_value_string: str, response_dict: dict) -> dict:
78
+ key_string_full, _, _ = key_value_string.partition(cls.SEPARATOR_KEY_VALUE)
79
+ is_key_nested = cls.SEPARATOR_PART_KEY in key_string_full
80
+
81
+ separator = cls.SEPARATOR_PART_KEY if is_key_nested else cls.SEPARATOR_KEY_VALUE
82
+ key, _, remainder_string = key_value_string.partition(separator)
83
+
84
+ match_list = re.search(cls.REGEX_PART_KEY_LIST, key)
85
+
86
+ if match_list:
87
+ key_list = match_list.group(cls.REGEX_MATCH_GROUP_KEY)
88
+ index_list_item = int(match_list.group(cls.REGEX_MATCH_GROUP_INDEX))
89
+
90
+ all_list_item = response_dict.setdefault(key_list, [])
91
+ is_list_item_existing = index_list_item < len(all_list_item)
92
+
93
+ if is_key_nested:
94
+ if is_list_item_existing:
95
+ list_item_dict = all_list_item[index_list_item]
96
+ else:
97
+ list_item_dict = {}
98
+
99
+ list_item_dict_updated = cls._parse_key_value(remainder_string, list_item_dict)
100
+ else:
101
+ value_string = remainder_string
102
+
103
+ list_item_dict_updated = cls._parse_value(value_string)
104
+
105
+ if is_list_item_existing:
106
+ all_list_item[index_list_item] = list_item_dict_updated
107
+ else:
108
+ all_list_item.append(list_item_dict_updated)
109
+
110
+ else:
111
+ if is_key_nested:
112
+ value_current = response_dict.get(key, {})
113
+ value_updated = cls._parse_key_value(remainder_string, value_current)
114
+ else:
115
+ value_string = remainder_string
116
+
117
+ value_updated = cls._parse_value(value_string)
118
+
119
+ response_dict[key] = value_updated
120
+
121
+ return response_dict
122
+
123
+
124
+ @classmethod
125
+ def _parse_value(cls, value_string: str) -> Any:
126
+ if value_string.isdigit():
127
+ return int(value_string)
128
+ elif cls._is_datetime_string(value_string):
129
+ return cls._parse_datetime_string(value_string)
130
+ else:
131
+ return value_string
132
+
133
+
134
+ @classmethod
135
+ def _is_datetime_string(cls, value_string: str) -> bool:
136
+ try:
137
+ cls._parse_datetime_string(value_string)
138
+
139
+ return True
140
+ except ValueError:
141
+ return False
142
+
143
+
144
+ @classmethod
145
+ def _parse_datetime_string(cls, value_string: str) -> datetime:
146
+ try:
147
+ return datetime.fromisoformat(value_string)
148
+ except ValueError:
149
+ # TODO: magic number.
150
+ return datetime.strptime(value_string, "%Y-%m-%d %H:%M:%S")
@@ -0,0 +1,9 @@
1
+ from datetime import datetime
2
+
3
+ class ApiDahuaUtil:
4
+ # Format constants.
5
+ FORMAT_DATETIME_DAHUA_STRING = "%Y-%m-%dT%H:%M:%SZ"
6
+
7
+ @classmethod
8
+ def determine_datetime_dahua_string(cls, datetime: datetime):
9
+ return datetime.strftime(cls.FORMAT_DATETIME_DAHUA_STRING)
@@ -0,0 +1,37 @@
1
+ from dataclasses import dataclass
2
+ from datetime import datetime
3
+ from pathlib import Path
4
+
5
+ from opendahua.api_dahua.object.api_dahua_video_stream import ApiDahuaVideoStream
6
+
7
+
8
+ @dataclass(frozen=True)
9
+ class ApiDahuaMediaFileFindResultItem:
10
+ _channel: int
11
+ _time_start: datetime
12
+ _time_end: datetime
13
+ _path_file: Path
14
+ _file_type: str
15
+ _video_stream: ApiDahuaVideoStream
16
+
17
+ def get_channel_int(self) -> int:
18
+ return self._channel
19
+
20
+
21
+ def get_time_start(self) -> datetime:
22
+ return self._time_start
23
+
24
+
25
+ def get_time_end(self) -> datetime:
26
+ return self._time_end
27
+
28
+
29
+ def get_path_file(self) -> Path:
30
+ return self._path_file
31
+
32
+
33
+ def get_file_type(self) -> str:
34
+ return self._file_type
35
+
36
+ def get_video_stream(self) -> ApiDahuaVideoStream:
37
+ return self._video_stream
@@ -0,0 +1,13 @@
1
+ from dataclasses import dataclass
2
+
3
+
4
+ @dataclass(frozen=True)
5
+ class ApiDahuaMediaFileFinderIdentifier:
6
+ _media_file_identifier_int: int
7
+
8
+ def get_media_file_identifier_int(self) -> int:
9
+ return self._media_file_identifier_int
10
+
11
+ def __str__(self):
12
+ return str(self.get_media_file_identifier_int())
13
+
@@ -0,0 +1,8 @@
1
+ from enum import Enum
2
+
3
+
4
+ class ApiDahuaVideoStream(Enum):
5
+ MAIN = "Main"
6
+ EXTRA1 = "Extra1"
7
+ EXTRA2 = "Extra2"
8
+ EXTRA3 = "Extra3"
@@ -0,0 +1,8 @@
1
+ from datetime import datetime
2
+
3
+ class DateTimeDahua(datetime):
4
+ # Format constants.
5
+ FORMAT_DAHUA_DATETIME_STRING = "%Y-%m-%dT%H:%M:%SZ"
6
+
7
+ def get_datetime_dahua_string(self) -> str:
8
+ return self.strftime(self.FORMAT_DAHUA_DATETIME_STRING)
@@ -0,0 +1,9 @@
1
+ from typing import TypeVar, Generic
2
+
3
+ from opendahua.api.api_request import ApiRequest
4
+ from opendahua.api_dahua.response.api_response_dahua import ApiResponseDahua
5
+
6
+ T = TypeVar("T", bound=ApiResponseDahua)
7
+
8
+ class ApiRequestDahua(ApiRequest[T], Generic[T]):
9
+ pass
@@ -0,0 +1,28 @@
1
+ from typing import Type
2
+
3
+ from opendahua.api.api_request import T
4
+ from opendahua.api_dahua.request.api_request_dahua import ApiRequestDahua
5
+ from opendahua.api_dahua.response.api_response_dahua_machine_name_read import ApiResponseDahuaMachineNameRead
6
+ from opendahua.http.http_request_body import HttpRequestBody
7
+ from opendahua.http.http_request_method import HttpRequestMethod
8
+ from opendahua.object.url import Url
9
+
10
+
11
+ class ApiRequestDahuaMachineNameRead(ApiRequestDahua[ApiResponseDahuaMachineNameRead]):
12
+ # Request constants.
13
+ REQUEST_ENDPOINT = "/cgi-bin/magicBox.cgi?action=getMachineName"
14
+
15
+ def determine_endpoint(self) -> Url:
16
+ return Url(self.REQUEST_ENDPOINT)
17
+
18
+
19
+ def get_request_method(self) -> HttpRequestMethod:
20
+ return HttpRequestMethod.GET
21
+
22
+
23
+ def determine_body_or_none(self) -> HttpRequestBody | None:
24
+ return None
25
+
26
+
27
+ def get_response_class(self) -> Type[T]:
28
+ return ApiResponseDahuaMachineNameRead
@@ -0,0 +1,36 @@
1
+ from pathlib import Path
2
+ from typing import Type
3
+
4
+ from opendahua.api.api_request import T
5
+ from opendahua.api_dahua.request.api_request_dahua import ApiRequestDahua
6
+ from opendahua.api_dahua.response.api_response_dahua_media_file_download import ApiResponseDahuaMediaFileDownload
7
+ from opendahua.http.http_request_body import HttpRequestBody
8
+ from opendahua.http.http_request_method import HttpRequestMethod
9
+ from opendahua.object.url import Url
10
+
11
+
12
+ class ApiRequestDahuaMediaFileDownload(ApiRequestDahua[ApiResponseDahuaMediaFileDownload]):
13
+ # Request constants.
14
+ REQUEST_ENDPOINT = "/cgi-bin/RPC_Loadfile/{path}"
15
+
16
+ # Character constants.
17
+ CHARACTER_LEADING_SLASH = "/"
18
+
19
+ def __init__(self, path: Path):
20
+ self._path: Path = path
21
+
22
+
23
+ def determine_endpoint(self) -> Url:
24
+ return Url(self.REQUEST_ENDPOINT.format(path=str(self._path).strip(self.CHARACTER_LEADING_SLASH)))
25
+
26
+
27
+ def get_request_method(self) -> HttpRequestMethod:
28
+ return HttpRequestMethod.GET
29
+
30
+
31
+ def determine_body_or_none(self) -> HttpRequestBody | None:
32
+ return None
33
+
34
+
35
+ def get_response_class(self) -> Type[T]:
36
+ return ApiResponseDahuaMediaFileDownload