From 0606e83f19af51a831725267874d04d3aebb2b8a Mon Sep 17 00:00:00 2001 From: Andrew Herrington Date: Thu, 30 Dec 2021 23:15:08 -0600 Subject: [PATCH] add context manager features --- docs/usage.rst | 44 ++++++ src/healthchecks_io/__init__.py | 6 + src/healthchecks_io/client/__init__.py | 3 +- src/healthchecks_io/client/async_client.py | 34 ++++- src/healthchecks_io/client/check_trap.py | 165 +++++++++++++++++++++ src/healthchecks_io/client/exceptions.py | 12 ++ src/healthchecks_io/client/sync_client.py | 26 +++- tests/client/test_async.py | 4 +- tests/client/test_check_trap.py | 97 ++++++++++++ tests/client/test_sync.py | 2 +- 10 files changed, 373 insertions(+), 20 deletions(-) create mode 100644 src/healthchecks_io/client/check_trap.py create mode 100644 tests/client/test_check_trap.py diff --git a/docs/usage.rst b/docs/usage.rst index ad2330b..c633720 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -19,6 +19,14 @@ Either the Client or AsyncClient can be used as a ContextManager (or Async Conte This is probably the easiest way to use the Clients for one-off scripts. If you do not need to keep a client open for multiple requests, just use the context manager. +.. note:: + When using either of the client types as a context manager, the httpx client underlying the client will be closed when the context manager exits. + + Since we allow you to pass in a client on creation, its possible to use a shared client with this library. If you then use the client as a contextmanager, + it will close that shared client. + + Just a thing to be aware of! + Sync ---- @@ -95,3 +103,39 @@ If you want to use the client in an async program, use AsyncClient instead of Cl check = await client.create_check(CreateCheck(name="New Check", tags="tag1 tag2") print(check) + + +CheckTrap +--------- + +Ever wanted to run some code and wrape it in a healthcheck check without thinking about it? + +That's what CheckTrap is for. + +.. code-block:: python + + from healthchecks_io import Client, AsyncClient, CheckCreate, CheckTrap + + client = Client(api_key="myapikey") + + # create a new check, or use an existing one already with just its uuid. + check = await client.create_check(CreateCheck(name="New Check", tags="tag1 tag2") + + with CheckTrap(client, check.uuid): + # when entering the context manager, sends a start ping to your check + run_my_thing_to_monitor() + + # If your method exits without an exception, sends a success ping + # If there's an exception, a failure ping will be sent with the exception and traceback + + client = AsyncClient(ping_key="ping_key") + + # works with async too, and the ping api and slugs + with CheckTrap(client, check.slug) as ct: + # when entering the context manager, sends a start ping to your check + # Add custom logs to what gets sent to healthchecks. Reminder, only the first 10k bytes get saved + ct.add_log("My custom log message") + run_my_thing_to_monitor() + + # If your method exits without an exception, sends a success ping + # If there's an exception, a failure ping will be sent with the exception and traceback diff --git a/src/healthchecks_io/__init__.py b/src/healthchecks_io/__init__.py index 9f7a410..dc97a36 100644 --- a/src/healthchecks_io/__init__.py +++ b/src/healthchecks_io/__init__.py @@ -4,18 +4,22 @@ __version__ = "0.0.0" # noqa: E402 from .client import AsyncClient # noqa: F401, E402 from .client import Client # noqa: F401, E402 +from .client import CheckTrap # noqa: F401, E402 from .client.exceptions import BadAPIRequestError # noqa: F401, E402 from .client.exceptions import CheckNotFoundError # noqa: F401, E402 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 .client.exceptions import WrongClientError # noqa: F401, E402 +from .client.exceptions import PingFailedError # noqa: F401, E402 from .schemas import Check, CheckCreate, CheckPings, CheckStatuses # noqa: F401, E402 from .schemas import Integration, Badges, CheckUpdate # noqa: F401, E402 __all__ = [ "AsyncClient", "Client", + "CheckTrap", "BadAPIRequestError", "CheckNotFoundError", "HCAPIAuthError", @@ -23,6 +27,8 @@ __all__ = [ "CheckNotFoundError", "HCAPIRateLimitError", "NonUniqueSlugError", + "WrongClientError", + "PingFailedError", "Check", "CheckCreate", "CheckUpdate", diff --git a/src/healthchecks_io/client/__init__.py b/src/healthchecks_io/client/__init__.py index 12a4e6b..22512f8 100644 --- a/src/healthchecks_io/client/__init__.py +++ b/src/healthchecks_io/client/__init__.py @@ -1,5 +1,6 @@ """healthchecks_io clients.""" from .async_client import AsyncClient # noqa: F401 +from .check_trap import CheckTrap # noqa: F401 from .sync_client import Client # noqa: F401 -__all__ = ["AsyncClient", "Client"] +__all__ = ["AsyncClient", "Client", "CheckTrap"] diff --git a/src/healthchecks_io/client/async_client.py b/src/healthchecks_io/client/async_client.py index 248c635..40d42ba 100644 --- a/src/healthchecks_io/client/async_client.py +++ b/src/healthchecks_io/client/async_client.py @@ -341,7 +341,9 @@ class AsyncClient(AbstractClient): for key, item in response.json()["badges"].items() } - async def success_ping(self, uuid: str = "", slug: str = "") -> Tuple[bool, str]: + async def success_ping( + self, uuid: str = "", slug: str = "", data: str = "" + ) -> Tuple[bool, str]: """Signals to Healthchecks.io that a job has completed successfully. Can also be used to indicate a continuously running process is still running and healthy. @@ -357,6 +359,7 @@ class AsyncClient(AbstractClient): Args: uuid (str): Check's UUID. Defaults to "". slug (str): Check's Slug. Defaults to "". + data (str): Text data to append to this check. Defaults to "". Raises: HCAPIAuthError: Raised when status_code == 401 or 403 @@ -371,10 +374,14 @@ class AsyncClient(AbstractClient): Tuple[bool, str]: success (true or false) and the response text """ ping_url = self._get_ping_url(uuid, slug, "") - response = self.check_ping_response(await self._client.get(ping_url)) + response = self.check_ping_response( + await self._client.post(ping_url, content=data) + ) return (True if response.status_code == 200 else False, response.text) - async def start_ping(self, uuid: str = "", slug: str = "") -> Tuple[bool, str]: + async def start_ping( + self, uuid: str = "", slug: str = "", data: str = "" + ) -> Tuple[bool, str]: """Sends a "job has started!" message to Healthchecks.io. Sending a "start" signal is optional, but it enables a few extra features: @@ -392,6 +399,7 @@ class AsyncClient(AbstractClient): Args: uuid (str): Check's UUID. Defaults to "". slug (str): Check's Slug. Defaults to "". + data (str): Text data to append to this check. Defaults to "". Raises: HCAPIAuthError: Raised when status_code == 401 or 403 @@ -406,10 +414,14 @@ class AsyncClient(AbstractClient): Tuple[bool, str]: success (true or false) and the response text """ ping_url = self._get_ping_url(uuid, slug, "/start") - response = self.check_ping_response(await self._client.get(ping_url)) + response = self.check_ping_response( + await self._client.post(ping_url, content=data) + ) return (True if response.status_code == 200 else False, response.text) - async def fail_ping(self, uuid: str = "", slug: str = "") -> Tuple[bool, str]: + async def fail_ping( + self, uuid: str = "", slug: str = "", data: str = "" + ) -> Tuple[bool, str]: """Signals to Healthchecks.io that the job has failed. Actively signaling a failure minimizes the delay from your monitored service failing to you receiving an alert. @@ -425,6 +437,7 @@ class AsyncClient(AbstractClient): Args: uuid (str): Check's UUID. Defaults to "". slug (str): Check's Slug. Defaults to "". + data (str): Text data to append to this check. Defaults to "". Raises: HCAPIAuthError: Raised when status_code == 401 or 403 @@ -439,11 +452,13 @@ class AsyncClient(AbstractClient): Tuple[bool, str]: success (true or false) and the response text """ ping_url = self._get_ping_url(uuid, slug, "/fail") - response = self.check_ping_response(await self._client.get(ping_url)) + response = self.check_ping_response( + await self._client.post(ping_url, content=data) + ) return (True if response.status_code == 200 else False, response.text) async def exit_code_ping( - self, exit_code: int, uuid: str = "", slug: str = "" + self, exit_code: int, uuid: str = "", slug: str = "", data: str = "" ) -> Tuple[bool, str]: """Signals to Healthchecks.io that the job has failed. @@ -461,6 +476,7 @@ class AsyncClient(AbstractClient): exit_code (int): Exit code to sent, int from 0 to 255 uuid (str): Check's UUID. Defaults to "". slug (str): Check's Slug. Defaults to "". + data (str): Text data to append to this check. Defaults to "". Raises: HCAPIAuthError: Raised when status_code == 401 or 403 @@ -475,5 +491,7 @@ class AsyncClient(AbstractClient): Tuple[bool, str]: success (true or false) and the response text """ ping_url = self._get_ping_url(uuid, slug, f"/{exit_code}") - response = self.check_ping_response(await self._client.get(ping_url)) + response = self.check_ping_response( + await self._client.post(ping_url, content=data) + ) return (True if response.status_code == 200 else False, response.text) diff --git a/src/healthchecks_io/client/check_trap.py b/src/healthchecks_io/client/check_trap.py new file mode 100644 index 0000000..bb04879 --- /dev/null +++ b/src/healthchecks_io/client/check_trap.py @@ -0,0 +1,165 @@ +"""CheckTrap is a context manager to wrap around python code to communicate results to a Healthchecks check.""" +from types import TracebackType +from typing import List +from typing import Optional +from typing import Type +from typing import Union + +from .async_client import AsyncClient +from .exceptions import PingFailedError +from .exceptions import WrongClientError +from .sync_client import Client + + +class CheckTrap: + """CheckTrap is a context manager to wrap around python code to communicate results to a Healthchecks check.""" + + def __init__( + self, + client: Union[Client, AsyncClient], + uuid: str = "", + slug: str = "", + suppress_exceptions: bool = False, + ) -> None: + """A context manager to wrap around python code to communicate results to a Healthchecks check. + + Args: + client (Union[Client, AsyncClient]): healthchecks_io client, async or sync + uuid (str): uuid of the check. Defaults to "". + slug (str): slug of the check, exclusion wiht uuid. Defaults to "". + suppress_exceptions (bool): If true, do not raise any exceptions. Defaults to False. + + Raises: + Exception: Raised if a slug and a uuid is passed + """ + if uuid == "" and slug == "": + raise Exception("Must pass a slug or an uuid") + self.client: Union[Client, AsyncClient] = client + self.uuid: str = uuid + self.slug: str = slug + self.log_lines: List[str] = list() + self.suppress_exceptions: bool = suppress_exceptions + + def add_log(self, line: str) -> None: + """Add a line to the context manager's log that is sent with the check. + + Args: + line (str): String to add to the logs + """ + self.log_lines.append(line) + + def __enter__(self) -> "CheckTrap": + """Enter the context manager. + + Sends a start ping to the check represented by self.uuid or self.slug. + + Raises: + WrongClientError: Raised when using an AsyncClient with this as a sync client manager + PingFailedError: When a ping fails for any reason not handled by a custom exception + HCAPIAuthError: Raised when status_code == 401 or 403 + HCAPIError: Raised when status_code is 5xx + CheckNotFoundError: Raised when status_code is 404 or response text has "not found" in it + BadAPIRequestError: Raised when status_code is 400, or if you pass a uuid and a slug, or if + pinging by a slug and do not have a ping key set + HCAPIRateLimitError: Raised when status code is 429 or response text has "rate limited" in it + NonUniqueSlugError: Raused when status code is 409. + + Returns: + CheckTrap: self + """ + if isinstance(self.client, AsyncClient): + raise WrongClientError( + "You passed an AsyncClient, use this as an async context manager" + ) + result = self.client.start_ping(uuid=self.uuid, slug=self.slug) + if not result[0]: + raise PingFailedError(result[1]) + return self + + def __exit__( + self, + exc_type: Optional[Type[BaseException]], + exc: Optional[BaseException], + traceback: Optional[TracebackType], + ) -> Optional[bool]: + """Exit the context manager. + + If there is an exception, add it to any log lines and send a fail ping. + Otherwise, send a success ping with any log lines appended. + + Args: + exc_type (Optional[Type[BaseException]]): [description] + exc (Optional[BaseException]): [description] + traceback (Optional[TracebackType]): [description] + + Returns: + Optional[bool]: self.suppress_exceptions, if true will not raise any exceptions + """ + if exc_type is None: + self.client.success_ping( + self.uuid, self.slug, data="\n".join(self.log_lines) + ) + else: + self.add_log(str(exc)) + self.add_log(str(traceback)) + self.client.fail_ping(self.uuid, self.slug, data="\n".join(self.log_lines)) + return self.suppress_exceptions + + async def __aenter__(self) -> "CheckTrap": + """Enter the context manager. + + Sends a start ping to the check represented by self.uuid or self.slug. + + Raises: + WrongClientError: Raised when using an AsyncClient with this as a sync client manager + PingFailedError: When a ping fails for any reason not handled by a custom exception + HCAPIAuthError: Raised when status_code == 401 or 403 + HCAPIError: Raised when status_code is 5xx + CheckNotFoundError: Raised when status_code is 404 or response text has "not found" in it + BadAPIRequestError: Raised when status_code is 400, or if you pass a uuid and a slug, or if + pinging by a slug and do not have a ping key set + HCAPIRateLimitError: Raised when status code is 429 or response text has "rate limited" in it + NonUniqueSlugError: Raused when status code is 409. + + Returns: + CheckTrap: self + """ + if isinstance(self.client, Client): + raise WrongClientError( + "You passed a sync Client, use this as a regular context manager" + ) + result = await self.client.start_ping(self.uuid, self.slug) + if not result[0]: + raise PingFailedError(result[1]) + return self + + async def __aexit__( + self, + exc_type: Optional[Type[BaseException]], + exc: Optional[BaseException], + traceback: Optional[TracebackType], + ) -> Optional[bool]: + """Exit the context manager. + + If there is an exception, add it to any log lines and send a fail ping. + Otherwise, send a success ping with any log lines appended. + + Args: + exc_type (Optional[Type[BaseException]]): [description] + exc (Optional[BaseException]): [description] + traceback (Optional[TracebackType]): [description] + + Returns: + Optional[bool]: self.suppress_exceptions, if true will not raise any exceptions + """ + if exc_type is None: + await self.client.success_ping( + self.uuid, self.slug, data="\n".join(self.log_lines) + ) + else: + self.add_log(str(exc)) + self.add_log(str(traceback)) + await self.client.fail_ping( + self.uuid, self.slug, data="\n".join(self.log_lines) + ) + return self.suppress_exceptions diff --git a/src/healthchecks_io/client/exceptions.py b/src/healthchecks_io/client/exceptions.py index c21b09a..6d28048 100644 --- a/src/healthchecks_io/client/exceptions.py +++ b/src/healthchecks_io/client/exceptions.py @@ -35,3 +35,15 @@ class NonUniqueSlugError(HCAPIError): """Thrown when the api returns a 409 when pinging.""" ... + + +class WrongClientError(HCAPIError): + """Thrown when trying to use a CheckTrap with the wrong client type.""" + + ... + + +class PingFailedError(HCAPIError): + """Thrown when a ping fails.""" + + ... diff --git a/src/healthchecks_io/client/sync_client.py b/src/healthchecks_io/client/sync_client.py index 4b4161c..8ee159b 100644 --- a/src/healthchecks_io/client/sync_client.py +++ b/src/healthchecks_io/client/sync_client.py @@ -324,7 +324,9 @@ class Client(AbstractClient): for key, item in response.json()["badges"].items() } - def success_ping(self, uuid: str = "", slug: str = "") -> Tuple[bool, str]: + def success_ping( + self, uuid: str = "", slug: str = "", data: str = "" + ) -> Tuple[bool, str]: """Signals to Healthchecks.io that a job has completed successfully. Can also be used to indicate a continuously running process is still running and healthy. @@ -340,6 +342,7 @@ class Client(AbstractClient): Args: uuid (str): Check's UUID. Defaults to "". slug (str): Check's Slug. Defaults to "". + data (str): Text data to append to this check. Defaults to "" Raises: HCAPIAuthError: Raised when status_code == 401 or 403 @@ -354,10 +357,12 @@ class Client(AbstractClient): Tuple[bool, str]: success (true or false) and the response text """ ping_url = self._get_ping_url(uuid, slug, "") - response = self.check_ping_response(self._client.get(ping_url)) + response = self.check_ping_response(self._client.post(ping_url, content=data)) return (True if response.status_code == 200 else False, response.text) - def start_ping(self, uuid: str = "", slug: str = "") -> Tuple[bool, str]: + def start_ping( + self, uuid: str = "", slug: str = "", data: str = "" + ) -> Tuple[bool, str]: """Sends a "job has started!" message to Healthchecks.io. Sending a "start" signal is optional, but it enables a few extra features: @@ -375,6 +380,7 @@ class Client(AbstractClient): Args: uuid (str): Check's UUID. Defaults to "". slug (str): Check's Slug. Defaults to "". + data (str): Text data to append to this check. Defaults to "" Raises: HCAPIAuthError: Raised when status_code == 401 or 403 @@ -389,10 +395,12 @@ class Client(AbstractClient): Tuple[bool, str]: success (true or false) and the response text """ ping_url = self._get_ping_url(uuid, slug, "/start") - response = self.check_ping_response(self._client.get(ping_url)) + response = self.check_ping_response(self._client.post(ping_url, content=data)) return (True if response.status_code == 200 else False, response.text) - def fail_ping(self, uuid: str = "", slug: str = "") -> Tuple[bool, str]: + def fail_ping( + self, uuid: str = "", slug: str = "", data: str = "" + ) -> Tuple[bool, str]: """Signals to Healthchecks.io that the job has failed. Actively signaling a failure minimizes the delay from your monitored service failing to you receiving an alert. @@ -408,6 +416,7 @@ class Client(AbstractClient): Args: uuid (str): Check's UUID. Defaults to "". slug (str): Check's Slug. Defaults to "". + data (str): Text data to append to this check. Defaults to "" Raises: HCAPIAuthError: Raised when status_code == 401 or 403 @@ -422,11 +431,11 @@ class Client(AbstractClient): Tuple[bool, str]: success (true or false) and the response text """ ping_url = self._get_ping_url(uuid, slug, "/fail") - response = self.check_ping_response(self._client.get(ping_url)) + response = self.check_ping_response(self._client.post(ping_url, content=data)) return (True if response.status_code == 200 else False, response.text) def exit_code_ping( - self, exit_code: int, uuid: str = "", slug: str = "" + self, exit_code: int, uuid: str = "", slug: str = "", data: str = "" ) -> Tuple[bool, str]: """Signals to Healthchecks.io that the job has failed. @@ -444,6 +453,7 @@ class Client(AbstractClient): exit_code (int): Exit code to sent, int from 0 to 255 uuid (str): Check's UUID. Defaults to "". slug (str): Check's Slug. Defaults to "". + data (str): Text data to append to this check. Defaults to "" Raises: HCAPIAuthError: Raised when status_code == 401 or 403 @@ -458,5 +468,5 @@ class Client(AbstractClient): Tuple[bool, str]: success (true or false) and the response text """ ping_url = self._get_ping_url(uuid, slug, f"/{exit_code}") - response = self.check_ping_response(self._client.get(ping_url)) + response = self.check_ping_response(self._client.post(ping_url, content=data)) return (True if response.status_code == 200 else False, response.text) diff --git a/tests/client/test_async.py b/tests/client/test_async.py index 6d79bf5..79e3dce 100644 --- a/tests/client/test_async.py +++ b/tests/client/test_async.py @@ -393,9 +393,9 @@ ping_test_parameters = [ @pytest.mark.parametrize( "respx_mocker, tc, url, ping_method, method_kwargs", ping_test_parameters ) -async def test_success_ping(respx_mocker, tc, url, ping_method, method_kwargs): +async def test_asuccess_ping(respx_mocker, tc, url, ping_method, method_kwargs): channels_url = urljoin(tc._ping_url, url) - respx_mocker.get(channels_url).mock( + respx_mocker.post(channels_url).mock( return_value=Response(status_code=200, text="OK") ) ping_method = getattr(tc, ping_method) diff --git a/tests/client/test_check_trap.py b/tests/client/test_check_trap.py new file mode 100644 index 0000000..9a640f9 --- /dev/null +++ b/tests/client/test_check_trap.py @@ -0,0 +1,97 @@ +from urllib.parse import urljoin + +import pytest +import respx +from httpx import Client as HTTPXClient +from httpx import Response + +from healthchecks_io import CheckCreate +from healthchecks_io import CheckTrap +from healthchecks_io import CheckUpdate +from healthchecks_io import PingFailedError +from healthchecks_io import WrongClientError + + +@pytest.mark.respx +def test_check_trap_sync(respx_mock, test_client): + start_url = urljoin(test_client._ping_url, "test/start") + respx_mock.post(start_url).mock(return_value=Response(status_code=200, text="OK")) + success_url = urljoin(test_client._ping_url, "test") + respx_mock.post(success_url).mock(return_value=Response(status_code=200, text="OK")) + + with CheckTrap(test_client, uuid="test") as ct: + pass + + +@pytest.mark.respx +def test_check_trap_sync_failed_ping(respx_mock, test_client): + start_url = urljoin(test_client._ping_url, "test/start") + respx_mock.post(start_url).mock(return_value=Response(status_code=444, text="OK")) + with pytest.raises(PingFailedError): + with CheckTrap(test_client, uuid="test") as ct: + pass + + +@pytest.mark.respx +def test_check_trap_sync_exception(respx_mock, test_client): + start_url = urljoin(test_client._ping_url, "test/start") + respx_mock.post(start_url).mock(return_value=Response(status_code=200, text="OK")) + fail_url = urljoin(test_client._ping_url, "test/fail") + respx_mock.post(fail_url).mock(return_value=Response(status_code=200, text="OK")) + with pytest.raises(Exception): + with CheckTrap(test_client, uuid="test") as ct: + raise Exception("Exception") + + +@pytest.mark.asyncio +@pytest.mark.respx +async def test_check_trap_async(respx_mock, test_async_client): + start_url = urljoin(test_async_client._ping_url, "test/start") + respx_mock.post(start_url).mock(return_value=Response(status_code=200, text="OK")) + success_url = urljoin(test_async_client._ping_url, "test") + respx_mock.post(success_url).mock(return_value=Response(status_code=200, text="OK")) + + async with CheckTrap(test_async_client, uuid="test") as ct: + pass + + +@pytest.mark.asyncio +@pytest.mark.respx +async def test_check_trap_async_failed_ping(respx_mock, test_async_client): + start_url = urljoin(test_async_client._ping_url, "test/start") + respx_mock.post(start_url).mock(return_value=Response(status_code=444, text="OK")) + with pytest.raises(PingFailedError): + async with CheckTrap(test_async_client, uuid="test") as ct: + pass + + +@pytest.mark.asyncio +@pytest.mark.respx +async def test_check_trap_async_exception(respx_mock, test_async_client): + start_url = urljoin(test_async_client._ping_url, "test/start") + respx_mock.post(start_url).mock(return_value=Response(status_code=200, text="OK")) + fail_url = urljoin(test_async_client._ping_url, "test/fail") + respx_mock.post(fail_url).mock(return_value=Response(status_code=200, text="OK")) + + with pytest.raises(Exception): + async with CheckTrap(test_async_client, uuid="test") as ct: + raise Exception("Exception") + + +@pytest.mark.asyncio +async def test_check_trap_wrong_client_error(test_client, test_async_client): + + with pytest.raises(WrongClientError): + async with CheckTrap(test_client, uuid="test") as ct: + pass + + with pytest.raises(WrongClientError): + with CheckTrap(test_async_client, uuid="test") as ct: + pass + + +def test_check_trap_no_uuid_or_slug(test_client): + with pytest.raises(Exception) as exc: + with CheckTrap(test_client): + pass + assert str(exc) == "Must pass a slug or an uuid" diff --git a/tests/client/test_sync.py b/tests/client/test_sync.py index a7585fb..e5da7ca 100644 --- a/tests/client/test_sync.py +++ b/tests/client/test_sync.py @@ -358,7 +358,7 @@ ping_test_parameters = [ ) def test_success_ping(respx_mocker, tc, url, ping_method, method_kwargs): channels_url = urljoin(tc._ping_url, url) - respx_mocker.get(channels_url).mock( + respx_mocker.post(channels_url).mock( return_value=Response(status_code=200, text="OK") ) ping_method = getattr(tc, ping_method)