๐Ÿ“… Calendar file generator for triathlonlive.tv upcoming events triathlon-live-calendar.fly.dev

Refactors the logger

+71 -15
+24 -4
triathlon_live_calendar/__main__.py
··· 1 1 import asyncio 2 + from logging import CRITICAL, DEBUG, ERROR, INFO, WARNING 2 3 from os import getenv 3 4 from pathlib import Path 4 5 from typing import Optional 5 6 6 - from typer import Typer, Option 7 + from typer import Typer, Option, echo 7 8 from uvicorn import run # type: ignore 8 9 9 10 from triathlon_live_calendar.calendar import calendar ··· 12 13 13 14 DEFAULT_HOST = "0.0.0.0" 14 15 DEFAULT_PORT = 5000 16 + DEFAULT_LOG_LEVEL = "info" 15 17 PORT_HELP = "[default: 5000 or PORT from environment variable]" 18 + LEVELS = { 19 + "critical": CRITICAL, 20 + "error": ERROR, 21 + "warning": WARNING, 22 + "info": INFO, 23 + "debug": DEBUG, 24 + } 16 25 17 26 app = Typer() 18 27 ··· 27 36 return DEFAULT_PORT 28 37 29 38 39 + def get_log_level(value: str) -> str: 40 + if value not in LEVELS.keys(): 41 + echo(f"Invalid log level: {value}. Options are: {', '.join(LEVELS.keys())}") 42 + return DEFAULT_LOG_LEVEL 43 + 44 + return value 45 + 46 + 30 47 @app.command() 31 48 def web( 32 49 host: str = DEFAULT_HOST, 33 50 port: Optional[int] = Option(None, callback=get_port, help=PORT_HELP), 34 - log_level: Optional[str] = None, 51 + log_level: Optional[str] = Option(None, callback=get_log_level), 35 52 reload: Optional[bool] = None, 36 53 ): 37 54 """Starts the web server.""" ··· 46 63 47 64 48 65 @app.command() 49 - def generate(path: Path, verbose: bool = False): 66 + def generate( 67 + path: Path, 68 + log_level: str = Option(DEFAULT_LOG_LEVEL, callback=get_log_level), 69 + ): 50 70 """Generates the calendar .ics file""" 51 - logger = Logger(use_typer_echo=True) if verbose else None 71 + logger = Logger(LEVELS[log_level]) 52 72 contents = asyncio.run(calendar(logger)) 53 73 path.write_text(str(contents)) 54 74
+46 -10
triathlon_live_calendar/logger.py
··· 1 - import logging 2 1 from dataclasses import dataclass 2 + from functools import cached_property, wraps 3 + from logging import INFO, Formatter, StreamHandler, getLogger 4 + from sys import stdout 3 5 from typing import Iterable, Union 4 6 5 - from typer import echo 7 + 8 + def multiline(func): 9 + @wraps(func) 10 + def normalized(self, text: Union[str, Iterable[str]]): 11 + if not isinstance(text, str): 12 + text = "\n".join(text) 13 + 14 + return func(self, text) 15 + 16 + return normalized 6 17 7 18 8 19 @dataclass 9 20 class Logger: 10 - use_typer_echo: bool = False 21 + log_level: int = INFO 11 22 12 - def info(self, text: Union[str, Iterable[str]]) -> None: 13 - if not isinstance(text, str): 14 - text = "\n".join(text) 23 + def __post_init__(self): 24 + self.logger = getLogger() 25 + self.logger.setLevel(self.log_level) 26 + self.logger.addHandler(self.handler) 15 27 16 - if self.use_typer_echo: 17 - echo(text) 18 - else: 19 - logging.info(text) 28 + @cached_property 29 + def handler(self): 30 + handler = StreamHandler(stdout) 31 + handler.setLevel(self.log_level) 32 + handler.setFormatter( 33 + Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s") 34 + ) 35 + return handler 36 + 37 + @multiline 38 + def critical(self, text): 39 + self.logger.critical(text) 40 + 41 + @multiline 42 + def debug(self, text): 43 + self.logger.debug(text) 44 + 45 + @multiline 46 + def error(self, text): 47 + self.logger.error(text) 48 + 49 + @multiline 50 + def info(self, text): 51 + self.logger.info(text) 52 + 53 + @multiline 54 + def warning(self, text): 55 + self.logger.warning(text)
+1 -1
triathlon_live_calendar/scraper.py
··· 37 37 if logger: 38 38 tz, *_ = tzname 39 39 local = begin.to(tz).format(DATETIME_FORMAT[:-2]) 40 - logger.info((f"Parsed {url}", f" Title: {title}", f" Begin: {local}")) 40 + logger.debug((f"Parsed {url}", f" Title: {title}", f" Begin: {local}")) 41 41 42 42 return Event( 43 43 name=title,