ucapi 0.3.0__tar.gz → 0.3.1__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.
- {ucapi-0.3.0 → ucapi-0.3.1}/CHANGELOG.md +4 -0
- {ucapi-0.3.0/ucapi.egg-info → ucapi-0.3.1}/PKG-INFO +1 -1
- ucapi-0.3.1/tests/test_api.py +95 -0
- {ucapi-0.3.0 → ucapi-0.3.1}/ucapi/_version.py +2 -2
- {ucapi-0.3.0 → ucapi-0.3.1}/ucapi/api.py +33 -37
- {ucapi-0.3.0 → ucapi-0.3.1/ucapi.egg-info}/PKG-INFO +1 -1
- {ucapi-0.3.0 → ucapi-0.3.1}/ucapi.egg-info/SOURCES.txt +1 -0
- {ucapi-0.3.0 → ucapi-0.3.1}/CONTRIBUTING.md +0 -0
- {ucapi-0.3.0 → ucapi-0.3.1}/LICENSE +0 -0
- {ucapi-0.3.0 → ucapi-0.3.1}/README.md +0 -0
- {ucapi-0.3.0 → ucapi-0.3.1}/docs/code_guidelines.md +0 -0
- {ucapi-0.3.0 → ucapi-0.3.1}/docs/setup.md +0 -0
- {ucapi-0.3.0 → ucapi-0.3.1}/examples/README.md +0 -0
- {ucapi-0.3.0 → ucapi-0.3.1}/examples/hello_integration.json +0 -0
- {ucapi-0.3.0 → ucapi-0.3.1}/examples/hello_integration.py +0 -0
- {ucapi-0.3.0 → ucapi-0.3.1}/examples/remote.json +0 -0
- {ucapi-0.3.0 → ucapi-0.3.1}/examples/remote.py +0 -0
- {ucapi-0.3.0 → ucapi-0.3.1}/examples/remote_ui_page.json +0 -0
- {ucapi-0.3.0 → ucapi-0.3.1}/examples/setup_flow.json +0 -0
- {ucapi-0.3.0 → ucapi-0.3.1}/examples/setup_flow.py +0 -0
- {ucapi-0.3.0 → ucapi-0.3.1}/pyproject.toml +0 -0
- {ucapi-0.3.0 → ucapi-0.3.1}/requirements.txt +0 -0
- {ucapi-0.3.0 → ucapi-0.3.1}/setup.cfg +0 -0
- {ucapi-0.3.0 → ucapi-0.3.1}/test-requirements.txt +0 -0
- {ucapi-0.3.0 → ucapi-0.3.1}/ucapi/__init__.py +0 -0
- {ucapi-0.3.0 → ucapi-0.3.1}/ucapi/api_definitions.py +0 -0
- {ucapi-0.3.0 → ucapi-0.3.1}/ucapi/button.py +0 -0
- {ucapi-0.3.0 → ucapi-0.3.1}/ucapi/climate.py +0 -0
- {ucapi-0.3.0 → ucapi-0.3.1}/ucapi/cover.py +0 -0
- {ucapi-0.3.0 → ucapi-0.3.1}/ucapi/entities.py +0 -0
- {ucapi-0.3.0 → ucapi-0.3.1}/ucapi/entity.py +0 -0
- {ucapi-0.3.0 → ucapi-0.3.1}/ucapi/light.py +0 -0
- {ucapi-0.3.0 → ucapi-0.3.1}/ucapi/media_player.py +0 -0
- {ucapi-0.3.0 → ucapi-0.3.1}/ucapi/remote.py +0 -0
- {ucapi-0.3.0 → ucapi-0.3.1}/ucapi/sensor.py +0 -0
- {ucapi-0.3.0 → ucapi-0.3.1}/ucapi/switch.py +0 -0
- {ucapi-0.3.0 → ucapi-0.3.1}/ucapi/ui.py +0 -0
- {ucapi-0.3.0 → ucapi-0.3.1}/ucapi.egg-info/dependency_links.txt +0 -0
- {ucapi-0.3.0 → ucapi-0.3.1}/ucapi.egg-info/requires.txt +0 -0
- {ucapi-0.3.0 → ucapi-0.3.1}/ucapi.egg-info/top_level.txt +0 -0
|
@@ -11,6 +11,10 @@ _Changes in the next release_
|
|
|
11
11
|
|
|
12
12
|
---
|
|
13
13
|
|
|
14
|
+
## v0.3.1 - 2024-05-14
|
|
15
|
+
### Fixed
|
|
16
|
+
- Filtered log messages may not modify original data. This sporadically removed media artwork URLs ([#27](https://github.com/unfoldedcircle/integration-python-library/pull/27)).
|
|
17
|
+
|
|
14
18
|
## v0.3.0 - 2024-04-25
|
|
15
19
|
### Added
|
|
16
20
|
- New media-player attribute MEDIA_POSITION_UPDATED_AT ([feature-and-bug-tracker#443](https://github.com/unfoldedcircle/feature-and-bug-tracker/issues/443)).
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import unittest
|
|
2
|
+
from copy import deepcopy
|
|
3
|
+
|
|
4
|
+
from ucapi.api import filter_log_msg_data
|
|
5
|
+
from ucapi.media_player import Attributes
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class TestFilterLogMsgData(unittest.TestCase):
|
|
9
|
+
|
|
10
|
+
def test_no_modification_when_no_msg_data(self):
|
|
11
|
+
data = {}
|
|
12
|
+
result = filter_log_msg_data(data)
|
|
13
|
+
self.assertEqual(result, {}, "The result should be an empty dictionary")
|
|
14
|
+
|
|
15
|
+
def test_no_changes_when_media_image_url_not_present(self):
|
|
16
|
+
data = {"msg_data": {"attributes": {"state": "playing", "volume": 50}}}
|
|
17
|
+
original = deepcopy(data)
|
|
18
|
+
|
|
19
|
+
result = filter_log_msg_data(data)
|
|
20
|
+
|
|
21
|
+
self.assertEqual(
|
|
22
|
+
result,
|
|
23
|
+
original,
|
|
24
|
+
"The result should be the same as the input when no filtered attributes are present",
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
def test_filtering_media_image_url_in_dict(self):
|
|
28
|
+
data = {
|
|
29
|
+
"msg_data": {
|
|
30
|
+
"attributes": {
|
|
31
|
+
"state": "playing",
|
|
32
|
+
Attributes.MEDIA_IMAGE_URL: "data:image/png;base64,encodeddata",
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
expected_result = deepcopy(data)
|
|
37
|
+
expected_result["msg_data"]["attributes"][
|
|
38
|
+
Attributes.MEDIA_IMAGE_URL
|
|
39
|
+
] = "data:***"
|
|
40
|
+
|
|
41
|
+
result = filter_log_msg_data(data)
|
|
42
|
+
|
|
43
|
+
self.assertEqual(
|
|
44
|
+
result, expected_result, "The MEDIA_IMAGE_URL attribute should be filtered"
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
def test_filtering_media_image_url_in_list(self):
|
|
48
|
+
data = {
|
|
49
|
+
"msg_data": [
|
|
50
|
+
{
|
|
51
|
+
"attributes": {
|
|
52
|
+
"state": "paused",
|
|
53
|
+
Attributes.MEDIA_IMAGE_URL: "data:image/png;base64,exampledata",
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
"attributes": {
|
|
58
|
+
"state": "playing",
|
|
59
|
+
Attributes.MEDIA_IMAGE_URL: "data:image/jpg;base64,exampledata",
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
{"attributes": {"state": "stopped"}},
|
|
63
|
+
]
|
|
64
|
+
}
|
|
65
|
+
expected_result = deepcopy(data)
|
|
66
|
+
expected_result["msg_data"][0]["attributes"][
|
|
67
|
+
Attributes.MEDIA_IMAGE_URL
|
|
68
|
+
] = "data:***"
|
|
69
|
+
expected_result["msg_data"][1]["attributes"][
|
|
70
|
+
Attributes.MEDIA_IMAGE_URL
|
|
71
|
+
] = "data:***"
|
|
72
|
+
|
|
73
|
+
result = filter_log_msg_data(data)
|
|
74
|
+
|
|
75
|
+
self.assertEqual(
|
|
76
|
+
result,
|
|
77
|
+
expected_result,
|
|
78
|
+
"All MEDIA_IMAGE_URL attributes in the list should be filtered",
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
def test_input_is_not_modified(self):
|
|
82
|
+
data = {
|
|
83
|
+
"msg_data": {
|
|
84
|
+
"attributes": {
|
|
85
|
+
Attributes.MEDIA_IMAGE_URL: "data:image/png;base64,encodeddata"
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
original_data = deepcopy(data)
|
|
90
|
+
|
|
91
|
+
filter_log_msg_data(data)
|
|
92
|
+
|
|
93
|
+
self.assertEqual(
|
|
94
|
+
data, original_data, "The input data should not be modified by the function"
|
|
95
|
+
)
|
|
@@ -11,6 +11,7 @@ import logging
|
|
|
11
11
|
import os
|
|
12
12
|
import socket
|
|
13
13
|
from asyncio import AbstractEventLoop
|
|
14
|
+
from copy import deepcopy
|
|
14
15
|
from typing import Any, Callable
|
|
15
16
|
|
|
16
17
|
import websockets
|
|
@@ -25,8 +26,8 @@ from zeroconf import IPVersion
|
|
|
25
26
|
from zeroconf.asyncio import AsyncServiceInfo, AsyncZeroconf
|
|
26
27
|
|
|
27
28
|
import ucapi.api_definitions as uc
|
|
28
|
-
from ucapi import media_player
|
|
29
29
|
from ucapi.entities import Entities
|
|
30
|
+
from ucapi.media_player import Attributes as MediaAttr
|
|
30
31
|
|
|
31
32
|
try:
|
|
32
33
|
from ._version import version as __version__
|
|
@@ -249,8 +250,9 @@ class IntegrationAPI:
|
|
|
249
250
|
if websocket in self._clients:
|
|
250
251
|
data_dump = json.dumps(data)
|
|
251
252
|
if _LOG.isEnabledFor(logging.DEBUG):
|
|
252
|
-
|
|
253
|
-
|
|
253
|
+
_LOG.debug(
|
|
254
|
+
"[%s] ->: %s", websocket.remote_address, filter_log_msg_data(data)
|
|
255
|
+
)
|
|
254
256
|
await websocket.send(data_dump)
|
|
255
257
|
else:
|
|
256
258
|
_LOG.error("Error sending response: connection no longer established")
|
|
@@ -270,14 +272,12 @@ class IntegrationAPI:
|
|
|
270
272
|
"""
|
|
271
273
|
data = {"kind": "event", "msg": msg, "msg_data": msg_data, "cat": category}
|
|
272
274
|
data_dump = json.dumps(data)
|
|
273
|
-
# filter fields
|
|
274
|
-
data_log = ""
|
|
275
|
-
if _LOG.isEnabledFor(logging.DEBUG):
|
|
276
|
-
data_log = json.dumps(data) if filter_log_msg_data(data) else data_dump
|
|
277
275
|
|
|
278
276
|
for websocket in self._clients:
|
|
279
277
|
if _LOG.isEnabledFor(logging.DEBUG):
|
|
280
|
-
_LOG.debug(
|
|
278
|
+
_LOG.debug(
|
|
279
|
+
"[%s] =>: %s", websocket.remote_address, filter_log_msg_data(data)
|
|
280
|
+
)
|
|
281
281
|
try:
|
|
282
282
|
await websocket.send(data_dump)
|
|
283
283
|
except websockets.exceptions.WebSocketException:
|
|
@@ -302,8 +302,9 @@ class IntegrationAPI:
|
|
|
302
302
|
|
|
303
303
|
if websocket in self._clients:
|
|
304
304
|
if _LOG.isEnabledFor(logging.DEBUG):
|
|
305
|
-
|
|
306
|
-
|
|
305
|
+
_LOG.debug(
|
|
306
|
+
"[%s] ->: %s", websocket.remote_address, filter_log_msg_data(data)
|
|
307
|
+
)
|
|
307
308
|
await websocket.send(data_dump)
|
|
308
309
|
else:
|
|
309
310
|
_LOG.error("Error sending event: connection no longer established")
|
|
@@ -864,47 +865,42 @@ def local_hostname() -> str:
|
|
|
864
865
|
)
|
|
865
866
|
|
|
866
867
|
|
|
867
|
-
def filter_log_msg_data(data: dict[str, Any]) ->
|
|
868
|
+
def filter_log_msg_data(data: dict[str, Any]) -> dict[str, Any]:
|
|
868
869
|
"""
|
|
869
870
|
Filter attribute fields to exclude for log messages in the given msg data dict.
|
|
870
871
|
|
|
871
|
-
Attention: the dictionary is modified!
|
|
872
|
-
|
|
873
872
|
- Attributes are filtered in `data["msg_data"]`:
|
|
874
873
|
- dict object: key `attributes`
|
|
875
874
|
- list object: every list item `attributes`
|
|
876
875
|
- Filtered attributes: `MEDIA_IMAGE_URL`
|
|
877
876
|
|
|
878
877
|
:param data: the message data dict
|
|
879
|
-
:return:
|
|
878
|
+
:return: copy of the message data dict with filtered attributes
|
|
880
879
|
"""
|
|
880
|
+
# do not modify the original dict
|
|
881
|
+
log_upd = deepcopy(data)
|
|
882
|
+
if not log_upd:
|
|
883
|
+
return {}
|
|
884
|
+
|
|
881
885
|
# filter out base64 encoded images in the media player's media_image_url attribute
|
|
882
|
-
if "msg_data" in
|
|
886
|
+
if "msg_data" in log_upd:
|
|
883
887
|
if (
|
|
884
|
-
"attributes" in
|
|
885
|
-
and
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
].startswith("data:")
|
|
888
|
+
"attributes" in log_upd["msg_data"]
|
|
889
|
+
and MediaAttr.MEDIA_IMAGE_URL in log_upd["msg_data"]["attributes"]
|
|
890
|
+
and log_upd["msg_data"]["attributes"][MediaAttr.MEDIA_IMAGE_URL].startswith(
|
|
891
|
+
"data:"
|
|
892
|
+
)
|
|
890
893
|
):
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
return True
|
|
895
|
-
|
|
896
|
-
if isinstance(data["msg_data"], list):
|
|
897
|
-
for item in data["msg_data"]:
|
|
894
|
+
log_upd["msg_data"]["attributes"][MediaAttr.MEDIA_IMAGE_URL] = "data:***"
|
|
895
|
+
elif isinstance(log_upd["msg_data"], list):
|
|
896
|
+
for item in log_upd["msg_data"]:
|
|
898
897
|
if (
|
|
899
898
|
"attributes" in item
|
|
900
|
-
and
|
|
901
|
-
and item["attributes"][
|
|
902
|
-
|
|
903
|
-
|
|
899
|
+
and MediaAttr.MEDIA_IMAGE_URL in item["attributes"]
|
|
900
|
+
and item["attributes"][MediaAttr.MEDIA_IMAGE_URL].startswith(
|
|
901
|
+
"data:"
|
|
902
|
+
)
|
|
904
903
|
):
|
|
905
|
-
item["attributes"][
|
|
906
|
-
media_player.Attributes.MEDIA_IMAGE_URL
|
|
907
|
-
] = "data:***"
|
|
908
|
-
return True
|
|
904
|
+
item["attributes"][MediaAttr.MEDIA_IMAGE_URL] = "data:***"
|
|
909
905
|
|
|
910
|
-
return
|
|
906
|
+
return log_upd
|
|
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
|