Coverage for fastblocks/cli.py: 0%

144 statements  

« prev     ^ index     » next       coverage.py v7.10.6, created at 2025-09-21 04:50 -0700

1import asyncio 

2import logging 

3import os 

4import signal 

5import typing as t 

6from contextlib import suppress 

7from enum import Enum 

8from importlib.metadata import version as get_version 

9from pathlib import Path 

10from subprocess import DEVNULL 

11from subprocess import run as execute 

12from typing import Annotated 

13 

14with suppress(ImportError): 

15 from acb.logger import InterceptHandler 

16 

17 for logger_name in ("uvicorn", "uvicorn.access", "uvicorn.error"): 

18 logger = logging.getLogger(logger_name) 

19 logger.handlers.clear() 

20 logger.addHandler(InterceptHandler()) 

21 logger.setLevel(logging.DEBUG) 

22 logger.propagate = False 

23 

24import nest_asyncio 

25import typer 

26import uvicorn 

27from acb.actions.encode import dump, load 

28from acb.console import console 

29from anyio import Path as AsyncPath 

30from granian import Granian 

31 

32nest_asyncio.apply() 

33__all__ = ("cli", "components", "create", "dev", "run") 

34default_adapters = { 

35 "routes": "default", 

36 "templates": "jinja2", 

37 "auth": "basic", 

38 "sitemap": "asgi", 

39} 

40fastblocks_path = Path(__file__).parent 

41apps_path = Path.cwd() 

42if Path.cwd() == fastblocks_path: 

43 msg = "FastBlocks can not be run in the same directory as FastBlocks itself. Run `python -m fastblocks create`. Move into the app directory and try again." 

44 raise SystemExit( 

45 msg, 

46 ) 

47cli = typer.Typer(rich_markup_mode="rich") 

48 

49 

50class Styles(str, Enum): 

51 bulma = "bulma" 

52 webawesome = "webawesome" 

53 custom = "custom" 

54 

55 def __str__(self) -> str: 

56 return self.value 

57 

58 

59run_args = {"app": "main:app"} 

60dev_args = run_args | {"port": 8000, "reload": True} 

61granian_dev_args = dev_args | { 

62 "address": "127.0.0.1", 

63 "reload_paths": [Path.cwd(), fastblocks_path], 

64 "interface": "asgi", 

65 "log_enabled": False, 

66 "log_access": True, 

67 "reload_ignore_dirs": ["tmp", "settings", "templates"], 

68} 

69uvicorn_dev_args = dev_args | { 

70 "host": "127.0.0.1", 

71 "reload_includes": ["*.py", str(Path.cwd()), str(fastblocks_path)], 

72 "reload_excludes": ["tmp/*", "settings/*", "templates/*"], 

73 "lifespan": "on", 

74} 

75 

76 

77def setup_signal_handlers() -> None: 

78 import sys 

79 

80 def signal_handler(_signum: int, _frame: t.Any) -> None: 

81 sys.exit(0) 

82 

83 signal.signal(signal.SIGINT, signal_handler) 

84 signal.signal(signal.SIGTERM, signal_handler) 

85 

86 

87@cli.command() 

88def run(docker: bool = False, granian: bool = False, host: str = "127.0.0.1") -> None: 

89 if docker: 

90 execute( 

91 f"docker run -it -ePORT=8080 -p8080:8080 {Path.cwd().stem}".split(), 

92 ) 

93 else: 

94 setup_signal_handlers() 

95 if granian: 

96 from granian.constants import Interfaces 

97 

98 Granian("main:app", address=host, interface=Interfaces.ASGI).serve() 

99 else: 

100 uvicorn.run(app=run_args["app"], host=host, lifespan="on", log_config=None) 

101 

102 

103@cli.command() 

104def dev(granian: bool = False) -> None: 

105 setup_signal_handlers() 

106 if granian: 

107 from granian.constants import Interfaces 

108 

109 Granian( 

110 "main:app", 

111 address="127.0.0.1", 

112 port=8000, 

113 reload=True, 

114 reload_paths=[Path.cwd(), fastblocks_path], 

115 interface=Interfaces.ASGI, 

116 log_enabled=False, 

117 log_access=True, 

118 ).serve() 

119 else: 

120 uvicorn.run( 

121 app="main:app", 

122 host="127.0.0.1", 

123 port=8000, 

124 reload=True, 

125 reload_includes=["*.py", str(Path.cwd()), str(fastblocks_path)], 

126 reload_excludes=["tmp/*", "settings/*", "templates/*"], 

127 lifespan="on", 

128 log_config=None, 

129 ) 

130 

131 

132def _display_adapters() -> None: 

133 from acb.adapters import get_adapters 

134 

135 console.print("[bold green]Available Adapters:[/bold green]") 

136 adapters = get_adapters() 

137 if not adapters: 

138 console.print(" [dim]No adapters found[/dim]") 

139 return 

140 categories = {} 

141 for adapter in adapters: 

142 if adapter.category not in categories: 

143 categories[adapter.category] = [] 

144 categories[adapter.category].append(adapter) 

145 for category in sorted(categories.keys()): 

146 console.print(f"\n [bold cyan]{category.upper()}:[/bold cyan]") 

