diff --git a/.gitignore b/.gitignore index be9d161..fcaa10b 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,6 @@ __pycache__/ .flake8 .venv/ test/ +build/ +dist/ +*.egg-info/ \ No newline at end of file diff --git a/.hb.yaml b/.hb.yaml index b755b10..046af05 100644 --- a/.hb.yaml +++ b/.hb.yaml @@ -11,11 +11,17 @@ watchhosts: # "haschloss" : # "cotgate": # "wentworth": + "y": + notify: +4915123456789 + src: "signal" "winter": notify: +14168226179 src: "signal" dyndnshosts: {"haschloss", "wayback", "wertvoll", "weekend", "cotgate", "rvgate", "draper", "eris"} drophosts: {"unknown", "wookie15", "wort"} nsupdate_bin: "/usr/local/bin/nsupdate" +pushover_token: "ac7NLX2rPjXFareeDgLpXNoDf4iFmf" +pushover_user: "uDhH33UjQQDYtNzJb1ThRiWb9ingGK" pushsrv: "pushover" + dyndomains: {"wrede.org"} diff --git a/NOTES.txt b/NOTES.txt deleted file mode 100644 index 40951c9..0000000 --- a/NOTES.txt +++ /dev/null @@ -1,22 +0,0 @@ - - -to install on home assistant OS: - -- copy hbc and run_hbc to /config/bin on ha - -- add these two lines to configuration.yaml, set XXXX to name of the HA instance -shell_command: - run_hbc: /config/bin/run_hbc -n XXXXX -d hbd.wrede.ca - -- add these lines to automation.yaml -- id: '1641839711280' - alias: run hbc on boot - description: '' - trigger: - - platform: homeassistant - event: start - condition: [] - action: - - service: shell_command.run_hbc - mode: single - diff --git a/build/lib/hbd/__init__.py b/build/lib/hbd/__init__.py deleted file mode 100644 index 5896eaa..0000000 --- a/build/lib/hbd/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -"""hbd package - scaffolding for heartbeat daemon - -This package contains the refactored modules for the original monolithic -`hbd` script. The initial implementation contains small scaffolds so you can -start moving functionality into the package. -""" - -__all__ = ["main", "__version__"] -__version__ = "0.1" - -from .cli import main diff --git a/build/lib/hbd/cli.py b/build/lib/hbd/cli.py deleted file mode 100644 index 82dbfd5..0000000 --- a/build/lib/hbd/cli.py +++ /dev/null @@ -1,45 +0,0 @@ -"""Command line interface for hbd package.""" -import argparse - -from .config import load_config -from .server import run as run_server - -PUSHSRVS = ["all", "pushover", "mattermost"] - - -def build_parser(): - parser = argparse.ArgumentParser( - prog="hbd", - description="HeartBeatDaemon - Wait for heartbeat messages and act on them (or their absence)", - formatter_class=argparse.RawDescriptionHelpFormatter, - ) - parser.add_argument("-c", "--config", dest="configfile", help="Config file path (YAML)") - parser.add_argument("-f", "--foreground", action="store_true", help="Run in foreground") - parser.add_argument("-v", "--verbose", action="store_true", help="Verbose output") - parser.add_argument("-p", "--pushsrv", dest="pushsrv", choices=PUSHSRVS, help="Push service to use") - parser.add_argument("-x", "--debug", action="count", default=0, help="Increase debug level") - return parser - - -def main(argv=None): - parser = build_parser() - args = parser.parse_args(argv) - - config = load_config(args.configfile) - - # Apply CLI overrides - if args.foreground: - config["foreground"] = True - if args.verbose: - config["verbose"] = True - if args.pushsrv: - config["pushsrv"] = args.pushsrv - if args.debug: - config.setdefault("debug", 0) - config["debug"] += args.debug - - run_server(config) - - -if __name__ == "__main__": - main() diff --git a/build/lib/hbd/config.py b/build/lib/hbd/config.py deleted file mode 100644 index c7a53b5..0000000 --- a/build/lib/hbd/config.py +++ /dev/null @@ -1,54 +0,0 @@ -"""Configuration loader and defaults for hbd.""" -import os - -try: - import yaml -except Exception: - yaml = None - -DEFAULTS = { - "hb_port": 50003, - "hbd_port": 50004, - "hbd_host": "", - "pickfile": "/tmp/hb.pick", - "logfile": "/var/log/heartbeat.log", - "logfmt": "text", - "pushsrv": "pushover", - "interval": 20, - "grace": 2, - "dyndomains": ["wrede.org"], - "watchhosts": [], - "dyndnshosts": [], - "drophosts": [], - "nsupdate_bin": "/usr/bin/nsupdate", - "foreground": False, - "verbose": False, - "debug": 0, -} - - -def load_config(path=None): - """Load configuration from a YAML file and merge with defaults. - - If YAML is not available or the file does not exist, defaults are returned. - """ - cfg = DEFAULTS.copy() - if not path: - # default path (~/.hb.yaml) - path = os.path.join(os.path.expanduser("~"), ".hb.yaml") - - if os.path.exists(path): - if yaml: - with open(path) as fh: - data = yaml.safe_load(fh) or {} - # only keep known keys - for k, v in data.items(): - if k in cfg: - cfg[k] = v - else: - # ignore unknown keys for now - pass - else: - # yaml not installed: do not attempt to parse; user must ensure defaults - pass - return cfg diff --git a/build/lib/hbd/dns.py b/build/lib/hbd/dns.py deleted file mode 100644 index 7088c85..0000000 --- a/build/lib/hbd/dns.py +++ /dev/null @@ -1,91 +0,0 @@ -"""DNS update helper and thread for heartbeat daemon.""" -from __future__ import annotations -import threading -import subprocess -from subprocess import Popen, PIPE, STDOUT -from typing import Optional - - -def create_nsupdate_payload(hostname: str, newip: str, dyndomain: str, dnsttl: str = "5") -> str: - D = {"domain": dyndomain, "fqdn": f"{hostname}.dy.{dyndomain}", "dnsttl": dnsttl, "newip": newip, "ts": __import__("time").strftime("%Y-%m-%d.%H:%M:%S", __import__("time").gmtime())} - if ":" in newip: - nsup = ( - """update delete %(fqdn)s AAAA -update add %(fqdn)s %(dnsttl)s AAAA %(newip)s -update delete %(fqdn)s TXT -update add %(fqdn)s %(dnsttl)s TXT "Created: %(ts)s" -send -answer - -""" % D - ) - else: - nsup = ( - """update delete %(fqdn)s A -update add %(fqdn)s %(dnsttl)s A %(newip)s -update delete %(fqdn)s TXT -update add %(fqdn)s %(dnsttl)s TXT "Created: %(ts)s" -send -answer - -""" % D - ) - return nsup - - -def nsupdate(hostname: str, newip: str, dyndomain: str, nsupdate_bin: str = "/usr/local/bin/nsupdate", rndc_key: str = "/etc/dhcpc/rndc-key") -> Optional[str]: - """Perform DNS update via nsupdate command. - - Returns None on success, else returns combined stdout/stderr as a string. - """ - nsup = create_nsupdate_payload(hostname, newip, dyndomain) - cmd = [nsupdate_bin, "-k", rndc_key, "-v"] - try: - p = Popen(cmd, shell=False, bufsize=0, stdin=PIPE, stdout=PIPE, stderr=STDOUT) - except OSError as e: - return f"nsupdate: execution failed: {e}" - except Exception as e: - return f"nsupdate: some error occured: {e}" - - (output, err) = p.communicate(nsup.encode()) - out = output.decode() if output else "" - if out.find("status: NOERROR") >= 0: - return None - return out - - -def dnsupdatethread(hbdclass, cfg: dict, log: Optional[callable] = None, email: Optional[callable] = None): - """Thread target: process dns update queue from hbdclass.Host.dnsQ. - - hbdclass: module with Host class that exposes dnsQ queue - cfg: configuration mapping with 'dyndomains' and 'nsupdate_bin' - log: callable(host, message) - email: callable(subject, message) - """ - while True: - name, addr = hbdclass.Host.dnsQ.get() - m = f"changed address to {addr}" - for dyndomain in cfg.get("dyndomains", []): - err = nsupdate(name, addr, dyndomain, nsupdate_bin=cfg.get("nsupdate_bin", "/usr/local/bin/nsupdate")) - if err: - m += f", DNS update failed: {err}" - if email: - try: - email("error: nsupdate failed", f"{name}.dy.{dyndomain}: {m}") - except Exception: - pass - else: - m += ", DNS updated." - hbdclass.Host.dnsQ.task_done() - if log: - try: - log(name, m) - except Exception: - pass - - -def start_dns_thread(hbdclass, cfg: dict, log: Optional[callable] = None, email: Optional[callable] = None) -> threading.Thread: - t = threading.Thread(target=dnsupdatethread, args=(hbdclass, cfg, log, email)) - t.daemon = True - t.start() - return t diff --git a/build/lib/hbd/http.py b/build/lib/hbd/http.py deleted file mode 100644 index 5eaf243..0000000 --- a/build/lib/hbd/http.py +++ /dev/null @@ -1,235 +0,0 @@ -"""HTTP server and handler scaffolds (thin wrappers around http.server).""" -from http import server -import json -import time -import urllib.parse - -from urllib3 import request -from fastapi.templating import Jinja2Templates - -class HttpServer(server.ThreadingHTTPServer): - allow_reuse_address = True - - def threaded(self): - pass - - -def make_handler_class( - config, - hbdclass, - msgs_getter, - log=None, - email=None, - pushmsg=None, - msg_to_websockets=None, - tcss=None, - DEBUG=0, - verbose=False, - get_now=None, - VER="", -): - """Return a BaseHTTPRequestHandler subclass bound to runtime objects. - - `msgs_getter` should be a callable that returns a list-like of messages. - """ - templates = Jinja2Templates(directory="templates") - get_now = get_now or (lambda: time.time()) - - class CustomHandler(server.BaseHTTPRequestHandler): - - server_version = f"HeartbeatHTTP/{VER}" - - def version_string(self): - return self.server_version - - def handle(self): - try: - return server.BaseHTTPRequestHandler.handle(self) - except Exception as e: - self.log_error("Request went away: %r", e) - self.close_connection = 1 - return - - def do_HEAD(self): - self.setheaders(200) - - def setheaders(self, code, headerdict={}): - self.send_response(code) - self.send_header( - "Last-Modified", - time.strftime("%a, %d %b %Y %H:%M:%S GMT", time.gmtime(get_now())), - ) - for h in headerdict: - self.send_header(h, headerdict[h]) - self.end_headers() - - def buildhead(self, title="Heartbeat", refresh=None, extras=None): - res = [] - res.append('') - res.append("") - res.append("") - res.append("%s" % (title)) - if refresh: - res.append("\n" % refresh) - if extras: - res.append(extras) - res.append("") - res.append('') - return res - - def buildpage(self): - res = self.buildhead(refresh=60, extras=tcss) - res.append("

Heartbeat status %s

" % VER) - res += hbdclass.ubHost.buildhosttable() - res += hbdclass.ubHost.buildmsgtable(msgs_getter()) - res.append( - "

%s (%s)

" % (time.strftime("%H:%M:%S", time.localtime(get_now())), config.get("tz", "CET-1CDT")) - ) - res.append("") - return res - - def builderror(self, code, cause, lcause): - res = [] - res.append('') - res.append("") - res.append("%s %s" % (code, cause)) - res.append("") - res.append("

%s

" % (cause)) - res.append("

%s

