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.
Files changed (40) hide show
  1. {ucapi-0.3.0 → ucapi-0.3.1}/CHANGELOG.md +4 -0
  2. {ucapi-0.3.0/ucapi.egg-info → ucapi-0.3.1}/PKG-INFO +1 -1
  3. ucapi-0.3.1/tests/test_api.py +95 -0
  4. {ucapi-0.3.0 → ucapi-0.3.1}/ucapi/_version.py +2 -2
  5. {ucapi-0.3.0 → ucapi-0.3.1}/ucapi/api.py +33 -37
  6. {ucapi-0.3.0 → ucapi-0.3.1/ucapi.egg-info}/PKG-INFO +1 -1
  7. {ucapi-0.3.0 → ucapi-0.3.1}/ucapi.egg-info/SOURCES.txt +1 -0
  8. {ucapi-0.3.0 → ucapi-0.3.1}/CONTRIBUTING.md +0 -0
  9. {ucapi-0.3.0 → ucapi-0.3.1}/LICENSE +0 -0
  10. {ucapi-0.3.0 → ucapi-0.3.1}/README.md +0 -0
  11. {ucapi-0.3.0 → ucapi-0.3.1}/docs/code_guidelines.md +0 -0
  12. {ucapi-0.3.0 → ucapi-0.3.1}/docs/setup.md +0 -0
  13. {ucapi-0.3.0 → ucapi-0.3.1}/examples/README.md +0 -0
  14. {ucapi-0.3.0 → ucapi-0.3.1}/examples/hello_integration.json +0 -0
  15. {ucapi-0.3.0 → ucapi-0.3.1}/examples/hello_integration.py +0 -0
  16. {ucapi-0.3.0 → ucapi-0.3.1}/examples/remote.json +0 -0
  17. {ucapi-0.3.0 → ucapi-0.3.1}/examples/remote.py +0 -0
  18. {ucapi-0.3.0 → ucapi-0.3.1}/examples/remote_ui_page.json +0 -0
  19. {ucapi-0.3.0 → ucapi-0.3.1}/examples/setup_flow.json +0 -0
  20. {ucapi-0.3.0 → ucapi-0.3.1}/examples/setup_flow.py +0 -0
  21. {ucapi-0.3.0 → ucapi-0.3.1}/pyproject.toml +0 -0
  22. {ucapi-0.3.0 → ucapi-0.3.1}/requirements.txt +0 -0
  23. {ucapi-0.3.0 → ucapi-0.3.1}/setup.cfg +0 -0
  24. {ucapi-0.3.0 → ucapi-0.3.1}/test-requirements.txt +0 -0
  25. {ucapi-0.3.0 → ucapi-0.3.1}/ucapi/__init__.py +0 -0
  26. {ucapi-0.3.0 → ucapi-0.3.1}/ucapi/api_definitions.py +0 -0
  27. {ucapi-0.3.0 → ucapi-0.3.1}/ucapi/button.py +0 -0
  28. {ucapi-0.3.0 → ucapi-0.3.1}/ucapi/climate.py +0 -0
  29. {ucapi-0.3.0 → ucapi-0.3.1}/ucapi/cover.py +0 -0
  30. {ucapi-0.3.0 → ucapi-0.3.1}/ucapi/entities.py +0 -0
  31. {ucapi-0.3.0 → ucapi-0.3.1}/ucapi/entity.py +0 -0
  32. {ucapi-0.3.0 → ucapi-0.3.1}/ucapi/light.py +0 -0
  33. {ucapi-0.3.0 → ucapi-0.3.1}/ucapi/media_player.py +0 -0
  34. {ucapi-0.3.0 → ucapi-0.3.1}/ucapi/remote.py +0 -0
  35. {ucapi-0.3.0 → ucapi-0.3.1}/ucapi/sensor.py +0 -0
  36. {ucapi-0.3.0 → ucapi-0.3.1}/ucapi/switch.py +0 -0
  37. {ucapi-0.3.0 → ucapi-0.3.1}/ucapi/ui.py +0 -0
  38. {ucapi-0.3.0 → ucapi-0.3.1}/ucapi.egg-info/dependency_links.txt +0 -0
  39. {ucapi-0.3.0 → ucapi-0.3.1}/ucapi.egg-info/requires.txt +0 -0
  40. {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)).
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ucapi
3
- Version: 0.3.0
3
+ Version: 0.3.1
4
4
  Summary: Python wrapper for the Unfolded Circle Integration API
5
5
  Author-email: Unfolded Circle ApS <hello@unfoldedcircle.com>
6
6
  License: MPL-2.0
@@ -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
+ )
@@ -17,5 +17,5 @@ __version__: str
17
17
  __version_tuple__: VERSION_TUPLE
18
18
  version_tuple: VERSION_TUPLE
19
19
 
20
- __version__ = version = '0.3.0'
21
- __version_tuple__ = version_tuple = (0, 3, 0)
20
+ __version__ = version = '0.3.1'
21
+ __version_tuple__ = version_tuple = (0, 3, 1)
@@ -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
- data_log = json.dumps(data) if filter_log_msg_data(data) else data_dump
253
- _LOG.debug("[%s] ->: %s", websocket.remote_address, data_log)
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("[%s] =>: %s", websocket.remote_address, data_log)
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
- data_log = json.dumps(data) if filter_log_msg_data(data) else data_dump
306
- _LOG.debug("[%s] ->: %s", websocket.remote_address, data_log)
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]) -> bool:
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: True if a field was filtered, False otherwise
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 data:
886
+ if "msg_data" in log_upd:
883
887
  if (
884
- "attributes" in data["msg_data"]
885
- and media_player.Attributes.MEDIA_IMAGE_URL
886
- in data["msg_data"]["attributes"]
887
- and data["msg_data"]["attributes"][
888
- media_player.Attributes.MEDIA_IMAGE_URL
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
- data["msg_data"]["attributes"][
892
- media_player.Attributes.MEDIA_IMAGE_URL
893
- ] = "data:***"
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 media_player.Attributes.MEDIA_IMAGE_URL in item["attributes"]
901
- and item["attributes"][
902
- media_player.Attributes.MEDIA_IMAGE_URL
903
- ].startswith("data:")
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 False
906
+ return log_upd
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ucapi
3
- Version: 0.3.0
3
+ Version: 0.3.1
4
4
  Summary: Python wrapper for the Unfolded Circle Integration API
5
5
  Author-email: Unfolded Circle ApS <hello@unfoldedcircle.com>
6
6
  License: MPL-2.0
@@ -16,6 +16,7 @@ examples/remote.py
16
16
  examples/remote_ui_page.json
17
17
  examples/setup_flow.json
18
18
  examples/setup_flow.py
19
+ tests/test_api.py
19
20
  ucapi/__init__.py
20
21
  ucapi/_version.py
21
22
  ucapi/api.py
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