Diiegoal commited on
Commit
afbef75
·
1 Parent(s): 011e5b6

Actualizo la app

Browse files
Files changed (2) hide show
  1. README.md +60 -2
  2. app/core/config.py +11 -5
README.md CHANGED
@@ -201,8 +201,14 @@ Variables principales:
201
  - `ORACULO_RATE_LIMIT_REQUESTS`
202
  - `ORACULO_RATE_LIMIT_WINDOW_SECONDS`
203
  - `ORACULO_MAX_REQUEST_SIZE_BYTES`
 
 
204
 
205
- Toma como base el archivo `.env.example`.
 
 
 
 
206
 
207
  ## Ejecución local
208
 
@@ -242,7 +248,7 @@ venv\Scripts\pytest -q
242
 
243
  Estado actual de la suite:
244
 
245
- - `33 passed`
246
 
247
  ## Despliegue en Render
248
 
@@ -286,6 +292,7 @@ Pasos:
286
  - `ORACULO_SEED_ADMIN_EMAIL`
287
  - `ORACULO_SEED_ADMIN_PASSWORD`
288
  - `ORACULO_ALLOWED_HOSTS`
 
289
 
290
  5. Si quieres persistencia real para SQLite, usa almacenamiento persistente y define:
291
 
@@ -299,9 +306,60 @@ Notas importantes para Spaces:
299
 
300
  - Swagger abrirá en `/docs` porque el Space usa `base_path: /docs`.
301
  - El contenedor escucha en `7860`, que es el puerto esperado por el Space.
 
 
302
  - Si quieres ocultar Swagger más adelante, cambia `ORACULO_DOCS_ENABLED=false`.
303
  - El modelo `pipeline_produccion.pkl`, `adult.csv` y el código backend deben permanecer en el repositorio o en el contexto del contenedor.
304
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
305
  ## Qué falta para una versión todavía más dura
306
 
307
  Si quieres llevarla más arriba todavía, las siguientes mejoras son naturales:
 
201
  - `ORACULO_RATE_LIMIT_REQUESTS`
202
  - `ORACULO_RATE_LIMIT_WINDOW_SECONDS`
203
  - `ORACULO_MAX_REQUEST_SIZE_BYTES`
204
+ - `ORACULO_DOCS_ENABLED`
205
+ - `ORACULO_SECURITY_HEADERS_ENABLED`
206
 
207
+ Uso recomendado por entorno:
208
+
209
+ - Local: crea un archivo `.env` en la raíz del proyecto.
210
+ - Hugging Face Spaces: configura estas variables desde `Settings > Variables and secrets`.
211
+ - Producción tradicional: usa variables de entorno del proveedor, no hardcodes secretos en el repositorio.
212
 
213
  ## Ejecución local
214
 
 
248
 
249
  Estado actual de la suite:
250
 
251
+ - `35 passed`
252
 
253
  ## Despliegue en Render
254
 
 
292
  - `ORACULO_SEED_ADMIN_EMAIL`
293
  - `ORACULO_SEED_ADMIN_PASSWORD`
294
  - `ORACULO_ALLOWED_HOSTS`
295
+ - `ORACULO_DOCS_ENABLED=true`
296
 
297
  5. Si quieres persistencia real para SQLite, usa almacenamiento persistente y define:
298
 
 
306
 
307
  - Swagger abrirá en `/docs` porque el Space usa `base_path: /docs`.
308
  - El contenedor escucha en `7860`, que es el puerto esperado por el Space.
309
+ - El `Dockerfile` arranca con `alembic upgrade head` y luego `uvicorn app.main:app --host 0.0.0.0 --port 7860`.
310
+ - No necesitas un archivo `.env` dentro del Space si ya definiste las variables en `Settings > Variables and secrets`.
311
  - Si quieres ocultar Swagger más adelante, cambia `ORACULO_DOCS_ENABLED=false`.
312
  - El modelo `pipeline_produccion.pkl`, `adult.csv` y el código backend deben permanecer en el repositorio o en el contexto del contenedor.
313
 