" % lcause) - res.append("
") - res.append( - "
hbd (Unix) Server at %s:%s
" % (config.get("hbd_host"), config.get("hbd_port")) - ) - res.append("") - return code, res - - def do_GET(self): - xsig = 0 - rqAcceptEncoding = self.headers.get("Accept-encoding", {}) - headerdict = {"Content-Type": "text/html; charset = ISO-8859-1"} - qr = urllib.parse.urlparse(self.path) - qa = urllib.parse.parse_qs(qr.query) - - if qr.path == "/": - res = self.buildpage() - - elif qr.path == "/c": # command on host /c?h=melschserver&c=sudo%20ls - uname = qa.get("h", [None])[0] - ucmd = qa.get("c", [None])[0] - if not ucmd or not uname: - code, res = self.builderror(400, "Argument error", "need h= and c= arguments") - elif uname not in hbdclass.Host.hosts: - code, res = self.builderror(400, "Data error", "h=%s not found" % uname) - else: - hbdclass.Host.hosts[uname].cmds.append(("CMD", {"cmd": urllib.parse.unquote(ucmd)})) - res = self.buildhead() - res.append("cmd %s queued for host %s" % (uname, ucmd)) - - elif qr.path == "/d": # drop host /d?h=melschserver - uname = qa.get("h", [None])[0] - if not uname: - code, res = self.builderror(400, "Argument error", "need h= argument") - if uname not in hbdclass.Host.hosts: - code, res = self.builderror(400, "Data error", "h=%s not found" % uname) - else: - if log: - log(uname, "dropped") - del hbdclass.Host.hosts[uname] - res = self.buildhead() - res.append("Done") - - elif qr.path == "/n": # register name - uname = qa.get("h", [None])[0] - if not uname: - code, res = self.builderror(400, "Argument error", "need h= argument") - if uname not in hbdclass.Host.hosts: - code, res = self.builderror(400, "Data error", "h=%s not found" % uname) - else: - ll = hbdclass.Host.hosts[uname].registerDns() - res = self.buildhead() - res.append(ll) - if log: - log(uname, ll) - - elif qr.path == "/u": # update - uname = urllib.parse.unquote(qa.get("h", [None])[0]) - ucode = qa.get("c", [None])[0] - if not ucode or not uname: - code, res = self.builderror(400, "Argument error", "need h= and c= arguments") - elif uname != "All" and uname not in hbdclass.Host.hosts: - code, res = self.builderror(400, "Data error", "h=%s not found" % uname) - else: - res = self.buildhead() - if uname != "All": - names = [uname] - else: - names = [] - for n in hbdclass.Host.hosts: - if hbdclass.Host.hosts[n].cver >= 2: # earliest version that supports update - names.append(n) - for n in names: - err = None - try: - from hbd import proto - # read code from a file name, fallback to sending ucode as data - err = None - # attempt to send update command to host - r = {"csum": None, "code": ucode} - hbdclass.Host.hosts[n].cmds.append(("UPD", r)) - except Exception as e: - err = str(e) - res.append("update started for %s: %s
" % (n, err if err else "OK")) - res.append("Done") - - elif qr.path == "/api/0/hosts": # api access to host table - headerdict = {"Content-Type": "application/json; charset=utf-8"} - lst = [] - for h in hbdclass.Host.hosts: - lst.append(hbdclass.Host.hosts[h].jsons()) - res = ["[" + ",".join(lst) + "]"] - - elif qr.path == "/api/0/messages": # api access to host table - headerdict = {"Content-Type": "application/json; charset=utf-8"} - lst = msgs_getter()[-30:] - res = [json.dumps(lst)] - - elif qr.path == "/r": # restart - res = self.buildhead() - res.append("restart request") - xsig = 1 # signal.SIGHUP will be handled by application - if log: - log(None, "restart request") - elif qr.path == "/live": # show - heartbeat_ws_url = f"wss://{host}:50006/hbd" - res = templates.TemplateResponse( - "heartbeat.html", - { - "title": "Heartbeat", - "header": "Heartbeat", - "request": request, - "heartbeat_ws_url": heartbeat_ws_url, - "extra_scripts": extra_scripts, - }, - ) - else: - code, res = self.builderror(404, "Not Found", "requested URL was not found on this server.") - - if "deflate" in rqAcceptEncoding: - headerdict["Content-Encoding"] = "deflate" - towrite = __import__("zlib").compress("\n".join(res).encode(), 6) - else: - towrite = "\n".join(res) - headerdict["Content-Length"] = len(towrite) - headerdict["Cache-Control"] = "private, must-revalidate, max-age=0" - headerdict["Expires"] = "Thu, 01 Jan 1970 00:00:00 GMT" - self.setheaders(200 if 'res' in locals() else code, headerdict) - self.wfile.write(towrite if isinstance(towrite, bytes) else towrite.encode()) - - if xsig: - # inform application via setting a flag on the server instance - try: - self.server.xsig = xsig - except Exception: - pass - - return CustomHandler diff --git a/build/lib/hbd/notify.py b/build/lib/hbd/notify.py deleted file mode 100644 index 8c99e76..0000000 --- a/build/lib/hbd/notify.py +++ /dev/null @@ -1,163 +0,0 @@ -"""Notification helpers: email, pushover, mattermost, signal and dispatcher.""" -from typing import Optional -import http.client -import urllib.parse -import subprocess -import smtplib -import time -import traceback - -DEFAULT_PUSHPROVIDERS = ["all", "pushover", "mattermost", "signal"] - -# module-level configuration set via setup() -_config = {} - - -def setup(cfg: dict): - """Initialize notifier defaults from a configuration dict.""" - global _config - _config = dict(cfg) - - -def send_email(aemail, smtpserver, sender, subject, body, debug=0): - """Send a plain email via SMTP. Returns True on success.""" - try: - server = smtplib.SMTP(smtpserver) - if debug > 0: - server.set_debuglevel(1) - server.sendmail(sender, aemail, body) - except Exception as e: - if debug: - print("email send failed:", e) - try: - server.quit() - except Exception: - pass - return False - try: - server.quit() - except Exception: - pass - return True - - -def email(subject: str, msg: str, debug: int = 0) -> bool: - """Convenience wrapper exposed to the rest of the application. - - Uses module-level configuration to supply recipient list, smtp server - and sender address. - """ - toaddrs = _config.get("AEMAIL") or _config.get("aemail") or _config.get("email_to") or [] - fromemail = _config.get("fromemail") or _config.get("sender") or f"aew.heartbeat@{_config.get('domain','local') }" - smtpserver = _config.get("SMTPSERVER") or _config.get("smtpserver") or _config.get("SMTPSERVER", "localhost") - date = time.strftime("%a, %d %b %Y %H:%M:%S %z", time.localtime()) - body = "To: %s\nFrom: %s\nSubject: %s\nDate: %s\n\n%s" % ( - toaddrs[0] if toaddrs else "", - fromemail, - subject, - date, - msg, - ) - return send_email(toaddrs, smtpserver, fromemail, subject, body, debug=debug) - - -def pushover(token: str, user: str, msg: str, debug: int = 0) -> bool: - """Send message via Pushover API.""" - conn = http.client.HTTPSConnection("api.pushover.net:443") - try: - conn.request( - "POST", - "/1/messages.json", - urllib.parse.urlencode({"token": token, "user": user, "message": msg}), - {"Content-type": "application/x-www-form-urlencoded"}, - ) - r = conn.getresponse() - if debug: - print("pushover response:", r.status, r.reason) - return r.status == 200 - except Exception as e: - if debug: - print("pushover error:", e) - return False - - -def pushmattermost(host: str, token: str, channel: str, msg: str, username: str = "hbd", icon: Optional[str] = None, debug: int = 0) -> bool: - """Send a message to Mattermost via simple webhook driver if available. - - This helper tries to import mattermostdriver.Driver and uses webhooks if present. - If the import fails it returns False. - """ - try: - from mattermostdriver import Driver - except Exception: - return False - ses = {"url": host, "scheme": "http", "basepath": "/api/v4", "port": 8065} - mm = Driver(ses) - payload = {"text": msg, "channel": channel, "username": username} - if icon: - payload["icon_url"] = icon - try: - rc = mm.webhooks.call_webhook(token, payload) - if debug: - print("mattermost rc:", rc) - return bool(rc is None or rc == "") - except Exception as e: - if debug: - print("mattermost error:", e) - return False - - -def pushsignal(signal_cli_bin: str, user: str, recipient: str, msg: str, debug: int = 0) -> bool: - """Send a message via signal-cli (requires local installation). - - Uses subprocess to call signal-cli. Returns True if the command succeeded. - """ - CLI = [signal_cli_bin, "-u", user, "send", "-m", msg, recipient] - if debug: - print("signal cli: ", CLI) - try: - res = subprocess.run(CLI, capture_output=True) - if res.returncode != 0: - if debug: - print("signal failed:", res.stderr.decode()) - return False - if debug: - print("signal sent:", res.stdout.decode()) - return True - except Exception as e: - if debug: - print("signal exception:", e) - return False - - -def pushmsg(cfg: dict, msg: str, debug: int = 0): - """Dispatch push notifications according to `cfg['pushsrv']`. - - cfg is expected to contain keys for different services when needed, e.g. - - cfg['pushsrv'] : one of 'all', 'pushover', 'mattermost', 'signal' - - cfg['pushover_token'], cfg['pushover_user'] - - cfg['matter_host'], cfg['matter_token'], cfg['matter_channel'] - - cfg['signal_cli'], cfg['signal_user'], cfg['signal_recipient'] - - Returns a dict of results per provider. - """ - results = {} - p = cfg.get("pushsrv", "pushover") - if p in ("all", "pushover"): - ok = pushover(cfg.get("pushover_token", ""), cfg.get("pushover_user", ""), msg, debug=debug) - results["pushover"] = ok - if p in ("all", "mattermost"): - ok = pushmattermost(cfg.get("matter_host", ""), cfg.get("matter_token", ""), cfg.get("matter_channel", ""), msg, username=cfg.get("matter_username", "hbd"), icon=cfg.get("matter_icon"), debug=debug) - results["mattermost"] = ok - if p in ("all", "signal"): - ok = pushsignal(cfg.get("signal_cli", "/usr/local/bin/signal-cli"), cfg.get("signal_user", ""), cfg.get("signal_recipient", ""), msg, debug=debug) - results["signal"] = ok - if debug: - print("push results:", results) - return results - - -def pushmsg_from_config(msg: str, debug: int = 0) -> dict: - """Use the module-level configuration dict to dispatch a push message.""" - return pushmsg(_config, msg, debug=debug) - diff --git a/build/lib/hbd/proto.py b/build/lib/hbd/proto.py deleted file mode 100644 index 8212960..0000000 --- a/build/lib/hbd/proto.py +++ /dev/null @@ -1,81 +0,0 @@ -"""Message encoding/decoding utilities for hbd protocol.""" -from typing import Dict, Any -import zlib - - -def dicttos(ID: str, d: Dict[str, Any], compress: bool = False): - """Serialize a dict to protocol message bytes. - - If compress is True, the payload is zlib-compressed and the message is - prefixed with `!ID:` as the original script did. Otherwise the format is - `ID:key=value;...` (bytes). - """ - s = [] - for k in d: - v = d[k] - if isinstance(v, float): - s.append(f"{k}={v:0.5f}") - else: - s.append(f"{k}={v}") - pk = ";".join(s) - if compress: - zpk = zlib.compress(pk.encode(), 6) - hdr = ("!" + ID + ":").encode() - return hdr + zpk - else: - return (ID + ":" + pk).encode() - - -def stodict(msg: bytes): - """Deserialize a protocol message into a dict. - - Mirrors original behaviour: detects compressed messages starting with - '!' and decodes accordingly. Returns a dict with key 'ID' set to the - message ID and the parsed key/value pairs. - """ - d = {} - if len(msg) > 0 and chr(msg[0]) == "!": - # message is: b'!ID:' + compressed_payload - # original code used msg[1:4].decode() for ID (3 bytes including colon) - try: - pk = zlib.decompress(msg[5:]).decode() - except Exception: - # malformed compressed payload - return {} - d["ID"] = msg[1:4].decode() - else: - try: - r0 = msg.split(b":", 1) - pk = r0[1].decode() - d["ID"] = r0[0].decode() - except Exception: - return {} - if not pk: - return d - parts = pk.split(";") - for v in parts: - if not v: - continue - vr = v.split("=", 1) - k = vr[0].strip() - if len(vr) == 1: - d[k] = None - else: - val = vr[1].strip() - if val and val[0].isdigit(): - try: - val_e = eval(val) - except Exception: - val_e = val - d[k] = val_e - else: - d[k] = val - return d - - -def oldmtodict(msg: bytes): - """Compatibility wrapper for old-style messages (no ID prefix). - - The original implementation prefixed with 'HTB:' and called stodict. - """ - return stodict(b"HTB:" + msg) diff --git a/build/lib/hbd/server.py b/build/lib/hbd/server.py deleted file mode 100644 index ae379c4..0000000 --- a/build/lib/hbd/server.py +++ /dev/null @@ -1,128 +0,0 @@ -"""Server runtime: starts UDP listener, HTTP server and websocket stubs.""" -import asyncio -import logging - -from . import udp - -logger = logging.getLogger(__name__) - - -async def _run_async(config): - loop = asyncio.get_running_loop() - - # shared runtime collections and helpers - msgs = [] - - # prepare runtime dependencies - import threading - import time - import hbdclass - from . import http as http_mod - from . import ws as ws_mod - from . import dns as dns_mod - from . import notify as notify_mod - - notify_mod.setup(config) - - def log(host, m, service=None): - ts = time.time() - s = f"{time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(ts))} {host or ''} {m}" - msgs.append(s) - logger.info(s) - - email = notify_mod.email - pushmsg = notify_mod.pushmsg_from_config - msg_to_websockets = ws_mod.broadcast - - # UDP server endpoint (handler wired to handle_datagram with context) - bind_addr = ("0.0.0.0", config.get("hb_port", 50003)) - logger.info("Starting UDP server on %s:%s", *bind_addr) - - def udp_handler(msg, addr, transport): - ctx = dict( - config=config, - hbdclass=hbdclass, - log=log, - email=email, - pushmsg=pushmsg, - msg_to_websockets=msg_to_websockets, - msgs=msgs, - DEBUG=config.get("debug", 0), - verbose=config.get("verbose", False), - ) - udp.handle_datagram(msg, addr, transport, ctx) - - transport, protocol = await loop.create_datagram_endpoint( - lambda: udp.EchoServerProtocol(config=config, handler=udp_handler), - local_addr=bind_addr, - ) - - # HTTP server (runs in its own thread) - try: - handler_cls = http_mod.make_handler_class( - config=config, - hbdclass=hbdclass, - msgs_getter=lambda: msgs, - log=log, - email=email, - pushmsg=pushmsg, - msg_to_websockets=msg_to_websockets, - tcss=None, - DEBUG=config.get("debug", 0), - verbose=config.get("verbose", False), - get_now=lambda: time.time(), - VER="", - ) - serv = http_mod.HttpServer((config.get("hbd_host", ""), config.get("hbd_port", 50004)), handler_cls) - http_thread = threading.Thread(target=serv.serve_forever, daemon=True) - http_thread.start() - logger.info("HTTP server started on %s:%s", config.get("hbd_host", ""), config.get("hbd_port", 50004)) - except Exception as e: - logger.exception("failed to start HTTP server: %s", e) - - # start dns update thread - dns_mod.start_dns_thread(hbdclass, config, log=log, email=email) - logger.info("dns update thread started") - - # Start the websocket servers as a background task - try: - ws_task = asyncio.create_task( - ws_mod.start( - host=config.get("hbd_host", ""), - ws_port=config.get("ws_port", 50005), - wss_port=config.get("wss_port", None), - ssl_context=None, - get_hosts=lambda: [hbdclass.Host.hosts[h].stateinfo() for h in sorted(hbdclass.Host.hosts)], - get_msgs=lambda: msgs, - verbose=config.get("verbose", False), - ) - ) - logger.info("WebSocket task started") - except Exception as e: - logger.exception("websocket server failed to start: %s", e) - - try: - # run forever - await asyncio.Future() - finally: - transport.close() - try: - serv.shutdown() - except Exception: - pass - try: - ws_task.cancel() - except Exception: - pass - - -def run(config): - """Start the hbd service (blocking). - - This is a thin wrapper around asyncio.run to host the async services. - """ - logging.basicConfig(level=logging.DEBUG if config.get("debug", 0) > 0 else logging.INFO) - try: - asyncio.run(_run_async(config)) - except KeyboardInterrupt: - logger.info("Shutting down (KeyboardInterrupt)") diff --git a/build/lib/hbd/udp.py b/build/lib/hbd/udp.py deleted file mode 100644 index 8004ac3..0000000 --- a/build/lib/hbd/udp.py +++ /dev/null @@ -1,235 +0,0 @@ -"""UDP listener and datagram processing.""" -import asyncio -from compression import zlib -import logging - -logger = logging.getLogger(__name__) - - -from .proto import stodict, oldmtodict -from hbd.utils import dur - - -class EchoServerProtocol(asyncio.DatagramProtocol): - def __init__(self, config=None, handler=None): - super().__init__() - self.config = config or {} - self.handler = handler - - def connection_made(self, transport): - self.transport = transport - logger.info("UDP Server listening...") - - def datagram_received(self, data, addr): - logger.debug("Received from %s", addr) - try: - msg = parse_message(data) - if self.handler: - # handler can be a callable provided by the application - # pass the transport so handlers can send replies (ACKs/commands) - self.handler(msg, addr, self.transport) - except Exception: - logger.exception("Error while processing datagram from %s", addr) - - -def parse_message(data: bytes): - """Parse a raw datagram into a message dict. - - Uses the protocol decoding helpers and falls back to old format when - decoding returns an empty dict (compat with older clients). - """ - msg = stodict(data) - if not msg: - # fallback to old format - msg = oldmtodict(data) - return msg - -def dicttos(ID, d, compress=False): - s = [] - for k in d: - if isinstance(d[k], float): - s.append("%s=%0.5f" % (k, d[k])) - else: - s.append("%s=%s" % (k, d[k])) - pk = ";".join(s) - if compress: - zpk = zlib.compress(pk.encode(), 6) - ID = "!" + ID + ":" - opk = ID.encode() + zpk - else: - zpk = pk - opk = ID + ":" + zpk - return opk - -def handle_datagram(msg: dict, addr, transport, ctx: dict): - """Handle a parsed datagram message. - - ctx is a dictionary with runtime dependencies: - - config: dict of configuration - - hbdclass: module providing Host/Connection classes - - log: callable(loghost, message) - - email: callable(subject, message) - - pushmsg: callable(message) - - msg_to_websockets: callable(typ, data) - - msgs: list for storing message strings - - DEBUG, verbose - """ - if not msg: - return - now = __import__("time").time() - cfg = ctx.get("config", {}) - hbdcls = ctx.get("hbdclass") - log = ctx.get("log") - email = ctx.get("email") - pushmsg = ctx.get("pushmsg") - msg_to_websockets = ctx.get("msg_to_websockets") - msgs = ctx.get("msgs") - DEBUG = ctx.get("DEBUG", 0) - verbose = ctx.get("verbose", False) - - # normalize addr (ip, port) - ip = addr[0] if isinstance(addr, (list, tuple)) else addr - name = msg.get("name", "unknown") - from hbd.utils import shortname - uname = shortname(name) - - if uname not in hbdcls.Host.hosts: - host = hbdcls.Host(uname) - host.dyn = uname in cfg.get("dyndnshosts", []) - if verbose: - print(("XX: New host, num now %s" % (len(hbdcls.Host.hosts)))) - newh = True - else: - host = hbdcls.Host.hosts[uname] - newh = False - - cid = msg.get("id", 0) - try: - rtt = float(msg.get("rtt", None)) - except Exception: - rtt = None - - if msg.get("ID") == "HTB": - host.doesack = msg.get("acks", -1) - host.setcver(msg.get("ver", 0)) - - try: - conn, res = host.conndata(cid, ip, rtt, now) - except Exception as e: - if DEBUG > 0: - print("conndata failed: %s" % e) - return - - if res: - if log: - log(uname, res) - if uname in cfg.get("watchhosts", []): - if email: - email("address change", "%s %s" % (host.name, res)) - if pushmsg: - pushmsg("%s %s" % (host.name, res)) - - interval = int(msg.get("interval", 0) or 0) - shutdown = msg.get("shutdown", 0) - service = msg.get("service", "unknown") - message = msg.get("msg", None) - boot = msg.get("boot", 0) - - if boot: - if log: - log(uname, "booted") - if uname in cfg.get("watchhosts", []): - m = "%s booted" % (host.name) - if email: - email("booted", m) - if pushmsg: - pushmsg(m) - if message: - if log: - log(uname, "msg: %s" % message, service=service) - if uname in cfg.get("watchhosts", []): - if email: - email("msg", message) - if pushmsg: - pushmsg(message) - - if conn.getstate() != hbdcls.Connection.UP: - lasts = conn.state - d = conn.newstate(hbdcls.Connection.UP, now) - m = "%s back after being %s for %s" % (conn.afam, lasts, dur(d)) - if log: - log(uname, m) - if uname in cfg.get("watchhosts", []): - if email: - email("%s back" % conn.afam, uname) - if pushmsg: - pushmsg("%s %s is back" % (uname, conn.afam)) - - if boot or newh: - host.upcount = host.doesack - else: - host.upcount += 1 - - if shutdown: - if log: - log(uname, "%s shutdown" % conn.afam) - if uname in cfg.get("watchhosts", []): - if email: - email("shutdown", "%s %s shutdown" % (uname, conn.afam)) - if pushmsg: - pushmsg("%s %s shutdown" % (uname, conn.afam)) - conn.newstate(hbdcls.Connection.DOWN, now) - - if interval > 0: - host.interval = interval - - # send ACK back - rmsg = {"time": __import__("time").time()} - if host.cver < 1: - opkt = b"ACK" - else: - opkt = dicttos("ACK", rmsg, host.cver > 1) - try: - transport.sendto(opkt, addr) - except Exception as e: - if DEBUG > 0: - print(("cannot send ack: %s" % e)) - - # send any commands we have queued - while len(host.cmds): - op, rmsg = host.cmds[0] - if op == "CMD": - if email: - email("%s cmd exec" % uname, "command '%s' sent" % rmsg) - del host.cmds[0] - if log: - log(uname, "command sent") - if host.cver < 1: - rmsg = rmsg["cmd"] - elif op == "UPD": - del host.cmds[0] - if log: - log(uname, "update initiated") - if host.cver < 1: - if log: - log(uname, " ver 0 does not support UPD") - continue - if host.cver < 1: - opkt = rmsg if isinstance(rmsg, (bytes, str)) else str(rmsg) - if isinstance(opkt, str): - opkt = opkt.encode() - else: - opkt = dicttos(op, rmsg, True) - try: - transport.sendto(opkt, addr) - except Exception as e: - if DEBUG > 0: - print(("cannot send cmd/update: %s" % e)) - - if msg_to_websockets: - try: - msg_to_websockets("host", host.stateinfo()) - except Exception: - pass - - diff --git a/build/lib/hbd/utils.py b/build/lib/hbd/utils.py deleted file mode 100644 index 7188dd9..0000000 --- a/build/lib/hbd/utils.py +++ /dev/null @@ -1,36 +0,0 @@ -"""Utility helpers extracted from the original script.""" -import time - - -def shortname(name: str) -> str: - return name.split(".")[0] - - -def dur(sec: int) -> str: - sec = int(sec) - h = int(sec / 3600) - m = int((sec - h * 3600) / 60) - s = int((sec - h * 3600) % 60) - if h > 0: - return "%d:%02d:%02d" % (h, m, s) - if m > 0: - return "%d:%02d" % (m, s) - return "0:%02d" % s - - -def initlog(logfile: str): - """Open logfile for appending; fall back to creating it or returning stderr. - - This mirrors the original behaviour from the monolithic script. - """ - try: - return open(logfile, "a+") - except Exception: - pass - try: - return open(logfile, "w") - except Exception as e: - import sys - - print(f"cannot open logfile {logfile}, using STDERR: {e}") - return sys.stderr diff --git a/build/lib/hbd/ws.py b/build/lib/hbd/ws.py deleted file mode 100644 index 6f85cdb..0000000 --- a/build/lib/hbd/ws.py +++ /dev/null @@ -1,125 +0,0 @@ -"""WebSocket server and broadcast helpers for hbd. - -Provides an asyncio-based WebSocket server and a thread-safe broadcast -function that other threads or synchronous code can call. -""" -import asyncio -import json -import logging -from typing import Callable, Iterable, Optional - -import websockets - -logger = logging.getLogger(__name__) - -_connections = set() -_loop: Optional[asyncio.AbstractEventLoop] = None -_get_hosts: Optional[Callable[[], Iterable]] = None -_get_msgs: Optional[Callable[[], Iterable]] = None -_verbose = False - - -async def _handler(websocket, path): - global _connections - _connections.add(websocket) - remote_address = websocket.remote_address - if _verbose: - logger.info("DBG ws_serve: %s: %s", remote_address, path) - try: - # send initial hosts - if _get_hosts: - for h in _get_hosts(): - jmsg = json.dumps({"type": "host", "data": h}) - await websocket.send(jmsg) - # send recent messages - if _get_msgs: - for m in list(_get_msgs())[-100:]: - jmsg = json.dumps({"type": "message", "data": m}) - await websocket.send(jmsg) - - # keep connection open until client disconnects - async for _ in websocket: - # we don't expect meaningful incoming messages besides the initial - # client 'hello' that some clients send; ignore for now - if _verbose: - logger.debug("received ws data: %s", _) - - except (websockets.exceptions.ConnectionClosedOK, websockets.exceptions.ConnectionClosedError) as e: - if _verbose: - logger.info("ws closed: %r", e) - except Exception as e: - logger.exception("ws handler exception: %s", e) - finally: - try: - _connections.remove(websocket) - except KeyError: - pass - await websocket.wait_closed() - - -async def start(host: str, ws_port: int, wss_port: Optional[int] = None, ssl_context=None, get_hosts: Optional[Callable] = None, get_msgs: Optional[Callable] = None, verbose: bool = False): - """Start WebSocket servers and block until cancelled. - - This is intended to be awaited inside the main asyncio event loop. - If `wss_port` and `ssl_context` are provided, a WSS server will also be - started. - """ - global _loop, _get_hosts, _get_msgs, _verbose - _loop = asyncio.get_running_loop() - _get_hosts = get_hosts - _get_msgs = get_msgs - _verbose = verbose - - servers = [] - # plain WebSocket - ws_server = websockets.serve(_handler, host, ws_port, subprotocols=["hbd"]) - servers.append(ws_server) - - # secure WebSocket (optional) - if wss_port and ssl_context: - wss_server = websockets.serve(_handler, host, wss_port, ssl=ssl_context, subprotocols=["hbd"]) - servers.append(wss_server) - - # await starting of all servers - for srv in servers: - await srv - - if _verbose: - logger.info("WebSocket server started on port %s (wss %s)", ws_port, wss_port) - - # block forever (until loop is stopped or cancelled) - await asyncio.Future() - - -def broadcast(typ: str, data) -> bool: - """Thread-safe broadcast helper. - - Schedules coroutine(s) on the running loop to send message to all - connected websockets. Returns False if server was not running. - """ - global _loop - if not _loop: - return False - jmsg = json.dumps({"type": typ, "data": data}) - to_close = [] - for ws in list(_connections): - if ws.closed: - to_close.append(ws) - continue - try: - asyncio.run_coroutine_threadsafe(ws.send(jmsg), _loop) - except Exception: - to_close.append(ws) - logger.debug("ws.send exception: closed") - for ws in to_close: - try: - asyncio.run_coroutine_threadsafe(ws.wait_closed(), _loop) - except Exception: - pass - if ws in _connections: - _connections.remove(ws) - return True - - -def connection_count() -> int: - return len(_connections) diff --git a/callhome b/callhome deleted file mode 100755 index d380e4c..0000000 --- a/callhome +++ /dev/null @@ -1,7 +0,0 @@ -#!/bin/sh - -# excute on remote machine -# forwared 2 ports to wig: 5903 to screen shareing and 5922 to ssh - -HOST=192.168.10.10 -/usr/bin/ssh -f -N -C -R $HOST:5903:127.0.0.1:5900 -R $HOST:5922:127.0.0.1:22 home.wrede.ca diff --git a/dist/heartbeat-0.1.0-py3-none-any.whl b/dist/heartbeat-0.1.0-py3-none-any.whl deleted file mode 100644 index 9187da9..0000000 Binary files a/dist/heartbeat-0.1.0-py3-none-any.whl and /dev/null differ diff --git a/dist/heartbeat-0.1.0.tar.gz b/dist/heartbeat-0.1.0.tar.gz deleted file mode 100644 index 6e92a28..0000000 Binary files a/dist/heartbeat-0.1.0.tar.gz and /dev/null differ diff --git a/hbd.sh b/hbd.sh deleted file mode 100755 index abfbb6e..0000000 --- a/hbd.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/sh - -# $Id: hbd.sh,v 1.1 2010/04/02 11:09:05 andreas Exp $ -while true; do - /home/andreas/bin/hbd -f > /tmp/hbd.$$.log 2>&1 - cat /tmp/hbd.$$.log | mail -s "hbd died" andreas@wrede.ca - sleep 10 -done diff --git a/hbd/__init__.py b/hbd/__init__.py index 5896eaa..8fb72b6 100644 --- a/hbd/__init__.py +++ b/hbd/__init__.py @@ -6,6 +6,6 @@ start moving functionality into the package. """ __all__ = ["main", "__version__"] -__version__ = "0.1" +__version__ = "5.0" from .cli import main diff --git a/hbd/config.py b/hbd/config.py index c7a53b5..c34b2bf 100644 --- a/hbd/config.py +++ b/hbd/config.py @@ -1,4 +1,5 @@ """Configuration loader and defaults for hbd.""" +import logging import os try: @@ -14,6 +15,8 @@ DEFAULTS = { "logfile": "/var/log/heartbeat.log", "logfmt": "text", "pushsrv": "pushover", + "pushover_token": "", + "pushover_user": "", "interval": 20, "grace": 2, "dyndomains": ["wrede.org"], @@ -40,14 +43,13 @@ def load_config(path=None): if os.path.exists(path): if yaml: with open(path) as fh: - data = yaml.safe_load(fh) or {} + data = yaml.safe_load(fh) # only keep known keys for k, v in data.items(): if k in cfg: cfg[k] = v else: - # ignore unknown keys for now - pass + logging.warning("unknown config key %s in %s", k, path) else: # yaml not installed: do not attempt to parse; user must ensure defaults pass diff --git a/hbd/dns.py b/hbd/dns.py index 7088c85..c16b29a 100644 --- a/hbd/dns.py +++ b/hbd/dns.py @@ -1,9 +1,9 @@ -"""DNS update helper and thread for heartbeat daemon.""" +"""DNS update helper and pure asyncio worker for heartbeat daemon.""" from __future__ import annotations -import threading import subprocess from subprocess import Popen, PIPE, STDOUT from typing import Optional +import asyncio def create_nsupdate_payload(hostname: str, newip: str, dyndomain: str, dnsttl: str = "5") -> str: @@ -54,38 +54,118 @@ def nsupdate(hostname: str, newip: str, dyndomain: str, nsupdate_bin: str = "/us return out -def dnsupdatethread(hbdclass, cfg: dict, log: Optional[callable] = None, email: Optional[callable] = None): - """Thread target: process dns update queue from hbdclass.Host.dnsQ. +async def dns_update_worker(hbdclass, cfg: dict, async_queue=None, log: Optional[callable] = None, email: Optional[callable] = None, loop: Optional[asyncio.AbstractEventLoop] = None): + """Pure async DNS worker that processes updates from asyncio.Queue. - hbdclass: module with Host class that exposes dnsQ queue - cfg: configuration mapping with 'dyndomains' and 'nsupdate_bin' - log: callable(host, message) - email: callable(subject, message) + Exits when it receives a None sentinel. """ + if loop is None: + loop = asyncio.get_running_loop() + + dnsq = async_queue + if not dnsq: + if log: + try: + await loop.run_in_executor(None, log, None, "dns_update_worker: no queue available") + except Exception: + pass + return + while True: - name, addr = hbdclass.Host.dnsQ.get() + try: + item = await dnsq.get() + except Exception as e: + if log: + try: + await loop.run_in_executor(None, log, None, f"dns_update_worker: error getting item: {e}") + except Exception: + pass + break + + if item is None: + break + + try: + name, addr = item + except Exception: + try: + dnsq.task_done() + except Exception: + pass + continue + m = f"changed address to {addr}" for dyndomain in cfg.get("dyndomains", []): - err = nsupdate(name, addr, dyndomain, nsupdate_bin=cfg.get("nsupdate_bin", "/usr/local/bin/nsupdate")) + err = await loop.run_in_executor(None, nsupdate, name, addr, dyndomain, cfg.get("nsupdate_bin", "/usr/local/bin/nsupdate"), cfg.get("rndc_key", "/etc/dhcpc/rndc-key")) if err: m += f", DNS update failed: {err}" if email: try: - email("error: nsupdate failed", f"{name}.dy.{dyndomain}: {m}") + await loop.run_in_executor(None, email, "error: nsupdate failed", f"{name}.dy.{dyndomain}: {m}") except Exception: pass else: m += ", DNS updated." - hbdclass.Host.dnsQ.task_done() + + try: + dnsq.task_done() + except Exception: + pass + if log: try: - log(name, m) + await loop.run_in_executor(None, log, name, m) except Exception: pass + if log: + try: + await loop.run_in_executor(None, log, None, "dns_update_worker exiting") + except Exception: + pass -def start_dns_thread(hbdclass, cfg: dict, log: Optional[callable] = None, email: Optional[callable] = None) -> threading.Thread: - t = threading.Thread(target=dnsupdatethread, args=(hbdclass, cfg, log, email)) - t.daemon = True - t.start() - return t + +def start_dns_worker(hbdclass, cfg: dict, log: Optional[callable] = None, email: Optional[callable] = None, loop: Optional[asyncio.AbstractEventLoop] = None): + """Start the async DNS worker and return the Task. + + Replaces Host.dnsQ with an asyncio.Queue wrapped in a thread-safe bridge + so legacy synchronous put() calls from UDP handlers still work. + """ + if loop is None: + loop = asyncio.get_event_loop() + + # Create asyncio.Queue and wrap in a bridge for thread-safe puts + async_q = asyncio.Queue() + + class _QueueBridge: + """Thread-safe wrapper around asyncio.Queue for synchronous callers.""" + def __init__(self, loop, aq): + self._loop = loop + self._aq = aq + + def put(self, item): + """Thread-safe put that schedules onto event loop.""" + try: + # Try to detect if we're in the event loop thread + asyncio.get_running_loop() + # We're in event loop context, use put_nowait directly + self._aq.put_nowait(item) + except RuntimeError: + # We're in a different thread, schedule safely + try: + self._loop.call_soon_threadsafe(self._aq.put_nowait, item) + except Exception: + pass + + def task_done(self): + """Delegate task_done to asyncio.Queue.""" + try: + self._aq.task_done() + except Exception: + pass + + bridge = _QueueBridge(loop, async_q) + hbdclass.Host.dnsQ = bridge + + task = loop.create_task(dns_update_worker(hbdclass, cfg, async_queue=async_q, log=log, email=email, loop=loop)) + return task diff --git a/hbdclass.py b/hbd/hbdclass.py similarity index 100% rename from hbdclass.py rename to hbd/hbdclass.py diff --git a/hbd/http.py b/hbd/http.py index eb701b1..f18d918 100644 --- a/hbd/http.py +++ b/hbd/http.py @@ -1,20 +1,23 @@ -"""HTTP server and handler scaffolds (thin wrappers around http.server).""" -from http import server +"""HTTP server implementation using aiohttp and jinja2.""" +import asyncio import json import time import urllib.parse - -from urllib3 import request +import os +import logging +from aiohttp import web from fastapi.templating import Jinja2Templates +import jinja2 -class HttpServer(server.ThreadingHTTPServer): - allow_reuse_address = True +logger = logging.getLogger(__name__) - def threaded(self): - pass +def _render_template(html_str: str, **context) -> str: + tmpl = jinja2.Template(html_str) + return tmpl.render(**context) - -def make_handler_class( +async def start( + host: str, + port: int, config, hbdclass, msgs_getter, @@ -28,209 +31,165 @@ def make_handler_class( get_now=None, VER="", ): - """Return a BaseHTTPRequestHandler subclass bound to runtime objects. + """Start an aiohttp web server and block until cancelled. - `msgs_getter` should be a callable that returns a list-like of messages. + This function is intended to be awaited inside the main asyncio event loop. """ - templates = Jinja2Templates(directory="templates") get_now = get_now or (lambda: time.time()) - class CustomHandler(server.BaseHTTPRequestHandler): + async def index(request): + res = [] + res.append('') + res.append("") + res.append("") + res.append(f"Heartbeat") + if tcss: + res.append(tcss) + res.append("") + res.append('') + res.append(f"

Heartbeat status {VER}

") + res += hbdclass.ubHost.buildhosttable() + res += hbdclass.ubHost.buildmsgtable(msgs_getter()) + res.append( + "

%s (%s)

" % (time.strftime("%H:%M:%S", time.localtime(get_now())), config.get("tz", "CET-1CDT")) + ) + res.append("") + body = "\n".join(res) + return web.Response(text=body, content_type="text/html") - server_version = f"HeartbeatHTTP/{VER}" + async def api_hosts(request): + lst = [hbdclass.Host.hosts[h].jsons() for h in hbdclass.Host.hosts] + return web.json_response(json.loads("[" + ",".join(lst) + "]")) - def version_string(self): - return self.server_version + async def api_messages(request): + lst = msgs_getter()[-30:] + return web.json_response(lst) - def handle(self): + async def cmd(request): + qa = request.rel_url.query + uname = qa.get("h") + ucmd = qa.get("c") + if not ucmd or not uname: + return web.Response(status=400, text="need h= and c= arguments") + if uname not in hbdclass.Host.hosts: + return web.Response(status=400, text=f"h={uname} not found") + hbdclass.Host.hosts[uname].cmds.append(("CMD", {"cmd": urllib.parse.unquote(ucmd)})) + return web.Response(text=f"cmd {uname} queued") + + async def drop(request): + qa = request.rel_url.query + uname = qa.get("h") + if not uname: + return web.Response(status=400, text="need h= argument") + if uname not in hbdclass.Host.hosts: + return web.Response(status=400, text=f"h={uname} not found") + if log: + log(uname, "dropped") + del hbdclass.Host.hosts[uname] + return web.Response(text="Done") + + async def register(request): + qa = request.rel_url.query + uname = qa.get("h") + if not uname: + return web.Response(status=400, text="need h= argument") + if uname not in hbdclass.Host.hosts: + return web.Response(status=400, text=f"h={uname} not found") + ll = hbdclass.Host.hosts[uname].registerDns() + if log: + log(uname, ll) + return web.Response(text=str(ll)) + + async def update(request): + qa = request.rel_url.query + uname = urllib.parse.unquote(qa.get("h", "")) + ucode = qa.get("c") + if not ucode or not uname: + return web.Response(status=400, text="need h= and c= arguments") + if uname != "All" and uname not in hbdclass.Host.hosts: + return web.Response(status=400, text=f"h={uname} not found") + if uname != "All": + names = [uname] + else: + names = [n for n in hbdclass.Host.hosts if hbdclass.Host.hosts[n].cver >= 2] + out = [] + for n in names: + err = None try: - return server.BaseHTTPRequestHandler.handle(self) + r = {"csum": None, "code": ucode} + hbdclass.Host.hosts[n].cmds.append(("UPD", r)) except Exception as e: - self.log_error("Request went away: %r", e) - self.close_connection = 1 - return + err = str(e) + out.append(f"update started for {n}: {err if err else 'OK'}") + return web.Response(text="\n".join(out)) - def do_HEAD(self): - self.setheaders(200) + async def restart(request): + # signal main application to perform restart if needed + # not implemented here - return OK + if log: + log(None, "restart request") + return web.Response(text="restart request") - def setheaders(self, code, headerdict={}): - self.send_response(code) - self.send_header( - "Last-Modified", - time.strftime("%a, %d %b %Y %H:%M:%S GMT", time.gmtime(get_now())), - ) - for h in headerdict: - self.send_header(h, headerdict[h]) - self.end_headers() + async def live(request): + # render template from templates/live.html using Jinja2 + env = jinja2.Environment(loader=jinja2.FileSystemLoader(config.get("templates_dir", "templates"))) + host = config.get("hb_host", "localhost") + extra_scripts = config.get("http_extra_scripts", "") + heartbeat_ws_url = f"ws://{host}:{config.get('ws_port', 50005)}/hbd" + tmpl = env.get_template("live.html") + body = tmpl.render( + title="Heartbeat", + header="Heartbeat", + request=request, + heartbeat_ws_url=heartbeat_ws_url, + extra_scripts=extra_scripts, + hosts=[hbdclass.Host.hosts[h].stateinfo() for h in sorted(hbdclass.Host.hosts)], + messages=msgs_getter()[-30:], + ) + return web.Response(text=body, content_type="text/html") - def buildhead(self, title="Heartbeat", refresh=None, extras=None): - res = [] - res.append('') - res.append("") - res.append("") - res.append("%s" % (title)) - if refresh: - res.append("\n" % refresh) - if extras: - res.append(extras) - res.append("") - res.append('') - return res + async def static(request): + """Serve files from the package static directory. - def buildpage(self): - res = self.buildhead(refresh=60, extras=tcss) - res.append("

