tinkly 0.2.0__tar.gz → 0.2.2__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.
- tinkly-0.2.2/PKG-INFO +64 -0
- {tinkly-0.2.0 → tinkly-0.2.2}/README.md +3 -0
- {tinkly-0.2.0 → tinkly-0.2.2}/pyproject.toml +22 -1
- {tinkly-0.2.0 → tinkly-0.2.2}/scripts/griffe_tinkly.py +7 -15
- {tinkly-0.2.0 → tinkly-0.2.2}/tests/test_documentation.py +19 -3
- {tinkly-0.2.0 → tinkly-0.2.2}/tinkly/client.py +8 -3
- tinkly-0.2.2/tinkly.egg-info/PKG-INFO +64 -0
- {tinkly-0.2.0 → tinkly-0.2.2}/uv.lock +2 -2
- tinkly-0.2.0/PKG-INFO +0 -5
- tinkly-0.2.0/tinkly.egg-info/PKG-INFO +0 -5
- {tinkly-0.2.0 → tinkly-0.2.2}/MANIFEST.in +0 -0
- {tinkly-0.2.0 → tinkly-0.2.2}/doc/contributing.md +0 -0
- {tinkly-0.2.0 → tinkly-0.2.2}/doc/index.md +0 -0
- {tinkly-0.2.0 → tinkly-0.2.2}/scripts/build_docs.py +0 -0
- {tinkly-0.2.0 → tinkly-0.2.2}/setup.cfg +0 -0
- {tinkly-0.2.0 → tinkly-0.2.2}/tests/test_authentication.py +0 -0
- {tinkly-0.2.0 → tinkly-0.2.2}/tests/test_client.py +0 -0
- {tinkly-0.2.0 → tinkly-0.2.2}/tests/test_client_authentication.py +0 -0
- {tinkly-0.2.0 → tinkly-0.2.2}/tests/test_discovery.py +0 -0
- {tinkly-0.2.0 → tinkly-0.2.2}/tests/test_frame.py +0 -0
- {tinkly-0.2.0 → tinkly-0.2.2}/tinkly/__init__.py +0 -0
- {tinkly-0.2.0 → tinkly-0.2.2}/tinkly/authentication.py +0 -0
- {tinkly-0.2.0 → tinkly-0.2.2}/tinkly/discovery.py +0 -0
- {tinkly-0.2.0 → tinkly-0.2.2}/tinkly/errors.py +0 -0
- {tinkly-0.2.0 → tinkly-0.2.2}/tinkly/frame.py +0 -0
- {tinkly-0.2.0 → tinkly-0.2.2}/tinkly.egg-info/SOURCES.txt +0 -0
- {tinkly-0.2.0 → tinkly-0.2.2}/tinkly.egg-info/dependency_links.txt +0 -0
- {tinkly-0.2.0 → tinkly-0.2.2}/tinkly.egg-info/top_level.txt +0 -0
tinkly-0.2.2/PKG-INFO
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tinkly
|
|
3
|
+
Version: 0.2.2
|
|
4
|
+
Summary: Low-level Twinkly discovery, authentication, HTTP, and realtime UDP transport
|
|
5
|
+
Author-email: Tom Ritchford <tom@swirly.com>
|
|
6
|
+
Project-URL: Documentation, https://rec.github.io/tinkly/
|
|
7
|
+
Project-URL: Repository, https://github.com/rec/tinkly
|
|
8
|
+
Project-URL: Issues, https://github.com/rec/tinkly/issues
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.15
|
|
14
|
+
Requires-Python: >=3.12
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
|
|
17
|
+
# tinkly: a tiny interface to Twinkly v2 lights
|
|
18
|
+
|
|
19
|
+
I love the Twinkly lights but the [xled library](https://github.com/scrool/xled) is
|
|
20
|
+
filled with complicated cruft which obscures the actual communication layer, and seems
|
|
21
|
+
to no longer be in development.
|
|
22
|
+
|
|
23
|
+
In particular, xled has a totally unnecessary ZMQ queue that
|
|
24
|
+
slows everything down and prevents two clients from existing.
|
|
25
|
+
|
|
26
|
+
I re-implemented just the Twinkly communication part, and left all the other
|
|
27
|
+
parts behind. In particular, there is no CLI - for that, you'll have to see my
|
|
28
|
+
upcoming general project, "lyte".
|
|
29
|
+
|
|
30
|
+
Check out https://xled-docs.readthedocs.io/en/latest/ for more information about the
|
|
31
|
+
API protocol.
|
|
32
|
+
|
|
33
|
+
## Device communication
|
|
34
|
+
|
|
35
|
+
This package provides UDP discovery, authentication, HTTP endpoint access for generation
|
|
36
|
+
2 Twinkly lights.
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
from tinkly.client import TwinklyClient
|
|
40
|
+
from tinkly.discovery import discover
|
|
41
|
+
from tinkly.frame import send_frame
|
|
42
|
+
|
|
43
|
+
devices = list(discover(timeout=2.0))
|
|
44
|
+
client = TwinklyClient(host=devices[0].ip_address)
|
|
45
|
+
info = client.get('gestalt', authenticated=False).data
|
|
46
|
+
client.mac = str(info['mac'])
|
|
47
|
+
token = client.authenticate()
|
|
48
|
+
|
|
49
|
+
client.set_realtime_mode()
|
|
50
|
+
frame = bytes((255, 0, 0)) * int(str(info['number_of_led']))
|
|
51
|
+
send_frame(client.host, token.value, frame)
|
|
52
|
+
client.set_off_mode()
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Frame buffers
|
|
56
|
+
|
|
57
|
+
`lyte` uses `numpy`, but I decided not to make that a dependency. Instead, we use
|
|
58
|
+
Python's buffer protocol `collections.abc.Buffer`. The input is C-style unsigned bytes
|
|
59
|
+
in RGB order, with three bytes per LED. Supported inputs include `bytes`, `bytearray`,
|
|
60
|
+
`array.array('B')`, `memoryview`, and C-contiguous NumPy `uint8` arrays.
|
|
61
|
+
|
|
62
|
+
## The API
|
|
63
|
+
|
|
64
|
+
https://rec.github.io/tinkly/
|
|
@@ -11,6 +11,9 @@ I re-implemented just the Twinkly communication part, and left all the other
|
|
|
11
11
|
parts behind. In particular, there is no CLI - for that, you'll have to see my
|
|
12
12
|
upcoming general project, "lyte".
|
|
13
13
|
|
|
14
|
+
Check out https://xled-docs.readthedocs.io/en/latest/ for more information about the
|
|
15
|
+
API protocol.
|
|
16
|
+
|
|
14
17
|
## Device communication
|
|
15
18
|
|
|
16
19
|
This package provides UDP discovery, authentication, HTTP endpoint access for generation
|
|
@@ -4,9 +4,23 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "tinkly"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.2"
|
|
8
8
|
description = "Low-level Twinkly discovery, authentication, HTTP, and realtime UDP transport"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
authors = [{ name = "Tom Ritchford", email = "tom@swirly.com" }]
|
|
9
11
|
requires-python = ">=3.12"
|
|
12
|
+
classifiers = [
|
|
13
|
+
"Programming Language :: Python :: 3",
|
|
14
|
+
"Programming Language :: Python :: 3.12",
|
|
15
|
+
"Programming Language :: Python :: 3.13",
|
|
16
|
+
"Programming Language :: Python :: 3.14",
|
|
17
|
+
"Programming Language :: Python :: 3.15",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
[project.urls]
|
|
21
|
+
Documentation = "https://rec.github.io/tinkly/"
|
|
22
|
+
Repository = "https://github.com/rec/tinkly"
|
|
23
|
+
Issues = "https://github.com/rec/tinkly/issues"
|
|
10
24
|
|
|
11
25
|
[tool.setuptools.packages.find]
|
|
12
26
|
where = ["."]
|
|
@@ -15,8 +29,12 @@ include = ["tinkly*"]
|
|
|
15
29
|
[tool.ruff.format]
|
|
16
30
|
quote-style = "single"
|
|
17
31
|
|
|
32
|
+
[tool.ruff.lint]
|
|
33
|
+
select = ["B", "E", "F", "I"]
|
|
34
|
+
|
|
18
35
|
[tool.pytest.ini_options]
|
|
19
36
|
pythonpath = ["."]
|
|
37
|
+
testpaths = ["tests"]
|
|
20
38
|
|
|
21
39
|
[dependency-groups]
|
|
22
40
|
dev = [
|
|
@@ -29,6 +47,9 @@ dev = [
|
|
|
29
47
|
"tyro",
|
|
30
48
|
]
|
|
31
49
|
|
|
50
|
+
[tool.uv]
|
|
51
|
+
exclude-newer = "1 week"
|
|
52
|
+
|
|
32
53
|
[tool.uv.sources]
|
|
33
54
|
reccy = { git = "ssh://git@github.com/rec/reccy.git", branch = "main" }
|
|
34
55
|
|
|
@@ -37,7 +37,7 @@ class EndpointProperties(griffe.Extension):
|
|
|
37
37
|
"""Add property labels, response types, and HTTP behavior to factory calls.
|
|
38
38
|
|
|
39
39
|
Only assignments on tinkly.client.TwinklyClient are processed. Preserve
|
|
40
|
-
|
|
40
|
+
endpoint-specific descriptions; shared behavior belongs on the class.
|
|
41
41
|
"""
|
|
42
42
|
if attr.parent is None or attr.parent.path != 'tinkly.client.TwinklyClient':
|
|
43
43
|
return
|
|
@@ -61,22 +61,14 @@ class EndpointProperties(griffe.Extension):
|
|
|
61
61
|
True,
|
|
62
62
|
)
|
|
63
63
|
writable = call.func.id == 'setter'
|
|
64
|
-
description =
|
|
65
|
-
f'HTTP endpoint `GET {path}`.\n\n'
|
|
66
|
-
'Each read sends a fresh request and returns a '
|
|
67
|
-
'`tinkly.client.TwinklyResponse` containing `http_status` and `data`.\n\n'
|
|
68
|
-
f'Authentication is {"required" if authenticated else "not required"}.'
|
|
69
|
-
)
|
|
64
|
+
description = f'`GET {path}`'
|
|
70
65
|
if writable:
|
|
71
|
-
description +=
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
else:
|
|
76
|
-
description += '\n\nThis property is read-only.'
|
|
77
|
-
description += '\n\nRequest errors propagate to the caller.'
|
|
66
|
+
description += f', `POST {path}`'
|
|
67
|
+
description += '.'
|
|
68
|
+
if not authenticated:
|
|
69
|
+
description += ' Authentication is not required.'
|
|
78
70
|
if attr.docstring is not None:
|
|
79
|
-
description
|
|
71
|
+
description = f'{attr.docstring.value}\n\n{description}'
|
|
80
72
|
|
|
81
73
|
attr.labels.difference_update({'class-attribute', 'instance-attribute'})
|
|
82
74
|
attr.labels.add('property')
|
|
@@ -28,15 +28,31 @@ class DocumentationTests(unittest.TestCase):
|
|
|
28
28
|
self.assertIsNone(brightness.value)
|
|
29
29
|
self.assertIn('GET led/out/brightness', brightness.docstring.value)
|
|
30
30
|
self.assertIn('POST led/out/brightness', brightness.docstring.value)
|
|
31
|
-
self.assertIn('dict[str, object]',
|
|
32
|
-
self.assertIn('Authentication is required',
|
|
31
|
+
self.assertIn('dict[str, object]', client.docstring.value)
|
|
32
|
+
self.assertIn('Authentication is required', client.docstring.value)
|
|
33
|
+
self.assertIn('Request errors propagate', client.docstring.value)
|
|
34
|
+
self.assertIn('read-only', client.docstring.value)
|
|
35
|
+
self.assertIn('http_status and data', client.docstring.value)
|
|
36
|
+
self.assertIn('output brightness', brightness.docstring.value)
|
|
37
|
+
for attribute in client.attributes.values():
|
|
38
|
+
if 'property' in attribute.labels:
|
|
39
|
+
with self.subTest(property=attribute.name):
|
|
40
|
+
self.assertNotIn('Each read sends', attribute.docstring.value)
|
|
41
|
+
self.assertNotIn(
|
|
42
|
+
'Authentication is required', attribute.docstring.value
|
|
43
|
+
)
|
|
44
|
+
self.assertNotIn(
|
|
45
|
+
'This property is read-only', attribute.docstring.value
|
|
46
|
+
)
|
|
47
|
+
self.assertNotIn(
|
|
48
|
+
'Request errors propagate', attribute.docstring.value
|
|
49
|
+
)
|
|
33
50
|
for attribute in (firmware, status):
|
|
34
51
|
with self.subTest(property=attribute.name):
|
|
35
52
|
self.assertEqual(attribute.labels, {'property'})
|
|
36
53
|
self.assertIn(
|
|
37
54
|
'Authentication is not required', attribute.docstring.value
|
|
38
55
|
)
|
|
39
|
-
self.assertIn('read-only', attribute.docstring.value)
|
|
40
56
|
self.assertIn('GET fw/version', firmware.docstring.value)
|
|
41
57
|
self.assertIn('GET status', status.docstring.value)
|
|
42
58
|
self.assertEqual(
|
|
@@ -81,9 +81,14 @@ class AuthToken:
|
|
|
81
81
|
class TwinklyClient:
|
|
82
82
|
"""Synchronous HTTP client for a single Twinkly device.
|
|
83
83
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
84
|
+
Each endpoint property read sends a fresh GET request and returns a
|
|
85
|
+
TwinklyResponse containing http_status and data. Properties marked writable
|
|
86
|
+
accept a dict[str, object] JSON body and POST assignments; assignment discards
|
|
87
|
+
the response, so use post() when it is needed. Other properties are read-only.
|
|
88
|
+
Authentication is required and obtained automatically unless stated otherwise.
|
|
89
|
+
Request errors propagate to the caller.
|
|
90
|
+
|
|
91
|
+
Each request opens and closes an HTTP connection;
|
|
87
92
|
retries, realtime frame pacing, and reconnection belong to the caller.
|
|
88
93
|
Serialize all use of a client; instances are not thread-safe.
|
|
89
94
|
"""
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tinkly
|
|
3
|
+
Version: 0.2.2
|
|
4
|
+
Summary: Low-level Twinkly discovery, authentication, HTTP, and realtime UDP transport
|
|
5
|
+
Author-email: Tom Ritchford <tom@swirly.com>
|
|
6
|
+
Project-URL: Documentation, https://rec.github.io/tinkly/
|
|
7
|
+
Project-URL: Repository, https://github.com/rec/tinkly
|
|
8
|
+
Project-URL: Issues, https://github.com/rec/tinkly/issues
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.15
|
|
14
|
+
Requires-Python: >=3.12
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
|
|
17
|
+
# tinkly: a tiny interface to Twinkly v2 lights
|
|
18
|
+
|
|
19
|
+
I love the Twinkly lights but the [xled library](https://github.com/scrool/xled) is
|
|
20
|
+
filled with complicated cruft which obscures the actual communication layer, and seems
|
|
21
|
+
to no longer be in development.
|
|
22
|
+
|
|
23
|
+
In particular, xled has a totally unnecessary ZMQ queue that
|
|
24
|
+
slows everything down and prevents two clients from existing.
|
|
25
|
+
|
|
26
|
+
I re-implemented just the Twinkly communication part, and left all the other
|
|
27
|
+
parts behind. In particular, there is no CLI - for that, you'll have to see my
|
|
28
|
+
upcoming general project, "lyte".
|
|
29
|
+
|
|
30
|
+
Check out https://xled-docs.readthedocs.io/en/latest/ for more information about the
|
|
31
|
+
API protocol.
|
|
32
|
+
|
|
33
|
+
## Device communication
|
|
34
|
+
|
|
35
|
+
This package provides UDP discovery, authentication, HTTP endpoint access for generation
|
|
36
|
+
2 Twinkly lights.
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
from tinkly.client import TwinklyClient
|
|
40
|
+
from tinkly.discovery import discover
|
|
41
|
+
from tinkly.frame import send_frame
|
|
42
|
+
|
|
43
|
+
devices = list(discover(timeout=2.0))
|
|
44
|
+
client = TwinklyClient(host=devices[0].ip_address)
|
|
45
|
+
info = client.get('gestalt', authenticated=False).data
|
|
46
|
+
client.mac = str(info['mac'])
|
|
47
|
+
token = client.authenticate()
|
|
48
|
+
|
|
49
|
+
client.set_realtime_mode()
|
|
50
|
+
frame = bytes((255, 0, 0)) * int(str(info['number_of_led']))
|
|
51
|
+
send_frame(client.host, token.value, frame)
|
|
52
|
+
client.set_off_mode()
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Frame buffers
|
|
56
|
+
|
|
57
|
+
`lyte` uses `numpy`, but I decided not to make that a dependency. Instead, we use
|
|
58
|
+
Python's buffer protocol `collections.abc.Buffer`. The input is C-style unsigned bytes
|
|
59
|
+
in RGB order, with three bytes per LED. Supported inputs include `bytes`, `bytearray`,
|
|
60
|
+
`array.array('B')`, `memoryview`, and C-contiguous NumPy `uint8` arrays.
|
|
61
|
+
|
|
62
|
+
## The API
|
|
63
|
+
|
|
64
|
+
https://rec.github.io/tinkly/
|
|
@@ -7,7 +7,7 @@ resolution-markers = [
|
|
|
7
7
|
]
|
|
8
8
|
|
|
9
9
|
[options]
|
|
10
|
-
exclude-newer = "2026-11-04T19:
|
|
10
|
+
exclude-newer = "2026-11-04T19:41:26.290514Z"
|
|
11
11
|
exclude-newer-span = "P1W"
|
|
12
12
|
|
|
13
13
|
[[package]]
|
|
@@ -327,7 +327,7 @@ wheels = [
|
|
|
327
327
|
|
|
328
328
|
[[package]]
|
|
329
329
|
name = "tinkly"
|
|
330
|
-
version = "0.2.
|
|
330
|
+
version = "0.2.2"
|
|
331
331
|
source = { editable = "." }
|
|
332
332
|
|
|
333
333
|
[package.dev-dependencies]
|
tinkly-0.2.0/PKG-INFO
DELETED
|
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
|