Coverage for src / lexigram / contracts / admin / protocols.py: 100%

50 statements  

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

1"""Admin contract protocols — service boundaries for the admin contributor system.""" 

2 

3from __future__ import annotations 

4 

5from collections.abc import Sequence 

6from typing import TYPE_CHECKING, Protocol, runtime_checkable 

7 

8if TYPE_CHECKING: 

9 from lexigram.contracts.admin.errors import AdminError 

10 from lexigram.contracts.admin.health_payload import HealthCheckPayload 

11 from lexigram.contracts.admin.types import ( 

12 AdminActionDefinition, 

13 AdminHealthDefinition, 

14 AdminRouteSpec, 

15 DashboardWidgetDefinition, 

16 ManagementPageDefinition, 

17 NavigationContribution, 

18 SettingsPanelDefinition, 

19 WidgetParams, 

20 WidgetViewModel, 

21 ) 

22 from lexigram.contracts.core.di import ContainerResolverProtocol 

23 from lexigram.contracts.core.result import Result 

24 

25 

26@runtime_checkable 

27class AdminContributorProtocol(Protocol): 

28 """Contract for packages that contribute admin dashboard surfaces. 

29 

30 Any lexigram extension package can implement this protocol and register 

31 it via the ``lexigram.admin.contributors`` entry point. The admin 

32 dashboard discovers contributors at boot and assembles their widgets, 

33 pages, navigation, and actions into the unified admin UI. 

34 """ 

35 

36 @property 

37 def name(self) -> str: 

38 """Unique contributor identifier (e.g. 'cache', 'events', 'ai').""" 

39 ... 

40 

41 @property 

42 def display_name(self) -> str: 

43 """Human-readable contributor name for the admin UI.""" 

44 ... 

45 

46 @property 

47 def group(self) -> str: 

48 """Navigation group this contributor belongs to.""" 

49 ... 

50 

51 @property 

52 def icon(self) -> str: 

53 """Lucide icon name for the contributor.""" 

54 ... 

55 

56 @property 

57 def depends_on(self) -> tuple[str, ...]: 

58 """Contributor names that must boot before this contributor.""" 

59 ... 

60 

61 @property 

62 def priority(self) -> int: 

63 """Ordering priority within its group (lower = first).""" 

64 ... 

65 

66 @property 

67 def version(self) -> str: 

68 """Semantic version of this contributor (e.g. '1.2.3').""" 

69 ... 

70 

71 @property 

72 def package_source(self) -> str: 

73 """Python package name that provides this contributor (e.g. 'lexigram-cache').""" 

74 ... 

75 

76 @property 

77 def contributor_id(self) -> str: 

78 """Stable unique identifier used for lookup and RBAC keying (equals ``name``).""" 

79 ... 

80 

81 @property 

82 def required_permissions(self) -> frozenset[str]: 

83 """Permissions a user must hold to execute any action on this contributor.""" 

84 ... 

85 

86 def get_resources(self) -> Sequence[type]: 

87 """Return resource classes managed by this contributor.""" 

88 ... 

89 

90 def get_dashboard_widgets(self) -> Sequence[DashboardWidgetDefinition]: 

91 """Return widget definitions for the main dashboard.""" 

92 ... 

93 

94 def get_navigation_items(self) -> Sequence[NavigationContribution]: 

95 """Return navigation entries for the admin sidebar.""" 

96 ... 

97 

98 def get_management_pages(self) -> Sequence[ManagementPageDefinition]: 

99 """Return full management page definitions.""" 

100 ... 

101 

102 def get_settings_panels(self) -> Sequence[SettingsPanelDefinition]: 

103 """Return settings panel definitions.""" 

104 ... 

105 

106 def get_health_definitions(self) -> Sequence[AdminHealthDefinition]: 

107 """Return health check definitions to surface in the dashboard.""" 

108 ... 

109 

110 def get_actions(self) -> Sequence[AdminActionDefinition]: 

111 """Return framework-level actions.""" 

112 ... 

113 

