paststack 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.
- paststack-0.1.0/LICENSE +21 -0
- paststack-0.1.0/PKG-INFO +129 -0
- paststack-0.1.0/README.md +113 -0
- paststack-0.1.0/paststack/__init__.py +1 -0
- paststack-0.1.0/paststack/banner.py +19 -0
- paststack-0.1.0/paststack/cli.py +353 -0
- paststack-0.1.0/paststack/combinations.py +68 -0
- paststack-0.1.0/paststack/models.py +30 -0
- paststack-0.1.0/paststack/prompts.py +92 -0
- paststack-0.1.0/paststack/templates/base/.env.example +2 -0
- paststack-0.1.0/paststack/templates/base/.gitignore +15 -0
- paststack-0.1.0/paststack/templates/base/README.md +39 -0
- paststack-0.1.0/paststack/templates/base/pyproject.toml +52 -0
- paststack-0.1.0/paststack/templates/base/src/app/__init__.py +5 -0
- paststack-0.1.0/paststack/templates/base/src/app/api/__init__.py +1 -0
- paststack-0.1.0/paststack/templates/base/src/app/api/deps.py +1 -0
- paststack-0.1.0/paststack/templates/base/src/app/api/router.py +6 -0
- paststack-0.1.0/paststack/templates/base/src/app/api/routes/__init__.py +1 -0
- paststack-0.1.0/paststack/templates/base/src/app/api/routes/health.py +17 -0
- paststack-0.1.0/paststack/templates/base/src/app/core/__init__.py +1 -0
- paststack-0.1.0/paststack/templates/base/src/app/core/config.py +22 -0
- paststack-0.1.0/paststack/templates/base/src/app/database.py +13 -0
- paststack-0.1.0/paststack/templates/base/src/app/main.py +42 -0
- paststack-0.1.0/paststack/templates/base/src/app/models/__init__.py +1 -0
- paststack-0.1.0/paststack/templates/base/src/app/schemas/__init__.py +1 -0
- paststack-0.1.0/paststack/templates/database/postgres/docker-compose.yml +14 -0
- paststack-0.1.0/paststack/templates/database/postgres/none/database.py +27 -0
- paststack-0.1.0/paststack/templates/database/postgres/sqlmodel/api/deps.py +10 -0
- paststack-0.1.0/paststack/templates/database/postgres/sqlmodel/database.py +38 -0
- paststack-0.1.0/paststack/templates/database/postgres/sqlmodel/models/__init__.py +5 -0
- paststack-0.1.0/paststack/templates/database/sqlite/none/database.py +27 -0
- paststack-0.1.0/paststack/templates/database/sqlite/sqlmodel/api/deps.py +10 -0
- paststack-0.1.0/paststack/templates/database/sqlite/sqlmodel/database.py +40 -0
- paststack-0.1.0/paststack/templates/database/sqlite/sqlmodel/models/__init__.py +5 -0
- paststack-0.1.0/paststack/templates/rate_limiting/src/app/core/rate_limit.py +14 -0
- paststack-0.1.0/paststack.egg-info/PKG-INFO +129 -0
- paststack-0.1.0/paststack.egg-info/SOURCES.txt +42 -0
- paststack-0.1.0/paststack.egg-info/dependency_links.txt +1 -0
- paststack-0.1.0/paststack.egg-info/entry_points.txt +2 -0
- paststack-0.1.0/paststack.egg-info/requires.txt +5 -0
- paststack-0.1.0/paststack.egg-info/top_level.txt +1 -0
- paststack-0.1.0/pyproject.toml +52 -0
- paststack-0.1.0/setup.cfg +4 -0
- paststack-0.1.0/tests/test_cli_generation.py +103 -0
paststack-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Quentin Van Steenwinkel
|
|
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.
|
paststack-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: paststack
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Opinionated CLI to generate production-ready FastAPI boilerplate with typing and linting.
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Project-URL: Repository, https://github.com/initd-fr/paststack
|
|
7
|
+
Requires-Python: >=3.12
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Requires-Dist: click>=8.1.0
|
|
11
|
+
Requires-Dist: pydantic>=2.12.5
|
|
12
|
+
Requires-Dist: questionary>=2.1.1
|
|
13
|
+
Requires-Dist: rich>=13.0.0
|
|
14
|
+
Requires-Dist: typer>=0.24.1
|
|
15
|
+
Dynamic: license-file
|
|
16
|
+
|
|
17
|
+
# paststack
|
|
18
|
+
|
|
19
|
+

