fastapi-minimal-template 0.1.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.
Files changed (25) hide show
  1. fastapi_minimal_template-0.1.0/.github/workflows/ci.yml +26 -0
  2. fastapi_minimal_template-0.1.0/.github/workflows/release.yml +48 -0
  3. fastapi_minimal_template-0.1.0/.gitignore +9 -0
  4. fastapi_minimal_template-0.1.0/.python-version +1 -0
  5. fastapi_minimal_template-0.1.0/LICENSE +21 -0
  6. fastapi_minimal_template-0.1.0/PKG-INFO +125 -0
  7. fastapi_minimal_template-0.1.0/README.md +104 -0
  8. fastapi_minimal_template-0.1.0/pyproject.toml +65 -0
  9. fastapi_minimal_template-0.1.0/src/fastapi_minimal_template/__init__.py +3 -0
  10. fastapi_minimal_template-0.1.0/src/fastapi_minimal_template/__main__.py +4 -0
  11. fastapi_minimal_template-0.1.0/src/fastapi_minimal_template/cli.py +88 -0
  12. fastapi_minimal_template-0.1.0/src/fastapi_minimal_template/generator.py +200 -0
  13. fastapi_minimal_template-0.1.0/src/fastapi_minimal_template/template/.dockerignore +6 -0
  14. fastapi_minimal_template-0.1.0/src/fastapi_minimal_template/template/.gitignore +5 -0
  15. fastapi_minimal_template-0.1.0/src/fastapi_minimal_template/template/Dockerfile +13 -0
  16. fastapi_minimal_template-0.1.0/src/fastapi_minimal_template/template/README.md.template +45 -0
  17. fastapi_minimal_template-0.1.0/src/fastapi_minimal_template/template/compose.yaml +6 -0
  18. fastapi_minimal_template-0.1.0/src/fastapi_minimal_template/template/pyproject.toml.template +33 -0
  19. fastapi_minimal_template-0.1.0/src/fastapi_minimal_template/template/src/app/__init__.py +1 -0
  20. fastapi_minimal_template-0.1.0/src/fastapi_minimal_template/template/src/app/main.py +16 -0
  21. fastapi_minimal_template-0.1.0/src/fastapi_minimal_template/template/tests/test_main.py +18 -0
  22. fastapi_minimal_template-0.1.0/tests/test_cli.py +94 -0
  23. fastapi_minimal_template-0.1.0/tests/test_generated_project.py +35 -0
  24. fastapi_minimal_template-0.1.0/tests/test_generator.py +105 -0
  25. fastapi_minimal_template-0.1.0/uv.lock +1082 -0
