more api methods

This commit is contained in:
Andrew Herrington
2021-12-10 22:42:03 -06:00
parent 15d1d41340
commit 7c3f263b5a
6 changed files with 284 additions and 8 deletions

View File

@@ -15,7 +15,7 @@ from weakref import finalize
from httpx import Client, Response from httpx import Client, Response
from healthchecks_io.schemas import checks from healthchecks_io.schemas import checks
from .exceptions import HCAPIAuthError, HCAPIError, CheckNotFoundError from .exceptions import HCAPIAuthError, HCAPIError, CheckNotFoundError, BadAPIRequestError
class AbstractClient(ABC): class AbstractClient(ABC):
@@ -90,6 +90,7 @@ class AbstractClient(ABC):
Raises: Raises:
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
Returns: Returns:
Response: the passed in response object Response: the passed in response object
@@ -103,6 +104,12 @@ class AbstractClient(ABC):
f"Status Code {response.status_code}. Response {response.text}" f"Status Code {response.status_code}. Response {response.text}"
) )
if response.status_code == 404:
raise CheckNotFoundError(f"CHeck not found at {response.request.url}")
if response.status_code == 400:
raise BadAPIRequestError(f"Bad request when requesting {response.request.url}. {response.text}")
return response return response
@staticmethod @staticmethod

View File

@@ -6,8 +6,6 @@ from typing import Optional
from httpx import AsyncClient as HTTPXAsyncClient from httpx import AsyncClient as HTTPXAsyncClient
from ._abstract import AbstractClient from ._abstract import AbstractClient
from .exceptions import HCAPIAuthError, CheckNotFoundError
from .exceptions import HCAPIError
from healthchecks_io import VERSION from healthchecks_io import VERSION
from healthchecks_io.schemas import checks from healthchecks_io.schemas import checks
@@ -89,11 +87,119 @@ class AsyncClient(AbstractClient):
checks.Check: the check checks.Check: the check
Raises: Raises:
CheckNotFoundError: when no check with check_id is found HCAPIAuthError: Raised when status_code == 401 or 403
HCAPIError: Raised when status_code is 5xx
CheckNotFoundError: Raised when status_code is 404
""" """
request_url = self._get_api_request_url(f"checks/{check_id}") request_url = self._get_api_request_url(f"checks/{check_id}")
response = self.check_response(await self._client.get(request_url)) response = self.check_response(await self._client.get(request_url))
if response.status_code == 404:
raise CheckNotFoundError(f"{check_id} not found at {request_url}")
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:
"""Disables monitoring for a check without removing it.
The check goes into a "paused" state.
You can resume monitoring of the check by pinging it.
check_id must be a uuid, not a unique id
Args:
check_id (str): check's uuid
Returns:
checks.Check: the check just paused
Raises:
HCAPIAuthError: Raised when status_code == 401 or 403
HCAPIError: Raised when status_code is 5xx
CheckNotFoundError: Raised when status_code is 404
"""
request_url = self._get_api_request_url(f"checks/{check_id}/pause")
response = self.check_response(await self._client.post(request_url , data={}))
return checks.Check.from_api_result(response.json())
async def delete_check(self, check_id: str) -> checks.Check:
"""Permanently deletes the check from the user's account.
check_id must be a uuid, not a unique id
Args:
check_id (str): check's uuid
Returns:
checks.Check: the check just deleted
Raises:
HCAPIAuthError: Raised when status_code == 401 or 403
HCAPIError: Raised when status_code is 5xx
CheckNotFoundError: Raised when status_code is 404
"""
request_url = self._get_api_request_url(f"checks/{check_id}")
response = self.check_response(await self._client.delete(request_url))
return checks.Check.from_api_result(response.json())
async def get_check_pings(self, check_id: str) -> List[checks.CheckPings]:
"""Returns a list of pings this check has received.
This endpoint returns pings in reverse order (most recent first),
and the total number of returned pings depends on the account's
billing plan: 100 for free accounts, 1000 for paid accounts.
Args:
check_id (str): check's uuid
Returns:
List[checks.CheckPings]: list of pings this check has received
Raises:
HCAPIAuthError: Raised when status_code == 401 or 403
HCAPIError: Raised when status_code is 5xx
CheckNotFoundError: Raised when status_code is 404
"""
request_url = self._get_api_request_url(f"checks/{check_id}/pings/")
response = self.check_response(await self._client.get(request_url))
return [
checks.CheckPings.from_api_result(check_data)
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]:
"""
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").
Raises:
HCAPIAuthError: Raised when status_code == 401 or 403
HCAPIError: Raised when status_code is 5xx
CheckNotFoundError: Raised when status_code is 404
BadAPIRequestError: Raised when status_code is 400
Args:
check_id (str): check uuid
seconds (Optional[int], optional): Returns the flips from the last value seconds. Defaults to None.
start (Optional[int], optional): Returns flips that are newer than the specified UNIX timestamp.. Defaults to None.
end (Optional[int], optional): Returns flips that are older than the specified UNIX timestamp.. Defaults to None.
Returns:
List[checks.CheckStatuses]: List of status flips for this check
"""
params = dict()
if seconds is not None and seconds >=0:
params['seconds'] = seconds
if start is not None and start >= 0:
params['start'] = start
if end is not None and end >= 0:
params['end'] = end
request_url = self._get_api_request_url(f"checks/{check_id}/flips/", params)
response = self.check_response(await self._client.get(request_url))
return [
checks.CheckStatuses(**status_data)
for status_data in response.json()
]

