feat: daily paper rss generator with dockerfile + entrypoint
This commit is contained in:
38
.gitignore
vendored
38
.gitignore
vendored
@@ -1,37 +1,9 @@
|
|||||||
# Byte-compiled / optimized files
|
# Python
|
||||||
__pycache__/
|
__pycache__/
|
||||||
*.py[cod]
|
*.py[cod]
|
||||||
*$py.class
|
|
||||||
|
|
||||||
# Distribution / packaging
|
|
||||||
*.egg-info/
|
|
||||||
dist/
|
|
||||||
build/
|
|
||||||
|
|
||||||
# Virtual environments
|
|
||||||
venv/
|
|
||||||
env/
|
|
||||||
.env/
|
|
||||||
.venv/
|
.venv/
|
||||||
|
venv/
|
||||||
|
|
||||||
# IDE specific files
|
# Local test output
|
||||||
.idea/
|
feeds/
|
||||||
.vscode/
|
test_*/
|
||||||
*.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
|
|
||||||
|
|||||||
47
Dockerfile
Normal file
47
Dockerfile
Normal file
@@ -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"]
|
||||||
92
run.py
Normal file
92
run.py
Normal file
@@ -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()
|
||||||
16
run.sh
Normal file
16
run.sh
Normal file
@@ -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" "$@"
|
||||||
82
server.py
Normal file
82
server.py
Normal file
@@ -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()
|
||||||
Reference in New Issue
Block a user