Heartbeat status %s

" % VER) - res += hbdclass.ubHost.buildhosttable() - res += hbdclass.ubHost.buildmsgtable(msgs_getter()) - res.append( - "

%s (%s)

" % (time.strftime("%H:%M:%S", time.localtime(get_now())), config.get("tz", "CET-1CDT")) - ) - res.append("") - return res + URL form: /static/ + """ + p = request.match_info.get("path", "") + base = os.path.abspath(os.path.join(os.path.dirname(__file__), "static")) + # normalize and prevent directory traversal + target = os.path.abspath(os.path.normpath(os.path.join(base, p))) + if not target.startswith(base + os.sep) and target != base: + return web.Response(status=403, text="Forbidden") + if not os.path.exists(target) or not os.path.isfile(target): + return web.Response(status=404, text="Not Found") + logger.info("serving static file: %s", target) + return web.FileResponse(path=target) - def builderror(self, code, cause, lcause): - res = [] - res.append('') - res.append("") - res.append("%s %s" % (code, cause)) - res.append("") - res.append("

%s

" % (cause)) - res.append("

%s

" % lcause) - res.append("
") - res.append( - "
hbd (Unix) Server at %s:%s
" % (config.get("hbd_host"), config.get("hbd_port")) - ) - res.append("") - return code, res + app = web.Application() + app.add_routes( + [ + web.get("/", index), + web.get("/api/0/hosts", api_hosts), + web.get("/api/0/messages", api_messages), + web.get("/c", cmd), + web.get("/d", drop), + web.get("/n", register), + web.get("/u", update), + web.get("/r", restart), + web.get("/live", live), + web.get("/static/{path:.*}", static), + ] + ) - def do_GET(self): - xsig = 0 - rqAcceptEncoding = self.headers.get("Accept-encoding", {}) - headerdict = {"Content-Type": "text/html; charset = ISO-8859-1"} - qr = urllib.parse.urlparse(self.path) - qa = urllib.parse.parse_qs(qr.query) + runner = web.AppRunner(app) + await runner.setup() + site = web.TCPSite(runner, host, port) + await site.start() - if qr.path == "/": - res = self.buildpage() + if verbose: + print(f"HTTP server started on {host}:{port}") - elif qr.path == "/c": # command on host /c?h=melschserver&c=sudo%20ls - uname = qa.get("h", [None])[0] - ucmd = qa.get("c", [None])[0] - if not ucmd or not uname: - code, res = self.builderror(400, "Argument error", "need h= and c= arguments") - elif uname not in hbdclass.Host.hosts: - code, res = self.builderror(400, "Data error", "h=%s not found" % uname) - else: - hbdclass.Host.hosts[uname].cmds.append(("CMD", {"cmd": urllib.parse.unquote(ucmd)})) - res = self.buildhead() - res.append("cmd %s queued for host %s" % (uname, ucmd)) + try: + await asyncio.Future() + finally: + await runner.cleanup() - elif qr.path == "/d": # drop host /d?h=melschserver - uname = qa.get("h", [None])[0] - if not uname: - code, res = self.builderror(400, "Argument error", "need h= argument") - if uname not in hbdclass.Host.hosts: - code, res = self.builderror(400, "Data error", "h=%s not found" % uname) - else: - if log: - log(uname, "dropped") - del hbdclass.Host.hosts[uname] - res = self.buildhead() - res.append("Done") - - elif qr.path == "/n": # register name - uname = qa.get("h", [None])[0] - if not uname: - code, res = self.builderror(400, "Argument error", "need h= argument") - if uname not in hbdclass.Host.hosts: - code, res = self.builderror(400, "Data error", "h=%s not found" % uname) - else: - ll = hbdclass.Host.hosts[uname].registerDns() - res = self.buildhead() - res.append(ll) - if log: - log(uname, ll) - - elif qr.path == "/u": # update - uname = urllib.parse.unquote(qa.get("h", [None])[0]) - ucode = qa.get("c", [None])[0] - if not ucode or not uname: - code, res = self.builderror(400, "Argument error", "need h= and c= arguments") - elif uname != "All" and uname not in hbdclass.Host.hosts: - code, res = self.builderror(400, "Data error", "h=%s not found" % uname) - else: - res = self.buildhead() - if uname != "All": - names = [uname] - else: - names = [] - for n in hbdclass.Host.hosts: - if hbdclass.Host.hosts[n].cver >= 2: # earliest version that supports update - names.append(n) - for n in names: - err = None - try: - from hbd import proto - # read code from a file name, fallback to sending ucode as data - err = None - # attempt to send update command to host - r = {"csum": None, "code": ucode} - hbdclass.Host.hosts[n].cmds.append(("UPD", r)) - except Exception as e: - err = str(e) - res.append("update started for %s: %s
" % (n, err if err else "OK")) - res.append("Done") - - elif qr.path == "/api/0/hosts": # api access to host table - headerdict = {"Content-Type": "application/json; charset=utf-8"} - lst = [] - for h in hbdclass.Host.hosts: - lst.append(hbdclass.Host.hosts[h].jsons()) - res = ["[" + ",".join(lst) + "]"] - - elif qr.path == "/api/0/messages": # api access to host table - headerdict = {"Content-Type": "application/json; charset=utf-8"} - lst = msgs_getter()[-30:] - res = [json.dumps(lst)] - - elif qr.path == "/r": # restart - res = self.buildhead() - res.append("restart request") - xsig = 1 # signal.SIGHUP will be handled by application - if log: - log(None, "restart request") - elif qr.path == "/live": # show live view with websockets - host = config.get("hb_host", "localhost") - extra_scripts = '' # '' - heartbeat_ws_url = f"ws://{host}:50005/hbd" - res = templates.TemplateResponse( - "live.html ", - { - "title": "Heartbeat", - "header": "Heartbeat", - "heartbeat_ws_url": heartbeat_ws_url, - "extra_scripts": extra_scripts, - }, - ) - else: - code, res = self.builderror(404, "Not Found", "requested URL was not found on this server.") - - if "deflate" in rqAcceptEncoding: - headerdict["Content-Encoding"] = "deflate" - towrite = __import__("zlib").compress("\n".join(res).encode(), 6) - else: - towrite = "\n".join(res) - headerdict["Content-Length"] = len(towrite) - headerdict["Cache-Control"] = "private, must-revalidate, max-age=0" - headerdict["Expires"] = "Thu, 01 Jan 1970 00:00:00 GMT" - self.setheaders(200 if 'res' in locals() else code, headerdict) - self.wfile.write(towrite if isinstance(towrite, bytes) else towrite.encode()) - - if xsig: - # inform application via setting a flag on the server instance - try: - self.server.xsig = xsig - except Exception: - pass - - return CustomHandler diff --git a/hbd/monitor.py b/hbd/monitor.py new file mode 100644 index 0000000..c407127 --- /dev/null +++ b/hbd/monitor.py @@ -0,0 +1,46 @@ +"""monitor helper and thread for heartbeat daemon.""" +from __future__ import annotations +import asyncio +import threading +import subprocess +import time +from subprocess import Popen, PIPE, STDOUT +from typing import Optional +from . import hbdclass +DROPOVERDUE = 7 * 24 * 3600 + +def checkoverdue(config: dict, hbdclass, log: callable, email: callable, pushmsg: callable, msg_to_websockets: callable): + now = time.time() + for h in list(hbdclass.Host.hosts.keys()): + pmsg = [] + for c in hbdclass.Host.hosts[h].connections: + conn = hbdclass.Host.hosts[h].connections[c] + if conn.state == hbdclass.Connection.DOWN: + continue + timeout = hbdclass.Host.hosts[h].interval + config.get("grace", 10) + if conn.state == hbdclass.Connection.UP and (now - conn.lastbeat) > timeout: + conn.newstate(hbdclass.Connection.OVERDUE, now, config.get("grace", 10)) + pmsg.append(conn.afam) + if ( + conn.state == hbdclass.Connection.OVERDUE and (now - conn.lastbeat) > DROPOVERDUE + ): + conn.newstate(hbdclass.Connection.UNKNOWN, conn.lastbeat) + if pmsg != []: + if h in config.get("watchhosts", []): + email("overdue", "%s overdue" % " and ".join(pmsg)) + pushmsg("%s %s overdue" % (h, " and ".join(pmsg))) + log(h, "%s overdue" % " and ".join(pmsg)) + msg_to_websockets("host", hbdclass.Host.hosts[h].stateinfo()) + +async def start( + config: dict, + hbdclass: callable, + log=None, + email=None, + pushmsg=None, + msg_to_websockets=None, +): + """ start a monitor loop that checks for overdue hosts every minute """ + while True: + await asyncio.sleep(15) # 15 seconds between checks + checkoverdue(config, hbdclass, log, email, pushmsg, msg_to_websockets) diff --git a/hbd/notify.py b/hbd/notify.py index 8c99e76..39e7f17 100644 --- a/hbd/notify.py +++ b/hbd/notify.py @@ -1,4 +1,5 @@ """Notification helpers: email, pushover, mattermost, signal and dispatcher.""" +import logging from typing import Optional import http.client import urllib.parse @@ -11,6 +12,7 @@ DEFAULT_PUSHPROVIDERS = ["all", "pushover", "mattermost", "signal"] # module-level configuration set via setup() _config = {} +logger = logging.getLogger(__name__) def setup(cfg: dict): @@ -27,8 +29,7 @@ def send_email(aemail, smtpserver, sender, subject, body, debug=0): server.set_debuglevel(1) server.sendmail(sender, aemail, body) except Exception as e: - if debug: - print("email send failed:", e) + logger.warning("email send failed: %s", e) try: server.quit() except Exception: @@ -72,12 +73,10 @@ def pushover(token: str, user: str, msg: str, debug: int = 0) -> bool: {"Content-type": "application/x-www-form-urlencoded"}, ) r = conn.getresponse() - if debug: - print("pushover response:", r.status, r.reason) + logger.debug("pushover response: %s %s", r.status, r.reason) return r.status == 200 except Exception as e: - if debug: - print("pushover error:", e) + logger.error("pushover error: %s", e) return False @@ -98,12 +97,10 @@ def pushmattermost(host: str, token: str, channel: str, msg: str, username: str payload["icon_url"] = icon try: rc = mm.webhooks.call_webhook(token, payload) - if debug: - print("mattermost rc:", rc) + logger.debug("mattermost rc: %s", rc) return bool(rc is None or rc == "") except Exception as e: - if debug: - print("mattermost error:", e) + logger.error("mattermost error: %s", e) return False @@ -113,20 +110,16 @@ def pushsignal(signal_cli_bin: str, user: str, recipient: str, msg: str, debug: Uses subprocess to call signal-cli. Returns True if the command succeeded. """ CLI = [signal_cli_bin, "-u", user, "send", "-m", msg, recipient] - if debug: - print("signal cli: ", CLI) + logger.debug("signal cli: %s", CLI) try: res = subprocess.run(CLI, capture_output=True) if res.returncode != 0: - if debug: - print("signal failed:", res.stderr.decode()) + logger.error("signal failed: %s". res.stderr.decode()) return False - if debug: - print("signal sent:", res.stdout.decode()) + logger.debug("signal sent: %s", res.stdout.decode()) return True except Exception as e: - if debug: - print("signal exception:", e) + logger.exception("signal exception: %s", e) return False @@ -152,8 +145,7 @@ def pushmsg(cfg: dict, msg: str, debug: int = 0): if p in ("all", "signal"): ok = pushsignal(cfg.get("signal_cli", "/usr/local/bin/signal-cli"), cfg.get("signal_user", ""), cfg.get("signal_recipient", ""), msg, debug=debug) results["signal"] = ok - if debug: - print("push results:", results) + logger.debug("push results: %s", results) return results diff --git a/hbd/server.py b/hbd/server.py index ae379c4..435b435 100644 --- a/hbd/server.py +++ b/hbd/server.py @@ -1,38 +1,88 @@ """Server runtime: starts UDP listener, HTTP server and websocket stubs.""" import asyncio import logging +import atexit +import time +import signal +import sys +from . import __version__ from . import udp +from . import hbdclass +from . import ws as ws_mod logger = logging.getLogger(__name__) +msg_to_websockets = ws_mod.broadcast +logf = None +lastfm = ["", "", ""] +# shared runtime collections and helpers +msgs = [] + +def initlog(logfile): + try: + return open(logfile, "a+") + except Exception as e: + import sys + print("cannot open loffile %s, using STDERR: %s" % (logfile, e)) + return sys.stderr + +def log(host, m, service=None): + ts = time.time() + s = f"{time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(ts))} {host or ''} {m}" + msgs.append(s) + logger.info(s) + if logf: + try: + logf.write(s + "\n") + logf.flush() + except Exception as e: + logger.warning("failed to write to logfile: %s", e) + msg_to_websockets("message", s) + +def cleanup_function(config): + """This function will be executed upon program exit.""" + logger.info("Running cleanup function...") + import pickle + pickfile = config.get("pickfile", "hbd.pickle") + + pickf = open(pickfile, "wb") + pick = pickle.Pickler(pickf) + pick.dump(hbdclass.Host.hosts) + pick.dump(msgs) + pick.dump(lastfm) + pickf.close() + + logger.info("Cleanup complete.") + async def _run_async(config): + global msgs loop = asyncio.get_running_loop() + shutdown_event = asyncio.Event() - # shared runtime collections and helpers - msgs = [] + # Signal handlers for graceful shutdown + def signal_handler(signum, frame): + sig_name = signal.Signals(signum).name if hasattr(signal, 'Signals') else signum + logger.info(f"Received {sig_name}, initiating shutdown...") + loop.call_soon_threadsafe(shutdown_event.set) + + # Register signal handlers + loop.add_signal_handler(signal.SIGINT, signal_handler, signal.SIGINT, None) + loop.add_signal_handler(signal.SIGTERM, signal_handler, signal.SIGTERM, None) # prepare runtime dependencies import threading - import time - import hbdclass + from . import hbdclass from . import http as http_mod - from . import ws as ws_mod from . import dns as dns_mod from . import notify as notify_mod + from . import monitor as monitor_mod notify_mod.setup(config) - def log(host, m, service=None): - ts = time.time() - s = f"{time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(ts))} {host or ''} {m}" - msgs.append(s) - logger.info(s) - email = notify_mod.email pushmsg = notify_mod.pushmsg_from_config - msg_to_websockets = ws_mod.broadcast # UDP server endpoint (handler wired to handle_datagram with context) bind_addr = ("0.0.0.0", config.get("hb_port", 50003)) @@ -46,7 +96,6 @@ async def _run_async(config): email=email, pushmsg=pushmsg, msg_to_websockets=msg_to_websockets, - msgs=msgs, DEBUG=config.get("debug", 0), verbose=config.get("verbose", False), ) @@ -57,32 +106,37 @@ async def _run_async(config): local_addr=bind_addr, ) - # HTTP server (runs in its own thread) + # HTTP server (asyncio-based via aiohttp) try: - handler_cls = http_mod.make_handler_class( - config=config, - hbdclass=hbdclass, - msgs_getter=lambda: msgs, - log=log, - email=email, - pushmsg=pushmsg, - msg_to_websockets=msg_to_websockets, - tcss=None, - DEBUG=config.get("debug", 0), - verbose=config.get("verbose", False), - get_now=lambda: time.time(), - VER="", + http_task = asyncio.create_task( + http_mod.start( + host=config.get("hbd_host", ""), + port=config.get("hbd_port", 50004), + config=config, + hbdclass=hbdclass, + msgs_getter=lambda: msgs, + log=log, + email=email, + pushmsg=pushmsg, + msg_to_websockets=msg_to_websockets, + tcss=None, + DEBUG=config.get("debug", 0), + verbose=config.get("verbose", False), + get_now=lambda: time.time(), + VER="", + ) ) - serv = http_mod.HttpServer((config.get("hbd_host", ""), config.get("hbd_port", 50004)), handler_cls) - http_thread = threading.Thread(target=serv.serve_forever, daemon=True) - http_thread.start() logger.info("HTTP server started on %s:%s", config.get("hbd_host", ""), config.get("hbd_port", 50004)) except Exception as e: logger.exception("failed to start HTTP server: %s", e) - # start dns update thread - dns_mod.start_dns_thread(hbdclass, config, log=log, email=email) - logger.info("dns update thread started") + # start dns update worker (async) + dns_task = None + try: + dns_task = dns_mod.start_dns_worker(hbdclass, config, log=log, email=email, loop=loop) + logger.info("dns update worker started") + except Exception as e: + logger.exception("dns worker failed to start: %s", e) # Start the websocket servers as a background task try: @@ -101,28 +155,168 @@ async def _run_async(config): except Exception as e: logger.exception("websocket server failed to start: %s", e) + # Start the monitor thread as a background task try: - # run forever - await asyncio.Future() - finally: - transport.close() - try: - serv.shutdown() - except Exception: - pass - try: - ws_task.cancel() - except Exception: - pass + monitor_task = asyncio.create_task( + monitor_mod.start( + config=config, + hbdclass=hbdclass, + log=log, + email=email, + pushmsg=pushmsg, + msg_to_websockets=msg_to_websockets, + ) + ) + logger.info("Monitor task started") + except Exception as e: + logger.exception("monitor task failed to start: %s", e) + try: + # run forever until shutdown event is set + await shutdown_event.wait() + logger.info("Shutdown signal received, stopping services...") + except Exception as e: + logger.exception("Error in main loop: %s", e) + finally: + # Cancel all running tasks + logger.info("Cancelling tasks...") + try: + transport.close() + except Exception as e: + logger.warning("Error closing UDP transport: %s", e) + + tasks_to_cancel = [http_task, ws_task, monitor_task] + for task in tasks_to_cancel: + if task: + try: + task.cancel() + logger.debug("Cancelled task: %s", task) + except Exception as e: + logger.warning("Error cancelling task: %s", e) + + # Wait for tasks to finish cancellation with timeout + remaining_tasks = [t for t in tasks_to_cancel if t] + if remaining_tasks: + try: + await asyncio.wait_for(asyncio.gather(*remaining_tasks, return_exceptions=True), timeout=2.0) + except asyncio.TimeoutError: + logger.warning("Timeout waiting for tasks to cancel") + except Exception as e: + logger.debug("Exception during task cancellation: %s", e) + + # Signal DNS worker to exit and await it + try: + if 'dns_task' in locals() and dns_task: + try: + hbdclass.Host.dnsQ.put(None) + except Exception: + pass + try: + await asyncio.wait_for(dns_task, timeout=2.0) + logger.info("DNS worker finished") + except asyncio.TimeoutError: + logger.warning("Timeout waiting for DNS worker to finish") + dns_task.cancel() + except asyncio.CancelledError: + logger.info("DNS worker was cancelled") + except Exception as e: + logger.warning("Error awaiting DNS worker: %s", e) + finally: + # Clear queue bridge to release any held references + hbdclass.Host.dnsQ = None + except Exception as e: + logger.warning("Error stopping DNS worker: %s", e) + + logger.info("All tasks cancelled") + + +def load_pickled_hosts(config, hbdclass): + """Load pickled hosts from file, if available.""" + global lastfm, msgs + import os + import pickle + + pickfile = config.get("pickfile", "hbd.pickle") + dyndnshosts = config.get("dyndnshosts", []) + watchhosts = config.get("watchhosts", []) + drophosts = config.get("drophosts", []) + if 1 and os.path.exists(pickfile): + if config.get("verbose", False): + logger.info("opening pickls %s", pickfile) + pickf = open(pickfile, "rb") + pick = pickle.Unpickler(pickf) + try: + hbdclass.Host.hosts = pick.load() + msgs = pick.load() + try: + lastfm = pick.load() + except: + lastfm = ["", "", ""] + pickf.close() + except Exception as e: + print(("load pickled failed: %s" % e)) + os.unlink(pickfile) + hbdclass.Connection.htab = {} + for h in list(hbdclass.Host.hosts.keys()): + hbdclass.Host.hosts[h].dyn = h in dyndnshosts + hbdclass.Host.hosts[h].watched = h in watchhosts + hbdclass.Host.hosts[h].fixup() + for h in drophosts: + if h in hbdclass.Host.hosts: + del hbdclass.Host.hosts[h] + if config.get("verbose", False): + logger.info("%s pickled hosts loaded", len(hbdclass.Host.hosts)) + else: + if config.get("verbose", False): + logger.info("no pickled data") def run(config): """Start the hbd service (blocking). - This is a thin wrapper around asyncio.run to host the async services. + Manually manages the event loop to ensure clean shutdown. """ + global logf + import os + import threading + import time as time_module + logging.basicConfig(level=logging.DEBUG if config.get("debug", 0) > 0 else logging.INFO) + load_pickled_hosts(config, hbdclass) + + logf = initlog(logfile=config.get("logfile", "messages.log")) + log(None, f"hbd version {__version__} starting up") + + # Create and set the event loop manually + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + try: - asyncio.run(_run_async(config)) + loop.run_until_complete(_run_async(config)) except KeyboardInterrupt: - logger.info("Shutting down (KeyboardInterrupt)") + logger.info("Received KeyboardInterrupt, shutting down...") + except Exception as e: + logger.exception("Unhandled exception in main: %s", e) + finally: + cleanup_function(config) + logger.info("hbd shutdown complete") + if logf and logf != sys.stderr: + try: + logf.close() + except Exception: + pass + # Explicitly close the loop + try: + # Cancel all remaining tasks + pending = asyncio.all_tasks(loop) + for task in pending: + task.cancel() + # Run one more cycle to process cancellations + if pending: + loop.run_until_complete(asyncio.gather(*pending, return_exceptions=True)) + except Exception: + pass + finally: + loop.close() + + # Exit + os._exit(0) diff --git a/hbd/static/images/favicon.ico b/hbd/static/images/favicon.ico new file mode 100644 index 0000000..0eefe81 Binary files /dev/null and b/hbd/static/images/favicon.ico differ diff --git a/hbd/static/style.css b/hbd/static/style.css new file mode 100644 index 0000000..dbdab5b --- /dev/null +++ b/hbd/static/style.css @@ -0,0 +1,142 @@ + +/* http://www.designcouch.com/home/why/2014/04/23/pure-css-drawer-menu/ */ +* { + box-sizing: border-box; + /* adds animation for all transitions */ + + transition: .25s ease-in-out; + /* margin: 0; + padding: 0; */ + /* text-size-adjust: none; */ + } + /* Makes sure that everything is 100% height */ + + html, + body { + height: 100%; + overflow: hidden; + color:#303030; + background:#fafafa top left repeat-y; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", "Helvetica", "Arial", sans-serif; + font-size:100%; + margin: 0; + } + + #drawer-toggle { + position: absolute; + opacity: 0; + } + + + #drawer ul a { + display: block; + padding: 10px; + color: #c7c7c7; + text-decoration: none; + } + + #drawer-toggle-label { + user-select: none; + left: 0px; + height: 50px; + width: 50px; + display: block; + position: fixed; + color: rgb(242, 242, 242); + background: rgba(255, 255, 255, .0); + z-index: 1; + } + + /* adds our "hamburger" menu icon */ + + #drawer-toggle-label:before { + content: ''; + display: block; + position: absolute; + height: 2px; + width: 24px; + background: #8d8d8d; + left: 13px; + top: 18px; + box-shadow: 0 6px 0 #8d8d8d, 0 12px 0 #8d8d8d; + } + + header { + width: 100%; + position: fixed; + left: 0px; + background: #efefef; + padding: 10px 10px 10px 50px; + font-size: 30px; + line-height: 30px; + z-index: 0; + } + + + /* drawer menu pane - note the 0px width */ + #drawer { + position: fixed; + top: 0; + width: 150px; + left: -150px; + height: 100%; + background: #2f2f2f; + overflow-x: hidden; + overflow-y: scroll; + padding: 0px; + } + + @media all and (min-resolution: 150dpi) { + header { + font-size: 30px; +/* line-height: 45px; */ + } + #drawer { + font-size: 120%; + } +/* body { + background-color: lightyellow; + } */ + } + + /* actual page content pane */ + + #content { + margin-left: 0px; + margin-top: 30px; + /* width: 100%; */ + height: calc(100% - 50px); + overflow-x: hidden; + overflow-y: scroll; + padding: 20px; + flex: auto; + } + + /* checked styles (menu open state) */ + + #drawer-toggle:checked ~ #drawer-toggle-label { + height: 100%; + width: calc(100% - 150px); + color: rgb(242, 242, 242); + background: rgba(255, 255, 255, .8); + } + + #drawer-toggle:checked ~ #drawer-toggle-label, + #drawer-toggle:checked ~ header { + left: 150px; + } + + #drawer-toggle:checked ~ #drawer { + left: 0px; + } + + #drawer-toggle:checked ~ #content { + margin-left: 150px; + } + + + #copyright { + font-size: 9px; + float: left; + } + \ No newline at end of file diff --git a/hbd/udp.py b/hbd/udp.py index 8004ac3..509c422 100644 --- a/hbd/udp.py +++ b/hbd/udp.py @@ -71,7 +71,6 @@ def handle_datagram(msg: dict, addr, transport, ctx: dict): - email: callable(subject, message) - pushmsg: callable(message) - msg_to_websockets: callable(typ, data) - - msgs: list for storing message strings - DEBUG, verbose """ if not msg: @@ -83,7 +82,6 @@ def handle_datagram(msg: dict, addr, transport, ctx: dict): email = ctx.get("email") pushmsg = ctx.get("pushmsg") msg_to_websockets = ctx.get("msg_to_websockets") - msgs = ctx.get("msgs") DEBUG = ctx.get("DEBUG", 0) verbose = ctx.get("verbose", False) diff --git a/hbd/ws.py b/hbd/ws.py index 6f85cdb..4aaf597 100644 --- a/hbd/ws.py +++ b/hbd/ws.py @@ -19,10 +19,14 @@ _get_msgs: Optional[Callable[[], Iterable]] = None _verbose = False -async def _handler(websocket, path): +async def _handler(websocket, path=None): + # Some versions of the websockets library call handler(connection) only; + # accept optional path and fall back to websocket.path when missing. global _connections _connections.add(websocket) - remote_address = websocket.remote_address + remote_address = getattr(websocket, "remote_address", None) + if path is None: + path = getattr(websocket, "path", None) if _verbose: logger.info("DBG ws_serve: %s: %s", remote_address, path) try: @@ -72,23 +76,36 @@ async def start(host: str, ws_port: int, wss_port: Optional[int] = None, ssl_con servers = [] # plain WebSocket - ws_server = websockets.serve(_handler, host, ws_port, subprotocols=["hbd"]) + ws_server = websockets.serve(_handler, host, ws_port) #, subprotocols=["hbd"]) + websockets_logger = logging.getLogger("websockets.server") + websockets_logger.setLevel(logging.INFO) servers.append(ws_server) # secure WebSocket (optional) if wss_port and ssl_context: - wss_server = websockets.serve(_handler, host, wss_port, ssl=ssl_context, subprotocols=["hbd"]) + wss_server = websockets.serve(_handler, host, wss_port, ssl=ssl_context) #, subprotocols=["hbd"]) servers.append(wss_server) # await starting of all servers - for srv in servers: - await srv + try: + for srv in servers: + await srv - if _verbose: - logger.info("WebSocket server started on port %s (wss %s)", ws_port, wss_port) + if _verbose: + logger.info("WebSocket server started on port %s (wss %s)", ws_port, wss_port) - # block forever (until loop is stopped or cancelled) - await asyncio.Future() + # block forever (until loop is stopped or cancelled) + await asyncio.Future() + except asyncio.CancelledError: + logger.info("WebSocket server shutting down...") + # Close all active connections + for conn in list(_connections): + try: + await conn.close() + except Exception: + pass + _connections.clear() + raise def broadcast(typ: str, data) -> bool: @@ -98,12 +115,13 @@ def broadcast(typ: str, data) -> bool: connected websockets. Returns False if server was not running. """ global _loop + if not _loop: return False jmsg = json.dumps({"type": typ, "data": data}) to_close = [] for ws in list(_connections): - if ws.closed: + if ws.state != websockets.protocol.State.OPEN: to_close.append(ws) continue try: diff --git a/heartbeat.egg-info/PKG-INFO b/heartbeat.egg-info/PKG-INFO index 687dd07..84bf9a4 100644 --- a/heartbeat.egg-info/PKG-INFO +++ b/heartbeat.egg-info/PKG-INFO @@ -10,7 +10,8 @@ Description-Content-Type: text/markdown Requires-Dist: websockets>=13.2 Requires-Dist: mattermostdriver>=7.3.0 Requires-Dist: PyYAML>=6.0 -Requires-Dist: fastapi>=0.95.0 +Requires-Dist: aiohttp>=3.8 +Requires-Dist: Jinja2>=3.1.0 Provides-Extra: dev Requires-Dist: pytest>=7.0; extra == "dev" Requires-Dist: pytest-cov>=4.0; extra == "dev" diff --git a/heartbeat.egg-info/SOURCES.txt b/heartbeat.egg-info/SOURCES.txt index 3824948..87ff4b2 100644 --- a/heartbeat.egg-info/SOURCES.txt +++ b/heartbeat.egg-info/SOURCES.txt @@ -4,7 +4,9 @@ hbd/__init__.py hbd/cli.py hbd/config.py hbd/dns.py +hbd/hbdclass.py hbd/http.py +hbd/monitor.py hbd/notify.py hbd/proto.py hbd/server.py diff --git a/heartbeat.egg-info/requires.txt b/heartbeat.egg-info/requires.txt index 67c791e..dbed87f 100644 --- a/heartbeat.egg-info/requires.txt +++ b/heartbeat.egg-info/requires.txt @@ -1,7 +1,8 @@ websockets>=13.2 mattermostdriver>=7.3.0 PyYAML>=6.0 -fastapi>=0.95.0 +aiohttp>=3.8 +Jinja2>=3.1.0 [dev] pytest>=7.0 diff --git a/install.sh b/install.sh deleted file mode 100755 index 9c9104e..0000000 --- a/install.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/bin/sh - - -mkdir $HOME/bin 2>/dev/null -cp -rp hbd hbdclass.py hbc daemon lockfile $HOME/bin/ - -echo "use: $HOME/bin/hbc -d hbd.wrede.ca to start a heartbeat" diff --git a/lockfile/__init__.py b/lockfile/__init__.py deleted file mode 100644 index 889933c..0000000 --- a/lockfile/__init__.py +++ /dev/null @@ -1,377 +0,0 @@ -# -*- coding: utf-8 -*- - -""" -lockfile.py - Platform-independent advisory file locks. - -Requires Python 2.5 unless you apply 2.4.diff -Locking is done on a per-thread basis instead of a per-process basis. - -Usage: - ->>> lock = LockFile('somefile') ->>> try: -... lock.acquire() -... except AlreadyLocked: -... print 'somefile', 'is locked already.' -... except LockFailed: -... print 'somefile', 'can\\'t be locked.' -... else: -... print 'got lock' -got lock ->>> print lock.is_locked() -True ->>> lock.release() - ->>> lock = LockFile('somefile') ->>> print lock.is_locked() -False ->>> with lock: -... print lock.is_locked() -True ->>> print lock.is_locked() -False - ->>> lock = LockFile('somefile') ->>> # It is okay to lock twice from the same thread... ->>> with lock: -... lock.acquire() -... ->>> # Though no counter is kept, so you can't unlock multiple times... ->>> print lock.is_locked() -False - -Exceptions: - - Error - base class for other exceptions - LockError - base class for all locking exceptions - AlreadyLocked - Another thread or process already holds the lock - LockFailed - Lock failed for some other reason - UnlockError - base class for all unlocking exceptions - AlreadyUnlocked - File was not locked. - NotMyLock - File was locked but not by the current thread/process -""" - -from __future__ import absolute_import - -import functools -import os -import socket -import threading -import warnings - -# Work with PEP8 and non-PEP8 versions of threading module. -if not hasattr(threading, "current_thread"): - threading.current_thread = threading.currentThread -if not hasattr(threading.Thread, "get_name"): - threading.Thread.get_name = threading.Thread.getName - -__all__ = [ - "Error", - "LockError", - "LockTimeout", - "AlreadyLocked", - "LockFailed", - "UnlockError", - "NotLocked", - "NotMyLock", - "LinkFileLock", - "MkdirFileLock", - "SQLiteFileLock", - "LockBase", - "locked", -] - - -class Error(Exception): - """ - Base class for other exceptions. - - >>> try: - ... raise Error - ... except Exception: - ... pass - """ - - pass - - -class LockError(Error): - """ - Base class for error arising from attempts to acquire the lock. - - >>> try: - ... raise LockError - ... except Error: - ... pass - """ - - pass - - -class LockTimeout(LockError): - """Raised when lock creation fails within a user-defined period of time. - - >>> try: - ... raise LockTimeout - ... except LockError: - ... pass - """ - - pass - - -class AlreadyLocked(LockError): - """Some other thread/process is locking the file. - - >>> try: - ... raise AlreadyLocked - ... except LockError: - ... pass - """ - - pass - - -class LockFailed(LockError): - """Lock file creation failed for some other reason. - - >>> try: - ... raise LockFailed - ... except LockError: - ... pass - """ - - pass - - -class UnlockError(Error): - """ - Base class for errors arising from attempts to release the lock. - - >>> try: - ... raise UnlockError - ... except Error: - ... pass - """ - - pass - - -class NotLocked(UnlockError): - """Raised when an attempt is made to unlock an unlocked file. - - >>> try: - ... raise NotLocked - ... except UnlockError: - ... pass - """ - - pass - - -class NotMyLock(UnlockError): - """Raised when an attempt is made to unlock a file someone else locked. - - >>> try: - ... raise NotMyLock - ... except UnlockError: - ... pass - """ - - pass - - -class _SharedBase(object): - def __init__(self, path): - self.path = path - - def acquire(self, timeout=None): - """ - Acquire the lock. - - * If timeout is omitted (or None), wait forever trying to lock the - file. - - * If timeout > 0, try to acquire the lock for that many seconds. If - the lock period expires and the file is still locked, raise - LockTimeout. - - * If timeout <= 0, raise AlreadyLocked immediately if the file is - already locked. - """ - raise NotImplemented("implement in subclass") - - def release(self): - """ - Release the lock. - - If the file is not locked, raise NotLocked. - """ - raise NotImplemented("implement in subclass") - - def __enter__(self): - """ - Context manager support. - """ - self.acquire() - return self - - def __exit__(self, *_exc): - """ - Context manager support. - """ - self.release() - - def __repr__(self): - return "<%s: %r>" % (self.__class__.__name__, self.path) - - -class LockBase(_SharedBase): - """Base class for platform-specific lock classes.""" - - def __init__(self, path, threaded=True, timeout=None): - """ - >>> lock = LockBase('somefile') - >>> lock = LockBase('somefile', threaded=False) - """ - super(LockBase, self).__init__(path) - self.lock_file = os.path.abspath(path) + ".lock" - self.hostname = socket.gethostname() - self.pid = os.getpid() - if threaded: - t = threading.current_thread() - # Thread objects in Python 2.4 and earlier do not have ident - # attrs. Worm around that. - ident = getattr(t, "ident", hash(t)) - self.tname = "-%x" % (ident & 0xFFFFFFFF) - else: - self.tname = "" - dirname = os.path.dirname(self.lock_file) - - # unique name is mostly about the current process, but must - # also contain the path -- otherwise, two adjacent locked - # files conflict (one file gets locked, creating lock-file and - # unique file, the other one gets locked, creating lock-file - # and overwriting the already existing lock-file, then one - # gets unlocked, deleting both lock-file and unique file, - # finally the last lock errors out upon releasing. - self.unique_name = os.path.join( - dirname, - "%s%s.%s%s" % (self.hostname, self.tname, self.pid, hash(self.path)), - ) - self.timeout = timeout - - def is_locked(self): - """ - Tell whether or not the file is locked. - """ - raise NotImplemented("implement in subclass") - - def i_am_locking(self): - """ - Return True if this object is locking the file. - """ - raise NotImplemented("implement in subclass") - - def break_lock(self): - """ - Remove a lock. Useful if a locking thread failed to unlock. - """ - raise NotImplemented("implement in subclass") - - def __repr__(self): - return "<%s: %r -- %r>" % (self.__class__.__name__, self.unique_name, self.path) - - -def _fl_helper(cls, mod, *args, **kwds): - warnings.warn( - "Import from %s module instead of lockfile package" % mod, - DeprecationWarning, - stacklevel=2, - ) - # This is a bit funky, but it's only for awhile. The way the unit tests - # are constructed this function winds up as an unbound method, so it - # actually takes three args, not two. We want to toss out self. - if not isinstance(args[0], str): - # We are testing, avoid the first arg - args = args[1:] - if len(args) == 1 and not kwds: - kwds["threaded"] = True - return cls(*args, **kwds) - - -def LinkFileLock(*args, **kwds): - """Factory function provided for backwards compatibility. - - Do not use in new code. Instead, import LinkLockFile from the - lockfile.linklockfile module. - """ - from . import linklockfile - - return _fl_helper(linklockfile.LinkLockFile, "lockfile.linklockfile", *args, **kwds) - - -def MkdirFileLock(*args, **kwds): - """Factory function provided for backwards compatibility. - - Do not use in new code. Instead, import MkdirLockFile from the - lockfile.mkdirlockfile module. - """ - from . import mkdirlockfile - - return _fl_helper( - mkdirlockfile.MkdirLockFile, "lockfile.mkdirlockfile", *args, **kwds - ) - - -def SQLiteFileLock(*args, **kwds): - """Factory function provided for backwards compatibility. - - Do not use in new code. Instead, import SQLiteLockFile from the - lockfile.mkdirlockfile module. - """ - from . import sqlitelockfile - - return _fl_helper( - sqlitelockfile.SQLiteLockFile, "lockfile.sqlitelockfile", *args, **kwds - ) - - -def locked(path, timeout=None): - """Decorator which enables locks for decorated function. - - Arguments: - - path: path for lockfile. - - timeout (optional): Timeout for acquiring lock. - - Usage: - @locked('/var/run/myname', timeout=0) - def myname(...): - ... - """ - - def decor(func): - @functools.wraps(func) - def wrapper(*args, **kwargs): - lock = FileLock(path, timeout=timeout) - lock.acquire() - try: - return func(*args, **kwargs) - finally: - lock.release() - - return wrapper - - return decor - - -if hasattr(os, "link"): - from . import linklockfile as _llf - - LockFile = _llf.LinkLockFile -else: - from . import mkdirlockfile as _mlf - - LockFile = _mlf.MkdirLockFile - -FileLock = LockFile diff --git a/lockfile/linklockfile.py b/lockfile/linklockfile.py deleted file mode 100644 index d48c514..0000000 --- a/lockfile/linklockfile.py +++ /dev/null @@ -1,73 +0,0 @@ -from __future__ import absolute_import - -import time -import os - -from . import LockBase, LockFailed, NotLocked, NotMyLock, LockTimeout, AlreadyLocked - - -class LinkLockFile(LockBase): - """Lock access to a file using atomic property of link(2). - - >>> lock = LinkLockFile('somefile') - >>> lock = LinkLockFile('somefile', threaded=False) - """ - - def acquire(self, timeout=None): - try: - open(self.unique_name, "wb").close() - except IOError: - raise LockFailed("failed to create %s" % self.unique_name) - - timeout = timeout if timeout is not None else self.timeout - end_time = time.time() - if timeout is not None and timeout > 0: - end_time += timeout - - while True: - # Try and create a hard link to it. - try: - os.link(self.unique_name, self.lock_file) - except OSError: - # Link creation failed. Maybe we've double-locked? - nlinks = os.stat(self.unique_name).st_nlink - if nlinks == 2: - # The original link plus the one I created == 2. We're - # good to go. - return - else: - # Otherwise the lock creation failed. - if timeout is not None and time.time() > end_time: - os.unlink(self.unique_name) - if timeout > 0: - raise LockTimeout( - "Timeout waiting to acquire" " lock for %s" % self.path - ) - else: - raise AlreadyLocked("%s is already locked" % self.path) - time.sleep(timeout is not None and timeout / 10 or 0.1) - else: - # Link creation succeeded. We're good to go. - return - - def release(self): - if not self.is_locked(): - raise NotLocked("%s is not locked" % self.path) - elif not os.path.exists(self.unique_name): - raise NotMyLock("%s is locked, but not by me" % self.path) - os.unlink(self.unique_name) - os.unlink(self.lock_file) - - def is_locked(self): - return os.path.exists(self.lock_file) - - def i_am_locking(self): - return ( - self.is_locked() - and os.path.exists(self.unique_name) - and os.stat(self.unique_name).st_nlink == 2 - ) - - def break_lock(self): - if os.path.exists(self.lock_file): - os.unlink(self.lock_file) diff --git a/lockfile/mkdirlockfile.py b/lockfile/mkdirlockfile.py deleted file mode 100644 index b12eac1..0000000 --- a/lockfile/mkdirlockfile.py +++ /dev/null @@ -1,81 +0,0 @@ -from __future__ import absolute_import, division - -import time -import os -import sys -import errno - -from . import LockBase, LockFailed, NotLocked, NotMyLock, LockTimeout, AlreadyLocked - - -class MkdirLockFile(LockBase): - """Lock file by creating a directory.""" - - def __init__(self, path, threaded=True, timeout=None): - """ - >>> lock = MkdirLockFile('somefile') - >>> lock = MkdirLockFile('somefile', threaded=False) - """ - LockBase.__init__(self, path, threaded, timeout) - # Lock file itself is a directory. Place the unique file name into - # it. - self.unique_name = os.path.join( - self.lock_file, "%s.%s%s" % (self.hostname, self.tname, self.pid) - ) - - def acquire(self, timeout=None): - timeout = timeout if timeout is not None else self.timeout - end_time = time.time() - if timeout is not None and timeout > 0: - end_time += timeout - - if timeout is None: - wait = 0.1 - else: - wait = max(0, timeout / 10) - - while True: - try: - os.mkdir(self.lock_file) - except OSError: - err = sys.exc_info()[1] - if err.errno == errno.EEXIST: - # Already locked. - if os.path.exists(self.unique_name): - # Already locked by me. - return - if timeout is not None and time.time() > end_time: - if timeout > 0: - raise LockTimeout( - "Timeout waiting to acquire" " lock for %s" % self.path - ) - else: - # Someone else has the lock. - raise AlreadyLocked("%s is already locked" % self.path) - time.sleep(wait) - else: - # Couldn't create the lock for some other reason - raise LockFailed("failed to create %s" % self.lock_file) - else: - open(self.unique_name, "wb").close() - return - - def release(self): - if not self.is_locked(): - raise NotLocked("%s is not locked" % self.path) - elif not os.path.exists(self.unique_name): - raise NotMyLock("%s is locked, but not by me" % self.path) - os.unlink(self.unique_name) - os.rmdir(self.lock_file) - - def is_locked(self): - return os.path.exists(self.lock_file) - - def i_am_locking(self): - return self.is_locked() and os.path.exists(self.unique_name) - - def break_lock(self): - if os.path.exists(self.lock_file): - for name in os.listdir(self.lock_file): - os.unlink(os.path.join(self.lock_file, name)) - os.rmdir(self.lock_file) diff --git a/lockfile/pidlockfile.py b/lockfile/pidlockfile.py deleted file mode 100644 index 14c4859..0000000 --- a/lockfile/pidlockfile.py +++ /dev/null @@ -1,188 +0,0 @@ -# -*- coding: utf-8 -*- - -# pidlockfile.py -# -# Copyright © 2008–2009 Ben Finney -# -# This is free software: you may copy, modify, and/or distribute this work -# under the terms of the Python Software Foundation License, version 2 or -# later as published by the Python Software Foundation. -# No warranty expressed or implied. See the file LICENSE.PSF-2 for details. - -""" Lockfile behaviour implemented via Unix PID files. - """ - -from __future__ import absolute_import - -import errno -import os -import time - -from . import LockBase, AlreadyLocked, LockFailed, NotLocked, NotMyLock, LockTimeout - - -class PIDLockFile(LockBase): - """ Lockfile implemented as a Unix PID file. - - The lock file is a normal file named by the attribute `path`. - A lock's PID file contains a single line of text, containing - the process ID (PID) of the process that acquired the lock. - - >>> lock = PIDLockFile('somefile') - >>> lock = PIDLockFile('somefile') - """ - - def __init__(self, path, threaded=False, timeout=None): - # pid lockfiles don't support threaded operation, so always force - # False as the threaded arg. - LockBase.__init__(self, path, False, timeout) - self.unique_name = self.path - - def read_pid(self): - """ Get the PID from the lock file. - """ - return read_pid_from_pidfile(self.path) - - def is_locked(self): - """ Test if the lock is currently held. - - The lock is held if the PID file for this lock exists. - - """ - return os.path.exists(self.path) - - def i_am_locking(self): - """ Test if the lock is held by the current process. - - Returns ``True`` if the current process ID matches the - number stored in the PID file. - """ - return self.is_locked() and os.getpid() == self.read_pid() - - def acquire(self, timeout=None): - """ Acquire the lock. - - Creates the PID file for this lock, or raises an error if - the lock could not be acquired. - """ - - timeout = timeout if timeout is not None else self.timeout - end_time = time.time() - if timeout is not None and timeout > 0: - end_time += timeout - - while True: - try: - write_pid_to_pidfile(self.path) - except OSError as exc: - if exc.errno == errno.EEXIST: - # The lock creation failed. Maybe sleep a bit. - if time.time() > end_time: - if timeout is not None and timeout > 0: - raise LockTimeout( - "Timeout waiting to acquire" " lock for %s" % self.path - ) - else: - raise AlreadyLocked("%s is already locked" % self.path) - time.sleep(timeout is not None and timeout / 10 or 0.1) - else: - raise LockFailed("failed to create %s" % self.path) - else: - return - - def release(self): - """ Release the lock. - - Removes the PID file to release the lock, or raises an - error if the current process does not hold the lock. - - """ - if not self.is_locked(): - raise NotLocked("%s is not locked" % self.path) - if not self.i_am_locking(): - raise NotMyLock("%s is locked, but not by me" % self.path) - remove_existing_pidfile(self.path) - - def break_lock(self): - """ Break an existing lock. - - Removes the PID file if it already exists, otherwise does - nothing. - - """ - remove_existing_pidfile(self.path) - - -def read_pid_from_pidfile(pidfile_path): - """ Read the PID recorded in the named PID file. - - Read and return the numeric PID recorded as text in the named - PID file. If the PID file cannot be read, or if the content is - not a valid PID, return ``None``. - - """ - pid = None - try: - pidfile = open(pidfile_path, "r") - except IOError: - pass - else: - # According to the FHS 2.3 section on PID files in /var/run: - # - # The file must consist of the process identifier in - # ASCII-encoded decimal, followed by a newline character. - # - # Programs that read PID files should be somewhat flexible - # in what they accept; i.e., they should ignore extra - # whitespace, leading zeroes, absence of the trailing - # newline, or additional lines in the PID file. - - line = pidfile.readline().strip() - try: - pid = int(line) - except ValueError: - pass - pidfile.close() - - return pid - - -def write_pid_to_pidfile(pidfile_path): - """ Write the PID in the named PID file. - - Get the numeric process ID (“PID”) of the current process - and write it to the named file as a line of text. - - """ - open_flags = os.O_CREAT | os.O_EXCL | os.O_WRONLY - open_mode = 0o644 - pidfile_fd = os.open(pidfile_path, open_flags, open_mode) - pidfile = os.fdopen(pidfile_fd, "w") - - # According to the FHS 2.3 section on PID files in /var/run: - # - # The file must consist of the process identifier in - # ASCII-encoded decimal, followed by a newline character. For - # example, if crond was process number 25, /var/run/crond.pid - # would contain three characters: two, five, and newline. - - pid = os.getpid() - pidfile.write("%s\n" % pid) - pidfile.close() - - -def remove_existing_pidfile(pidfile_path): - """ Remove the named PID file if it exists. - - Removing a PID file that doesn't already exist puts us in the - desired state, so we ignore the condition if the file does not - exist. - - """ - try: - os.remove(pidfile_path) - except OSError as exc: - if exc.errno == errno.ENOENT: - pass - else: - raise diff --git a/lockfile/sqlitelockfile.py b/lockfile/sqlitelockfile.py deleted file mode 100644 index 9fa2ab2..0000000 --- a/lockfile/sqlitelockfile.py +++ /dev/null @@ -1,162 +0,0 @@ -from __future__ import absolute_import, division - -import time -import os - -try: - unicode -except NameError: - unicode = str - -from . import LockBase, NotLocked, NotMyLock, LockTimeout, AlreadyLocked - - -class SQLiteLockFile(LockBase): - "Demonstrate SQL-based locking." - - testdb = None - - def __init__(self, path, threaded=True, timeout=None): - """ - >>> lock = SQLiteLockFile('somefile') - >>> lock = SQLiteLockFile('somefile', threaded=False) - """ - LockBase.__init__(self, path, threaded, timeout) - self.lock_file = unicode(self.lock_file) - self.unique_name = unicode(self.unique_name) - - if SQLiteLockFile.testdb is None: - import tempfile - - _fd, testdb = tempfile.mkstemp() - os.close(_fd) - os.unlink(testdb) - del _fd, tempfile - SQLiteLockFile.testdb = testdb - - import sqlite3 - - self.connection = sqlite3.connect(SQLiteLockFile.testdb) - - c = self.connection.cursor() - try: - c.execute( - "create table locks" - "(" - " lock_file varchar(32)," - " unique_name varchar(32)" - ")" - ) - except sqlite3.OperationalError: - pass - else: - self.connection.commit() - import atexit - - atexit.register(os.unlink, SQLiteLockFile.testdb) - - def acquire(self, timeout=None): - timeout = timeout if timeout is not None else self.timeout - end_time = time.time() - if timeout is not None and timeout > 0: - end_time += timeout - - if timeout is None: - wait = 0.1 - elif timeout <= 0: - wait = 0 - else: - wait = timeout / 10 - - cursor = self.connection.cursor() - - while True: - if not self.is_locked(): - # Not locked. Try to lock it. - cursor.execute( - "insert into locks" - " (lock_file, unique_name)" - " values" - " (?, ?)", - (self.lock_file, self.unique_name), - ) - self.connection.commit() - - # Check to see if we are the only lock holder. - cursor.execute( - "select * from locks" " where unique_name = ?", (self.unique_name,) - ) - rows = cursor.fetchall() - if len(rows) > 1: - # Nope. Someone else got there. Remove our lock. - cursor.execute( - "delete from locks" " where unique_name = ?", - (self.unique_name,), - ) - self.connection.commit() - else: - # Yup. We're done, so go home. - return - else: - # Check to see if we are the only lock holder. - cursor.execute( - "select * from locks" " where unique_name = ?", (self.unique_name,) - ) - rows = cursor.fetchall() - if len(rows) == 1: - # We're the locker, so go home. - return - - # Maybe we should wait a bit longer. - if timeout is not None and time.time() > end_time: - if timeout > 0: - # No more waiting. - raise LockTimeout( - "Timeout waiting to acquire" " lock for %s" % self.path - ) - else: - # Someone else has the lock and we are impatient.. - raise AlreadyLocked("%s is already locked" % self.path) - - # Well, okay. We'll give it a bit longer. - time.sleep(wait) - - def release(self): - if not self.is_locked(): - raise NotLocked("%s is not locked" % self.path) - if not self.i_am_locking(): - raise NotMyLock( - "%s is locked, but not by me (by %s)" - % (self.unique_name, self._who_is_locking()) - ) - cursor = self.connection.cursor() - cursor.execute( - "delete from locks" " where unique_name = ?", (self.unique_name,) - ) - self.connection.commit() - - def _who_is_locking(self): - cursor = self.connection.cursor() - cursor.execute( - "select unique_name from locks" " where lock_file = ?", (self.lock_file,) - ) - return cursor.fetchone()[0] - - def is_locked(self): - cursor = self.connection.cursor() - cursor.execute("select * from locks" " where lock_file = ?", (self.lock_file,)) - rows = cursor.fetchall() - return not not rows - - def i_am_locking(self): - cursor = self.connection.cursor() - cursor.execute( - "select * from locks" " where lock_file = ?" " and unique_name = ?", - (self.lock_file, self.unique_name), - ) - return not not cursor.fetchall() - - def break_lock(self): - cursor = self.connection.cursor() - cursor.execute("delete from locks" " where lock_file = ?", (self.lock_file,)) - self.connection.commit() diff --git a/lockfile/symlinklockfile.py b/lockfile/symlinklockfile.py deleted file mode 100644 index 7bb3c77..0000000 --- a/lockfile/symlinklockfile.py +++ /dev/null @@ -1,70 +0,0 @@ -from __future__ import absolute_import - -import os -import time - -from . import LockBase, NotLocked, NotMyLock, LockTimeout, AlreadyLocked - - -class SymlinkLockFile(LockBase): - """Lock access to a file using symlink(2).""" - - def __init__(self, path, threaded=True, timeout=None): - # super(SymlinkLockFile).__init(...) - LockBase.__init__(self, path, threaded, timeout) - # split it back! - self.unique_name = os.path.split(self.unique_name)[1] - - def acquire(self, timeout=None): - # Hopefully unnecessary for symlink. - # try: - # open(self.unique_name, "wb").close() - # except IOError: - # raise LockFailed("failed to create %s" % self.unique_name) - timeout = timeout if timeout is not None else self.timeout - end_time = time.time() - if timeout is not None and timeout > 0: - end_time += timeout - - while True: - # Try and create a symbolic link to it. - try: - os.symlink(self.unique_name, self.lock_file) - except OSError: - # Link creation failed. Maybe we've double-locked? - if self.i_am_locking(): - # Linked to out unique name. Proceed. - return - else: - # Otherwise the lock creation failed. - if timeout is not None and time.time() > end_time: - if timeout > 0: - raise LockTimeout( - "Timeout waiting to acquire" " lock for %s" % self.path - ) - else: - raise AlreadyLocked("%s is already locked" % self.path) - time.sleep(timeout / 10 if timeout is not None else 0.1) - else: - # Link creation succeeded. We're good to go. - return - - def release(self): - if not self.is_locked(): - raise NotLocked("%s is not locked" % self.path) - elif not self.i_am_locking(): - raise NotMyLock("%s is locked, but not by me" % self.path) - os.unlink(self.lock_file) - - def is_locked(self): - return os.path.islink(self.lock_file) - - def i_am_locking(self): - return ( - os.path.islink(self.lock_file) - and os.readlink(self.lock_file) == self.unique_name - ) - - def break_lock(self): - if os.path.islink(self.lock_file): # exists && link - os.unlink(self.lock_file) diff --git a/mlog.cgi b/mlog.cgi deleted file mode 100755 index 63127ca..0000000 --- a/mlog.cgi +++ /dev/null @@ -1,58 +0,0 @@ -#!/usr/bin/env python - -import string, os -VER = "mlog 1.00" - -hosts = ["weekend", "wingbat"] - -URI = "/~andreas/private/mlog.cgi?" - -print "Content-type: text/html" -print "max-age: 0" -print "expires: 0" -print "pragma: no-cache" -print "" -print "" -print "" -print "" -print "Motion Log" -print "" -print "

