PyHolfuy 0.2__tar.gz → 0.3__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyholfuy
3
- Version: 0.2
3
+ Version: 0.3
4
4
  Summary: A Python library for interacting with Holfuy weather stations.
5
5
  Author-email: Håvard Dagenborg <haavardj@dagenborg.net>
6
6
  License-Expression: MIT
@@ -26,7 +26,7 @@ A Python library for talking to [Holfuy Weather Stations](https://holfuy.com/).
26
26
 
27
27
  You need an API key to access the service. To obtain one, please visit the [Holfuy API](https://api.holfuy.com/live/) pages.
28
28
 
29
- The Library only access live data.
29
+ The library can only access live data.
30
30
 
31
31
  ## Example Code
32
32
  Here is an example of how to use the library.
@@ -38,10 +38,22 @@ from holfuy import HolfuyService
38
38
 
39
39
  async def main():
40
40
  async with aiohttp.ClientSession() as session:
41
- s = HolfuyService("", session)
42
- data = await s.fetch_data(["101"])
43
- print(data)
41
+ s = HolfuyService("", session)
42
+ data = await s.fetch_data(["101"])
43
+ print(data)
44
44
 
45
45
  if __name__ == "__main__":
46
46
  asyncio.run(main())
47
47
  ```
48
+
49
+ ## Return value and errors
50
+
51
+ `fetch_data()` returns `{"measurements": [...]}` with one entry per station, also when
52
+ a single station is requested. A station the key cannot read is still listed, with
53
+ `error` and `errorCode` instead of data.
54
+
55
+ It raises, all subclasses of `holfuy.HolfuyError`:
56
+
57
+ - `HolfuyAuthError`: the API key is rejected, or it has no access to any requested station.
58
+ - `HolfuyConnectionError`: network error, timeout, or an HTTP error status.
59
+ - `HolfuyError`: any other unexpected reply.
pyholfuy-0.3/README.md ADDED
@@ -0,0 +1,38 @@
1
+ ![Holfy Logo](https://holfuy.com/image/logo/holfuy-logo.png)
2
+
3
+ # PyHolfuy
4
+ A Python library for talking to [Holfuy Weather Stations](https://holfuy.com/).
5
+
6
+ You need an API key to access the service. To obtain one, please visit the [Holfuy API](https://api.holfuy.com/live/) pages.
7
+
8
+ The library can only access live data.
9
+
10
+ ## Example Code
11
+ Here is an example of how to use the library.
12
+
13
+ ```python
14
+ import aiohttp
15
+ import asyncio
16
+ from holfuy import HolfuyService
17
+
18
+ async def main():
19
+ async with aiohttp.ClientSession() as session:
20
+ s = HolfuyService("", session)
21
+ data = await s.fetch_data(["101"])
22
+ print(data)
23
+
24
+ if __name__ == "__main__":
25
+ asyncio.run(main())
26
+ ```
27
+
28
+ ## Return value and errors
29
+
30
+ `fetch_data()` returns `{"measurements": [...]}` with one entry per station, also when
31
+ a single station is requested. A station the key cannot read is still listed, with
32
+ `error` and `errorCode` instead of data.
33
+
34
+ It raises, all subclasses of `holfuy.HolfuyError`:
35
+
36
+ - `HolfuyAuthError`: the API key is rejected, or it has no access to any requested station.
37
+ - `HolfuyConnectionError`: network error, timeout, or an HTTP error status.
38
+ - `HolfuyError`: any other unexpected reply.
@@ -15,15 +15,23 @@ from .const import (
15
15
  CONF_MODE_JSON,
16
16
  STATIONS_ALL,
17
17
  )
18
+ from .exceptions import HolfuyAuthError, HolfuyConnectionError, HolfuyError
19
+
20
+ __all__ = [
21
+ "HolfuyAuthError",
22
+ "HolfuyConnectionError",
23
+ "HolfuyError",
24
+ "HolfuyService",
25
+ ]
18
26
 
19
27
  TIMEOUT = 30
20
28
  _LOGGER = logging.getLogger(__name__)
21
29
 
22
- API_URL = "http://api.holfuy.com/live/"
30
+ API_URL = "https://api.holfuy.com/live/"
23
31
 
24
32
  # noqa: E501
25
33
  #### API Description
26
- # URL Format: http://api.holfuy.com/live/?s=101&pw=pass&m=JSON&tu=C&su=m/s
34
+ # URL Format: https://api.holfuy.com/live/?s=101&pw=pass&m=JSON&tu=C&su=m/s
27
35
  #
28
36
  # s: station(s) ID e.g. s=101 or you can get the data from more stations by one query as well (coma separated list) e.g. s=101,102,103 . If you would get all stations' data (to which you have access) by one single API call please use the "all" string -> s=all .
29
37
  # pw: password for the API access (it could be sent by a POST "pw" parameter also)
@@ -68,8 +76,16 @@ class HolfuyService:
68
76
  include_battery=False,
69
77
  include_daily=False,
70
78
  include_location=False,
71
- ) -> dict[str, Any] | None:
72
- """Get the latest data from Holfuy."""
79
+ ) -> dict[str, Any]:
80
+ """Get the latest data from Holfuy.
81
+
82
+ Returns {"measurements": [...]}, one entry per station. A station the key
83
+ cannot read is still listed, with "error" and "errorCode" instead of data.
84
+
85
+ Raises HolfuyAuthError if the key is rejected or can read none of the
86
+ requested stations, HolfuyConnectionError on network errors, timeouts and
87
+ HTTP errors, and HolfuyError on any other unexpected reply.
88
+ """
73
89
 
74
90
  params = {
75
91
  "pw": self._api_key,
@@ -97,29 +113,50 @@ class HolfuyService:
97
113
  params["s"] = STATIONS_ALL
98
114
 
99
115
  try:
100
-
101
- async with asyncio.timeout(TIMEOUT):
102
- resp = await self._session.get(self._api_url, params=params)
103
-
104
- if resp.status >= 400:
105
- _LOGGER.error("%s returned %s", self._api_url, resp.status)
106
- return None
107
-
108
- result = await resp.json()
109
-
110
- # Request for a single station is not enveloped in a list of
111
- # measurements objects. Make data consistent for all request types before it
112
- # is returned.
113
- if stations and len(stations) == 1:
114
- result = {"measurements": [result]}
115
-
116
- return result
116
+ # The timeout covers the whole exchange, body included, and the
117
+ # context manager releases the connection back to the pool.
118
+ async with (
119
+ asyncio.timeout(TIMEOUT),
120
+ self._session.get(self._api_url, params=params) as resp,
121
+ ):
122
+ if resp.status >= 400:
123
+ raise HolfuyConnectionError(
124
+ f"Holfuy API returned HTTP {resp.status}"
125
+ )
126
+
127
+ # A rejected key gets HTTP 200 with the plain text "No access".
128
+ if resp.content_type != "application/json":
129
+ text = (await resp.text()).strip()
130
+ if text.lower().startswith("no access"):
131
+ raise HolfuyAuthError("Holfuy API rejected the API key")
132
+ raise HolfuyError(
133
+ f"Unexpected response from Holfuy API: {text[:100]!r}"
134
+ )
135
+
136
+ result = await resp.json()
117
137
 
118
138
  except (TimeoutError, aiohttp.ClientError) as err:
119
- _LOGGER.error(
120
- "Access to %s returned error '%s'", self._api_url, type(err).__name__
121
- )
122
- return None
123
- except ValueError:
124
- _LOGGER.exception("Unable to parse json response from %s", self._api_url)
125
- return None
139
+ # Only the type name: aiohttp messages can contain the URL, and
140
+ # with it the API key.
141
+ raise HolfuyConnectionError(
142
+ f"Cannot reach Holfuy API: {type(err).__name__}"
143
+ ) from err
144
+ except ValueError as err:
145
+ raise HolfuyError("Unable to parse JSON response from Holfuy API") from err
146
+
147
+ # Request for a single station is not enveloped in a list of
148
+ # measurements objects. Make data consistent for all request types before it
149
+ # is returned.
150
+ if stations and len(stations) == 1:
151
+ result = {"measurements": [result]}
152
+
153
+ measurements = result.get("measurements") if isinstance(result, dict) else None
154
+ if not isinstance(measurements, list):
155
+ raise HolfuyError("Unexpected response from Holfuy API: no measurements")
156
+
157
+ if measurements and all(
158
+ m.get("errorCode") == "no_access" for m in measurements
159
+ ):
160
+ raise HolfuyAuthError("API key has no access to the requested stations")
161
+
162
+ return result
@@ -1,6 +1,6 @@
1
1
  CONF_MODE_JSON = "JSON" # We only handle JSON from the api
2
- CONF_MODE_XML = "CSV"
3
- CONF_MODE_CSV = "XML"
2
+ CONF_MODE_XML = "XML"
3
+ CONF_MODE_CSV = "CSV"
4
4
 
5
5
  CONF_AVERAGING_NEWEST = 0
6
6
  CONF_AVERAGING_QUARTERLY = 1
@@ -11,8 +11,8 @@ CONF_WIND_SPEED_UNIT_KMH = "km/h"
11
11
  CONF_WIND_SPEED_UNIT_MS = "m/s"
12
12
  CONF_WIND_SPEED_UNIT_MPH = "mph"
13
13
 
14
- CONF_TEMPERATURE_UNIT_CELSIUS = "Celsius"
15
- CONF_TEMPERATURE_UNIT_FAHRENHEIT = "Fahrenheit"
14
+ CONF_TEMPERATURE_UNIT_CELSIUS = "C"
15
+ CONF_TEMPERATURE_UNIT_FAHRENHEIT = "F"
16
16
 
17
17
  CONF_OPTION_BATT = "batt"
18
18
  CONF_OPTION_DAILY = "daily"
@@ -0,0 +1,13 @@
1
+ """Exceptions raised by the Holfuy library."""
2
+
3
+
4
+ class HolfuyError(Exception):
5
+ """Base class for all Holfuy errors, raised as is for unexpected replies."""
6
+
7
+
8
+ class HolfuyConnectionError(HolfuyError):
9
+ """The API could not be reached, timed out or returned an HTTP error."""
10
+
11
+
12
+ class HolfuyAuthError(HolfuyError):
13
+ """The API key was rejected, or it has no access to any requested station."""
@@ -1,6 +1,6 @@
1
1
  import asyncio
2
2
  from unittest import IsolatedAsyncioTestCase, mock
3
- from unittest.mock import AsyncMock, patch
3
+ from unittest.mock import AsyncMock, MagicMock, patch
4
4
  import json
5
5
 
6
6
  import aiohttp
@@ -24,7 +24,19 @@ RESP_AUTHERROR=json.loads("""
24
24
  {"measurements":[{"stationId":105,"error":"Sorry, you don't have access for this station.","errorCode":"no_access"},
25
25
  {"stationId":106,"error":"Sorry, you don't have access for this station.","errorCode":"no_access"}]}""")
26
26
 
27
- from holfuy import HolfuyService
27
+ from holfuy import (
28
+ HolfuyAuthError,
29
+ HolfuyConnectionError,
30
+ HolfuyService,
31
+ )
32
+
33
+
34
+ def _get_returning(resp):
35
+ """Mock session.get() as an async context manager yielding resp."""
36
+ ctx = MagicMock()
37
+ ctx.__aenter__ = AsyncMock(return_value=resp)
38
+ ctx.__aexit__ = AsyncMock(return_value=False)
39
+ return MagicMock(return_value=ctx)
28
40
 
29
41
  from holfuy.const import (
30
42
  CONF_TEMPERATURE_UNIT_CELSIUS,
@@ -51,9 +63,10 @@ class TestHolfuyService(IsolatedAsyncioTestCase):
51
63
 
52
64
  mock_resp = AsyncMock()
53
65
  mock_resp.status = 200
66
+ mock_resp.content_type = "application/json"
54
67
  mock_resp.json = AsyncMock(return_value=mock_response)
55
68
 
56
- self.session.get = AsyncMock(return_value=mock_resp)
69
+ self.session.get = _get_returning(mock_resp)
57
70
 
58
71
  result = await self.api.fetch_data(["142"])
59
72
 
@@ -67,7 +80,7 @@ class TestHolfuyService(IsolatedAsyncioTestCase):
67
80
  }
68
81
 
69
82
  self.session.get.assert_called_once_with(
70
- "http://api.holfuy.com/live/",
83
+ "https://api.holfuy.com/live/",
71
84
  params=expected_params
72
85
  )
73
86
 
@@ -79,8 +92,9 @@ class TestHolfuyService(IsolatedAsyncioTestCase):
79
92
 
80
93
  mock_resp = AsyncMock()
81
94
  mock_resp.status = 200
95
+ mock_resp.content_type = "application/json"
82
96
  mock_resp.json = AsyncMock(return_value=mock_response)
83
- self.session.get = AsyncMock(return_value= mock_resp)
97
+ self.session.get = _get_returning(mock_resp)
84
98
 
85
99
  result = await self.api.fetch_data(None)
86
100
 
@@ -94,7 +108,7 @@ class TestHolfuyService(IsolatedAsyncioTestCase):
94
108
  }
95
109
 
96
110
  self.session.get.assert_called_once_with(
97
- "http://api.holfuy.com/live/",
111
+ "https://api.holfuy.com/live/",
98
112
  params=expected_params
99
113
  )
100
114
 
@@ -105,23 +119,24 @@ class TestHolfuyService(IsolatedAsyncioTestCase):
105
119
  mock_resp = AsyncMock()
106
120
  mock_resp.status = 404
107
121
 
108
- self.session.get = AsyncMock(return_value=mock_resp)
122
+ self.session.get = _get_returning(mock_resp)
109
123
 
110
- result = await self.api.fetch_data(None)
111
-
112
- self.assertIsNone(result)
124
+ with self.assertRaises(HolfuyConnectionError):
125
+ await self.api.fetch_data(None)
113
126
 
114
127
  async def test_fetch_data_invalid_access_token(self):
115
128
  """Test handling of error responses."""
116
129
  mock_resp = AsyncMock()
117
130
  mock_resp.status = 200
131
+ mock_resp.content_type = "application/json"
118
132
  mock_response = RESP_AUTHERROR
119
133
 
120
134
  mock_resp.json = AsyncMock(return_value=mock_response)
121
135
 
122
- self.session.get = AsyncMock(return_value=mock_resp)
136
+ self.session.get = _get_returning(mock_resp)
123
137
 
124
- result = await self.api.fetch_data(["105","106"])
138
+ with self.assertRaises(HolfuyAuthError):
139
+ await self.api.fetch_data(["105","106"])
125
140
 
126
141
  expected_params = {
127
142
  "pw": "test_api_key",
@@ -133,24 +148,31 @@ class TestHolfuyService(IsolatedAsyncioTestCase):
133
148
  }
134
149
 
135
150
  self.session.get.assert_called_once_with(
136
- "http://api.holfuy.com/live/",
151
+ "https://api.holfuy.com/live/",
137
152
  params=expected_params
138
153
  )
139
154
 
140
- self.assertEqual(result["measurements"][0]["stationId"], 105)
141
-
142
155
  async def test_fetch_data_timeout(self):
143
156
  """Test handling of timeout errors."""
144
- self.session.get = AsyncMock(side_effect=asyncio.TimeoutError)
157
+ self.session.get = MagicMock(side_effect=asyncio.TimeoutError)
145
158
 
146
- result = await self.api.fetch_data(None)
147
-
148
- self.assertIsNone(result)
159
+ with self.assertRaises(HolfuyConnectionError):
160
+ await self.api.fetch_data(None)
149
161
 
150
162
  async def test_fetch_data_client_error(self):
151
163
  """Test handling of client errors."""
152
- self.session.get = AsyncMock(side_effect=aiohttp.ClientError)
164
+ self.session.get = MagicMock(side_effect=aiohttp.ClientError)
153
165
 
154
- result = await self.api.fetch_data(None)
166
+ with self.assertRaises(HolfuyConnectionError):
167
+ await self.api.fetch_data(None)
168
+
169
+ async def test_fetch_data_rejected_key(self):
170
+ """A rejected key gets HTTP 200 with a plain-text "No access" body."""
171
+ mock_resp = AsyncMock()
172
+ mock_resp.status = 200
173
+ mock_resp.content_type = "text/html"
174
+ mock_resp.text = AsyncMock(return_value="No access")
175
+ self.session.get = _get_returning(mock_resp)
155
176
 
156
- self.assertIsNone(result)
177
+ with self.assertRaises(HolfuyAuthError):
178
+ await self.api.fetch_data(None)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyholfuy
3
- Version: 0.2
3
+ Version: 0.3
4
4
  Summary: A Python library for interacting with Holfuy weather stations.
5
5
  Author-email: Håvard Dagenborg <haavardj@dagenborg.net>
6
6
  License-Expression: MIT
@@ -26,7 +26,7 @@ A Python library for talking to [Holfuy Weather Stations](https://holfuy.com/).
26
26
 
27
27
  You need an API key to access the service. To obtain one, please visit the [Holfuy API](https://api.holfuy.com/live/) pages.
28
28
 
29
- The Library only access live data.
29
+ The library can only access live data.
30
30
 
31
31
  ## Example Code
32
32
  Here is an example of how to use the library.
@@ -38,10 +38,22 @@ from holfuy import HolfuyService
38
38
 
39
39
  async def main():
40
40
  async with aiohttp.ClientSession() as session:
41
- s = HolfuyService("", session)
42
- data = await s.fetch_data(["101"])
43
- print(data)
41
+ s = HolfuyService("", session)
42
+ data = await s.fetch_data(["101"])
43
+ print(data)
44
44
 
45
45
  if __name__ == "__main__":
46
46
  asyncio.run(main())
47
47
  ```
48
+
49
+ ## Return value and errors
50
+
51
+ `fetch_data()` returns `{"measurements": [...]}` with one entry per station, also when
52
+ a single station is requested. A station the key cannot read is still listed, with
53
+ `error` and `errorCode` instead of data.
54
+
55
+ It raises, all subclasses of `holfuy.HolfuyError`:
56
+
57
+ - `HolfuyAuthError`: the API key is rejected, or it has no access to any requested station.
58
+ - `HolfuyConnectionError`: network error, timeout, or an HTTP error status.
59
+ - `HolfuyError`: any other unexpected reply.
@@ -4,6 +4,7 @@ README.md
4
4
  pyproject.toml
5
5
  holfuy/__init__.py
6
6
  holfuy/const.py
7
+ holfuy/exceptions.py
7
8
  holfuy/test_holfuy.py
8
9
  pyholfuy.egg-info/PKG-INFO
9
10
  pyholfuy.egg-info/SOURCES.txt
@@ -6,7 +6,7 @@ build-backend = "setuptools.build_meta"
6
6
 
7
7
  [project]
8
8
  name = "pyholfuy"
9
- version = "0.2"
9
+ version = "0.3"
10
10
  description = "A Python library for interacting with Holfuy weather stations."
11
11
  authors = [{name="Håvard Dagenborg", email="haavardj@dagenborg.net"}]
12
12
  license = "MIT"
pyholfuy-0.2/README.md DELETED
@@ -1,26 +0,0 @@
1
- ![Holfy Logo](https://holfuy.com/image/logo/holfuy-logo.png)
2
-
3
- # PyHolfuy
4
- A Python library for talking to [Holfuy Weather Stations](https://holfuy.com/).
5
-
6
- You need an API key to access the service. To obtain one, please visit the [Holfuy API](https://api.holfuy.com/live/) pages.
7
-
8
- The Library only access live data.
9
-
10
- ## Example Code
11
- Here is an example of how to use the library.
12
-
13
- ```python
14
- import aiohttp
15
- import asyncio
16
- from holfuy import HolfuyService
17
-
18
- async def main():
19
- async with aiohttp.ClientSession() as session:
20
- s = HolfuyService("", session)
21
- data = await s.fetch_data(["101"])
22
- print(data)
23
-
24
- if __name__ == "__main__":
25
- asyncio.run(main())
26
- ```
File without changes
File without changes
File without changes