Skip to content

Commit

Permalink
connection.py: convert to use aiohttp and asyncio
Browse files Browse the repository at this point in the history
Signed-off-by: Lukas Rusak <lorusak@gmail.com>
  • Loading branch information
lrusak committed May 15, 2024
1 parent c8484cd commit a8a401d
Show file tree
Hide file tree
Showing 4 changed files with 69 additions and 91 deletions.
123 changes: 48 additions & 75 deletions libeagle/connection.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
# SPDX-License-Identifier: GPL-2.0-only

import urllib.parse
import urllib.request
import aiohttp

import logging

import xml.etree.ElementTree as etree

from types import TracebackType
from typing import (
Optional,
Type,
)

import aiohttp.client_exceptions

logger = logging.getLogger()

class Connection(object):
Expand All @@ -20,47 +27,44 @@ def __init__(self, hostname, username, password, port=80, debug=False):
port:int The port to use for the connection (This is only needed for testing)
debug:bool Enable debug logging
"""
self._hostname = hostname
self._username = username
self._password = password
self._port = port

global logger

logger.setLevel(logging.DEBUG) if debug else logger.setLevel(logging.INFO)
self._debug = debug
self._url = self._getUrl()
self._opener = self._getOpener()

def setHostname(self, hostname):
self._hostname = hostname
self._url = self._getUrl()
self._opener = self._getOpener()
self.url = f"http://{hostname}:{port}"

def setUsername(self, username):
self._username = username
self._opener = self._getOpener()
headers = {
"Content-type": "text/xml"
}

def setPassword(self, password):
self._password = password
self._opener = self._getOpener()
auth = aiohttp.BasicAuth(username, password)

def setPort(self, port):
self._port = port
self._url = self._getUrl()
self._opener = self._getOpener()
self.session = aiohttp.ClientSession(self.url, headers=headers, auth=auth)

def setDebug(self, debug):
logger.setLevel(logging.DEBUG) if debug else logger.setLevel(logging.INFO)
self._debug = debug

hostname = property(lambda s: s._hostname, setHostname)
username = property(lambda s: s._username, setUsername)
password = property(lambda s: s._password, setPassword)
port = property(lambda s: s._port, setPort)
debug = property(lambda s: s._debug, setDebug)
def __enter__(self) -> None:
raise TypeError("Use async with instead")

def __exit__(
self,
exc_type: Optional[Type[BaseException]],
exc_val: Optional[BaseException],
exc_tb: Optional[TracebackType],
) -> None:
pass

def device_list(self):
async def __aenter__(self) -> "Connection":
return self

async def __aexit__(
self,
exc_type: Optional[Type[BaseException]],
exc_val: Optional[BaseException],
exc_tb: Optional[TracebackType],
) -> None:
await self.session.close()

async def device_list(self) -> dict:
"""
Returns the device list
"""
Expand All @@ -71,8 +75,7 @@ def device_list(self):

logger.debug(f"POST data: {etree.tostring(root).decode()}")

req = self._getRequest(values)
res = self._doRequest(req)
res = await self._doRequest(values)
xml = etree.fromstring(res)

logger.debug(f"return data: {etree.tostring(xml).decode()}")
Expand All @@ -97,7 +100,7 @@ def device_list(self):

return data

def device_details(self, address):
async def device_details(self, address) -> dict:
"""
Returns the device details for a given hardware address
Expand All @@ -112,8 +115,7 @@ def device_details(self, address):

logger.debug(f"POST data: {etree.tostring(root).decode()}")

req = self._getRequest(values)
res = self._doRequest(req)
res = await self._doRequest(values)
xml = etree.fromstring(res)

logger.debug(f"return data: {etree.tostring(xml).decode()}")
Expand Down Expand Up @@ -154,7 +156,7 @@ def device_details(self, address):

return data

