Skip to content

Commit 0ded910

Browse files
committed
Add a way to discover Evas on your network programmatically
1 parent b2a7fd6 commit 0ded910

12 files changed

+293
-13
lines changed

.github/workflows/build.yml

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,27 +12,31 @@ jobs:
1212
python-version: [3.5, 3.6, 3.7]
1313

1414
steps:
15-
- uses: actions/checkout@v1
15+
- uses: actions/checkout@v2
1616
- name: Set up Python ${{ matrix.python-version }}
17-
uses: actions/setup-python@v1
17+
uses: actions/setup-python@v2
1818
with:
1919
python-version: ${{ matrix.python-version }}
2020
- name: Install pipenv
2121
uses: dschep/install-pipenv-action@v1
2222
- name: Install dependencies
23-
uses: "VaultVulp/action-pipenv@master"
23+
uses: VaultVulp/action-pipenv@v2.0.1
2424
with:
2525
command: install --dev
2626
- name: Run linter
27-
uses: "VaultVulp/action-pipenv@master"
27+
uses: VaultVulp/action-pipenv@v2.0.1
2828
with:
29-
command: run flake8
29+
command: run lint
30+
- name: Run type checker
31+
uses: VaultVulp/action-pipenv@v2.0.1
32+
with:
33+
command: run type
3034
- name: Run tests
31-
uses: "VaultVulp/action-pipenv@master"
35+
uses: VaultVulp/action-pipenv@v2.0.1
3236
with:
3337
command: run test --cov=evasdk --cov-branch --cov-report=xml
3438
- name: Upload to codecov
35-
uses: codecov/codecov-action@v1.0.3
39+
uses: codecov/codecov-action@v1.0.7
3640
with:
3741
token: ${{secrets.CODECOV_TOKEN}}
3842
file: ./coverage.xml

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@ build/
55
dist/
66
.pytest_cache
77
*coverage*
8+
.mypy_cache

LICENSE

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
Copyright 2018 Automata Technologies Ltd
1+
Copyright 2015-2020 Automata Technologies Ltd
22

33
Licensed under the Apache License, Version 2.0 (the "License");
44
you may not use this file except in compliance with the License.

Pipfile

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,21 @@ name = "pypi"
66
[packages]
77
requests = "*"
88
websockets = "*"
9+
zeroconf = "==0.27.1"
910

1011
[dev-packages]
1112
flake8 = "*"
1213
requests-mock = "*"
1314
pytest = "*"
1415
"pytest-cov" = "*"
16+
"pytest-flake8" = "*"
17+
"pytest-mypy" = "*"
18+
mypy = "*"
1519

1620
[requires]
1721

1822
[scripts]
19-
test = "python -m pytest tests/"
23+
test = "pipenv run testd tests/"
24+
testd = "python -m pytest --mypy --flake8"
2025
lint = "flake8"
26+
type = "mypy evasdk examples tests"

Pipfile.lock

Lines changed: 100 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,7 @@ $ pipenv run test
162162

163163
# or to run a single test file:
164164
$ pipenv shell
165-
$ python -m pytest tests/<test-name>_test.py
165+
$ pipenv run testd tests/<test-name>_test.py
166166

167167
# some test require supplying ip and token via the `--ip` and `--token` arguements:
168168
$ pipenv run test --ip 172.16.16.2 --token abc-123-def-456

