coder16 commited on
Commit
57090be
·
1 Parent(s): 67aa1ca
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. README.md +79 -0
  2. app/__pycache__/__init__.cpython-314.pyc +0 -0
  3. app/__pycache__/main.cpython-314.pyc +0 -0
  4. app/core/__pycache__/__init__.cpython-314.pyc +0 -0
  5. app/core/__pycache__/app.cpython-314.pyc +0 -0
  6. app/core/__pycache__/config.cpython-314.pyc +0 -0
  7. app/core/__pycache__/exception_handlers.cpython-314.pyc +0 -0
  8. app/core/__pycache__/lifespan.cpython-314.pyc +0 -0
  9. app/core/__pycache__/middleware.cpython-314.pyc +0 -0
  10. app/core/__pycache__/security.cpython-314.pyc +0 -0
  11. app/core/app.py +13 -1
  12. app/core/config.py +2 -0
  13. app/core/lifespan.py +17 -3
  14. app/database/__pycache__/__init__.cpython-314.pyc +0 -0
  15. app/database/__pycache__/collections.cpython-314.pyc +0 -0
  16. app/database/__pycache__/indexes.cpython-314.pyc +0 -0
  17. app/database/__pycache__/mongodb.cpython-314.pyc +0 -0
  18. app/database/__pycache__/objectid.cpython-314.pyc +0 -0
  19. app/database/mongodb.py +62 -8
  20. app/exceptions/__pycache__/__init__.cpython-314.pyc +0 -0
  21. app/exceptions/__pycache__/base.cpython-314.pyc +0 -0
  22. app/models/__pycache__/__init__.cpython-314.pyc +0 -0
  23. app/models/__pycache__/categoria.cpython-314.pyc +0 -0
  24. app/models/__pycache__/common.cpython-314.pyc +0 -0
  25. app/models/__pycache__/item.cpython-314.pyc +0 -0
  26. app/models/__pycache__/negocio.cpython-314.pyc +0 -0
  27. app/models/__pycache__/resena.cpython-314.pyc +0 -0
  28. app/models/__pycache__/user.cpython-314.pyc +0 -0
  29. app/rutas/__pycache__/__init__.cpython-314.pyc +0 -0
  30. app/rutas/__pycache__/auth.cpython-314.pyc +0 -0
  31. app/rutas/__pycache__/categorias.cpython-314.pyc +0 -0
  32. app/rutas/__pycache__/deps.cpython-314.pyc +0 -0
  33. app/rutas/__pycache__/items.cpython-314.pyc +0 -0
  34. app/rutas/__pycache__/negocios.cpython-314.pyc +0 -0
  35. app/rutas/__pycache__/resenas.cpython-314.pyc +0 -0
  36. app/schemas/__pycache__/__init__.cpython-314.pyc +0 -0
  37. app/schemas/__pycache__/auth.cpython-314.pyc +0 -0
  38. app/schemas/__pycache__/categoria.cpython-314.pyc +0 -0
  39. app/schemas/__pycache__/common.cpython-314.pyc +0 -0
  40. app/schemas/__pycache__/item.cpython-314.pyc +0 -0
  41. app/schemas/__pycache__/negocio.cpython-314.pyc +0 -0
  42. app/schemas/__pycache__/resena.cpython-314.pyc +0 -0
  43. app/services/__pycache__/__init__.cpython-314.pyc +0 -0
  44. app/services/__pycache__/auth.cpython-314.pyc +0 -0
  45. app/services/__pycache__/base.cpython-314.pyc +0 -0
  46. app/services/__pycache__/categoria.cpython-314.pyc +0 -0
  47. app/services/__pycache__/item.cpython-314.pyc +0 -0
  48. app/services/__pycache__/negocio.cpython-314.pyc +0 -0
  49. app/services/__pycache__/resena.cpython-314.pyc +0 -0
  50. app/services/__pycache__/utils.cpython-314.pyc +0 -0
README.md CHANGED
@@ -51,6 +51,85 @@ app/
51
  └── main.py Punto de entrada (crea la app).
52
  ```
53
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
54
  La API quedará disponible en `http://localhost:7860` (Swagger en `/docs`).
55
 
56
  ## Despliegue en HuggingFace Spaces (Docker)
 
51
  └── main.py Punto de entrada (crea la app).
