v4 Schritt 4: News-Board
- routers/news.py: aggregiert kuratierte RSS/Atom-Feeds (HuggingFace, llama.cpp, Ollama, r/LocalLLaMA, Simon Willison) via httpx + stdlib xml.etree, In-memory-Cache 30 min, Hardware-relevante Beitraege (rocm/gfx1151/...) markiert. Degradiert pro Feed sauber. - news.js: Beitragsliste (Quelle, Titel-Link, relatives Datum) + Aktualisieren. - Nav 'News' + View + main.js. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
"""
|
||||
News-Router: aggregiert kuratierte RSS/Atom-Feeds rund um lokale LLMs.
|
||||
Serverseitig geholt (httpx) + mit stdlib geparst (kein neuer Dep), In-memory-Cache (30 min).
|
||||
Degradiert sauber: einzelne Feeds dürfen fehlschlagen, der Rest kommt trotzdem.
|
||||
"""
|
||||
|
||||
import time
|
||||
import xml.etree.ElementTree as ET
|
||||
from datetime import datetime
|
||||
from email.utils import parsedate_to_datetime
|
||||
|
||||
import httpx
|
||||
from fastapi import APIRouter, Depends
|
||||
|
||||
from auth import auth
|
||||
|
||||
router = APIRouter(prefix="/api", dependencies=[Depends(auth)])
|
||||
|
||||
FEEDS = [
|
||||
("HuggingFace", "https://huggingface.co/blog/feed.xml"),
|
||||
("llama.cpp", "https://github.com/ggml-org/llama.cpp/releases.atom"),
|
||||
("Ollama", "https://github.com/ollama/ollama/releases.atom"),
|
||||
("r/LocalLLaMA", "https://www.reddit.com/r/LocalLLaMA/.rss"),
|
||||
("Simon Willison", "https://simonwillison.net/atom/everything/"),
|
||||
]
|
||||
_UA = {"User-Agent": "Mozilla/5.0 (MissionControl NewsBot)"}
|
||||
_TTL = 1800 # 30 min
|
||||
_CACHE = {"ts": 0.0, "items": []}
|
||||
_RELEVANT = ("rocm", "gfx1151", "strix", "amd", "vulkan", "llama.cpp", "gguf", "quant")
|
||||
|
||||
|
||||
def _tag(t):
|
||||
return t.split("}")[-1]
|
||||
|
||||
|
||||
def _epoch(s):
|
||||
if not s:
|
||||
return 0.0
|
||||
try:
|
||||
return parsedate_to_datetime(s).timestamp()
|
||||
except Exception: # noqa: BLE001
|
||||
pass
|
||||
try:
|
||||
return datetime.fromisoformat(s.replace("Z", "+00:00")).timestamp()
|
||||
except Exception: # noqa: BLE001
|
||||
return 0.0
|
||||
|
||||
|
||||
def _parse(source, xml_text):
|
||||
out = []
|
||||
try:
|
||||
root = ET.fromstring(xml_text)
|
||||
except Exception: # noqa: BLE001
|
||||
return out
|
||||
for el in root.iter():
|
||||
if _tag(el.tag) not in ("item", "entry"):
|
||||
continue
|
||||
title = link = date = ""
|
||||
for ch in el:
|
||||
t = _tag(ch.tag)
|
||||
if t == "title":
|
||||
title = (ch.text or "").strip()
|
||||
elif t == "link":
|
||||
link = ch.get("href") or (ch.text or "").strip() or link
|
||||
elif t in ("pubDate", "published", "updated") and not date:
|
||||
date = (ch.text or "").strip()
|
||||
if title:
|
||||
low = title.lower()
|
||||
out.append({
|
||||
"source": source, "title": title, "link": link,
|
||||
"ts": _epoch(date),
|
||||
"relevant": any(k in low for k in _RELEVANT),
|
||||
})
|
||||
return out[:8]
|
||||
|
||||
|
||||
def get_news():
|
||||
now = time.time()
|
||||
if now - _CACHE["ts"] < _TTL and _CACHE["items"]:
|
||||
return _CACHE["items"]
|
||||
items = []
|
||||
with httpx.Client(timeout=8.0, headers=_UA, follow_redirects=True) as c:
|
||||
for source, url in FEEDS:
|
||||
try:
|
||||
r = c.get(url)
|
||||
r.raise_for_status()
|
||||
items += _parse(source, r.text)
|
||||
except Exception: # noqa: BLE001
|
||||
continue
|
||||
items.sort(key=lambda x: x["ts"], reverse=True)
|
||||
items = items[:24]
|
||||
_CACHE.update(ts=now, items=items)
|
||||
return items
|
||||
|
||||
|
||||
@router.get("/news")
|
||||
def news():
|
||||
return {"items": get_news()}
|
||||
Reference in New Issue
Block a user