Motion Log

" -print "" -print '' -print "
"
-
-
-def hstlines(host):
-	cmd = "/sbin/ping -c 1 -w 2 %s 2>&1 >/dev/null" % host
-	r = os.system(cmd)
-	if r == 0:
-		p = os.popen("rsh %s tail -200 .heyu/logs/motion.log | grep -v again | tail -32" % host, "r")
-		l = p.readlines()
-	else:
-		l = ["Host %s is unreachable      " % host]
-	return l
-
-rep = []
-for host in hosts:
-	rep.append(hstlines(host))
-
-print ''
-i = 0
-print "" % string.join(hosts, '" % string.join(line, '      
%s
') -while 1: - line = [] - f = 0 - for h in rep: - try: - line.append(h[i][:-1]) - f = 1 - except: - line.append("") - if f == 0: - break - print "
%s
') - i += 1 -print '
' - -execfile("/home/andreas/cgi-bin/trailer.py") diff --git a/monif b/monif deleted file mode 100755 index 53466ba..0000000 --- a/monif +++ /dev/null @@ -1,58 +0,0 @@ -#!/usr/bin/env python3 - -# Monitor Interfaces, send hb msg when add changes -import time -import os - -SLEEP = 60 -SRV = "colo2.wapanafa.org" -DBG = 0 - -home = os.environ.get("HOME", "/var/tmp") -HBC = "%s/bin/hbc" % home - -IFS = [] -f = os.popen("/sbin/ifconfig -a 2>/dev/null", "r") -for l in f.readlines(): - if len(l) > 1 and not l[0] in [" ", "\t"]: - r = l.split() - if DBG: - print(r) - if r[0][-1] == ":": - r[0] = r[0][:-1] - if r[0][:2] == "lo": - continue - IFS.append(r[0]) -if DBG: - print(IFS) -addrs = {} -for I in IFS: - addrs[I] = "" - -while 1: - - for I in IFS: - f = os.popen("/sbin/ifconfig %s 2>/dev/null" % I, "r") - ifaddrs = [] - for l in f.readlines(): - r = l.split() - if DBG > 1: - print("x2", r) - if len(r) == 0 or (r[0] != "inet" and r[0] != "inet6"): - continue - if r[1].find("addr:") == 0: - ifaddr = r[1][5:] - else: - ifaddr = r[1] - ifaddrs.append(ifaddr) - - if ifaddrs != [] and ifaddrs != addrs[I]: - msg = '%s -m "ifadd %s %s" %s' % (HBC, I, ",".join(ifaddrs), SRV) - if DBG: - print(msg) - else: - os.system(msg) - addrs[I] = ifaddrs - f.close() - - time.sleep(SLEEP) diff --git a/poetry.lock b/poetry.lock deleted file mode 100644 index 0f671f0..0000000 --- a/poetry.lock +++ /dev/null @@ -1,1298 +0,0 @@ -# This file is automatically @generated by Poetry 2.3.1 and should not be changed by hand. - -[[package]] -name = "annotated-doc" -version = "0.0.4" -description = "Document parameters, class attributes, return types, and variables inline, with Annotated." -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320"}, - {file = "annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4"}, -] - -[[package]] -name = "annotated-types" -version = "0.7.0" -description = "Reusable constraint types to use with typing.Annotated" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, - {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, -] - -[[package]] -name = "anyio" -version = "4.12.1" -description = "High-level concurrency and networking framework on top of asyncio or Trio" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c"}, - {file = "anyio-4.12.1.tar.gz", hash = "sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703"}, -] - -[package.dependencies] -exceptiongroup = {version = ">=1.0.2", markers = "python_version < \"3.11\""} -idna = ">=2.8" -typing_extensions = {version = ">=4.5", markers = "python_version < \"3.13\""} - -[package.extras] -trio = ["trio (>=0.31.0) ; python_version < \"3.10\"", "trio (>=0.32.0) ; python_version >= \"3.10\""] - -[[package]] -name = "certifi" -version = "2026.1.4" -description = "Python package for providing Mozilla's CA Bundle." -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "certifi-2026.1.4-py3-none-any.whl", hash = "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c"}, - {file = "certifi-2026.1.4.tar.gz", hash = "sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120"}, -] - -[[package]] -name = "charset-normalizer" -version = "3.4.4" -description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "charset_normalizer-3.4.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-win32.whl", hash = "sha256:f34be2938726fc13801220747472850852fe6b1ea75869a048d6f896838c896f"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-win_arm64.whl", hash = "sha256:cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-win32.whl", hash = "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ce8a0633f41a967713a59c4139d29110c07e826d131a316b50ce11b1d79b4f84"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eaabd426fe94daf8fd157c32e571c85cb12e66692f15516a83a03264b08d06c3"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c4ef880e27901b6cc782f1b95f82da9313c0eb95c3af699103088fa0ac3ce9ac"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2aaba3b0819274cc41757a1da876f810a3e4d7b6eb25699253a4effef9e8e4af"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:778d2e08eda00f4256d7f672ca9fef386071c9202f5e4607920b86d7803387f2"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f155a433c2ec037d4e8df17d18922c3a0d9b3232a396690f17175d2946f0218d"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a8bf8d0f749c5757af2142fe7903a9df1d2e8aa3841559b2bad34b08d0e2bcf3"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:194f08cbb32dc406d6e1aea671a68be0823673db2832b38405deba2fb0d88f63"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_armv7l.whl", hash = "sha256:6aee717dcfead04c6eb1ce3bd29ac1e22663cdea57f943c87d1eab9a025438d7"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:cd4b7ca9984e5e7985c12bc60a6f173f3c958eae74f3ef6624bb6b26e2abbae4"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_riscv64.whl", hash = "sha256:b7cf1017d601aa35e6bb650b6ad28652c9cd78ee6caff19f3c28d03e1c80acbf"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:e912091979546adf63357d7e2ccff9b44f026c075aeaf25a52d0e95ad2281074"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:5cb4d72eea50c8868f5288b7f7f33ed276118325c1dfd3957089f6b519e1382a"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-win32.whl", hash = "sha256:837c2ce8c5a65a2035be9b3569c684358dfbf109fd3b6969630a87535495ceaa"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-win_amd64.whl", hash = "sha256:44c2a8734b333e0578090c4cd6b16f275e07aa6614ca8715e6c038e865e70576"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:a9768c477b9d7bd54bc0c86dbaebdec6f03306675526c9927c0e8a04e8f94af9"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1bee1e43c28aa63cb16e5c14e582580546b08e535299b8b6158a7c9c768a1f3d"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:fd44c878ea55ba351104cb93cc85e74916eb8fa440ca7903e57575e97394f608"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0f04b14ffe5fdc8c4933862d8306109a2c51e0704acfa35d51598eb45a1e89fc"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:cd09d08005f958f370f539f186d10aec3377d55b9eeb0d796025d4886119d76e"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4fe7859a4e3e8457458e2ff592f15ccb02f3da787fcd31e0183879c3ad4692a1"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fa09f53c465e532f4d3db095e0c55b615f010ad81803d383195b6b5ca6cbf5f3"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:7fa17817dc5625de8a027cb8b26d9fefa3ea28c8253929b8d6649e705d2835b6"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:5947809c8a2417be3267efc979c47d76a079758166f7d43ef5ae8e9f92751f88"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:4902828217069c3c5c71094537a8e623f5d097858ac6ca8252f7b4d10b7560f1"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:7c308f7e26e4363d79df40ca5b2be1c6ba9f02bdbccfed5abddb7859a6ce72cf"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:2c9d3c380143a1fedbff95a312aa798578371eb29da42106a29019368a475318"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:cb01158d8b88ee68f15949894ccc6712278243d95f344770fa7593fa2d94410c"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-win32.whl", hash = "sha256:2677acec1a2f8ef614c6888b5b4ae4060cc184174a938ed4e8ef690e15d3e505"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-win_amd64.whl", hash = "sha256:f8e160feb2aed042cd657a72acc0b481212ed28b1b9a95c0cee1621b524e1966"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-win_arm64.whl", hash = "sha256:b5d84d37db046c5ca74ee7bb47dd6cbc13f80665fdde3e8040bdd3fb015ecb50"}, - {file = "charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f"}, - {file = "charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a"}, -] - -[[package]] -name = "colorama" -version = "0.4.6" -description = "Cross-platform colored terminal text." -optional = true -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" -groups = ["main"] -markers = "extra == \"dev\" and sys_platform == \"win32\"" -files = [ - {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, - {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, -] - -[[package]] -name = "coverage" -version = "7.13.3" -description = "Code coverage measurement for Python" -optional = true -python-versions = ">=3.10" -groups = ["main"] -markers = "extra == \"dev\"" -files = [ - {file = "coverage-7.13.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0b4f345f7265cdbdb5ec2521ffff15fa49de6d6c39abf89fc7ad68aa9e3a55f0"}, - {file = "coverage-7.13.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:96c3be8bae9d0333e403cc1a8eb078a7f928b5650bae94a18fb4820cc993fb9b"}, - {file = "coverage-7.13.3-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d6f4a21328ea49d38565b55599e1c02834e76583a6953e5586d65cb1efebd8f8"}, - {file = "coverage-7.13.3-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fc970575799a9d17d5c3fafd83a0f6ccf5d5117cdc9ad6fbd791e9ead82418b0"}, - {file = "coverage-7.13.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:87ff33b652b3556b05e204ae20793d1f872161b0fa5ec8a9ac76f8430e152ed6"}, - {file = "coverage-7.13.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7df8759ee57b9f3f7b66799b7660c282f4375bef620ade1686d6a7b03699e75f"}, - {file = "coverage-7.13.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f45c9bcb16bee25a798ccba8a2f6a1251b19de6a0d617bb365d7d2f386c4e20e"}, - {file = "coverage-7.13.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:318b2e4753cbf611061e01b6cc81477e1cdfeb69c36c4a14e6595e674caadb56"}, - {file = "coverage-7.13.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:24db3959de8ee394eeeca89ccb8ba25305c2da9a668dd44173394cbd5aa0777f"}, - {file = "coverage-7.13.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:be14d0622125edef21b3a4d8cd2d138c4872bf6e38adc90fd92385e3312f406a"}, - {file = "coverage-7.13.3-cp310-cp310-win32.whl", hash = "sha256:53be4aab8ddef18beb6188f3a3fdbf4d1af2277d098d4e618be3a8e6c88e74be"}, - {file = "coverage-7.13.3-cp310-cp310-win_amd64.whl", hash = "sha256:bfeee64ad8b4aae3233abb77eb6b52b51b05fa89da9645518671b9939a78732b"}, - {file = "coverage-7.13.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5907605ee20e126eeee2abe14aae137043c2c8af2fa9b38d2ab3b7a6b8137f73"}, - {file = "coverage-7.13.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a88705500988c8acad8b8fd86c2a933d3aa96bec1ddc4bc5cb256360db7bbd00"}, - {file = "coverage-7.13.3-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7bbb5aa9016c4c29e3432e087aa29ebee3f8fda089cfbfb4e6d64bd292dcd1c2"}, - {file = "coverage-7.13.3-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0c2be202a83dde768937a61cdc5d06bf9fb204048ca199d93479488e6247656c"}, - {file = "coverage-7.13.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f45e32ef383ce56e0ca099b2e02fcdf7950be4b1b56afaab27b4ad790befe5b"}, - {file = "coverage-7.13.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6ed2e787249b922a93cd95c671cc9f4c9797a106e81b455c83a9ddb9d34590c0"}, - {file = "coverage-7.13.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:05dd25b21afffe545e808265897c35f32d3e4437663923e0d256d9ab5031fb14"}, - {file = "coverage-7.13.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:46d29926349b5c4f1ea4fca95e8c892835515f3600995a383fa9a923b5739ea4"}, - {file = "coverage-7.13.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:fae6a21537519c2af00245e834e5bf2884699cc7c1055738fd0f9dc37a3644ad"}, - {file = "coverage-7.13.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c672d4e2f0575a4ca2bf2aa0c5ced5188220ab806c1bb6d7179f70a11a017222"}, - {file = "coverage-7.13.3-cp311-cp311-win32.whl", hash = "sha256:fcda51c918c7a13ad93b5f89a58d56e3a072c9e0ba5c231b0ed81404bf2648fb"}, - {file = "coverage-7.13.3-cp311-cp311-win_amd64.whl", hash = "sha256:d1a049b5c51b3b679928dd35e47c4a2235e0b6128b479a7596d0ef5b42fa6301"}, - {file = "coverage-7.13.3-cp311-cp311-win_arm64.whl", hash = "sha256:79f2670c7e772f4917895c3d89aad59e01f3dbe68a4ed2d0373b431fad1dcfba"}, - {file = "coverage-7.13.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ed48b4170caa2c4420e0cd27dc977caaffc7eecc317355751df8373dddcef595"}, - {file = "coverage-7.13.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8f2adf4bcffbbec41f366f2e6dffb9d24e8172d16e91da5799c9b7ed6b5716e6"}, - {file = "coverage-7.13.3-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:01119735c690786b6966a1e9f098da4cd7ca9174c4cfe076d04e653105488395"}, - {file = "coverage-7.13.3-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8bb09e83c603f152d855f666d70a71765ca8e67332e5829e62cb9466c176af23"}, - {file = "coverage-7.13.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b607a40cba795cfac6d130220d25962931ce101f2f478a29822b19755377fb34"}, - {file = "coverage-7.13.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:44f14a62f5da2e9aedf9080e01d2cda61df39197d48e323538ec037336d68da8"}, - {file = "coverage-7.13.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:debf29e0b157769843dff0981cc76f79e0ed04e36bb773c6cac5f6029054bd8a"}, - {file = "coverage-7.13.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:824bb95cd71604031ae9a48edb91fd6effde669522f960375668ed21b36e3ec4"}, - {file = "coverage-7.13.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:8f1010029a5b52dc427c8e2a8dbddb2303ddd180b806687d1acd1bb1d06649e7"}, - {file = "coverage-7.13.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:cd5dee4fd7659d8306ffa79eeaaafd91fa30a302dac3af723b9b469e549247e0"}, - {file = "coverage-7.13.3-cp312-cp312-win32.whl", hash = "sha256:f7f153d0184d45f3873b3ad3ad22694fd73aadcb8cdbc4337ab4b41ea6b4dff1"}, - {file = "coverage-7.13.3-cp312-cp312-win_amd64.whl", hash = "sha256:03a6e5e1e50819d6d7436f5bc40c92ded7e484e400716886ac921e35c133149d"}, - {file = "coverage-7.13.3-cp312-cp312-win_arm64.whl", hash = "sha256:51c4c42c0e7d09a822b08b6cf79b3c4db8333fffde7450da946719ba0d45730f"}, - {file = "coverage-7.13.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:853c3d3c79ff0db65797aad79dee6be020efd218ac4510f15a205f1e8d13ce25"}, - {file = "coverage-7.13.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f75695e157c83d374f88dcc646a60cb94173304a9258b2e74ba5a66b7614a51a"}, - {file = "coverage-7.13.3-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2d098709621d0819039f3f1e471ee554f55a0b2ac0d816883c765b14129b5627"}, - {file = "coverage-7.13.3-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:16d23d6579cf80a474ad160ca14d8b319abaa6db62759d6eef53b2fc979b58c8"}, - {file = "coverage-7.13.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:00d34b29a59d2076e6f318b30a00a69bf63687e30cd882984ed444e753990cc1"}, - {file = "coverage-7.13.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ab6d72bffac9deb6e6cb0f61042e748de3f9f8e98afb0375a8e64b0b6e11746b"}, - {file = "coverage-7.13.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e129328ad1258e49cae0123a3b5fcb93d6c2fa90d540f0b4c7cdcdc019aaa3dc"}, - {file = "coverage-7.13.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2213a8d88ed35459bda71597599d4eec7c2ebad201c88f0bfc2c26fd9b0dd2ea"}, - {file = "coverage-7.13.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:00dd3f02de6d5f5c9c3d95e3e036c3c2e2a669f8bf2d3ceb92505c4ce7838f67"}, - {file = "coverage-7.13.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f9bada7bc660d20b23d7d312ebe29e927b655cf414dadcdb6335a2075695bd86"}, - {file = "coverage-7.13.3-cp313-cp313-win32.whl", hash = "sha256:75b3c0300f3fa15809bd62d9ca8b170eb21fcf0100eb4b4154d6dc8b3a5bbd43"}, - {file = "coverage-7.13.3-cp313-cp313-win_amd64.whl", hash = "sha256:a2f7589c6132c44c53f6e705e1a6677e2b7821378c22f7703b2cf5388d0d4587"}, - {file = "coverage-7.13.3-cp313-cp313-win_arm64.whl", hash = "sha256:123ceaf2b9d8c614f01110f908a341e05b1b305d6b2ada98763b9a5a59756051"}, - {file = "coverage-7.13.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:cc7fd0f726795420f3678ac82ff882c7fc33770bd0074463b5aef7293285ace9"}, - {file = "coverage-7.13.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d358dc408edc28730aed5477a69338e444e62fba0b7e9e4a131c505fadad691e"}, - {file = "coverage-7.13.3-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5d67b9ed6f7b5527b209b24b3df9f2e5bf0198c1bbf99c6971b0e2dcb7e2a107"}, - {file = "coverage-7.13.3-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:59224bfb2e9b37c1335ae35d00daa3a5b4e0b1a20f530be208fff1ecfa436f43"}, - {file = "coverage-7.13.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ae9306b5299e31e31e0d3b908c66bcb6e7e3ddca143dea0266e9ce6c667346d3"}, - {file = "coverage-7.13.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:343aaeb5f8bb7bcd38620fd7bc56e6ee8207847d8c6103a1e7b72322d381ba4a"}, - {file = "coverage-7.13.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b2182129f4c101272ff5f2f18038d7b698db1bf8e7aa9e615cb48440899ad32e"}, - {file = "coverage-7.13.3-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:94d2ac94bd0cc57c5626f52f8c2fffed1444b5ae8c9fc68320306cc2b255e155"}, - {file = "coverage-7.13.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:65436cde5ecabe26fb2f0bf598962f0a054d3f23ad529361326ac002c61a2a1e"}, - {file = "coverage-7.13.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:db83b77f97129813dbd463a67e5335adc6a6a91db652cc085d60c2d512746f96"}, - {file = "coverage-7.13.3-cp313-cp313t-win32.whl", hash = "sha256:dfb428e41377e6b9ba1b0a32df6db5409cb089a0ed1d0a672dc4953ec110d84f"}, - {file = "coverage-7.13.3-cp313-cp313t-win_amd64.whl", hash = "sha256:5badd7e596e6b0c89aa8ec6d37f4473e4357f982ce57f9a2942b0221cd9cf60c"}, - {file = "coverage-7.13.3-cp313-cp313t-win_arm64.whl", hash = "sha256:989aa158c0eb19d83c76c26f4ba00dbb272485c56e452010a3450bdbc9daafd9"}, - {file = "coverage-7.13.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:c6f6169bbdbdb85aab8ac0392d776948907267fcc91deeacf6f9d55f7a83ae3b"}, - {file = "coverage-7.13.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2f5e731627a3d5ef11a2a35aa0c6f7c435867c7ccbc391268eb4f2ca5dbdcc10"}, - {file = "coverage-7.13.3-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9db3a3285d91c0b70fab9f39f0a4aa37d375873677efe4e71e58d8321e8c5d39"}, - {file = "coverage-7.13.3-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:06e49c5897cb12e3f7ecdc111d44e97c4f6d0557b81a7a0204ed70a8b038f86f"}, - {file = "coverage-7.13.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fb25061a66802df9fc13a9ba1967d25faa4dae0418db469264fd9860a921dde4"}, - {file = "coverage-7.13.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:99fee45adbb1caeb914da16f70e557fb7ff6ddc9e4b14de665bd41af631367ef"}, - {file = "coverage-7.13.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:318002f1fd819bdc1651c619268aa5bc853c35fa5cc6d1e8c96bd9cd6c828b75"}, - {file = "coverage-7.13.3-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:71295f2d1d170b9977dc386d46a7a1b7cbb30e5405492529b4c930113a33f895"}, - {file = "coverage-7.13.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:5b1ad2e0dc672625c44bc4fe34514602a9fd8b10d52ddc414dc585f74453516c"}, - {file = "coverage-7.13.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b2beb64c145593a50d90db5c7178f55daeae129123b0d265bdb3cbec83e5194a"}, - {file = "coverage-7.13.3-cp314-cp314-win32.whl", hash = "sha256:3d1aed4f4e837a832df2f3b4f68a690eede0de4560a2dbc214ea0bc55aabcdb4"}, - {file = "coverage-7.13.3-cp314-cp314-win_amd64.whl", hash = "sha256:9f9efbbaf79f935d5fbe3ad814825cbce4f6cdb3054384cb49f0c0f496125fa0"}, - {file = "coverage-7.13.3-cp314-cp314-win_arm64.whl", hash = "sha256:31b6e889c53d4e6687ca63706148049494aace140cffece1c4dc6acadb70a7b3"}, - {file = "coverage-7.13.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:c5e9787cec750793a19a28df7edd85ac4e49d3fb91721afcdc3b86f6c08d9aa8"}, - {file = "coverage-7.13.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e5b86db331c682fd0e4be7098e6acee5e8a293f824d41487c667a93705d415ca"}, - {file = "coverage-7.13.3-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:edc7754932682d52cf6e7a71806e529ecd5ce660e630e8bd1d37109a2e5f63ba"}, - {file = "coverage-7.13.3-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d3a16d6398666510a6886f67f43d9537bfd0e13aca299688a19daa84f543122f"}, - {file = "coverage-7.13.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:303d38b19626c1981e1bb067a9928236d88eb0e4479b18a74812f05a82071508"}, - {file = "coverage-7.13.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:284e06eadfe15ddfee2f4ee56631f164ef897a7d7d5a15bca5f0bb88889fc5ba"}, - {file = "coverage-7.13.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d401f0864a1d3198422816878e4e84ca89ec1c1bf166ecc0ae01380a39b888cd"}, - {file = "coverage-7.13.3-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3f379b02c18a64de78c4ccdddf1c81c2c5ae1956c72dacb9133d7dd7809794ab"}, - {file = "coverage-7.13.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:7a482f2da9086971efb12daca1d6547007ede3674ea06e16d7663414445c683e"}, - {file = "coverage-7.13.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:562136b0d401992118d9b49fbee5454e16f95f85b120a4226a04d816e33fe024"}, - {file = "coverage-7.13.3-cp314-cp314t-win32.whl", hash = "sha256:ca46e5c3be3b195098dd88711890b8011a9fa4feca942292bb84714ce5eab5d3"}, - {file = "coverage-7.13.3-cp314-cp314t-win_amd64.whl", hash = "sha256:06d316dbb3d9fd44cca05b2dbcfbef22948493d63a1f28e828d43e6cc505fed8"}, - {file = "coverage-7.13.3-cp314-cp314t-win_arm64.whl", hash = "sha256:299d66e9218193f9dc6e4880629ed7c4cd23486005166247c283fb98531656c3"}, - {file = "coverage-7.13.3-py3-none-any.whl", hash = "sha256:90a8af9dba6429b2573199622d72e0ebf024d6276f16abce394ad4d181bb0910"}, - {file = "coverage-7.13.3.tar.gz", hash = "sha256:f7f6182d3dfb8802c1747eacbfe611b669455b69b7c037484bb1efbbb56711ac"}, -] - -[package.dependencies] -tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} - -[package.extras] -toml = ["tomli ; python_full_version <= \"3.11.0a6\""] - -[[package]] -name = "exceptiongroup" -version = "1.3.1" -description = "Backport of PEP 654 (exception groups)" -optional = false -python-versions = ">=3.7" -groups = ["main"] -markers = "python_version == \"3.10\"" -files = [ - {file = "exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598"}, - {file = "exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219"}, -] - -[package.dependencies] -typing-extensions = {version = ">=4.6.0", markers = "python_version < \"3.13\""} - -[package.extras] -test = ["pytest (>=6)"] - -[[package]] -name = "fastapi" -version = "0.128.0" -description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "fastapi-0.128.0-py3-none-any.whl", hash = "sha256:aebd93f9716ee3b4f4fcfe13ffb7cf308d99c9f3ab5622d8877441072561582d"}, - {file = "fastapi-0.128.0.tar.gz", hash = "sha256:1cc179e1cef10a6be60ffe429f79b829dce99d8de32d7acb7e6c8dfdf7f2645a"}, -] - -[package.dependencies] -annotated-doc = ">=0.0.2" -pydantic = ">=2.7.0" -starlette = ">=0.40.0,<0.51.0" -typing-extensions = ">=4.8.0" - -[package.extras] -all = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.8)", "httpx (>=0.23.0,<1.0.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=3.1.5)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.18)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"] -standard = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.8)", "httpx (>=0.23.0,<1.0.0)", "jinja2 (>=3.1.5)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.18)", "uvicorn[standard] (>=0.12.0)"] -standard-no-fastapi-cloud-cli = ["email-validator (>=2.0.0)", "fastapi-cli[standard-no-fastapi-cloud-cli] (>=0.0.8)", "httpx (>=0.23.0,<1.0.0)", "jinja2 (>=3.1.5)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.18)", "uvicorn[standard] (>=0.12.0)"] - -[[package]] -name = "flake8" -version = "7.3.0" -description = "the modular source code checker: pep8 pyflakes and co" -optional = true -python-versions = ">=3.9" -groups = ["main"] -markers = "extra == \"dev\"" -files = [ - {file = "flake8-7.3.0-py2.py3-none-any.whl", hash = "sha256:b9696257b9ce8beb888cdbe31cf885c90d31928fe202be0889a7cdafad32f01e"}, - {file = "flake8-7.3.0.tar.gz", hash = "sha256:fe044858146b9fc69b551a4b490d69cf960fcb78ad1edcb84e7fbb1b4a8e3872"}, -] - -[package.dependencies] -mccabe = ">=0.7.0,<0.8.0" -pycodestyle = ">=2.14.0,<2.15.0" -pyflakes = ">=3.4.0,<3.5.0" - -[[package]] -name = "idna" -version = "3.11" -description = "Internationalized Domain Names in Applications (IDNA)" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea"}, - {file = "idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902"}, -] - -[package.extras] -all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] - -[[package]] -name = "iniconfig" -version = "2.3.0" -description = "brain-dead simple config-ini parsing" -optional = true -python-versions = ">=3.10" -groups = ["main"] -markers = "extra == \"dev\"" -files = [ - {file = "iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12"}, - {file = "iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730"}, -] - -[[package]] -name = "jinja2" -version = "3.1.6" -description = "A very fast and expressive template engine." -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67"}, - {file = "jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d"}, -] - -[package.dependencies] -MarkupSafe = ">=2.0" - -[package.extras] -i18n = ["Babel (>=2.7)"] - -[[package]] -name = "librt" -version = "0.7.8" -description = "Mypyc runtime library" -optional = true -python-versions = ">=3.9" -groups = ["main"] -markers = "extra == \"dev\" and platform_python_implementation != \"PyPy\"" -files = [ - {file = "librt-0.7.8-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b45306a1fc5f53c9330fbee134d8b3227fe5da2ab09813b892790400aa49352d"}, - {file = "librt-0.7.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:864c4b7083eeee250ed55135d2127b260d7eb4b5e953a9e5df09c852e327961b"}, - {file = "librt-0.7.8-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6938cc2de153bc927ed8d71c7d2f2ae01b4e96359126c602721340eb7ce1a92d"}, - {file = "librt-0.7.8-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:66daa6ac5de4288a5bbfbe55b4caa7bf0cd26b3269c7a476ffe8ce45f837f87d"}, - {file = "librt-0.7.8-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4864045f49dc9c974dadb942ac56a74cd0479a2aafa51ce272c490a82322ea3c"}, - {file = "librt-0.7.8-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a36515b1328dc5b3ffce79fe204985ca8572525452eacabee2166f44bb387b2c"}, - {file = "librt-0.7.8-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b7e7f140c5169798f90b80d6e607ed2ba5059784968a004107c88ad61fb3641d"}, - {file = "librt-0.7.8-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ff71447cb778a4f772ddc4ce360e6ba9c95527ed84a52096bd1bbf9fee2ec7c0"}, - {file = "librt-0.7.8-cp310-cp310-win32.whl", hash = "sha256:047164e5f68b7a8ebdf9fae91a3c2161d3192418aadd61ddd3a86a56cbe3dc85"}, - {file = "librt-0.7.8-cp310-cp310-win_amd64.whl", hash = "sha256:d6f254d096d84156a46a84861183c183d30734e52383602443292644d895047c"}, - {file = "librt-0.7.8-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ff3e9c11aa260c31493d4b3197d1e28dd07768594a4f92bec4506849d736248f"}, - {file = "librt-0.7.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ddb52499d0b3ed4aa88746aaf6f36a08314677d5c346234c3987ddc506404eac"}, - {file = "librt-0.7.8-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e9c0afebbe6ce177ae8edba0c7c4d626f2a0fc12c33bb993d163817c41a7a05c"}, - {file = "librt-0.7.8-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:631599598e2c76ded400c0a8722dec09217c89ff64dc54b060f598ed68e7d2a8"}, - {file = "librt-0.7.8-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c1ba843ae20db09b9d5c80475376168feb2640ce91cd9906414f23cc267a1ff"}, - {file = "librt-0.7.8-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b5b007bb22ea4b255d3ee39dfd06d12534de2fcc3438567d9f48cdaf67ae1ae3"}, - {file = "librt-0.7.8-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:dbd79caaf77a3f590cbe32dc2447f718772d6eea59656a7dcb9311161b10fa75"}, - {file = "librt-0.7.8-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:87808a8d1e0bd62a01cafc41f0fd6818b5a5d0ca0d8a55326a81643cdda8f873"}, - {file = "librt-0.7.8-cp311-cp311-win32.whl", hash = "sha256:31724b93baa91512bd0a376e7cf0b59d8b631ee17923b1218a65456fa9bda2e7"}, - {file = "librt-0.7.8-cp311-cp311-win_amd64.whl", hash = "sha256:978e8b5f13e52cf23a9e80f3286d7546baa70bc4ef35b51d97a709d0b28e537c"}, - {file = "librt-0.7.8-cp311-cp311-win_arm64.whl", hash = "sha256:20e3946863d872f7cabf7f77c6c9d370b8b3d74333d3a32471c50d3a86c0a232"}, - {file = "librt-0.7.8-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9b6943885b2d49c48d0cff23b16be830ba46b0152d98f62de49e735c6e655a63"}, - {file = "librt-0.7.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:46ef1f4b9b6cc364b11eea0ecc0897314447a66029ee1e55859acb3dd8757c93"}, - {file = "librt-0.7.8-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:907ad09cfab21e3c86e8f1f87858f7049d1097f77196959c033612f532b4e592"}, - {file = "librt-0.7.8-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2991b6c3775383752b3ca0204842743256f3ad3deeb1d0adc227d56b78a9a850"}, - {file = "librt-0.7.8-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:03679b9856932b8c8f674e87aa3c55ea11c9274301f76ae8dc4d281bda55cf62"}, - {file = "librt-0.7.8-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3968762fec1b2ad34ce57458b6de25dbb4142713e9ca6279a0d352fa4e9f452b"}, - {file = "librt-0.7.8-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:bb7a7807523a31f03061288cc4ffc065d684c39db7644c676b47d89553c0d714"}, - {file = "librt-0.7.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad64a14b1e56e702e19b24aae108f18ad1bf7777f3af5fcd39f87d0c5a814449"}, - {file = "librt-0.7.8-cp312-cp312-win32.whl", hash = "sha256:0241a6ed65e6666236ea78203a73d800dbed896cf12ae25d026d75dc1fcd1dac"}, - {file = "librt-0.7.8-cp312-cp312-win_amd64.whl", hash = "sha256:6db5faf064b5bab9675c32a873436b31e01d66ca6984c6f7f92621656033a708"}, - {file = "librt-0.7.8-cp312-cp312-win_arm64.whl", hash = "sha256:57175aa93f804d2c08d2edb7213e09276bd49097611aefc37e3fa38d1fb99ad0"}, - {file = "librt-0.7.8-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4c3995abbbb60b3c129490fa985dfe6cac11d88fc3c36eeb4fb1449efbbb04fc"}, - {file = "librt-0.7.8-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:44e0c2cbc9bebd074cf2cdbe472ca185e824be4e74b1c63a8e934cea674bebf2"}, - {file = "librt-0.7.8-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4d2f1e492cae964b3463a03dc77a7fe8742f7855d7258c7643f0ee32b6651dd3"}, - {file = "librt-0.7.8-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:451e7ffcef8f785831fdb791bd69211f47e95dc4c6ddff68e589058806f044c6"}, - {file = "librt-0.7.8-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3469e1af9f1380e093ae06bedcbdd11e407ac0b303a56bbe9afb1d6824d4982d"}, - {file = "librt-0.7.8-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f11b300027ce19a34f6d24ebb0a25fd0e24a9d53353225a5c1e6cadbf2916b2e"}, - {file = "librt-0.7.8-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4adc73614f0d3c97874f02f2c7fd2a27854e7e24ad532ea6b965459c5b757eca"}, - {file = "librt-0.7.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:60c299e555f87e4c01b2eca085dfccda1dde87f5a604bb45c2906b8305819a93"}, - {file = "librt-0.7.8-cp313-cp313-win32.whl", hash = "sha256:b09c52ed43a461994716082ee7d87618096851319bf695d57ec123f2ab708951"}, - {file = "librt-0.7.8-cp313-cp313-win_amd64.whl", hash = "sha256:f8f4a901a3fa28969d6e4519deceab56c55a09d691ea7b12ca830e2fa3461e34"}, - {file = "librt-0.7.8-cp313-cp313-win_arm64.whl", hash = "sha256:43d4e71b50763fcdcf64725ac680d8cfa1706c928b844794a7aa0fa9ac8e5f09"}, - {file = "librt-0.7.8-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:be927c3c94c74b05128089a955fba86501c3b544d1d300282cc1b4bd370cb418"}, - {file = "librt-0.7.8-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7b0803e9008c62a7ef79058233db7ff6f37a9933b8f2573c05b07ddafa226611"}, - {file = "librt-0.7.8-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:79feb4d00b2a4e0e05c9c56df707934f41fcb5fe53fd9efb7549068d0495b758"}, - {file = "librt-0.7.8-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b9122094e3f24aa759c38f46bd8863433820654927370250f460ae75488b66ea"}, - {file = "librt-0.7.8-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7e03bea66af33c95ce3addf87a9bf1fcad8d33e757bc479957ddbc0e4f7207ac"}, - {file = "librt-0.7.8-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f1ade7f31675db00b514b98f9ab9a7698c7282dad4be7492589109471852d398"}, - {file = "librt-0.7.8-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a14229ac62adcf1b90a15992f1ab9c69ae8b99ffb23cb64a90878a6e8a2f5b81"}, - {file = "librt-0.7.8-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5bcaaf624fd24e6a0cb14beac37677f90793a96864c67c064a91458611446e83"}, - {file = "librt-0.7.8-cp314-cp314-win32.whl", hash = "sha256:7aa7d5457b6c542ecaed79cec4ad98534373c9757383973e638ccced0f11f46d"}, - {file = "librt-0.7.8-cp314-cp314-win_amd64.whl", hash = "sha256:3d1322800771bee4a91f3b4bd4e49abc7d35e65166821086e5afd1e6c0d9be44"}, - {file = "librt-0.7.8-cp314-cp314-win_arm64.whl", hash = "sha256:5363427bc6a8c3b1719f8f3845ea53553d301382928a86e8fab7984426949bce"}, - {file = "librt-0.7.8-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:ca916919793a77e4a98d4a1701e345d337ce53be4a16620f063191f7322ac80f"}, - {file = "librt-0.7.8-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:54feb7b4f2f6706bb82325e836a01be805770443e2400f706e824e91f6441dde"}, - {file = "librt-0.7.8-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:39a4c76fee41007070f872b648cc2f711f9abf9a13d0c7162478043377b52c8e"}, - {file = "librt-0.7.8-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ac9c8a458245c7de80bc1b9765b177055efff5803f08e548dd4bb9ab9a8d789b"}, - {file = "librt-0.7.8-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:95b67aa7eff150f075fda09d11f6bfb26edffd300f6ab1666759547581e8f666"}, - {file = "librt-0.7.8-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:535929b6eff670c593c34ff435d5440c3096f20fa72d63444608a5aef64dd581"}, - {file = "librt-0.7.8-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:63937bd0f4d1cb56653dc7ae900d6c52c41f0015e25aaf9902481ee79943b33a"}, - {file = "librt-0.7.8-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cf243da9e42d914036fd362ac3fa77d80a41cadcd11ad789b1b5eec4daaf67ca"}, - {file = "librt-0.7.8-cp314-cp314t-win32.whl", hash = "sha256:171ca3a0a06c643bd0a2f62a8944e1902c94aa8e5da4db1ea9a8daf872685365"}, - {file = "librt-0.7.8-cp314-cp314t-win_amd64.whl", hash = "sha256:445b7304145e24c60288a2f172b5ce2ca35c0f81605f5299f3fa567e189d2e32"}, - {file = "librt-0.7.8-cp314-cp314t-win_arm64.whl", hash = "sha256:8766ece9de08527deabcd7cb1b4f1a967a385d26e33e536d6d8913db6ef74f06"}, - {file = "librt-0.7.8-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c7e8f88f79308d86d8f39c491773cbb533d6cb7fa6476f35d711076ee04fceb6"}, - {file = "librt-0.7.8-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:389bd25a0db916e1d6bcb014f11aa9676cedaa485e9ec3752dfe19f196fd377b"}, - {file = "librt-0.7.8-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:73fd300f501a052f2ba52ede721232212f3b06503fa12665408ecfc9d8fd149c"}, - {file = "librt-0.7.8-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d772edc6a5f7835635c7562f6688e031f0b97e31d538412a852c49c9a6c92d5"}, - {file = "librt-0.7.8-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bfde8a130bd0f239e45503ab39fab239ace094d63ee1d6b67c25a63d741c0f71"}, - {file = "librt-0.7.8-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:fdec6e2368ae4f796fc72fad7fd4bd1753715187e6d870932b0904609e7c878e"}, - {file = "librt-0.7.8-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:00105e7d541a8f2ee5be52caacea98a005e0478cfe78c8080fbb7b5d2b340c63"}, - {file = "librt-0.7.8-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:c6f8947d3dfd7f91066c5b4385812c18be26c9d5a99ca56667547f2c39149d94"}, - {file = "librt-0.7.8-cp39-cp39-win32.whl", hash = "sha256:41d7bb1e07916aeb12ae4a44e3025db3691c4149ab788d0315781b4d29b86afb"}, - {file = "librt-0.7.8-cp39-cp39-win_amd64.whl", hash = "sha256:e90a8e237753c83b8e484d478d9a996dc5e39fd5bd4c6ce32563bc8123f132be"}, - {file = "librt-0.7.8.tar.gz", hash = "sha256:1a4ede613941d9c3470b0368be851df6bb78ab218635512d0370b27a277a0862"}, -] - -[[package]] -name = "markupsafe" -version = "3.0.3" -description = "Safely add untrusted strings to HTML/XML markup." -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559"}, - {file = "markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419"}, - {file = "markupsafe-3.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695"}, - {file = "markupsafe-3.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591"}, - {file = "markupsafe-3.0.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c"}, - {file = "markupsafe-3.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f"}, - {file = "markupsafe-3.0.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6"}, - {file = "markupsafe-3.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1"}, - {file = "markupsafe-3.0.3-cp310-cp310-win32.whl", hash = "sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa"}, - {file = "markupsafe-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8"}, - {file = "markupsafe-3.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1"}, - {file = "markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad"}, - {file = "markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a"}, - {file = "markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50"}, - {file = "markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf"}, - {file = "markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f"}, - {file = "markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a"}, - {file = "markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115"}, - {file = "markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a"}, - {file = "markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19"}, - {file = "markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01"}, - {file = "markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c"}, - {file = "markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e"}, - {file = "markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce"}, - {file = "markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d"}, - {file = "markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d"}, - {file = "markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a"}, - {file = "markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b"}, - {file = "markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f"}, - {file = "markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b"}, - {file = "markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d"}, - {file = "markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c"}, - {file = "markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f"}, - {file = "markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795"}, - {file = "markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219"}, - {file = "markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6"}, - {file = "markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676"}, - {file = "markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9"}, - {file = "markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1"}, - {file = "markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc"}, - {file = "markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12"}, - {file = "markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed"}, - {file = "markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5"}, - {file = "markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485"}, - {file = "markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73"}, - {file = "markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37"}, - {file = "markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19"}, - {file = "markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025"}, - {file = "markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6"}, - {file = "markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f"}, - {file = "markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb"}, - {file = "markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009"}, - {file = "markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354"}, - {file = "markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218"}, - {file = "markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287"}, - {file = "markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe"}, - {file = "markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026"}, - {file = "markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737"}, - {file = "markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97"}, - {file = "markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d"}, - {file = "markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda"}, - {file = "markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf"}, - {file = "markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe"}, - {file = "markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9"}, - {file = "markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581"}, - {file = "markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4"}, - {file = "markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab"}, - {file = "markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175"}, - {file = "markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634"}, - {file = "markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50"}, - {file = "markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e"}, - {file = "markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5"}, - {file = "markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523"}, - {file = "markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc"}, - {file = "markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d"}, - {file = "markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9"}, - {file = "markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa"}, - {file = "markupsafe-3.0.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:15d939a21d546304880945ca1ecb8a039db6b4dc49b2c5a400387cdae6a62e26"}, - {file = "markupsafe-3.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f71a396b3bf33ecaa1626c255855702aca4d3d9fea5e051b41ac59a9c1c41edc"}, - {file = "markupsafe-3.0.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f4b68347f8c5eab4a13419215bdfd7f8c9b19f2b25520968adfad23eb0ce60c"}, - {file = "markupsafe-3.0.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e8fc20152abba6b83724d7ff268c249fa196d8259ff481f3b1476383f8f24e42"}, - {file = "markupsafe-3.0.3-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:949b8d66bc381ee8b007cd945914c721d9aba8e27f71959d750a46f7c282b20b"}, - {file = "markupsafe-3.0.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:3537e01efc9d4dccdf77221fb1cb3b8e1a38d5428920e0657ce299b20324d758"}, - {file = "markupsafe-3.0.3-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:591ae9f2a647529ca990bc681daebdd52c8791ff06c2bfa05b65163e28102ef2"}, - {file = "markupsafe-3.0.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a320721ab5a1aba0a233739394eb907f8c8da5c98c9181d1161e77a0c8e36f2d"}, - {file = "markupsafe-3.0.3-cp39-cp39-win32.whl", hash = "sha256:df2449253ef108a379b8b5d6b43f4b1a8e81a061d6537becd5582fba5f9196d7"}, - {file = "markupsafe-3.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:7c3fb7d25180895632e5d3148dbdc29ea38ccb7fd210aa27acbd1201a1902c6e"}, - {file = "markupsafe-3.0.3-cp39-cp39-win_arm64.whl", hash = "sha256:38664109c14ffc9e7437e86b4dceb442b0096dfe3541d7864d9cbe1da4cf36c8"}, - {file = "markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698"}, -] - -[[package]] -name = "mattermostdriver" -version = "7.3.2" -description = "A Python Mattermost Driver" -optional = false -python-versions = ">=3.5" -groups = ["main"] -files = [ - {file = "mattermostdriver-7.3.2-py3-none-any.whl", hash = "sha256:8c6f15da34873b6c88da8fa8da0342f94bef77fcd16294befd92fea7e008cd97"}, - {file = "mattermostdriver-7.3.2.tar.gz", hash = "sha256:2e4d7b4a17d3013e279c6f993746ea18cd60b45d8fa3be24f47bc2de22b9b3b4"}, -] - -[package.dependencies] -requests = ">=2.25" -websockets = ">=8" - -[[package]] -name = "mccabe" -version = "0.7.0" -description = "McCabe checker, plugin for flake8" -optional = true -python-versions = ">=3.6" -groups = ["main"] -markers = "extra == \"dev\"" -files = [ - {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, - {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, -] - -[[package]] -name = "mypy" -version = "1.19.1" -description = "Optional static typing for Python" -optional = true -python-versions = ">=3.9" -groups = ["main"] -markers = "extra == \"dev\"" -files = [ - {file = "mypy-1.19.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5f05aa3d375b385734388e844bc01733bd33c644ab48e9684faa54e5389775ec"}, - {file = "mypy-1.19.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:022ea7279374af1a5d78dfcab853fe6a536eebfda4b59deab53cd21f6cd9f00b"}, - {file = "mypy-1.19.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee4c11e460685c3e0c64a4c5de82ae143622410950d6be863303a1c4ba0e36d6"}, - {file = "mypy-1.19.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de759aafbae8763283b2ee5869c7255391fbc4de3ff171f8f030b5ec48381b74"}, - {file = "mypy-1.19.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ab43590f9cd5108f41aacf9fca31841142c786827a74ab7cc8a2eacb634e09a1"}, - {file = "mypy-1.19.1-cp310-cp310-win_amd64.whl", hash = "sha256:2899753e2f61e571b3971747e302d5f420c3fd09650e1951e99f823bc3089dac"}, - {file = "mypy-1.19.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d8dfc6ab58ca7dda47d9237349157500468e404b17213d44fc1cb77bce532288"}, - {file = "mypy-1.19.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e3f276d8493c3c97930e354b2595a44a21348b320d859fb4a2b9f66da9ed27ab"}, - {file = "mypy-1.19.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2abb24cf3f17864770d18d673c85235ba52456b36a06b6afc1e07c1fdcd3d0e6"}, - {file = "mypy-1.19.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a009ffa5a621762d0c926a078c2d639104becab69e79538a494bcccb62cc0331"}, - {file = "mypy-1.19.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f7cee03c9a2e2ee26ec07479f38ea9c884e301d42c6d43a19d20fb014e3ba925"}, - {file = "mypy-1.19.1-cp311-cp311-win_amd64.whl", hash = "sha256:4b84a7a18f41e167f7995200a1d07a4a6810e89d29859df936f1c3923d263042"}, - {file = "mypy-1.19.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a8174a03289288c1f6c46d55cef02379b478bfbc8e358e02047487cad44c6ca1"}, - {file = "mypy-1.19.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffcebe56eb09ff0c0885e750036a095e23793ba6c2e894e7e63f6d89ad51f22e"}, - {file = "mypy-1.19.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b64d987153888790bcdb03a6473d321820597ab8dd9243b27a92153c4fa50fd2"}, - {file = "mypy-1.19.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c35d298c2c4bba75feb2195655dfea8124d855dfd7343bf8b8c055421eaf0cf8"}, - {file = "mypy-1.19.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:34c81968774648ab5ac09c29a375fdede03ba253f8f8287847bd480782f73a6a"}, - {file = "mypy-1.19.1-cp312-cp312-win_amd64.whl", hash = "sha256:b10e7c2cd7870ba4ad9b2d8a6102eb5ffc1f16ca35e3de6bfa390c1113029d13"}, - {file = "mypy-1.19.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e3157c7594ff2ef1634ee058aafc56a82db665c9438fd41b390f3bde1ab12250"}, - {file = "mypy-1.19.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdb12f69bcc02700c2b47e070238f42cb87f18c0bc1fc4cdb4fb2bc5fd7a3b8b"}, - {file = "mypy-1.19.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f859fb09d9583a985be9a493d5cfc5515b56b08f7447759a0c5deaf68d80506e"}, - {file = "mypy-1.19.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c9a6538e0415310aad77cb94004ca6482330fece18036b5f360b62c45814c4ef"}, - {file = "mypy-1.19.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:da4869fc5e7f62a88f3fe0b5c919d1d9f7ea3cef92d3689de2823fd27e40aa75"}, - {file = "mypy-1.19.1-cp313-cp313-win_amd64.whl", hash = "sha256:016f2246209095e8eda7538944daa1d60e1e8134d98983b9fc1e92c1fc0cb8dd"}, - {file = "mypy-1.19.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:06e6170bd5836770e8104c8fdd58e5e725cfeb309f0a6c681a811f557e97eac1"}, - {file = "mypy-1.19.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:804bd67b8054a85447c8954215a906d6eff9cabeabe493fb6334b24f4bfff718"}, - {file = "mypy-1.19.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21761006a7f497cb0d4de3d8ef4ca70532256688b0523eee02baf9eec895e27b"}, - {file = "mypy-1.19.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:28902ee51f12e0f19e1e16fbe2f8f06b6637f482c459dd393efddd0ec7f82045"}, - {file = "mypy-1.19.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:481daf36a4c443332e2ae9c137dfee878fcea781a2e3f895d54bd3002a900957"}, - {file = "mypy-1.19.1-cp314-cp314-win_amd64.whl", hash = "sha256:8bb5c6f6d043655e055be9b542aa5f3bdd30e4f3589163e85f93f3640060509f"}, - {file = "mypy-1.19.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7bcfc336a03a1aaa26dfce9fff3e287a3ba99872a157561cbfcebe67c13308e3"}, - {file = "mypy-1.19.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b7951a701c07ea584c4fe327834b92a30825514c868b1f69c30445093fdd9d5a"}, - {file = "mypy-1.19.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b13cfdd6c87fc3efb69ea4ec18ef79c74c3f98b4e5498ca9b85ab3b2c2329a67"}, - {file = "mypy-1.19.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f28f99c824ecebcdaa2e55d82953e38ff60ee5ec938476796636b86afa3956e"}, - {file = "mypy-1.19.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:c608937067d2fc5a4dd1a5ce92fd9e1398691b8c5d012d66e1ddd430e9244376"}, - {file = "mypy-1.19.1-cp39-cp39-win_amd64.whl", hash = "sha256:409088884802d511ee52ca067707b90c883426bd95514e8cfda8281dc2effe24"}, - {file = "mypy-1.19.1-py3-none-any.whl", hash = "sha256:f1235f5ea01b7db5468d53ece6aaddf1ad0b88d9e7462b86ef96fe04995d7247"}, - {file = "mypy-1.19.1.tar.gz", hash = "sha256:19d88bb05303fe63f71dd2c6270daca27cb9401c4ca8255fe50d1d920e0eb9ba"}, -] - -[package.dependencies] -librt = {version = ">=0.6.2", markers = "platform_python_implementation != \"PyPy\""} -mypy_extensions = ">=1.0.0" -pathspec = ">=0.9.0" -tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} -typing_extensions = ">=4.6.0" - -[package.extras] -dmypy = ["psutil (>=4.0)"] -faster-cache = ["orjson"] -install-types = ["pip"] -mypyc = ["setuptools (>=50)"] -reports = ["lxml"] - -[[package]] -name = "mypy-extensions" -version = "1.1.0" -description = "Type system extensions for programs checked with the mypy type checker." -optional = true -python-versions = ">=3.8" -groups = ["main"] -markers = "extra == \"dev\"" -files = [ - {file = "mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505"}, - {file = "mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558"}, -] - -[[package]] -name = "packaging" -version = "26.0" -description = "Core utilities for Python packages" -optional = true -python-versions = ">=3.8" -groups = ["main"] -markers = "extra == \"dev\"" -files = [ - {file = "packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529"}, - {file = "packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4"}, -] - -[[package]] -name = "pathspec" -version = "1.0.4" -description = "Utility library for gitignore style pattern matching of file paths." -optional = true -python-versions = ">=3.9" -groups = ["main"] -markers = "extra == \"dev\"" -files = [ - {file = "pathspec-1.0.4-py3-none-any.whl", hash = "sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723"}, - {file = "pathspec-1.0.4.tar.gz", hash = "sha256:0210e2ae8a21a9137c0d470578cb0e595af87edaa6ebf12ff176f14a02e0e645"}, -] - -[package.extras] -hyperscan = ["hyperscan (>=0.7)"] -optional = ["typing-extensions (>=4)"] -re2 = ["google-re2 (>=1.1)"] -tests = ["pytest (>=9)", "typing-extensions (>=4.15)"] - -[[package]] -name = "pluggy" -version = "1.6.0" -description = "plugin and hook calling mechanisms for python" -optional = true -python-versions = ">=3.9" -groups = ["main"] -markers = "extra == \"dev\"" -files = [ - {file = "pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746"}, - {file = "pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3"}, -] - -[package.extras] -dev = ["pre-commit", "tox"] -testing = ["coverage", "pytest", "pytest-benchmark"] - -[[package]] -name = "pycodestyle" -version = "2.14.0" -description = "Python style guide checker" -optional = true -python-versions = ">=3.9" -groups = ["main"] -markers = "extra == \"dev\"" -files = [ - {file = "pycodestyle-2.14.0-py2.py3-none-any.whl", hash = "sha256:dd6bf7cb4ee77f8e016f9c8e74a35ddd9f67e1d5fd4184d86c3b98e07099f42d"}, - {file = "pycodestyle-2.14.0.tar.gz", hash = "sha256:c4b5b517d278089ff9d0abdec919cd97262a3367449ea1c8b49b91529167b783"}, -] - -[[package]] -name = "pydantic" -version = "2.12.5" -description = "Data validation using Python type hints" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d"}, - {file = "pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49"}, -] - -[package.dependencies] -annotated-types = ">=0.6.0" -pydantic-core = "2.41.5" -typing-extensions = ">=4.14.1" -typing-inspection = ">=0.4.2" - -[package.extras] -email = ["email-validator (>=2.0.0)"] -timezone = ["tzdata ; python_version >= \"3.9\" and platform_system == \"Windows\""] - -[[package]] -name = "pydantic-core" -version = "2.41.5" -description = "Core functionality for Pydantic validation and serialization" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "pydantic_core-2.41.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:77b63866ca88d804225eaa4af3e664c5faf3568cea95360d21f4725ab6e07146"}, - {file = "pydantic_core-2.41.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dfa8a0c812ac681395907e71e1274819dec685fec28273a28905df579ef137e2"}, - {file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5921a4d3ca3aee735d9fd163808f5e8dd6c6972101e4adbda9a4667908849b97"}, - {file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e25c479382d26a2a41b7ebea1043564a937db462816ea07afa8a44c0866d52f9"}, - {file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f547144f2966e1e16ae626d8ce72b4cfa0caedc7fa28052001c94fb2fcaa1c52"}, - {file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f52298fbd394f9ed112d56f3d11aabd0d5bd27beb3084cc3d8ad069483b8941"}, - {file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:100baa204bb412b74fe285fb0f3a385256dad1d1879f0a5cb1499ed2e83d132a"}, - {file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:05a2c8852530ad2812cb7914dc61a1125dc4e06252ee98e5638a12da6cc6fb6c"}, - {file = "pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:29452c56df2ed968d18d7e21f4ab0ac55e71dc59524872f6fc57dcf4a3249ed2"}, - {file = "pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:d5160812ea7a8a2ffbe233d8da666880cad0cbaf5d4de74ae15c313213d62556"}, - {file = "pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:df3959765b553b9440adfd3c795617c352154e497a4eaf3752555cfb5da8fc49"}, - {file = "pydantic_core-2.41.5-cp310-cp310-win32.whl", hash = "sha256:1f8d33a7f4d5a7889e60dc39856d76d09333d8a6ed0f5f1190635cbec70ec4ba"}, - {file = "pydantic_core-2.41.5-cp310-cp310-win_amd64.whl", hash = "sha256:62de39db01b8d593e45871af2af9e497295db8d73b085f6bfd0b18c83c70a8f9"}, - {file = "pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6"}, - {file = "pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b"}, - {file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a"}, - {file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8"}, - {file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e"}, - {file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1"}, - {file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b"}, - {file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b"}, - {file = "pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284"}, - {file = "pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594"}, - {file = "pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e"}, - {file = "pydantic_core-2.41.5-cp311-cp311-win32.whl", hash = "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b"}, - {file = "pydantic_core-2.41.5-cp311-cp311-win_amd64.whl", hash = "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe"}, - {file = "pydantic_core-2.41.5-cp311-cp311-win_arm64.whl", hash = "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f"}, - {file = "pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7"}, - {file = "pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0"}, - {file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69"}, - {file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75"}, - {file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05"}, - {file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc"}, - {file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c"}, - {file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5"}, - {file = "pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c"}, - {file = "pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294"}, - {file = "pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1"}, - {file = "pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d"}, - {file = "pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815"}, - {file = "pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3"}, - {file = "pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9"}, - {file = "pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34"}, - {file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0"}, - {file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33"}, - {file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e"}, - {file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2"}, - {file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586"}, - {file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d"}, - {file = "pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740"}, - {file = "pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e"}, - {file = "pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858"}, - {file = "pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36"}, - {file = "pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11"}, - {file = "pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd"}, - {file = "pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a"}, - {file = "pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14"}, - {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1"}, - {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66"}, - {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869"}, - {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2"}, - {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375"}, - {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553"}, - {file = "pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90"}, - {file = "pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07"}, - {file = "pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb"}, - {file = "pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23"}, - {file = "pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf"}, - {file = "pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008"}, - {file = "pydantic_core-2.41.5-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:8bfeaf8735be79f225f3fefab7f941c712aaca36f1128c9d7e2352ee1aa87bdf"}, - {file = "pydantic_core-2.41.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:346285d28e4c8017da95144c7f3acd42740d637ff41946af5ce6e5e420502dd5"}, - {file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a75dafbf87d6276ddc5b2bf6fae5254e3d0876b626eb24969a574fff9149ee5d"}, - {file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7b93a4d08587e2b7e7882de461e82b6ed76d9026ce91ca7915e740ecc7855f60"}, - {file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e8465ab91a4bd96d36dde3263f06caa6a8a6019e4113f24dc753d79a8b3a3f82"}, - {file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:299e0a22e7ae2b85c1a57f104538b2656e8ab1873511fd718a1c1c6f149b77b5"}, - {file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:707625ef0983fcfb461acfaf14de2067c5942c6bb0f3b4c99158bed6fedd3cf3"}, - {file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f41eb9797986d6ebac5e8edff36d5cef9de40def462311b3eb3eeded1431e425"}, - {file = "pydantic_core-2.41.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0384e2e1021894b1ff5a786dbf94771e2986ebe2869533874d7e43bc79c6f504"}, - {file = "pydantic_core-2.41.5-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:f0cd744688278965817fd0839c4a4116add48d23890d468bc436f78beb28abf5"}, - {file = "pydantic_core-2.41.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:753e230374206729bf0a807954bcc6c150d3743928a73faffee51ac6557a03c3"}, - {file = "pydantic_core-2.41.5-cp39-cp39-win32.whl", hash = "sha256:873e0d5b4fb9b89ef7c2d2a963ea7d02879d9da0da8d9d4933dee8ee86a8b460"}, - {file = "pydantic_core-2.41.5-cp39-cp39-win_amd64.whl", hash = "sha256:e4f4a984405e91527a0d62649ee21138f8e3d0ef103be488c1dc11a80d7f184b"}, - {file = "pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034"}, - {file = "pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c"}, - {file = "pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2"}, - {file = "pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad"}, - {file = "pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd"}, - {file = "pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc"}, - {file = "pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56"}, - {file = "pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b"}, - {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b5819cd790dbf0c5eb9f82c73c16b39a65dd6dd4d1439dcdea7816ec9adddab8"}, - {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5a4e67afbc95fa5c34cf27d9089bca7fcab4e51e57278d710320a70b956d1b9a"}, - {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ece5c59f0ce7d001e017643d8d24da587ea1f74f6993467d85ae8a5ef9d4f42b"}, - {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:16f80f7abe3351f8ea6858914ddc8c77e02578544a0ebc15b4c2e1a0e813b0b2"}, - {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:33cb885e759a705b426baada1fe68cbb0a2e68e34c5d0d0289a364cf01709093"}, - {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:c8d8b4eb992936023be7dee581270af5c6e0697a8559895f527f5b7105ecd36a"}, - {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:242a206cd0318f95cd21bdacff3fcc3aab23e79bba5cac3db5a841c9ef9c6963"}, - {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d3a978c4f57a597908b7e697229d996d77a6d3c94901e9edee593adada95ce1a"}, - {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26"}, - {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808"}, - {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc"}, - {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1"}, - {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84"}, - {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770"}, - {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f"}, - {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51"}, - {file = "pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e"}, -] - -[package.dependencies] -typing-extensions = ">=4.14.1" - -[[package]] -name = "pyflakes" -version = "3.4.0" -description = "passive checker of Python programs" -optional = true -python-versions = ">=3.9" -groups = ["main"] -markers = "extra == \"dev\"" -files = [ - {file = "pyflakes-3.4.0-py2.py3-none-any.whl", hash = "sha256:f742a7dbd0d9cb9ea41e9a24a918996e8170c799fa528688d40dd582c8265f4f"}, - {file = "pyflakes-3.4.0.tar.gz", hash = "sha256:b24f96fafb7d2ab0ec5075b7350b3d2d2218eab42003821c06344973d3ea2f58"}, -] - -[[package]] -name = "pygments" -version = "2.19.2" -description = "Pygments is a syntax highlighting package written in Python." -optional = true -python-versions = ">=3.8" -groups = ["main"] -markers = "extra == \"dev\"" -files = [ - {file = "pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b"}, - {file = "pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887"}, -] - -[package.extras] -windows-terminal = ["colorama (>=0.4.6)"] - -[[package]] -name = "pytest" -version = "9.0.2" -description = "pytest: simple powerful testing with Python" -optional = true -python-versions = ">=3.10" -groups = ["main"] -markers = "extra == \"dev\"" -files = [ - {file = "pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b"}, - {file = "pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11"}, -] - -[package.dependencies] -colorama = {version = ">=0.4", markers = "sys_platform == \"win32\""} -exceptiongroup = {version = ">=1", markers = "python_version < \"3.11\""} -iniconfig = ">=1.0.1" -packaging = ">=22" -pluggy = ">=1.5,<2" -pygments = ">=2.7.2" -tomli = {version = ">=1", markers = "python_version < \"3.11\""} - -[package.extras] -dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "requests", "setuptools", "xmlschema"] - -[[package]] -name = "pytest-cov" -version = "7.0.0" -description = "Pytest plugin for measuring coverage." -optional = true -python-versions = ">=3.9" -groups = ["main"] -markers = "extra == \"dev\"" -files = [ - {file = "pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861"}, - {file = "pytest_cov-7.0.0.tar.gz", hash = "sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1"}, -] - -[package.dependencies] -coverage = {version = ">=7.10.6", extras = ["toml"]} -pluggy = ">=1.2" -pytest = ">=7" - -[package.extras] -testing = ["process-tests", "pytest-xdist", "virtualenv"] - -[[package]] -name = "pyyaml" -version = "6.0.3" -description = "YAML parser and emitter for Python" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "PyYAML-6.0.3-cp38-cp38-macosx_10_13_x86_64.whl", hash = "sha256:c2514fceb77bc5e7a2f7adfaa1feb2fb311607c9cb518dbc378688ec73d8292f"}, - {file = "PyYAML-6.0.3-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c57bb8c96f6d1808c030b1687b9b5fb476abaa47f0db9c0101f5e9f394e97f4"}, - {file = "PyYAML-6.0.3-cp38-cp38-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:efd7b85f94a6f21e4932043973a7ba2613b059c4a000551892ac9f1d11f5baf3"}, - {file = "PyYAML-6.0.3-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22ba7cfcad58ef3ecddc7ed1db3409af68d023b7f940da23c6c2a1890976eda6"}, - {file = "PyYAML-6.0.3-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:6344df0d5755a2c9a276d4473ae6b90647e216ab4757f8426893b5dd2ac3f369"}, - {file = "PyYAML-6.0.3-cp38-cp38-win32.whl", hash = "sha256:3ff07ec89bae51176c0549bc4c63aa6202991da2d9a6129d7aef7f1407d3f295"}, - {file = "PyYAML-6.0.3-cp38-cp38-win_amd64.whl", hash = "sha256:5cf4e27da7e3fbed4d6c3d8e797387aaad68102272f8f9752883bc32d61cb87b"}, - {file = "pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b"}, - {file = "pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956"}, - {file = "pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8"}, - {file = "pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198"}, - {file = "pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b"}, - {file = "pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0"}, - {file = "pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69"}, - {file = "pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e"}, - {file = "pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c"}, - {file = "pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e"}, - {file = "pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824"}, - {file = "pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c"}, - {file = "pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00"}, - {file = "pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d"}, - {file = "pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a"}, - {file = "pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4"}, - {file = "pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b"}, - {file = "pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf"}, - {file = "pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196"}, - {file = "pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0"}, - {file = "pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28"}, - {file = "pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c"}, - {file = "pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc"}, - {file = "pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e"}, - {file = "pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea"}, - {file = "pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5"}, - {file = "pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b"}, - {file = "pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd"}, - {file = "pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8"}, - {file = "pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1"}, - {file = "pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c"}, - {file = "pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5"}, - {file = "pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6"}, - {file = "pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6"}, - {file = "pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be"}, - {file = "pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26"}, - {file = "pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c"}, - {file = "pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb"}, - {file = "pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac"}, - {file = "pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310"}, - {file = "pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7"}, - {file = "pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788"}, - {file = "pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5"}, - {file = "pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764"}, - {file = "pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35"}, - {file = "pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac"}, - {file = "pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3"}, - {file = "pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3"}, - {file = "pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba"}, - {file = "pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c"}, - {file = "pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702"}, - {file = "pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c"}, - {file = "pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065"}, - {file = "pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65"}, - {file = "pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9"}, - {file = "pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b"}, - {file = "pyyaml-6.0.3-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:b865addae83924361678b652338317d1bd7e79b1f4596f96b96c77a5a34b34da"}, - {file = "pyyaml-6.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c3355370a2c156cffb25e876646f149d5d68f5e0a3ce86a5084dd0b64a994917"}, - {file = "pyyaml-6.0.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3c5677e12444c15717b902a5798264fa7909e41153cdf9ef7ad571b704a63dd9"}, - {file = "pyyaml-6.0.3-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5ed875a24292240029e4483f9d4a4b8a1ae08843b9c54f43fcc11e404532a8a5"}, - {file = "pyyaml-6.0.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0150219816b6a1fa26fb4699fb7daa9caf09eb1999f3b70fb6e786805e80375a"}, - {file = "pyyaml-6.0.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:fa160448684b4e94d80416c0fa4aac48967a969efe22931448d853ada8baf926"}, - {file = "pyyaml-6.0.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:27c0abcb4a5dac13684a37f76e701e054692a9b2d3064b70f5e4eb54810553d7"}, - {file = "pyyaml-6.0.3-cp39-cp39-win32.whl", hash = "sha256:1ebe39cb5fc479422b83de611d14e2c0d3bb2a18bbcb01f229ab3cfbd8fee7a0"}, - {file = "pyyaml-6.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:2e71d11abed7344e42a8849600193d15b6def118602c4c176f748e4583246007"}, - {file = "pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f"}, -] - -[[package]] -name = "requests" -version = "2.32.5" -description = "Python HTTP for Humans." -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6"}, - {file = "requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf"}, -] - -[package.dependencies] -certifi = ">=2017.4.17" -charset_normalizer = ">=2,<4" -idna = ">=2.5,<4" -urllib3 = ">=1.21.1,<3" - -[package.extras] -socks = ["PySocks (>=1.5.6,!=1.5.7)"] -use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] - -[[package]] -name = "starlette" -version = "0.50.0" -description = "The little ASGI library that shines." -optional = false -python-versions = ">=3.10" -groups = ["main"] -files = [ - {file = "starlette-0.50.0-py3-none-any.whl", hash = "sha256:9e5391843ec9b6e472eed1365a78c8098cfceb7a74bfd4d6b1c0c0095efb3bca"}, - {file = "starlette-0.50.0.tar.gz", hash = "sha256:a2a17b22203254bcbc2e1f926d2d55f3f9497f769416b3190768befe598fa3ca"}, -] - -[package.dependencies] -anyio = ">=3.6.2,<5" -typing-extensions = {version = ">=4.10.0", markers = "python_version < \"3.13\""} - -[package.extras] -full = ["httpx (>=0.27.0,<0.29.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.18)", "pyyaml"] - -[[package]] -name = "tomli" -version = "2.4.0" -description = "A lil' TOML parser" -optional = true -python-versions = ">=3.8" -groups = ["main"] -markers = "extra == \"dev\" and python_full_version <= \"3.11.0a6\"" -files = [ - {file = "tomli-2.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b5ef256a3fd497d4973c11bf142e9ed78b150d36f5773f1ca6088c230ffc5867"}, - {file = "tomli-2.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5572e41282d5268eb09a697c89a7bee84fae66511f87533a6f88bd2f7b652da9"}, - {file = "tomli-2.4.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:551e321c6ba03b55676970b47cb1b73f14a0a4dce6a3e1a9458fd6d921d72e95"}, - {file = "tomli-2.4.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e3f639a7a8f10069d0e15408c0b96a2a828cfdec6fca05296ebcdcc28ca7c76"}, - {file = "tomli-2.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1b168f2731796b045128c45982d3a4874057626da0e2ef1fdd722848b741361d"}, - {file = "tomli-2.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:133e93646ec4300d651839d382d63edff11d8978be23da4cc106f5a18b7d0576"}, - {file = "tomli-2.4.0-cp311-cp311-win32.whl", hash = "sha256:b6c78bdf37764092d369722d9946cb65b8767bfa4110f902a1b2542d8d173c8a"}, - {file = "tomli-2.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:d3d1654e11d724760cdb37a3d7691f0be9db5fbdaef59c9f532aabf87006dbaa"}, - {file = "tomli-2.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:cae9c19ed12d4e8f3ebf46d1a75090e4c0dc16271c5bce1c833ac168f08fb614"}, - {file = "tomli-2.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:920b1de295e72887bafa3ad9f7a792f811847d57ea6b1215154030cf131f16b1"}, - {file = "tomli-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d6d9a4aee98fac3eab4952ad1d73aee87359452d1c086b5ceb43ed02ddb16b8"}, - {file = "tomli-2.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36b9d05b51e65b254ea6c2585b59d2c4cb91c8a3d91d0ed0f17591a29aaea54a"}, - {file = "tomli-2.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c8a885b370751837c029ef9bc014f27d80840e48bac415f3412e6593bbc18c1"}, - {file = "tomli-2.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8768715ffc41f0008abe25d808c20c3d990f42b6e2e58305d5da280ae7d1fa3b"}, - {file = "tomli-2.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b438885858efd5be02a9a133caf5812b8776ee0c969fea02c45e8e3f296ba51"}, - {file = "tomli-2.4.0-cp312-cp312-win32.whl", hash = "sha256:0408e3de5ec77cc7f81960c362543cbbd91ef883e3138e81b729fc3eea5b9729"}, - {file = "tomli-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:685306e2cc7da35be4ee914fd34ab801a6acacb061b6a7abca922aaf9ad368da"}, - {file = "tomli-2.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:5aa48d7c2356055feef06a43611fc401a07337d5b006be13a30f6c58f869e3c3"}, - {file = "tomli-2.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84d081fbc252d1b6a982e1870660e7330fb8f90f676f6e78b052ad4e64714bf0"}, - {file = "tomli-2.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9a08144fa4cba33db5255f9b74f0b89888622109bd2776148f2597447f92a94e"}, - {file = "tomli-2.4.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c73add4bb52a206fd0c0723432db123c0c75c280cbd67174dd9d2db228ebb1b4"}, - {file = "tomli-2.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fb2945cbe303b1419e2706e711b7113da57b7db31ee378d08712d678a34e51e"}, - {file = "tomli-2.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bbb1b10aa643d973366dc2cb1ad94f99c1726a02343d43cbc011edbfac579e7c"}, - {file = "tomli-2.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4cbcb367d44a1f0c2be408758b43e1ffb5308abe0ea222897d6bfc8e8281ef2f"}, - {file = "tomli-2.4.0-cp313-cp313-win32.whl", hash = "sha256:7d49c66a7d5e56ac959cb6fc583aff0651094ec071ba9ad43df785abc2320d86"}, - {file = "tomli-2.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:3cf226acb51d8f1c394c1b310e0e0e61fecdd7adcb78d01e294ac297dd2e7f87"}, - {file = "tomli-2.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:d20b797a5c1ad80c516e41bc1fb0443ddb5006e9aaa7bda2d71978346aeb9132"}, - {file = "tomli-2.4.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:26ab906a1eb794cd4e103691daa23d95c6919cc2fa9160000ac02370cc9dd3f6"}, - {file = "tomli-2.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:20cedb4ee43278bc4f2fee6cb50daec836959aadaf948db5172e776dd3d993fc"}, - {file = "tomli-2.4.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:39b0b5d1b6dd03684b3fb276407ebed7090bbec989fa55838c98560c01113b66"}, - {file = "tomli-2.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a26d7ff68dfdb9f87a016ecfd1e1c2bacbe3108f4e0f8bcd2228ef9a766c787d"}, - {file = "tomli-2.4.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:20ffd184fb1df76a66e34bd1b36b4a4641bd2b82954befa32fe8163e79f1a702"}, - {file = "tomli-2.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75c2f8bbddf170e8effc98f5e9084a8751f8174ea6ccf4fca5398436e0320bc8"}, - {file = "tomli-2.4.0-cp314-cp314-win32.whl", hash = "sha256:31d556d079d72db7c584c0627ff3a24c5d3fb4f730221d3444f3efb1b2514776"}, - {file = "tomli-2.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:43e685b9b2341681907759cf3a04e14d7104b3580f808cfde1dfdb60ada85475"}, - {file = "tomli-2.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:3d895d56bd3f82ddd6faaff993c275efc2ff38e52322ea264122d72729dca2b2"}, - {file = "tomli-2.4.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:5b5807f3999fb66776dbce568cc9a828544244a8eb84b84b9bafc080c99597b9"}, - {file = "tomli-2.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c084ad935abe686bd9c898e62a02a19abfc9760b5a79bc29644463eaf2840cb0"}, - {file = "tomli-2.4.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f2e3955efea4d1cfbcb87bc321e00dc08d2bcb737fd1d5e398af111d86db5df"}, - {file = "tomli-2.4.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e0fe8a0b8312acf3a88077a0802565cb09ee34107813bba1c7cd591fa6cfc8d"}, - {file = "tomli-2.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:413540dce94673591859c4c6f794dfeaa845e98bf35d72ed59636f869ef9f86f"}, - {file = "tomli-2.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0dc56fef0e2c1c470aeac5b6ca8cc7b640bb93e92d9803ddaf9ea03e198f5b0b"}, - {file = "tomli-2.4.0-cp314-cp314t-win32.whl", hash = "sha256:d878f2a6707cc9d53a1be1414bbb419e629c3d6e67f69230217bb663e76b5087"}, - {file = "tomli-2.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2add28aacc7425117ff6364fe9e06a183bb0251b03f986df0e78e974047571fd"}, - {file = "tomli-2.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2b1e3b80e1d5e52e40e9b924ec43d81570f0e7d09d11081b797bc4692765a3d4"}, - {file = "tomli-2.4.0-py3-none-any.whl", hash = "sha256:1f776e7d669ebceb01dee46484485f43a4048746235e683bcdffacdf1fb4785a"}, - {file = "tomli-2.4.0.tar.gz", hash = "sha256:aa89c3f6c277dd275d8e243ad24f3b5e701491a860d5121f2cdd399fbb31fc9c"}, -] - -[[package]] -name = "typing-extensions" -version = "4.15.0" -description = "Backported and Experimental Type Hints for Python 3.9+" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"}, - {file = "typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466"}, -] - -[[package]] -name = "typing-inspection" -version = "0.4.2" -description = "Runtime typing introspection tools" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7"}, - {file = "typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464"}, -] - -[package.dependencies] -typing-extensions = ">=4.12.0" - -[[package]] -name = "urllib3" -version = "2.6.3" -description = "HTTP library with thread-safe connection pooling, file post, and more." -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4"}, - {file = "urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed"}, -] - -[package.extras] -brotli = ["brotli (>=1.2.0) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=1.2.0.0) ; platform_python_implementation != \"CPython\""] -h2 = ["h2 (>=4,<5)"] -socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] -zstd = ["backports-zstd (>=1.0.0) ; python_version < \"3.14\""] - -[[package]] -name = "websockets" -version = "16.0" -description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)" -optional = false -python-versions = ">=3.10" -groups = ["main"] -files = [ - {file = "websockets-16.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:04cdd5d2d1dacbad0a7bf36ccbcd3ccd5a30ee188f2560b7a62a30d14107b31a"}, - {file = "websockets-16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8ff32bb86522a9e5e31439a58addbb0166f0204d64066fb955265c4e214160f0"}, - {file = "websockets-16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:583b7c42688636f930688d712885cf1531326ee05effd982028212ccc13e5957"}, - {file = "websockets-16.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7d837379b647c0c4c2355c2499723f82f1635fd2c26510e1f587d89bc2199e72"}, - {file = "websockets-16.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:df57afc692e517a85e65b72e165356ed1df12386ecb879ad5693be08fac65dde"}, - {file = "websockets-16.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:2b9f1e0d69bc60a4a87349d50c09a037a2607918746f07de04df9e43252c77a3"}, - {file = "websockets-16.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:335c23addf3d5e6a8633f9f8eda77efad001671e80b95c491dd0924587ece0b3"}, - {file = "websockets-16.0-cp310-cp310-win32.whl", hash = "sha256:37b31c1623c6605e4c00d466c9d633f9b812ea430c11c8a278774a1fde1acfa9"}, - {file = "websockets-16.0-cp310-cp310-win_amd64.whl", hash = "sha256:8e1dab317b6e77424356e11e99a432b7cb2f3ec8c5ab4dabbcee6add48f72b35"}, - {file = "websockets-16.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:31a52addea25187bde0797a97d6fc3d2f92b6f72a9370792d65a6e84615ac8a8"}, - {file = "websockets-16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:417b28978cdccab24f46400586d128366313e8a96312e4b9362a4af504f3bbad"}, - {file = "websockets-16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:af80d74d4edfa3cb9ed973a0a5ba2b2a549371f8a741e0800cb07becdd20f23d"}, - {file = "websockets-16.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:08d7af67b64d29823fed316505a89b86705f2b7981c07848fb5e3ea3020c1abe"}, - {file = "websockets-16.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7be95cfb0a4dae143eaed2bcba8ac23f4892d8971311f1b06f3c6b78952ee70b"}, - {file = "websockets-16.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d6297ce39ce5c2e6feb13c1a996a2ded3b6832155fcfc920265c76f24c7cceb5"}, - {file = "websockets-16.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1c1b30e4f497b0b354057f3467f56244c603a79c0d1dafce1d16c283c25f6e64"}, - {file = "websockets-16.0-cp311-cp311-win32.whl", hash = "sha256:5f451484aeb5cafee1ccf789b1b66f535409d038c56966d6101740c1614b86c6"}, - {file = "websockets-16.0-cp311-cp311-win_amd64.whl", hash = "sha256:8d7f0659570eefb578dacde98e24fb60af35350193e4f56e11190787bee77dac"}, - {file = "websockets-16.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:71c989cbf3254fbd5e84d3bff31e4da39c43f884e64f2551d14bb3c186230f00"}, - {file = "websockets-16.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8b6e209ffee39ff1b6d0fa7bfef6de950c60dfb91b8fcead17da4ee539121a79"}, - {file = "websockets-16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:86890e837d61574c92a97496d590968b23c2ef0aeb8a9bc9421d174cd378ae39"}, - {file = "websockets-16.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9b5aca38b67492ef518a8ab76851862488a478602229112c4b0d58d63a7a4d5c"}, - {file = "websockets-16.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e0334872c0a37b606418ac52f6ab9cfd17317ac26365f7f65e203e2d0d0d359f"}, - {file = "websockets-16.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a0b31e0b424cc6b5a04b8838bbaec1688834b2383256688cf47eb97412531da1"}, - {file = "websockets-16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:485c49116d0af10ac698623c513c1cc01c9446c058a4e61e3bf6c19dff7335a2"}, - {file = "websockets-16.0-cp312-cp312-win32.whl", hash = "sha256:eaded469f5e5b7294e2bdca0ab06becb6756ea86894a47806456089298813c89"}, - {file = "websockets-16.0-cp312-cp312-win_amd64.whl", hash = "sha256:5569417dc80977fc8c2d43a86f78e0a5a22fee17565d78621b6bb264a115d4ea"}, - {file = "websockets-16.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:878b336ac47938b474c8f982ac2f7266a540adc3fa4ad74ae96fea9823a02cc9"}, - {file = "websockets-16.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:52a0fec0e6c8d9a784c2c78276a48a2bdf099e4ccc2a4cad53b27718dbfd0230"}, - {file = "websockets-16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e6578ed5b6981005df1860a56e3617f14a6c307e6a71b4fff8c48fdc50f3ed2c"}, - {file = "websockets-16.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:95724e638f0f9c350bb1c2b0a7ad0e83d9cc0c9259f3ea94e40d7b02a2179ae5"}, - {file = "websockets-16.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0204dc62a89dc9d50d682412c10b3542d748260d743500a85c13cd1ee4bde82"}, - {file = "websockets-16.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:52ac480f44d32970d66763115edea932f1c5b1312de36df06d6b219f6741eed8"}, - {file = "websockets-16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6e5a82b677f8f6f59e8dfc34ec06ca6b5b48bc4fcda346acd093694cc2c24d8f"}, - {file = "websockets-16.0-cp313-cp313-win32.whl", hash = "sha256:abf050a199613f64c886ea10f38b47770a65154dc37181bfaff70c160f45315a"}, - {file = "websockets-16.0-cp313-cp313-win_amd64.whl", hash = "sha256:3425ac5cf448801335d6fdc7ae1eb22072055417a96cc6b31b3861f455fbc156"}, - {file = "websockets-16.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8cc451a50f2aee53042ac52d2d053d08bf89bcb31ae799cb4487587661c038a0"}, - {file = "websockets-16.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:daa3b6ff70a9241cf6c7fc9e949d41232d9d7d26fd3522b1ad2b4d62487e9904"}, - {file = "websockets-16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:fd3cb4adb94a2a6e2b7c0d8d05cb94e6f1c81a0cf9dc2694fb65c7e8d94c42e4"}, - {file = "websockets-16.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:781caf5e8eee67f663126490c2f96f40906594cb86b408a703630f95550a8c3e"}, - {file = "websockets-16.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:caab51a72c51973ca21fa8a18bd8165e1a0183f1ac7066a182ff27107b71e1a4"}, - {file = "websockets-16.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:19c4dc84098e523fd63711e563077d39e90ec6702aff4b5d9e344a60cb3c0cb1"}, - {file = "websockets-16.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a5e18a238a2b2249c9a9235466b90e96ae4795672598a58772dd806edc7ac6d3"}, - {file = "websockets-16.0-cp314-cp314-win32.whl", hash = "sha256:a069d734c4a043182729edd3e9f247c3b2a4035415a9172fd0f1b71658a320a8"}, - {file = "websockets-16.0-cp314-cp314-win_amd64.whl", hash = "sha256:c0ee0e63f23914732c6d7e0cce24915c48f3f1512ec1d079ed01fc629dab269d"}, - {file = "websockets-16.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:a35539cacc3febb22b8f4d4a99cc79b104226a756aa7400adc722e83b0d03244"}, - {file = "websockets-16.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:b784ca5de850f4ce93ec85d3269d24d4c82f22b7212023c974c401d4980ebc5e"}, - {file = "websockets-16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:569d01a4e7fba956c5ae4fc988f0d4e187900f5497ce46339c996dbf24f17641"}, - {file = "websockets-16.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:50f23cdd8343b984957e4077839841146f67a3d31ab0d00e6b824e74c5b2f6e8"}, - {file = "websockets-16.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:152284a83a00c59b759697b7f9e9cddf4e3c7861dd0d964b472b70f78f89e80e"}, - {file = "websockets-16.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bc59589ab64b0022385f429b94697348a6a234e8ce22544e3681b2e9331b5944"}, - {file = "websockets-16.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:32da954ffa2814258030e5a57bc73a3635463238e797c7375dc8091327434206"}, - {file = "websockets-16.0-cp314-cp314t-win32.whl", hash = "sha256:5a4b4cc550cb665dd8a47f868c8d04c8230f857363ad3c9caf7a0c3bf8c61ca6"}, - {file = "websockets-16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b14dc141ed6d2dde437cddb216004bcac6a1df0935d79656387bd41632ba0bbd"}, - {file = "websockets-16.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:349f83cd6c9a415428ee1005cadb5c2c56f4389bc06a9af16103c3bc3dcc8b7d"}, - {file = "websockets-16.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:4a1aba3340a8dca8db6eb5a7986157f52eb9e436b74813764241981ca4888f03"}, - {file = "websockets-16.0-pp311-pypy311_pp73-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f4a32d1bd841d4bcbffdcb3d2ce50c09c3909fbead375ab28d0181af89fd04da"}, - {file = "websockets-16.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0298d07ee155e2e9fda5be8a9042200dd2e3bb0b8a38482156576f863a9d457c"}, - {file = "websockets-16.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:a653aea902e0324b52f1613332ddf50b00c06fdaf7e92624fbf8c77c78fa5767"}, - {file = "websockets-16.0-py3-none-any.whl", hash = "sha256:1637db62fad1dc833276dded54215f2c7fa46912301a24bd94d45d46a011ceec"}, - {file = "websockets-16.0.tar.gz", hash = "sha256:5f6261a5e56e8d5c42a4497b364ea24d94d9563e8fbd44e78ac40879c60179b5"}, -] - -[extras] -dev = ["flake8", "mypy", "pytest", "pytest-cov"] - -[metadata] -lock-version = "2.1" -python-versions = ">=3.10" -content-hash = "15b749b39d9e26563c1ca77896e371e0505979ad053627c31a47f5de8e02bc32" diff --git a/pushHeartbeat.py b/pushHeartbeat.py deleted file mode 100755 index 0dde1a4..0000000 --- a/pushHeartbeat.py +++ /dev/null @@ -1,30 +0,0 @@ -#!/usr/bin/python - -import sys -import http.client, urllib.request, urllib.parse, urllib.error - - -def pushover(msg): - conn = http.client.HTTPSConnection("api.pushover.net:443") - conn.request( - "POST", - "/1/messages.json", - urllib.parse.urlencode( - { - "token": "ac7NLX2rPjXFareeDgLpXNoDf4iFmf", - "user": "uDhH33UjQQDYtNzJb1ThRiWb9ingGK", - "message": msg, - } - ), - {"Content-type": "application/x-www-form-urlencoded"}, - ) - r1 = conn.getresponse() - # print r1.status, r1.reason - return r1.status == 200 - - -v = " ".join(sys.argv[1:]) -if pushover(v): - print("delivered") -else: - print("NOT delivered") diff --git a/pushNagios.py b/pushNagios.py deleted file mode 100755 index b7fd775..0000000 --- a/pushNagios.py +++ /dev/null @@ -1,56 +0,0 @@ -#!/usr/bin/python - -import sys -import http.client, urllib.request, urllib.parse, urllib.error -import getopt - - -def pushover(msg, title=""): - conn = http.client.HTTPSConnection("api.pushover.net:443") - conn.request( - "POST", - "/1/messages.json", - urllib.parse.urlencode( - { - "token": "aNY2xeYydxzabzihTjb3P2LMHhqhr2", - "user": "uDhH33UjQQDYtNzJb1ThRiWb9ingGK", - "message": msg, - "title": title, - } - ), - {"Content-type": "application/x-www-form-urlencoded"}, - ) - r1 = conn.getresponse() - # print r1.status, r1.reason - return r1.status == 200 - - -# -# Main -# - -helpflag = False -verbose = False -title = "Nagios" - -optslist, args = [], [] -try: - optslist, args = getopt.getopt(sys.argv[1:], "ht:v") -except getopt.error as cause: - helpflag = True - -lastyear = 0 -for o, a in optslist: - if o == "-v": - verbose = True - elif o == "-t": - title = a - - -v = " ".join(args) -rc = pushover(v, title) -if verbose: - if rc: - print("delivered") - else: - print("NOT delivered") diff --git a/pyproject.toml b/pyproject.toml index 0fc6107..bc1ce16 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,8 +3,8 @@ requires = ["setuptools>=61.0", "wheel"] build-backend = "setuptools.build_meta" [project] -name = "heartbeat" -version = "0.1.0" +name = "hbd" +version = "5.0" description = "Heartbeat daemon (hbd) — receive heartbeats and act on them" readme = "README.md" requires-python = ">=3.10" @@ -19,7 +19,8 @@ dependencies = [ "websockets>=13.2", "mattermostdriver>=7.3.0", "PyYAML>=6.0", - "Jinja2>=3.1.0",s + "aiohttp>=3.8", + "Jinja2>=3.1.0", "fastapi>=0.95.0", ] @@ -29,6 +30,10 @@ dev = [ "pytest-cov>=4.0", "flake8>=5.0", "mypy>=1.10", + "black>=23.0", + "isort>=5.0", + "re-commit>=3.0", + "tox>=4.0", ] [project.scripts] diff --git a/requirements-dev.txt b/requirements-dev.txt deleted file mode 100644 index 1fc2af8..0000000 --- a/requirements-dev.txt +++ /dev/null @@ -1,9 +0,0 @@ -# Development requirements -pytest>=7.0 -pytest-cov>=4.0 -flake8>=5.0 -mypy>=1.10 -black>=23.0 -isort>=5.0 -pre-commit>=3.0 -tox>=4.0 diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index a10a0e1..0000000 --- a/requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -websockets>=13.2 -mattermostdriver>=7.3.0 diff --git a/rndc-key b/rndc-key new file mode 100644 index 0000000..e1b3726 --- /dev/null +++ b/rndc-key @@ -0,0 +1,4 @@ +key "rndc-key" { + algorithm hmac-md5; + secret "qlGa+AYKtyOgWNuozqECMw=="; +}; diff --git a/run_hbc b/run_hbc deleted file mode 100755 index 7d9f378..0000000 --- a/run_hbc +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/sh - -D=$(dirname $0) -pkill -f $D/hbc 2>/dev/null -sleep 1 -while true; do - ping -qAW 6 -c 2 1.1.1.1 - if [ $? -eq 0 ]; then - break - fi -done -$D/hbc $@ - diff --git a/scripts/bumpminor.sh b/scripts/bumpminor.sh new file mode 100755 index 0000000..cbcccf3 --- /dev/null +++ b/scripts/bumpminor.sh @@ -0,0 +1,12 @@ +#!/bin/sh + +uv version --bump patch +VER=$(uv version --short) +sed -i "s/__version__ = \"[0-9.]*\"\(.*\)$/__version__ = \"$VER\"\1/" moninbox/const.py + +# commit pyproject.toml +git commit -m "version $VER" pyproject.toml moninbox/const.py +git push +# tag version +git tag -a v$VER -m "Version $VER" +git push --tags diff --git a/selfcheck b/selfcheck deleted file mode 100755 index d8e5c31..0000000 --- a/selfcheck +++ /dev/null @@ -1,265 +0,0 @@ -#!/usr/bin/env python -# -RCSID="$Id: selfcheck,v 1.3 2006/11/08 15:42:17 andreas Exp $" -# check internet connectivity -# - -import os, sys, string, time, socket - -PPPIF="pppoe0" -DBG=0 - -ADDR="204.29.161.33" -PORT=50003 - -def sendheartbeatmsg(tosend): - iam=socket.gethostname() - sock=socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - msgboot="msg=%s;service=%s;name=%s" % (tosend, "selfcheck", iam) - sock.sendto(msgboot, (ADDR, PORT)) - time.sleep(1) - sock.close() - -class Pppoe: - def __init__(self, interface): - DBG=0 - self.interface=interface - self.foundauthinfo=0 - - fh=os.popen("/sbin/pppoectl %s" % self.interface,"r") - while 1: - l=fh.readline() - if len(l) == 0: - fh.close() - break - if DBG: print l[:-1] - r=string.split(l[:-1]) - if DBG: print r - if r[0] == self.interface+':': - s=string.split(r[1],'=') - self.phase=s[1] - elif r[0][:11] == 'myauthproto': - self.myauthproto=r[0][12:] - self.myauthname=r[1][11:] - self.foundauthinfo=1 - elif r[0] == 'lcp' and r[1] == 'timeout:': - self.lcptimeount=r[2] - elif r[0] == 'idle' and r[1] == 'timeout' and r[2] == "=": - self.idletimeout=r[3] - elif r[0] == 'max-auth-failure' and r[1] == '=': - self.maxauthfailure=r[2] - elif r[0] == 'max-noreceive' and r[1] == '=': - self.maxnoreceive=string.join(r[2:], ' ') - elif r[0] == 'max-alive-missed' and r[1] == '=': - self.maxalivemissed = string.join(r[2:], ' ') - - fh=os.popen("/sbin/pppoectl -d %s" % self.interface,"r") - while 1: - l=fh.readline() - if len(l) == 0: - fh.close() - break - if DBG: print l[:-1] - r=string.split(l[:-1]) - if DBG: print r - if r[0] == self.interface+':': - self.state=string.join(r[3:],' ') - elif r[0] == 'Session' and r[1] == 'ID:': - self.sessionid=r[2] - elif r[0] == 'PADI' and r[1] == 'retries:': - self.PADIretries=r[2] - elif r[0] == 'PADR' and r[1] == 'retries:': - self.PADRretries=r[2] - - - fh=os.popen("/sbin/ifconfig %s" % self.interface,"r") - if DBG: print fh - self.foundinet=0 - while 1: - l=fh.readline() - if len(l) == 0: - fh.close() - break - if DBG: print l[:-1] - r=string.split(l[:-1]) - if DBG: print r - if r[0] == self.interface+':': - s=string.split(r[1],'=') - s1=string.split(s[1],'<') - self.flagshex=s1[0] - self.flags=string.split(s1[1][:-1],',') - self.mtu=r[3] - elif r[0] == 'inet': - self.foundinet=1 - self.ipaddr=r[1] - self.ipgw=r[3] - self.ipmask=r[5] - - - def __repr__(self): - r=[] - keys=self.__dict__.keys() - keys.sort() - for k in keys: - r.append("%-14s: %s" % (k, self.__dict__[k])) - return string.join(r,'\n') - - -def notee(): - if not self.foundauthinfo: - print"PROBLEM: no login info configured, use \n'pppoectl %s myauthproto=pap myauthname=\"\" myauthsecret=\"\"'" - return - -def checkall(IF): - Res="The interface is" - if "UP" in IF.flags: - Res+=" up" - else: - Res=".\nPROBLEM: the interface is down, use 'ifconfig %s up'" % (PPPIF) - return(Res) - - if IF.foundauthinfo: - Res+=", has authentication information" - else: - Res+=".\nPROBLEM: pppoe has no authentication information." - return(Res) - - if IF.foundinet: - Res+=", is configured" - else: - Res+=".\nPROBLEM: %s is not configured, use 'ifconfig %s inet 0.0.0.0 0.0.0.1'" % (PPPIF, PPPIF) - return(Res) - - if IF.ipaddr != '0.0.0.0': - Res+=", has an IP address" - else: - Res+=".\nPROBLEM: The inteface has no address" - return(Res) - - if IF.ipgw != '0.0.0.1': - Res+=", has an IP gateway" - else: - Res+=".\nPROBLEM: The interfaces has no gateway" - return(Res) - -# ec=os.system("/sbin/ping -n -c 1 %s >/dev/null 2>&1" % "217.237.157.246") - ec=os.system("/sbin/ping -n -c 1 -w 2 %s >/dev/null 2>&1" % IF.ipgw) - if ec == 0: - Res+=".\nThe gateway is reachable." - else: - Res+=".\nPROBLEM: The gateway is not reachable." - return(Res) - - Res+=".\n\n All appears to be well." - return(Res) - - - -# -# Main -# - -uname=os.uname() - -lines=[] -if sys.stdin.isatty(): - uri='/selfcheck' -else: - while 1: - l2=sys.stdin.readline() -# print "
[",len(l2),l2[:-2],"]
" - sys.stdout.flush() - if l2[:-2] == "": - break - lines.append(l2[:-2]) - uri=string.split(lines[0],' ')[1] - if uri != '/favicon.ico': - sendheartbeatmsg(uri) - -date=time.strftime("%a, %d %b %Y %H:%M:%S GMT", time.gmtime()) - - -if uri != "/selfcheck": - print "HTTP/1.1 404 Not Found" - print "Date: %s" % date - print "Connection: close" - print "Content-Type: text/html; charset=iso-8859-1" - print "" - print "Nothing here, move along.." - sys.exit(0) - -print """HTTP/1.1 200 OK -Date: %s -Server: Selfcheck/1.0 (Python) -Last-Modified: %s -Accept-Ranges: bytes -Connection: close -Content-Type: text/html; charset=ISO-8859-1""" % (date, date) - -print """ - - -ADSL Check - - -