def device_query(self, address, component_name=None, variable_name=None):
async def device_query(self, address, component_name=None, variable_name=None) -> dict:
"""
Returns the device query for a given hardware address, name, and variable
Expand Down Expand Up @@ -188,8 +190,7 @@ def device_query(self, address, component_name=None, variable_name=None):

logger.debug(f"POST data: {etree.tostring(root).decode()}")

req = self._getRequest(values)
res = self._doRequest(req)
res = await self._doRequest(values)
xml = etree.fromstring(res)

logger.debug(f"return data: {etree.tostring(xml).decode()}")
Expand Down Expand Up @@ -217,42 +218,14 @@ def device_query(self, address, component_name=None, variable_name=None):

return data

def _getUrl(self):
url = "http://%s:%d/cgi-bin/post_manager" % (self._hostname, self._port)

logger.debug(f"URL: {url}")

return url

def _getOpener(self):
password_mgr = urllib.request.HTTPPasswordMgrWithDefaultRealm()

password_mgr.add_password(None, self._url, self._username, self._password)
handler = urllib.request.HTTPBasicAuthHandler(password_mgr)

opener = urllib.request.build_opener(handler)
urllib.request.install_opener(opener)

return opener

def _getRequest(self, values):
headers = {"Content-type": "text/xml"}

logger.debug(f"data: {values.decode()}")

req = urllib.request.Request(self._url, values, headers)

logger.debug(f"custom headers: {req.headers}")

return req
async def _doRequest(self, values) -> str:

def _doRequest(self, req):
try:
res = self._opener.open(req)
logger.debug(f"default headers: {req.unredirected_hdrs}")
async with self.session.post("/cgi-bin/post_manager", data=values) as response:
if response.status == 200:
return await response.content.read()

except urllib.error.HTTPError as e:
raise e
except aiohttp.client_exceptions.ClientConnectionError:
pass

return res.read().decode()

2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,13 @@ packages = [{include = "*", from="libeagle"}]
python = "^3.10"
Flask = "^3.0.3"
Flask-HTTPAuth = "^4.8.0"
aiohttp = "^3.9.5"

[tool.poetry.group.dev.dependencies]
pytest = "^8.2.0"
pytest-flask = "^1.3.0"
tox = "^4.15.0"
pytest-asyncio = "^0.23.6"

[build-system]
requires = ["poetry-core"]
Expand Down
34 changes: 18 additions & 16 deletions tests/test_eagle200.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,28 +16,30 @@ def app():

@pytest.mark.usefixtures("live_server")
class TestLiveServer:
def test_eagle200(self):

@pytest.mark.asyncio
async def test_eagle200(self):

url = urlsplit(url_for("process_request", _external=True))

conn = libeagle.Connection(url.hostname, "0077dd", "6e61a3a94882eef9", port=url.port, debug=True)
async with libeagle.Connection(url.hostname, "0077dd", "6e61a3a94882eef9", port=url.port, debug=True) as conn:

devices = conn.device_list()
devices = await conn.device_list()

details = conn.device_details(devices[0]["HardwareAddress"])
details = await conn.device_details(devices[0]["HardwareAddress"])

query = conn.device_query(
devices[0]["HardwareAddress"],
details[0]["Name"],
details[0]["Variables"][0],
)
query = await conn.device_query(
devices[0]["HardwareAddress"],
details[0]["Name"],
details[0]["Variables"][0],
)

assert (
query[0]["Variables"]["zigbee:InstantaneousDemand"] == "21.499 kW"
)
assert (
query[0]["Variables"]["zigbee:InstantaneousDemand"] == "21.499 kW"
)

query = conn.device_query(devices[0]["HardwareAddress"])
query = await conn.device_query(devices[0]["HardwareAddress"])

assert (
query[0]["Variables"]["zigbee:Message"] == "Hello, World!"
)
assert (
query[0]["Variables"]["zigbee:Message"] == "Hello, World!"
)
1 change: 1 addition & 0 deletions tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,6 @@ envlist = py310, py311, py312
deps =
pytest-flask
pytest
pytest-asyncio
commands =
pytest

0 comments on commit a8a401d

Please sign in to comment.