147 for adapter in sorted(categories[category], key=lambda a: a.name): 

148 _display_adapter_info(adapter) 

149 

150 

151def _display_adapter_info(adapter: t.Any) -> None: 

152 status = "[green]✓[/green]" if adapter.installed else "[red]✗[/red]" 

153 enabled = "[yellow]enabled[/yellow]" if adapter.enabled else "[dim]disabled[/dim]" 

154 console.print(f" {status} [white]{adapter.name}[/white] - {enabled}") 

155 if adapter.module: 

156 console.print(f" [dim]{adapter.module}[/dim]") 

157 

158 

159def _display_default_config() -> None: 

160 console.print("\n[bold green]FastBlocks Default Configuration:[/bold green]") 

161 for category, default_name in default_adapters.items(): 

162 console.print(f" [cyan]{category}[/cyan]: [white]{default_name}[/white]") 

163 

164 

165def _display_actions() -> None: 

166 console.print("\n[bold green]FastBlocks Actions:[/bold green]") 

167 try: 

168 from fastblocks.actions.minify import minify 

169 

170 console.print(" [cyan]minify[/cyan]:") 

171 console.print(f" [white]- css[/white] ([dim]{minify.css.__name__}[/dim])") 

172 console.print(f" [white]- js[/white] ([dim]{minify.js.__name__}[/dim])") 

173 except ImportError: 

174 console.print(" [dim]Minify actions not available[/dim]") 

175 

176 

177@cli.command() 

178def components() -> None: 

179 try: 

180 console.print("\n[bold blue]FastBlocks Components[/bold blue]\n") 

181 _display_adapters() 

182 _display_default_config() 

183 _display_actions() 

184 except Exception as e: 

185 console.print(f"[red]Error displaying components: {e}[/red]") 

186 console.print("[dim]Make sure you're in a FastBlocks project directory[/dim]") 

187 

188 

189@cli.command() 

190def create( 

191 app_name: Annotated[ 

192 str, 

193 typer.Option(prompt=True, help="Name of your application"), 

194 ], 

195 style: Annotated[ 

196 Styles, 

197 typer.Option( 

198 prompt=True, 

199 help="The style (css, or web component, framework) you want to use[{','.join(Styles._member_names_)}]", 

200 ), 

201 ] = Styles.bulma, 

202 domain: Annotated[ 

203 str, 

204 typer.Option(prompt=True, help="Application domain"), 

205 ] = "example.com", 

206) -> None: 

207 app_path = apps_path / app_name 

208 app_path.mkdir(exist_ok=True) 

209 os.chdir(app_path) 

210 templates = Path("templates") 

211 for p in ( 

212 templates / "base/blocks", 

213 templates / f"{style}/blocks", 

214 templates / f"{style}/theme", 

215 Path("adapters"), 

216 Path("actions"), 

217 ): 

218 p.mkdir(parents=True, exist_ok=True) 

219 for p in ( 

220 "models.py", 

221 "routes.py", 

222 "main.py", 

223 ".envrc", 

224 "pyproject.toml", 

225 "__init__.py", 

226 "adapters/__init__.py", 

227 "actions/__init__.py", 

228 ): 

229 Path(p).touch() 

230 for p in ("main.py.tmpl", ".envrc", "pyproject.toml.tmpl", "Procfile.tmpl"): 

231 Path(p.replace(".tmpl", "")).write_text( 

232 (fastblocks_path / p).read_text().replace("APP_NAME", app_name), 

233 ) 

234 commands = ( 

235 ["direnv", "allow", "."], 

236 ["pdm", "install"], 

237 ["python", "-m", "fastblocks", "run"], 

238 ) 

239 for command in commands: 

240 execute(command, stdout=DEVNULL, stderr=DEVNULL) 

241 

242 async def update_settings(settings: str, values: dict[str, t.Any]) -> None: 

243 settings_path = AsyncPath(app_path / "settings") 

244 settings_dict = await load.yaml(settings_path / f"{settings}.yml") 

245 settings_dict.update(values) 

246 await dump.yaml(settings_dict, settings_path / f"{settings}.yml") 

247 

248 async def update_configs() -> None: 

249 await update_settings("debug", {"fastblocks": False}) 

250 await update_settings("adapters", default_adapters) 

251 await update_settings( 

252 "app", {"title": "Welcome to FastBlocks", "domain": domain} 

253 ) 

254 

255 asyncio.run(update_configs()) 

256 console.print( 

257 f"\n[bold][white]Project is initialized. Please configure [green]'adapters.yml'[/] and [green]'app.yml'[/] in the [blue]'{app_name}/settings'[/] directory before running [magenta]`python -m fastblocks dev`[/] or [magenta]`python -m fastblocks run`[/] from the [blue]'{app_name}'[/] directory.[/][/]", 

258 ) 

259 console.print( 

260 "\n[dim]Use [white]`python -m fastblocks components`[/white] to see available adapters and actions.[/dim]", 

261 ) 

262 raise SystemExit 

263 

264 

265@cli.command() 

266def version() -> None: 

267 try: 

268 __version__ = get_version("fastblocks") 

269 console.print(f"FastBlocks v{__version__}") 

270 except Exception: 

271 console.print("Unable to determine FastBlocks version")