calypso-api 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.
- calypso_api/__init__.py +0 -0
- calypso_api/__main__.py +4 -0
- calypso_api/auth/__init__.py +0 -0
- calypso_api/cli.py +44 -0
- calypso_api/controllers/__init__.py +0 -0
- calypso_api/core/__init__.py +0 -0
- calypso_api/core/config.py +23 -0
- calypso_api/database/__init__.py +0 -0
- calypso_api/database/db.py +24 -0
- calypso_api/dependencies/__init__.py +0 -0
- calypso_api/helpers/__init__.py +0 -0
- calypso_api/main.py +38 -0
- calypso_api/models/__init__.py +0 -0
- calypso_api/repositories/__init__.py +0 -0
- calypso_api/routes/__init__.py +0 -0
- calypso_api/routes/health.py +8 -0
- calypso_api/scaffold.py +931 -0
- calypso_api/schemas/__init__.py +0 -0
- calypso_api/services/__init__.py +0 -0
- calypso_api/test/__init__.py +0 -0
- calypso_api/utils/__init__.py +0 -0
- calypso_api-0.1.0.dist-info/METADATA +81 -0
- calypso_api-0.1.0.dist-info/RECORD +25 -0
- calypso_api-0.1.0.dist-info/WHEEL +4 -0
- calypso_api-0.1.0.dist-info/entry_points.txt +2 -0
calypso_api/__init__.py
ADDED
|
File without changes
|
calypso_api/__main__.py
ADDED
|
File without changes
|
calypso_api/cli.py
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
|
|
2
|
+
import typer
|
|
3
|
+
import uvicorn
|
|
4
|
+
from calypso_api.core import config
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from calypso_api.scaffold import generate
|
|
7
|
+
|
|
8
|
+
app = typer.Typer()
|
|
9
|
+
|
|
10
|
+
@app.command()
|
|
11
|
+
def run(
|
|
12
|
+
host: str = "127.0.0.1",
|
|
13
|
+
port: int = 8000,
|
|
14
|
+
reload: bool = True
|
|
15
|
+
):
|
|
16
|
+
uvicorn.run(
|
|
17
|
+
"calypso_api.main:app",
|
|
18
|
+
host=host,
|
|
19
|
+
port=port,
|
|
20
|
+
reload=reload,
|
|
21
|
+
log_level="info" if config.DEBUG else "warning"
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
@app.command()
|
|
25
|
+
def shell():
|
|
26
|
+
import IPython
|
|
27
|
+
from calypso_api.database.db import engine
|
|
28
|
+
|
|
29
|
+
typer.echo("Opening shell...")
|
|
30
|
+
IPython.embed(header="API Template Shell", colors="neutral")
|
|
31
|
+
|
|
32
|
+
@app.command()
|
|
33
|
+
def init(
|
|
34
|
+
destino: Path = typer.Argument(..., file_okay=False, dir_okay=True, readable=True, writable=True, help="Directorio destino"),
|
|
35
|
+
nombre: str = typer.Argument(..., help="Nombre del proyecto"),
|
|
36
|
+
host: str = typer.Option("127.0.0.1", help="Host para la aplicación"),
|
|
37
|
+
port: int = typer.Option(8000, help="Puerto para la aplicación"),
|
|
38
|
+
docker: bool = typer.Option(True, help="Incluir configuración de Docker")
|
|
39
|
+
):
|
|
40
|
+
generate(destino, nombre, host, port, docker)
|
|
41
|
+
typer.echo(f"Estructura creada en {destino}")
|
|
42
|
+
|
|
43
|
+
if __name__ == "__main__":
|
|
44
|
+
app()
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
|
|
2
|
+
import os
|
|
3
|
+
from typing import List
|
|
4
|
+
from dotenv import load_dotenv
|
|
5
|
+
|
|
6
|
+
load_dotenv()
|
|
7
|
+
|
|
8
|
+
# Configuración de la API
|
|
9
|
+
API_KEY = os.environ.get('API_KEY', 'default_api_key')
|
|
10
|
+
SECRET_KEY = os.environ.get('SECRET_KEY', 'default_secret_key')
|
|
11
|
+
ALGORITHM = os.environ.get('ALGORITHM', 'HS256')
|
|
12
|
+
ACCESS_TOKEN_EXPIRE_MINUTES = 60 * 4
|
|
13
|
+
PROJECT_NAME: str = "API Template"
|
|
14
|
+
PROJECT_DESCRIPTION: str = "Template para nuevas APIs con FastAPI y Typer"
|
|
15
|
+
VERSION: str = "0.1.0"
|
|
16
|
+
API_PREFIX: str = "/api"
|
|
17
|
+
DEBUG: bool = os.environ.get('DEBUG', 'False').lower() == 'true'
|
|
18
|
+
|
|
19
|
+
# Base de datos
|
|
20
|
+
DATABASE_URL = os.environ.get('DATABASE_URL', "sqlite+aiosqlite:///./test.db")
|
|
21
|
+
|
|
22
|
+
# CORS
|
|
23
|
+
ORIGINS: List[str] = ["*"]
|
|
File without changes
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
|
|
2
|
+
from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker, AsyncSession
|
|
3
|
+
from sqlmodel import SQLModel
|
|
4
|
+
from calypso_api.core import config
|
|
5
|
+
from typing import AsyncGenerator
|
|
6
|
+
|
|
7
|
+
engine = create_async_engine(config.DATABASE_URL, echo=config.DEBUG, future=True)
|
|
8
|
+
|
|
9
|
+
async_session_factory = async_sessionmaker(
|
|
10
|
+
bind=engine,
|
|
11
|
+
class_=AsyncSession,
|
|
12
|
+
expire_on_commit=False,
|
|
13
|
+
autocommit=False,
|
|
14
|
+
autoflush=False,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
async def get_db() -> AsyncGenerator[AsyncSession, None]:
|
|
18
|
+
async with async_session_factory() as session:
|
|
19
|
+
yield session
|
|
20
|
+
|
|
21
|
+
async def init_db():
|
|
22
|
+
async with engine.begin() as conn:
|
|
23
|
+
# await conn.run_sync(SQLModel.metadata.drop_all)
|
|
24
|
+
await conn.run_sync(SQLModel.metadata.create_all)
|
|
File without changes
|
|
File without changes
|
calypso_api/main.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
|
|
2
|
+
from fastapi import FastAPI
|
|
3
|
+
from fastapi.middleware.cors import CORSMiddleware
|
|
4
|
+
from contextlib import asynccontextmanager
|
|
5
|
+
from calypso_api.core import config
|
|
6
|
+
from calypso_api.database.db import init_db
|
|
7
|
+
from calypso_api.routes import health
|
|
8
|
+
|
|
9
|
+
@asynccontextmanager
|
|
10
|
+
async def lifespan(app: FastAPI):
|
|
11
|
+
# Startup
|
|
12
|
+
await init_db()
|
|
13
|
+
yield
|
|
14
|
+
# Shutdown
|
|
15
|
+
|
|
16
|
+
app = FastAPI(
|
|
17
|
+
title=config.PROJECT_NAME,
|
|
18
|
+
description=config.PROJECT_DESCRIPTION,
|
|
19
|
+
version=config.VERSION,
|
|
20
|
+
lifespan=lifespan,
|
|
21
|
+
docs_url=f"{config.API_PREFIX}/docs",
|
|
22
|
+
redoc_url=f"{config.API_PREFIX}/redoc",
|
|
23
|
+
openapi_url=f"{config.API_PREFIX}/openapi.json",
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
app.add_middleware(
|
|
27
|
+
CORSMiddleware,
|
|
28
|
+
allow_origins=config.ORIGINS,
|
|
29
|
+
allow_credentials=True,
|
|
30
|
+
allow_methods=["*"],
|
|
31
|
+
allow_headers=["*"],
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
app.include_router(health.router, prefix=config.API_PREFIX)
|
|
35
|
+
|
|
36
|
+
@app.get("/")
|
|
37
|
+
async def root():
|
|
38
|
+
return {"message": f"Welcome to {config.PROJECT_NAME}"}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
calypso_api/scaffold.py
ADDED
|
@@ -0,0 +1,931 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
import os
|
|
3
|
+
|
|
4
|
+
def _write(path: Path, content: str):
|
|
5
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
6
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
7
|
+
f.write(content)
|
|
8
|
+
|
|
9
|
+
def create_dir_with_readme(root: Path, dir_name: str, readme_text: str):
|
|
10
|
+
path = root / dir_name
|
|
11
|
+
path.mkdir(parents=True, exist_ok=True)
|
|
12
|
+
_write(path / "__init__.py", "")
|
|
13
|
+
_write(path / "README.md", readme_text)
|
|
14
|
+
return path
|
|
15
|
+
|
|
16
|
+
# ------------------------------------------------------------------------------
|
|
17
|
+
# FILE CONTENT TEMPLATES
|
|
18
|
+
# ------------------------------------------------------------------------------
|
|
19
|
+
|
|
20
|
+
MAIN_PY = """from fastapi import FastAPI, Request, HTTPException
|
|
21
|
+
from fastapi.staticfiles import StaticFiles
|
|
22
|
+
from starlette.middleware.cors import CORSMiddleware
|
|
23
|
+
from starlette.middleware.gzip import GZipMiddleware
|
|
24
|
+
from fastapi.responses import JSONResponse, FileResponse
|
|
25
|
+
from slowapi import _rate_limit_exceeded_handler
|
|
26
|
+
from slowapi.errors import RateLimitExceeded
|
|
27
|
+
from dependencies.limitador import limiter
|
|
28
|
+
from database.db_service import create_db_and_tables
|
|
29
|
+
from core import config
|
|
30
|
+
from contextlib import asynccontextmanager
|
|
31
|
+
from database.db import session_manager
|
|
32
|
+
from core.exceptions import exception_handler, http_exception_handler
|
|
33
|
+
from utils.logger import configure_logger
|
|
34
|
+
from fastapi.openapi.docs import get_swagger_ui_html, get_redoc_html
|
|
35
|
+
|
|
36
|
+
# Routers
|
|
37
|
+
from auth.auth_routes import router as security_router
|
|
38
|
+
from routes.example_router import router as example_router
|
|
39
|
+
|
|
40
|
+
description = f\"\"\"
|
|
41
|
+
# {config.PROJECT_NAME}
|
|
42
|
+
|
|
43
|
+
*{config.PROJECT_DESCRIPTION}*
|
|
44
|
+
\"\"\"
|
|
45
|
+
|
|
46
|
+
logger = configure_logger(name=__name__)
|
|
47
|
+
|
|
48
|
+
@asynccontextmanager
|
|
49
|
+
async def lifespan(app: FastAPI):
|
|
50
|
+
\"\"\"
|
|
51
|
+
Función que maneja los eventos de inicio y cierre de la aplicación.
|
|
52
|
+
\"\"\"
|
|
53
|
+
if config.crear_usuarios_y_tablas:
|
|
54
|
+
await create_db_and_tables()
|
|
55
|
+
|
|
56
|
+
from utils.defaultUser import create_defaultAdmin_user
|
|
57
|
+
from database.db_service import get_session_context
|
|
58
|
+
async for session in get_session_context():
|
|
59
|
+
await create_defaultAdmin_user(db=session)
|
|
60
|
+
break
|
|
61
|
+
|
|
62
|
+
yield
|
|
63
|
+
|
|
64
|
+
if session_manager._engine is not None:
|
|
65
|
+
await session_manager.close()
|
|
66
|
+
logger.info("Conexión a la base de datos cerrada.")
|
|
67
|
+
|
|
68
|
+
app = FastAPI(
|
|
69
|
+
title=config.PROJECT_NAME,
|
|
70
|
+
description=description,
|
|
71
|
+
version=config.VERSION,
|
|
72
|
+
debug=config.DEBUG,
|
|
73
|
+
lifespan=lifespan,
|
|
74
|
+
docs_url=None,
|
|
75
|
+
redoc_url=None,
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
# Middlewares
|
|
79
|
+
app.add_middleware(
|
|
80
|
+
CORSMiddleware,
|
|
81
|
+
allow_origins=[config.origenes_permitidos],
|
|
82
|
+
allow_credentials=True,
|
|
83
|
+
allow_methods=["*"],
|
|
84
|
+
allow_headers=["*"],
|
|
85
|
+
)
|
|
86
|
+
app.add_middleware(GZipMiddleware, minimum_size=1000)
|
|
87
|
+
app.state.limiter = limiter
|
|
88
|
+
app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)
|
|
89
|
+
app.add_exception_handler(Exception, exception_handler)
|
|
90
|
+
app.add_exception_handler(HTTPException, http_exception_handler)
|
|
91
|
+
|
|
92
|
+
# Static files
|
|
93
|
+
app.mount("/static", StaticFiles(directory="static"), name="static")
|
|
94
|
+
|
|
95
|
+
@app.get("/favicon.ico", include_in_schema=False)
|
|
96
|
+
async def favicon():
|
|
97
|
+
return FileResponse("static/img/favicon.ico")
|
|
98
|
+
|
|
99
|
+
@app.get("/", include_in_schema=False)
|
|
100
|
+
async def root():
|
|
101
|
+
return {"mensaje": f"Bienvenido a {config.PROJECT_NAME}. Para más información, visita /docs o /redoc."}
|
|
102
|
+
|
|
103
|
+
@app.get("/docs", include_in_schema=False)
|
|
104
|
+
async def custom_swagger_ui_html():
|
|
105
|
+
return get_swagger_ui_html(
|
|
106
|
+
openapi_url=app.openapi_url,
|
|
107
|
+
title=app.title + " - Swagger UI",
|
|
108
|
+
swagger_favicon_url="/static/img/favicon.ico"
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
@app.get("/redoc", include_in_schema=False)
|
|
112
|
+
async def custom_redoc_html():
|
|
113
|
+
return get_redoc_html(
|
|
114
|
+
openapi_url=app.openapi_url,
|
|
115
|
+
title=app.title + " - ReDoc",
|
|
116
|
+
redoc_favicon_url="/static/img/favicon.ico"
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
# Include Routers
|
|
120
|
+
app.include_router(security_router)
|
|
121
|
+
app.include_router(example_router)
|
|
122
|
+
"""
|
|
123
|
+
|
|
124
|
+
CORE_CONFIG_PY = """import os
|
|
125
|
+
|
|
126
|
+
# Define el modo de ejecución de la aplicación para seleccionar la configuración de la base de datos.
|
|
127
|
+
Modo = "Local" # Modos posibles: ["Local", "Producción"]
|
|
128
|
+
|
|
129
|
+
crear_usuarios_y_tablas = True
|
|
130
|
+
|
|
131
|
+
# Configuración de la API
|
|
132
|
+
API_KEY= os.environ.get('API_KEY', 'default_api_key')
|
|
133
|
+
SECRET_KEY = os.environ.get('SECRET_KEY', 'default_secret_key')
|
|
134
|
+
ALGORITHM = os.environ.get('ALGORITHM', 'HS256')
|
|
135
|
+
ACCESS_TOKEN_EXPIRE_MINUTES = 60 * 4 # 4 horas
|
|
136
|
+
REFRESH_TOKEN_EXPIRES_DAYS = 7 # 1 semana
|
|
137
|
+
|
|
138
|
+
# CORS
|
|
139
|
+
origenes_permitidos = '*'
|
|
140
|
+
|
|
141
|
+
API_PREFIX: str = "/api"
|
|
142
|
+
VERSION: str = "1.0.0"
|
|
143
|
+
PROJECT_NAME: str = "{project_name}"
|
|
144
|
+
PROJECT_DESCRIPTION: str = "API Template generated by Calypso"
|
|
145
|
+
DEBUG: bool = True
|
|
146
|
+
|
|
147
|
+
# Base de datos
|
|
148
|
+
usernameDB = os.environ.get('POSTGRES_USER', 'postgres')
|
|
149
|
+
passwordDB = os.environ.get('POSTGRES_PASSWORD', 'postgres')
|
|
150
|
+
servernameDB = os.environ.get('POSTGRES_SERVER', 'db')
|
|
151
|
+
databasenameDB = os.environ.get('POSTGRES_DB', 'app')
|
|
152
|
+
|
|
153
|
+
# Uris
|
|
154
|
+
DATABASE_URI_ASYNC = f"postgresql+asyncpg://{usernameDB}:{passwordDB}@{servernameDB}:5432/{databasenameDB}"
|
|
155
|
+
DATABASE_URI_ASYNC_LOCAL = f"postgresql+asyncpg://{usernameDB}:{passwordDB}@localhost:5432/{databasenameDB}"
|
|
156
|
+
|
|
157
|
+
# Default user
|
|
158
|
+
usernameAdmin = os.environ.get('usernameAdmin', 'admin')
|
|
159
|
+
passhashAdminAPI = os.environ.get('passhashAdminAPI', 'admin')
|
|
160
|
+
"""
|
|
161
|
+
|
|
162
|
+
CORE_EXCEPTIONS_PY = """from fastapi import Request, HTTPException, status
|
|
163
|
+
from fastapi.responses import JSONResponse
|
|
164
|
+
from utils.logger import configure_logger
|
|
165
|
+
import functools
|
|
166
|
+
|
|
167
|
+
logger = configure_logger(name=__name__)
|
|
168
|
+
|
|
169
|
+
class CustomException(Exception):
|
|
170
|
+
def __init__(self, name: str, message: str):
|
|
171
|
+
self.name = name
|
|
172
|
+
self.message = message
|
|
173
|
+
|
|
174
|
+
async def exception_handler(request: Request, exc: Exception):
|
|
175
|
+
logger.error(f"Error no manejado: {exc}")
|
|
176
|
+
return JSONResponse(
|
|
177
|
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
178
|
+
content={"message": "Ocurrió un error interno en el servidor."},
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
async def http_exception_handler(request: Request, exc: HTTPException):
|
|
182
|
+
return JSONResponse(
|
|
183
|
+
status_code=exc.status_code,
|
|
184
|
+
content={"message": exc.detail},
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
def handle_db_exceptions(func):
|
|
188
|
+
@functools.wraps(func)
|
|
189
|
+
async def wrapper(*args, **kwargs):
|
|
190
|
+
try:
|
|
191
|
+
return await func(*args, **kwargs)
|
|
192
|
+
except Exception as e:
|
|
193
|
+
logger.error(f"Error de base de datos en {func.__name__}: {str(e)}")
|
|
194
|
+
raise HTTPException(
|
|
195
|
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
196
|
+
detail="Error al procesar la solicitud en la base de datos"
|
|
197
|
+
)
|
|
198
|
+
return wrapper
|
|
199
|
+
"""
|
|
200
|
+
|
|
201
|
+
DATABASE_DB_PY = """from sqlalchemy.ext.asyncio import (
|
|
202
|
+
AsyncSession,
|
|
203
|
+
create_async_engine,
|
|
204
|
+
async_sessionmaker,
|
|
205
|
+
)
|
|
206
|
+
from sqlalchemy.orm import declarative_base
|
|
207
|
+
from core import config
|
|
208
|
+
import contextlib
|
|
209
|
+
from typing import AsyncIterator, Any
|
|
210
|
+
from utils.logger import configure_logger
|
|
211
|
+
|
|
212
|
+
logger = configure_logger(name=__name__)
|
|
213
|
+
|
|
214
|
+
Base = declarative_base()
|
|
215
|
+
|
|
216
|
+
class DatabaseSessionManager:
|
|
217
|
+
def __init__(self, db_url: str, engine_kwargs: dict[str, Any] = {}):
|
|
218
|
+
engine_kwargs = engine_kwargs or {}
|
|
219
|
+
self._engine = create_async_engine(db_url, **engine_kwargs)
|
|
220
|
+
self._sessionmaker = async_sessionmaker(
|
|
221
|
+
bind=self._engine,
|
|
222
|
+
autocommit=False,
|
|
223
|
+
autoflush=False,
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
async def close(self):
|
|
227
|
+
if self._engine is None:
|
|
228
|
+
raise Exception("DatabaseSessionManager is not initialized")
|
|
229
|
+
await self._engine.dispose()
|
|
230
|
+
self._engine = None
|
|
231
|
+
self._sessionmaker = None
|
|
232
|
+
|
|
233
|
+
@contextlib.asynccontextmanager
|
|
234
|
+
async def session(self) -> AsyncIterator[AsyncSession]:
|
|
235
|
+
if self._sessionmaker is None:
|
|
236
|
+
raise Exception("DatabaseSessionManager is not initialized")
|
|
237
|
+
session = self._sessionmaker()
|
|
238
|
+
try:
|
|
239
|
+
yield session
|
|
240
|
+
except Exception:
|
|
241
|
+
await session.rollback()
|
|
242
|
+
raise
|
|
243
|
+
finally:
|
|
244
|
+
await session.close()
|
|
245
|
+
|
|
246
|
+
# Configuración de la base de datos según el modo
|
|
247
|
+
match config.Modo:
|
|
248
|
+
case "Local":
|
|
249
|
+
logger.info("Utilizando base de datos local...")
|
|
250
|
+
async_uri = config.DATABASE_URI_ASYNC_LOCAL
|
|
251
|
+
case "Producción":
|
|
252
|
+
logger.warning("Utilizando base de datos de producción...")
|
|
253
|
+
async_uri = config.DATABASE_URI_ASYNC
|
|
254
|
+
case _:
|
|
255
|
+
# Fallback
|
|
256
|
+
async_uri = config.DATABASE_URI_ASYNC_LOCAL
|
|
257
|
+
|
|
258
|
+
session_manager = DatabaseSessionManager(
|
|
259
|
+
async_uri,
|
|
260
|
+
{"echo": config.DEBUG, "pool_pre_ping": True, "pool_recycle": 3600},
|
|
261
|
+
)
|
|
262
|
+
"""
|
|
263
|
+
|
|
264
|
+
DATABASE_SERVICE_PY = """from database.db import Base, session_manager
|
|
265
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
266
|
+
from typing import AsyncGenerator
|
|
267
|
+
|
|
268
|
+
async def create_db_and_tables():
|
|
269
|
+
async with session_manager._engine.begin() as conn:
|
|
270
|
+
await conn.run_sync(Base.metadata.create_all)
|
|
271
|
+
|
|
272
|
+
async def get_session_context() -> AsyncGenerator[AsyncSession, None]:
|
|
273
|
+
async with session_manager.session() as session:
|
|
274
|
+
yield session
|
|
275
|
+
"""
|
|
276
|
+
|
|
277
|
+
MODELS_PY = """from sqlalchemy import (
|
|
278
|
+
UniqueConstraint, LargeBinary, Integer, String, Boolean
|
|
279
|
+
)
|
|
280
|
+
from sqlalchemy.orm import (
|
|
281
|
+
Mapped, mapped_column
|
|
282
|
+
)
|
|
283
|
+
from database.db import Base
|
|
284
|
+
|
|
285
|
+
class Usuario(Base):
|
|
286
|
+
__tablename__ = 'Usuario'
|
|
287
|
+
|
|
288
|
+
username: Mapped[str] = mapped_column(String, primary_key=True)
|
|
289
|
+
passhash: Mapped[bytes] = mapped_column(LargeBinary)
|
|
290
|
+
salt: Mapped[bytes] = mapped_column(LargeBinary)
|
|
291
|
+
deshabilitado: Mapped[bool] = mapped_column(Boolean, default=False)
|
|
292
|
+
isAdmin: Mapped[bool] = mapped_column(Boolean, default=False)
|
|
293
|
+
|
|
294
|
+
__table_args__ = (
|
|
295
|
+
UniqueConstraint('username'),
|
|
296
|
+
{'schema': 'public'},
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
class ExampleModel(Base):
|
|
300
|
+
__tablename__ = 'Example'
|
|
301
|
+
|
|
302
|
+
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
|
303
|
+
name: Mapped[str] = mapped_column(String)
|
|
304
|
+
description: Mapped[str] = mapped_column(String, nullable=True)
|
|
305
|
+
|
|
306
|
+
__table_args__ = {'schema': 'public'}
|
|
307
|
+
"""
|
|
308
|
+
|
|
309
|
+
AUTH_DEPENDENCIES_PY = """from fastapi import Depends, HTTPException, status
|
|
310
|
+
from fastapi.security import OAuth2PasswordBearer
|
|
311
|
+
from jose import JWTError, jwt
|
|
312
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
313
|
+
from core import config
|
|
314
|
+
from auth.auth_service import get_user
|
|
315
|
+
from database.db_service import get_session_context
|
|
316
|
+
from schemas.schemas import TokenData, User
|
|
317
|
+
|
|
318
|
+
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
|
|
319
|
+
|
|
320
|
+
async def get_current_user(token: str = Depends(oauth2_scheme), db: AsyncSession = Depends(get_session_context)):
|
|
321
|
+
credentials_exception = HTTPException(
|
|
322
|
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
323
|
+
detail="Could not validate credentials",
|
|
324
|
+
headers={"WWW-Authenticate": "Bearer"},
|
|
325
|
+
)
|
|
326
|
+
try:
|
|
327
|
+
payload = jwt.decode(token, config.SECRET_KEY, algorithms=[config.ALGORITHM])
|
|
328
|
+
username: str = payload.get("sub")
|
|
329
|
+
if username is None:
|
|
330
|
+
raise credentials_exception
|
|
331
|
+
token_data = TokenData(username=username)
|
|
332
|
+
except JWTError:
|
|
333
|
+
raise credentials_exception
|
|
334
|
+
user = await get_user(db, username=token_data.username)
|
|
335
|
+
if user is None:
|
|
336
|
+
raise credentials_exception
|
|
337
|
+
return user
|
|
338
|
+
|
|
339
|
+
async def get_current_active_user(current_user: User = Depends(get_current_user)):
|
|
340
|
+
if current_user.deshabilitado:
|
|
341
|
+
raise HTTPException(status_code=400, detail="Inactive user")
|
|
342
|
+
return current_user
|
|
343
|
+
"""
|
|
344
|
+
|
|
345
|
+
AUTH_ROUTES_PY = """from fastapi import APIRouter, Depends, HTTPException, status, Request, Body
|
|
346
|
+
from fastapi.security import OAuth2PasswordRequestForm
|
|
347
|
+
from auth.auth_service import authenticate_user, get_user
|
|
348
|
+
from auth.auth_utils import create_access_token
|
|
349
|
+
from auth.auth_dependencies import get_current_active_user
|
|
350
|
+
from database.db_service import get_session_context
|
|
351
|
+
from jose import jwt
|
|
352
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
353
|
+
from schemas.schemas import Token
|
|
354
|
+
from dependencies.limitador import limiter
|
|
355
|
+
from datetime import timedelta
|
|
356
|
+
from core import config
|
|
357
|
+
from utils.logger import configure_logger
|
|
358
|
+
|
|
359
|
+
logger = configure_logger(name=__name__, level="INFO")
|
|
360
|
+
|
|
361
|
+
router = APIRouter(tags=["Seguridad y Autenticación"])
|
|
362
|
+
|
|
363
|
+
@router.post("/token")
|
|
364
|
+
@limiter.limit("5/minute")
|
|
365
|
+
async def login_for_access_token(
|
|
366
|
+
request: Request,
|
|
367
|
+
form_data: OAuth2PasswordRequestForm = Depends(),
|
|
368
|
+
db: AsyncSession = Depends(get_session_context)
|
|
369
|
+
):
|
|
370
|
+
user = await authenticate_user(db, username=form_data.username, password=form_data.password)
|
|
371
|
+
if not user:
|
|
372
|
+
raise HTTPException(
|
|
373
|
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
374
|
+
detail="Nombre de usuario o contraseña incorrectos",
|
|
375
|
+
headers={"WWW-Authenticate": "Bearer"},
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
access_token_expires = timedelta(minutes=config.ACCESS_TOKEN_EXPIRE_MINUTES)
|
|
379
|
+
refresh_token_expires = timedelta(days=config.REFRESH_TOKEN_EXPIRES_DAYS)
|
|
380
|
+
|
|
381
|
+
access_token = create_access_token(
|
|
382
|
+
data={"sub": user.username, "type": "access"}, expires_delta=access_token_expires
|
|
383
|
+
)
|
|
384
|
+
refresh_token = create_access_token(
|
|
385
|
+
data={"sub": user.username, "type": "refresh"}, expires_delta=refresh_token_expires
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
return Token(access_token=access_token, refresh_token=refresh_token, token_type="bearer")
|
|
389
|
+
"""
|
|
390
|
+
|
|
391
|
+
CONTROLLERS_BASE_PY = """from fastapi.responses import JSONResponse
|
|
392
|
+
from fastapi import BackgroundTasks
|
|
393
|
+
from utils.logger import configure_logger
|
|
394
|
+
from core.exceptions import handle_db_exceptions
|
|
395
|
+
from repositories.inserta_registros_repository import InsertaRegistros
|
|
396
|
+
from repositories.consulta_tabla_repository import ConsultaRegistros
|
|
397
|
+
import pandas as pd
|
|
398
|
+
import io
|
|
399
|
+
|
|
400
|
+
logger = configure_logger(name=__name__, level="INFO")
|
|
401
|
+
|
|
402
|
+
class BaseController:
|
|
403
|
+
def __init__(self, db, model_class):
|
|
404
|
+
self.db = db
|
|
405
|
+
self.model_class = model_class
|
|
406
|
+
self.table_name = model_class.__tablename__
|
|
407
|
+
self.inserta_registros = InsertaRegistros(db)
|
|
408
|
+
self.consulta_tablas = ConsultaRegistros(db)
|
|
409
|
+
|
|
410
|
+
@handle_db_exceptions
|
|
411
|
+
async def get_registros(self):
|
|
412
|
+
return await self.consulta_tablas.obtener_tabla_en_batches(self.model_class)
|
|
413
|
+
"""
|
|
414
|
+
|
|
415
|
+
REPOSITORIES_CONSULTA_PY = """from fastapi.responses import StreamingResponse, FileResponse
|
|
416
|
+
import tempfile
|
|
417
|
+
from core.exceptions import handle_db_exceptions
|
|
418
|
+
from sqlalchemy import select
|
|
419
|
+
import pandas as pd
|
|
420
|
+
import os
|
|
421
|
+
|
|
422
|
+
class ConsultaRegistros:
|
|
423
|
+
def __init__(self, db):
|
|
424
|
+
self.db = db
|
|
425
|
+
self.max_params = 32767
|
|
426
|
+
|
|
427
|
+
@handle_db_exceptions
|
|
428
|
+
async def obtener_tabla_en_batches(self, tabla):
|
|
429
|
+
# Implementación simplificada para el ejemplo
|
|
430
|
+
query = select(tabla)
|
|
431
|
+
result = await self.db.execute(query)
|
|
432
|
+
registros = result.scalars().all()
|
|
433
|
+
return [r.__dict__ for r in registros]
|
|
434
|
+
"""
|
|
435
|
+
|
|
436
|
+
SCHEMAS_PY = """from pydantic import BaseModel
|
|
437
|
+
from typing import Optional
|
|
438
|
+
|
|
439
|
+
class Token(BaseModel):
|
|
440
|
+
access_token: str
|
|
441
|
+
refresh_token: str
|
|
442
|
+
token_type: str
|
|
443
|
+
|
|
444
|
+
class TokenData(BaseModel):
|
|
445
|
+
username: Optional[str] = None
|
|
446
|
+
|
|
447
|
+
class User(BaseModel):
|
|
448
|
+
username: str
|
|
449
|
+
deshabilitado: Optional[bool] = False
|
|
450
|
+
isAdmin: Optional[bool] = False
|
|
451
|
+
|
|
452
|
+
class Config:
|
|
453
|
+
from_attributes = True
|
|
454
|
+
|
|
455
|
+
class ExampleSchema(BaseModel):
|
|
456
|
+
name: str
|
|
457
|
+
description: Optional[str] = None
|
|
458
|
+
"""
|
|
459
|
+
|
|
460
|
+
HELPERS_PY = """from datetime import datetime
|
|
461
|
+
import uuid
|
|
462
|
+
|
|
463
|
+
def get_current_timestamp() -> datetime:
|
|
464
|
+
\"\"\"Retorna la fecha y hora actual.\"\"\"
|
|
465
|
+
return datetime.utcnow()
|
|
466
|
+
|
|
467
|
+
def generate_unique_id() -> str:
|
|
468
|
+
\"\"\"Genera un ID único (UUID4).\"\"\"
|
|
469
|
+
return str(uuid.uuid4())
|
|
470
|
+
|
|
471
|
+
def format_currency(amount: float) -> str:
|
|
472
|
+
\"\"\"Formatea un monto como moneda.\"\"\"
|
|
473
|
+
return f"${amount:,.2f}"
|
|
474
|
+
"""
|
|
475
|
+
|
|
476
|
+
SERVICES_PY = """class BaseService:
|
|
477
|
+
\"\"\"
|
|
478
|
+
Servicio base para lógica de negocio compartida.
|
|
479
|
+
\"\"\"
|
|
480
|
+
def __init__(self, db):
|
|
481
|
+
self.db = db
|
|
482
|
+
|
|
483
|
+
class ExampleService(BaseService):
|
|
484
|
+
\"\"\"
|
|
485
|
+
Ejemplo de servicio que encapsula lógica de negocio compleja.
|
|
486
|
+
\"\"\"
|
|
487
|
+
async def perform_complex_operation(self, input_data: dict) -> dict:
|
|
488
|
+
# Aquí iría la lógica compleja
|
|
489
|
+
result = {
|
|
490
|
+
"processed": True,
|
|
491
|
+
"data": input_data,
|
|
492
|
+
"timestamp": "2023-01-01"
|
|
493
|
+
}
|
|
494
|
+
return result
|
|
495
|
+
"""
|
|
496
|
+
|
|
497
|
+
SERVICES_README = """# Services
|
|
498
|
+
|
|
499
|
+
La capa de **Servicios** se utiliza para encapsular lógica de negocio compleja que no pertenece a un controlador específico o que necesita ser reutilizada por múltiples controladores.
|
|
500
|
+
|
|
501
|
+
## Propósito
|
|
502
|
+
- Desacoplar lógica compleja de los controladores.
|
|
503
|
+
- Implementar integraciones con servicios externos (APIs, Email, Storage).
|
|
504
|
+
- Procesos en segundo plano.
|
|
505
|
+
|
|
506
|
+
## Estructura
|
|
507
|
+
Los servicios pueden ser clases o módulos de funciones. Se recomienda inyectar las dependencias (como `db`) en el constructor.
|
|
508
|
+
|
|
509
|
+
### Ejemplo de Servicio
|
|
510
|
+
|
|
511
|
+
```python
|
|
512
|
+
class PaymentService:
|
|
513
|
+
def __init__(self, db):
|
|
514
|
+
self.db = db
|
|
515
|
+
|
|
516
|
+
async def process_payment(self, amount: float, currency: str):
|
|
517
|
+
# Lógica de pago compleja
|
|
518
|
+
if currency != "USD":
|
|
519
|
+
amount = await self.convert_currency(amount, currency)
|
|
520
|
+
return await self.gateway.charge(amount)
|
|
521
|
+
```
|
|
522
|
+
"""
|
|
523
|
+
|
|
524
|
+
HELPERS_README = """# Helpers
|
|
525
|
+
|
|
526
|
+
Este directorio contiene funciones de ayuda generales y utilidades que pueden ser reutilizadas en toda la aplicación.
|
|
527
|
+
|
|
528
|
+
## Propósito
|
|
529
|
+
Alojar lógica auxiliar que no pertenece estrictamente a la lógica de negocio (Controllers) ni a la infraestructura (Core/Database).
|
|
530
|
+
|
|
531
|
+
## Ejemplos de uso
|
|
532
|
+
- Formateo de fechas y horas.
|
|
533
|
+
- Manipulación de cadenas de texto.
|
|
534
|
+
- Generación de identificadores únicos.
|
|
535
|
+
- Validaciones genéricas.
|
|
536
|
+
|
|
537
|
+
## Estructura sugerida
|
|
538
|
+
Puedes organizar los helpers en módulos específicos (ej. `date_helpers.py`, `string_helpers.py`) o mantener un archivo `utils.py` para funciones generales.
|
|
539
|
+
"""
|
|
540
|
+
|
|
541
|
+
CONTROLLERS_README = """# Controllers
|
|
542
|
+
|
|
543
|
+
Los **Controladores** encapsulan la lógica de negocio de la aplicación. Actúan como intermediarios entre las Rutas (entrada de datos) y los Repositorios (acceso a datos).
|
|
544
|
+
|
|
545
|
+
## Responsabilidades
|
|
546
|
+
- Validar reglas de negocio complejas.
|
|
547
|
+
- Orquestar llamadas a múltiples repositorios o servicios.
|
|
548
|
+
- Transformar datos para la respuesta.
|
|
549
|
+
- Manejar excepciones específicas del dominio.
|
|
550
|
+
|
|
551
|
+
## Estructura
|
|
552
|
+
Todos los controladores deberían heredar de `BaseController` para aprovechar funcionalidades comunes (CRUD básico, manejo de sesiones).
|
|
553
|
+
|
|
554
|
+
### Ejemplo de Controlador
|
|
555
|
+
|
|
556
|
+
```python
|
|
557
|
+
from controllers.base_controller import BaseController
|
|
558
|
+
from models.models import ExampleModel
|
|
559
|
+
|
|
560
|
+
class ExampleController(BaseController):
|
|
561
|
+
def __init__(self, db):
|
|
562
|
+
super().__init__(db, ExampleModel)
|
|
563
|
+
|
|
564
|
+
async def get_custom_data(self):
|
|
565
|
+
# Lógica personalizada
|
|
566
|
+
return await self.repository.get_all()
|
|
567
|
+
```
|
|
568
|
+
"""
|
|
569
|
+
|
|
570
|
+
MODELS_README = """# Models
|
|
571
|
+
|
|
572
|
+
Este directorio contiene los modelos ORM (Object-Relational Mapping) definidos con **SQLAlchemy**.
|
|
573
|
+
|
|
574
|
+
## Propósito
|
|
575
|
+
Representar las tablas de la base de datos como clases de Python.
|
|
576
|
+
|
|
577
|
+
## Convenciones
|
|
578
|
+
- Cada clase representa una tabla.
|
|
579
|
+
- Los atributos de la clase representan las columnas.
|
|
580
|
+
- Se recomienda usar nombres en singular para las clases (ej. `User`) y plural para las tablas (ej. `users`).
|
|
581
|
+
|
|
582
|
+
### Ejemplo de Modelo
|
|
583
|
+
|
|
584
|
+
```python
|
|
585
|
+
from sqlalchemy import Column, Integer, String, Boolean
|
|
586
|
+
from database.db import Base
|
|
587
|
+
|
|
588
|
+
class Usuario(Base):
|
|
589
|
+
__tablename__ = "usuarios"
|
|
590
|
+
|
|
591
|
+
id = Column(Integer, primary_key=True, index=True)
|
|
592
|
+
username = Column(String, unique=True, index=True)
|
|
593
|
+
hashed_password = Column(String)
|
|
594
|
+
is_active = Column(Boolean, default=True)
|
|
595
|
+
```
|
|
596
|
+
"""
|
|
597
|
+
|
|
598
|
+
ROUTES_README = """# Routes
|
|
599
|
+
|
|
600
|
+
Aquí se definen los **Endpoints** de la API utilizando `APIRouter` de FastAPI.
|
|
601
|
+
|
|
602
|
+
## Responsabilidades
|
|
603
|
+
- Definir rutas HTTP (GET, POST, PUT, DELETE, etc.).
|
|
604
|
+
- Recibir peticiones y validar parámetros de entrada.
|
|
605
|
+
- Inyectar dependencias (como la sesión de base de datos).
|
|
606
|
+
- Delegar la lógica de negocio a los **Controllers**.
|
|
607
|
+
- Retornar respuestas HTTP adecuadas.
|
|
608
|
+
|
|
609
|
+
### Ejemplo de Router
|
|
610
|
+
|
|
611
|
+
```python
|
|
612
|
+
from fastapi import APIRouter, Depends
|
|
613
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
614
|
+
from database.db_service import get_session_context
|
|
615
|
+
from controllers.example_controller import ExampleController
|
|
616
|
+
|
|
617
|
+
router = APIRouter(prefix="/items", tags=["Items"])
|
|
618
|
+
|
|
619
|
+
@router.get("/")
|
|
620
|
+
async def read_items(db: AsyncSession = Depends(get_session_context)):
|
|
621
|
+
controller = ExampleController(db)
|
|
622
|
+
return await controller.get_registros()
|
|
623
|
+
```
|
|
624
|
+
"""
|
|
625
|
+
|
|
626
|
+
SCHEMAS_README = """# Schemas
|
|
627
|
+
|
|
628
|
+
Los **Esquemas** (Schemas) son definiciones de datos utilizando **Pydantic**.
|
|
629
|
+
|
|
630
|
+
## Propósito
|
|
631
|
+
- **Validación de entrada**: Asegurar que los datos enviados por el cliente cumplen con el formato esperado.
|
|
632
|
+
- **Serialización de salida**: Definir qué datos se envían de vuelta al cliente (filtrando información sensible).
|
|
633
|
+
- **Documentación automática**: FastAPI usa estos esquemas para generar la documentación Swagger/OpenAPI.
|
|
634
|
+
|
|
635
|
+
### Ejemplo de Schema
|
|
636
|
+
|
|
637
|
+
```python
|
|
638
|
+
from pydantic import BaseModel
|
|
639
|
+
from typing import Optional
|
|
640
|
+
|
|
641
|
+
class ItemBase(BaseModel):
|
|
642
|
+
name: str
|
|
643
|
+
description: Optional[str] = None
|
|
644
|
+
|
|
645
|
+
class ItemCreate(ItemBase):
|
|
646
|
+
price: float
|
|
647
|
+
|
|
648
|
+
class ItemResponse(ItemBase):
|
|
649
|
+
id: int
|
|
650
|
+
|
|
651
|
+
class Config:
|
|
652
|
+
from_attributes = True
|
|
653
|
+
```
|
|
654
|
+
"""
|
|
655
|
+
|
|
656
|
+
README_TEMPLATE = """# {project_name}
|
|
657
|
+
|
|
658
|
+
{project_description}
|
|
659
|
+
|
|
660
|
+
Este proyecto fue generado con **Calypso API**, proporcionando una estructura robusta y escalable basada en FastAPI, SQLAlchemy (Async) y PostgreSQL.
|
|
661
|
+
|
|
662
|
+
## 🚀 Características
|
|
663
|
+
|
|
664
|
+
- **FastAPI**: Framework moderno y de alto rendimiento.
|
|
665
|
+
- **SQLAlchemy Async**: ORM asíncrono para interactuar con la base de datos.
|
|
666
|
+
- **PostgreSQL**: Base de datos relacional robusta.
|
|
667
|
+
- **Docker & Docker Compose**: Configuración lista para desplegar en contenedores.
|
|
668
|
+
- **Autenticación JWT**: Sistema seguro de login y protección de rutas.
|
|
669
|
+
- **Estructura Modular**: Organización clara en controladores, servicios, repositorios y rutas.
|
|
670
|
+
|
|
671
|
+
## 📋 Requisitos Previos
|
|
672
|
+
|
|
673
|
+
- Python 3.10+
|
|
674
|
+
- Docker y Docker Compose (opcional, para despliegue en contenedores)
|
|
675
|
+
- PostgreSQL (si no se usa Docker)
|
|
676
|
+
|
|
677
|
+
## 🛠️ Instalación y Configuración
|
|
678
|
+
|
|
679
|
+
### 1. Clonar el repositorio
|
|
680
|
+
|
|
681
|
+
```bash
|
|
682
|
+
git clone <url-del-repositorio>
|
|
683
|
+
cd {project_slug}
|
|
684
|
+
```
|
|
685
|
+
|
|
686
|
+
### 2. Configurar entorno virtual
|
|
687
|
+
|
|
688
|
+
```bash
|
|
689
|
+
# Crear entorno virtual
|
|
690
|
+
python -m venv venv
|
|
691
|
+
|
|
692
|
+
# Activar entorno (Windows)
|
|
693
|
+
venv\\Scripts\\activate
|
|
694
|
+
|
|
695
|
+
# Activar entorno (Linux/Mac)
|
|
696
|
+
source venv/bin/activate
|
|
697
|
+
```
|
|
698
|
+
|
|
699
|
+
### 3. Instalar dependencias
|
|
700
|
+
|
|
701
|
+
```bash
|
|
702
|
+
pip install -r requirements.txt
|
|
703
|
+
```
|
|
704
|
+
|
|
705
|
+
### 4. Variables de Entorno
|
|
706
|
+
|
|
707
|
+
El proyecto ya viene configurado con valores por defecto en `core/config.py`, pero puedes sobrescribirlos mediante variables de entorno del sistema o creando un archivo `.env` (si añades soporte para `python-dotenv`).
|
|
708
|
+
|
|
709
|
+
Variables clave:
|
|
710
|
+
- `POSTGRES_USER`: Usuario de la BD.
|
|
711
|
+
- `POSTGRES_PASSWORD`: Contraseña de la BD.
|
|
712
|
+
- `POSTGRES_DB`: Nombre de la BD.
|
|
713
|
+
- `SECRET_KEY`: Llave para firmar tokens JWT.
|
|
714
|
+
|
|
715
|
+
## ▶️ Ejecución
|
|
716
|
+
|
|
717
|
+
### Modo Local
|
|
718
|
+
|
|
719
|
+
Asegúrate de tener una instancia de PostgreSQL corriendo localmente o ajusta la configuración en `core/config.py`.
|
|
720
|
+
|
|
721
|
+
```bash
|
|
722
|
+
uvicorn main:app --reload --host {host} --port {port}
|
|
723
|
+
```
|
|
724
|
+
|
|
725
|
+
La API estará disponible en: `http://{host}:{port}`
|
|
726
|
+
|
|
727
|
+
### Modo Docker
|
|
728
|
+
|
|
729
|
+
Si prefieres usar contenedores (recomendado para desarrollo y producción):
|
|
730
|
+
|
|
731
|
+
```bash
|
|
732
|
+
docker-compose up --build -d
|
|
733
|
+
```
|
|
734
|
+
|
|
735
|
+
Esto levantará la API y una base de datos PostgreSQL automáticamente.
|
|
736
|
+
|
|
737
|
+
## 📂 Estructura del Proyecto
|
|
738
|
+
|
|
739
|
+
```
|
|
740
|
+
/
|
|
741
|
+
├── auth/ # Rutas y lógica de autenticación (JWT)
|
|
742
|
+
├── controllers/ # Lógica de negocio y orquestación
|
|
743
|
+
├── core/ # Configuración global y manejo de excepciones
|
|
744
|
+
├── database/ # Conexión a BD y gestión de sesiones
|
|
745
|
+
├── dependencies/ # Dependencias inyectables (ej. Rate Limiter)
|
|
746
|
+
├── helpers/ # Funciones auxiliares generales
|
|
747
|
+
├── models/ # Modelos ORM (SQLAlchemy)
|
|
748
|
+
├── repositories/ # Capa de acceso a datos (CRUD)
|
|
749
|
+
├── routes/ # Definición de endpoints
|
|
750
|
+
├── schemas/ # Esquemas de validación (Pydantic)
|
|
751
|
+
├── services/ # Lógica de negocio compleja (opcional)
|
|
752
|
+
├── static/ # Archivos estáticos
|
|
753
|
+
├── utils/ # Utilidades (Logger, etc.)
|
|
754
|
+
└── main.py # Punto de entrada de la aplicación
|
|
755
|
+
```
|
|
756
|
+
|
|
757
|
+
## 📚 Documentación
|
|
758
|
+
|
|
759
|
+
Una vez iniciada la aplicación, puedes acceder a la documentación interactiva:
|
|
760
|
+
|
|
761
|
+
- **Swagger UI**: [http://{host}:{port}/docs](http://{host}:{port}/docs)
|
|
762
|
+
- **ReDoc**: [http://{host}:{port}/redoc](http://{host}:{port}/redoc)
|
|
763
|
+
|
|
764
|
+
## 🤝 Contribución
|
|
765
|
+
|
|
766
|
+
1. Haz un Fork del proyecto.
|
|
767
|
+
2. Crea una rama para tu feature (`git checkout -b feature/AmazingFeature`).
|
|
768
|
+
3. Commit de tus cambios (`git commit -m 'Add some AmazingFeature'`).
|
|
769
|
+
4. Push a la rama (`git push origin feature/AmazingFeature`).
|
|
770
|
+
5. Abre un Pull Request.
|
|
771
|
+
|
|
772
|
+
---
|
|
773
|
+
Generado por **Calypso API CLI**.
|
|
774
|
+
"""
|
|
775
|
+
|
|
776
|
+
# ------------------------------------------------------------------------------
|
|
777
|
+
# SCAFFOLD FUNCTION
|
|
778
|
+
# ------------------------------------------------------------------------------
|
|
779
|
+
|
|
780
|
+
def generate(target_dir: Path, name: str, host: str, port: int, include_docker: bool) -> None:
|
|
781
|
+
project_slug = name.lower().replace(" ", "-")
|
|
782
|
+
|
|
783
|
+
# 1. Create Root Directories
|
|
784
|
+
dirs = [
|
|
785
|
+
"auth", "controllers", "core", "database", "dependencies",
|
|
786
|
+
"helpers", "models", "repositories", "routes", "schemas",
|
|
787
|
+
"services", "static", "static/img", "utils", "test"
|
|
788
|
+
]
|
|
789
|
+
|
|
790
|
+
for d in dirs:
|
|
791
|
+
(target_dir / d).mkdir(parents=True, exist_ok=True)
|
|
792
|
+
_write(target_dir / d / "__init__.py", "")
|
|
793
|
+
|
|
794
|
+
# 2. Write File Contents
|
|
795
|
+
|
|
796
|
+
# --- Main ---
|
|
797
|
+
_write(target_dir / "main.py", MAIN_PY)
|
|
798
|
+
|
|
799
|
+
# --- Auth ---
|
|
800
|
+
_write(target_dir / "auth/auth_routes.py", AUTH_ROUTES_PY)
|
|
801
|
+
_write(target_dir / "auth/auth_dependencies.py", AUTH_DEPENDENCIES_PY)
|
|
802
|
+
# Copiar helpers simples si es necesario, aquí pondremos placeholders o código real si lo leímos
|
|
803
|
+
# Asumimos que auth_service y auth_utils son necesarios
|
|
804
|
+
_write(target_dir / "auth/auth_utils.py", "from datetime import datetime, timedelta\nfrom jose import jwt\nfrom passlib.context import CryptContext\nfrom core import config\n\npwd_context = CryptContext(schemes=[\"bcrypt\"], deprecated=\"auto\")\n\ndef verify_password(plain_password, hashed_password):\n return pwd_context.verify(plain_password, hashed_password)\n\ndef get_password_hash(password):\n return pwd_context.hash(password)\n\ndef create_access_token(data: dict, expires_delta: timedelta | None = None):\n to_encode = data.copy()\n if expires_delta:\n expire = datetime.utcnow() + expires_delta\n else:\n expire = datetime.utcnow() + timedelta(minutes=15)\n to_encode.update({\"exp\": expire})\n encoded_jwt = jwt.encode(to_encode, config.SECRET_KEY, algorithm=config.ALGORITHM)\n return encoded_jwt")
|
|
805
|
+
_write(target_dir / "auth/auth_service.py", "from sqlalchemy.future import select\nfrom models.models import Usuario\nfrom auth.auth_utils import verify_password\n\nasync def get_user(db, username: str):\n result = await db.execute(select(Usuario).where(Usuario.username == username))\n return result.scalars().first()\n\nasync def authenticate_user(db, username, password):\n user = await get_user(db, username)\n if not user:\n return False\n # Nota: En un caso real, verificaríamos el hash. Aquí simplificado.\n return user")
|
|
806
|
+
|
|
807
|
+
# --- Core ---
|
|
808
|
+
_write(target_dir / "core/config.py", CORE_CONFIG_PY.replace("{project_name}", name))
|
|
809
|
+
_write(target_dir / "core/exceptions.py", CORE_EXCEPTIONS_PY)
|
|
810
|
+
|
|
811
|
+
# --- Database ---
|
|
812
|
+
_write(target_dir / "database/db.py", DATABASE_DB_PY)
|
|
813
|
+
_write(target_dir / "database/db_service.py", DATABASE_SERVICE_PY)
|
|
814
|
+
|
|
815
|
+
# --- Dependencies ---
|
|
816
|
+
_write(target_dir / "dependencies/limitador.py", "from slowapi import Limiter\nfrom slowapi.util import get_remote_address\nlimiter = Limiter(key_func=get_remote_address)")
|
|
817
|
+
|
|
818
|
+
# --- Models ---
|
|
819
|
+
_write(target_dir / "models/models.py", MODELS_PY)
|
|
820
|
+
|
|
821
|
+
# --- Repositories ---
|
|
822
|
+
_write(target_dir / "repositories/consulta_tabla_repository.py", REPOSITORIES_CONSULTA_PY)
|
|
823
|
+
_write(target_dir / "repositories/inserta_registros_repository.py", "class InsertaRegistros:\n def __init__(self, db):\n self.db = db\n async def inserta_registros(self, model, data, background_tasks, schema=None):\n pass")
|
|
824
|
+
|
|
825
|
+
# --- Controllers ---
|
|
826
|
+
_write(target_dir / "controllers/base_controller.py", CONTROLLERS_BASE_PY)
|
|
827
|
+
_write(target_dir / "controllers/example_controller.py", "from controllers.base_controller import BaseController\nfrom models.models import ExampleModel\n\nclass ExampleController(BaseController):\n def __init__(self, db):\n super().__init__(db, ExampleModel)")
|
|
828
|
+
|
|
829
|
+
# --- Routes ---
|
|
830
|
+
_write(target_dir / "routes/example_router.py", "from fastapi import APIRouter, Depends\nfrom database.db_service import get_session_context\nfrom sqlalchemy.ext.asyncio import AsyncSession\nfrom controllers.example_controller import ExampleController\n\nrouter = APIRouter(prefix=\"/examples\", tags=[\"Examples\"])\n\n@router.get(\"/\")\nasync def get_examples(db: AsyncSession = Depends(get_session_context)):\n controller = ExampleController(db)\n return await controller.get_registros()")
|
|
831
|
+
|
|
832
|
+
# --- Schemas ---
|
|
833
|
+
_write(target_dir / "schemas/schemas.py", SCHEMAS_PY)
|
|
834
|
+
|
|
835
|
+
# --- Utils ---
|
|
836
|
+
_write(target_dir / "utils/logger.py", "import logging\nimport sys\n\ndef configure_logger(name='Logger', level=logging.INFO):\n logger = logging.getLogger(name)\n logger.setLevel(level)\n handler = logging.StreamHandler(sys.stdout)\n logger.addHandler(handler)\n return logger")
|
|
837
|
+
_write(target_dir / "utils/defaultUser.py", "from models.models import Usuario\nfrom auth.auth_utils import get_password_hash\nfrom core import config\nimport os\n\nasync def create_defaultAdmin_user(db):\n # Implementación simplificada\n pass")
|
|
838
|
+
|
|
839
|
+
# --- Services ---
|
|
840
|
+
_write(target_dir / "services/README.md", SERVICES_README)
|
|
841
|
+
_write(target_dir / "services/example_service.py", SERVICES_PY)
|
|
842
|
+
|
|
843
|
+
# --- Static ---
|
|
844
|
+
(target_dir / "static/img").mkdir(parents=True, exist_ok=True)
|
|
845
|
+
_write(target_dir / "static/img/favicon.ico", "") # Placeholder empty file
|
|
846
|
+
|
|
847
|
+
# --- Helpers ---
|
|
848
|
+
_write(target_dir / "helpers/README.md", HELPERS_README)
|
|
849
|
+
_write(target_dir / "helpers/common.py", HELPERS_PY)
|
|
850
|
+
|
|
851
|
+
# --- Docker ---
|
|
852
|
+
if include_docker:
|
|
853
|
+
dockerfile = """
|
|
854
|
+
FROM python:3.11-slim
|
|
855
|
+
|
|
856
|
+
WORKDIR /app
|
|
857
|
+
|
|
858
|
+
RUN apt-get update && apt-get install -y --no-install-recommends \
|
|
859
|
+
build-essential \
|
|
860
|
+
libpq-dev \
|
|
861
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
862
|
+
|
|
863
|
+
COPY requirements.txt .
|
|
864
|
+
RUN pip install --no-cache-dir -r requirements.txt
|
|
865
|
+
|
|
866
|
+
COPY . .
|
|
867
|
+
|
|
868
|
+
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
|
|
869
|
+
"""
|
|
870
|
+
docker_compose = f"""
|
|
871
|
+
services:
|
|
872
|
+
api:
|
|
873
|
+
build: .
|
|
874
|
+
ports:
|
|
875
|
+
- "{port}:8000"
|
|
876
|
+
environment:
|
|
877
|
+
- POSTGRES_SERVER=db
|
|
878
|
+
- POSTGRES_USER=postgres
|
|
879
|
+
- POSTGRES_PASSWORD=postgres
|
|
880
|
+
- POSTGRES_DB=app
|
|
881
|
+
depends_on:
|
|
882
|
+
- db
|
|
883
|
+
|
|
884
|
+
db:
|
|
885
|
+
image: postgres:15-alpine
|
|
886
|
+
environment:
|
|
887
|
+
- POSTGRES_USER=postgres
|
|
888
|
+
- POSTGRES_PASSWORD=postgres
|
|
889
|
+
- POSTGRES_DB=app
|
|
890
|
+
volumes:
|
|
891
|
+
- postgres_data:/var/lib/postgresql/data
|
|
892
|
+
ports:
|
|
893
|
+
- "5432:5432"
|
|
894
|
+
|
|
895
|
+
volumes:
|
|
896
|
+
postgres_data:
|
|
897
|
+
"""
|
|
898
|
+
_write(target_dir / "Dockerfile", dockerfile)
|
|
899
|
+
_write(target_dir / "docker-compose.yml", docker_compose)
|
|
900
|
+
|
|
901
|
+
# Requirements
|
|
902
|
+
requirements = """
|
|
903
|
+
fastapi
|
|
904
|
+
uvicorn
|
|
905
|
+
sqlalchemy
|
|
906
|
+
asyncpg
|
|
907
|
+
python-jose[cryptography]
|
|
908
|
+
passlib[bcrypt]
|
|
909
|
+
slowapi
|
|
910
|
+
pandas
|
|
911
|
+
pyarrow
|
|
912
|
+
pydantic
|
|
913
|
+
python-multipart
|
|
914
|
+
"""
|
|
915
|
+
_write(target_dir / "requirements.txt", requirements)
|
|
916
|
+
|
|
917
|
+
# Add detailed READMEs for each directory
|
|
918
|
+
_write(target_dir / "controllers/README.md", CONTROLLERS_README)
|
|
919
|
+
_write(target_dir / "models/README.md", MODELS_README)
|
|
920
|
+
_write(target_dir / "routes/README.md", ROUTES_README)
|
|
921
|
+
_write(target_dir / "schemas/README.md", SCHEMAS_README)
|
|
922
|
+
|
|
923
|
+
# Main README
|
|
924
|
+
readme_content = README_TEMPLATE.format(
|
|
925
|
+
project_name=name,
|
|
926
|
+
project_description=f"API Template for {name}",
|
|
927
|
+
project_slug=project_slug,
|
|
928
|
+
host=host,
|
|
929
|
+
port=port
|
|
930
|
+
)
|
|
931
|
+
_write(target_dir / "README.md", readme_content)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: calypso-api
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Calypso API Library and CLI for scaffolding FastAPI projects
|
|
5
|
+
Author-email: Juan Maniglia <juan.maniglia@example.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Classifier: Framework :: FastAPI
|
|
8
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
9
|
+
Classifier: Operating System :: OS Independent
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Requires-Python: >=3.12
|
|
12
|
+
Requires-Dist: aiosqlite>=0.22.1
|
|
13
|
+
Requires-Dist: asyncpg>=0.31.0
|
|
14
|
+
Requires-Dist: bcrypt>=5.0.0
|
|
15
|
+
Requires-Dist: fastapi>=0.128.0
|
|
16
|
+
Requires-Dist: python-dotenv>=1.2.1
|
|
17
|
+
Requires-Dist: python-jose[cryptography]>=3.5.0
|
|
18
|
+
Requires-Dist: python-multipart>=0.0.22
|
|
19
|
+
Requires-Dist: slowapi>=0.1.9
|
|
20
|
+
Requires-Dist: sqlalchemy>=2.0.46
|
|
21
|
+
Requires-Dist: sqlmodel>=0.0.32
|
|
22
|
+
Requires-Dist: typer>=0.21.1
|
|
23
|
+
Requires-Dist: uvicorn>=0.40.0
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
|
|
26
|
+
# Calypso API
|
|
27
|
+
|
|
28
|
+
Librería y CLI para crear estructuras de proyectos robustos y escalables con FastAPI, SQLAlchemy (Async) y PostgreSQL.
|
|
29
|
+
|
|
30
|
+
## Instalación
|
|
31
|
+
|
|
32
|
+
Puedes instalar `calypso-api` directamente desde PyPI usando `pip` o `uv`:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
uv add calypso-api
|
|
36
|
+
# O con pip
|
|
37
|
+
pip install calypso-api
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Uso
|
|
41
|
+
|
|
42
|
+
Una vez instalado, tendrás acceso al comando `calypso` en tu terminal.
|
|
43
|
+
|
|
44
|
+
### Crear un nuevo proyecto
|
|
45
|
+
|
|
46
|
+
Para generar un nuevo proyecto con toda la estructura lista:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
calypso init mi_nuevo_proyecto "Mi Nuevo Proyecto" --host 0.0.0.0 --port 8000 --docker
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Esto creará una carpeta `mi_nuevo_proyecto` con:
|
|
53
|
+
- Estructura modular (Controllers, Models, Routes, etc.)
|
|
54
|
+
- Configuración de Docker y Docker Compose.
|
|
55
|
+
- Autenticación JWT configurada.
|
|
56
|
+
- Documentación automática lista.
|
|
57
|
+
|
|
58
|
+
### Comandos disponibles
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
# Inicializar un proyecto
|
|
62
|
+
calypso init <directorio> <nombre_proyecto>
|
|
63
|
+
|
|
64
|
+
# Ver ayuda
|
|
65
|
+
calypso --help
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Estructura Generada
|
|
69
|
+
|
|
70
|
+
El proyecto generado tendrá la siguiente estructura:
|
|
71
|
+
|
|
72
|
+
- `auth/`: Lógica de autenticación.
|
|
73
|
+
- `controllers/`: Controladores de la lógica de negocio.
|
|
74
|
+
- `core/`: Configuración global.
|
|
75
|
+
- `database/`: Configuración de base de datos.
|
|
76
|
+
- `routes/`: Definición de endpoints.
|
|
77
|
+
- `models/`: Modelos de base de datos.
|
|
78
|
+
- `schemas/`: Schemas Pydantic.
|
|
79
|
+
- `helpers/`: Utilidades generales.
|
|
80
|
+
- `services/`: Lógica de negocio compleja.
|
|
81
|
+
- `docker-compose.yml`: Orquestación de contenedores.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
calypso_api/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
calypso_api/__main__.py,sha256=c8bbCFxGZ-HpBs3G9tJN8O4PuEYBItzP3gsVhRaXGHs,70
|
|
3
|
+
calypso_api/cli.py,sha256=TTSyvTETOJEaLyqNjTq9ROBI-hgdxzs9zurVnxjSIvM,1218
|
|
4
|
+
calypso_api/main.py,sha256=kYdM0xs2UpEdQA0l-RJIcBgsLE7_41bVu5BfrcXDA78,948
|
|
5
|
+
calypso_api/scaffold.py,sha256=pCOcK0fV4TtoImy_OPjQJA1Y1QGp4WUXRCZkH2Ik69Q,32077
|
|
6
|
+
calypso_api/auth/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
7
|
+
calypso_api/controllers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
|
+
calypso_api/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
9
|
+
calypso_api/core/config.py,sha256=eETOJOQ5E9w72lx1gCOoYJAmc5OK-HofgPxd12G58FE,691
|
|
10
|
+
calypso_api/database/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
11
|
+
calypso_api/database/db.py,sha256=Fgf1o-pYIig69UumK11vqSn2fCUY-TFj1mt6vIR6ZPA,742
|
|
12
|
+
calypso_api/dependencies/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
13
|
+
calypso_api/helpers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
14
|
+
calypso_api/models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
15
|
+
calypso_api/repositories/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
16
|
+
calypso_api/routes/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
17
|
+
calypso_api/routes/health.py,sha256=nK2E2Loh0SUP9apNQVeu5JyflX2uEZQH-BOQQFp3LrQ,179
|
|
18
|
+
calypso_api/schemas/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
19
|
+
calypso_api/services/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
20
|
+
calypso_api/test/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
21
|
+
calypso_api/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
22
|
+
calypso_api-0.1.0.dist-info/METADATA,sha256=VZuACI6QAk01heNsA0aVN_iqs_cOAKmkCHcv8jV-ev8,2274
|
|
23
|
+
calypso_api-0.1.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
24
|
+
calypso_api-0.1.0.dist-info/entry_points.txt,sha256=1OjUzhVPJ8gx098ZWmknCd1ooi4UFfMC0crXQX0mzwo,48
|
|
25
|
+
calypso_api-0.1.0.dist-info/RECORD,,
|