creates and updates and a start on docs

This commit is contained in:
Andrew Herrington
2021-12-15 18:46:35 -06:00
parent fb50209a5f
commit d73ff88d60
10 changed files with 350 additions and 35 deletions

View File

@@ -1,3 +0,0 @@
# py-healthchecks.io
A python client for healthchecks.io. Supports the management api and ping api

View File

@@ -35,18 +35,22 @@ Py Healthchecks.Io
:target: https://github.com/psf/black
:alt: Black
A python client for healthchecks.io. Supports the management api and ping api
A python client for healthchecks.io. Supports the management api and ping api.
Features
--------
* TODO
* Sync and Async clients based on HTTPX
* Supports the management api and the ping api
* Supports Healthchecks.io SAAS and self-hosted instances
Requirements
------------
* TODO
* httpx
* pytz
* pydantic
Installation
@@ -62,7 +66,7 @@ You can install *Py Healthchecks.Io* via pip_ from PyPI_:
Usage
-----
Please see the `Command-line Reference <Usage_>`_ for details.
Please see the `Usage <Usage_>`_ for details.
Contributing
@@ -100,4 +104,4 @@ This project was generated from `@cjolowicz`_'s `Hypermodern Python Cookiecutter
.. _pip: https://pip.pypa.io/
.. github-only
.. _Contributor Guide: CONTRIBUTING.rst
.. _Usage: https://py-healthchecks.io.readthedocs.io/en/latest/usage.html
.. _Usage: https://py-healthchecksio.readthedocs.io/en/latest/usage.html

View File

@@ -1,2 +1,4 @@
Usage
=====
After installation, you can import and instantiate a client and then use the

View File

