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.
@@ -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,2 @@
1
+ #__init__.py
2
+ from .main import FlussApiClient, FlussApiClientAuthenticationError, FlussApiClientCommunicationError,FlussApiClientError, FlussDeviceError
@@ -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,2 @@
1
+ fluss_api
2
+ tests
@@ -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"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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"}