52
  ```
53
 
54
+ ### Cómo extender
55
+
56
+ Añadir una nueva entidad es directo gracias al `BaseService`
57
+ (`app/services/base.py`), que implementa el CRUD genérico:
58
+
59
+ 1. Define el modelo en `models/` y los esquemas en `schemas/`.
60
+ 2. Crea un servicio que herede de `BaseService` (fija `collection_name`).
61
+ 3. Añade el router en `rutas/` e inclúyelo en `rutas/__init__.py`.
62
+ 4. (Opcional) Declara índices en `database/indexes.py`.
63
+
64
+ ## Requisitos
65
+
66
+ - Python 3.11+
67
+ - MongoDB 4.4+ en ejecución (local o remoto)
68
+
69
+ ## Instalación
70
+
71
+ ```bash
72
+ python -m venv .venv
73
+ source .venv/bin/activate # En Windows: .venv\Scripts\activate
74
+ pip install -r requirements.txt
75
+ ```
76
+
77
+ ## Configuración
78
+
79
+ Copia `.env.example` a `.env` y ajusta los valores:
80
+
81
+ ```bash
82
+ cp .env.example .env
83
+ ```
84
+
85
+ Genera una clave secreta segura para `SECRET_KEY`:
86
+
87
+ ```bash
88
+ python -c "import secrets; print(secrets.token_urlsafe(48))"
89
+ ```
90
+
91
+ | Variable | Descripción | Por defecto |
92
+ |-------------------------------|-----------------------------------------------|-----------------------------|
93
+ | `APP_NAME` | Nombre de la aplicación | `Directorio Online API` |
94
+ | `APP_VERSION` | Versión | `1.0.0` |
95
+ | `APP_DESCRIPTION` | Descripción de la API | (ver `.env.example`) |
96
+ | `DEBUG` | Modo depuración | `false` |
97
+ | `API_PREFIX` | Prefijo de las rutas | `/api/v1` |
98
+ | `PORT` | Puerto de escucha del contenedor | `7860` |
99
+ | `MONGODB_URI` | URI de conexión a MongoDB | `mongodb://localhost:27017` |
100
+ | `MONGODB_DB_NAME` | Nombre de la base de datos | `directorio_online` |
101
+ | `SECRET_KEY` | Clave para firmar los JWT (**obligatoria**) | — |
102
+ | `ALGORITHM` | Algoritmo de firma del JWT | `HS256` |
103
+ | `ACCESS_TOKEN_EXPIRE_MINUTES` | Validez del token de acceso (minutos) | `60` |
104
+ | `CORS_ORIGINS` | Orígenes permitidos (lista por comas o `*`) | `*` |
105
+
106
+ ## Ejecución
107
+
108
+ ```bash
109
+ uvicorn app.main:app --reload
110
+ ```
111
+
112
+ - Documentación interactiva (Swagger): `http://localhost:8000/docs`
113
+ - Documentación alternativa (ReDoc): `http://localhost:8000/redoc`
114
+ - Comprobación de salud: `http://localhost:8000/health`
115
+
116
+ ## Docker
117
+
118
+ La aplicación se empaqueta con el `Dockerfile` incluido. El contenedor
119
+ escucha en el puerto indicado por la variable `PORT` (por defecto `7860`).
120
+
121
+ ```bash
122
+ # Construir la imagen
123
+ docker build -t directorio-online-backend .
124
+
125
+ # Ejecutar (las variables se pasan con -e o con --env-file)
126
+ docker run --rm -p 7860:7860 \
127
+ -e SECRET_KEY="$(python -c 'import secrets; print(secrets.token_urlsafe(48))')" \
128
+ -e MONGODB_URI="mongodb+srv://usuario:password@cluster.mongodb.net" \
129
+ -e MONGODB_DB_NAME="directorio_online" \
130
+ directorio-online-backend
131
+ ```
132
+
133
  La API quedará disponible en `http://localhost:7860` (Swagger en `/docs`).
134
 
135
  ## Despliegue en HuggingFace Spaces (Docker)
app/__pycache__/__init__.cpython-314.pyc ADDED
Binary file (436 Bytes). View file
 
app/__pycache__/main.cpython-314.pyc ADDED
Binary file (369 Bytes). View file
 
app/core/__pycache__/__init__.cpython-314.pyc ADDED
Binary file (321 Bytes). View file
 
app/core/__pycache__/app.cpython-314.pyc ADDED
Binary file (2.71 kB). View file
 
app/core/__pycache__/config.cpython-314.pyc ADDED
Binary file (3.19 kB). View file
 
app/core/__pycache__/exception_handlers.cpython-314.pyc ADDED
Binary file (2.51 kB). View file
 
app/core/__pycache__/lifespan.cpython-314.pyc ADDED
Binary file (2.3 kB). View file
 
app/core/__pycache__/middleware.cpython-314.pyc ADDED
Binary file (1.08 kB). View file
 
app/core/__pycache__/security.cpython-314.pyc ADDED
Binary file (6.41 kB). View file
 