314
+ ### Troubleshooting en Spaces
315
+
316
+ #### El contenedor arrancó, pero el `App` tab dice "ha rechazado la conexión"
317
+
318
+ Si en los logs ves algo como esto:
319
+
320
+ - `Uvicorn running on http://0.0.0.0:7860`
321
+ - `GET /docs HTTP/1.1" 200`
322
+
323
+ entonces el contenedor sí arrancó bien y el problema no suele ser el `Dockerfile`.
324
+
325
+ La causa más común en este proyecto era de seguridad de embebido:
326
+
327
+ - Hugging Face renderiza el `App` tab dentro de un `iframe`.
328
+ - Si la API responde con `X-Frame-Options: DENY` o `Content-Security-Policy` con `frame-ancestors 'none'`, el navegador bloquea la interfaz aunque `/docs` responda `200`.
329
+
330
+ La versión actual del proyecto ya quedó ajustada para ese escenario:
331
+
332
+ - mantiene headers estrictos para tráfico normal
333
+ - permite embebido únicamente cuando la petición llega desde `*.hf.space`
334
+ - conserva Swagger funcional en `/docs`
335
+
336
+ Checklist rápida cuando pase esto:
337
+
338
+ 1. Abre primero la URL directa del Space, por ejemplo `https://tu-space.hf.space/docs`.
339
+ 2. Si la URL directa carga y el `App` tab no, haz `Factory reboot` del Space.
340
+ 3. Verifica que tu Space realmente tenga la última versión del repositorio.
341
+ 4. Revisa que `ORACULO_ALLOWED_HOSTS` incluya `*.hf.space,*.huggingface.co`.
342
+ 5. Confirma que `ORACULO_DOCS_ENABLED=true` si quieres exponer Swagger.
343
+
344
+ #### No tengo `.env` en Hugging Face
345
+
346
+ Eso es normal.
347
+
348
+ - En Spaces no necesitas subir `.env`.
349
+ - Hugging Face inyecta variables y secretos desde la configuración del Space.
350
+ - Localmente sí puedes usar `.env` para desarrollar.
351
+
352
+ #### SQLite se reinicia sola en Spaces
353
+
354
+ Eso también es normal si no configuraste almacenamiento persistente.
355
+
356
+ - Sin volumen persistente, la base local es efímera.
357
+ - Si quieres conservar usuarios, historial y seeds entre reinicios, usa `/data` y configura:
358
+
359
+ ```bash
360
+ ORACULO_DATABASE_URL=sqlite:////data/oraculo.db
361
+ ```
362
+
363
  ## Qué falta para una versión todavía más dura
364
 
365
  Si quieres llevarla más arriba todavía, las siguientes mejoras son naturales:
app/core/config.py CHANGED
@@ -1,11 +1,12 @@
1
  from __future__ import annotations
2
 
 
3
  from functools import lru_cache
4
  from pathlib import Path
5
- from typing import Literal
6
 
7
  from pydantic import Field, field_validator
8
- from pydantic_settings import BaseSettings, SettingsConfigDict
9
 
10
 
11
  class Settings(BaseSettings):
@@ -41,7 +42,7 @@ class Settings(BaseSettings):
41
  jwt_algorithm: str = "HS256"
42
  access_token_expire_minutes: int = 60
43
 
44
- allowed_hosts: list[str] = Field(
45
  default_factory=lambda: [
46
  "localhost",
47
  "127.0.0.1",
@@ -50,7 +51,7 @@ class Settings(BaseSettings):
50
  "*.huggingface.co",
51
  ]
52
  )
53
- cors_allow_origins: list[str] = Field(
54
  default_factory=lambda: ["http://localhost:3000", "http://127.0.0.1:3000"]
55
  )
56
 
@@ -58,7 +59,7 @@ class Settings(BaseSettings):
58
  rate_limit_enabled: bool = True
59
  rate_limit_requests: int = 60
60
  rate_limit_window_seconds: int = 60
61
- rate_limit_exempt_paths: list[str] = Field(
62
  default_factory=lambda: [
63
  "/",
64
  "/docs",
@@ -80,6 +81,11 @@ class Settings(BaseSettings):
80
  return value
81
  if not value:
82
  return []
 
 
 
 
 
83
  return [item.strip() for item in value.split(",") if item.strip()]
84
 
85
  @property
 
1
  from __future__ import annotations
2
 
3
+ import json
4
  from functools import lru_cache
5
  from pathlib import Path
6
+ from typing import Annotated, Literal
7
 
8
  from pydantic import Field, field_validator
9
+ from pydantic_settings import BaseSettings, NoDecode, SettingsConfigDict
10
 
11
 
12
  class Settings(BaseSettings):
 
42
  jwt_algorithm: str = "HS256"
43
  access_token_expire_minutes: int = 60
44
 
45
+ allowed_hosts: Annotated[list[str], NoDecode] = Field(
46
  default_factory=lambda: [
47
  "localhost",
48
  "127.0.0.1",
 
51
  "*.huggingface.co",
52
  ]
53
  )
54
+ cors_allow_origins: Annotated[list[str], NoDecode] = Field(
55
  default_factory=lambda: ["http://localhost:3000", "http://127.0.0.1:3000"]
56
  )
57
 
 
59
  rate_limit_enabled: bool = True
60
  rate_limit_requests: int = 60
61
  rate_limit_window_seconds: int = 60
62
+ rate_limit_exempt_paths: Annotated[list[str], NoDecode] = Field(
63
  default_factory=lambda: [
64
  "/",
65
  "/docs",
 
81
  return value
82
  if not value:
83
  return []
84
+ normalized_value = value.strip()
85
+ if normalized_value.startswith("["):
86
+ parsed_value = json.loads(normalized_value)
87
+ if isinstance(parsed_value, list):
88
+ return [str(item).strip() for item in parsed_value if str(item).strip()]
89
  return [item.strip() for item in value.split(",") if item.strip()]
90
 
91
  @property