Skip to content

Commit 174a746

Browse files
committed
Initial test implementation and update timeout api usage
1 parent 55c25d8 commit 174a746

File tree

4 files changed

+142
-63
lines changed

4 files changed

+142
-63
lines changed

pyproject.toml

+1
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ pycodestyle = "^2.6.0"
4343
pylint = "^2.10.2"
4444
pytest = "^6.2.1"
4545
pytest-cov = "^2.12.1"
46+
pytest-asyncio = "^0.25.3"
4647

4748
[tool.poetry.scripts]
4849
surepy = 'surepy.surecli:cli'

surepy/client.py

+64-63
Original file line numberDiff line numberDiff line change
@@ -67,9 +67,7 @@ def token_seems_valid(token: str) -> bool:
6767
Returns:
6868
bool: True if ``token`` seems valid
6969
"""
70-
return (
71-
(token is not None) and token.isascii() and token.isprintable() and (320 < len(token))
72-
)
70+
return (token is not None) and token.isascii() and token.isprintable() and (320 < len(token))
7371

7472

7573
def find_token() -> str | None:
@@ -228,76 +226,79 @@ async def call(
228226

229227
response_data = None
230228

231-
session = self._session if self._session else aiohttp.ClientSession()
232-
233229
try:
234-
with async_timeout.timeout(self._api_timeout):
235-
headers = self._generate_headers()
230+
session = (
231+
self._session
232+
if self._session
233+
else aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=self._api_timeout))
234+
)
236235

237-
# use etag if available
238-
if resource in self._etags:
239-
headers[ETAG] = str(self._etags.get(resource))
240-
# logger.debug("🐾 \x1b[38;2;255;26;102m·\x1b[0m etag: %s", headers[ETAG])
236+
headers = self._generate_headers()
241237

242-
await session.options(resource, headers=headers)
243-
response: aiohttp.ClientResponse = await session.request(
244-
method, resource, headers=headers, json=data
245-
)
238+
# use etag if available
239+
if resource in self._etags:
240+
headers[ETAG] = str(self._etags.get(resource))
241+
# logger.debug("🐾 \x1b[38;2;255;26;102m·\x1b[0m etag: %s", headers[ETAG])
242+
243+
await session.options(resource, headers=headers)
244+
response: aiohttp.ClientResponse = await session.request(
245+
method, resource, headers=headers, json=data, timeout=self._api_timeout
246+
)
246247

247-
if response.status == HTTPStatus.OK or response.status == HTTPStatus.CREATED:
248-
self.resources[resource] = response_data = await response.json()
249-
250-
if ETAG in response.headers:
251-
self._etags[resource] = response.headers[ETAG].strip('"')
252-
253-
elif response.status == HTTPStatus.NOT_MODIFIED:
254-
# Etag header matched, no new data available
255-
logger.debug(
256-
"🐾 \x1b[38;2;0;255;0m·\x1b[0m %d: etag matched - no new data available",
257-
response.status,
258-
)
259-
260-
elif response.status == HTTPStatus.UNAUTHORIZED:
261-
logger.error(
262-
"🐾 \x1b[38;2;255;26;102m·\x1b[0m %s %s: %d | %s",
263-
method,
264-
resource.replace("https://", ""),
265-
response.status,
266-
response,
267-
)
268-
self._auth_token = None
269-
if not second_try:
270-
token_refreshed = await self.get_token()
271-
if token_refreshed:
272-
await self.call(method="GET", resource=resource, second_try=True)
273-
274-
raise SurePetcareAuthenticationError()
275-
276-
else:
277-
logger.info(
278-
"🐾 \x1b[38;2;255;0;255m·\x1b[0m %s %s: %d | %s",
279-
method,
280-
resource.replace("https://", ""),
281-
response.status,
282-
response,
283-
)
284-
285-
if response_data:
286-
responselen = len(response_data.get("data", []))
287-
else:
288-
responselen = 0
248+
if response.status == HTTPStatus.OK or response.status == HTTPStatus.CREATED:
249+
self.resources[resource] = response_data = await response.json()
250+
251+
if ETAG in response.headers:
252+
self._etags[resource] = response.headers[ETAG].strip('"')
253+
254+
elif response.status == HTTPStatus.NOT_MODIFIED:
255+
# Etag header matched, no new data available
289256
logger.debug(
290-
"🐾 \x1b[38;2;0;255;0m·\x1b[0m %s %s | %d",
257+
"🐾 \x1b[38;2;0;255;0m·\x1b[0m %d: etag matched - no new data available",
258+
response.status,
259+
)
260+
261+
elif response.status == HTTPStatus.UNAUTHORIZED:
262+
logger.error(
263+
"🐾 \x1b[38;2;255;26;102m·\x1b[0m %s %s: %d | %s",
291264
method,
292265
resource.replace("https://", ""),
293-
responselen,
266+
response.status,
267+
response,
294268
)
269+
self._auth_token = None
270+
if not second_try:
271+
token_refreshed = await self.get_token()
272+
if token_refreshed:
273+
await self.call(method="GET", resource=resource, second_try=True)
274+
275+
raise SurePetcareAuthenticationError()
276+
277+
else:
278+
logger.info(
279+
"🐾 \x1b[38;2;255;0;255m·\x1b[0m %s %s: %d | %s",
280+
method,
281+
resource.replace("https://", ""),
282+
response.status,
283+
response,
284+
)
285+
286+
if response_data:
287+
responselen = len(response_data.get("data", []))
288+
else:
289+
responselen = 0
290+
logger.debug(
291+
"🐾 \x1b[38;2;0;255;0m·\x1b[0m %s %s | %d",
292+
method,
293+
resource.replace("https://", ""),
294+
responselen,
295+
)
295296

296-
if method == "DELETE" and response.status == HTTPStatus.NO_CONTENT:
297-
# TODO: this does not return any data, is there a better way?
298-
return "DELETE 204 No Content"
297+
if method == "DELETE" and response.status == HTTPStatus.NO_CONTENT:
298+
# TODO: this does not return any data, is there a better way?
299+
return "DELETE 204 No Content"
299300

300-
return response_data
301+
return response_data
301302

302303
except (asyncio.TimeoutError, aiohttp.ClientError) as error:
303304
logger.error("Can not load data from %s", resource)

tests/__init__.py

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""Tests for SurePy."""

