From 23fed6dd6478073e5f789bc437c998c0eb862437 Mon Sep 17 00:00:00 2001 From: Kunthawat Greethong Date: Sat, 27 Jun 2026 14:36:43 +0700 Subject: [PATCH] feat: daily paper rss generator with dockerfile + entrypoint --- .gitignore | 38 +++------------------- Dockerfile | 47 ++++++++++++++++++++++++++++ run.py | 92 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ run.sh | 16 ++++++++++ server.py | 82 ++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 242 insertions(+), 33 deletions(-) create mode 100644 Dockerfile create mode 100644 run.py create mode 100644 run.sh create mode 100644 server.py diff --git a/.gitignore b/.gitignore index 051c9bc..273c8df 100644 --- a/.gitignore +++ b/.gitignore @@ -1,37 +1,9 @@ -# Byte-compiled / optimized files +# Python __pycache__/ *.py[cod] -*$py.class - -# Distribution / packaging -*.egg-info/ -dist/ -build/ - -# Virtual environments -venv/ -env/ -.env/ .venv/ +venv/ -# IDE specific files -.idea/ -.vscode/ -*.swp -*.swo - -# OS specific files -.DS_Store -Thumbs.db - -# Local testing/development -*.bak -*.tmp -.coverage -htmlcov/ - -# Exclude the HTML file downloaded from HF (if you keep test files locally) -*Daily Papers - Hugging Face.html - -# Uncomment the line below if you don't want to commit generated feed files -# *.xml \ No newline at end of file +# Local test output +feeds/ +test_*/ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..f5bff4f --- /dev/null +++ b/Dockerfile @@ -0,0 +1,47 @@ +# ───────────────────────────────────────────── +# HuggingFace Daily Paper RSS — Dockerfile +# ───────────────────────────────────────────── +# Two modes: +# 1. Run-once (default, for cron): +# docker run --rm -v ./feeds:/data hf-paper-rss +# 2. HTTP server (serve feeds): +# docker run --rm -p 8080:8080 -v ./feeds:/data hf-paper-rss serve +# ───────────────────────────────────────────── + +FROM python:3.11-slim AS base + +WORKDIR /app + +# Install system deps +RUN apt-get update && apt-get install -y --no-install-recommends \ + ca-certificates \ + && rm -rf /var/lib/apt/lists/* + +# Install Python deps +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +# Copy app code +COPY parser.py rss_generator.py run.py run.sh ./ + +# Create output directory +RUN mkdir -p /data && chmod 777 /data + +# Health check +HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ + CMD python -c "import sys; sys.exit(0 if __import__('os').path.exists('/data/feed.xml') else 1)" \ + || true # Allow healthcheck to be optional + +ENTRYPOINT ["python", "run.py"] +CMD ["--output-dir", "/data"] + + +# ─── Serve mode ─── +FROM base AS serve + +COPY server.py . + +EXPOSE 8080 + +ENTRYPOINT ["python", "server.py"] +CMD ["--dir", "/data", "--port", "8080"] diff --git a/run.py b/run.py new file mode 100644 index 0000000..e1c2b3b --- /dev/null +++ b/run.py @@ -0,0 +1,92 @@ +#!/usr/bin/env python3 +""" +run.py — entrypoint for HF Daily Paper RSS generation. + +Generates: + - Daily feed (always) + - Weekly feed (every Monday) + - Monthly feed (1st of month) + +Usage: + python run.py # output to ./feeds/ + python run.py --output-dir /data/feeds + python run.py --date 2026-06-27 # simulate date +""" +import argparse +import datetime +import os +import subprocess +import sys + + +def main(): + parser = argparse.ArgumentParser(description="HF Daily Paper RSS Generator") + parser.add_argument("--output-dir", default=os.path.join(os.getcwd(), "feeds"), + help="Directory to save RSS feeds") + parser.add_argument("--date", default=None, + help="Simulate a specific date (YYYY-MM-DD)") + args = parser.parse_args() + + output_dir = os.path.abspath(args.output_dir) + os.makedirs(output_dir, exist_ok=True) + + # Resolve date + if args.date: + today = datetime.date.fromisoformat(args.date) + else: + today = datetime.date.today() + + year = today.year + month = f"{today.month:02d}" + week_num = today.isocalendar()[1] + is_monday = today.isoweekday() == 1 + is_first = today.day == 1 + + script_dir = os.path.dirname(os.path.abspath(__file__)) + parser_py = os.path.join(script_dir, "parser.py") + + print(f"[run.py] Date: {today.isoformat()} " + f"(Year={year} Month={month} Week={week_num} " + f"Mon={is_monday} First={is_first})") + print(f"[run.py] Output: {output_dir}") + + def run_parser(source, outfile): + full_path = os.path.join(output_dir, outfile) + print(f"\n── Generating {outfile} ──") + result = subprocess.run( + [sys.executable, parser_py, "--source", source, "--output", full_path], + capture_output=False, text=True, cwd=script_dir, + ) + return result.returncode + + # 1. Daily (always) + run_parser("https://huggingface.co/papers", "feed.xml") + + # 2. Weekly (Mondays) + if is_monday: + run_parser( + f"https://huggingface.co/papers/week/{year}-W{week_num}", + "feed_weekly.xml", + ) + else: + print("[run.py] Skipping weekly (not Monday)") + + # 3. Monthly (1st) + if is_first: + run_parser( + f"https://huggingface.co/papers/month/{year}-{month}", + "feed_monthly.xml", + ) + else: + print("[run.py] Skipping monthly (not 1st)") + + print(f"\n[run.py] ✅ Done — feeds in {output_dir}") + for f in sorted(os.listdir(output_dir)): + if f.endswith(".xml"): + fpath = os.path.join(output_dir, f) + size = os.path.getsize(fpath) + print(f" {f} ({size:,} bytes)") + + +if __name__ == "__main__": + main() diff --git a/run.sh b/run.sh new file mode 100644 index 0000000..aca8a10 --- /dev/null +++ b/run.sh @@ -0,0 +1,16 @@ +#!/bin/bash +set -euo pipefail +# ───────────────────────────────────────────── +# HuggingFace Daily Paper RSS — Entrypoint Script +# ───────────────────────────────────────────── +# Convenience wrapper — delegates to run.py +# +# Usage: +# ./run.sh # output to ./feeds/ +# ./run.sh --output-dir /data/feeds +# ./run.sh --date 2026-06-27 # simulate date +# ───────────────────────────────────────────── + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PYTHON=$(command -v python3 || command -v python) +exec "$PYTHON" "$SCRIPT_DIR/run.py" "$@" diff --git a/server.py b/server.py new file mode 100644 index 0000000..412c61b --- /dev/null +++ b/server.py @@ -0,0 +1,82 @@ +#!/usr/bin/env python3 +""" +Minimal HTTP server to serve generated RSS feed files. + +Usage: + python server.py # port 8080, dir ./feeds + python server.py --dir /data --port 8080 +""" +import argparse +import http.server +import os +import sys +import datetime + + +class RSSFeedHandler(http.server.SimpleHTTPRequestHandler): + """Custom handler with CORS headers and auto-refresh.""" + + def __init__(self, *args, directory=None, **kwargs): + super().__init__(*args, directory=directory, **kwargs) + + def end_headers(self): + # CORS — allow RSS readers to fetch from any origin + self.send_header("Access-Control-Allow-Origin", "*") + self.send_header("Access-Control-Allow-Methods", "GET, OPTIONS") + self.send_header("Cache-Control", "no-cache, no-store, must-revalidate") + self.send_header("Pragma", "no-cache") + self.send_header("Expires", "0") + super().end_headers() + + def do_OPTIONS(self): + self.send_response(204) + self.end_headers() + + def log_message(self, format, *args): + timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") + sys.stderr.write(f"[{timestamp}] {args[0]} {args[1]} {args[2]}\n") + + +def main(): + parser = argparse.ArgumentParser(description="RSS Feed HTTP Server") + parser.add_argument("--dir", default=os.path.join(os.getcwd(), "feeds"), + help="Directory containing RSS feeds") + parser.add_argument("--port", type=int, default=8080, + help="Port to serve on") + parser.add_argument("--host", default="0.0.0.0", + help="Host to bind to") + args = parser.parse_args() + + feed_dir = os.path.abspath(args.dir) + if not os.path.exists(feed_dir): + print(f"[server.py] Creating feed directory: {feed_dir}") + os.makedirs(feed_dir, exist_ok=True) + + # Ensure at least one feed exists + feeds = sorted(f for f in os.listdir(feed_dir) if f.endswith(".xml")) + if feeds: + print(f"[server.py] Found {len(feeds)} feed(s): {', '.join(feeds)}") + else: + print(f"[server.py] No feeds yet in {feed_dir}. Run 'run.py' first or wait for cron.") + + class Handler(RSSFeedHandler): + def __init__(self, *args, **kwargs): + super().__init__(*args, directory=feed_dir, **kwargs) + + server = http.server.HTTPServer((args.host, args.port), Handler) + print(f"[server.py] Serving RSS feeds from {feed_dir}") + print(f"[server.py] Listening on http://{args.host}:{args.port}") + print(f"[server.py] Feed URLs:") + print(f" http://localhost:{args.port}/feed.xml (daily)") + print(f" http://localhost:{args.port}/feed_weekly.xml (weekly)") + print(f" http://localhost:{args.port}/feed_monthly.xml (monthly)") + + try: + server.serve_forever() + except KeyboardInterrupt: + print("\n[server.py] Shutting down...") + server.server_close() + + +if __name__ == "__main__": + main()