Skip to content
This repository has been archived by the owner on Sep 2, 2024. It is now read-only.

Commit

Permalink
refactor: change ping endponint to use LivenessHealthBackend
Browse files Browse the repository at this point in the history
  • Loading branch information
hartungstenio committed May 2, 2024
1 parent ff094bf commit d9c7471
Show file tree
Hide file tree
Showing 8 changed files with 68 additions and 17 deletions.
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ classifiers = [
]
dependencies = [
"django>=4.2",
"typing-extensions; python_version<'3.11'"
"typing-extensions; python_version<'3.12'"
]

[project.urls]
Expand Down
20 changes: 12 additions & 8 deletions src/healthy/backends.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
# SPDX-FileCopyrightText: 2024-present OLIST TINY TECNOLOGIA LTDA
#
# SPDX-License-Identifier: MIT
from dataclasses import dataclass, field
from __future__ import annotations

from abc import ABC, abstractmethod
from typing import overload, Optional, Union
from dataclasses import dataclass, field
from typing import overload

from .compat import Self, StrEnum, override

from .compat import Self, override, StrEnum

class HealthStatus(StrEnum):
UP = "up"
DOWN = "down"


@dataclass
class Health:
status: HealthStatus
Expand All @@ -27,9 +31,9 @@ def up(cls, details: dict) -> Self:
...

@classmethod
def up(cls, details: Optional[dict] = None) -> Self:
def up(cls, details: dict | None = None) -> Self:
if details is None:
details = dict()
details = {}

return cls(status=HealthStatus.UP, details=details)

Expand All @@ -49,9 +53,9 @@ def down(cls, details: dict) -> Self:
...

@classmethod
def down(cls, details: Optional[Union[dict, Exception]] = None) -> Self:
def down(cls, details: dict | Exception | None = None) -> Self:
if details is None:
details = dict()
details = {}
elif isinstance(details, Exception):
details = {"error": str(details)}

Expand All @@ -62,7 +66,7 @@ class HealthBackend(ABC):
async def run(self) -> Health:
try:
health = await self.run_health_check()
except Exception as exc:
except Exception as exc: # noqa: BLE001
health = Health.down(exc)

return health
Expand Down
2 changes: 1 addition & 1 deletion src/healthy/compat.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

try:
from typing import Self
except ImportError:
Expand All @@ -17,6 +16,7 @@
class StrEnum(str, Enum):
pass


__all__ = [
"Self",
"override",
Expand Down
15 changes: 15 additions & 0 deletions src/healthy/responses.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# SPDX-FileCopyrightText: 2024-present OLIST TINY TECNOLOGIA LTDA
#
# SPDX-License-Identifier: MIT
from dataclasses import asdict
from http import HTTPStatus

from django.http import JsonResponse

from .backends import Health, HealthStatus


class HealthResponse(JsonResponse):
def __init__(self, health: Health):
status = HTTPStatus.OK if health.status == HealthStatus.UP else HTTPStatus.INTERNAL_SERVER_ERROR
super().__init__(data=asdict(health), status=status)
4 changes: 2 additions & 2 deletions src/healthy/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@
# SPDX-License-Identifier: MIT
from django.urls import path

from .views import PingView
from .views import LivenessView

app_name = "healthy"

urlpatterns = [
path("ping/", PingView.as_view(), name="ping"),
path("ping/", LivenessView.as_view(), name="ping"),
]
10 changes: 7 additions & 3 deletions src/healthy/views.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
# SPDX-FileCopyrightText: 2024-present OLIST TINY TECNOLOGIA LTDA
#
# SPDX-License-Identifier: MIT
from http import HTTPStatus
from typing import ClassVar

from django.http import HttpRequest, HttpResponse
from django.views import View

from .backends import LivenessHealthBackend
from .responses import HealthResponse

class PingView(View):

class LivenessView(View):
http_method_names: ClassVar = [
"get",
"head",
Expand All @@ -17,4 +19,6 @@ class PingView(View):
]

async def get(self, request: HttpRequest) -> HttpResponse: # noqa: ARG002
return HttpResponse("Pong", status=HTTPStatus.OK)
backend = LivenessHealthBackend()
health = await backend.run()
return HealthResponse(health)
8 changes: 6 additions & 2 deletions tests/test_backends.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from healthy import backends
from healthy.compat import override


class TestHealth:
def test_up_without_args(self):
got = backends.Health.up()
Expand Down Expand Up @@ -39,13 +40,15 @@ def test_down_with_exception_details(self):
assert got.status == backends.HealthStatus.DOWN
assert got.details == {"error": given_message}


@pytest.mark.asyncio
class TestHealthBackend:
async def test_run_handles_exceptions(self):
class FaultHealthBackend(backends.HealthBackend):
@override
async def run_health_check(self) -> backends.Health:
raise RuntimeError("Something went wrong")
msg = "Something went wrong"
raise RuntimeError(msg)

backend = FaultHealthBackend()
got = await backend.run()
Expand All @@ -57,6 +60,7 @@ async def run_health_check(self) -> backends.Health:

async def test_run_with_successful_check(self):
expected = backends.Health.up({"message": "It's fine"})

class ProxyHealthBackend(backends.HealthBackend):
def __init__(self, health: backends.Health):
self.health = health
Expand All @@ -72,7 +76,7 @@ async def run_health_check(self) -> backends.Health:
assert got == expected


@pytest.mark.asyncio()
@pytest.mark.asyncio
class TestLivenessHealthBackend:
async def test_run_health_check(self):
backend = backends.LivenessHealthBackend()
Expand Down
24 changes: 24 additions & 0 deletions tests/test_responses.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# SPDX-FileCopyrightText: 2024-present OLIST TINY TECNOLOGIA LTDA
#
# SPDX-License-Identifier: MIT
import json
from http import HTTPStatus

from healthy.backends import Health
from healthy.responses import HealthResponse


class TestHealthResponse:
def test_with_up_health(self):
health = Health.up({"message": "It's fine"})
response = HealthResponse(health)

assert response.status_code == HTTPStatus.OK
assert json.loads(response.content) == {"status": "up", "details": {"message": "It's fine"}}

def test_with_down_health(self):
health = Health.down({"message": "Something went wrong"})
response = HealthResponse(health)

assert response.status_code == HTTPStatus.INTERNAL_SERVER_ERROR
assert json.loads(response.content) == {"status": "down", "details": {"message": "Something went wrong"}}

0 comments on commit d9c7471

Please sign in to comment.