app/core/app.py CHANGED
@@ -27,7 +27,19 @@ def create_app() -> FastAPI:
27
 
28
  @app.get("/health", tags=["Salud"], summary="Estado del servicio")
29
  async def health() -> dict[str, str]:
30
- """Endpoint de comprobación de salud."""
31
  return {"status": "ok"}
32
 
 
 
 
 
 
 
 
 
 
 
 
 
33
  return app
 
27
 
28
  @app.get("/health", tags=["Salud"], summary="Estado del servicio")
29
  async def health() -> dict[str, str]:
30
+ """Liveness: responde siempre y rápido (no depende de la base de datos)."""
31
  return {"status": "ok"}
32
 
33
+ @app.get(
34
+ "/health/db",
35
+ tags=["Salud"],
36
+ summary="Estado de la conexión a MongoDB",
37
+ )
38
+ async def health_db() -> dict[str, str]:
39
+ """Readiness: comprueba si MongoDB responde en este momento."""
40
+ from app.database.mongodb import mongodb
41
+
42
+ conectado = await mongodb.ping()
43
+ return {"database": "connected" if conectado else "disconnected"}
44
+
45
  return app
app/core/config.py CHANGED
@@ -32,6 +32,8 @@ class Settings(BaseSettings):
32
  # --- Base de datos (MongoDB) ---
33
  MONGODB_URI: str = "mongodb://localhost:27017"
34
  MONGODB_DB_NAME: str = "directorio_online"
 
 
35
 
36
  # --- Seguridad / JWT ---
37
  SECRET_KEY: str
 
32
  # --- Base de datos (MongoDB) ---
33
  MONGODB_URI: str = "mongodb://localhost:27017"
34
  MONGODB_DB_NAME: str = "directorio_online"
35
+ # Tiempo máximo (ms) para seleccionar un servidor de MongoDB.
36
+ MONGODB_TIMEOUT_MS: int = 5000
37
 
38
  # --- Seguridad / JWT ---
39
  SECRET_KEY: str
app/core/lifespan.py CHANGED
@@ -1,11 +1,13 @@
1
  """Ciclo de vida de la aplicación.
2
 
3
- Abre la conexión con MongoDB y asegura los índices al arrancar; cierra
4
- la conexión de forma ordenada al apagar.
 
5
  """
6
 
7
  from __future__ import annotations
8
 
 
9
  from contextlib import asynccontextmanager
10
  from typing import AsyncIterator
11
 
@@ -14,12 +16,24 @@ from fastapi import FastAPI
14
  from app.database.indexes import ensure_indexes
15
  from app.database.mongodb import mongodb
16
 
 
 
17
 
18
  @asynccontextmanager
19
  async def lifespan(app: FastAPI) -> AsyncIterator[None]:
20
  """Gestiona los recursos de la aplicación durante su ciclo de vida."""
21
  await mongodb.connect()
22
- await ensure_indexes()
 
 
 
 
 
 
 
 
 
 
23
  try:
24
  yield
25
  finally:
 
1
  """Ciclo de vida de la aplicación.
2
 
3
+ Crea el cliente de MongoDB y, si responde, asegura los índices. Si la base
4
+ de datos no está disponible al arrancar, la aplicación arranca igualmente
5
+ (no se cae) y la conexión se reintentará en cada petición.
6
  """
7
 
8
  from __future__ import annotations
9
 
10
+ import logging
11
  from contextlib import asynccontextmanager
12
  from typing import AsyncIterator
13
 
 
16
  from app.database.indexes import ensure_indexes
17
  from app.database.mongodb import mongodb
18
 
19
+ logger = logging.getLogger(__name__)
20
+
21
 
22
  @asynccontextmanager
23
  async def lifespan(app: FastAPI) -> AsyncIterator[None]:
24
  """Gestiona los recursos de la aplicación durante su ciclo de vida."""
25
  await mongodb.connect()
26
+ if await mongodb.ping():
27
+ try:
28
+ await ensure_indexes()
29
+ except Exception as exc: # noqa: BLE001
30
+ logger.warning("No se pudieron crear los índices: %s", exc)
31
+ else:
32
+ logger.warning(
33
+ "La aplicación arranca SIN conexión a MongoDB. Se reintentará "
34
+ "en cada petición. Revisa MONGODB_URI y el IP Access List de "
35
+ "Atlas (debe incluir 0.0.0.0/0 para hosts en la nube)."
36
+ )
37
  try:
38
  yield
39
  finally:
app/database/__pycache__/__init__.cpython-314.pyc ADDED
Binary file (472 Bytes). View file
 
