Coverage for src/lexigram/web/module.py: 39%

33 statements  

« prev     ^ index     » next       coverage.py v7.15.4, created at 2026-08-25 04:37 +0800

1"""Web module for dependency injection.""" 

2 

3from __future__ import annotations 

4 

5from typing import Any 

6 

7from lexigram.contracts.web import WebRateLimiterProtocol 

8from lexigram.di.module import DynamicModule, Module, module 

9from lexigram.web.config import ServerConfig, WebConfig # ServerConfig used in stub() 

10from lexigram.web.di.provider import WebProvider 

11 

12 

13@module() 

14class WebModule(Module): 

15 """Web module with HTTP provider.""" 

16 

17 @classmethod 

18 def configure( 

19 cls, 

20 controllers: list[type] | None = None, 

21 discover: list[str] | tuple[str, ...] | None = None, 

22 host: str | None = None, 

23 port: int | None = None, 

24 **kwargs: Any, 

25 ) -> DynamicModule: 

26 """Create a WebModule with explicit configuration. 

27 

28 Configuration (CORS, CSRF, server host/port, etc.) is loaded from the 

29 ``[web]`` section of ``application.yaml`` by the orchestrator and injected 

30 into the provider at registration time. Pass a ``web_config`` kwarg 

31 only if you need to bypass YAML entirely. 

32 

33 Args: 

34 controllers: Controller classes to register with the web server. 

35 discover: Package paths to scan for controllers and merge with 

36 any explicitly provided controller classes. 

37 host: Override the server host (builds a ``WebConfig`` internally). 

38 port: Override the server port (builds a ``WebConfig`` internally). 

39 **kwargs: Additional keyword arguments forwarded to 

40 :class:`~lexigram.web.di.provider.WebProvider`. 

41 """ 

42 resolved_controllers = list(controllers or []) 

43 if discover: 

44 from lexigram.web.routing.discovery import discover_controllers 

45 

46 for controller in discover_controllers(list(discover)): 

47 if controller not in resolved_controllers: 

48 resolved_controllers.append(controller) 

49 

50 if (host is not None or port is not None) and "web_config" not in kwargs: 

51 server_kwargs: dict[str, Any] = {} 

52 if host is not None: 

53 server_kwargs["host"] = host 

54 if port is not None: 

55 server_kwargs["port"] = port 

56 kwargs["web_config"] = WebConfig(server=ServerConfig(**server_kwargs)) 

57 

58 from lexigram.web.admin.contributor import WebAdminContributor 

59 from lexigram.web.admin.handlers.active_connections import ( 

60 ActiveConnectionsWidgetHandler, 

61 ) 

62 from lexigram.web.admin.handlers.request_rate import RequestRateWidgetHandler 

63 from lexigram.web.admin.handlers.server_status import ServerStatusWidgetHandler 

64 

65 return DynamicModule( 

66 module=cls, 

67 providers=[WebProvider(controllers=resolved_controllers, **kwargs)], 

68 is_global=True, 

69 exports=[ 

70 WebProvider, 

71 WebRateLimiterProtocol, 

72 WebAdminContributor, 

73 ServerStatusWidgetHandler, 

74 ActiveConnectionsWidgetHandler, 

75 RequestRateWidgetHandler, 

76 ], 

77 ) 

78 

79 @classmethod 

80 def stub(cls, config: Any = None) -> DynamicModule: 

81 """Return a no-op WebModule suitable for unit testing. 

82 

83 Registers WebProvider with no controllers and test-safe 

84 configuration. No real HTTP server is started. 

85 

86 Args: 

87 config: Optional test configuration override. 

88 

89 Returns: 

90 A DynamicModule with in-memory web configuration. 

91 """ 

92 web_config = WebConfig(server=ServerConfig(host="127.0.0.1", port=8000)) 

93 return DynamicModule( 

94 module=cls, 

95 providers=[WebProvider(web_config=web_config, controllers=[])], 

96 is_global=True, 

97 exports=[WebProvider, WebRateLimiterProtocol], 

98 ) 

99 

100 

101__all__ = ["WebModule"]