fabrik-fastapi 1.0.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.
- fabrik/__init__.py +18 -0
- fabrik/__main__.py +8 -0
- fabrik/core/.dockerignore +7 -0
- fabrik/core/.env.example +9 -0
- fabrik/core/.gitignore +12 -0
- fabrik/core/_templates/Dockerfile.tpl +7 -0
- fabrik/core/_templates/env.tpl +8 -0
- fabrik/core/_templates/main.py.tpl +86 -0
- fabrik/core/alembic/README +1 -0
- fabrik/core/alembic/env.py +42 -0
- fabrik/core/alembic/script.py.mako +23 -0
- fabrik/core/alembic/versions/.gitkeep +0 -0
- fabrik/core/alembic.ini +39 -0
- fabrik/core/create_superuser.py +56 -0
- fabrik/core/docker-compose.yml +34 -0
- fabrik/core/pytest.ini +3 -0
- fabrik/core/requirements.txt +38 -0
- fabrik/core/src/__init__.py +0 -0
- fabrik/core/src/admin/__init__.py +0 -0
- fabrik/core/src/admin/router.py +563 -0
- fabrik/core/src/admin/static/admin.css +1086 -0
- fabrik/core/src/admin/templates/base.html +115 -0
- fabrik/core/src/admin/templates/dashboard.html +158 -0
- fabrik/core/src/admin/templates/form.html +71 -0
- fabrik/core/src/admin/templates/list.html +143 -0
- fabrik/core/src/admin/templates/login.html +36 -0
- fabrik/core/src/core/__init__.py +0 -0
- fabrik/core/src/core/config.py +35 -0
- fabrik/core/src/core/mixins.py +13 -0
- fabrik/core/src/core/pagination.py +32 -0
- fabrik/core/src/core/security.py +70 -0
- fabrik/core/src/database.py +25 -0
- fabrik/core/src/tasks.py +79 -0
- fabrik/core/src/users/__init__.py +0 -0
- fabrik/core/src/users/models.py +22 -0
- fabrik/core/src/users/router.py +74 -0
- fabrik/core/src/users/schemas.py +36 -0
- fabrik/core/src/users/service.py +56 -0
- fabrik/core/tests/__init__.py +0 -0
- fabrik/core/tests/conftest.py +54 -0
- fabrik/core/tests/test_tasks.py +30 -0
- fabrik/core/tests/test_users.py +55 -0
- fabrik/core/worker.py +18 -0
- fabrik/scaffold.py +1173 -0
- fabrik_fastapi-1.0.0.dist-info/METADATA +220 -0
- fabrik_fastapi-1.0.0.dist-info/RECORD +50 -0
- fabrik_fastapi-1.0.0.dist-info/WHEEL +5 -0
- fabrik_fastapi-1.0.0.dist-info/entry_points.txt +2 -0
- fabrik_fastapi-1.0.0.dist-info/licenses/LICENSE +21 -0
- fabrik_fastapi-1.0.0.dist-info/top_level.txt +1 -0
fabrik/__init__.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Fabrik -- Generateur de projet FastAPI async + opinionated.
|
|
3
|
+
|
|
4
|
+
Usage CLI :
|
|
5
|
+
fabrik new mon-api
|
|
6
|
+
fabrik add videos
|
|
7
|
+
fabrik upgrade
|
|
8
|
+
fabrik test-self
|
|
9
|
+
|
|
10
|
+
Voir : https://github.com/FalandyJEAN/fabrik
|
|
11
|
+
"""
|
|
12
|
+
__version__ = "1.0.0"
|
|
13
|
+
__author__ = "Falandy Jean"
|
|
14
|
+
__license__ = "MIT"
|
|
15
|
+
|
|
16
|
+
from fabrik.scaffold import SCAFFOLD_VERSION, build_files, main
|
|
17
|
+
|
|
18
|
+
__all__ = ["SCAFFOLD_VERSION", "build_files", "main", "__version__"]
|
fabrik/__main__.py
ADDED
fabrik/core/.env.example
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
SECRET_KEY=your_secret_key_here
|
|
2
|
+
DATABASE_URL=sqlite:///./app.db
|
|
3
|
+
# PostgreSQL: DATABASE_URL=postgresql://user:password@localhost:5432/dbname
|
|
4
|
+
# (le driver asyncpg/aiosqlite est ajoute automatiquement au runtime)
|
|
5
|
+
ACCESS_TOKEN_EXPIRE_MINUTES=30
|
|
6
|
+
REFRESH_TOKEN_EXPIRE_DAYS=7
|
|
7
|
+
# Origines autorisees (CORS) -- separees par des virgules
|
|
8
|
+
# En prod : https://mon-front.com,https://app.mon-front.com
|
|
9
|
+
BACKEND_CORS_ORIGINS=http://localhost:3000,http://localhost:5173,http://localhost:8080
|
fabrik/core/.gitignore
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
SECRET_KEY=${secret_key}
|
|
2
|
+
DATABASE_URL=${db_url}
|
|
3
|
+
ACCESS_TOKEN_EXPIRE_MINUTES=30
|
|
4
|
+
REFRESH_TOKEN_EXPIRE_DAYS=7
|
|
5
|
+
BACKEND_CORS_ORIGINS=http://localhost:3000,http://localhost:5173,http://localhost:8080
|
|
6
|
+
# Background tasks (ARQ + Redis). Si Redis est down, l'API marche quand meme
|
|
7
|
+
# (les routes qui veulent enqueue renvoient 503 Service Unavailable).
|
|
8
|
+
REDIS_URL=redis://localhost:6379
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from contextlib import asynccontextmanager
|
|
3
|
+
from fastapi import FastAPI
|
|
4
|
+
from fastapi.middleware.cors import CORSMiddleware
|
|
5
|
+
from fastapi.staticfiles import StaticFiles
|
|
6
|
+
import logging
|
|
7
|
+
import uvicorn
|
|
8
|
+
|
|
9
|
+
from arq import create_pool
|
|
10
|
+
from arq.connections import RedisSettings
|
|
11
|
+
|
|
12
|
+
from src.users.router import router as users_router
|
|
13
|
+
from src.admin.router import router as admin_router
|
|
14
|
+
from src.database import engine, Base
|
|
15
|
+
from src.core.config import settings
|
|
16
|
+
|
|
17
|
+
logging.basicConfig(
|
|
18
|
+
level=logging.INFO,
|
|
19
|
+
format="%(asctime)s %(levelname)-8s %(name)s %(message)s",
|
|
20
|
+
)
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@asynccontextmanager
|
|
25
|
+
async def lifespan(app: FastAPI):
|
|
26
|
+
# Cree les tables au demarrage (utile en dev ; en prod, prefere Alembic)
|
|
27
|
+
async with engine.begin() as conn:
|
|
28
|
+
await conn.run_sync(Base.metadata.create_all)
|
|
29
|
+
|
|
30
|
+
# Pool Redis pour ARQ (background tasks)
|
|
31
|
+
# Si Redis est down, l'API marche quand meme : les routes qui veulent
|
|
32
|
+
# enqueue renvoient 503 (voir src/tasks.py:get_arq).
|
|
33
|
+
# asyncio.wait_for(timeout=2) protege contre un hang (Redis injoignable
|
|
34
|
+
# mais TCP qui ne fail pas vite, ex: derriere un firewall qui drop).
|
|
35
|
+
app.state.arq_pool = None
|
|
36
|
+
try:
|
|
37
|
+
redis_settings = RedisSettings.from_dsn(settings.REDIS_URL)
|
|
38
|
+
# Force un conn_timeout court pour fail-fast en CI / dev sans Redis
|
|
39
|
+
redis_settings.conn_timeout = 2
|
|
40
|
+
app.state.arq_pool = await asyncio.wait_for(
|
|
41
|
+
create_pool(redis_settings),
|
|
42
|
+
timeout=3.0,
|
|
43
|
+
)
|
|
44
|
+
logger.info("ARQ pool connecte a %s", settings.REDIS_URL)
|
|
45
|
+
except (asyncio.TimeoutError, Exception) as e:
|
|
46
|
+
logger.warning("Redis indisponible (%s) -- background tasks desactives",
|
|
47
|
+
type(e).__name__)
|
|
48
|
+
|
|
49
|
+
yield
|
|
50
|
+
|
|
51
|
+
if app.state.arq_pool is not None:
|
|
52
|
+
await app.state.arq_pool.close()
|
|
53
|
+
await engine.dispose()
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
app = FastAPI(title="${title}", lifespan=lifespan)
|
|
57
|
+
|
|
58
|
+
app.add_middleware(
|
|
59
|
+
CORSMiddleware,
|
|
60
|
+
allow_origins=settings.cors_origins,
|
|
61
|
+
allow_credentials=True,
|
|
62
|
+
allow_methods=["*"],
|
|
63
|
+
allow_headers=["*"],
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
app.mount("/admin/static", StaticFiles(directory="src/admin/static"), name="admin_static")
|
|
67
|
+
|
|
68
|
+
app.include_router(users_router)
|
|
69
|
+
app.include_router(admin_router)
|
|
70
|
+
|
|
71
|
+
# ── Tes modules ici ──────────────────────────────────────────────────────────
|
|
72
|
+
# from src.videos.router import router as videos_router
|
|
73
|
+
# app.include_router(videos_router)
|
|
74
|
+
# (les modules sont detectes automatiquement dans l'admin via Base.registry)
|
|
75
|
+
|
|
76
|
+
@app.get("/")
|
|
77
|
+
async def health_check():
|
|
78
|
+
arq_pool = getattr(app.state, "arq_pool", None)
|
|
79
|
+
return {
|
|
80
|
+
"status": "ok",
|
|
81
|
+
"project": "${title}",
|
|
82
|
+
"background_tasks": arq_pool is not None,
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if __name__ == "__main__":
|
|
86
|
+
uvicorn.run("main:app", host="127.0.0.1", port=${port}, reload=True)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Generic single-database configuration.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
from logging.config import fileConfig
|
|
2
|
+
from sqlalchemy import engine_from_config, pool
|
|
3
|
+
from alembic import context
|
|
4
|
+
from src.core.config import settings
|
|
5
|
+
from src.database import Base
|
|
6
|
+
import src.users.models # noqa: F401
|
|
7
|
+
# import src.videos.models # noqa: F401 <-- ajoute tes modules ici
|
|
8
|
+
|
|
9
|
+
config = context.config
|
|
10
|
+
|
|
11
|
+
if config.config_file_name is not None:
|
|
12
|
+
fileConfig(config.config_file_name)
|
|
13
|
+
|
|
14
|
+
config.set_main_option("sqlalchemy.url", settings.DATABASE_URL)
|
|
15
|
+
target_metadata = Base.metadata
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def run_migrations_offline() -> None:
|
|
19
|
+
url = config.get_main_option("sqlalchemy.url")
|
|
20
|
+
context.configure(
|
|
21
|
+
url=url, target_metadata=target_metadata,
|
|
22
|
+
literal_binds=True, dialect_opts={"paramstyle": "named"},
|
|
23
|
+
)
|
|
24
|
+
with context.begin_transaction():
|
|
25
|
+
context.run_migrations()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def run_migrations_online() -> None:
|
|
29
|
+
connectable = engine_from_config(
|
|
30
|
+
config.get_section(config.config_ini_section, {}),
|
|
31
|
+
prefix="sqlalchemy.", poolclass=pool.NullPool,
|
|
32
|
+
)
|
|
33
|
+
with connectable.connect() as connection:
|
|
34
|
+
context.configure(connection=connection, target_metadata=target_metadata)
|
|
35
|
+
with context.begin_transaction():
|
|
36
|
+
context.run_migrations()
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
if context.is_offline_mode():
|
|
40
|
+
run_migrations_offline()
|
|
41
|
+
else:
|
|
42
|
+
run_migrations_online()
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""${message}
|
|
2
|
+
|
|
3
|
+
Revision ID: ${up_revision}
|
|
4
|
+
Revises: ${down_revision | comma,n}
|
|
5
|
+
Create Date: ${create_date}
|
|
6
|
+
"""
|
|
7
|
+
from typing import Sequence, Union
|
|
8
|
+
from alembic import op
|
|
9
|
+
import sqlalchemy as sa
|
|
10
|
+
${imports if imports else ""}
|
|
11
|
+
|
|
12
|
+
revision: str = ${repr(up_revision)}
|
|
13
|
+
down_revision: Union[str, None] = ${repr(down_revision)}
|
|
14
|
+
branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)}
|
|
15
|
+
depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)}
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def upgrade() -> None:
|
|
19
|
+
${upgrades if upgrades else "pass"}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def downgrade() -> None:
|
|
23
|
+
${downgrades if downgrades else "pass"}
|
|
File without changes
|
fabrik/core/alembic.ini
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
[alembic]
|
|
2
|
+
script_location = alembic
|
|
3
|
+
prepend_sys_path = .
|
|
4
|
+
version_path_separator = os
|
|
5
|
+
sqlalchemy.url = sqlite:///./app.db
|
|
6
|
+
|
|
7
|
+
[loggers]
|
|
8
|
+
keys = root,sqlalchemy,alembic
|
|
9
|
+
|
|
10
|
+
[handlers]
|
|
11
|
+
keys = console
|
|
12
|
+
|
|
13
|
+
[formatters]
|
|
14
|
+
keys = generic
|
|
15
|
+
|
|
16
|
+
[logger_root]
|
|
17
|
+
level = WARN
|
|
18
|
+
handlers = console
|
|
19
|
+
qualname =
|
|
20
|
+
|
|
21
|
+
[logger_sqlalchemy]
|
|
22
|
+
level = WARN
|
|
23
|
+
handlers =
|
|
24
|
+
qualname = sqlalchemy.engine
|
|
25
|
+
|
|
26
|
+
[logger_alembic]
|
|
27
|
+
level = INFO
|
|
28
|
+
handlers =
|
|
29
|
+
qualname = alembic
|
|
30
|
+
|
|
31
|
+
[handler_console]
|
|
32
|
+
class = StreamHandler
|
|
33
|
+
args = (sys.stderr,)
|
|
34
|
+
level = NOTSET
|
|
35
|
+
formatter = generic
|
|
36
|
+
|
|
37
|
+
[formatter_generic]
|
|
38
|
+
format = %(levelname)-5.5s [%(name)s] %(message)s
|
|
39
|
+
datefmt = %H:%M:%S
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Cree un super-utilisateur (admin) pour acceder a /admin
|
|
3
|
+
|
|
4
|
+
Usage : python create_superuser.py
|
|
5
|
+
"""
|
|
6
|
+
import asyncio
|
|
7
|
+
from getpass import getpass
|
|
8
|
+
from sqlalchemy import select
|
|
9
|
+
from src.database import AsyncSessionLocal, engine, Base
|
|
10
|
+
from src.users.models import User
|
|
11
|
+
from src.core.security import get_password_hash
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
async def main():
|
|
15
|
+
# S'assurer que les tables existent
|
|
16
|
+
async with engine.begin() as conn:
|
|
17
|
+
await conn.run_sync(Base.metadata.create_all)
|
|
18
|
+
|
|
19
|
+
print("\n=== Creation d'un super-utilisateur ===\n")
|
|
20
|
+
email = input("Email : ").strip()
|
|
21
|
+
password = getpass("Mot de passe : ")
|
|
22
|
+
confirm = getpass("Confirmer : ")
|
|
23
|
+
|
|
24
|
+
if password != confirm:
|
|
25
|
+
print("[!] Les mots de passe ne correspondent pas.")
|
|
26
|
+
return
|
|
27
|
+
if len(password) < 6:
|
|
28
|
+
print("[!] Mot de passe trop court (min 6 caracteres).")
|
|
29
|
+
return
|
|
30
|
+
|
|
31
|
+
async with AsyncSessionLocal() as db:
|
|
32
|
+
result = await db.execute(select(User).where(User.email == email))
|
|
33
|
+
existing = result.scalar_one_or_none()
|
|
34
|
+
if existing:
|
|
35
|
+
existing.password = get_password_hash(password)
|
|
36
|
+
existing.is_superuser = True
|
|
37
|
+
existing.is_active = True
|
|
38
|
+
await db.commit()
|
|
39
|
+
print(f"\n [OK] {email} promu super-utilisateur.")
|
|
40
|
+
else:
|
|
41
|
+
user = User(
|
|
42
|
+
email=email,
|
|
43
|
+
password=get_password_hash(password),
|
|
44
|
+
is_active=True,
|
|
45
|
+
is_superuser=True,
|
|
46
|
+
)
|
|
47
|
+
db.add(user)
|
|
48
|
+
await db.commit()
|
|
49
|
+
print(f"\n [OK] Super-utilisateur cree : {email}")
|
|
50
|
+
print(" Connectez-vous sur http://127.0.0.1:8000/admin/login\n")
|
|
51
|
+
|
|
52
|
+
await engine.dispose()
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
if __name__ == "__main__":
|
|
56
|
+
asyncio.run(main())
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Services de developpement (Redis pour ARQ, PostgreSQL optionnel).
|
|
2
|
+
# Demarre avec : docker compose up -d
|
|
3
|
+
# Arrete avec : docker compose down
|
|
4
|
+
|
|
5
|
+
services:
|
|
6
|
+
redis:
|
|
7
|
+
image: redis:7-alpine
|
|
8
|
+
container_name: fabrik_redis
|
|
9
|
+
ports:
|
|
10
|
+
- "6379:6379"
|
|
11
|
+
volumes:
|
|
12
|
+
- redis_data:/data
|
|
13
|
+
restart: unless-stopped
|
|
14
|
+
|
|
15
|
+
# Decommente si tu veux PostgreSQL en local au lieu de SQLite.
|
|
16
|
+
# Pense aussi a mettre a jour DATABASE_URL dans .env :
|
|
17
|
+
# DATABASE_URL=postgresql://fabrik:fabrik@localhost:5432/fabrik
|
|
18
|
+
#
|
|
19
|
+
# postgres:
|
|
20
|
+
# image: postgres:16-alpine
|
|
21
|
+
# container_name: fabrik_postgres
|
|
22
|
+
# environment:
|
|
23
|
+
# POSTGRES_USER: fabrik
|
|
24
|
+
# POSTGRES_PASSWORD: fabrik
|
|
25
|
+
# POSTGRES_DB: fabrik
|
|
26
|
+
# ports:
|
|
27
|
+
# - "5432:5432"
|
|
28
|
+
# volumes:
|
|
29
|
+
# - pg_data:/var/lib/postgresql/data
|
|
30
|
+
# restart: unless-stopped
|
|
31
|
+
|
|
32
|
+
volumes:
|
|
33
|
+
redis_data:
|
|
34
|
+
# pg_data:
|
fabrik/core/pytest.ini
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Bornes minimales avec caps majeurs : pip resout vers la derniere version
|
|
2
|
+
# compatible. Plus robuste qu'un pin exact (qui peut pointer vers une version
|
|
3
|
+
# non publiee si on regenere des annees plus tard).
|
|
4
|
+
|
|
5
|
+
# FastAPI stack
|
|
6
|
+
fastapi>=0.115,<1.0
|
|
7
|
+
uvicorn>=0.30,<1.0
|
|
8
|
+
|
|
9
|
+
# Database (async)
|
|
10
|
+
sqlalchemy>=2.0,<3.0
|
|
11
|
+
alembic>=1.13,<2.0
|
|
12
|
+
aiosqlite>=0.20,<1.0
|
|
13
|
+
asyncpg>=0.29,<1.0
|
|
14
|
+
greenlet>=3.0,<4.0
|
|
15
|
+
|
|
16
|
+
# Validation & config
|
|
17
|
+
pydantic>=2.9,<3.0
|
|
18
|
+
pydantic-settings>=2.5,<3.0
|
|
19
|
+
|
|
20
|
+
# Auth
|
|
21
|
+
pyjwt>=2.9,<3.0
|
|
22
|
+
bcrypt>=4.2,<6.0
|
|
23
|
+
|
|
24
|
+
# Background tasks
|
|
25
|
+
arq>=0.26,<1.0
|
|
26
|
+
redis>=5.0,<6.0
|
|
27
|
+
|
|
28
|
+
# Tests
|
|
29
|
+
pytest>=8.0,<9.0
|
|
30
|
+
pytest-asyncio>=0.24,<1.0
|
|
31
|
+
httpx>=0.27,<1.0
|
|
32
|
+
|
|
33
|
+
# Templates
|
|
34
|
+
jinja2>=3.1,<4.0
|
|
35
|
+
|
|
36
|
+
# Divers
|
|
37
|
+
python-dotenv>=1.0,<2.0
|
|
38
|
+
python-multipart>=0.0.12
|
|
File without changes
|
|
File without changes
|