@@ -9,6 +9,8 @@ from .client.exceptions import HCAPIAuthError # noqa: F401, E402
from .client.exceptions import HCAPIError # noqa: F401, E402
from .client.exceptions import HCAPIRateLimitError # noqa: F401, E402
from .client.exceptions import NonUniqueSlugError # noqa: F401, E402
from .schemas import Check, CheckCreate, CheckPings, CheckStatuses # noqa: F401, E402
from .schemas import Integration, Badges, CheckUpdate # noqa: F401, E402
__all__ = [
"AsyncClient",
@@ -20,4 +22,11 @@ __all__ = [
"CheckNotFoundError",
"HCAPIRateLimitError",
"NonUniqueSlugError",
"Check",
"CheckCreate",
"CheckUpdate",
"CheckPings",
"CheckStatuses",
"Integration",
"Badges",
]

View File

@@ -9,9 +9,12 @@ from httpx import AsyncClient as HTTPXAsyncClient
from ._abstract import AbstractClient
from healthchecks_io import VERSION
from healthchecks_io.schemas import badges
from healthchecks_io.schemas import checks
from healthchecks_io.schemas import integrations
from healthchecks_io.schemas import Badges
from healthchecks_io.schemas import Check
from healthchecks_io.schemas import CheckCreate
from healthchecks_io.schemas import CheckPings
from healthchecks_io.schemas import CheckStatuses
from healthchecks_io.schemas import Integration
class AsyncClient(AbstractClient):
@@ -59,7 +62,47 @@ class AsyncClient(AbstractClient):
"""Finalizer coroutine that closes our client connections."""
await self._client.aclose()
async def get_checks(self, tags: Optional[List[str]] = None) -> List[checks.Check]:
async def create_check(self, new_check: CheckCreate) -> Check:
"""Creates a new check and returns it.
With this API call, you can create both Simple and Cron checks:
* To create a Simple check, specify the timeout parameter.
* To create a Cron check, specify the schedule and tz parameters.
Args:
new_check (CheckCreate): New check you are wanting to create
Returns:
Check: check that was just created
"""
request_url = self._get_api_request_url("checks/")
response = self.check_response(
await self._client.post(request_url, json=new_check.dict())
)
return Check.from_api_result(response.json())
async def update_check(self, uuid: str, update_check: CheckCreate) -> Check:
"""Updates an existing check.
If you omit any parameter in update_check, Healthchecks.io will leave
its value unchanged.
Args:
uuid (str): UUID for the check to update
update_check (CheckCreate): Check values you want to update
Returns:
Check: check that was just updated
"""
request_url = self._get_api_request_url(f"checks/{uuid}")
response = self.check_response(
await self._client.post(
request_url, json=update_check.dict(exclude_unset=True)
)
)
return Check.from_api_result(response.json())
async def get_checks(self, tags: Optional[List[str]] = None) -> List[Check]:
"""Get a list of checks from the healthchecks api.
Args:
@@ -73,7 +116,7 @@ class AsyncClient(AbstractClient):
Returns:
List[checks.Check]: [description]
List[Check]: [description]
"""
request_url = self._get_api_request_url("checks/")
if tags is not None:
@@ -85,11 +128,11 @@ class AsyncClient(AbstractClient):
response = self.check_response(await self._client.get(request_url))
return [
checks.Check.from_api_result(check_data)
Check.from_api_result(check_data)
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) -> Check:
"""Get a single check by id.
check_id can either be a check uuid if using a read/write api key
@@ -99,7 +142,7 @@ class AsyncClient(AbstractClient):
check_id (str): check's uuid or unique id
Returns:
checks.Check: the check
Check: the check
Raises:
HCAPIAuthError: Raised when status_code == 401 or 403
@@ -111,9 +154,9 @@ class AsyncClient(AbstractClient):
"""
request_url = self._get_api_request_url(f"checks/{check_id}")
response = self.check_response(await self._client.get(request_url))
return checks.Check.from_api_result(response.json())
return Check.from_api_result(response.json())
async def pause_check(self, check_id: str) -> checks.Check:
async def pause_check(self, check_id: str) -> Check:
"""Disables monitoring for a check without removing it.
The check goes into a "paused" state.
@@ -125,7 +168,7 @@ class AsyncClient(AbstractClient):
check_id (str): check's uuid
Returns:
checks.Check: the check just paused
Check: the check just paused
Raises:
HCAPIAuthError: Raised when status_code == 401 or 403
@@ -135,9 +178,9 @@ class AsyncClient(AbstractClient):
"""
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())
return Check.from_api_result(response.json())
async def delete_check(self, check_id: str) -> checks.Check:
async def delete_check(self, check_id: str) -> Check:
"""Permanently deletes the check from the user's account.
check_id must be a uuid, not a unique id
@@ -146,7 +189,7 @@ class AsyncClient(AbstractClient):
check_id (str): check's uuid
Returns:
checks.Check: the check just deleted
Check: the check just deleted
Raises:
HCAPIAuthError: Raised when status_code == 401 or 403
@@ -157,9 +200,9 @@ class AsyncClient(AbstractClient):
"""
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())
return Check.from_api_result(response.json())
async def get_check_pings(self, check_id: str) -> List[checks.CheckPings]:
async def get_check_pings(self, check_id: str) -> List[CheckPings]:
"""Returns a list of pings this check has received.
This endpoint returns pings in reverse order (most recent first),
@@ -170,7 +213,7 @@ class AsyncClient(AbstractClient):
check_id (str): check's uuid
Returns:
List[checks.CheckPings]: list of pings this check has received
List[CheckPings]: list of pings this check has received
Raises:
HCAPIAuthError: Raised when status_code == 401 or 403
@@ -183,7 +226,7 @@ class AsyncClient(AbstractClient):
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)
CheckPings.from_api_result(check_data)
for check_data in response.json()["pings"]
]
@@ -193,7 +236,7 @@ class AsyncClient(AbstractClient):
seconds: Optional[int] = None,
start: Optional[int] = None,
end: Optional[int] = None,
) -> List[checks.CheckStatuses]:
) -> List[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").
@@ -213,7 +256,7 @@ class AsyncClient(AbstractClient):
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
List[CheckStatuses]: List of status flips for this check
"""
params = dict()
@@ -226,9 +269,9 @@ class AsyncClient(AbstractClient):
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()]
return [CheckStatuses(**status_data) for status_data in response.json()]
async def get_integrations(self) -> List[Optional[integrations.Integration]]:
async def get_integrations(self) -> List[Optional[Integration]]:
"""Returns a list of integrations belonging to the project.
Raises:
@@ -237,17 +280,17 @@ class AsyncClient(AbstractClient):
HCAPIRateLimitError: Raised when status code is 429
Returns:
List[Optional[integrations.Integration]]: List of integrations for the project
List[Optional[Integration]]: List of integrations for the project
"""
request_url = self._get_api_request_url("channels/")
response = self.check_response(await self._client.get(request_url))
return [
integrations.Integration.from_api_result(integration_dict)
Integration.from_api_result(integration_dict)
for integration_dict in response.json()["channels"]
]
async def get_badges(self) -> Dict[str, badges.Badges]:
async def get_badges(self) -> Dict[str, Badges]:
"""Returns a dict of all tags in the project, with badge URLs for each tag.
Healthchecks.io provides badges in a few different formats:
@@ -268,12 +311,12 @@ class AsyncClient(AbstractClient):
HCAPIRateLimitError: Raised when status code is 429
Returns:
Dict[str, badges.Badges]: Dictionary of all tags in the project with badges
Dict[str, Badges]: Dictionary of all tags in the project with badges
"""
request_url = self._get_api_request_url("badges/")
response = self.check_response(await self._client.get(request_url))
return {
key: badges.Badges.from_api_result(item)
key: Badges.from_api_result(item)
for key, item in response.json()["badges"].items()
}

