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.
- fastapi_minimal_template-0.1.0/.github/workflows/ci.yml +26 -0
- fastapi_minimal_template-0.1.0/.github/workflows/release.yml +48 -0
- fastapi_minimal_template-0.1.0/.gitignore +9 -0
- fastapi_minimal_template-0.1.0/.python-version +1 -0
- fastapi_minimal_template-0.1.0/LICENSE +21 -0
- fastapi_minimal_template-0.1.0/PKG-INFO +125 -0
- fastapi_minimal_template-0.1.0/README.md +104 -0
- fastapi_minimal_template-0.1.0/pyproject.toml +65 -0
- fastapi_minimal_template-0.1.0/src/fastapi_minimal_template/__init__.py +3 -0
- fastapi_minimal_template-0.1.0/src/fastapi_minimal_template/__main__.py +4 -0
- fastapi_minimal_template-0.1.0/src/fastapi_minimal_template/cli.py +88 -0
- fastapi_minimal_template-0.1.0/src/fastapi_minimal_template/generator.py +200 -0
- fastapi_minimal_template-0.1.0/src/fastapi_minimal_template/template/.dockerignore +6 -0
- fastapi_minimal_template-0.1.0/src/fastapi_minimal_template/template/.gitignore +5 -0
- fastapi_minimal_template-0.1.0/src/fastapi_minimal_template/template/Dockerfile +13 -0
- fastapi_minimal_template-0.1.0/src/fastapi_minimal_template/template/README.md.template +45 -0
- fastapi_minimal_template-0.1.0/src/fastapi_minimal_template/template/compose.yaml +6 -0
- fastapi_minimal_template-0.1.0/src/fastapi_minimal_template/template/pyproject.toml.template +33 -0
- fastapi_minimal_template-0.1.0/src/fastapi_minimal_template/template/src/app/__init__.py +1 -0
- fastapi_minimal_template-0.1.0/src/fastapi_minimal_template/template/src/app/main.py +16 -0
- fastapi_minimal_template-0.1.0/src/fastapi_minimal_template/template/tests/test_main.py +18 -0
- fastapi_minimal_template-0.1.0/tests/test_cli.py +94 -0
- fastapi_minimal_template-0.1.0/tests/test_generated_project.py +35 -0
- fastapi_minimal_template-0.1.0/tests/test_generator.py +105 -0
- 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 @@
|
|
|
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,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()
|