fluss-api 0.1.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fluss_api-0.1.1/PKG-INFO +16 -0
- fluss_api-0.1.1/README.md +0 -0
- fluss_api-0.1.1/fluss_api/__init__.py +2 -0
- fluss_api-0.1.1/fluss_api/main.py +102 -0
- fluss_api-0.1.1/fluss_api.egg-info/PKG-INFO +16 -0
- fluss_api-0.1.1/fluss_api.egg-info/SOURCES.txt +11 -0
- fluss_api-0.1.1/fluss_api.egg-info/dependency_links.txt +1 -0
- fluss_api-0.1.1/fluss_api.egg-info/top_level.txt +2 -0
- fluss_api-0.1.1/pyproject.toml +17 -0
- fluss_api-0.1.1/setup.cfg +4 -0
- fluss_api-0.1.1/setup.py +20 -0
- fluss_api-0.1.1/tests/__init__.py +0 -0
- fluss_api-0.1.1/tests/test_main.py +164 -0
fluss_api-0.1.1/PKG-INFO
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: fluss_api
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: A library to integrate the Fluss API into Home Assistant
|
|
5
|
+
Home-page: UNKNOWN
|
|
6
|
+
Author: Njeru Ndegwa
|
|
7
|
+
Author-email: njeru@fluss.io
|
|
8
|
+
License: UNKNOWN
|
|
9
|
+
Platform: UNKNOWN
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Requires-Python: >=3.10
|
|
14
|
+
|
|
15
|
+
UNKNOWN
|
|
16
|
+
|
|
File without changes
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
#main.py
|
|
2
|
+
"""Fluss+ API Client."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import asyncio
|
|
7
|
+
import datetime
|
|
8
|
+
import logging
|
|
9
|
+
import socket
|
|
10
|
+
import typing
|
|
11
|
+
|
|
12
|
+
from aiohttp import ClientSession
|
|
13
|
+
from homeassistant.helpers import aiohttp_client # type: ignore
|
|
14
|
+
|
|
15
|
+
LOGGER = logging.getLogger(__package__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class FlussApiClientError(Exception):
|
|
20
|
+
"""Exception to indicate a general API error."""
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class FlussDeviceError(Exception):
|
|
24
|
+
"""Exception to indicate that an error occurred when retrieving devices."""
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class FlussApiClientCommunicationError(FlussApiClientError):
|
|
28
|
+
"""Exception to indicate a communication error."""
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class FlussApiClientAuthenticationError(FlussApiClientError):
|
|
32
|
+
"""Exception to indicate an authentication error."""
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class FlussApiClient:
|
|
36
|
+
"""Fluss+ API Client."""
|
|
37
|
+
|
|
38
|
+
def __init__(self, api_key: str, hass: HomeAssistant = None) -> None:
|
|
39
|
+
"""Initialize the Fluss+ API Client."""
|
|
40
|
+
self._api_key = api_key
|
|
41
|
+
self._session: ClientSession = aiohttp_client.async_get_clientsession(hass) if hass else ClientSession()
|
|
42
|
+
|
|
43
|
+
async def async_get_devices(self) -> typing.Any:
|
|
44
|
+
"""Get data from the API."""
|
|
45
|
+
try:
|
|
46
|
+
return await self._api_wrapper(
|
|
47
|
+
method="GET",
|
|
48
|
+
url="https://zgekzokxrl.execute-api.eu-west-1.amazonaws.com/v1/api/device/list",
|
|
49
|
+
headers={"Authorization": self._api_key},
|
|
50
|
+
)
|
|
51
|
+
except FlussApiClientError as error:
|
|
52
|
+
LOGGER.error("Failed to get devices: %s", error)
|
|
53
|
+
raise FlussDeviceError("Failed to retrieve devices") from error
|
|
54
|
+
|
|
55
|
+
async def async_trigger_device(self, deviceId: str) -> typing.Any:
|
|
56
|
+
"""Trigger the device."""
|
|
57
|
+
timestamp = int(datetime.datetime.now().timestamp() * 1000)
|
|
58
|
+
return await self._api_wrapper(
|
|
59
|
+
method="POST",
|
|
60
|
+
url=f"https://zgekzokxrl.execute-api.eu-west-1.amazonaws.com/v1/api/device/{deviceId}/trigger",
|
|
61
|
+
headers={"Authorization": self._api_key},
|
|
62
|
+
data={"timeStamp": timestamp, "metaData": {}},
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
async def _api_wrapper(
|
|
66
|
+
self,
|
|
67
|
+
method: str,
|
|
68
|
+
url: str,
|
|
69
|
+
data: dict | None = None,
|
|
70
|
+
headers: dict | None = None,
|
|
71
|
+
) -> typing.Any:
|
|
72
|
+
"""Get information from the API."""
|
|
73
|
+
try:
|
|
74
|
+
async with asyncio.timeout(10):
|
|
75
|
+
response = await self._session.request(
|
|
76
|
+
method=method,
|
|
77
|
+
url=url,
|
|
78
|
+
headers=headers,
|
|
79
|
+
json=data,
|
|
80
|
+
)
|
|
81
|
+
if response.status in (401, 403):
|
|
82
|
+
raise FlussApiClientAuthenticationError("Invalid credentials")
|
|
83
|
+
response.raise_for_status()
|
|
84
|
+
return await response.json()
|
|
85
|
+
|
|
86
|
+
except asyncio.TimeoutError as e:
|
|
87
|
+
LOGGER.error("Timeout error fetching information from %s", url)
|
|
88
|
+
raise FlussApiClientCommunicationError("Timeout error fetching information") from e
|
|
89
|
+
except (aiohttp.ClientError, socket.gaierror) as ex:
|
|
90
|
+
LOGGER.error("Error fetching information from %s: %s", url, ex)
|
|
91
|
+
raise FlussApiClientCommunicationError("Error fetching information") from ex
|
|
92
|
+
except FlussApiClientAuthenticationError as auth_ex:
|
|
93
|
+
LOGGER.error("Authentication error: %s", auth_ex)
|
|
94
|
+
raise
|
|
95
|
+
except Exception as exception: # pylint: disable=broad-except
|
|
96
|
+
LOGGER.error("Unexpected error occurred: %s", exception)
|
|
97
|
+
raise FlussApiClientError("Something really wrong happened!") from exception
|
|
98
|
+
|
|
99
|
+
async def close(self) -> None:
|
|
100
|
+
"""Close the aiohttp session."""
|
|
101
|
+
if self._session:
|
|
102
|
+
await self._session.close()
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: fluss-api
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: A library to integrate the Fluss API into Home Assistant
|
|
5
|
+
Home-page: UNKNOWN
|
|
6
|
+
Author: Njeru Ndegwa
|
|
7
|
+
Author-email: njeru@fluss.io
|
|
8
|
+
License: UNKNOWN
|
|
9
|
+
Platform: UNKNOWN
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Requires-Python: >=3.10
|
|
14
|
+
|
|
15
|
+
UNKNOWN
|
|
16
|
+
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
setup.py
|
|
4
|
+
fluss_api/__init__.py
|
|
5
|
+
fluss_api/main.py
|
|
6
|
+
fluss_api.egg-info/PKG-INFO
|
|
7
|
+
fluss_api.egg-info/SOURCES.txt
|
|
8
|
+
fluss_api.egg-info/dependency_links.txt
|
|
9
|
+
fluss_api.egg-info/top_level.txt
|
|
10
|
+
tests/__init__.py
|
|
11
|
+
tests/test_main.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "fluss_api"
|
|
7
|
+
version = "0.1.1"
|
|
8
|
+
description = "Fluss+ API Client"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.7"
|
|
11
|
+
dependencies = ["aiohttp"]
|
|
12
|
+
|
|
13
|
+
[tool.setuptools.packages.find]
|
|
14
|
+
where = ["fluss_api"]
|
|
15
|
+
|
|
16
|
+
[project.urls]
|
|
17
|
+
"Homepage" = "https://github.com/yourusername/flussapi"
|
fluss_api-0.1.1/setup.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import setuptools
|
|
2
|
+
from setuptools import setup, find_packages
|
|
3
|
+
|
|
4
|
+
setup(
|
|
5
|
+
name="fluss_api",
|
|
6
|
+
version="0.1.1",
|
|
7
|
+
packages= find_packages(),
|
|
8
|
+
install_requires =[
|
|
9
|
+
|
|
10
|
+
],
|
|
11
|
+
author="Njeru Ndegwa",
|
|
12
|
+
author_email="njeru@fluss.io",
|
|
13
|
+
description='A library to integrate the Fluss API into Home Assistant',
|
|
14
|
+
classifiers=[
|
|
15
|
+
'Programming Language :: Python :: 3',
|
|
16
|
+
'License :: OSI Approved :: MIT License', # License type
|
|
17
|
+
'Operating System :: OS Independent',
|
|
18
|
+
],
|
|
19
|
+
python_requires='>=3.10',
|
|
20
|
+
)
|
|
File without changes
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import asyncio # noqa: D100
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
import socket
|
|
4
|
+
from unittest.mock import AsyncMock, Mock, patch
|
|
5
|
+
|
|
6
|
+
import aiohttp
|
|
7
|
+
import pytest
|
|
8
|
+
|
|
9
|
+
from fluss_api.main import (
|
|
10
|
+
FlussApiClient,
|
|
11
|
+
FlussApiClientAuthenticationError,
|
|
12
|
+
FlussApiClientCommunicationError,
|
|
13
|
+
FlussApiClientError,
|
|
14
|
+
FlussDeviceError,
|
|
15
|
+
)
|
|
16
|
+
from aiohttp import ClientSession
|
|
17
|
+
|
|
18
|
+
@pytest.fixture
|
|
19
|
+
async def mock_hass(): # noqa: D103
|
|
20
|
+
hass = Mock(spec=ClientSession)
|
|
21
|
+
hass.data = {} # Add missing attribute
|
|
22
|
+
hass.bus = Mock()
|
|
23
|
+
hass.async_update_entry = AsyncMock() # Add missing method
|
|
24
|
+
return hass
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@pytest.fixture
|
|
28
|
+
async def api_client(mock_hass) -> FlussApiClient: # type: ignore # noqa: D103, PGH003
|
|
29
|
+
client = FlussApiClient("test_api_key", mock_hass)
|
|
30
|
+
yield client
|
|
31
|
+
await client.close()
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@pytest.mark.asyncio
|
|
35
|
+
async def test_async_get_devices(api_client) -> None: # noqa: D103
|
|
36
|
+
"""Test the async_get_devices method."""
|
|
37
|
+
with patch.object(
|
|
38
|
+
api_client, "_api_wrapper", new=AsyncMock(return_value={"devices": []})
|
|
39
|
+
) as mock_api_wrapper:
|
|
40
|
+
devices = await api_client.async_get_devices()
|
|
41
|
+
assert devices == {"devices": []}
|
|
42
|
+
mock_api_wrapper.assert_called_once_with(
|
|
43
|
+
method="get",
|
|
44
|
+
url="https://zgekzokxrl.execute-api.eu-west-1.amazonaws.com/v1/api/device/list",
|
|
45
|
+
headers={"Authorization": "test_api_key"},
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@pytest.mark.asyncio
|
|
50
|
+
async def test_async_get_devices_error(api_client) -> None:
|
|
51
|
+
"""Test error handling in async_get_devices method."""
|
|
52
|
+
with (
|
|
53
|
+
patch.object(
|
|
54
|
+
api_client,
|
|
55
|
+
"_api_wrapper",
|
|
56
|
+
new=AsyncMock(side_effect=FlussApiClientError("Error")),
|
|
57
|
+
) as mock_api_wrapper,
|
|
58
|
+
patch("homeassistant.components.fluss.api.LOGGER.error") as mock_logger,
|
|
59
|
+
):
|
|
60
|
+
with pytest.raises(FlussDeviceError):
|
|
61
|
+
await api_client.async_get_devices()
|
|
62
|
+
mock_api_wrapper.assert_called_once()
|
|
63
|
+
# Comparing string representations to avoid object identity issues
|
|
64
|
+
mock_logger.assert_called_once()
|
|
65
|
+
logged_args = mock_logger.call_args[0]
|
|
66
|
+
assert logged_args[0] == "Failed to get devices: %s"
|
|
67
|
+
assert str(logged_args[1]) == "Error"
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@pytest.mark.asyncio
|
|
71
|
+
async def test_async_trigger_device(api_client) -> None: # noqa: D103
|
|
72
|
+
"""Test the async_trigger_device method."""
|
|
73
|
+
with patch.object(
|
|
74
|
+
api_client, "_api_wrapper", new=AsyncMock(return_value={})
|
|
75
|
+
) as mock_api_wrapper:
|
|
76
|
+
response = await api_client.async_trigger_device("device_id")
|
|
77
|
+
assert response == {}
|
|
78
|
+
mock_api_wrapper.assert_called_once_with(
|
|
79
|
+
method="post",
|
|
80
|
+
url="https://zgekzokxrl.execute-api.eu-west-1.amazonaws.com/v1/api/device/device_id/trigger",
|
|
81
|
+
headers={"Authorization": "test_api_key"},
|
|
82
|
+
data={
|
|
83
|
+
"timeStamp": int(datetime.now().timestamp() * 1000),
|
|
84
|
+
"metaData": {},
|
|
85
|
+
},
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@pytest.mark.asyncio
|
|
90
|
+
async def test_api_wrapper_authentication_error(api_client) -> None: # noqa: D103
|
|
91
|
+
"""Test authentication error handling in _api_wrapper."""
|
|
92
|
+
mock_response = AsyncMock()
|
|
93
|
+
mock_response.status = 401
|
|
94
|
+
with (
|
|
95
|
+
patch.object(
|
|
96
|
+
api_client._session, "request", new=AsyncMock(return_value=mock_response)
|
|
97
|
+
),
|
|
98
|
+
pytest.raises(FlussApiClientAuthenticationError),
|
|
99
|
+
):
|
|
100
|
+
await api_client._api_wrapper("get", "test_url")
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@pytest.mark.asyncio
|
|
104
|
+
async def test_api_wrapper_communication_error(api_client) -> None: # noqa: D103
|
|
105
|
+
"""Test communication error handling in _api_wrapper."""
|
|
106
|
+
with (
|
|
107
|
+
patch.object(
|
|
108
|
+
api_client._session,
|
|
109
|
+
"request",
|
|
110
|
+
new=AsyncMock(side_effect=aiohttp.ClientError),
|
|
111
|
+
),
|
|
112
|
+
pytest.raises(FlussApiClientCommunicationError),
|
|
113
|
+
):
|
|
114
|
+
await api_client._api_wrapper("get", "test_url")
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
@pytest.mark.asyncio
|
|
118
|
+
async def test_api_wrapper_timeout_error(api_client) -> None: # noqa: D103
|
|
119
|
+
"""Test timeout error handling in _api_wrapper."""
|
|
120
|
+
with (
|
|
121
|
+
patch.object(
|
|
122
|
+
api_client._session,
|
|
123
|
+
"request",
|
|
124
|
+
new=AsyncMock(side_effect=asyncio.TimeoutError),
|
|
125
|
+
),
|
|
126
|
+
pytest.raises(FlussApiClientCommunicationError),
|
|
127
|
+
):
|
|
128
|
+
await api_client._api_wrapper("get", "test_url")
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
@pytest.mark.asyncio
|
|
132
|
+
async def test_api_wrapper_socket_error(api_client) -> None: # noqa: D103
|
|
133
|
+
"""Test socket error handling in _api_wrapper."""
|
|
134
|
+
with (
|
|
135
|
+
patch.object(
|
|
136
|
+
api_client._session, "request", new=AsyncMock(side_effect=socket.gaierror)
|
|
137
|
+
),
|
|
138
|
+
pytest.raises(FlussApiClientCommunicationError),
|
|
139
|
+
):
|
|
140
|
+
await api_client._api_wrapper("get", "test_url")
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
@pytest.mark.asyncio
|
|
144
|
+
async def test_api_wrapper_general_error(api_client) -> None: # noqa: D103
|
|
145
|
+
"""Test general error handling in _api_wrapper."""
|
|
146
|
+
with (
|
|
147
|
+
patch.object(
|
|
148
|
+
api_client._session, "request", new=AsyncMock(side_effect=Exception)
|
|
149
|
+
),
|
|
150
|
+
pytest.raises(FlussApiClientError),
|
|
151
|
+
):
|
|
152
|
+
await api_client._api_wrapper("get", "test_url")
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
@pytest.mark.asyncio
|
|
156
|
+
async def test_api_wrapper_success(api_client) -> None:
|
|
157
|
+
mock_response = AsyncMock()
|
|
158
|
+
mock_response.status = 200
|
|
159
|
+
mock_response.json.return_value = {"key": "value"}
|
|
160
|
+
with patch.object(
|
|
161
|
+
api_client._session, "request", new=AsyncMock(return_value=mock_response)
|
|
162
|
+
):
|
|
163
|
+
result = await api_client._api_wrapper("get", "test_url")
|
|
164
|
+
assert result == {"key": "value"}
|