Spaces:
Sleeping
Sleeping
Actualizo la app
Browse files- README.md +60 -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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 206 |
|
| 207 |
## Ejecución local
|
| 208 |
|
|
@@ -242,7 +248,7 @@ venv\Scripts\pytest -q
|
|
| 242 |
|
| 243 |
Estado actual de la suite:
|
| 244 |
|
| 245 |
-
- `
|
| 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
|