app/database/__pycache__/collections.cpython-314.pyc ADDED
Binary file (510 Bytes). View file
 
app/database/__pycache__/indexes.cpython-314.pyc ADDED
Binary file (3.04 kB). View file
 
app/database/__pycache__/mongodb.cpython-314.pyc ADDED
Binary file (6.74 kB). View file
 
app/database/__pycache__/objectid.cpython-314.pyc ADDED
Binary file (1.31 kB). View file
 
app/database/mongodb.py CHANGED
@@ -3,11 +3,16 @@
3
  Se utiliza el driver oficial ``pymongo`` con su API asíncrona
4
  (:class:`pymongo.AsyncMongoClient`), disponible de forma estable a
5
  partir de PyMongo 4.13.
 
 
 
 
6
  """
7
 
8
  from __future__ import annotations
9
 
10
  import logging
 
11
 
12
  from pymongo import AsyncMongoClient
13
  from pymongo.asynchronous.collection import AsyncCollection
@@ -22,23 +27,72 @@ class MongoDB:
22
  """Contenedor del cliente y la base de datos de MongoDB.
23
 
24
  Mantiene una única instancia del cliente durante el ciclo de vida de
25
- la aplicación. La conexión se abre en el ``lifespan`` y se cierra al
26
- apagar el servidor.
27
  """
28
 
29
  client: AsyncMongoClient | None = None
30
  database: AsyncDatabase | None = None
31
 
32
  async def connect(self) -> None:
33
- """Abre la conexión con MongoDB y verifica su disponibilidad."""
 
 
 
 
 
34
  if self.client is not None:
35
  return
36
- logger.info("Conectando a MongoDB en %s", settings.MONGODB_URI)
37
- self.client = AsyncMongoClient(settings.MONGODB_URI, tz_aware=True)
 
 
 
38
  self.database = self.client[settings.MONGODB_DB_NAME]
39
- # Verifica que la conexión es válida.
40
- await self.client.admin.command("ping")
41
- logger.info("Conexión a MongoDB establecida (db=%s)", settings.MONGODB_DB_NAME)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
42
 
43
  async def close(self) -> None:
44
  """Cierra la conexión con MongoDB."""
 
3
  Se utiliza el driver oficial ``pymongo`` con su API asíncrona
4
  (:class:`pymongo.AsyncMongoClient`), disponible de forma estable a
5
  partir de PyMongo 4.13.
6
+
7
+ El cliente se crea de forma perezosa: la conexión real se abre en la
8
+ primera operación, por lo que la aplicación puede arrancar aunque la
9
+ base de datos no esté disponible todavía (no se bloquea el arranque).
10
  """
11
 
12
  from __future__ import annotations
13
 
14
  import logging
15
+ import socket
16
 
17
  from pymongo import AsyncMongoClient
18
  from pymongo.asynchronous.collection import AsyncCollection
 
27
  """Contenedor del cliente y la base de datos de MongoDB.
28
 
29
  Mantiene una única instancia del cliente durante el ciclo de vida de
30
+ la aplicación.
 
31
  """
32
 
33
  client: AsyncMongoClient | None = None
34
  database: AsyncDatabase | None = None
35
 
36
  async def connect(self) -> None:
37
+ """Crea el cliente de MongoDB sin bloquear el arranque.
38
+
39
+ No se hace ``ping`` aquí a propósito: el cliente es perezoso y la
40
+ conexión se abre en la primera operación. Así la app arranca aunque
41
+ Mongo no responda todavía.
42
+ """
43
  if self.client is not None:
44
  return
45
+ self.client = AsyncMongoClient(
46
+ settings.MONGODB_URI,
47
+ tz_aware=True,
48
+ serverSelectionTimeoutMS=settings.MONGODB_TIMEOUT_MS,
49
+ )
50
  self.database = self.client[settings.MONGODB_DB_NAME]