|
|
20
|
+
|
|
21
|
+
A CLI to generate production-ready FastAPI backends with a clean, opinionated architecture.
|
|
22
|
+
|
|
23
|
+

|
|
24
|
+

|
|
25
|
+

|
|
26
|
+

|
|
27
|
+

|
|
28
|
+

|
|
29
|
+

|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## Vue d’ensemble
|
|
34
|
+
|
|
35
|
+
**paststack** génère un projet prêt au développement : arborescence `src/app/` (core, api, routes, models, schemas), configuration **pydantic-settings**, CORS, santé `/health` et `/ready`.
|
|
36
|
+
|
|
37
|
+
Décisions actuelles de la v1 :
|
|
38
|
+
|
|
39
|
+
| Sujet | Choix |
|
|
40
|
+
| ----------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
|
|
41
|
+
| Gestionnaire de paquets | **uv** |
|
|
42
|
+
| Base de données | **none**, **SQLite**, **PostgreSQL** |
|
|
43
|
+
| Couche données | **sans ORM** (driver async) ou **SQLModel** (ORM unique) |
|
|
44
|
+
| Rate limiting | **slowapi** (optionnel), limite par IP |
|
|
45
|
+
| PostgreSQL | **docker-compose** fourni ; URL alignée sur le conteneur |
|
|
46
|
+
| Dépendances | **extras** dans le `pyproject.toml` généré (`sqlite-none`, `sqlite-sqlmodel`, `postgres-none`, `postgres-sqlmodel`, `rate-limit`) |
|
|
47
|
+
| Git | `git init` optionnel |
|
|
48
|
+
| Messages de commit | **[git-z](https://github.com/ejpcmac/git-z)** optionnel (`git z init` dans le projet généré) |
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## Fonctionnalités
|
|
53
|
+
|
|
54
|
+
- Assistant interactif (`questionary`) : nom du projet, CORS, base, ORM, rate limiting, installation `uv`, git, git-z
|
|
55
|
+
- Copie des templates embarqués dans le package (`templates/**/*`)
|
|
56
|
+
- Création d’un venv + `uv sync` avec les bons `--extra` si demandé
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## Utilisation
|
|
61
|
+
|
|
62
|
+
Une fois le package installé (`pip install paststack` depuis PyPI, ou `uv pip install -e .` depuis ce dépôt) :
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
paststack
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Puis ouvrir le dossier créé, copier `.env.example` vers `.env`, lancer l’API (voir le `README.md` généré dans le projet).
|
|
69
|
+
|
|
70
|
+
### Développement (ce dépôt)
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
git clone https://github.com/initd-fr/paststack.git
|
|
74
|
+
cd paststack
|
|
75
|
+
uv sync
|
|
76
|
+
uv pip install -e .
|
|
77
|
+
paststack
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### Qualité (ce dépôt)
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
uv run ruff check .
|
|
84
|
+
uv run mypy .
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Tests (ce dépôt)
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
uv sync --group dev
|
|
91
|
+
uv run pytest tests/ -v
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Les combinaisons valides (SGBD × ORM × rate limiting) sont exposées dans `paststack.combinations` pour les tests ou un usage programmatique.
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
## Conventions de commit (ce dépôt)
|
|
99
|
+
|
|
100
|
+
Format décrit dans `git-z.toml` : `TYPE description (scope)` (types et scopes listés dans le fichier).
|
|
101
|
+
|
|
102
|
+
Pour utiliser l’assistant [git-z](https://github.com/ejpcmac/git-z) en local : `git z init` (après installation de l’outil). Le générateur peut lancer `git z init` dans le **nouveau** projet si tu coches l’option correspondante.
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## Feuille de route (indicative)
|
|
107
|
+
|
|
108
|
+
### v0.x — générateur actuel
|
|
109
|
+
|
|
110
|
+
- [x] CLI interactive + modèle `Project` typé
|
|
111
|
+
- [x] Template FastAPI (`core`, `api`, routes, models, schemas)
|
|
112
|
+
- [x] SQLite / Postgres × ORM ou driver seul
|
|
113
|
+
- [x] Rate limiting (slowapi) en option
|
|
114
|
+
- [x] Venv + `uv sync` avec extras
|
|
115
|
+
- [x] `git init` / `git z init` en option
|
|
116
|
+
|
|
117
|
+
### Plus tard
|
|
118
|
+
|
|
119
|
+
- Variantes d’architecture (minimal / modulable avancée), autres SGBD, observabilité, etc.
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
## Pourquoi ce projet
|
|
124
|
+
|
|
125
|
+
Poser une base FastAPI propre (structure, typing, lint, DB) prend du temps. Ce CLI applique les mêmes défauts à chaque nouveau service.
|
|
126
|
+
|
|
127
|
+
## Licence
|
|
128
|
+
|
|
129
|
+
MIT
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# paststack
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+
|
|
5
|
+
A CLI to generate production-ready FastAPI backends with a clean, opinionated architecture.
|
|
6
|
+
|
|
7
|
+

|
|
8
|
+

|
|
9
|
+

|
|
10
|
+

|
|
11
|
+

|
|
12
|
+

|
|
13
|
+

|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Vue d’ensemble
|
|
18
|
+
|
|
19
|
+
**paststack** génère un projet prêt au développement : arborescence `src/app/` (core, api, routes, models, schemas), configuration **pydantic-settings**, CORS, santé `/health` et `/ready`.
|
|
20
|
+
|
|
21
|
+
Décisions actuelles de la v1 :
|
|
22
|
+
|
|
23
|
+
| Sujet | Choix |
|
|
24
|
+
| ----------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
|
|
25
|
+
| Gestionnaire de paquets | **uv** |
|
|
26
|
+
| Base de données | **none**, **SQLite**, **PostgreSQL** |
|
|
27
|
+
| Couche données | **sans ORM** (driver async) ou **SQLModel** (ORM unique) |
|
|
28
|
+
| Rate limiting | **slowapi** (optionnel), limite par IP |
|
|
29
|
+
| PostgreSQL | **docker-compose** fourni ; URL alignée sur le conteneur |
|
|
30
|
+
| Dépendances | **extras** dans le `pyproject.toml` généré (`sqlite-none`, `sqlite-sqlmodel`, `postgres-none`, `postgres-sqlmodel`, `rate-limit`) |
|
|
31
|
+
| Git | `git init` optionnel |
|
|
32
|
+
| Messages de commit | **[git-z](https://github.com/ejpcmac/git-z)** optionnel (`git z init` dans le projet généré) |
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## Fonctionnalités
|
|
37
|
+
|
|
38
|
+
- Assistant interactif (`questionary`) : nom du projet, CORS, base, ORM, rate limiting, installation `uv`, git, git-z
|
|
39
|
+
- Copie des templates embarqués dans le package (`templates/**/*`)
|
|
40
|
+
- Création d’un venv + `uv sync` avec les bons `--extra` si demandé
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## Utilisation
|
|
45
|
+
|
|
46
|
+
Une fois le package installé (`pip install paststack` depuis PyPI, ou `uv pip install -e .` depuis ce dépôt) :
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
paststack
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Puis ouvrir le dossier créé, copier `.env.example` vers `.env`, lancer l’API (voir le `README.md` généré dans le projet).
|
|
53
|
+
|
|
54
|
+
### Développement (ce dépôt)
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
git clone https://github.com/initd-fr/paststack.git
|
|
58
|
+
cd paststack
|
|
59
|
+
uv sync
|
|
60
|
+
uv pip install -e .
|
|
61
|
+
paststack
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Qualité (ce dépôt)
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
uv run ruff check .
|
|
68
|
+
uv run mypy .
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Tests (ce dépôt)
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
uv sync --group dev
|
|
75
|
+
uv run pytest tests/ -v
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Les combinaisons valides (SGBD × ORM × rate limiting) sont exposées dans `paststack.combinations` pour les tests ou un usage programmatique.
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## Conventions de commit (ce dépôt)
|
|
83
|
+
|
|
84
|
+
Format décrit dans `git-z.toml` : `TYPE description (scope)` (types et scopes listés dans le fichier).
|
|
85
|
+
|
|
86
|
+
Pour utiliser l’assistant [git-z](https://github.com/ejpcmac/git-z) en local : `git z init` (après installation de l’outil). Le générateur peut lancer `git z init` dans le **nouveau** projet si tu coches l’option correspondante.
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## Feuille de route (indicative)
|
|
91
|
+
|
|
92
|
+
### v0.x — générateur actuel
|
|
93
|
+
|
|
94
|
+
- [x] CLI interactive + modèle `Project` typé
|
|
95
|
+
- [x] Template FastAPI (`core`, `api`, routes, models, schemas)
|
|
96
|
+
- [x] SQLite / Postgres × ORM ou driver seul
|
|
97
|
+
- [x] Rate limiting (slowapi) en option
|
|
98
|
+
- [x] Venv + `uv sync` avec extras
|
|
99
|
+
- [x] `git init` / `git z init` en option
|
|
100
|
+
|
|
101
|
+
### Plus tard
|
|
102
|
+
|
|
103
|
+
- Variantes d’architecture (minimal / modulable avancée), autres SGBD, observabilité, etc.
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
## Pourquoi ce projet
|
|
108
|
+
|
|
109
|
+
Poser une base FastAPI propre (structure, typing, lint, DB) prend du temps. Ce CLI applique les mêmes défauts à chaque nouveau service.
|
|
110
|
+
|
|
111
|
+
## Licence
|
|
112
|
+
|
|
113
|
+
MIT
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import click
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def display_banner() -> None:
|
|
5
|
+
click.echo(
|
|
6
|
+
r"""
|
|
7
|
+
/$$$$$$$ /$$ /$$$$$$ /$$ /$$
|
|
8
|
+
| $$__ $$ | $$ /$$__ $$ | $$ | $$
|
|
9
|
+
| $$ \ $$ /$$$$$$ /$$$$$$$ /$$$$$$ | $$ \__//$$$$$$ /$$$$$$ /$$$$$$$| $$ /$$
|
|
10
|
+
| $$$$$$$/|____ $$ /$$_____/|_ $$_/ | $$$$$$|_ $$_/ |____ $$ /$$_____/| $$ /$$/
|
|
11
|
+
| $$____/ /$$$$$$$| $$$$$$ | $$ \____ $$ | $$ /$$$$$$$| $$ | $$$$$$/
|
|
12
|
+
| $$ /$$__ $$ \____ $$ | $$ /$$ /$$ \ $$ | $$ /$$ /$$__ $$| $$ | $$_ $$
|
|
13
|
+
| $$ | $$$$$$$ /$$$$$$$/ | $$$$/| $$$$$$/ | $$$$/| $$$$$$$| $$$$$$$| $$ \ $$
|
|
14
|
+
|__/ \_______/|_______/ \___/ \______/ \___/ \_______/ \_______/|__/ \__/
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
"""
|
|
19
|
+
)
|
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import subprocess
|
|
3
|
+
import sys
|
|
4
|
+
import time
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from shutil import copyfile, which
|
|
7
|
+
|
|
8
|
+
import click
|
|
9
|
+
import questionary
|
|
10
|
+
from rich.progress import (
|
|
11
|
+
BarColumn,
|
|
12
|
+
Progress,
|
|
13
|
+
SpinnerColumn,
|
|
14
|
+
TaskProgressColumn,
|
|
15
|
+
TextColumn,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
from paststack.banner import display_banner
|
|
19
|
+
from paststack.models import Database, Orm, Project
|
|
20
|
+
from paststack.prompts import ask_questions, show_summary
|
|
21
|
+
|
|
22
|
+
TEMPLATES_ROOT = Path(__file__).resolve().parent / "templates"
|
|
23
|
+
BASE = TEMPLATES_ROOT / "base"
|
|
24
|
+
DATABASE = TEMPLATES_ROOT / "database"
|
|
25
|
+
RATE_LIMITING = TEMPLATES_ROOT / "rate_limiting"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _venv_python(venv_path: Path) -> Path:
|
|
29
|
+
if sys.platform == "win32":
|
|
30
|
+
return venv_path / "Scripts" / "python.exe"
|
|
31
|
+
return venv_path / "bin" / "python"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _database_url(project: Project) -> str:
|
|
35
|
+
if project.database == Database.NONE:
|
|
36
|
+
return "sqlite+aiosqlite:///./data/app.db"
|
|
37
|
+
if project.database == Database.SQLITE:
|
|
38
|
+
return "sqlite+aiosqlite:///./data/app.db"
|
|
39
|
+
if project.orm == Orm.SQLMODEL:
|
|
40
|
+
return "postgresql+asyncpg://app:app@127.0.0.1:5432/app"
|
|
41
|
+
return "postgresql://app:app@127.0.0.1:5432/app"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _env_database_block(project: Project) -> str:
|
|
45
|
+
if project.database == Database.NONE:
|
|
46
|
+
return "# Pas de base de données (DATABASE_URL ignoré)"
|
|
47
|
+
return f"DATABASE_URL={_database_url(project)}"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _build_extra(project: Project) -> dict[str, str]:
|
|
51
|
+
extra: dict[str, str] = {
|
|
52
|
+
"{{ database_url }}": _database_url(project),
|
|
53
|
+
"{{ env_database_block }}": _env_database_block(project),
|
|
54
|
+
}
|
|
55
|
+
if project.rate_limiting:
|
|
56
|
+
extra["{{ rate_limit_imports }}"] = (
|
|
57
|
+
"from app.core.rate_limit import setup_rate_limiting\n"
|
|
58
|
+
)
|
|
59
|
+
extra["{{ rate_limit_setup }}"] = "setup_rate_limiting(app)\n"
|
|
60
|
+
else:
|
|
61
|
+
extra["{{ rate_limit_imports }}"] = ""
|
|
62
|
+
extra["{{ rate_limit_setup }}"] = ""
|
|
63
|
+
return extra
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _subprocess_env_without_venv() -> dict[str, str]:
|
|
67
|
+
env = os.environ.copy()
|
|
68
|
+
env.pop("VIRTUAL_ENV", None)
|
|
69
|
+
return env
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _docker_compose_up_postgres(project_root: Path) -> bool:
|
|
73
|
+
"""Démarre le conteneur Postgres du projet. Retourne False si Docker indisponible ou échec."""
|
|
74
|
+
if not (project_root / "docker-compose.yml").is_file():
|
|
75
|
+
click.echo("Aucun docker-compose.yml — impossible de lancer PostgreSQL ici.", err=True)
|
|
76
|
+
return False
|
|
77
|
+
if which("docker") is None:
|
|
78
|
+
click.echo(
|
|
79
|
+
click.style(
|
|
80
|
+
"La commande `docker` est introuvable.\n"
|
|
81
|
+
"→ Installe Docker Desktop : https://www.docker.com/products/docker-desktop/\n"
|
|
82
|
+
"→ Puis dans le projet : docker compose up -d",
|
|
83
|
+
fg="red",
|
|
84
|
+
),
|
|
85
|
+
err=True,
|
|
86
|
+
)
|
|
87
|
+
return False
|
|
88
|
+
result = subprocess.run(
|
|
89
|
+
["docker", "compose", "up", "-d"],
|
|
90
|
+
cwd=project_root,
|
|
91
|
+
capture_output=True,
|
|
92
|
+
text=True,
|
|
93
|
+
env=_subprocess_env_without_venv(),
|
|
94
|
+
)
|
|
95
|
+
if result.returncode != 0:
|
|
96
|
+
click.echo(click.style("docker compose up -d a échoué.", fg="red"), err=True)
|
|
97
|
+
if result.stderr.strip():
|
|
98
|
+
click.echo(result.stderr, err=True)
|
|
99
|
+
if result.stdout.strip():
|
|
100
|
+
click.echo(result.stdout, err=True)
|
|
101
|
+
click.echo(
|
|
102
|
+
click.style(
|
|
103
|
+
"Vérifie que Docker Desktop est démarré (icône baleine active).\n"
|
|
104
|
+
"Ensuite, à la main : docker compose up -d",
|
|
105
|
+
fg="yellow",
|
|
106
|
+
),
|
|
107
|
+
err=True,
|
|
108
|
+
)
|
|
109
|
+
return False
|
|
110
|
+
click.echo(click.style("PostgreSQL : conteneur démarré (docker compose up -d).", fg="green"))
|
|
111
|
+
return True
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _uv_extras(project: Project) -> list[str]:
|
|
115
|
+
extras: list[str] = []
|
|
116
|
+
if project.database == Database.SQLITE:
|
|
117
|
+
extras.append(
|
|
118
|
+
"sqlite-sqlmodel" if project.orm == Orm.SQLMODEL else "sqlite-none"
|
|
119
|
+
)
|
|
120
|
+
elif project.database == Database.POSTGRES:
|
|
121
|
+
extras.append(
|
|
122
|
+
"postgres-sqlmodel" if project.orm == Orm.SQLMODEL else "postgres-none"
|
|
123
|
+
)
|
|
124
|
+
if project.rate_limiting:
|
|
125
|
+
extras.append("rate-limit")
|
|
126
|
+
return extras
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
@click.command()
|
|
130
|
+
def main() -> None:
|
|
131
|
+
try:
|
|
132
|
+
display_banner()
|
|
133
|
+
while True:
|
|
134
|
+
project = ask_questions()
|
|
135
|
+
show_summary(project)
|
|
136
|
+
if questionary.confirm("Proceed with project setup ?").unsafe_ask():
|
|
137
|
+
break
|
|
138
|
+
setup_project(project)
|
|
139
|
+
except KeyboardInterrupt:
|
|
140
|
+
click.echo("\nAborted.")
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _create_directories(base: Path, paths: list[tuple[str, ...]]) -> None:
|
|
144
|
+
for parts in paths:
|
|
145
|
+
(base / Path(*parts)).mkdir(parents=True, exist_ok=True)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _copy_and_render(
|
|
149
|
+
template: Path,
|
|
150
|
+
destination: Path,
|
|
151
|
+
project: Project,
|
|
152
|
+
extra: dict[str, str] | None = None,
|
|
153
|
+
) -> None:
|
|
154
|
+
destination.parent.mkdir(parents=True, exist_ok=True)
|
|
155
|
+
copyfile(template, destination)
|
|
156
|
+
content = destination.read_text(encoding="utf-8")
|
|
157
|
+
content = content.replace("{{ project_name }}", project.project_name)
|
|
158
|
+
origins = project.allowed_origins or []
|
|
159
|
+
content = content.replace("{{ allowed_origins }}", repr(origins))
|
|
160
|
+
content = content.replace("{{project.allowed_origins}}", repr(origins))
|
|
161
|
+
content = content.replace("{{ database }}", project.database.value)
|
|
162
|
+
content = content.replace("{{ orm }}", project.orm.value)
|
|
163
|
+
for key, value in (extra or {}).items():
|
|
164
|
+
content = content.replace(key, value)
|
|
165
|
+
destination.write_text(content, encoding="utf-8")
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _copy_template_tree(
|
|
169
|
+
template_dir: Path,
|
|
170
|
+
dest_root: Path,
|
|
171
|
+
project: Project,
|
|
172
|
+
extra: dict[str, str] | None = None,
|
|
173
|
+
) -> None:
|
|
174
|
+
if not template_dir.is_dir():
|
|
175
|
+
return
|
|
176
|
+
for path in sorted(template_dir.rglob("*")):
|
|
177
|
+
if path.is_file():
|
|
178
|
+
rel = path.relative_to(template_dir)
|
|
179
|
+
_copy_and_render(path, dest_root / rel, project, extra)
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def setup_project(project: Project) -> None:
|
|
183
|
+
main_directory = Path(project.project_name).resolve()
|
|
184
|
+
|
|
185
|
+
try:
|
|
186
|
+
main_directory.mkdir()
|
|
187
|
+
except FileExistsError:
|
|
188
|
+
click.echo(f"Directory '{main_directory}' already exists.")
|
|
189
|
+
return
|
|
190
|
+
except PermissionError:
|
|
191
|
+
click.echo(f"Permission denied: unable to create '{main_directory}'.")
|
|
192
|
+
return
|
|
193
|
+
except OSError as e:
|
|
194
|
+
click.echo(f"An error occurred: {e}")
|
|
195
|
+
return
|
|
196
|
+
|
|
197
|
+
extra = _build_extra(project)
|
|
198
|
+
_copy_template_tree(BASE, main_directory, project, extra)
|
|
199
|
+
|
|
200
|
+
db_dest = main_directory / "src" / "app" / "database.py"
|
|
201
|
+
|
|
202
|
+
match (project.database, project.orm):
|
|
203
|
+
case (Database.NONE, _):
|
|
204
|
+
pass
|
|
205
|
+
case (Database.SQLITE, Orm.NONE):
|
|
206
|
+
_copy_and_render(
|
|
207
|
+
DATABASE / "sqlite" / "none" / "database.py", db_dest, project, extra
|
|
208
|
+
)
|
|
209
|
+
case (Database.SQLITE, Orm.SQLMODEL):
|
|
210
|
+
_copy_template_tree(
|
|
211
|
+
DATABASE / "sqlite" / "sqlmodel",
|
|
212
|
+
main_directory / "src" / "app",
|
|
213
|
+
project,
|
|
214
|
+
extra,
|
|
215
|
+
)
|
|
216
|
+
case (Database.POSTGRES, Orm.NONE):
|
|
217
|
+
_copy_and_render(
|
|
218
|
+
DATABASE / "postgres" / "none" / "database.py", db_dest, project, extra
|
|
219
|
+
)
|
|
220
|
+
_copy_and_render(
|
|
221
|
+
DATABASE / "postgres" / "docker-compose.yml",
|
|
222
|
+
main_directory / "docker-compose.yml",
|
|
223
|
+
project,
|
|
224
|
+
extra,
|
|
225
|
+
)
|
|
226
|
+
case (Database.POSTGRES, Orm.SQLMODEL):
|
|
227
|
+
_copy_template_tree(
|
|
228
|
+
DATABASE / "postgres" / "sqlmodel",
|
|
229
|
+
main_directory / "src" / "app",
|
|
230
|
+
project,
|
|
231
|
+
extra,
|
|
232
|
+
)
|
|
233
|
+
_copy_and_render(
|
|
234
|
+
DATABASE / "postgres" / "docker-compose.yml",
|
|
235
|
+
main_directory / "docker-compose.yml",
|
|
236
|
+
project,
|
|
237
|
+
extra,
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
if project.rate_limiting:
|
|
241
|
+
_copy_template_tree(RATE_LIMITING, main_directory, project, extra)
|
|
242
|
+
|
|
243
|
+
venv_path = (main_directory / project.project_name).resolve()
|
|
244
|
+
py_exe = _venv_python(venv_path).resolve()
|
|
245
|
+
|
|
246
|
+
with Progress(
|
|
247
|
+
SpinnerColumn(),
|
|
248
|
+
TextColumn("[bold blue]{task.description}"),
|
|
249
|
+
BarColumn(bar_width=40),
|
|
250
|
+
TaskProgressColumn(),
|
|
251
|
+
TextColumn(" "),
|
|
252
|
+
TextColumn("[progress.percentage]{task.percentage:>3.0f}%"),
|
|
253
|
+
) as progress:
|
|
254
|
+
t_venv = progress.add_task("Création du venv…", total=1)
|
|
255
|
+
subprocess.run(
|
|
256
|
+
[sys.executable, "-m", "venv", str(venv_path)],
|
|
257
|
+
check=True,
|
|
258
|
+
env=_subprocess_env_without_venv(),
|
|
259
|
+
)
|
|
260
|
+
progress.update(t_venv, completed=1)
|
|
261
|
+
|
|
262
|
+
if project.run_install:
|
|
263
|
+
uv_extras = _uv_extras(project)
|
|
264
|
+
cmd = ["uv", "sync", "-p", str(py_exe)]
|
|
265
|
+
for e in uv_extras:
|
|
266
|
+
cmd.append(f"--extra={e}")
|
|
267
|
+
t_sync = progress.add_task("uv sync (dépendances)…", total=1)
|
|
268
|
+
subprocess.run(
|
|
269
|
+
cmd,
|
|
270
|
+
cwd=main_directory,
|
|
271
|
+
check=True,
|
|
272
|
+
env=_subprocess_env_without_venv(),
|
|
273
|
+
)
|
|
274
|
+
progress.update(t_sync, completed=1)
|
|
275
|
+
|
|
276
|
+
if project.git_z:
|
|
277
|
+
subprocess.run(["git", "init"], cwd=main_directory, check=True)
|
|
278
|
+
# --default : config git-z sans wizard interactif (sinon le CLI semble « figé »)
|
|
279
|
+
result = subprocess.run(
|
|
280
|
+
["git", "z", "init", "--default"],
|
|
281
|
+
cwd=main_directory,
|
|
282
|
+
capture_output=True,
|
|
283
|
+
text=True,
|
|
284
|
+
)
|
|
285
|
+
if result.returncode != 0:
|
|
286
|
+
click.echo(
|
|
287
|
+
"git z init a échoué (git-z installé ?). "
|
|
288
|
+
"Voir https://github.com/ejpcmac/git-z",
|
|
289
|
+
err=True,
|
|
290
|
+
)
|
|
291
|
+
if result.stderr:
|
|
292
|
+
click.echo(result.stderr, err=True)
|
|
293
|
+
elif project.git:
|
|
294
|
+
subprocess.run(["git", "init"], cwd=main_directory, check=True)
|
|
295
|
+
|
|
296
|
+
env_example = main_directory / ".env.example"
|
|
297
|
+
env_dest = main_directory / ".env"
|
|
298
|
+
if env_example.is_file() and not env_dest.is_file():
|
|
299
|
+
copyfile(env_example, env_dest)
|
|
300
|
+
click.echo("Fichier .env créé à partir de .env.example.")
|
|
301
|
+
|
|
302
|
+
start_uvicorn = project.run_install and questionary.confirm(
|
|
303
|
+
"Démarrer l’API maintenant (uvicorn --reload) ?",
|
|
304
|
+
default=True,
|
|
305
|
+
).unsafe_ask()
|
|
306
|
+
|
|
307
|
+
if start_uvicorn and project.database == Database.POSTGRES:
|
|
308
|
+
if questionary.confirm(
|
|
309
|
+
"Lancer PostgreSQL avec Docker (`docker compose up -d`) ?",
|
|
310
|
+
default=True,
|
|
311
|
+
).unsafe_ask():
|
|
312
|
+
if not _docker_compose_up_postgres(main_directory):
|
|
313
|
+
start_uvicorn = questionary.confirm(
|
|
314
|
+
"Lancer uvicorn quand même ? (l’API échouera si Postgres n’est pas joignable.)",
|
|
315
|
+
default=False,
|
|
316
|
+
).unsafe_ask()
|
|
317
|
+
else:
|
|
318
|
+
time.sleep(2)
|
|
319
|
+
else:
|
|
320
|
+
click.echo(
|
|
321
|
+
click.style(
|
|
322
|
+
"Sans conteneur Docker, assure-toi que Postgres tourne déjà "
|
|
323
|
+
"(même URL que dans `.env`), sinon le démarrage de l’API échouera.",
|
|
324
|
+
fg="yellow",
|
|
325
|
+
)
|
|
326
|
+
)
|
|
327
|
+
start_uvicorn = questionary.confirm(
|
|
328
|
+
"Lancer uvicorn quand même ?",
|
|
329
|
+
default=False,
|
|
330
|
+
).unsafe_ask()
|
|
331
|
+
|
|
332
|
+
if start_uvicorn:
|
|
333
|
+
click.echo(
|
|
334
|
+
click.style(
|
|
335
|
+
"\n→ http://127.0.0.1:8000/docs (Ctrl+C pour arrêter)\n",
|
|
336
|
+
fg="green",
|
|
337
|
+
)
|
|
338
|
+
)
|
|
339
|
+
subprocess.run(
|
|
340
|
+
[
|
|
341
|
+
"uv",
|
|
342
|
+
"run",
|
|
343
|
+
"--python",
|
|
344
|
+
str(py_exe),
|
|
345
|
+
"uvicorn",
|
|
346
|
+
"app.main:app",
|
|
347
|
+
"--reload",
|
|
348
|
+
"--app-dir",
|
|
349
|
+
"src",
|
|
350
|
+
],
|
|
351
|
+
cwd=main_directory,
|
|
352
|
+
env=_subprocess_env_without_venv(),
|
|
353
|
+
)
|