paststack 0.1.0__py3-none-any.whl

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 (38) hide show
  1. paststack/__init__.py +1 -0
  2. paststack/banner.py +19 -0
  3. paststack/cli.py +353 -0
  4. paststack/combinations.py +68 -0
  5. paststack/models.py +30 -0
  6. paststack/prompts.py +92 -0
  7. paststack/templates/base/.env.example +2 -0
  8. paststack/templates/base/.gitignore +15 -0
  9. paststack/templates/base/README.md +39 -0
  10. paststack/templates/base/pyproject.toml +52 -0
  11. paststack/templates/base/src/app/__init__.py +5 -0
  12. paststack/templates/base/src/app/api/__init__.py +1 -0
  13. paststack/templates/base/src/app/api/deps.py +1 -0
  14. paststack/templates/base/src/app/api/router.py +6 -0
  15. paststack/templates/base/src/app/api/routes/__init__.py +1 -0
  16. paststack/templates/base/src/app/api/routes/health.py +17 -0
  17. paststack/templates/base/src/app/core/__init__.py +1 -0
  18. paststack/templates/base/src/app/core/config.py +22 -0
  19. paststack/templates/base/src/app/database.py +13 -0
  20. paststack/templates/base/src/app/main.py +42 -0
  21. paststack/templates/base/src/app/models/__init__.py +1 -0
  22. paststack/templates/base/src/app/schemas/__init__.py +1 -0
  23. paststack/templates/database/postgres/docker-compose.yml +14 -0
  24. paststack/templates/database/postgres/none/database.py +27 -0
  25. paststack/templates/database/postgres/sqlmodel/api/deps.py +10 -0
  26. paststack/templates/database/postgres/sqlmodel/database.py +38 -0
  27. paststack/templates/database/postgres/sqlmodel/models/__init__.py +5 -0
  28. paststack/templates/database/sqlite/none/database.py +27 -0
  29. paststack/templates/database/sqlite/sqlmodel/api/deps.py +10 -0
  30. paststack/templates/database/sqlite/sqlmodel/database.py +40 -0
  31. paststack/templates/database/sqlite/sqlmodel/models/__init__.py +5 -0
  32. paststack/templates/rate_limiting/src/app/core/rate_limit.py +14 -0
  33. paststack-0.1.0.dist-info/METADATA +129 -0
  34. paststack-0.1.0.dist-info/RECORD +38 -0
  35. paststack-0.1.0.dist-info/WHEEL +5 -0
  36. paststack-0.1.0.dist-info/entry_points.txt +2 -0
  37. paststack-0.1.0.dist-info/licenses/LICENSE +21 -0
  38. paststack-0.1.0.dist-info/top_level.txt +1 -0