tests/test_client.py

+76
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
from pathlib import Path
2+
import asyncio
3+
import pytest
4+
import async_timeout
5+
import pprint
6+
from aiohttp import ClientSession, TCPConnector
7+
from shutil import copyfile
8+
from datetime import datetime, timedelta
9+
import configparser
10+
11+
from surepy import Surepy
12+
from surepy.client import SureAPIClient
13+
from surepy.entities import SurepyEntity
14+
from surepy.entities.devices import Feeder, Felaqua, Flap, Hub, SurepyDevice
15+
from surepy.entities.pet import Pet
16+
from surepy.enums import EntityType
17+
18+
token_file = Path("~/.surepy.token").expanduser()
19+
old_token_file = token_file.with_suffix(".old_token")
20+
auth_file = Path("~/.surepy.auth").expanduser()
21+
config = configparser.ConfigParser()
22+
config.read(str(auth_file))
23+
24+
25+
def file_older_then(file: Path, delta: timedelta) -> bool:
26+
return datetime.fromtimestamp(file.stat().st_mtime) < (datetime.now() - delta)
27+
28+
29+
async def get_surepy() -> Surepy | None:
30+
if not token_file.exists() or file_older_then(token_file, timedelta(minutes=5)):
31+
32+
spy = Surepy(
33+
email=config.get("Login", "email"),
34+
password=config.get("Login", "password"),
35+
)
36+
37+
if surepy_token := await spy.sac.get_token():
38+
39+
if token_file.exists() and surepy_token != token_file.read_text(encoding="utf-8"):
40+
copyfile(token_file, old_token_file)
41+
42+
token_file.write_text(surepy_token, encoding="utf-8")
43+
44+
return spy
45+
else:
46+
return Surepy(auth_token=token_file.read_text(encoding="utf-8"))
47+
48+
49+
@pytest.mark.asyncio
50+
async def test_get_entities() -> None:
51+
spy = await get_surepy()
52+
53+
response = await spy.get_entities(refresh=True)
54+
55+
with open("response.txt", "w") as file:
56+
file.write(pprint.pformat(response))
57+
58+
assert len(response) > 0
59+
60+
61+
@pytest.mark.asyncio
62+
async def test_get_actions() -> None:
63+
spy = await get_surepy()
64+
65+
response = await spy.get_actions(household_id=config.get("IDs", "household"))
66+
67+
assert len(response) > 0
68+
69+
70+
@pytest.mark.asyncio
71+
async def test_get_latest_anonymous_drinks() -> None:
72+
spy = await get_surepy()
73+
74+
response = await spy.get_latest_anonymous_drinks(household_id=config.get("IDs", "household"))
75+
76+
assert len(response) > 0

0 commit comments

Comments
 (0)