View File

@@ -9,6 +9,8 @@ from httpx import Client as HTTPXClient
from ._abstract import AbstractClient
from healthchecks_io import VERSION
from healthchecks_io.schemas import badges
from healthchecks_io.schemas import Check
from healthchecks_io.schemas import CheckCreate
from healthchecks_io.schemas import checks
from healthchecks_io.schemas import integrations
@@ -102,6 +104,50 @@ class Client(AbstractClient):
response = self.check_response(self._client.get(request_url))
return checks.Check.from_api_result(response.json())
def create_check(self, new_check: CheckCreate) -> Check:
"""Creates a new check and returns it.
With this API call, you can create both Simple and Cron checks:
* To create a Simple check, specify the timeout parameter.
* To create a Cron check, specify the schedule and tz parameters.
Args:
new_check (CheckCreate): New check you are wanting to create
Returns:
Check: check that was just created
"""
request_url = self._get_api_request_url("checks/")
response = self.check_response(
self._client.post(request_url, json=new_check.dict())
)
return Check.from_api_result(response.json())
def update_check(self, uuid: str, update_check: CheckCreate) -> Check:
"""Updates an existing check.
If you omit any parameter in update_check, Healthchecks.io will leave
its value unchanged.
With this API call, you can create both Simple and Cron checks:
* To create a Simple check, specify the timeout parameter.
* To create a Cron check, specify the schedule and tz parameters.
Args:
uuid (str): UUID for the check to update
update_check (CheckCreate): Check values you want to update
Returns:
Check: check that was just updated
"""
request_url = self._get_api_request_url(f"checks/{uuid}")
response = self.check_response(
self._client.post(request_url, json=update_check.dict(exclude_unset=True))
)
return Check.from_api_result(response.json())
def pause_check(self, check_id: str) -> checks.Check:
"""Disables monitoring for a check without removing it.

View File

@@ -1 +1,18 @@
"""Schemas for healthchecks_io."""
from .badges import Badges
from .checks import Check
from .checks import CheckCreate
from .checks import CheckPings
from .checks import CheckStatuses
from .checks import CheckUpdate
from .integrations import Integration
__all__ = [
"Check",
"CheckCreate",
"CheckPings",
"CheckUpdate",
"CheckStatuses",
"Badges",
"Integration",
]

View File

@@ -163,6 +163,73 @@ class CheckCreate(BaseModel):
return value
class CheckUpdate(CheckCreate):
"""Pydantic object for updating a check."""
name: Optional[str] = Field(None, description="Name of the check")
tags: Optional[str] = Field(
None, description="String separated list of tags to apply"
)
timeout: Optional[int] = Field(
None,
description="The expected period of this check in seconds.",
gte=60,
lte=31536000,
)
grace: Optional[int] = Field(
None,
description="The grace period for this check in seconds.",
gte=60,
lte=31536000,
)
schedule: Optional[str] = Field(
None,
description="A cron expression defining this check's schedule. "
"If you specify both timeout and schedule parameters, "
"Healthchecks.io will create a Cron check and ignore the "
"timeout value.",
)
tz: Optional[str] = Field(
None,
description="Server's timezone. This setting only has an effect "
"in combination with the schedule parameter.",
)
manual_resume: Optional[bool] = Field(
None,
description="Controls whether a paused check automatically resumes "
"when pinged (the default) or not. If set to false, a paused "
"check will leave the paused state when it receives a ping. "
"If set to true, a paused check will ignore pings and stay "
"paused until you manually resume it from the web dashboard.",
)
methods: Optional[str] = Field(
None,
description="Specifies the allowed HTTP methods for making "
"ping requests. Must be one of the two values: an empty "
"string or POST. Set this field to an empty string to "
"allow HEAD, GET, and POST requests. Set this field to "
"POST to allow only POST requests.",
)
channels: Optional[str] = Field(
None,
description="By default, this API call assigns no integrations"
"to the newly created check. By default, this API call "
"assigns no integrations to the newly created check. "
"To assign specific integrations, use a comma-separated list "
"of integration UUIDs.",
)
unique: Optional[List[Optional[str]]] = Field(
None,
description="Enables upsert functionality. Before creating a check, "
"Healthchecks.io looks for existing checks, filtered by fields listed "
"in unique. If Healthchecks.io does not find a matching check, it "
"creates a new check and returns it with the HTTP status code 201 "
"If Healthchecks.io finds a matching check, it updates the existing "
"check and returns it with HTTP status code 200. The accepted values "
"for the unique field are name, tags, timeout, and grace.",
)
class CheckPings(BaseModel):
"""A Pydantic schema for a check's Pings."""