evasdk/EvaDiscoverer.py

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
from typing import Callable
2+
from dataclasses import dataclass
3+
from threading import Condition
4+
from .Eva import Eva
5+
from zeroconf import ServiceBrowser, Zeroconf
6+
7+
8+
CHOREO_SERVICE = "_automata-eva._tcp.local."
9+
10+
11+
@dataclass
12+
class DiscoveredEva:
13+
name: str
14+
host: str
15+
16+
def connect(self, token) -> Eva:
17+
return Eva(self.host, token)
18+
19+
20+
DiscoverCallback = Callable[[str, DiscoveredEva], None]
21+
22+
23+
class EvaDiscoverer:
24+
25+
26+
def __init__(self, callback: DiscoverCallback, name: str = None):
27+
self.name = name
28+
self.callback = callback
29+
self.zeroconf = None
30+
31+
def __enter__(self):
32+
self.zeroconf = Zeroconf()
33+
self.browser = ServiceBrowser(self.zeroconf, CHOREO_SERVICE, self)
34+
35+
def __exit__(self, exc_type, exc_val, exc_tb):
36+
self.zeroconf.close()
37+
38+
def __get_eva(self, zeroconf: Zeroconf, service_type: str, service_name: str):
39+
info = zeroconf.get_service_info(service_type, service_name)
40+
if info is None:
41+
return None
42+
return DiscoveredEva(host=info.server, name=info.properties[b'name'].decode("utf-8"))
43+
44+
def __filter_name(self, eva):
45+
return self.name is not None and self.name != eva.name
46+
47+
def add_service(self, zeroconf: Zeroconf, service_type: str, service_name: str):
48+
eva = self.__get_eva(zeroconf, service_type, service_name)
49+
if eva is None or self.__filter_name(eva):
50+
return
51+
self.callback('added', eva)
52+
53+
def remove_service(self, zeroconf: Zeroconf, service_type: str, service_name: str):
54+
eva = self.__get_eva(zeroconf, service_type, service_name)
55+
if eva is None or self.__filter_name(eva):
56+
return
57+
self.callback('removed', eva)
58+
59+
60+
def __find_evas(callback: DiscoverCallback, timeout: float, name: str = None, condition: Condition = None):
61+
if condition is None:
62+
condition = Condition()
63+
with EvaDiscoverer(name=name, callback=callback):
64+
with condition:
65+
condition.wait(timeout=timeout)
66+
67+
68+
def find_evas(timeout: float = 5):
69+
"""Blocks for `timeout` seconds and returns a dictionary of DiscoveredEva (with their names as key) discovered in that time"""
70+
evas = {}
71+
72+
def __callback(event: str, eva: DiscoveredEva):
73+
if event == 'added':
74+
evas[eva.name] = eva
75+
elif event == 'deleted':
76+
del evas[eva.name]
77+
78+
__find_evas(callback=__callback, timeout=timeout)
79+
return evas
80+
81+
82+
def find_eva(name: str, timeout: float = 5):
83+
"""Blocks for a maximum of `timeout` seconds and returns a DiscoveredEva if a robot named `name` was found, or `None`"""
84+
eva = None
85+
cv = Condition()
86+
87+
def __callback(event: str, eva_found: DiscoveredEva):
88+
nonlocal eva
89+
if event == 'added':
90+
eva = eva_found
91+
with cv:
92+
cv.notify()
93+
94+
__find_evas(name=name, callback=__callback, timeout=timeout, condition=cv)
95+
return eva
96+
97+
98+
def find_first_eva(timeout: float = 5):
99+
"""Blocks for a maximum of `timeout` seconds and returns a DiscoveredEva if one was found, or `None`"""
100+
eva = None
101+
cv = Condition()
102+
103+
def __callback(event: str, eva_found: DiscoveredEva):
104+
nonlocal eva
105+
if event == 'added' and eva is None:
106+
eva = eva_found
107+
with cv:
108+
cv.notify()
109+
110+
__find_evas(callback=__callback, timeout=timeout, condition=cv)
111+
return eva
112+
113+
114+
def discover_evas(callback: DiscoverCallback):
115+
"""Returns a context that will discovers robots until exited
116+
117+
It will call `callback` with 2 arguments: the event (either `added` or `removed`) and a Discovered Eva object
118+
119+
Note that `callback` will be called from another thread so you will need to ensure any data accessed there is done in a thread-safe manner
120+
"""
121+
return EvaDiscoverer(callback=callback)

evasdk/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,3 +77,7 @@
7777
EvaValidationError, EvaAuthError, EvaAutoRenewError,
7878
EvaAdminError, EvaServerError)
7979
from .version import __version__
80+
from .EvaDiscoverer import (
81+
DiscoverCallback, DiscoveredEva,
82+
find_evas, find_eva, find_first_eva, discover_evas,
83+
)

evasdk/eva_http_client.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,7 @@ def gpio_get(self, pin, pin_type):
186186
def __globals_editing(self, keys, values):
187187
data = {'changes': []}
188188
if (isinstance(keys, list) and isinstance(values, list)):
189-
[data['changes'].append({'key': c[0], 'value': c[1]}) for c in zip(keys, values)]
189+
data['changes'] = [{'key': k, 'value': v} for k, v in zip(keys, values)]
190190
else:
191191
data['changes'].append({'key': keys, 'value': values})
192192
data = json.dumps(data)

mypy.ini

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
[mypy]
2+
3+
[mypy-pytest.*]
4+
ignore_missing_imports = True

setup.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,11 @@
1515
packages=setuptools.find_packages(),
1616
long_description=long_description,
1717
long_description_content_type="text/markdown",
18-
install_requires=['requests', 'websockets'],
18+
install_requires=[
19+
'requests',
20+
'websockets',
21+
'zeroconf',
22+
],
1923
classifiers=[
2024
"Programming Language :: Python :: 3",
2125
"Development Status :: 4 - Beta",

0 commit comments

Comments
 (0)