114 def get_routes(self) -> Sequence[AdminRouteSpec]: 

115 """Return route specifications for the admin router.""" 

116 ... 

117 

118 async def on_admin_boot(self, container: ContainerResolverProtocol) -> None: 

119 """Called when the admin dashboard boots.""" 

120 ... 

121 

122 async def on_admin_shutdown(self) -> None: 

123 """Called when the admin dashboard shuts down.""" 

124 ... 

125 

126 async def render_widget( 

127 self, 

128 widget_name: str, 

129 params: WidgetParams, 

130 resolver: ContainerResolverProtocol | None = None, 

131 ) -> Result[WidgetViewModel, AdminError]: 

132 """Render a named widget to a typed WidgetViewModel. 

133 

134 Args: 

135 widget_name: Name of the widget to render. 

136 params: Typed, validated widget parameters. 

137 resolver: Optional DI resolver for lazy dependency injection. 

138 

139 Returns: 

140 Ok(WidgetViewModel) with structured content in ``content`` on success. 

141 Err(WidgetNotFoundError) when the widget name is unknown. 

142 Err(AdminError) for other expected domain failures. 

143 Infrastructure exceptions propagate (not caught here). 

144 """ 

145 ... 

146 

147 async def render_health_check( 

148 self, 

149 check_name: str, 

150 ) -> Result[HealthCheckPayload, AdminError]: 

151 """Run a health check and return a structured health-check payload. 

152 

153 Args: 

154 check_name: Name of the health check to run (matches check ID 

155 from ``get_health_definitions``). 

156 

157 Returns: 

158 ``Ok(HealthCheckPayload)`` describing the check result on 

159 success — the host renders it as HTML. 

160 ``Err(HealthCheckNotFoundError)`` if *check_name* is not served 

161 by this contributor. 

162 ``Err(AdminError)`` if the check fails for any other reason. 

163 """ 

164 ... 

165 

166 

167@runtime_checkable 

168class AdminContributorRegistryProtocol(Protocol): 

169 """Registry that collects and manages admin contributors.""" 

170 

171 def register(self, contributor: AdminContributorProtocol) -> None: 

172 """Register a contributor.""" 

173 ... 

174 

175 def get(self, name: str) -> AdminContributorProtocol | None: 

176 """Get contributor by name.""" 

177 ... 

178 

179 def get_all(self) -> Sequence[AdminContributorProtocol]: 

180 """Get all registered contributors, ordered by priority.""" 

181 ... 

182 

183 def get_by_group(self, group: str) -> Sequence[AdminContributorProtocol]: 

184 """Get contributors in a specific group.""" 

185 ... 

186 

187 

188@runtime_checkable 

189class AdminDashboardProtocol(Protocol): 

190 """Protocol for the assembled admin dashboard service.""" 

191 

192 async def get_all_widgets(self) -> Sequence[DashboardWidgetDefinition]: 

193 """Collect widgets from all contributors.""" 

194 ... 

195 

196 async def get_all_navigation(self) -> Sequence[NavigationContribution]: 

197 """Collect navigation from all contributors.""" 

198 ... 

199 

200 async def get_framework_health(self) -> dict[str, object]: 

201 """Aggregate health from all contributors.""" 

202 ... 

203 

204 async def execute_action( 

205 self, 

206 contributor_id: str, 

207 action_name: str, 

208 params: dict[str, object], 

209 user_permissions: frozenset[str], 

210 ) -> object: 

211 """Execute a framework-level action from a contributor. 

212 

213 Args: 

214 contributor_id: Identifier of the target contributor. 

215 action_name: Name of the action to execute. 

216 params: Parameters forwarded to the action handler. 

217 user_permissions: Permissions held by the requesting user. 

218 

219 Returns: 

220 Whatever the action handler returns. 

221 """ 

222 ... 

223 

224 

225__all__ = [ 

226 "AdminContributorProtocol", 

227 "AdminContributorRegistryProtocol", 

228 "AdminDashboardProtocol", 

229]