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.
Files changed (28) hide show
  1. tinkly-0.2.2/PKG-INFO +64 -0
  2. {tinkly-0.2.0 → tinkly-0.2.2}/README.md +3 -0
  3. {tinkly-0.2.0 → tinkly-0.2.2}/pyproject.toml +22 -1
  4. {tinkly-0.2.0 → tinkly-0.2.2}/scripts/griffe_tinkly.py +7 -15
  5. {tinkly-0.2.0 → tinkly-0.2.2}/tests/test_documentation.py +19 -3
  6. {tinkly-0.2.0 → tinkly-0.2.2}/tinkly/client.py +8 -3
  7. tinkly-0.2.2/tinkly.egg-info/PKG-INFO +64 -0
  8. {tinkly-0.2.0 → tinkly-0.2.2}/uv.lock +2 -2
  9. tinkly-0.2.0/PKG-INFO +0 -5
  10. tinkly-0.2.0/tinkly.egg-info/PKG-INFO +0 -5
  11. {tinkly-0.2.0 → tinkly-0.2.2}/MANIFEST.in +0 -0
  12. {tinkly-0.2.0 → tinkly-0.2.2}/doc/contributing.md +0 -0
  13. {tinkly-0.2.0 → tinkly-0.2.2}/doc/index.md +0 -0
  14. {tinkly-0.2.0 → tinkly-0.2.2}/scripts/build_docs.py +0 -0
  15. {tinkly-0.2.0 → tinkly-0.2.2}/setup.cfg +0 -0
  16. {tinkly-0.2.0 → tinkly-0.2.2}/tests/test_authentication.py +0 -0
  17. {tinkly-0.2.0 → tinkly-0.2.2}/tests/test_client.py +0 -0
  18. {tinkly-0.2.0 → tinkly-0.2.2}/tests/test_client_authentication.py +0 -0
  19. {tinkly-0.2.0 → tinkly-0.2.2}/tests/test_discovery.py +0 -0
  20. {tinkly-0.2.0 → tinkly-0.2.2}/tests/test_frame.py +0 -0
  21. {tinkly-0.2.0 → tinkly-0.2.2}/tinkly/__init__.py +0 -0
  22. {tinkly-0.2.0 → tinkly-0.2.2}/tinkly/authentication.py +0 -0
  23. {tinkly-0.2.0 → tinkly-0.2.2}/tinkly/discovery.py +0 -0
  24. {tinkly-0.2.0 → tinkly-0.2.2}/tinkly/errors.py +0 -0
  25. {tinkly-0.2.0 → tinkly-0.2.2}/tinkly/frame.py +0 -0
  26. {tinkly-0.2.0 → tinkly-0.2.2}/tinkly.egg-info/SOURCES.txt +0 -0
  27. {tinkly-0.2.0 → tinkly-0.2.2}/tinkly.egg-info/dependency_links.txt +0 -0
  28. {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.0"
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
- any endpoint-specific attribute docstring after the generated details.
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
- f'\n\nAssign a `dict[str, object]` JSON body to send `POST {path}`. '
73
- 'Assignment does not return the response; use `post()` when needed.'
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 += f'\n\n{attr.docstring.value}'
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]', brightness.docstring.value)
32
- self.assertIn('Authentication is required', brightness.docstring.value)
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
- Endpoint properties return TwinklyResponse records on every read. Writable
85
- properties POST assigned JSON dictionaries. Authentication is automatic
86
- for protected endpoints. Each request opens and closes an HTTP connection;
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:29:17.139037Z"
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.0"
330
+ version = "0.2.2"
331
331
  source = { editable = "." }
332
332
 
333
333
  [package.dev-dependencies]
tinkly-0.2.0/PKG-INFO DELETED
@@ -1,5 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: tinkly
3
- Version: 0.2.0
4
- Summary: Low-level Twinkly discovery, authentication, HTTP, and realtime UDP transport
5
- Requires-Python: >=3.12
@@ -1,5 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: tinkly
3
- Version: 0.2.0
4
- Summary: Low-level Twinkly discovery, authentication, HTTP, and realtime UDP transport
5
- Requires-Python: >=3.12
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