Skip to content

Commit 8ab5314

Browse files
authored
add scripting engine (#48)
1 parent 27a5ea2 commit 8ab5314

5 files changed

Lines changed: 157 additions & 1 deletion

File tree

roku/scripting.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import logging
2+
import os
3+
import re
4+
import time
5+
from collections import namedtuple
6+
7+
SCRIPT_RE = re.compile(r"(?P<command>\w+)(?:\:(?P<param>[\w\s]+))?(?:\@(?P<count>\d+))?(?:\*(?P<sleep>[\d\.]+))?") # noqa
8+
9+
Command = namedtuple('Command', ['command', 'param', 'count', 'sleep'])
10+
11+
logger = logging.getLogger('roku.scripting')
12+
13+
14+
def load_script(path, params=None, raw=False):
15+
if not os.path.exists(path):
16+
raise ValueError(f'script at {path} not found')
17+
with open(path) as infile:
18+
content = infile.read()
19+
if params:
20+
content = content.format(**params)
21+
if not raw:
22+
content = content.strip().split('\n')
23+
return content
24+
25+
26+
def parse_script(script):
27+
commands = []
28+
for line in script:
29+
if not line:
30+
continue
31+
m = SCRIPT_RE.match(line)
32+
if m:
33+
data = m.groupdict()
34+
data['count'] = int(data['count'] or 1)
35+
data['sleep'] = float(data['sleep']) if data['sleep'] else None
36+
commands.append(Command(**data))
37+
return commands
38+
39+
40+
def run_script(roku, script, sleep=0.5):
41+
for cmd in script:
42+
logger.debug(cmd)
43+
for i in range(cmd.count or 1):
44+
func = getattr(roku, cmd.command)
45+
if func:
46+
if cmd.param:
47+
func(cmd.param)
48+
else:
49+
func()
50+
time.sleep(cmd.sleep or sleep)

roku/tests/conftest.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import pytest
2+
3+
from roku import Application, Roku
4+
5+
6+
class Fauxku(Roku):
7+
8+
def __init__(self, *args, **kwargs):
9+
super(Fauxku, self).__init__(*args, **kwargs)
10+
self._calls = []
11+
12+
def _call(self, method, path, *args, **kwargs):
13+
self._calls.append((method, path, args, kwargs))
14+
return ''
15+
16+
def calls(self):
17+
return self._calls
18+
19+
def last_call(self):
20+
return self._calls[-1]
21+
22+
23+
@pytest.fixture
24+
def roku():
25+
return Fauxku('0.0.0.0')
26+
27+
28+
@pytest.fixture
29+
def apps(roku):
30+
faux_apps = [
31+
Application('11', '1.0.1', 'Fauxku Channel Store', roku),
32+
Application('22', '2.0.2', 'Faux Netflix', roku),
33+
Application('33', '3.0.3', 'Faux YouTube', roku),
34+
Application('44HL', '4.0.4', 'Faux Hulu', roku),
35+
]
36+
return faux_apps

roku/tests/scripts/testscript.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
literal:barbecue
2+
literal:{avar}

roku/tests/test_roku.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
# -*- coding: utf-8 -*-
21
import os
32
from urllib.parse import quote_plus
43

roku/tests/test_scripting.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import pytest
2+
3+
from roku import scripting
4+
5+
6+
SCRIPT_PATH = 'roku/tests/scripts/testscript.txt'
7+
8+
9+
def test_loading():
10+
scripting.load_script(SCRIPT_PATH)
11+
12+
13+
def test_loading_params():
14+
params = {
15+
'avar': 'here',
16+
'notavar': 'missing',
17+
}
18+
content = scripting.load_script(SCRIPT_PATH, params=params, raw=True)
19+
assert "literal:here" in content
20+
assert "literal:missing" not in content
21+
22+
23+
def test_loading_notfound():
24+
with pytest.raises(ValueError):
25+
scripting.load_script('thisisnotarealscript.txt')
26+
27+
28+
def test_parse_command_only():
29+
content = ('home',)
30+
script = scripting.parse_script(content)
31+
command = script[0]
32+
assert command == scripting.Command('home', None, 1, None)
33+
34+
35+
def test_parse_command_param():
36+
content = ('literal:barbecue',)
37+
script = scripting.parse_script(content)
38+
command = script[0]
39+
assert command == scripting.Command('literal', 'barbecue', 1, None)
40+
41+
42+
def test_parse_command_count():
43+
content = ('left@10',)
44+
script = scripting.parse_script(content)
45+
command = script[0]
46+
assert command == scripting.Command('left', None, 10, None)
47+
48+
49+
def test_parse_command_sleep():
50+
content = ('left*2',)
51+
script = scripting.parse_script(content)
52+
command = script[0]
53+
assert command == scripting.Command('left', None, 1, 2.0)
54+
55+
56+
def test_parse_command_all():
57+
content = ('literal:barbecue@3*5.1',)
58+
script = scripting.parse_script(content)
59+
command = script[0]
60+
assert command == scripting.Command('literal', 'barbecue', 3, 5.1)
61+
62+
63+
def test_run_script(roku):
64+
content = ('home', 'literal:x')
65+
script = scripting.parse_script(content)
66+
scripting.run_script(roku, script)
67+
calls = roku.calls()
68+
assert 'keypress/Home' in calls[0][1]
69+
assert 'keypress/Lit_x' in calls[1][1]

0 commit comments

Comments
 (0)