| from __future__ import annotations |
| import os |
| from contextlib import asynccontextmanager |
| from pathlib import Path |
| from fastapi import FastAPI |
| from fastapi.responses import ORJSONResponse, HTMLResponse |
| from fastapi.middleware.cors import CORSMiddleware |
| from elasticsearch import AsyncElasticsearch |
| from .core.config_loader import load_config |
| from .core.indexer import setup_index |
| from .routes_generic import build_router |
|
|
| CONFIG_PATH = os.getenv("CONFIG_PATH", "/app/config-assuntos-juridicos.yaml") |
| config = load_config(CONFIG_PATH) |
| _es: AsyncElasticsearch | None = None |
|
|
| def get_es(): |
| global _es |
| if _es is None: |
| _es = AsyncElasticsearch([config.es_host],request_timeout=config.elasticsearch.timeout,retry_on_timeout=True,max_retries=config.elasticsearch.max_retries) |
| return _es |
|
|
| @asynccontextmanager |
| async def lifespan(app): |
| await setup_index(get_es(), config) |
| yield |
| if _es: await _es.close() |
|
|
| app = FastAPI(title=config.display_name or config.index_name,version="2.0.0",default_response_class=ORJSONResponse,lifespan=lifespan) |
| app.add_middleware(CORSMiddleware,allow_origins=["*"],allow_credentials=True,allow_methods=["*"],allow_headers=["*"]) |
| app.include_router(build_router(config, get_es)) |
|
|
| @app.get("/", response_class=HTMLResponse, include_in_schema=False) |
| async def root(): |
| return """ |
| <!DOCTYPE html> |
| <html lang="pt-BR"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>Assuntos Jurídicos API</title> |
| <style> |
| * { box-sizing: border-box; margin: 0; padding: 0; } |
| body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; |
| background: #0f172a; color: #e2e8f0; min-height: 100vh; |
| display: flex; align-items: center; justify-content: center; padding: 16px; } |
| .card { background: #1e293b; border-radius: 16px; padding: 40px; |
| max-width: 640px; width: 100%; box-shadow: 0 25px 50px rgba(0,0,0,.5); } |
| h1 { font-size: 1.9rem; font-weight: 700; color: #38bdf8; margin-bottom: 8px; } |
| .sub { color: #94a3b8; margin-bottom: 28px; line-height: 1.6; font-size: .95rem; } |
| .badge { display: inline-block; background: #0ea5e9; color: #fff; |
| font-size: .7rem; padding: 2px 10px; border-radius: 99px; |
| font-weight: 700; margin-left: 8px; vertical-align: middle; } |
| .links { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; } |
| a.btn { display: block; padding: 13px 18px; border-radius: 10px; |
| text-decoration: none; font-weight: 600; font-size: .9rem; |
| transition: transform .15s, box-shadow .15s; text-align: center; } |
| a.btn:hover { transform: translateY(-2px); box-shadow: 0 8px 20px rgba(0,0,0,.4); } |
| .primary { background: #0ea5e9; color: #fff; } |
| .secondary { background: #334155; color: #e2e8f0; } |
| .stat { margin-top: 24px; padding: 16px 18px; background: #0f172a; |
| border-radius: 10px; font-size: .85rem; color: #64748b; |
| display: flex; flex-wrap: wrap; gap: 8px; align-items: center; } |
| .stat .pill { background: #1e293b; border: 1px solid #334155; |
| border-radius: 8px; padding: 4px 12px; color: #38bdf8; |
| font-weight: 700; font-size: .8rem; } |
| .stat .lbl { color: #475569; font-size: .78rem; } |
| .dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; |
| background: #22c55e; margin-right: 6px; } |
| .dot.yellow { background: #eab308; } |
| .dot.red { background: #ef4444; } |
| #es-status { color: #94a3b8; font-size: .78rem; } |
| </style> |
| </head> |
| <body> |
| <div class="card"> |
| <h1>⚖️ Assuntos Jurídicos <span class="badge">v1.0</span></h1> |
| <p class="sub"> |
| API de busca full-text e navegação hierárquica sobre os assuntos jurídicos |
| do STJ/TST, indexados no Elasticsearch com <em>analyzer</em> de português. |
| </p> |
| |
| <div class="links"> |
| <a class="btn primary" href="/docs">📖 Swagger UI</a> |
| <a class="btn secondary" href="/redoc">📚 ReDoc</a> |
| <a class="btn secondary" href="/hierarquia">🌳 Hierarquia</a> |
| <a class="btn secondary" href="/health">💚 Health</a> |
| <a class="btn secondary" href="/busca?q=responsabilidade+civil&com_facets=true">🔍 Busca Demo</a> |
| <a class="btn secondary" href="/autocomplete?q=furto">⚡ Autocomplete</a> |
| </div> |
| |
| <div class="stat" id="stat-bar"> |
| <span class="lbl">Carregando stats...</span> |
| </div> |
| </div> |
| |
| <script> |
| (async function() { |
| const bar = document.getElementById('stat-bar'); |
| try { |
| const r = await fetch('/health'); |
| if (!r.ok) throw new Error('HTTP ' + r.status); |
| const d = await r.json(); |
| const dotClass = d.es_status === 'green' ? '' : d.es_status === 'yellow' ? 'yellow' : 'red'; |
| bar.innerHTML = |
| '<span class="pill">' + (d.total_docs || '?').toLocaleString('pt-BR') + ' docs</span>' + |
| '<span class="lbl">· índice:</span>' + |
| '<span class="pill">' + (d.index || '?') + '</span>' + |
| '<span id="es-status"><span class="dot ' + dotClass + '"></span>ES ' + (d.es_status || '?') + '</span>'; |
| } catch(e) { |
| bar.innerHTML = |
| '<span class="pill">5.184 assuntos</span>' + |
| '<span class="lbl">· 22 ramos · Elasticsearch 8.x · FastAPI · Porta 7860</span>'; |
| } |
| })(); |
| </script> |
| </body> |
| </html> |
| """ |
|
|