This commit is contained in:
Andrew Herrington
2021-12-10 22:43:32 -06:00
parent 7c3f263b5a
commit dffb9be1bf
6 changed files with 90 additions and 79 deletions

View File

@@ -1,6 +1,6 @@
[flake8] [flake8]
select = B,B9,C,D,DAR,E,F,N,RST,S,W select = B,B9,C,D,DAR,E,F,N,RST,S,W
ignore = E203,E501,RST201,RST203,RST301,W503,B902,N805 ignore = E203,E501,RST201,RST203,RST301,W503,B902,N805,DAR402
max-line-length = 119 max-line-length = 119
max-complexity = 10 max-complexity = 10
docstring-convention = google docstring-convention = google

View File

@@ -12,10 +12,14 @@ from urllib.parse import urljoin
from urllib.parse import urlparse from urllib.parse import urlparse
from weakref import finalize from weakref import finalize
from httpx import Client, Response from httpx import Client
from httpx import Response
from .exceptions import BadAPIRequestError
from .exceptions import CheckNotFoundError
from .exceptions import HCAPIAuthError
from .exceptions import HCAPIError
from healthchecks_io.schemas import checks from healthchecks_io.schemas import checks
from .exceptions import HCAPIAuthError, HCAPIError, CheckNotFoundError, BadAPIRequestError
class AbstractClient(ABC): class AbstractClient(ABC):
@@ -91,6 +95,7 @@ class AbstractClient(ABC):
HCAPIAuthError: Raised when status_code == 401 or 403 HCAPIAuthError: Raised when status_code == 401 or 403
HCAPIError: Raised when status_code is 5xx HCAPIError: Raised when status_code is 5xx
CheckNotFoundError: Raised when status_code is 404 CheckNotFoundError: Raised when status_code is 404
BadAPIRequestError: Raised when status_code is 400
Returns: Returns:
Response: the passed in response object Response: the passed in response object
@@ -103,12 +108,14 @@ class AbstractClient(ABC):
f"Error when reaching out to HC API at {response.request.url}. " f"Error when reaching out to HC API at {response.request.url}. "
f"Status Code {response.status_code}. Response {response.text}" f"Status Code {response.status_code}. Response {response.text}"
) )
if response.status_code == 404: if response.status_code == 404:
raise CheckNotFoundError(f"CHeck not found at {response.request.url}") raise CheckNotFoundError(f"CHeck not found at {response.request.url}")
if response.status_code == 400: if response.status_code == 400:
raise BadAPIRequestError(f"Bad request when requesting {response.request.url}. {response.text}") raise BadAPIRequestError(
f"Bad request when requesting {response.request.url}. {response.text}"
)
return response return response

View File

