para.AI_ASSUNTOS_CNJ / app /main_generic.py
Carlexxx
para.AI beta
19a52e4
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>&#x2696;&#xFE0F; Assuntos Jur&#xED;dicos <span class="badge">v1.0</span></h1>
<p class="sub">
API de busca full-text e navega&#xE7;&#xE3;o hier&#xE1;rquica sobre os assuntos jur&#xED;dicos
do STJ/TST, indexados no Elasticsearch com <em>analyzer</em> de portugu&#xEA;s.
</p>
<div class="links">
<a class="btn primary" href="/docs">&#x1F4D6; Swagger UI</a>
<a class="btn secondary" href="/redoc">&#x1F4DA; ReDoc</a>
<a class="btn secondary" href="/hierarquia">&#x1F333; Hierarquia</a>
<a class="btn secondary" href="/health">&#x1F49A; Health</a>
<a class="btn secondary" href="/busca?q=responsabilidade+civil&com_facets=true">&#x1F50D; Busca Demo</a>
<a class="btn secondary" href="/autocomplete?q=furto">&#x26A1; 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">&#xB7; &#xED;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">&#xB7; 22 ramos &#xB7; Elasticsearch 8.x &#xB7; FastAPI &#xB7; Porta 7860</span>';
}
})();
</script>
</body>
</html>
"""