├── .python-version
├── src
└── fasthtml_cli
│ ├── __init__.py
│ ├── toml.py
│ ├── utils.py
│ ├── cli.py
│ ├── template_builder.py
│ └── gallery_fetcher.py
├── .gitignore
├── cli.png
├── TODO.md
├── CLAUDE.md
├── pyproject.toml
├── LICENSE
├── CHANGELOG.md
├── README.md
└── uv.lock
/.python-version:
--------------------------------------------------------------------------------
1 | 3.12
2 |
--------------------------------------------------------------------------------
/src/fasthtml_cli/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .venv
2 | .DS_Store
3 | __pycache__
4 |
--------------------------------------------------------------------------------
/cli.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ExploringML/fasthtml-cli/HEAD/cli.png
--------------------------------------------------------------------------------
/TODO.md:
--------------------------------------------------------------------------------
1 | - Add an importer similar to --gallery flag feature but for any URL? Maybe show a warning when using this flag about untrusted sources. Do some simple checks to make sure it is a Fasthtml app and no malicious code.
2 | - Delete the legacy fh-init CLI.
3 | - Be able to paste in full URL of gallery url too?
--------------------------------------------------------------------------------
/src/fasthtml_cli/toml.py:
--------------------------------------------------------------------------------
1 | def config(name: str = "", dependencies: list = None):
2 | """Generate pyproject.toml content with the given dependencies."""
3 | if dependencies is None:
4 | dependencies = ["python-fasthtml"]
5 |
6 | # Format dependencies for TOML
7 | deps_str = ', '.join([f'"{dep}"' for dep in dependencies])
8 |
9 | return f"""[project]
10 | name = "{name}"
11 | version = "0.1.0"
12 | description = "Add your description here"
13 | readme = "README.md"
14 | requires-python = ">=3.12"
15 | dependencies = [{deps_str}]"""
--------------------------------------------------------------------------------
/CLAUDE.md:
--------------------------------------------------------------------------------
1 | # FastHTML CLI
2 |
3 | See the 'Development' section of README.md for details on setting up the development version of the CLI for local testing.
4 |
5 | # Update MD Files
6 |
7 | As you make changes ALWAYS keep the README.md and CHANGELOG.md files up to date. This is very important to keep users informed of updates. Make sure you only update the changelog section relating to the current version specified in pyproject.toml.
8 |
9 | # Publish to PyPi
10 |
11 | Here are the instructions to push new CLI versions to PyPi.
12 |
13 | Delete the dist folder and run:
14 |
15 | ```
16 | uv build
17 | ```
18 |
19 | Then make sure the PyPi token has been set with:
20 |
21 | ```
22 | export UV_PUBLISH_TOKEN=pypi-AgEIc...
23 | ```
24 |
25 | Publish to PyPi with:
26 |
27 | ```
28 | uv publish
29 | ```
30 |
31 | # Resources
32 |
33 | - https://youtu.be/qh98qOND6MI
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [project]
2 | name = "fh-cli"
3 | version = "0.1.6"
4 | description = "Quickly scaffold FastHTML apps!"
5 | readme = "README.md"
6 | authors = [
7 | { name = "David Gwyer", email = "d.v.gwyer@gmail.com" }
8 | ]
9 | requires-python = ">=3.12"
10 | dependencies = [
11 | "typer>=0.17.0,<1.0.0",
12 | "requests>=2.32.0,<3.0.0",
13 | ]
14 |
15 | [project.urls]
16 | Homepage = "https://github.com/ExploringML/fasthtml-cli"
17 | Repository = "https://github.com/ExploringML/fasthtml-cli"
18 | "Bug Tracker" = "https://github.com/ExploringML/fasthtml-cli/issues"
19 | Documentation = "https://github.com/ExploringML/fasthtml-cli#readme"
20 |
21 | [project.scripts]
22 | fh-cli = "fasthtml_cli.cli:app"
23 | fh-init = "fasthtml_cli.cli:app"
24 | fasthtml-init = "fasthtml_cli.cli:app"
25 |
26 | [tool.hatch.build.targets.wheel]
27 | packages = ["src/fasthtml_cli"]
28 |
29 | [build-system]
30 | requires = ["hatchling"]
31 | build-backend = "hatchling.build"
32 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2025 ExploringML
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/fasthtml_cli/utils.py:
--------------------------------------------------------------------------------
1 | from fasthtml_cli.template_builder import TemplateBuilder
2 | from fasthtml_cli.gallery_fetcher import GalleryFetcher
3 | from fasthtml_cli import toml
4 |
5 | def create_main_py(name: str, template: str, tailwind: bool, reload: bool, pico: bool, deps: str = "", gallery: str = ""):
6 | """Create the main.py file using the flexible template builder or gallery example."""
7 |
8 | # Gallery mode: ignore all other template options
9 | if gallery:
10 | fetcher = GalleryFetcher()
11 | try:
12 | example = fetcher.fetch_example(gallery)
13 | return example['code']
14 | except Exception as e:
15 | print(f"Warning: Failed to fetch gallery example '{gallery}': {e}")
16 | print("Falling back to basic template...")
17 | # Fall through to regular template mode
18 |
19 | # Regular template mode
20 | builder = TemplateBuilder(name)
21 |
22 | # Add features based on options
23 | if tailwind:
24 | builder.add_tailwind()
25 | elif pico: # pico is default unless tailwind is used
26 | builder.add_pico()
27 |
28 | if reload:
29 | builder.add_live_reload()
30 |
31 | # Add custom dependencies
32 | if deps:
33 | custom_deps = [dep.strip() for dep in deps.split() if dep.strip()]
34 | builder.add_dependencies(custom_deps)
35 |
36 | return builder.build_main_py()
37 |
38 | def create_pyproject_toml(name: str, deps: str = "", gallery: str = ""):
39 | """Create the pyproject.toml file with selected config options."""
40 |
41 | # Gallery mode: use gallery dependencies
42 | if gallery:
43 | fetcher = GalleryFetcher()
44 | try:
45 | example = fetcher.fetch_example(gallery)
46 | dependencies = example['dependencies']
47 | return toml.config(name, dependencies)
48 | except Exception as e:
49 | print(f"Warning: Failed to fetch gallery dependencies for '{gallery}': {e}")
50 | print("Using basic FastHTML dependencies...")
51 | # Fall through to regular mode
52 |
53 | # Regular mode: use template builder
54 | builder = TemplateBuilder(name)
55 | if deps:
56 | custom_deps = [dep.strip() for dep in deps.split() if dep.strip()]
57 | builder.add_dependencies(custom_deps)
58 |
59 | dependencies = builder.build_pyproject_toml()
60 | return toml.config(name, dependencies)
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | All notable changes to this project will be documented in this file.
4 |
5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7 |
8 | ## [0.1.6] - 2025-09-13
9 |
10 | ### Changed
11 | - **BREAKING**: Package name changed from `fh-init` to `fh-cli` for consistency
12 | - Primary command is now `fh-cli` instead of `fh-init`
13 | - Backward compatibility: `fh-init` command still available during transition period
14 |
15 | ### Migration Guide
16 | - **Old:** `uvx fh-init my-app`
17 | - **New:** `uvx fh-cli my-app`
18 | - Both commands work during transition period
19 | - Update your scripts and documentation to use `fh-cli` going forward
20 |
21 | ## [0.1.5] - 2025-09-13
22 |
23 | ### Added
24 | - Added `--version` flag to display current version
25 | - Version is now automatically read from package metadata to stay in sync with `pyproject.toml`
26 | - Added `--deps` option to specify additional Python dependencies (space-separated)
27 | - Dependencies are now automatically imported in generated main.py files with proper aliases (e.g., `pandas as pd`, `numpy as np`)
28 | - **NEW**: Added `--gallery` option to bootstrap projects from the official [FastHTML Gallery](https://gallery.fastht.ml/)
29 | - Pull complete working examples with `fh-init my-app --gallery category/example`
30 | - Automatic dependency detection from gallery code
31 | - When `--gallery` is used, all other template options are ignored (by design)
32 |
33 | ### Fixed
34 | - Fixed bug where `Path` was not imported, causing import errors
35 | - Fixed duplicate parameter warning for `-p` flag by changing `--template` shorthand to `-tp`
36 | - Fixed bug where `--deps` dependencies were only added to pyproject.toml but not imported in main.py
37 | - Fixed single-item tuple syntax for headers (now properly includes trailing comma)
38 |
39 | ### Security
40 | - Added input sanitization to prevent path traversal attacks in project names
41 | - Enhanced error message sanitization to prevent information disclosure
42 | - Added version constraints to dependencies to improve supply chain security
43 | - Implemented comprehensive security audit with 0 vulnerabilities found
44 |
45 | ### Changed
46 | - **BREAKING**: Replaced rigid template system with flexible component-based TemplateBuilder
47 | - Improved version detection system to work reliably with different installation methods
48 | - Updated Tailwind CSS CDN link to new `@tailwindcss/browser@4` URL
49 | - Restructured codebase: moved toml.py and removed fasthtml_templates folder
--------------------------------------------------------------------------------
/src/fasthtml_cli/cli.py:
--------------------------------------------------------------------------------
1 | from pathlib import Path
2 | import re
3 | import typer
4 | from typing_extensions import Annotated
5 | from fasthtml_cli.utils import create_main_py, create_pyproject_toml
6 |
7 | def sanitize_project_name(name: str) -> str:
8 | """Sanitize project name to prevent path traversal and ensure valid directory names."""
9 | # Allow only alphanumeric, hyphens, underscores, and dots
10 | # Remove any path separators or parent directory references
11 | sanitized = re.sub(r'[^a-zA-Z0-9._-]', '', name)
12 |
13 | # Remove leading dots to prevent hidden files
14 | sanitized = sanitized.lstrip('.')
15 |
16 | # Ensure name is not empty after sanitization
17 | if not sanitized:
18 | return "fasthtml-app"
19 |
20 | return sanitized
21 |
22 | def get_version():
23 | """Get version from package metadata"""
24 | try:
25 | from importlib.metadata import version
26 | return version("fh-cli")
27 | except Exception:
28 | return "Error: Version not found"
29 |
30 | app = typer.Typer()
31 |
32 | def version_callback(value: bool):
33 | if value:
34 | print(f"fh-cli version {get_version()}")
35 | raise typer.Exit()
36 |
37 | @app.command()
38 | def main(
39 | name: Annotated[str, typer.Argument(help="FastHTML app name.")],
40 | template: Annotated[str, typer.Option("--template", "-tp", help="The name of the FastHTML template to use.")] = "base",
41 | reload: Annotated[bool, typer.Option("--reload", "-r", help="Enable live reload.")] = False,
42 | pico: Annotated[bool, typer.Option("--pico", "-p", help="Enable Pico CSS.")] = True,
43 | uv: Annotated[bool, typer.Option(help="Use uv to manage project dependencies.")] = True,
44 | tailwind: Annotated[bool, typer.Option("--tailwind", "-t", help="Enable Tailwind CSS.")] = False,
45 | deps: Annotated[str, typer.Option("--deps", "-d", help="Space-separated list of Python dependencies to add (e.g., 'pandas numpy requests').")] = "",
46 | gallery: Annotated[str, typer.Option("--gallery", "-g", help="Use a FastHTML Gallery example (e.g., 'todo_series/beginner'). When used, other template options are ignored.")] = "",
47 | version: Annotated[bool, typer.Option("--version", callback=version_callback, help="Show version and exit.")] = False,
48 | ):
49 | """
50 | Scaffold a new FastHTML application.
51 | """
52 |
53 | # Sanitize project name to prevent path traversal
54 | sanitized_name = sanitize_project_name(name)
55 | if sanitized_name != name:
56 | print(f"Error: Invalid project name. Only alphanumeric characters, hyphens, and underscores are allowed.")
57 | return
58 |
59 | # Create the project path.
60 | path = Path(sanitized_name)
61 |
62 | # Check if directory exists.
63 | if path.exists():
64 | print(f"Error: Directory '{sanitized_name}' already exists")
65 | return
66 |
67 | try:
68 | # Create directory
69 | path.mkdir(parents=True)
70 |
71 | # Create main.py
72 | main_file = path/'main.py'
73 | if main_file.exists():
74 | print(f"Error: {main_file} already exists, skipping")
75 | else:
76 | main_file.write_text(create_main_py(name, template, tailwind, reload, pico, deps, gallery))
77 |
78 | # Create pyproject.toml if uv is enabled.
79 | if uv:
80 | pyproject_file = path/'pyproject.toml'
81 | if pyproject_file.exists():
82 | print(f"Error: {pyproject_file} already exists, skipping")
83 | else:
84 | pyproject_file.write_text(create_pyproject_toml(name, deps, gallery))
85 | except PermissionError:
86 | print(f"Error: Permission denied creating {name}")
87 | except OSError as e:
88 | print(f"Error creating project: {e}")
89 | else:
90 | print(f"\n✨ New FastHTML app created successfully!")
91 |
92 | print("\nTo get started, enter:\n")
93 | print(f" $ cd {name}")
94 |
95 | if uv:
96 | print(" $ uv run main.py\n")
97 | else:
98 | print(" $ python main.py\n")
99 |
100 |
101 | if __name__ == "__main__":
102 | app()
--------------------------------------------------------------------------------
/src/fasthtml_cli/template_builder.py:
--------------------------------------------------------------------------------
1 | class TemplateBuilder:
2 | """A flexible component-based template builder for FastHTML apps."""
3 |
4 | def __init__(self, name: str):
5 | self.name = name
6 | self.imports = ["from fasthtml.common import *"]
7 | self.headers = []
8 | self.app_options = []
9 | self.dependencies = ["python-fasthtml"]
10 |
11 | def add_tailwind(self):
12 | """Add Tailwind CSS support."""
13 | self.headers.append('Script(src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4")')
14 | # Tailwind disables Pico CSS
15 | self.app_options = [opt for opt in self.app_options if not opt.startswith("pico=")]
16 | self.app_options.append("pico=False")
17 | return self
18 |
19 | def add_live_reload(self):
20 | """Add live reload support."""
21 | if "live=True" not in self.app_options:
22 | self.app_options.append("live=True")
23 | return self
24 |
25 | def add_pico(self):
26 | """Add Pico CSS support (default unless Tailwind is used)."""
27 | # Only add if no pico option exists and no Tailwind
28 | has_pico = any(opt.startswith("pico=") for opt in self.app_options)
29 | has_tailwind = any("tailwindcss" in header for header in self.headers)
30 |
31 | if not has_pico and not has_tailwind:
32 | self.app_options.append("pico=True")
33 | return self
34 |
35 | def add_dependencies(self, deps: list):
36 | """Add custom Python dependencies."""
37 | for dep in deps:
38 | if dep not in self.dependencies:
39 | self.dependencies.append(dep)
40 | # Add import statement for common packages
41 | import_name = self._get_import_name(dep)
42 | if import_name:
43 | import_statement = f"import {import_name}"
44 | if import_statement not in self.imports:
45 | self.imports.append(import_statement)
46 | return self
47 |
48 | def _get_import_name(self, dep: str):
49 | """Get the import name for common packages."""
50 | # Handle common package name mappings
51 | common_mappings = {
52 | 'pandas': 'pandas as pd',
53 | 'numpy': 'numpy as np',
54 | 'requests': 'requests',
55 | 'matplotlib': 'matplotlib.pyplot as plt',
56 | 'seaborn': 'seaborn as sns',
57 | 'scikit-learn': 'sklearn',
58 | 'pillow': 'PIL',
59 | 'beautifulsoup4': 'bs4',
60 | }
61 |
62 | # Extract base package name (remove version specifiers)
63 | base_dep = dep.split('>=')[0].split('==')[0].split('<')[0].strip()
64 |
65 | return common_mappings.get(base_dep, base_dep)
66 |
67 | def build_main_py(self):
68 | """Generate the main.py file content."""
69 | # Build imports
70 | imports = "\n".join(self.imports)
71 |
72 | # Build headers
73 | headers_section = ""
74 | if self.headers:
75 | headers_list = ", ".join(self.headers)
76 | # Ensure proper tuple syntax with trailing comma for single items
77 | if len(self.headers) == 1:
78 | headers_section = f"hdrs = ({headers_list},)\n"
79 | else:
80 | headers_section = f"hdrs = ({headers_list})\n"
81 |
82 | # Build app configuration
83 | app_config_parts = []
84 | if self.headers:
85 | app_config_parts.append("hdrs=hdrs")
86 | app_config_parts.extend(self.app_options)
87 | app_config = ", ".join(app_config_parts)
88 |
89 | # Default route
90 | default_route = '''@rt('/')
91 | def get():
92 | return Div(
93 | H1("Hello, FastHTML!"),
94 | P("Your app is running!")
95 | )'''
96 |
97 | # Assemble the complete template
98 | template = f"""{imports}
99 |
100 | {headers_section}app, rt = fast_app({app_config})
101 |
102 | {default_route}
103 |
104 | serve()"""
105 |
106 | return template
107 |
108 | def build_pyproject_toml(self):
109 | """Generate the pyproject.toml dependencies list."""
110 | return self.dependencies
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # FastHTML CLI
2 |
3 | [](https://pypi.org/project/fh-cli/)
4 | [](https://github.com/your-username/your-repo/blob/main/LICENSE)
5 | [](https://github.com/sponsors/ExploringML)
6 |
7 | Fastest way to scaffold FastHTML apps!
8 |
9 | 
10 |
11 | > **📦 Package Name Change:** This package was previously named `fh-init` but has been renamed to `fh-cli` for consistency. Both commands (`fh-cli` and `fh-init`) work during the transition period. We recommend using `fh-cli` going forward.
12 |
13 | ## Usage
14 |
15 | To create a new FastHTML application, use the `fh-cli` command. Make sure [`uv`](https://docs.astral.sh/uv/getting-started/installation/) is installed before running `uvx`:
16 |
17 | ```bash
18 | uvx fh-cli [OPTIONS] NAME
19 | ```
20 |
21 | ### Arguments
22 |
23 | * `NAME`: The name of your FastHTML application (required).
24 |
25 | ### Options
26 |
27 | * `--template, -tp TEXT`: The name of the FastHTML template to use (default: `base`).
28 | * `--reload, -r`: Enable live reload.
29 | * `--pico, -p`: Enable Pico CSS (default: `True`).
30 | * `--uv / --no-uv`: Use uv to manage project dependencies (default: `uv`).
31 | * `--tailwind, -t`: Enable Tailwind CSS.
32 | * `--deps, -d TEXT`: Space-separated list of Python dependencies to add (e.g., `pandas numpy requests`).
33 | * `--gallery, -g TEXT`: Use a FastHTML Gallery example (e.g., `todo_series/beginner`). When used, other template options are ignored.
34 | * `--version`: Show version and exit.
35 | * `--install-completion`: Install tab completion for the current shell (run once to enable auto-completion).
36 | * `--show-completion`: Show completion script to copy or customize the installation.
37 | * `--help`: Show the help message and exit.
38 |
39 | ### Examples
40 |
41 | ```bash
42 | # Create a basic app
43 | uvx fh-cli my_awesome_app
44 |
45 | # Create an app with live reload and Tailwind CSS
46 | uvx fh-cli my_awesome_app --reload --tailwind
47 |
48 | # Create an app with additional Python dependencies
49 | uvx fh-cli data_app --deps "pandas numpy matplotlib"
50 |
51 | # Create an app from the FastHTML Gallery
52 | uvx fh-cli my-todo --gallery todo_series/beginner
53 | uvx fh-cli my-viz --gallery visualizations/observable_plot
54 | ```
55 |
56 | Then to run the FastHTML app:
57 |
58 | ```bash
59 | cd my_awesome_app
60 | uv run main.py
61 | ```
62 |
63 | ### Shell Completion
64 |
65 | For a better CLI experience, you can enable tab completion:
66 |
67 | ```bash
68 | # Install completion for your current shell (run once)
69 | fh-cli --install-completion
70 |
71 | # After installation, you can use tab completion
72 | fh-cli # Shows available commands and options
73 | fh-cli -- # Shows all available flags
74 | ```
75 |
76 | ### FastHTML Gallery Integration
77 |
78 | Bootstrap your projects with real-world examples from the official [FastHTML Gallery](https://gallery.fastht.ml/):
79 |
80 | ```bash
81 | # Browse examples at https://gallery.fastht.ml/
82 | # Use the format: category/example_name
83 |
84 | # Todo applications
85 | uvx fh-cli my-todo --gallery todo_series/beginner
86 |
87 | # Data visualizations
88 | uvx fh-cli my-charts --gallery visualizations/observable_plot
89 |
90 | # Interactive applications
91 | uvx fh-cli my-app --gallery applications/csv_editor
92 | ```
93 |
94 | **Gallery Features:**
95 | - **Complete Examples**: Get fully working FastHTML applications instantly
96 | - **Auto Dependencies**: Required packages automatically added to `pyproject.toml`
97 | - **No Modifications**: Gallery code copied exactly as-is for authentic examples
98 | - **Exclusive Mode**: When using `--gallery`, other template options (`--tailwind`, `--deps`, etc.) are ignored
99 |
100 | **Available Categories:**
101 | - `applications/` - Full-featured apps (csv_editor, tic_tac_toe, etc.)
102 | - `todo_series/` - Todo app examples of varying complexity
103 | - `visualizations/` - Data visualization examples
104 | - `widgets/` - Reusable UI components
105 | - `svg/` - SVG and graphics examples
106 | - `dynamic_user_interface_(htmx)/` - Advanced HTMX interactions
107 |
108 | ## Development
109 |
110 | ### Initial Setup
111 |
112 | For first-time setup:
113 |
114 | 1. **Activate the virtual environment:**
115 | ```bash
116 | source .venv/bin/activate
117 | ```
118 |
119 | 2. **Install the CLI locally in editable mode:**
120 | ```bash
121 | uv pip install -e .
122 | ```
123 |
124 | ### Quick Development Workflow
125 |
126 | After making any changes to code or `pyproject.toml`:
127 |
128 | 1. **In the CLI root folder, run:**
129 | ```bash
130 | source .venv/bin/activate && uv pip install -e . --force-reinstall && uv cache clean
131 | ```
132 |
133 | 2. **Use the local development CLI from any other folder:**
134 | ```bash
135 | # Check version
136 | uvx --from fh-cli --version
137 |
138 | # Create a test project
139 | uvx --from fh-cli test-app
140 | ```
141 |
142 | ### Troubleshooting
143 |
144 | - If uvx shows old version after changes, the single update command above should resolve it
145 | - The `--version` flag reads from package metadata to ensure version consistency with `pyproject.toml`
146 |
147 | ### Switching Back to PyPI Version
148 |
149 | After local development, you may want to return to using the official PyPI version:
150 |
151 | 1. **Deactivate the development environment:**
152 | ```bash
153 | deactivate
154 | ```
155 |
156 | 2. **Use the official PyPI version:**
157 | ```bash
158 | # This will automatically use the latest PyPI version
159 | uvx fh-cli --version
160 | uvx fh-cli my-app
161 | ```
162 |
163 | **Notes:**
164 | - `uvx` automatically isolates package environments, so your local development doesn't affect the global PyPI version
165 | - The `--from ` flag in development only affects that specific command
166 | - Once you deactivate the virtual environment, `uvx fh-cli` will use the official PyPI package
167 | - No need to uninstall anything - `uvx` handles package isolation automatically
168 |
--------------------------------------------------------------------------------
/uv.lock:
--------------------------------------------------------------------------------
1 | version = 1
2 | revision = 1
3 | requires-python = ">=3.12"
4 |
5 | [[package]]
6 | name = "click"
7 | version = "8.1.8"
8 | source = { registry = "https://pypi.org/simple" }
9 | dependencies = [
10 | { name = "colorama", marker = "sys_platform == 'win32'" },
11 | ]
12 | sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593 }
13 | wheels = [
14 | { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188 },
15 | ]
16 |
17 | [[package]]
18 | name = "colorama"
19 | version = "0.4.6"
20 | source = { registry = "https://pypi.org/simple" }
21 | sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 }
22 | wheels = [
23 | { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 },
24 | ]
25 |
26 | [[package]]
27 | name = "fh-init"
28 | version = "0.1.1"
29 | source = { editable = "." }
30 | dependencies = [
31 | { name = "typer" },
32 | ]
33 |
34 | [package.metadata]
35 | requires-dist = [{ name = "typer" }]
36 |
37 | [[package]]
38 | name = "markdown-it-py"
39 | version = "3.0.0"
40 | source = { registry = "https://pypi.org/simple" }
41 | dependencies = [
42 | { name = "mdurl" },
43 | ]
44 | sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596 }
45 | wheels = [
46 | { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528 },
47 | ]
48 |
49 | [[package]]
50 | name = "mdurl"
51 | version = "0.1.2"
52 | source = { registry = "https://pypi.org/simple" }
53 | sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729 }
54 | wheels = [
55 | { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979 },
56 | ]
57 |
58 | [[package]]
59 | name = "pygments"
60 | version = "2.19.1"
61 | source = { registry = "https://pypi.org/simple" }
62 | sdist = { url = "https://files.pythonhosted.org/packages/7c/2d/c3338d48ea6cc0feb8446d8e6937e1408088a72a39937982cc6111d17f84/pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", size = 4968581 }
63 | wheels = [
64 | { url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293 },
65 | ]
66 |
67 | [[package]]
68 | name = "rich"
69 | version = "14.0.0"
70 | source = { registry = "https://pypi.org/simple" }
71 | dependencies = [
72 | { name = "markdown-it-py" },
73 | { name = "pygments" },
74 | ]
75 | sdist = { url = "https://files.pythonhosted.org/packages/a1/53/830aa4c3066a8ab0ae9a9955976fb770fe9c6102117c8ec4ab3ea62d89e8/rich-14.0.0.tar.gz", hash = "sha256:82f1bc23a6a21ebca4ae0c45af9bdbc492ed20231dcb63f297d6d1021a9d5725", size = 224078 }
76 | wheels = [
77 | { url = "https://files.pythonhosted.org/packages/0d/9b/63f4c7ebc259242c89b3acafdb37b41d1185c07ff0011164674e9076b491/rich-14.0.0-py3-none-any.whl", hash = "sha256:1c9491e1951aac09caffd42f448ee3d04e58923ffe14993f6e83068dc395d7e0", size = 243229 },
78 | ]
79 |
80 | [[package]]
81 | name = "shellingham"
82 | version = "1.5.4"
83 | source = { registry = "https://pypi.org/simple" }
84 | sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310 }
85 | wheels = [
86 | { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755 },
87 | ]
88 |
89 | [[package]]
90 | name = "typer"
91 | version = "0.15.2"
92 | source = { registry = "https://pypi.org/simple" }
93 | dependencies = [
94 | { name = "click" },
95 | { name = "rich" },
96 | { name = "shellingham" },
97 | { name = "typing-extensions" },
98 | ]
99 | sdist = { url = "https://files.pythonhosted.org/packages/8b/6f/3991f0f1c7fcb2df31aef28e0594d8d54b05393a0e4e34c65e475c2a5d41/typer-0.15.2.tar.gz", hash = "sha256:ab2fab47533a813c49fe1f16b1a370fd5819099c00b119e0633df65f22144ba5", size = 100711 }
100 | wheels = [
101 | { url = "https://files.pythonhosted.org/packages/7f/fc/5b29fea8cee020515ca82cc68e3b8e1e34bb19a3535ad854cac9257b414c/typer-0.15.2-py3-none-any.whl", hash = "sha256:46a499c6107d645a9c13f7ee46c5d5096cae6f5fc57dd11eccbbb9ae3e44ddfc", size = 45061 },
102 | ]
103 |
104 | [[package]]
105 | name = "typing-extensions"
106 | version = "4.13.2"
107 | source = { registry = "https://pypi.org/simple" }
108 | sdist = { url = "https://files.pythonhosted.org/packages/f6/37/23083fcd6e35492953e8d2aaaa68b860eb422b34627b13f2ce3eb6106061/typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef", size = 106967 }
109 | wheels = [
110 | { url = "https://files.pythonhosted.org/packages/8b/54/b1ae86c0973cc6f0210b53d508ca3641fb6d0c56823f288d108bc7ab3cc8/typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c", size = 45806 },
111 | ]
112 |
--------------------------------------------------------------------------------
/src/fasthtml_cli/gallery_fetcher.py:
--------------------------------------------------------------------------------
1 | """
2 | FastHTML Gallery integration for fetching example code from the official repository.
3 | """
4 | import re
5 | import ast
6 | from typing import Dict, List, Optional, Tuple
7 | from pathlib import Path
8 |
9 |
10 | class GalleryFetcher:
11 | """Fetches and analyzes FastHTML examples from the official gallery repository."""
12 |
13 | BASE_URL = "https://raw.githubusercontent.com/AnswerDotAI/FastHTML-Gallery/main/examples"
14 | GITHUB_API_URL = "https://api.github.com/repos/AnswerDotAI/FastHTML-Gallery/contents/examples"
15 |
16 | def __init__(self):
17 | self.session = None # Will be set up when needed
18 |
19 | def validate_slug(self, slug: str) -> bool:
20 | """Validate that a gallery slug has the correct format."""
21 | if not slug or "/" not in slug:
22 | return False
23 |
24 | parts = slug.split("/")
25 | if len(parts) != 2:
26 | return False
27 |
28 | category, example = parts
29 | return bool(category.strip() and example.strip())
30 |
31 | def fetch_example_code(self, slug: str) -> str:
32 | """Fetch the app.py code from a gallery example."""
33 | if not self.validate_slug(slug):
34 | raise ValueError(f"Invalid gallery slug format: '{slug}'. Expected format: 'category/example'")
35 |
36 | url = f"{self.BASE_URL}/{slug}/app.py"
37 |
38 | try:
39 | import requests
40 | response = requests.get(url, timeout=10)
41 | response.raise_for_status()
42 | return response.text
43 | except ImportError:
44 | raise ImportError("requests library is required for gallery functionality")
45 | except requests.RequestException as e:
46 | # Sanitize error message to avoid information disclosure
47 | if "404" in str(e):
48 | raise ConnectionError(f"Gallery example '{slug}' not found")
49 | else:
50 | raise ConnectionError(f"Failed to fetch gallery example '{slug}': Network error")
51 |
52 | def extract_dependencies(self, code: str) -> List[str]:
53 | """Extract import statements and infer dependencies from FastHTML code."""
54 | dependencies = ["python-fasthtml"] # Always include FastHTML
55 |
56 | # Parse imports using AST
57 | try:
58 | tree = ast.parse(code)
59 | for node in ast.walk(tree):
60 | if isinstance(node, ast.Import):
61 | for alias in node.names:
62 | dep = self._map_import_to_dependency(alias.name)
63 | if dep and dep not in dependencies:
64 | dependencies.append(dep)
65 | elif isinstance(node, ast.ImportFrom):
66 | if node.module:
67 | dep = self._map_import_to_dependency(node.module)
68 | if dep and dep not in dependencies:
69 | dependencies.append(dep)
70 | except SyntaxError:
71 | # If code parsing fails, fall back to regex
72 | dependencies.extend(self._extract_dependencies_regex(code))
73 |
74 | return dependencies
75 |
76 | def _extract_dependencies_regex(self, code: str) -> List[str]:
77 | """Fallback dependency extraction using regex patterns."""
78 | dependencies = []
79 |
80 | # Common patterns to look for
81 | patterns = {
82 | r'import\s+(numpy|np)': 'numpy',
83 | r'from\s+numpy': 'numpy',
84 | r'import\s+pandas': 'pandas',
85 | r'from\s+pandas': 'pandas',
86 | r'import\s+requests': 'requests',
87 | r'from\s+requests': 'requests',
88 | r'import\s+httpx': 'httpx',
89 | r'from\s+httpx': 'httpx',
90 | r'fastsql': 'fastsql',
91 | r'apswutils': 'apswutils',
92 | r'matplotlib': 'matplotlib',
93 | r'seaborn': 'seaborn',
94 | }
95 |
96 | for pattern, dep in patterns.items():
97 | if re.search(pattern, code, re.IGNORECASE):
98 | if dep not in dependencies:
99 | dependencies.append(dep)
100 |
101 | return dependencies
102 |
103 | def _map_import_to_dependency(self, import_name: str) -> Optional[str]:
104 | """Map Python import names to pip package names."""
105 | # Common mappings where import name != pip package name
106 | mappings = {
107 | 'cv2': 'opencv-python',
108 | 'PIL': 'pillow',
109 | 'sklearn': 'scikit-learn',
110 | 'yaml': 'pyyaml',
111 | 'bs4': 'beautifulsoup4',
112 | 'flask': 'flask',
113 | 'django': 'django',
114 | 'fastapi': 'fastapi',
115 | 'starlette': 'starlette',
116 | # FastHTML ecosystem
117 | 'fasthtml': 'python-fasthtml',
118 | 'fastsql': 'fastsql',
119 | 'apswutils': 'apswutils',
120 | # Common data science
121 | 'numpy': 'numpy',
122 | 'pandas': 'pandas',
123 | 'matplotlib': 'matplotlib',
124 | 'seaborn': 'seaborn',
125 | 'requests': 'requests',
126 | 'httpx': 'httpx',
127 | }
128 |
129 | # Get base module name (before any dots)
130 | base_import = import_name.split('.')[0]
131 |
132 | # Skip standard library modules
133 | stdlib_modules = {
134 | 'os', 'sys', 'json', 'csv', 'sqlite3', 'uuid', 'datetime',
135 | 'pathlib', 'typing', 'collections', 'itertools', 'functools',
136 | 'asyncio', 're', 'random', 'math', 'time', 'io', 'base64'
137 | }
138 |
139 | if base_import in stdlib_modules:
140 | return None
141 |
142 | return mappings.get(base_import, base_import)
143 |
144 | def fetch_example(self, slug: str) -> Dict[str, any]:
145 | """
146 | Fetch a complete gallery example with code and metadata.
147 |
148 | Returns:
149 | {
150 | 'code': str, # The app.py code
151 | 'dependencies': list, # Detected dependencies
152 | 'slug': str # The original slug
153 | }
154 | """
155 | code = self.fetch_example_code(slug)
156 | dependencies = self.extract_dependencies(code)
157 |
158 | return {
159 | 'code': code,
160 | 'dependencies': dependencies,
161 | 'slug': slug
162 | }
163 |
164 | def list_categories(self) -> List[str]:
165 | """
166 | List available gallery categories (future feature).
167 | For now, returns known categories.
168 | """
169 | return [
170 | "applications",
171 | "dynamic_user_interface_(htmx)",
172 | "svg",
173 | "todo_series",
174 | "visualizations",
175 | "widgets"
176 | ]
--------------------------------------------------------------------------------