BLAPO v2 — manual sencillo
Backend como un ORM público — protocolo de lógica temporal
BLAPO no está atado a ningún framework. Piénsalo como una receta que todos siguen: el cliente envía filtros temporales ambiguos; tu backend los normaliza a una sola forma canónica y luego decide entre respuesta tipo rollup o mapa por periodo.
Mantén esta abstracción estable en todas partes (Python, Node, capa SQL, edge workers)—la semántica es idéntica.
Tabla de contenidos
- Vista general
- Tres modos (PRC · PRSC · MPRC)
- Campos de entrada que acepta tu API
- Forma interna canónica
- Resolver una vez, ramificar sin ambigüedad
- Piezas SQL de apoyo
- Lista · trampas · cierre
Vista general
| Código | Significa |
|---|---|
| PRC | Una ventana continua inclusiva [inicio … fin] |
| PRSC | Rebanadas elegidas a mano (meses y/o días)—sin meses relleno |
| MPRC | Rangos más fechas sueltas—cualquier “específico” fuerza partición |
Reglas memorables:
- Muchos formatos de entrada ⇒ solo una
{ "ranges": [...], "specificPeriods": [...] }después de normalizar. - Si algo queda en
specificPeriods(tras merges) ⇒ payload particionado (diccionario con clave por periodo). - Solo rangos continuos, sin específicos ⇒ payload agregado (rollup / arreglos).
PRC · barrido continuo
Ejemplo de consulta:
GET /analytics?startDate=2025-01-01&endDate=2025-01-31
Forma interna normalizada (el idioma del contrato es json; en Python/JS usa dict/objeto):
{
"ranges": [{ "startDate": "2025-01-01", "endDate": "2025-01-31" }],
"specificPeriods": []
}
Intuición de respuesta agregada:
{ "total": 100, "value": 5000 }
PRSC · meses o días puntualmente elegidos
GET /analytics?specificPeriods=2025-01,2025-03,2025-05
Forma interna:
{
"ranges": [],
"specificPeriods": ["2025-01", "2025-03", "2025-05"]
}
Intuición particionada:
{
"2025-01": { "total": 30, "value": 1500 },
"2025-03": { "total": 40, "value": 2000 },
"2025-05": { "total": 30, "value": 1500 }
}
MPRC · combina rangos con fechas puntual
Ejemplo de payload:
{
"ranges": [{ "start": "2025-01-01", "end": "2025-01-07" }],
"specifics": ["2025-02-14", "2025-03-15"]
}
Variante apta para URL:
GET /analytics?temporalLogic={"ranges":[{"start":"2025-01-01","end":"2025-01-07"}],"specifics":["2025-02-14","2025-03-15"]}
Normalizado internamente:
{
"ranges": [{ "startDate": "2025-01-01", "endDate": "2025-01-07" }],
"specificPeriods": ["2025-02-14", "2025-03-15"]
}
Regla de oro:
| Después de normalizar | Estilo de respuesta |
|---|---|
Persiste cualquier specificPeriods | Mapa particionado |
| Solo rangos, sin específicos | Rollup agregado |
Campos de entrada — abstracción de la forma de la petición
Trátalo como agnóstico de schema (OpenAPI/Pydantic/Dataclass/Zod—todo válido). Contrato JSON mínimo:
{
"startDate": "optional ISO month or date",
"endDate": "paired with startDate",
"specificPeriods": "array or comma string",
"temporalLogic": "object OR JSON string for MPRC"
}
Los validadores deben garantizar:
- Al menos un control entre
startDate+endDate,specificPeriods,temporalLogic. - Tokens
YYYY-MMoYYYY-MM-DD. - Si
temporalLogices cadena ⇒ parsear JSON antes de usar.
Helpers reutilizables
| Nombre | Responsabilidad |
|---|---|
resolve_temporal_selection | Entrada polimórfica ⇒ canónico { ranges, specific_periods } |
normalize_date | Tokens mes-only expanden límites inclusivos |
normalize_period_key | Llaves de diccionario ⇄ proyección SQL |
enumerate_dates_inclusive | Rangos ⇄ helpers por día al fusionar |
filter_valid_periods | Descarta tokens mal formados en silencio |
filter_valid_periods — Python
import re
def filter_valid_periods(periods):
out = []
for p in periods:
t = str(p).strip()
if re.fullmatch(r"\d{4}-\d{2}", t) or re.fullmatch(r"\d{4}-\d{2}-\d{2}", t):
out.append(t)
return out
filter_valid_periods — JavaScript
function filterValidPeriods(periods) {
return periods.map(String).map((t) => t.trim()).filter((t) =>
/^\d{4}-\d{2}$/.test(t) || /^\d{4}-\d{2}-\d{2}$/.test(t),
);
}
Orden de resolución en una función
Sin importar el runtime, mantén esta prioridad:
- MPRC — parsea
temporalLogic; mapea"start/end"⇒ranges,"specifics"⇒specificPeriods; ejecutafilter_valid_periods. - PRSC — CSV / cadenas tipo JSON ⇒
specificPeriods. - PRC — un rango si
startDate+endDatesobreviven validación.
Devuelve errores de estructura claros (JSON mal, triple estado absurdo) en lugar de 500 opacos—pero puedes silenciar meses basura si el producto prefiere UX silenciosa.
Fusionar specifics con rangos expandidos Python
Antes del SQL unifica llaves:
def merged_period_keys(ranges, specifics):
keys = set(filter_valid_periods(specifics))
for r in ranges:
for d in enumerate_dates_inclusive(r["startDate"], r["endDate"]):
keys.add(d)
return sorted(keys)
La misma pieza JavaScript
function mergedPeriodKeys(ranges, specifics) {
const keys = new Set(filterValidPeriods(specifics));
for (const r of ranges) {
for (const d of enumerateDatesInclusive(r.startDate, r.endDate)) {
keys.add(d);
}
}
return [...keys].sort();
}
Implementa enumerate_dates_inclusive una vez por runtime—solo aritmética de fechas.
Esquema de handler HTTP (agnóstico de framework)
Python (Flask-style)
@app.get("/analytics")
def analytics_metrics():
qs = flask.request.args
selection = resolve_temporal_selection(dict(qs))
return metrics_service.rollups(selection)
JavaScript (Express-style)
app.get("/analytics", async (req, res) => {
const selection = resolveTemporalSelection(req.query);
res.json(await metricsService.rollups(selection));
});
El único acoplamiento es { ranges, specificPeriods } hacia la capa que toca SQL.
Predicados SQL — esbozo Python compatible MySQL
Los dialectos difieren (DATE_TRUNC vs DATE_FORMAT). Mantén el orden de argumentos determinista.
def build_temporal_where(column_expr, selection):
parts, values = [], []
if selection.get("ranges"):
for r in selection["ranges"]:
parts.append(f"({column_expr} BETWEEN ? AND ?)")
values.extend([r["startDate"], r["endDate"]])
specs = filter_valid_periods(selection.get("specificPeriods") or [])
months = [p for p in specs if len(p) == 7]
days = [p for p in specs if len(p) == 10]
if months:
placeholders = ", ".join(["?"] * len(months))
parts.append(f"(DATE_FORMAT({column_expr}, '%Y-%m') IN ({placeholders}))")
values.extend(months)
if days:
placeholders = ", ".join(["?"] * len(days))
parts.append(f"(DATE({column_expr}) IN ({placeholders}))")
values.extend(days)
if not parts:
return "(1 = 0)", []
return "(" + " OR ".join(parts) + ")", values
Idea portable: el ORM/driver que uses, reutiliza el mismo ramificado al emitir SQL.
Los agregados particionados necesitan period_key determinista—reutiliza la intuición mes/día del texto (COALESCE en MySQL, date_trunc/CASE en Postgres).
Consulta particionada — esquema SQL
SELECT
/* period_key expression */
SUM(amount) AS value
FROM fact_table
WHERE ( /* temporal OR-clauses */ )
AND /* tenant predicates */
GROUP BY period_key
ORDER BY period_key;
Vallas:
- Nunca emitas
IN ()vacío—usa'1 = 0'. - Parámetros enlazados en orden documentado junto a la consulta.
Lista de verificación · alinear equipos
Antes de codificar
- Qué URLs/cuerpos ingieren filtros temporales
- Nombre canónico de columna timestamp
- Forma de respuesta objetivo (
single,array, mapa con clave)
Capa HTTP
- Parsea query/json a dict plano—no trocear strings ad hoc en todos lados
- Errores estructurales en voz alta, typos cosméticos suave (política)
Capa servicio
- Un solo punto de entrada
resolve_temporal_selection - Helpers
merged_period_*cuando haya entradas mixtas - Ruta particionada si
len(specificPeriods) > 0tras merges
Capa almacenamiento
- Constructor compartido de
WHEREtemporal - Ruta opcional
GROUP BY period_key - Métodos legacy adapados a
{ ranges, specificPeriods }
Trampas frecuentes
- Orden binders ≠ orden docs — documenta el orden junto al SQL.
IN ()vacío — protege con tautología falsa.- Formas mezcladas (
2025-01vs2025-01-15) — normaliza antes del mapa. - Buckets vacíos perdidos — pre-rellena claves esperadas (
0). - Zona horaria — deja UTC vs warehouse-local explícito.
Frase final
Normaliza cualquier entrada a { ranges, specificPeriods }, bifurca partición vs agregación, mantén builders SQL simétricos con listas de bind y expón handlers HTTP finos (Python, JavaScript, u otros)—la abstracción es intención temporal, jamás el runtime del proveedor.