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.
File without changes
@@ -0,0 +1,4 @@
1
+ from calypso_api.cli import app
2
+
3
+ if __name__ == "__main__":
4
+ app()
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
@@ -0,0 +1,8 @@
1
+
2
+ from fastapi import APIRouter
3
+
4
+ router = APIRouter(tags=["Health"])
5
+
6
+ @router.get("/health")
7
+ async def health_check():
8
+ return {"status": "ok", "message": "Service is healthy"}
@@ -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,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.28.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ calypso = calypso_api.cli:app