View File

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

View File

@@ -173,7 +173,7 @@ class CheckPings(BaseModel):
remote_addr: str remote_addr: str
method: str method: str
user_agent: str user_agent: str
duration: float duration: Optional[float] = None
@classmethod @classmethod
def from_api_result( def from_api_result(

View File

@@ -7,7 +7,7 @@ from httpx import Response
from healthchecks_io.client import AsyncClient from healthchecks_io.client import AsyncClient
from healthchecks_io.client.exceptions import HCAPIAuthError from healthchecks_io.client.exceptions import HCAPIAuthError
from healthchecks_io.client.exceptions import HCAPIError, CheckNotFoundError from healthchecks_io.client.exceptions import HCAPIError, CheckNotFoundError, BadAPIRequestError
@pytest.mark.asyncio @pytest.mark.asyncio
@@ -98,3 +98,101 @@ async def test_check_get_404(respx_mock, test_async_client):
) )
with pytest.raises(CheckNotFoundError): with pytest.raises(CheckNotFoundError):
await test_async_client.get_check("test") await test_async_client.get_check("test")
@pytest.mark.asyncio
@pytest.mark.respx
async def test_pause_check_200(fake_check_api_result, respx_mock, test_async_client):
assert test_async_client._client is not None
checks_url = urljoin(test_async_client._api_url, "checks/test/pause")
respx_mock.post(checks_url).mock(
return_value=Response(status_code=200, json=fake_check_api_result)
)
check = await test_async_client.pause_check(check_id="test")
assert check.name == fake_check_api_result["name"]
@pytest.mark.asyncio
@pytest.mark.respx
async def test_check_pause_404(respx_mock, test_async_client):
assert test_async_client._client is not None
checks_url = urljoin(test_async_client._api_url, "checks/test/pause")
respx_mock.post(checks_url).mock(
return_value=Response(status_code=404)
)
with pytest.raises(CheckNotFoundError):
await test_async_client.pause_check("test")
@pytest.mark.asyncio
@pytest.mark.respx
async def test_delete_check_200(fake_check_api_result, respx_mock, test_async_client):
assert test_async_client._client is not None
checks_url = urljoin(test_async_client._api_url, "checks/test")
respx_mock.delete(checks_url).mock(
return_value=Response(status_code=200, json=fake_check_api_result)
)
check = await test_async_client.delete_check(check_id="test")
assert check.name == fake_check_api_result["name"]
@pytest.mark.asyncio
@pytest.mark.respx
async def test_delete_pause404(respx_mock, test_async_client):
assert test_async_client._client is not None
checks_url = urljoin(test_async_client._api_url, "checks/test")
respx_mock.delete(checks_url).mock(
return_value=Response(status_code=404)
)
with pytest.raises(CheckNotFoundError):
await test_async_client.delete_check("test")
@pytest.mark.asyncio
@pytest.mark.respx
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
checks_url = urljoin(test_async_client._api_url, "checks/test/pings/")
respx_mock.get(checks_url).mock(
return_value=Response(status_code=200, json={"pings": fake_check_pings_api_result})
)
pings = await test_async_client.get_check_pings("test")
assert len(pings) == len(fake_check_pings_api_result)
assert pings[0].type == fake_check_pings_api_result[0]['type']
@pytest.mark.asyncio
@pytest.mark.respx
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
checks_url = urljoin(test_async_client._api_url, "checks/test/flips/")
respx_mock.get(checks_url).mock(
return_value=Response(status_code=200, json=fake_check_flips_api_result)
)
flips = await test_async_client.get_check_flips("test")
assert len(flips) == len(fake_check_flips_api_result)
assert flips[0].up == fake_check_flips_api_result[0]['up']
@pytest.mark.asyncio
@pytest.mark.respx
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
checks_url = urljoin(test_async_client._api_url, "checks/test/flips/?seconds=1&start=1&end=1")
respx_mock.get(checks_url).mock(
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)
assert len(flips) == len(fake_check_flips_api_result)
assert flips[0].up == fake_check_flips_api_result[0]['up']
@pytest.mark.asyncio
@pytest.mark.respx
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
checks_url = urljoin(test_async_client._api_url, "checks/test/flips/")
respx_mock.get(checks_url).mock(
return_value=Response(status_code=400)
)
with pytest.raises(BadAPIRequestError):
await test_async_client.get_check_flips("test")

