Skip to content

Commit

Permalink
♻️ refactor: Submodules
Browse files Browse the repository at this point in the history
Refactor into submodules, to selectively expose the API related
functions rather than everything including utils functions.
We now have:
```
edilkamin/
├── api.py
├── constants.py
├── __init__.py
├── __main__.py
└── utils.py
```
Document API functions with docstring and updated a few function def
to return API response string rather than entire response object.
  • Loading branch information
AndreMiras committed Oct 19, 2022
1 parent 62a0268 commit aa79531
Show file tree
Hide file tree
Showing 6 changed files with 139 additions and 106 deletions.
92 changes: 16 additions & 76 deletions edilkamin/__init__.py
100755 → 100644
Original file line number Diff line number Diff line change
@@ -1,76 +1,16 @@
#!/usr/bin/env python
import os
import typing

import requests
from pycognito import Cognito

from edilkamin import constants


def sign_in(username, password):
cognito = Cognito(constants.USER_POOL_ID, constants.CLIENT_ID, username=username)
cognito.authenticate(password)
user = cognito.get_user()
return user._metadata["access_token"]


def get_endpoint(url: str) -> str:
return constants.BACKEND_URL + url


def get_headers(token: str):
return {"Authorization": f"Bearer {token}"}


def device_info(token: str, mac: str):
"""Retrieve device info for a given MAC address in the format `aabbccddeeff`."""
headers = get_headers(token)
url = get_endpoint(f"device/{mac}/info")
response = requests.get(url, headers=headers)
response.raise_for_status()
return response.json()


def mqtt_command(
token: str, mac_address: str, payload: typing.Dict
) -> requests.models.Response:
headers = get_headers(token)
url = get_endpoint("mqtt/command")
data = {"mac_address": mac_address, **payload}
response = requests.put(url, json=data, headers=headers)
response.raise_for_status()
return response


def set_power(token: str, mac_address: str, value: int) -> requests.models.Response:
return mqtt_command(token, mac_address, {"name": "power", "value": value})


def set_power_on(token: str, mac_address: str):
return set_power(token, mac_address, 1)


def set_power_off(token: str, mac_address: str):
return set_power(token, mac_address, 0)


def assert_env(name: str) -> str:
env = os.environ.get(name)
assert env
return env


def main():
username = assert_env("USERNAME")
password = assert_env("PASSWORD")
mac_address = assert_env("MAC_ADDRESS")
token = sign_in(username, password)
info = device_info(token, mac_address)
print(info)
result = set_power_off(token, mac_address)
print(result)


if __name__ == "__main__":
main()
__all__ = [
"sign_in",
"device_info",
"mqtt_command",
"set_power",
"set_power_on",
"set_power_off",
]
from edilkamin.api import (
device_info,
mqtt_command,
set_power,
set_power_off,
set_power_on,
sign_in,
)
18 changes: 18 additions & 0 deletions edilkamin/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
#!/usr/bin/env python
from edilkamin.api import device_info, set_power_off, sign_in
from edilkamin.utils import assert_env


def main():
username = assert_env("USERNAME")
password = assert_env("PASSWORD")
mac_address = assert_env("MAC_ADDRESS")
token = sign_in(username, password)
info = device_info(token, mac_address)
print(info)
result = set_power_off(token, mac_address)
print(result)


if __name__ == "__main__":
main()
55 changes: 55 additions & 0 deletions edilkamin/api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import typing

import requests
from pycognito import Cognito

from edilkamin import constants
from edilkamin.utils import get_endpoint, get_headers


def sign_in(username: str, password: str) -> str:
"""Sign in and return token."""
cognito = Cognito(constants.USER_POOL_ID, constants.CLIENT_ID, username=username)
cognito.authenticate(password)
user = cognito.get_user()
return user._metadata["access_token"]


def device_info(token: str, mac: str) -> typing.Dict:
"""Retrieve device info for a given MAC address in the format `aabbccddeeff`."""
headers = get_headers(token)
url = get_endpoint(f"device/{mac}/info")
response = requests.get(url, headers=headers)
response.raise_for_status()
return response.json()


def mqtt_command(token: str, mac_address: str, payload: typing.Dict) -> str:
"""
Send a MQTT command to the device identified with the MAC address.
Return the response string.
"""
headers = get_headers(token)
url = get_endpoint("mqtt/command")
data = {"mac_address": mac_address, **payload}
response = requests.put(url, json=data, headers=headers)
response.raise_for_status()
return response.json()


def set_power(token: str, mac_address: str, value: int) -> str:
"""
Set device power on (1) or off (0).
Return response string e.g.
- "Value is already 0"
- "Command 0123456789abcdef executed successfully"
"""
return mqtt_command(token, mac_address, {"name": "power", "value": value})


def set_power_on(token: str, mac_address: str) -> str:
return set_power(token, mac_address, 1)


def set_power_off(token: str, mac_address: str) -> str:
return set_power(token, mac_address, 0)
17 changes: 17 additions & 0 deletions edilkamin/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import os

from edilkamin import constants


