social media crossposting tool. 3rd time's the charm
mastodon misskey crossposting bluesky
at next 77 lines 2.0 kB view raw
1import logging 2import os 3import sys 4from collections.abc import Callable 5from typing import Any, cast 6 7import env 8 9 10shutdown_hook: list[Callable[[], None]] = [] 11 12logging.basicConfig(stream=sys.stderr, level=logging.DEBUG if env.DEV else logging.INFO) 13logging.getLogger("httpx").setLevel(logging.WARNING) 14logging.getLogger("httpcore").setLevel(logging.WARNING) 15LOGGER = logging.getLogger("XPost") 16 17 18class Result[V, E]: 19 _value: V 20 _error: E | None 21 22 def __init__(self, value: V, err: E) -> None: 23 self._value = value 24 self._error = err 25 26 def error(self) -> E: 27 if self._error is None: 28 raise ValueError("self._error not set!") 29 return self._error 30 31 def value(self) -> V: 32 return self._value 33 34 def is_ok(self) -> bool: 35 return self._error is None 36 37 @classmethod 38 def err(cls, err: E) -> "Result[V, E]": 39 return cast("Result[V, E]", Result(None, err)) 40 41 @classmethod 42 def ok(cls, val: V) -> "Result[V, E]": 43 return cast("Result[V, E]", Result(val, None)) 44 45 46def normalize_service_url(url: str) -> str: 47 if not url.startswith("https://") and not url.startswith("http://"): 48 raise ValueError(f"Invalid service url {url}! Must start with http/https!") 49 50 return url[:-1] if url.endswith("/") else url 51 52 53def _read_env(data: Any) -> None: 54 match data: 55 case dict(): 56 read_env(data) 57 case list(): 58 for v in data: 59 _read_env(v) 60 case _: 61 pass 62 63 64def read_env(data: dict[str, Any]) -> None: 65 keys = list(data.keys()) 66 for key in keys: 67 val = data[key] 68 match val: 69 case str(): 70 if val.startswith("env:"): 71 envval = os.environ.get(val[4:]) 72 if envval is None: 73 del data[key] 74 else: 75 data[key] = envval 76 case _: 77 _read_env(val)