ucapi 0.5.2__tar.gz → 0.6.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.
- {ucapi-0.5.2 → ucapi-0.6.0}/CHANGELOG.md +11 -0
- {ucapi-0.5.2/ucapi.egg-info → ucapi-0.6.0}/PKG-INFO +2 -2
- {ucapi-0.5.2 → ucapi-0.6.0}/pyproject.toml +1 -1
- ucapi-0.6.0/tests/test_media_player.py +159 -0
- ucapi-0.6.0/tests/test_paging.py +131 -0
- {ucapi-0.5.2 → ucapi-0.6.0}/ucapi/__init__.py +16 -1
- ucapi-0.6.0/ucapi/_version.py +24 -0
- {ucapi-0.5.2 → ucapi-0.6.0}/ucapi/api.py +212 -17
- {ucapi-0.5.2 → ucapi-0.6.0}/ucapi/api_definitions.py +145 -18
- {ucapi-0.5.2 → ucapi-0.6.0}/ucapi/button.py +11 -4
- {ucapi-0.5.2 → ucapi-0.6.0}/ucapi/climate.py +14 -7
- {ucapi-0.5.2 → ucapi-0.6.0}/ucapi/cover.py +14 -7
- {ucapi-0.5.2 → ucapi-0.6.0}/ucapi/entities.py +4 -0
- {ucapi-0.5.2 → ucapi-0.6.0}/ucapi/entity.py +31 -3
- {ucapi-0.5.2 → ucapi-0.6.0}/ucapi/ir_emitter.py +13 -7
- {ucapi-0.5.2 → ucapi-0.6.0}/ucapi/light.py +14 -7
- ucapi-0.6.0/ucapi/media_player.py +738 -0
- ucapi-0.6.0/ucapi/msg_definitions.py +68 -0
- {ucapi-0.5.2 → ucapi-0.6.0}/ucapi/remote.py +13 -6
- {ucapi-0.5.2 → ucapi-0.6.0}/ucapi/select.py +13 -7
- {ucapi-0.5.2 → ucapi-0.6.0}/ucapi/sensor.py +14 -7
- {ucapi-0.5.2 → ucapi-0.6.0}/ucapi/switch.py +14 -7
- {ucapi-0.5.2 → ucapi-0.6.0}/ucapi/ui.py +2 -2
- {ucapi-0.5.2 → ucapi-0.6.0}/ucapi/voice_assistant.py +15 -8
- {ucapi-0.5.2 → ucapi-0.6.0/ucapi.egg-info}/PKG-INFO +2 -2
- {ucapi-0.5.2 → ucapi-0.6.0}/ucapi.egg-info/SOURCES.txt +3 -0
- ucapi-0.5.2/ucapi/_version.py +0 -34
- ucapi-0.5.2/ucapi/media_player.py +0 -233
- {ucapi-0.5.2 → ucapi-0.6.0}/CONTRIBUTING.md +0 -0
- {ucapi-0.5.2 → ucapi-0.6.0}/LICENSE +0 -0
- {ucapi-0.5.2 → ucapi-0.6.0}/README.md +0 -0
- {ucapi-0.5.2 → ucapi-0.6.0}/docs/code_guidelines.md +0 -0
- {ucapi-0.5.2 → ucapi-0.6.0}/docs/setup.md +0 -0
- {ucapi-0.5.2 → ucapi-0.6.0}/examples/README.md +0 -0
- {ucapi-0.5.2 → ucapi-0.6.0}/examples/hello_integration.json +0 -0
- {ucapi-0.5.2 → ucapi-0.6.0}/examples/hello_integration.py +0 -0
- {ucapi-0.5.2 → ucapi-0.6.0}/examples/remote.json +0 -0
- {ucapi-0.5.2 → ucapi-0.6.0}/examples/remote.py +0 -0
- {ucapi-0.5.2 → ucapi-0.6.0}/examples/remote_ui_page.json +0 -0
- {ucapi-0.5.2 → ucapi-0.6.0}/examples/setup_flow.json +0 -0
- {ucapi-0.5.2 → ucapi-0.6.0}/examples/setup_flow.py +0 -0
- {ucapi-0.5.2 → ucapi-0.6.0}/examples/voice.json +0 -0
- {ucapi-0.5.2 → ucapi-0.6.0}/examples/voice.py +0 -0
- {ucapi-0.5.2 → ucapi-0.6.0}/requirements.txt +0 -0
- {ucapi-0.5.2 → ucapi-0.6.0}/scripts/compile_protos.py +0 -0
- {ucapi-0.5.2 → ucapi-0.6.0}/setup.cfg +0 -0
- {ucapi-0.5.2 → ucapi-0.6.0}/test-requirements.txt +0 -0
- {ucapi-0.5.2 → ucapi-0.6.0}/tests/test_api.py +0 -0
- {ucapi-0.5.2 → ucapi-0.6.0}/tests/test_voice_assistant.py +0 -0
- {ucapi-0.5.2 → ucapi-0.6.0}/ucapi/proto/__init__.py +0 -0
- {ucapi-0.5.2 → ucapi-0.6.0}/ucapi/proto/ucr_integration_voice.proto +0 -0
- {ucapi-0.5.2 → ucapi-0.6.0}/ucapi/proto/ucr_integration_voice_pb2.py +0 -0
- {ucapi-0.5.2 → ucapi-0.6.0}/ucapi/proto/ucr_integration_voice_pb2.pyi +0 -0
- {ucapi-0.5.2 → ucapi-0.6.0}/ucapi/voice_stream.py +0 -0
- {ucapi-0.5.2 → ucapi-0.6.0}/ucapi.egg-info/dependency_links.txt +0 -0
- {ucapi-0.5.2 → ucapi-0.6.0}/ucapi.egg-info/requires.txt +0 -0
- {ucapi-0.5.2 → ucapi-0.6.0}/ucapi.egg-info/top_level.txt +0 -0
|
@@ -11,6 +11,17 @@ _Changes in the next release_
|
|
|
11
11
|
|
|
12
12
|
---
|
|
13
13
|
|
|
14
|
+
## v0.6.0 - 2026-04-10
|
|
15
|
+
### Breaking Changes
|
|
16
|
+
- Renamed `MediaType` to `MediaContentType` and changed enums to lowercase. See media-player entity documentation for more information ([#50](https://github.com/unfoldedcircle/integration-python-library/pull/50)).
|
|
17
|
+
- Changed `str, Enum` to new Python 3.11 `StrEnum` class ([#54](https://github.com/unfoldedcircle/integration-python-library/pull/54)).
|
|
18
|
+
- All entity constructors require named parameters for the optional fields ([#49](https://github.com/unfoldedcircle/integration-python-library/pull/49), [#54](https://github.com/unfoldedcircle/integration-python-library/pull/54)).
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
- Media browsing and searching features to media-player entity ([#50](https://github.com/unfoldedcircle/integration-python-library/pull/50)).
|
|
22
|
+
- Allow integrations to provide entity icon and description ([#55](https://github.com/unfoldedcircle/integration-python-library/pull/55)).
|
|
23
|
+
|
|
24
|
+
|
|
14
25
|
## v0.5.2 - 2026-01-30
|
|
15
26
|
### Added
|
|
16
27
|
- Add Select Entity support by @JackJPowell ([#44](https://github.com/unfoldedcircle/integration-python-library/pull/44)).
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ucapi
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.0
|
|
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
|
|
@@ -10,7 +10,7 @@ Project-URL: Bug Reports, https://github.com/unfoldedcircle/integration-python-l
|
|
|
10
10
|
Project-URL: Discord, http://unfolded.chat/
|
|
11
11
|
Project-URL: Forum, https://unfolded.community/
|
|
12
12
|
Platform: any
|
|
13
|
-
Classifier: Development Status ::
|
|
13
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
14
14
|
Classifier: Intended Audience :: Developers
|
|
15
15
|
Classifier: License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)
|
|
16
16
|
Classifier: Operating System :: OS Independent
|
|
@@ -10,7 +10,7 @@ authors = [
|
|
|
10
10
|
license = {text = "MPL-2.0"}
|
|
11
11
|
description = "Python wrapper for the Unfolded Circle Integration API"
|
|
12
12
|
classifiers = [
|
|
13
|
-
"Development Status ::
|
|
13
|
+
"Development Status :: 5 - Production/Stable",
|
|
14
14
|
"Intended Audience :: Developers",
|
|
15
15
|
"License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)",
|
|
16
16
|
"Operating System :: OS Independent",
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tests for media player entity.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import unittest
|
|
6
|
+
from ucapi.media_player import SearchMediaFilter, MediaClass, BrowseMediaItem
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class TestMediaPlayer(unittest.TestCase):
|
|
10
|
+
"""Media player tests."""
|
|
11
|
+
|
|
12
|
+
def test_search_media_filter_media_classes(self):
|
|
13
|
+
"""Test SearchMediaFilter media_classes with standard and custom values."""
|
|
14
|
+
# Test with standard MediaClass
|
|
15
|
+
smf = SearchMediaFilter(media_classes=[MediaClass.ALBUM, MediaClass.TRACK])
|
|
16
|
+
self.assertEqual(smf.media_classes, [MediaClass.ALBUM, MediaClass.TRACK])
|
|
17
|
+
|
|
18
|
+
# Test with strings that match MediaClass
|
|
19
|
+
smf = SearchMediaFilter(media_classes=["album", "track"])
|
|
20
|
+
self.assertEqual(smf.media_classes, [MediaClass.ALBUM, MediaClass.TRACK])
|
|
21
|
+
|
|
22
|
+
# Test with custom string values
|
|
23
|
+
try:
|
|
24
|
+
smf = SearchMediaFilter(media_classes=["custom_class", "another_one"])
|
|
25
|
+
self.assertEqual(smf.media_classes, ["custom_class", "another_one"])
|
|
26
|
+
except ValueError as e:
|
|
27
|
+
self.fail(
|
|
28
|
+
f"SearchMediaFilter raised ValueError for custom media classes: {e}"
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
def test_search_media_filter_mixed_classes(self):
|
|
32
|
+
"""Test SearchMediaFilter with a mix of MediaClass and custom strings."""
|
|
33
|
+
try:
|
|
34
|
+
smf = SearchMediaFilter(media_classes=[MediaClass.ALBUM, "custom_class"])
|
|
35
|
+
self.assertEqual(smf.media_classes, [MediaClass.ALBUM, "custom_class"])
|
|
36
|
+
except ValueError as e:
|
|
37
|
+
self.fail(
|
|
38
|
+
f"SearchMediaFilter raised ValueError for mixed media classes: {e}"
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
def test_search_media_filter_none(self):
|
|
42
|
+
"""Test SearchMediaFilter with None media_classes."""
|
|
43
|
+
smf = SearchMediaFilter(media_classes=None)
|
|
44
|
+
self.assertIsNone(smf.media_classes)
|
|
45
|
+
|
|
46
|
+
def test_search_media_filter_from_dict(self):
|
|
47
|
+
"""Test SearchMediaFilter.from_dict with custom values."""
|
|
48
|
+
data = {
|
|
49
|
+
"media_classes": ["album", "custom_class"],
|
|
50
|
+
"artist": "Some Artist",
|
|
51
|
+
"album": "Some Album",
|
|
52
|
+
}
|
|
53
|
+
try:
|
|
54
|
+
smf = SearchMediaFilter.from_dict(data)
|
|
55
|
+
self.assertEqual(smf.media_classes, [MediaClass.ALBUM, "custom_class"])
|
|
56
|
+
self.assertEqual(smf.artist, "Some Artist")
|
|
57
|
+
self.assertEqual(smf.album, "Some Album")
|
|
58
|
+
except ValueError as e:
|
|
59
|
+
self.fail(
|
|
60
|
+
f"SearchMediaFilter.from_dict raised ValueError for custom media classes: {e}"
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
def test_browse_media_item_validation_mandatory(self):
|
|
64
|
+
"""Test BrowseMediaItem mandatory field validation."""
|
|
65
|
+
# Valid mandatory fields
|
|
66
|
+
item = BrowseMediaItem(media_id="id1", title="Title")
|
|
67
|
+
self.assertEqual(item.media_id, "id1")
|
|
68
|
+
self.assertEqual(item.title, "Title")
|
|
69
|
+
|
|
70
|
+
# media_id empty
|
|
71
|
+
with self.assertRaisesRegex(
|
|
72
|
+
ValueError, "media_id must be at least 1 characters"
|
|
73
|
+
):
|
|
74
|
+
BrowseMediaItem(media_id="", title="Title")
|
|
75
|
+
|
|
76
|
+
# media_id too long
|
|
77
|
+
with self.assertRaisesRegex(
|
|
78
|
+
ValueError, "media_id must be at most 255 characters"
|
|
79
|
+
):
|
|
80
|
+
BrowseMediaItem(media_id="a" * 256, title="Title")
|
|
81
|
+
|
|
82
|
+
# media_id wrong type
|
|
83
|
+
with self.assertRaisesRegex(TypeError, "media_id must be str, got int"):
|
|
84
|
+
BrowseMediaItem(media_id=123, title="Title")
|
|
85
|
+
|
|
86
|
+
# title empty
|
|
87
|
+
with self.assertRaisesRegex(ValueError, "title must be at least 1 characters"):
|
|
88
|
+
BrowseMediaItem(media_id="id1", title="")
|
|
89
|
+
|
|
90
|
+
# title too long
|
|
91
|
+
with self.assertRaisesRegex(ValueError, "title must be at most 255 characters"):
|
|
92
|
+
BrowseMediaItem(media_id="id1", title="a" * 256)
|
|
93
|
+
|
|
94
|
+
def test_browse_media_item_validation_optional(self):
|
|
95
|
+
"""Test BrowseMediaItem optional field validation."""
|
|
96
|
+
# subtitle
|
|
97
|
+
with self.assertRaisesRegex(
|
|
98
|
+
ValueError, "subtitle must be at least 1 characters"
|
|
99
|
+
):
|
|
100
|
+
BrowseMediaItem(media_id="id1", title="Title", subtitle="")
|
|
101
|
+
with self.assertRaisesRegex(
|
|
102
|
+
ValueError, "subtitle must be at most 255 characters"
|
|
103
|
+
):
|
|
104
|
+
BrowseMediaItem(media_id="id1", title="Title", subtitle="a" * 256)
|
|
105
|
+
|
|
106
|
+
# artist
|
|
107
|
+
with self.assertRaisesRegex(ValueError, "artist must be at least 1 characters"):
|
|
108
|
+
BrowseMediaItem(media_id="id1", title="Title", artist="")
|
|
109
|
+
with self.assertRaisesRegex(
|
|
110
|
+
ValueError, "artist must be at most 255 characters"
|
|
111
|
+
):
|
|
112
|
+
BrowseMediaItem(media_id="id1", title="Title", artist="a" * 256)
|
|
113
|
+
|
|
114
|
+
# album
|
|
115
|
+
with self.assertRaisesRegex(ValueError, "album must be at least 1 characters"):
|
|
116
|
+
BrowseMediaItem(media_id="id1", title="Title", album="")
|
|
117
|
+
with self.assertRaisesRegex(ValueError, "album must be at most 255 characters"):
|
|
118
|
+
BrowseMediaItem(media_id="id1", title="Title", album="a" * 256)
|
|
119
|
+
|
|
120
|
+
# media_class (only when it's a string)
|
|
121
|
+
# Note: media class is allowed to be empty!
|
|
122
|
+
BrowseMediaItem(media_id="id1", title="Title", media_class="")
|
|
123
|
+
|
|
124
|
+
with self.assertRaisesRegex(
|
|
125
|
+
ValueError, "media_class must be at most 255 characters"
|
|
126
|
+
):
|
|
127
|
+
BrowseMediaItem(media_id="id1", title="Title", media_class="a" * 256)
|
|
128
|
+
# Verify it accepts MediaClass enum without error
|
|
129
|
+
BrowseMediaItem(media_id="id1", title="Title", media_class=MediaClass.ALBUM)
|
|
130
|
+
|
|
131
|
+
# media_type (only when it's a string)
|
|
132
|
+
# Note: media type is allowed to be empty!
|
|
133
|
+
BrowseMediaItem(media_id="id1", title="Title", media_type="")
|
|
134
|
+
|
|
135
|
+
with self.assertRaisesRegex(
|
|
136
|
+
ValueError, "media_type must be at most 255 characters"
|
|
137
|
+
):
|
|
138
|
+
BrowseMediaItem(media_id="id1", title="Title", media_type="a" * 256)
|
|
139
|
+
|
|
140
|
+
def test_browse_media_item_validation_thumbnail(self):
|
|
141
|
+
"""Test BrowseMediaItem thumbnail field validation."""
|
|
142
|
+
# Valid length
|
|
143
|
+
BrowseMediaItem(media_id="id1", title="Title", thumbnail="a" * 32768)
|
|
144
|
+
|
|
145
|
+
# Too short (empty)
|
|
146
|
+
with self.assertRaisesRegex(
|
|
147
|
+
ValueError, "thumbnail must be at least 1 characters"
|
|
148
|
+
):
|
|
149
|
+
BrowseMediaItem(media_id="id1", title="Title", thumbnail="")
|
|
150
|
+
|
|
151
|
+
# Too long
|
|
152
|
+
with self.assertRaisesRegex(
|
|
153
|
+
ValueError, "thumbnail must be at most 32768 characters"
|
|
154
|
+
):
|
|
155
|
+
BrowseMediaItem(media_id="id1", title="Title", thumbnail="a" * 32769)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
if __name__ == "__main__":
|
|
159
|
+
unittest.main()
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"""Unit tests for Paging and Pagination classes."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import unittest
|
|
5
|
+
from dataclasses import asdict
|
|
6
|
+
|
|
7
|
+
from ucapi.api_definitions import Pagination, Paging
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class TestPaging(unittest.TestCase):
|
|
11
|
+
"""Test cases for the Paging class."""
|
|
12
|
+
|
|
13
|
+
def test_paging_default(self):
|
|
14
|
+
"""Test default Paging instantiation."""
|
|
15
|
+
paging = Paging()
|
|
16
|
+
self.assertEqual(paging.page, 1)
|
|
17
|
+
self.assertEqual(paging.limit, 10)
|
|
18
|
+
self.assertEqual(paging.offset, 0)
|
|
19
|
+
|
|
20
|
+
def test_paging_custom(self):
|
|
21
|
+
"""Test custom Paging instantiation."""
|
|
22
|
+
paging = Paging(page=2, limit=20)
|
|
23
|
+
self.assertEqual(paging.page, 2)
|
|
24
|
+
self.assertEqual(paging.limit, 20)
|
|
25
|
+
self.assertEqual(paging.offset, 20)
|
|
26
|
+
|
|
27
|
+
def test_paging_invalid_page(self):
|
|
28
|
+
"""Test validation for invalid page number."""
|
|
29
|
+
with self.assertRaises(ValueError) as cm:
|
|
30
|
+
Paging(page=0)
|
|
31
|
+
self.assertIn("Invalid page: 0", str(cm.exception))
|
|
32
|
+
|
|
33
|
+
with self.assertRaises(ValueError):
|
|
34
|
+
Paging(page=-1)
|
|
35
|
+
|
|
36
|
+
def test_paging_invalid_limit(self):
|
|
37
|
+
"""Test validation for invalid limit."""
|
|
38
|
+
with self.assertRaises(ValueError) as cm:
|
|
39
|
+
Paging(limit=0)
|
|
40
|
+
self.assertIn("Invalid limit: 0", str(cm.exception))
|
|
41
|
+
|
|
42
|
+
with self.assertRaises(ValueError):
|
|
43
|
+
Paging(limit=-1)
|
|
44
|
+
with self.assertRaises(ValueError):
|
|
45
|
+
Paging(limit=10000)
|
|
46
|
+
|
|
47
|
+
def test_paging_from_dict(self):
|
|
48
|
+
"""Test constructing Paging from a dictionary."""
|
|
49
|
+
data = {"page": 3, "limit": 50}
|
|
50
|
+
paging = Paging.from_dict(data)
|
|
51
|
+
self.assertEqual(paging.page, 3)
|
|
52
|
+
self.assertEqual(paging.limit, 50)
|
|
53
|
+
|
|
54
|
+
def test_paging_from_dict_defaults(self):
|
|
55
|
+
"""Test constructing Paging from an empty dictionary using defaults."""
|
|
56
|
+
paging = Paging.from_dict({})
|
|
57
|
+
self.assertEqual(paging.page, 1)
|
|
58
|
+
self.assertEqual(paging.limit, 10)
|
|
59
|
+
|
|
60
|
+
def test_paging_serialization(self):
|
|
61
|
+
"""Test Paging JSON serialization."""
|
|
62
|
+
paging = Paging(page=2, limit=25)
|
|
63
|
+
serialized = asdict(paging)
|
|
64
|
+
self.assertEqual(serialized, {"page": 2, "limit": 25})
|
|
65
|
+
|
|
66
|
+
# Verify JSON round-trip
|
|
67
|
+
json_str = json.dumps(serialized)
|
|
68
|
+
self.assertEqual(json.loads(json_str), {"page": 2, "limit": 25})
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class TestPagination(unittest.TestCase):
|
|
72
|
+
"""Test cases for the Pagination class."""
|
|
73
|
+
|
|
74
|
+
def test_pagination_instantiation(self):
|
|
75
|
+
"""Test Pagination instantiation with all fields."""
|
|
76
|
+
pagination = Pagination(page=1, limit=10, count=100)
|
|
77
|
+
self.assertEqual(pagination.page, 1)
|
|
78
|
+
self.assertEqual(pagination.limit, 10)
|
|
79
|
+
self.assertEqual(pagination.count, 100)
|
|
80
|
+
|
|
81
|
+
def test_pagination_no_count(self):
|
|
82
|
+
"""Test Pagination instantiation without count."""
|
|
83
|
+
pagination = Pagination(page=2, limit=20)
|
|
84
|
+
self.assertEqual(pagination.page, 2)
|
|
85
|
+
self.assertEqual(pagination.limit, 20)
|
|
86
|
+
self.assertIsNone(pagination.count)
|
|
87
|
+
|
|
88
|
+
def test_pagination_invalid_page(self):
|
|
89
|
+
"""Test validation for invalid page number."""
|
|
90
|
+
with self.assertRaises(ValueError) as cm:
|
|
91
|
+
Pagination(page=0, limit=10)
|
|
92
|
+
self.assertIn("page must be >= 1", str(cm.exception))
|
|
93
|
+
|
|
94
|
+
def test_pagination_invalid_limit(self):
|
|
95
|
+
"""Test validation for invalid limit."""
|
|
96
|
+
with self.assertRaises(ValueError) as cm:
|
|
97
|
+
Pagination(page=1, limit=-1)
|
|
98
|
+
self.assertIn("Invalid limit", str(cm.exception))
|
|
99
|
+
|
|
100
|
+
def test_pagination_limit_out_of_range(self):
|
|
101
|
+
"""Test validation for invalid limit."""
|
|
102
|
+
with self.assertRaises(ValueError) as cm:
|
|
103
|
+
Pagination(page=1, limit=10000)
|
|
104
|
+
self.assertIn("Invalid limit", str(cm.exception))
|
|
105
|
+
|
|
106
|
+
def test_pagination_invalid_count(self):
|
|
107
|
+
"""Test validation for invalid count."""
|
|
108
|
+
with self.assertRaises(ValueError) as cm:
|
|
109
|
+
Pagination(page=1, limit=10, count=-1)
|
|
110
|
+
self.assertIn("count cannot be negative", str(cm.exception))
|
|
111
|
+
|
|
112
|
+
def test_pagination_serialization(self):
|
|
113
|
+
"""Test Pagination JSON serialization."""
|
|
114
|
+
pagination = Pagination(page=1, limit=10, count=100)
|
|
115
|
+
serialized = asdict(pagination)
|
|
116
|
+
self.assertEqual(serialized, {"page": 1, "limit": 10, "count": 100})
|
|
117
|
+
|
|
118
|
+
def test_pagination_serialization_no_count(self):
|
|
119
|
+
"""Test Pagination JSON serialization when count is None."""
|
|
120
|
+
pagination = Pagination(page=2, limit=20)
|
|
121
|
+
json_data = asdict(pagination)
|
|
122
|
+
|
|
123
|
+
# Note: In JS/TS, undefined keys are omitted during JSON.stringify.
|
|
124
|
+
# In Python, None becomes `null` in JSON.
|
|
125
|
+
# This is not an issue: the Remote core service treats `null` as "not existing".
|
|
126
|
+
self.assertIn("count", json_data)
|
|
127
|
+
self.assertIsNone(json_data["count"])
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
if __name__ == "__main__":
|
|
131
|
+
unittest.main()
|
|
@@ -19,6 +19,8 @@ from .api_definitions import ( # isort:skip # noqa: F401
|
|
|
19
19
|
DriverSetupRequest,
|
|
20
20
|
Events,
|
|
21
21
|
IntegrationSetupError,
|
|
22
|
+
Paging,
|
|
23
|
+
Pagination,
|
|
22
24
|
RequestUserConfirmation,
|
|
23
25
|
RequestUserInput,
|
|
24
26
|
SetupAction,
|
|
@@ -42,7 +44,20 @@ from .button import Button # noqa: F401
|
|
|
42
44
|
from .climate import Climate # noqa: F401
|
|
43
45
|
from .cover import Cover # noqa: F401
|
|
44
46
|
from .light import Light # noqa: F401
|
|
45
|
-
from .media_player import
|
|
47
|
+
from .media_player import ( # noqa: F401
|
|
48
|
+
BrowseMediaItem,
|
|
49
|
+
BrowseOptions,
|
|
50
|
+
BrowseResults,
|
|
51
|
+
MediaClass,
|
|
52
|
+
MediaContentType,
|
|
53
|
+
MediaPlayAction,
|
|
54
|
+
MediaPlayer,
|
|
55
|
+
RepeatMode,
|
|
56
|
+
SearchMediaFilter,
|
|
57
|
+
SearchMediaItem,
|
|
58
|
+
SearchOptions,
|
|
59
|
+
SearchResults,
|
|
60
|
+
)
|
|
46
61
|
from .remote import Remote # noqa: F401
|
|
47
62
|
from .select import Select # noqa: F401
|
|
48
63
|
from .sensor import Sensor # noqa: F401
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# file generated by vcs-versioning
|
|
2
|
+
# don't change, don't track in version control
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"__version__",
|
|
7
|
+
"__version_tuple__",
|
|
8
|
+
"version",
|
|
9
|
+
"version_tuple",
|
|
10
|
+
"__commit_id__",
|
|
11
|
+
"commit_id",
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
version: str
|
|
15
|
+
__version__: str
|
|
16
|
+
__version_tuple__: tuple[int | str, ...]
|
|
17
|
+
version_tuple: tuple[int | str, ...]
|
|
18
|
+
commit_id: str | None
|
|
19
|
+
__commit_id__: str | None
|
|
20
|
+
|
|
21
|
+
__version__ = version = '0.6.0'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 6, 0)
|
|
23
|
+
|
|
24
|
+
__commit_id__ = commit_id = 'g755510031'
|