Skip to content

Commit 9d0de26

Browse files
authored
Add async mode of operation (#107)
* async support and basic CLI for discovery * update setup-uv action to v8 * correct setup-uv version * workflow updates * black formatting
1 parent 24df942 commit 9d0de26

File tree

18 files changed

+1544
-160
lines changed

18 files changed

+1544
-160
lines changed

.github/workflows/pythonpackage.yml

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
name: Python package
22

33
on:
4-
push:
54
workflow_dispatch:
5+
push:
6+
branches: [main]
7+
pull_request:
8+
branches: [main]
69

710
jobs:
811
build:
@@ -16,7 +19,9 @@ jobs:
1619
steps:
1720
- uses: actions/checkout@v6
1821
- name: Install uv
19-
uses: astral-sh/setup-uv@v6
22+
uses: "astral-sh/setup-uv@v8.0.0"
23+
with:
24+
cache-suffix: ${{ matrix.python-version }}
2025
- name: Set up Python ${{ matrix.python-version }}
2126
run: uv python install ${{ matrix.python-version }}
2227
- name: Install dependencies

README.md

Lines changed: 91 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,18 @@ or
1616
pip install roku
1717
```
1818

19+
To use the async client, install with the `async` extra:
20+
21+
```
22+
uv add "roku[async]"
23+
```
24+
25+
To use the CLI, install with the `cli` extra:
26+
27+
```
28+
uv add "roku[cli]"
29+
```
30+
1931
## Usage
2032

2133
### The Basics
@@ -123,6 +135,85 @@ What if I now want to watch *The Informant!*? Again, with the search open and wa
123135

124136
This will iterate over each character, sending it individually to the Roku.
125137

138+
## Async
139+
140+
An async client is available for use with `asyncio`. The `AsyncRoku` class provides the same functionality as the synchronous `Roku` class, but with async methods.
141+
142+
```python
143+
>>> import asyncio
144+
>>> from roku._async import AsyncRoku
145+
```
146+
147+
Create an instance and use it as an async context manager:
148+
149+
```python
150+
>>> async def main():
151+
... async with AsyncRoku('192.168.10.163') as roku:
152+
... await roku.home()
153+
... await roku.right()
154+
... await roku.select()
155+
...
156+
>>> asyncio.run(main())
157+
```
158+
159+
Properties like `apps`, `active_app`, and `device_info` are replaced with async methods:
160+
161+
```python
162+
>>> async def main():
163+
... async with AsyncRoku('192.168.10.163') as roku:
164+
... apps = await roku.get_apps()
165+
... current = await roku.get_active_app()
166+
... info = await roku.get_device_info()
167+
...
168+
>>> asyncio.run(main())
169+
```
170+
171+
Discovery works as an async class method:
172+
173+
```python
174+
>>> async def main():
175+
... rokus = await AsyncRoku.discover()
176+
... for roku in rokus:
177+
... async with roku:
178+
... info = await roku.get_device_info()
179+
... print(info.user_device_name)
180+
...
181+
>>> asyncio.run(main())
182+
```
183+
184+
## CLI
185+
186+
A command-line interface is available for device discovery. Install with the `cli` extra and use the `roku` command:
187+
188+
```
189+
$ roku discover
190+
192.168.10.163:8060
191+
```
192+
193+
Use `-i` / `--inspect` to display device details:
194+
195+
```
196+
$ roku discover -i
197+
192.168.10.163:8060
198+
Name: Living Room Roku
199+
Model: Roku Ultra (4800X)
200+
Type: Box
201+
Software: 11.5.0.4312
202+
Serial: YH009N854321
203+
```
204+
205+
You can adjust the discovery `--timeout` and `--retries`:
206+
207+
```
208+
$ roku discover --timeout 10 --retries 3
209+
```
210+
211+
The CLI also supports the async client with the `--async` flag:
212+
213+
```
214+
$ roku --async discover
215+
```
216+
126217
## Advanced Stuff
127218

128219
### Discovery
@@ -196,6 +287,4 @@ More information about input, touch, and sensors is available in the [Roku Exter
196287
## TODO
197288

198289
- Multitouch support.
199-
- A Flask proxy server that can listen to requests and forward them to devices on the local network. Control multiple devices at once, eh?
200-
- A server that mimics the Roku interface so you can make your own Roku-like stuff.
201290
- A task runner that will take a set of commands and run them with delays that are appropriate for most devices.

pyproject.toml

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,9 @@ authors = [
99
]
1010
requires-python = ">=3.10"
1111
dependencies = [
12-
"requests<3",
12+
"requests>=2.32,<4",
1313
]
14+
1415
classifiers = [
1516
"Development Status :: 5 - Production/Stable",
1617
"Intended Audience :: Developers",
@@ -24,18 +25,30 @@ classifiers = [
2425
"Programming Language :: Python :: 3.14",
2526
]
2627

28+
[project.optional-dependencies]
29+
async = ["aiohttp>=3.9,<4"]
30+
cli = ["click>=8,<9"]
31+
32+
[project.scripts]
33+
roku = "roku.cli:cli"
34+
2735
[project.urls]
2836
Homepage = "https://github.com/jcarbaugh/python-roku"
2937

3038
[dependency-groups]
3139
dev = [
40+
"aiohttp>=3.9,<4",
3241
"black",
3342
"flake8",
3443
"pytest",
44+
"pytest-asyncio",
3545
"pytest-mock",
3646
"twine",
3747
]
3848

49+
[tool.pytest.ini_options]
50+
asyncio_mode = "auto"
51+
3952
[build-system]
4053
requires = ["hatchling"]
4154
build-backend = "hatchling.build"

roku/__init__.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,10 @@
1-
from roku.core import Roku, Application, Channel, RokuException, __version__ # noqa
1+
from roku.core import Roku, __version__ # noqa
2+
from roku.models import Application, Channel, RokuException # noqa
3+
4+
5+
def __getattr__(name):
6+
if name == "AsyncRoku":
7+
from roku._async import AsyncRoku
8+
9+
return AsyncRoku
10+
raise AttributeError(f"module 'roku' has no attribute {name}")

roku/_async/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from roku._async.core import AsyncRoku # noqa

0 commit comments

Comments
 (0)