fabrik-fastapi 1.0.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.
- fabrik_fastapi-1.0.0/ARCHITECTURE.md +414 -0
- fabrik_fastapi-1.0.0/LICENSE +21 -0
- fabrik_fastapi-1.0.0/MANIFEST.in +26 -0
- fabrik_fastapi-1.0.0/PKG-INFO +220 -0
- fabrik_fastapi-1.0.0/README.md +187 -0
- fabrik_fastapi-1.0.0/docs/PUBLISHING.md +192 -0
- fabrik_fastapi-1.0.0/docs/USAGE.md +619 -0
- fabrik_fastapi-1.0.0/fabrik/__init__.py +18 -0
- fabrik_fastapi-1.0.0/fabrik/__main__.py +8 -0
- fabrik_fastapi-1.0.0/fabrik/core/.dockerignore +7 -0
- fabrik_fastapi-1.0.0/fabrik/core/.env.example +9 -0
- fabrik_fastapi-1.0.0/fabrik/core/.gitignore +12 -0
- fabrik_fastapi-1.0.0/fabrik/core/_templates/Dockerfile.tpl +7 -0
- fabrik_fastapi-1.0.0/fabrik/core/_templates/env.tpl +8 -0
- fabrik_fastapi-1.0.0/fabrik/core/_templates/main.py.tpl +86 -0
- fabrik_fastapi-1.0.0/fabrik/core/alembic/README +1 -0
- fabrik_fastapi-1.0.0/fabrik/core/alembic/env.py +42 -0
- fabrik_fastapi-1.0.0/fabrik/core/alembic/script.py.mako +23 -0
- fabrik_fastapi-1.0.0/fabrik/core/alembic/versions/.gitkeep +0 -0
- fabrik_fastapi-1.0.0/fabrik/core/alembic.ini +39 -0
- fabrik_fastapi-1.0.0/fabrik/core/create_superuser.py +56 -0
- fabrik_fastapi-1.0.0/fabrik/core/docker-compose.yml +34 -0
- fabrik_fastapi-1.0.0/fabrik/core/pytest.ini +3 -0
- fabrik_fastapi-1.0.0/fabrik/core/requirements.txt +38 -0
- fabrik_fastapi-1.0.0/fabrik/core/src/__init__.py +0 -0
- fabrik_fastapi-1.0.0/fabrik/core/src/admin/__init__.py +0 -0
- fabrik_fastapi-1.0.0/fabrik/core/src/admin/router.py +563 -0
- fabrik_fastapi-1.0.0/fabrik/core/src/admin/static/admin.css +1086 -0
- fabrik_fastapi-1.0.0/fabrik/core/src/admin/templates/base.html +115 -0
- fabrik_fastapi-1.0.0/fabrik/core/src/admin/templates/dashboard.html +158 -0
- fabrik_fastapi-1.0.0/fabrik/core/src/admin/templates/form.html +71 -0
- fabrik_fastapi-1.0.0/fabrik/core/src/admin/templates/list.html +143 -0
- fabrik_fastapi-1.0.0/fabrik/core/src/admin/templates/login.html +36 -0
- fabrik_fastapi-1.0.0/fabrik/core/src/core/__init__.py +0 -0
- fabrik_fastapi-1.0.0/fabrik/core/src/core/config.py +35 -0
- fabrik_fastapi-1.0.0/fabrik/core/src/core/mixins.py +13 -0
- fabrik_fastapi-1.0.0/fabrik/core/src/core/pagination.py +32 -0
- fabrik_fastapi-1.0.0/fabrik/core/src/core/security.py +70 -0
- fabrik_fastapi-1.0.0/fabrik/core/src/database.py +25 -0
- fabrik_fastapi-1.0.0/fabrik/core/src/tasks.py +79 -0
- fabrik_fastapi-1.0.0/fabrik/core/src/users/__init__.py +0 -0
- fabrik_fastapi-1.0.0/fabrik/core/src/users/models.py +22 -0
- fabrik_fastapi-1.0.0/fabrik/core/src/users/router.py +74 -0
- fabrik_fastapi-1.0.0/fabrik/core/src/users/schemas.py +36 -0
- fabrik_fastapi-1.0.0/fabrik/core/src/users/service.py +56 -0
- fabrik_fastapi-1.0.0/fabrik/core/tests/__init__.py +0 -0
- fabrik_fastapi-1.0.0/fabrik/core/tests/conftest.py +54 -0
- fabrik_fastapi-1.0.0/fabrik/core/tests/test_tasks.py +30 -0
- fabrik_fastapi-1.0.0/fabrik/core/tests/test_users.py +55 -0
- fabrik_fastapi-1.0.0/fabrik/core/worker.py +18 -0
- fabrik_fastapi-1.0.0/fabrik/scaffold.py +1173 -0
- fabrik_fastapi-1.0.0/fabrik_fastapi.egg-info/PKG-INFO +220 -0
- fabrik_fastapi-1.0.0/fabrik_fastapi.egg-info/SOURCES.txt +57 -0
- fabrik_fastapi-1.0.0/fabrik_fastapi.egg-info/dependency_links.txt +1 -0
- fabrik_fastapi-1.0.0/fabrik_fastapi.egg-info/entry_points.txt +2 -0
- fabrik_fastapi-1.0.0/fabrik_fastapi.egg-info/requires.txt +4 -0
- fabrik_fastapi-1.0.0/fabrik_fastapi.egg-info/top_level.txt +1 -0
- fabrik_fastapi-1.0.0/pyproject.toml +67 -0
- fabrik_fastapi-1.0.0/setup.cfg +4 -0
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
# Architecture de Fabrik
|
|
2
|
+
|
|
3
|
+
> Document de reference sur les decisions de conception, le decoupage du code,
|
|
4
|
+
> et le cycle de vie d'un projet genere.
|
|
5
|
+
|
|
6
|
+
**Auteur :** Falandy Jean
|
|
7
|
+
**Version :** 1.0.0
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## 1. Philosophie
|
|
12
|
+
|
|
13
|
+
Fabrik est un **opinionated framework** construit au-dessus de FastAPI. Il
|
|
14
|
+
prend des decisions pour toi sur :
|
|
15
|
+
|
|
16
|
+
- L'asynchronisme (tout est `async`, pas de blocage thread sur les I/O DB)
|
|
17
|
+
- La structure de fichiers (`src/<module>/` avec separation
|
|
18
|
+
`models / schemas / service / router`)
|
|
19
|
+
- La securite par defaut (CORS strict, SECRET_KEY 256 bits, bcrypt, JWT)
|
|
20
|
+
- Les outils de developpement (tests isoles, migrations, admin UI)
|
|
21
|
+
|
|
22
|
+
**Ce que Fabrik refuse de faire :**
|
|
23
|
+
- Devenir un framework "universel" (pas de plugins, pas d'ecosysteme tiers)
|
|
24
|
+
- Cacher SQLAlchemy ou FastAPI (tu vois et controle tout)
|
|
25
|
+
- Etre retro-compatible avec les vieux Python (Python 3.13+ uniquement)
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## 2. Decoupage du repo
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
fabrik/ ← repo GitHub
|
|
33
|
+
├── pyproject.toml Metadata PyPI (nom, version, deps, entry_points)
|
|
34
|
+
├── MANIFEST.in Inclusion de core/ dans la sdist
|
|
35
|
+
├── README.md Vitrine + installation
|
|
36
|
+
├── ARCHITECTURE.md ← ce fichier
|
|
37
|
+
├── LICENSE MIT
|
|
38
|
+
├── docs/
|
|
39
|
+
│ ├── USAGE.md Guide utilisateur complet
|
|
40
|
+
│ └── PUBLISHING.md Workflow de release PyPI
|
|
41
|
+
├── .github/workflows/ci.yml CI (lance test-self a chaque commit)
|
|
42
|
+
└── fabrik/ ← PACKAGE Python publie sur PyPI
|
|
43
|
+
├── __init__.py Version + exports
|
|
44
|
+
├── __main__.py Pour `python -m fabrik`
|
|
45
|
+
├── scaffold.py Moteur CLI
|
|
46
|
+
└── core/ Templates copies dans chaque projet
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Quand l'utilisateur fait `pip install fabrik-cli`, c'est le dossier
|
|
50
|
+
**inner `fabrik/`** qui est installe dans le `site-packages` de son Python.
|
|
51
|
+
La commande `fabrik` est creee par l'entry point `[project.scripts]` de
|
|
52
|
+
`pyproject.toml` qui pointe vers `fabrik.scaffold:main`.
|
|
53
|
+
|
|
54
|
+
### 2.1 Pourquoi `core/` est un dossier separe ?
|
|
55
|
+
|
|
56
|
+
A la version 0 du prototype, **tous les templates etaient des chaines Python
|
|
57
|
+
embarquees dans `scaffold.py`** (3902 lignes). Probleme :
|
|
58
|
+
|
|
59
|
+
- Pas de coloration syntaxique dans l'IDE pour les templates Jinja2 / HTML / CSS
|
|
60
|
+
- Difficile de chercher et editer un fichier specifique
|
|
61
|
+
- Mauvais ratio signal/bruit a la lecture de `scaffold.py`
|
|
62
|
+
|
|
63
|
+
Depuis v1.0, **les templates vivent comme de vrais fichiers dans `core/`** :
|
|
64
|
+
|
|
65
|
+
- `core/<chemin>` -> fichier statique copie tel quel
|
|
66
|
+
- `core/_templates/*.tpl` -> fichier avec substitution `string.Template`
|
|
67
|
+
(`${title}`, `${port}`, `${secret_key}`, `${db_url}`)
|
|
68
|
+
|
|
69
|
+
`build_files()` est passe de 2750 lignes hardcodees a 20 lignes qui font un
|
|
70
|
+
`rglob` sur `core/`.
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
## 3. Cycle de vie d'un projet
|
|
75
|
+
|
|
76
|
+
### 3.1 `scaffold.py new mon-api`
|
|
77
|
+
|
|
78
|
+
```
|
|
79
|
+
cmd_new()
|
|
80
|
+
│
|
|
81
|
+
├── build_files(title, port, db_url)
|
|
82
|
+
│ └── walk core/ -> dict {chemin: contenu}
|
|
83
|
+
│ └── string.Template.substitute() pour core/_templates/*.tpl
|
|
84
|
+
│
|
|
85
|
+
├── Pour chaque (chemin, contenu) : f.write_text(contenu)
|
|
86
|
+
│
|
|
87
|
+
├── Ecrit .scaffold-version (JSON : version + date + patches_applied)
|
|
88
|
+
│
|
|
89
|
+
├── subprocess: python -m venv venv
|
|
90
|
+
├── subprocess: venv/pip install -r requirements.txt
|
|
91
|
+
└── subprocess: venv/python -m alembic revision/upgrade
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### 3.2 `scaffold.py add videos` (depuis la racine du projet)
|
|
95
|
+
|
|
96
|
+
```
|
|
97
|
+
cmd_add()
|
|
98
|
+
│
|
|
99
|
+
├── Genere les 5 fichiers du module
|
|
100
|
+
│ (models.py, schemas.py, service.py, router.py, test_videos.py)
|
|
101
|
+
│ via les Templates string.Template MODULE_MODELS, MODULE_SCHEMAS, ...
|
|
102
|
+
│
|
|
103
|
+
├── Auto-wiring (idempotent) :
|
|
104
|
+
│ ├── main.py : ajoute import + include_router
|
|
105
|
+
│ ├── alembic/env.py : ajoute import src.videos.models
|
|
106
|
+
│ └── src/users/models.py : ajoute relation back_populates
|
|
107
|
+
│
|
|
108
|
+
├── subprocess: alembic revision --autogenerate -m add_videos
|
|
109
|
+
├── subprocess: alembic upgrade head
|
|
110
|
+
└── subprocess: pytest tests/test_videos.py
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
L'auto-wiring utilise `_insert_after(content, anchor, new_line)` qui :
|
|
114
|
+
1. Verifie que la ligne (strip) n'est pas deja presente dans le fichier
|
|
115
|
+
2. Trouve la premiere occurrence de l'anchor
|
|
116
|
+
3. Insere la nouvelle ligne juste apres
|
|
117
|
+
|
|
118
|
+
Cette idempotence permet de relancer `add` sans dupliquer les imports.
|
|
119
|
+
|
|
120
|
+
### 3.3 `scaffold.py upgrade` (depuis la racine du projet)
|
|
121
|
+
|
|
122
|
+
```
|
|
123
|
+
cmd_upgrade()
|
|
124
|
+
│
|
|
125
|
+
├── Lit .scaffold-version -> version courante du projet
|
|
126
|
+
├── Compare avec SCAFFOLD_VERSION (constante en haut de scaffold.py)
|
|
127
|
+
├── Si projet < scaffold :
|
|
128
|
+
│ └── Pour chaque patch dans PATCHES dont [from, to] est compris :
|
|
129
|
+
│ └── patch["apply"](root) ← fonction idempotente
|
|
130
|
+
│ └── Met a jour .scaffold-version
|
|
131
|
+
│
|
|
132
|
+
└── Si projet >= scaffold : "deja a jour"
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Les fonctions de patch :
|
|
136
|
+
- Recoivent `root: Path` (racine du projet a patcher)
|
|
137
|
+
- Doivent etre **idempotentes** : safe a relancer N fois
|
|
138
|
+
- Doivent ecrire un backup `.bak` avant tout ecrasement destructif
|
|
139
|
+
- Retournent un `dict {chemin: statut}` pour le rapport
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
## 4. Decisions de conception : le projet genere
|
|
144
|
+
|
|
145
|
+
### 4.1 Pourquoi `src/<module>/` au lieu d'un layout plat ?
|
|
146
|
+
|
|
147
|
+
Le decoupage en `models / schemas / service / router` impose **la separation
|
|
148
|
+
des responsabilites** :
|
|
149
|
+
|
|
150
|
+
- `models.py` : ORM SQLAlchemy (donnees + relations)
|
|
151
|
+
- `schemas.py` : validation Pydantic (input/output API)
|
|
152
|
+
- `service.py` : logique metier (operations sur la DB)
|
|
153
|
+
- `router.py` : routes HTTP (mapping URL -> service)
|
|
154
|
+
|
|
155
|
+
Effet de bord : chaque module est **extractible en microservice** plus tard
|
|
156
|
+
sans refactor douloureux.
|
|
157
|
+
|
|
158
|
+
### 4.2 Pourquoi tout en async ?
|
|
159
|
+
|
|
160
|
+
FastAPI + Starlette + uvicorn sont conçus pour async. Une route sync bloque
|
|
161
|
+
le worker pendant l'attente DB. Avec `AsyncSession` + `asyncpg`, un seul
|
|
162
|
+
process Python peut servir des milliers de requetes/seconde.
|
|
163
|
+
|
|
164
|
+
Cout : la syntaxe `await` partout, et `db.execute(select(...))` au lieu de
|
|
165
|
+
`db.query(...).filter(...).all()`. Mais c'est le standard SQLAlchemy 2.0.
|
|
166
|
+
|
|
167
|
+
### 4.3 Pourquoi un `lifespan` ?
|
|
168
|
+
|
|
169
|
+
```python
|
|
170
|
+
@asynccontextmanager
|
|
171
|
+
async def lifespan(app: FastAPI):
|
|
172
|
+
async with engine.begin() as conn:
|
|
173
|
+
await conn.run_sync(Base.metadata.create_all)
|
|
174
|
+
yield
|
|
175
|
+
await engine.dispose()
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
Le lifespan remplace l'ancien `@app.on_event("startup")` deprecie. Il :
|
|
179
|
+
- Cree les tables si elles n'existent pas (utile en dev sans Alembic)
|
|
180
|
+
- Garantit le `dispose()` du pool de connexions a l'arret (pas de leak)
|
|
181
|
+
- Est `async` natif (vs les hooks synchrones de l'ancienne API)
|
|
182
|
+
|
|
183
|
+
### 4.4 Pourquoi SECRET_KEY est generee a chaque `new` ?
|
|
184
|
+
|
|
185
|
+
`secrets.token_urlsafe(32)` = 256 bits d'entropie. Chaque projet a son propre
|
|
186
|
+
secret des le depart, jamais commit accidentellement (le `.env` est dans
|
|
187
|
+
`.gitignore`). PyJWT exige 32+ bytes pour HMAC-SHA256.
|
|
188
|
+
|
|
189
|
+
### 4.5 Pourquoi CORS strict par defaut ?
|
|
190
|
+
|
|
191
|
+
```python
|
|
192
|
+
BACKEND_CORS_ORIGINS=http://localhost:3000,http://localhost:5173
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
Avec `allow_origins=["*"]` + `allow_credentials=True`, Starlette desactive
|
|
196
|
+
silencieusement les cookies (specification CORS). En forcant une liste
|
|
197
|
+
explicite, on evite cette piege ET on empeche un site malveillant de
|
|
198
|
+
proxifier l'API au nom de l'utilisateur.
|
|
199
|
+
|
|
200
|
+
### 4.6 Pourquoi tests isoles via fixture `client` ?
|
|
201
|
+
|
|
202
|
+
```python
|
|
203
|
+
# tests/conftest.py
|
|
204
|
+
@pytest_asyncio.fixture
|
|
205
|
+
async def client(test_engine):
|
|
206
|
+
async def override_get_db():
|
|
207
|
+
async with TestSession() as session:
|
|
208
|
+
yield session
|
|
209
|
+
app.dependency_overrides[get_db] = override_get_db
|
|
210
|
+
async with AsyncClient(transport=ASGITransport(app=app), ...) as c:
|
|
211
|
+
yield c
|
|
212
|
+
app.dependency_overrides.clear()
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
Chaque test recoit une **SQLite in-memory toute fraiche** (avec `StaticPool`
|
|
216
|
+
pour partager la meme connexion entre fixture et requete). La vraie `app.db`
|
|
217
|
+
de dev n'est jamais touchee par pytest.
|
|
218
|
+
|
|
219
|
+
---
|
|
220
|
+
|
|
221
|
+
## 5. Decisions de conception : l'admin UI
|
|
222
|
+
|
|
223
|
+
### 5.1 Auto-discovery via `Base.registry.mappers`
|
|
224
|
+
|
|
225
|
+
Plutot que de demander aux utilisateurs de declarer leurs modeles dans
|
|
226
|
+
l'admin (a la Django `admin.site.register()`), Fabrik **scanne dynamiquement
|
|
227
|
+
SQLAlchemy** :
|
|
228
|
+
|
|
229
|
+
```python
|
|
230
|
+
def get_admin_models() -> dict:
|
|
231
|
+
return {m.class_.__tablename__: m.class_ for m in Base.registry.mappers}
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
Resultat : des qu'un module ajoute une classe heritant de `Base`, elle
|
|
235
|
+
apparait dans la sidebar. Zero configuration.
|
|
236
|
+
|
|
237
|
+
### 5.2 Formulaires generes par introspection des colonnes
|
|
238
|
+
|
|
239
|
+
`col_input_type(col)` mappe les types SQLAlchemy vers des `<input type="...">` :
|
|
240
|
+
|
|
241
|
+
| Type SQLAlchemy | input HTML |
|
|
242
|
+
|------------------------|-------------------|
|
|
243
|
+
| `Boolean` | `checkbox` |
|
|
244
|
+
| `Integer`, `Numeric` | `number` |
|
|
245
|
+
| `DateTime` | `datetime-local` |
|
|
246
|
+
| `Date` | `date` |
|
|
247
|
+
| nom contient "email" | `email` |
|
|
248
|
+
| nom == "password" | `password` |
|
|
249
|
+
| presence de FK | `select` (dropdown avec resolution display field) |
|
|
250
|
+
| autre | `text` |
|
|
251
|
+
|
|
252
|
+
### 5.3 FK dropdowns intelligents
|
|
253
|
+
|
|
254
|
+
Quand une colonne est une FK, l'admin :
|
|
255
|
+
1. Detecte la table cible via `col.foreign_keys`
|
|
256
|
+
2. Charge jusqu'a 500 lignes de la table cible
|
|
257
|
+
3. Choisit la meilleure colonne d'affichage : `email` > `name` > `title` >
|
|
258
|
+
`label` > `username` > `id`
|
|
259
|
+
4. Rend un `<select>` avec `<option value="{uuid}">email@example.com ({uuid_court})</option>`
|
|
260
|
+
|
|
261
|
+
### 5.4 Multi-column search (v1)
|
|
262
|
+
|
|
263
|
+
Recherche sans configuration : `?q=foo` -> ILIKE `%foo%` sur **toutes** les
|
|
264
|
+
colonnes `varchar`/`string`/`text` (sauf `password`), combinees en `or_(...)`.
|
|
265
|
+
|
|
266
|
+
### 5.5 Bulk delete (v1)
|
|
267
|
+
|
|
268
|
+
Checkboxes par ligne + action bar sticky. La route `POST /admin/{table}/bulk-delete`
|
|
269
|
+
recoit `ids[]` et execute `delete().where(Model.id.in_(ids))` -- **une seule
|
|
270
|
+
requete SQL** pour N suppressions.
|
|
271
|
+
|
|
272
|
+
### 5.6 CSV export (v1)
|
|
273
|
+
|
|
274
|
+
`GET /admin/{table}/export.csv` -> `StreamingResponse` avec `csv.writer`.
|
|
275
|
+
Nom de fichier : `{table}-{YYYY-MM-DD}.csv`. Toutes les colonnes sauf
|
|
276
|
+
`password`.
|
|
277
|
+
|
|
278
|
+
### 5.7 Responsive design
|
|
279
|
+
|
|
280
|
+
Le CSS utilise un `@media (max-width: 768px)` qui transforme la sidebar en
|
|
281
|
+
**drawer slide-in** avec hamburger + backdrop. Les `input` mobile sont en
|
|
282
|
+
`font-size: 16px` pour empecher le zoom iOS au focus.
|
|
283
|
+
|
|
284
|
+
---
|
|
285
|
+
|
|
286
|
+
## 6. Background tasks : pourquoi ARQ
|
|
287
|
+
|
|
288
|
+
### 6.1 Le besoin
|
|
289
|
+
|
|
290
|
+
Toute application un peu serieuse a besoin de **deleguer des operations
|
|
291
|
+
lentes** hors du cycle requete/reponse HTTP :
|
|
292
|
+
- Envoi d'emails / notifications push
|
|
293
|
+
- Ingestion / parsing de fichiers volumineux (PDFs, CSVs)
|
|
294
|
+
- Calculs longs (rapports, exports, machine learning)
|
|
295
|
+
- Appels d'API externes lents ou peu fiables (retry)
|
|
296
|
+
|
|
297
|
+
Faire ces operations dans la route bloque le worker et timeout cote client.
|
|
298
|
+
|
|
299
|
+
### 6.2 Pourquoi pas un worker Go ?
|
|
300
|
+
|
|
301
|
+
Tentation classique : "Python est lent, mettons un worker en Go pour la
|
|
302
|
+
performance." En realite, **95% des taches typiques sont I/O-bound** (attente
|
|
303
|
+
API externe, requete DB, lecture disque). Sur ces operations, Python async
|
|
304
|
+
= Go en performance, a la milliseconde pres.
|
|
305
|
+
|
|
306
|
+
Le cout d'ajouter Go est massif :
|
|
307
|
+
- Nouvelle toolchain (`go build`, `go mod`)
|
|
308
|
+
- 2e langage a maintenir / debugger
|
|
309
|
+
- 2e binaire / image Docker / pipeline de deploy
|
|
310
|
+
- Communication inter-langage (queue ou cgo) avec sa propre complexite
|
|
311
|
+
|
|
312
|
+
Reserve Go pour le 1% de cas ou tu as **vraiment** mesure que Python CPU
|
|
313
|
+
est le bottleneck (parsing binaire intensif, math sur grands tenseurs).
|
|
314
|
+
|
|
315
|
+
### 6.3 Pourquoi ARQ et pas Celery ?
|
|
316
|
+
|
|
317
|
+
| | Celery | ARQ |
|
|
318
|
+
|---|---|---|
|
|
319
|
+
| Age | 2009 | 2017 |
|
|
320
|
+
| Async natif | Non (sync, support async ajoute apres) | **Oui** |
|
|
321
|
+
| Broker | RabbitMQ/Redis/etc. | Redis uniquement |
|
|
322
|
+
| Taille code | Lourd | Leger (~3k lignes) |
|
|
323
|
+
| Battle-tested | Instagram, Mozilla, Pinterest | Plus modeste |
|
|
324
|
+
| Ecosysteme | Flower, beat, multiple plugins | Minimal |
|
|
325
|
+
| Cohabitation FastAPI | Demande plomberie | Naturelle |
|
|
326
|
+
|
|
327
|
+
ARQ est **conçu pour Python async** depuis le debut. Dans un projet ou
|
|
328
|
+
**tout** est `async def` (routes, services, dependances), Celery introduit
|
|
329
|
+
une rupture mentale (workers sync) ; ARQ reste coherent.
|
|
330
|
+
|
|
331
|
+
Pour des cas de scale extreme (millions de jobs/jour, multi-broker, monitoring
|
|
332
|
+
sophistique), Celery garde l'avantage. Pour 99% des projets, ARQ suffit
|
|
333
|
+
largement.
|
|
334
|
+
|
|
335
|
+
### 6.4 Architecture cote API
|
|
336
|
+
|
|
337
|
+
Le pool Redis est cree dans le `lifespan` :
|
|
338
|
+
|
|
339
|
+
```python
|
|
340
|
+
@asynccontextmanager
|
|
341
|
+
async def lifespan(app: FastAPI):
|
|
342
|
+
# ... create_all tables ...
|
|
343
|
+
try:
|
|
344
|
+
app.state.arq_pool = await create_pool(RedisSettings.from_dsn(settings.REDIS_URL))
|
|
345
|
+
except Exception as e:
|
|
346
|
+
logger.warning("Redis indisponible (%s) -- background tasks desactives", e)
|
|
347
|
+
app.state.arq_pool = None
|
|
348
|
+
yield
|
|
349
|
+
if app.state.arq_pool is not None:
|
|
350
|
+
await app.state.arq_pool.close()
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
**Degradation gracieuse** : si Redis n'est pas joignable au demarrage, l'app
|
|
354
|
+
demarre quand meme. Les routes qui veulent enqueue retournent 503 via la
|
|
355
|
+
dependance `get_arq`. Le reste (admin, CRUD users, API) fonctionne sans
|
|
356
|
+
difference.
|
|
357
|
+
|
|
358
|
+
### 6.5 Architecture cote worker
|
|
359
|
+
|
|
360
|
+
`worker.py` a la racine = entrypoint trivial qui appelle `run_worker(WorkerSettings)`.
|
|
361
|
+
`WorkerSettings` vit dans `src/tasks.py` (a cote des taches qu'il execute) :
|
|
362
|
+
|
|
363
|
+
- `functions: list` -> liste des fonctions appelables via `enqueue_job(name, ...)`
|
|
364
|
+
- `redis_settings` -> connexion Redis (meme URL que cote API)
|
|
365
|
+
- `max_jobs` -> concurrence par worker (10 par defaut)
|
|
366
|
+
- `job_timeout` -> timeout par tache (5 min par defaut)
|
|
367
|
+
|
|
368
|
+
Tu peux lancer N workers en parallele sur N machines : Redis joue le role
|
|
369
|
+
de broker partage. C'est exactement le pattern Celery sans la lourdeur.
|
|
370
|
+
|
|
371
|
+
---
|
|
372
|
+
|
|
373
|
+
## 7. Le mecanisme `test-self`
|
|
374
|
+
|
|
375
|
+
`cmd_test_self()` est la garantie que **Fabrik genere toujours un projet qui
|
|
376
|
+
demarre vraiment**. Le workflow :
|
|
377
|
+
|
|
378
|
+
1. `tempfile.mkdtemp()` -> projet jetable
|
|
379
|
+
2. `cmd_new(absolute_path, no_input=True)` -> generation complete
|
|
380
|
+
3. Verifications : venv existe, `.scaffold-version` ecrit, migration appliquee
|
|
381
|
+
4. `subprocess pytest tests/ -q` -> doit retourner 0
|
|
382
|
+
5. `subprocess uvicorn main:app --port <random>` en background
|
|
383
|
+
6. Attente de l'ouverture du port (timeout 25s)
|
|
384
|
+
7. HTTP GET sur `/`, `/admin/login`, `/docs` -> doit retourner 200
|
|
385
|
+
8. Kill du serveur
|
|
386
|
+
9. `cmd_add("articles")` -> test du module + auto-wiring
|
|
387
|
+
10. `ast.parse()` sur les 5 fichiers generes -> doit etre du Python valide
|
|
388
|
+
11. `shutil.rmtree(tmp)` (sauf si `--keep`)
|
|
389
|
+
|
|
390
|
+
Le CI (`.github/workflows/ci.yml`) lance ce test a chaque push -- un commit
|
|
391
|
+
qui casse la generation se voit immediatement.
|
|
392
|
+
|
|
393
|
+
---
|
|
394
|
+
|
|
395
|
+
## 8. Limites connues
|
|
396
|
+
|
|
397
|
+
- **Pas de plugins externes.** Fabrik est volontairement monolithique. Le
|
|
398
|
+
jour ou tu veux brancher un module tiers (ex: OAuth Google), tu copies le
|
|
399
|
+
code dans `src/<module>/` au lieu de `pip install fabrik-plugin-X`.
|
|
400
|
+
- **Python 3.13+ obligatoire.** On utilise les nouveautes (PEP 695, etc.) et
|
|
401
|
+
on ne supporte pas les versions plus anciennes.
|
|
402
|
+
- **PostgreSQL recommande en prod.** SQLite marche pour le dev mais
|
|
403
|
+
manque de concurrence pour la production.
|
|
404
|
+
- **L'admin scanne TOUS les modeles SQLAlchemy.** Pas (encore) de mecanisme
|
|
405
|
+
pour exclure des modeles internes.
|
|
406
|
+
|
|
407
|
+
---
|
|
408
|
+
|
|
409
|
+
## 9. Ressources
|
|
410
|
+
|
|
411
|
+
- [README.md](README.md) : presentation generale
|
|
412
|
+
- [docs/USAGE.md](docs/USAGE.md) : guide utilisateur complet
|
|
413
|
+
- [scaffold.py](scaffold.py) : code source du moteur
|
|
414
|
+
- [core/](core/) : templates copies dans chaque projet
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Falandy Jean
|
|
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,26 @@
|
|
|
1
|
+
# Inclut tous les fichiers de /core/ dans la distribution source (sdist).
|
|
2
|
+
# Necessaire car setuptools n'inclut pas les non-Python par defaut.
|
|
3
|
+
recursive-include fabrik/core *
|
|
4
|
+
recursive-include fabrik/core/.* *
|
|
5
|
+
|
|
6
|
+
# Dotfiles dans core/ (essentiels pour les projets generes)
|
|
7
|
+
include fabrik/core/.gitignore
|
|
8
|
+
include fabrik/core/.env.example
|
|
9
|
+
include fabrik/core/.dockerignore
|
|
10
|
+
include fabrik/core/alembic/versions/.gitkeep
|
|
11
|
+
|
|
12
|
+
# Meta du repo (pour la sdist)
|
|
13
|
+
include README.md
|
|
14
|
+
include LICENSE
|
|
15
|
+
include ARCHITECTURE.md
|
|
16
|
+
include pyproject.toml
|
|
17
|
+
include MANIFEST.in
|
|
18
|
+
recursive-include docs *.md
|
|
19
|
+
|
|
20
|
+
# Exclusions
|
|
21
|
+
global-exclude __pycache__
|
|
22
|
+
global-exclude *.py[cod]
|
|
23
|
+
global-exclude *.bak
|
|
24
|
+
global-exclude .DS_Store
|
|
25
|
+
prune .github
|
|
26
|
+
prune tests
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: fabrik-fastapi
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Generateur de projet FastAPI async + opinionated (auth JWT, admin UI, ARQ, tests)
|
|
5
|
+
Author-email: Falandy Jean <falandyjean@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/FalandyJEAN/fabrik
|
|
8
|
+
Project-URL: Repository, https://github.com/FalandyJEAN/fabrik
|
|
9
|
+
Project-URL: Documentation, https://github.com/FalandyJEAN/fabrik/blob/main/docs/USAGE.md
|
|
10
|
+
Project-URL: Issues, https://github.com/FalandyJEAN/fabrik/issues
|
|
11
|
+
Project-URL: Changelog, https://github.com/FalandyJEAN/fabrik/releases
|
|
12
|
+
Project-URL: Logo, https://raw.githubusercontent.com/FalandyJEAN/fabrik/main/docs/assets/logo.png
|
|
13
|
+
Keywords: fastapi,scaffold,generator,boilerplate,async,sqlalchemy,alembic,admin,jwt,cli
|
|
14
|
+
Classifier: Development Status :: 4 - Beta
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
17
|
+
Classifier: Operating System :: OS Independent
|
|
18
|
+
Classifier: Programming Language :: Python
|
|
19
|
+
Classifier: Programming Language :: Python :: 3
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Topic :: Software Development :: Code Generators
|
|
22
|
+
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
|
|
23
|
+
Classifier: Topic :: Internet :: WWW/HTTP :: HTTP Servers
|
|
24
|
+
Classifier: Framework :: FastAPI
|
|
25
|
+
Classifier: Environment :: Console
|
|
26
|
+
Requires-Python: >=3.13
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
License-File: LICENSE
|
|
29
|
+
Provides-Extra: dev
|
|
30
|
+
Requires-Dist: build>=1.0; extra == "dev"
|
|
31
|
+
Requires-Dist: twine>=5.0; extra == "dev"
|
|
32
|
+
Dynamic: license-file
|
|
33
|
+
|
|
34
|
+
<p align="center">
|
|
35
|
+
<img src="docs/assets/logo.png" alt="Fabrik logo" width="200" />
|
|
36
|
+
</p>
|
|
37
|
+
|
|
38
|
+
<h1 align="center">Fabrik</h1>
|
|
39
|
+
|
|
40
|
+
<p align="center">
|
|
41
|
+
<strong>Generateur de projet FastAPI async + opinionated.</strong><br>
|
|
42
|
+
En 60 secondes : auth JWT, admin UI auto-decouverte, tests isoles,<br>
|
|
43
|
+
background tasks (ARQ), migrations Alembic, CORS strict, responsive.
|
|
44
|
+
</p>
|
|
45
|
+
|
|
46
|
+
[](https://pypi.org/project/fabrik-cli/)
|
|
47
|
+
[](https://www.python.org/downloads/)
|
|
48
|
+
[](https://github.com/FalandyJEAN/fabrik/actions/workflows/ci.yml)
|
|
49
|
+
[](https://opensource.org/licenses/MIT)
|
|
50
|
+
|
|
51
|
+
**Version :** `1.0.0` · **Auteur :** Falandy Jean · **Licence :** MIT
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## Installation
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
pip install fabrik-cli
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Verifier l'installation :
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
fabrik --help
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Pre-requis : **Python 3.13+**.
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## 60 secondes pour demarrer
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
fabrik new mon-api
|
|
75
|
+
cd mon-api
|
|
76
|
+
venv\Scripts\activate # Windows
|
|
77
|
+
# source venv/bin/activate # Linux/Mac
|
|
78
|
+
python create_superuser.py
|
|
79
|
+
python -m uvicorn main:app --reload
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Ouvre :
|
|
83
|
+
- **Admin UI** : http://127.0.0.1:8000/admin
|
|
84
|
+
- **Swagger** : http://127.0.0.1:8000/docs
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
## Ce que tu obtiens out-of-the-box
|
|
89
|
+
|
|
90
|
+
| Domaine | Choix par defaut |
|
|
91
|
+
|--------------------|---------------------------------------------------------------|
|
|
92
|
+
| Framework | FastAPI 0.136 + Starlette |
|
|
93
|
+
| ORM | SQLAlchemy 2.0 **async** (`AsyncSession`) |
|
|
94
|
+
| Driver DB | `aiosqlite` (dev) / `asyncpg` (prod PostgreSQL) |
|
|
95
|
+
| Auth | JWT (access + refresh) avec bcrypt |
|
|
96
|
+
| Admin | UI auto-decouverte responsive, multi-search, bulk delete, CSV |
|
|
97
|
+
| Migrations | Alembic (autogenerate) |
|
|
98
|
+
| Background tasks | ARQ (Redis-backed, async-native) avec degradation gracieuse |
|
|
99
|
+
| Tests | pytest + pytest-asyncio + DB SQLite in-memory isolee |
|
|
100
|
+
| CORS | Strict via `BACKEND_CORS_ORIGINS` (pas de `*`) |
|
|
101
|
+
| Config | `pydantic-settings` (12-factor) |
|
|
102
|
+
| Docker | `Dockerfile` Python 3.13-slim + `docker-compose.yml` |
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## Les 4 commandes
|
|
107
|
+
|
|
108
|
+
| Commande | Effet |
|
|
109
|
+
|-------------------------------------|---------------------------------------------------------------|
|
|
110
|
+
| `fabrik new <nom>` | Genere un projet complet (venv + deps + migration initiale) |
|
|
111
|
+
| `fabrik add <module>` | Ajoute un module CRUD (auto-wire + migration + tests) |
|
|
112
|
+
| `fabrik upgrade` | Met a jour un projet existant a la derniere version |
|
|
113
|
+
| `fabrik test-self` | Meta-test : verifie que le scaffold genere un projet OK |
|
|
114
|
+
|
|
115
|
+
Detail complet dans [docs/USAGE.md](docs/USAGE.md).
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
## Pourquoi Fabrik ?
|
|
120
|
+
|
|
121
|
+
Fabrik couvre l'angle mort entre **Django** (rigide, monolithique, sync) et
|
|
122
|
+
**FastAPI nu** (zero opinion, 2 jours de plomberie a chaque projet).
|
|
123
|
+
|
|
124
|
+
| | FastAPI standard | Django | **Fabrik** |
|
|
125
|
+
|---|---|---|---|
|
|
126
|
+
| Stack async | oui | non | **oui** |
|
|
127
|
+
| Auth JWT prete | non | tierce | **oui** |
|
|
128
|
+
| Admin UI auto-genere | non | oui (sync) | **oui (async, responsive)** |
|
|
129
|
+
| Background tasks | non | Celery (tiers) | **oui (ARQ inclus)** |
|
|
130
|
+
| Tests isoles fournis | non | oui | **oui** |
|
|
131
|
+
| Architecture modulaire | a faire | apps | **`src/<module>/`** |
|
|
132
|
+
| Migration de projet | non | manuel | **`fabrik upgrade`** |
|
|
133
|
+
| Demarrage zero-config | 2j | 30 min | **60 secondes** |
|
|
134
|
+
|
|
135
|
+
Detail des choix de conception : [ARCHITECTURE.md](ARCHITECTURE.md).
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
## Structure d'un projet genere
|
|
140
|
+
|
|
141
|
+
```
|
|
142
|
+
mon-api/
|
|
143
|
+
├── main.py FastAPI app + lifespan + pool ARQ
|
|
144
|
+
├── worker.py Entrypoint ARQ (python worker.py)
|
|
145
|
+
├── docker-compose.yml Redis + (PostgreSQL optionnel)
|
|
146
|
+
├── .env SECRET_KEY 256 bits + CORS + DB + Redis
|
|
147
|
+
├── .scaffold-version Trace de la version Fabrik (pour upgrade)
|
|
148
|
+
├── create_superuser.py
|
|
149
|
+
├── requirements.txt
|
|
150
|
+
├── pytest.ini
|
|
151
|
+
├── alembic.ini + alembic/
|
|
152
|
+
├── src/
|
|
153
|
+
│ ├── database.py AsyncEngine + get_db
|
|
154
|
+
│ ├── tasks.py ARQ tasks + WorkerSettings + get_arq dep
|
|
155
|
+
│ ├── core/ Config, security, mixins, pagination
|
|
156
|
+
│ ├── users/ Module exemple (models/schemas/service/router)
|
|
157
|
+
│ └── admin/ UI auto-decouverte (router + templates + static)
|
|
158
|
+
└── tests/
|
|
159
|
+
├── conftest.py Fixtures async (DB in-memory isolee)
|
|
160
|
+
├── test_users.py
|
|
161
|
+
└── test_tasks.py
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
---
|
|
165
|
+
|
|
166
|
+
## Architecture du repo Fabrik
|
|
167
|
+
|
|
168
|
+
```
|
|
169
|
+
fabrik/ Repo GitHub
|
|
170
|
+
├── pyproject.toml Package metadata (PyPI)
|
|
171
|
+
├── MANIFEST.in
|
|
172
|
+
├── README.md, ARCHITECTURE.md, LICENSE
|
|
173
|
+
├── docs/
|
|
174
|
+
│ ├── USAGE.md Guide utilisateur
|
|
175
|
+
│ └── PUBLISHING.md Workflow de release PyPI
|
|
176
|
+
├── .github/workflows/ci.yml CI : lance test-self a chaque commit
|
|
177
|
+
└── fabrik/ Package Python
|
|
178
|
+
├── __init__.py
|
|
179
|
+
├── __main__.py Pour `python -m fabrik`
|
|
180
|
+
├── scaffold.py Moteur CLI
|
|
181
|
+
└── core/ Templates copies a chaque `new`
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
---
|
|
185
|
+
|
|
186
|
+
## Developpement local
|
|
187
|
+
|
|
188
|
+
Si tu veux contribuer ou tester en local sans publier sur PyPI :
|
|
189
|
+
|
|
190
|
+
```bash
|
|
191
|
+
git clone https://github.com/FalandyJEAN/fabrik.git
|
|
192
|
+
cd fabrik
|
|
193
|
+
pip install -e . # installation en mode editable
|
|
194
|
+
fabrik test-self # verifie que tout fonctionne
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
Tout changement qui modifie la structure des projets generes doit :
|
|
198
|
+
|
|
199
|
+
1. Bumper `SCAFFOLD_VERSION` dans `fabrik/scaffold.py` (et `version` dans `pyproject.toml`)
|
|
200
|
+
2. Ajouter une fonction `patch_vN_to_vM(root)` **idempotente**
|
|
201
|
+
3. L'enregistrer dans `PATCHES`
|
|
202
|
+
4. `fabrik test-self` doit passer en local et en CI
|
|
203
|
+
|
|
204
|
+
---
|
|
205
|
+
|
|
206
|
+
## Publication PyPI
|
|
207
|
+
|
|
208
|
+
Workflow complet dans [docs/PUBLISHING.md](docs/PUBLISHING.md). En resume :
|
|
209
|
+
|
|
210
|
+
```bash
|
|
211
|
+
pip install -e ".[dev]"
|
|
212
|
+
python -m build
|
|
213
|
+
twine upload dist/*
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
---
|
|
217
|
+
|
|
218
|
+
## Licence
|
|
219
|
+
|
|
220
|
+
MIT © 2026 Falandy Jean — voir [LICENSE](LICENSE).
|