View File

@@ -5,6 +5,8 @@ import respx
from httpx import AsyncClient as HTTPXAsyncClient
from httpx import Response
from healthchecks_io import CheckCreate
from healthchecks_io import CheckUpdate
from healthchecks_io.client import AsyncClient
from healthchecks_io.client.exceptions import BadAPIRequestError
from healthchecks_io.client.exceptions import CheckNotFoundError
@@ -12,6 +14,72 @@ from healthchecks_io.client.exceptions import HCAPIAuthError
from healthchecks_io.client.exceptions import HCAPIError
@pytest.mark.asyncio
@pytest.mark.respx
async def test_acreate_check_200(fake_check_api_result, respx_mock, test_async_client):
checks_url = urljoin(test_async_client._api_url, "checks/")
respx_mock.post(checks_url).mock(
return_value=Response(
status_code=200,
json={
"channels": "",
"desc": "",
"grace": 60,
"last_ping": None,
"n_pings": 0,
"name": "Backups",
"slug": "backups",
"next_ping": None,
"manual_resume": False,
"methods": "",
"pause_url": "https://healthchecks.io/api/v1/checks/f618072a-7bde-4eee-af63-71a77c5723bc/pause",
"ping_url": "https://hc-ping.com/f618072a-7bde-4eee-af63-71a77c5723bc",
"status": "new",
"tags": "prod www",
"timeout": 3600,
"update_url": "https://healthchecks.io/api/v1/checks/f618072a-7bde-4eee-af63-71a77c5723bc",
},
)
)
check = await test_async_client.create_check(
CheckCreate(name="test", tags="test", desc="test")
)
assert check.name == "Backups"
@pytest.mark.asyncio
@pytest.mark.respx
async def test_aupdate_check_200(fake_check_api_result, respx_mock, test_async_client):
checks_url = urljoin(test_async_client._api_url, "checks/test")
respx_mock.post(checks_url).mock(
return_value=Response(
status_code=200,
json={
"channels": "",
"desc": "",
"grace": 60,
"last_ping": None,
"n_pings": 0,
"name": "Backups",
"slug": "backups",
"next_ping": None,
"manual_resume": False,
"methods": "",
"pause_url": "https://healthchecks.io/api/v1/checks/f618072a-7bde-4eee-af63-71a77c5723bc/pause",
"ping_url": "https://hc-ping.com/f618072a-7bde-4eee-af63-71a77c5723bc",
"status": "new",
"tags": "prod www",
"timeout": 3600,
"update_url": "https://healthchecks.io/api/v1/checks/f618072a-7bde-4eee-af63-71a77c5723bc",
},
)
)
check = await test_async_client.update_check(
"test", CheckUpdate(name="test", desc="test")
)
assert check.name == "Backups"
@pytest.mark.asyncio
@pytest.mark.respx
async def test_aget_checks_200(fake_check_api_result, respx_mock, test_async_client):

