Coverage for src / lexigram / ui / htmx / action_response.py: 100%

30 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-08-10 04:11 +0800

1"""Typed builder for HTMX action responses. 

2 

3Consolidates the HX-Trigger header construction that Piccolina's action 

4bridge (and other handlers) build by hand. 

5""" 

6 

7from __future__ import annotations 

8 

9from dataclasses import dataclass, field 

10from enum import Enum 

11from typing import Any 

12 

13from lexigram.serialization import dumps_str 

14from lexigram.ui.layouts.server_toasts import ToastData 

15 

16 

17class ToastType(str, Enum): 

18 """Severity levels for the ``show-toast`` HTMX event payload.""" 

19 

20 SUCCESS = "success" 

21 ERROR = "error" 

22 WARNING = "warning" 

23 INFO = "info" 

24 

25 

26@dataclass 

27class HtmxActionResponse: 

28 """Builder for HTMX action responses with merged HX-Trigger headers. 

29 

30 Args: 

31 toast: Optional toast notification to include in the trigger payload. 

32 trigger: Additional HX-Trigger events to merge alongside the toast. 

33 status_code: HTTP status code for the response (default 200). 

34 

35 Usage:: 

36 

37 return HtmxActionResponse( 

38 toast=ToastData(message="User deleted", type=ToastType.SUCCESS), 

39 trigger={"refresh-list": True}, 

40 status_code=200, 

41 ).to_response() 

42 """ 

43 

44 toast: ToastData | None = None 

45 trigger: dict[str, Any] = field(default_factory=dict) 

46 status_code: int = 200 

47 

48 def _build_trigger(self) -> dict[str, Any]: 

49 payload: dict[str, Any] = {} 

50 if self.toast is not None: 

51 payload["show-toast"] = { 

52 "message": self.toast.message, 

53 "type": str(self.toast.type), 

54 } 

55 payload.update(self.trigger) 

56 return payload 

57 

58 def to_response(self) -> Any: 

59 """Return a Starlette ``HTMLResponse`` with the merged HX-Trigger header. 

60 

61 Returns: 

62 ``starlette.responses.HTMLResponse`` with empty body and 

63 ``HX-Trigger`` header set. 

64 """ 

65 from starlette.responses import HTMLResponse 

66 

67 trigger_payload = self._build_trigger() 

68 headers: dict[str, str] = {} 

69 if trigger_payload: 

70 headers["HX-Trigger"] = dumps_str(trigger_payload) 

71 return HTMLResponse("", status_code=self.status_code, headers=headers) 

72 

73 

74__all__ = ["HtmxActionResponse", "ToastData", "ToastType"]