ADSL Check

-
"""
-print "Current Time:    %s" % date
-print "Machine:         %s" % uname[1]
-print "OS:              %s" % uname[0]+"/"+uname[4]+" "+uname[2]
-print "Uptime:          ",
-sys.stdout.flush()
-os.system("uptime")
-
-print "
" -print "Checking interface %s" % PPPIF - -IF=Pppoe(PPPIF) - -print checkall(IF) - -print "
" -print "Additional information" - -print "
Users" -sys.stdout.flush() -os.system("w") -print "
" -print "Interface data" -print IF -print - -print "
" -if 0: - print "Ping" - sys.stdout.flush() - ec=os.system("/sbin/ping -n -q -c 3 204.29.161.37") - if ec != 0: - print " ping failed!" -else: - print "Traceroute" - sys.stdout.flush() -# ec=os.system("/usr/sbin/traceroute -w 2 204.29.161.37") - ec=os.system("/usr/pkg/sbin/mtr -r -c 2 204.29.161.37") - if ec != 0: - print " traceroute failed!" - -print "
" -print "Routing Table" -sys.stdout.flush() -os.system("/usr/bin/netstat -rnLfinet") - -print "
" -print "Relevant log entries" -sys.stdout.flush() -os.system("grep %s: /var/log/messages" % IF.interface) - - -if DBG: - for l in lines: - print l - -print "
" -print "All done" -print "" -print "" diff --git a/templates/menu.html b/templates/menu.html index 04d375a..4d8d1ce 100644 --- a/templates/menu.html +++ b/templates/menu.html @@ -1,20 +1,3 @@ -
{{ header }}
- + diff --git a/wstest.py b/wstest.py deleted file mode 100644 index 94edc21..0000000 --- a/wstest.py +++ /dev/null @@ -1,22 +0,0 @@ -import asyncio -import websockets - - -async def hello(): - uri = "ws://localhost:50005/messages" - async with websockets.connect(uri) as websocket: - name = "Andreas" - - await websocket.send(name) - print(f"> {name}") - - while True: - greeting = await websocket.recv() - print(f"< {greeting}") - if greeting == "bye": - break - - print("out of here") - - -asyncio.get_event_loop().run_until_complete(hello())