51
+
52
+ async def ping(self) -> bool:
53
+ """Comprueba si MongoDB responde. No lanza excepción."""
54
+ if self.client is None:
55
+ return False
56
+ try:
57
+ await self.client.admin.command("ping")
58
+ except Exception as exc: # noqa: BLE001
59
+ logger.warning("MongoDB no responde: %s", exc)
60
+ self._diagnostico_red()
61
+ return False
62
+ logger.info("MongoDB conectado (db=%s).", settings.MONGODB_DB_NAME)
63
+ return True
64
+
65
+ def _diagnostico_red(self) -> None:
66
+ """Registra un diagnóstico de red hacia los nodos de MongoDB.
67
+
68
+ Resuelve el DNS y prueba un socket TCP crudo a cada nodo para
69
+ distinguir un bloqueo de red de un problema del driver/credenciales.
70
+ """
71
+ try:
72
+ nodos = self.client.topology_description.server_descriptions()
73
+ except Exception: # noqa: BLE001
74
+ return
75
+ for host, port in nodos:
76
+ try:
77
+ ip = socket.gethostbyname(host)
78
+ except OSError as exc:
79
+ logger.warning("DIAG DNS %s -> FALLA (%s)", host, exc)
80
+ continue
81
+ sock = socket.socket()
82
+ sock.settimeout(5)
83
+ try:
84
+ sock.connect((host, port))
85
+ logger.warning("DIAG TCP %s:%s (%s) -> OK", host, port, ip)
86
+ except OSError as exc:
87
+ logger.warning(
88
+ "DIAG TCP %s:%s (%s) -> BLOQUEADO/timeout (%s)",
89
+ host,
90
+ port,
91
+ ip,
92
+ type(exc).__name__,
93
+ )
94
+ finally:
95
+ sock.close()
96
 
97
  async def close(self) -> None:
98
  """Cierra la conexión con MongoDB."""
app/exceptions/__pycache__/__init__.cpython-314.pyc ADDED
Binary file (495 Bytes). View file
 
app/exceptions/__pycache__/base.cpython-314.pyc ADDED
Binary file (3.17 kB). View file
 
app/models/__pycache__/__init__.cpython-314.pyc ADDED
Binary file (932 Bytes). View file
 
app/models/__pycache__/categoria.cpython-314.pyc ADDED
Binary file (938 Bytes). View file
 
app/models/__pycache__/common.cpython-314.pyc ADDED
Binary file (5.74 kB). View file
 
app/models/__pycache__/item.cpython-314.pyc ADDED
Binary file (1.21 kB). View file
 
app/models/__pycache__/negocio.cpython-314.pyc ADDED
Binary file (1.74 kB). View file
 
app/models/__pycache__/resena.cpython-314.pyc ADDED
Binary file (1.08 kB). View file
 
app/models/__pycache__/user.cpython-314.pyc ADDED
Binary file (1.16 kB). View file
 
app/rutas/__pycache__/__init__.cpython-314.pyc ADDED
Binary file (852 Bytes). View file
 
app/rutas/__pycache__/auth.cpython-314.pyc ADDED
Binary file (3.02 kB). View file
 
app/rutas/__pycache__/categorias.cpython-314.pyc ADDED
Binary file (4.26 kB). View file
 
app/rutas/__pycache__/deps.cpython-314.pyc ADDED
Binary file (4.42 kB). View file
 
app/rutas/__pycache__/items.cpython-314.pyc ADDED
Binary file (4.22 kB). View file
 
app/rutas/__pycache__/negocios.cpython-314.pyc ADDED
Binary file (4.3 kB). View file
 
app/rutas/__pycache__/resenas.cpython-314.pyc ADDED
Binary file (4.16 kB). View file
 
app/schemas/__pycache__/__init__.cpython-314.pyc ADDED
Binary file (1.06 kB). View file
 
app/schemas/__pycache__/auth.cpython-314.pyc ADDED
Binary file (2.58 kB). View file
 
app/schemas/__pycache__/categoria.cpython-314.pyc ADDED
Binary file (1.78 kB). View file
 
app/schemas/__pycache__/common.cpython-314.pyc ADDED
Binary file (1.15 kB). View file
 
app/schemas/__pycache__/item.cpython-314.pyc ADDED
Binary file (2.16 kB). View file
 
app/schemas/__pycache__/negocio.cpython-314.pyc ADDED
Binary file (2.83 kB). View file
 
app/schemas/__pycache__/resena.cpython-314.pyc ADDED
Binary file (1.83 kB). View file
 
app/services/__pycache__/__init__.cpython-314.pyc ADDED
Binary file (582 Bytes). View file
 
app/services/__pycache__/auth.cpython-314.pyc ADDED
Binary file (3.56 kB). View file
 
app/services/__pycache__/base.cpython-314.pyc ADDED
Binary file (6.83 kB). View file
 
app/services/__pycache__/categoria.cpython-314.pyc ADDED
Binary file (6.79 kB). View file
 
app/services/__pycache__/item.cpython-314.pyc ADDED
Binary file (4.6 kB). View file
 
app/services/__pycache__/negocio.cpython-314.pyc ADDED
Binary file (6.07 kB). View file
 
app/services/__pycache__/resena.cpython-314.pyc ADDED
Binary file (4.48 kB). View file
 
app/services/__pycache__/utils.cpython-314.pyc ADDED
Binary file (1.26 kB). View file