View File

@@ -86,3 +86,63 @@ def test_async_client():
"""An AsyncClient for testing, set to a nonsense url so we aren't pinging healtchecks.""" """An AsyncClient for testing, set to a nonsense url so we aren't pinging healtchecks."""
yield AsyncClient(api_key="test", api_url="https://localhost/api") yield AsyncClient(api_key="test", api_url="https://localhost/api")
@pytest.fixture
def fake_check_pings_api_result():
return [
{
"type": "success",
"date": "2020-06-09T14:51:06.113073+00:00",
"n": 4,
"scheme": "http",
"remote_addr": "192.0.2.0",
"method": "GET",
"ua": "curl/7.68.0",
"duration": 2.896736
},
{
"type": "start",
"date": "2020-06-09T14:51:03.216337+00:00",
"n": 3,
"scheme": "http",
"remote_addr": "192.0.2.0",
"method": "GET",
"ua": "curl/7.68.0"
},
{
"type": "success",
"date": "2020-06-09T14:50:59.633577+00:00",
"n": 2,
"scheme": "http",
"remote_addr": "192.0.2.0",
"method": "GET",
"ua": "curl/7.68.0",
"duration": 2.997976
},
{
"type": "start",
"date": "2020-06-09T14:50:56.635601+00:00",
"n": 1,
"scheme": "http",
"remote_addr": "192.0.2.0",
"method": "GET",
"ua": "curl/7.68.0"
}
]
@pytest.fixture
def fake_check_flips_api_result():
return [
{
"timestamp": "2020-03-23T10:18:23+00:00",
"up": 1
},
{
"timestamp": "2020-03-23T10:17:15+00:00",
"up": 0
},
{
"timestamp": "2020-03-23T10:16:18+00:00",
"up": 1
}
]