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.
Files changed (50) hide show
  1. fabrik/__init__.py +18 -0
  2. fabrik/__main__.py +8 -0
  3. fabrik/core/.dockerignore +7 -0
  4. fabrik/core/.env.example +9 -0
  5. fabrik/core/.gitignore +12 -0
  6. fabrik/core/_templates/Dockerfile.tpl +7 -0
  7. fabrik/core/_templates/env.tpl +8 -0
  8. fabrik/core/_templates/main.py.tpl +86 -0
  9. fabrik/core/alembic/README +1 -0
  10. fabrik/core/alembic/env.py +42 -0
  11. fabrik/core/alembic/script.py.mako +23 -0
  12. fabrik/core/alembic/versions/.gitkeep +0 -0
  13. fabrik/core/alembic.ini +39 -0
  14. fabrik/core/create_superuser.py +56 -0
  15. fabrik/core/docker-compose.yml +34 -0
  16. fabrik/core/pytest.ini +3 -0
  17. fabrik/core/requirements.txt +38 -0
  18. fabrik/core/src/__init__.py +0 -0
  19. fabrik/core/src/admin/__init__.py +0 -0
  20. fabrik/core/src/admin/router.py +563 -0
  21. fabrik/core/src/admin/static/admin.css +1086 -0
  22. fabrik/core/src/admin/templates/base.html +115 -0
  23. fabrik/core/src/admin/templates/dashboard.html +158 -0
  24. fabrik/core/src/admin/templates/form.html +71 -0
  25. fabrik/core/src/admin/templates/list.html +143 -0
  26. fabrik/core/src/admin/templates/login.html +36 -0
  27. fabrik/core/src/core/__init__.py +0 -0
  28. fabrik/core/src/core/config.py +35 -0
  29. fabrik/core/src/core/mixins.py +13 -0
  30. fabrik/core/src/core/pagination.py +32 -0
  31. fabrik/core/src/core/security.py +70 -0
  32. fabrik/core/src/database.py +25 -0
  33. fabrik/core/src/tasks.py +79 -0
  34. fabrik/core/src/users/__init__.py +0 -0
  35. fabrik/core/src/users/models.py +22 -0
  36. fabrik/core/src/users/router.py +74 -0
  37. fabrik/core/src/users/schemas.py +36 -0
  38. fabrik/core/src/users/service.py +56 -0
  39. fabrik/core/tests/__init__.py +0 -0
  40. fabrik/core/tests/conftest.py +54 -0
  41. fabrik/core/tests/test_tasks.py +30 -0
  42. fabrik/core/tests/test_users.py +55 -0
  43. fabrik/core/worker.py +18 -0
  44. fabrik/scaffold.py +1173 -0
  45. fabrik_fastapi-1.0.0.dist-info/METADATA +220 -0
  46. fabrik_fastapi-1.0.0.dist-info/RECORD +50 -0
  47. fabrik_fastapi-1.0.0.dist-info/WHEEL +5 -0
  48. fabrik_fastapi-1.0.0.dist-info/entry_points.txt +2 -0
  49. fabrik_fastapi-1.0.0.dist-info/licenses/LICENSE +21 -0
  50. 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
@@ -0,0 +1,8 @@
1
+ """
2
+ Permet d'invoquer Fabrik via : `python -m fabrik new mon-api`
3
+ (equivalent a la commande `fabrik` installee via pip).
4
+ """
5
+ from fabrik.scaffold import main
6
+
7
+ if __name__ == "__main__":
8
+ main()
@@ -0,0 +1,7 @@
1
+ __pycache__/
2
+ *.pyc
3
+ *.db
4
+ .env
5
+ venv/
6
+ alembic/versions/
7
+ .pytest_cache/
@@ -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,12 @@
1
+ venv/
2
+ .venv/
3
+ __pycache__/
4
+ *.pyc
5
+ .env
6
+ *.db
7
+ alembic/versions/
8
+ .vscode/
9
+ .idea/
10
+ .pytest_cache/
11
+ htmlcov/
12
+ .coverage
@@ -0,0 +1,7 @@
1
+ FROM python:3.13-slim
2
+ WORKDIR /app
3
+ COPY requirements.txt .
4
+ RUN pip install --no-cache-dir -r requirements.txt
5
+ COPY . .
6
+ EXPOSE ${port}
7
+ CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "${port}"]
@@ -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
@@ -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,3 @@
1
+ [pytest]
2
+ asyncio_mode = auto
3
+ testpaths = tests
@@ -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