paststack/__init__.py ADDED
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
paststack/banner.py ADDED
@@ -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
+ )
paststack/cli.py ADDED
@@ -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
+ )
@@ -0,0 +1,68 @@
1
+ """Combinaisons valides du générateur — réutilisables par les tests ou d’autres outils."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Iterator
6
+
7
+ from paststack.models import Database, Orm, Project
8
+
9
+
10
+ def iter_database_orm_rate_limit_triples() -> Iterator[tuple[Database, Orm, bool]]:
11
+ """Toutes les combinaisons métier valides (SGBD × ORM × rate limiting)."""
12
+ for rate_limiting in (False, True):
13
+ yield (Database.NONE, Orm.NONE, rate_limiting)
14
+
15
+ for database in (Database.SQLITE, Database.POSTGRES):
16
+ for orm in (Orm.NONE, Orm.SQLMODEL):
17
+ for rate_limiting in (False, True):
18
+ yield (database, orm, rate_limiting)
19
+
20
+
21
+ def make_project(
22
+ database: Database,
23
+ orm: Orm,
24
+ *,
25
+ rate_limiting: bool = False,
26
+ project_name: str = "generated_app",
27
+ run_install: bool = False,
28
+ git: bool = False,
29
+ git_z: bool = False,
30
+ ) -> Project:
31
+ """Construit un `Project` cohérent (sans base → ORM forcé à none)."""
32
+ if database == Database.NONE:
33
+ orm = Orm.NONE
34
+ return Project(
35
+ project_name=project_name,
36
+ package_manager="uv",
37
+ use_typing=True,
38
+ use_ruff=True,
39
+ enable_cors=False,
40
+ allowed_origins=None,
41
+ database=database,
42
+ orm=orm,
43
+ rate_limiting=rate_limiting,
44
+ config=True,
45
+ git=git,
46
+ git_z=git_z,
47
+ run_install=run_install,
48
+ )
49
+
50
+
51
+ def iter_all_projects(
52
+ *,
53
+ project_name: str = "generated_app",
54
+ run_install: bool = False,
55
+ ) -> Iterator[Project]:
56
+ """Itère un `Project` par combinaison valide."""
57
+ for database, orm, rate_limiting in iter_database_orm_rate_limit_triples():
58
+ yield make_project(
59
+ database,
60
+ orm,
61
+ rate_limiting=rate_limiting,
62
+ project_name=project_name,
63
+ run_install=run_install,
64
+ )
65
+
66
+
67
+ def combination_count() -> int:
68
+ return sum(1 for _ in iter_database_orm_rate_limit_triples())
paststack/models.py ADDED
@@ -0,0 +1,30 @@
1
+ from enum import Enum
2
+
3
+ from pydantic import BaseModel
4
+
5
+
6
+ class Database(str, Enum):
7
+ NONE = "none"
8
+ SQLITE = "sqlite"
9
+ POSTGRES = "postgres"
10
+
11
+
12
+ class Orm(str, Enum):
13
+ NONE = "none"
14
+ SQLMODEL = "sqlmodel"
15
+
16
+
17
+ class Project(BaseModel):
18
+ project_name: str
19
+ package_manager: str
20
+ use_typing: bool
21
+ use_ruff: bool
22
+ enable_cors: bool
23
+ allowed_origins: list[str] | None = None
24
+ database: Database
25
+ orm: Orm
26
+ rate_limiting: bool
27
+ config: bool
28
+ git: bool
29
+ git_z: bool
30
+ run_install: bool
paststack/prompts.py ADDED
@@ -0,0 +1,92 @@
1
+ import click
2
+ import questionary
3
+
4
+ from paststack.models import Database, Orm, Project
5
+
6
+
7
+ def ask_questions() -> Project:
8
+ project_name = questionary.text(
9
+ "Project name",
10
+ default="my_fastapi_app",
11
+ ).unsafe_ask()
12
+
13
+ raw_allowed_origins: str = questionary.text(
14
+ "Allowed origins (CORS) [comma-separated, empty = disabled]",
15
+ default="",
16
+ ).unsafe_ask()
17
+
18
+ allowed_origins: list[str] = [
19
+ origin.strip() for origin in raw_allowed_origins.split(",") if origin.strip()
20
+ ]
21
+
22
+ raw_database: str = questionary.select(
23
+ "Database",
24
+ choices=[e.value for e in Database],
25
+ default="none",
26
+ ).unsafe_ask()
27
+
28
+ raw_orm: str = "none"
29
+ if raw_database != "none":
30
+ raw_orm = questionary.select(
31
+ "Data layer (SQLModel = ORM + Pydantic, none = driver only)",
32
+ choices=[e.value for e in Orm],
33
+ default="sqlmodel",
34
+ ).unsafe_ask()
35
+
36
+ rate_limiting: bool = questionary.confirm(
37
+ "Enable rate limiting",
38
+ default=False,
39
+ ).unsafe_ask()
40
+
41
+ run_install: bool = questionary.confirm(
42
+ "Run install after setup",
43
+ default=True,
44
+ ).unsafe_ask()
45
+
46
+ git: bool = questionary.confirm(
47
+ "Initialize git repository in the new project?",
48
+ default=True,
49
+ ).unsafe_ask()
50
+
51
+ git_z: bool = False
52
+ if git:
53
+ git_z = questionary.confirm(
54
+ "Run git z init (git-z commit wizard)? Requires git-z — https://github.com/ejpcmac/git-z",
55
+ default=False,
56
+ ).unsafe_ask()
57
+
58
+ project = Project(
59
+ project_name=project_name,
60
+ package_manager="uv",
61
+ use_typing=True,
62
+ use_ruff=True,
63
+ enable_cors=bool(allowed_origins),
64
+ allowed_origins=allowed_origins,
65
+ database=Database(raw_database),
66
+ orm=Orm(raw_orm),
67
+ rate_limiting=rate_limiting,
68
+ config=True,
69
+ git=git,
70
+ git_z=git_z,
71
+ run_install=run_install,
72
+ )
73
+
74
+ return project
75
+
76
+
77
+ def show_summary(project: Project) -> None:
78
+ click.echo()
79
+ click.echo(f"Project : {project.project_name}")
80
+ click.echo(f"Package Manager : {project.package_manager}")
81
+
82
+ click.echo(f"CORS enabled : {'yes' if project.enable_cors else 'no'}")
83
+ if project.enable_cors:
84
+ click.echo(f"Allowed origins : {project.allowed_origins}")
85
+
86
+ click.echo(f"Database : {project.database.value}")
87
+ click.echo(f"ORM : {project.orm.value}")
88
+ click.echo(f"Rate limiting : {'yes' if project.rate_limiting else 'no'}")
89
+ click.echo(f"Run install : {'yes' if project.run_install else 'no'}")
90
+ click.echo(f"Git init : {'yes' if project.git else 'no'}")
91
+ click.echo(f"git-z init : {'yes' if project.git_z else 'no'}")
92
+ click.echo()
@@ -0,0 +1,2 @@
1
+ # Copier vers .env et adapter
2
+ {{ env_database_block }}
@@ -0,0 +1,15 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ .eggs/
5
+ dist/
6
+ build/
7
+ .venv/
8
+ venv/
9
+ .env
10
+ .env.*
11
+ !.env.example
12
+ .mypy_cache/
13
+ .ruff_cache/
14
+ .pytest_cache/
15
+ .DS_Store
@@ -0,0 +1,39 @@
1
+ # {{ project_name }}
2
+
3
+ Projet généré avec [paststack](https://github.com/initd-fr/paststack).
4
+
5
+ ## Structure
6
+
7
+ - `src/app/core/` — configuration (`pydantic-settings`)
8
+ - `src/app/api/` — routeurs, dépendances, routes
9
+ - `src/app/models/` — modèles **SQLModel** (si ORM activé)
10
+ - `src/app/schemas/` — schémas Pydantic pour l’API
11
+ - `src/app/database.py` — couche base (générée selon SQLite / Postgres, ORM ou non)
12
+
13
+ ## Dépendances optionnelles
14
+
15
+ Le générateur installe les extras `uv` adaptés (ex. `sqlite-sqlmodel`, `postgres-none`, `rate-limit`).
16
+ Pour réinstaller à la main :
17
+
18
+ ```bash
19
+ uv sync --extra <nom> # voir [project.optional-dependencies] dans pyproject.toml
20
+ ```
21
+
22
+ ## Lancer en dev
23
+
24
+ ```bash
25
+ cp .env.example .env
26
+ uv run uvicorn app.main:app --reload --app-dir src
27
+ ```
28
+
29
+ PostgreSQL : démarre la base avec `docker compose up -d` (fichier fourni si tu as choisi Postgres).
30
+
31
+ Documentation interactive : http://127.0.0.1:8000/docs
32
+
33
+ ## Qualité
34
+
35
+ ```bash
36
+ uv sync --group dev
37
+ uv run ruff check src
38
+ uv run mypy src
39
+ ```
@@ -0,0 +1,52 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "{{ project_name }}"
7
+ version = "0.1.0"
8
+ description = "FastAPI application"
9
+ readme = "README.md"
10
+ requires-python = ">=3.12"
11
+ dependencies = [
12
+ "fastapi>=0.115.0",
13
+ "pydantic-settings>=2.6.0",
14
+ "uvicorn[standard]>=0.32.0",
15
+ ]
16
+
17
+ [project.optional-dependencies]
18
+ sqlite-none = ["aiosqlite>=0.20.0"]
19
+ sqlite-sqlmodel = [
20
+ "aiosqlite>=0.20.0",
21
+ "sqlalchemy[asyncio]>=2.0.0",
22
+ "sqlmodel>=0.0.22",
23
+ ]
24
+ postgres-none = ["psycopg[binary]>=3.2.0"]
25
+ postgres-sqlmodel = [
26
+ "asyncpg>=0.30.0",
27
+ "sqlalchemy[asyncio]>=2.0.0",
28
+ "sqlmodel>=0.0.22",
29
+ ]
30
+ rate-limit = ["slowapi>=0.1.9"]
31
+
32
+ [tool.setuptools.packages.find]
33
+ where = ["src"]
34
+
35
+ [dependency-groups]
36
+ dev = [
37
+ "mypy>=1.13.0",
38
+ "ruff>=0.8.0",
39
+ ]
40
+
41
+ [tool.ruff]
42
+ target-version = "py312"
43
+ line-length = 100
44
+
45
+ [tool.ruff.lint]
46
+ select = ["E", "F", "I", "UP"]
47
+
48
+ [tool.mypy]
49
+ python_version = "3.12"
50
+ strict = true
51
+ mypy_path = "src"
52
+ explicit_package_bases = true
@@ -0,0 +1,5 @@
1
+ """Code de ton application FastAPI (point d’entrée : `main.create_app`)."""
2
+
3
+ from app.main import create_app
4
+
5
+ __all__ = ["create_app"]
@@ -0,0 +1 @@
1
+ """HTTP : routeurs, endpoints et dépendances FastAPI."""
@@ -0,0 +1 @@
1
+ """Dépendances injectées dans les routes via `Depends()`."""
@@ -0,0 +1,6 @@
1
+ """Routeur racine de l’API (monté sous `/api/v1` dans `main.py`)."""
2
+
3
+ from fastapi import APIRouter
4
+
5
+ api_router = APIRouter()
6
+ # Branche tes routeurs métier : api_router.include_router(users.router, prefix="/users")
@@ -0,0 +1 @@
1
+ """Endpoints HTTP par domaine : un fichier (ou sous-package) par thème métier."""
@@ -0,0 +1,17 @@
1
+ """Endpoints `/health` et `/ready` (supervision, déploiement)."""
2
+
3
+ from fastapi import APIRouter
4
+
5
+ from app.database import is_database_configured
6
+
7
+ router = APIRouter(tags=["health"])
8
+
9
+
10
+ @router.get("/health")
11
+ def health() -> dict[str, str]:
12
+ return {"status": "ok"}
13
+
14
+
15
+ @router.get("/ready")
16
+ def ready() -> dict[str, str | bool]:
17
+ return {"status": "ok", "database": is_database_configured()}
@@ -0,0 +1 @@
1
+ """Réglages et constantes partagés par toute l’app."""
@@ -0,0 +1,22 @@
1
+ from pydantic_settings import BaseSettings, SettingsConfigDict
2
+
3
+
4
+ class Settings(BaseSettings):
5
+ """Variables lues depuis l’environnement (fichier `.env` à la racine du projet)."""
6
+
7
+ model_config = SettingsConfigDict(
8
+ env_file=".env",
9
+ env_file_encoding="utf-8",
10
+ extra="ignore",
11
+ )
12
+
13
+ app_name: str = "{{ project_name }}"
14
+ debug: bool = False
15
+
16
+ cors_origins: list[str] = {{ allowed_origins }}
17
+
18
+ # URL async (SQLite ou Postgres) ; sans base de données, la valeur n’est pas utilisée
19
+ database_url: str = "{{ database_url }}"
20
+
21
+
22
+ settings = Settings()
@@ -0,0 +1,13 @@
1
+ """Accès données : démarrage / arrêt du moteur ou pool, état « DB prête »."""
2
+
3
+
4
+ async def lifespan_startup() -> None:
5
+ return
6
+
7
+
8
+ async def lifespan_shutdown() -> None:
9
+ return
10
+
11
+
12
+ def is_database_configured() -> bool:
13
+ return False
@@ -0,0 +1,42 @@
1
+ from collections.abc import AsyncIterator
2
+ from contextlib import asynccontextmanager
3
+
4
+ from fastapi import FastAPI
5
+ from starlette.middleware.cors import CORSMiddleware
6
+
7
+ from app.api.router import api_router
8
+ from app.api.routes import health
9
+ from app.core.config import settings
10
+ from app.database import lifespan_shutdown, lifespan_startup
11
+
12
+ {{ rate_limit_imports }}
13
+
14
+ @asynccontextmanager
15
+ async def lifespan(app: FastAPI) -> AsyncIterator[None]:
16
+ await lifespan_startup()
17
+ yield
18
+ await lifespan_shutdown()
19
+
20
+
21
+ def create_app() -> FastAPI:
22
+ app = FastAPI(
23
+ title=settings.app_name,
24
+ version="0.1.0",
25
+ lifespan=lifespan,
26
+ )
27
+ {{ rate_limit_setup }}
28
+ if settings.cors_origins:
29
+ app.add_middleware(
30
+ CORSMiddleware,
31
+ allow_origins=settings.cors_origins,
32
+ allow_credentials=True,
33
+ allow_methods=["*"],
34
+ allow_headers=["*"],
35
+ )
36
+
37
+ app.include_router(health.router)
38
+ app.include_router(api_router, prefix="/api/v1")
39
+ return app
40
+
41
+
42
+ app = create_app()
@@ -0,0 +1 @@
1
+ """Modèles ORM (tables SQL) — SQLModel ou SQLAlchemy selon ton projet."""
@@ -0,0 +1 @@
1
+ """Schémas Pydantic : corps de requête, query params, réponses JSON."""
@@ -0,0 +1,14 @@
1
+ services:
2
+ db:
3
+ image: postgres:16-alpine
4
+ environment:
5
+ POSTGRES_USER: app
6
+ POSTGRES_PASSWORD: app
7
+ POSTGRES_DB: app
8
+ ports:
9
+ - "5432:5432"
10
+ volumes:
11
+ - postgres_data:/var/lib/postgresql/data
12
+
13
+ volumes:
14
+ postgres_data:
@@ -0,0 +1,27 @@
1
+ """PostgreSQL sans ORM — connexion async via psycopg 3."""
2
+
3
+ from app.core.config import settings
4
+ from psycopg import AsyncConnection
5
+
6
+ _conn: AsyncConnection | None = None
7
+
8
+
9
+ async def lifespan_startup() -> None:
10
+ global _conn
11
+ _conn = await AsyncConnection.connect(settings.database_url)
12
+
13
+
14
+ async def lifespan_shutdown() -> None:
15
+ if _conn is not None:
16
+ await _conn.close()
17
+
18
+
19
+ def is_database_configured() -> bool:
20
+ return True
21
+
22
+
23
+ def get_connection() -> AsyncConnection:
24
+ if _conn is None:
25
+ msg = "Database not initialized"
26
+ raise RuntimeError(msg)
27
+ return _conn
@@ -0,0 +1,10 @@
1
+ """Injection d’une session SQLModel async dans les routes (`Depends(get_session)`)."""
2
+
3
+ from typing import Annotated
4
+
5
+ from fastapi import Depends
6
+ from sqlalchemy.ext.asyncio import AsyncSession
7
+
8
+ from app.database import get_session
9
+
10
+ SessionDep = Annotated[AsyncSession, Depends(get_session)]
@@ -0,0 +1,38 @@
1
+ """PostgreSQL + SQLModel (async via asyncpg)."""
2
+
3
+ from collections.abc import AsyncGenerator
4
+
5
+ from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession, async_sessionmaker, create_async_engine
6
+ from sqlmodel import SQLModel
7
+
8
+ from app.core.config import settings
9
+
10
+ engine: AsyncEngine = create_async_engine(
11
+ settings.database_url,
12
+ echo=settings.debug,
13
+ )
14
+ async_session_maker = async_sessionmaker(
15
+ engine,
16
+ class_=AsyncSession,
17
+ expire_on_commit=False,
18
+ )
19
+
20
+
21
+ async def lifespan_startup() -> None:
22
+ import app.models # noqa: F401 — pour enregistrer les tables auprès de SQLModel
23
+
24
+ async with engine.begin() as conn:
25
+ await conn.run_sync(SQLModel.metadata.create_all)
26
+
27
+
28
+ async def lifespan_shutdown() -> None:
29
+ await engine.dispose()
30
+
31
+
32
+ def is_database_configured() -> bool:
33
+ return True
34
+
35
+
36
+ async def get_session() -> AsyncGenerator[AsyncSession, None]:
37
+ async with async_session_maker() as session:
38
+ yield session
@@ -0,0 +1,5 @@
1
+ """Tables SQLModel : définis tes modèles ici (`SQLModel`, `Field`, …)."""
2
+
3
+ from sqlmodel import SQLModel
4
+
5
+ __all__ = ["SQLModel"]
@@ -0,0 +1,27 @@
1
+ """SQLite sans ORM — connexions async via aiosqlite."""
2
+
3
+ from pathlib import Path
4
+
5
+ import aiosqlite
6
+
7
+ DATA_DIR = Path(__file__).resolve().parent.parent.parent / "data"
8
+ DB_PATH = DATA_DIR / "app.db"
9
+
10
+
11
+ async def lifespan_startup() -> None:
12
+ DATA_DIR.mkdir(parents=True, exist_ok=True)
13
+ async with aiosqlite.connect(DB_PATH) as db:
14
+ await db.execute("SELECT 1")
15
+
16
+
17
+ async def lifespan_shutdown() -> None:
18
+ return
19
+
20
+
21
+ def is_database_configured() -> bool:
22
+ return True
23
+
24
+
25
+ async def get_connection() -> aiosqlite.Connection:
26
+ """Ouvrir une connexion : `db = await get_connection()` puis `await db.close()`."""
27
+ return await aiosqlite.connect(DB_PATH)
@@ -0,0 +1,10 @@
1
+ """Injection d’une session SQLModel async dans les routes (`Depends(get_session)`)."""
2
+
3
+ from typing import Annotated
4
+
5
+ from fastapi import Depends
6
+ from sqlalchemy.ext.asyncio import AsyncSession
7
+
8
+ from app.database import get_session
9
+
10
+ SessionDep = Annotated[AsyncSession, Depends(get_session)]
@@ -0,0 +1,40 @@
1
+ """SQLite + SQLModel (async)."""
2
+
3
+ from collections.abc import AsyncGenerator
4
+ from pathlib import Path
5
+
6
+ from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession, async_sessionmaker, create_async_engine
7
+ from sqlmodel import SQLModel
8
+
9
+ from app.core.config import settings
10
+
11
+ engine: AsyncEngine = create_async_engine(
12
+ settings.database_url,
13
+ echo=settings.debug,
14
+ )
15
+ async_session_maker = async_sessionmaker(
16
+ engine,
17
+ class_=AsyncSession,
18
+ expire_on_commit=False,
19
+ )
20
+
21
+
22
+ async def lifespan_startup() -> None:
23
+ (Path(__file__).resolve().parent.parent.parent / "data").mkdir(parents=True, exist_ok=True)
24
+ import app.models # noqa: F401 — pour enregistrer les tables auprès de SQLModel
25
+
26
+ async with engine.begin() as conn:
27
+ await conn.run_sync(SQLModel.metadata.create_all)
28
+
29
+
30
+ async def lifespan_shutdown() -> None:
31
+ await engine.dispose()
32
+
33
+
34
+ def is_database_configured() -> bool:
35
+ return True
36
+
37
+
38
+ async def get_session() -> AsyncGenerator[AsyncSession, None]:
39
+ async with async_session_maker() as session:
40
+ yield session
@@ -0,0 +1,5 @@
1
+ """Tables SQLModel : définis tes modèles ici (`SQLModel`, `Field`, …)."""
2
+
3
+ from sqlmodel import SQLModel
4
+
5
+ __all__ = ["SQLModel"]
@@ -0,0 +1,14 @@
1
+ """Limite le débit des requêtes par IP (slowapi / Limiter)."""
2
+
3
+ from fastapi import FastAPI
4
+ from slowapi import Limiter, _rate_limit_exceeded_handler
5
+ from slowapi.errors import RateLimitExceeded
6
+ from slowapi.middleware import SlowAPIMiddleware
7
+ from slowapi.util import get_remote_address
8
+
9
+
10
+ def setup_rate_limiting(app: FastAPI) -> None:
11
+ limiter = Limiter(key_func=get_remote_address, default_limits=["120/minute"])
12
+ app.state.limiter = limiter
13
+ app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)
14
+ app.add_middleware(SlowAPIMiddleware)
@@ -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,38 @@
1
+ paststack/__init__.py,sha256=kUR5RAFc7HCeiqdlX36dZOHkUI5wI6V_43RpEcD8b-0,22
2
+ paststack/banner.py,sha256=q4oXk0FY5zVm6WXmnfYbKd7u2xMVnhtQ3u3iq0lwkck,1063
3
+ paststack/cli.py,sha256=GYSRgqji1BNQLr3h7Zf8Bl9EFhUYatrf4La7MyutmP8,11944
4
+ paststack/combinations.py,sha256=KO9kSbd0a77LvJjBQIqPLK_1g1DWm0aKP_knZSnBYjM,1986
5
+ paststack/models.py,sha256=-GnOSgXelxBgl_OmXGRY65RwQ2dtKgL4QvGOiz0Nx9M,528
6
+ paststack/prompts.py,sha256=JpCZPCvRDpU_MmRaaN6AqzK77L4wI08jRALytTFKyA0,2743
7
+ paststack/templates/base/.env.example,sha256=-Rvx9TNbGbAkGduAaRd8U9jh4b_7TIqNmTdHwE9rRvk,55
8
+ paststack/templates/base/.gitignore,sha256=c3xgCgkYf30P98o6lA1AS_N9JwyMYkD65OvToNnIlNA,145
9
+ paststack/templates/base/README.md,sha256=A66RZhAY23hhZaFhrigOdWJQazsa-S28tKcExFfAcdU,1040
10
+ paststack/templates/base/pyproject.toml,sha256=qiud3FMRJBzJaUMcf_BJ06hmgBXX6R0BkG4ld-kcqSw,1011
11
+ paststack/templates/base/src/app/__init__.py,sha256=HKzpEnD18-CnTkVK7Ldxc0yRVzSC0ChpFcCxMPaDh0U,138
12
+ paststack/templates/base/src/app/database.py,sha256=3mfMGpFPDoufaD_GEP-M-DKV-CJVJ-ukIw_RplH1Sko,246
13
+ paststack/templates/base/src/app/main.py,sha256=uUW_QgaGlEK9b50HRGdg411d0mGngyowqmwECk26eUk,1055
14
+ paststack/templates/base/src/app/api/__init__.py,sha256=1i_0TYs6aJPs-0m5KLXwAeGX1-fFTyAny6G1wjhc_Dg,58
15
+ paststack/templates/base/src/app/api/deps.py,sha256=onq0VM_Ek53mWoV4UoWq25ZEjm8urh8eZjlM0p5OTuA,63
16
+ paststack/templates/base/src/app/api/router.py,sha256=VDYoTr8Z7JNJAKY_8I8IMtaUrId3X_1wUNS9CiybRMU,219
17
+ paststack/templates/base/src/app/api/routes/__init__.py,sha256=Lz-jtC92Bm2pwWYlCb0su-_eZ8ygJwLWnVqMPCcymFo,84
18
+ paststack/templates/base/src/app/api/routes/health.py,sha256=B_SQRBXH2vi12pYIBaBm8hZwAlHdv_mcAgeaHyGWlhU,397
19
+ paststack/templates/base/src/app/core/__init__.py,sha256=_CEqFxRWNQfJz_coVMRcYqAqhgsOUVSCxEqXWx-2Fj4,59
20
+ paststack/templates/base/src/app/core/config.py,sha256=CqTM6Y4Xg3a_hi4JOrfh5IPkmtfWhVJmRX0JEjSzfiw,599
21
+ paststack/templates/base/src/app/models/__init__.py,sha256=aNCTsy0qEpHNwdsLcwlJGSuKZUR0hhCP9b0nYQu8itc,77
22
+ paststack/templates/base/src/app/schemas/__init__.py,sha256=Bjnprg3q75H3nplHydYz6gk6d-FDxB68erOeVga5hU4,75
23
+ paststack/templates/database/postgres/docker-compose.yml,sha256=E7Gle_6GZN03hacGBwSploHOEpFfwtl3kEi5ihKUlGY,258
24
+ paststack/templates/database/postgres/none/database.py,sha256=r4iEpHH7ry4bPeX7ltKfDQwRo6X26Z_kgG1QF7LWrgM,601
25
+ paststack/templates/database/postgres/sqlmodel/database.py,sha256=1qx9htyCGrqTtBLY51E29wT2l4MVzoBtqpetVLplALM,957
26
+ paststack/templates/database/postgres/sqlmodel/api/deps.py,sha256=BOojAC9sQnbsO153QGZI24DS3KVVZwywGStVDMpyIlI,294
27
+ paststack/templates/database/postgres/sqlmodel/models/__init__.py,sha256=2oYxJudsBFH4so4CNw8L_3CGT1BG4qomB9y-zes8BAE,133
28
+ paststack/templates/database/sqlite/none/database.py,sha256=xlT4mHcA2MLMY-OtTE_1MAGMWiH3Iy3lfVnuFw8hyEE,667
29
+ paststack/templates/database/sqlite/sqlmodel/database.py,sha256=ZUTiAUfBv3GrcFK2AzY7yLkgxHVe8BZ64z8ctW8tHDQ,1062
30
+ paststack/templates/database/sqlite/sqlmodel/api/deps.py,sha256=BOojAC9sQnbsO153QGZI24DS3KVVZwywGStVDMpyIlI,294
31
+ paststack/templates/database/sqlite/sqlmodel/models/__init__.py,sha256=2oYxJudsBFH4so4CNw8L_3CGT1BG4qomB9y-zes8BAE,133
32
+ paststack/templates/rate_limiting/src/app/core/rate_limit.py,sha256=QcdS7Ny6tprNw0CTQy6trtdLneYTCvHm6Qic0bubp1I,574
33
+ paststack-0.1.0.dist-info/licenses/LICENSE,sha256=kfpGx5BL8MZ6FculLtl3jKSa22TPkZKNpaTThxj-Yaw,1080
34
+ paststack-0.1.0.dist-info/METADATA,sha256=AqqPnoRXk2z9o17aWEsO9QxeVbbquqALPCGy98SnJ7A,4979
35
+ paststack-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
36
+ paststack-0.1.0.dist-info/entry_points.txt,sha256=Ikb6kTNbasiFCF7fd0HbpH6Dv-7DCh47YFpDlaNSvkQ,49
37
+ paststack-0.1.0.dist-info/top_level.txt,sha256=zKdgU3u1iisr9qqlj2H8cXpy71NgmGjmZfRhBnZ6E9c,10
38
+ paststack-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ paststack = paststack.cli:main
@@ -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 @@
1
+ paststack