from __future__ import annotations import argparse import asyncio import logging import os import signal import sys from contextlib import suppress from hypercorn.asyncio import serve as hypercorn_serve from hypercorn.config import Config as HypercornConfig import repub.crawl as crawl_module from repub.web import SHUTDOWN_EVENT_KEY, create_app FeedNameFilter = crawl_module.FeedNameFilter check_runtime = crawl_module.check_runtime __all__ = ["FeedNameFilter", "check_runtime", "entrypoint", "parse_args"] logger = logging.getLogger(__name__) logger.setLevel(logging.DEBUG) logger.propagate = False if not logger.handlers: handler = logging.StreamHandler() handler.setLevel(logging.DEBUG) handler.setFormatter( logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s") ) logger.addHandler(handler) def parse_args(argv: list[str] | None = None) -> tuple[str, argparse.Namespace]: raw_args = list(argv) if argv is not None else sys.argv[1:] parser = argparse.ArgumentParser(description="Mirror RSS and Atom feeds") subparsers = parser.add_subparsers(dest="command") serve_parser = subparsers.add_parser("serve", help="Start the republisher web UI") serve_parser.add_argument( "--host", default=os.environ.get("REPUBLISHER_HOST", "127.0.0.1"), help="Host interface for the web UI", ) serve_parser.add_argument( "--port", default=os.environ.get("REPUBLISHER_PORT", "8080"), help="Port for the web UI", ) serve_parser.add_argument( "--dev-mode", action="store_true", help="Serve published feeds from /feeds for local development", ) crawl_parser = subparsers.add_parser("crawl", help="Run the feed crawler once") crawl_parser.add_argument( "-c", "--config", default="repub.toml", help="Path to runtime config TOML file", ) if not raw_args: raw_args = ["serve", "--dev-mode"] elif raw_args[0] in {"-c", "--config"}: raw_args = ["crawl", *raw_args] elif raw_args[0] not in {"serve", "crawl"}: raw_args = ["serve", "--dev-mode", *raw_args] args = parser.parse_args(raw_args) command = args.command or "serve" return command, args def _install_signal_handlers(stop_event: asyncio.Event) -> None: loop = asyncio.get_running_loop() def request_stop(*_: object) -> None: if not stop_event.is_set(): stop_event.set() for signum in (signal.SIGINT, signal.SIGTERM): try: loop.add_signal_handler(signum, request_stop) except NotImplementedError: signal.signal(signum, request_stop) async def _serve_app(*, host: str, port: int, dev_mode: bool) -> None: stop_event = asyncio.Event() _install_signal_handlers(stop_event) app = create_app(dev_mode=dev_mode) app.extensions[SHUTDOWN_EVENT_KEY] = stop_event config = HypercornConfig() config.bind = [f"{host}:{port}"] config.use_reloader = False config.accesslog = "-" config.errorlog = "-" async def shutdown_trigger() -> None: await stop_event.wait() try: await hypercorn_serve(app, config, shutdown_trigger=shutdown_trigger) finally: stop_event.set() def entrypoint(argv: list[str] | None = None) -> int: command, args = parse_args(argv) if command == "crawl": crawl_module.check_runtime = check_runtime return crawl_module.crawl_from_config(args.config) try: port = int(args.port) except ValueError: logger.error("Invalid REPUBLISHER_PORT/--port value: %s", args.port) return 2 with suppress(KeyboardInterrupt): asyncio.run(_serve_app(host=args.host, port=port, dev_mode=bool(args.dev_mode))) return 0 if __name__ == "__main__": sys.exit(entrypoint())