import datetime as dt, json, sys
import requests, pandas as pd
from tenacity import retry, stop_after_attempt, wait_exponential
from dotenv import load_dotenv
import helpers
load_dotenv()
CITIES = {"Buenos Aires": (-34.61, -58.38), "Santiago": (-33.45, -70.67), "Lima": (-12.05, -77.04), "Bogotá": (4.71, -74.07), "Madrid": (40.42, -3.70)}
@retry(stop=stop_after_attempt(4), wait=wait_exponential(min=1, max=10))
def fetch(lat, lon, start, end):
r = requests.get("https://archive-api.open-meteo.com/v1/archive", params={"latitude": lat, "longitude": lon, "start_date": start,
"end_date": end, "daily": "temperature_2m_max,precipitation_sum", "timezone": "auto"}, timeout=30)
r.raise_for_status()
return r.json()["daily"]
end = dt.date(2026, 10, 10); start = end - dt.timedelta(days=29)
rows = []
for city, (lat, lon) in CITIES.items():
d = fetch(lat, lon, start.isoformat(), end.isoformat())
rows += [{"city": city, "date": f, "tmax": t, "rain": p} for f, t, p in zip(d["time"], d["temperature_2m_max"], d["precipitation_sum"])]
df = pd.DataFrame(rows)
df.to_parquet(sys.argv[1] + "/weather_python.parquet", index=False)
summary = df.groupby("city").agg(tmax_mean=("tmax", "mean"), rain_total=("rain", "sum")).reset_index()
summary["tmax_mean"] = summary["tmax_mean"].map(helpers.round1)
summary.to_csv(sys.argv[1] + "/summary_python.csv", index=False)
print(summary.to_string(index=False))
print("what the imported module can do:", json.dumps(helpers.capabilities(), ensure_ascii=False))← Todos los casos
Caso 3 · Medido el 2026-10-16 · Synsema 0.6.44 · mismos datos y mismo modelo de los dos lados
Qué puede hacer una dependencia
Tu ETL importa un módulo auxiliar. ¿Qué puede hacer ese módulo con los permisos de tu programa?
14 vs 0
paquetes de terceros en el mismo ETL de 29 líneas
El mismo ETL: 14 paquetes y 169 MB de código ajeno en Python, cero en Synsema.
El problema
Los paquetes envenenados en npm y PyPI que roban tokens y secretos de CI ya son cosa de todos los meses.
En Python o Node, cualquier módulo importado corre con todos los permisos del proceso. Un ETL chico trae una docena de paquetes que nadie lee.
Qué construimos
- Un ETL real, escrito dos veces en 29 líneas: clima diario de cinco ciudades desde Open-Meteo a Parquet más un resumen CSV. Los dos dan los mismos números.
- Cada versión importa un módulo auxiliar que representa cualquier dependencia. Formatea números y además informa qué podría hacer: leer .env, abrir una conexión a un host no declarado.
- Cantidad de dependencias, tamaño y vulnerabilidades conocidas (pip-audit) del lado de Python; synsema code caps y synsema build del lado de Synsema.
Resultados
| Python | Synsema | |
|---|---|---|
| Paquetes, directos y transitivos | ✗14 | ✓0 |
| Código de terceros en disco | ✗169 MB · 2156 archivos .py · 99 binarios nativos | ✓ninguno |
| Vulnerabilidades conocidas (pip-audit) | ✗3 avisos en requests, pyarrow, python-dotenv | ✓nada que auditar |
| El módulo importado puede leer .env | ✗sí | ✓no |
| El módulo importado puede ir a un host no declarado | ✗sí | ✓no |
| Saber qué toca antes de ejecutar | ✗no, hay que leer los 14 paquetes | ✓sí, code caps marca MISSING file.read(".env") y net("example.com") |
| Distribución | ·intérprete + venv + 14 paquetes | ✓un binario de 45,9 MB |
| Tiempo de ejecución | ·6,8 a 9,3 s (1,3 s solo en importar; ciudades en serie) | ·1,4 s (ciudades en paralelo) |
El código, lado a lado
Lo habitual
Synsema
use "./helpers.syn" as helpers
require net("archive-api.open-meteo.com")
require file.write("./cases/03-supply-chain/results/*")
let CITIES be [{"c": "Buenos Aires", "lat": -34.61, "lon": -58.38}, {"c": "Santiago", "lat": -33.45, "lon": -70.67},
{"c": "Lima", "lat": -12.05, "lon": -77.04}, {"c": "Bogotá", "lat": 4.71, "lon": -74.07}, {"c": "Madrid", "lat": 40.42, "lon": -3.70}]
let END be date("2026-10-10")
let START be END - duration(days = 29)
task fetch(x)
let n be 0
while n < 4
let r be http_get("https://archive-api.open-meteo.com/v1/archive", {}, {"latitude": text(x.lat), "longitude": text(x.lon),
"start_date": text(START), "end_date": text(END), "daily": "temperature_2m_max,precipitation_sum", "timezone": "auto"}, 30)
when r.ok
let d be (json of r).daily
give apply((e) => {"city": x.c, "date": e.item, "tmax": d.temperature_2m_max[e.index], "rain": d.precipitation_sum[e.index]}, enumerate(d.time))
set n to n + 1
raise(`could not fetch {x.c}`)
let rows be flatten(parallel_map(fetch, CITIES, 5))
write_file("./cases/03-supply-chain/results/weather_synsema.parquet", parquet_write(rows))
let summary be apply((r) => merge(r, {"tmax_mean": helpers.round1(r.tmax_mean)}), summarize(rows, "city", {"tmax_mean": mean_of("tmax"), "rain_total": sum_of("rain")}))
write_file("./cases/03-supply-chain/results/summary_synsema.csv", csv_encode(summary))
each r in summary
print(`{r.city}: {r.tmax_mean} °C, {round_to(r.rain_total, 1)} mm`)
print(`what the imported module can do: {helpers.capabilities()}`)Por qué Synsema
- No hay gestor de paquetes ni código de terceros. Los módulos son archivos locales.
- Un módulo recibe solo las capabilities que declaró el archivo de entrada. Si el ETL declara un host, ningún módulo puede hablar con otro.
- synsema code caps es una revisión estática: muestra qué necesita cada task y qué falta declarar, antes de ejecutar.
Lo que no resuelve
- Los avisos de pip-audit se van actualizando, que es justamente el mantenimiento continuo que pide Python.
- Un módulo local de Synsema hereda las capabilities del programa. Si el programa declara de más, el módulo también las tiene.
- La diferencia de tiempo viene sobre todo de bajar en serie contra en paralelo, no del lenguaje.
Reproducirlo
git clone https://github.com/synsema/why-synsema && cd why-synsema # cases/03-supply-chain synsema run cases/03-supply-chain/synsema/etl.syn synsema code caps cases/03-supply-chain/synsema/etl.syn uv run --with-requirements cases/03-supply-chain/python/requirements.txt python cases/03-supply-chain/python/etl.py cases/03-supply-chain/results uvx pip-audit -r cases/03-supply-chain/python/requirements.txt
github.com/synsema/why-synsema/tree/main/cases/03-supply-chain