forked from Wavyzz/py-healthchecks.io
add some client work
This commit is contained in:
@@ -32,7 +32,7 @@ repos:
|
|||||||
entry: flake8
|
entry: flake8
|
||||||
language: system
|
language: system
|
||||||
types: [python]
|
types: [python]
|
||||||
exclude: "tests/*"
|
exclude: "^(tests/*|noxfile.py)"
|
||||||
require_serial: true
|
require_serial: true
|
||||||
- id: pyupgrade
|
- id: pyupgrade
|
||||||
name: pyupgrade
|
name: pyupgrade
|
||||||
|
|||||||
12
noxfile.py
12
noxfile.py
@@ -26,6 +26,7 @@ python_versions = ["3.10", "3.9", "3.8", "3.7"]
|
|||||||
nox.needs_version = ">= 2021.6.6"
|
nox.needs_version = ">= 2021.6.6"
|
||||||
nox.options.sessions = (
|
nox.options.sessions = (
|
||||||
"pre-commit",
|
"pre-commit",
|
||||||
|
"bandit",
|
||||||
"safety",
|
"safety",
|
||||||
"mypy",
|
"mypy",
|
||||||
"tests",
|
"tests",
|
||||||
@@ -139,13 +140,18 @@ def safety(session: Session) -> None:
|
|||||||
@session(python=python_versions)
|
@session(python=python_versions)
|
||||||
def mypy(session: Session) -> None:
|
def mypy(session: Session) -> None:
|
||||||
"""Type-check using mypy."""
|
"""Type-check using mypy."""
|
||||||
args = session.posargs or ["src", "docs/conf.py"]
|
args = session.posargs or ["src"]
|
||||||
session.install(".")
|
session.install(".")
|
||||||
session.install("mypy", "pytest")
|
session.install("mypy", "pytest")
|
||||||
session.install(*mypy_type_packages)
|
session.install(*mypy_type_packages)
|
||||||
session.run("mypy", *args)
|
session.run("mypy", *args)
|
||||||
if not session.posargs:
|
|
||||||
session.run("mypy", f"--python-executable={sys.executable}", "noxfile.py")
|
|
||||||
|
@session(python=python_versions[0])
|
||||||
|
def bandit(session: Session) -> None:
|
||||||
|
"""Run bandit security tests"""
|
||||||
|
args = session.posargs or ["-r", "./src"]
|
||||||
|
session.run("bandit", *args)
|
||||||
|
|
||||||
|
|
||||||
@session(python=python_versions)
|
@session(python=python_versions)
|
||||||
|
|||||||
65
poetry.lock
generated
65
poetry.lock
generated
@@ -752,6 +752,28 @@ toml = "*"
|
|||||||
[package.extras]
|
[package.extras]
|
||||||
testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"]
|
testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pytest-async"
|
||||||
|
version = "0.1.1"
|
||||||
|
description = "pytest-async - Run your coroutine in event loop without decorator"
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.6"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pytest-asyncio"
|
||||||
|
version = "0.16.0"
|
||||||
|
description = "Pytest support for asyncio."
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">= 3.6"
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
pytest = ">=5.4.0"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
testing = ["coverage", "hypothesis (>=5.7.1)"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pytest-cov"
|
name = "pytest-cov"
|
||||||
version = "3.0.0"
|
version = "3.0.0"
|
||||||
@@ -767,6 +789,20 @@ pytest = ">=4.6"
|
|||||||
[package.extras]
|
[package.extras]
|
||||||
testing = ["fields", "hunter", "process-tests", "six", "pytest-xdist", "virtualenv"]
|
testing = ["fields", "hunter", "process-tests", "six", "pytest-xdist", "virtualenv"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pytest-mock"
|
||||||
|
version = "3.6.1"
|
||||||
|
description = "Thin-wrapper around the mock package for easier use with pytest"
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.6"
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
pytest = ">=5.0"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
dev = ["pre-commit", "tox", "pytest-asyncio"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "python-dateutil"
|
name = "python-dateutil"
|
||||||
version = "2.8.2"
|
version = "2.8.2"
|
||||||
@@ -842,6 +878,17 @@ urllib3 = ">=1.21.1,<1.27"
|
|||||||
socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"]
|
socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"]
|
||||||
use_chardet_on_py3 = ["chardet (>=3.0.2,<5)"]
|
use_chardet_on_py3 = ["chardet (>=3.0.2,<5)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "respx"
|
||||||
|
version = "0.19.0"
|
||||||
|
description = "A utility for mocking out the Python HTTPX and HTTP Core libraries."
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.6"
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
httpx = ">=0.21.0"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "restructuredtext-lint"
|
name = "restructuredtext-lint"
|
||||||
version = "1.3.2"
|
version = "1.3.2"
|
||||||
@@ -1231,7 +1278,7 @@ testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytes
|
|||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "1.1"
|
lock-version = "1.1"
|
||||||
python-versions = "^3.7"
|
python-versions = "^3.7"
|
||||||
content-hash = "062c8a09c8967fafd0410a702d9bdc17ef0a260965b65345c6058a3a134a05a3"
|
content-hash = "4609efb7758ecc2785e3e38e1b1bad140bd0311d253ffebd86b9e5d4e4d45766"
|
||||||
|
|
||||||
[metadata.files]
|
[metadata.files]
|
||||||
alabaster = [
|
alabaster = [
|
||||||
@@ -1637,10 +1684,22 @@ pytest = [
|
|||||||
{file = "pytest-6.2.5-py3-none-any.whl", hash = "sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134"},
|
{file = "pytest-6.2.5-py3-none-any.whl", hash = "sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134"},
|
||||||
{file = "pytest-6.2.5.tar.gz", hash = "sha256:131b36680866a76e6781d13f101efb86cf674ebb9762eb70d3082b6f29889e89"},
|
{file = "pytest-6.2.5.tar.gz", hash = "sha256:131b36680866a76e6781d13f101efb86cf674ebb9762eb70d3082b6f29889e89"},
|
||||||
]
|
]
|
||||||
|
pytest-async = [
|
||||||
|
{file = "pytest_async-0.1.1-py3-none-any.whl", hash = "sha256:11cc41eef82592951d56c2bb9b0e6ab21b2f0f00663e78d95694a80d965be930"},
|
||||||
|
{file = "pytest_async-0.1.1.tar.gz", hash = "sha256:0d6ffd3ebac2f3aa47d606dbae1984750268a89dc8caf4a908ba61c60299cdfd"},
|
||||||
|
]
|
||||||
|
pytest-asyncio = [
|
||||||
|
{file = "pytest-asyncio-0.16.0.tar.gz", hash = "sha256:7496c5977ce88c34379df64a66459fe395cd05543f0a2f837016e7144391fcfb"},
|
||||||
|
{file = "pytest_asyncio-0.16.0-py3-none-any.whl", hash = "sha256:5f2a21273c47b331ae6aa5b36087047b4899e40f03f18397c0e65fa5cca54e9b"},
|
||||||
|
]
|
||||||
pytest-cov = [
|
pytest-cov = [
|
||||||
{file = "pytest-cov-3.0.0.tar.gz", hash = "sha256:e7f0f5b1617d2210a2cabc266dfe2f4c75a8d32fb89eafb7ad9d06f6d076d470"},
|
{file = "pytest-cov-3.0.0.tar.gz", hash = "sha256:e7f0f5b1617d2210a2cabc266dfe2f4c75a8d32fb89eafb7ad9d06f6d076d470"},
|
||||||
{file = "pytest_cov-3.0.0-py3-none-any.whl", hash = "sha256:578d5d15ac4a25e5f961c938b85a05b09fdaae9deef3bb6de9a6e766622ca7a6"},
|
{file = "pytest_cov-3.0.0-py3-none-any.whl", hash = "sha256:578d5d15ac4a25e5f961c938b85a05b09fdaae9deef3bb6de9a6e766622ca7a6"},
|
||||||
]
|
]
|
||||||
|
pytest-mock = [
|
||||||
|
{file = "pytest-mock-3.6.1.tar.gz", hash = "sha256:40217a058c52a63f1042f0784f62009e976ba824c418cced42e88d5f40ab0e62"},
|
||||||
|
{file = "pytest_mock-3.6.1-py3-none-any.whl", hash = "sha256:30c2f2cc9759e76eee674b81ea28c9f0b94f8f0445a1b87762cadf774f0df7e3"},
|
||||||
|
]
|
||||||
python-dateutil = [
|
python-dateutil = [
|
||||||
{file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"},
|
{file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"},
|
||||||
{file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"},
|
{file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"},
|
||||||
@@ -1772,6 +1831,10 @@ requests = [
|
|||||||
{file = "requests-2.26.0-py2.py3-none-any.whl", hash = "sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24"},
|
{file = "requests-2.26.0-py2.py3-none-any.whl", hash = "sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24"},
|
||||||
{file = "requests-2.26.0.tar.gz", hash = "sha256:b8aa58f8cf793ffd8782d3d8cb19e66ef36f7aba4353eec859e74678b01b07a7"},
|
{file = "requests-2.26.0.tar.gz", hash = "sha256:b8aa58f8cf793ffd8782d3d8cb19e66ef36f7aba4353eec859e74678b01b07a7"},
|
||||||
]
|
]
|
||||||
|
respx = [
|
||||||
|
{file = "respx-0.19.0-py2.py3-none-any.whl", hash = "sha256:1ac1cc99bf892ffd3e33108ae43d71d8309a58ac226965f4bd81ec055600f265"},
|
||||||
|
{file = "respx-0.19.0.tar.gz", hash = "sha256:4a09e15803c7450d45303520ec528794c9fd77b05984263bc83b78aabbb39413"},
|
||||||
|
]
|
||||||
restructuredtext-lint = [
|
restructuredtext-lint = [
|
||||||
{file = "restructuredtext_lint-1.3.2.tar.gz", hash = "sha256:d3b10a1fe2ecac537e51ae6d151b223b78de9fafdd50e5eb6b08c243df173c80"},
|
{file = "restructuredtext_lint-1.3.2.tar.gz", hash = "sha256:d3b10a1fe2ecac537e51ae6d151b223b78de9fafdd50e5eb6b08c243df173c80"},
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -50,6 +50,10 @@ furo = ">=2021.11.12"
|
|||||||
pytest-cov = "^3.0.0"
|
pytest-cov = "^3.0.0"
|
||||||
types-croniter = "^1.0.3"
|
types-croniter = "^1.0.3"
|
||||||
types-pytz = "^2021.3.1"
|
types-pytz = "^2021.3.1"
|
||||||
|
pytest_async = "^0.1.1"
|
||||||
|
pytest-asyncio = "^0.16.0"
|
||||||
|
respx = "^0.19.0"
|
||||||
|
pytest-mock = "^3.6.1"
|
||||||
|
|
||||||
[tool.coverage.paths]
|
[tool.coverage.paths]
|
||||||
source = ["src", "*/site-packages"]
|
source = ["src", "*/site-packages"]
|
||||||
|
|||||||
@@ -1 +1,3 @@
|
|||||||
"""Py Healthchecks.Io."""
|
"""Py Healthchecks.Io."""
|
||||||
|
|
||||||
|
VERSION = "0.1"
|
||||||
|
|||||||
2
src/healthchecks_io/client/__init__.py
Normal file
2
src/healthchecks_io/client/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
"""healthchecks_io clients."""
|
||||||
|
from .asyncclient import AsyncClient # noqa: F401
|
||||||
146
src/healthchecks_io/client/_abstract.py
Normal file
146
src/healthchecks_io/client/_abstract.py
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
from abc import ABC
|
||||||
|
from abc import abstractmethod
|
||||||
|
from json import dumps
|
||||||
|
from typing import Dict
|
||||||
|
from typing import List
|
||||||
|
from typing import Optional
|
||||||
|
from urllib.parse import parse_qsl
|
||||||
|
from urllib.parse import ParseResult
|
||||||
|
from urllib.parse import unquote
|
||||||
|
from urllib.parse import urlencode
|
||||||
|
from urllib.parse import urljoin
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
from weakref import finalize
|
||||||
|
|
||||||
|
from httpx import Client
|
||||||
|
|
||||||
|
from healthchecks_io.schemas import checks
|
||||||
|
|
||||||
|
|
||||||
|
class AbstractClient(ABC):
|
||||||
|
"""An abstract client class that can be implemented by client classes."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
api_key: str,
|
||||||
|
api_url: Optional[str] = "https://healthchecks.io/api/",
|
||||||
|
api_version: Optional[int] = 1,
|
||||||
|
client: Optional[Client] = None,
|
||||||
|
) -> None:
|
||||||
|
"""An AbstractClient that other clients can implement.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
api_key (str): Healthchecks.io API key
|
||||||
|
api_url (Optional[str], optional): API URL. Defaults to "https://healthchecks.io/api/".
|
||||||
|
api_version (Optional[int], optional): Versiopn of the api to use. Defaults to 1.
|
||||||
|
client (Optional[Client], optional): A httpx.Client. If not
|
||||||
|
passed in, one will be created for this object. Defaults to None.
|
||||||
|
"""
|
||||||
|
self._api_key = api_key
|
||||||
|
self._client = client
|
||||||
|
if not api_url.endswith("/"):
|
||||||
|
api_url = f"{api_url}/"
|
||||||
|
self._api_url = urljoin(api_url, f"v{api_version}/")
|
||||||
|
self._finalizer = finalize(self, self._finalizer_method)
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def _finalizer_method(self):
|
||||||
|
"""Finalizer method is called by weakref.finalize when the object is dereferenced to do cleanup of clients."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_checks(self, tags: Optional[List[str]]) -> List[checks.Check]:
|
||||||
|
"""Calls the API's /checks/ endpoint to get a list of checks."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _get_api_request_url(
|
||||||
|
self, path: str, params: Optional[Dict[str, str]] = None
|
||||||
|
) -> str:
|
||||||
|
"""Get a full request url for the healthchecks api.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
path (str): Path to request from
|
||||||
|
params (Optional[Dict[str, str]], optional): URL Parameters. Defaults to None.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: url
|
||||||
|
"""
|
||||||
|
url = urljoin(self._api_url, path)
|
||||||
|
return self._add_url_params(url, params) if params is not None else url
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_closed(self) -> bool:
|
||||||
|
"""Is the client closed?
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: is the client closed
|
||||||
|
"""
|
||||||
|
return self._client.is_closed
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _add_url_params(url: str, params: Dict[str, str], replace: bool = True):
|
||||||
|
"""Add GET params to provided URL being aware of existing.
|
||||||
|
|
||||||
|
:param url: string of target URL
|
||||||
|
:param params: dict containing requested params to be added
|
||||||
|
:param replace: bool True If true, replace params if they exist with new values, otherwise append
|
||||||
|
:return: string with updated URL
|
||||||
|
|
||||||
|
>> url = 'http://stackoverflow.com/test?answers=true'
|
||||||
|
>> new_params = {'answers': False, 'data': ['some','values']}
|
||||||
|
>> add_url_params(url, new_params)
|
||||||
|
'http://stackoverflow.com/test?data=some&data=values&answers=false'
|
||||||
|
"""
|
||||||
|
# Unquoting URL first so we don't loose existing args
|
||||||
|
url = unquote(url)
|
||||||
|
# Extracting url info
|
||||||
|
parsed_url = urlparse(url)
|
||||||
|
# Extracting URL arguments from parsed URL
|
||||||
|
get_args = parsed_url.query
|
||||||
|
# Converting URL arguments to dict
|
||||||
|
parsed_get_args = dict(parse_qsl(get_args))
|
||||||
|
if replace:
|
||||||
|
# Merging URL arguments dict with new params
|
||||||
|
parsed_get_args.update(params)
|
||||||
|
extra_parameters = ""
|
||||||
|
else:
|
||||||
|
# get all the duplicated keys from params and urlencode them, we'll concat this to the params string later
|
||||||
|
duplicated_params = [x for x in params if x in parsed_get_args]
|
||||||
|
# get all the args that aren't duplicated and add them to parsed_get_args
|
||||||
|
parsed_get_args.update(
|
||||||
|
{
|
||||||
|
key: params[key]
|
||||||
|
for key in [x for x in params if x not in parsed_get_args]
|
||||||
|
}
|
||||||
|
)
|
||||||
|
# if we have any duplicated parameters, urlencode them, we append them later
|
||||||
|
extra_parameters = (
|
||||||
|
f"&{urlencode({key: params[key] for key in duplicated_params}, doseq=True)}"
|
||||||
|
if len(duplicated_params) > 0
|
||||||
|
else ""
|
||||||
|
)
|
||||||
|
|
||||||
|
# Bool and Dict values should be converted to json-friendly values
|
||||||
|
# you may throw this part away if you don't like it :)
|
||||||
|
parsed_get_args.update(
|
||||||
|
{
|
||||||
|
k: dumps(v)
|
||||||
|
for k, v in parsed_get_args.items()
|
||||||
|
if isinstance(v, (bool, dict))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Converting URL argument to proper query string
|
||||||
|
encoded_get_args = f"{urlencode(parsed_get_args, doseq=True)}{extra_parameters}"
|
||||||
|
# Creating new parsed result object based on provided with new
|
||||||
|
# URL arguments. Same thing happens inside of urlparse.
|
||||||
|
new_url = ParseResult(
|
||||||
|
parsed_url.scheme,
|
||||||
|
parsed_url.netloc,
|
||||||
|
parsed_url.path,
|
||||||
|
parsed_url.params,
|
||||||
|
encoded_get_args,
|
||||||
|
parsed_url.fragment,
|
||||||
|
).geturl()
|
||||||
|
|
||||||
|
return new_url
|
||||||
86
src/healthchecks_io/client/asyncclient.py
Normal file
86
src/healthchecks_io/client/asyncclient.py
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
"""An async healthchecks.io client."""
|
||||||
|
import asyncio
|
||||||
|
from typing import List
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from httpx import AsyncClient as HTTPXAsyncClient
|
||||||
|
|
||||||
|
from ._abstract import AbstractClient
|
||||||
|
from .exceptions import HCAPIAuthError
|
||||||
|
from .exceptions import HCAPIError
|
||||||
|
from healthchecks_io import VERSION
|
||||||
|
from healthchecks_io.schemas import checks
|
||||||
|
|
||||||
|
|
||||||
|
class AsyncClient(AbstractClient):
|
||||||
|
"""A Healthchecks.io client implemented using httpx's Async methods."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
api_key: str,
|
||||||
|
api_url: Optional[str] = "https://healthchecks.io/api/",
|
||||||
|
api_version: Optional[int] = 1,
|
||||||
|
client: Optional[HTTPXAsyncClient] = None,
|
||||||
|
) -> None:
|
||||||
|
"""An AsyncClient can be used in code using asyncio to work with the Healthchecks.io api.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
api_key (str): Healthchecks.io API key
|
||||||
|
api_url (Optional[str], optional): API URL. Defaults to "https://healthchecks.io/api/".
|
||||||
|
api_version (Optional[int], optional): Versiopn of the api to use. Defaults to 1.
|
||||||
|
client (Optional[HTTPXAsyncClient], optional): A httpx.Asyncclient. If not
|
||||||
|
passed in, one will be created for this object. Defaults to None.
|
||||||
|
"""
|
||||||
|
if client is None:
|
||||||
|
client = HTTPXAsyncClient()
|
||||||
|
super().__init__(
|
||||||
|
api_key=api_key, api_url=api_url, api_version=api_version, client=client
|
||||||
|
)
|
||||||
|
self._client.headers["X-Api-Key"] = self._api_key
|
||||||
|
self._client.headers["user-agent"] = f"py-healthchecks.io/{VERSION}"
|
||||||
|
self._client.headers["Content-type"] = "application/json"
|
||||||
|
|
||||||
|
def _finalizer_method(self):
|
||||||
|
"""Calls _afinalizer_method from a sync context to work with weakref.finalizer."""
|
||||||
|
asyncio.run(self._afinalizer_method())
|
||||||
|
|
||||||
|
async def _afinalizer_method(self):
|
||||||
|
"""Finalizer coroutine that closes our client connections."""
|
||||||
|
await self._client.aclose()
|
||||||
|
|
||||||
|
async def get_checks(self, tags: Optional[List[str]] = None) -> List[checks.Check]:
|
||||||
|
"""Get a list of checks from the healthchecks api.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
tags (Optional[List[str]], optional): Filters the checks and returns only
|
||||||
|
the checks that are tagged with the specified value. Defaults to None.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
HCAPIAuthError: When the API returns a 401, indicates an api key issue
|
||||||
|
HCAPIError: When the API returns anything other than a 200 or 401
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List[checks.Check]: [description]
|
||||||
|
"""
|
||||||
|
request_url = self._get_api_request_url("checks/")
|
||||||
|
if tags is not None:
|
||||||
|
for tag in tags:
|
||||||
|
request_url = self._add_url_params(
|
||||||
|
request_url, {"tag": tag}, replace=False
|
||||||
|
)
|
||||||
|
|
||||||
|
response = await self._client.get(request_url)
|
||||||
|
|
||||||
|
if response.status_code == 401:
|
||||||
|
raise HCAPIAuthError("Auth failure when getting checks")
|
||||||
|
|
||||||
|
if response.status_code != 200:
|
||||||
|
raise HCAPIError(
|
||||||
|
f"Error when reaching out to HC API at {request_url}. "
|
||||||
|
f"Status Code {response.status_code}. Response {response.text}"
|
||||||
|
)
|
||||||
|
|
||||||
|
return [
|
||||||
|
checks.Check.from_api_result(check_data)
|
||||||
|
for check_data in response.json()["checks"]
|
||||||
|
]
|
||||||
13
src/healthchecks_io/client/exceptions.py
Normal file
13
src/healthchecks_io/client/exceptions.py
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
"""healthchecks_io exceptions."""
|
||||||
|
|
||||||
|
|
||||||
|
class HCAPIError(Exception):
|
||||||
|
"""API Exception for when we have an error with the healthchecks api."""
|
||||||
|
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
class HCAPIAuthError(HCAPIError):
|
||||||
|
"""Thrown when we fail to auth to the Healthchecks api."""
|
||||||
|
|
||||||
|
...
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
"""Schemas for healthchecks_io."""
|
||||||
|
|||||||
76
tests/client/test_async.py
Normal file
76
tests/client/test_async.py
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
from urllib.parse import urljoin
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
import respx
|
||||||
|
from httpx import AsyncClient as HTTPXAsyncClient
|
||||||
|
from httpx import Response
|
||||||
|
|
||||||
|
from healthchecks_io.client import AsyncClient
|
||||||
|
from healthchecks_io.client.exceptions import HCAPIAuthError
|
||||||
|
from healthchecks_io.client.exceptions import HCAPIError
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
@pytest.mark.respx
|
||||||
|
async def test_get_checks_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/")
|
||||||
|
respx_mock.get(checks_url).mock(
|
||||||
|
return_value=Response(status_code=200, json={"checks": [fake_check_api_result]})
|
||||||
|
)
|
||||||
|
checks = await test_async_client.get_checks()
|
||||||
|
assert len(checks) == 1
|
||||||
|
assert checks[0].name == fake_check_api_result["name"]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
@pytest.mark.respx
|
||||||
|
async def test_get_checks_pass_in_client(fake_check_api_result, respx_mock):
|
||||||
|
httpx_client = HTTPXAsyncClient()
|
||||||
|
test_async_client = AsyncClient(
|
||||||
|
api_key="test", api_url="http://localhost/api/", client=httpx_client
|
||||||
|
)
|
||||||
|
checks_url = urljoin(test_async_client._api_url, "checks/")
|
||||||
|
respx_mock.get(checks_url).mock(
|
||||||
|
return_value=Response(status_code=200, json={"checks": [fake_check_api_result]})
|
||||||
|
)
|
||||||
|
checks = await test_async_client.get_checks()
|
||||||
|
assert len(checks) == 1
|
||||||
|
assert checks[0].name == fake_check_api_result["name"]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
@pytest.mark.respx
|
||||||
|
async def test_get_checks_exceptions(
|
||||||
|
fake_check_api_result, respx_mock, test_async_client
|
||||||
|
):
|
||||||
|
checks_url = urljoin(test_async_client._api_url, "checks/")
|
||||||
|
# test exceptions
|
||||||
|
respx_mock.get(checks_url).mock(return_value=Response(status_code=401))
|
||||||
|
with pytest.raises(HCAPIAuthError):
|
||||||
|
await test_async_client.get_checks()
|
||||||
|
|
||||||
|
respx_mock.get(checks_url).mock(return_value=Response(status_code=500))
|
||||||
|
with pytest.raises(HCAPIError):
|
||||||
|
await test_async_client.get_checks()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
@pytest.mark.respx
|
||||||
|
async def test_get_checks_tags(fake_check_api_result, respx_mock, test_async_client):
|
||||||
|
"""Test get_checks with tags"""
|
||||||
|
checks_url = urljoin(test_async_client._api_url, "checks/")
|
||||||
|
respx_mock.get(f"{checks_url}?tag=test&tag=test2").mock(
|
||||||
|
return_value=Response(status_code=200, json={"checks": [fake_check_api_result]})
|
||||||
|
)
|
||||||
|
checks = await test_async_client.get_checks(tags=["test", "test2"])
|
||||||
|
assert len(checks) == 1
|
||||||
|
assert checks[0].name == fake_check_api_result["name"]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
def test_finalizer_closes(test_async_client):
|
||||||
|
"""Tests our finalizer works to close the method"""
|
||||||
|
assert not test_async_client.is_closed
|
||||||
|
test_async_client._finalizer_method()
|
||||||
|
assert test_async_client.is_closed
|
||||||
@@ -4,6 +4,7 @@ from typing import Union
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
from healthchecks_io.client import AsyncClient
|
||||||
from healthchecks_io.schemas import checks
|
from healthchecks_io.schemas import checks
|
||||||
|
|
||||||
|
|
||||||
@@ -78,3 +79,10 @@ def fake_ro_check(fake_check: checks.Check):
|
|||||||
fake_check.update_url = None
|
fake_check.update_url = None
|
||||||
fake_check.pause_url = None
|
fake_check.pause_url = None
|
||||||
yield fake_check
|
yield fake_check
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def test_async_client():
|
||||||
|
"""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")
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
from healthchecks_io.schemas.badges import Badges
|
from healthchecks_io.schemas.badges import Badges
|
||||||
|
|
||||||
|
|
||||||
def test_badge_from_api_result():
|
def test_badge_from_api_result():
|
||||||
badges_dict = {
|
badges_dict = {
|
||||||
"svg": "https://healthchecks.io/badge/67541b37-8b9c-4d17-b952-690eae/LOegDs5M-2/backup.svg",
|
"svg": "https://healthchecks.io/badge/67541b37-8b9c-4d17-b952-690eae/LOegDs5M-2/backup.svg",
|
||||||
@@ -7,8 +8,8 @@ def test_badge_from_api_result():
|
|||||||
"json": "https://healthchecks.io/badge/67541b37-8b9c-4d17-b952-690eae/LOegDs5M-2/backup.json",
|
"json": "https://healthchecks.io/badge/67541b37-8b9c-4d17-b952-690eae/LOegDs5M-2/backup.json",
|
||||||
"json3": "https://healthchecks.io/badge/67541b37-8b9c-4d17-b952-690eae/LOegDs5M/backup.json",
|
"json3": "https://healthchecks.io/badge/67541b37-8b9c-4d17-b952-690eae/LOegDs5M/backup.json",
|
||||||
"shields": "https://healthchecks.io/badge/67541b37-8b9c-4d17-b952-690eae/LOegDs5M-2/backup.shields",
|
"shields": "https://healthchecks.io/badge/67541b37-8b9c-4d17-b952-690eae/LOegDs5M-2/backup.shields",
|
||||||
"shields3": "https://healthchecks.io/badge/67541b37-8b9c-4d17-b952-690eae/LOegDs5M/backup.shields"
|
"shields3": "https://healthchecks.io/badge/67541b37-8b9c-4d17-b952-690eae/LOegDs5M/backup.shields",
|
||||||
}
|
}
|
||||||
this_badge = Badges.from_api_result(badges_dict)
|
this_badge = Badges.from_api_result(badges_dict)
|
||||||
assert this_badge.svg == badges_dict['svg']
|
assert this_badge.svg == badges_dict["svg"]
|
||||||
assert this_badge.json_url == badges_dict['json']
|
assert this_badge.json_url == badges_dict["json"]
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
from healthchecks_io.schemas.integrations import Integration
|
from healthchecks_io.schemas.integrations import Integration
|
||||||
|
|
||||||
|
|
||||||
def test_badge_from_api_result():
|
def test_badge_from_api_result():
|
||||||
int_dict = {
|
int_dict = {
|
||||||
"id": "4ec5a071-2d08-4baa-898a-eb4eb3cd6941",
|
"id": "4ec5a071-2d08-4baa-898a-eb4eb3cd6941",
|
||||||
"name": "My Work Email",
|
"name": "My Work Email",
|
||||||
"kind": "email"
|
"kind": "email",
|
||||||
}
|
}
|
||||||
this_integration = Integration.from_api_result(int_dict)
|
this_integration = Integration.from_api_result(int_dict)
|
||||||
assert this_integration.id == int_dict['id']
|
assert this_integration.id == int_dict["id"]
|
||||||
assert this_integration.name == int_dict['name']
|
assert this_integration.name == int_dict["name"]
|
||||||
|
|||||||
Reference in New Issue
Block a user