View File

@@ -5,6 +5,8 @@ import respx
from httpx import Client as HTTPXClient
from httpx import Response
from healthchecks_io import CheckCreate
from healthchecks_io import CheckUpdate
from healthchecks_io import Client
from healthchecks_io.client.exceptions import BadAPIRequestError
from healthchecks_io.client.exceptions import CheckNotFoundError
@@ -12,6 +14,66 @@ from healthchecks_io.client.exceptions import HCAPIAuthError
from healthchecks_io.client.exceptions import HCAPIError
@pytest.mark.respx
def test_create_check_200(fake_check_api_result, respx_mock, test_client):
checks_url = urljoin(test_client._api_url, "checks/")
respx_mock.post(checks_url).mock(
return_value=Response(
status_code=200,
json={
"channels": "",
"desc": "",
"grace": 60,
"last_ping": None,
"n_pings": 0,
"name": "Backups",
"slug": "backups",
"next_ping": None,
"manual_resume": False,
"methods": "",
"pause_url": "https://healthchecks.io/api/v1/checks/f618072a-7bde-4eee-af63-71a77c5723bc/pause",
"ping_url": "https://hc-ping.com/f618072a-7bde-4eee-af63-71a77c5723bc",
"status": "new",
"tags": "prod www",
"timeout": 3600,
"update_url": "https://healthchecks.io/api/v1/checks/f618072a-7bde-4eee-af63-71a77c5723bc",
},
)
)
check = test_client.create_check(CheckCreate(name="test", tags="test", desc="test"))
assert check.name == "Backups"
@pytest.mark.respx
def test_update_check_200(fake_check_api_result, respx_mock, test_client):
checks_url = urljoin(test_client._api_url, "checks/test")
respx_mock.post(checks_url).mock(
return_value=Response(
status_code=200,
json={
"channels": "",
"desc": "",
"grace": 60,
"last_ping": None,
"n_pings": 0,
"name": "Backups",
"slug": "backups",
"next_ping": None,
"manual_resume": False,
"methods": "",
"pause_url": "https://healthchecks.io/api/v1/checks/f618072a-7bde-4eee-af63-71a77c5723bc/pause",
"ping_url": "https://hc-ping.com/f618072a-7bde-4eee-af63-71a77c5723bc",
"status": "new",
"tags": "prod www",
"timeout": 3600,
"update_url": "https://healthchecks.io/api/v1/checks/f618072a-7bde-4eee-af63-71a77c5723bc",
},
)
)
check = test_client.update_check("test", CheckUpdate(name="test", desc="test"))
assert check.name == "Backups"
@pytest.mark.respx
def test_get_checks_200(fake_check_api_result, respx_mock, test_client):
assert test_client._client is not None