Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,2 +1,7 @@
LOGGING_LEVEL=INFO
LOGGING_FORMATTER=pretty

APP_HOST=127.0.0.1
APP_PORT=8000
DOCS_HOST=127.0.0.1
DOCS_PORT=8008
12 changes: 12 additions & 0 deletions justfile
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ set dotenv-load := true
VENV_DIR := ".venv"
BUILD_DIR := "build"
CACHE_DIR := ".cache"

APP_HOST := env_var_or_default("APP_HOST", "127.0.0.1")
APP_PORT := env_var_or_default("APP_PORT", "8000")
DOCS_HOST := env_var_or_default("DOCS_HOST", "127.0.0.1")
DOCS_PORT := env_var_or_default("DOCS_PORT", "8008")

Expand Down Expand Up @@ -60,3 +63,12 @@ changelog-build:

changelog-fragment:
@uv run --group=changelog towncrier create

app-dev-serve:
@uv run --group=dev granian \
--host="{{ APP_HOST }}" \
--port="{{ APP_PORT }}" \
--interface="asgi" \
--factory \
--reload \
app.interface.http.asgi:create_asgi_application
6 changes: 6 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,15 @@ requires-python = ">=3.14"
classifiers = [ "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.14" ]
dynamic = [ "version" ]
dependencies = [
"granian>=2.6",
"litestar>=2.19",
"msgspec>=0.20",
"pydantic-settings>=2.12",
]

[dependency-groups]
dev = [
"granian[reload]>=2.6",
{ include-group = "changelog" },
{ include-group = "docs" },
{ include-group = "lint" },
Expand Down Expand Up @@ -118,6 +122,7 @@ lint.isort.lines-after-imports = 2
lint.pydocstyle.convention = "numpy"

[tool.pytest.ini_options]
anyio_mode = "auto"
cache_dir = ".cache/pytest"
pythonpath = [ "src" ]
testpaths = [ "tests" ]
Expand All @@ -132,6 +137,7 @@ log_cli = true
log_cli_level = "INFO"
filterwarnings = [
"error",
"ignore:Core Pydantic V1 functionality isn't compatible with Python 3.14 or greater.:UserWarning",
]

[tool.coverage.run]
Expand Down
7 changes: 5 additions & 2 deletions src/app/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
from .__meta__ import __version__
from .__meta__ import __api_version__, __version__


__all__ = ("__version__",)
__all__ = (
"__api_version__",
"__version__",
)
1 change: 1 addition & 0 deletions src/app/__meta__.py
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
__version__ = "0.0.0a0"
__api_version__ = "0.0.0a0"
Empty file added src/app/interface/__init__.py
Empty file.
4 changes: 4 additions & 0 deletions src/app/interface/http/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from . import asgi


__all__ = ("asgi",)
27 changes: 27 additions & 0 deletions src/app/interface/http/asgi.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
from litestar import Litestar
from litestar.openapi import OpenAPIConfig

from app import __api_version__
from app.interface.http.controller.system import health
from app.platform.config.loaders import load_logging_config
from app.platform.logging import configure_logging


def create_asgi_application() -> Litestar:
"""Create and configure the ASGI application.

Returns
-------
Litestar
ASGI application.
"""
app = Litestar(
route_handlers=[
health,
],
openapi_config=OpenAPIConfig(title="Land Sight API", version=__api_version__),
)
logging_config = load_logging_config()
configure_logging(config=logging_config)

return app
4 changes: 4 additions & 0 deletions src/app/interface/http/controller/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from . import system


__all__ = ("system",)
9 changes: 9 additions & 0 deletions src/app/interface/http/controller/system.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from litestar import get

from app.interface.http.schema.system import HealthResponse


@get("/health", sync_to_thread=False)
def health() -> HealthResponse:
"""Check service availability."""
return HealthResponse(status="ok")
4 changes: 4 additions & 0 deletions src/app/interface/http/schema/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from . import system


__all__ = ("system",)
9 changes: 9 additions & 0 deletions src/app/interface/http/schema/system.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import typing

from msgspec import Struct


class HealthResponse(Struct):
"""Health response schema."""

status: typing.Literal["ok"] = "ok"
27 changes: 27 additions & 0 deletions tests/integration/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import typing

import pytest
from litestar.testing import AsyncTestClient

from app.interface.http.asgi import create_asgi_application


if typing.TYPE_CHECKING:
from collections.abc import AsyncIterator

from litestar import Litestar


@pytest.fixture(scope="session")
def app() -> Litestar:
"""Provide Litestar application instance for tests."""
app = create_asgi_application()
app.debug = True
return app


@pytest.fixture(scope="function")
async def test_client(app: Litestar) -> AsyncIterator[AsyncTestClient[Litestar]]:
"""Provide async test client bound to the Litestar app."""
async with AsyncTestClient(app=app) as client:
yield client
14 changes: 14 additions & 0 deletions tests/integration/interface/http/controller/test_system.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import typing

from litestar import Litestar, status_codes


if typing.TYPE_CHECKING:
from litestar.testing import AsyncTestClient


async def test_health_check_endpoint(test_client: AsyncTestClient[Litestar]) -> None:
"""Health check endpoint returns HTTP 200 and ok status."""
response = await test_client.get("/health")
assert response.status_code == status_codes.HTTP_200_OK
assert response.json() == {"status": "ok"}