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.
Files changed (44) hide show
  1. paststack-0.1.0/LICENSE +21 -0
  2. paststack-0.1.0/PKG-INFO +129 -0
  3. paststack-0.1.0/README.md +113 -0
  4. paststack-0.1.0/paststack/__init__.py +1 -0
  5. paststack-0.1.0/paststack/banner.py +19 -0
  6. paststack-0.1.0/paststack/cli.py +353 -0
  7. paststack-0.1.0/paststack/combinations.py +68 -0
  8. paststack-0.1.0/paststack/models.py +30 -0
  9. paststack-0.1.0/paststack/prompts.py +92 -0
  10. paststack-0.1.0/paststack/templates/base/.env.example +2 -0
  11. paststack-0.1.0/paststack/templates/base/.gitignore +15 -0
  12. paststack-0.1.0/paststack/templates/base/README.md +39 -0
  13. paststack-0.1.0/paststack/templates/base/pyproject.toml +52 -0
  14. paststack-0.1.0/paststack/templates/base/src/app/__init__.py +5 -0
  15. paststack-0.1.0/paststack/templates/base/src/app/api/__init__.py +1 -0
  16. paststack-0.1.0/paststack/templates/base/src/app/api/deps.py +1 -0
  17. paststack-0.1.0/paststack/templates/base/src/app/api/router.py +6 -0
  18. paststack-0.1.0/paststack/templates/base/src/app/api/routes/__init__.py +1 -0
  19. paststack-0.1.0/paststack/templates/base/src/app/api/routes/health.py +17 -0
  20. paststack-0.1.0/paststack/templates/base/src/app/core/__init__.py +1 -0
  21. paststack-0.1.0/paststack/templates/base/src/app/core/config.py +22 -0
  22. paststack-0.1.0/paststack/templates/base/src/app/database.py +13 -0
  23. paststack-0.1.0/paststack/templates/base/src/app/main.py +42 -0
  24. paststack-0.1.0/paststack/templates/base/src/app/models/__init__.py +1 -0
  25. paststack-0.1.0/paststack/templates/base/src/app/schemas/__init__.py +1 -0
  26. paststack-0.1.0/paststack/templates/database/postgres/docker-compose.yml +14 -0
  27. paststack-0.1.0/paststack/templates/database/postgres/none/database.py +27 -0
  28. paststack-0.1.0/paststack/templates/database/postgres/sqlmodel/api/deps.py +10 -0
  29. paststack-0.1.0/paststack/templates/database/postgres/sqlmodel/database.py +38 -0
  30. paststack-0.1.0/paststack/templates/database/postgres/sqlmodel/models/__init__.py +5 -0
  31. paststack-0.1.0/paststack/templates/database/sqlite/none/database.py +27 -0
  32. paststack-0.1.0/paststack/templates/database/sqlite/sqlmodel/api/deps.py +10 -0
  33. paststack-0.1.0/paststack/templates/database/sqlite/sqlmodel/database.py +40 -0
  34. paststack-0.1.0/paststack/templates/database/sqlite/sqlmodel/models/__init__.py +5 -0
  35. paststack-0.1.0/paststack/templates/rate_limiting/src/app/core/rate_limit.py +14 -0
  36. paststack-0.1.0/paststack.egg-info/PKG-INFO +129 -0
  37. paststack-0.1.0/paststack.egg-info/SOURCES.txt +42 -0
  38. paststack-0.1.0/paststack.egg-info/dependency_links.txt +1 -0
  39. paststack-0.1.0/paststack.egg-info/entry_points.txt +2 -0
  40. paststack-0.1.0/paststack.egg-info/requires.txt +5 -0
  41. paststack-0.1.0/paststack.egg-info/top_level.txt +1 -0
  42. paststack-0.1.0/pyproject.toml +52 -0
  43. paststack-0.1.0/setup.cfg +4 -0
  44. paststack-0.1.0/tests/test_cli_generation.py +103 -0
@@ -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.
@@ -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
+ ![logo](./img/logo.png)
20
+
21
+ A CLI to generate production-ready FastAPI backends with a clean, opinionated architecture.
22
+
23
+ ![python](https://img.shields.io/badge/python-3.12%2B-blue)
24
+ ![cli](https://img.shields.io/badge/interface-CLI-black)
25
+ ![ruff](https://img.shields.io/badge/ruff-linting-red)
26
+ ![mypy](https://img.shields.io/badge/mypy-typing-blue)
27
+ ![pydantic](https://img.shields.io/badge/pydantic-validation-blue)
28
+ ![status](https://img.shields.io/badge/status-active--development-orange)
29
+ ![license](https://img.shields.io/badge/license-MIT-green)
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
+ ![logo](./img/logo.png)
4
+
5
+ A CLI to generate production-ready FastAPI backends with a clean, opinionated architecture.
6
+
7
+ ![python](https://img.shields.io/badge/python-3.12%2B-blue)
8
+ ![cli](https://img.shields.io/badge/interface-CLI-black)
9
+ ![ruff](https://img.shields.io/badge/ruff-linting-red)
10
+ ![mypy](https://img.shields.io/badge/mypy-typing-blue)
11
+ ![pydantic](https://img.shields.io/badge/pydantic-validation-blue)
12
+ ![status](https://img.shields.io/badge/status-active--development-orange)
13
+ ![license](https://img.shields.io/badge/license-MIT-green)
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
+ )