Coverage for src/lexigram/admin/builders/builder.py: 76%
117 statements
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-21 14:56 +0800
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-21 14:56 +0800
1from __future__ import annotations
3from collections.abc import Callable, Sequence
4from typing import TYPE_CHECKING, Any, Literal
6from lexigram.admin.config import (
7 AdminConfig,
8 AdminFeaturesConfig,
9 AdminNavigationGroup,
10)
11from lexigram.primitives.builder import AbstractBuilder
13if TYPE_CHECKING:
14 from lexigram.admin.di.bundle_provider import AdminProvider
17class AdminBuilder(AbstractBuilder["AdminProvider"]):
18 """Fluent builder for AdminProvider."""
20 def __init__(self) -> None:
21 super().__init__()
22 self._title: str = "Admin"
23 self._prefix: str = "/admin"
24 self._require_auth: bool = True
25 self._theme: Literal["light", "dark", "system"] = "system"
26 self._debug: bool = False
27 self._features: dict[str, Any] = {}
28 self._extensions: dict[str, dict[str, Any]] = {}
29 self._resources: list[Any] = []
30 self._controllers: list[Any] = []
31 self._navigation_groups: dict[str, AdminNavigationGroup] = {}
32 self._commands: list[Any] = []
33 self._service_factories: dict[type, Callable[..., Any]] = {}
34 self._base_config: AdminConfig | None = None
36 @classmethod
37 def create(cls) -> AdminBuilder:
38 return cls()
40 @classmethod
41 def from_config(cls, config: AdminConfig) -> AdminBuilder:
42 builder = cls()
43 builder._title = config.title
44 builder._prefix = config.prefix
45 builder._require_auth = config.require_auth
46 builder._debug = config.debug
47 builder._theme = config.ui.theme
49 if config.features:
50 builder._features = config.features.model_dump()
52 builder._navigation_groups = dict(config.navigation_groups)
53 builder._commands = list(config.commands)
54 builder._extensions = dict(config.extensions) if config.extensions else {}
55 builder._base_config = config
57 return builder
59 def title(self, title: str) -> AdminBuilder:
60 self._title = title
61 return self
63 def prefix(self, prefix: str) -> AdminBuilder:
64 self._prefix = prefix
65 return self
67 def require_auth(self, require: bool) -> AdminBuilder:
68 self._require_auth = require
69 return self
71 def theme(self, theme: Literal["light", "dark", "system"]) -> AdminBuilder:
72 self._theme = theme
73 return self
75 def debug(self, enabled: bool = True) -> AdminBuilder:
76 self._debug = enabled
77 return self
79 def features(self, features: dict[str, Any]) -> AdminBuilder:
80 self._features.update(features)
81 return self
83 def feature(self, name: str, enabled: bool = True) -> AdminBuilder:
84 self._features[name] = enabled
85 return self
87 def extension(
88 self,
89 name: str,
90 enabled: bool = True,
91 **options: Any,
92 ) -> AdminBuilder:
93 self._extensions[name] = {"enabled": enabled, **options}
94 return self
96 def resources(self, resources: Sequence[Any]) -> AdminBuilder:
97 self._resources.extend(resources)
98 return self
100 def resource(
101 self,
102 resource: Any,
103 *,
104 name: str | None = None,
105 group: str | None = None,
106 ) -> AdminBuilder:
107 self._resources.append((resource, name, group))
108 return self
110 def controllers(self, controllers: Sequence[Any]) -> AdminBuilder:
111 self._controllers.extend(controllers)
112 return self
114 def controller(self, controller: Any) -> AdminBuilder:
115 self._controllers.append(controller)
116 return self
118 def navigation_groups(
119 self,
120 groups: dict[str, AdminNavigationGroup],
121 ) -> AdminBuilder:
122 self._navigation_groups.update(groups)
123 return self
125 def navigation_group(
126 self,
127 name: str,
128 group: AdminNavigationGroup,
129 ) -> AdminBuilder:
130 self._navigation_groups[name] = group
131 return self
133 def commands(self, commands: Sequence[Any]) -> AdminBuilder:
134 self._commands.extend(commands)
135 return self
137 def command(
138 self,
139 label: str,
140 href: str,
141 *,
142 icon: str = "",
143 shortcut: str = "",
144 ) -> AdminBuilder:
145 self._commands.append(
146 {"label": label, "href": href, "icon": icon, "shortcut": shortcut},
147 )
148 return self
150 def service(
151 self,
152 service_type: type,
153 factory: Callable[..., Any],
154 ) -> AdminBuilder:
155 self._service_factories[service_type] = factory
156 return self
158 def build(self) -> AdminProvider:
159 from lexigram.admin.di.bundle_provider import AdminProvider
161 if self._base_config is not None:
162 config = self._base_config.model_copy(deep=True)
163 config.title = self._title
164 config.prefix = self._prefix
165 config.require_auth = self._require_auth
166 config.debug = self._debug
167 else:
168 features_config = AdminFeaturesConfig(**self._features)
169 config_data: dict[str, Any] = {
170 "title": self._title,
171 "prefix": self._prefix,
172 "require_auth": self._require_auth,
173 "debug": self._debug,
174 "features": features_config,
175 "ui": {"theme": self._theme},
176 }
177 if self._extensions:
178 config_data["extensions"] = self._extensions
179 config = AdminConfig.model_validate(config_data)
181 for name, group in self._navigation_groups.items():
182 config.navigation_groups[name] = group
184 for cmd in self._commands:
185 if isinstance(cmd, dict):
186 from lexigram.admin.models.provider_models import Command
188 config.commands.append(
189 Command(
190 label=cmd.get("label", ""),
191 href=cmd.get("href", ""),
192 icon=cmd.get("icon", ""),
193 shortcut=cmd.get("shortcut", ""),
194 )
195 )
196 else:
197 config.commands.append(cmd)
199 resources_list = []
200 for res in self._resources:
201 if isinstance(res, tuple):
202 resource, name, group = res
203 resources_list.append(resource)
204 else:
205 resources_list.append(res)
207 return AdminProvider(
208 config=config,
209 resources=resources_list,
210 controllers=list(self._controllers),
211 )