def get_endpoint(url: str) -> str:
return constants.BACKEND_URL + url


def get_headers(token: str):
return {"Authorization": f"Bearer {token}"}


def assert_env(name: str) -> str:
env = os.environ.get(name)
assert env
return env
42 changes: 12 additions & 30 deletions tests/test_edilkamin.py → tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from requests.exceptions import HTTPError
from requests.models import Response

import edilkamin
from edilkamin import api

token = "token"
mac_address = "aabbccddeeff"
Expand All @@ -17,7 +17,7 @@ def patch_requests(method, json_response=None, status_code=200):
response.status_code = status_code
response.raw = BytesIO(json.dumps(json_response).encode())
m_method = mock.Mock(return_value=response)
return mock.patch(f"edilkamin.requests.{method}", m_method)
return mock.patch(f"edilkamin.api.requests.{method}", m_method)


def patch_requests_get(json_response=None, status_code=200):
Expand All @@ -33,7 +33,7 @@ def patch_cognito(access_token):
m_get_user._metadata = {"access_token": access_token}
m_cognito = mock.Mock()
m_cognito.return_value.get_user.return_value = m_get_user
return mock.patch("edilkamin.Cognito", m_cognito)
return mock.patch("edilkamin.api.Cognito", m_cognito)


def test_sign_in():
Expand All @@ -43,15 +43,15 @@ def test_sign_in():
m_get_user = mock.Mock()
m_get_user._metadata = {"access_token": access_token}
with patch_cognito(access_token) as m_cognito:
assert edilkamin.sign_in(username, password) == access_token
assert api.sign_in(username, password) == access_token
assert m_cognito().authenticate.call_args_list == [mock.call(password)]
assert m_cognito().get_user.call_args_list == [mock.call()]


def test_device_info():
json_response = {}
with patch_requests_get(json_response) as m_get:
assert edilkamin.device_info(token, mac_address) == json_response
assert api.device_info(token, mac_address) == json_response
assert m_get.call_args_list == [
mock.call(
"https://fxtj7xkgc6.execute-api.eu-central-1.amazonaws.com/prod/"
Expand All @@ -68,17 +68,15 @@ def test_device_info_error():
with patch_requests_get(json_response, status_code) as m_get, pytest.raises(
HTTPError, match="401 Client Error"
):
edilkamin.device_info(token, mac_address)
api.device_info(token, mac_address)
assert m_get.call_count == 1


def test_mqtt_command():
json_response = {}
json_response = '"Command 0123456789abcdef executed successfully"'
payload = {"key": "value"}
with patch_requests_put(json_response) as m_put:
assert (
edilkamin.mqtt_command(token, mac_address, payload).json() == json_response
)
assert api.mqtt_command(token, mac_address, payload) == json_response
assert m_put.call_args_list == [
mock.call(
"https://fxtj7xkgc6.execute-api.eu-central-1.amazonaws.com/prod/"
Expand All @@ -97,7 +95,7 @@ def test_mqtt_command_error():
with patch_requests_put(json_response, status_code) as m_put, pytest.raises(
HTTPError, match="401 Client Error"
):
edilkamin.mqtt_command(token, mac_address, payload)
api.mqtt_command(token, mac_address, payload)
assert m_put.call_count == 1


Expand All @@ -109,10 +107,10 @@ def test_mqtt_command_error():
),
)
def test_set_power(method, expected_value):
json_response = {}
set_power_method = getattr(edilkamin, method)
json_response = '"Value is already x"'
set_power_method = getattr(api, method)
with patch_requests_put(json_response) as m_put:
assert isinstance(set_power_method(token, mac_address), Response)
assert set_power_method(token, mac_address) == json_response
assert m_put.call_args_list == [
mock.call(
"https://fxtj7xkgc6.execute-api.eu-central-1.amazonaws.com/prod/"
Expand All @@ -125,19 +123,3 @@ def test_set_power(method, expected_value):
headers={"Authorization": "Bearer token"},
)
]


def test_main():
access_token = "token"
env = {
"USERNAME": "username",
"PASSWORD": "password",
"MAC_ADDRESS": "mac_address",
}
with mock.patch.dict("os.environ", env), patch_cognito(
access_token
) as m_cognito, patch_requests_get() as m_get, patch_requests_put() as m_put:
assert edilkamin.main() is None
assert m_cognito.called is True
assert m_get.called is True
assert m_put.called is True
21 changes: 21 additions & 0 deletions tests/test_main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from unittest import mock

from test_api import patch_cognito, patch_requests_get, patch_requests_put

from edilkamin import __main__


def test_main():
access_token = "token"
env = {
"USERNAME": "username",
"PASSWORD": "password",
"MAC_ADDRESS": "mac_address",
}
with mock.patch.dict("os.environ", env), patch_cognito(
access_token
) as m_cognito, patch_requests_get() as m_get, patch_requests_put() as m_put:
assert __main__.main() is None
assert m_cognito.called is True
assert m_get.called is True
assert m_put.called is True

0 comments on commit aa79531

Please sign in to comment.