@@ -0,0 +1,26 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ pull_request:
6
+
7
+ jobs:
8
+ test:
9
+ runs-on: ubuntu-latest
10
+ steps:
11
+ - uses: actions/checkout@v4
12
+ - uses: astral-sh/setup-uv@v6
13
+ with:
14
+ enable-cache: true
15
+ - name: Set up Python
16
+ run: uv python install 3.12
17
+ - name: Install dependencies
18
+ run: uv sync --locked
19
+ - name: Lint
20
+ run: uv run ruff check .
21
+ - name: Check formatting
22
+ run: uv run ruff format --check .
23
+ - name: Test
24
+ run: uv run pytest
25
+ - name: Build package
26
+ run: uv build
@@ -0,0 +1,48 @@
1
+ name: Publicar no PyPI
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - "v*"
7
+
8
+ jobs:
9
+ publish:
10
+ name: Publicar pacote
11
+ runs-on: ubuntu-latest
12
+ environment:
13
+ name: pypi
14
+ url: https://pypi.org/project/fastapi-minimal-template/
15
+
16
+ permissions:
17
+ contents: read
18
+ id-token: write
19
+
20
+ steps:
21
+ - name: Checkout
22
+ uses: actions/checkout@v4
23
+
24
+ - name: Instalar uv
25
+ uses: astral-sh/setup-uv@v6
26
+ with:
27
+ enable-cache: true
28
+
29
+ - name: Configurar Python
30
+ run: uv python install 3.12
31
+
32
+ - name: Instalar dependências
33
+ run: uv sync --locked
34
+
35
+ - name: Executar lint
36
+ run: uv run ruff check .
37
+
38
+ - name: Verificar formatação
39
+ run: uv run ruff format --check .
40
+
41
+ - name: Executar testes
42
+ run: uv run pytest
43
+
44
+ - name: Construir pacote
45
+ run: uv build --no-sources
46
+
47
+ - name: Publicar no PyPI
48
+ run: uv publish
@@ -0,0 +1,9 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ .pytest_cache/
4
+ .ruff_cache/
5
+ .venv/
6
+ .tools/
7
+ dist/
8
+ build/
9
+ *.egg-info/
@@ -0,0 +1 @@
1
+ 3.12
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 fastapi-minimal-template contributors
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,125 @@
1
+ Metadata-Version: 2.4
2
+ Name: fastapi-minimal-template
3
+ Version: 0.1.0
4
+ Summary: Generate a minimal FastAPI project for learning Docker and uv
5
+ Project-URL: Homepage, https://github.com/dirleiflsilva/fastapi-minimal-template
6
+ Project-URL: Issues, https://github.com/dirleiflsilva/fastapi-minimal-template/issues
7
+ Author: fastapi-minimal-template contributors
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Keywords: cli,docker,fastapi,template,uv
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Environment :: Console
13
+ Classifier: Framework :: FastAPI
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Topic :: Software Development :: Code Generators
18
+ Requires-Python: >=3.12
19
+ Requires-Dist: typer>=0.12
20
+ Description-Content-Type: text/markdown
21
+
22
+ # fastapi-minimal-template
23
+
24
+ Uma pequena ferramenta CLI de código aberto que cria uma aplicação FastAPI
25
+ deliberadamente mínima. Ela foi projetada para aulas práticas sobre Python, APIs
26
+ REST, Docker, Docker Compose e gerenciamento de dependências com
27
+ [uv](https://docs.astral.sh/uv/).
28
+
29
+ O projeto gerado contém dois endpoints, testes e arquivos opcionais para Docker.
30
+ Intencionalmente, ele não inclui banco de dados, autenticação, ORM ou
31
+ infraestrutura de produção.
32
+
33
+ ## Início rápido
34
+
35
+ Execute a versão mais recente publicada sem instalá-la:
36
+
37
+ ```bash
38
+ uvx fastapi-minimal-template@latest new hello-api
39
+ cd hello-api
40
+ uv sync
41
+ uv run fastapi dev src/app/main.py
42
+ ```
43
+
44
+ Acesse:
45
+
46
+ - API: <http://localhost:8000>
47
+ - Swagger: <http://localhost:8000/docs>
48
+ - Verificação de saúde: <http://localhost:8000/health>
49
+
50
+ A forma abreviada também é aceita:
51
+
52
+ ```bash
53
+ uvx fastapi-minimal-template@latest hello-api
54
+ ```
55
+
56
+ Use `--no-docker` para omitir os arquivos do Docker. Um destino existente nunca
57
+ será alterado, a menos que `--force` seja informado explicitamente:
58
+
59
+ ```bash
60
+ fastapi-minimal-template new hello-api --no-docker
61
+ fastapi-minimal-template new hello-api --force
62
+ ```
63
+
64
+ ## Outras opções de instalação
65
+
66
+ Execute uma versão específica:
67
+
68
+ ```bash
69
+ uvx fastapi-minimal-template@0.1.0 new hello-api
70
+ ```
71
+
72
+ Instale o comando permanentemente:
73
+
74
+ ```bash
75
+ uv tool install fastapi-minimal-template
76
+ fastapi-minimal-template new hello-api
77
+ ```
78
+
79
+ ## Desenvolvimento da CLI
80
+
81
+ ```bash
82
+ git clone https://github.com/dirleiflsilva/fastapi-minimal-template.git
83
+ cd fastapi-minimal-template
84
+ uv sync
85
+ uv run fastapi-minimal-template new hello-api
86
+ ```
87
+
88
+ Para reproduzir a experiência do `uvx` antes da publicação:
89
+
90
+ ```bash
91
+ uvx --from . fastapi-minimal-template new hello-api
92
+ ```
93
+
94
+ Um caminho absoluto pode ser usado após `--from` caso a forma com o diretório
95
+ atual não seja resolvida por uma versão mais antiga do uv.
96
+
97
+ Execute todas as verificações de qualidade e construa a distribuição:
98
+
99
+ ```bash
100
+ uv run ruff check .
101
+ uv run ruff format --check .
102
+ uv run pytest
103
+ uv build
104
+ ```
105
+
106
+ ## Publicação
107
+
108
+ Depois de atualizar a única declaração de versão em
109
+ `src/fastapi_minimal_template/__init__.py`, valide e construa o pacote:
110
+
111
+ ```bash
112
+ uv sync
113
+ uv run ruff check .
114
+ uv run ruff format --check .
115
+ uv run pytest
116
+ uv build
117
+ uv publish
118
+ ```
119
+
120
+ A publicação requer credenciais do PyPI e deve ser realizada a partir de um
121
+ commit de release limpo e revisado.
122
+
123
+ ## Licença
124
+
125
+ [MIT](LICENSE)
@@ -0,0 +1,104 @@
1
+ # fastapi-minimal-template
2
+
3
+ Uma pequena ferramenta CLI de código aberto que cria uma aplicação FastAPI
4
+ deliberadamente mínima. Ela foi projetada para aulas práticas sobre Python, APIs
5
+ REST, Docker, Docker Compose e gerenciamento de dependências com
6
+ [uv](https://docs.astral.sh/uv/).
7
+
8
+ O projeto gerado contém dois endpoints, testes e arquivos opcionais para Docker.
9
+ Intencionalmente, ele não inclui banco de dados, autenticação, ORM ou
10
+ infraestrutura de produção.
11
+
12
+ ## Início rápido
13
+
14
+ Execute a versão mais recente publicada sem instalá-la:
15
+
16
+ ```bash
17
+ uvx fastapi-minimal-template@latest new hello-api
18
+ cd hello-api
19
+ uv sync
20
+ uv run fastapi dev src/app/main.py
21
+ ```
22
+
23
+ Acesse:
24
+
25
+ - API: <http://localhost:8000>
26
+ - Swagger: <http://localhost:8000/docs>
27
+ - Verificação de saúde: <http://localhost:8000/health>
28
+
29
+ A forma abreviada também é aceita:
30
+
31
+ ```bash
32
+ uvx fastapi-minimal-template@latest hello-api
33
+ ```
34
+
35
+ Use `--no-docker` para omitir os arquivos do Docker. Um destino existente nunca
36
+ será alterado, a menos que `--force` seja informado explicitamente:
37
+
38
+ ```bash
39
+ fastapi-minimal-template new hello-api --no-docker
40
+ fastapi-minimal-template new hello-api --force
41
+ ```
42
+
43
+ ## Outras opções de instalação
44
+
45
+ Execute uma versão específica:
46
+
47
+ ```bash
48
+ uvx fastapi-minimal-template@0.1.0 new hello-api
49
+ ```
50
+
51
+ Instale o comando permanentemente:
52
+
53
+ ```bash
54
+ uv tool install fastapi-minimal-template
55
+ fastapi-minimal-template new hello-api
56
+ ```
57
+
58
+ ## Desenvolvimento da CLI
59
+
60
+ ```bash
61
+ git clone https://github.com/dirleiflsilva/fastapi-minimal-template.git
62
+ cd fastapi-minimal-template
63
+ uv sync
64
+ uv run fastapi-minimal-template new hello-api
65
+ ```
66
+
67
+ Para reproduzir a experiência do `uvx` antes da publicação:
68
+
69
+ ```bash
70
+ uvx --from . fastapi-minimal-template new hello-api
71
+ ```
72
+
73
+ Um caminho absoluto pode ser usado após `--from` caso a forma com o diretório
74
+ atual não seja resolvida por uma versão mais antiga do uv.
75
+
76
+ Execute todas as verificações de qualidade e construa a distribuição:
77
+
78
+ ```bash
79
+ uv run ruff check .
80
+ uv run ruff format --check .
81
+ uv run pytest
82
+ uv build
83
+ ```
84
+
85
+ ## Publicação
86
+
87
+ Depois de atualizar a única declaração de versão em
88
+ `src/fastapi_minimal_template/__init__.py`, valide e construa o pacote:
89
+
90
+ ```bash
91
+ uv sync
92
+ uv run ruff check .
93
+ uv run ruff format --check .
94
+ uv run pytest
95
+ uv build
96
+ uv publish
97
+ ```
98
+
99
+ A publicação requer credenciais do PyPI e deve ser realizada a partir de um
100
+ commit de release limpo e revisado.
101
+
102
+ ## Licença
103
+
104
+ [MIT](LICENSE)
@@ -0,0 +1,65 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "fastapi-minimal-template"
7
+ dynamic = ["version"]
8
+ description = "Generate a minimal FastAPI project for learning Docker and uv"
9
+ readme = "README.md"
10
+ requires-python = ">=3.12"
11
+ license = "MIT"
12
+ authors = [
13
+ { name = "fastapi-minimal-template contributors" },
14
+ ]
15
+ keywords = ["fastapi", "docker", "template", "cli", "uv"]
16
+ classifiers = [
17
+ "Development Status :: 3 - Alpha",
18
+ "Environment :: Console",
19
+ "Framework :: FastAPI",
20
+ "License :: OSI Approved :: MIT License",
21
+ "Programming Language :: Python :: 3",
22
+ "Programming Language :: Python :: 3.12",
23
+ "Topic :: Software Development :: Code Generators",
24
+ ]
25
+ dependencies = [
26
+ "typer>=0.12",
27
+ ]
28
+
29
+ [project.urls]
30
+ Homepage = "https://github.com/dirleiflsilva/fastapi-minimal-template"
31
+ Issues = "https://github.com/dirleiflsilva/fastapi-minimal-template/issues"
32
+
33
+ [project.scripts]
34
+ fastapi-minimal-template = "fastapi_minimal_template.cli:app"
35
+
36
+ [dependency-groups]
37
+ dev = [
38
+ "fastapi[standard]>=0.115,<0.116",
39
+ "httpx",
40
+ "pytest",
41
+ "ruff",
42
+ ]
43
+
44
+ [tool.uv]
45
+ constraint-dependencies = [
46
+ "anyio<4.10",
47
+ "pydantic<2.12",
48
+ ]
49
+
50
+ [tool.hatch.build.targets.wheel]
51
+ packages = ["src/fastapi_minimal_template"]
52
+
53
+ [tool.hatch.version]
54
+ path = "src/fastapi_minimal_template/__init__.py"
55
+
56
+ [tool.pytest.ini_options]
57
+ pythonpath = ["src"]
58
+ testpaths = ["tests"]
59
+
60
+ [tool.ruff]
61
+ line-length = 88
62
+ target-version = "py312"
63
+
64
+ [tool.ruff.lint]
65
+ select = ["E", "F", "I", "UP", "B", "SIM"]
@@ -0,0 +1,3 @@
1
+ """Generate minimal FastAPI projects."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,4 @@
1
+ from fastapi_minimal_template.cli import app
2
+
3
+ if __name__ == "__main__":
4
+ app()
@@ -0,0 +1,88 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+
5
+ import typer
6
+ from typer.core import TyperGroup
7
+
8
+ from fastapi_minimal_template import __version__
9
+ from fastapi_minimal_template.generator import GenerationError, generate_project
10
+
11
+
12
+ class ShortcutGroup(TyperGroup):
13
+ """Treat an unknown first positional argument as `new PROJECT_NAME`."""
14
+
15
+ def parse_args(self, ctx: typer.Context, args: list[str]) -> list[str]:
16
+ if args and not args[0].startswith("-") and args[0] != "new":
17
+ args.insert(0, "new")
18
+ return super().parse_args(ctx, args)
19
+
20
+
21
+ app = typer.Typer(
22
+ cls=ShortcutGroup,
23
+ help="Generate a minimal FastAPI project.",
24
+ no_args_is_help=True,
25
+ add_completion=False,
26
+ )
27
+
28
+
29
+ def _version_callback(value: bool) -> None:
30
+ if value:
31
+ typer.echo(f"fastapi-minimal-template {__version__}")
32
+ raise typer.Exit()
33
+
34
+
35
+ @app.callback()
36
+ def main(
37
+ version: bool = typer.Option(
38
+ False,
39
+ "--version",
40
+ callback=_version_callback,
41
+ is_eager=True,
42
+ help="Show the version and exit.",
43
+ ),
44
+ ) -> None:
45
+ """Generate a minimal FastAPI project."""
46
+
47
+
48
+ @app.command()
49
+ def new(
50
+ project_name: str = typer.Argument(..., metavar="PROJECT_NAME"),
51
+ force: bool = typer.Option(
52
+ False,
53
+ "--force",
54
+ help="Replace an existing destination.",
55
+ ),
56
+ no_docker: bool = typer.Option(
57
+ False,
58
+ "--no-docker",
59
+ help="Do not generate Docker files.",
60
+ ),
61
+ ) -> None:
62
+ """Create a new FastAPI project."""
63
+ typer.echo(f"Creating FastAPI project: {project_name}\n")
64
+ try:
65
+ result = generate_project(
66
+ project_name,
67
+ base_dir=Path.cwd(),
68
+ force=force,
69
+ include_docker=not no_docker,
70
+ )
71
+ except (GenerationError, OSError) as error:
72
+ typer.echo(f"Error: {error}", err=True)
73
+ raise typer.Exit(code=1) from error
74
+
75
+ typer.echo("✓ Project directory created")
76
+ typer.echo("✓ Python files generated")
77
+ if result.docker_enabled:
78
+ typer.echo("✓ Dockerfile generated")
79
+ typer.echo("✓ Docker Compose configuration generated")
80
+ typer.echo("✓ Tests generated")
81
+ typer.echo("\nProject created successfully.\n")
82
+ typer.echo("Next steps:\n")
83
+ typer.echo(f" cd {result.destination.name}")
84
+ typer.echo(" uv sync")
85
+ typer.echo(" uv run fastapi dev src/app/main.py")
86
+ typer.echo("\nAPI: http://localhost:8000")
87
+ typer.echo("Swagger: http://localhost:8000/docs")
88
+ typer.echo("Health check: http://localhost:8000/health")
@@ -0,0 +1,200 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+ import shutil
5
+ import tempfile
6
+ import uuid
7
+ from dataclasses import dataclass
8
+ from importlib.resources import files
9
+ from pathlib import Path
10
+ from typing import BinaryIO
11
+
12
+ PLACEHOLDERS = (
13
+ "{{ project_name }}",
14
+ "{{ project_slug }}",
15
+ "{{ project_title }}",
16
+ "{{ python_package_name }}",
17
+ )
18
+ NAME_PATTERN = re.compile(r"^[A-Za-z0-9][A-Za-z0-9_-]*$")
19
+ TEXT_FILE_NAMES = {
20
+ ".dockerignore",
21
+ ".gitignore",
22
+ "Dockerfile",
23
+ "compose.yaml",
24
+ }
25
+ TEXT_SUFFIXES = {".md", ".py", ".template", ".toml", ".yaml", ".yml"}
26
+ DOCKER_FILES = {"Dockerfile", "compose.yaml", ".dockerignore"}
27
+
28
+
29
+ class GenerationError(ValueError):
30
+ """Raised when a project cannot be generated safely."""
31
+
32
+
33
+ @dataclass(frozen=True)
34
+ class ProjectNames:
35
+ original: str
36
+ slug: str
37
+ title: str
38
+ python_package: str
39
+
40
+ @property
41
+ def replacements(self) -> dict[str, str]:
42
+ return {
43
+ "{{ project_name }}": self.original,
44
+ "{{ project_slug }}": self.slug,
45
+ "{{ project_title }}": self.title,
46
+ "{{ python_package_name }}": self.python_package,
47
+ }
48
+
49
+
50
+ @dataclass(frozen=True)
51
+ class GenerationResult:
52
+ destination: Path
53
+ names: ProjectNames
54
+ docker_enabled: bool
55
+
56
+
57
+ def normalize_project_name(project_name: str) -> ProjectNames:
58
+ """Validate a user-supplied name and calculate its derived forms."""
59
+ if not project_name or project_name in {".", ".."}:
60
+ raise GenerationError("Project name cannot be empty, '.' or '..'.")
61
+
62
+ candidate = Path(project_name)
63
+ if candidate.is_absolute() or "/" in project_name or "\\" in project_name:
64
+ raise GenerationError("Project name must not contain a path.")
65
+ if not NAME_PATTERN.fullmatch(project_name):
66
+ raise GenerationError(
67
+ "Project name may contain only letters, numbers, '-' and '_', "
68
+ "and must start with a letter or number."
69
+ )
70
+
71
+ words = [word for word in re.split(r"[-_]+", project_name) if word]
72
+ slug = "-".join(words).lower()
73
+ python_package = "_".join(words).lower()
74
+ if python_package[0].isdigit():
75
+ python_package = f"_{python_package}"
76
+
77
+ return ProjectNames(
78
+ original=project_name,
79
+ slug=slug,
80
+ title=" ".join(word.capitalize() for word in words),
81
+ python_package=python_package,
82
+ )
83
+
84
+
85
+ def generate_project(
86
+ project_name: str,
87
+ *,
88
+ base_dir: Path | None = None,
89
+ force: bool = False,
90
+ include_docker: bool = True,
91
+ ) -> GenerationResult:
92
+ """Create a project atomically inside base_dir."""
93
+ names = normalize_project_name(project_name)
94
+ parent = (base_dir or Path.cwd()).resolve()
95
+ if not parent.is_dir():
96
+ raise GenerationError(f"Destination parent does not exist: {parent}")
97
+
98
+ destination = parent / names.original
99
+ if destination.exists() and not force:
100
+ raise GenerationError(
101
+ f"Destination already exists: {destination}. Use --force to replace it."
102
+ )
103
+
104
+ staging = Path(tempfile.mkdtemp(prefix=f".{names.slug}-", dir=parent))
105
+ try:
106
+ _copy_template(staging, names, include_docker=include_docker)
107
+ _install_staged_project(staging, destination, force=force)
108
+ except Exception:
109
+ if staging.exists():
110
+ shutil.rmtree(staging)
111
+ raise
112
+
113
+ return GenerationResult(
114
+ destination=destination,
115
+ names=names,
116
+ docker_enabled=include_docker,
117
+ )
118
+
119
+
120
+ def _copy_template(
121
+ destination: Path,
122
+ names: ProjectNames,
123
+ *,
124
+ include_docker: bool,
125
+ ) -> None:
126
+ template_root = files("fastapi_minimal_template").joinpath("template")
127
+ _copy_directory(
128
+ template_root,
129
+ destination,
130
+ names.replacements,
131
+ include_docker=include_docker,
132
+ at_root=True,
133
+ )
134
+
135
+
136
+ def _copy_directory(
137
+ source: object,
138
+ destination: Path,
139
+ replacements: dict[str, str],
140
+ *,
141
+ include_docker: bool,
142
+ at_root: bool = False,
143
+ ) -> None:
144
+ destination.mkdir(parents=True, exist_ok=True)
145
+ for item in source.iterdir(): # type: ignore[attr-defined]
146
+ if at_root and not include_docker and item.name in DOCKER_FILES:
147
+ continue
148
+
149
+ output_name = item.name.removesuffix(".template")
150
+ output_path = destination / output_name
151
+ if item.is_dir():
152
+ _copy_directory(
153
+ item,
154
+ output_path,
155
+ replacements,
156
+ include_docker=include_docker,
157
+ )
158
+ elif _is_text_file(item.name):
159
+ text = item.read_text(encoding="utf-8")
160
+ for placeholder, value in replacements.items():
161
+ text = text.replace(placeholder, value)
162
+ output_path.write_text(text, encoding="utf-8")
163
+ else:
164
+ with item.open("rb") as source_file, output_path.open("wb") as output_file:
165
+ _copy_binary(source_file, output_file)
166
+
167
+
168
+ def _copy_binary(source: BinaryIO, destination: BinaryIO) -> None:
169
+ shutil.copyfileobj(source, destination)
170
+
171
+
172
+ def _is_text_file(name: str) -> bool:
173
+ return name in TEXT_FILE_NAMES or Path(name).suffix in TEXT_SUFFIXES
174
+
175
+
176
+ def _install_staged_project(
177
+ staging: Path,
178
+ destination: Path,
179
+ *,
180
+ force: bool,
181
+ ) -> None:
182
+ if not destination.exists():
183
+ staging.replace(destination)
184
+ return
185
+
186
+ if not force:
187
+ raise GenerationError(f"Destination already exists: {destination}")
188
+
189
+ backup = destination.with_name(f".{destination.name}.backup-{uuid.uuid4().hex}")
190
+ destination.replace(backup)
191
+ try:
192
+ staging.replace(destination)
193
+ except Exception:
194
+ backup.replace(destination)
195
+ raise
196
+ else:
197
+ if backup.is_dir():
198
+ shutil.rmtree(backup)
199
+ else:
200
+ backup.unlink()