fastgen-cli 0.4.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fastgen_cli-0.4.0/.gitignore +6 -0
- fastgen_cli-0.4.0/LICENSE +21 -0
- fastgen_cli-0.4.0/PKG-INFO +86 -0
- fastgen_cli-0.4.0/README.md +64 -0
- fastgen_cli-0.4.0/pyproject.toml +38 -0
- fastgen_cli-0.4.0/src/fastgen/__init__.py +1 -0
- fastgen_cli-0.4.0/src/fastgen/cli.py +80 -0
- fastgen_cli-0.4.0/src/fastgen/generators/__init__.py +0 -0
- fastgen_cli-0.4.0/src/fastgen/generators/base.py +45 -0
- fastgen_cli-0.4.0/src/fastgen/generators/core.py +43 -0
- fastgen_cli-0.4.0/src/fastgen/generators/module.py +40 -0
- fastgen_cli-0.4.0/src/fastgen/generators/registry.py +91 -0
- fastgen_cli-0.4.0/src/fastgen/naming.py +41 -0
- fastgen_cli-0.4.0/src/fastgen/templates/core/config.py.j2 +12 -0
- fastgen_cli-0.4.0/src/fastgen/templates/core/database.py.j2 +24 -0
- fastgen_cli-0.4.0/src/fastgen/templates/module/__init__.py.j2 +4 -0
- fastgen_cli-0.4.0/src/fastgen/templates/module/router.py.j2 +10 -0
- fastgen_cli-0.4.0/src/fastgen/templates/module/schemas.py.j2 +5 -0
- fastgen_cli-0.4.0/src/fastgen/templates/module/service.py.j2 +2 -0
- fastgen_cli-0.4.0/src/fastgen/writers.py +43 -0
- fastgen_cli-0.4.0/uv.lock +191 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 YIbaikaishui
|
|
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.
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: fastgen-cli
|
|
3
|
+
Version: 0.4.0
|
|
4
|
+
Summary: FastAPI feature-based module manager (nest-cli style for FastAPI)
|
|
5
|
+
Author-email: YIbaikaishui <162815827+YIbaikaishui@users.noreply.github.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Keywords: cli,code-generator,fastapi,nest-cli,scaffold
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
|
+
Classifier: Environment :: Console
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
15
|
+
Classifier: Topic :: Software Development :: Code Generators
|
|
16
|
+
Classifier: Typing :: Typed
|
|
17
|
+
Requires-Python: >=3.11
|
|
18
|
+
Requires-Dist: jinja2>=3.1
|
|
19
|
+
Requires-Dist: rich>=13.7
|
|
20
|
+
Requires-Dist: typer>=0.12
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
|
|
23
|
+
# fastgen
|
|
24
|
+
|
|
25
|
+
A `nest cli`-style module manager for FastAPI: it scaffolds a minimal,
|
|
26
|
+
zero-config module skeleton (entity, service, router, shared session
|
|
27
|
+
dependency) and keeps a module registry so AI agents and developers can see
|
|
28
|
+
the project structure at a glance. Fill in the business logic yourself.
|
|
29
|
+
|
|
30
|
+
## Install
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pip install fastgen-cli
|
|
34
|
+
# or
|
|
35
|
+
uv add fastgen-cli
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Usage
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
fastgen --help
|
|
42
|
+
|
|
43
|
+
# Scaffold a feature module and register it
|
|
44
|
+
fastgen make module user
|
|
45
|
+
|
|
46
|
+
# List registered modules and their boundaries
|
|
47
|
+
fastgen list
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Both commands support `--dir <root>` (project root, defaults to cwd),
|
|
51
|
+
`--dry-run` (preview without writing) and `--force` (overwrite existing
|
|
52
|
+
skeleton files).
|
|
53
|
+
|
|
54
|
+
Running `fastgen make module <feature>` generates:
|
|
55
|
+
|
|
56
|
+
```
|
|
57
|
+
app/
|
|
58
|
+
├── core/ # auto-created on first use
|
|
59
|
+
│ ├── config.py # pydantic-settings Settings (DATABASE_URL in .env)
|
|
60
|
+
│ └── database.py # Base (AsyncAttrs), async engine, get_session dependency
|
|
61
|
+
└── modules/
|
|
62
|
+
├── __init__.py # module registry (auto-maintained, do not edit)
|
|
63
|
+
└── user/
|
|
64
|
+
├── __init__.py # re-exports router
|
|
65
|
+
├── schemas.py # Pydantic entity skeleton: class User(BaseModel): pass
|
|
66
|
+
├── service.py # business layer boundary: class UserService
|
|
67
|
+
└── router.py # APIRouter + SessionDep (Annotated[AsyncSession, Depends(get_session)])
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Existing code is never overwritten: `app/core/config.py` / `database.py` are
|
|
71
|
+
only generated when missing or empty.
|
|
72
|
+
|
|
73
|
+
## Conventions (fixed)
|
|
74
|
+
|
|
75
|
+
- Modules live in `app/modules/<feature>/`, one business unit per folder.
|
|
76
|
+
- The router exposes `prefix="/<plural>"` and tags; the shared session
|
|
77
|
+
dependency is imported from `app.core.database`.
|
|
78
|
+
- `app/modules/__init__.py` is a fastgen-maintained registry
|
|
79
|
+
(`modules: dict[str, str]`) mapping module name to import path; `fastgen list`
|
|
80
|
+
reads it (and each module's docstring) to show the whole project.
|
|
81
|
+
|
|
82
|
+
## Roadmap
|
|
83
|
+
|
|
84
|
+
- Phase 1: `make module` (schemas + service skeleton) ✅
|
|
85
|
+
- Phase 2: module registry + `fastgen list` ✅
|
|
86
|
+
- Phase 3: `make resource` (full CRUD router), Alembic migration hints
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# fastgen
|
|
2
|
+
|
|
3
|
+
A `nest cli`-style module manager for FastAPI: it scaffolds a minimal,
|
|
4
|
+
zero-config module skeleton (entity, service, router, shared session
|
|
5
|
+
dependency) and keeps a module registry so AI agents and developers can see
|
|
6
|
+
the project structure at a glance. Fill in the business logic yourself.
|
|
7
|
+
|
|
8
|
+
## Install
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
pip install fastgen-cli
|
|
12
|
+
# or
|
|
13
|
+
uv add fastgen-cli
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Usage
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
fastgen --help
|
|
20
|
+
|
|
21
|
+
# Scaffold a feature module and register it
|
|
22
|
+
fastgen make module user
|
|
23
|
+
|
|
24
|
+
# List registered modules and their boundaries
|
|
25
|
+
fastgen list
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Both commands support `--dir <root>` (project root, defaults to cwd),
|
|
29
|
+
`--dry-run` (preview without writing) and `--force` (overwrite existing
|
|
30
|
+
skeleton files).
|
|
31
|
+
|
|
32
|
+
Running `fastgen make module <feature>` generates:
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
app/
|
|
36
|
+
├── core/ # auto-created on first use
|
|
37
|
+
│ ├── config.py # pydantic-settings Settings (DATABASE_URL in .env)
|
|
38
|
+
│ └── database.py # Base (AsyncAttrs), async engine, get_session dependency
|
|
39
|
+
└── modules/
|
|
40
|
+
├── __init__.py # module registry (auto-maintained, do not edit)
|
|
41
|
+
└── user/
|
|
42
|
+
├── __init__.py # re-exports router
|
|
43
|
+
├── schemas.py # Pydantic entity skeleton: class User(BaseModel): pass
|
|
44
|
+
├── service.py # business layer boundary: class UserService
|
|
45
|
+
└── router.py # APIRouter + SessionDep (Annotated[AsyncSession, Depends(get_session)])
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Existing code is never overwritten: `app/core/config.py` / `database.py` are
|
|
49
|
+
only generated when missing or empty.
|
|
50
|
+
|
|
51
|
+
## Conventions (fixed)
|
|
52
|
+
|
|
53
|
+
- Modules live in `app/modules/<feature>/`, one business unit per folder.
|
|
54
|
+
- The router exposes `prefix="/<plural>"` and tags; the shared session
|
|
55
|
+
dependency is imported from `app.core.database`.
|
|
56
|
+
- `app/modules/__init__.py` is a fastgen-maintained registry
|
|
57
|
+
(`modules: dict[str, str]`) mapping module name to import path; `fastgen list`
|
|
58
|
+
reads it (and each module's docstring) to show the whole project.
|
|
59
|
+
|
|
60
|
+
## Roadmap
|
|
61
|
+
|
|
62
|
+
- Phase 1: `make module` (schemas + service skeleton) ✅
|
|
63
|
+
- Phase 2: module registry + `fastgen list` ✅
|
|
64
|
+
- Phase 3: `make resource` (full CRUD router), Alembic migration hints
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "fastgen-cli"
|
|
3
|
+
version = "0.4.0"
|
|
4
|
+
description = "FastAPI feature-based module manager (nest-cli style for FastAPI)"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.11"
|
|
7
|
+
license = { text = "MIT" }
|
|
8
|
+
authors = [{ name = "YIbaikaishui", email = "162815827+YIbaikaishui@users.noreply.github.com" }]
|
|
9
|
+
keywords = ["fastapi", "code-generator", "scaffold", "cli", "nest-cli"]
|
|
10
|
+
classifiers = [
|
|
11
|
+
"Development Status :: 4 - Beta",
|
|
12
|
+
"Environment :: Console",
|
|
13
|
+
"Programming Language :: Python :: 3",
|
|
14
|
+
"Programming Language :: Python :: 3.11",
|
|
15
|
+
"Programming Language :: Python :: 3.12",
|
|
16
|
+
"Programming Language :: Python :: 3.13",
|
|
17
|
+
"Topic :: Software Development :: Code Generators",
|
|
18
|
+
"Typing :: Typed",
|
|
19
|
+
]
|
|
20
|
+
dependencies = [
|
|
21
|
+
"jinja2>=3.1",
|
|
22
|
+
"rich>=13.7",
|
|
23
|
+
"typer>=0.12",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
[project.scripts]
|
|
27
|
+
fastgen = "fastgen.cli:app"
|
|
28
|
+
|
|
29
|
+
[build-system]
|
|
30
|
+
requires = ["hatchling"]
|
|
31
|
+
build-backend = "hatchling.build"
|
|
32
|
+
|
|
33
|
+
[tool.hatch.build.targets.wheel]
|
|
34
|
+
packages = ["src/fastgen"]
|
|
35
|
+
|
|
36
|
+
[tool.ruff]
|
|
37
|
+
line-length = 100
|
|
38
|
+
target-version = "py311"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.4.0"
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""fastgen CLI entrypoint."""
|
|
2
|
+
|
|
3
|
+
# ruff: noqa: B008 (Typer's idiomatic Option/Argument-in-default pattern)
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
import typer
|
|
10
|
+
from rich.table import Table
|
|
11
|
+
|
|
12
|
+
from . import __version__
|
|
13
|
+
from .generators.core import generate_core
|
|
14
|
+
from .generators.module import generate_module
|
|
15
|
+
from .generators.registry import list_registered, register_module
|
|
16
|
+
from .writers import console, report
|
|
17
|
+
|
|
18
|
+
app = typer.Typer(
|
|
19
|
+
name="fastgen",
|
|
20
|
+
help="FastAPI feature-based module manager (nest-cli style).",
|
|
21
|
+
no_args_is_help=True,
|
|
22
|
+
)
|
|
23
|
+
make_app = typer.Typer(help="Scaffold modules and core infrastructure.", no_args_is_help=True)
|
|
24
|
+
app.add_typer(make_app, name="make")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@app.callback(invoke_without_command=True)
|
|
28
|
+
def main(
|
|
29
|
+
version: bool = typer.Option(False, "--version", "-V", help="Show version and exit."),
|
|
30
|
+
) -> None:
|
|
31
|
+
if version:
|
|
32
|
+
typer.echo(f"fastgen {__version__}")
|
|
33
|
+
raise typer.Exit()
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@make_app.command("module")
|
|
37
|
+
def make_module(
|
|
38
|
+
feature: str = typer.Argument(..., help="Feature name, e.g. user"),
|
|
39
|
+
directory: Path = typer.Option(Path.cwd(), "--dir", "-d", help="Target project root."),
|
|
40
|
+
dry_run: bool = typer.Option(False, "--dry-run", help="Preview files without writing."),
|
|
41
|
+
force: bool = typer.Option(False, "--force", "-f", help="Overwrite existing files."),
|
|
42
|
+
) -> None:
|
|
43
|
+
"""Scaffold a feature module and register it.
|
|
44
|
+
|
|
45
|
+
Generates a minimal module skeleton (schemas / service / router /
|
|
46
|
+
__init__) that outlines the module's shape; fill in the entity fields and
|
|
47
|
+
business logic yourself. The shared app/core/ database scaffolding and the
|
|
48
|
+
module registry are created automatically.
|
|
49
|
+
"""
|
|
50
|
+
files = generate_module(feature, directory, force=force, dry_run=dry_run)
|
|
51
|
+
files += generate_core(directory, dry_run=dry_run)
|
|
52
|
+
files.append(register_module(directory, feature, dry_run=dry_run))
|
|
53
|
+
report(files)
|
|
54
|
+
skipped = [f for f in files if f.status == "skipped"]
|
|
55
|
+
if skipped and not force:
|
|
56
|
+
typer.secho(
|
|
57
|
+
f"{len(skipped)} file(s) already exist. Re-run with --force to overwrite.",
|
|
58
|
+
fg=typer.colors.YELLOW,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@app.command("list")
|
|
63
|
+
def list_modules(
|
|
64
|
+
directory: Path = typer.Option(Path.cwd(), "--dir", "-d", help="Target project root."),
|
|
65
|
+
) -> None:
|
|
66
|
+
"""List registered modules and their boundaries."""
|
|
67
|
+
rows = list_registered(directory)
|
|
68
|
+
table = Table(title="Registered modules")
|
|
69
|
+
table.add_column("module", style="cyan")
|
|
70
|
+
table.add_column("path", style="magenta")
|
|
71
|
+
table.add_column("description")
|
|
72
|
+
for name, import_path, doc in rows:
|
|
73
|
+
table.add_row(name, import_path, doc)
|
|
74
|
+
console.print(table)
|
|
75
|
+
if not rows:
|
|
76
|
+
typer.secho("No modules registered yet. Run `fastgen make module <name>`.", fg="yellow")
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
if __name__ == "__main__":
|
|
80
|
+
app()
|
|
File without changes
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""Jinja2 rendering helpers shared by all generators."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from jinja2 import Environment, FileSystemLoader, StrictUndefined
|
|
8
|
+
|
|
9
|
+
from ..writers import GeneratedFile, write_file
|
|
10
|
+
|
|
11
|
+
TEMPLATES_DIR = Path(__file__).resolve().parent.parent / "templates"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _env(template_dir: str) -> Environment:
|
|
15
|
+
return Environment(
|
|
16
|
+
loader=FileSystemLoader(TEMPLATES_DIR / template_dir),
|
|
17
|
+
trim_blocks=True,
|
|
18
|
+
lstrip_blocks=True,
|
|
19
|
+
undefined=StrictUndefined,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def render_template(template_dir: str, name: str, context: dict) -> str:
|
|
24
|
+
"""Render a single ``*.j2`` file without writing anything."""
|
|
25
|
+
return _env(template_dir).get_template(name).render(**context)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def render_tree(
|
|
29
|
+
template_dir: str,
|
|
30
|
+
context: dict,
|
|
31
|
+
dest_dir: Path,
|
|
32
|
+
*,
|
|
33
|
+
force: bool = False,
|
|
34
|
+
dry_run: bool = False,
|
|
35
|
+
) -> list[GeneratedFile]:
|
|
36
|
+
"""Render every ``*.j2`` file in ``template_dir`` into ``dest_dir``."""
|
|
37
|
+
src = TEMPLATES_DIR / template_dir
|
|
38
|
+
env = _env(template_dir)
|
|
39
|
+
files: list[GeneratedFile] = []
|
|
40
|
+
for tmpl in sorted(src.rglob("*.j2")):
|
|
41
|
+
rel = tmpl.relative_to(src)
|
|
42
|
+
rendered = env.get_template(str(rel)).render(**context)
|
|
43
|
+
target = dest_dir / rel.with_suffix("")
|
|
44
|
+
files.append(write_file(target, rendered, force=force, dry_run=dry_run))
|
|
45
|
+
return files
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Core scaffolding generator: app/core/ (config.py + database.py).
|
|
2
|
+
|
|
3
|
+
Files are only generated when they are missing or empty; existing code is
|
|
4
|
+
never overwritten (``force`` does not apply here).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from ..writers import GeneratedFile, write_file
|
|
12
|
+
from .base import render_template
|
|
13
|
+
|
|
14
|
+
CORE_TEMPLATE = "core"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _has_code(path: Path) -> bool:
|
|
18
|
+
return path.exists() and bool(path.read_text(encoding="utf-8").strip())
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def generate_core(
|
|
22
|
+
project_root: Path,
|
|
23
|
+
*,
|
|
24
|
+
dry_run: bool = False,
|
|
25
|
+
) -> list[GeneratedFile]:
|
|
26
|
+
core_dir = project_root / "app" / "core"
|
|
27
|
+
files: list[GeneratedFile] = []
|
|
28
|
+
|
|
29
|
+
init_py = core_dir / "__init__.py"
|
|
30
|
+
if not init_py.exists():
|
|
31
|
+
files.append(write_file(init_py, "", dry_run=dry_run))
|
|
32
|
+
|
|
33
|
+
config_py = core_dir / "config.py"
|
|
34
|
+
if not _has_code(config_py):
|
|
35
|
+
content = render_template(CORE_TEMPLATE, "config.py.j2", {})
|
|
36
|
+
files.append(write_file(config_py, content, force=True, dry_run=dry_run))
|
|
37
|
+
|
|
38
|
+
database_py = core_dir / "database.py"
|
|
39
|
+
if not _has_code(database_py):
|
|
40
|
+
content = render_template(CORE_TEMPLATE, "database.py.j2", {})
|
|
41
|
+
files.append(write_file(database_py, content, force=True, dry_run=dry_run))
|
|
42
|
+
|
|
43
|
+
return files
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""Feature-module generator: schemas.py + service.py + router.py + __init__.py.
|
|
2
|
+
|
|
3
|
+
The skeleton only sketches the module's shape (entity, business layer, API
|
|
4
|
+
boundary, shared session dependency) so AI agents can reason about it; real
|
|
5
|
+
business code is filled in by the developer.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
from ..naming import to_kebab, to_pascal, to_plural, to_snake
|
|
13
|
+
from ..writers import GeneratedFile
|
|
14
|
+
from .base import render_tree
|
|
15
|
+
|
|
16
|
+
MODULE_TEMPLATE = "module"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def module_context(feature: str) -> dict[str, str]:
|
|
20
|
+
snake = to_snake(feature)
|
|
21
|
+
return {
|
|
22
|
+
"pascal": to_pascal(feature),
|
|
23
|
+
"snake": snake,
|
|
24
|
+
"kebab": to_kebab(feature),
|
|
25
|
+
"plural": to_plural(snake),
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def generate_module(
|
|
30
|
+
feature: str,
|
|
31
|
+
project_root: Path,
|
|
32
|
+
*,
|
|
33
|
+
force: bool = False,
|
|
34
|
+
dry_run: bool = False,
|
|
35
|
+
) -> list[GeneratedFile]:
|
|
36
|
+
snake = to_snake(feature)
|
|
37
|
+
module_dir = project_root / "app" / "modules" / snake
|
|
38
|
+
return render_tree(
|
|
39
|
+
MODULE_TEMPLATE, module_context(feature), module_dir, force=force, dry_run=dry_run
|
|
40
|
+
)
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""Module registry: an auto-maintained ``app/modules/__init__.py``.
|
|
2
|
+
|
|
3
|
+
The registry maps each module name to its import path so AI agents (and the
|
|
4
|
+
``fastgen list`` command) can discover the project's module structure at a
|
|
5
|
+
glance without scanning the filesystem.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import ast
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
from ..naming import to_snake
|
|
14
|
+
from ..writers import GeneratedFile, write_file
|
|
15
|
+
|
|
16
|
+
REGISTRY_HEADER = (
|
|
17
|
+
'"""Module registry - auto-generated by fastgen. Do not edit by hand."""'
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
_MODULES_VAR = "modules"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def registry_path(project_root: Path) -> Path:
|
|
24
|
+
return project_root / "app" / "modules" / "__init__.py"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def read_registry(project_root: Path) -> dict[str, str]:
|
|
28
|
+
"""Read the ``modules`` mapping from ``app/modules/__init__.py``."""
|
|
29
|
+
path = registry_path(project_root)
|
|
30
|
+
if not path.exists():
|
|
31
|
+
return {}
|
|
32
|
+
tree = ast.parse(path.read_text(encoding="utf-8"))
|
|
33
|
+
for node in tree.body:
|
|
34
|
+
if isinstance(node, ast.Assign):
|
|
35
|
+
targets = node.targets
|
|
36
|
+
elif isinstance(node, ast.AnnAssign):
|
|
37
|
+
targets = (node.target,) if node.value is not None else ()
|
|
38
|
+
else:
|
|
39
|
+
continue
|
|
40
|
+
if any(isinstance(t, ast.Name) and t.id == _MODULES_VAR for t in targets):
|
|
41
|
+
value = ast.literal_eval(node.value)
|
|
42
|
+
return {name: path for name, path in value.items()}
|
|
43
|
+
return {}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def registry_content(modules: dict[str, str]) -> str:
|
|
47
|
+
body = "\n".join(f' "{name}": "{path}",' for name, path in sorted(modules.items()))
|
|
48
|
+
return (
|
|
49
|
+
f"{REGISTRY_HEADER}\n\n"
|
|
50
|
+
f"{_MODULES_VAR}: dict[str, str] = {{\n{body}\n}}\n\n"
|
|
51
|
+
'__all__ = ["' + _MODULES_VAR + '"]\n'
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def register_module(
|
|
56
|
+
project_root: Path,
|
|
57
|
+
feature: str,
|
|
58
|
+
*,
|
|
59
|
+
dry_run: bool = False,
|
|
60
|
+
) -> GeneratedFile:
|
|
61
|
+
"""Add ``feature`` to the registry and rewrite ``app/modules/__init__.py``."""
|
|
62
|
+
name = to_snake(feature)
|
|
63
|
+
modules = read_registry(project_root)
|
|
64
|
+
modules[name] = f"app.modules.{name}"
|
|
65
|
+
content = registry_content(modules)
|
|
66
|
+
path = registry_path(project_root)
|
|
67
|
+
if path.exists() and path.read_text(encoding="utf-8").rstrip("\n") == content.rstrip("\n"):
|
|
68
|
+
return GeneratedFile(path=path, content=content, status="skipped")
|
|
69
|
+
return write_file(path, content, force=True, dry_run=dry_run)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def list_registered(project_root: Path) -> list[tuple[str, str, str]]:
|
|
73
|
+
"""Return (name, import path, one-line docstring) for every module."""
|
|
74
|
+
modules = read_registry(project_root)
|
|
75
|
+
rows: list[tuple[str, str, str]] = []
|
|
76
|
+
for name, import_path in modules.items():
|
|
77
|
+
doc = _module_doc(project_root / "app" / "modules" / name / "__init__.py")
|
|
78
|
+
rows.append((name, import_path, doc))
|
|
79
|
+
return rows
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _module_doc(init_file: Path) -> str:
|
|
83
|
+
if not init_file.exists():
|
|
84
|
+
return ""
|
|
85
|
+
try:
|
|
86
|
+
tree = ast.parse(init_file.read_text(encoding="utf-8"))
|
|
87
|
+
except SyntaxError:
|
|
88
|
+
return ""
|
|
89
|
+
doc = ast.get_docstring(tree) or ""
|
|
90
|
+
first_line = doc.strip().splitlines()
|
|
91
|
+
return first_line[0] if first_line else ""
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""Name transformation helpers (snake_case / PascalCase / kebab-case + pluralization)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
|
|
7
|
+
_WORD_SPLIT_RE = re.compile(r"[_\-\s]+")
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _split(name: str) -> list[str]:
|
|
11
|
+
parts: list[str] = []
|
|
12
|
+
for chunk in _WORD_SPLIT_RE.split(name):
|
|
13
|
+
if not chunk:
|
|
14
|
+
continue
|
|
15
|
+
parts.extend(re.findall(r"[A-Z]+(?=[A-Z][a-z])|[A-Z]?[a-z]+|[A-Z]+|\d+", chunk))
|
|
16
|
+
return parts
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def to_snake(name: str) -> str:
|
|
20
|
+
return "_".join(p.lower() for p in _split(name))
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def to_pascal(name: str) -> str:
|
|
24
|
+
return "".join(p.capitalize() for p in _split(name))
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def to_kebab(name: str) -> str:
|
|
28
|
+
return "-".join(p.lower() for p in _split(name))
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def to_plural(word: str) -> str:
|
|
32
|
+
"""Simple English pluralization."""
|
|
33
|
+
if word.endswith(("s", "x", "z", "ch", "sh")):
|
|
34
|
+
return f"{word}es"
|
|
35
|
+
if word.endswith("y") and len(word) > 1 and word[-2] not in "aeiou":
|
|
36
|
+
return f"{word[:-1]}ies"
|
|
37
|
+
if word.endswith("f"):
|
|
38
|
+
return f"{word[:-1]}ves"
|
|
39
|
+
if word.endswith("fe"):
|
|
40
|
+
return f"{word[:-2]}ves"
|
|
41
|
+
return f"{word}s"
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class Settings(BaseSettings):
|
|
5
|
+
"""Application settings, loaded from environment / .env."""
|
|
6
|
+
|
|
7
|
+
model_config = SettingsConfigDict(env_file=".env", extra="ignore")
|
|
8
|
+
|
|
9
|
+
database_url: str = "sqlite+aiosqlite:///./app.db"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
settings = Settings()
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from collections.abc import AsyncGenerator
|
|
2
|
+
|
|
3
|
+
from sqlalchemy.ext.asyncio import (
|
|
4
|
+
AsyncAttrs,
|
|
5
|
+
AsyncSession,
|
|
6
|
+
async_sessionmaker,
|
|
7
|
+
create_async_engine,
|
|
8
|
+
)
|
|
9
|
+
from sqlalchemy.orm import DeclarativeBase
|
|
10
|
+
|
|
11
|
+
from app.core.config import settings
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Base(AsyncAttrs, DeclarativeBase):
|
|
15
|
+
"""Declarative base shared by all ORM models."""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
engine = create_async_engine(settings.database_url, echo=False)
|
|
19
|
+
async_session = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
async def get_session() -> AsyncGenerator[AsyncSession, None]:
|
|
23
|
+
async with async_session() as session:
|
|
24
|
+
yield session
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
from typing import Annotated
|
|
2
|
+
|
|
3
|
+
from fastapi import APIRouter, Depends
|
|
4
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
5
|
+
|
|
6
|
+
from app.core.database import get_session
|
|
7
|
+
|
|
8
|
+
SessionDep = Annotated[AsyncSession, Depends(get_session)]
|
|
9
|
+
|
|
10
|
+
router = APIRouter(prefix="/{{ plural }}", tags=["{{ plural }}"])
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""File writing helpers with dry-run and overwrite protection."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
|
|
10
|
+
console = Console()
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class GeneratedFile:
|
|
15
|
+
path: Path
|
|
16
|
+
content: str
|
|
17
|
+
status: str = field(default="created")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def write_file(
|
|
21
|
+
path: Path, content: str, *, force: bool = False, dry_run: bool = False
|
|
22
|
+
) -> GeneratedFile:
|
|
23
|
+
if dry_run:
|
|
24
|
+
return GeneratedFile(path=path, content=content, status="dry-run")
|
|
25
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
26
|
+
if path.exists() and not force:
|
|
27
|
+
return GeneratedFile(path=path, content=content, status="skipped")
|
|
28
|
+
path.write_text(content if content.endswith("\n") else content + "\n")
|
|
29
|
+
return GeneratedFile(path=path, content=content, status="created")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def report(files: list[GeneratedFile], *, dry_run: bool = False) -> None:
|
|
33
|
+
prefix = "[yellow][dry-run][/yellow] " if dry_run else ""
|
|
34
|
+
for f in files:
|
|
35
|
+
status_style = {
|
|
36
|
+
"created": "green",
|
|
37
|
+
"skipped": "yellow",
|
|
38
|
+
"dry-run": "cyan",
|
|
39
|
+
}[f.status]
|
|
40
|
+
if f.status == "skipped":
|
|
41
|
+
console.print(f"{prefix}[{status_style}][skipped][/] {f.path}")
|
|
42
|
+
else:
|
|
43
|
+
console.print(f"{prefix}[{status_style}][{f.status}][/] {f.path}")
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
version = 1
|
|
2
|
+
revision = 3
|
|
3
|
+
requires-python = ">=3.11"
|
|
4
|
+
|
|
5
|
+
[[package]]
|
|
6
|
+
name = "annotated-doc"
|
|
7
|
+
version = "0.0.5"
|
|
8
|
+
source = { registry = "https://pypi.org/simple" }
|
|
9
|
+
sdist = { url = "https://files.pythonhosted.org/packages/5a/8e/38aa427ed5402449e226975b649c5dc73ccadfefeb95e6aecb8f8ea4b6b6/annotated_doc-0.0.5.tar.gz", hash = "sha256:c7e58ce09192557605d8bbd92836d7e1d520ac9580096042c0bfd197efacf1bb", size = 10758, upload-time = "2026-07-28T13:50:58.129Z" }
|
|
10
|
+
wheels = [
|
|
11
|
+
{ url = "https://files.pythonhosted.org/packages/3e/30/e900b21425a860e195f32e37657aa1f7c7f2b1bfb26f03ca209b90933c06/annotated_doc-0.0.5-py3-none-any.whl", hash = "sha256:117bac03a25ede5df5440e855b32d556049ca169ead221505badf432fed4b101", size = 5302, upload-time = "2026-07-28T13:50:57.239Z" },
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
[[package]]
|
|
15
|
+
name = "colorama"
|
|
16
|
+
version = "0.4.6"
|
|
17
|
+
source = { registry = "https://pypi.org/simple" }
|
|
18
|
+
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
|
|
19
|
+
wheels = [
|
|
20
|
+
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
[[package]]
|
|
24
|
+
name = "fastgen-cli"
|
|
25
|
+
version = "0.4.0"
|
|
26
|
+
source = { editable = "." }
|
|
27
|
+
dependencies = [
|
|
28
|
+
{ name = "jinja2" },
|
|
29
|
+
{ name = "rich" },
|
|
30
|
+
{ name = "typer" },
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
[package.metadata]
|
|
34
|
+
requires-dist = [
|
|
35
|
+
{ name = "jinja2", specifier = ">=3.1" },
|
|
36
|
+
{ name = "rich", specifier = ">=13.7" },
|
|
37
|
+
{ name = "typer", specifier = ">=0.12" },
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
[[package]]
|
|
41
|
+
name = "jinja2"
|
|
42
|
+
version = "3.1.6"
|
|
43
|
+
source = { registry = "https://pypi.org/simple" }
|
|
44
|
+
dependencies = [
|
|
45
|
+
{ name = "markupsafe" },
|
|
46
|
+
]
|
|
47
|
+
sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" }
|
|
48
|
+
wheels = [
|
|
49
|
+
{ url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" },
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
[[package]]
|
|
53
|
+
name = "markdown-it-py"
|
|
54
|
+
version = "4.2.0"
|
|
55
|
+
source = { registry = "https://pypi.org/simple" }
|
|
56
|
+
dependencies = [
|
|
57
|
+
{ name = "mdurl" },
|
|
58
|
+
]
|
|
59
|
+
sdist = { url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", size = 82454, upload-time = "2026-05-07T12:08:28.36Z" }
|
|
60
|
+
wheels = [
|
|
61
|
+
{ url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687, upload-time = "2026-05-07T12:08:27.182Z" },
|
|
62
|
+
]
|
|
63
|
+
|
|
64
|
+
[[package]]
|
|
65
|
+
name = "markupsafe"
|
|
66
|
+
version = "3.0.3"
|
|
67
|
+
source = { registry = "https://pypi.org/simple" }
|
|
68
|
+
sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" }
|
|
69
|
+
wheels = [
|
|
70
|
+
{ url = "https://files.pythonhosted.org/packages/08/db/fefacb2136439fc8dd20e797950e749aa1f4997ed584c62cfb8ef7c2be0e/markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", size = 11631, upload-time = "2025-09-27T18:36:18.185Z" },
|
|
71
|
+
{ url = "https://files.pythonhosted.org/packages/e1/2e/5898933336b61975ce9dc04decbc0a7f2fee78c30353c5efba7f2d6ff27a/markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", size = 12058, upload-time = "2025-09-27T18:36:19.444Z" },
|
|
72
|
+
{ url = "https://files.pythonhosted.org/packages/1d/09/adf2df3699d87d1d8184038df46a9c80d78c0148492323f4693df54e17bb/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", size = 24287, upload-time = "2025-09-27T18:36:20.768Z" },
|
|
73
|
+
{ url = "https://files.pythonhosted.org/packages/30/ac/0273f6fcb5f42e314c6d8cd99effae6a5354604d461b8d392b5ec9530a54/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf", size = 22940, upload-time = "2025-09-27T18:36:22.249Z" },
|
|
74
|
+
{ url = "https://files.pythonhosted.org/packages/19/ae/31c1be199ef767124c042c6c3e904da327a2f7f0cd63a0337e1eca2967a8/markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f", size = 21887, upload-time = "2025-09-27T18:36:23.535Z" },
|
|
75
|
+
{ url = "https://files.pythonhosted.org/packages/b2/76/7edcab99d5349a4532a459e1fe64f0b0467a3365056ae550d3bcf3f79e1e/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a", size = 23692, upload-time = "2025-09-27T18:36:24.823Z" },
|
|
76
|
+
{ url = "https://files.pythonhosted.org/packages/a4/28/6e74cdd26d7514849143d69f0bf2399f929c37dc2b31e6829fd2045b2765/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115", size = 21471, upload-time = "2025-09-27T18:36:25.95Z" },
|
|
77
|
+
{ url = "https://files.pythonhosted.org/packages/62/7e/a145f36a5c2945673e590850a6f8014318d5577ed7e5920a4b3448e0865d/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a", size = 22923, upload-time = "2025-09-27T18:36:27.109Z" },
|
|
78
|
+
{ url = "https://files.pythonhosted.org/packages/0f/62/d9c46a7f5c9adbeeeda52f5b8d802e1094e9717705a645efc71b0913a0a8/markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19", size = 14572, upload-time = "2025-09-27T18:36:28.045Z" },
|
|
79
|
+
{ url = "https://files.pythonhosted.org/packages/83/8a/4414c03d3f891739326e1783338e48fb49781cc915b2e0ee052aa490d586/markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01", size = 15077, upload-time = "2025-09-27T18:36:29.025Z" },
|
|
80
|
+
{ url = "https://files.pythonhosted.org/packages/35/73/893072b42e6862f319b5207adc9ae06070f095b358655f077f69a35601f0/markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c", size = 13876, upload-time = "2025-09-27T18:36:29.954Z" },
|
|
81
|
+
{ url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" },
|
|
82
|
+
{ url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" },
|
|
83
|
+
{ url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" },
|
|
84
|
+
{ url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" },
|
|
85
|
+
{ url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" },
|
|
86
|
+
{ url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" },
|
|
87
|
+
{ url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" },
|
|
88
|
+
{ url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" },
|
|
89
|
+
{ url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" },
|
|
90
|
+
{ url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" },
|
|
91
|
+
{ url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" },
|
|
92
|
+
{ url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" },
|
|
93
|
+
{ url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" },
|
|
94
|
+
{ url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" },
|
|
95
|
+
{ url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" },
|
|
96
|
+
{ url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" },
|
|
97
|
+
{ url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" },
|
|
98
|
+
{ url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" },
|
|
99
|
+
{ url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" },
|
|
100
|
+
{ url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" },
|
|
101
|
+
{ url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" },
|
|
102
|
+
{ url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" },
|
|
103
|
+
{ url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" },
|
|
104
|
+
{ url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" },
|
|
105
|
+
{ url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" },
|
|
106
|
+
{ url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" },
|
|
107
|
+
{ url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" },
|
|
108
|
+
{ url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" },
|
|
109
|
+
{ url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" },
|
|
110
|
+
{ url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" },
|
|
111
|
+
{ url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" },
|
|
112
|
+
{ url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" },
|
|
113
|
+
{ url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" },
|
|
114
|
+
{ url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" },
|
|
115
|
+
{ url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" },
|
|
116
|
+
{ url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" },
|
|
117
|
+
{ url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" },
|
|
118
|
+
{ url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" },
|
|
119
|
+
{ url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" },
|
|
120
|
+
{ url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" },
|
|
121
|
+
{ url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" },
|
|
122
|
+
{ url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" },
|
|
123
|
+
{ url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" },
|
|
124
|
+
{ url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" },
|
|
125
|
+
{ url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" },
|
|
126
|
+
{ url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" },
|
|
127
|
+
{ url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" },
|
|
128
|
+
{ url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" },
|
|
129
|
+
{ url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" },
|
|
130
|
+
{ url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" },
|
|
131
|
+
{ url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" },
|
|
132
|
+
{ url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" },
|
|
133
|
+
{ url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" },
|
|
134
|
+
{ url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" },
|
|
135
|
+
{ url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" },
|
|
136
|
+
]
|
|
137
|
+
|
|
138
|
+
[[package]]
|
|
139
|
+
name = "mdurl"
|
|
140
|
+
version = "0.1.2"
|
|
141
|
+
source = { registry = "https://pypi.org/simple" }
|
|
142
|
+
sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" }
|
|
143
|
+
wheels = [
|
|
144
|
+
{ url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" },
|
|
145
|
+
]
|
|
146
|
+
|
|
147
|
+
[[package]]
|
|
148
|
+
name = "pygments"
|
|
149
|
+
version = "2.20.0"
|
|
150
|
+
source = { registry = "https://pypi.org/simple" }
|
|
151
|
+
sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" }
|
|
152
|
+
wheels = [
|
|
153
|
+
{ url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" },
|
|
154
|
+
]
|
|
155
|
+
|
|
156
|
+
[[package]]
|
|
157
|
+
name = "rich"
|
|
158
|
+
version = "15.0.0"
|
|
159
|
+
source = { registry = "https://pypi.org/simple" }
|
|
160
|
+
dependencies = [
|
|
161
|
+
{ name = "markdown-it-py" },
|
|
162
|
+
{ name = "pygments" },
|
|
163
|
+
]
|
|
164
|
+
sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" }
|
|
165
|
+
wheels = [
|
|
166
|
+
{ url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" },
|
|
167
|
+
]
|
|
168
|
+
|
|
169
|
+
[[package]]
|
|
170
|
+
name = "shellingham"
|
|
171
|
+
version = "1.5.4"
|
|
172
|
+
source = { registry = "https://pypi.org/simple" }
|
|
173
|
+
sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" }
|
|
174
|
+
wheels = [
|
|
175
|
+
{ url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" },
|
|
176
|
+
]
|
|
177
|
+
|
|
178
|
+
[[package]]
|
|
179
|
+
name = "typer"
|
|
180
|
+
version = "0.27.1"
|
|
181
|
+
source = { registry = "https://pypi.org/simple" }
|
|
182
|
+
dependencies = [
|
|
183
|
+
{ name = "annotated-doc" },
|
|
184
|
+
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
|
185
|
+
{ name = "rich" },
|
|
186
|
+
{ name = "shellingham" },
|
|
187
|
+
]
|
|
188
|
+
sdist = { url = "https://files.pythonhosted.org/packages/ae/40/4a3db7990d1f62a53182aa96eaef57aeb2886a27f90a195bc66713565d31/typer-0.27.1.tar.gz", hash = "sha256:a79bef8469a79c45498e7b814ecf8d603cc7644e9acbd9e19cac0334240b18df", size = 203994, upload-time = "2026-08-03T14:41:03.438Z" }
|
|
189
|
+
wheels = [
|
|
190
|
+
{ url = "https://files.pythonhosted.org/packages/43/89/9518bc0c3929bee36b3a4a8e3daddd6e03f92f9961c66d4983b837160543/typer-0.27.1-py3-none-any.whl", hash = "sha256:53150287edd11baeb4e4722c8e394fcdf8181c0ae89485cba8d25c778d5edd56", size = 122874, upload-time = "2026-08-03T14:41:04.391Z" },
|
|
191
|
+
]
|