@@ -73,9 +73,9 @@ class AsyncClient(AbstractClient):
checks.Check.from_api_result(check_data) checks.Check.from_api_result(check_data)
for check_data in response.json()["checks"] for check_data in response.json()["checks"]
] ]
async def get_check(self, check_id: str) -> checks.Check: async def get_check(self, check_id: str) -> checks.Check:
"""Get a single check by id. """Get a single check by id.
check_id can either be a check uuid if using a read/write api key check_id can either be a check uuid if using a read/write api key
or a unique key if using a read only api key. or a unique key if using a read only api key.
@@ -97,9 +97,9 @@ class AsyncClient(AbstractClient):
return checks.Check.from_api_result(response.json()) return checks.Check.from_api_result(response.json())
async def pause_check(self, check_id: str) -> checks.Check: async def pause_check(self, check_id: str) -> checks.Check:
"""Disables monitoring for a check without removing it. """Disables monitoring for a check without removing it.
The check goes into a "paused" state. The check goes into a "paused" state.
You can resume monitoring of the check by pinging it. You can resume monitoring of the check by pinging it.
check_id must be a uuid, not a unique id check_id must be a uuid, not a unique id
@@ -117,11 +117,11 @@ class AsyncClient(AbstractClient):
""" """
request_url = self._get_api_request_url(f"checks/{check_id}/pause") request_url = self._get_api_request_url(f"checks/{check_id}/pause")
response = self.check_response(await self._client.post(request_url , data={})) response = self.check_response(await self._client.post(request_url, data={}))
return checks.Check.from_api_result(response.json()) return checks.Check.from_api_result(response.json())
async def delete_check(self, check_id: str) -> checks.Check: async def delete_check(self, check_id: str) -> checks.Check:
"""Permanently deletes the check from the user's account. """Permanently deletes the check from the user's account.
check_id must be a uuid, not a unique id check_id must be a uuid, not a unique id
@@ -143,9 +143,9 @@ class AsyncClient(AbstractClient):
async def get_check_pings(self, check_id: str) -> List[checks.CheckPings]: async def get_check_pings(self, check_id: str) -> List[checks.CheckPings]:
"""Returns a list of pings this check has received. """Returns a list of pings this check has received.
This endpoint returns pings in reverse order (most recent first), This endpoint returns pings in reverse order (most recent first),
and the total number of returned pings depends on the account's and the total number of returned pings depends on the account's
billing plan: 100 for free accounts, 1000 for paid accounts. billing plan: 100 for free accounts, 1000 for paid accounts.
Args: Args:
@@ -167,10 +167,15 @@ class AsyncClient(AbstractClient):
for check_data in response.json()["pings"] for check_data in response.json()["pings"]
] ]
async def get_check_flips(self, check_id: str, seconds: Optional[int] = None, start: Optional[int] = None, end: Optional[int] = None) -> List[checks.CheckStatuses]: async def get_check_flips(
""" self,
Returns a list of "flips" this check has experienced. check_id: str,
seconds: Optional[int] = None,
start: Optional[int] = None,
end: Optional[int] = None,
) -> List[checks.CheckStatuses]:
"""Returns a list of "flips" this check has experienced.
A flip is a change of status (from "down" to "up," or from "up" to "down"). A flip is a change of status (from "down" to "up," or from "up" to "down").
Raises: Raises:
@@ -187,19 +192,16 @@ class AsyncClient(AbstractClient):
Returns: Returns:
List[checks.CheckStatuses]: List of status flips for this check List[checks.CheckStatuses]: List of status flips for this check
""" """
params = dict() params = dict()
if seconds is not None and seconds >=0: if seconds is not None and seconds >= 0:
params['seconds'] = seconds params["seconds"] = seconds
if start is not None and start >= 0: if start is not None and start >= 0:
params['start'] = start params["start"] = start
if end is not None and end >= 0: if end is not None and end >= 0:
params['end'] = end params["end"] = end
request_url = self._get_api_request_url(f"checks/{check_id}/flips/", params) request_url = self._get_api_request_url(f"checks/{check_id}/flips/", params)
response = self.check_response(await self._client.get(request_url)) response = self.check_response(await self._client.get(request_url))
return [ return [checks.CheckStatuses(**status_data) for status_data in response.json()]
checks.CheckStatuses(**status_data)
for status_data in response.json()
]

View File

@@ -12,12 +12,14 @@ class HCAPIAuthError(HCAPIError):
... ...
class CheckNotFoundError(HCAPIError): class CheckNotFoundError(HCAPIError):
"""Thrown when getting a check returns a 404.""" """Thrown when getting a check returns a 404."""
... ...
class BadAPIRequestError(HCAPIError): class BadAPIRequestError(HCAPIError):
"""Thrown when an api request returns a 400.""" """Thrown when an api request returns a 400."""
... ...

View File

@@ -6,8 +6,10 @@ from httpx import AsyncClient as HTTPXAsyncClient
from httpx import Response from httpx import Response
from healthchecks_io.client import AsyncClient from healthchecks_io.client import AsyncClient
from healthchecks_io.client.exceptions import BadAPIRequestError
from healthchecks_io.client.exceptions import CheckNotFoundError
from healthchecks_io.client.exceptions import HCAPIAuthError from healthchecks_io.client.exceptions import HCAPIAuthError
from healthchecks_io.client.exceptions import HCAPIError, CheckNotFoundError, BadAPIRequestError from healthchecks_io.client.exceptions import HCAPIError
@pytest.mark.asyncio @pytest.mark.asyncio
@@ -93,9 +95,7 @@ async def test_get_check_200(fake_check_api_result, respx_mock, test_async_clien
async def test_check_get_404(respx_mock, test_async_client): async def test_check_get_404(respx_mock, test_async_client):
assert test_async_client._client is not None assert test_async_client._client is not None
checks_url = urljoin(test_async_client._api_url, "checks/test") checks_url = urljoin(test_async_client._api_url, "checks/test")
respx_mock.get(checks_url).mock( respx_mock.get(checks_url).mock(return_value=Response(status_code=404))
return_value=Response(status_code=404)
)
with pytest.raises(CheckNotFoundError): with pytest.raises(CheckNotFoundError):
await test_async_client.get_check("test") await test_async_client.get_check("test")
@@ -117,9 +117,7 @@ async def test_pause_check_200(fake_check_api_result, respx_mock, test_async_cli
async def test_check_pause_404(respx_mock, test_async_client): async def test_check_pause_404(respx_mock, test_async_client):
assert test_async_client._client is not None assert test_async_client._client is not None
checks_url = urljoin(test_async_client._api_url, "checks/test/pause") checks_url = urljoin(test_async_client._api_url, "checks/test/pause")
respx_mock.post(checks_url).mock( respx_mock.post(checks_url).mock(return_value=Response(status_code=404))
return_value=Response(status_code=404)
)
with pytest.raises(CheckNotFoundError): with pytest.raises(CheckNotFoundError):
await test_async_client.pause_check("test") await test_async_client.pause_check("test")
@@ -141,28 +139,33 @@ async def test_delete_check_200(fake_check_api_result, respx_mock, test_async_cl
async def test_delete_pause404(respx_mock, test_async_client): async def test_delete_pause404(respx_mock, test_async_client):
assert test_async_client._client is not None assert test_async_client._client is not None
checks_url = urljoin(test_async_client._api_url, "checks/test") checks_url = urljoin(test_async_client._api_url, "checks/test")
respx_mock.delete(checks_url).mock( respx_mock.delete(checks_url).mock(return_value=Response(status_code=404))
return_value=Response(status_code=404)
)
with pytest.raises(CheckNotFoundError): with pytest.raises(CheckNotFoundError):
await test_async_client.delete_check("test") await test_async_client.delete_check("test")
@pytest.mark.asyncio @pytest.mark.asyncio
@pytest.mark.respx @pytest.mark.respx
async def test_get_check_pings_200(fake_check_pings_api_result, respx_mock, test_async_client): async def test_get_check_pings_200(
fake_check_pings_api_result, respx_mock, test_async_client
):
assert test_async_client._client is not None assert test_async_client._client is not None
checks_url = urljoin(test_async_client._api_url, "checks/test/pings/") checks_url = urljoin(test_async_client._api_url, "checks/test/pings/")
respx_mock.get(checks_url).mock( respx_mock.get(checks_url).mock(
return_value=Response(status_code=200, json={"pings": fake_check_pings_api_result}) return_value=Response(
status_code=200, json={"pings": fake_check_pings_api_result}
)
) )
pings = await test_async_client.get_check_pings("test") pings = await test_async_client.get_check_pings("test")
assert len(pings) == len(fake_check_pings_api_result) assert len(pings) == len(fake_check_pings_api_result)
assert pings[0].type == fake_check_pings_api_result[0]['type'] assert pings[0].type == fake_check_pings_api_result[0]["type"]
@pytest.mark.asyncio @pytest.mark.asyncio
@pytest.mark.respx @pytest.mark.respx
async def test_get_check_flips_200(fake_check_flips_api_result, respx_mock, test_async_client): async def test_get_check_flips_200(
fake_check_flips_api_result, respx_mock, test_async_client
):
assert test_async_client._client is not None assert test_async_client._client is not None
checks_url = urljoin(test_async_client._api_url, "checks/test/flips/") checks_url = urljoin(test_async_client._api_url, "checks/test/flips/")
respx_mock.get(checks_url).mock( respx_mock.get(checks_url).mock(
@@ -170,29 +173,33 @@ async def test_get_check_flips_200(fake_check_flips_api_result, respx_mock, test
) )
flips = await test_async_client.get_check_flips("test") flips = await test_async_client.get_check_flips("test")
assert len(flips) == len(fake_check_flips_api_result) assert len(flips) == len(fake_check_flips_api_result)
assert flips[0].up == fake_check_flips_api_result[0]['up'] assert flips[0].up == fake_check_flips_api_result[0]["up"]
@pytest.mark.asyncio @pytest.mark.asyncio
@pytest.mark.respx @pytest.mark.respx
async def test_get_check_flips_params_200(fake_check_flips_api_result, respx_mock, test_async_client): async def test_get_check_flips_params_200(
fake_check_flips_api_result, respx_mock, test_async_client
):
assert test_async_client._client is not None assert test_async_client._client is not None
checks_url = urljoin(test_async_client._api_url, "checks/test/flips/?seconds=1&start=1&end=1") checks_url = urljoin(
test_async_client._api_url, "checks/test/flips/?seconds=1&start=1&end=1"
)
respx_mock.get(checks_url).mock( respx_mock.get(checks_url).mock(
return_value=Response(status_code=200, json=fake_check_flips_api_result) return_value=Response(status_code=200, json=fake_check_flips_api_result)
) )
flips = await test_async_client.get_check_flips("test", seconds=1, start=1, end=1) flips = await test_async_client.get_check_flips("test", seconds=1, start=1, end=1)
assert len(flips) == len(fake_check_flips_api_result) assert len(flips) == len(fake_check_flips_api_result)
assert flips[0].up == fake_check_flips_api_result[0]['up'] assert flips[0].up == fake_check_flips_api_result[0]["up"]
@pytest.mark.asyncio @pytest.mark.asyncio
@pytest.mark.respx @pytest.mark.respx
async def test_get_check_flips_400(fake_check_flips_api_result, respx_mock, test_async_client): async def test_get_check_flips_400(
fake_check_flips_api_result, respx_mock, test_async_client
):
assert test_async_client._client is not None assert test_async_client._client is not None
checks_url = urljoin(test_async_client._api_url, "checks/test/flips/") checks_url = urljoin(test_async_client._api_url, "checks/test/flips/")
respx_mock.get(checks_url).mock( respx_mock.get(checks_url).mock(return_value=Response(status_code=400))
return_value=Response(status_code=400)
)
with pytest.raises(BadAPIRequestError): with pytest.raises(BadAPIRequestError):
await test_async_client.get_check_flips("test") await test_async_client.get_check_flips("test")

View File

@@ -87,10 +87,11 @@ def test_async_client():
yield AsyncClient(api_key="test", api_url="https://localhost/api") yield AsyncClient(api_key="test", api_url="https://localhost/api")
@pytest.fixture @pytest.fixture
def fake_check_pings_api_result(): def fake_check_pings_api_result():
return [ return [
{ {
"type": "success", "type": "success",
"date": "2020-06-09T14:51:06.113073+00:00", "date": "2020-06-09T14:51:06.113073+00:00",
"n": 4, "n": 4,
@@ -98,18 +99,18 @@ def fake_check_pings_api_result():
"remote_addr": "192.0.2.0", "remote_addr": "192.0.2.0",
"method": "GET", "method": "GET",
"ua": "curl/7.68.0", "ua": "curl/7.68.0",
"duration": 2.896736 "duration": 2.896736,
}, },
{ {
"type": "start", "type": "start",
"date": "2020-06-09T14:51:03.216337+00:00", "date": "2020-06-09T14:51:03.216337+00:00",
"n": 3, "n": 3,
"scheme": "http", "scheme": "http",
"remote_addr": "192.0.2.0", "remote_addr": "192.0.2.0",
"method": "GET", "method": "GET",
"ua": "curl/7.68.0" "ua": "curl/7.68.0",
}, },
{ {
"type": "success", "type": "success",
"date": "2020-06-09T14:50:59.633577+00:00", "date": "2020-06-09T14:50:59.633577+00:00",
"n": 2, "n": 2,
@@ -117,32 +118,24 @@ def fake_check_pings_api_result():
"remote_addr": "192.0.2.0", "remote_addr": "192.0.2.0",
"method": "GET", "method": "GET",
"ua": "curl/7.68.0", "ua": "curl/7.68.0",
"duration": 2.997976 "duration": 2.997976,
}, },
{ {
"type": "start", "type": "start",
"date": "2020-06-09T14:50:56.635601+00:00", "date": "2020-06-09T14:50:56.635601+00:00",
"n": 1, "n": 1,
"scheme": "http", "scheme": "http",
"remote_addr": "192.0.2.0", "remote_addr": "192.0.2.0",
"method": "GET", "method": "GET",
"ua": "curl/7.68.0" "ua": "curl/7.68.0",
} },
] ]
@pytest.fixture @pytest.fixture
def fake_check_flips_api_result(): def fake_check_flips_api_result():
return [ return [
{ {"timestamp": "2020-03-23T10:18:23+00:00", "up": 1},
"timestamp": "2020-03-23T10:18:23+00:00", {"timestamp": "2020-03-23T10:17:15+00:00", "up": 0},
"up": 1 {"timestamp": "2020-03-23T10:16:18+00:00", "up": 1},
}, ]
{
"timestamp": "2020-03-23T10:17:15+00:00",
"up": 0
},
{
"timestamp": "2020-03-23T10:16:18+00:00",
"up": 1
}
]