#!/usr/bin/env catnip
# Dashboard servi depuis la mémoire, zéro fichier temporaire
# Fetch (httpx) -> transform (Catnip) -> serve (http.server)
#
# DEPS: httpx
# INTERNET: oui (API GitHub)
builtins = import('builtins')
http_server = import('http.server')
webbrowser = import('webbrowser')
math = import('math')
httpx = import('httpx')
# ---------------------------------------------------------------------------
# Handler HTTP -- bridge Python (les lambdas Catnip ne passent pas le
# protocole descripteur, on exec une classe minimale)
# ---------------------------------------------------------------------------
ns = dict()
builtins.exec("""
import http.server
class _H(http.server.BaseHTTPRequestHandler):
content = b""
def do_GET(self):
self.send_response(200)
self.send_header("Content-Type", "text/html; charset=utf-8")
self.end_headers()
self.wfile.write(self.content)
def log_message(self, *a):
pass
""", ns)
Handler = ns['_H']
# ---------------------------------------------------------------------------
# Données : fetch repos GitHub via l'API publique
# ---------------------------------------------------------------------------
struct Repo {
name: str; lang: str; stars: int; description: str
}
resp = httpx.get(
"https://api.github.com/search/repositories",
params=dict(q="language:rust stars:>5000", sort="stars", per_page=12),
headers=dict(Accept="application/vnd.github+json"),
timeout=10.0,
)
data = resp.json()
# for loop : le broadcasting aplatirait les dicts (itération sur les clés)
repos = list()
for item in data['items'] {
repos = repos + list(Repo(
item['full_name'],
item['language'] ?? "n/a",
item['stargazers_count'],
item['description'] ?? "",
))
}
# ---------------------------------------------------------------------------
# Spirale de Fibonacci en SVG (broadcasting sur scalaires)
# ---------------------------------------------------------------------------
golden = math.pi * (3 - math.sqrt(5))
n_pts = 200
circles = range(n_pts).[(i: int): str => {
r = math.sqrt(i) * 8
theta = i * golden
cx = 250 + r * math.cos(theta)
cy = 250 + r * math.sin(theta)
hue = int((i * 360) / n_pts)
rad = 3 + i * 0.12
'<circle cx="' + str(round(cx, 1)) + '" cy="' + str(round(cy, 1)) + '" r="' + str(round(rad, 1)) + '" fill="hsl(' +
str(hue) +
',75%,55%)" opacity=".85"/>'
}]
svg_body = "\n ".join(circles)
# ---------------------------------------------------------------------------
# HTML -- template .format() (les f-strings ne supportent pas {{ pour les
# accolades CSS, et .format() gère ça nativement)
# ---------------------------------------------------------------------------
row_tpl = '<tr><td><a href="https://github.com/{name}">{name}</a></td><td>{lang}</td><td class="stars">{stars}<span class="bar" style="width:{bar_w}px"></span></td></tr>'
rows = list()
for r in repos {
rows = rows + list(row_tpl.format(
name=r.name, lang=r.lang, stars=r.stars, bar_w=int(r.stars / 500),
))
}
table_body = "\n ".join(rows)
CSS = ' * { margin:0; padding:0; box-sizing:border-box }
body { font-family:system-ui,-apple-system,sans-serif; background:#0d1117; color:#c9d1d9;
max-width:960px; margin:0 auto; padding:32px 16px }
h1 { color:#58a6ff; font-size:1.6rem; margin-bottom:4px }
.sub { color:#8b949e; font-size:.85rem; margin-bottom:24px }
.tag { display:inline-block; padding:2px 10px; border-radius:12px;
background:#238636; color:#fff; font-size:11px; vertical-align:middle }
.grid { display:grid; grid-template-columns:1fr 1fr; gap:24px; align-items:start }
.card { background:#161b22; border:1px solid #30363d; border-radius:8px; padding:16px; overflow:auto }
.card h2 { color:#58a6ff; font-size:1rem; margin-bottom:12px }
table { width:100%; border-collapse:collapse; font-size:.85rem }
th,td { padding:6px 10px; text-align:left; border-bottom:1px solid #21262d }
th { color:#8b949e; font-weight:500 }
a { color:#58a6ff; text-decoration:none }
a:hover { text-decoration:underline }
.stars { white-space:nowrap }
.bar { display:inline-block; height:10px; background:#238636; border-radius:4px;
vertical-align:middle; margin-left:6px }
svg { display:block; margin:0 auto }
@media(max-width:700px) { .grid { grid-template-columns:1fr } }
'
PAGE = '<!DOCTYPE html>
<html lang="en"><head><meta charset="utf-8">
<title>Catnip Dashboard</title>
<style>{css}</style></head>
<body>
<h1>Catnip <span class="tag">live</span></h1>
<p class="sub">Fetch → Transform → Serve. Tout en Catnip.</p>
<div class="grid">
<div class="card">
<h2>Top Rust repos (GitHub API)</h2>
<table><tr><th>Repo</th><th>Lang</th><th>Stars</th></tr>
{table}
</table>
</div>
<div class="card" style="text-align:center">
<h2>Spirale de Fibonacci</h2>
<p style="color:#8b949e;font-size:.8rem;margin-bottom:8px">{n} points, angle d'or</p>
<svg width="500" height="500" xmlns="http://www.w3.org/2000/svg"
style="background:#0d1117;border-radius:12px">
{svg}
</svg>
</div>
</div>
<p style="margin-top:20px;color:#484f58;font-size:.75rem;text-align:center">
Servi depuis la mémoire par http.server.
</p>
</body></html>'
html = PAGE.format(css=CSS, table=table_body, svg=svg_body, n=n_pts)
# ---------------------------------------------------------------------------
# Serveur one-shot : ouvre le navigateur, sert une requête, puis quitte
# (même pattern que les dashboards hvplot et folium)
# ---------------------------------------------------------------------------
Handler.content = html.encode('utf-8')
server = http_server.HTTPServer(tuple("127.0.0.1", 0), Handler)
port = server.server_address[1]
print(f"⇒ Dashboard : http://127.0.0.1:{port}")
webbrowser.open(f"http://127.0.0.1:{port}")
server.handle_request()
server.server_close()