Coverage for fastblocks/cli.py: 0%
144 statements
« prev ^ index » next coverage.py v7.10.6, created at 2025-09-21 04:50 -0700
« 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
14with suppress(ImportError):
15 from acb.logger import InterceptHandler
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
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
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")
50class Styles(str, Enum):
51 bulma = "bulma"
52 webawesome = "webawesome"
53 custom = "custom"
55 def __str__(self) -> str:
56 return self.value
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}
77def setup_signal_handlers() -> None:
78 import sys
80 def signal_handler(_signum: int, _frame: t.Any) -> None:
81 sys.exit(0)
83 signal.signal(signal.SIGINT, signal_handler)
84 signal.signal(signal.SIGTERM, signal_handler)
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
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)
103@cli.command()
104def dev(granian: bool = False) -> None:
105 setup_signal_handlers()
106 if granian:
107 from granian.constants import Interfaces
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 )
132def _display_adapters() -> None:
133 from acb.adapters import get_adapters
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)
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]")
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]")
165def _display_actions() -> None:
166 console.print("\n[bold green]FastBlocks Actions:[/bold green]")
167 try:
168 from fastblocks.actions.minify import minify
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]")
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]")
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)
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")
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 )
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
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")