From 4fc491f61a565de1d1bf53bfae4493e88ce295ea Mon Sep 17 00:00:00 2001 From: Alex Tau Date: Sun, 27 Apr 2025 20:39:07 +0300 Subject: [PATCH 01/56] move existing stuff to archive dir (for now) --- .gitignore => archive-arch/.gitignore | 0 LICENSE => archive-arch/LICENSE | 0 README.md => archive-arch/README.md | 0 {alerting => archive-arch/alerting}/alerts.py | 0 {alerting => archive-arch/alerting}/delayed.py | 0 {alerting => archive-arch/alerting}/enum.py | 0 {alerting => archive-arch/alerting}/login.py | 0 {assets => archive-arch/assets}/lego-login-alert | 0 {assets => archive-arch/assets}/lego-monitoring.service | 0 config.example.json => archive-arch/config.example.json | 0 {misc => archive-arch/misc}/checkers.py | 0 {misc => archive-arch/misc}/checks.py | 0 {misc => archive-arch/misc}/common.py | 0 {misc => archive-arch/misc}/config.py | 0 {misc => archive-arch/misc}/cvars.py | 0 {misc => archive-arch/misc}/disks.py | 0 {misc => archive-arch/misc}/docker_registry.py | 0 {misc => archive-arch/misc}/sensors.py | 0 {misc => archive-arch/misc}/vuln.py | 0 prettyprint.py => archive-arch/prettyprint.py | 0 requirements.txt => archive-arch/requirements.txt | 0 send_login_alert.py => archive-arch/send_login_alert.py | 0 send_ups_alert.py => archive-arch/send_ups_alert.py | 0 service.py => archive-arch/service.py | 0 {tests => archive-arch/tests}/smartctl_ata_hdd.json | 0 {tests => archive-arch/tests}/smartctl_ata_ssd.json | 0 {tests => archive-arch/tests}/smartctl_nvme_ssd.json | 0 {tests => archive-arch/tests}/test_disks.py | 0 {tests => archive-arch/tests}/test_vuln.py | 0 {wrappers => archive-arch/wrappers}/login_wrapper.sh | 0 {wrappers => archive-arch/wrappers}/send_local_login_alert.sh | 0 {wrappers => archive-arch/wrappers}/send_login_alert.sh | 0 32 files changed, 0 insertions(+), 0 deletions(-) rename .gitignore => archive-arch/.gitignore (100%) rename LICENSE => archive-arch/LICENSE (100%) rename README.md => archive-arch/README.md (100%) rename {alerting => archive-arch/alerting}/alerts.py (100%) rename {alerting => archive-arch/alerting}/delayed.py (100%) rename {alerting => archive-arch/alerting}/enum.py (100%) rename {alerting => archive-arch/alerting}/login.py (100%) rename {assets => archive-arch/assets}/lego-login-alert (100%) rename {assets => archive-arch/assets}/lego-monitoring.service (100%) rename config.example.json => archive-arch/config.example.json (100%) rename {misc => archive-arch/misc}/checkers.py (100%) rename {misc => archive-arch/misc}/checks.py (100%) rename {misc => archive-arch/misc}/common.py (100%) rename {misc => archive-arch/misc}/config.py (100%) rename {misc => archive-arch/misc}/cvars.py (100%) rename {misc => archive-arch/misc}/disks.py (100%) rename {misc => archive-arch/misc}/docker_registry.py (100%) rename {misc => archive-arch/misc}/sensors.py (100%) rename {misc => archive-arch/misc}/vuln.py (100%) rename prettyprint.py => archive-arch/prettyprint.py (100%) rename requirements.txt => archive-arch/requirements.txt (100%) rename send_login_alert.py => archive-arch/send_login_alert.py (100%) rename send_ups_alert.py => archive-arch/send_ups_alert.py (100%) rename service.py => archive-arch/service.py (100%) rename {tests => archive-arch/tests}/smartctl_ata_hdd.json (100%) rename {tests => archive-arch/tests}/smartctl_ata_ssd.json (100%) rename {tests => archive-arch/tests}/smartctl_nvme_ssd.json (100%) rename {tests => archive-arch/tests}/test_disks.py (100%) rename {tests => archive-arch/tests}/test_vuln.py (100%) rename {wrappers => archive-arch/wrappers}/login_wrapper.sh (100%) rename {wrappers => archive-arch/wrappers}/send_local_login_alert.sh (100%) rename {wrappers => archive-arch/wrappers}/send_login_alert.sh (100%) diff --git a/.gitignore b/archive-arch/.gitignore similarity index 100% rename from .gitignore rename to archive-arch/.gitignore diff --git a/LICENSE b/archive-arch/LICENSE similarity index 100% rename from LICENSE rename to archive-arch/LICENSE diff --git a/README.md b/archive-arch/README.md similarity index 100% rename from README.md rename to archive-arch/README.md diff --git a/alerting/alerts.py b/archive-arch/alerting/alerts.py similarity index 100% rename from alerting/alerts.py rename to archive-arch/alerting/alerts.py diff --git a/alerting/delayed.py b/archive-arch/alerting/delayed.py similarity index 100% rename from alerting/delayed.py rename to archive-arch/alerting/delayed.py diff --git a/alerting/enum.py b/archive-arch/alerting/enum.py similarity index 100% rename from alerting/enum.py rename to archive-arch/alerting/enum.py diff --git a/alerting/login.py b/archive-arch/alerting/login.py similarity index 100% rename from alerting/login.py rename to archive-arch/alerting/login.py diff --git a/assets/lego-login-alert b/archive-arch/assets/lego-login-alert similarity index 100% rename from assets/lego-login-alert rename to archive-arch/assets/lego-login-alert diff --git a/assets/lego-monitoring.service b/archive-arch/assets/lego-monitoring.service similarity index 100% rename from assets/lego-monitoring.service rename to archive-arch/assets/lego-monitoring.service diff --git a/config.example.json b/archive-arch/config.example.json similarity index 100% rename from config.example.json rename to archive-arch/config.example.json diff --git a/misc/checkers.py b/archive-arch/misc/checkers.py similarity index 100% rename from misc/checkers.py rename to archive-arch/misc/checkers.py diff --git a/misc/checks.py b/archive-arch/misc/checks.py similarity index 100% rename from misc/checks.py rename to archive-arch/misc/checks.py diff --git a/misc/common.py b/archive-arch/misc/common.py similarity index 100% rename from misc/common.py rename to archive-arch/misc/common.py diff --git a/misc/config.py b/archive-arch/misc/config.py similarity index 100% rename from misc/config.py rename to archive-arch/misc/config.py diff --git a/misc/cvars.py b/archive-arch/misc/cvars.py similarity index 100% rename from misc/cvars.py rename to archive-arch/misc/cvars.py diff --git a/misc/disks.py b/archive-arch/misc/disks.py similarity index 100% rename from misc/disks.py rename to archive-arch/misc/disks.py diff --git a/misc/docker_registry.py b/archive-arch/misc/docker_registry.py similarity index 100% rename from misc/docker_registry.py rename to archive-arch/misc/docker_registry.py diff --git a/misc/sensors.py b/archive-arch/misc/sensors.py similarity index 100% rename from misc/sensors.py rename to archive-arch/misc/sensors.py diff --git a/misc/vuln.py b/archive-arch/misc/vuln.py similarity index 100% rename from misc/vuln.py rename to archive-arch/misc/vuln.py diff --git a/prettyprint.py b/archive-arch/prettyprint.py similarity index 100% rename from prettyprint.py rename to archive-arch/prettyprint.py diff --git a/requirements.txt b/archive-arch/requirements.txt similarity index 100% rename from requirements.txt rename to archive-arch/requirements.txt diff --git a/send_login_alert.py b/archive-arch/send_login_alert.py similarity index 100% rename from send_login_alert.py rename to archive-arch/send_login_alert.py diff --git a/send_ups_alert.py b/archive-arch/send_ups_alert.py similarity index 100% rename from send_ups_alert.py rename to archive-arch/send_ups_alert.py diff --git a/service.py b/archive-arch/service.py similarity index 100% rename from service.py rename to archive-arch/service.py diff --git a/tests/smartctl_ata_hdd.json b/archive-arch/tests/smartctl_ata_hdd.json similarity index 100% rename from tests/smartctl_ata_hdd.json rename to archive-arch/tests/smartctl_ata_hdd.json diff --git a/tests/smartctl_ata_ssd.json b/archive-arch/tests/smartctl_ata_ssd.json similarity index 100% rename from tests/smartctl_ata_ssd.json rename to archive-arch/tests/smartctl_ata_ssd.json diff --git a/tests/smartctl_nvme_ssd.json b/archive-arch/tests/smartctl_nvme_ssd.json similarity index 100% rename from tests/smartctl_nvme_ssd.json rename to archive-arch/tests/smartctl_nvme_ssd.json diff --git a/tests/test_disks.py b/archive-arch/tests/test_disks.py similarity index 100% rename from tests/test_disks.py rename to archive-arch/tests/test_disks.py diff --git a/tests/test_vuln.py b/archive-arch/tests/test_vuln.py similarity index 100% rename from tests/test_vuln.py rename to archive-arch/tests/test_vuln.py diff --git a/wrappers/login_wrapper.sh b/archive-arch/wrappers/login_wrapper.sh similarity index 100% rename from wrappers/login_wrapper.sh rename to archive-arch/wrappers/login_wrapper.sh diff --git a/wrappers/send_local_login_alert.sh b/archive-arch/wrappers/send_local_login_alert.sh similarity index 100% rename from wrappers/send_local_login_alert.sh rename to archive-arch/wrappers/send_local_login_alert.sh diff --git a/wrappers/send_login_alert.sh b/archive-arch/wrappers/send_login_alert.sh similarity index 100% rename from wrappers/send_login_alert.sh rename to archive-arch/wrappers/send_login_alert.sh From 83a5ff3909565edf4bf243c16f27c5656d530e7d Mon Sep 17 00:00:00 2001 From: Alex Tau Date: Sun, 27 Apr 2025 21:42:05 +0300 Subject: [PATCH 02/56] hello, uv2nix world --- README.md | 0 flake.lock | 99 +++++++++++++++++ flake.nix | 212 ++++++++++++++++++++++++++++++++++++ pyproject.toml | 21 ++++ src/hello_world/__init__.py | 2 + uv.lock | 56 ++++++++++ 6 files changed, 390 insertions(+) create mode 100644 README.md create mode 100644 flake.lock create mode 100644 flake.nix create mode 100644 pyproject.toml create mode 100644 src/hello_world/__init__.py create mode 100644 uv.lock diff --git a/README.md b/README.md new file mode 100644 index 0000000..e69de29 diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..2d367f7 --- /dev/null +++ b/flake.lock @@ -0,0 +1,99 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1745487689, + "narHash": "sha256-FQoi3R0NjQeBAsEOo49b5tbDPcJSMWc3QhhaIi9eddw=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "5630cf13cceac06cefe9fc607e8dfa8fb342dde3", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-24.11", + "repo": "nixpkgs", + "type": "github" + } + }, + "pyproject-build-systems": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ], + "pyproject-nix": [ + "pyproject-nix" + ], + "uv2nix": [ + "uv2nix" + ] + }, + "locked": { + "lastModified": 1744599653, + "narHash": "sha256-nysSwVVjG4hKoOjhjvE6U5lIKA8sEr1d1QzEfZsannU=", + "owner": "pyproject-nix", + "repo": "build-system-pkgs", + "rev": "7dba6dbc73120e15b558754c26024f6c93015dd7", + "type": "github" + }, + "original": { + "owner": "pyproject-nix", + "repo": "build-system-pkgs", + "type": "github" + } + }, + "pyproject-nix": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1743438845, + "narHash": "sha256-1GSaoubGtvsLRwoYwHjeKYq40tLwvuFFVhGrG8J9Oek=", + "owner": "pyproject-nix", + "repo": "pyproject.nix", + "rev": "8063ec98edc459571d042a640b1c5e334ecfca1e", + "type": "github" + }, + "original": { + "owner": "pyproject-nix", + "repo": "pyproject.nix", + "type": "github" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs", + "pyproject-build-systems": "pyproject-build-systems", + "pyproject-nix": "pyproject-nix", + "uv2nix": "uv2nix" + } + }, + "uv2nix": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ], + "pyproject-nix": [ + "pyproject-nix" + ] + }, + "locked": { + "lastModified": 1745697651, + "narHash": "sha256-r4A/fkiCenEapHkjJWPiNUZEfviuXMCr6mRozJ5dC4o=", + "owner": "pyproject-nix", + "repo": "uv2nix", + "rev": "cb6508484d534dafd097713b575f2aebc3417de0", + "type": "github" + }, + "original": { + "owner": "pyproject-nix", + "repo": "uv2nix", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..f2b406e --- /dev/null +++ b/flake.nix @@ -0,0 +1,212 @@ +{ + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.11"; + + pyproject-nix = { + url = "github:pyproject-nix/pyproject.nix"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + + uv2nix = { + url = "github:pyproject-nix/uv2nix"; + inputs.pyproject-nix.follows = "pyproject-nix"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + + pyproject-build-systems = { + url = "github:pyproject-nix/build-system-pkgs"; + inputs.pyproject-nix.follows = "pyproject-nix"; + inputs.uv2nix.follows = "uv2nix"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + }; + + outputs = + { + self, + nixpkgs, + uv2nix, + pyproject-nix, + pyproject-build-systems, + ... + }: + let + inherit (nixpkgs) lib; + + # Load a uv workspace from a workspace root. + # Uv2nix treats all uv projects as workspace projects. + workspace = uv2nix.lib.workspace.loadWorkspace { workspaceRoot = ./.; }; + + # Create package overlay from workspace. + overlay = workspace.mkPyprojectOverlay { + # Prefer prebuilt binary wheels as a package source. + # Sdists are less likely to "just work" because of the metadata missing from uv.lock. + # Binary wheels are more likely to, but may still require overrides for library dependencies. + sourcePreference = "wheel"; # or sourcePreference = "sdist"; + # Optionally customise PEP 508 environment + # environ = { + # platform_release = "5.10.65"; + # }; + }; + + # Extend generated overlay with build fixups + # + # Uv2nix can only work with what it has, and uv.lock is missing essential metadata to perform some builds. + # This is an additional overlay implementing build fixups. + # See: + # - https://pyproject-nix.github.io/uv2nix/FAQ.html + pyprojectOverrides = _final: _prev: { + # Implement build fixups here. + # Note that uv2nix is _not_ using Nixpkgs buildPythonPackage. + # It's using https://pyproject-nix.github.io/pyproject.nix/build.html + }; + + # This example is only using x86_64-linux + pkgs = nixpkgs.legacyPackages.x86_64-linux; + + # Use Python 3.12 from nixpkgs + python = pkgs.python312; + + # Construct package set + pythonSet = + # Use base package set from pyproject.nix builders + (pkgs.callPackage pyproject-nix.build.packages { + inherit python; + }).overrideScope + ( + lib.composeManyExtensions [ + pyproject-build-systems.overlays.default + overlay + pyprojectOverrides + ] + ); + + in + { + # Package a virtual environment as our main application. + # + # Enable no optional dependencies for production build. + packages.x86_64-linux.default = pythonSet.mkVirtualEnv "hello-world-env" workspace.deps.default; + + # Make hello runnable with `nix run` + apps.x86_64-linux = { + default = { + type = "app"; + program = "${self.packages.x86_64-linux.default}/bin/hello"; + }; + }; + + # This example provides two different modes of development: + # - Impurely using uv to manage virtual environments + # - Pure development using uv2nix to manage virtual environments + devShells.x86_64-linux = { + # It is of course perfectly OK to keep using an impure virtualenv workflow and only use uv2nix to build packages. + # This devShell simply adds Python and undoes the dependency leakage done by Nixpkgs Python infrastructure. + impure = pkgs.mkShell { + packages = [ + python + pkgs.uv + ]; + env = + { + # Prevent uv from managing Python downloads + UV_PYTHON_DOWNLOADS = "never"; + # Force uv to use nixpkgs Python interpreter + UV_PYTHON = python.interpreter; + } + // lib.optionalAttrs pkgs.stdenv.isLinux { + # Python libraries often load native shared objects using dlopen(3). + # Setting LD_LIBRARY_PATH makes the dynamic library loader aware of libraries without using RPATH for lookup. + LD_LIBRARY_PATH = lib.makeLibraryPath pkgs.pythonManylinuxPackages.manylinux1; + }; + shellHook = '' + unset PYTHONPATH + ''; + }; + + # This devShell uses uv2nix to construct a virtual environment purely from Nix, using the same dependency specification as the application. + # The notable difference is that we also apply another overlay here enabling editable mode ( https://setuptools.pypa.io/en/latest/userguide/development_mode.html ). + # + # This means that any changes done to your local files do not require a rebuild. + # + # Note: Editable package support is still unstable and subject to change. + uv2nix = + let + # Create an overlay enabling editable mode for all local dependencies. + editableOverlay = workspace.mkEditablePyprojectOverlay { + # Use environment variable + root = "$REPO_ROOT"; + # Optional: Only enable editable for these packages + # members = [ "hello-world" ]; + }; + + # Override previous set with our overrideable overlay. + editablePythonSet = pythonSet.overrideScope ( + lib.composeManyExtensions [ + editableOverlay + + # Apply fixups for building an editable package of your workspace packages + (final: prev: { + hello-world = prev.hello-world.overrideAttrs (old: { + # It's a good idea to filter the sources going into an editable build + # so the editable package doesn't have to be rebuilt on every change. + src = lib.fileset.toSource { + root = old.src; + fileset = lib.fileset.unions [ + (old.src + "/pyproject.toml") + (old.src + "/README.md") + (old.src + "/src/hello_world/__init__.py") + ]; + }; + + # Hatchling (our build system) has a dependency on the `editables` package when building editables. + # + # In normal Python flows this dependency is dynamically handled, and doesn't need to be explicitly declared. + # This behaviour is documented in PEP-660. + # + # With Nix the dependency needs to be explicitly declared. + nativeBuildInputs = + old.nativeBuildInputs + ++ final.resolveBuildSystem { + editables = [ ]; + }; + }); + + }) + ] + ); + + # Build virtual environment, with local packages being editable. + # + # Enable all optional dependencies for development. + virtualenv = editablePythonSet.mkVirtualEnv "hello-world-dev-env" workspace.deps.all; + + in + pkgs.mkShell { + packages = [ + virtualenv + pkgs.uv + ]; + + env = { + # Don't create venv using uv + UV_NO_SYNC = "1"; + + # Force uv to use Python interpreter from venv + UV_PYTHON = "${virtualenv}/bin/python"; + + # Prevent uv from downloading managed Python's + UV_PYTHON_DOWNLOADS = "never"; + }; + + shellHook = '' + # Undo dependency propagation by nixpkgs. + unset PYTHONPATH + + # Get repository root using git. This is expanded at runtime by the editable `.pth` machinery. + export REPO_ROOT=$(git rev-parse --show-toplevel) + ''; + }; + }; + }; +} diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..95475f0 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,21 @@ +[project] +name = "hello-world" +version = "0.1.0" +description = "Add your description here" +readme = "README.md" +requires-python = ">=3.12" +dependencies = [ + "urllib3>=2.2.3", +] + +[project.scripts] +hello = "hello_world:hello" + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[dependency-groups] +dev = [ + "ruff>=0.6.7", +] diff --git a/src/hello_world/__init__.py b/src/hello_world/__init__.py new file mode 100644 index 0000000..aa5ad22 --- /dev/null +++ b/src/hello_world/__init__.py @@ -0,0 +1,2 @@ +def hello() -> None: + print("Hello from hello-world!") diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..0d5c92e --- /dev/null +++ b/uv.lock @@ -0,0 +1,56 @@ +version = 1 +revision = 1 +requires-python = ">=3.12" + +[[package]] +name = "hello-world" +version = "0.1.0" +source = { editable = "." } +dependencies = [ + { name = "urllib3" }, +] + +[package.dev-dependencies] +dev = [ + { name = "ruff" }, +] + +[package.metadata] +requires-dist = [{ name = "urllib3", specifier = ">=2.2.3" }] + +[package.metadata.requires-dev] +dev = [{ name = "ruff", specifier = ">=0.6.7" }] + +[[package]] +name = "ruff" +version = "0.11.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/90/61/fb87430f040e4e577e784e325351186976516faef17d6fcd921fe28edfd7/ruff-0.11.2.tar.gz", hash = "sha256:ec47591497d5a1050175bdf4e1a4e6272cddff7da88a2ad595e1e326041d8d94", size = 3857511 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/99/102578506f0f5fa29fd7e0df0a273864f79af044757aef73d1cae0afe6ad/ruff-0.11.2-py3-none-linux_armv6l.whl", hash = "sha256:c69e20ea49e973f3afec2c06376eb56045709f0212615c1adb0eda35e8a4e477", size = 10113146 }, + { url = "https://files.pythonhosted.org/packages/74/ad/5cd4ba58ab602a579997a8494b96f10f316e874d7c435bcc1a92e6da1b12/ruff-0.11.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:2c5424cc1c4eb1d8ecabe6d4f1b70470b4f24a0c0171356290b1953ad8f0e272", size = 10867092 }, + { url = "https://files.pythonhosted.org/packages/fc/3e/d3f13619e1d152c7b600a38c1a035e833e794c6625c9a6cea6f63dbf3af4/ruff-0.11.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:ecf20854cc73f42171eedb66f006a43d0a21bfb98a2523a809931cda569552d9", size = 10224082 }, + { url = "https://files.pythonhosted.org/packages/90/06/f77b3d790d24a93f38e3806216f263974909888fd1e826717c3ec956bbcd/ruff-0.11.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0c543bf65d5d27240321604cee0633a70c6c25c9a2f2492efa9f6d4b8e4199bb", size = 10394818 }, + { url = "https://files.pythonhosted.org/packages/99/7f/78aa431d3ddebfc2418cd95b786642557ba8b3cb578c075239da9ce97ff9/ruff-0.11.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:20967168cc21195db5830b9224be0e964cc9c8ecf3b5a9e3ce19876e8d3a96e3", size = 9952251 }, + { url = "https://files.pythonhosted.org/packages/30/3e/f11186d1ddfaca438c3bbff73c6a2fdb5b60e6450cc466129c694b0ab7a2/ruff-0.11.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:955a9ce63483999d9f0b8f0b4a3ad669e53484232853054cc8b9d51ab4c5de74", size = 11563566 }, + { url = "https://files.pythonhosted.org/packages/22/6c/6ca91befbc0a6539ee133d9a9ce60b1a354db12c3c5d11cfdbf77140f851/ruff-0.11.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:86b3a27c38b8fce73bcd262b0de32e9a6801b76d52cdb3ae4c914515f0cef608", size = 12208721 }, + { url = "https://files.pythonhosted.org/packages/19/b0/24516a3b850d55b17c03fc399b681c6a549d06ce665915721dc5d6458a5c/ruff-0.11.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a3b66a03b248c9fcd9d64d445bafdf1589326bee6fc5c8e92d7562e58883e30f", size = 11662274 }, + { url = "https://files.pythonhosted.org/packages/d7/65/76be06d28ecb7c6070280cef2bcb20c98fbf99ff60b1c57d2fb9b8771348/ruff-0.11.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0397c2672db015be5aa3d4dac54c69aa012429097ff219392c018e21f5085147", size = 13792284 }, + { url = "https://files.pythonhosted.org/packages/ce/d2/4ceed7147e05852876f3b5f3fdc23f878ce2b7e0b90dd6e698bda3d20787/ruff-0.11.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:869bcf3f9abf6457fbe39b5a37333aa4eecc52a3b99c98827ccc371a8e5b6f1b", size = 11327861 }, + { url = "https://files.pythonhosted.org/packages/c4/78/4935ecba13706fd60ebe0e3dc50371f2bdc3d9bc80e68adc32ff93914534/ruff-0.11.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:2a2b50ca35457ba785cd8c93ebbe529467594087b527a08d487cf0ee7b3087e9", size = 10276560 }, + { url = "https://files.pythonhosted.org/packages/81/7f/1b2435c3f5245d410bb5dc80f13ec796454c21fbda12b77d7588d5cf4e29/ruff-0.11.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:7c69c74bf53ddcfbc22e6eb2f31211df7f65054bfc1f72288fc71e5f82db3eab", size = 9945091 }, + { url = "https://files.pythonhosted.org/packages/39/c4/692284c07e6bf2b31d82bb8c32f8840f9d0627d92983edaac991a2b66c0a/ruff-0.11.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:6e8fb75e14560f7cf53b15bbc55baf5ecbe373dd5f3aab96ff7aa7777edd7630", size = 10977133 }, + { url = "https://files.pythonhosted.org/packages/94/cf/8ab81cb7dd7a3b0a3960c2769825038f3adcd75faf46dd6376086df8b128/ruff-0.11.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:842a472d7b4d6f5924e9297aa38149e5dcb1e628773b70e6387ae2c97a63c58f", size = 11378514 }, + { url = "https://files.pythonhosted.org/packages/d9/3a/a647fa4f316482dacf2fd68e8a386327a33d6eabd8eb2f9a0c3d291ec549/ruff-0.11.2-py3-none-win32.whl", hash = "sha256:aca01ccd0eb5eb7156b324cfaa088586f06a86d9e5314b0eb330cb48415097cc", size = 10319835 }, + { url = "https://files.pythonhosted.org/packages/86/54/3c12d3af58012a5e2cd7ebdbe9983f4834af3f8cbea0e8a8c74fa1e23b2b/ruff-0.11.2-py3-none-win_amd64.whl", hash = "sha256:3170150172a8f994136c0c66f494edf199a0bbea7a409f649e4bc8f4d7084080", size = 11373713 }, + { url = "https://files.pythonhosted.org/packages/d6/d4/dd813703af8a1e2ac33bf3feb27e8a5ad514c9f219df80c64d69807e7f71/ruff-0.11.2-py3-none-win_arm64.whl", hash = "sha256:52933095158ff328f4c77af3d74f0379e34fd52f175144cefc1b192e7ccd32b4", size = 10441990 }, +] + +[[package]] +name = "urllib3" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/63/e53da845320b757bf29ef6a9062f5c669fe997973f966045cb019c3f4b66/urllib3-2.3.0.tar.gz", hash = "sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d", size = 307268 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/19/4ec628951a74043532ca2cf5d97b7b14863931476d117c471e8e2b1eb39f/urllib3-2.3.0-py3-none-any.whl", hash = "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df", size = 128369 }, +] From 75ff4edeed70d8e60c98f01114e7d92e6e8d2642 Mon Sep 17 00:00:00 2001 From: Alex Tau Date: Sun, 27 Apr 2025 21:48:18 +0300 Subject: [PATCH 03/56] can I at least change the project name? kthxbye --- flake.nix | 12 ++++++------ pyproject.toml | 10 +++++----- src/{hello_world => lego_monitoring}/__init__.py | 2 +- 3 files changed, 12 insertions(+), 12 deletions(-) rename src/{hello_world => lego_monitoring}/__init__.py (63%) diff --git a/flake.nix b/flake.nix index f2b406e..109ef27 100644 --- a/flake.nix +++ b/flake.nix @@ -86,13 +86,13 @@ # Package a virtual environment as our main application. # # Enable no optional dependencies for production build. - packages.x86_64-linux.default = pythonSet.mkVirtualEnv "hello-world-env" workspace.deps.default; + packages.x86_64-linux.default = pythonSet.mkVirtualEnv "lego-monitoring-env" workspace.deps.default; - # Make hello runnable with `nix run` + # Make service runnable with `nix run` apps.x86_64-linux = { default = { type = "app"; - program = "${self.packages.x86_64-linux.default}/bin/hello"; + program = "${self.packages.x86_64-linux.default}/bin/service"; }; }; @@ -147,7 +147,7 @@ # Apply fixups for building an editable package of your workspace packages (final: prev: { - hello-world = prev.hello-world.overrideAttrs (old: { + lego-monitoring = prev.lego-monitoring.overrideAttrs (old: { # It's a good idea to filter the sources going into an editable build # so the editable package doesn't have to be rebuilt on every change. src = lib.fileset.toSource { @@ -155,7 +155,7 @@ fileset = lib.fileset.unions [ (old.src + "/pyproject.toml") (old.src + "/README.md") - (old.src + "/src/hello_world/__init__.py") + (old.src + "/src/lego_monitoring/__init__.py") ]; }; @@ -179,7 +179,7 @@ # Build virtual environment, with local packages being editable. # # Enable all optional dependencies for development. - virtualenv = editablePythonSet.mkVirtualEnv "hello-world-dev-env" workspace.deps.all; + virtualenv = editablePythonSet.mkVirtualEnv "lego-monitoring-dev-env" workspace.deps.all; in pkgs.mkShell { diff --git a/pyproject.toml b/pyproject.toml index 95475f0..52ead61 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,15 +1,15 @@ [project] -name = "hello-world" +name = "lego-monitoring" version = "0.1.0" -description = "Add your description here" +description = "Monitoring software for the lego server" readme = "README.md" requires-python = ">=3.12" dependencies = [ - "urllib3>=2.2.3", + #"urllib3>=2.2.3", ] [project.scripts] -hello = "hello_world:hello" +service = "lego_monitoring:main" [build-system] requires = ["hatchling"] @@ -17,5 +17,5 @@ build-backend = "hatchling.build" [dependency-groups] dev = [ - "ruff>=0.6.7", + #"ruff>=0.6.7", ] diff --git a/src/hello_world/__init__.py b/src/lego_monitoring/__init__.py similarity index 63% rename from src/hello_world/__init__.py rename to src/lego_monitoring/__init__.py index aa5ad22..6ee5cb9 100644 --- a/src/hello_world/__init__.py +++ b/src/lego_monitoring/__init__.py @@ -1,2 +1,2 @@ -def hello() -> None: +def main() -> None: print("Hello from hello-world!") From 96e98a304470ccf684906ebe3010a00048c77571 Mon Sep 17 00:00:00 2001 From: Alex Tau Date: Sun, 27 Apr 2025 22:33:31 +0300 Subject: [PATCH 04/56] main() is a service --- src/lego_monitoring/__init__.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/lego_monitoring/__init__.py b/src/lego_monitoring/__init__.py index 6ee5cb9..57b015c 100644 --- a/src/lego_monitoring/__init__.py +++ b/src/lego_monitoring/__init__.py @@ -1,2 +1,6 @@ +import time + def main() -> None: - print("Hello from hello-world!") + while True: + print("service running...") + time.sleep(300) From cda20a654f26f0c7bc91c83c9bf712ca53f83bc4 Mon Sep 17 00:00:00 2001 From: Alex Tau Date: Mon, 28 Apr 2025 18:11:42 +0300 Subject: [PATCH 05/56] nixos module that does nothing --- flake.nix | 2 ++ modules/default.nix | 14 ++++++++++++++ 2 files changed, 16 insertions(+) create mode 100644 modules/default.nix diff --git a/flake.nix b/flake.nix index 109ef27..292a023 100644 --- a/flake.nix +++ b/flake.nix @@ -83,6 +83,8 @@ in { + nixosModules.default = (import ./modules/default.nix) self.packages.x86_64-linux.default; + # Package a virtual environment as our main application. # # Enable no optional dependencies for production build. diff --git a/modules/default.nix b/modules/default.nix new file mode 100644 index 0000000..3eb2d71 --- /dev/null +++ b/modules/default.nix @@ -0,0 +1,14 @@ +package: + +{ + lib, + ... +}: + +{ + options.services.lego-monitoring = { + enable = lib.mkEnableOption "Enable lego-monitoring service." + }; + + config = {}; +} From 4d9724080efc1fcdd9de18292e172a37ade0f232 Mon Sep 17 00:00:00 2001 From: Alex Tau Date: Mon, 28 Apr 2025 18:18:10 +0300 Subject: [PATCH 06/56] mkEnableOption prefixes with "Whether to enable" anyway --- modules/default.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/default.nix b/modules/default.nix index 3eb2d71..ecc5588 100644 --- a/modules/default.nix +++ b/modules/default.nix @@ -7,7 +7,7 @@ package: { options.services.lego-monitoring = { - enable = lib.mkEnableOption "Enable lego-monitoring service." + enable = lib.mkEnableOption "lego-monitoring service." }; config = {}; From 19984b43f468fe61feda09d84013a5bcefc18db2 Mon Sep 17 00:00:00 2001 From: Alex Tau Date: Mon, 28 Apr 2025 18:19:05 +0300 Subject: [PATCH 07/56] fix missing ; --- modules/default.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/default.nix b/modules/default.nix index ecc5588..d8b08d3 100644 --- a/modules/default.nix +++ b/modules/default.nix @@ -7,7 +7,7 @@ package: { options.services.lego-monitoring = { - enable = lib.mkEnableOption "lego-monitoring service." + enable = lib.mkEnableOption "lego-monitoring service."; }; config = {}; From f158bc37783b7b632fcb2aad4240aec2fec4bb57 Mon Sep 17 00:00:00 2001 From: Alex Tau Date: Mon, 28 Apr 2025 20:04:04 +0300 Subject: [PATCH 08/56] stub service --- .gitignore | 3 ++ modules/default.nix | 30 +++++++++++++++- pyproject.toml | 7 +--- src/lego_monitoring/__init__.py | 15 +++++++- src/lego_monitoring/core/config.py | 32 +++++++++++++++++ uv.lock | 57 +++++++----------------------- 6 files changed, 92 insertions(+), 52 deletions(-) create mode 100644 .gitignore create mode 100644 src/lego_monitoring/core/config.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d13d903 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +__pycache__ +.venv +/result diff --git a/modules/default.nix b/modules/default.nix index d8b08d3..d5d9d59 100644 --- a/modules/default.nix +++ b/modules/default.nix @@ -1,14 +1,42 @@ package: { + config, lib, + pkgs, ... }: { options.services.lego-monitoring = { enable = lib.mkEnableOption "lego-monitoring service."; + + nonSecretConfigOption = lib.mkOption { + type = lib.types.str; + default = "defaultValue"; + description = "An example non-secret config option."; + }; + + configOptionSecretPath = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = "Path to an example secret config option."; + }; }; - config = {}; + config = let + cfg = config.services.lego-monitoring; + json = pkgs.formats.json {}; + serviceConfigFile = json.generate "config.json" { + non_secret_config_option = cfg.nonSecretConfigOption; + config_option_secret_path = cfg.configOptionSecretPath; + }; + in lib.mkIf cfg.enable { + systemd.services.lego-monitoring = { + name = "lego-monitoring.service"; + description = "Lego-monitoring service"; + script = "${package}/bin/service -c ${serviceConfigFile}"; + wantedBy = [ "multi-user.target" ]; + }; + }; } diff --git a/pyproject.toml b/pyproject.toml index 52ead61..ed0e578 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ description = "Monitoring software for the lego server" readme = "README.md" requires-python = ">=3.12" dependencies = [ - #"urllib3>=2.2.3", + "alt-utils>=0.0.6", ] [project.scripts] @@ -14,8 +14,3 @@ service = "lego_monitoring:main" [build-system] requires = ["hatchling"] build-backend = "hatchling.build" - -[dependency-groups] -dev = [ - #"ruff>=0.6.7", -] diff --git a/src/lego_monitoring/__init__.py b/src/lego_monitoring/__init__.py index 57b015c..15bd42e 100644 --- a/src/lego_monitoring/__init__.py +++ b/src/lego_monitoring/__init__.py @@ -1,6 +1,19 @@ +import argparse import time +from .core.config import load_config + + def main() -> None: + parser = argparse.ArgumentParser( + prog="lego-monitoring", + description="Lego-monitoring service", + ) + parser.add_argument('-c', '--config', required=True) + + config_path = parser.parse_args().config + config = load_config(config_path) + while True: - print("service running...") + print(f"service running... opt 1 is {config.non_secret_config_option}, opt 2 is secret, but if you really wanna know, it's {config.config_option}", flush=True) time.sleep(300) diff --git a/src/lego_monitoring/core/config.py b/src/lego_monitoring/core/config.py new file mode 100644 index 0000000..25566e8 --- /dev/null +++ b/src/lego_monitoring/core/config.py @@ -0,0 +1,32 @@ +import json +from dataclasses import dataclass +from typing import Optional + +from alt_utils import NestedDeserializableDataclass + + +@dataclass +class Config(NestedDeserializableDataclass): + non_secret_config_option: str + config_option: Optional[str] + +def load_config(filepath: str) -> Config: + with open(filepath) as f: + cfg_dict = json.load(f) + + # load secrets from paths + new_cfg_dict = {} + for k in cfg_dict: + if k.endswith('_secret_path'): + actual_opt_key = k[:-12] + secret_path = cfg_dict[k] + if secret_path is None: + new_cfg_dict[actual_opt_key] = None + else: + with open(secret_path) as sf: + new_cfg_dict[actual_opt_key] = sf.read().rstrip() + else: + new_cfg_dict[k] = cfg_dict[k] + + cfg = Config.from_dict(new_cfg_dict) + return cfg diff --git a/uv.lock b/uv.lock index 0d5c92e..7934399 100644 --- a/uv.lock +++ b/uv.lock @@ -1,56 +1,25 @@ version = 1 -revision = 1 requires-python = ">=3.12" [[package]] -name = "hello-world" +name = "alt-utils" +version = "0.0.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/22/d2/b4a3ea37f773696b07a545e8964c37e98e4939d5f8e3dae949d2cd4e4f53/alt_utils-0.0.6.tar.gz", hash = "sha256:91b8ca633238e819848e1f8b351892f4c148c7fddef120d5e966e3a0b5d06f81", size = 6001 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/27/0c963d6c64150e3fb2f98eb01773e2f9cf9b51f5b65632944bff67a68ec2/alt_utils-0.0.6-py3-none-any.whl", hash = "sha256:e4fd04394827eb49ae0d835f645ea03de1d9637a77acd5674a35890ae22abbef", size = 6260 }, +] + +[[package]] +name = "lego-monitoring" version = "0.1.0" source = { editable = "." } dependencies = [ - { name = "urllib3" }, -] - -[package.dev-dependencies] -dev = [ - { name = "ruff" }, + { name = "alt-utils" }, ] [package.metadata] -requires-dist = [{ name = "urllib3", specifier = ">=2.2.3" }] +requires-dist = [{ name = "alt-utils", specifier = ">=0.0.6" }] [package.metadata.requires-dev] -dev = [{ name = "ruff", specifier = ">=0.6.7" }] - -[[package]] -name = "ruff" -version = "0.11.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/90/61/fb87430f040e4e577e784e325351186976516faef17d6fcd921fe28edfd7/ruff-0.11.2.tar.gz", hash = "sha256:ec47591497d5a1050175bdf4e1a4e6272cddff7da88a2ad595e1e326041d8d94", size = 3857511 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/62/99/102578506f0f5fa29fd7e0df0a273864f79af044757aef73d1cae0afe6ad/ruff-0.11.2-py3-none-linux_armv6l.whl", hash = "sha256:c69e20ea49e973f3afec2c06376eb56045709f0212615c1adb0eda35e8a4e477", size = 10113146 }, - { url = "https://files.pythonhosted.org/packages/74/ad/5cd4ba58ab602a579997a8494b96f10f316e874d7c435bcc1a92e6da1b12/ruff-0.11.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:2c5424cc1c4eb1d8ecabe6d4f1b70470b4f24a0c0171356290b1953ad8f0e272", size = 10867092 }, - { url = "https://files.pythonhosted.org/packages/fc/3e/d3f13619e1d152c7b600a38c1a035e833e794c6625c9a6cea6f63dbf3af4/ruff-0.11.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:ecf20854cc73f42171eedb66f006a43d0a21bfb98a2523a809931cda569552d9", size = 10224082 }, - { url = "https://files.pythonhosted.org/packages/90/06/f77b3d790d24a93f38e3806216f263974909888fd1e826717c3ec956bbcd/ruff-0.11.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0c543bf65d5d27240321604cee0633a70c6c25c9a2f2492efa9f6d4b8e4199bb", size = 10394818 }, - { url = "https://files.pythonhosted.org/packages/99/7f/78aa431d3ddebfc2418cd95b786642557ba8b3cb578c075239da9ce97ff9/ruff-0.11.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:20967168cc21195db5830b9224be0e964cc9c8ecf3b5a9e3ce19876e8d3a96e3", size = 9952251 }, - { url = "https://files.pythonhosted.org/packages/30/3e/f11186d1ddfaca438c3bbff73c6a2fdb5b60e6450cc466129c694b0ab7a2/ruff-0.11.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:955a9ce63483999d9f0b8f0b4a3ad669e53484232853054cc8b9d51ab4c5de74", size = 11563566 }, - { url = "https://files.pythonhosted.org/packages/22/6c/6ca91befbc0a6539ee133d9a9ce60b1a354db12c3c5d11cfdbf77140f851/ruff-0.11.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:86b3a27c38b8fce73bcd262b0de32e9a6801b76d52cdb3ae4c914515f0cef608", size = 12208721 }, - { url = "https://files.pythonhosted.org/packages/19/b0/24516a3b850d55b17c03fc399b681c6a549d06ce665915721dc5d6458a5c/ruff-0.11.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a3b66a03b248c9fcd9d64d445bafdf1589326bee6fc5c8e92d7562e58883e30f", size = 11662274 }, - { url = "https://files.pythonhosted.org/packages/d7/65/76be06d28ecb7c6070280cef2bcb20c98fbf99ff60b1c57d2fb9b8771348/ruff-0.11.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0397c2672db015be5aa3d4dac54c69aa012429097ff219392c018e21f5085147", size = 13792284 }, - { url = "https://files.pythonhosted.org/packages/ce/d2/4ceed7147e05852876f3b5f3fdc23f878ce2b7e0b90dd6e698bda3d20787/ruff-0.11.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:869bcf3f9abf6457fbe39b5a37333aa4eecc52a3b99c98827ccc371a8e5b6f1b", size = 11327861 }, - { url = "https://files.pythonhosted.org/packages/c4/78/4935ecba13706fd60ebe0e3dc50371f2bdc3d9bc80e68adc32ff93914534/ruff-0.11.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:2a2b50ca35457ba785cd8c93ebbe529467594087b527a08d487cf0ee7b3087e9", size = 10276560 }, - { url = "https://files.pythonhosted.org/packages/81/7f/1b2435c3f5245d410bb5dc80f13ec796454c21fbda12b77d7588d5cf4e29/ruff-0.11.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:7c69c74bf53ddcfbc22e6eb2f31211df7f65054bfc1f72288fc71e5f82db3eab", size = 9945091 }, - { url = "https://files.pythonhosted.org/packages/39/c4/692284c07e6bf2b31d82bb8c32f8840f9d0627d92983edaac991a2b66c0a/ruff-0.11.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:6e8fb75e14560f7cf53b15bbc55baf5ecbe373dd5f3aab96ff7aa7777edd7630", size = 10977133 }, - { url = "https://files.pythonhosted.org/packages/94/cf/8ab81cb7dd7a3b0a3960c2769825038f3adcd75faf46dd6376086df8b128/ruff-0.11.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:842a472d7b4d6f5924e9297aa38149e5dcb1e628773b70e6387ae2c97a63c58f", size = 11378514 }, - { url = "https://files.pythonhosted.org/packages/d9/3a/a647fa4f316482dacf2fd68e8a386327a33d6eabd8eb2f9a0c3d291ec549/ruff-0.11.2-py3-none-win32.whl", hash = "sha256:aca01ccd0eb5eb7156b324cfaa088586f06a86d9e5314b0eb330cb48415097cc", size = 10319835 }, - { url = "https://files.pythonhosted.org/packages/86/54/3c12d3af58012a5e2cd7ebdbe9983f4834af3f8cbea0e8a8c74fa1e23b2b/ruff-0.11.2-py3-none-win_amd64.whl", hash = "sha256:3170150172a8f994136c0c66f494edf199a0bbea7a409f649e4bc8f4d7084080", size = 11373713 }, - { url = "https://files.pythonhosted.org/packages/d6/d4/dd813703af8a1e2ac33bf3feb27e8a5ad514c9f219df80c64d69807e7f71/ruff-0.11.2-py3-none-win_arm64.whl", hash = "sha256:52933095158ff328f4c77af3d74f0379e34fd52f175144cefc1b192e7ccd32b4", size = 10441990 }, -] - -[[package]] -name = "urllib3" -version = "2.3.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/aa/63/e53da845320b757bf29ef6a9062f5c669fe997973f966045cb019c3f4b66/urllib3-2.3.0.tar.gz", hash = "sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d", size = 307268 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c8/19/4ec628951a74043532ca2cf5d97b7b14863931476d117c471e8e2b1eb39f/urllib3-2.3.0-py3-none-any.whl", hash = "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df", size = 128369 }, -] +dev = [] From ffdd0429b3048404a43c7e9c140695a5a8b48d1d Mon Sep 17 00:00:00 2001 From: Alex Tau Date: Wed, 30 Apr 2025 00:16:27 +0300 Subject: [PATCH 09/56] some actual alerts and telegram client --- flake.nix | 6 +++ modules/default.nix | 28 ++++++---- pyproject.toml | 1 + src/lego_monitoring/__init__.py | 54 +++++++++++++++++-- src/lego_monitoring/alerting/alerts.py | 73 ++++++++++++++++++++++++++ src/lego_monitoring/alerting/enum.py | 23 ++++++++ src/lego_monitoring/core/checkers.py | 53 +++++++++++++++++++ src/lego_monitoring/core/config.py | 43 +++++++++------ src/lego_monitoring/core/cvars.py | 8 +++ uv.lock | 58 ++++++++++++++++++-- 10 files changed, 314 insertions(+), 33 deletions(-) create mode 100644 src/lego_monitoring/alerting/alerts.py create mode 100644 src/lego_monitoring/alerting/enum.py create mode 100644 src/lego_monitoring/core/checkers.py create mode 100644 src/lego_monitoring/core/cvars.py diff --git a/flake.nix b/flake.nix index 292a023..1d8e21e 100644 --- a/flake.nix +++ b/flake.nix @@ -59,6 +59,12 @@ # Implement build fixups here. # Note that uv2nix is _not_ using Nixpkgs buildPythonPackage. # It's using https://pyproject-nix.github.io/pyproject.nix/build.html + + pyaes = _prev.pyaes.overrideAttrs ( + old: { + buildInputs = old.buildInputs or [ ] ++ [ _prev.setuptools ]; + } + ); }; # This example is only using x86_64-linux diff --git a/modules/default.nix b/modules/default.nix index d5d9d59..8808560 100644 --- a/modules/default.nix +++ b/modules/default.nix @@ -11,16 +11,21 @@ package: options.services.lego-monitoring = { enable = lib.mkEnableOption "lego-monitoring service."; - nonSecretConfigOption = lib.mkOption { - type = lib.types.str; - default = "defaultValue"; - description = "An example non-secret config option."; + enabledCheckerSets = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ ]; + description = "List of enabled checker sets. Each checker set is a module which checks something and generates alerts based on check results."; }; - configOptionSecretPath = lib.mkOption { - type = lib.types.nullOr lib.types.str; - default = null; - description = "Path to an example secret config option."; + telegram = { + credsSecretPath = lib.mkOption { + type = lib.types.str; + description = "Path to a file containing Telegram api_id, api_hash, and bot token, separated by the `,` character."; + }; + roomId = lib.mkOption { + type = lib.types.int; + description = "ID of chat where to send alerts."; + }; }; }; @@ -28,8 +33,11 @@ package: cfg = config.services.lego-monitoring; json = pkgs.formats.json {}; serviceConfigFile = json.generate "config.json" { - non_secret_config_option = cfg.nonSecretConfigOption; - config_option_secret_path = cfg.configOptionSecretPath; + enabled_checker_sets = cfg.enabledCheckerSets; + telegram = with cfg.telegram; { + creds_secret_path = credsSecretPath; + room_id = roomId; + }; }; in lib.mkIf cfg.enable { systemd.services.lego-monitoring = { diff --git a/pyproject.toml b/pyproject.toml index ed0e578..11bdaf3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,6 +6,7 @@ readme = "README.md" requires-python = ">=3.12" dependencies = [ "alt-utils>=0.0.6", + "telethon>=1.40.0", ] [project.scripts] diff --git a/src/lego_monitoring/__init__.py b/src/lego_monitoring/__init__.py index 15bd42e..bd1d8f0 100644 --- a/src/lego_monitoring/__init__.py +++ b/src/lego_monitoring/__init__.py @@ -1,19 +1,65 @@ import argparse +import asyncio +import logging +import signal import time +from .alerting import alerts +from .core import cvars from .core.config import load_config +stopping = False + + +def stop_gracefully(signum, frame): + global stopping + stopping = True + def main() -> None: + logging.basicConfig(level=logging.INFO) + + asyncio.run(async_main()) + + +async def async_main(): parser = argparse.ArgumentParser( prog="lego-monitoring", description="Lego-monitoring service", ) - parser.add_argument('-c', '--config', required=True) + parser.add_argument("-c", "--config", required=True) config_path = parser.parse_args().config config = load_config(config_path) + cvars.config.set(config) - while True: - print(f"service running... opt 1 is {config.non_secret_config_option}, opt 2 is secret, but if you really wanna know, it's {config.config_option}", flush=True) - time.sleep(300) + tg_client = await alerts.get_client() + cvars.tg_client.set(tg_client) + + checker_sets = { + "start": [ + alerts.send_start_alert(), + ], + "stop": [], # this is checked later + } + + checkers = [] + for enabled_set in config.enabled_checker_sets: + for checker in checker_sets[enabled_set]: + checkers.append(checker) + + signal.signal(signal.SIGTERM, stop_gracefully) + + async with asyncio.TaskGroup() as tg: + checker_tasks: set[asyncio.Task] = set() + for c in checkers: + task = tg.create_task(c) + checker_tasks.add(task) + while True: + if stopping: + if "stop" in config.enabled_checker_sets: + await alerts.send_stop_alert() + await tg_client.disconnect() + raise SystemExit + else: + await asyncio.sleep(3) diff --git a/src/lego_monitoring/alerting/alerts.py b/src/lego_monitoring/alerting/alerts.py new file mode 100644 index 0000000..e5b4ccf --- /dev/null +++ b/src/lego_monitoring/alerting/alerts.py @@ -0,0 +1,73 @@ +from dataclasses import dataclass + +from telethon import TelegramClient +from telethon.sessions import MemorySession + +from ..core import cvars +from .enum import AlertType, Severity + + +@dataclass +class Alert: + alert_type: AlertType + message: str + severity: Severity + + +async def get_client() -> TelegramClient: + config = cvars.config.get() + api_id, api_hash, bot_token = config.telegram.creds.split(",") + client = await TelegramClient(MemorySession(), api_id, api_hash).start(bot_token=bot_token) + client.parse_mode = "html" + return client + + +def format_message(alert: Alert) -> str: + match alert.severity: + case Severity.INFO: + severity_emoji = "ℹ️" + case Severity.WARNING: + severity_emoji = "⚠️" + case Severity.CRITICAL: + severity_emoji = "🆘" + message = f"{severity_emoji} {alert.alert_type} Alert\n{alert.message}" + return message + + +async def send_alert(alert: Alert) -> None: + try: + client = cvars.tg_client.get() + except LookupError: # being called standalone + # cvars.config.set(get_config()) + # temp_client = True + # client = await get_client() + # cvars.matrix_client.set(client) + raise NotImplementedError # TODO + else: + ... # temp_client = False + room_id = cvars.config.get().telegram.room_id + message = format_message(alert) + await client.send_message(entity=room_id, message=message) + # if temp_client: + # await client.close() + + +async def send_start_alert() -> None: + config = cvars.config.get() + await send_alert( + Alert( + alert_type=AlertType.BOOT, + message=f"Service running with enabled checkers: {', '.join(config.enabled_checker_sets)}", + severity=Severity.INFO, + ) + ) + + +async def send_stop_alert() -> None: + await send_alert( + Alert( + alert_type=AlertType.BOOT, + message="Service stopping.", + severity=Severity.INFO, + ) + ) diff --git a/src/lego_monitoring/alerting/enum.py b/src/lego_monitoring/alerting/enum.py new file mode 100644 index 0000000..1cb6a08 --- /dev/null +++ b/src/lego_monitoring/alerting/enum.py @@ -0,0 +1,23 @@ +from enum import StrEnum + + +class AlertType(StrEnum): + BOOT = "BOOT" + TEST = "TEST" + # ERROR = "ERROR" + # RAM = "RAM" + # CPU = "CPU" + # TEMP = "TEMP" + # VULN = "VULN" + # LOGIN = "LOGIN" + # SMART = "SMART" # TODO + # RAID = "RAID" + # DISKS = "DISKS" + # UPS = "UPS" + # UPDATE = "UPDATE" + + +class Severity(StrEnum): + INFO = "INFO" + WARNING = "WARNING" + CRITICAL = "CRITICAL" diff --git a/src/lego_monitoring/core/checkers.py b/src/lego_monitoring/core/checkers.py new file mode 100644 index 0000000..12db2b8 --- /dev/null +++ b/src/lego_monitoring/core/checkers.py @@ -0,0 +1,53 @@ +import asyncio +import datetime +import logging +from typing import Callable, Coroutine + +from ..alerting import alerts + + +async def _call_check(check: Callable | Coroutine, *args, **kwargs) -> list[alerts.Alert]: + if isinstance(check, Callable): + result = check(*args, **kwargs) + if isinstance(result, Coroutine): + result = await result + elif isinstance(check, Coroutine): + result = await check + else: + raise TypeError(f"check is {type(check)}, neither function nor coroutine") + return result + + +async def interval_checker(check: Callable | Coroutine, interval: datetime.timedelta, *args, **kwargs): + interval_secs = interval.total_seconds() + while True: + logging.info(f"Calling {check.__name__}") + result = await _call_check(check, *args, **kwargs) + logging.info(f"Got {len(result)} alerts") + for alert in result: + await alerts.send_alert(alert) + await asyncio.sleep(interval_secs) + + +async def scheduled_checker( + check: Callable | Coroutine, period: datetime.timedelta, when: datetime.time, *args, **kwargs +): + match period: + case datetime.timedelta(days=1): + while True: + now = datetime.datetime.now() + next_datetime = datetime.datetime.combine(datetime.date.today(), when) + if next_datetime < now: + next_datetime += datetime.timedelta(days=1) + logging.info(f"Scheduled to call {check.__name__} at {next_datetime.isoformat()}") + await asyncio.sleep( + (next_datetime - now).total_seconds() + ) # might be negative at this point, asyncio doesn't care + + logging.info(f"Calling {check.__name__}") + result = await _call_check(check, *args, **kwargs) + logging.info(f"Got {len(result)} alerts") + for alert in result: + await alerts.send_alert(alert) + case _: + raise NotImplementedError diff --git a/src/lego_monitoring/core/config.py b/src/lego_monitoring/core/config.py index 25566e8..71cc766 100644 --- a/src/lego_monitoring/core/config.py +++ b/src/lego_monitoring/core/config.py @@ -1,32 +1,43 @@ import json from dataclasses import dataclass -from typing import Optional from alt_utils import NestedDeserializableDataclass +@dataclass +class TelegramConfig: + creds: str + room_id: int + + @dataclass class Config(NestedDeserializableDataclass): - non_secret_config_option: str - config_option: Optional[str] + enabled_checker_sets: list[str] + telegram: TelegramConfig + def load_config(filepath: str) -> Config: + def load_secrets(d: dict) -> dict: + new_d = {} + for k in d: + if k.endswith("_secret_path"): + actual_opt_key = k[:-12] + secret_path = d[k] + if secret_path is None: + new_d[actual_opt_key] = None + else: + with open(secret_path) as sf: + new_d[actual_opt_key] = sf.read().rstrip() + elif type(d[k]) == dict: + new_d[k] = load_secrets(d[k]) + else: + new_d[k] = d[k] + return new_d + with open(filepath) as f: cfg_dict = json.load(f) - # load secrets from paths - new_cfg_dict = {} - for k in cfg_dict: - if k.endswith('_secret_path'): - actual_opt_key = k[:-12] - secret_path = cfg_dict[k] - if secret_path is None: - new_cfg_dict[actual_opt_key] = None - else: - with open(secret_path) as sf: - new_cfg_dict[actual_opt_key] = sf.read().rstrip() - else: - new_cfg_dict[k] = cfg_dict[k] + new_cfg_dict = load_secrets(cfg_dict) cfg = Config.from_dict(new_cfg_dict) return cfg diff --git a/src/lego_monitoring/core/cvars.py b/src/lego_monitoring/core/cvars.py new file mode 100644 index 0000000..b1f7b50 --- /dev/null +++ b/src/lego_monitoring/core/cvars.py @@ -0,0 +1,8 @@ +from contextvars import ContextVar + +from telethon import TelegramClient + +from .config import Config + +config: ContextVar[Config] = ContextVar("config") +tg_client: ContextVar[TelegramClient] = ContextVar("tg_client") diff --git a/uv.lock b/uv.lock index 7934399..41ce1c9 100644 --- a/uv.lock +++ b/uv.lock @@ -16,10 +16,62 @@ version = "0.1.0" source = { editable = "." } dependencies = [ { name = "alt-utils" }, + { name = "setuptools" }, + { name = "telethon" }, ] [package.metadata] -requires-dist = [{ name = "alt-utils", specifier = ">=0.0.6" }] +requires-dist = [ + { name = "alt-utils", specifier = ">=0.0.6" }, + { name = "setuptools", specifier = ">=80.0.0" }, + { name = "telethon", specifier = ">=1.40.0" }, +] -[package.metadata.requires-dev] -dev = [] +[[package]] +name = "pyaes" +version = "1.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/44/66/2c17bae31c906613795711fc78045c285048168919ace2220daa372c7d72/pyaes-1.6.1.tar.gz", hash = "sha256:02c1b1405c38d3c370b085fb952dd8bea3fadcee6411ad99f312cc129c536d8f", size = 28536 } + +[[package]] +name = "pyasn1" +version = "0.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/e9/01f1a64245b89f039897cb0130016d79f77d52669aae6ee7b159a6c4c018/pyasn1-0.6.1.tar.gz", hash = "sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034", size = 145322 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/f1/d6a797abb14f6283c0ddff96bbdd46937f64122b8c925cab503dd37f8214/pyasn1-0.6.1-py3-none-any.whl", hash = "sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629", size = 83135 }, +] + +[[package]] +name = "rsa" +version = "4.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyasn1" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/da/8a/22b7beea3ee0d44b1916c0c1cb0ee3af23b700b6da9f04991899d0c555d4/rsa-4.9.1.tar.gz", hash = "sha256:e7bdbfdb5497da4c07dfd35530e1a902659db6ff241e39d9953cad06ebd0ae75", size = 29034 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/8d/0133e4eb4beed9e425d9a98ed6e081a55d195481b7632472be1af08d2f6b/rsa-4.9.1-py3-none-any.whl", hash = "sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762", size = 34696 }, +] + +[[package]] +name = "setuptools" +version = "80.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/44/80/97e25f0f1e4067677806084b7382a6ff9979f3d15119375c475c288db9d7/setuptools-80.0.0.tar.gz", hash = "sha256:c40a5b3729d58dd749c0f08f1a07d134fb8a0a3d7f87dc33e7c5e1f762138650", size = 1354221 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/23/63/5517029d6696ddf2bd378d46f63f479be001c31b462303170a1da57650cb/setuptools-80.0.0-py3-none-any.whl", hash = "sha256:a38f898dcd6e5380f4da4381a87ec90bd0a7eec23d204a5552e80ee3cab6bd27", size = 1240907 }, +] + +[[package]] +name = "telethon" +version = "1.40.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyaes" }, + { name = "rsa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/58/af/9b7111e3f63fffe8e55b7ceb8bda023173e2052f420b6debcb25fd2fbc15/telethon-1.40.0.tar.gz", hash = "sha256:40e83326877a2e68b754d4b6d0d1ca5ac924110045b039e02660f2d67add97db", size = 646723 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/5a/c5370edb3215d19a6e858f4169b8eec725ba55f9d39df0f557508048c037/Telethon-1.40.0-py3-none-any.whl", hash = "sha256:146fd4cb2a7afa66bc67f9c2167756096a37b930f65711a3e7399ec9874dcfa7", size = 722013 }, +] From 19ee6f487b0a2f13072ea5450a51fc8c698d51ac Mon Sep 17 00:00:00 2001 From: Alex Tau Date: Wed, 30 Apr 2025 17:24:52 +0300 Subject: [PATCH 10/56] enum instead of str for checker types, use "lego-monitoring" instead of "service" bin name --- flake.nix | 2 +- modules/default.nix | 7 +++++-- pyproject.toml | 2 +- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/flake.nix b/flake.nix index 1d8e21e..a445160 100644 --- a/flake.nix +++ b/flake.nix @@ -100,7 +100,7 @@ apps.x86_64-linux = { default = { type = "app"; - program = "${self.packages.x86_64-linux.default}/bin/service"; + program = "${self.packages.x86_64-linux.default}/bin/lego-monitoring"; }; }; diff --git a/modules/default.nix b/modules/default.nix index 8808560..516c50c 100644 --- a/modules/default.nix +++ b/modules/default.nix @@ -12,7 +12,10 @@ package: enable = lib.mkEnableOption "lego-monitoring service."; enabledCheckerSets = lib.mkOption { - type = lib.types.listOf lib.types.str; + type = lib.types.listOf (lib.types.enum [ + "start" + "stop" + ]); default = [ ]; description = "List of enabled checker sets. Each checker set is a module which checks something and generates alerts based on check results."; }; @@ -43,7 +46,7 @@ package: systemd.services.lego-monitoring = { name = "lego-monitoring.service"; description = "Lego-monitoring service"; - script = "${package}/bin/service -c ${serviceConfigFile}"; + script = "${package}/bin/lego-monitoring -c ${serviceConfigFile}"; wantedBy = [ "multi-user.target" ]; }; }; diff --git a/pyproject.toml b/pyproject.toml index 11bdaf3..5ed90ee 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,7 +10,7 @@ dependencies = [ ] [project.scripts] -service = "lego_monitoring:main" +lego-monitoring = "lego_monitoring:main" [build-system] requires = ["hatchling"] From 758438382debc9df7a73d9e12458080cd2d4de48 Mon Sep 17 00:00:00 2001 From: Alex Tau Date: Fri, 2 May 2025 15:25:27 +0300 Subject: [PATCH 11/56] add temp monitoring --- modules/default.nix | 54 ++++++++++++++- modules/submodules/tempSensorOptions.nix | 49 ++++++++++++++ pyproject.toml | 1 + src/lego_monitoring/__init__.py | 29 ++++++-- src/lego_monitoring/alerting/alerts.py | 2 +- src/lego_monitoring/alerting/enum.py | 2 +- src/lego_monitoring/checks/__init__.py | 1 + src/lego_monitoring/checks/temp/__init__.py | 29 ++++++++ src/lego_monitoring/checks/temp/sensors.py | 66 +++++++++++++++++++ .../{core/config.py => config/__init__.py} | 10 ++- src/lego_monitoring/config/checks/temp.py | 24 +++++++ src/lego_monitoring/core/cvars.py | 2 +- uv.lock | 28 ++++---- 13 files changed, 272 insertions(+), 25 deletions(-) create mode 100644 modules/submodules/tempSensorOptions.nix create mode 100644 src/lego_monitoring/checks/__init__.py create mode 100644 src/lego_monitoring/checks/temp/__init__.py create mode 100644 src/lego_monitoring/checks/temp/sensors.py rename src/lego_monitoring/{core/config.py => config/__init__.py} (84%) create mode 100644 src/lego_monitoring/config/checks/temp.py diff --git a/modules/default.nix b/modules/default.nix index 516c50c..d7f3ab3 100644 --- a/modules/default.nix +++ b/modules/default.nix @@ -7,17 +7,21 @@ package: ... }: +let + tempSensorOptions = (import ./submodules/tempSensorOptions.nix) { inherit lib; }; +in { options.services.lego-monitoring = { enable = lib.mkEnableOption "lego-monitoring service."; - enabledCheckerSets = lib.mkOption { + enabledCheckSets = lib.mkOption { type = lib.types.listOf (lib.types.enum [ "start" "stop" + "temp" ]); default = [ ]; - description = "List of enabled checker sets. Each checker set is a module which checks something and generates alerts based on check results."; + description = "List of enabled check sets. Each check set is a module which checks something and generates alerts based on check results."; }; telegram = { @@ -30,17 +34,61 @@ package: description = "ID of chat where to send alerts."; }; }; + + checks = { + temp = { + sensors = lib.mkOption { + type = lib.types.attrsOf (lib.types.submodule tempSensorOptions); + default = { }; + description = '' + Temp sensor override definitions. Sensors not defined here, or missing options in definitions, will be read with default parameters. + + To get list of sensors and their default configurations, run `lego-monitoring --print-temp`.''; + example = lib.literalExpression '' + { + amdgpu.readings.edge.label = "Integrated GPU"; + k10temp.readings = { + Tctl = { + label = "AMD CPU"; + criticalTemp = 95.0; + }; + Tccd1.enabled = false; + Tccd2.enabled = false; + }; + nvme.readings = { + "Sensor 1".enabled = false; + "Sensor 2".enabled = false; + }; + } + ''; + }; + }; + }; }; config = let cfg = config.services.lego-monitoring; json = pkgs.formats.json {}; serviceConfigFile = json.generate "config.json" { - enabled_checker_sets = cfg.enabledCheckerSets; + enabled_check_sets = cfg.enabledCheckSets; telegram = with cfg.telegram; { creds_secret_path = credsSecretPath; room_id = roomId; }; + checks = { + temp.sensors = lib.mapAttrs (_: sensorCfg: { + + inherit (sensorCfg) name enabled; + readings = lib.mapAttrs (_: readingCfg: { + + inherit (readingCfg) label enabled; + warning_temp = readingCfg.warningTemp; + critical_temp = readingCfg.criticalTemp; + + }) sensorCfg.readings; + + }) cfg.checks.temp.sensors; + }; }; in lib.mkIf cfg.enable { systemd.services.lego-monitoring = { diff --git a/modules/submodules/tempSensorOptions.nix b/modules/submodules/tempSensorOptions.nix new file mode 100644 index 0000000..31d72fc --- /dev/null +++ b/modules/submodules/tempSensorOptions.nix @@ -0,0 +1,49 @@ +{ + lib, +}: + +let + tempReadingOptions = { + options = { + label = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = "Friendly label of the reading."; + }; + enabled = lib.mkOption { + type = lib.types.bool; + default = true; + description = "Whether this reading is enabled."; + }; + warningTemp = lib.mkOption { + type = lib.types.nullOr lib.types.float; + default = null; + description = "Warning temperature threshold."; + }; + criticalTemp = lib.mkOption { + type = lib.types.nullOr lib.types.float; + default = null; + description = "Critical temperature threshold."; + }; + }; + }; +in +{ + options = { + name = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = "Friendly name of the sensor."; + }; + enabled = lib.mkOption { + type = lib.types.bool; + default = true; + description = "Whether sensor is enabled."; + }; + readings = lib.mkOption { + type = lib.types.attrsOf (lib.types.submodule tempReadingOptions); + default = { }; + description = "Overrides for specific readings of the sensor, by label."; + }; + }; +} diff --git a/pyproject.toml b/pyproject.toml index 5ed90ee..f21fd01 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,6 +6,7 @@ readme = "README.md" requires-python = ">=3.12" dependencies = [ "alt-utils>=0.0.6", + "psutil>=7.0.0", "telethon>=1.40.0", ] diff --git a/src/lego_monitoring/__init__.py b/src/lego_monitoring/__init__.py index bd1d8f0..981951e 100644 --- a/src/lego_monitoring/__init__.py +++ b/src/lego_monitoring/__init__.py @@ -1,12 +1,16 @@ import argparse import asyncio +import datetime import logging import signal import time +from . import checks from .alerting import alerts +from .checks.temp.sensors import print_readings +from .config import load_config from .core import cvars -from .core.config import load_config +from .core.checkers import interval_checker stopping = False @@ -27,11 +31,21 @@ async def async_main(): prog="lego-monitoring", description="Lego-monitoring service", ) - parser.add_argument("-c", "--config", required=True) + parser.add_argument("-c", "--config", help="config file") + parser.add_argument("--print-temp", help="print temp sensor readings and exit", action="store_true") + args = parser.parse_args() - config_path = parser.parse_args().config - config = load_config(config_path) - cvars.config.set(config) + if args.config: + config_path = parser.parse_args().config + config = load_config(config_path) + cvars.config.set(config) + + if args.print_temp: + print_readings() + raise SystemExit + + if not args.config: + raise RuntimeError("--config must be specified in standard operating mode") tg_client = await alerts.get_client() cvars.tg_client.set(tg_client) @@ -41,10 +55,11 @@ async def async_main(): alerts.send_start_alert(), ], "stop": [], # this is checked later + "temp": [interval_checker(checks.temp_check, datetime.timedelta(minutes=5))], } checkers = [] - for enabled_set in config.enabled_checker_sets: + for enabled_set in config.enabled_check_sets: for checker in checker_sets[enabled_set]: checkers.append(checker) @@ -57,7 +72,7 @@ async def async_main(): checker_tasks.add(task) while True: if stopping: - if "stop" in config.enabled_checker_sets: + if "stop" in config.enabled_check_sets: await alerts.send_stop_alert() await tg_client.disconnect() raise SystemExit diff --git a/src/lego_monitoring/alerting/alerts.py b/src/lego_monitoring/alerting/alerts.py index e5b4ccf..2241b6e 100644 --- a/src/lego_monitoring/alerting/alerts.py +++ b/src/lego_monitoring/alerting/alerts.py @@ -57,7 +57,7 @@ async def send_start_alert() -> None: await send_alert( Alert( alert_type=AlertType.BOOT, - message=f"Service running with enabled checkers: {', '.join(config.enabled_checker_sets)}", + message=f"Service running with enabled checks: {', '.join(config.enabled_check_sets)}", severity=Severity.INFO, ) ) diff --git a/src/lego_monitoring/alerting/enum.py b/src/lego_monitoring/alerting/enum.py index 1cb6a08..2e121c7 100644 --- a/src/lego_monitoring/alerting/enum.py +++ b/src/lego_monitoring/alerting/enum.py @@ -3,11 +3,11 @@ from enum import StrEnum class AlertType(StrEnum): BOOT = "BOOT" + TEMP = "TEMP" TEST = "TEST" # ERROR = "ERROR" # RAM = "RAM" # CPU = "CPU" - # TEMP = "TEMP" # VULN = "VULN" # LOGIN = "LOGIN" # SMART = "SMART" # TODO diff --git a/src/lego_monitoring/checks/__init__.py b/src/lego_monitoring/checks/__init__.py new file mode 100644 index 0000000..af7a958 --- /dev/null +++ b/src/lego_monitoring/checks/__init__.py @@ -0,0 +1 @@ +from .temp import temp_check diff --git a/src/lego_monitoring/checks/temp/__init__.py b/src/lego_monitoring/checks/temp/__init__.py new file mode 100644 index 0000000..4f965dc --- /dev/null +++ b/src/lego_monitoring/checks/temp/__init__.py @@ -0,0 +1,29 @@ +from lego_monitoring.alerting import alerts +from lego_monitoring.alerting.enum import AlertType, Severity + +from . import sensors + +IS_TESTING = False + + +def temp_check() -> list[alerts.Alert]: + alert_list = [] + temps = sensors.get_readings() + for sensor, readings in temps.items(): + for r in readings: + if r.critical_temp is not None and (IS_TESTING or r.current_temp > r.critical_temp): + alert = alerts.Alert( + alert_type=AlertType.TEMP, + message=f"{sensor} {r.label}: {r.current_temp}°C > {r.critical_temp}°C", + severity=Severity.CRITICAL, + ) + elif r.warning_temp is not None and (IS_TESTING or r.current_temp > r.warning_temp): + alert = alerts.Alert( + alert_type=AlertType.TEMP, + message=f"{sensor} {r.label}: {r.current_temp}°C > {r.warning_temp}°C", + severity=Severity.WARNING, + ) + else: + continue + alert_list.append(alert) + return alert_list diff --git a/src/lego_monitoring/checks/temp/sensors.py b/src/lego_monitoring/checks/temp/sensors.py new file mode 100644 index 0000000..ac1d7e4 --- /dev/null +++ b/src/lego_monitoring/checks/temp/sensors.py @@ -0,0 +1,66 @@ +from dataclasses import dataclass +from typing import Optional + +from psutil import sensors_temperatures + +from lego_monitoring.config.checks.temp import TempSensorConfig +from lego_monitoring.core import cvars + + +@dataclass +class TemperatureReading: + label: str + current_temp: float + warning_temp: Optional[float] + critical_temp: Optional[float] + + +def print_readings(): + sensor_readings = get_readings() + for sensor, readings in sensor_readings.items(): + print(f"*** Sensor {sensor}***\n") + for r in readings: + print(f"Label: {r.label}") + print(f"Current temp: {r.current_temp}") + print(f"Warning temp: {r.warning_temp}") + print(f"Critical temp: {r.critical_temp}\n") + + +def get_readings() -> dict[str, list[TemperatureReading]]: + try: + config = cvars.config.get().checks.temp.sensors + except LookupError: + config: dict[str, TempSensorConfig] = {} + + psutil_temperatures = sensors_temperatures() + + sensor_readings = {} + for sensor, readings in psutil_temperatures.items(): + if sensor in config: + if not config[sensor].enabled: + continue + sensor_friendly_name = config[sensor].name if config[sensor].name else sensor + else: + sensor_friendly_name = sensor + + sensor_readings[sensor_friendly_name] = [] + + for r in readings: + try: + config_r = config[sensor].readings[r.label] + except KeyError: + friendly_r = TemperatureReading( + label=r.label, current_temp=r.current, warning_temp=r.high, critical_temp=r.critical + ) + else: + if not config_r.enabled: + continue + friendly_r = TemperatureReading( + label=config_r.label if config_r.label else r.label, + current_temp=r.current, + warning_temp=config_r.warning_temp if config_r.warning_temp else r.high, + critical_temp=config_r.critical_temp if config_r.critical_temp else r.critical, + ) + sensor_readings[sensor_friendly_name].append(friendly_r) + + return sensor_readings diff --git a/src/lego_monitoring/core/config.py b/src/lego_monitoring/config/__init__.py similarity index 84% rename from src/lego_monitoring/core/config.py rename to src/lego_monitoring/config/__init__.py index 71cc766..bebca3d 100644 --- a/src/lego_monitoring/core/config.py +++ b/src/lego_monitoring/config/__init__.py @@ -3,6 +3,13 @@ from dataclasses import dataclass from alt_utils import NestedDeserializableDataclass +from .checks.temp import TempCheckConfig + + +@dataclass +class ChecksConfig(NestedDeserializableDataclass): + temp: TempCheckConfig + @dataclass class TelegramConfig: @@ -12,7 +19,8 @@ class TelegramConfig: @dataclass class Config(NestedDeserializableDataclass): - enabled_checker_sets: list[str] + enabled_check_sets: list[str] + checks: ChecksConfig telegram: TelegramConfig diff --git a/src/lego_monitoring/config/checks/temp.py b/src/lego_monitoring/config/checks/temp.py new file mode 100644 index 0000000..dea6b8b --- /dev/null +++ b/src/lego_monitoring/config/checks/temp.py @@ -0,0 +1,24 @@ +from dataclasses import dataclass +from typing import Optional + +from alt_utils import NestedDeserializableDataclass + + +@dataclass +class TempReadingConfig: + label: Optional[str] + enabled: bool + warning_temp: Optional[float] + critical_temp: Optional[float] + + +@dataclass +class TempSensorConfig(NestedDeserializableDataclass): + name: Optional[str] + enabled: bool + readings: dict[str, TempReadingConfig] + + +@dataclass +class TempCheckConfig(NestedDeserializableDataclass): + sensors: dict[str, TempSensorConfig] diff --git a/src/lego_monitoring/core/cvars.py b/src/lego_monitoring/core/cvars.py index b1f7b50..a4781c5 100644 --- a/src/lego_monitoring/core/cvars.py +++ b/src/lego_monitoring/core/cvars.py @@ -2,7 +2,7 @@ from contextvars import ContextVar from telethon import TelegramClient -from .config import Config +from ..config import Config config: ContextVar[Config] = ContextVar("config") tg_client: ContextVar[TelegramClient] = ContextVar("tg_client") diff --git a/uv.lock b/uv.lock index 41ce1c9..2e89def 100644 --- a/uv.lock +++ b/uv.lock @@ -16,17 +16,32 @@ version = "0.1.0" source = { editable = "." } dependencies = [ { name = "alt-utils" }, - { name = "setuptools" }, + { name = "psutil" }, { name = "telethon" }, ] [package.metadata] requires-dist = [ { name = "alt-utils", specifier = ">=0.0.6" }, - { name = "setuptools", specifier = ">=80.0.0" }, + { name = "psutil", specifier = ">=7.0.0" }, { name = "telethon", specifier = ">=1.40.0" }, ] +[[package]] +name = "psutil" +version = "7.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2a/80/336820c1ad9286a4ded7e845b2eccfcb27851ab8ac6abece774a6ff4d3de/psutil-7.0.0.tar.gz", hash = "sha256:7be9c3eba38beccb6495ea33afd982a44074b78f28c434a1f51cc07fd315c456", size = 497003 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ed/e6/2d26234410f8b8abdbf891c9da62bee396583f713fb9f3325a4760875d22/psutil-7.0.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:101d71dc322e3cffd7cea0650b09b3d08b8e7c4109dd6809fe452dfd00e58b25", size = 238051 }, + { url = "https://files.pythonhosted.org/packages/04/8b/30f930733afe425e3cbfc0e1468a30a18942350c1a8816acfade80c005c4/psutil-7.0.0-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:39db632f6bb862eeccf56660871433e111b6ea58f2caea825571951d4b6aa3da", size = 239535 }, + { url = "https://files.pythonhosted.org/packages/2a/ed/d362e84620dd22876b55389248e522338ed1bf134a5edd3b8231d7207f6d/psutil-7.0.0-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1fcee592b4c6f146991ca55919ea3d1f8926497a713ed7faaf8225e174581e91", size = 275004 }, + { url = "https://files.pythonhosted.org/packages/bf/b9/b0eb3f3cbcb734d930fdf839431606844a825b23eaf9a6ab371edac8162c/psutil-7.0.0-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b1388a4f6875d7e2aff5c4ca1cc16c545ed41dd8bb596cefea80111db353a34", size = 277986 }, + { url = "https://files.pythonhosted.org/packages/eb/a2/709e0fe2f093556c17fbafda93ac032257242cabcc7ff3369e2cb76a97aa/psutil-7.0.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5f098451abc2828f7dc6b58d44b532b22f2088f4999a937557b603ce72b1993", size = 279544 }, + { url = "https://files.pythonhosted.org/packages/50/e6/eecf58810b9d12e6427369784efe814a1eec0f492084ce8eb8f4d89d6d61/psutil-7.0.0-cp37-abi3-win32.whl", hash = "sha256:ba3fcef7523064a6c9da440fc4d6bd07da93ac726b5733c29027d7dc95b39d99", size = 241053 }, + { url = "https://files.pythonhosted.org/packages/50/1b/6921afe68c74868b4c9fa424dad3be35b095e16687989ebbb50ce4fceb7c/psutil-7.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:4cf3d4eb1aa9b348dec30105c55cd9b7d4629285735a102beb4441e38db90553", size = 244885 }, +] + [[package]] name = "pyaes" version = "1.6.1" @@ -54,15 +69,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/64/8d/0133e4eb4beed9e425d9a98ed6e081a55d195481b7632472be1af08d2f6b/rsa-4.9.1-py3-none-any.whl", hash = "sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762", size = 34696 }, ] -[[package]] -name = "setuptools" -version = "80.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/44/80/97e25f0f1e4067677806084b7382a6ff9979f3d15119375c475c288db9d7/setuptools-80.0.0.tar.gz", hash = "sha256:c40a5b3729d58dd749c0f08f1a07d134fb8a0a3d7f87dc33e7c5e1f762138650", size = 1354221 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/23/63/5517029d6696ddf2bd378d46f63f479be001c31b462303170a1da57650cb/setuptools-80.0.0-py3-none-any.whl", hash = "sha256:a38f898dcd6e5380f4da4381a87ec90bd0a7eec23d204a5552e80ee3cab6bd27", size = 1240907 }, -] - [[package]] name = "telethon" version = "1.40.0" From 436855d8c1b957b8dd08be5f4270a46309bd8f02 Mon Sep 17 00:00:00 2001 From: Alex Tau Date: Fri, 9 May 2025 15:27:22 +0300 Subject: [PATCH 12/56] vulnix integration --- flake.nix | 9 ++++ modules/default.nix | 32 +++++++++++++ modules/submodules/vulnixWhitelistRule.nix | 27 +++++++++++ src/lego_monitoring/__init__.py | 1 + src/lego_monitoring/alerting/enum.py | 4 +- src/lego_monitoring/checks/__init__.py | 1 + src/lego_monitoring/checks/vulnix/__init__.py | 48 +++++++++++++++++++ src/lego_monitoring/checks/vulnix/vulnix.py | 41 ++++++++++++++++ src/lego_monitoring/config/__init__.py | 2 + src/lego_monitoring/config/checks/vulnix.py | 8 ++++ src/lego_monitoring/core/const.py | 1 + 11 files changed, 172 insertions(+), 2 deletions(-) create mode 100644 modules/submodules/vulnixWhitelistRule.nix create mode 100644 src/lego_monitoring/checks/vulnix/__init__.py create mode 100644 src/lego_monitoring/checks/vulnix/vulnix.py create mode 100644 src/lego_monitoring/config/checks/vulnix.py create mode 100644 src/lego_monitoring/core/const.py diff --git a/flake.nix b/flake.nix index a445160..1d9ea40 100644 --- a/flake.nix +++ b/flake.nix @@ -65,6 +65,15 @@ buildInputs = old.buildInputs or [ ] ++ [ _prev.setuptools ]; } ); + + lego-monitoring = _prev.lego-monitoring.overrideAttrs ( + old: { + postPatch = '' + substituteInPlace src/lego_monitoring/core/const.py \ + --replace-fail 'VULNIX_PATH: str = ...' 'VULNIX_PATH = "${lib.getExe pkgs.vulnix}"' + ''; + } + ); }; # This example is only using x86_64-linux diff --git a/modules/default.nix b/modules/default.nix index d7f3ab3..5c34d7f 100644 --- a/modules/default.nix +++ b/modules/default.nix @@ -9,6 +9,7 @@ package: let tempSensorOptions = (import ./submodules/tempSensorOptions.nix) { inherit lib; }; + vulnixWhitelistRule = (import ./submodules/vulnixWhitelistRule.nix) { inherit lib; }; in { options.services.lego-monitoring = { @@ -19,6 +20,7 @@ in "start" "stop" "temp" + "vulnix" ]); default = [ ]; description = "List of enabled check sets. Each check set is a module which checks something and generates alerts based on check results."; @@ -63,12 +65,40 @@ in ''; }; }; + + vulnix = { + whitelist = lib.mkOption { + type = lib.types.attrsOf (lib.types.submodule vulnixWhitelistRule); + default = { }; + description = "Whitelist rules for vulnix. Attr name is package with version, package name, or `*`."; + example = lib.literalExpression ''{ + "ffmpeg-3.4.2" = { + cve = [ "CVE-2018-6912" "CVE-2018-7557" ]; + until = "2018-05-01"; + issueUrl = "https://issues.example.com/29952"; + }; + }''; + }; + }; }; }; config = let cfg = config.services.lego-monitoring; json = pkgs.formats.json {}; + toml = pkgs.formats.toml {}; + + # This monstrous incantation has the effect of converting the options to snake_case + # and removing those that are null (because TOML does not support null values) + vulnixWhitelistFile = toml.generate "vulnix-whitelist.toml" (lib.attrsets.filterAttrsRecursive ( + k: v: v != null + ) ( + lib.mapAttrs (_: rule: { + inherit (rule) cve until; + issue_url = rule.issueUrl; + }) cfg.checks.vulnix.whitelist + )); + serviceConfigFile = json.generate "config.json" { enabled_check_sets = cfg.enabledCheckSets; telegram = with cfg.telegram; { @@ -88,6 +118,8 @@ in }) sensorCfg.readings; }) cfg.checks.temp.sensors; + + vulnix.whitelist_path = vulnixWhitelistFile; }; }; in lib.mkIf cfg.enable { diff --git a/modules/submodules/vulnixWhitelistRule.nix b/modules/submodules/vulnixWhitelistRule.nix new file mode 100644 index 0000000..9d14045 --- /dev/null +++ b/modules/submodules/vulnixWhitelistRule.nix @@ -0,0 +1,27 @@ +{ + lib, +}: + +{ + options = { + cve = lib.mkOption { + type = lib.types.nullOr (lib.types.listOf lib.types.str); + default = null; + description = '' + List of CVE identifiers to match. The whitelist rule is valid as long as the detected CVEs are a subset of the CVEs listed here. + If additional CVEs are detected, this whitelist rule is not effective anymore. If null, all CVEs are matched.''; + }; + until = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = '' + Date in the form "YYYY-MM-DD" which confines this rule's lifetime. Null means forever. + On the specified date and later, this whitelist rule is not effective anymore.''; + }; + issueUrl = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = "URL or list of URLs that point to any issue tracker. Informational only."; + }; + }; +} diff --git a/src/lego_monitoring/__init__.py b/src/lego_monitoring/__init__.py index 981951e..12191b0 100644 --- a/src/lego_monitoring/__init__.py +++ b/src/lego_monitoring/__init__.py @@ -56,6 +56,7 @@ async def async_main(): ], "stop": [], # this is checked later "temp": [interval_checker(checks.temp_check, datetime.timedelta(minutes=5))], + "vulnix": [interval_checker(checks.vulnix_check, datetime.timedelta(days=3))], } checkers = [] diff --git a/src/lego_monitoring/alerting/enum.py b/src/lego_monitoring/alerting/enum.py index 2e121c7..de6ca5e 100644 --- a/src/lego_monitoring/alerting/enum.py +++ b/src/lego_monitoring/alerting/enum.py @@ -5,10 +5,10 @@ class AlertType(StrEnum): BOOT = "BOOT" TEMP = "TEMP" TEST = "TEST" - # ERROR = "ERROR" + VULN = "VULN" + ERROR = "ERROR" # RAM = "RAM" # CPU = "CPU" - # VULN = "VULN" # LOGIN = "LOGIN" # SMART = "SMART" # TODO # RAID = "RAID" diff --git a/src/lego_monitoring/checks/__init__.py b/src/lego_monitoring/checks/__init__.py index af7a958..df26ddf 100644 --- a/src/lego_monitoring/checks/__init__.py +++ b/src/lego_monitoring/checks/__init__.py @@ -1 +1,2 @@ from .temp import temp_check +from .vulnix import vulnix_check diff --git a/src/lego_monitoring/checks/vulnix/__init__.py b/src/lego_monitoring/checks/vulnix/__init__.py new file mode 100644 index 0000000..c9f7a10 --- /dev/null +++ b/src/lego_monitoring/checks/vulnix/__init__.py @@ -0,0 +1,48 @@ +from lego_monitoring.alerting import alerts +from lego_monitoring.alerting.enum import AlertType, Severity + +from .vulnix import get_vulnix_output + +IS_TESTING = False + + +def vulnix_check() -> list[alerts.Alert]: + alert_list = [] + try: + vulnix_output = get_vulnix_output(IS_TESTING) + except Exception as e: + alerts.send_alert( + alerts.Alert( + alert_type=AlertType.ERROR, + message=f"Exception {type(e).__name__} while calling vulnix: {e}", + severity=Severity.CRITICAL, + ) + ) + return [] + for finding in vulnix_output: + if not IS_TESTING: + non_whitelisted_cves = [k for k in finding.description if k not in finding.whitelisted] + else: + non_whitelisted_cves = finding.description.keys() + if len(non_whitelisted_cves) == 0: + continue + message = f"New findings in derivation {finding.derivation}:" + for cve in non_whitelisted_cves: + if cve in finding.cvssv3_basescore: + score_str = f"(CVSSv3 = {finding.cvssv3_basescore[cve]})" + else: + score_str = "(not scored by CVSSv3)" + message += f'\n* {cve} - {finding.description[cve]} {score_str}' + + alert = alerts.Alert( + alert_type=AlertType.VULN, + message=message, + severity=Severity.WARNING, + ) + alert_list.append(alert) + + if IS_TESTING: + alert_list[0].message += "\n(just testing)" + return [alert_list[0]] + else: + return alert_list diff --git a/src/lego_monitoring/checks/vulnix/vulnix.py b/src/lego_monitoring/checks/vulnix/vulnix.py new file mode 100644 index 0000000..ff31662 --- /dev/null +++ b/src/lego_monitoring/checks/vulnix/vulnix.py @@ -0,0 +1,41 @@ +import json +import logging +import subprocess +from dataclasses import dataclass + +from lego_monitoring.core import cvars +from lego_monitoring.core.const import VULNIX_PATH + + +@dataclass +class VulnixPackageFindings: + pname: str + version: str + derivation: str + whitelisted: list[str] + cvssv3_basescore: dict[str, float] + description: dict[str, float] + + +def get_vulnix_output(is_testing=False) -> list[VulnixPackageFindings]: + whitelist_path = cvars.config.get().checks.vulnix.whitelist_path + cmd = [VULNIX_PATH, "--system", "-w", whitelist_path, "--json"] + if is_testing: + cmd.append("-s") + vulnix_run = subprocess.run(cmd, capture_output=True, check=False) + if vulnix_run.returncode not in range(0, 3): + logging.error(f"Vulnix returned error code {vulnix_run.returncode}, stderr: {vulnix_run.stderr}") + raise Exception(f"vulnix return error code {vulnix_run.returncode}, check logs") + vulnix_findings_json = json.loads(vulnix_run.stdout) + vulnix_findings = [ + VulnixPackageFindings( + pname=f["pname"], + version=f["version"], + derivation=f["derivation"], + whitelisted=f["whitelisted"], + cvssv3_basescore=f["cvssv3_basescore"], + description=f["description"], + ) + for f in vulnix_findings_json + ] + return vulnix_findings diff --git a/src/lego_monitoring/config/__init__.py b/src/lego_monitoring/config/__init__.py index bebca3d..31b58c5 100644 --- a/src/lego_monitoring/config/__init__.py +++ b/src/lego_monitoring/config/__init__.py @@ -4,11 +4,13 @@ from dataclasses import dataclass from alt_utils import NestedDeserializableDataclass from .checks.temp import TempCheckConfig +from .checks.vulnix import VulnixCheckConfig @dataclass class ChecksConfig(NestedDeserializableDataclass): temp: TempCheckConfig + vulnix: VulnixCheckConfig @dataclass diff --git a/src/lego_monitoring/config/checks/vulnix.py b/src/lego_monitoring/config/checks/vulnix.py new file mode 100644 index 0000000..e68fc03 --- /dev/null +++ b/src/lego_monitoring/config/checks/vulnix.py @@ -0,0 +1,8 @@ +from dataclasses import dataclass + +from alt_utils import NestedDeserializableDataclass + + +@dataclass +class VulnixCheckConfig(NestedDeserializableDataclass): + whitelist_path: str diff --git a/src/lego_monitoring/core/const.py b/src/lego_monitoring/core/const.py new file mode 100644 index 0000000..dde2ddf --- /dev/null +++ b/src/lego_monitoring/core/const.py @@ -0,0 +1 @@ +VULNIX_PATH: str = ... # path to vulnix executable From 1b3666276edaee703334a9350814572f0e335f1a Mon Sep 17 00:00:00 2001 From: Alex Tau Date: Sat, 10 May 2025 14:58:10 +0300 Subject: [PATCH 13/56] don't require configs for disabled checks --- pyproject.toml | 2 +- src/lego_monitoring/config/__init__.py | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index f21fd01..87702ac 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ description = "Monitoring software for the lego server" readme = "README.md" requires-python = ">=3.12" dependencies = [ - "alt-utils>=0.0.6", + "alt-utils>=0.0.7", "psutil>=7.0.0", "telethon>=1.40.0", ] diff --git a/src/lego_monitoring/config/__init__.py b/src/lego_monitoring/config/__init__.py index 31b58c5..d6518bb 100644 --- a/src/lego_monitoring/config/__init__.py +++ b/src/lego_monitoring/config/__init__.py @@ -1,5 +1,6 @@ import json from dataclasses import dataclass +from typing import Optional from alt_utils import NestedDeserializableDataclass @@ -9,8 +10,8 @@ from .checks.vulnix import VulnixCheckConfig @dataclass class ChecksConfig(NestedDeserializableDataclass): - temp: TempCheckConfig - vulnix: VulnixCheckConfig + temp: Optional[TempCheckConfig] = None + vulnix: Optional[VulnixCheckConfig] = None @dataclass From fdaf68b8b57e26b3afb11e87ad6ca34ec6ada82e Mon Sep 17 00:00:00 2001 From: Alex Tau Date: Sat, 10 May 2025 16:14:44 +0300 Subject: [PATCH 14/56] autogenerated docs --- docs/nixos-options.md | 368 ++++++++++++++++++ flake.nix | 5 +- mkdocs.nix | 15 + modules/default.nix | 79 +--- modules/options.nix | 81 ++++ .../tempSensorOptions.nix | 0 .../vulnixWhitelistRule.nix | 0 7 files changed, 472 insertions(+), 76 deletions(-) create mode 100644 docs/nixos-options.md create mode 100644 mkdocs.nix create mode 100644 modules/options.nix rename modules/{submodules => suboptions}/tempSensorOptions.nix (100%) rename modules/{submodules => suboptions}/vulnixWhitelistRule.nix (100%) diff --git a/docs/nixos-options.md b/docs/nixos-options.md new file mode 100644 index 0000000..8c53dc7 --- /dev/null +++ b/docs/nixos-options.md @@ -0,0 +1,368 @@ +## services\.lego-monitoring\.enable + + + +Whether to enable lego-monitoring service… + + + +*Type:* +boolean + + + +*Default:* +` false ` + + + +*Example:* +` true ` + +*Declared by:* + - [/nix/store/32aaw5svwp38dh1wqby10d9bx0vjvv33-source/modules/options\.nix](file:///nix/store/32aaw5svwp38dh1wqby10d9bx0vjvv33-source/modules/options.nix) + + + +## services\.lego-monitoring\.enabledCheckSets + + + +List of enabled check sets\. Each check set is a module which checks something and generates alerts based on check results\. + + + +*Type:* +list of (one of “start”, “stop”, “temp”, “vulnix”) + + + +*Default:* +` [ ] ` + +*Declared by:* + - [/nix/store/32aaw5svwp38dh1wqby10d9bx0vjvv33-source/modules/options\.nix](file:///nix/store/32aaw5svwp38dh1wqby10d9bx0vjvv33-source/modules/options.nix) + + + +## services\.lego-monitoring\.checks\.temp\.sensors + +Temp sensor override definitions\. Sensors not defined here, or missing options in definitions, will be read with default parameters\. + +To get list of sensors and their default configurations, run ` lego-monitoring --print-temp `\. + + + +*Type:* +attribute set of (submodule) + + + +*Default:* +` { } ` + + + +*Example:* + +``` +{ + amdgpu.readings.edge.label = "Integrated GPU"; + k10temp.readings = { + Tctl = { + label = "AMD CPU"; + criticalTemp = 95.0; + }; + Tccd1.enabled = false; + Tccd2.enabled = false; + }; + nvme.readings = { + "Sensor 1".enabled = false; + "Sensor 2".enabled = false; + }; +} +``` + +*Declared by:* + - [/nix/store/32aaw5svwp38dh1wqby10d9bx0vjvv33-source/modules/options\.nix](file:///nix/store/32aaw5svwp38dh1wqby10d9bx0vjvv33-source/modules/options.nix) + + + +## services\.lego-monitoring\.checks\.temp\.sensors\.\\.enabled + + + +Whether sensor is enabled\. + + + +*Type:* +boolean + + + +*Default:* +` true ` + +*Declared by:* + - [/nix/store/32aaw5svwp38dh1wqby10d9bx0vjvv33-source/modules/options\.nix](file:///nix/store/32aaw5svwp38dh1wqby10d9bx0vjvv33-source/modules/options.nix) + + + +## services\.lego-monitoring\.checks\.temp\.sensors\.\\.name + + + +Friendly name of the sensor\. + + + +*Type:* +null or string + + + +*Default:* +` null ` + +*Declared by:* + - [/nix/store/32aaw5svwp38dh1wqby10d9bx0vjvv33-source/modules/options\.nix](file:///nix/store/32aaw5svwp38dh1wqby10d9bx0vjvv33-source/modules/options.nix) + + + +## services\.lego-monitoring\.checks\.temp\.sensors\.\\.readings + + + +Overrides for specific readings of the sensor, by label\. + + + +*Type:* +attribute set of (submodule) + + + +*Default:* +` { } ` + +*Declared by:* + - [/nix/store/32aaw5svwp38dh1wqby10d9bx0vjvv33-source/modules/options\.nix](file:///nix/store/32aaw5svwp38dh1wqby10d9bx0vjvv33-source/modules/options.nix) + + + +## services\.lego-monitoring\.checks\.temp\.sensors\.\\.readings\.\\.enabled + + + +Whether this reading is enabled\. + + + +*Type:* +boolean + + + +*Default:* +` true ` + +*Declared by:* + - [/nix/store/32aaw5svwp38dh1wqby10d9bx0vjvv33-source/modules/options\.nix](file:///nix/store/32aaw5svwp38dh1wqby10d9bx0vjvv33-source/modules/options.nix) + + + +## services\.lego-monitoring\.checks\.temp\.sensors\.\\.readings\.\\.criticalTemp + + + +Critical temperature threshold\. + + + +*Type:* +null or floating point number + + + +*Default:* +` null ` + +*Declared by:* + - [/nix/store/32aaw5svwp38dh1wqby10d9bx0vjvv33-source/modules/options\.nix](file:///nix/store/32aaw5svwp38dh1wqby10d9bx0vjvv33-source/modules/options.nix) + + + +## services\.lego-monitoring\.checks\.temp\.sensors\.\\.readings\.\\.label + + + +Friendly label of the reading\. + + + +*Type:* +null or string + + + +*Default:* +` null ` + +*Declared by:* + - [/nix/store/32aaw5svwp38dh1wqby10d9bx0vjvv33-source/modules/options\.nix](file:///nix/store/32aaw5svwp38dh1wqby10d9bx0vjvv33-source/modules/options.nix) + + + +## services\.lego-monitoring\.checks\.temp\.sensors\.\\.readings\.\\.warningTemp + + + +Warning temperature threshold\. + + + +*Type:* +null or floating point number + + + +*Default:* +` null ` + +*Declared by:* + - [/nix/store/32aaw5svwp38dh1wqby10d9bx0vjvv33-source/modules/options\.nix](file:///nix/store/32aaw5svwp38dh1wqby10d9bx0vjvv33-source/modules/options.nix) + + + +## services\.lego-monitoring\.checks\.vulnix\.whitelist + + + +Whitelist rules for vulnix\. Attr name is package with version, package name, or ` * `\. + + + +*Type:* +attribute set of (submodule) + + + +*Default:* +` { } ` + + + +*Example:* + +``` +{ + "ffmpeg-3.4.2" = { + cve = [ "CVE-2018-6912" "CVE-2018-7557" ]; + until = "2018-05-01"; + issueUrl = "https://issues.example.com/29952"; + }; +} +``` + +*Declared by:* + - [/nix/store/32aaw5svwp38dh1wqby10d9bx0vjvv33-source/modules/options\.nix](file:///nix/store/32aaw5svwp38dh1wqby10d9bx0vjvv33-source/modules/options.nix) + + + +## services\.lego-monitoring\.checks\.vulnix\.whitelist\.\\.cve + + + +List of CVE identifiers to match\. The whitelist rule is valid as long as the detected CVEs are a subset of the CVEs listed here\. +If additional CVEs are detected, this whitelist rule is not effective anymore\. If null, all CVEs are matched\. + + + +*Type:* +null or (list of string) + + + +*Default:* +` null ` + +*Declared by:* + - [/nix/store/32aaw5svwp38dh1wqby10d9bx0vjvv33-source/modules/options\.nix](file:///nix/store/32aaw5svwp38dh1wqby10d9bx0vjvv33-source/modules/options.nix) + + + +## services\.lego-monitoring\.checks\.vulnix\.whitelist\.\\.issueUrl + + + +URL or list of URLs that point to any issue tracker\. Informational only\. + + + +*Type:* +null or string + + + +*Default:* +` null ` + +*Declared by:* + - [/nix/store/32aaw5svwp38dh1wqby10d9bx0vjvv33-source/modules/options\.nix](file:///nix/store/32aaw5svwp38dh1wqby10d9bx0vjvv33-source/modules/options.nix) + + + +## services\.lego-monitoring\.checks\.vulnix\.whitelist\.\\.until + + + +Date in the form “YYYY-MM-DD” which confines this rule’s lifetime\. Null means forever\. +On the specified date and later, this whitelist rule is not effective anymore\. + + + +*Type:* +null or string + + + +*Default:* +` null ` + +*Declared by:* + - [/nix/store/32aaw5svwp38dh1wqby10d9bx0vjvv33-source/modules/options\.nix](file:///nix/store/32aaw5svwp38dh1wqby10d9bx0vjvv33-source/modules/options.nix) + + + +## services\.lego-monitoring\.telegram\.credsSecretPath + + + +Path to a file containing Telegram api_id, api_hash, and bot token, separated by the ` , ` character\. + + + +*Type:* +string + +*Declared by:* + - [/nix/store/32aaw5svwp38dh1wqby10d9bx0vjvv33-source/modules/options\.nix](file:///nix/store/32aaw5svwp38dh1wqby10d9bx0vjvv33-source/modules/options.nix) + + + +## services\.lego-monitoring\.telegram\.roomId + + + +ID of chat where to send alerts\. + + + +*Type:* +signed integer + +*Declared by:* + - [/nix/store/32aaw5svwp38dh1wqby10d9bx0vjvv33-source/modules/options\.nix](file:///nix/store/32aaw5svwp38dh1wqby10d9bx0vjvv33-source/modules/options.nix) + + diff --git a/flake.nix b/flake.nix index 1d9ea40..6bced14 100644 --- a/flake.nix +++ b/flake.nix @@ -103,7 +103,10 @@ # Package a virtual environment as our main application. # # Enable no optional dependencies for production build. - packages.x86_64-linux.default = pythonSet.mkVirtualEnv "lego-monitoring-env" workspace.deps.default; + packages.x86_64-linux = { + default = pythonSet.mkVirtualEnv "lego-monitoring-env" workspace.deps.default; + docs = pkgs.callPackage ./mkdocs.nix {}; + }; # Make service runnable with `nix run` apps.x86_64-linux = { diff --git a/mkdocs.nix b/mkdocs.nix new file mode 100644 index 0000000..6a38c55 --- /dev/null +++ b/mkdocs.nix @@ -0,0 +1,15 @@ +{ + lib, + pkgs, + ... +}: + +let + optEval = lib.evalModules { modules = [ + ./modules/options.nix + ]; }; + optionsDoc = pkgs.nixosOptionsDoc { + options = builtins.removeAttrs optEval.options [ "_module" ]; + }; +in + pkgs.runCommand "options-doc.md" {} "cat ${optionsDoc.optionsCommonMark} >> $out" diff --git a/modules/default.nix b/modules/default.nix index 5c34d7f..d48361c 100644 --- a/modules/default.nix +++ b/modules/default.nix @@ -7,82 +7,11 @@ package: ... }: -let - tempSensorOptions = (import ./submodules/tempSensorOptions.nix) { inherit lib; }; - vulnixWhitelistRule = (import ./submodules/vulnixWhitelistRule.nix) { inherit lib; }; -in { - options.services.lego-monitoring = { - enable = lib.mkEnableOption "lego-monitoring service."; - - enabledCheckSets = lib.mkOption { - type = lib.types.listOf (lib.types.enum [ - "start" - "stop" - "temp" - "vulnix" - ]); - default = [ ]; - description = "List of enabled check sets. Each check set is a module which checks something and generates alerts based on check results."; - }; - - telegram = { - credsSecretPath = lib.mkOption { - type = lib.types.str; - description = "Path to a file containing Telegram api_id, api_hash, and bot token, separated by the `,` character."; - }; - roomId = lib.mkOption { - type = lib.types.int; - description = "ID of chat where to send alerts."; - }; - }; - - checks = { - temp = { - sensors = lib.mkOption { - type = lib.types.attrsOf (lib.types.submodule tempSensorOptions); - default = { }; - description = '' - Temp sensor override definitions. Sensors not defined here, or missing options in definitions, will be read with default parameters. - - To get list of sensors and their default configurations, run `lego-monitoring --print-temp`.''; - example = lib.literalExpression '' - { - amdgpu.readings.edge.label = "Integrated GPU"; - k10temp.readings = { - Tctl = { - label = "AMD CPU"; - criticalTemp = 95.0; - }; - Tccd1.enabled = false; - Tccd2.enabled = false; - }; - nvme.readings = { - "Sensor 1".enabled = false; - "Sensor 2".enabled = false; - }; - } - ''; - }; - }; - - vulnix = { - whitelist = lib.mkOption { - type = lib.types.attrsOf (lib.types.submodule vulnixWhitelistRule); - default = { }; - description = "Whitelist rules for vulnix. Attr name is package with version, package name, or `*`."; - example = lib.literalExpression ''{ - "ffmpeg-3.4.2" = { - cve = [ "CVE-2018-6912" "CVE-2018-7557" ]; - until = "2018-05-01"; - issueUrl = "https://issues.example.com/29952"; - }; - }''; - }; - }; - }; - }; - + imports = [ + ./options.nix + ]; + config = let cfg = config.services.lego-monitoring; json = pkgs.formats.json {}; diff --git a/modules/options.nix b/modules/options.nix new file mode 100644 index 0000000..c692366 --- /dev/null +++ b/modules/options.nix @@ -0,0 +1,81 @@ +{ + lib, + ... +}: + +let + tempSensorOptions = (import ./suboptions/tempSensorOptions.nix) { inherit lib; }; + vulnixWhitelistRule = (import ./suboptions/vulnixWhitelistRule.nix) { inherit lib; }; +in +{ + options.services.lego-monitoring = { + enable = lib.mkEnableOption "lego-monitoring service."; + + enabledCheckSets = lib.mkOption { + type = lib.types.listOf (lib.types.enum [ + "start" + "stop" + "temp" + "vulnix" + ]); + default = [ ]; + description = "List of enabled check sets. Each check set is a module which checks something and generates alerts based on check results."; + }; + + telegram = { + credsSecretPath = lib.mkOption { + type = lib.types.str; + description = "Path to a file containing Telegram api_id, api_hash, and bot token, separated by the `,` character."; + }; + roomId = lib.mkOption { + type = lib.types.int; + description = "ID of chat where to send alerts."; + }; + }; + + checks = { + temp = { + sensors = lib.mkOption { + type = lib.types.attrsOf (lib.types.submodule tempSensorOptions); + default = { }; + description = '' + Temp sensor override definitions. Sensors not defined here, or missing options in definitions, will be read with default parameters. + + To get list of sensors and their default configurations, run `lego-monitoring --print-temp`.''; + example = lib.literalExpression '' + { + amdgpu.readings.edge.label = "Integrated GPU"; + k10temp.readings = { + Tctl = { + label = "AMD CPU"; + criticalTemp = 95.0; + }; + Tccd1.enabled = false; + Tccd2.enabled = false; + }; + nvme.readings = { + "Sensor 1".enabled = false; + "Sensor 2".enabled = false; + }; + }''; + }; + }; + + vulnix = { + whitelist = lib.mkOption { + type = lib.types.attrsOf (lib.types.submodule vulnixWhitelistRule); + default = { }; + description = "Whitelist rules for vulnix. Attr name is package with version, package name, or `*`."; + example = lib.literalExpression '' + { + "ffmpeg-3.4.2" = { + cve = [ "CVE-2018-6912" "CVE-2018-7557" ]; + until = "2018-05-01"; + issueUrl = "https://issues.example.com/29952"; + }; + }''; + }; + }; + }; + }; +} diff --git a/modules/submodules/tempSensorOptions.nix b/modules/suboptions/tempSensorOptions.nix similarity index 100% rename from modules/submodules/tempSensorOptions.nix rename to modules/suboptions/tempSensorOptions.nix diff --git a/modules/submodules/vulnixWhitelistRule.nix b/modules/suboptions/vulnixWhitelistRule.nix similarity index 100% rename from modules/submodules/vulnixWhitelistRule.nix rename to modules/suboptions/vulnixWhitelistRule.nix From adb967c2829bfe971f94a91ff759643f20686aea Mon Sep 17 00:00:00 2001 From: Alex Tau Date: Sat, 10 May 2025 16:16:09 +0300 Subject: [PATCH 15/56] fix triple dot --- docs/nixos-options.md | 34 +++++++++++++++++----------------- modules/options.nix | 2 +- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/docs/nixos-options.md b/docs/nixos-options.md index 8c53dc7..df3c918 100644 --- a/docs/nixos-options.md +++ b/docs/nixos-options.md @@ -2,7 +2,7 @@ -Whether to enable lego-monitoring service… +Whether to enable lego-monitoring service\. @@ -20,7 +20,7 @@ boolean ` true ` *Declared by:* - - [/nix/store/32aaw5svwp38dh1wqby10d9bx0vjvv33-source/modules/options\.nix](file:///nix/store/32aaw5svwp38dh1wqby10d9bx0vjvv33-source/modules/options.nix) + - [/nix/store/jm7cx941zr5pl113s5nxvfqpaqf89ysl-source/modules/options\.nix](file:///nix/store/jm7cx941zr5pl113s5nxvfqpaqf89ysl-source/modules/options.nix) @@ -41,7 +41,7 @@ list of (one of “start”, “stop”, “temp”, “vulnix”) ` [ ] ` *Declared by:* - - [/nix/store/32aaw5svwp38dh1wqby10d9bx0vjvv33-source/modules/options\.nix](file:///nix/store/32aaw5svwp38dh1wqby10d9bx0vjvv33-source/modules/options.nix) + - [/nix/store/jm7cx941zr5pl113s5nxvfqpaqf89ysl-source/modules/options\.nix](file:///nix/store/jm7cx941zr5pl113s5nxvfqpaqf89ysl-source/modules/options.nix) @@ -84,7 +84,7 @@ attribute set of (submodule) ``` *Declared by:* - - [/nix/store/32aaw5svwp38dh1wqby10d9bx0vjvv33-source/modules/options\.nix](file:///nix/store/32aaw5svwp38dh1wqby10d9bx0vjvv33-source/modules/options.nix) + - [/nix/store/jm7cx941zr5pl113s5nxvfqpaqf89ysl-source/modules/options\.nix](file:///nix/store/jm7cx941zr5pl113s5nxvfqpaqf89ysl-source/modules/options.nix) @@ -105,7 +105,7 @@ boolean ` true ` *Declared by:* - - [/nix/store/32aaw5svwp38dh1wqby10d9bx0vjvv33-source/modules/options\.nix](file:///nix/store/32aaw5svwp38dh1wqby10d9bx0vjvv33-source/modules/options.nix) + - [/nix/store/jm7cx941zr5pl113s5nxvfqpaqf89ysl-source/modules/options\.nix](file:///nix/store/jm7cx941zr5pl113s5nxvfqpaqf89ysl-source/modules/options.nix) @@ -126,7 +126,7 @@ null or string ` null ` *Declared by:* - - [/nix/store/32aaw5svwp38dh1wqby10d9bx0vjvv33-source/modules/options\.nix](file:///nix/store/32aaw5svwp38dh1wqby10d9bx0vjvv33-source/modules/options.nix) + - [/nix/store/jm7cx941zr5pl113s5nxvfqpaqf89ysl-source/modules/options\.nix](file:///nix/store/jm7cx941zr5pl113s5nxvfqpaqf89ysl-source/modules/options.nix) @@ -147,7 +147,7 @@ attribute set of (submodule) ` { } ` *Declared by:* - - [/nix/store/32aaw5svwp38dh1wqby10d9bx0vjvv33-source/modules/options\.nix](file:///nix/store/32aaw5svwp38dh1wqby10d9bx0vjvv33-source/modules/options.nix) + - [/nix/store/jm7cx941zr5pl113s5nxvfqpaqf89ysl-source/modules/options\.nix](file:///nix/store/jm7cx941zr5pl113s5nxvfqpaqf89ysl-source/modules/options.nix) @@ -168,7 +168,7 @@ boolean ` true ` *Declared by:* - - [/nix/store/32aaw5svwp38dh1wqby10d9bx0vjvv33-source/modules/options\.nix](file:///nix/store/32aaw5svwp38dh1wqby10d9bx0vjvv33-source/modules/options.nix) + - [/nix/store/jm7cx941zr5pl113s5nxvfqpaqf89ysl-source/modules/options\.nix](file:///nix/store/jm7cx941zr5pl113s5nxvfqpaqf89ysl-source/modules/options.nix) @@ -189,7 +189,7 @@ null or floating point number ` null ` *Declared by:* - - [/nix/store/32aaw5svwp38dh1wqby10d9bx0vjvv33-source/modules/options\.nix](file:///nix/store/32aaw5svwp38dh1wqby10d9bx0vjvv33-source/modules/options.nix) + - [/nix/store/jm7cx941zr5pl113s5nxvfqpaqf89ysl-source/modules/options\.nix](file:///nix/store/jm7cx941zr5pl113s5nxvfqpaqf89ysl-source/modules/options.nix) @@ -210,7 +210,7 @@ null or string ` null ` *Declared by:* - - [/nix/store/32aaw5svwp38dh1wqby10d9bx0vjvv33-source/modules/options\.nix](file:///nix/store/32aaw5svwp38dh1wqby10d9bx0vjvv33-source/modules/options.nix) + - [/nix/store/jm7cx941zr5pl113s5nxvfqpaqf89ysl-source/modules/options\.nix](file:///nix/store/jm7cx941zr5pl113s5nxvfqpaqf89ysl-source/modules/options.nix) @@ -231,7 +231,7 @@ null or floating point number ` null ` *Declared by:* - - [/nix/store/32aaw5svwp38dh1wqby10d9bx0vjvv33-source/modules/options\.nix](file:///nix/store/32aaw5svwp38dh1wqby10d9bx0vjvv33-source/modules/options.nix) + - [/nix/store/jm7cx941zr5pl113s5nxvfqpaqf89ysl-source/modules/options\.nix](file:///nix/store/jm7cx941zr5pl113s5nxvfqpaqf89ysl-source/modules/options.nix) @@ -266,7 +266,7 @@ attribute set of (submodule) ``` *Declared by:* - - [/nix/store/32aaw5svwp38dh1wqby10d9bx0vjvv33-source/modules/options\.nix](file:///nix/store/32aaw5svwp38dh1wqby10d9bx0vjvv33-source/modules/options.nix) + - [/nix/store/jm7cx941zr5pl113s5nxvfqpaqf89ysl-source/modules/options\.nix](file:///nix/store/jm7cx941zr5pl113s5nxvfqpaqf89ysl-source/modules/options.nix) @@ -288,7 +288,7 @@ null or (list of string) ` null ` *Declared by:* - - [/nix/store/32aaw5svwp38dh1wqby10d9bx0vjvv33-source/modules/options\.nix](file:///nix/store/32aaw5svwp38dh1wqby10d9bx0vjvv33-source/modules/options.nix) + - [/nix/store/jm7cx941zr5pl113s5nxvfqpaqf89ysl-source/modules/options\.nix](file:///nix/store/jm7cx941zr5pl113s5nxvfqpaqf89ysl-source/modules/options.nix) @@ -309,7 +309,7 @@ null or string ` null ` *Declared by:* - - [/nix/store/32aaw5svwp38dh1wqby10d9bx0vjvv33-source/modules/options\.nix](file:///nix/store/32aaw5svwp38dh1wqby10d9bx0vjvv33-source/modules/options.nix) + - [/nix/store/jm7cx941zr5pl113s5nxvfqpaqf89ysl-source/modules/options\.nix](file:///nix/store/jm7cx941zr5pl113s5nxvfqpaqf89ysl-source/modules/options.nix) @@ -331,7 +331,7 @@ null or string ` null ` *Declared by:* - - [/nix/store/32aaw5svwp38dh1wqby10d9bx0vjvv33-source/modules/options\.nix](file:///nix/store/32aaw5svwp38dh1wqby10d9bx0vjvv33-source/modules/options.nix) + - [/nix/store/jm7cx941zr5pl113s5nxvfqpaqf89ysl-source/modules/options\.nix](file:///nix/store/jm7cx941zr5pl113s5nxvfqpaqf89ysl-source/modules/options.nix) @@ -347,7 +347,7 @@ Path to a file containing Telegram api_id, api_hash, and bot token, separated by string *Declared by:* - - [/nix/store/32aaw5svwp38dh1wqby10d9bx0vjvv33-source/modules/options\.nix](file:///nix/store/32aaw5svwp38dh1wqby10d9bx0vjvv33-source/modules/options.nix) + - [/nix/store/jm7cx941zr5pl113s5nxvfqpaqf89ysl-source/modules/options\.nix](file:///nix/store/jm7cx941zr5pl113s5nxvfqpaqf89ysl-source/modules/options.nix) @@ -363,6 +363,6 @@ ID of chat where to send alerts\. signed integer *Declared by:* - - [/nix/store/32aaw5svwp38dh1wqby10d9bx0vjvv33-source/modules/options\.nix](file:///nix/store/32aaw5svwp38dh1wqby10d9bx0vjvv33-source/modules/options.nix) + - [/nix/store/jm7cx941zr5pl113s5nxvfqpaqf89ysl-source/modules/options\.nix](file:///nix/store/jm7cx941zr5pl113s5nxvfqpaqf89ysl-source/modules/options.nix) diff --git a/modules/options.nix b/modules/options.nix index c692366..2a65787 100644 --- a/modules/options.nix +++ b/modules/options.nix @@ -9,7 +9,7 @@ let in { options.services.lego-monitoring = { - enable = lib.mkEnableOption "lego-monitoring service."; + enable = lib.mkEnableOption "lego-monitoring service"; enabledCheckSets = lib.mkOption { type = lib.types.listOf (lib.types.enum [ From 4cbeb4e4919c12a133fee56e995b5a92b381c85e Mon Sep 17 00:00:00 2001 From: Alex Tau Date: Sat, 10 May 2025 16:45:48 +0300 Subject: [PATCH 16/56] replace links in autogenerated docs for better viewing on gitlab --- docs/nixos-options.md | 32 ++++++++++++++++---------------- mkdocs.nix | 5 ++++- 2 files changed, 20 insertions(+), 17 deletions(-) diff --git a/docs/nixos-options.md b/docs/nixos-options.md index df3c918..7fc56f5 100644 --- a/docs/nixos-options.md +++ b/docs/nixos-options.md @@ -20,7 +20,7 @@ boolean ` true ` *Declared by:* - - [/nix/store/jm7cx941zr5pl113s5nxvfqpaqf89ysl-source/modules/options\.nix](file:///nix/store/jm7cx941zr5pl113s5nxvfqpaqf89ysl-source/modules/options.nix) + - [../modules/options\.nix](../modules/options.nix) @@ -41,7 +41,7 @@ list of (one of “start”, “stop”, “temp”, “vulnix”) ` [ ] ` *Declared by:* - - [/nix/store/jm7cx941zr5pl113s5nxvfqpaqf89ysl-source/modules/options\.nix](file:///nix/store/jm7cx941zr5pl113s5nxvfqpaqf89ysl-source/modules/options.nix) + - [../modules/options\.nix](../modules/options.nix) @@ -84,7 +84,7 @@ attribute set of (submodule) ``` *Declared by:* - - [/nix/store/jm7cx941zr5pl113s5nxvfqpaqf89ysl-source/modules/options\.nix](file:///nix/store/jm7cx941zr5pl113s5nxvfqpaqf89ysl-source/modules/options.nix) + - [../modules/options\.nix](../modules/options.nix) @@ -105,7 +105,7 @@ boolean ` true ` *Declared by:* - - [/nix/store/jm7cx941zr5pl113s5nxvfqpaqf89ysl-source/modules/options\.nix](file:///nix/store/jm7cx941zr5pl113s5nxvfqpaqf89ysl-source/modules/options.nix) + - [../modules/options\.nix](../modules/options.nix) @@ -126,7 +126,7 @@ null or string ` null ` *Declared by:* - - [/nix/store/jm7cx941zr5pl113s5nxvfqpaqf89ysl-source/modules/options\.nix](file:///nix/store/jm7cx941zr5pl113s5nxvfqpaqf89ysl-source/modules/options.nix) + - [../modules/options\.nix](../modules/options.nix) @@ -147,7 +147,7 @@ attribute set of (submodule) ` { } ` *Declared by:* - - [/nix/store/jm7cx941zr5pl113s5nxvfqpaqf89ysl-source/modules/options\.nix](file:///nix/store/jm7cx941zr5pl113s5nxvfqpaqf89ysl-source/modules/options.nix) + - [../modules/options\.nix](../modules/options.nix) @@ -168,7 +168,7 @@ boolean ` true ` *Declared by:* - - [/nix/store/jm7cx941zr5pl113s5nxvfqpaqf89ysl-source/modules/options\.nix](file:///nix/store/jm7cx941zr5pl113s5nxvfqpaqf89ysl-source/modules/options.nix) + - [../modules/options\.nix](../modules/options.nix) @@ -189,7 +189,7 @@ null or floating point number ` null ` *Declared by:* - - [/nix/store/jm7cx941zr5pl113s5nxvfqpaqf89ysl-source/modules/options\.nix](file:///nix/store/jm7cx941zr5pl113s5nxvfqpaqf89ysl-source/modules/options.nix) + - [../modules/options\.nix](../modules/options.nix) @@ -210,7 +210,7 @@ null or string ` null ` *Declared by:* - - [/nix/store/jm7cx941zr5pl113s5nxvfqpaqf89ysl-source/modules/options\.nix](file:///nix/store/jm7cx941zr5pl113s5nxvfqpaqf89ysl-source/modules/options.nix) + - [../modules/options\.nix](../modules/options.nix) @@ -231,7 +231,7 @@ null or floating point number ` null ` *Declared by:* - - [/nix/store/jm7cx941zr5pl113s5nxvfqpaqf89ysl-source/modules/options\.nix](file:///nix/store/jm7cx941zr5pl113s5nxvfqpaqf89ysl-source/modules/options.nix) + - [../modules/options\.nix](../modules/options.nix) @@ -266,7 +266,7 @@ attribute set of (submodule) ``` *Declared by:* - - [/nix/store/jm7cx941zr5pl113s5nxvfqpaqf89ysl-source/modules/options\.nix](file:///nix/store/jm7cx941zr5pl113s5nxvfqpaqf89ysl-source/modules/options.nix) + - [../modules/options\.nix](../modules/options.nix) @@ -288,7 +288,7 @@ null or (list of string) ` null ` *Declared by:* - - [/nix/store/jm7cx941zr5pl113s5nxvfqpaqf89ysl-source/modules/options\.nix](file:///nix/store/jm7cx941zr5pl113s5nxvfqpaqf89ysl-source/modules/options.nix) + - [../modules/options\.nix](../modules/options.nix) @@ -309,7 +309,7 @@ null or string ` null ` *Declared by:* - - [/nix/store/jm7cx941zr5pl113s5nxvfqpaqf89ysl-source/modules/options\.nix](file:///nix/store/jm7cx941zr5pl113s5nxvfqpaqf89ysl-source/modules/options.nix) + - [../modules/options\.nix](../modules/options.nix) @@ -331,7 +331,7 @@ null or string ` null ` *Declared by:* - - [/nix/store/jm7cx941zr5pl113s5nxvfqpaqf89ysl-source/modules/options\.nix](file:///nix/store/jm7cx941zr5pl113s5nxvfqpaqf89ysl-source/modules/options.nix) + - [../modules/options\.nix](../modules/options.nix) @@ -347,7 +347,7 @@ Path to a file containing Telegram api_id, api_hash, and bot token, separated by string *Declared by:* - - [/nix/store/jm7cx941zr5pl113s5nxvfqpaqf89ysl-source/modules/options\.nix](file:///nix/store/jm7cx941zr5pl113s5nxvfqpaqf89ysl-source/modules/options.nix) + - [../modules/options\.nix](../modules/options.nix) @@ -363,6 +363,6 @@ ID of chat where to send alerts\. signed integer *Declared by:* - - [/nix/store/jm7cx941zr5pl113s5nxvfqpaqf89ysl-source/modules/options\.nix](file:///nix/store/jm7cx941zr5pl113s5nxvfqpaqf89ysl-source/modules/options.nix) + - [../modules/options\.nix](../modules/options.nix) diff --git a/mkdocs.nix b/mkdocs.nix index 6a38c55..a82a16c 100644 --- a/mkdocs.nix +++ b/mkdocs.nix @@ -11,5 +11,8 @@ let optionsDoc = pkgs.nixosOptionsDoc { options = builtins.removeAttrs optEval.options [ "_module" ]; }; + replaceLinkNamesPattern = ''sR\[/nix/store/[a-z0-9]+-source/R[../R''; + replaceLinkContentsPattern = ''sR\(file:///nix/store/[a-z0-9]+-source/R(../R''; in - pkgs.runCommand "options-doc.md" {} "cat ${optionsDoc.optionsCommonMark} >> $out" + pkgs.runCommand "options-doc.md" {} '' + sed -r '${replaceLinkNamesPattern};${replaceLinkContentsPattern}' '${optionsDoc.optionsCommonMark}' >> $out'' From 8709b019ead2041b850a347992a598b54d69dec4 Mon Sep 17 00:00:00 2001 From: Alex Tau Date: Sat, 10 May 2025 16:46:36 +0300 Subject: [PATCH 17/56] do not use relative paths in link text --- docs/nixos-options.md | 32 ++++++++++++++++---------------- mkdocs.nix | 2 +- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/docs/nixos-options.md b/docs/nixos-options.md index 7fc56f5..0e3a81f 100644 --- a/docs/nixos-options.md +++ b/docs/nixos-options.md @@ -20,7 +20,7 @@ boolean ` true ` *Declared by:* - - [../modules/options\.nix](../modules/options.nix) + - [modules/options\.nix](../modules/options.nix) @@ -41,7 +41,7 @@ list of (one of “start”, “stop”, “temp”, “vulnix”) ` [ ] ` *Declared by:* - - [../modules/options\.nix](../modules/options.nix) + - [modules/options\.nix](../modules/options.nix) @@ -84,7 +84,7 @@ attribute set of (submodule) ``` *Declared by:* - - [../modules/options\.nix](../modules/options.nix) + - [modules/options\.nix](../modules/options.nix) @@ -105,7 +105,7 @@ boolean ` true ` *Declared by:* - - [../modules/options\.nix](../modules/options.nix) + - [modules/options\.nix](../modules/options.nix) @@ -126,7 +126,7 @@ null or string ` null ` *Declared by:* - - [../modules/options\.nix](../modules/options.nix) + - [modules/options\.nix](../modules/options.nix) @@ -147,7 +147,7 @@ attribute set of (submodule) ` { } ` *Declared by:* - - [../modules/options\.nix](../modules/options.nix) + - [modules/options\.nix](../modules/options.nix) @@ -168,7 +168,7 @@ boolean ` true ` *Declared by:* - - [../modules/options\.nix](../modules/options.nix) + - [modules/options\.nix](../modules/options.nix) @@ -189,7 +189,7 @@ null or floating point number ` null ` *Declared by:* - - [../modules/options\.nix](../modules/options.nix) + - [modules/options\.nix](../modules/options.nix) @@ -210,7 +210,7 @@ null or string ` null ` *Declared by:* - - [../modules/options\.nix](../modules/options.nix) + - [modules/options\.nix](../modules/options.nix) @@ -231,7 +231,7 @@ null or floating point number ` null ` *Declared by:* - - [../modules/options\.nix](../modules/options.nix) + - [modules/options\.nix](../modules/options.nix) @@ -266,7 +266,7 @@ attribute set of (submodule) ``` *Declared by:* - - [../modules/options\.nix](../modules/options.nix) + - [modules/options\.nix](../modules/options.nix) @@ -288,7 +288,7 @@ null or (list of string) ` null ` *Declared by:* - - [../modules/options\.nix](../modules/options.nix) + - [modules/options\.nix](../modules/options.nix) @@ -309,7 +309,7 @@ null or string ` null ` *Declared by:* - - [../modules/options\.nix](../modules/options.nix) + - [modules/options\.nix](../modules/options.nix) @@ -331,7 +331,7 @@ null or string ` null ` *Declared by:* - - [../modules/options\.nix](../modules/options.nix) + - [modules/options\.nix](../modules/options.nix) @@ -347,7 +347,7 @@ Path to a file containing Telegram api_id, api_hash, and bot token, separated by string *Declared by:* - - [../modules/options\.nix](../modules/options.nix) + - [modules/options\.nix](../modules/options.nix) @@ -363,6 +363,6 @@ ID of chat where to send alerts\. signed integer *Declared by:* - - [../modules/options\.nix](../modules/options.nix) + - [modules/options\.nix](../modules/options.nix) diff --git a/mkdocs.nix b/mkdocs.nix index a82a16c..1efe88c 100644 --- a/mkdocs.nix +++ b/mkdocs.nix @@ -11,7 +11,7 @@ let optionsDoc = pkgs.nixosOptionsDoc { options = builtins.removeAttrs optEval.options [ "_module" ]; }; - replaceLinkNamesPattern = ''sR\[/nix/store/[a-z0-9]+-source/R[../R''; + replaceLinkNamesPattern = ''sR\[/nix/store/[a-z0-9]+-source/R[R''; replaceLinkContentsPattern = ''sR\(file:///nix/store/[a-z0-9]+-source/R(../R''; in pkgs.runCommand "options-doc.md" {} '' From 5095057a1381c3611a7f3027dd05dbfd6a9b4b61 Mon Sep 17 00:00:00 2001 From: Alex Tau Date: Sat, 10 May 2025 22:43:29 +0300 Subject: [PATCH 18/56] add cpu check --- docs/nixos-options.md | 44 ++++++++++++++++++++++- modules/default.nix | 7 +++- modules/options.nix | 14 ++++++++ pyproject.toml | 2 +- src/lego_monitoring/__init__.py | 2 +- src/lego_monitoring/alerting/enum.py | 4 +-- src/lego_monitoring/checks/__init__.py | 1 + src/lego_monitoring/checks/cpu.py | 30 ++++++++++++++++ src/lego_monitoring/config/__init__.py | 6 ++-- src/lego_monitoring/config/checks/cpu.py | 8 +++++ src/lego_monitoring/config/checks/temp.py | 18 +++++----- uv.lock | 8 ++--- 12 files changed, 123 insertions(+), 21 deletions(-) create mode 100644 src/lego_monitoring/checks/cpu.py create mode 100644 src/lego_monitoring/config/checks/cpu.py diff --git a/docs/nixos-options.md b/docs/nixos-options.md index 0e3a81f..1839ef9 100644 --- a/docs/nixos-options.md +++ b/docs/nixos-options.md @@ -33,7 +33,7 @@ List of enabled check sets\. Each check set is a module which checks something a *Type:* -list of (one of “start”, “stop”, “temp”, “vulnix”) +list of (one of “start”, “stop”, “temp”, “cpu”, “vulnix”) @@ -45,8 +45,50 @@ list of (one of “start”, “stop”, “temp”, “vulnix”) +## services\.lego-monitoring\.checks\.cpu\.criticalPercentage + +CPU load percentage for a critical alert to be sent\. Null means never generate a CPU critical alert\. + + + +*Type:* +null or floating point number + + + +*Default:* +` 90.0 ` + +*Declared by:* + - [modules/options\.nix](../modules/options.nix) + + + +## services\.lego-monitoring\.checks\.cpu\.warningPercentage + + + +CPU load percentage for a warning alert is sent\. Null means never generate a CPU warning alert\. + + + +*Type:* +null or floating point number + + + +*Default:* +` 80.0 ` + +*Declared by:* + - [modules/options\.nix](../modules/options.nix) + + + ## services\.lego-monitoring\.checks\.temp\.sensors + + Temp sensor override definitions\. Sensors not defined here, or missing options in definitions, will be read with default parameters\. To get list of sensors and their default configurations, run ` lego-monitoring --print-temp `\. diff --git a/modules/default.nix b/modules/default.nix index d48361c..448fe11 100644 --- a/modules/default.nix +++ b/modules/default.nix @@ -11,7 +11,7 @@ package: imports = [ ./options.nix ]; - + config = let cfg = config.services.lego-monitoring; json = pkgs.formats.json {}; @@ -49,6 +49,11 @@ package: }) cfg.checks.temp.sensors; vulnix.whitelist_path = vulnixWhitelistFile; + + cpu = with cfg.checks.cpu; { + warning_percentage = warningPercentage; + critical_percentage = criticalPercentage; + }; }; }; in lib.mkIf cfg.enable { diff --git a/modules/options.nix b/modules/options.nix index 2a65787..27e9f82 100644 --- a/modules/options.nix +++ b/modules/options.nix @@ -16,6 +16,7 @@ in "start" "stop" "temp" + "cpu" "vulnix" ]); default = [ ]; @@ -76,6 +77,19 @@ in }''; }; }; + + cpu = { + warningPercentage = lib.mkOption { + type = lib.types.nullOr lib.types.float; + default = 80.0; + description = "CPU load percentage for a warning alert is sent. Null means never generate a CPU warning alert."; + }; + criticalPercentage = lib.mkOption { + type = lib.types.nullOr lib.types.float; + default = 90.0; + description = "CPU load percentage for a critical alert to be sent. Null means never generate a CPU critical alert."; + }; + }; }; }; } diff --git a/pyproject.toml b/pyproject.toml index 87702ac..dcb602a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ description = "Monitoring software for the lego server" readme = "README.md" requires-python = ">=3.12" dependencies = [ - "alt-utils>=0.0.7", + "alt-utils>=0.0.8", "psutil>=7.0.0", "telethon>=1.40.0", ] diff --git a/src/lego_monitoring/__init__.py b/src/lego_monitoring/__init__.py index 12191b0..f1299cc 100644 --- a/src/lego_monitoring/__init__.py +++ b/src/lego_monitoring/__init__.py @@ -3,7 +3,6 @@ import asyncio import datetime import logging import signal -import time from . import checks from .alerting import alerts @@ -57,6 +56,7 @@ async def async_main(): "stop": [], # this is checked later "temp": [interval_checker(checks.temp_check, datetime.timedelta(minutes=5))], "vulnix": [interval_checker(checks.vulnix_check, datetime.timedelta(days=3))], + "cpu": [interval_checker(checks.cpu_check, datetime.timedelta(minutes=5))], } checkers = [] diff --git a/src/lego_monitoring/alerting/enum.py b/src/lego_monitoring/alerting/enum.py index de6ca5e..0b92bb1 100644 --- a/src/lego_monitoring/alerting/enum.py +++ b/src/lego_monitoring/alerting/enum.py @@ -3,12 +3,12 @@ from enum import StrEnum class AlertType(StrEnum): BOOT = "BOOT" + CPU = "CPU" + ERROR = "ERROR" TEMP = "TEMP" TEST = "TEST" VULN = "VULN" - ERROR = "ERROR" # RAM = "RAM" - # CPU = "CPU" # LOGIN = "LOGIN" # SMART = "SMART" # TODO # RAID = "RAID" diff --git a/src/lego_monitoring/checks/__init__.py b/src/lego_monitoring/checks/__init__.py index df26ddf..8818d25 100644 --- a/src/lego_monitoring/checks/__init__.py +++ b/src/lego_monitoring/checks/__init__.py @@ -1,2 +1,3 @@ +from .cpu import cpu_check from .temp import temp_check from .vulnix import vulnix_check diff --git a/src/lego_monitoring/checks/cpu.py b/src/lego_monitoring/checks/cpu.py new file mode 100644 index 0000000..de46820 --- /dev/null +++ b/src/lego_monitoring/checks/cpu.py @@ -0,0 +1,30 @@ +from psutil import cpu_percent + +from lego_monitoring.alerting import alerts +from lego_monitoring.alerting.enum import AlertType, Severity +from lego_monitoring.core import cvars + +IS_TESTING = False + + +def cpu_check() -> list[alerts.Alert]: + percentage = cpu_percent() + config = cvars.config.get().checks.cpu + if config.critical_percentage and (IS_TESTING or percentage > config.critical_percentage): + return [ + alerts.Alert( + alert_type=AlertType.CPU, + message=f"CPU load: {percentage:.2f}% > {config.critical_percentage:.2f}%", + severity=Severity.CRITICAL, + ) + ] + elif config.warning_percentage and (IS_TESTING or percentage > config.warning_percentage): + return [ + alerts.Alert( + alert_type=AlertType.CPU, + message=f"CPU load: {percentage:.2f}% > {config.warning_percentage:.2f}%", + severity=Severity.WARNING, + ) + ] + else: + return [] diff --git a/src/lego_monitoring/config/__init__.py b/src/lego_monitoring/config/__init__.py index d6518bb..e08fdd9 100644 --- a/src/lego_monitoring/config/__init__.py +++ b/src/lego_monitoring/config/__init__.py @@ -1,15 +1,17 @@ import json -from dataclasses import dataclass +from dataclasses import dataclass, field from typing import Optional from alt_utils import NestedDeserializableDataclass +from .checks.cpu import CpuCheckConfig from .checks.temp import TempCheckConfig from .checks.vulnix import VulnixCheckConfig @dataclass class ChecksConfig(NestedDeserializableDataclass): + cpu: Optional[CpuCheckConfig] = None temp: Optional[TempCheckConfig] = None vulnix: Optional[VulnixCheckConfig] = None @@ -22,9 +24,9 @@ class TelegramConfig: @dataclass class Config(NestedDeserializableDataclass): - enabled_check_sets: list[str] checks: ChecksConfig telegram: TelegramConfig + enabled_check_sets: list[str] = field(default_factory=list) def load_config(filepath: str) -> Config: diff --git a/src/lego_monitoring/config/checks/cpu.py b/src/lego_monitoring/config/checks/cpu.py new file mode 100644 index 0000000..174687a --- /dev/null +++ b/src/lego_monitoring/config/checks/cpu.py @@ -0,0 +1,8 @@ +from dataclasses import dataclass +from typing import Optional + + +@dataclass +class CpuCheckConfig: + warning_percentage: Optional[float] = 80 + critical_percentage: Optional[float] = 90 diff --git a/src/lego_monitoring/config/checks/temp.py b/src/lego_monitoring/config/checks/temp.py index dea6b8b..da36d9f 100644 --- a/src/lego_monitoring/config/checks/temp.py +++ b/src/lego_monitoring/config/checks/temp.py @@ -1,4 +1,4 @@ -from dataclasses import dataclass +from dataclasses import dataclass, field from typing import Optional from alt_utils import NestedDeserializableDataclass @@ -6,19 +6,19 @@ from alt_utils import NestedDeserializableDataclass @dataclass class TempReadingConfig: - label: Optional[str] - enabled: bool - warning_temp: Optional[float] - critical_temp: Optional[float] + label: Optional[str] = None + enabled: bool = True + warning_temp: Optional[float] = None + critical_temp: Optional[float] = None @dataclass class TempSensorConfig(NestedDeserializableDataclass): - name: Optional[str] - enabled: bool - readings: dict[str, TempReadingConfig] + name: Optional[str] = None + enabled: bool = True + readings: dict[str, TempReadingConfig] = field(default_factory=dict) @dataclass class TempCheckConfig(NestedDeserializableDataclass): - sensors: dict[str, TempSensorConfig] + sensors: dict[str, TempSensorConfig] = field(default_factory=dict) diff --git a/uv.lock b/uv.lock index 2e89def..f5ec571 100644 --- a/uv.lock +++ b/uv.lock @@ -3,11 +3,11 @@ requires-python = ">=3.12" [[package]] name = "alt-utils" -version = "0.0.6" +version = "0.0.8" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/22/d2/b4a3ea37f773696b07a545e8964c37e98e4939d5f8e3dae949d2cd4e4f53/alt_utils-0.0.6.tar.gz", hash = "sha256:91b8ca633238e819848e1f8b351892f4c148c7fddef120d5e966e3a0b5d06f81", size = 6001 } +sdist = { url = "https://files.pythonhosted.org/packages/31/15/67246107a8c808a9e99b34fd0024bebe954a67f3c315821eae985b87db7f/alt_utils-0.0.8.tar.gz", hash = "sha256:4b2901df0be4af736210277d58e231d4c4bce597a8fc665a8dd3e7b582705081", size = 6103 } wheels = [ - { url = "https://files.pythonhosted.org/packages/c1/27/0c963d6c64150e3fb2f98eb01773e2f9cf9b51f5b65632944bff67a68ec2/alt_utils-0.0.6-py3-none-any.whl", hash = "sha256:e4fd04394827eb49ae0d835f645ea03de1d9637a77acd5674a35890ae22abbef", size = 6260 }, + { url = "https://files.pythonhosted.org/packages/9a/5a/7fe15b55fa0ff5528643750c409cd14da005406aef312b32512d8a8487ab/alt_utils-0.0.8-py3-none-any.whl", hash = "sha256:af5549c49543ff4a02b735308bc2a5bfb7f20755620652fd969a648bbaecbc47", size = 6378 }, ] [[package]] @@ -22,7 +22,7 @@ dependencies = [ [package.metadata] requires-dist = [ - { name = "alt-utils", specifier = ">=0.0.6" }, + { name = "alt-utils", specifier = ">=0.0.8" }, { name = "psutil", specifier = ">=7.0.0" }, { name = "telethon", specifier = ">=1.40.0" }, ] From da85a566c4684a9ecb5bb02175285f9eeda8d5ec Mon Sep 17 00:00:00 2001 From: Alex Tau Date: Tue, 13 May 2025 14:15:56 +0300 Subject: [PATCH 19/56] ram check, configurable loglevel --- docs/nixos-options.md | 67 +++++++++++++++++++++++- modules/default.nix | 6 +++ modules/options.nix | 32 ++++++++++- src/lego_monitoring/__init__.py | 20 +++---- src/lego_monitoring/alerting/enum.py | 2 +- src/lego_monitoring/checks/__init__.py | 1 + src/lego_monitoring/checks/ram.py | 30 +++++++++++ src/lego_monitoring/config/__init__.py | 12 +++-- src/lego_monitoring/config/checks/ram.py | 8 +++ src/lego_monitoring/config/enums.py | 20 +++++++ 10 files changed, 180 insertions(+), 18 deletions(-) create mode 100644 src/lego_monitoring/checks/ram.py create mode 100644 src/lego_monitoring/config/checks/ram.py create mode 100644 src/lego_monitoring/config/enums.py diff --git a/docs/nixos-options.md b/docs/nixos-options.md index 1839ef9..596cc5e 100644 --- a/docs/nixos-options.md +++ b/docs/nixos-options.md @@ -33,7 +33,7 @@ List of enabled check sets\. Each check set is a module which checks something a *Type:* -list of (one of “start”, “stop”, “temp”, “cpu”, “vulnix”) +list of (one of “start”, “stop”, “cpu”, “ram”, “temp”, “vulnix”) @@ -68,7 +68,49 @@ null or floating point number -CPU load percentage for a warning alert is sent\. Null means never generate a CPU warning alert\. +CPU load percentage for a warning alert to be sent\. Null means never generate a CPU warning alert\. + + + +*Type:* +null or floating point number + + + +*Default:* +` 80.0 ` + +*Declared by:* + - [modules/options\.nix](../modules/options.nix) + + + +## services\.lego-monitoring\.checks\.ram\.criticalPercentage + + + +RAM usage percentage for a critical alert to be sent\. Null means never generate a RAM critical alert\. + + + +*Type:* +null or floating point number + + + +*Default:* +` 90.0 ` + +*Declared by:* + - [modules/options\.nix](../modules/options.nix) + + + +## services\.lego-monitoring\.checks\.ram\.warningPercentage + + + +RAM usage percentage for a warning alert to be sent\. Null means never generate a RAM warning alert\. @@ -377,6 +419,27 @@ null or string +## services\.lego-monitoring\.logLevel + + + +Level of logging\. INFO generates a log message with every check\. + + + +*Type:* +one of “CRITICAL”, “ERROR”, “WARNING”, “INFO”, “DEBUG” + + + +*Default:* +` "INFO" ` + +*Declared by:* + - [modules/options\.nix](../modules/options.nix) + + + ## services\.lego-monitoring\.telegram\.credsSecretPath diff --git a/modules/default.nix b/modules/default.nix index 448fe11..18153ab 100644 --- a/modules/default.nix +++ b/modules/default.nix @@ -30,6 +30,7 @@ package: serviceConfigFile = json.generate "config.json" { enabled_check_sets = cfg.enabledCheckSets; + log_level = cfg.logLevel; telegram = with cfg.telegram; { creds_secret_path = credsSecretPath; room_id = roomId; @@ -54,6 +55,11 @@ package: warning_percentage = warningPercentage; critical_percentage = criticalPercentage; }; + + ram = with cfg.checks.ram; { + warning_percentage = warningPercentage; + critical_percentage = criticalPercentage; + }; }; }; in lib.mkIf cfg.enable { diff --git a/modules/options.nix b/modules/options.nix index 27e9f82..815a640 100644 --- a/modules/options.nix +++ b/modules/options.nix @@ -11,12 +11,27 @@ in options.services.lego-monitoring = { enable = lib.mkEnableOption "lego-monitoring service"; + logLevel = lib.mkOption { + type = lib.types.enum [ + "CRITICAL" + "ERROR" + "WARNING" + "INFO" + "DEBUG" + ]; + default = "INFO"; + description = "Level of logging. INFO generates a log message with every check."; + }; + enabledCheckSets = lib.mkOption { type = lib.types.listOf (lib.types.enum [ "start" "stop" - "temp" + "cpu" + "ram" + "temp" + "vulnix" ]); default = [ ]; @@ -82,7 +97,7 @@ in warningPercentage = lib.mkOption { type = lib.types.nullOr lib.types.float; default = 80.0; - description = "CPU load percentage for a warning alert is sent. Null means never generate a CPU warning alert."; + description = "CPU load percentage for a warning alert to be sent. Null means never generate a CPU warning alert."; }; criticalPercentage = lib.mkOption { type = lib.types.nullOr lib.types.float; @@ -90,6 +105,19 @@ in description = "CPU load percentage for a critical alert to be sent. Null means never generate a CPU critical alert."; }; }; + + ram = { + warningPercentage = lib.mkOption { + type = lib.types.nullOr lib.types.float; + default = 80.0; + description = "RAM usage percentage for a warning alert to be sent. Null means never generate a RAM warning alert."; + }; + criticalPercentage = lib.mkOption { + type = lib.types.nullOr lib.types.float; + default = 90.0; + description = "RAM usage percentage for a critical alert to be sent. Null means never generate a RAM critical alert."; + }; + }; }; }; } diff --git a/src/lego_monitoring/__init__.py b/src/lego_monitoring/__init__.py index f1299cc..987c225 100644 --- a/src/lego_monitoring/__init__.py +++ b/src/lego_monitoring/__init__.py @@ -7,6 +7,7 @@ import signal from . import checks from .alerting import alerts from .checks.temp.sensors import print_readings +from .config import enums as config_enums from .config import load_config from .core import cvars from .core.checkers import interval_checker @@ -20,8 +21,6 @@ def stop_gracefully(signum, frame): def main() -> None: - logging.basicConfig(level=logging.INFO) - asyncio.run(async_main()) @@ -46,17 +45,20 @@ async def async_main(): if not args.config: raise RuntimeError("--config must be specified in standard operating mode") + logging.basicConfig(level=config.log_level) + tg_client = await alerts.get_client() cvars.tg_client.set(tg_client) + check_sets = config_enums.CheckSet + checker_sets = { - "start": [ - alerts.send_start_alert(), - ], - "stop": [], # this is checked later - "temp": [interval_checker(checks.temp_check, datetime.timedelta(minutes=5))], - "vulnix": [interval_checker(checks.vulnix_check, datetime.timedelta(days=3))], - "cpu": [interval_checker(checks.cpu_check, datetime.timedelta(minutes=5))], + check_sets.START: [alerts.send_start_alert()], + check_sets.STOP: [], # this is checked later + check_sets.CPU: [interval_checker(checks.cpu_check, datetime.timedelta(minutes=5))], + check_sets.RAM: [interval_checker(checks.ram_check, datetime.timedelta(minutes=1))], + check_sets.TEMP: [interval_checker(checks.temp_check, datetime.timedelta(minutes=5))], + check_sets.VULNIX: [interval_checker(checks.vulnix_check, datetime.timedelta(days=3))], } checkers = [] diff --git a/src/lego_monitoring/alerting/enum.py b/src/lego_monitoring/alerting/enum.py index 0b92bb1..d3b6a18 100644 --- a/src/lego_monitoring/alerting/enum.py +++ b/src/lego_monitoring/alerting/enum.py @@ -5,10 +5,10 @@ class AlertType(StrEnum): BOOT = "BOOT" CPU = "CPU" ERROR = "ERROR" + RAM = "RAM" TEMP = "TEMP" TEST = "TEST" VULN = "VULN" - # RAM = "RAM" # LOGIN = "LOGIN" # SMART = "SMART" # TODO # RAID = "RAID" diff --git a/src/lego_monitoring/checks/__init__.py b/src/lego_monitoring/checks/__init__.py index 8818d25..ff19608 100644 --- a/src/lego_monitoring/checks/__init__.py +++ b/src/lego_monitoring/checks/__init__.py @@ -1,3 +1,4 @@ from .cpu import cpu_check +from .ram import ram_check from .temp import temp_check from .vulnix import vulnix_check diff --git a/src/lego_monitoring/checks/ram.py b/src/lego_monitoring/checks/ram.py new file mode 100644 index 0000000..334465f --- /dev/null +++ b/src/lego_monitoring/checks/ram.py @@ -0,0 +1,30 @@ +from psutil import virtual_memory + +from lego_monitoring.alerting import alerts +from lego_monitoring.alerting.enum import AlertType, Severity +from lego_monitoring.core import cvars + +IS_TESTING = False + + +def ram_check() -> list[alerts.Alert]: + percentage = virtual_memory().percent + config = cvars.config.get().checks.ram + if config.critical_percentage and (IS_TESTING or percentage > config.critical_percentage): + return [ + alerts.Alert( + alert_type=AlertType.RAM, + message=f"RAM usage: {percentage:.2f}% > {config.critical_percentage:.2f}%", + severity=Severity.CRITICAL, + ) + ] + elif config.warning_percentage and (IS_TESTING or percentage > config.warning_percentage): + return [ + alerts.Alert( + alert_type=AlertType.RAM, + message=f"RAM usage: {percentage:.2f}% > {config.warning_percentage:.2f}%", + severity=Severity.WARNING, + ) + ] + else: + return [] diff --git a/src/lego_monitoring/config/__init__.py b/src/lego_monitoring/config/__init__.py index e08fdd9..3d8917e 100644 --- a/src/lego_monitoring/config/__init__.py +++ b/src/lego_monitoring/config/__init__.py @@ -4,16 +4,19 @@ from typing import Optional from alt_utils import NestedDeserializableDataclass +from . import enums from .checks.cpu import CpuCheckConfig +from .checks.ram import RamCheckConfig from .checks.temp import TempCheckConfig from .checks.vulnix import VulnixCheckConfig @dataclass class ChecksConfig(NestedDeserializableDataclass): - cpu: Optional[CpuCheckConfig] = None - temp: Optional[TempCheckConfig] = None - vulnix: Optional[VulnixCheckConfig] = None + cpu: CpuCheckConfig = field(default_factory=CpuCheckConfig) + ram: RamCheckConfig = field(default_factory=RamCheckConfig) + temp: TempCheckConfig = field(default_factory=TempCheckConfig) + vulnix: Optional[VulnixCheckConfig] = None # vulnix check WILL raise if this config section is None @dataclass @@ -26,7 +29,8 @@ class TelegramConfig: class Config(NestedDeserializableDataclass): checks: ChecksConfig telegram: TelegramConfig - enabled_check_sets: list[str] = field(default_factory=list) + enabled_check_sets: list[enums.CheckSet] = field(default_factory=list) + log_level: enums.LogLevelName = enums.LogLevelName.INFO def load_config(filepath: str) -> Config: diff --git a/src/lego_monitoring/config/checks/ram.py b/src/lego_monitoring/config/checks/ram.py new file mode 100644 index 0000000..2f46d8d --- /dev/null +++ b/src/lego_monitoring/config/checks/ram.py @@ -0,0 +1,8 @@ +from dataclasses import dataclass +from typing import Optional + + +@dataclass +class RamCheckConfig: + warning_percentage: Optional[float] = 80 + critical_percentage: Optional[float] = 90 diff --git a/src/lego_monitoring/config/enums.py b/src/lego_monitoring/config/enums.py new file mode 100644 index 0000000..54954cf --- /dev/null +++ b/src/lego_monitoring/config/enums.py @@ -0,0 +1,20 @@ +from enum import StrEnum + + +class CheckSet(StrEnum): + START = "start" + STOP = "stop" + + CPU = "cpu" + RAM = "ram" + TEMP = "temp" + + VULNIX = "vulnix" + + +class LogLevelName(StrEnum): + CRITICAL = "CRITICAL" + ERROR = "ERROR" + WARNING = "WARNING" + INFO = "INFO" + DEBUG = "DEBUG" From 2488cf6b070f7594647d7ba6e3fdba6bae3e6947 Mon Sep 17 00:00:00 2001 From: Alex Tau Date: Fri, 30 May 2025 16:55:26 +0300 Subject: [PATCH 20/56] restart service if it fails --- modules/default.nix | 1 + 1 file changed, 1 insertion(+) diff --git a/modules/default.nix b/modules/default.nix index 18153ab..28d1203 100644 --- a/modules/default.nix +++ b/modules/default.nix @@ -68,6 +68,7 @@ package: description = "Lego-monitoring service"; script = "${package}/bin/lego-monitoring -c ${serviceConfigFile}"; wantedBy = [ "multi-user.target" ]; + serviceConfig.Restart = "on-failure"; }; }; } From 5d2759c63c7214a587f74d6933a1b89a17382f8b Mon Sep 17 00:00:00 2001 From: Alex Tau Date: Sat, 31 May 2025 18:48:23 +0300 Subject: [PATCH 21/56] retry telegram connection forever --- src/lego_monitoring/alerting/alerts.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lego_monitoring/alerting/alerts.py b/src/lego_monitoring/alerting/alerts.py index 2241b6e..dfa61a4 100644 --- a/src/lego_monitoring/alerting/alerts.py +++ b/src/lego_monitoring/alerting/alerts.py @@ -17,7 +17,7 @@ class Alert: async def get_client() -> TelegramClient: config = cvars.config.get() api_id, api_hash, bot_token = config.telegram.creds.split(",") - client = await TelegramClient(MemorySession(), api_id, api_hash).start(bot_token=bot_token) + client = await TelegramClient(MemorySession(), api_id, api_hash, connection_retries=None).start(bot_token=bot_token) client.parse_mode = "html" return client From eef6ec59b0baf808ca41a078cc0eecaa4eb8aefa Mon Sep 17 00:00:00 2001 From: Alex Tau Date: Thu, 5 Jun 2025 21:45:01 +0300 Subject: [PATCH 22/56] checkers are now objects, lay foundation for persistent alerts --- modules/default.nix | 9 +- src/lego_monitoring/__init__.py | 28 +++-- src/lego_monitoring/alerting/alert.py | 10 ++ .../alerting/{alerts.py => channel.py} | 10 +- src/lego_monitoring/alerting/current.py | 26 ++++ src/lego_monitoring/alerting/enum.py | 11 +- src/lego_monitoring/checks/cpu.py | 8 +- src/lego_monitoring/checks/ram.py | 8 +- src/lego_monitoring/checks/temp/__init__.py | 8 +- src/lego_monitoring/checks/vulnix/__init__.py | 11 +- src/lego_monitoring/core/checkers.py | 112 +++++++++++------- src/lego_monitoring/core/cvars.py | 2 + 12 files changed, 162 insertions(+), 81 deletions(-) create mode 100644 src/lego_monitoring/alerting/alert.py rename src/lego_monitoring/alerting/{alerts.py => channel.py} (95%) create mode 100644 src/lego_monitoring/alerting/current.py diff --git a/modules/default.nix b/modules/default.nix index 28d1203..1cff5ad 100644 --- a/modules/default.nix +++ b/modules/default.nix @@ -68,7 +68,14 @@ package: description = "Lego-monitoring service"; script = "${package}/bin/lego-monitoring -c ${serviceConfigFile}"; wantedBy = [ "multi-user.target" ]; - serviceConfig.Restart = "on-failure"; + serviceConfig = { + Restart = "on-failure"; + RestartSec = "5"; + }; + unitConfig = { + StartLimitIntervalSec = 20; + StartLimitBurst = 3; + }; }; }; } diff --git a/src/lego_monitoring/__init__.py b/src/lego_monitoring/__init__.py index 987c225..1971316 100644 --- a/src/lego_monitoring/__init__.py +++ b/src/lego_monitoring/__init__.py @@ -5,12 +5,12 @@ import logging import signal from . import checks -from .alerting import alerts +from .alerting import channel from .checks.temp.sensors import print_readings from .config import enums as config_enums from .config import load_config from .core import cvars -from .core.checkers import interval_checker +from .core.checkers import IntervalChecker stopping = False @@ -47,18 +47,28 @@ async def async_main(): logging.basicConfig(level=config.log_level) - tg_client = await alerts.get_client() + tg_client = await channel.get_client() cvars.tg_client.set(tg_client) check_sets = config_enums.CheckSet checker_sets = { - check_sets.START: [alerts.send_start_alert()], + check_sets.START: [channel.send_start_alert()], check_sets.STOP: [], # this is checked later - check_sets.CPU: [interval_checker(checks.cpu_check, datetime.timedelta(minutes=5))], - check_sets.RAM: [interval_checker(checks.ram_check, datetime.timedelta(minutes=1))], - check_sets.TEMP: [interval_checker(checks.temp_check, datetime.timedelta(minutes=5))], - check_sets.VULNIX: [interval_checker(checks.vulnix_check, datetime.timedelta(days=3))], + check_sets.CPU: [ + IntervalChecker(checks.cpu_check, interval=datetime.timedelta(minutes=5), persistent=True).run_checker() + ], + check_sets.RAM: [ + IntervalChecker(checks.ram_check, interval=datetime.timedelta(minutes=1), persistent=True).run_checker() + ], + check_sets.TEMP: [ + IntervalChecker(checks.temp_check, interval=datetime.timedelta(minutes=5), persistent=True).run_checker() + ], + check_sets.VULNIX: [ + IntervalChecker( + checks.vulnix_check, interval=datetime.timedelta(days=3), persistent=True, send_same_state=True + ).run_checker() + ], } checkers = [] @@ -76,7 +86,7 @@ async def async_main(): while True: if stopping: if "stop" in config.enabled_check_sets: - await alerts.send_stop_alert() + await channel.send_stop_alert() await tg_client.disconnect() raise SystemExit else: diff --git a/src/lego_monitoring/alerting/alert.py b/src/lego_monitoring/alerting/alert.py new file mode 100644 index 0000000..a593d60 --- /dev/null +++ b/src/lego_monitoring/alerting/alert.py @@ -0,0 +1,10 @@ +from dataclasses import dataclass + +from .enum import AlertType, Severity + + +@dataclass +class Alert: + alert_type: AlertType + message: str + severity: Severity diff --git a/src/lego_monitoring/alerting/alerts.py b/src/lego_monitoring/alerting/channel.py similarity index 95% rename from src/lego_monitoring/alerting/alerts.py rename to src/lego_monitoring/alerting/channel.py index dfa61a4..09a72d7 100644 --- a/src/lego_monitoring/alerting/alerts.py +++ b/src/lego_monitoring/alerting/channel.py @@ -4,16 +4,10 @@ from telethon import TelegramClient from telethon.sessions import MemorySession from ..core import cvars +from .alert import Alert from .enum import AlertType, Severity -@dataclass -class Alert: - alert_type: AlertType - message: str - severity: Severity - - async def get_client() -> TelegramClient: config = cvars.config.get() api_id, api_hash, bot_token = config.telegram.creds.split(",") @@ -24,6 +18,8 @@ async def get_client() -> TelegramClient: def format_message(alert: Alert) -> str: match alert.severity: + case Severity.OK: + severity_emoji = "🟢" case Severity.INFO: severity_emoji = "ℹ️" case Severity.WARNING: diff --git a/src/lego_monitoring/alerting/current.py b/src/lego_monitoring/alerting/current.py new file mode 100644 index 0000000..dae575e --- /dev/null +++ b/src/lego_monitoring/alerting/current.py @@ -0,0 +1,26 @@ +from typing import Optional + +from .alert import Alert +from .enum import AlertType, Severity + + +class CurrentAlerts(list[Alert]): + def get_severity(self) -> Optional[Severity]: + max_severity = None + for a in self: + if max_severity is None or a.severity > max_severity: + max_severity = a.severity + return a.severity + + def get_types(self) -> set[AlertType]: + types = set() + for a in self: + types.add(a.alert_type) + return types + + def update(self, alerts: list[Alert]) -> tuple[Optional[Severity], Optional[Severity]]: + old_severity = self.get_severity() + self.clear() + self.extend(alerts) + new_severity = self.get_severity() + return (old_severity, new_severity) diff --git a/src/lego_monitoring/alerting/enum.py b/src/lego_monitoring/alerting/enum.py index d3b6a18..8ce5164 100644 --- a/src/lego_monitoring/alerting/enum.py +++ b/src/lego_monitoring/alerting/enum.py @@ -1,4 +1,4 @@ -from enum import StrEnum +from enum import IntEnum, StrEnum class AlertType(StrEnum): @@ -17,7 +17,8 @@ class AlertType(StrEnum): # UPDATE = "UPDATE" -class Severity(StrEnum): - INFO = "INFO" - WARNING = "WARNING" - CRITICAL = "CRITICAL" +class Severity(IntEnum): + OK = 0 # should only be used when persistent alerts resolve + INFO = 1 + WARNING = 2 + CRITICAL = 3 diff --git a/src/lego_monitoring/checks/cpu.py b/src/lego_monitoring/checks/cpu.py index de46820..ae335e9 100644 --- a/src/lego_monitoring/checks/cpu.py +++ b/src/lego_monitoring/checks/cpu.py @@ -1,18 +1,18 @@ from psutil import cpu_percent -from lego_monitoring.alerting import alerts +from lego_monitoring.alerting.alert import Alert from lego_monitoring.alerting.enum import AlertType, Severity from lego_monitoring.core import cvars IS_TESTING = False -def cpu_check() -> list[alerts.Alert]: +def cpu_check() -> list[Alert]: percentage = cpu_percent() config = cvars.config.get().checks.cpu if config.critical_percentage and (IS_TESTING or percentage > config.critical_percentage): return [ - alerts.Alert( + Alert( alert_type=AlertType.CPU, message=f"CPU load: {percentage:.2f}% > {config.critical_percentage:.2f}%", severity=Severity.CRITICAL, @@ -20,7 +20,7 @@ def cpu_check() -> list[alerts.Alert]: ] elif config.warning_percentage and (IS_TESTING or percentage > config.warning_percentage): return [ - alerts.Alert( + Alert( alert_type=AlertType.CPU, message=f"CPU load: {percentage:.2f}% > {config.warning_percentage:.2f}%", severity=Severity.WARNING, diff --git a/src/lego_monitoring/checks/ram.py b/src/lego_monitoring/checks/ram.py index 334465f..eff87f7 100644 --- a/src/lego_monitoring/checks/ram.py +++ b/src/lego_monitoring/checks/ram.py @@ -1,18 +1,18 @@ from psutil import virtual_memory -from lego_monitoring.alerting import alerts +from lego_monitoring.alerting.alert import Alert from lego_monitoring.alerting.enum import AlertType, Severity from lego_monitoring.core import cvars IS_TESTING = False -def ram_check() -> list[alerts.Alert]: +def ram_check() -> list[Alert]: percentage = virtual_memory().percent config = cvars.config.get().checks.ram if config.critical_percentage and (IS_TESTING or percentage > config.critical_percentage): return [ - alerts.Alert( + Alert( alert_type=AlertType.RAM, message=f"RAM usage: {percentage:.2f}% > {config.critical_percentage:.2f}%", severity=Severity.CRITICAL, @@ -20,7 +20,7 @@ def ram_check() -> list[alerts.Alert]: ] elif config.warning_percentage and (IS_TESTING or percentage > config.warning_percentage): return [ - alerts.Alert( + Alert( alert_type=AlertType.RAM, message=f"RAM usage: {percentage:.2f}% > {config.warning_percentage:.2f}%", severity=Severity.WARNING, diff --git a/src/lego_monitoring/checks/temp/__init__.py b/src/lego_monitoring/checks/temp/__init__.py index 4f965dc..9c68a3b 100644 --- a/src/lego_monitoring/checks/temp/__init__.py +++ b/src/lego_monitoring/checks/temp/__init__.py @@ -1,4 +1,4 @@ -from lego_monitoring.alerting import alerts +from lego_monitoring.alerting.alert import Alert from lego_monitoring.alerting.enum import AlertType, Severity from . import sensors @@ -6,19 +6,19 @@ from . import sensors IS_TESTING = False -def temp_check() -> list[alerts.Alert]: +def temp_check() -> list[Alert]: alert_list = [] temps = sensors.get_readings() for sensor, readings in temps.items(): for r in readings: if r.critical_temp is not None and (IS_TESTING or r.current_temp > r.critical_temp): - alert = alerts.Alert( + alert = Alert( alert_type=AlertType.TEMP, message=f"{sensor} {r.label}: {r.current_temp}°C > {r.critical_temp}°C", severity=Severity.CRITICAL, ) elif r.warning_temp is not None and (IS_TESTING or r.current_temp > r.warning_temp): - alert = alerts.Alert( + alert = Alert( alert_type=AlertType.TEMP, message=f"{sensor} {r.label}: {r.current_temp}°C > {r.warning_temp}°C", severity=Severity.WARNING, diff --git a/src/lego_monitoring/checks/vulnix/__init__.py b/src/lego_monitoring/checks/vulnix/__init__.py index c9f7a10..302bdc4 100644 --- a/src/lego_monitoring/checks/vulnix/__init__.py +++ b/src/lego_monitoring/checks/vulnix/__init__.py @@ -1,4 +1,5 @@ -from lego_monitoring.alerting import alerts +from lego_monitoring.alerting.alert import Alert +from lego_monitoring.alerting.channel import send_alert from lego_monitoring.alerting.enum import AlertType, Severity from .vulnix import get_vulnix_output @@ -6,13 +7,13 @@ from .vulnix import get_vulnix_output IS_TESTING = False -def vulnix_check() -> list[alerts.Alert]: +def vulnix_check() -> list[Alert]: alert_list = [] try: vulnix_output = get_vulnix_output(IS_TESTING) except Exception as e: - alerts.send_alert( - alerts.Alert( + send_alert( + Alert( alert_type=AlertType.ERROR, message=f"Exception {type(e).__name__} while calling vulnix: {e}", severity=Severity.CRITICAL, @@ -34,7 +35,7 @@ def vulnix_check() -> list[alerts.Alert]: score_str = "(not scored by CVSSv3)" message += f'\n* {cve} - {finding.description[cve]} {score_str}' - alert = alerts.Alert( + alert = Alert( alert_type=AlertType.VULN, message=message, severity=Severity.WARNING, diff --git a/src/lego_monitoring/core/checkers.py b/src/lego_monitoring/core/checkers.py index 12db2b8..5c2c97d 100644 --- a/src/lego_monitoring/core/checkers.py +++ b/src/lego_monitoring/core/checkers.py @@ -1,53 +1,81 @@ import asyncio import datetime import logging -from typing import Callable, Coroutine +from dataclasses import KW_ONLY, dataclass, field +from typing import Any, Callable, Coroutine -from ..alerting import alerts +from ..alerting.alert import Alert +from ..alerting.channel import send_alert -async def _call_check(check: Callable | Coroutine, *args, **kwargs) -> list[alerts.Alert]: - if isinstance(check, Callable): - result = check(*args, **kwargs) - if isinstance(result, Coroutine): - result = await result - elif isinstance(check, Coroutine): - result = await check - else: - raise TypeError(f"check is {type(check)}, neither function nor coroutine") - return result +@dataclass +class BaseChecker: + check: Callable | Coroutine + persistent: bool + send_same_state: bool = False + check_args: list = field(default_factory=list) + check_kwargs: dict[str, Any] = field(default_factory=dict) + + async def _call_check(self) -> list[Alert]: + if isinstance(self.check, Callable): + result = self.check(*self.check_args, **self.check_kwargs) + if isinstance(result, Coroutine): + result = await result + elif isinstance(self.check, Coroutine): + result = await self.check + else: + raise TypeError(f"check is {type(self.check)}, neither function nor coroutine") + return result + + async def _handle_alert(alert: Alert, persistent: bool, send_same_state: bool) -> None: + if not persistent: + await send_alert(alert) + return + ... + + async def run_checker(self): + raise NotImplementedError -async def interval_checker(check: Callable | Coroutine, interval: datetime.timedelta, *args, **kwargs): - interval_secs = interval.total_seconds() - while True: - logging.info(f"Calling {check.__name__}") - result = await _call_check(check, *args, **kwargs) - logging.info(f"Got {len(result)} alerts") - for alert in result: - await alerts.send_alert(alert) - await asyncio.sleep(interval_secs) +@dataclass +class IntervalChecker(BaseChecker): + _: KW_ONLY + interval: datetime.timedelta + + async def run_checker(self): + interval_secs = self.interval.total_seconds() + while True: + logging.info(f"Calling {self.check.__name__}") + result = await self._call_check() + logging.info(f"Got {len(result)} alerts") + for alert in result: + await send_alert(alert) + await asyncio.sleep(interval_secs) -async def scheduled_checker( - check: Callable | Coroutine, period: datetime.timedelta, when: datetime.time, *args, **kwargs -): - match period: - case datetime.timedelta(days=1): - while True: - now = datetime.datetime.now() - next_datetime = datetime.datetime.combine(datetime.date.today(), when) - if next_datetime < now: - next_datetime += datetime.timedelta(days=1) - logging.info(f"Scheduled to call {check.__name__} at {next_datetime.isoformat()}") - await asyncio.sleep( - (next_datetime - now).total_seconds() - ) # might be negative at this point, asyncio doesn't care +@dataclass +class ScheduledChecker(BaseChecker): + _: KW_ONLY + period: datetime.timedelta + when: datetime.time - logging.info(f"Calling {check.__name__}") - result = await _call_check(check, *args, **kwargs) - logging.info(f"Got {len(result)} alerts") - for alert in result: - await alerts.send_alert(alert) - case _: - raise NotImplementedError + async def run_checker(self): + match self.period: + case datetime.timedelta(days=1): + while True: + now = datetime.datetime.now() + next_datetime = datetime.datetime.combine(datetime.date.today(), self.when) + if next_datetime < now: + next_datetime += datetime.timedelta(days=1) + logging.info(f"Scheduled to call {self.check.__name__} at {next_datetime.isoformat()}") + await asyncio.sleep( + (next_datetime - now).total_seconds() + ) # might be negative at this point, asyncio doesn't care + + logging.info(f"Calling {self.check.__name__}") + result = await self._call_check() + logging.info(f"Got {len(result)} alerts") + for alert in result: + await send_alert(alert) + case _: + raise NotImplementedError diff --git a/src/lego_monitoring/core/cvars.py b/src/lego_monitoring/core/cvars.py index a4781c5..78dff36 100644 --- a/src/lego_monitoring/core/cvars.py +++ b/src/lego_monitoring/core/cvars.py @@ -2,7 +2,9 @@ from contextvars import ContextVar from telethon import TelegramClient +from ..alerting.current import CurrentAlerts from ..config import Config config: ContextVar[Config] = ContextVar("config") tg_client: ContextVar[TelegramClient] = ContextVar("tg_client") +current_alerts: ContextVar[list[CurrentAlerts]] = ContextVar("current_alerts", default=[]) From 2c234b2fd06fac0d80941219edf064ae520de785 Mon Sep 17 00:00:00 2001 From: Alex Tau Date: Thu, 5 Jun 2025 22:52:57 +0300 Subject: [PATCH 23/56] persistent alerts --- src/lego_monitoring/__init__.py | 4 +-- src/lego_monitoring/alerting/channel.py | 9 ++++--- src/lego_monitoring/alerting/current.py | 2 +- src/lego_monitoring/core/checkers.py | 35 ++++++++++++++++--------- src/lego_monitoring/core/cvars.py | 2 -- 5 files changed, 31 insertions(+), 21 deletions(-) diff --git a/src/lego_monitoring/__init__.py b/src/lego_monitoring/__init__.py index 1971316..eabd599 100644 --- a/src/lego_monitoring/__init__.py +++ b/src/lego_monitoring/__init__.py @@ -56,7 +56,7 @@ async def async_main(): check_sets.START: [channel.send_start_alert()], check_sets.STOP: [], # this is checked later check_sets.CPU: [ - IntervalChecker(checks.cpu_check, interval=datetime.timedelta(minutes=5), persistent=True).run_checker() + IntervalChecker(checks.cpu_check, interval=datetime.timedelta(minutes=3), persistent=True).run_checker() ], check_sets.RAM: [ IntervalChecker(checks.ram_check, interval=datetime.timedelta(minutes=1), persistent=True).run_checker() @@ -66,7 +66,7 @@ async def async_main(): ], check_sets.VULNIX: [ IntervalChecker( - checks.vulnix_check, interval=datetime.timedelta(days=3), persistent=True, send_same_state=True + checks.vulnix_check, interval=datetime.timedelta(days=3), persistent=True, send_any_state=True ).run_checker() ], } diff --git a/src/lego_monitoring/alerting/channel.py b/src/lego_monitoring/alerting/channel.py index 09a72d7..3124b6b 100644 --- a/src/lego_monitoring/alerting/channel.py +++ b/src/lego_monitoring/alerting/channel.py @@ -16,7 +16,7 @@ async def get_client() -> TelegramClient: return client -def format_message(alert: Alert) -> str: +def format_message(alert: Alert, persistent: bool) -> str: match alert.severity: case Severity.OK: severity_emoji = "🟢" @@ -26,11 +26,12 @@ def format_message(alert: Alert) -> str: severity_emoji = "⚠️" case Severity.CRITICAL: severity_emoji = "🆘" - message = f"{severity_emoji} {alert.alert_type} Alert\n{alert.message}" + persistent_marker = " - ongoing" if persistent else "" + message = f"{severity_emoji} {alert.alert_type} Alert{persistent_marker}\n{alert.message}" return message -async def send_alert(alert: Alert) -> None: +async def send_alert(alert: Alert, persistent: bool = False) -> None: try: client = cvars.tg_client.get() except LookupError: # being called standalone @@ -42,7 +43,7 @@ async def send_alert(alert: Alert) -> None: else: ... # temp_client = False room_id = cvars.config.get().telegram.room_id - message = format_message(alert) + message = format_message(alert, persistent) await client.send_message(entity=room_id, message=message) # if temp_client: # await client.close() diff --git a/src/lego_monitoring/alerting/current.py b/src/lego_monitoring/alerting/current.py index dae575e..bdd65e6 100644 --- a/src/lego_monitoring/alerting/current.py +++ b/src/lego_monitoring/alerting/current.py @@ -10,7 +10,7 @@ class CurrentAlerts(list[Alert]): for a in self: if max_severity is None or a.severity > max_severity: max_severity = a.severity - return a.severity + return max_severity def get_types(self) -> set[AlertType]: types = set() diff --git a/src/lego_monitoring/core/checkers.py b/src/lego_monitoring/core/checkers.py index 5c2c97d..9f976a3 100644 --- a/src/lego_monitoring/core/checkers.py +++ b/src/lego_monitoring/core/checkers.py @@ -6,15 +6,19 @@ from typing import Any, Callable, Coroutine from ..alerting.alert import Alert from ..alerting.channel import send_alert +from ..alerting.current import CurrentAlerts +from ..alerting.enum import Severity +from . import cvars @dataclass class BaseChecker: check: Callable | Coroutine persistent: bool - send_same_state: bool = False + send_any_state: bool = False check_args: list = field(default_factory=list) check_kwargs: dict[str, Any] = field(default_factory=dict) + current_alerts: CurrentAlerts = field(default_factory=CurrentAlerts, init=False) async def _call_check(self) -> list[Alert]: if isinstance(self.check, Callable): @@ -27,13 +31,22 @@ class BaseChecker: raise TypeError(f"check is {type(self.check)}, neither function nor coroutine") return result - async def _handle_alert(alert: Alert, persistent: bool, send_same_state: bool) -> None: - if not persistent: - await send_alert(alert) + async def _handle_alerts(self, alerts: list[Alert]) -> None: + if not self.persistent: + for alert in alerts: + await send_alert(alert) return - ... + old_types = self.current_alerts.get_types() + old_severity, new_severity = self.current_alerts.update(alerts) + new_types = self.current_alerts.get_types() + if old_severity != new_severity or self.send_any_state: + for alert in alerts: + await send_alert(alert, persistent=True) + for alert_type in old_types - new_types: + alert = Alert(alert_type=alert_type, message="Situation resolved", severity=Severity.OK) + await send_alert(alert) - async def run_checker(self): + async def run_checker(self) -> None: raise NotImplementedError @@ -42,14 +55,13 @@ class IntervalChecker(BaseChecker): _: KW_ONLY interval: datetime.timedelta - async def run_checker(self): + async def run_checker(self) -> None: interval_secs = self.interval.total_seconds() while True: logging.info(f"Calling {self.check.__name__}") result = await self._call_check() logging.info(f"Got {len(result)} alerts") - for alert in result: - await send_alert(alert) + await self._handle_alerts(result) await asyncio.sleep(interval_secs) @@ -59,7 +71,7 @@ class ScheduledChecker(BaseChecker): period: datetime.timedelta when: datetime.time - async def run_checker(self): + async def run_checker(self) -> None: match self.period: case datetime.timedelta(days=1): while True: @@ -75,7 +87,6 @@ class ScheduledChecker(BaseChecker): logging.info(f"Calling {self.check.__name__}") result = await self._call_check() logging.info(f"Got {len(result)} alerts") - for alert in result: - await send_alert(alert) + await self._handle_alerts(result) case _: raise NotImplementedError diff --git a/src/lego_monitoring/core/cvars.py b/src/lego_monitoring/core/cvars.py index 78dff36..a4781c5 100644 --- a/src/lego_monitoring/core/cvars.py +++ b/src/lego_monitoring/core/cvars.py @@ -2,9 +2,7 @@ from contextvars import ContextVar from telethon import TelegramClient -from ..alerting.current import CurrentAlerts from ..config import Config config: ContextVar[Config] = ContextVar("config") tg_client: ContextVar[TelegramClient] = ContextVar("tg_client") -current_alerts: ContextVar[list[CurrentAlerts]] = ContextVar("current_alerts", default=[]) From f691180e9b385bf7a6d485f4c5de135481bf9813 Mon Sep 17 00:00:00 2001 From: Alex Tau Date: Fri, 6 Jun 2025 00:46:44 +0300 Subject: [PATCH 24/56] remind about persistent alerts --- modules/options.nix | 1 + src/lego_monitoring/__init__.py | 39 ++++++++++++++-------- src/lego_monitoring/alerting/channel.py | 22 ++++--------- src/lego_monitoring/alerting/enum.py | 9 ++++++ src/lego_monitoring/checks/__init__.py | 1 + src/lego_monitoring/checks/remind.py | 43 +++++++++++++++++++++++++ src/lego_monitoring/config/enums.py | 1 + src/lego_monitoring/core/checkers.py | 26 +++++++++++++-- src/lego_monitoring/core/cvars.py | 3 ++ 9 files changed, 115 insertions(+), 30 deletions(-) create mode 100644 src/lego_monitoring/checks/remind.py diff --git a/modules/options.nix b/modules/options.nix index 815a640..b4984d6 100644 --- a/modules/options.nix +++ b/modules/options.nix @@ -27,6 +27,7 @@ in type = lib.types.listOf (lib.types.enum [ "start" "stop" + "remind" "cpu" "ram" diff --git a/src/lego_monitoring/__init__.py b/src/lego_monitoring/__init__.py index eabd599..389e785 100644 --- a/src/lego_monitoring/__init__.py +++ b/src/lego_monitoring/__init__.py @@ -3,6 +3,7 @@ import asyncio import datetime import logging import signal +from typing import Coroutine from . import checks from .alerting import channel @@ -10,7 +11,7 @@ from .checks.temp.sensors import print_readings from .config import enums as config_enums from .config import load_config from .core import cvars -from .core.checkers import IntervalChecker +from .core.checkers import BaseChecker, IntervalChecker, ScheduledChecker stopping = False @@ -52,25 +53,35 @@ async def async_main(): check_sets = config_enums.CheckSet - checker_sets = { + checker_sets: dict[config_enums.CheckSet, list[Coroutine | BaseChecker]] = { check_sets.START: [channel.send_start_alert()], check_sets.STOP: [], # this is checked later - check_sets.CPU: [ - IntervalChecker(checks.cpu_check, interval=datetime.timedelta(minutes=3), persistent=True).run_checker() - ], - check_sets.RAM: [ - IntervalChecker(checks.ram_check, interval=datetime.timedelta(minutes=1), persistent=True).run_checker() - ], - check_sets.TEMP: [ - IntervalChecker(checks.temp_check, interval=datetime.timedelta(minutes=5), persistent=True).run_checker() - ], + check_sets.CPU: [IntervalChecker(checks.cpu_check, interval=datetime.timedelta(minutes=3), persistent=True)], + check_sets.RAM: [IntervalChecker(checks.ram_check, interval=datetime.timedelta(minutes=1), persistent=True)], + check_sets.TEMP: [IntervalChecker(checks.temp_check, interval=datetime.timedelta(minutes=5), persistent=True)], check_sets.VULNIX: [ IntervalChecker( - checks.vulnix_check, interval=datetime.timedelta(days=3), persistent=True, send_any_state=True - ).run_checker() + checks.vulnix_check, + interval=datetime.timedelta(days=3), + persistent=True, + send_any_state=True, + # As those are checked less often than daily, reminds could lead to awkward situations + # when the vuln is fixed but you still get reminders about it for 2 more days. + remind=False, + ) + ], + check_sets.REMIND: [ + ScheduledChecker( + checks.remind_check, + period=datetime.timedelta(days=1), + when=datetime.time(hour=0, minute=0), + persistent=False, + ) ], } + checker_sets[check_sets.REMIND][0].check_args = [checker_sets] + checkers = [] for enabled_set in config.enabled_check_sets: for checker in checker_sets[enabled_set]: @@ -81,6 +92,8 @@ async def async_main(): async with asyncio.TaskGroup() as tg: checker_tasks: set[asyncio.Task] = set() for c in checkers: + if isinstance(c, BaseChecker): + c = c.run_checker() task = tg.create_task(c) checker_tasks.add(task) while True: diff --git a/src/lego_monitoring/alerting/channel.py b/src/lego_monitoring/alerting/channel.py index 3124b6b..b85dbff 100644 --- a/src/lego_monitoring/alerting/channel.py +++ b/src/lego_monitoring/alerting/channel.py @@ -5,7 +5,7 @@ from telethon.sessions import MemorySession from ..core import cvars from .alert import Alert -from .enum import AlertType, Severity +from .enum import SEVERITY_TO_EMOJI, AlertType, Severity async def get_client() -> TelegramClient: @@ -16,22 +16,14 @@ async def get_client() -> TelegramClient: return client -def format_message(alert: Alert, persistent: bool) -> str: - match alert.severity: - case Severity.OK: - severity_emoji = "🟢" - case Severity.INFO: - severity_emoji = "ℹ️" - case Severity.WARNING: - severity_emoji = "⚠️" - case Severity.CRITICAL: - severity_emoji = "🆘" - persistent_marker = " - ongoing" if persistent else "" - message = f"{severity_emoji} {alert.alert_type} Alert{persistent_marker}\n{alert.message}" +def format_message(alert: Alert, note: str) -> str: + severity_emoji = SEVERITY_TO_EMOJI[alert.severity] + note_formatted = f" - {note}" if note else "" + message = f"{severity_emoji} {alert.alert_type} Alert{note_formatted}\n{alert.message}" return message -async def send_alert(alert: Alert, persistent: bool = False) -> None: +async def send_alert(alert: Alert, note: str = "") -> None: try: client = cvars.tg_client.get() except LookupError: # being called standalone @@ -43,7 +35,7 @@ async def send_alert(alert: Alert, persistent: bool = False) -> None: else: ... # temp_client = False room_id = cvars.config.get().telegram.room_id - message = format_message(alert, persistent) + message = format_message(alert, note) await client.send_message(entity=room_id, message=message) # if temp_client: # await client.close() diff --git a/src/lego_monitoring/alerting/enum.py b/src/lego_monitoring/alerting/enum.py index 8ce5164..003398b 100644 --- a/src/lego_monitoring/alerting/enum.py +++ b/src/lego_monitoring/alerting/enum.py @@ -9,6 +9,7 @@ class AlertType(StrEnum): TEMP = "TEMP" TEST = "TEST" VULN = "VULN" + REMIND = "REMIND" # LOGIN = "LOGIN" # SMART = "SMART" # TODO # RAID = "RAID" @@ -22,3 +23,11 @@ class Severity(IntEnum): INFO = 1 WARNING = 2 CRITICAL = 3 + + +SEVERITY_TO_EMOJI = { + Severity.OK: "🟢", + Severity.INFO: "ℹ️", + Severity.WARNING: "⚠️", + Severity.CRITICAL: "🆘", +} diff --git a/src/lego_monitoring/checks/__init__.py b/src/lego_monitoring/checks/__init__.py index ff19608..f076917 100644 --- a/src/lego_monitoring/checks/__init__.py +++ b/src/lego_monitoring/checks/__init__.py @@ -1,4 +1,5 @@ from .cpu import cpu_check from .ram import ram_check +from .remind import remind_check from .temp import temp_check from .vulnix import vulnix_check diff --git a/src/lego_monitoring/checks/remind.py b/src/lego_monitoring/checks/remind.py new file mode 100644 index 0000000..7676eea --- /dev/null +++ b/src/lego_monitoring/checks/remind.py @@ -0,0 +1,43 @@ +from typing import Any, Coroutine + +from lego_monitoring.alerting.alert import Alert +from lego_monitoring.config.enums import CheckSet +from lego_monitoring.core.checkers import BaseChecker + + +def remind_check(checker_sets: dict[CheckSet, list[Coroutine | BaseChecker]]) -> list[Alert]: + alerts = [] + for checker_set in checker_sets.values(): + for c in checker_set: + if not isinstance(c, BaseChecker) or not c.persistent or not c.remind: + continue + alerts.extend(c.current_alerts) + return alerts + + # alert_num_by_state_with_max_type: dict[AlertType, list[Severity | int]] = {} + # for checker_set in checker_sets.values(): + # for c in checker_set: + # if not isinstance(c, BaseChecker) or not c.persistent: + # continue + # for a in c.current_alerts: + # if a.alert_type not in alert_num_by_state_with_max_type: + # alert_num_by_state_with_max_type[a.alert_type] = [a.severity, 1] + # else: + # existing_list = alert_num_by_state_with_max_type[a.alert_type] + # if a.severity > existing_list[0]: + # existing_list[0] = a.severity + # existing_list[1] += 1 + + # if len(alert_num_by_state_with_max_type) == 0: + # return [] + + # message = "There are ongoing events:" + # for at, sev_count in alert_num_by_state_with_max_type.items(): + # message += f"\n* {SEVERITY_TO_EMOJI[sev_count[0]]} {str(at)} - {sev_count[1]} alerts" + # message += ( + # "\n\nUse /ongoing to see them or /status to see this short reminder again (NOT IMPLEMENTED YET)." + # + "\nYou will also be reminded daily until the situation is resolved." + # ) + + # alert = Alert(alert_type=AlertType.REMIND, message=message, severity=max(alert_num_by_state_with_max_type.keys())) + # return [alert] diff --git a/src/lego_monitoring/config/enums.py b/src/lego_monitoring/config/enums.py index 54954cf..fc3e38e 100644 --- a/src/lego_monitoring/config/enums.py +++ b/src/lego_monitoring/config/enums.py @@ -4,6 +4,7 @@ from enum import StrEnum class CheckSet(StrEnum): START = "start" STOP = "stop" + REMIND = "remind" CPU = "cpu" RAM = "ram" diff --git a/src/lego_monitoring/core/checkers.py b/src/lego_monitoring/core/checkers.py index 9f976a3..ea179a0 100644 --- a/src/lego_monitoring/core/checkers.py +++ b/src/lego_monitoring/core/checkers.py @@ -8,14 +8,36 @@ from ..alerting.alert import Alert from ..alerting.channel import send_alert from ..alerting.current import CurrentAlerts from ..alerting.enum import Severity -from . import cvars @dataclass class BaseChecker: check: Callable | Coroutine + persistent: bool + """ + Whether this checker remembers its last alerts. + Logically, persistent alerts show the system's ongoing state, rather that one-time events + """ + send_any_state: bool = False + """ + False: this persistent checker only emits messages when its max alert severity is changed + + True: this persistent checker emits messages every times it checks + + Has no effect if persistent == False + """ + + remind: bool = True + """ + False: this persistent checker's last alerts are reminded daily + + True: this persistent checker's last alerts are not reminded daily + + Has no effect if persistent == False + """ + check_args: list = field(default_factory=list) check_kwargs: dict[str, Any] = field(default_factory=dict) current_alerts: CurrentAlerts = field(default_factory=CurrentAlerts, init=False) @@ -41,7 +63,7 @@ class BaseChecker: new_types = self.current_alerts.get_types() if old_severity != new_severity or self.send_any_state: for alert in alerts: - await send_alert(alert, persistent=True) + await send_alert(alert, note="ongoing") for alert_type in old_types - new_types: alert = Alert(alert_type=alert_type, message="Situation resolved", severity=Severity.OK) await send_alert(alert) diff --git a/src/lego_monitoring/core/cvars.py b/src/lego_monitoring/core/cvars.py index a4781c5..185514f 100644 --- a/src/lego_monitoring/core/cvars.py +++ b/src/lego_monitoring/core/cvars.py @@ -2,7 +2,10 @@ from contextvars import ContextVar from telethon import TelegramClient +from lego_monitoring.alerting.current import CurrentAlerts + from ..config import Config config: ContextVar[Config] = ContextVar("config") tg_client: ContextVar[TelegramClient] = ContextVar("tg_client") +current_alerts: ContextVar[list[CurrentAlerts]] = ContextVar("current_alerts", default=[]) From 62a25410ccd641206a79616141eb07af154d1f58 Mon Sep 17 00:00:00 2001 From: Alex Tau Date: Fri, 6 Jun 2025 01:14:25 +0300 Subject: [PATCH 25/56] update for 25.05 --- flake.nix | 2 +- src/lego_monitoring/checks/vulnix/__init__.py | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/flake.nix b/flake.nix index 6bced14..7fb2bd2 100644 --- a/flake.nix +++ b/flake.nix @@ -1,6 +1,6 @@ { inputs = { - nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.11"; + nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.05"; pyproject-nix = { url = "github:pyproject-nix/pyproject.nix"; diff --git a/src/lego_monitoring/checks/vulnix/__init__.py b/src/lego_monitoring/checks/vulnix/__init__.py index 302bdc4..b494400 100644 --- a/src/lego_monitoring/checks/vulnix/__init__.py +++ b/src/lego_monitoring/checks/vulnix/__init__.py @@ -28,12 +28,17 @@ def vulnix_check() -> list[Alert]: if len(non_whitelisted_cves) == 0: continue message = f"New findings in derivation {finding.derivation}:" + short_message = f"New findings in {finding.derivation} (short ver):" for cve in non_whitelisted_cves: if cve in finding.cvssv3_basescore: score_str = f"(CVSSv3 = {finding.cvssv3_basescore[cve]})" else: score_str = "(not scored by CVSSv3)" message += f'\n* {cve} - {finding.description[cve]} {score_str}' + short_message += f'\n * {cve}' + + if len(message) > 3700: + message = short_message alert = Alert( alert_type=AlertType.VULN, From 6cc3966221c3aec72fcde2c005197d91fdd15624 Mon Sep 17 00:00:00 2001 From: Alex Tau Date: Fri, 6 Jun 2025 15:38:48 +0300 Subject: [PATCH 26/56] /status and /ongoing --- src/lego_monitoring/__init__.py | 21 +++-- src/lego_monitoring/alerting/commands.py | 88 +++++++++++++++++++ .../alerting/{channel.py => sender.py} | 0 src/lego_monitoring/checks/remind.py | 42 ++------- src/lego_monitoring/checks/vulnix/__init__.py | 2 +- src/lego_monitoring/core/checkers.py | 15 +++- 6 files changed, 121 insertions(+), 47 deletions(-) create mode 100644 src/lego_monitoring/alerting/commands.py rename src/lego_monitoring/alerting/{channel.py => sender.py} (100%) diff --git a/src/lego_monitoring/__init__.py b/src/lego_monitoring/__init__.py index 389e785..b070580 100644 --- a/src/lego_monitoring/__init__.py +++ b/src/lego_monitoring/__init__.py @@ -6,7 +6,8 @@ import signal from typing import Coroutine from . import checks -from .alerting import channel +from .alerting import sender +from .alerting.commands import CommandHandlerManager from .checks.temp.sensors import print_readings from .config import enums as config_enums from .config import load_config @@ -48,13 +49,15 @@ async def async_main(): logging.basicConfig(level=config.log_level) - tg_client = await channel.get_client() - cvars.tg_client.set(tg_client) + tg_client = await sender.get_client() + cvars.tg_client.set(tg_client) + my_username = (await tg_client.get_me()).username + logging.info(f"Logged in as @{my_username}") check_sets = config_enums.CheckSet checker_sets: dict[config_enums.CheckSet, list[Coroutine | BaseChecker]] = { - check_sets.START: [channel.send_start_alert()], + check_sets.START: [sender.send_start_alert()], check_sets.STOP: [], # this is checked later check_sets.CPU: [IntervalChecker(checks.cpu_check, interval=datetime.timedelta(minutes=3), persistent=True)], check_sets.RAM: [IntervalChecker(checks.ram_check, interval=datetime.timedelta(minutes=1), persistent=True)], @@ -76,17 +79,21 @@ async def async_main(): period=datetime.timedelta(days=1), when=datetime.time(hour=0, minute=0), persistent=False, + is_reminder=True, ) ], } - checker_sets[check_sets.REMIND][0].check_args = [checker_sets] - checkers = [] for enabled_set in config.enabled_check_sets: for checker in checker_sets[enabled_set]: checkers.append(checker) + checker_sets[check_sets.REMIND][0].check_args = checkers + + command_manager = CommandHandlerManager(checkers) + await command_manager.attach_handlers(tg_client) + signal.signal(signal.SIGTERM, stop_gracefully) async with asyncio.TaskGroup() as tg: @@ -99,7 +106,7 @@ async def async_main(): while True: if stopping: if "stop" in config.enabled_check_sets: - await channel.send_stop_alert() + await sender.send_stop_alert() await tg_client.disconnect() raise SystemExit else: diff --git a/src/lego_monitoring/alerting/commands.py b/src/lego_monitoring/alerting/commands.py new file mode 100644 index 0000000..a07bb34 --- /dev/null +++ b/src/lego_monitoring/alerting/commands.py @@ -0,0 +1,88 @@ +from dataclasses import dataclass +from typing import Awaitable, Callable + +from telethon import TelegramClient, events, functions, types + +from lego_monitoring.core import cvars +from lego_monitoring.core.checkers import BaseChecker + +from .enum import SEVERITY_TO_EMOJI, AlertType, Severity +from .sender import format_message + + +def admin_chat_only( + handler: Callable[[events.NewMessage.Event], Awaitable[None]], +) -> Callable[[events.NewMessage.Event], Awaitable[None]]: + admin_room_id = cvars.config.get().telegram.room_id + + async def safe_handler(event: events.NewMessage.Event) -> None: + if event.chat_id == admin_room_id: + return await handler(event) + + return safe_handler + + +@dataclass +class CommandHandlerManager: + checkers: list[BaseChecker] + + async def attach_handlers(self, tg_client: TelegramClient): + my_username = (await tg_client.get_me()).username + + @tg_client.on(events.NewMessage(pattern=f"/status(?:@{my_username})?")) + @admin_chat_only + async def status(event: events.NewMessage.Event): + return await self.status_handler(event) + + @tg_client.on(events.NewMessage(pattern=f"/ongoing(?:@{my_username})?")) + @admin_chat_only + async def status(event: events.NewMessage.Event): + return await self.ongoing_handler(event) + + await tg_client( + functions.bots.SetBotCommandsRequest( + scope=types.BotCommandScopeDefault(), + lang_code="en", + commands=[ + types.BotCommand(command="status", description="Get current system status"), + types.BotCommand(command="ongoing", description="Show ongoing alerts"), + ], + ) + ) + + async def status_handler(self, event: events.NewMessage.Event): + alert_num_by_state_with_max_type: dict[AlertType, list[Severity | int]] = {} + for c in self.checkers: + if not isinstance(c, BaseChecker) or not c.persistent: + continue + for a in c.current_alerts: + if a.alert_type not in alert_num_by_state_with_max_type: + alert_num_by_state_with_max_type[a.alert_type] = [a.severity, 1] + else: + existing_list = alert_num_by_state_with_max_type[a.alert_type] + if a.severity > existing_list[0]: + existing_list[0] = a.severity + existing_list[1] += 1 + + if len(alert_num_by_state_with_max_type) == 0: + message = "🟢 There are no ongoing events." + else: + message = "There are ongoing events:" + for at, sev_count in alert_num_by_state_with_max_type.items(): + message += f"\n* {SEVERITY_TO_EMOJI[sev_count[0]]} {str(at)} - {sev_count[1]} alerts" + message += "\n\nUse /ongoing to see them." + + await event.respond(message) + + async def ongoing_handler(self, event: events.NewMessage.Event): + messages = set() + for c in self.checkers: + if not isinstance(c, BaseChecker) or not c.persistent: + continue + for a in c.current_alerts: + message = format_message(a, note="ongoing") + messages.add(message) + if len(messages) == 0: + await event.respond("🟢 There are no ongoing events.") + for message in messages: + await event.respond(message) diff --git a/src/lego_monitoring/alerting/channel.py b/src/lego_monitoring/alerting/sender.py similarity index 100% rename from src/lego_monitoring/alerting/channel.py rename to src/lego_monitoring/alerting/sender.py diff --git a/src/lego_monitoring/checks/remind.py b/src/lego_monitoring/checks/remind.py index 7676eea..ca3eb61 100644 --- a/src/lego_monitoring/checks/remind.py +++ b/src/lego_monitoring/checks/remind.py @@ -1,43 +1,13 @@ -from typing import Any, Coroutine +from typing import Coroutine from lego_monitoring.alerting.alert import Alert -from lego_monitoring.config.enums import CheckSet from lego_monitoring.core.checkers import BaseChecker -def remind_check(checker_sets: dict[CheckSet, list[Coroutine | BaseChecker]]) -> list[Alert]: +def remind_check(checkers: list[Coroutine | BaseChecker]) -> list[Alert]: alerts = [] - for checker_set in checker_sets.values(): - for c in checker_set: - if not isinstance(c, BaseChecker) or not c.persistent or not c.remind: - continue - alerts.extend(c.current_alerts) + for c in checkers: + if not isinstance(c, BaseChecker) or not c.persistent or not c.remind: + continue + alerts.extend(c.current_alerts) return alerts - - # alert_num_by_state_with_max_type: dict[AlertType, list[Severity | int]] = {} - # for checker_set in checker_sets.values(): - # for c in checker_set: - # if not isinstance(c, BaseChecker) or not c.persistent: - # continue - # for a in c.current_alerts: - # if a.alert_type not in alert_num_by_state_with_max_type: - # alert_num_by_state_with_max_type[a.alert_type] = [a.severity, 1] - # else: - # existing_list = alert_num_by_state_with_max_type[a.alert_type] - # if a.severity > existing_list[0]: - # existing_list[0] = a.severity - # existing_list[1] += 1 - - # if len(alert_num_by_state_with_max_type) == 0: - # return [] - - # message = "There are ongoing events:" - # for at, sev_count in alert_num_by_state_with_max_type.items(): - # message += f"\n* {SEVERITY_TO_EMOJI[sev_count[0]]} {str(at)} - {sev_count[1]} alerts" - # message += ( - # "\n\nUse /ongoing to see them or /status to see this short reminder again (NOT IMPLEMENTED YET)." - # + "\nYou will also be reminded daily until the situation is resolved." - # ) - - # alert = Alert(alert_type=AlertType.REMIND, message=message, severity=max(alert_num_by_state_with_max_type.keys())) - # return [alert] diff --git a/src/lego_monitoring/checks/vulnix/__init__.py b/src/lego_monitoring/checks/vulnix/__init__.py index b494400..81f4820 100644 --- a/src/lego_monitoring/checks/vulnix/__init__.py +++ b/src/lego_monitoring/checks/vulnix/__init__.py @@ -1,6 +1,6 @@ from lego_monitoring.alerting.alert import Alert -from lego_monitoring.alerting.channel import send_alert from lego_monitoring.alerting.enum import AlertType, Severity +from lego_monitoring.alerting.sender import send_alert from .vulnix import get_vulnix_output diff --git a/src/lego_monitoring/core/checkers.py b/src/lego_monitoring/core/checkers.py index ea179a0..5000ee7 100644 --- a/src/lego_monitoring/core/checkers.py +++ b/src/lego_monitoring/core/checkers.py @@ -5,9 +5,9 @@ from dataclasses import KW_ONLY, dataclass, field from typing import Any, Callable, Coroutine from ..alerting.alert import Alert -from ..alerting.channel import send_alert from ..alerting.current import CurrentAlerts from ..alerting.enum import Severity +from ..alerting.sender import send_alert @dataclass @@ -34,10 +34,19 @@ class BaseChecker: False: this persistent checker's last alerts are reminded daily True: this persistent checker's last alerts are not reminded daily - + Has no effect if persistent == False """ + is_reminder: bool = False + """ + False: this non-persistent checker's alerts are tagged as normal + + True: this non-persistent checker's alerts are tagged as ongoing + + Has no effect if persistent == True + """ + check_args: list = field(default_factory=list) check_kwargs: dict[str, Any] = field(default_factory=dict) current_alerts: CurrentAlerts = field(default_factory=CurrentAlerts, init=False) @@ -56,7 +65,7 @@ class BaseChecker: async def _handle_alerts(self, alerts: list[Alert]) -> None: if not self.persistent: for alert in alerts: - await send_alert(alert) + await send_alert(alert, "ongoing" if self.is_reminder else "") return old_types = self.current_alerts.get_types() old_severity, new_severity = self.current_alerts.update(alerts) From 8af7b683b6c9ad39cd3f2a65c11e888e51685c24 Mon Sep 17 00:00:00 2001 From: Alex Tau Date: Sat, 7 Jun 2025 00:38:31 +0300 Subject: [PATCH 27/56] fix crash on remind run --- flake.lock | 8 ++++---- src/lego_monitoring/__init__.py | 2 +- src/lego_monitoring/checks/vulnix/__init__.py | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/flake.lock b/flake.lock index 2d367f7..3738870 100644 --- a/flake.lock +++ b/flake.lock @@ -2,16 +2,16 @@ "nodes": { "nixpkgs": { "locked": { - "lastModified": 1745487689, - "narHash": "sha256-FQoi3R0NjQeBAsEOo49b5tbDPcJSMWc3QhhaIi9eddw=", + "lastModified": 1749086602, + "narHash": "sha256-DJcgJMekoxVesl9kKjfLPix2Nbr42i7cpEHJiTnBUwU=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "5630cf13cceac06cefe9fc607e8dfa8fb342dde3", + "rev": "4792576cb003c994bd7cc1edada3129def20b27d", "type": "github" }, "original": { "owner": "NixOS", - "ref": "nixos-24.11", + "ref": "nixos-25.05", "repo": "nixpkgs", "type": "github" } diff --git a/src/lego_monitoring/__init__.py b/src/lego_monitoring/__init__.py index b070580..aafc1ca 100644 --- a/src/lego_monitoring/__init__.py +++ b/src/lego_monitoring/__init__.py @@ -89,7 +89,7 @@ async def async_main(): for checker in checker_sets[enabled_set]: checkers.append(checker) - checker_sets[check_sets.REMIND][0].check_args = checkers + checker_sets[check_sets.REMIND][0].check_args = [checkers] command_manager = CommandHandlerManager(checkers) await command_manager.attach_handlers(tg_client) diff --git a/src/lego_monitoring/checks/vulnix/__init__.py b/src/lego_monitoring/checks/vulnix/__init__.py index 81f4820..9fefcff 100644 --- a/src/lego_monitoring/checks/vulnix/__init__.py +++ b/src/lego_monitoring/checks/vulnix/__init__.py @@ -7,12 +7,12 @@ from .vulnix import get_vulnix_output IS_TESTING = False -def vulnix_check() -> list[Alert]: +async def vulnix_check() -> list[Alert]: alert_list = [] try: vulnix_output = get_vulnix_output(IS_TESTING) except Exception as e: - send_alert( + await send_alert( Alert( alert_type=AlertType.ERROR, message=f"Exception {type(e).__name__} while calling vulnix: {e}", From 8b18d407d7f662c18aa660fae7afd9ae0f529155 Mon Sep 17 00:00:00 2001 From: Alex Tau Date: Sat, 7 Jun 2025 15:59:05 +0300 Subject: [PATCH 28/56] network monitoring --- docs/nixos-options.md | 174 +++++++++++++++++++- flake.lock | 23 ++- flake.nix | 8 +- modules/default.nix | 9 + modules/options.nix | 25 ++- modules/suboptions/netInterfaceOptions.nix | 39 +++++ modules/suboptions/tempSensorOptions.nix | 4 +- pyproject.toml | 1 + src/lego_monitoring/__init__.py | 3 + src/lego_monitoring/alerting/alert.py | 4 +- src/lego_monitoring/alerting/enum.py | 9 +- src/lego_monitoring/alerting/sender.py | 8 +- src/lego_monitoring/checks/__init__.py | 1 + src/lego_monitoring/checks/cpu.py | 8 +- src/lego_monitoring/checks/net.py | 87 ++++++++++ src/lego_monitoring/checks/ram.py | 8 +- src/lego_monitoring/checks/temp/__init__.py | 8 +- src/lego_monitoring/config/__init__.py | 2 + src/lego_monitoring/config/checks/net.py | 19 +++ src/lego_monitoring/config/enums.py | 1 + uv.lock | 46 ++++-- 21 files changed, 434 insertions(+), 53 deletions(-) create mode 100644 modules/suboptions/netInterfaceOptions.nix create mode 100644 src/lego_monitoring/checks/net.py create mode 100644 src/lego_monitoring/config/checks/net.py diff --git a/docs/nixos-options.md b/docs/nixos-options.md index 596cc5e..296d9c6 100644 --- a/docs/nixos-options.md +++ b/docs/nixos-options.md @@ -33,7 +33,7 @@ List of enabled check sets\. Each check set is a module which checks something a *Type:* -list of (one of “start”, “stop”, “cpu”, “ram”, “temp”, “vulnix”) +list of (one of “start”, “stop”, “remind”, “cpu”, “ram”, “temp”, “net”, “vulnix”) @@ -52,7 +52,7 @@ CPU load percentage for a critical alert to be sent\. Null means never generate *Type:* -null or floating point number +null or (positive integer or floating point number, meaning >0) @@ -73,7 +73,7 @@ CPU load percentage for a warning alert to be sent\. Null means never generate a *Type:* -null or floating point number +null or (positive integer or floating point number, meaning >0) @@ -85,6 +85,166 @@ null or floating point number +## services\.lego-monitoring\.checks\.net\.interfaces + + + +Per-interface configuration of IO byte thresholds\. + + + +*Type:* +attribute set of (submodule) + + + +*Default:* +` { } ` + + + +*Example:* + +``` +{ + br0 = { + warningThresholdCombBytes = 700 * 1024 * 128; # 700 Megabits + criticalThresholdCombBytes = 1 * 1024 * 1024 * 128; # 1 Gigabit + }; +} +``` + +*Declared by:* + - [modules/options\.nix](../modules/options.nix) + + + +## services\.lego-monitoring\.checks\.net\.interfaces\.\\.criticalThresholdCombBytes + + + +Combined (sent + received) bytes per second threshold for a critical alert to be sent\. If null, this threshold is disabled and not checked\. + + + +*Type:* +null or (positive integer, meaning >0) + + + +*Default:* +` null ` + +*Declared by:* + - [modules/options\.nix](../modules/options.nix) + + + +## services\.lego-monitoring\.checks\.net\.interfaces\.\\.criticalThresholdRecvBytes + + + +Received bytes per second threshold for a critical alert to be sent\. If null, this threshold is disabled and not checked\. + + + +*Type:* +null or (positive integer, meaning >0) + + + +*Default:* +` null ` + +*Declared by:* + - [modules/options\.nix](../modules/options.nix) + + + +## services\.lego-monitoring\.checks\.net\.interfaces\.\\.criticalThresholdSentBytes + + + +Sent bytes per second threshold for a critical alert to be sent\. If null, this threshold is disabled and not checked\. + + + +*Type:* +null or (positive integer, meaning >0) + + + +*Default:* +` null ` + +*Declared by:* + - [modules/options\.nix](../modules/options.nix) + + + +## services\.lego-monitoring\.checks\.net\.interfaces\.\\.warningThresholdCombBytes + + + +Combined (sent + received) bytes per second threshold for a warning alert to be sent\. If null, this threshold is disabled and not checked\. + + + +*Type:* +null or (positive integer, meaning >0) + + + +*Default:* +` null ` + +*Declared by:* + - [modules/options\.nix](../modules/options.nix) + + + +## services\.lego-monitoring\.checks\.net\.interfaces\.\\.warningThresholdRecvBytes + + + +Received bytes per second threshold for a warning alert to be sent\. If null, this threshold is disabled and not checked\. + + + +*Type:* +null or (positive integer, meaning >0) + + + +*Default:* +` null ` + +*Declared by:* + - [modules/options\.nix](../modules/options.nix) + + + +## services\.lego-monitoring\.checks\.net\.interfaces\.\\.warningThresholdSentBytes + + + +Sent bytes per second threshold for a warning alert to be sent\. If null, this threshold is disabled and not checked\. + + + +*Type:* +null or (positive integer, meaning >0) + + + +*Default:* +` null ` + +*Declared by:* + - [modules/options\.nix](../modules/options.nix) + + + ## services\.lego-monitoring\.checks\.ram\.criticalPercentage @@ -94,7 +254,7 @@ RAM usage percentage for a critical alert to be sent\. Null means never generate *Type:* -null or floating point number +null or (positive integer or floating point number, meaning >0) @@ -115,7 +275,7 @@ RAM usage percentage for a warning alert to be sent\. Null means never generate *Type:* -null or floating point number +null or (positive integer or floating point number, meaning >0) @@ -265,7 +425,7 @@ Critical temperature threshold\. *Type:* -null or floating point number +null or (positive integer or floating point number, meaning >0) @@ -307,7 +467,7 @@ Warning temperature threshold\. *Type:* -null or floating point number +null or (positive integer or floating point number, meaning >0) diff --git a/flake.lock b/flake.lock index 3738870..1cc4fe8 100644 --- a/flake.lock +++ b/flake.lock @@ -16,6 +16,22 @@ "type": "github" } }, + "nixpkgs-29335f": { + "locked": { + "lastModified": 1745804731, + "narHash": "sha256-v/sK3AS0QKu/Tu5sHIfddiEHCvrbNYPv8X10Fpux68g=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "29335f23bea5e34228349ea739f31ee79e267b88", + "type": "github" + }, + "original": { + "owner": "nixos", + "repo": "nixpkgs", + "rev": "29335f23bea5e34228349ea739f31ee79e267b88", + "type": "github" + } + }, "pyproject-build-systems": { "inputs": { "nixpkgs": [ @@ -65,6 +81,7 @@ "root": { "inputs": { "nixpkgs": "nixpkgs", + "nixpkgs-29335f": "nixpkgs-29335f", "pyproject-build-systems": "pyproject-build-systems", "pyproject-nix": "pyproject-nix", "uv2nix": "uv2nix" @@ -80,11 +97,11 @@ ] }, "locked": { - "lastModified": 1745697651, - "narHash": "sha256-r4A/fkiCenEapHkjJWPiNUZEfviuXMCr6mRozJ5dC4o=", + "lastModified": 1749170547, + "narHash": "sha256-zOptuFhTr9P0A+unFaOBFx5E5T6yx0qE8VrUGVrM96U=", "owner": "pyproject-nix", "repo": "uv2nix", - "rev": "cb6508484d534dafd097713b575f2aebc3417de0", + "rev": "7ae60727d4fc2e41aefd30da665e4f92ba8298f1", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index 7fb2bd2..28f27fa 100644 --- a/flake.nix +++ b/flake.nix @@ -1,6 +1,8 @@ { inputs = { nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.05"; + # this is for uv 0.6.17, 0.7.0 has a change uv2nix doesn't yet support: https://github.com/astral-sh/uv/pull/13176 + nixpkgs-29335f.url = "github:nixos/nixpkgs/29335f23bea5e34228349ea739f31ee79e267b88"; pyproject-nix = { url = "github:pyproject-nix/pyproject.nix"; @@ -25,6 +27,7 @@ { self, nixpkgs, + nixpkgs-29335f, uv2nix, pyproject-nix, pyproject-build-systems, @@ -78,6 +81,7 @@ # This example is only using x86_64-linux pkgs = nixpkgs.legacyPackages.x86_64-linux; + pkgs-29335f = nixpkgs-29335f.legacyPackages.x86_64-linux; # Use Python 3.12 from nixpkgs python = pkgs.python312; @@ -125,7 +129,7 @@ impure = pkgs.mkShell { packages = [ python - pkgs.uv + pkgs-29335f.uv ]; env = { @@ -205,7 +209,7 @@ pkgs.mkShell { packages = [ virtualenv - pkgs.uv + pkgs-29335f.uv ]; env = { diff --git a/modules/default.nix b/modules/default.nix index 1cff5ad..4dd838d 100644 --- a/modules/default.nix +++ b/modules/default.nix @@ -60,6 +60,15 @@ package: warning_percentage = warningPercentage; critical_percentage = criticalPercentage; }; + + net.interfaces = lib.mapAttrs (_: interfaceCfg: { + warning_threshold_sent_bytes = interfaceCfg.warningThresholdSentBytes; + critical_threshold_sent_bytes = interfaceCfg.criticalThresholdSentBytes; + warning_threshold_recv_bytes = interfaceCfg.warningThresholdRecvBytes; + critical_threshold_recv_bytes = interfaceCfg.warningThresholdRecvBytes; + warning_threshold_comb_bytes = interfaceCfg.warningThresholdCombBytes; + critical_threshold_comb_bytes = interfaceCfg.criticalThresholdCombBytes; + }) cfg.checks.net.interfaces; }; }; in lib.mkIf cfg.enable { diff --git a/modules/options.nix b/modules/options.nix index b4984d6..6cf9a24 100644 --- a/modules/options.nix +++ b/modules/options.nix @@ -6,6 +6,7 @@ let tempSensorOptions = (import ./suboptions/tempSensorOptions.nix) { inherit lib; }; vulnixWhitelistRule = (import ./suboptions/vulnixWhitelistRule.nix) { inherit lib; }; + netInterfaceOptions = (import ./suboptions/netInterfaceOptions.nix) { inherit lib; }; in { options.services.lego-monitoring = { @@ -32,6 +33,7 @@ in "cpu" "ram" "temp" + "net" "vulnix" ]); @@ -96,12 +98,12 @@ in cpu = { warningPercentage = lib.mkOption { - type = lib.types.nullOr lib.types.float; + type = lib.types.nullOr lib.types.numbers.positive; default = 80.0; description = "CPU load percentage for a warning alert to be sent. Null means never generate a CPU warning alert."; }; criticalPercentage = lib.mkOption { - type = lib.types.nullOr lib.types.float; + type = lib.types.nullOr lib.types.numbers.positive; default = 90.0; description = "CPU load percentage for a critical alert to be sent. Null means never generate a CPU critical alert."; }; @@ -109,16 +111,31 @@ in ram = { warningPercentage = lib.mkOption { - type = lib.types.nullOr lib.types.float; + type = lib.types.nullOr lib.types.numbers.positive; default = 80.0; description = "RAM usage percentage for a warning alert to be sent. Null means never generate a RAM warning alert."; }; criticalPercentage = lib.mkOption { - type = lib.types.nullOr lib.types.float; + type = lib.types.nullOr lib.types.numbers.positive; default = 90.0; description = "RAM usage percentage for a critical alert to be sent. Null means never generate a RAM critical alert."; }; }; + + net = { + interfaces = lib.mkOption { + type = lib.types.attrsOf (lib.types.submodule netInterfaceOptions); + default = { }; + description = "Per-interface configuration of IO byte thresholds."; + example = lib.literalExpression '' + { + br0 = { + warningThresholdCombBytes = 700 * 1024 * 128; # 700 Megabits + criticalThresholdCombBytes = 1 * 1024 * 1024 * 128; # 1 Gigabit + }; + }''; + }; + }; }; }; } diff --git a/modules/suboptions/netInterfaceOptions.nix b/modules/suboptions/netInterfaceOptions.nix new file mode 100644 index 0000000..7abd4cf --- /dev/null +++ b/modules/suboptions/netInterfaceOptions.nix @@ -0,0 +1,39 @@ +{ + lib, + ... +}: + +{ + options = { + warningThresholdSentBytes = lib.mkOption { + type = lib.types.nullOr lib.types.ints.positive; + default = null; + description = "Sent bytes per second threshold for a warning alert to be sent. If null, this threshold is disabled and not checked."; + }; + criticalThresholdSentBytes = lib.mkOption { + type = lib.types.nullOr lib.types.ints.positive; + default = null; + description = "Sent bytes per second threshold for a critical alert to be sent. If null, this threshold is disabled and not checked."; + }; + warningThresholdRecvBytes = lib.mkOption { + type = lib.types.nullOr lib.types.ints.positive; + default = null; + description = "Received bytes per second threshold for a warning alert to be sent. If null, this threshold is disabled and not checked."; + }; + criticalThresholdRecvBytes = lib.mkOption { + type = lib.types.nullOr lib.types.ints.positive; + default = null; + description = "Received bytes per second threshold for a critical alert to be sent. If null, this threshold is disabled and not checked."; + }; + warningThresholdCombBytes = lib.mkOption { + type = lib.types.nullOr lib.types.ints.positive; + default = null; + description = "Combined (sent + received) bytes per second threshold for a warning alert to be sent. If null, this threshold is disabled and not checked."; + }; + criticalThresholdCombBytes = lib.mkOption { + type = lib.types.nullOr lib.types.ints.positive; + default = null; + description = "Combined (sent + received) bytes per second threshold for a critical alert to be sent. If null, this threshold is disabled and not checked."; + }; + }; +} diff --git a/modules/suboptions/tempSensorOptions.nix b/modules/suboptions/tempSensorOptions.nix index 31d72fc..c986fe7 100644 --- a/modules/suboptions/tempSensorOptions.nix +++ b/modules/suboptions/tempSensorOptions.nix @@ -16,12 +16,12 @@ let description = "Whether this reading is enabled."; }; warningTemp = lib.mkOption { - type = lib.types.nullOr lib.types.float; + type = lib.types.nullOr lib.types.numbers.positive; default = null; description = "Warning temperature threshold."; }; criticalTemp = lib.mkOption { - type = lib.types.nullOr lib.types.float; + type = lib.types.nullOr lib.types.numbers.positive; default = null; description = "Critical temperature threshold."; }; diff --git a/pyproject.toml b/pyproject.toml index dcb602a..b28402e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,6 +6,7 @@ readme = "README.md" requires-python = ">=3.12" dependencies = [ "alt-utils>=0.0.8", + "humanize>=4.12.3", "psutil>=7.0.0", "telethon>=1.40.0", ] diff --git a/src/lego_monitoring/__init__.py b/src/lego_monitoring/__init__.py index aafc1ca..655e102 100644 --- a/src/lego_monitoring/__init__.py +++ b/src/lego_monitoring/__init__.py @@ -82,6 +82,9 @@ async def async_main(): is_reminder=True, ) ], + check_sets.NET: [ + IntervalChecker(checks.NetIOTracker().net_check, interval=datetime.timedelta(minutes=5), persistent=True) + ], } checkers = [] diff --git a/src/lego_monitoring/alerting/alert.py b/src/lego_monitoring/alerting/alert.py index a593d60..88d758b 100644 --- a/src/lego_monitoring/alerting/alert.py +++ b/src/lego_monitoring/alerting/alert.py @@ -1,4 +1,5 @@ -from dataclasses import dataclass +from dataclasses import dataclass, field +from datetime import datetime from .enum import AlertType, Severity @@ -8,3 +9,4 @@ class Alert: alert_type: AlertType message: str severity: Severity + created: datetime = field(default_factory=datetime.now) diff --git a/src/lego_monitoring/alerting/enum.py b/src/lego_monitoring/alerting/enum.py index 003398b..d014145 100644 --- a/src/lego_monitoring/alerting/enum.py +++ b/src/lego_monitoring/alerting/enum.py @@ -3,13 +3,16 @@ from enum import IntEnum, StrEnum class AlertType(StrEnum): BOOT = "BOOT" - CPU = "CPU" ERROR = "ERROR" + TEST = "TEST" + REMIND = "REMIND" + + CPU = "CPU" + NET = "NET" RAM = "RAM" TEMP = "TEMP" - TEST = "TEST" + VULN = "VULN" - REMIND = "REMIND" # LOGIN = "LOGIN" # SMART = "SMART" # TODO # RAID = "RAID" diff --git a/src/lego_monitoring/alerting/sender.py b/src/lego_monitoring/alerting/sender.py index b85dbff..53f09ed 100644 --- a/src/lego_monitoring/alerting/sender.py +++ b/src/lego_monitoring/alerting/sender.py @@ -18,8 +18,12 @@ async def get_client() -> TelegramClient: def format_message(alert: Alert, note: str) -> str: severity_emoji = SEVERITY_TO_EMOJI[alert.severity] - note_formatted = f" - {note}" if note else "" - message = f"{severity_emoji} {alert.alert_type} Alert{note_formatted}\n{alert.message}" + note_formatted = f"{note}, " if note else "" + if "ongoing" in note_formatted: + note_formatted += f"since {alert.created.isoformat()}" + else: + note_formatted += f"at {alert.created.isoformat()}" + message = f"{severity_emoji} {alert.alert_type} Alert - {note_formatted}\n{alert.message}" return message diff --git a/src/lego_monitoring/checks/__init__.py b/src/lego_monitoring/checks/__init__.py index f076917..62a11da 100644 --- a/src/lego_monitoring/checks/__init__.py +++ b/src/lego_monitoring/checks/__init__.py @@ -1,4 +1,5 @@ from .cpu import cpu_check +from .net import NetIOTracker from .ram import ram_check from .remind import remind_check from .temp import temp_check diff --git a/src/lego_monitoring/checks/cpu.py b/src/lego_monitoring/checks/cpu.py index ae335e9..dc19ecf 100644 --- a/src/lego_monitoring/checks/cpu.py +++ b/src/lego_monitoring/checks/cpu.py @@ -10,19 +10,19 @@ IS_TESTING = False def cpu_check() -> list[Alert]: percentage = cpu_percent() config = cvars.config.get().checks.cpu - if config.critical_percentage and (IS_TESTING or percentage > config.critical_percentage): + if config.critical_percentage and (IS_TESTING or percentage >= config.critical_percentage): return [ Alert( alert_type=AlertType.CPU, - message=f"CPU load: {percentage:.2f}% > {config.critical_percentage:.2f}%", + message=f"CPU load: {percentage:.2f}% >= {config.critical_percentage:.2f}%", severity=Severity.CRITICAL, ) ] - elif config.warning_percentage and (IS_TESTING or percentage > config.warning_percentage): + elif config.warning_percentage and (IS_TESTING or percentage >= config.warning_percentage): return [ Alert( alert_type=AlertType.CPU, - message=f"CPU load: {percentage:.2f}% > {config.warning_percentage:.2f}%", + message=f"CPU load: {percentage:.2f}% >= {config.warning_percentage:.2f}%", severity=Severity.WARNING, ) ] diff --git a/src/lego_monitoring/checks/net.py b/src/lego_monitoring/checks/net.py new file mode 100644 index 0000000..62c59c8 --- /dev/null +++ b/src/lego_monitoring/checks/net.py @@ -0,0 +1,87 @@ +from dataclasses import dataclass, field +from typing import Optional + +from humanize import naturalsize +from psutil import net_io_counters + +from lego_monitoring.alerting.alert import Alert +from lego_monitoring.alerting.enum import AlertType, Severity +from lego_monitoring.core import cvars + +IS_TESTING = False +SECONDS_BETWEEN_CHECKS = 5 * 60 + + +@dataclass +class NetIOTracker: + sent_per_interface: dict[str, int] = field(default_factory=dict, init=False) + recv_per_interface: dict[str, int] = field(default_factory=dict, init=False) + + @staticmethod + def check_threshold( + current_stat_bytes_per_sec: float, + critical_threshold: Optional[int], + warning_threshold: Optional[int], + stat_name: str, + interface: str, + ) -> Optional[Alert]: + if critical_threshold and (IS_TESTING or current_stat_bytes_per_sec >= critical_threshold): + current_stat_natural = naturalsize(current_stat_bytes_per_sec, binary=True) + critical_threshold_natural = naturalsize(critical_threshold, binary=True) + return Alert( + alert_type=AlertType.NET, + message=f"Interface {interface} {stat_name} {current_stat_natural}/s >= {critical_threshold_natural}/s", + severity=Severity.CRITICAL, + ) + elif warning_threshold and (IS_TESTING or current_stat_bytes_per_sec >= warning_threshold): + current_stat_natural = naturalsize(current_stat_bytes_per_sec, binary=True) + warning_threshold_natural = naturalsize(warning_threshold, binary=True) + return Alert( + alert_type=AlertType.NET, + message=f"Interface {interface} {stat_name} {current_stat_natural}/s >= {warning_threshold_natural}/s", + severity=Severity.WARNING, + ) + + def net_check(self) -> list[Alert]: + alerts = [] + current_stats = net_io_counters(pernic=True) + config = cvars.config.get().checks.net + for interface, thresholds in config.interfaces.items(): + if interface in self.sent_per_interface and interface in self.recv_per_interface: + sent_since_last_check_per_sec = ( + current_stats[interface].bytes_sent - self.sent_per_interface[interface] + ) / SECONDS_BETWEEN_CHECKS + recv_since_last_check_per_sec = ( + current_stats[interface].bytes_recv - self.recv_per_interface[interface] + ) / SECONDS_BETWEEN_CHECKS + comb_since_last_check_per_sec = sent_since_last_check_per_sec + recv_since_last_check_per_sec + + if alert := self.check_threshold( + sent_since_last_check_per_sec, + thresholds.critical_threshold_sent_bytes, + thresholds.warning_threshold_sent_bytes, + "sent", + interface, + ): + alerts.append(alert) + if alert := self.check_threshold( + recv_since_last_check_per_sec, + thresholds.critical_threshold_recv_bytes, + thresholds.warning_threshold_recv_bytes, + "recv", + interface, + ): + alerts.append(alert) + if alert := self.check_threshold( + comb_since_last_check_per_sec, + thresholds.critical_threshold_comb_bytes, + thresholds.warning_threshold_comb_bytes, + "comb", + interface, + ): + alerts.append(alert) + + self.sent_per_interface[interface] = current_stats[interface].bytes_sent + self.recv_per_interface[interface] = current_stats[interface].bytes_recv + + return alerts diff --git a/src/lego_monitoring/checks/ram.py b/src/lego_monitoring/checks/ram.py index eff87f7..f1eb40d 100644 --- a/src/lego_monitoring/checks/ram.py +++ b/src/lego_monitoring/checks/ram.py @@ -10,19 +10,19 @@ IS_TESTING = False def ram_check() -> list[Alert]: percentage = virtual_memory().percent config = cvars.config.get().checks.ram - if config.critical_percentage and (IS_TESTING or percentage > config.critical_percentage): + if config.critical_percentage and (IS_TESTING or percentage >= config.critical_percentage): return [ Alert( alert_type=AlertType.RAM, - message=f"RAM usage: {percentage:.2f}% > {config.critical_percentage:.2f}%", + message=f"RAM usage: {percentage:.2f}% >= {config.critical_percentage:.2f}%", severity=Severity.CRITICAL, ) ] - elif config.warning_percentage and (IS_TESTING or percentage > config.warning_percentage): + elif config.warning_percentage and (IS_TESTING or percentage >= config.warning_percentage): return [ Alert( alert_type=AlertType.RAM, - message=f"RAM usage: {percentage:.2f}% > {config.warning_percentage:.2f}%", + message=f"RAM usage: {percentage:.2f}% >= {config.warning_percentage:.2f}%", severity=Severity.WARNING, ) ] diff --git a/src/lego_monitoring/checks/temp/__init__.py b/src/lego_monitoring/checks/temp/__init__.py index 9c68a3b..6322a72 100644 --- a/src/lego_monitoring/checks/temp/__init__.py +++ b/src/lego_monitoring/checks/temp/__init__.py @@ -11,16 +11,16 @@ def temp_check() -> list[Alert]: temps = sensors.get_readings() for sensor, readings in temps.items(): for r in readings: - if r.critical_temp is not None and (IS_TESTING or r.current_temp > r.critical_temp): + if r.critical_temp is not None and (IS_TESTING or r.current_temp >= r.critical_temp): alert = Alert( alert_type=AlertType.TEMP, - message=f"{sensor} {r.label}: {r.current_temp}°C > {r.critical_temp}°C", + message=f"{sensor} {r.label}: {r.current_temp}°C >= {r.critical_temp}°C", severity=Severity.CRITICAL, ) - elif r.warning_temp is not None and (IS_TESTING or r.current_temp > r.warning_temp): + elif r.warning_temp is not None and (IS_TESTING or r.current_temp >= r.warning_temp): alert = Alert( alert_type=AlertType.TEMP, - message=f"{sensor} {r.label}: {r.current_temp}°C > {r.warning_temp}°C", + message=f"{sensor} {r.label}: {r.current_temp}°C >= {r.warning_temp}°C", severity=Severity.WARNING, ) else: diff --git a/src/lego_monitoring/config/__init__.py b/src/lego_monitoring/config/__init__.py index 3d8917e..4354df8 100644 --- a/src/lego_monitoring/config/__init__.py +++ b/src/lego_monitoring/config/__init__.py @@ -6,6 +6,7 @@ from alt_utils import NestedDeserializableDataclass from . import enums from .checks.cpu import CpuCheckConfig +from .checks.net import NetCheckConfig from .checks.ram import RamCheckConfig from .checks.temp import TempCheckConfig from .checks.vulnix import VulnixCheckConfig @@ -17,6 +18,7 @@ class ChecksConfig(NestedDeserializableDataclass): ram: RamCheckConfig = field(default_factory=RamCheckConfig) temp: TempCheckConfig = field(default_factory=TempCheckConfig) vulnix: Optional[VulnixCheckConfig] = None # vulnix check WILL raise if this config section is None + net: NetCheckConfig = field(default_factory=NetCheckConfig) @dataclass diff --git a/src/lego_monitoring/config/checks/net.py b/src/lego_monitoring/config/checks/net.py new file mode 100644 index 0000000..5da92c9 --- /dev/null +++ b/src/lego_monitoring/config/checks/net.py @@ -0,0 +1,19 @@ +from dataclasses import dataclass, field +from typing import Optional + +from alt_utils import NestedDeserializableDataclass + + +@dataclass +class NetInterfaceConfig: + warning_threshold_sent_bytes: Optional[int] = None + critical_threshold_sent_bytes: Optional[int] = None + warning_threshold_recv_bytes: Optional[int] = None + critical_threshold_recv_bytes: Optional[int] = None + warning_threshold_comb_bytes: Optional[int] = None + critical_threshold_comb_bytes: Optional[int] = None + + +@dataclass +class NetCheckConfig(NestedDeserializableDataclass): + interfaces: dict[str, NetInterfaceConfig] = field(default_factory=dict) diff --git a/src/lego_monitoring/config/enums.py b/src/lego_monitoring/config/enums.py index fc3e38e..a020a8b 100644 --- a/src/lego_monitoring/config/enums.py +++ b/src/lego_monitoring/config/enums.py @@ -9,6 +9,7 @@ class CheckSet(StrEnum): CPU = "cpu" RAM = "ram" TEMP = "temp" + NET = "net" VULNIX = "vulnix" diff --git a/uv.lock b/uv.lock index f5ec571..bc590c7 100644 --- a/uv.lock +++ b/uv.lock @@ -1,13 +1,23 @@ version = 1 +revision = 2 requires-python = ">=3.12" [[package]] name = "alt-utils" version = "0.0.8" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/31/15/67246107a8c808a9e99b34fd0024bebe954a67f3c315821eae985b87db7f/alt_utils-0.0.8.tar.gz", hash = "sha256:4b2901df0be4af736210277d58e231d4c4bce597a8fc665a8dd3e7b582705081", size = 6103 } +sdist = { url = "https://files.pythonhosted.org/packages/31/15/67246107a8c808a9e99b34fd0024bebe954a67f3c315821eae985b87db7f/alt_utils-0.0.8.tar.gz", hash = "sha256:4b2901df0be4af736210277d58e231d4c4bce597a8fc665a8dd3e7b582705081", size = 6103, upload_time = "2025-05-10T19:36:49.187Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9a/5a/7fe15b55fa0ff5528643750c409cd14da005406aef312b32512d8a8487ab/alt_utils-0.0.8-py3-none-any.whl", hash = "sha256:af5549c49543ff4a02b735308bc2a5bfb7f20755620652fd969a648bbaecbc47", size = 6378 }, + { url = "https://files.pythonhosted.org/packages/9a/5a/7fe15b55fa0ff5528643750c409cd14da005406aef312b32512d8a8487ab/alt_utils-0.0.8-py3-none-any.whl", hash = "sha256:af5549c49543ff4a02b735308bc2a5bfb7f20755620652fd969a648bbaecbc47", size = 6378, upload_time = "2025-05-10T19:36:47.954Z" }, +] + +[[package]] +name = "humanize" +version = "4.12.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/22/d1/bbc4d251187a43f69844f7fd8941426549bbe4723e8ff0a7441796b0789f/humanize-4.12.3.tar.gz", hash = "sha256:8430be3a615106fdfceb0b2c1b41c4c98c6b0fc5cc59663a5539b111dd325fb0", size = 80514, upload_time = "2025-04-30T11:51:07.98Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/1e/62a2ec3104394a2975a2629eec89276ede9dbe717092f6966fcf963e1bf0/humanize-4.12.3-py3-none-any.whl", hash = "sha256:2cbf6370af06568fa6d2da77c86edb7886f3160ecd19ee1ffef07979efc597f6", size = 128487, upload_time = "2025-04-30T11:51:06.468Z" }, ] [[package]] @@ -16,6 +26,7 @@ version = "0.1.0" source = { editable = "." } dependencies = [ { name = "alt-utils" }, + { name = "humanize" }, { name = "psutil" }, { name = "telethon" }, ] @@ -23,6 +34,7 @@ dependencies = [ [package.metadata] requires-dist = [ { name = "alt-utils", specifier = ">=0.0.8" }, + { name = "humanize", specifier = ">=4.12.3" }, { name = "psutil", specifier = ">=7.0.0" }, { name = "telethon", specifier = ">=1.40.0" }, ] @@ -31,30 +43,30 @@ requires-dist = [ name = "psutil" version = "7.0.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/2a/80/336820c1ad9286a4ded7e845b2eccfcb27851ab8ac6abece774a6ff4d3de/psutil-7.0.0.tar.gz", hash = "sha256:7be9c3eba38beccb6495ea33afd982a44074b78f28c434a1f51cc07fd315c456", size = 497003 } +sdist = { url = "https://files.pythonhosted.org/packages/2a/80/336820c1ad9286a4ded7e845b2eccfcb27851ab8ac6abece774a6ff4d3de/psutil-7.0.0.tar.gz", hash = "sha256:7be9c3eba38beccb6495ea33afd982a44074b78f28c434a1f51cc07fd315c456", size = 497003, upload_time = "2025-02-13T21:54:07.946Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ed/e6/2d26234410f8b8abdbf891c9da62bee396583f713fb9f3325a4760875d22/psutil-7.0.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:101d71dc322e3cffd7cea0650b09b3d08b8e7c4109dd6809fe452dfd00e58b25", size = 238051 }, - { url = "https://files.pythonhosted.org/packages/04/8b/30f930733afe425e3cbfc0e1468a30a18942350c1a8816acfade80c005c4/psutil-7.0.0-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:39db632f6bb862eeccf56660871433e111b6ea58f2caea825571951d4b6aa3da", size = 239535 }, - { url = "https://files.pythonhosted.org/packages/2a/ed/d362e84620dd22876b55389248e522338ed1bf134a5edd3b8231d7207f6d/psutil-7.0.0-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1fcee592b4c6f146991ca55919ea3d1f8926497a713ed7faaf8225e174581e91", size = 275004 }, - { url = "https://files.pythonhosted.org/packages/bf/b9/b0eb3f3cbcb734d930fdf839431606844a825b23eaf9a6ab371edac8162c/psutil-7.0.0-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b1388a4f6875d7e2aff5c4ca1cc16c545ed41dd8bb596cefea80111db353a34", size = 277986 }, - { url = "https://files.pythonhosted.org/packages/eb/a2/709e0fe2f093556c17fbafda93ac032257242cabcc7ff3369e2cb76a97aa/psutil-7.0.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5f098451abc2828f7dc6b58d44b532b22f2088f4999a937557b603ce72b1993", size = 279544 }, - { url = "https://files.pythonhosted.org/packages/50/e6/eecf58810b9d12e6427369784efe814a1eec0f492084ce8eb8f4d89d6d61/psutil-7.0.0-cp37-abi3-win32.whl", hash = "sha256:ba3fcef7523064a6c9da440fc4d6bd07da93ac726b5733c29027d7dc95b39d99", size = 241053 }, - { url = "https://files.pythonhosted.org/packages/50/1b/6921afe68c74868b4c9fa424dad3be35b095e16687989ebbb50ce4fceb7c/psutil-7.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:4cf3d4eb1aa9b348dec30105c55cd9b7d4629285735a102beb4441e38db90553", size = 244885 }, + { url = "https://files.pythonhosted.org/packages/ed/e6/2d26234410f8b8abdbf891c9da62bee396583f713fb9f3325a4760875d22/psutil-7.0.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:101d71dc322e3cffd7cea0650b09b3d08b8e7c4109dd6809fe452dfd00e58b25", size = 238051, upload_time = "2025-02-13T21:54:12.36Z" }, + { url = "https://files.pythonhosted.org/packages/04/8b/30f930733afe425e3cbfc0e1468a30a18942350c1a8816acfade80c005c4/psutil-7.0.0-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:39db632f6bb862eeccf56660871433e111b6ea58f2caea825571951d4b6aa3da", size = 239535, upload_time = "2025-02-13T21:54:16.07Z" }, + { url = "https://files.pythonhosted.org/packages/2a/ed/d362e84620dd22876b55389248e522338ed1bf134a5edd3b8231d7207f6d/psutil-7.0.0-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1fcee592b4c6f146991ca55919ea3d1f8926497a713ed7faaf8225e174581e91", size = 275004, upload_time = "2025-02-13T21:54:18.662Z" }, + { url = "https://files.pythonhosted.org/packages/bf/b9/b0eb3f3cbcb734d930fdf839431606844a825b23eaf9a6ab371edac8162c/psutil-7.0.0-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b1388a4f6875d7e2aff5c4ca1cc16c545ed41dd8bb596cefea80111db353a34", size = 277986, upload_time = "2025-02-13T21:54:21.811Z" }, + { url = "https://files.pythonhosted.org/packages/eb/a2/709e0fe2f093556c17fbafda93ac032257242cabcc7ff3369e2cb76a97aa/psutil-7.0.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5f098451abc2828f7dc6b58d44b532b22f2088f4999a937557b603ce72b1993", size = 279544, upload_time = "2025-02-13T21:54:24.68Z" }, + { url = "https://files.pythonhosted.org/packages/50/e6/eecf58810b9d12e6427369784efe814a1eec0f492084ce8eb8f4d89d6d61/psutil-7.0.0-cp37-abi3-win32.whl", hash = "sha256:ba3fcef7523064a6c9da440fc4d6bd07da93ac726b5733c29027d7dc95b39d99", size = 241053, upload_time = "2025-02-13T21:54:34.31Z" }, + { url = "https://files.pythonhosted.org/packages/50/1b/6921afe68c74868b4c9fa424dad3be35b095e16687989ebbb50ce4fceb7c/psutil-7.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:4cf3d4eb1aa9b348dec30105c55cd9b7d4629285735a102beb4441e38db90553", size = 244885, upload_time = "2025-02-13T21:54:37.486Z" }, ] [[package]] name = "pyaes" version = "1.6.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/44/66/2c17bae31c906613795711fc78045c285048168919ace2220daa372c7d72/pyaes-1.6.1.tar.gz", hash = "sha256:02c1b1405c38d3c370b085fb952dd8bea3fadcee6411ad99f312cc129c536d8f", size = 28536 } +sdist = { url = "https://files.pythonhosted.org/packages/44/66/2c17bae31c906613795711fc78045c285048168919ace2220daa372c7d72/pyaes-1.6.1.tar.gz", hash = "sha256:02c1b1405c38d3c370b085fb952dd8bea3fadcee6411ad99f312cc129c536d8f", size = 28536, upload_time = "2017-09-20T21:17:54.23Z" } [[package]] name = "pyasn1" version = "0.6.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ba/e9/01f1a64245b89f039897cb0130016d79f77d52669aae6ee7b159a6c4c018/pyasn1-0.6.1.tar.gz", hash = "sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034", size = 145322 } +sdist = { url = "https://files.pythonhosted.org/packages/ba/e9/01f1a64245b89f039897cb0130016d79f77d52669aae6ee7b159a6c4c018/pyasn1-0.6.1.tar.gz", hash = "sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034", size = 145322, upload_time = "2024-09-10T22:41:42.55Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c8/f1/d6a797abb14f6283c0ddff96bbdd46937f64122b8c925cab503dd37f8214/pyasn1-0.6.1-py3-none-any.whl", hash = "sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629", size = 83135 }, + { url = "https://files.pythonhosted.org/packages/c8/f1/d6a797abb14f6283c0ddff96bbdd46937f64122b8c925cab503dd37f8214/pyasn1-0.6.1-py3-none-any.whl", hash = "sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629", size = 83135, upload_time = "2024-09-11T16:00:36.122Z" }, ] [[package]] @@ -64,9 +76,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyasn1" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/da/8a/22b7beea3ee0d44b1916c0c1cb0ee3af23b700b6da9f04991899d0c555d4/rsa-4.9.1.tar.gz", hash = "sha256:e7bdbfdb5497da4c07dfd35530e1a902659db6ff241e39d9953cad06ebd0ae75", size = 29034 } +sdist = { url = "https://files.pythonhosted.org/packages/da/8a/22b7beea3ee0d44b1916c0c1cb0ee3af23b700b6da9f04991899d0c555d4/rsa-4.9.1.tar.gz", hash = "sha256:e7bdbfdb5497da4c07dfd35530e1a902659db6ff241e39d9953cad06ebd0ae75", size = 29034, upload_time = "2025-04-16T09:51:18.218Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/64/8d/0133e4eb4beed9e425d9a98ed6e081a55d195481b7632472be1af08d2f6b/rsa-4.9.1-py3-none-any.whl", hash = "sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762", size = 34696 }, + { url = "https://files.pythonhosted.org/packages/64/8d/0133e4eb4beed9e425d9a98ed6e081a55d195481b7632472be1af08d2f6b/rsa-4.9.1-py3-none-any.whl", hash = "sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762", size = 34696, upload_time = "2025-04-16T09:51:17.142Z" }, ] [[package]] @@ -77,7 +89,7 @@ dependencies = [ { name = "pyaes" }, { name = "rsa" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/58/af/9b7111e3f63fffe8e55b7ceb8bda023173e2052f420b6debcb25fd2fbc15/telethon-1.40.0.tar.gz", hash = "sha256:40e83326877a2e68b754d4b6d0d1ca5ac924110045b039e02660f2d67add97db", size = 646723 } +sdist = { url = "https://files.pythonhosted.org/packages/58/af/9b7111e3f63fffe8e55b7ceb8bda023173e2052f420b6debcb25fd2fbc15/telethon-1.40.0.tar.gz", hash = "sha256:40e83326877a2e68b754d4b6d0d1ca5ac924110045b039e02660f2d67add97db", size = 646723, upload_time = "2025-04-21T09:12:10.506Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2c/5a/c5370edb3215d19a6e858f4169b8eec725ba55f9d39df0f557508048c037/Telethon-1.40.0-py3-none-any.whl", hash = "sha256:146fd4cb2a7afa66bc67f9c2167756096a37b930f65711a3e7399ec9874dcfa7", size = 722013 }, + { url = "https://files.pythonhosted.org/packages/2c/5a/c5370edb3215d19a6e858f4169b8eec725ba55f9d39df0f557508048c037/Telethon-1.40.0-py3-none-any.whl", hash = "sha256:146fd4cb2a7afa66bc67f9c2167756096a37b930f65711a3e7399ec9874dcfa7", size = 722013, upload_time = "2025-04-21T09:12:08.399Z" }, ] From 4558cf9e6f5104884c1bff5f9ee4d2cfa175ca3d Mon Sep 17 00:00:00 2001 From: Alex Tau Date: Sat, 14 Jun 2025 15:18:58 +0300 Subject: [PATCH 29/56] write a readme --- README.md | 67 +++++++++++++++++++++++++++++++++++++++++ config.example.json | 15 +++++++++ docs/nixos-options.md | 11 ++++++- lego-monitoring.service | 12 ++++++++ modules/options.nix | 11 ++++++- pyproject.toml | 2 +- uv.lock | 2 +- 7 files changed, 116 insertions(+), 4 deletions(-) create mode 100644 config.example.json create mode 100644 lego-monitoring.service diff --git a/README.md b/README.md index e69de29..f929963 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,67 @@ +# lego-monitoring + +Simple system monitoring service. Sends alerts in Telegram. Currently supports monitoring: +* CPU/RAM/network usage +* temperature readings +* [vulnix](https://github.com/nix-community/vulnix) readings (NixOS only) + +## Setup + +### NixOS + +Only flake-based setups are supported. + +Include the module in your `flake.nix`: + +```nix +{ + inputs = { + # ... your other inputs ... + lego-monitoring = { + url = "git+https://gitlab.altau.su/lego/lego-monitoring.git"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + }; + + outputs = { + nixpkgs, + lego-monitoring, + ... + }: { + # change `yourhostname` to your actual hostname + nixosConfigurations.yourhostname = nixpkgs.lib.nixosSystem { + # change to your system: + system = "x86_64-linux"; + modules = [ + lego-monitoring.nixosModules.default + ./configuration.nix + # ... your other modules ... + ]; + }; + }; +} +``` + +See [docs/nixos-options.md](docs/nixos-options.md) for available configuration options. + +### Non-NixOS + +Requires [uv](https://github.com/astral-sh/uv), systemd. + +```bash +cd /opt +git clone https://gitlab.altau.su/lego/lego-monitoring.git +cd lego-monitoring +uv sync +cp config.example.json config.json +``` + +Edit `config.json` to suit your usage scenario. The default configuration only sends alerts on service's start and stop. +You may refer to the NixOS option documentation, as its options are the same, except JSON uses snake_case instead of lowerCamelCase. + +Then enable and start the service: + +```bash +ln -s /opt/lego-monitoring/lego-monitoring.service /etc/systemd/system/lego-monitoring.service +systemctl enable --now lego-monitoring +``` diff --git a/config.example.json b/config.example.json new file mode 100644 index 0000000..b875087 --- /dev/null +++ b/config.example.json @@ -0,0 +1,15 @@ +{ + "log_level": "INFO", + "enabled_check_sets": [ + "start", + "stop", + "remind" + ], + "telegram": { + "creds_secret_path": "/opt/lego-monitoring/tg-creds.txt", + "roomId": "0" + }, + "checks": { + + } +} diff --git a/docs/nixos-options.md b/docs/nixos-options.md index 296d9c6..63bb9f9 100644 --- a/docs/nixos-options.md +++ b/docs/nixos-options.md @@ -28,7 +28,16 @@ boolean -List of enabled check sets\. Each check set is a module which checks something and generates alerts based on check results\. +List of enabled check sets\. Each check set is a module which checks something and generates alerts based on check results\. Available check sets: + + - start – send an alert when lego-monitoring is started + - stop – send an alert when lego-monitoring is stopped + - remind – periodically (daily by default) remind about ongoing unresolved alerts + - cpu – alerts when CPU usage is above threshold + - ram – alerts when RAM usage is above threshold + - temp – alerts when temperature readings are above thresholds + - net – alerts when network usage is above threshold + - vulnix – periodically scans system for known CVEs, alerts if any are found (NixOS only) diff --git a/lego-monitoring.service b/lego-monitoring.service new file mode 100644 index 0000000..8d5db50 --- /dev/null +++ b/lego-monitoring.service @@ -0,0 +1,12 @@ +[Unit] +Description=Lego-monitoring service +StartLimitBurst=3 +StartLimitIntervalSec=20 + +[Service] +ExecStart=/opt/lego-monitoring/.venv/bin/lego-monitoring -c /opt/lego-monitoring/config.json +Restart=on-failure +RestartSec=5 + +[Install] +WantedBy=multi-user.target diff --git a/modules/options.nix b/modules/options.nix index 6cf9a24..ad92929 100644 --- a/modules/options.nix +++ b/modules/options.nix @@ -38,7 +38,16 @@ in "vulnix" ]); default = [ ]; - description = "List of enabled check sets. Each check set is a module which checks something and generates alerts based on check results."; + description = '' + List of enabled check sets. Each check set is a module which checks something and generates alerts based on check results. Available check sets: + * start -- send an alert when lego-monitoring is started + * stop -- send an alert when lego-monitoring is stopped + * remind -- periodically (daily by default) remind about ongoing unresolved alerts + * cpu -- alerts when CPU usage is above threshold + * ram -- alerts when RAM usage is above threshold + * temp -- alerts when temperature readings are above thresholds + * net -- alerts when network usage is above threshold + * vulnix -- periodically scans system for known CVEs, alerts if any are found (NixOS only)''; }; telegram = { diff --git a/pyproject.toml b/pyproject.toml index b28402e..f12c2d4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "lego-monitoring" -version = "0.1.0" +version = "1.0.0" description = "Monitoring software for the lego server" readme = "README.md" requires-python = ">=3.12" diff --git a/uv.lock b/uv.lock index bc590c7..16c8603 100644 --- a/uv.lock +++ b/uv.lock @@ -22,7 +22,7 @@ wheels = [ [[package]] name = "lego-monitoring" -version = "0.1.0" +version = "1.0.0" source = { editable = "." } dependencies = [ { name = "alt-utils" }, From c01ab8303ce8ce027d03764af48dee23b83fa8a9 Mon Sep 17 00:00:00 2001 From: Alex Tau Date: Wed, 13 Aug 2025 16:59:23 +0300 Subject: [PATCH 30/56] prepare config for healthchecks integration --- modules/default.nix | 12 ++++++-- modules/options.nix | 32 +++++++++++++++----- src/lego_monitoring/__init__.py | 19 +++++++----- src/lego_monitoring/alerting/commands.py | 2 +- src/lego_monitoring/alerting/sender.py | 18 ++++++----- src/lego_monitoring/config/__init__.py | 9 ++---- src/lego_monitoring/config/alert_channels.py | 28 +++++++++++++++++ src/lego_monitoring/core/cvars.py | 3 +- 8 files changed, 89 insertions(+), 34 deletions(-) create mode 100644 src/lego_monitoring/config/alert_channels.py diff --git a/modules/default.nix b/modules/default.nix index 4dd838d..28124c1 100644 --- a/modules/default.nix +++ b/modules/default.nix @@ -31,9 +31,15 @@ package: serviceConfigFile = json.generate "config.json" { enabled_check_sets = cfg.enabledCheckSets; log_level = cfg.logLevel; - telegram = with cfg.telegram; { - creds_secret_path = credsSecretPath; - room_id = roomId; + alert_channels = { + telegram = with cfg.alertChannels.telegram; if enable then + { + creds_secret_path = credsSecretPath; + room_id = roomId; + } else null; + healthchecks = with cfg.alertChannels.healthchecks; if enable then { + pinging_keys_secret_path = pingingKeysSecretPath; + } else null; }; checks = { temp.sensors = lib.mapAttrs (_: sensorCfg: { diff --git a/modules/options.nix b/modules/options.nix index ad92929..63aa610 100644 --- a/modules/options.nix +++ b/modules/options.nix @@ -50,14 +50,32 @@ in * vulnix -- periodically scans system for known CVEs, alerts if any are found (NixOS only)''; }; - telegram = { - credsSecretPath = lib.mkOption { - type = lib.types.str; - description = "Path to a file containing Telegram api_id, api_hash, and bot token, separated by the `,` character."; + alertChannels = { + telegram = { + enable = lib.mkEnableOption "Telegram notification channel"; + credsSecretPath = lib.mkOption { + type = lib.types.str; + default = ""; + description = "Path to a file containing Telegram api_id, api_hash, and bot token, separated by the `,` character."; + }; + roomId = lib.mkOption { + type = lib.types.int; + default = 0; + description = "ID of chat where to send alerts."; + }; }; - roomId = lib.mkOption { - type = lib.types.int; - description = "ID of chat where to send alerts."; + healthchecks = { + enable = lib.mkEnableOption "[Healthchecks](https://healthchecks.io) notification channel"; + pingingKeysSecretPath = lib.mkOption { + type = lib.types.str; + default = ""; + description = '' + Path to a file containing the pinging keys in a `slug:key` format, one on each line (ex: `lego-cpu:aaaaaaaaaaaaaaaaaaaaaa`). + Specify `default` as the slug to use this key for check types that don't have a key explicitly assigned to them. + + If you are unsure of the exact slug a check will generate, it is recommended to try it out with the default key first, before + assigning a specific one.''; + }; }; }; diff --git a/src/lego_monitoring/__init__.py b/src/lego_monitoring/__init__.py index 655e102..ba33d0b 100644 --- a/src/lego_monitoring/__init__.py +++ b/src/lego_monitoring/__init__.py @@ -49,11 +49,6 @@ async def async_main(): logging.basicConfig(level=config.log_level) - tg_client = await sender.get_client() - - cvars.tg_client.set(tg_client) - my_username = (await tg_client.get_me()).username - logging.info(f"Logged in as @{my_username}") check_sets = config_enums.CheckSet checker_sets: dict[config_enums.CheckSet, list[Coroutine | BaseChecker]] = { @@ -94,8 +89,18 @@ async def async_main(): checker_sets[check_sets.REMIND][0].check_args = [checkers] - command_manager = CommandHandlerManager(checkers) - await command_manager.attach_handlers(tg_client) + if config.alert_channels.telegram is not None: + tg_client = await sender.get_client() + my_username = (await tg_client.get_me()).username + logging.info(f"Logged in as @{my_username}") + + command_manager = CommandHandlerManager(checkers) + await command_manager.attach_handlers(tg_client) + else: + logging.info("Telegram integration is disabled") + tg_client = None + + cvars.tg_client.set(tg_client) signal.signal(signal.SIGTERM, stop_gracefully) diff --git a/src/lego_monitoring/alerting/commands.py b/src/lego_monitoring/alerting/commands.py index a07bb34..96491a0 100644 --- a/src/lego_monitoring/alerting/commands.py +++ b/src/lego_monitoring/alerting/commands.py @@ -13,7 +13,7 @@ from .sender import format_message def admin_chat_only( handler: Callable[[events.NewMessage.Event], Awaitable[None]], ) -> Callable[[events.NewMessage.Event], Awaitable[None]]: - admin_room_id = cvars.config.get().telegram.room_id + admin_room_id = cvars.config.get().alert_channels.telegram.room_id async def safe_handler(event: events.NewMessage.Event) -> None: if event.chat_id == admin_room_id: diff --git a/src/lego_monitoring/alerting/sender.py b/src/lego_monitoring/alerting/sender.py index 53f09ed..2ab4933 100644 --- a/src/lego_monitoring/alerting/sender.py +++ b/src/lego_monitoring/alerting/sender.py @@ -1,5 +1,3 @@ -from dataclasses import dataclass - from telethon import TelegramClient from telethon.sessions import MemorySession @@ -10,7 +8,7 @@ from .enum import SEVERITY_TO_EMOJI, AlertType, Severity async def get_client() -> TelegramClient: config = cvars.config.get() - api_id, api_hash, bot_token = config.telegram.creds.split(",") + api_id, api_hash, bot_token = config.alert_channels.telegram.creds.split(",") client = await TelegramClient(MemorySession(), api_id, api_hash, connection_retries=None).start(bot_token=bot_token) client.parse_mode = "html" return client @@ -38,13 +36,17 @@ async def send_alert(alert: Alert, note: str = "") -> None: raise NotImplementedError # TODO else: ... # temp_client = False - room_id = cvars.config.get().telegram.room_id - message = format_message(alert, note) - await client.send_message(entity=room_id, message=message) - # if temp_client: - # await client.close() + if client is not None: + room_id = cvars.config.get().alert_channels.telegram.room_id + message = format_message(alert, note) + await client.send_message(entity=room_id, message=message) + # if temp_client: + # await client.close() + # TODO ping healthchecks if enabled +# TODO service itself has to be monitored like everything else - with regular pinging - if we're +# using healthchecks async def send_start_alert() -> None: config = cvars.config.get() await send_alert( diff --git a/src/lego_monitoring/config/__init__.py b/src/lego_monitoring/config/__init__.py index 4354df8..11e4c70 100644 --- a/src/lego_monitoring/config/__init__.py +++ b/src/lego_monitoring/config/__init__.py @@ -5,6 +5,7 @@ from typing import Optional from alt_utils import NestedDeserializableDataclass from . import enums +from .alert_channels import AlertChannelsConfig from .checks.cpu import CpuCheckConfig from .checks.net import NetCheckConfig from .checks.ram import RamCheckConfig @@ -21,16 +22,10 @@ class ChecksConfig(NestedDeserializableDataclass): net: NetCheckConfig = field(default_factory=NetCheckConfig) -@dataclass -class TelegramConfig: - creds: str - room_id: int - - @dataclass class Config(NestedDeserializableDataclass): checks: ChecksConfig - telegram: TelegramConfig + alert_channels: AlertChannelsConfig enabled_check_sets: list[enums.CheckSet] = field(default_factory=list) log_level: enums.LogLevelName = enums.LogLevelName.INFO diff --git a/src/lego_monitoring/config/alert_channels.py b/src/lego_monitoring/config/alert_channels.py new file mode 100644 index 0000000..22e99de --- /dev/null +++ b/src/lego_monitoring/config/alert_channels.py @@ -0,0 +1,28 @@ +from dataclasses import dataclass, field +from typing import Optional + +from alt_utils import NestedDeserializableDataclass + + +@dataclass +class TelegramConfig: + creds: str + room_id: int + + +@dataclass +class HealthchecksConfig: + pinging_keys: str | dict[str, str] + + def __post_init__(self): + lines = self.pinging_keys.split() + self.pinging_keys = {} + for l in lines: + slug, key = l.split(":") + self.pinging_keys[slug] = key + + +@dataclass +class AlertChannelsConfig(NestedDeserializableDataclass): + telegram: Optional[TelegramConfig] = None + healthchecks: Optional[HealthchecksConfig] = None diff --git a/src/lego_monitoring/core/cvars.py b/src/lego_monitoring/core/cvars.py index 185514f..89c68f4 100644 --- a/src/lego_monitoring/core/cvars.py +++ b/src/lego_monitoring/core/cvars.py @@ -1,4 +1,5 @@ from contextvars import ContextVar +from typing import Optional from telethon import TelegramClient @@ -7,5 +8,5 @@ from lego_monitoring.alerting.current import CurrentAlerts from ..config import Config config: ContextVar[Config] = ContextVar("config") -tg_client: ContextVar[TelegramClient] = ContextVar("tg_client") +tg_client: ContextVar[Optional[TelegramClient]] = ContextVar("tg_client") current_alerts: ContextVar[list[CurrentAlerts]] = ContextVar("current_alerts", default=[]) From d59d5ac4e2343f52293cbeb474f85b2a23aaab75 Mon Sep 17 00:00:00 2001 From: Alex Tau Date: Fri, 15 Aug 2025 02:56:27 +0300 Subject: [PATCH 31/56] add healthchecks client --- modules/default.nix | 1 + modules/options.nix | 6 + pyproject.toml | 3 + src/lego_monitoring/__init__.py | 11 +- src/lego_monitoring/alerting/alert.py | 7 + .../alerting/clients/common.py | 10 + .../alerting/clients/healthchecks.py | 20 + src/lego_monitoring/alerting/sender.py | 18 +- src/lego_monitoring/config/alert_channels.py | 1 + src/lego_monitoring/core/cvars.py | 4 +- uv.lock | 554 ++++++++++++++++++ 11 files changed, 630 insertions(+), 5 deletions(-) create mode 100644 src/lego_monitoring/alerting/clients/common.py create mode 100644 src/lego_monitoring/alerting/clients/healthchecks.py diff --git a/modules/default.nix b/modules/default.nix index 28124c1..a269aba 100644 --- a/modules/default.nix +++ b/modules/default.nix @@ -39,6 +39,7 @@ package: } else null; healthchecks = with cfg.alertChannels.healthchecks; if enable then { pinging_keys_secret_path = pingingKeysSecretPath; + pinging_api_endpoint = pingingApiEndpoint; } else null; }; checks = { diff --git a/modules/options.nix b/modules/options.nix index 63aa610..71be81c 100644 --- a/modules/options.nix +++ b/modules/options.nix @@ -66,6 +66,12 @@ in }; healthchecks = { enable = lib.mkEnableOption "[Healthchecks](https://healthchecks.io) notification channel"; + pingingApiEndpoint = lib.mkOption { + type = lib.types.str; + default = "https://hc-ping.com/"; + description = "Endpoint URL for Healthchecks pinging API."; + example = "https://your-healthchecks-instance.com/ping/"; + }; pingingKeysSecretPath = lib.mkOption { type = lib.types.str; default = ""; diff --git a/pyproject.toml b/pyproject.toml index f12c2d4..5317964 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,10 +5,13 @@ description = "Monitoring software for the lego server" readme = "README.md" requires-python = ">=3.12" dependencies = [ + "aiodns>=3.5.0", + "aiohttp>=3.12.15", "alt-utils>=0.0.8", "humanize>=4.12.3", "psutil>=7.0.0", "telethon>=1.40.0", + "uplink[aiohttp]>=0.10.0", ] [project.scripts] diff --git a/src/lego_monitoring/__init__.py b/src/lego_monitoring/__init__.py index ba33d0b..8da27fe 100644 --- a/src/lego_monitoring/__init__.py +++ b/src/lego_monitoring/__init__.py @@ -90,17 +90,24 @@ async def async_main(): checker_sets[check_sets.REMIND][0].check_args = [checkers] if config.alert_channels.telegram is not None: - tg_client = await sender.get_client() + tg_client = await sender.get_tg_client() my_username = (await tg_client.get_me()).username logging.info(f"Logged in as @{my_username}") command_manager = CommandHandlerManager(checkers) await command_manager.attach_handlers(tg_client) + cvars.tg_client.set(tg_client) else: logging.info("Telegram integration is disabled") tg_client = None - cvars.tg_client.set(tg_client) + if config.alert_channels.healthchecks is not None: + healthchecks_client = sender.get_healthchecks_client() + logging.info("Ready to send pings to healthchecks") + cvars.healthchecks_client.set(healthchecks_client) + else: + healthchecks_client = None + logging.info("Healthchecks integration is disabled") signal.signal(signal.SIGTERM, stop_gracefully) diff --git a/src/lego_monitoring/alerting/alert.py b/src/lego_monitoring/alerting/alert.py index 88d758b..a54b2fd 100644 --- a/src/lego_monitoring/alerting/alert.py +++ b/src/lego_monitoring/alerting/alert.py @@ -1,5 +1,6 @@ from dataclasses import dataclass, field from datetime import datetime +from typing import Optional from .enum import AlertType, Severity @@ -10,3 +11,9 @@ class Alert: message: str severity: Severity created: datetime = field(default_factory=datetime.now) + healthchecks_slug: Optional[str] = None + plain_message: Optional[str] = None + + def __post_init__(self): + if self.plain_message is None: + self.plain_message = self.message diff --git a/src/lego_monitoring/alerting/clients/common.py b/src/lego_monitoring/alerting/clients/common.py new file mode 100644 index 0000000..0c37c44 --- /dev/null +++ b/src/lego_monitoring/alerting/clients/common.py @@ -0,0 +1,10 @@ +class UnsuccessfulRequest(Exception): ... + + +def raise_for_status(response): + """Checks whether or not the response was successful.""" + if 200 <= response.status_code < 300: + # Pass through the response. + return response + + raise UnsuccessfulRequest(response.url) diff --git a/src/lego_monitoring/alerting/clients/healthchecks.py b/src/lego_monitoring/alerting/clients/healthchecks.py new file mode 100644 index 0000000..67098b6 --- /dev/null +++ b/src/lego_monitoring/alerting/clients/healthchecks.py @@ -0,0 +1,20 @@ +from typing import Optional + +from uplink import Body, Consumer, Path, Query, post, response_handler + +from .common import raise_for_status + + +@response_handler(raise_for_status) +class HealthchecksClient(Consumer): + @post("{key}/{slug}") + def _success(self, key: Path, slug: Path, create: Query, log: Body): ... + + @post("{key}/{slug}/fail") + def _failure(self, key: Path, slug: Path, create: Query, log: Body): ... + + def success(self, key: Path, slug: str, create: bool = False, log: Optional[str] = None): + return self._success(key, slug, int(create), log) + + def failure(self, key: Path, slug: str, create: bool = False, log: Optional[str] = None): + return self._failure(key, slug, int(create), log) diff --git a/src/lego_monitoring/alerting/sender.py b/src/lego_monitoring/alerting/sender.py index 2ab4933..534fcf3 100644 --- a/src/lego_monitoring/alerting/sender.py +++ b/src/lego_monitoring/alerting/sender.py @@ -1,12 +1,14 @@ from telethon import TelegramClient from telethon.sessions import MemorySession +from uplink import AiohttpClient from ..core import cvars from .alert import Alert +from .clients.healthchecks import HealthchecksClient from .enum import SEVERITY_TO_EMOJI, AlertType, Severity -async def get_client() -> TelegramClient: +async def get_tg_client() -> TelegramClient: config = cvars.config.get() api_id, api_hash, bot_token = config.alert_channels.telegram.creds.split(",") client = await TelegramClient(MemorySession(), api_id, api_hash, connection_retries=None).start(bot_token=bot_token) @@ -14,6 +16,15 @@ async def get_client() -> TelegramClient: return client +def get_healthchecks_client() -> HealthchecksClient: + config = cvars.config.get() + base_url = config.alert_channels.healthchecks.pinging_api_endpoint + client = HealthchecksClient( + base_url=config.alert_channels.healthchecks.pinging_api_endpoint, client=AiohttpClient() + ) + return client + + def format_message(alert: Alert, note: str) -> str: severity_emoji = SEVERITY_TO_EMOJI[alert.severity] note_formatted = f"{note}, " if note else "" @@ -31,7 +42,7 @@ async def send_alert(alert: Alert, note: str = "") -> None: except LookupError: # being called standalone # cvars.config.set(get_config()) # temp_client = True - # client = await get_client() + # client = await get_tg_client() # cvars.matrix_client.set(client) raise NotImplementedError # TODO else: @@ -42,7 +53,10 @@ async def send_alert(alert: Alert, note: str = "") -> None: await client.send_message(entity=room_id, message=message) # if temp_client: # await client.close() + # TODO ping healthchecks if enabled + if alert.healthchecks_slug is not None: + raise NotImplementedError # TODO service itself has to be monitored like everything else - with regular pinging - if we're diff --git a/src/lego_monitoring/config/alert_channels.py b/src/lego_monitoring/config/alert_channels.py index 22e99de..b2c4c97 100644 --- a/src/lego_monitoring/config/alert_channels.py +++ b/src/lego_monitoring/config/alert_channels.py @@ -13,6 +13,7 @@ class TelegramConfig: @dataclass class HealthchecksConfig: pinging_keys: str | dict[str, str] + pinging_api_endpoint: str def __post_init__(self): lines = self.pinging_keys.split() diff --git a/src/lego_monitoring/core/cvars.py b/src/lego_monitoring/core/cvars.py index 89c68f4..65dc6c0 100644 --- a/src/lego_monitoring/core/cvars.py +++ b/src/lego_monitoring/core/cvars.py @@ -3,10 +3,12 @@ from typing import Optional from telethon import TelegramClient +from lego_monitoring.alerting.clients.healthchecks import HealthchecksClient from lego_monitoring.alerting.current import CurrentAlerts from ..config import Config config: ContextVar[Config] = ContextVar("config") -tg_client: ContextVar[Optional[TelegramClient]] = ContextVar("tg_client") +tg_client: ContextVar[Optional[TelegramClient]] = ContextVar("tg_client", default=None) +healthchecks_client: ContextVar[Optional[HealthchecksClient]] = ContextVar("healthchecks_client", default=None) current_alerts: ContextVar[list[CurrentAlerts]] = ContextVar("current_alerts", default=[]) diff --git a/uv.lock b/uv.lock index 16c8603..9d95560 100644 --- a/uv.lock +++ b/uv.lock @@ -2,6 +2,91 @@ version = 1 revision = 2 requires-python = ">=3.12" +[[package]] +name = "aiodns" +version = "3.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycares" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/17/0a/163e5260cecc12de6abc259d158d9da3b8ec062ab863107dcdb1166cdcef/aiodns-3.5.0.tar.gz", hash = "sha256:11264edbab51896ecf546c18eb0dd56dff0428c6aa6d2cd87e643e07300eb310", size = 14380, upload_time = "2025-06-13T16:21:53.595Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f6/2c/711076e5f5d0707b8ec55a233c8bfb193e0981a800cd1b3b123e8ff61ca1/aiodns-3.5.0-py3-none-any.whl", hash = "sha256:6d0404f7d5215849233f6ee44854f2bb2481adf71b336b2279016ea5990ca5c5", size = 8068, upload_time = "2025-06-13T16:21:52.45Z" }, +] + +[[package]] +name = "aiohappyeyeballs" +version = "2.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/26/30/f84a107a9c4331c14b2b586036f40965c128aa4fee4dda5d3d51cb14ad54/aiohappyeyeballs-2.6.1.tar.gz", hash = "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558", size = 22760, upload_time = "2025-03-12T01:42:48.764Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8", size = 15265, upload_time = "2025-03-12T01:42:47.083Z" }, +] + +[[package]] +name = "aiohttp" +version = "3.12.15" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohappyeyeballs" }, + { name = "aiosignal" }, + { name = "attrs" }, + { name = "frozenlist" }, + { name = "multidict" }, + { name = "propcache" }, + { name = "yarl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9b/e7/d92a237d8802ca88483906c388f7c201bbe96cd80a165ffd0ac2f6a8d59f/aiohttp-3.12.15.tar.gz", hash = "sha256:4fc61385e9c98d72fcdf47e6dd81833f47b2f77c114c29cd64a361be57a763a2", size = 7823716, upload_time = "2025-07-29T05:52:32.215Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/63/97/77cb2450d9b35f517d6cf506256bf4f5bda3f93a66b4ad64ba7fc917899c/aiohttp-3.12.15-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:802d3868f5776e28f7bf69d349c26fc0efadb81676d0afa88ed00d98a26340b7", size = 702333, upload_time = "2025-07-29T05:50:46.507Z" }, + { url = "https://files.pythonhosted.org/packages/83/6d/0544e6b08b748682c30b9f65640d006e51f90763b41d7c546693bc22900d/aiohttp-3.12.15-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f2800614cd560287be05e33a679638e586a2d7401f4ddf99e304d98878c29444", size = 476948, upload_time = "2025-07-29T05:50:48.067Z" }, + { url = "https://files.pythonhosted.org/packages/3a/1d/c8c40e611e5094330284b1aea8a4b02ca0858f8458614fa35754cab42b9c/aiohttp-3.12.15-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8466151554b593909d30a0a125d638b4e5f3836e5aecde85b66b80ded1cb5b0d", size = 469787, upload_time = "2025-07-29T05:50:49.669Z" }, + { url = "https://files.pythonhosted.org/packages/38/7d/b76438e70319796bfff717f325d97ce2e9310f752a267bfdf5192ac6082b/aiohttp-3.12.15-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e5a495cb1be69dae4b08f35a6c4579c539e9b5706f606632102c0f855bcba7c", size = 1716590, upload_time = "2025-07-29T05:50:51.368Z" }, + { url = "https://files.pythonhosted.org/packages/79/b1/60370d70cdf8b269ee1444b390cbd72ce514f0d1cd1a715821c784d272c9/aiohttp-3.12.15-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6404dfc8cdde35c69aaa489bb3542fb86ef215fc70277c892be8af540e5e21c0", size = 1699241, upload_time = "2025-07-29T05:50:53.628Z" }, + { url = "https://files.pythonhosted.org/packages/a3/2b/4968a7b8792437ebc12186db31523f541943e99bda8f30335c482bea6879/aiohttp-3.12.15-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3ead1c00f8521a5c9070fcb88f02967b1d8a0544e6d85c253f6968b785e1a2ab", size = 1754335, upload_time = "2025-07-29T05:50:55.394Z" }, + { url = "https://files.pythonhosted.org/packages/fb/c1/49524ed553f9a0bec1a11fac09e790f49ff669bcd14164f9fab608831c4d/aiohttp-3.12.15-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6990ef617f14450bc6b34941dba4f12d5613cbf4e33805932f853fbd1cf18bfb", size = 1800491, upload_time = "2025-07-29T05:50:57.202Z" }, + { url = "https://files.pythonhosted.org/packages/de/5e/3bf5acea47a96a28c121b167f5ef659cf71208b19e52a88cdfa5c37f1fcc/aiohttp-3.12.15-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd736ed420f4db2b8148b52b46b88ed038d0354255f9a73196b7bbce3ea97545", size = 1719929, upload_time = "2025-07-29T05:50:59.192Z" }, + { url = "https://files.pythonhosted.org/packages/39/94/8ae30b806835bcd1cba799ba35347dee6961a11bd507db634516210e91d8/aiohttp-3.12.15-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c5092ce14361a73086b90c6efb3948ffa5be2f5b6fbcf52e8d8c8b8848bb97c", size = 1635733, upload_time = "2025-07-29T05:51:01.394Z" }, + { url = "https://files.pythonhosted.org/packages/7a/46/06cdef71dd03acd9da7f51ab3a9107318aee12ad38d273f654e4f981583a/aiohttp-3.12.15-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:aaa2234bb60c4dbf82893e934d8ee8dea30446f0647e024074237a56a08c01bd", size = 1696790, upload_time = "2025-07-29T05:51:03.657Z" }, + { url = "https://files.pythonhosted.org/packages/02/90/6b4cfaaf92ed98d0ec4d173e78b99b4b1a7551250be8937d9d67ecb356b4/aiohttp-3.12.15-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:6d86a2fbdd14192e2f234a92d3b494dd4457e683ba07e5905a0b3ee25389ac9f", size = 1718245, upload_time = "2025-07-29T05:51:05.911Z" }, + { url = "https://files.pythonhosted.org/packages/2e/e6/2593751670fa06f080a846f37f112cbe6f873ba510d070136a6ed46117c6/aiohttp-3.12.15-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a041e7e2612041a6ddf1c6a33b883be6a421247c7afd47e885969ee4cc58bd8d", size = 1658899, upload_time = "2025-07-29T05:51:07.753Z" }, + { url = "https://files.pythonhosted.org/packages/8f/28/c15bacbdb8b8eb5bf39b10680d129ea7410b859e379b03190f02fa104ffd/aiohttp-3.12.15-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5015082477abeafad7203757ae44299a610e89ee82a1503e3d4184e6bafdd519", size = 1738459, upload_time = "2025-07-29T05:51:09.56Z" }, + { url = "https://files.pythonhosted.org/packages/00/de/c269cbc4faa01fb10f143b1670633a8ddd5b2e1ffd0548f7aa49cb5c70e2/aiohttp-3.12.15-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:56822ff5ddfd1b745534e658faba944012346184fbfe732e0d6134b744516eea", size = 1766434, upload_time = "2025-07-29T05:51:11.423Z" }, + { url = "https://files.pythonhosted.org/packages/52/b0/4ff3abd81aa7d929b27d2e1403722a65fc87b763e3a97b3a2a494bfc63bc/aiohttp-3.12.15-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b2acbbfff69019d9014508c4ba0401822e8bae5a5fdc3b6814285b71231b60f3", size = 1726045, upload_time = "2025-07-29T05:51:13.689Z" }, + { url = "https://files.pythonhosted.org/packages/71/16/949225a6a2dd6efcbd855fbd90cf476052e648fb011aa538e3b15b89a57a/aiohttp-3.12.15-cp312-cp312-win32.whl", hash = "sha256:d849b0901b50f2185874b9a232f38e26b9b3d4810095a7572eacea939132d4e1", size = 423591, upload_time = "2025-07-29T05:51:15.452Z" }, + { url = "https://files.pythonhosted.org/packages/2b/d8/fa65d2a349fe938b76d309db1a56a75c4fb8cc7b17a398b698488a939903/aiohttp-3.12.15-cp312-cp312-win_amd64.whl", hash = "sha256:b390ef5f62bb508a9d67cb3bba9b8356e23b3996da7062f1a57ce1a79d2b3d34", size = 450266, upload_time = "2025-07-29T05:51:17.239Z" }, + { url = "https://files.pythonhosted.org/packages/f2/33/918091abcf102e39d15aba2476ad9e7bd35ddb190dcdd43a854000d3da0d/aiohttp-3.12.15-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:9f922ffd05034d439dde1c77a20461cf4a1b0831e6caa26151fe7aa8aaebc315", size = 696741, upload_time = "2025-07-29T05:51:19.021Z" }, + { url = "https://files.pythonhosted.org/packages/b5/2a/7495a81e39a998e400f3ecdd44a62107254803d1681d9189be5c2e4530cd/aiohttp-3.12.15-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2ee8a8ac39ce45f3e55663891d4b1d15598c157b4d494a4613e704c8b43112cd", size = 474407, upload_time = "2025-07-29T05:51:21.165Z" }, + { url = "https://files.pythonhosted.org/packages/49/fc/a9576ab4be2dcbd0f73ee8675d16c707cfc12d5ee80ccf4015ba543480c9/aiohttp-3.12.15-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3eae49032c29d356b94eee45a3f39fdf4b0814b397638c2f718e96cfadf4c4e4", size = 466703, upload_time = "2025-07-29T05:51:22.948Z" }, + { url = "https://files.pythonhosted.org/packages/09/2f/d4bcc8448cf536b2b54eed48f19682031ad182faa3a3fee54ebe5b156387/aiohttp-3.12.15-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b97752ff12cc12f46a9b20327104448042fce5c33a624f88c18f66f9368091c7", size = 1705532, upload_time = "2025-07-29T05:51:25.211Z" }, + { url = "https://files.pythonhosted.org/packages/f1/f3/59406396083f8b489261e3c011aa8aee9df360a96ac8fa5c2e7e1b8f0466/aiohttp-3.12.15-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:894261472691d6fe76ebb7fcf2e5870a2ac284c7406ddc95823c8598a1390f0d", size = 1686794, upload_time = "2025-07-29T05:51:27.145Z" }, + { url = "https://files.pythonhosted.org/packages/dc/71/164d194993a8d114ee5656c3b7ae9c12ceee7040d076bf7b32fb98a8c5c6/aiohttp-3.12.15-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5fa5d9eb82ce98959fc1031c28198b431b4d9396894f385cb63f1e2f3f20ca6b", size = 1738865, upload_time = "2025-07-29T05:51:29.366Z" }, + { url = "https://files.pythonhosted.org/packages/1c/00/d198461b699188a93ead39cb458554d9f0f69879b95078dce416d3209b54/aiohttp-3.12.15-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f0fa751efb11a541f57db59c1dd821bec09031e01452b2b6217319b3a1f34f3d", size = 1788238, upload_time = "2025-07-29T05:51:31.285Z" }, + { url = "https://files.pythonhosted.org/packages/85/b8/9e7175e1fa0ac8e56baa83bf3c214823ce250d0028955dfb23f43d5e61fd/aiohttp-3.12.15-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5346b93e62ab51ee2a9d68e8f73c7cf96ffb73568a23e683f931e52450e4148d", size = 1710566, upload_time = "2025-07-29T05:51:33.219Z" }, + { url = "https://files.pythonhosted.org/packages/59/e4/16a8eac9df39b48ae102ec030fa9f726d3570732e46ba0c592aeeb507b93/aiohttp-3.12.15-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:049ec0360f939cd164ecbfd2873eaa432613d5e77d6b04535e3d1fbae5a9e645", size = 1624270, upload_time = "2025-07-29T05:51:35.195Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f8/cd84dee7b6ace0740908fd0af170f9fab50c2a41ccbc3806aabcb1050141/aiohttp-3.12.15-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b52dcf013b57464b6d1e51b627adfd69a8053e84b7103a7cd49c030f9ca44461", size = 1677294, upload_time = "2025-07-29T05:51:37.215Z" }, + { url = "https://files.pythonhosted.org/packages/ce/42/d0f1f85e50d401eccd12bf85c46ba84f947a84839c8a1c2c5f6e8ab1eb50/aiohttp-3.12.15-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:9b2af240143dd2765e0fb661fd0361a1b469cab235039ea57663cda087250ea9", size = 1708958, upload_time = "2025-07-29T05:51:39.328Z" }, + { url = "https://files.pythonhosted.org/packages/d5/6b/f6fa6c5790fb602538483aa5a1b86fcbad66244997e5230d88f9412ef24c/aiohttp-3.12.15-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ac77f709a2cde2cc71257ab2d8c74dd157c67a0558a0d2799d5d571b4c63d44d", size = 1651553, upload_time = "2025-07-29T05:51:41.356Z" }, + { url = "https://files.pythonhosted.org/packages/04/36/a6d36ad545fa12e61d11d1932eef273928b0495e6a576eb2af04297fdd3c/aiohttp-3.12.15-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:47f6b962246f0a774fbd3b6b7be25d59b06fdb2f164cf2513097998fc6a29693", size = 1727688, upload_time = "2025-07-29T05:51:43.452Z" }, + { url = "https://files.pythonhosted.org/packages/aa/c8/f195e5e06608a97a4e52c5d41c7927301bf757a8e8bb5bbf8cef6c314961/aiohttp-3.12.15-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:760fb7db442f284996e39cf9915a94492e1896baac44f06ae551974907922b64", size = 1761157, upload_time = "2025-07-29T05:51:45.643Z" }, + { url = "https://files.pythonhosted.org/packages/05/6a/ea199e61b67f25ba688d3ce93f63b49b0a4e3b3d380f03971b4646412fc6/aiohttp-3.12.15-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ad702e57dc385cae679c39d318def49aef754455f237499d5b99bea4ef582e51", size = 1710050, upload_time = "2025-07-29T05:51:48.203Z" }, + { url = "https://files.pythonhosted.org/packages/b4/2e/ffeb7f6256b33635c29dbed29a22a723ff2dd7401fff42ea60cf2060abfb/aiohttp-3.12.15-cp313-cp313-win32.whl", hash = "sha256:f813c3e9032331024de2eb2e32a88d86afb69291fbc37a3a3ae81cc9917fb3d0", size = 422647, upload_time = "2025-07-29T05:51:50.718Z" }, + { url = "https://files.pythonhosted.org/packages/1b/8e/78ee35774201f38d5e1ba079c9958f7629b1fd079459aea9467441dbfbf5/aiohttp-3.12.15-cp313-cp313-win_amd64.whl", hash = "sha256:1a649001580bdb37c6fdb1bebbd7e3bc688e8ec2b5c6f52edbb664662b17dc84", size = 449067, upload_time = "2025-07-29T05:51:52.549Z" }, +] + +[[package]] +name = "aiosignal" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "frozenlist" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/61/62/06741b579156360248d1ec624842ad0edf697050bbaf7c3e46394e106ad1/aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7", size = 25007, upload_time = "2025-07-03T22:54:43.528Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", size = 7490, upload_time = "2025-07-03T22:54:42.156Z" }, +] + [[package]] name = "alt-utils" version = "0.0.8" @@ -11,6 +96,159 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9a/5a/7fe15b55fa0ff5528643750c409cd14da005406aef312b32512d8a8487ab/alt_utils-0.0.8-py3-none-any.whl", hash = "sha256:af5549c49543ff4a02b735308bc2a5bfb7f20755620652fd969a648bbaecbc47", size = 6378, upload_time = "2025-05-10T19:36:47.954Z" }, ] +[[package]] +name = "attrs" +version = "25.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/b0/1367933a8532ee6ff8d63537de4f1177af4bff9f3e829baf7331f595bb24/attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b", size = 812032, upload_time = "2025-03-13T11:10:22.779Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/06/bb80f5f86020c4551da315d78b3ab75e8228f89f0162f2c3a819e407941a/attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3", size = 63815, upload_time = "2025-03-13T11:10:21.14Z" }, +] + +[[package]] +name = "certifi" +version = "2025.8.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/dc/67/960ebe6bf230a96cda2e0abcf73af550ec4f090005363542f0765df162e0/certifi-2025.8.3.tar.gz", hash = "sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407", size = 162386, upload_time = "2025-08-03T03:07:47.08Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/48/1549795ba7742c948d2ad169c1c8cdbae65bc450d6cd753d124b17c8cd32/certifi-2025.8.3-py3-none-any.whl", hash = "sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5", size = 161216, upload_time = "2025-08-03T03:07:45.777Z" }, +] + +[[package]] +name = "cffi" +version = "1.17.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621, upload_time = "2024-09-04T20:45:21.852Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/84/e94227139ee5fb4d600a7a4927f322e1d4aea6fdc50bd3fca8493caba23f/cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4", size = 183178, upload_time = "2024-09-04T20:44:12.232Z" }, + { url = "https://files.pythonhosted.org/packages/da/ee/fb72c2b48656111c4ef27f0f91da355e130a923473bf5ee75c5643d00cca/cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c", size = 178840, upload_time = "2024-09-04T20:44:13.739Z" }, + { url = "https://files.pythonhosted.org/packages/cc/b6/db007700f67d151abadf508cbfd6a1884f57eab90b1bb985c4c8c02b0f28/cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36", size = 454803, upload_time = "2024-09-04T20:44:15.231Z" }, + { url = "https://files.pythonhosted.org/packages/1a/df/f8d151540d8c200eb1c6fba8cd0dfd40904f1b0682ea705c36e6c2e97ab3/cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5", size = 478850, upload_time = "2024-09-04T20:44:17.188Z" }, + { url = "https://files.pythonhosted.org/packages/28/c0/b31116332a547fd2677ae5b78a2ef662dfc8023d67f41b2a83f7c2aa78b1/cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff", size = 485729, upload_time = "2024-09-04T20:44:18.688Z" }, + { url = "https://files.pythonhosted.org/packages/91/2b/9a1ddfa5c7f13cab007a2c9cc295b70fbbda7cb10a286aa6810338e60ea1/cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99", size = 471256, upload_time = "2024-09-04T20:44:20.248Z" }, + { url = "https://files.pythonhosted.org/packages/b2/d5/da47df7004cb17e4955df6a43d14b3b4ae77737dff8bf7f8f333196717bf/cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93", size = 479424, upload_time = "2024-09-04T20:44:21.673Z" }, + { url = "https://files.pythonhosted.org/packages/0b/ac/2a28bcf513e93a219c8a4e8e125534f4f6db03e3179ba1c45e949b76212c/cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3", size = 484568, upload_time = "2024-09-04T20:44:23.245Z" }, + { url = "https://files.pythonhosted.org/packages/d4/38/ca8a4f639065f14ae0f1d9751e70447a261f1a30fa7547a828ae08142465/cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8", size = 488736, upload_time = "2024-09-04T20:44:24.757Z" }, + { url = "https://files.pythonhosted.org/packages/86/c5/28b2d6f799ec0bdecf44dced2ec5ed43e0eb63097b0f58c293583b406582/cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65", size = 172448, upload_time = "2024-09-04T20:44:26.208Z" }, + { url = "https://files.pythonhosted.org/packages/50/b9/db34c4755a7bd1cb2d1603ac3863f22bcecbd1ba29e5ee841a4bc510b294/cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903", size = 181976, upload_time = "2024-09-04T20:44:27.578Z" }, + { url = "https://files.pythonhosted.org/packages/8d/f8/dd6c246b148639254dad4d6803eb6a54e8c85c6e11ec9df2cffa87571dbe/cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e", size = 182989, upload_time = "2024-09-04T20:44:28.956Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f1/672d303ddf17c24fc83afd712316fda78dc6fce1cd53011b839483e1ecc8/cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2", size = 178802, upload_time = "2024-09-04T20:44:30.289Z" }, + { url = "https://files.pythonhosted.org/packages/0e/2d/eab2e858a91fdff70533cab61dcff4a1f55ec60425832ddfdc9cd36bc8af/cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3", size = 454792, upload_time = "2024-09-04T20:44:32.01Z" }, + { url = "https://files.pythonhosted.org/packages/75/b2/fbaec7c4455c604e29388d55599b99ebcc250a60050610fadde58932b7ee/cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683", size = 478893, upload_time = "2024-09-04T20:44:33.606Z" }, + { url = "https://files.pythonhosted.org/packages/4f/b7/6e4a2162178bf1935c336d4da8a9352cccab4d3a5d7914065490f08c0690/cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5", size = 485810, upload_time = "2024-09-04T20:44:35.191Z" }, + { url = "https://files.pythonhosted.org/packages/c7/8a/1d0e4a9c26e54746dc08c2c6c037889124d4f59dffd853a659fa545f1b40/cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4", size = 471200, upload_time = "2024-09-04T20:44:36.743Z" }, + { url = "https://files.pythonhosted.org/packages/26/9f/1aab65a6c0db35f43c4d1b4f580e8df53914310afc10ae0397d29d697af4/cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd", size = 479447, upload_time = "2024-09-04T20:44:38.492Z" }, + { url = "https://files.pythonhosted.org/packages/5f/e4/fb8b3dd8dc0e98edf1135ff067ae070bb32ef9d509d6cb0f538cd6f7483f/cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed", size = 484358, upload_time = "2024-09-04T20:44:40.046Z" }, + { url = "https://files.pythonhosted.org/packages/f1/47/d7145bf2dc04684935d57d67dff9d6d795b2ba2796806bb109864be3a151/cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9", size = 488469, upload_time = "2024-09-04T20:44:41.616Z" }, + { url = "https://files.pythonhosted.org/packages/bf/ee/f94057fa6426481d663b88637a9a10e859e492c73d0384514a17d78ee205/cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d", size = 172475, upload_time = "2024-09-04T20:44:43.733Z" }, + { url = "https://files.pythonhosted.org/packages/7c/fc/6a8cb64e5f0324877d503c854da15d76c1e50eb722e320b15345c4d0c6de/cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", size = 182009, upload_time = "2024-09-04T20:44:45.309Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/83/2d/5fd176ceb9b2fc619e63405525573493ca23441330fcdaee6bef9460e924/charset_normalizer-3.4.3.tar.gz", hash = "sha256:6fce4b8500244f6fcb71465d4a4930d132ba9ab8e71a7859e6a5d59851068d14", size = 122371, upload_time = "2025-08-09T07:57:28.46Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/5e/14c94999e418d9b87682734589404a25854d5f5d0408df68bc15b6ff54bb/charset_normalizer-3.4.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e28e334d3ff134e88989d90ba04b47d84382a828c061d0d1027b1b12a62b39b1", size = 205655, upload_time = "2025-08-09T07:56:08.475Z" }, + { url = "https://files.pythonhosted.org/packages/7d/a8/c6ec5d389672521f644505a257f50544c074cf5fc292d5390331cd6fc9c3/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0cacf8f7297b0c4fcb74227692ca46b4a5852f8f4f24b3c766dd94a1075c4884", size = 146223, upload_time = "2025-08-09T07:56:09.708Z" }, + { url = "https://files.pythonhosted.org/packages/fc/eb/a2ffb08547f4e1e5415fb69eb7db25932c52a52bed371429648db4d84fb1/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c6fd51128a41297f5409deab284fecbe5305ebd7e5a1f959bee1c054622b7018", size = 159366, upload_time = "2025-08-09T07:56:11.326Z" }, + { url = "https://files.pythonhosted.org/packages/82/10/0fd19f20c624b278dddaf83b8464dcddc2456cb4b02bb902a6da126b87a1/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3cfb2aad70f2c6debfbcb717f23b7eb55febc0bb23dcffc0f076009da10c6392", size = 157104, upload_time = "2025-08-09T07:56:13.014Z" }, + { url = "https://files.pythonhosted.org/packages/16/ab/0233c3231af734f5dfcf0844aa9582d5a1466c985bbed6cedab85af9bfe3/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1606f4a55c0fd363d754049cdf400175ee96c992b1f8018b993941f221221c5f", size = 151830, upload_time = "2025-08-09T07:56:14.428Z" }, + { url = "https://files.pythonhosted.org/packages/ae/02/e29e22b4e02839a0e4a06557b1999d0a47db3567e82989b5bb21f3fbbd9f/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:027b776c26d38b7f15b26a5da1044f376455fb3766df8fc38563b4efbc515154", size = 148854, upload_time = "2025-08-09T07:56:16.051Z" }, + { url = "https://files.pythonhosted.org/packages/05/6b/e2539a0a4be302b481e8cafb5af8792da8093b486885a1ae4d15d452bcec/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:42e5088973e56e31e4fa58eb6bd709e42fc03799c11c42929592889a2e54c491", size = 160670, upload_time = "2025-08-09T07:56:17.314Z" }, + { url = "https://files.pythonhosted.org/packages/31/e7/883ee5676a2ef217a40ce0bffcc3d0dfbf9e64cbcfbdf822c52981c3304b/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:cc34f233c9e71701040d772aa7490318673aa7164a0efe3172b2981218c26d93", size = 158501, upload_time = "2025-08-09T07:56:18.641Z" }, + { url = "https://files.pythonhosted.org/packages/c1/35/6525b21aa0db614cf8b5792d232021dca3df7f90a1944db934efa5d20bb1/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:320e8e66157cc4e247d9ddca8e21f427efc7a04bbd0ac8a9faf56583fa543f9f", size = 153173, upload_time = "2025-08-09T07:56:20.289Z" }, + { url = "https://files.pythonhosted.org/packages/50/ee/f4704bad8201de513fdc8aac1cabc87e38c5818c93857140e06e772b5892/charset_normalizer-3.4.3-cp312-cp312-win32.whl", hash = "sha256:fb6fecfd65564f208cbf0fba07f107fb661bcd1a7c389edbced3f7a493f70e37", size = 99822, upload_time = "2025-08-09T07:56:21.551Z" }, + { url = "https://files.pythonhosted.org/packages/39/f5/3b3836ca6064d0992c58c7561c6b6eee1b3892e9665d650c803bd5614522/charset_normalizer-3.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:86df271bf921c2ee3818f0522e9a5b8092ca2ad8b065ece5d7d9d0e9f4849bcc", size = 107543, upload_time = "2025-08-09T07:56:23.115Z" }, + { url = "https://files.pythonhosted.org/packages/65/ca/2135ac97709b400c7654b4b764daf5c5567c2da45a30cdd20f9eefe2d658/charset_normalizer-3.4.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:14c2a87c65b351109f6abfc424cab3927b3bdece6f706e4d12faaf3d52ee5efe", size = 205326, upload_time = "2025-08-09T07:56:24.721Z" }, + { url = "https://files.pythonhosted.org/packages/71/11/98a04c3c97dd34e49c7d247083af03645ca3730809a5509443f3c37f7c99/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41d1fc408ff5fdfb910200ec0e74abc40387bccb3252f3f27c0676731df2b2c8", size = 146008, upload_time = "2025-08-09T07:56:26.004Z" }, + { url = "https://files.pythonhosted.org/packages/60/f5/4659a4cb3c4ec146bec80c32d8bb16033752574c20b1252ee842a95d1a1e/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1bb60174149316da1c35fa5233681f7c0f9f514509b8e399ab70fea5f17e45c9", size = 159196, upload_time = "2025-08-09T07:56:27.25Z" }, + { url = "https://files.pythonhosted.org/packages/86/9e/f552f7a00611f168b9a5865a1414179b2c6de8235a4fa40189f6f79a1753/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:30d006f98569de3459c2fc1f2acde170b7b2bd265dc1943e87e1a4efe1b67c31", size = 156819, upload_time = "2025-08-09T07:56:28.515Z" }, + { url = "https://files.pythonhosted.org/packages/7e/95/42aa2156235cbc8fa61208aded06ef46111c4d3f0de233107b3f38631803/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:416175faf02e4b0810f1f38bcb54682878a4af94059a1cd63b8747244420801f", size = 151350, upload_time = "2025-08-09T07:56:29.716Z" }, + { url = "https://files.pythonhosted.org/packages/c2/a9/3865b02c56f300a6f94fc631ef54f0a8a29da74fb45a773dfd3dcd380af7/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6aab0f181c486f973bc7262a97f5aca3ee7e1437011ef0c2ec04b5a11d16c927", size = 148644, upload_time = "2025-08-09T07:56:30.984Z" }, + { url = "https://files.pythonhosted.org/packages/77/d9/cbcf1a2a5c7d7856f11e7ac2d782aec12bdfea60d104e60e0aa1c97849dc/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:fdabf8315679312cfa71302f9bd509ded4f2f263fb5b765cf1433b39106c3cc9", size = 160468, upload_time = "2025-08-09T07:56:32.252Z" }, + { url = "https://files.pythonhosted.org/packages/f6/42/6f45efee8697b89fda4d50580f292b8f7f9306cb2971d4b53f8914e4d890/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:bd28b817ea8c70215401f657edef3a8aa83c29d447fb0b622c35403780ba11d5", size = 158187, upload_time = "2025-08-09T07:56:33.481Z" }, + { url = "https://files.pythonhosted.org/packages/70/99/f1c3bdcfaa9c45b3ce96f70b14f070411366fa19549c1d4832c935d8e2c3/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:18343b2d246dc6761a249ba1fb13f9ee9a2bcd95decc767319506056ea4ad4dc", size = 152699, upload_time = "2025-08-09T07:56:34.739Z" }, + { url = "https://files.pythonhosted.org/packages/a3/ad/b0081f2f99a4b194bcbb1934ef3b12aa4d9702ced80a37026b7607c72e58/charset_normalizer-3.4.3-cp313-cp313-win32.whl", hash = "sha256:6fb70de56f1859a3f71261cbe41005f56a7842cc348d3aeb26237560bfa5e0ce", size = 99580, upload_time = "2025-08-09T07:56:35.981Z" }, + { url = "https://files.pythonhosted.org/packages/9a/8f/ae790790c7b64f925e5c953b924aaa42a243fb778fed9e41f147b2a5715a/charset_normalizer-3.4.3-cp313-cp313-win_amd64.whl", hash = "sha256:cf1ebb7d78e1ad8ec2a8c4732c7be2e736f6e5123a4146c5b89c9d1f585f8cef", size = 107366, upload_time = "2025-08-09T07:56:37.339Z" }, + { url = "https://files.pythonhosted.org/packages/8e/91/b5a06ad970ddc7a0e513112d40113e834638f4ca1120eb727a249fb2715e/charset_normalizer-3.4.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3cd35b7e8aedeb9e34c41385fda4f73ba609e561faedfae0a9e75e44ac558a15", size = 204342, upload_time = "2025-08-09T07:56:38.687Z" }, + { url = "https://files.pythonhosted.org/packages/ce/ec/1edc30a377f0a02689342f214455c3f6c2fbedd896a1d2f856c002fc3062/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b89bc04de1d83006373429975f8ef9e7932534b8cc9ca582e4db7d20d91816db", size = 145995, upload_time = "2025-08-09T07:56:40.048Z" }, + { url = "https://files.pythonhosted.org/packages/17/e5/5e67ab85e6d22b04641acb5399c8684f4d37caf7558a53859f0283a650e9/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2001a39612b241dae17b4687898843f254f8748b796a2e16f1051a17078d991d", size = 158640, upload_time = "2025-08-09T07:56:41.311Z" }, + { url = "https://files.pythonhosted.org/packages/f1/e5/38421987f6c697ee3722981289d554957c4be652f963d71c5e46a262e135/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8dcfc373f888e4fb39a7bc57e93e3b845e7f462dacc008d9749568b1c4ece096", size = 156636, upload_time = "2025-08-09T07:56:43.195Z" }, + { url = "https://files.pythonhosted.org/packages/a0/e4/5a075de8daa3ec0745a9a3b54467e0c2967daaaf2cec04c845f73493e9a1/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18b97b8404387b96cdbd30ad660f6407799126d26a39ca65729162fd810a99aa", size = 150939, upload_time = "2025-08-09T07:56:44.819Z" }, + { url = "https://files.pythonhosted.org/packages/02/f7/3611b32318b30974131db62b4043f335861d4d9b49adc6d57c1149cc49d4/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ccf600859c183d70eb47e05a44cd80a4ce77394d1ac0f79dbd2dd90a69a3a049", size = 148580, upload_time = "2025-08-09T07:56:46.684Z" }, + { url = "https://files.pythonhosted.org/packages/7e/61/19b36f4bd67f2793ab6a99b979b4e4f3d8fc754cbdffb805335df4337126/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:53cd68b185d98dde4ad8990e56a58dea83a4162161b1ea9272e5c9182ce415e0", size = 159870, upload_time = "2025-08-09T07:56:47.941Z" }, + { url = "https://files.pythonhosted.org/packages/06/57/84722eefdd338c04cf3030ada66889298eaedf3e7a30a624201e0cbe424a/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:30a96e1e1f865f78b030d65241c1ee850cdf422d869e9028e2fc1d5e4db73b92", size = 157797, upload_time = "2025-08-09T07:56:49.756Z" }, + { url = "https://files.pythonhosted.org/packages/72/2a/aff5dd112b2f14bcc3462c312dce5445806bfc8ab3a7328555da95330e4b/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d716a916938e03231e86e43782ca7878fb602a125a91e7acb8b5112e2e96ac16", size = 152224, upload_time = "2025-08-09T07:56:51.369Z" }, + { url = "https://files.pythonhosted.org/packages/b7/8c/9839225320046ed279c6e839d51f028342eb77c91c89b8ef2549f951f3ec/charset_normalizer-3.4.3-cp314-cp314-win32.whl", hash = "sha256:c6dbd0ccdda3a2ba7c2ecd9d77b37f3b5831687d8dc1b6ca5f56a4880cc7b7ce", size = 100086, upload_time = "2025-08-09T07:56:52.722Z" }, + { url = "https://files.pythonhosted.org/packages/ee/7a/36fbcf646e41f710ce0a563c1c9a343c6edf9be80786edeb15b6f62e17db/charset_normalizer-3.4.3-cp314-cp314-win_amd64.whl", hash = "sha256:73dc19b562516fc9bcf6e5d6e596df0b4eb98d87e4f79f3ae71840e6ed21361c", size = 107400, upload_time = "2025-08-09T07:56:55.172Z" }, + { url = "https://files.pythonhosted.org/packages/8a/1f/f041989e93b001bc4e44bb1669ccdcf54d3f00e628229a85b08d330615c5/charset_normalizer-3.4.3-py3-none-any.whl", hash = "sha256:ce571ab16d890d23b5c278547ba694193a45011ff86a9162a71307ed9f86759a", size = 53175, upload_time = "2025-08-09T07:57:26.864Z" }, +] + +[[package]] +name = "frozenlist" +version = "1.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/79/b1/b64018016eeb087db503b038296fd782586432b9c077fc5c7839e9cb6ef6/frozenlist-1.7.0.tar.gz", hash = "sha256:2e310d81923c2437ea8670467121cc3e9b0f76d3043cc1d2331d56c7fb7a3a8f", size = 45078, upload_time = "2025-06-09T23:02:35.538Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/a2/c8131383f1e66adad5f6ecfcce383d584ca94055a34d683bbb24ac5f2f1c/frozenlist-1.7.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3dbf9952c4bb0e90e98aec1bd992b3318685005702656bc6f67c1a32b76787f2", size = 81424, upload_time = "2025-06-09T23:00:42.24Z" }, + { url = "https://files.pythonhosted.org/packages/4c/9d/02754159955088cb52567337d1113f945b9e444c4960771ea90eb73de8db/frozenlist-1.7.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:1f5906d3359300b8a9bb194239491122e6cf1444c2efb88865426f170c262cdb", size = 47952, upload_time = "2025-06-09T23:00:43.481Z" }, + { url = "https://files.pythonhosted.org/packages/01/7a/0046ef1bd6699b40acd2067ed6d6670b4db2f425c56980fa21c982c2a9db/frozenlist-1.7.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3dabd5a8f84573c8d10d8859a50ea2dec01eea372031929871368c09fa103478", size = 46688, upload_time = "2025-06-09T23:00:44.793Z" }, + { url = "https://files.pythonhosted.org/packages/d6/a2/a910bafe29c86997363fb4c02069df4ff0b5bc39d33c5198b4e9dd42d8f8/frozenlist-1.7.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa57daa5917f1738064f302bf2626281a1cb01920c32f711fbc7bc36111058a8", size = 243084, upload_time = "2025-06-09T23:00:46.125Z" }, + { url = "https://files.pythonhosted.org/packages/64/3e/5036af9d5031374c64c387469bfcc3af537fc0f5b1187d83a1cf6fab1639/frozenlist-1.7.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c193dda2b6d49f4c4398962810fa7d7c78f032bf45572b3e04dd5249dff27e08", size = 233524, upload_time = "2025-06-09T23:00:47.73Z" }, + { url = "https://files.pythonhosted.org/packages/06/39/6a17b7c107a2887e781a48ecf20ad20f1c39d94b2a548c83615b5b879f28/frozenlist-1.7.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bfe2b675cf0aaa6d61bf8fbffd3c274b3c9b7b1623beb3809df8a81399a4a9c4", size = 248493, upload_time = "2025-06-09T23:00:49.742Z" }, + { url = "https://files.pythonhosted.org/packages/be/00/711d1337c7327d88c44d91dd0f556a1c47fb99afc060ae0ef66b4d24793d/frozenlist-1.7.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8fc5d5cda37f62b262405cf9652cf0856839c4be8ee41be0afe8858f17f4c94b", size = 244116, upload_time = "2025-06-09T23:00:51.352Z" }, + { url = "https://files.pythonhosted.org/packages/24/fe/74e6ec0639c115df13d5850e75722750adabdc7de24e37e05a40527ca539/frozenlist-1.7.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b0d5ce521d1dd7d620198829b87ea002956e4319002ef0bc8d3e6d045cb4646e", size = 224557, upload_time = "2025-06-09T23:00:52.855Z" }, + { url = "https://files.pythonhosted.org/packages/8d/db/48421f62a6f77c553575201e89048e97198046b793f4a089c79a6e3268bd/frozenlist-1.7.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:488d0a7d6a0008ca0db273c542098a0fa9e7dfaa7e57f70acef43f32b3f69dca", size = 241820, upload_time = "2025-06-09T23:00:54.43Z" }, + { url = "https://files.pythonhosted.org/packages/1d/fa/cb4a76bea23047c8462976ea7b7a2bf53997a0ca171302deae9d6dd12096/frozenlist-1.7.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:15a7eaba63983d22c54d255b854e8108e7e5f3e89f647fc854bd77a237e767df", size = 236542, upload_time = "2025-06-09T23:00:56.409Z" }, + { url = "https://files.pythonhosted.org/packages/5d/32/476a4b5cfaa0ec94d3f808f193301debff2ea42288a099afe60757ef6282/frozenlist-1.7.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:1eaa7e9c6d15df825bf255649e05bd8a74b04a4d2baa1ae46d9c2d00b2ca2cb5", size = 249350, upload_time = "2025-06-09T23:00:58.468Z" }, + { url = "https://files.pythonhosted.org/packages/8d/ba/9a28042f84a6bf8ea5dbc81cfff8eaef18d78b2a1ad9d51c7bc5b029ad16/frozenlist-1.7.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e4389e06714cfa9d47ab87f784a7c5be91d3934cd6e9a7b85beef808297cc025", size = 225093, upload_time = "2025-06-09T23:01:00.015Z" }, + { url = "https://files.pythonhosted.org/packages/bc/29/3a32959e68f9cf000b04e79ba574527c17e8842e38c91d68214a37455786/frozenlist-1.7.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:73bd45e1488c40b63fe5a7df892baf9e2a4d4bb6409a2b3b78ac1c6236178e01", size = 245482, upload_time = "2025-06-09T23:01:01.474Z" }, + { url = "https://files.pythonhosted.org/packages/80/e8/edf2f9e00da553f07f5fa165325cfc302dead715cab6ac8336a5f3d0adc2/frozenlist-1.7.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:99886d98e1643269760e5fe0df31e5ae7050788dd288947f7f007209b8c33f08", size = 249590, upload_time = "2025-06-09T23:01:02.961Z" }, + { url = "https://files.pythonhosted.org/packages/1c/80/9a0eb48b944050f94cc51ee1c413eb14a39543cc4f760ed12657a5a3c45a/frozenlist-1.7.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:290a172aae5a4c278c6da8a96222e6337744cd9c77313efe33d5670b9f65fc43", size = 237785, upload_time = "2025-06-09T23:01:05.095Z" }, + { url = "https://files.pythonhosted.org/packages/f3/74/87601e0fb0369b7a2baf404ea921769c53b7ae00dee7dcfe5162c8c6dbf0/frozenlist-1.7.0-cp312-cp312-win32.whl", hash = "sha256:426c7bc70e07cfebc178bc4c2bf2d861d720c4fff172181eeb4a4c41d4ca2ad3", size = 39487, upload_time = "2025-06-09T23:01:06.54Z" }, + { url = "https://files.pythonhosted.org/packages/0b/15/c026e9a9fc17585a9d461f65d8593d281fedf55fbf7eb53f16c6df2392f9/frozenlist-1.7.0-cp312-cp312-win_amd64.whl", hash = "sha256:563b72efe5da92e02eb68c59cb37205457c977aa7a449ed1b37e6939e5c47c6a", size = 43874, upload_time = "2025-06-09T23:01:07.752Z" }, + { url = "https://files.pythonhosted.org/packages/24/90/6b2cebdabdbd50367273c20ff6b57a3dfa89bd0762de02c3a1eb42cb6462/frozenlist-1.7.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee80eeda5e2a4e660651370ebffd1286542b67e268aa1ac8d6dbe973120ef7ee", size = 79791, upload_time = "2025-06-09T23:01:09.368Z" }, + { url = "https://files.pythonhosted.org/packages/83/2e/5b70b6a3325363293fe5fc3ae74cdcbc3e996c2a11dde2fd9f1fb0776d19/frozenlist-1.7.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d1a81c85417b914139e3a9b995d4a1c84559afc839a93cf2cb7f15e6e5f6ed2d", size = 47165, upload_time = "2025-06-09T23:01:10.653Z" }, + { url = "https://files.pythonhosted.org/packages/f4/25/a0895c99270ca6966110f4ad98e87e5662eab416a17e7fd53c364bf8b954/frozenlist-1.7.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cbb65198a9132ebc334f237d7b0df163e4de83fb4f2bdfe46c1e654bdb0c5d43", size = 45881, upload_time = "2025-06-09T23:01:12.296Z" }, + { url = "https://files.pythonhosted.org/packages/19/7c/71bb0bbe0832793c601fff68cd0cf6143753d0c667f9aec93d3c323f4b55/frozenlist-1.7.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dab46c723eeb2c255a64f9dc05b8dd601fde66d6b19cdb82b2e09cc6ff8d8b5d", size = 232409, upload_time = "2025-06-09T23:01:13.641Z" }, + { url = "https://files.pythonhosted.org/packages/c0/45/ed2798718910fe6eb3ba574082aaceff4528e6323f9a8570be0f7028d8e9/frozenlist-1.7.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6aeac207a759d0dedd2e40745575ae32ab30926ff4fa49b1635def65806fddee", size = 225132, upload_time = "2025-06-09T23:01:15.264Z" }, + { url = "https://files.pythonhosted.org/packages/ba/e2/8417ae0f8eacb1d071d4950f32f229aa6bf68ab69aab797b72a07ea68d4f/frozenlist-1.7.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bd8c4e58ad14b4fa7802b8be49d47993182fdd4023393899632c88fd8cd994eb", size = 237638, upload_time = "2025-06-09T23:01:16.752Z" }, + { url = "https://files.pythonhosted.org/packages/f8/b7/2ace5450ce85f2af05a871b8c8719b341294775a0a6c5585d5e6170f2ce7/frozenlist-1.7.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:04fb24d104f425da3540ed83cbfc31388a586a7696142004c577fa61c6298c3f", size = 233539, upload_time = "2025-06-09T23:01:18.202Z" }, + { url = "https://files.pythonhosted.org/packages/46/b9/6989292c5539553dba63f3c83dc4598186ab2888f67c0dc1d917e6887db6/frozenlist-1.7.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6a5c505156368e4ea6b53b5ac23c92d7edc864537ff911d2fb24c140bb175e60", size = 215646, upload_time = "2025-06-09T23:01:19.649Z" }, + { url = "https://files.pythonhosted.org/packages/72/31/bc8c5c99c7818293458fe745dab4fd5730ff49697ccc82b554eb69f16a24/frozenlist-1.7.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8bd7eb96a675f18aa5c553eb7ddc24a43c8c18f22e1f9925528128c052cdbe00", size = 232233, upload_time = "2025-06-09T23:01:21.175Z" }, + { url = "https://files.pythonhosted.org/packages/59/52/460db4d7ba0811b9ccb85af996019f5d70831f2f5f255f7cc61f86199795/frozenlist-1.7.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:05579bf020096fe05a764f1f84cd104a12f78eaab68842d036772dc6d4870b4b", size = 227996, upload_time = "2025-06-09T23:01:23.098Z" }, + { url = "https://files.pythonhosted.org/packages/ba/c9/f4b39e904c03927b7ecf891804fd3b4df3db29b9e487c6418e37988d6e9d/frozenlist-1.7.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:376b6222d114e97eeec13d46c486facd41d4f43bab626b7c3f6a8b4e81a5192c", size = 242280, upload_time = "2025-06-09T23:01:24.808Z" }, + { url = "https://files.pythonhosted.org/packages/b8/33/3f8d6ced42f162d743e3517781566b8481322be321b486d9d262adf70bfb/frozenlist-1.7.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:0aa7e176ebe115379b5b1c95b4096fb1c17cce0847402e227e712c27bdb5a949", size = 217717, upload_time = "2025-06-09T23:01:26.28Z" }, + { url = "https://files.pythonhosted.org/packages/3e/e8/ad683e75da6ccef50d0ab0c2b2324b32f84fc88ceee778ed79b8e2d2fe2e/frozenlist-1.7.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3fbba20e662b9c2130dc771e332a99eff5da078b2b2648153a40669a6d0e36ca", size = 236644, upload_time = "2025-06-09T23:01:27.887Z" }, + { url = "https://files.pythonhosted.org/packages/b2/14/8d19ccdd3799310722195a72ac94ddc677541fb4bef4091d8e7775752360/frozenlist-1.7.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:f3f4410a0a601d349dd406b5713fec59b4cee7e71678d5b17edda7f4655a940b", size = 238879, upload_time = "2025-06-09T23:01:29.524Z" }, + { url = "https://files.pythonhosted.org/packages/ce/13/c12bf657494c2fd1079a48b2db49fa4196325909249a52d8f09bc9123fd7/frozenlist-1.7.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e2cdfaaec6a2f9327bf43c933c0319a7c429058e8537c508964a133dffee412e", size = 232502, upload_time = "2025-06-09T23:01:31.287Z" }, + { url = "https://files.pythonhosted.org/packages/d7/8b/e7f9dfde869825489382bc0d512c15e96d3964180c9499efcec72e85db7e/frozenlist-1.7.0-cp313-cp313-win32.whl", hash = "sha256:5fc4df05a6591c7768459caba1b342d9ec23fa16195e744939ba5914596ae3e1", size = 39169, upload_time = "2025-06-09T23:01:35.503Z" }, + { url = "https://files.pythonhosted.org/packages/35/89/a487a98d94205d85745080a37860ff5744b9820a2c9acbcdd9440bfddf98/frozenlist-1.7.0-cp313-cp313-win_amd64.whl", hash = "sha256:52109052b9791a3e6b5d1b65f4b909703984b770694d3eb64fad124c835d7cba", size = 43219, upload_time = "2025-06-09T23:01:36.784Z" }, + { url = "https://files.pythonhosted.org/packages/56/d5/5c4cf2319a49eddd9dd7145e66c4866bdc6f3dbc67ca3d59685149c11e0d/frozenlist-1.7.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:a6f86e4193bb0e235ef6ce3dde5cbabed887e0b11f516ce8a0f4d3b33078ec2d", size = 84345, upload_time = "2025-06-09T23:01:38.295Z" }, + { url = "https://files.pythonhosted.org/packages/a4/7d/ec2c1e1dc16b85bc9d526009961953df9cec8481b6886debb36ec9107799/frozenlist-1.7.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:82d664628865abeb32d90ae497fb93df398a69bb3434463d172b80fc25b0dd7d", size = 48880, upload_time = "2025-06-09T23:01:39.887Z" }, + { url = "https://files.pythonhosted.org/packages/69/86/f9596807b03de126e11e7d42ac91e3d0b19a6599c714a1989a4e85eeefc4/frozenlist-1.7.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:912a7e8375a1c9a68325a902f3953191b7b292aa3c3fb0d71a216221deca460b", size = 48498, upload_time = "2025-06-09T23:01:41.318Z" }, + { url = "https://files.pythonhosted.org/packages/5e/cb/df6de220f5036001005f2d726b789b2c0b65f2363b104bbc16f5be8084f8/frozenlist-1.7.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9537c2777167488d539bc5de2ad262efc44388230e5118868e172dd4a552b146", size = 292296, upload_time = "2025-06-09T23:01:42.685Z" }, + { url = "https://files.pythonhosted.org/packages/83/1f/de84c642f17c8f851a2905cee2dae401e5e0daca9b5ef121e120e19aa825/frozenlist-1.7.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:f34560fb1b4c3e30ba35fa9a13894ba39e5acfc5f60f57d8accde65f46cc5e74", size = 273103, upload_time = "2025-06-09T23:01:44.166Z" }, + { url = "https://files.pythonhosted.org/packages/88/3c/c840bfa474ba3fa13c772b93070893c6e9d5c0350885760376cbe3b6c1b3/frozenlist-1.7.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:acd03d224b0175f5a850edc104ac19040d35419eddad04e7cf2d5986d98427f1", size = 292869, upload_time = "2025-06-09T23:01:45.681Z" }, + { url = "https://files.pythonhosted.org/packages/a6/1c/3efa6e7d5a39a1d5ef0abeb51c48fb657765794a46cf124e5aca2c7a592c/frozenlist-1.7.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f2038310bc582f3d6a09b3816ab01737d60bf7b1ec70f5356b09e84fb7408ab1", size = 291467, upload_time = "2025-06-09T23:01:47.234Z" }, + { url = "https://files.pythonhosted.org/packages/4f/00/d5c5e09d4922c395e2f2f6b79b9a20dab4b67daaf78ab92e7729341f61f6/frozenlist-1.7.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b8c05e4c8e5f36e5e088caa1bf78a687528f83c043706640a92cb76cd6999384", size = 266028, upload_time = "2025-06-09T23:01:48.819Z" }, + { url = "https://files.pythonhosted.org/packages/4e/27/72765be905619dfde25a7f33813ac0341eb6b076abede17a2e3fbfade0cb/frozenlist-1.7.0-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:765bb588c86e47d0b68f23c1bee323d4b703218037765dcf3f25c838c6fecceb", size = 284294, upload_time = "2025-06-09T23:01:50.394Z" }, + { url = "https://files.pythonhosted.org/packages/88/67/c94103a23001b17808eb7dd1200c156bb69fb68e63fcf0693dde4cd6228c/frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:32dc2e08c67d86d0969714dd484fd60ff08ff81d1a1e40a77dd34a387e6ebc0c", size = 281898, upload_time = "2025-06-09T23:01:52.234Z" }, + { url = "https://files.pythonhosted.org/packages/42/34/a3e2c00c00f9e2a9db5653bca3fec306349e71aff14ae45ecc6d0951dd24/frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:c0303e597eb5a5321b4de9c68e9845ac8f290d2ab3f3e2c864437d3c5a30cd65", size = 290465, upload_time = "2025-06-09T23:01:53.788Z" }, + { url = "https://files.pythonhosted.org/packages/bb/73/f89b7fbce8b0b0c095d82b008afd0590f71ccb3dee6eee41791cf8cd25fd/frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:a47f2abb4e29b3a8d0b530f7c3598badc6b134562b1a5caee867f7c62fee51e3", size = 266385, upload_time = "2025-06-09T23:01:55.769Z" }, + { url = "https://files.pythonhosted.org/packages/cd/45/e365fdb554159462ca12df54bc59bfa7a9a273ecc21e99e72e597564d1ae/frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:3d688126c242a6fabbd92e02633414d40f50bb6002fa4cf995a1d18051525657", size = 288771, upload_time = "2025-06-09T23:01:57.4Z" }, + { url = "https://files.pythonhosted.org/packages/00/11/47b6117002a0e904f004d70ec5194fe9144f117c33c851e3d51c765962d0/frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:4e7e9652b3d367c7bd449a727dc79d5043f48b88d0cbfd4f9f1060cf2b414104", size = 288206, upload_time = "2025-06-09T23:01:58.936Z" }, + { url = "https://files.pythonhosted.org/packages/40/37/5f9f3c3fd7f7746082ec67bcdc204db72dad081f4f83a503d33220a92973/frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:1a85e345b4c43db8b842cab1feb41be5cc0b10a1830e6295b69d7310f99becaf", size = 282620, upload_time = "2025-06-09T23:02:00.493Z" }, + { url = "https://files.pythonhosted.org/packages/0b/31/8fbc5af2d183bff20f21aa743b4088eac4445d2bb1cdece449ae80e4e2d1/frozenlist-1.7.0-cp313-cp313t-win32.whl", hash = "sha256:3a14027124ddb70dfcee5148979998066897e79f89f64b13328595c4bdf77c81", size = 43059, upload_time = "2025-06-09T23:02:02.072Z" }, + { url = "https://files.pythonhosted.org/packages/bb/ed/41956f52105b8dbc26e457c5705340c67c8cc2b79f394b79bffc09d0e938/frozenlist-1.7.0-cp313-cp313t-win_amd64.whl", hash = "sha256:3bf8010d71d4507775f658e9823210b7427be36625b387221642725b515dcf3e", size = 47516, upload_time = "2025-06-09T23:02:03.779Z" }, + { url = "https://files.pythonhosted.org/packages/ee/45/b82e3c16be2182bff01179db177fe144d58b5dc787a7d4492c6ed8b9317f/frozenlist-1.7.0-py3-none-any.whl", hash = "sha256:9a5af342e34f7e97caf8c995864c7a396418ae2859cc6fdf1b1073020d516a7e", size = 13106, upload_time = "2025-06-09T23:02:34.204Z" }, +] + [[package]] name = "humanize" version = "4.12.3" @@ -20,23 +258,158 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a0/1e/62a2ec3104394a2975a2629eec89276ede9dbe717092f6966fcf963e1bf0/humanize-4.12.3-py3-none-any.whl", hash = "sha256:2cbf6370af06568fa6d2da77c86edb7886f3160ecd19ee1ffef07979efc597f6", size = 128487, upload_time = "2025-04-30T11:51:06.468Z" }, ] +[[package]] +name = "idna" +version = "3.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload_time = "2024-09-15T18:07:39.745Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload_time = "2024-09-15T18:07:37.964Z" }, +] + [[package]] name = "lego-monitoring" version = "1.0.0" source = { editable = "." } dependencies = [ + { name = "aiodns" }, + { name = "aiohttp" }, { name = "alt-utils" }, { name = "humanize" }, { name = "psutil" }, { name = "telethon" }, + { name = "uplink", extra = ["aiohttp"] }, ] [package.metadata] requires-dist = [ + { name = "aiodns", specifier = ">=3.5.0" }, + { name = "aiohttp", specifier = ">=3.12.15" }, { name = "alt-utils", specifier = ">=0.0.8" }, { name = "humanize", specifier = ">=4.12.3" }, { name = "psutil", specifier = ">=7.0.0" }, { name = "telethon", specifier = ">=1.40.0" }, + { name = "uplink", extras = ["aiohttp"], specifier = ">=0.10.0" }, +] + +[[package]] +name = "multidict" +version = "6.6.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/69/7f/0652e6ed47ab288e3756ea9c0df8b14950781184d4bd7883f4d87dd41245/multidict-6.6.4.tar.gz", hash = "sha256:d2d4e4787672911b48350df02ed3fa3fffdc2f2e8ca06dd6afdf34189b76a9dd", size = 101843, upload_time = "2025-08-11T12:08:48.217Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/05/f6/512ffd8fd8b37fb2680e5ac35d788f1d71bbaf37789d21a820bdc441e565/multidict-6.6.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0ffb87be160942d56d7b87b0fdf098e81ed565add09eaa1294268c7f3caac4c8", size = 76516, upload_time = "2025-08-11T12:06:53.393Z" }, + { url = "https://files.pythonhosted.org/packages/99/58/45c3e75deb8855c36bd66cc1658007589662ba584dbf423d01df478dd1c5/multidict-6.6.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d191de6cbab2aff5de6c5723101705fd044b3e4c7cfd587a1929b5028b9714b3", size = 45394, upload_time = "2025-08-11T12:06:54.555Z" }, + { url = "https://files.pythonhosted.org/packages/fd/ca/e8c4472a93a26e4507c0b8e1f0762c0d8a32de1328ef72fd704ef9cc5447/multidict-6.6.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:38a0956dd92d918ad5feff3db8fcb4a5eb7dba114da917e1a88475619781b57b", size = 43591, upload_time = "2025-08-11T12:06:55.672Z" }, + { url = "https://files.pythonhosted.org/packages/05/51/edf414f4df058574a7265034d04c935aa84a89e79ce90fcf4df211f47b16/multidict-6.6.4-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:6865f6d3b7900ae020b495d599fcf3765653bc927951c1abb959017f81ae8287", size = 237215, upload_time = "2025-08-11T12:06:57.213Z" }, + { url = "https://files.pythonhosted.org/packages/c8/45/8b3d6dbad8cf3252553cc41abea09ad527b33ce47a5e199072620b296902/multidict-6.6.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a2088c126b6f72db6c9212ad827d0ba088c01d951cee25e758c450da732c138", size = 258299, upload_time = "2025-08-11T12:06:58.946Z" }, + { url = "https://files.pythonhosted.org/packages/3c/e8/8ca2e9a9f5a435fc6db40438a55730a4bf4956b554e487fa1b9ae920f825/multidict-6.6.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0f37bed7319b848097085d7d48116f545985db988e2256b2e6f00563a3416ee6", size = 242357, upload_time = "2025-08-11T12:07:00.301Z" }, + { url = "https://files.pythonhosted.org/packages/0f/84/80c77c99df05a75c28490b2af8f7cba2a12621186e0a8b0865d8e745c104/multidict-6.6.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:01368e3c94032ba6ca0b78e7ccb099643466cf24f8dc8eefcfdc0571d56e58f9", size = 268369, upload_time = "2025-08-11T12:07:01.638Z" }, + { url = "https://files.pythonhosted.org/packages/0d/e9/920bfa46c27b05fb3e1ad85121fd49f441492dca2449c5bcfe42e4565d8a/multidict-6.6.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8fe323540c255db0bffee79ad7f048c909f2ab0edb87a597e1c17da6a54e493c", size = 269341, upload_time = "2025-08-11T12:07:02.943Z" }, + { url = "https://files.pythonhosted.org/packages/af/65/753a2d8b05daf496f4a9c367fe844e90a1b2cac78e2be2c844200d10cc4c/multidict-6.6.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8eb3025f17b0a4c3cd08cda49acf312a19ad6e8a4edd9dbd591e6506d999402", size = 256100, upload_time = "2025-08-11T12:07:04.564Z" }, + { url = "https://files.pythonhosted.org/packages/09/54/655be13ae324212bf0bc15d665a4e34844f34c206f78801be42f7a0a8aaa/multidict-6.6.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bbc14f0365534d35a06970d6a83478b249752e922d662dc24d489af1aa0d1be7", size = 253584, upload_time = "2025-08-11T12:07:05.914Z" }, + { url = "https://files.pythonhosted.org/packages/5c/74/ab2039ecc05264b5cec73eb018ce417af3ebb384ae9c0e9ed42cb33f8151/multidict-6.6.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:75aa52fba2d96bf972e85451b99d8e19cc37ce26fd016f6d4aa60da9ab2b005f", size = 251018, upload_time = "2025-08-11T12:07:08.301Z" }, + { url = "https://files.pythonhosted.org/packages/af/0a/ccbb244ac848e56c6427f2392741c06302bbfba49c0042f1eb3c5b606497/multidict-6.6.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4fefd4a815e362d4f011919d97d7b4a1e566f1dde83dc4ad8cfb5b41de1df68d", size = 251477, upload_time = "2025-08-11T12:07:10.248Z" }, + { url = "https://files.pythonhosted.org/packages/0e/b0/0ed49bba775b135937f52fe13922bc64a7eaf0a3ead84a36e8e4e446e096/multidict-6.6.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:db9801fe021f59a5b375ab778973127ca0ac52429a26e2fd86aa9508f4d26eb7", size = 263575, upload_time = "2025-08-11T12:07:11.928Z" }, + { url = "https://files.pythonhosted.org/packages/3e/d9/7fb85a85e14de2e44dfb6a24f03c41e2af8697a6df83daddb0e9b7569f73/multidict-6.6.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:a650629970fa21ac1fb06ba25dabfc5b8a2054fcbf6ae97c758aa956b8dba802", size = 259649, upload_time = "2025-08-11T12:07:13.244Z" }, + { url = "https://files.pythonhosted.org/packages/03/9e/b3a459bcf9b6e74fa461a5222a10ff9b544cb1cd52fd482fb1b75ecda2a2/multidict-6.6.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:452ff5da78d4720d7516a3a2abd804957532dd69296cb77319c193e3ffb87e24", size = 251505, upload_time = "2025-08-11T12:07:14.57Z" }, + { url = "https://files.pythonhosted.org/packages/86/a2/8022f78f041dfe6d71e364001a5cf987c30edfc83c8a5fb7a3f0974cff39/multidict-6.6.4-cp312-cp312-win32.whl", hash = "sha256:8c2fcb12136530ed19572bbba61b407f655e3953ba669b96a35036a11a485793", size = 41888, upload_time = "2025-08-11T12:07:15.904Z" }, + { url = "https://files.pythonhosted.org/packages/c7/eb/d88b1780d43a56db2cba24289fa744a9d216c1a8546a0dc3956563fd53ea/multidict-6.6.4-cp312-cp312-win_amd64.whl", hash = "sha256:047d9425860a8c9544fed1b9584f0c8bcd31bcde9568b047c5e567a1025ecd6e", size = 46072, upload_time = "2025-08-11T12:07:17.045Z" }, + { url = "https://files.pythonhosted.org/packages/9f/16/b929320bf5750e2d9d4931835a4c638a19d2494a5b519caaaa7492ebe105/multidict-6.6.4-cp312-cp312-win_arm64.whl", hash = "sha256:14754eb72feaa1e8ae528468f24250dd997b8e2188c3d2f593f9eba259e4b364", size = 43222, upload_time = "2025-08-11T12:07:18.328Z" }, + { url = "https://files.pythonhosted.org/packages/3a/5d/e1db626f64f60008320aab00fbe4f23fc3300d75892a3381275b3d284580/multidict-6.6.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f46a6e8597f9bd71b31cc708195d42b634c8527fecbcf93febf1052cacc1f16e", size = 75848, upload_time = "2025-08-11T12:07:19.912Z" }, + { url = "https://files.pythonhosted.org/packages/4c/aa/8b6f548d839b6c13887253af4e29c939af22a18591bfb5d0ee6f1931dae8/multidict-6.6.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:22e38b2bc176c5eb9c0a0e379f9d188ae4cd8b28c0f53b52bce7ab0a9e534657", size = 45060, upload_time = "2025-08-11T12:07:21.163Z" }, + { url = "https://files.pythonhosted.org/packages/eb/c6/f5e97e5d99a729bc2aa58eb3ebfa9f1e56a9b517cc38c60537c81834a73f/multidict-6.6.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5df8afd26f162da59e218ac0eefaa01b01b2e6cd606cffa46608f699539246da", size = 43269, upload_time = "2025-08-11T12:07:22.392Z" }, + { url = "https://files.pythonhosted.org/packages/dc/31/d54eb0c62516776f36fe67f84a732f97e0b0e12f98d5685bebcc6d396910/multidict-6.6.4-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:49517449b58d043023720aa58e62b2f74ce9b28f740a0b5d33971149553d72aa", size = 237158, upload_time = "2025-08-11T12:07:23.636Z" }, + { url = "https://files.pythonhosted.org/packages/c4/1c/8a10c1c25b23156e63b12165a929d8eb49a6ed769fdbefb06e6f07c1e50d/multidict-6.6.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ae9408439537c5afdca05edd128a63f56a62680f4b3c234301055d7a2000220f", size = 257076, upload_time = "2025-08-11T12:07:25.049Z" }, + { url = "https://files.pythonhosted.org/packages/ad/86/90e20b5771d6805a119e483fd3d1e8393e745a11511aebca41f0da38c3e2/multidict-6.6.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:87a32d20759dc52a9e850fe1061b6e41ab28e2998d44168a8a341b99ded1dba0", size = 240694, upload_time = "2025-08-11T12:07:26.458Z" }, + { url = "https://files.pythonhosted.org/packages/e7/49/484d3e6b535bc0555b52a0a26ba86e4d8d03fd5587d4936dc59ba7583221/multidict-6.6.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:52e3c8d43cdfff587ceedce9deb25e6ae77daba560b626e97a56ddcad3756879", size = 266350, upload_time = "2025-08-11T12:07:27.94Z" }, + { url = "https://files.pythonhosted.org/packages/bf/b4/aa4c5c379b11895083d50021e229e90c408d7d875471cb3abf721e4670d6/multidict-6.6.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ad8850921d3a8d8ff6fbef790e773cecfc260bbfa0566998980d3fa8f520bc4a", size = 267250, upload_time = "2025-08-11T12:07:29.303Z" }, + { url = "https://files.pythonhosted.org/packages/80/e5/5e22c5bf96a64bdd43518b1834c6d95a4922cc2066b7d8e467dae9b6cee6/multidict-6.6.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:497a2954adc25c08daff36f795077f63ad33e13f19bfff7736e72c785391534f", size = 254900, upload_time = "2025-08-11T12:07:30.764Z" }, + { url = "https://files.pythonhosted.org/packages/17/38/58b27fed927c07035abc02befacab42491e7388ca105e087e6e0215ead64/multidict-6.6.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:024ce601f92d780ca1617ad4be5ac15b501cc2414970ffa2bb2bbc2bd5a68fa5", size = 252355, upload_time = "2025-08-11T12:07:32.205Z" }, + { url = "https://files.pythonhosted.org/packages/d0/a1/dad75d23a90c29c02b5d6f3d7c10ab36c3197613be5d07ec49c7791e186c/multidict-6.6.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:a693fc5ed9bdd1c9e898013e0da4dcc640de7963a371c0bd458e50e046bf6438", size = 250061, upload_time = "2025-08-11T12:07:33.623Z" }, + { url = "https://files.pythonhosted.org/packages/b8/1a/ac2216b61c7f116edab6dc3378cca6c70dc019c9a457ff0d754067c58b20/multidict-6.6.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:190766dac95aab54cae5b152a56520fd99298f32a1266d66d27fdd1b5ac00f4e", size = 249675, upload_time = "2025-08-11T12:07:34.958Z" }, + { url = "https://files.pythonhosted.org/packages/d4/79/1916af833b800d13883e452e8e0977c065c4ee3ab7a26941fbfdebc11895/multidict-6.6.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:34d8f2a5ffdceab9dcd97c7a016deb2308531d5f0fced2bb0c9e1df45b3363d7", size = 261247, upload_time = "2025-08-11T12:07:36.588Z" }, + { url = "https://files.pythonhosted.org/packages/c5/65/d1f84fe08ac44a5fc7391cbc20a7cedc433ea616b266284413fd86062f8c/multidict-6.6.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:59e8d40ab1f5a8597abcef00d04845155a5693b5da00d2c93dbe88f2050f2812", size = 257960, upload_time = "2025-08-11T12:07:39.735Z" }, + { url = "https://files.pythonhosted.org/packages/13/b5/29ec78057d377b195ac2c5248c773703a6b602e132a763e20ec0457e7440/multidict-6.6.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:467fe64138cfac771f0e949b938c2e1ada2b5af22f39692aa9258715e9ea613a", size = 250078, upload_time = "2025-08-11T12:07:41.525Z" }, + { url = "https://files.pythonhosted.org/packages/c4/0e/7e79d38f70a872cae32e29b0d77024bef7834b0afb406ddae6558d9e2414/multidict-6.6.4-cp313-cp313-win32.whl", hash = "sha256:14616a30fe6d0a48d0a48d1a633ab3b8bec4cf293aac65f32ed116f620adfd69", size = 41708, upload_time = "2025-08-11T12:07:43.405Z" }, + { url = "https://files.pythonhosted.org/packages/9d/34/746696dffff742e97cd6a23da953e55d0ea51fa601fa2ff387b3edcfaa2c/multidict-6.6.4-cp313-cp313-win_amd64.whl", hash = "sha256:40cd05eaeb39e2bc8939451f033e57feaa2ac99e07dbca8afe2be450a4a3b6cf", size = 45912, upload_time = "2025-08-11T12:07:45.082Z" }, + { url = "https://files.pythonhosted.org/packages/c7/87/3bac136181e271e29170d8d71929cdeddeb77f3e8b6a0c08da3a8e9da114/multidict-6.6.4-cp313-cp313-win_arm64.whl", hash = "sha256:f6eb37d511bfae9e13e82cb4d1af36b91150466f24d9b2b8a9785816deb16605", size = 43076, upload_time = "2025-08-11T12:07:46.746Z" }, + { url = "https://files.pythonhosted.org/packages/64/94/0a8e63e36c049b571c9ae41ee301ada29c3fee9643d9c2548d7d558a1d99/multidict-6.6.4-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:6c84378acd4f37d1b507dfa0d459b449e2321b3ba5f2338f9b085cf7a7ba95eb", size = 82812, upload_time = "2025-08-11T12:07:48.402Z" }, + { url = "https://files.pythonhosted.org/packages/25/1a/be8e369dfcd260d2070a67e65dd3990dd635cbd735b98da31e00ea84cd4e/multidict-6.6.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0e0558693063c75f3d952abf645c78f3c5dfdd825a41d8c4d8156fc0b0da6e7e", size = 48313, upload_time = "2025-08-11T12:07:49.679Z" }, + { url = "https://files.pythonhosted.org/packages/26/5a/dd4ade298674b2f9a7b06a32c94ffbc0497354df8285f27317c66433ce3b/multidict-6.6.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3f8e2384cb83ebd23fd07e9eada8ba64afc4c759cd94817433ab8c81ee4b403f", size = 46777, upload_time = "2025-08-11T12:07:51.318Z" }, + { url = "https://files.pythonhosted.org/packages/89/db/98aa28bc7e071bfba611ac2ae803c24e96dd3a452b4118c587d3d872c64c/multidict-6.6.4-cp313-cp313t-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:f996b87b420995a9174b2a7c1a8daf7db4750be6848b03eb5e639674f7963773", size = 229321, upload_time = "2025-08-11T12:07:52.965Z" }, + { url = "https://files.pythonhosted.org/packages/c7/bc/01ddda2a73dd9d167bd85d0e8ef4293836a8f82b786c63fb1a429bc3e678/multidict-6.6.4-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cc356250cffd6e78416cf5b40dc6a74f1edf3be8e834cf8862d9ed5265cf9b0e", size = 249954, upload_time = "2025-08-11T12:07:54.423Z" }, + { url = "https://files.pythonhosted.org/packages/06/78/6b7c0f020f9aa0acf66d0ab4eb9f08375bac9a50ff5e3edb1c4ccd59eafc/multidict-6.6.4-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:dadf95aa862714ea468a49ad1e09fe00fcc9ec67d122f6596a8d40caf6cec7d0", size = 228612, upload_time = "2025-08-11T12:07:55.914Z" }, + { url = "https://files.pythonhosted.org/packages/00/44/3faa416f89b2d5d76e9d447296a81521e1c832ad6e40b92f990697b43192/multidict-6.6.4-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7dd57515bebffd8ebd714d101d4c434063322e4fe24042e90ced41f18b6d3395", size = 257528, upload_time = "2025-08-11T12:07:57.371Z" }, + { url = "https://files.pythonhosted.org/packages/05/5f/77c03b89af0fcb16f018f668207768191fb9dcfb5e3361a5e706a11db2c9/multidict-6.6.4-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:967af5f238ebc2eb1da4e77af5492219fbd9b4b812347da39a7b5f5c72c0fa45", size = 256329, upload_time = "2025-08-11T12:07:58.844Z" }, + { url = "https://files.pythonhosted.org/packages/cf/e9/ed750a2a9afb4f8dc6f13dc5b67b514832101b95714f1211cd42e0aafc26/multidict-6.6.4-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2a4c6875c37aae9794308ec43e3530e4aa0d36579ce38d89979bbf89582002bb", size = 247928, upload_time = "2025-08-11T12:08:01.037Z" }, + { url = "https://files.pythonhosted.org/packages/1f/b5/e0571bc13cda277db7e6e8a532791d4403dacc9850006cb66d2556e649c0/multidict-6.6.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:7f683a551e92bdb7fac545b9c6f9fa2aebdeefa61d607510b3533286fcab67f5", size = 245228, upload_time = "2025-08-11T12:08:02.96Z" }, + { url = "https://files.pythonhosted.org/packages/f3/a3/69a84b0eccb9824491f06368f5b86e72e4af54c3067c37c39099b6687109/multidict-6.6.4-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:3ba5aaf600edaf2a868a391779f7a85d93bed147854925f34edd24cc70a3e141", size = 235869, upload_time = "2025-08-11T12:08:04.746Z" }, + { url = "https://files.pythonhosted.org/packages/a9/9d/28802e8f9121a6a0804fa009debf4e753d0a59969ea9f70be5f5fdfcb18f/multidict-6.6.4-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:580b643b7fd2c295d83cad90d78419081f53fd532d1f1eb67ceb7060f61cff0d", size = 243446, upload_time = "2025-08-11T12:08:06.332Z" }, + { url = "https://files.pythonhosted.org/packages/38/ea/6c98add069b4878c1d66428a5f5149ddb6d32b1f9836a826ac764b9940be/multidict-6.6.4-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:37b7187197da6af3ee0b044dbc9625afd0c885f2800815b228a0e70f9a7f473d", size = 252299, upload_time = "2025-08-11T12:08:07.931Z" }, + { url = "https://files.pythonhosted.org/packages/3a/09/8fe02d204473e14c0af3affd50af9078839dfca1742f025cca765435d6b4/multidict-6.6.4-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e1b93790ed0bc26feb72e2f08299691ceb6da5e9e14a0d13cc74f1869af327a0", size = 246926, upload_time = "2025-08-11T12:08:09.467Z" }, + { url = "https://files.pythonhosted.org/packages/37/3d/7b1e10d774a6df5175ecd3c92bff069e77bed9ec2a927fdd4ff5fe182f67/multidict-6.6.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a506a77ddee1efcca81ecbeae27ade3e09cdf21a8ae854d766c2bb4f14053f92", size = 243383, upload_time = "2025-08-11T12:08:10.981Z" }, + { url = "https://files.pythonhosted.org/packages/50/b0/a6fae46071b645ae98786ab738447de1ef53742eaad949f27e960864bb49/multidict-6.6.4-cp313-cp313t-win32.whl", hash = "sha256:f93b2b2279883d1d0a9e1bd01f312d6fc315c5e4c1f09e112e4736e2f650bc4e", size = 47775, upload_time = "2025-08-11T12:08:12.439Z" }, + { url = "https://files.pythonhosted.org/packages/b2/0a/2436550b1520091af0600dff547913cb2d66fbac27a8c33bc1b1bccd8d98/multidict-6.6.4-cp313-cp313t-win_amd64.whl", hash = "sha256:6d46a180acdf6e87cc41dc15d8f5c2986e1e8739dc25dbb7dac826731ef381a4", size = 53100, upload_time = "2025-08-11T12:08:13.823Z" }, + { url = "https://files.pythonhosted.org/packages/97/ea/43ac51faff934086db9c072a94d327d71b7d8b40cd5dcb47311330929ef0/multidict-6.6.4-cp313-cp313t-win_arm64.whl", hash = "sha256:756989334015e3335d087a27331659820d53ba432befdef6a718398b0a8493ad", size = 45501, upload_time = "2025-08-11T12:08:15.173Z" }, + { url = "https://files.pythonhosted.org/packages/fd/69/b547032297c7e63ba2af494edba695d781af8a0c6e89e4d06cf848b21d80/multidict-6.6.4-py3-none-any.whl", hash = "sha256:27d8f8e125c07cb954e54d75d04905a9bba8a439c1d84aca94949d4d03d8601c", size = 12313, upload_time = "2025-08-11T12:08:46.891Z" }, +] + +[[package]] +name = "propcache" +version = "0.3.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a6/16/43264e4a779dd8588c21a70f0709665ee8f611211bdd2c87d952cfa7c776/propcache-0.3.2.tar.gz", hash = "sha256:20d7d62e4e7ef05f221e0db2856b979540686342e7dd9973b815599c7057e168", size = 44139, upload_time = "2025-06-09T22:56:06.081Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/42/9ca01b0a6f48e81615dca4765a8f1dd2c057e0540f6116a27dc5ee01dfb6/propcache-0.3.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:8de106b6c84506b31c27168582cd3cb3000a6412c16df14a8628e5871ff83c10", size = 73674, upload_time = "2025-06-09T22:54:30.551Z" }, + { url = "https://files.pythonhosted.org/packages/af/6e/21293133beb550f9c901bbece755d582bfaf2176bee4774000bd4dd41884/propcache-0.3.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:28710b0d3975117239c76600ea351934ac7b5ff56e60953474342608dbbb6154", size = 43570, upload_time = "2025-06-09T22:54:32.296Z" }, + { url = "https://files.pythonhosted.org/packages/0c/c8/0393a0a3a2b8760eb3bde3c147f62b20044f0ddac81e9d6ed7318ec0d852/propcache-0.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce26862344bdf836650ed2487c3d724b00fbfec4233a1013f597b78c1cb73615", size = 43094, upload_time = "2025-06-09T22:54:33.929Z" }, + { url = "https://files.pythonhosted.org/packages/37/2c/489afe311a690399d04a3e03b069225670c1d489eb7b044a566511c1c498/propcache-0.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bca54bd347a253af2cf4544bbec232ab982f4868de0dd684246b67a51bc6b1db", size = 226958, upload_time = "2025-06-09T22:54:35.186Z" }, + { url = "https://files.pythonhosted.org/packages/9d/ca/63b520d2f3d418c968bf596839ae26cf7f87bead026b6192d4da6a08c467/propcache-0.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:55780d5e9a2ddc59711d727226bb1ba83a22dd32f64ee15594b9392b1f544eb1", size = 234894, upload_time = "2025-06-09T22:54:36.708Z" }, + { url = "https://files.pythonhosted.org/packages/11/60/1d0ed6fff455a028d678df30cc28dcee7af77fa2b0e6962ce1df95c9a2a9/propcache-0.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:035e631be25d6975ed87ab23153db6a73426a48db688070d925aa27e996fe93c", size = 233672, upload_time = "2025-06-09T22:54:38.062Z" }, + { url = "https://files.pythonhosted.org/packages/37/7c/54fd5301ef38505ab235d98827207176a5c9b2aa61939b10a460ca53e123/propcache-0.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ee6f22b6eaa39297c751d0e80c0d3a454f112f5c6481214fcf4c092074cecd67", size = 224395, upload_time = "2025-06-09T22:54:39.634Z" }, + { url = "https://files.pythonhosted.org/packages/ee/1a/89a40e0846f5de05fdc6779883bf46ba980e6df4d2ff8fb02643de126592/propcache-0.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7ca3aee1aa955438c4dba34fc20a9f390e4c79967257d830f137bd5a8a32ed3b", size = 212510, upload_time = "2025-06-09T22:54:41.565Z" }, + { url = "https://files.pythonhosted.org/packages/5e/33/ca98368586c9566a6b8d5ef66e30484f8da84c0aac3f2d9aec6d31a11bd5/propcache-0.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7a4f30862869fa2b68380d677cc1c5fcf1e0f2b9ea0cf665812895c75d0ca3b8", size = 222949, upload_time = "2025-06-09T22:54:43.038Z" }, + { url = "https://files.pythonhosted.org/packages/ba/11/ace870d0aafe443b33b2f0b7efdb872b7c3abd505bfb4890716ad7865e9d/propcache-0.3.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:b77ec3c257d7816d9f3700013639db7491a434644c906a2578a11daf13176251", size = 217258, upload_time = "2025-06-09T22:54:44.376Z" }, + { url = "https://files.pythonhosted.org/packages/5b/d2/86fd6f7adffcfc74b42c10a6b7db721d1d9ca1055c45d39a1a8f2a740a21/propcache-0.3.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:cab90ac9d3f14b2d5050928483d3d3b8fb6b4018893fc75710e6aa361ecb2474", size = 213036, upload_time = "2025-06-09T22:54:46.243Z" }, + { url = "https://files.pythonhosted.org/packages/07/94/2d7d1e328f45ff34a0a284cf5a2847013701e24c2a53117e7c280a4316b3/propcache-0.3.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:0b504d29f3c47cf6b9e936c1852246c83d450e8e063d50562115a6be6d3a2535", size = 227684, upload_time = "2025-06-09T22:54:47.63Z" }, + { url = "https://files.pythonhosted.org/packages/b7/05/37ae63a0087677e90b1d14710e532ff104d44bc1efa3b3970fff99b891dc/propcache-0.3.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:ce2ac2675a6aa41ddb2a0c9cbff53780a617ac3d43e620f8fd77ba1c84dcfc06", size = 234562, upload_time = "2025-06-09T22:54:48.982Z" }, + { url = "https://files.pythonhosted.org/packages/a4/7c/3f539fcae630408d0bd8bf3208b9a647ccad10976eda62402a80adf8fc34/propcache-0.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:62b4239611205294cc433845b914131b2a1f03500ff3c1ed093ed216b82621e1", size = 222142, upload_time = "2025-06-09T22:54:50.424Z" }, + { url = "https://files.pythonhosted.org/packages/7c/d2/34b9eac8c35f79f8a962546b3e97e9d4b990c420ee66ac8255d5d9611648/propcache-0.3.2-cp312-cp312-win32.whl", hash = "sha256:df4a81b9b53449ebc90cc4deefb052c1dd934ba85012aa912c7ea7b7e38b60c1", size = 37711, upload_time = "2025-06-09T22:54:52.072Z" }, + { url = "https://files.pythonhosted.org/packages/19/61/d582be5d226cf79071681d1b46b848d6cb03d7b70af7063e33a2787eaa03/propcache-0.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:7046e79b989d7fe457bb755844019e10f693752d169076138abf17f31380800c", size = 41479, upload_time = "2025-06-09T22:54:53.234Z" }, + { url = "https://files.pythonhosted.org/packages/dc/d1/8c747fafa558c603c4ca19d8e20b288aa0c7cda74e9402f50f31eb65267e/propcache-0.3.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ca592ed634a73ca002967458187109265e980422116c0a107cf93d81f95af945", size = 71286, upload_time = "2025-06-09T22:54:54.369Z" }, + { url = "https://files.pythonhosted.org/packages/61/99/d606cb7986b60d89c36de8a85d58764323b3a5ff07770a99d8e993b3fa73/propcache-0.3.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9ecb0aad4020e275652ba3975740f241bd12a61f1a784df044cf7477a02bc252", size = 42425, upload_time = "2025-06-09T22:54:55.642Z" }, + { url = "https://files.pythonhosted.org/packages/8c/96/ef98f91bbb42b79e9bb82bdd348b255eb9d65f14dbbe3b1594644c4073f7/propcache-0.3.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7f08f1cc28bd2eade7a8a3d2954ccc673bb02062e3e7da09bc75d843386b342f", size = 41846, upload_time = "2025-06-09T22:54:57.246Z" }, + { url = "https://files.pythonhosted.org/packages/5b/ad/3f0f9a705fb630d175146cd7b1d2bf5555c9beaed54e94132b21aac098a6/propcache-0.3.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d1a342c834734edb4be5ecb1e9fb48cb64b1e2320fccbd8c54bf8da8f2a84c33", size = 208871, upload_time = "2025-06-09T22:54:58.975Z" }, + { url = "https://files.pythonhosted.org/packages/3a/38/2085cda93d2c8b6ec3e92af2c89489a36a5886b712a34ab25de9fbca7992/propcache-0.3.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8a544caaae1ac73f1fecfae70ded3e93728831affebd017d53449e3ac052ac1e", size = 215720, upload_time = "2025-06-09T22:55:00.471Z" }, + { url = "https://files.pythonhosted.org/packages/61/c1/d72ea2dc83ac7f2c8e182786ab0fc2c7bd123a1ff9b7975bee671866fe5f/propcache-0.3.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:310d11aa44635298397db47a3ebce7db99a4cc4b9bbdfcf6c98a60c8d5261cf1", size = 215203, upload_time = "2025-06-09T22:55:01.834Z" }, + { url = "https://files.pythonhosted.org/packages/af/81/b324c44ae60c56ef12007105f1460d5c304b0626ab0cc6b07c8f2a9aa0b8/propcache-0.3.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c1396592321ac83157ac03a2023aa6cc4a3cc3cfdecb71090054c09e5a7cce3", size = 206365, upload_time = "2025-06-09T22:55:03.199Z" }, + { url = "https://files.pythonhosted.org/packages/09/73/88549128bb89e66d2aff242488f62869014ae092db63ccea53c1cc75a81d/propcache-0.3.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8cabf5b5902272565e78197edb682017d21cf3b550ba0460ee473753f28d23c1", size = 196016, upload_time = "2025-06-09T22:55:04.518Z" }, + { url = "https://files.pythonhosted.org/packages/b9/3f/3bdd14e737d145114a5eb83cb172903afba7242f67c5877f9909a20d948d/propcache-0.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0a2f2235ac46a7aa25bdeb03a9e7060f6ecbd213b1f9101c43b3090ffb971ef6", size = 205596, upload_time = "2025-06-09T22:55:05.942Z" }, + { url = "https://files.pythonhosted.org/packages/0f/ca/2f4aa819c357d3107c3763d7ef42c03980f9ed5c48c82e01e25945d437c1/propcache-0.3.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:92b69e12e34869a6970fd2f3da91669899994b47c98f5d430b781c26f1d9f387", size = 200977, upload_time = "2025-06-09T22:55:07.792Z" }, + { url = "https://files.pythonhosted.org/packages/cd/4a/e65276c7477533c59085251ae88505caf6831c0e85ff8b2e31ebcbb949b1/propcache-0.3.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:54e02207c79968ebbdffc169591009f4474dde3b4679e16634d34c9363ff56b4", size = 197220, upload_time = "2025-06-09T22:55:09.173Z" }, + { url = "https://files.pythonhosted.org/packages/7c/54/fc7152e517cf5578278b242396ce4d4b36795423988ef39bb8cd5bf274c8/propcache-0.3.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4adfb44cb588001f68c5466579d3f1157ca07f7504fc91ec87862e2b8e556b88", size = 210642, upload_time = "2025-06-09T22:55:10.62Z" }, + { url = "https://files.pythonhosted.org/packages/b9/80/abeb4a896d2767bf5f1ea7b92eb7be6a5330645bd7fb844049c0e4045d9d/propcache-0.3.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:fd3e6019dc1261cd0291ee8919dd91fbab7b169bb76aeef6c716833a3f65d206", size = 212789, upload_time = "2025-06-09T22:55:12.029Z" }, + { url = "https://files.pythonhosted.org/packages/b3/db/ea12a49aa7b2b6d68a5da8293dcf50068d48d088100ac016ad92a6a780e6/propcache-0.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4c181cad81158d71c41a2bce88edce078458e2dd5ffee7eddd6b05da85079f43", size = 205880, upload_time = "2025-06-09T22:55:13.45Z" }, + { url = "https://files.pythonhosted.org/packages/d1/e5/9076a0bbbfb65d1198007059c65639dfd56266cf8e477a9707e4b1999ff4/propcache-0.3.2-cp313-cp313-win32.whl", hash = "sha256:8a08154613f2249519e549de2330cf8e2071c2887309a7b07fb56098f5170a02", size = 37220, upload_time = "2025-06-09T22:55:15.284Z" }, + { url = "https://files.pythonhosted.org/packages/d3/f5/b369e026b09a26cd77aa88d8fffd69141d2ae00a2abaaf5380d2603f4b7f/propcache-0.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:e41671f1594fc4ab0a6dec1351864713cb3a279910ae8b58f884a88a0a632c05", size = 40678, upload_time = "2025-06-09T22:55:16.445Z" }, + { url = "https://files.pythonhosted.org/packages/a4/3a/6ece377b55544941a08d03581c7bc400a3c8cd3c2865900a68d5de79e21f/propcache-0.3.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:9a3cf035bbaf035f109987d9d55dc90e4b0e36e04bbbb95af3055ef17194057b", size = 76560, upload_time = "2025-06-09T22:55:17.598Z" }, + { url = "https://files.pythonhosted.org/packages/0c/da/64a2bb16418740fa634b0e9c3d29edff1db07f56d3546ca2d86ddf0305e1/propcache-0.3.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:156c03d07dc1323d8dacaa221fbe028c5c70d16709cdd63502778e6c3ccca1b0", size = 44676, upload_time = "2025-06-09T22:55:18.922Z" }, + { url = "https://files.pythonhosted.org/packages/36/7b/f025e06ea51cb72c52fb87e9b395cced02786610b60a3ed51da8af017170/propcache-0.3.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:74413c0ba02ba86f55cf60d18daab219f7e531620c15f1e23d95563f505efe7e", size = 44701, upload_time = "2025-06-09T22:55:20.106Z" }, + { url = "https://files.pythonhosted.org/packages/a4/00/faa1b1b7c3b74fc277f8642f32a4c72ba1d7b2de36d7cdfb676db7f4303e/propcache-0.3.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f066b437bb3fa39c58ff97ab2ca351db465157d68ed0440abecb21715eb24b28", size = 276934, upload_time = "2025-06-09T22:55:21.5Z" }, + { url = "https://files.pythonhosted.org/packages/74/ab/935beb6f1756e0476a4d5938ff44bf0d13a055fed880caf93859b4f1baf4/propcache-0.3.2-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f1304b085c83067914721e7e9d9917d41ad87696bf70f0bc7dee450e9c71ad0a", size = 278316, upload_time = "2025-06-09T22:55:22.918Z" }, + { url = "https://files.pythonhosted.org/packages/f8/9d/994a5c1ce4389610838d1caec74bdf0e98b306c70314d46dbe4fcf21a3e2/propcache-0.3.2-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ab50cef01b372763a13333b4e54021bdcb291fc9a8e2ccb9c2df98be51bcde6c", size = 282619, upload_time = "2025-06-09T22:55:24.651Z" }, + { url = "https://files.pythonhosted.org/packages/2b/00/a10afce3d1ed0287cef2e09506d3be9822513f2c1e96457ee369adb9a6cd/propcache-0.3.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fad3b2a085ec259ad2c2842666b2a0a49dea8463579c606426128925af1ed725", size = 265896, upload_time = "2025-06-09T22:55:26.049Z" }, + { url = "https://files.pythonhosted.org/packages/2e/a8/2aa6716ffa566ca57c749edb909ad27884680887d68517e4be41b02299f3/propcache-0.3.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:261fa020c1c14deafd54c76b014956e2f86991af198c51139faf41c4d5e83892", size = 252111, upload_time = "2025-06-09T22:55:27.381Z" }, + { url = "https://files.pythonhosted.org/packages/36/4f/345ca9183b85ac29c8694b0941f7484bf419c7f0fea2d1e386b4f7893eed/propcache-0.3.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:46d7f8aa79c927e5f987ee3a80205c987717d3659f035c85cf0c3680526bdb44", size = 268334, upload_time = "2025-06-09T22:55:28.747Z" }, + { url = "https://files.pythonhosted.org/packages/3e/ca/fcd54f78b59e3f97b3b9715501e3147f5340167733d27db423aa321e7148/propcache-0.3.2-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:6d8f3f0eebf73e3c0ff0e7853f68be638b4043c65a70517bb575eff54edd8dbe", size = 255026, upload_time = "2025-06-09T22:55:30.184Z" }, + { url = "https://files.pythonhosted.org/packages/8b/95/8e6a6bbbd78ac89c30c225210a5c687790e532ba4088afb8c0445b77ef37/propcache-0.3.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:03c89c1b14a5452cf15403e291c0ccd7751d5b9736ecb2c5bab977ad6c5bcd81", size = 250724, upload_time = "2025-06-09T22:55:31.646Z" }, + { url = "https://files.pythonhosted.org/packages/ee/b0/0dd03616142baba28e8b2d14ce5df6631b4673850a3d4f9c0f9dd714a404/propcache-0.3.2-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:0cc17efde71e12bbaad086d679ce575268d70bc123a5a71ea7ad76f70ba30bba", size = 268868, upload_time = "2025-06-09T22:55:33.209Z" }, + { url = "https://files.pythonhosted.org/packages/c5/98/2c12407a7e4fbacd94ddd32f3b1e3d5231e77c30ef7162b12a60e2dd5ce3/propcache-0.3.2-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:acdf05d00696bc0447e278bb53cb04ca72354e562cf88ea6f9107df8e7fd9770", size = 271322, upload_time = "2025-06-09T22:55:35.065Z" }, + { url = "https://files.pythonhosted.org/packages/35/91/9cb56efbb428b006bb85db28591e40b7736847b8331d43fe335acf95f6c8/propcache-0.3.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4445542398bd0b5d32df908031cb1b30d43ac848e20470a878b770ec2dcc6330", size = 265778, upload_time = "2025-06-09T22:55:36.45Z" }, + { url = "https://files.pythonhosted.org/packages/9a/4c/b0fe775a2bdd01e176b14b574be679d84fc83958335790f7c9a686c1f468/propcache-0.3.2-cp313-cp313t-win32.whl", hash = "sha256:f86e5d7cd03afb3a1db8e9f9f6eff15794e79e791350ac48a8c924e6f439f394", size = 41175, upload_time = "2025-06-09T22:55:38.436Z" }, + { url = "https://files.pythonhosted.org/packages/a4/ff/47f08595e3d9b5e149c150f88d9714574f1a7cbd89fe2817158a952674bf/propcache-0.3.2-cp313-cp313t-win_amd64.whl", hash = "sha256:9704bedf6e7cbe3c65eca4379a9b53ee6a83749f047808cbb5044d40d7d72198", size = 44857, upload_time = "2025-06-09T22:55:39.687Z" }, + { url = "https://files.pythonhosted.org/packages/cc/35/cc0aaecf278bb4575b8555f2b137de5ab821595ddae9da9d3cd1da4072c7/propcache-0.3.2-py3-none-any.whl", hash = "sha256:98f1ec44fb675f5052cccc8e609c46ed23a35a1cfd18545ad4e29002d858a43f", size = 12663, upload_time = "2025-06-09T22:56:04.484Z" }, ] [[package]] @@ -69,6 +442,67 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c8/f1/d6a797abb14f6283c0ddff96bbdd46937f64122b8c925cab503dd37f8214/pyasn1-0.6.1-py3-none-any.whl", hash = "sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629", size = 83135, upload_time = "2024-09-11T16:00:36.122Z" }, ] +[[package]] +name = "pycares" +version = "4.10.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e0/2f/5b46bb8e65070eb1f7f549d2f2e71db6b9899ef24ac9f82128014aeb1e25/pycares-4.10.0.tar.gz", hash = "sha256:9df70dce6e05afa5d477f48959170e569485e20dad1a089c4cf3b2d7ffbd8bf9", size = 654318, upload_time = "2025-08-05T22:35:34.863Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/ac/ff843ee3e4e6c39e8582772bc75c0898e00c9859906b094569a09f64a1c8/pycares-4.10.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:870354741adb5d212a521c33005b368b5c8baa81e2f0d3143e868c025c5bb32f", size = 145860, upload_time = "2025-08-05T22:34:41.587Z" }, + { url = "https://files.pythonhosted.org/packages/ee/db/9cb8a2d3bdd138a62334320bf940ea321ae15bce8deac9078fcb2bb17dba/pycares-4.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8912544250edc3da6a1fc97ef9543f69ee4bc2812f90e17d294397382d1ecc80", size = 140889, upload_time = "2025-08-05T22:34:42.738Z" }, + { url = "https://files.pythonhosted.org/packages/d3/51/c51e4bb52020388222ddf04d73ae32aba4fa1e17fe4decd7a842d85e5dfb/pycares-4.10.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:49d896bb5ae3c571bc359d3076c1484fd4f99bb5138c1c597da1f57979238771", size = 637789, upload_time = "2025-08-05T22:34:43.876Z" }, + { url = "https://files.pythonhosted.org/packages/75/72/778c4210d7918f1955a0388cd2c2f9131620e8a8939094aad0e24ff95583/pycares-4.10.0-cp312-cp312-manylinux_2_28_ppc64le.whl", hash = "sha256:08e3d70c714e3955dc5ccfe6abc132d2f410ca1c610375faee42fda6cc90ca0f", size = 687708, upload_time = "2025-08-05T22:34:45.365Z" }, + { url = "https://files.pythonhosted.org/packages/2e/35/10913ee20bb03210c3ac841448d143b83185bbbc3b611b33ea80126c9d26/pycares-4.10.0-cp312-cp312-manylinux_2_28_s390x.whl", hash = "sha256:f4f76946b1d6eae7bdbfefef0f143efb8acf5b55e37d631f7ec947fc9a8d6b06", size = 678317, upload_time = "2025-08-05T22:34:46.895Z" }, + { url = "https://files.pythonhosted.org/packages/0c/b4/6523d53a0644bb270c58f7186da0d80ce3f6fab481b27a9868f572044c03/pycares-4.10.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:cf99fbdb5f566320d5c1330e55de4f3cbe49ca42690b782db6380523bcfbb34b", size = 641024, upload_time = "2025-08-05T22:34:48.727Z" }, + { url = "https://files.pythonhosted.org/packages/77/dc/00b6c06343e74eb637eef4d0f774998c1bafaf2df42cc448624f04c31530/pycares-4.10.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ba103bbc7f85d0b7c386021cafed122317d05bee56c75c06c22707d8a0393a3d", size = 622312, upload_time = "2025-08-05T22:34:50.396Z" }, + { url = "https://files.pythonhosted.org/packages/2c/0f/0134aaa668f50d8cc3f322c9b2774773360647ceb081d1c3597546f9e002/pycares-4.10.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:6f0e546194fa64e751e70e16239f54fbf34ba216f4d3c7b55ca8ac50a5d82eb5", size = 670245, upload_time = "2025-08-05T22:34:52.239Z" }, + { url = "https://files.pythonhosted.org/packages/15/01/892aa72b16baababb7a54255344793c7943d439566fd6f554dc00fb6ee3a/pycares-4.10.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:5c32115f7004c1b9071c0f250c9092bacd9090bd31a289bd155d58a60d4434fa", size = 652913, upload_time = "2025-08-05T22:34:53.433Z" }, + { url = "https://files.pythonhosted.org/packages/01/cc/e0319118001fab9c00830a62ce7b1d6595825b43c12ac282860b2b48b7ba/pycares-4.10.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:259c9b6b4547e1400515a373c6910506f3cebe6e65bb9814be10e59c49dcb634", size = 629195, upload_time = "2025-08-05T22:34:54.521Z" }, + { url = "https://files.pythonhosted.org/packages/60/2c/5638e18ca83d9e42f005cf9dcebad9b24756aa55a62cdd63e860a366cf4e/pycares-4.10.0-cp312-cp312-win32.whl", hash = "sha256:f972732b3ce1300e6eec8670967920cae56b44df014fd63a793b990d930da64f", size = 118867, upload_time = "2025-08-05T22:34:55.563Z" }, + { url = "https://files.pythonhosted.org/packages/ca/31/5284d053c3c0b956c7f3b9f846dca108eaca97b1e1f0f8b7601c7e4fd238/pycares-4.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:489584abc1523d7e444b2b27a563d1c3c0c0852b40f3b453fa3a74cf10b38ebb", size = 144512, upload_time = "2025-08-05T22:34:57.375Z" }, + { url = "https://files.pythonhosted.org/packages/35/5f/bb1594ae9a8640bec69e953944145f86a621084c39debbd1904f3369a85b/pycares-4.10.0-cp312-cp312-win_arm64.whl", hash = "sha256:468aa3bb19e7f6f193ae5375d1b21722a0cad5726e17c9817bfefbcf29cd662e", size = 115647, upload_time = "2025-08-05T22:34:58.647Z" }, + { url = "https://files.pythonhosted.org/packages/21/bd/7a1448f5f0852628520dc9cdff21b4d6f01f4ab5faaf208d030fba28e0e2/pycares-4.10.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d4904ebd5e4d0c78e9fd56e6c974da005eaa721365961764922929e8e8f7dd0a", size = 145861, upload_time = "2025-08-05T22:35:00.01Z" }, + { url = "https://files.pythonhosted.org/packages/4d/6d/0e436ddb540a06fa898b8b6cd135babe44893d31d439935eee42bcd4f07b/pycares-4.10.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7144676e54b0686605333ec62ffdb7bb2b6cb4a6c53eed3e35ae3249dc64676b", size = 140893, upload_time = "2025-08-05T22:35:01.128Z" }, + { url = "https://files.pythonhosted.org/packages/22/7a/ec4734c1274205d0ac1419310464bfa5e1a96924a77312e760790c02769c/pycares-4.10.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:f9a259bf46cc51c51c7402a2bf32d1416f029b9a4af3de8b8973345520278092", size = 637754, upload_time = "2025-08-05T22:35:02.258Z" }, + { url = "https://files.pythonhosted.org/packages/12/1d/306d071837073eccff6efb93560fdb4e53d53ca0c1002260bb34e074f706/pycares-4.10.0-cp313-cp313-manylinux_2_28_ppc64le.whl", hash = "sha256:1dcfdda868ad2cee8d171288a4cd725a9ad67498a2f679428874a917396d464e", size = 687690, upload_time = "2025-08-05T22:35:03.623Z" }, + { url = "https://files.pythonhosted.org/packages/5e/e9/2b517302d42a9ff101201b58e9e2cbd2458c0a1ed68cca7d4dc1397ed246/pycares-4.10.0-cp313-cp313-manylinux_2_28_s390x.whl", hash = "sha256:f2d57bb27c884d130ac62d8c0ac57a158d27f8d75011f8700c7d44601f093652", size = 678273, upload_time = "2025-08-05T22:35:04.794Z" }, + { url = "https://files.pythonhosted.org/packages/2b/bd/de9ed896e752fb22141d6310f6680bcb62ea1d6aa07dc129d914377bd4b4/pycares-4.10.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:95f4d976bf2feb3f406aef6b1314845dc1384d2e4ea0c439c7d50631f2b6d166", size = 640968, upload_time = "2025-08-05T22:35:05.928Z" }, + { url = "https://files.pythonhosted.org/packages/07/9f/be45f60277a0825d03feed2378a283ce514b4feea64785e917b926b8441e/pycares-4.10.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f9eecd9e28e43254c6fb1c69518bd6b753bf18230579c23e7f272ac52036d41f", size = 622316, upload_time = "2025-08-05T22:35:07.058Z" }, + { url = "https://files.pythonhosted.org/packages/91/21/ca7bd328d07c560a1fe0ba29008c24a48e88184d3ade658946aeaef25992/pycares-4.10.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f4f8ec43ce0db38152cded6939a3fa4d8aba888e323803cda99f67fa3053fa15", size = 670246, upload_time = "2025-08-05T22:35:08.213Z" }, + { url = "https://files.pythonhosted.org/packages/01/56/47fda9dbc23c3acfe42fa6d57bb850db6ede65a2a9476641a54621166464/pycares-4.10.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:ef107d30a9d667c295db58897390c2d32c206eb1802b14d98ac643990be4e04f", size = 652930, upload_time = "2025-08-05T22:35:09.701Z" }, + { url = "https://files.pythonhosted.org/packages/86/30/cc865c630d5c9f72f488a89463aabfd33895984955c489f66b5a524f9573/pycares-4.10.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:56c843e69aad724dc5a795f32ebd6fec1d1592f58cabf89d2d148697c22c41be", size = 629187, upload_time = "2025-08-05T22:35:10.954Z" }, + { url = "https://files.pythonhosted.org/packages/92/88/3ff7be2a4bf5a400309d3ffaf9aa58596f7dc6f6fcb99f844fc5e4994a49/pycares-4.10.0-cp313-cp313-win32.whl", hash = "sha256:4310259be37b586ba8cd0b4983689e4c18e15e03709bd88b1076494e91ff424b", size = 118869, upload_time = "2025-08-05T22:35:12.375Z" }, + { url = "https://files.pythonhosted.org/packages/58/5f/cac05cee0556388cabd0abc332021ed01391d6be0685be7b5daff45088f6/pycares-4.10.0-cp313-cp313-win_amd64.whl", hash = "sha256:893020d802afb54d929afda5289fe322b50110cd5386080178479a7381241f97", size = 144512, upload_time = "2025-08-05T22:35:13.549Z" }, + { url = "https://files.pythonhosted.org/packages/45/2e/89b6e83a716935752d62a3c0622a077a9d28f7c2645b7f9b90d6951b37ba/pycares-4.10.0-cp313-cp313-win_arm64.whl", hash = "sha256:ffa3e0f7a13f287b575e64413f2f9af6cf9096e383d1fd40f2870591628d843b", size = 115648, upload_time = "2025-08-05T22:35:15.891Z" }, +] + +[[package]] +name = "pycparser" +version = "2.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", size = 172736, upload_time = "2024-03-30T13:22:22.564Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552, upload_time = "2024-03-30T13:22:20.476Z" }, +] + +[[package]] +name = "requests" +version = "2.32.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e1/0a/929373653770d8a0d7ea76c37de6e41f11eb07559b103b1c02cafb3f7cf8/requests-2.32.4.tar.gz", hash = "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422", size = 135258, upload_time = "2025-06-09T16:43:07.34Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/e4/56027c4a6b4ae70ca9de302488c5ca95ad4a39e190093d6c1a8ace08341b/requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c", size = 64847, upload_time = "2025-06-09T16:43:05.728Z" }, +] + [[package]] name = "rsa" version = "4.9.1" @@ -81,6 +515,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/64/8d/0133e4eb4beed9e425d9a98ed6e081a55d195481b7632472be1af08d2f6b/rsa-4.9.1-py3-none-any.whl", hash = "sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762", size = 34696, upload_time = "2025-04-16T09:51:17.142Z" }, ] +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload_time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload_time = "2024-12-04T17:35:26.475Z" }, +] + [[package]] name = "telethon" version = "1.40.0" @@ -93,3 +536,114 @@ sdist = { url = "https://files.pythonhosted.org/packages/58/af/9b7111e3f63fffe8e wheels = [ { url = "https://files.pythonhosted.org/packages/2c/5a/c5370edb3215d19a6e858f4169b8eec725ba55f9d39df0f557508048c037/Telethon-1.40.0-py3-none-any.whl", hash = "sha256:146fd4cb2a7afa66bc67f9c2167756096a37b930f65711a3e7399ec9874dcfa7", size = 722013, upload_time = "2025-04-21T09:12:08.399Z" }, ] + +[[package]] +name = "typing-extensions" +version = "4.14.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/98/5a/da40306b885cc8c09109dc2e1abd358d5684b1425678151cdaed4731c822/typing_extensions-4.14.1.tar.gz", hash = "sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36", size = 107673, upload_time = "2025-07-04T13:28:34.16Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b5/00/d631e67a838026495268c2f6884f3711a15a9a2a96cd244fdaea53b823fb/typing_extensions-4.14.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76", size = 43906, upload_time = "2025-07-04T13:28:32.743Z" }, +] + +[[package]] +name = "uplink" +version = "0.10.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "requests" }, + { name = "six" }, + { name = "uritemplate" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/72/e1/2e7d405f1bdef3dc52af7000d7299370fbd12eb2f9e2e30efde24bb9c945/uplink-0.10.0.tar.gz", hash = "sha256:a3b76b1cac5394126a72698d72b209bb80c8a94bad091870e463919979e4ab63", size = 210185, upload_time = "2025-06-14T20:24:08.708Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/5b/e22e345aff4ffcaa6d68d038f512ec1167ab7d2ec4d2b6ec8a0da7800fbd/uplink-0.10.0-py3-none-any.whl", hash = "sha256:03212163f8a83a608480ec15122884988eb82cc7a2368b9072d9af8ede2246d9", size = 71425, upload_time = "2025-06-14T20:24:07.779Z" }, +] + +[package.optional-dependencies] +aiohttp = [ + { name = "aiohttp" }, +] + +[[package]] +name = "uritemplate" +version = "4.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/98/60/f174043244c5306c9988380d2cb10009f91563fc4b31293d27e17201af56/uritemplate-4.2.0.tar.gz", hash = "sha256:480c2ed180878955863323eea31b0ede668795de182617fef9c6ca09e6ec9d0e", size = 33267, upload_time = "2025-06-02T15:12:06.318Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a9/99/3ae339466c9183ea5b8ae87b34c0b897eda475d2aec2307cae60e5cd4f29/uritemplate-4.2.0-py3-none-any.whl", hash = "sha256:962201ba1c4edcab02e60f9a0d3821e82dfc5d2d6662a21abd533879bdb8a686", size = 11488, upload_time = "2025-06-02T15:12:03.405Z" }, +] + +[[package]] +name = "urllib3" +version = "2.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185, upload_time = "2025-06-18T14:07:41.644Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload_time = "2025-06-18T14:07:40.39Z" }, +] + +[[package]] +name = "yarl" +version = "1.20.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "multidict" }, + { name = "propcache" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3c/fb/efaa23fa4e45537b827620f04cf8f3cd658b76642205162e072703a5b963/yarl-1.20.1.tar.gz", hash = "sha256:d017a4997ee50c91fd5466cef416231bb82177b93b029906cefc542ce14c35ac", size = 186428, upload_time = "2025-06-10T00:46:09.923Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5f/9a/cb7fad7d73c69f296eda6815e4a2c7ed53fc70c2f136479a91c8e5fbdb6d/yarl-1.20.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdcc4cd244e58593a4379fe60fdee5ac0331f8eb70320a24d591a3be197b94a9", size = 133667, upload_time = "2025-06-10T00:43:44.369Z" }, + { url = "https://files.pythonhosted.org/packages/67/38/688577a1cb1e656e3971fb66a3492501c5a5df56d99722e57c98249e5b8a/yarl-1.20.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b29a2c385a5f5b9c7d9347e5812b6f7ab267193c62d282a540b4fc528c8a9d2a", size = 91025, upload_time = "2025-06-10T00:43:46.295Z" }, + { url = "https://files.pythonhosted.org/packages/50/ec/72991ae51febeb11a42813fc259f0d4c8e0507f2b74b5514618d8b640365/yarl-1.20.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1112ae8154186dfe2de4732197f59c05a83dc814849a5ced892b708033f40dc2", size = 89709, upload_time = "2025-06-10T00:43:48.22Z" }, + { url = "https://files.pythonhosted.org/packages/99/da/4d798025490e89426e9f976702e5f9482005c548c579bdae792a4c37769e/yarl-1.20.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90bbd29c4fe234233f7fa2b9b121fb63c321830e5d05b45153a2ca68f7d310ee", size = 352287, upload_time = "2025-06-10T00:43:49.924Z" }, + { url = "https://files.pythonhosted.org/packages/1a/26/54a15c6a567aac1c61b18aa0f4b8aa2e285a52d547d1be8bf48abe2b3991/yarl-1.20.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:680e19c7ce3710ac4cd964e90dad99bf9b5029372ba0c7cbfcd55e54d90ea819", size = 345429, upload_time = "2025-06-10T00:43:51.7Z" }, + { url = "https://files.pythonhosted.org/packages/d6/95/9dcf2386cb875b234353b93ec43e40219e14900e046bf6ac118f94b1e353/yarl-1.20.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4a979218c1fdb4246a05efc2cc23859d47c89af463a90b99b7c56094daf25a16", size = 365429, upload_time = "2025-06-10T00:43:53.494Z" }, + { url = "https://files.pythonhosted.org/packages/91/b2/33a8750f6a4bc224242a635f5f2cff6d6ad5ba651f6edcccf721992c21a0/yarl-1.20.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:255b468adf57b4a7b65d8aad5b5138dce6a0752c139965711bdcb81bc370e1b6", size = 363862, upload_time = "2025-06-10T00:43:55.766Z" }, + { url = "https://files.pythonhosted.org/packages/98/28/3ab7acc5b51f4434b181b0cee8f1f4b77a65919700a355fb3617f9488874/yarl-1.20.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a97d67108e79cfe22e2b430d80d7571ae57d19f17cda8bb967057ca8a7bf5bfd", size = 355616, upload_time = "2025-06-10T00:43:58.056Z" }, + { url = "https://files.pythonhosted.org/packages/36/a3/f666894aa947a371724ec7cd2e5daa78ee8a777b21509b4252dd7bd15e29/yarl-1.20.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8570d998db4ddbfb9a590b185a0a33dbf8aafb831d07a5257b4ec9948df9cb0a", size = 339954, upload_time = "2025-06-10T00:43:59.773Z" }, + { url = "https://files.pythonhosted.org/packages/f1/81/5f466427e09773c04219d3450d7a1256138a010b6c9f0af2d48565e9ad13/yarl-1.20.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:97c75596019baae7c71ccf1d8cc4738bc08134060d0adfcbe5642f778d1dca38", size = 365575, upload_time = "2025-06-10T00:44:02.051Z" }, + { url = "https://files.pythonhosted.org/packages/2e/e3/e4b0ad8403e97e6c9972dd587388940a032f030ebec196ab81a3b8e94d31/yarl-1.20.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:1c48912653e63aef91ff988c5432832692ac5a1d8f0fb8a33091520b5bbe19ef", size = 365061, upload_time = "2025-06-10T00:44:04.196Z" }, + { url = "https://files.pythonhosted.org/packages/ac/99/b8a142e79eb86c926f9f06452eb13ecb1bb5713bd01dc0038faf5452e544/yarl-1.20.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4c3ae28f3ae1563c50f3d37f064ddb1511ecc1d5584e88c6b7c63cf7702a6d5f", size = 364142, upload_time = "2025-06-10T00:44:06.527Z" }, + { url = "https://files.pythonhosted.org/packages/34/f2/08ed34a4a506d82a1a3e5bab99ccd930a040f9b6449e9fd050320e45845c/yarl-1.20.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c5e9642f27036283550f5f57dc6156c51084b458570b9d0d96100c8bebb186a8", size = 381894, upload_time = "2025-06-10T00:44:08.379Z" }, + { url = "https://files.pythonhosted.org/packages/92/f8/9a3fbf0968eac704f681726eff595dce9b49c8a25cd92bf83df209668285/yarl-1.20.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:2c26b0c49220d5799f7b22c6838409ee9bc58ee5c95361a4d7831f03cc225b5a", size = 383378, upload_time = "2025-06-10T00:44:10.51Z" }, + { url = "https://files.pythonhosted.org/packages/af/85/9363f77bdfa1e4d690957cd39d192c4cacd1c58965df0470a4905253b54f/yarl-1.20.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:564ab3d517e3d01c408c67f2e5247aad4019dcf1969982aba3974b4093279004", size = 374069, upload_time = "2025-06-10T00:44:12.834Z" }, + { url = "https://files.pythonhosted.org/packages/35/99/9918c8739ba271dcd935400cff8b32e3cd319eaf02fcd023d5dcd487a7c8/yarl-1.20.1-cp312-cp312-win32.whl", hash = "sha256:daea0d313868da1cf2fac6b2d3a25c6e3a9e879483244be38c8e6a41f1d876a5", size = 81249, upload_time = "2025-06-10T00:44:14.731Z" }, + { url = "https://files.pythonhosted.org/packages/eb/83/5d9092950565481b413b31a23e75dd3418ff0a277d6e0abf3729d4d1ce25/yarl-1.20.1-cp312-cp312-win_amd64.whl", hash = "sha256:48ea7d7f9be0487339828a4de0360d7ce0efc06524a48e1810f945c45b813698", size = 86710, upload_time = "2025-06-10T00:44:16.716Z" }, + { url = "https://files.pythonhosted.org/packages/8a/e1/2411b6d7f769a07687acee88a062af5833cf1966b7266f3d8dfb3d3dc7d3/yarl-1.20.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:0b5ff0fbb7c9f1b1b5ab53330acbfc5247893069e7716840c8e7d5bb7355038a", size = 131811, upload_time = "2025-06-10T00:44:18.933Z" }, + { url = "https://files.pythonhosted.org/packages/b2/27/584394e1cb76fb771371770eccad35de400e7b434ce3142c2dd27392c968/yarl-1.20.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:14f326acd845c2b2e2eb38fb1346c94f7f3b01a4f5c788f8144f9b630bfff9a3", size = 90078, upload_time = "2025-06-10T00:44:20.635Z" }, + { url = "https://files.pythonhosted.org/packages/bf/9a/3246ae92d4049099f52d9b0fe3486e3b500e29b7ea872d0f152966fc209d/yarl-1.20.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f60e4ad5db23f0b96e49c018596707c3ae89f5d0bd97f0ad3684bcbad899f1e7", size = 88748, upload_time = "2025-06-10T00:44:22.34Z" }, + { url = "https://files.pythonhosted.org/packages/a3/25/35afe384e31115a1a801fbcf84012d7a066d89035befae7c5d4284df1e03/yarl-1.20.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:49bdd1b8e00ce57e68ba51916e4bb04461746e794e7c4d4bbc42ba2f18297691", size = 349595, upload_time = "2025-06-10T00:44:24.314Z" }, + { url = "https://files.pythonhosted.org/packages/28/2d/8aca6cb2cabc8f12efcb82749b9cefecbccfc7b0384e56cd71058ccee433/yarl-1.20.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:66252d780b45189975abfed839616e8fd2dbacbdc262105ad7742c6ae58f3e31", size = 342616, upload_time = "2025-06-10T00:44:26.167Z" }, + { url = "https://files.pythonhosted.org/packages/0b/e9/1312633d16b31acf0098d30440ca855e3492d66623dafb8e25b03d00c3da/yarl-1.20.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:59174e7332f5d153d8f7452a102b103e2e74035ad085f404df2e40e663a22b28", size = 361324, upload_time = "2025-06-10T00:44:27.915Z" }, + { url = "https://files.pythonhosted.org/packages/bc/a0/688cc99463f12f7669eec7c8acc71ef56a1521b99eab7cd3abb75af887b0/yarl-1.20.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e3968ec7d92a0c0f9ac34d5ecfd03869ec0cab0697c91a45db3fbbd95fe1b653", size = 359676, upload_time = "2025-06-10T00:44:30.041Z" }, + { url = "https://files.pythonhosted.org/packages/af/44/46407d7f7a56e9a85a4c207724c9f2c545c060380718eea9088f222ba697/yarl-1.20.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d1a4fbb50e14396ba3d375f68bfe02215d8e7bc3ec49da8341fe3157f59d2ff5", size = 352614, upload_time = "2025-06-10T00:44:32.171Z" }, + { url = "https://files.pythonhosted.org/packages/b1/91/31163295e82b8d5485d31d9cf7754d973d41915cadce070491778d9c9825/yarl-1.20.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:11a62c839c3a8eac2410e951301309426f368388ff2f33799052787035793b02", size = 336766, upload_time = "2025-06-10T00:44:34.494Z" }, + { url = "https://files.pythonhosted.org/packages/b4/8e/c41a5bc482121f51c083c4c2bcd16b9e01e1cf8729e380273a952513a21f/yarl-1.20.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:041eaa14f73ff5a8986b4388ac6bb43a77f2ea09bf1913df7a35d4646db69e53", size = 364615, upload_time = "2025-06-10T00:44:36.856Z" }, + { url = "https://files.pythonhosted.org/packages/e3/5b/61a3b054238d33d70ea06ebba7e58597891b71c699e247df35cc984ab393/yarl-1.20.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:377fae2fef158e8fd9d60b4c8751387b8d1fb121d3d0b8e9b0be07d1b41e83dc", size = 360982, upload_time = "2025-06-10T00:44:39.141Z" }, + { url = "https://files.pythonhosted.org/packages/df/a3/6a72fb83f8d478cb201d14927bc8040af901811a88e0ff2da7842dd0ed19/yarl-1.20.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:1c92f4390e407513f619d49319023664643d3339bd5e5a56a3bebe01bc67ec04", size = 369792, upload_time = "2025-06-10T00:44:40.934Z" }, + { url = "https://files.pythonhosted.org/packages/7c/af/4cc3c36dfc7c077f8dedb561eb21f69e1e9f2456b91b593882b0b18c19dc/yarl-1.20.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:d25ddcf954df1754ab0f86bb696af765c5bfaba39b74095f27eececa049ef9a4", size = 382049, upload_time = "2025-06-10T00:44:42.854Z" }, + { url = "https://files.pythonhosted.org/packages/19/3a/e54e2c4752160115183a66dc9ee75a153f81f3ab2ba4bf79c3c53b33de34/yarl-1.20.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:909313577e9619dcff8c31a0ea2aa0a2a828341d92673015456b3ae492e7317b", size = 384774, upload_time = "2025-06-10T00:44:45.275Z" }, + { url = "https://files.pythonhosted.org/packages/9c/20/200ae86dabfca89060ec6447649f219b4cbd94531e425e50d57e5f5ac330/yarl-1.20.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:793fd0580cb9664548c6b83c63b43c477212c0260891ddf86809e1c06c8b08f1", size = 374252, upload_time = "2025-06-10T00:44:47.31Z" }, + { url = "https://files.pythonhosted.org/packages/83/75/11ee332f2f516b3d094e89448da73d557687f7d137d5a0f48c40ff211487/yarl-1.20.1-cp313-cp313-win32.whl", hash = "sha256:468f6e40285de5a5b3c44981ca3a319a4b208ccc07d526b20b12aeedcfa654b7", size = 81198, upload_time = "2025-06-10T00:44:49.164Z" }, + { url = "https://files.pythonhosted.org/packages/ba/ba/39b1ecbf51620b40ab402b0fc817f0ff750f6d92712b44689c2c215be89d/yarl-1.20.1-cp313-cp313-win_amd64.whl", hash = "sha256:495b4ef2fea40596bfc0affe3837411d6aa3371abcf31aac0ccc4bdd64d4ef5c", size = 86346, upload_time = "2025-06-10T00:44:51.182Z" }, + { url = "https://files.pythonhosted.org/packages/43/c7/669c52519dca4c95153c8ad96dd123c79f354a376346b198f438e56ffeb4/yarl-1.20.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:f60233b98423aab21d249a30eb27c389c14929f47be8430efa7dbd91493a729d", size = 138826, upload_time = "2025-06-10T00:44:52.883Z" }, + { url = "https://files.pythonhosted.org/packages/6a/42/fc0053719b44f6ad04a75d7f05e0e9674d45ef62f2d9ad2c1163e5c05827/yarl-1.20.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:6f3eff4cc3f03d650d8755c6eefc844edde99d641d0dcf4da3ab27141a5f8ddf", size = 93217, upload_time = "2025-06-10T00:44:54.658Z" }, + { url = "https://files.pythonhosted.org/packages/4f/7f/fa59c4c27e2a076bba0d959386e26eba77eb52ea4a0aac48e3515c186b4c/yarl-1.20.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:69ff8439d8ba832d6bed88af2c2b3445977eba9a4588b787b32945871c2444e3", size = 92700, upload_time = "2025-06-10T00:44:56.784Z" }, + { url = "https://files.pythonhosted.org/packages/2f/d4/062b2f48e7c93481e88eff97a6312dca15ea200e959f23e96d8ab898c5b8/yarl-1.20.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3cf34efa60eb81dd2645a2e13e00bb98b76c35ab5061a3989c7a70f78c85006d", size = 347644, upload_time = "2025-06-10T00:44:59.071Z" }, + { url = "https://files.pythonhosted.org/packages/89/47/78b7f40d13c8f62b499cc702fdf69e090455518ae544c00a3bf4afc9fc77/yarl-1.20.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8e0fe9364ad0fddab2688ce72cb7a8e61ea42eff3c7caeeb83874a5d479c896c", size = 323452, upload_time = "2025-06-10T00:45:01.605Z" }, + { url = "https://files.pythonhosted.org/packages/eb/2b/490d3b2dc66f52987d4ee0d3090a147ea67732ce6b4d61e362c1846d0d32/yarl-1.20.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8f64fbf81878ba914562c672024089e3401974a39767747691c65080a67b18c1", size = 346378, upload_time = "2025-06-10T00:45:03.946Z" }, + { url = "https://files.pythonhosted.org/packages/66/ad/775da9c8a94ce925d1537f939a4f17d782efef1f973039d821cbe4bcc211/yarl-1.20.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f6342d643bf9a1de97e512e45e4b9560a043347e779a173250824f8b254bd5ce", size = 353261, upload_time = "2025-06-10T00:45:05.992Z" }, + { url = "https://files.pythonhosted.org/packages/4b/23/0ed0922b47a4f5c6eb9065d5ff1e459747226ddce5c6a4c111e728c9f701/yarl-1.20.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:56dac5f452ed25eef0f6e3c6a066c6ab68971d96a9fb441791cad0efba6140d3", size = 335987, upload_time = "2025-06-10T00:45:08.227Z" }, + { url = "https://files.pythonhosted.org/packages/3e/49/bc728a7fe7d0e9336e2b78f0958a2d6b288ba89f25a1762407a222bf53c3/yarl-1.20.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7d7f497126d65e2cad8dc5f97d34c27b19199b6414a40cb36b52f41b79014be", size = 329361, upload_time = "2025-06-10T00:45:10.11Z" }, + { url = "https://files.pythonhosted.org/packages/93/8f/b811b9d1f617c83c907e7082a76e2b92b655400e61730cd61a1f67178393/yarl-1.20.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:67e708dfb8e78d8a19169818eeb5c7a80717562de9051bf2413aca8e3696bf16", size = 346460, upload_time = "2025-06-10T00:45:12.055Z" }, + { url = "https://files.pythonhosted.org/packages/70/fd/af94f04f275f95da2c3b8b5e1d49e3e79f1ed8b6ceb0f1664cbd902773ff/yarl-1.20.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:595c07bc79af2494365cc96ddeb772f76272364ef7c80fb892ef9d0649586513", size = 334486, upload_time = "2025-06-10T00:45:13.995Z" }, + { url = "https://files.pythonhosted.org/packages/84/65/04c62e82704e7dd0a9b3f61dbaa8447f8507655fd16c51da0637b39b2910/yarl-1.20.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:7bdd2f80f4a7df852ab9ab49484a4dee8030023aa536df41f2d922fd57bf023f", size = 342219, upload_time = "2025-06-10T00:45:16.479Z" }, + { url = "https://files.pythonhosted.org/packages/91/95/459ca62eb958381b342d94ab9a4b6aec1ddec1f7057c487e926f03c06d30/yarl-1.20.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:c03bfebc4ae8d862f853a9757199677ab74ec25424d0ebd68a0027e9c639a390", size = 350693, upload_time = "2025-06-10T00:45:18.399Z" }, + { url = "https://files.pythonhosted.org/packages/a6/00/d393e82dd955ad20617abc546a8f1aee40534d599ff555ea053d0ec9bf03/yarl-1.20.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:344d1103e9c1523f32a5ed704d576172d2cabed3122ea90b1d4e11fe17c66458", size = 355803, upload_time = "2025-06-10T00:45:20.677Z" }, + { url = "https://files.pythonhosted.org/packages/9e/ed/c5fb04869b99b717985e244fd93029c7a8e8febdfcffa06093e32d7d44e7/yarl-1.20.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:88cab98aa4e13e1ade8c141daeedd300a4603b7132819c484841bb7af3edce9e", size = 341709, upload_time = "2025-06-10T00:45:23.221Z" }, + { url = "https://files.pythonhosted.org/packages/24/fd/725b8e73ac2a50e78a4534ac43c6addf5c1c2d65380dd48a9169cc6739a9/yarl-1.20.1-cp313-cp313t-win32.whl", hash = "sha256:b121ff6a7cbd4abc28985b6028235491941b9fe8fe226e6fdc539c977ea1739d", size = 86591, upload_time = "2025-06-10T00:45:25.793Z" }, + { url = "https://files.pythonhosted.org/packages/94/c3/b2e9f38bc3e11191981d57ea08cab2166e74ea770024a646617c9cddd9f6/yarl-1.20.1-cp313-cp313t-win_amd64.whl", hash = "sha256:541d050a355bbbc27e55d906bc91cb6fe42f96c01413dd0f4ed5a5240513874f", size = 93003, upload_time = "2025-06-10T00:45:27.752Z" }, + { url = "https://files.pythonhosted.org/packages/b4/2d/2345fce04cfd4bee161bf1e7d9cdc702e3e16109021035dbb24db654a622/yarl-1.20.1-py3-none-any.whl", hash = "sha256:83b8eb083fe4683c6115795d9fc1cfaf2cbbefb19b3a1cb68f6527460f483a77", size = 46542, upload_time = "2025-06-10T00:46:07.521Z" }, +] From 5f9952314d34441d905b99c7aeaa55d1a1345a7f Mon Sep 17 00:00:00 2001 From: Alex Tau Date: Fri, 15 Aug 2025 18:02:43 +0300 Subject: [PATCH 32/56] try to use OK alerts to reflect successful checks --- modules/options.nix | 5 ++++- src/lego_monitoring/__init__.py | 4 ++-- src/lego_monitoring/alerting/commands.py | 4 +++- src/lego_monitoring/alerting/enum.py | 2 +- src/lego_monitoring/alerting/sender.py | 6 +++--- src/lego_monitoring/checks/temp/__init__.py | 2 ++ src/lego_monitoring/core/checkers.py | 14 ++++++-------- src/lego_monitoring/core/cvars.py | 2 +- 8 files changed, 22 insertions(+), 17 deletions(-) diff --git a/modules/options.nix b/modules/options.nix index 71be81c..8af2ab5 100644 --- a/modules/options.nix +++ b/modules/options.nix @@ -80,7 +80,10 @@ in Specify `default` as the slug to use this key for check types that don't have a key explicitly assigned to them. If you are unsure of the exact slug a check will generate, it is recommended to try it out with the default key first, before - assigning a specific one.''; + assigning a specific one. + + **Note**: checks will be auto-provisioned, but correct intervals and grace periods have to be configured manually from the web console, + otherwise silent failures will not be recorded until after 1 day (the default healthchecks interval).''; }; }; }; diff --git a/src/lego_monitoring/__init__.py b/src/lego_monitoring/__init__.py index 8da27fe..488bf1a 100644 --- a/src/lego_monitoring/__init__.py +++ b/src/lego_monitoring/__init__.py @@ -96,17 +96,17 @@ async def async_main(): command_manager = CommandHandlerManager(checkers) await command_manager.attach_handlers(tg_client) - cvars.tg_client.set(tg_client) else: logging.info("Telegram integration is disabled") tg_client = None + cvars.tg_client.set(tg_client) + if config.alert_channels.healthchecks is not None: healthchecks_client = sender.get_healthchecks_client() logging.info("Ready to send pings to healthchecks") cvars.healthchecks_client.set(healthchecks_client) else: - healthchecks_client = None logging.info("Healthchecks integration is disabled") signal.signal(signal.SIGTERM, stop_gracefully) diff --git a/src/lego_monitoring/alerting/commands.py b/src/lego_monitoring/alerting/commands.py index 96491a0..f87f562 100644 --- a/src/lego_monitoring/alerting/commands.py +++ b/src/lego_monitoring/alerting/commands.py @@ -56,7 +56,7 @@ class CommandHandlerManager: if not isinstance(c, BaseChecker) or not c.persistent: continue for a in c.current_alerts: - if a.alert_type not in alert_num_by_state_with_max_type: + if a.alert_type not in alert_num_by_state_with_max_type and a.severity != Severity.OK: alert_num_by_state_with_max_type[a.alert_type] = [a.severity, 1] else: existing_list = alert_num_by_state_with_max_type[a.alert_type] @@ -80,6 +80,8 @@ class CommandHandlerManager: if not isinstance(c, BaseChecker) or not c.persistent: continue for a in c.current_alerts: + if a.severity == Severity.OK: + continue message = format_message(a, note="ongoing") messages.add(message) if len(messages) == 0: diff --git a/src/lego_monitoring/alerting/enum.py b/src/lego_monitoring/alerting/enum.py index d014145..b79abff 100644 --- a/src/lego_monitoring/alerting/enum.py +++ b/src/lego_monitoring/alerting/enum.py @@ -22,7 +22,7 @@ class AlertType(StrEnum): class Severity(IntEnum): - OK = 0 # should only be used when persistent alerts resolve + OK = 0 INFO = 1 WARNING = 2 CRITICAL = 3 diff --git a/src/lego_monitoring/alerting/sender.py b/src/lego_monitoring/alerting/sender.py index 534fcf3..b9508f9 100644 --- a/src/lego_monitoring/alerting/sender.py +++ b/src/lego_monitoring/alerting/sender.py @@ -38,7 +38,7 @@ def format_message(alert: Alert, note: str) -> str: async def send_alert(alert: Alert, note: str = "") -> None: try: - client = cvars.tg_client.get() + tg_client = cvars.tg_client.get() except LookupError: # being called standalone # cvars.config.set(get_config()) # temp_client = True @@ -47,10 +47,10 @@ async def send_alert(alert: Alert, note: str = "") -> None: raise NotImplementedError # TODO else: ... # temp_client = False - if client is not None: + if tg_client is not None: room_id = cvars.config.get().alert_channels.telegram.room_id message = format_message(alert, note) - await client.send_message(entity=room_id, message=message) + await tg_client.send_message(entity=room_id, message=message) # if temp_client: # await client.close() diff --git a/src/lego_monitoring/checks/temp/__init__.py b/src/lego_monitoring/checks/temp/__init__.py index 6322a72..4da183a 100644 --- a/src/lego_monitoring/checks/temp/__init__.py +++ b/src/lego_monitoring/checks/temp/__init__.py @@ -26,4 +26,6 @@ def temp_check() -> list[Alert]: else: continue alert_list.append(alert) + if len(alert_list) == 0: + alert_list.append(Alert(alert_type=AlertType.TEMP, message="All sensors nominal", severity=Severity.OK)) return alert_list diff --git a/src/lego_monitoring/core/checkers.py b/src/lego_monitoring/core/checkers.py index 5000ee7..ccb6c41 100644 --- a/src/lego_monitoring/core/checkers.py +++ b/src/lego_monitoring/core/checkers.py @@ -24,7 +24,7 @@ class BaseChecker: """ False: this persistent checker only emits messages when its max alert severity is changed - True: this persistent checker emits messages every times it checks + True: this persistent checker emits messages every times it checks and any non-OK alerts are present Has no effect if persistent == False """ @@ -65,17 +65,15 @@ class BaseChecker: async def _handle_alerts(self, alerts: list[Alert]) -> None: if not self.persistent: for alert in alerts: - await send_alert(alert, "ongoing" if self.is_reminder else "") + if alert.severity != Severity.OK: + await send_alert(alert, "ongoing" if self.is_reminder else "") return - old_types = self.current_alerts.get_types() old_severity, new_severity = self.current_alerts.update(alerts) - new_types = self.current_alerts.get_types() - if old_severity != new_severity or self.send_any_state: + if (old_severity != new_severity or self.send_any_state) and not ( + old_severity == None and new_severity == Severity.OK + ): for alert in alerts: await send_alert(alert, note="ongoing") - for alert_type in old_types - new_types: - alert = Alert(alert_type=alert_type, message="Situation resolved", severity=Severity.OK) - await send_alert(alert) async def run_checker(self) -> None: raise NotImplementedError diff --git a/src/lego_monitoring/core/cvars.py b/src/lego_monitoring/core/cvars.py index 65dc6c0..1abaefb 100644 --- a/src/lego_monitoring/core/cvars.py +++ b/src/lego_monitoring/core/cvars.py @@ -9,6 +9,6 @@ from lego_monitoring.alerting.current import CurrentAlerts from ..config import Config config: ContextVar[Config] = ContextVar("config") -tg_client: ContextVar[Optional[TelegramClient]] = ContextVar("tg_client", default=None) +tg_client: ContextVar[Optional[TelegramClient]] = ContextVar("tg_client") healthchecks_client: ContextVar[Optional[HealthchecksClient]] = ContextVar("healthchecks_client", default=None) current_alerts: ContextVar[list[CurrentAlerts]] = ContextVar("current_alerts", default=[]) From be7b3dbeedee32a5e47c849a36a9c0e60a87ec6e Mon Sep 17 00:00:00 2001 From: Alex Tau Date: Fri, 15 Aug 2025 18:15:13 +0300 Subject: [PATCH 33/56] adapt rest of checks to use OK alerts --- src/lego_monitoring/checks/cpu.py | 2 +- src/lego_monitoring/checks/net.py | 9 +++++++-- src/lego_monitoring/checks/ram.py | 4 +++- src/lego_monitoring/checks/temp/__init__.py | 9 ++++++--- src/lego_monitoring/checks/vulnix/__init__.py | 2 ++ 5 files changed, 19 insertions(+), 7 deletions(-) diff --git a/src/lego_monitoring/checks/cpu.py b/src/lego_monitoring/checks/cpu.py index dc19ecf..594c1d6 100644 --- a/src/lego_monitoring/checks/cpu.py +++ b/src/lego_monitoring/checks/cpu.py @@ -27,4 +27,4 @@ def cpu_check() -> list[Alert]: ) ] else: - return [] + return [Alert(alert_type=AlertType.CPU, message=f"CPU load: {percentage:.2f}% (nominal)", severity=Severity.OK)] diff --git a/src/lego_monitoring/checks/net.py b/src/lego_monitoring/checks/net.py index 62c59c8..073bd8f 100644 --- a/src/lego_monitoring/checks/net.py +++ b/src/lego_monitoring/checks/net.py @@ -25,8 +25,8 @@ class NetIOTracker: stat_name: str, interface: str, ) -> Optional[Alert]: + current_stat_natural = naturalsize(current_stat_bytes_per_sec, binary=True) if critical_threshold and (IS_TESTING or current_stat_bytes_per_sec >= critical_threshold): - current_stat_natural = naturalsize(current_stat_bytes_per_sec, binary=True) critical_threshold_natural = naturalsize(critical_threshold, binary=True) return Alert( alert_type=AlertType.NET, @@ -34,13 +34,18 @@ class NetIOTracker: severity=Severity.CRITICAL, ) elif warning_threshold and (IS_TESTING or current_stat_bytes_per_sec >= warning_threshold): - current_stat_natural = naturalsize(current_stat_bytes_per_sec, binary=True) warning_threshold_natural = naturalsize(warning_threshold, binary=True) return Alert( alert_type=AlertType.NET, message=f"Interface {interface} {stat_name} {current_stat_natural}/s >= {warning_threshold_natural}/s", severity=Severity.WARNING, ) + else: + return Alert( + alert_type=AlertType.NET, + message=f"Interface {interface} {stat_name} {current_stat_natural}/s (nominal)", + severity=Severity.OK, + ) def net_check(self) -> list[Alert]: alerts = [] diff --git a/src/lego_monitoring/checks/ram.py b/src/lego_monitoring/checks/ram.py index f1eb40d..e91c891 100644 --- a/src/lego_monitoring/checks/ram.py +++ b/src/lego_monitoring/checks/ram.py @@ -27,4 +27,6 @@ def ram_check() -> list[Alert]: ) ] else: - return [] + return [ + Alert(alert_type=AlertType.RAM, message=f"RAM usage: {percentage:.2f}% (nominal)", severity=Severity.OK) + ] diff --git a/src/lego_monitoring/checks/temp/__init__.py b/src/lego_monitoring/checks/temp/__init__.py index 4da183a..e461148 100644 --- a/src/lego_monitoring/checks/temp/__init__.py +++ b/src/lego_monitoring/checks/temp/__init__.py @@ -24,8 +24,11 @@ def temp_check() -> list[Alert]: severity=Severity.WARNING, ) else: - continue + alert = Alert( + alert_type=AlertType.TEMP, + message=f"{sensor} {r.label}: {r.current_temp}°C (nominal)", + severity=Severity.OK, + ) alert_list.append(alert) - if len(alert_list) == 0: - alert_list.append(Alert(alert_type=AlertType.TEMP, message="All sensors nominal", severity=Severity.OK)) + return alert_list diff --git a/src/lego_monitoring/checks/vulnix/__init__.py b/src/lego_monitoring/checks/vulnix/__init__.py index 9fefcff..cd43c57 100644 --- a/src/lego_monitoring/checks/vulnix/__init__.py +++ b/src/lego_monitoring/checks/vulnix/__init__.py @@ -50,5 +50,7 @@ async def vulnix_check() -> list[Alert]: if IS_TESTING: alert_list[0].message += "\n(just testing)" return [alert_list[0]] + elif len(alert_list) == 0: + return [Alert(AlertType.VULN, message="No vulnerabilities found", severity=Severity.OK)] else: return alert_list From 5c57e1765e7c8cadb38777c53cc97c0d2d8bd5bb Mon Sep 17 00:00:00 2001 From: Alex Tau Date: Fri, 15 Aug 2025 19:22:58 +0300 Subject: [PATCH 34/56] add slugs to checks, enabling sending them to healthchecks --- src/lego_monitoring/alerting/sender.py | 25 ++++++++++++++++--- src/lego_monitoring/checks/cpu.py | 16 +++++++++++- src/lego_monitoring/checks/net.py | 9 +++++++ src/lego_monitoring/checks/ram.py | 14 ++++++++++- src/lego_monitoring/checks/temp/__init__.py | 9 +++++++ src/lego_monitoring/checks/utils.py | 5 ++++ src/lego_monitoring/checks/vulnix/__init__.py | 11 +++++++- src/lego_monitoring/core/checkers.py | 5 +++- 8 files changed, 87 insertions(+), 7 deletions(-) create mode 100644 src/lego_monitoring/checks/utils.py diff --git a/src/lego_monitoring/alerting/sender.py b/src/lego_monitoring/alerting/sender.py index b9508f9..edd35c0 100644 --- a/src/lego_monitoring/alerting/sender.py +++ b/src/lego_monitoring/alerting/sender.py @@ -54,9 +54,28 @@ async def send_alert(alert: Alert, note: str = "") -> None: # if temp_client: # await client.close() - # TODO ping healthchecks if enabled - if alert.healthchecks_slug is not None: - raise NotImplementedError + +async def send_healthchecks_status(alert: Alert) -> None: + def get_pinging_key(keys: dict[str, str]): + if alert.healthchecks_slug in keys: + return keys[alert.healthchecks_slug] + else: + return keys["default"] + + if alert.healthchecks_slug is None: + return + try: + hc_client = cvars.healthchecks_client.get() + except LookupError: + raise NotImplementedError # TODO + if hc_client is None: + return + config = cvars.config.get() + key = get_pinging_key(config.alert_channels.healthchecks.pinging_keys) + if alert.severity == Severity.OK: + await hc_client.success(key, alert.healthchecks_slug, create=True, log=alert.plain_message) + else: + await hc_client.failure(key, alert.healthchecks_slug, create=True, log=alert.plain_message) # TODO service itself has to be monitored like everything else - with regular pinging - if we're diff --git a/src/lego_monitoring/checks/cpu.py b/src/lego_monitoring/checks/cpu.py index 594c1d6..f31cb7b 100644 --- a/src/lego_monitoring/checks/cpu.py +++ b/src/lego_monitoring/checks/cpu.py @@ -1,21 +1,27 @@ +from socket import gethostname + from psutil import cpu_percent from lego_monitoring.alerting.alert import Alert from lego_monitoring.alerting.enum import AlertType, Severity from lego_monitoring.core import cvars +from .utils import format_for_healthchecks_slug + IS_TESTING = False def cpu_check() -> list[Alert]: percentage = cpu_percent() config = cvars.config.get().checks.cpu + slug = f"{format_for_healthchecks_slug(gethostname())}-cpu" if config.critical_percentage and (IS_TESTING or percentage >= config.critical_percentage): return [ Alert( alert_type=AlertType.CPU, message=f"CPU load: {percentage:.2f}% >= {config.critical_percentage:.2f}%", severity=Severity.CRITICAL, + healthchecks_slug=slug, ) ] elif config.warning_percentage and (IS_TESTING or percentage >= config.warning_percentage): @@ -24,7 +30,15 @@ def cpu_check() -> list[Alert]: alert_type=AlertType.CPU, message=f"CPU load: {percentage:.2f}% >= {config.warning_percentage:.2f}%", severity=Severity.WARNING, + healthchecks_slug=slug, ) ] else: - return [Alert(alert_type=AlertType.CPU, message=f"CPU load: {percentage:.2f}% (nominal)", severity=Severity.OK)] + return [ + Alert( + alert_type=AlertType.CPU, + message=f"CPU load: {percentage:.2f}% (nominal)", + severity=Severity.OK, + healthchecks_slug=slug, + ) + ] diff --git a/src/lego_monitoring/checks/net.py b/src/lego_monitoring/checks/net.py index 073bd8f..10c6d03 100644 --- a/src/lego_monitoring/checks/net.py +++ b/src/lego_monitoring/checks/net.py @@ -1,4 +1,5 @@ from dataclasses import dataclass, field +from socket import gethostname from typing import Optional from humanize import naturalsize @@ -8,6 +9,8 @@ from lego_monitoring.alerting.alert import Alert from lego_monitoring.alerting.enum import AlertType, Severity from lego_monitoring.core import cvars +from .utils import format_for_healthchecks_slug + IS_TESTING = False SECONDS_BETWEEN_CHECKS = 5 * 60 @@ -25,6 +28,9 @@ class NetIOTracker: stat_name: str, interface: str, ) -> Optional[Alert]: + slug = ( + f"{format_for_healthchecks_slug(gethostname())}-net-{format_for_healthchecks_slug(interface)}-{stat_name}" + ) current_stat_natural = naturalsize(current_stat_bytes_per_sec, binary=True) if critical_threshold and (IS_TESTING or current_stat_bytes_per_sec >= critical_threshold): critical_threshold_natural = naturalsize(critical_threshold, binary=True) @@ -32,6 +38,7 @@ class NetIOTracker: alert_type=AlertType.NET, message=f"Interface {interface} {stat_name} {current_stat_natural}/s >= {critical_threshold_natural}/s", severity=Severity.CRITICAL, + healthchecks_slug=slug, ) elif warning_threshold and (IS_TESTING or current_stat_bytes_per_sec >= warning_threshold): warning_threshold_natural = naturalsize(warning_threshold, binary=True) @@ -39,12 +46,14 @@ class NetIOTracker: alert_type=AlertType.NET, message=f"Interface {interface} {stat_name} {current_stat_natural}/s >= {warning_threshold_natural}/s", severity=Severity.WARNING, + healthchecks_slug=slug, ) else: return Alert( alert_type=AlertType.NET, message=f"Interface {interface} {stat_name} {current_stat_natural}/s (nominal)", severity=Severity.OK, + healthchecks_slug=slug, ) def net_check(self) -> list[Alert]: diff --git a/src/lego_monitoring/checks/ram.py b/src/lego_monitoring/checks/ram.py index e91c891..07e8878 100644 --- a/src/lego_monitoring/checks/ram.py +++ b/src/lego_monitoring/checks/ram.py @@ -1,21 +1,27 @@ +from socket import gethostname + from psutil import virtual_memory from lego_monitoring.alerting.alert import Alert from lego_monitoring.alerting.enum import AlertType, Severity from lego_monitoring.core import cvars +from .utils import format_for_healthchecks_slug + IS_TESTING = False def ram_check() -> list[Alert]: percentage = virtual_memory().percent config = cvars.config.get().checks.ram + slug = f"{format_for_healthchecks_slug(gethostname())}-ram" if config.critical_percentage and (IS_TESTING or percentage >= config.critical_percentage): return [ Alert( alert_type=AlertType.RAM, message=f"RAM usage: {percentage:.2f}% >= {config.critical_percentage:.2f}%", severity=Severity.CRITICAL, + healthchecks_slug=slug, ) ] elif config.warning_percentage and (IS_TESTING or percentage >= config.warning_percentage): @@ -24,9 +30,15 @@ def ram_check() -> list[Alert]: alert_type=AlertType.RAM, message=f"RAM usage: {percentage:.2f}% >= {config.warning_percentage:.2f}%", severity=Severity.WARNING, + healthchecks_slug=slug, ) ] else: return [ - Alert(alert_type=AlertType.RAM, message=f"RAM usage: {percentage:.2f}% (nominal)", severity=Severity.OK) + Alert( + alert_type=AlertType.RAM, + message=f"RAM usage: {percentage:.2f}% (nominal)", + severity=Severity.OK, + healthchecks_slug=slug, + ) ] diff --git a/src/lego_monitoring/checks/temp/__init__.py b/src/lego_monitoring/checks/temp/__init__.py index e461148..4cb74bc 100644 --- a/src/lego_monitoring/checks/temp/__init__.py +++ b/src/lego_monitoring/checks/temp/__init__.py @@ -1,6 +1,9 @@ +from socket import gethostname + from lego_monitoring.alerting.alert import Alert from lego_monitoring.alerting.enum import AlertType, Severity +from ..utils import format_for_healthchecks_slug from . import sensors IS_TESTING = False @@ -11,23 +14,29 @@ def temp_check() -> list[Alert]: temps = sensors.get_readings() for sensor, readings in temps.items(): for r in readings: + sensor_slug = format_for_healthchecks_slug(sensor) + label_slug = format_for_healthchecks_slug(r.label) + slug = f"{format_for_healthchecks_slug(gethostname())}-temp-{sensor_slug}-{label_slug}" if r.critical_temp is not None and (IS_TESTING or r.current_temp >= r.critical_temp): alert = Alert( alert_type=AlertType.TEMP, message=f"{sensor} {r.label}: {r.current_temp}°C >= {r.critical_temp}°C", severity=Severity.CRITICAL, + healthchecks_slug=slug, ) elif r.warning_temp is not None and (IS_TESTING or r.current_temp >= r.warning_temp): alert = Alert( alert_type=AlertType.TEMP, message=f"{sensor} {r.label}: {r.current_temp}°C >= {r.warning_temp}°C", severity=Severity.WARNING, + healthchecks_slug=slug, ) else: alert = Alert( alert_type=AlertType.TEMP, message=f"{sensor} {r.label}: {r.current_temp}°C (nominal)", severity=Severity.OK, + healthchecks_slug=slug, ) alert_list.append(alert) diff --git a/src/lego_monitoring/checks/utils.py b/src/lego_monitoring/checks/utils.py new file mode 100644 index 0000000..140c3be --- /dev/null +++ b/src/lego_monitoring/checks/utils.py @@ -0,0 +1,5 @@ +import re + + +def format_for_healthchecks_slug(s: str) -> str: + return re.sub(r"[^a-z0-9_-]", "_", s.lower()) diff --git a/src/lego_monitoring/checks/vulnix/__init__.py b/src/lego_monitoring/checks/vulnix/__init__.py index cd43c57..2e866e6 100644 --- a/src/lego_monitoring/checks/vulnix/__init__.py +++ b/src/lego_monitoring/checks/vulnix/__init__.py @@ -1,7 +1,10 @@ +from socket import gethostname + from lego_monitoring.alerting.alert import Alert from lego_monitoring.alerting.enum import AlertType, Severity from lego_monitoring.alerting.sender import send_alert +from ..utils import format_for_healthchecks_slug from .vulnix import get_vulnix_output IS_TESTING = False @@ -9,6 +12,7 @@ IS_TESTING = False async def vulnix_check() -> list[Alert]: alert_list = [] + slug = f"{format_for_healthchecks_slug(gethostname())}-vulnix" try: vulnix_output = get_vulnix_output(IS_TESTING) except Exception as e: @@ -17,6 +21,7 @@ async def vulnix_check() -> list[Alert]: alert_type=AlertType.ERROR, message=f"Exception {type(e).__name__} while calling vulnix: {e}", severity=Severity.CRITICAL, + healthchecks_slug=slug, ) ) return [] @@ -29,6 +34,7 @@ async def vulnix_check() -> list[Alert]: continue message = f"New findings in derivation {finding.derivation}:" short_message = f"New findings in {finding.derivation} (short ver):" + plain_message = f"New findings in derivation {finding.derivation}:" for cve in non_whitelisted_cves: if cve in finding.cvssv3_basescore: score_str = f"(CVSSv3 = {finding.cvssv3_basescore[cve]})" @@ -36,6 +42,7 @@ async def vulnix_check() -> list[Alert]: score_str = "(not scored by CVSSv3)" message += f'\n* {cve} - {finding.description[cve]} {score_str}' short_message += f'\n * {cve}' + plain_message += f"\n* https://nvd.nist.gov/vuln/detail/{cve} - {finding.description[cve]} {score_str}" if len(message) > 3700: message = short_message @@ -44,6 +51,8 @@ async def vulnix_check() -> list[Alert]: alert_type=AlertType.VULN, message=message, severity=Severity.WARNING, + healthchecks_slug=slug, + plain_message=plain_message, ) alert_list.append(alert) @@ -51,6 +60,6 @@ async def vulnix_check() -> list[Alert]: alert_list[0].message += "\n(just testing)" return [alert_list[0]] elif len(alert_list) == 0: - return [Alert(AlertType.VULN, message="No vulnerabilities found", severity=Severity.OK)] + return [Alert(AlertType.VULN, message="No vulnerabilities found", severity=Severity.OK, healthchecks_slug=slug)] else: return alert_list diff --git a/src/lego_monitoring/core/checkers.py b/src/lego_monitoring/core/checkers.py index ccb6c41..8b4bb5a 100644 --- a/src/lego_monitoring/core/checkers.py +++ b/src/lego_monitoring/core/checkers.py @@ -7,7 +7,7 @@ from typing import Any, Callable, Coroutine from ..alerting.alert import Alert from ..alerting.current import CurrentAlerts from ..alerting.enum import Severity -from ..alerting.sender import send_alert +from ..alerting.sender import send_alert, send_healthchecks_status @dataclass @@ -75,6 +75,9 @@ class BaseChecker: for alert in alerts: await send_alert(alert, note="ongoing") + for alert in alerts: + await send_healthchecks_status(alert) + async def run_checker(self) -> None: raise NotImplementedError From 13fd4b05d941c2169037deeed0d38b41b2f0ed7c Mon Sep 17 00:00:00 2001 From: Alex Tau Date: Fri, 15 Aug 2025 19:30:57 +0300 Subject: [PATCH 35/56] do not send reminder alerts --- src/lego_monitoring/core/checkers.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/lego_monitoring/core/checkers.py b/src/lego_monitoring/core/checkers.py index 8b4bb5a..4cd14d0 100644 --- a/src/lego_monitoring/core/checkers.py +++ b/src/lego_monitoring/core/checkers.py @@ -75,8 +75,9 @@ class BaseChecker: for alert in alerts: await send_alert(alert, note="ongoing") - for alert in alerts: - await send_healthchecks_status(alert) + if not self.is_reminder: + for alert in alerts: + await send_healthchecks_status(alert) async def run_checker(self) -> None: raise NotImplementedError From 878a4fc092135ce45dc1ce1211e0b8ea8677b2a4 Mon Sep 17 00:00:00 2001 From: Alex Tau Date: Fri, 15 Aug 2025 20:11:32 +0300 Subject: [PATCH 36/56] send start and stop healthchecks signals correctly --- modules/options.nix | 6 ++---- src/lego_monitoring/__init__.py | 12 +++++++---- src/lego_monitoring/alerting/enum.py | 2 +- src/lego_monitoring/alerting/sender.py | 30 ++++++-------------------- src/lego_monitoring/checks/__init__.py | 1 + src/lego_monitoring/checks/self.py | 30 ++++++++++++++++++++++++++ src/lego_monitoring/config/enums.py | 3 +-- src/lego_monitoring/core/checkers.py | 8 +++---- 8 files changed, 54 insertions(+), 38 deletions(-) create mode 100644 src/lego_monitoring/checks/self.py diff --git a/modules/options.nix b/modules/options.nix index 8af2ab5..128dd72 100644 --- a/modules/options.nix +++ b/modules/options.nix @@ -26,8 +26,7 @@ in enabledCheckSets = lib.mkOption { type = lib.types.listOf (lib.types.enum [ - "start" - "stop" + "self" "remind" "cpu" @@ -40,8 +39,7 @@ in default = [ ]; description = '' List of enabled check sets. Each check set is a module which checks something and generates alerts based on check results. Available check sets: - * start -- send an alert when lego-monitoring is started - * stop -- send an alert when lego-monitoring is stopped + * self -- send an alert when lego-monitoring is started and stopped * remind -- periodically (daily by default) remind about ongoing unresolved alerts * cpu -- alerts when CPU usage is above threshold * ram -- alerts when RAM usage is above threshold diff --git a/src/lego_monitoring/__init__.py b/src/lego_monitoring/__init__.py index 488bf1a..16bde25 100644 --- a/src/lego_monitoring/__init__.py +++ b/src/lego_monitoring/__init__.py @@ -52,8 +52,10 @@ async def async_main(): check_sets = config_enums.CheckSet checker_sets: dict[config_enums.CheckSet, list[Coroutine | BaseChecker]] = { - check_sets.START: [sender.send_start_alert()], - check_sets.STOP: [], # this is checked later + check_sets.SELF: [ + sender.send_alert(checks.generate_start_alert()), + IntervalChecker(checks.self_check, interval=datetime.timedelta(minutes=5), persistent=False), + ], check_sets.CPU: [IntervalChecker(checks.cpu_check, interval=datetime.timedelta(minutes=3), persistent=True)], check_sets.RAM: [IntervalChecker(checks.ram_check, interval=datetime.timedelta(minutes=1), persistent=True)], check_sets.TEMP: [IntervalChecker(checks.temp_check, interval=datetime.timedelta(minutes=5), persistent=True)], @@ -120,8 +122,10 @@ async def async_main(): checker_tasks.add(task) while True: if stopping: - if "stop" in config.enabled_check_sets: - await sender.send_stop_alert() + if "self" in config.enabled_check_sets: + alert = checks.generate_stop_alert() + await sender.send_alert(alert) + await sender.send_healthchecks_status(alert) await tg_client.disconnect() raise SystemExit else: diff --git a/src/lego_monitoring/alerting/enum.py b/src/lego_monitoring/alerting/enum.py index b79abff..5e14034 100644 --- a/src/lego_monitoring/alerting/enum.py +++ b/src/lego_monitoring/alerting/enum.py @@ -2,7 +2,7 @@ from enum import IntEnum, StrEnum class AlertType(StrEnum): - BOOT = "BOOT" + SELF = "SELF" ERROR = "ERROR" TEST = "TEST" REMIND = "REMIND" diff --git a/src/lego_monitoring/alerting/sender.py b/src/lego_monitoring/alerting/sender.py index edd35c0..8601172 100644 --- a/src/lego_monitoring/alerting/sender.py +++ b/src/lego_monitoring/alerting/sender.py @@ -1,7 +1,11 @@ +import logging +from socket import gethostname + from telethon import TelegramClient from telethon.sessions import MemorySession from uplink import AiohttpClient +from ..checks.utils import format_for_healthchecks_slug from ..core import cvars from .alert import Alert from .clients.healthchecks import HealthchecksClient @@ -37,6 +41,7 @@ def format_message(alert: Alert, note: str) -> str: async def send_alert(alert: Alert, note: str = "") -> None: + logging.debug(f"Sending {alert.alert_type} alert to Telegram") try: tg_client = cvars.tg_client.get() except LookupError: # being called standalone @@ -62,6 +67,8 @@ async def send_healthchecks_status(alert: Alert) -> None: else: return keys["default"] + logging.debug(f"Sending {alert.alert_type} to Healthchecks") + if alert.healthchecks_slug is None: return try: @@ -76,26 +83,3 @@ async def send_healthchecks_status(alert: Alert) -> None: await hc_client.success(key, alert.healthchecks_slug, create=True, log=alert.plain_message) else: await hc_client.failure(key, alert.healthchecks_slug, create=True, log=alert.plain_message) - - -# TODO service itself has to be monitored like everything else - with regular pinging - if we're -# using healthchecks -async def send_start_alert() -> None: - config = cvars.config.get() - await send_alert( - Alert( - alert_type=AlertType.BOOT, - message=f"Service running with enabled checks: {', '.join(config.enabled_check_sets)}", - severity=Severity.INFO, - ) - ) - - -async def send_stop_alert() -> None: - await send_alert( - Alert( - alert_type=AlertType.BOOT, - message="Service stopping.", - severity=Severity.INFO, - ) - ) diff --git a/src/lego_monitoring/checks/__init__.py b/src/lego_monitoring/checks/__init__.py index 62a11da..ae61375 100644 --- a/src/lego_monitoring/checks/__init__.py +++ b/src/lego_monitoring/checks/__init__.py @@ -2,5 +2,6 @@ from .cpu import cpu_check from .net import NetIOTracker from .ram import ram_check from .remind import remind_check +from .self import generate_start_alert, generate_stop_alert, self_check from .temp import temp_check from .vulnix import vulnix_check diff --git a/src/lego_monitoring/checks/self.py b/src/lego_monitoring/checks/self.py new file mode 100644 index 0000000..a907055 --- /dev/null +++ b/src/lego_monitoring/checks/self.py @@ -0,0 +1,30 @@ +from socket import gethostname + +from lego_monitoring.alerting.alert import Alert +from lego_monitoring.alerting.enum import AlertType, Severity +from lego_monitoring.core import cvars + +from .utils import format_for_healthchecks_slug + + +def self_check() -> list[Alert]: + return [generate_start_alert()] + + +def generate_start_alert() -> Alert: + config = cvars.config.get() + return Alert( + alert_type=AlertType.SELF, + message=f"Host is up, lego-monitoring is running. Enabled checks: {', '.join(config.enabled_check_sets)}", + severity=Severity.OK, + healthchecks_slug=f"{format_for_healthchecks_slug(gethostname())}-lego_monitoring", + ) + + +def generate_stop_alert() -> Alert: + return Alert( + alert_type=AlertType.SELF, + message=f"Lego-monitoring service stopping.", + severity=Severity.INFO, + healthchecks_slug=f"{format_for_healthchecks_slug(gethostname())}-lego_monitoring", + ) diff --git a/src/lego_monitoring/config/enums.py b/src/lego_monitoring/config/enums.py index a020a8b..1e73da3 100644 --- a/src/lego_monitoring/config/enums.py +++ b/src/lego_monitoring/config/enums.py @@ -2,8 +2,7 @@ from enum import StrEnum class CheckSet(StrEnum): - START = "start" - STOP = "stop" + SELF = "self" REMIND = "remind" CPU = "cpu" diff --git a/src/lego_monitoring/core/checkers.py b/src/lego_monitoring/core/checkers.py index 4cd14d0..78168bf 100644 --- a/src/lego_monitoring/core/checkers.py +++ b/src/lego_monitoring/core/checkers.py @@ -63,6 +63,10 @@ class BaseChecker: return result async def _handle_alerts(self, alerts: list[Alert]) -> None: + if not self.is_reminder: + for alert in alerts: + await send_healthchecks_status(alert) + if not self.persistent: for alert in alerts: if alert.severity != Severity.OK: @@ -75,10 +79,6 @@ class BaseChecker: for alert in alerts: await send_alert(alert, note="ongoing") - if not self.is_reminder: - for alert in alerts: - await send_healthchecks_status(alert) - async def run_checker(self) -> None: raise NotImplementedError From 731b0b32fcadbff9811643e967e1cfe928d24774 Mon Sep 17 00:00:00 2001 From: Alex Tau Date: Sat, 16 Aug 2025 12:08:49 +0300 Subject: [PATCH 37/56] update docs to match new functionality --- README.md | 4 +- config.example.json | 8 +- docs/nixos-options.md | 185 ++++++++++++++++++++++++++++++++++-------- 3 files changed, 157 insertions(+), 40 deletions(-) diff --git a/README.md b/README.md index f929963..4d642a8 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # lego-monitoring -Simple system monitoring service. Sends alerts in Telegram. Currently supports monitoring: +Simple system monitoring service. Sends alerts in Telegram and/or reports status to [Healthchecks](https://healthchecks.io/). Currently supports monitoring: * CPU/RAM/network usage * temperature readings * [vulnix](https://github.com/nix-community/vulnix) readings (NixOS only) @@ -57,7 +57,7 @@ cp config.example.json config.json ``` Edit `config.json` to suit your usage scenario. The default configuration only sends alerts on service's start and stop. -You may refer to the NixOS option documentation, as its options are the same, except JSON uses snake_case instead of lowerCamelCase. +You may refer to the NixOS option documentation, as its options are the same, except JSON uses snake_case instead of lowerCamelCase, and `enable` NixOS options just make a config section present or absent in JSON. Then enable and start the service: diff --git a/config.example.json b/config.example.json index b875087..5e2d823 100644 --- a/config.example.json +++ b/config.example.json @@ -5,9 +5,11 @@ "stop", "remind" ], - "telegram": { - "creds_secret_path": "/opt/lego-monitoring/tg-creds.txt", - "roomId": "0" + "alert_channels": { + "telegram": { + "creds_secret_path": "/opt/lego-monitoring/tg-creds.txt", + "roomId": "0" + } }, "checks": { diff --git a/docs/nixos-options.md b/docs/nixos-options.md index 63bb9f9..bf01a66 100644 --- a/docs/nixos-options.md +++ b/docs/nixos-options.md @@ -30,8 +30,7 @@ boolean List of enabled check sets\. Each check set is a module which checks something and generates alerts based on check results\. Available check sets: - - start – send an alert when lego-monitoring is started - - stop – send an alert when lego-monitoring is stopped + - self – send an alert when lego-monitoring is started and stopped - remind – periodically (daily by default) remind about ongoing unresolved alerts - cpu – alerts when CPU usage is above threshold - ram – alerts when RAM usage is above threshold @@ -42,7 +41,7 @@ List of enabled check sets\. Each check set is a module which checks something a *Type:* -list of (one of “start”, “stop”, “remind”, “cpu”, “ram”, “temp”, “net”, “vulnix”) +list of (one of “self”, “remind”, “cpu”, “ram”, “temp”, “net”, “vulnix”) @@ -54,8 +53,156 @@ list of (one of “start”, “stop”, “remind”, “cpu”, “ram”, “ +## services\.lego-monitoring\.alertChannels\.healthchecks\.enable + +Whether to enable [Healthchecks](https://healthchecks\.io) notification channel\. + + + +*Type:* +boolean + + + +*Default:* +` false ` + + + +*Example:* +` true ` + +*Declared by:* + - [modules/options\.nix](../modules/options.nix) + + + +## services\.lego-monitoring\.alertChannels\.healthchecks\.pingingApiEndpoint + + + +Endpoint URL for Healthchecks pinging API\. + + + +*Type:* +string + + + +*Default:* +` "https://hc-ping.com/" ` + + + +*Example:* +` "https://your-healthchecks-instance.com/ping/" ` + +*Declared by:* + - [modules/options\.nix](../modules/options.nix) + + + +## services\.lego-monitoring\.alertChannels\.healthchecks\.pingingKeysSecretPath + + + +Path to a file containing the pinging keys in a ` slug:key ` format, one on each line (ex: ` lego-cpu:aaaaaaaaaaaaaaaaaaaaaa `)\. +Specify ` default ` as the slug to use this key for check types that don’t have a key explicitly assigned to them\. + +If you are unsure of the exact slug a check will generate, it is recommended to try it out with the default key first, before +assigning a specific one\. + +**Note**: checks will be auto-provisioned, but correct intervals and grace periods have to be configured manually from the web console, +otherwise silent failures will not be recorded until after 1 day (the default healthchecks interval)\. + + + +*Type:* +string + + + +*Default:* +` "" ` + +*Declared by:* + - [modules/options\.nix](../modules/options.nix) + + + +## services\.lego-monitoring\.alertChannels\.telegram\.enable + + + +Whether to enable Telegram notification channel\. + + + +*Type:* +boolean + + + +*Default:* +` false ` + + + +*Example:* +` true ` + +*Declared by:* + - [modules/options\.nix](../modules/options.nix) + + + +## services\.lego-monitoring\.alertChannels\.telegram\.credsSecretPath + + + +Path to a file containing Telegram api_id, api_hash, and bot token, separated by the ` , ` character\. + + + +*Type:* +string + + + +*Default:* +` "" ` + +*Declared by:* + - [modules/options\.nix](../modules/options.nix) + + + +## services\.lego-monitoring\.alertChannels\.telegram\.roomId + + + +ID of chat where to send alerts\. + + + +*Type:* +signed integer + + + +*Default:* +` 0 ` + +*Declared by:* + - [modules/options\.nix](../modules/options.nix) + + + ## services\.lego-monitoring\.checks\.cpu\.criticalPercentage + + CPU load percentage for a critical alert to be sent\. Null means never generate a CPU critical alert\. @@ -608,35 +755,3 @@ one of “CRITICAL”, “ERROR”, “WARNING”, “INFO”, “DEBUG” - [modules/options\.nix](../modules/options.nix) - -## services\.lego-monitoring\.telegram\.credsSecretPath - - - -Path to a file containing Telegram api_id, api_hash, and bot token, separated by the ` , ` character\. - - - -*Type:* -string - -*Declared by:* - - [modules/options\.nix](../modules/options.nix) - - - -## services\.lego-monitoring\.telegram\.roomId - - - -ID of chat where to send alerts\. - - - -*Type:* -signed integer - -*Declared by:* - - [modules/options\.nix](../modules/options.nix) - - From 240ef4dfab87acdfd65c4146c9a50ea9b452e567 Mon Sep 17 00:00:00 2001 From: Alex Tau Date: Sat, 16 Aug 2025 12:10:40 +0300 Subject: [PATCH 38/56] fix config example: replace start and stop with self --- config.example.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/config.example.json b/config.example.json index 5e2d823..d380bcf 100644 --- a/config.example.json +++ b/config.example.json @@ -1,8 +1,7 @@ { "log_level": "INFO", "enabled_check_sets": [ - "start", - "stop", + "self", "remind" ], "alert_channels": { From d78d21c3125c88066d47bfa4299717d1d4ea4a5e Mon Sep 17 00:00:00 2001 From: Alex Tau Date: Sat, 16 Aug 2025 12:11:42 +0300 Subject: [PATCH 39/56] bump ver to v1.1.0 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 5317964..6f55eba 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "lego-monitoring" -version = "1.0.0" +version = "1.1.0" description = "Monitoring software for the lego server" readme = "README.md" requires-python = ">=3.12" From 945be6422e32304ee1da4ffa4276604915da612b Mon Sep 17 00:00:00 2001 From: Alex Tau Date: Sat, 16 Aug 2025 12:11:42 +0300 Subject: [PATCH 40/56] bump ver to v1.1.0 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 5317964..6f55eba 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "lego-monitoring" -version = "1.0.0" +version = "1.1.0" description = "Monitoring software for the lego server" readme = "README.md" requires-python = ">=3.12" From c355583f59e78696df5ace4b0717f3fc4a247826 Mon Sep 17 00:00:00 2001 From: Alex Tau Date: Sat, 16 Aug 2025 13:24:47 +0300 Subject: [PATCH 41/56] meaningful exception handling for vulnix --- src/lego_monitoring/checks/vulnix/__init__.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/lego_monitoring/checks/vulnix/__init__.py b/src/lego_monitoring/checks/vulnix/__init__.py index 2e866e6..c25bfd6 100644 --- a/src/lego_monitoring/checks/vulnix/__init__.py +++ b/src/lego_monitoring/checks/vulnix/__init__.py @@ -2,7 +2,6 @@ from socket import gethostname from lego_monitoring.alerting.alert import Alert from lego_monitoring.alerting.enum import AlertType, Severity -from lego_monitoring.alerting.sender import send_alert from ..utils import format_for_healthchecks_slug from .vulnix import get_vulnix_output @@ -16,15 +15,14 @@ async def vulnix_check() -> list[Alert]: try: vulnix_output = get_vulnix_output(IS_TESTING) except Exception as e: - await send_alert( + return [ Alert( - alert_type=AlertType.ERROR, + AlertType.VULN, message=f"Exception {type(e).__name__} while calling vulnix: {e}", severity=Severity.CRITICAL, healthchecks_slug=slug, ) - ) - return [] + ] for finding in vulnix_output: if not IS_TESTING: non_whitelisted_cves = [k for k in finding.description if k not in finding.whitelisted] From 9b884788a6f7cb99aecb14971ef20c9e15f8a142 Mon Sep 17 00:00:00 2001 From: Alex Tau Date: Sat, 16 Aug 2025 13:35:15 +0300 Subject: [PATCH 42/56] ignore first cpu check to prevent guaranteed alert on machine startup --- src/lego_monitoring/__init__.py | 6 +++++- src/lego_monitoring/core/checkers.py | 7 ++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/lego_monitoring/__init__.py b/src/lego_monitoring/__init__.py index 16bde25..75e1312 100644 --- a/src/lego_monitoring/__init__.py +++ b/src/lego_monitoring/__init__.py @@ -56,7 +56,11 @@ async def async_main(): sender.send_alert(checks.generate_start_alert()), IntervalChecker(checks.self_check, interval=datetime.timedelta(minutes=5), persistent=False), ], - check_sets.CPU: [IntervalChecker(checks.cpu_check, interval=datetime.timedelta(minutes=3), persistent=True)], + check_sets.CPU: [ + IntervalChecker( + checks.cpu_check, interval=datetime.timedelta(minutes=3), persistent=True, ignore_first_run=True + ) + ], check_sets.RAM: [IntervalChecker(checks.ram_check, interval=datetime.timedelta(minutes=1), persistent=True)], check_sets.TEMP: [IntervalChecker(checks.temp_check, interval=datetime.timedelta(minutes=5), persistent=True)], check_sets.VULNIX: [ diff --git a/src/lego_monitoring/core/checkers.py b/src/lego_monitoring/core/checkers.py index 78168bf..d3e4a0f 100644 --- a/src/lego_monitoring/core/checkers.py +++ b/src/lego_monitoring/core/checkers.py @@ -87,14 +87,19 @@ class BaseChecker: class IntervalChecker(BaseChecker): _: KW_ONLY interval: datetime.timedelta + ignore_first_run: bool = False async def run_checker(self) -> None: interval_secs = self.interval.total_seconds() + ignore_first_run = self.ignore_first_run while True: logging.info(f"Calling {self.check.__name__}") result = await self._call_check() logging.info(f"Got {len(result)} alerts") - await self._handle_alerts(result) + if ignore_first_run: + ignore_first_run = False + else: + await self._handle_alerts(result) await asyncio.sleep(interval_secs) From 09eabcc6b2b00a91dcf0b4da011b7d756ffc6f1f Mon Sep 17 00:00:00 2001 From: Alex Tau Date: Sat, 16 Aug 2025 13:44:26 +0300 Subject: [PATCH 43/56] bump ver to v1.1.1 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 6f55eba..d9c6434 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "lego-monitoring" -version = "1.1.0" +version = "1.1.1" description = "Monitoring software for the lego server" readme = "README.md" requires-python = ">=3.12" From ce363c60cabd28a9a4ad4507a59490bd17466f3d Mon Sep 17 00:00:00 2001 From: Alex Tau Date: Tue, 2 Sep 2025 14:14:06 +0300 Subject: [PATCH 44/56] clean up a bit --- src/lego_monitoring/alerting/sender.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/lego_monitoring/alerting/sender.py b/src/lego_monitoring/alerting/sender.py index 8601172..11741ef 100644 --- a/src/lego_monitoring/alerting/sender.py +++ b/src/lego_monitoring/alerting/sender.py @@ -1,15 +1,13 @@ import logging -from socket import gethostname from telethon import TelegramClient from telethon.sessions import MemorySession from uplink import AiohttpClient -from ..checks.utils import format_for_healthchecks_slug from ..core import cvars from .alert import Alert from .clients.healthchecks import HealthchecksClient -from .enum import SEVERITY_TO_EMOJI, AlertType, Severity +from .enum import SEVERITY_TO_EMOJI, Severity async def get_tg_client() -> TelegramClient: @@ -23,9 +21,7 @@ async def get_tg_client() -> TelegramClient: def get_healthchecks_client() -> HealthchecksClient: config = cvars.config.get() base_url = config.alert_channels.healthchecks.pinging_api_endpoint - client = HealthchecksClient( - base_url=config.alert_channels.healthchecks.pinging_api_endpoint, client=AiohttpClient() - ) + client = HealthchecksClient(base_url=base_url, client=AiohttpClient()) return client From 2c6e804959791872a2d6bb8e7ffe5c7f92bf5ad9 Mon Sep 17 00:00:00 2001 From: Alex Tau Date: Fri, 12 Sep 2025 18:14:25 +0300 Subject: [PATCH 45/56] update nixpkgs for vulnix 1.12.0 --- flake.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/flake.lock b/flake.lock index 1cc4fe8..35547eb 100644 --- a/flake.lock +++ b/flake.lock @@ -2,11 +2,11 @@ "nodes": { "nixpkgs": { "locked": { - "lastModified": 1749086602, - "narHash": "sha256-DJcgJMekoxVesl9kKjfLPix2Nbr42i7cpEHJiTnBUwU=", + "lastModified": 1757545623, + "narHash": "sha256-mCxPABZ6jRjUQx3bPP4vjA68ETbPLNz9V2pk9tO7pRQ=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "4792576cb003c994bd7cc1edada3129def20b27d", + "rev": "8cd5ce828d5d1d16feff37340171a98fc3bf6526", "type": "github" }, "original": { From da480a7c4e079eb4f638b0f22eb55f6783dbe069 Mon Sep 17 00:00:00 2001 From: Alex Tau Date: Sat, 13 Sep 2025 14:40:02 +0300 Subject: [PATCH 46/56] add ups periodic checks --- flake.lock | 17 - flake.nix | 11 +- modules/default.nix | 4 + modules/options.nix | 10 + src/lego_monitoring/__init__.py | 1 + src/lego_monitoring/alerting/enum.py | 2 +- src/lego_monitoring/checks/__init__.py | 1 + src/lego_monitoring/checks/ups.py | 120 ++++ src/lego_monitoring/config/__init__.py | 2 + src/lego_monitoring/config/checks/ups.py | 9 + src/lego_monitoring/config/enums.py | 1 + src/lego_monitoring/core/const.py | 1 + uv.lock | 757 ++++++++++++----------- 13 files changed, 533 insertions(+), 403 deletions(-) create mode 100644 src/lego_monitoring/checks/ups.py create mode 100644 src/lego_monitoring/config/checks/ups.py diff --git a/flake.lock b/flake.lock index 1cc4fe8..c6a1e31 100644 --- a/flake.lock +++ b/flake.lock @@ -16,22 +16,6 @@ "type": "github" } }, - "nixpkgs-29335f": { - "locked": { - "lastModified": 1745804731, - "narHash": "sha256-v/sK3AS0QKu/Tu5sHIfddiEHCvrbNYPv8X10Fpux68g=", - "owner": "nixos", - "repo": "nixpkgs", - "rev": "29335f23bea5e34228349ea739f31ee79e267b88", - "type": "github" - }, - "original": { - "owner": "nixos", - "repo": "nixpkgs", - "rev": "29335f23bea5e34228349ea739f31ee79e267b88", - "type": "github" - } - }, "pyproject-build-systems": { "inputs": { "nixpkgs": [ @@ -81,7 +65,6 @@ "root": { "inputs": { "nixpkgs": "nixpkgs", - "nixpkgs-29335f": "nixpkgs-29335f", "pyproject-build-systems": "pyproject-build-systems", "pyproject-nix": "pyproject-nix", "uv2nix": "uv2nix" diff --git a/flake.nix b/flake.nix index 28f27fa..763f49d 100644 --- a/flake.nix +++ b/flake.nix @@ -1,8 +1,6 @@ { inputs = { nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.05"; - # this is for uv 0.6.17, 0.7.0 has a change uv2nix doesn't yet support: https://github.com/astral-sh/uv/pull/13176 - nixpkgs-29335f.url = "github:nixos/nixpkgs/29335f23bea5e34228349ea739f31ee79e267b88"; pyproject-nix = { url = "github:pyproject-nix/pyproject.nix"; @@ -27,7 +25,6 @@ { self, nixpkgs, - nixpkgs-29335f, uv2nix, pyproject-nix, pyproject-build-systems, @@ -73,7 +70,8 @@ old: { postPatch = '' substituteInPlace src/lego_monitoring/core/const.py \ - --replace-fail 'VULNIX_PATH: str = ...' 'VULNIX_PATH = "${lib.getExe pkgs.vulnix}"' + --replace-fail 'VULNIX_PATH: str = ...' 'VULNIX_PATH = "${lib.getExe pkgs.vulnix}"' \ + --replace-fail 'UPSC_PATH = "/usr/bin/upsc"' 'UPSC_PATH = "${pkgs.nut}/bin/upsc"' ''; } ); @@ -81,7 +79,6 @@ # This example is only using x86_64-linux pkgs = nixpkgs.legacyPackages.x86_64-linux; - pkgs-29335f = nixpkgs-29335f.legacyPackages.x86_64-linux; # Use Python 3.12 from nixpkgs python = pkgs.python312; @@ -129,7 +126,7 @@ impure = pkgs.mkShell { packages = [ python - pkgs-29335f.uv + pkgs.uv ]; env = { @@ -209,7 +206,7 @@ pkgs.mkShell { packages = [ virtualenv - pkgs-29335f.uv + pkgs.uv ]; env = { diff --git a/modules/default.nix b/modules/default.nix index a269aba..7035f05 100644 --- a/modules/default.nix +++ b/modules/default.nix @@ -76,6 +76,10 @@ package: warning_threshold_comb_bytes = interfaceCfg.warningThresholdCombBytes; critical_threshold_comb_bytes = interfaceCfg.criticalThresholdCombBytes; }) cfg.checks.net.interfaces; + + ups = with cfg.checks.ups; { + ups_to_check = upsToCheck; + }; }; }; in lib.mkIf cfg.enable { diff --git a/modules/options.nix b/modules/options.nix index 128dd72..a3af83e 100644 --- a/modules/options.nix +++ b/modules/options.nix @@ -33,6 +33,7 @@ in "ram" "temp" "net" + "ups" "vulnix" ]); @@ -45,6 +46,7 @@ in * ram -- alerts when RAM usage is above threshold * temp -- alerts when temperature readings are above thresholds * net -- alerts when network usage is above threshold + * ups -- alerts on UPS events * vulnix -- periodically scans system for known CVEs, alerts if any are found (NixOS only)''; }; @@ -170,6 +172,14 @@ in }''; }; }; + + ups = { + upsToCheck = lib.mkOption { + type = with lib.types; nullOr (listOf str); + default = null; + description = "List of UPS's to monitor, in `upsc`-compatible format. If null, all UPS's connected to localhost are checked."; + }; + }; }; }; } diff --git a/src/lego_monitoring/__init__.py b/src/lego_monitoring/__init__.py index 75e1312..63536dd 100644 --- a/src/lego_monitoring/__init__.py +++ b/src/lego_monitoring/__init__.py @@ -86,6 +86,7 @@ async def async_main(): check_sets.NET: [ IntervalChecker(checks.NetIOTracker().net_check, interval=datetime.timedelta(minutes=5), persistent=True) ], + check_sets.UPS: [IntervalChecker(checks.ups_check, interval=datetime.timedelta(minutes=5), persistent=True)], } checkers = [] diff --git a/src/lego_monitoring/alerting/enum.py b/src/lego_monitoring/alerting/enum.py index 5e14034..905a995 100644 --- a/src/lego_monitoring/alerting/enum.py +++ b/src/lego_monitoring/alerting/enum.py @@ -11,13 +11,13 @@ class AlertType(StrEnum): NET = "NET" RAM = "RAM" TEMP = "TEMP" + UPS = "UPS" VULN = "VULN" # LOGIN = "LOGIN" # SMART = "SMART" # TODO # RAID = "RAID" # DISKS = "DISKS" - # UPS = "UPS" # UPDATE = "UPDATE" diff --git a/src/lego_monitoring/checks/__init__.py b/src/lego_monitoring/checks/__init__.py index ae61375..c5518ee 100644 --- a/src/lego_monitoring/checks/__init__.py +++ b/src/lego_monitoring/checks/__init__.py @@ -4,4 +4,5 @@ from .ram import ram_check from .remind import remind_check from .self import generate_start_alert, generate_stop_alert, self_check from .temp import temp_check +from .ups import ups_check from .vulnix import vulnix_check diff --git a/src/lego_monitoring/checks/ups.py b/src/lego_monitoring/checks/ups.py new file mode 100644 index 0000000..447bbc8 --- /dev/null +++ b/src/lego_monitoring/checks/ups.py @@ -0,0 +1,120 @@ +import subprocess +from dataclasses import dataclass +from datetime import timedelta +from enum import StrEnum +from socket import gethostname + +from lego_monitoring.alerting.alert import Alert +from lego_monitoring.alerting.enum import AlertType, Severity +from lego_monitoring.core import cvars +from lego_monitoring.core.const import UPSC_PATH + +from .utils import format_for_healthchecks_slug + + +class UPSStatus(StrEnum): + """https://networkupstools.org/docs/developer-guide.chunked/new-drivers.html#_status_data""" + + ON_LINE = "OL" + ON_BATTERY = "OB" + BATTERY_LOW = "LB" + BATTERY_HIGH = "HB" + BATTERY_REPLACE = "RB" + BATTERY_CHARGING = "CHRG" + BATTERY_DISCHARGING = "DISCHRG" + UPS_BYPASS = "BYPASS" + UPS_OFFLINE = "OFF" + UPS_OVERLOAD = "OVER" + UPS_CALIBRATION = "CAL" + UPS_TRIM = "TRIM" + UPS_BOOST = "BOOST" + UPS_FSD = "FSD" + ALARM = "ALARM" + WAIT = "WAIT" + + +@dataclass +class UPSStats: + ups_status: list[UPSStatus] = None + battery_charge_percentage: int = None + battery_warning_percentage: int = 20 + battery_critical_percentage: int = 10 + battery_runtime: int = 1000 + + def __str__(self): + return f"""Status: {' '.join(self.ups_status)} +Battery: {self.battery_charge_percentage}% +Remaining runtime: {timedelta(seconds=self.battery_runtime)} +Will warn at {self.battery_warning_percentage}% +Will shut down at {self.battery_critical_percentage}% +""" + + +def get_ups_list() -> list[str]: + run_results = subprocess.run([UPSC_PATH, "-l"], stdout=subprocess.PIPE, encoding="utf-8") + return run_results.stdout.splitlines() + + +def get_ups_stats(ups: str) -> UPSStats: + stats = UPSStats() + + run_results = subprocess.run([UPSC_PATH, ups], stdout=subprocess.PIPE, encoding="utf-8") + for line in run_results.stdout.splitlines(): + variable, value = line.split(": ")[:2] + match variable: + case "battery.charge": + stats.battery_charge_percentage = int(value) + case "battery.charge.low": + stats.battery_critical_percentage = int(value) + case "battery.charge.warning": + stats.battery_warning_percentage = int(value) + case "battery.runtime": + stats.battery_runtime = int(value) + case "ups.status": + stats.ups_status = [UPSStatus(status) for status in value.split()] + case _: + ... + return stats + + +def ups_check() -> list[Alert]: + config = cvars.config.get().checks.ups + if config.ups_to_check is None: + ups_list = get_ups_list() + else: + ups_list = config.ups_to_check + alerts = [] + for ups in ups_list: + stats = get_ups_stats(ups) + slug = f"{format_for_healthchecks_slug(gethostname())}-ups-{format_for_healthchecks_slug(ups)}-periodic" + severity = Severity.OK + reasons_for_severity = [] + + if stats.battery_charge_percentage < stats.battery_critical_percentage: + severity = Severity.CRITICAL + reasons_for_severity.append("Critical percentage reached") + elif stats.battery_charge_percentage < stats.battery_critical_percentage: + severity = Severity.WARNING + reasons_for_severity.append("Warning percentage reached") + + for status in stats.ups_status: + if status == UPSStatus.UPS_OVERLOAD: + severity = Severity.CRITICAL + reasons_for_severity.append("UPS is overloaded") + elif status == UPSStatus.ON_BATTERY: + severity = max(Severity.WARNING, severity) + reasons_for_severity.append("UPS is on battery") + elif status == UPSStatus.ALARM: + severity = max(Severity.WARNING, severity) + reasons_for_severity.append("Alarm triggered") + elif status == UPSStatus.WAIT: + severity = max(Severity.INFO, severity) + reasons_for_severity.append("Waiting for info from UPS driver") + + if len(reasons_for_severity) > 0: + message = f"NOTE: {', '.join(reasons_for_severity)}\n{stats}" + else: + message = str(stats) + alerts.append(Alert(alert_type=AlertType.UPS, message=message, severity=severity, healthchecks_slug=slug)) + + return alerts diff --git a/src/lego_monitoring/config/__init__.py b/src/lego_monitoring/config/__init__.py index 11e4c70..70662c0 100644 --- a/src/lego_monitoring/config/__init__.py +++ b/src/lego_monitoring/config/__init__.py @@ -10,6 +10,7 @@ from .checks.cpu import CpuCheckConfig from .checks.net import NetCheckConfig from .checks.ram import RamCheckConfig from .checks.temp import TempCheckConfig +from .checks.ups import UPSCheckConfig from .checks.vulnix import VulnixCheckConfig @@ -20,6 +21,7 @@ class ChecksConfig(NestedDeserializableDataclass): temp: TempCheckConfig = field(default_factory=TempCheckConfig) vulnix: Optional[VulnixCheckConfig] = None # vulnix check WILL raise if this config section is None net: NetCheckConfig = field(default_factory=NetCheckConfig) + ups: UPSCheckConfig = field(default_factory=UPSCheckConfig) @dataclass diff --git a/src/lego_monitoring/config/checks/ups.py b/src/lego_monitoring/config/checks/ups.py new file mode 100644 index 0000000..f0ee64d --- /dev/null +++ b/src/lego_monitoring/config/checks/ups.py @@ -0,0 +1,9 @@ +from dataclasses import dataclass +from typing import Optional + +from alt_utils import NestedDeserializableDataclass + + +@dataclass +class UPSCheckConfig: + ups_to_check: Optional[list] = None diff --git a/src/lego_monitoring/config/enums.py b/src/lego_monitoring/config/enums.py index 1e73da3..2c4064c 100644 --- a/src/lego_monitoring/config/enums.py +++ b/src/lego_monitoring/config/enums.py @@ -9,6 +9,7 @@ class CheckSet(StrEnum): RAM = "ram" TEMP = "temp" NET = "net" + UPS = "ups" VULNIX = "vulnix" diff --git a/src/lego_monitoring/core/const.py b/src/lego_monitoring/core/const.py index dde2ddf..0a84522 100644 --- a/src/lego_monitoring/core/const.py +++ b/src/lego_monitoring/core/const.py @@ -1 +1,2 @@ VULNIX_PATH: str = ... # path to vulnix executable +UPSC_PATH = "/usr/bin/upsc" diff --git a/uv.lock b/uv.lock index 9d95560..66dc199 100644 --- a/uv.lock +++ b/uv.lock @@ -9,18 +9,18 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pycares" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/17/0a/163e5260cecc12de6abc259d158d9da3b8ec062ab863107dcdb1166cdcef/aiodns-3.5.0.tar.gz", hash = "sha256:11264edbab51896ecf546c18eb0dd56dff0428c6aa6d2cd87e643e07300eb310", size = 14380, upload_time = "2025-06-13T16:21:53.595Z" } +sdist = { url = "https://files.pythonhosted.org/packages/17/0a/163e5260cecc12de6abc259d158d9da3b8ec062ab863107dcdb1166cdcef/aiodns-3.5.0.tar.gz", hash = "sha256:11264edbab51896ecf546c18eb0dd56dff0428c6aa6d2cd87e643e07300eb310", size = 14380, upload-time = "2025-06-13T16:21:53.595Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f6/2c/711076e5f5d0707b8ec55a233c8bfb193e0981a800cd1b3b123e8ff61ca1/aiodns-3.5.0-py3-none-any.whl", hash = "sha256:6d0404f7d5215849233f6ee44854f2bb2481adf71b336b2279016ea5990ca5c5", size = 8068, upload_time = "2025-06-13T16:21:52.45Z" }, + { url = "https://files.pythonhosted.org/packages/f6/2c/711076e5f5d0707b8ec55a233c8bfb193e0981a800cd1b3b123e8ff61ca1/aiodns-3.5.0-py3-none-any.whl", hash = "sha256:6d0404f7d5215849233f6ee44854f2bb2481adf71b336b2279016ea5990ca5c5", size = 8068, upload-time = "2025-06-13T16:21:52.45Z" }, ] [[package]] name = "aiohappyeyeballs" version = "2.6.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/26/30/f84a107a9c4331c14b2b586036f40965c128aa4fee4dda5d3d51cb14ad54/aiohappyeyeballs-2.6.1.tar.gz", hash = "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558", size = 22760, upload_time = "2025-03-12T01:42:48.764Z" } +sdist = { url = "https://files.pythonhosted.org/packages/26/30/f84a107a9c4331c14b2b586036f40965c128aa4fee4dda5d3d51cb14ad54/aiohappyeyeballs-2.6.1.tar.gz", hash = "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558", size = 22760, upload-time = "2025-03-12T01:42:48.764Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8", size = 15265, upload_time = "2025-03-12T01:42:47.083Z" }, + { url = "https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8", size = 15265, upload-time = "2025-03-12T01:42:47.083Z" }, ] [[package]] @@ -36,42 +36,42 @@ dependencies = [ { name = "propcache" }, { name = "yarl" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9b/e7/d92a237d8802ca88483906c388f7c201bbe96cd80a165ffd0ac2f6a8d59f/aiohttp-3.12.15.tar.gz", hash = "sha256:4fc61385e9c98d72fcdf47e6dd81833f47b2f77c114c29cd64a361be57a763a2", size = 7823716, upload_time = "2025-07-29T05:52:32.215Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9b/e7/d92a237d8802ca88483906c388f7c201bbe96cd80a165ffd0ac2f6a8d59f/aiohttp-3.12.15.tar.gz", hash = "sha256:4fc61385e9c98d72fcdf47e6dd81833f47b2f77c114c29cd64a361be57a763a2", size = 7823716, upload-time = "2025-07-29T05:52:32.215Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/63/97/77cb2450d9b35f517d6cf506256bf4f5bda3f93a66b4ad64ba7fc917899c/aiohttp-3.12.15-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:802d3868f5776e28f7bf69d349c26fc0efadb81676d0afa88ed00d98a26340b7", size = 702333, upload_time = "2025-07-29T05:50:46.507Z" }, - { url = "https://files.pythonhosted.org/packages/83/6d/0544e6b08b748682c30b9f65640d006e51f90763b41d7c546693bc22900d/aiohttp-3.12.15-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f2800614cd560287be05e33a679638e586a2d7401f4ddf99e304d98878c29444", size = 476948, upload_time = "2025-07-29T05:50:48.067Z" }, - { url = "https://files.pythonhosted.org/packages/3a/1d/c8c40e611e5094330284b1aea8a4b02ca0858f8458614fa35754cab42b9c/aiohttp-3.12.15-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8466151554b593909d30a0a125d638b4e5f3836e5aecde85b66b80ded1cb5b0d", size = 469787, upload_time = "2025-07-29T05:50:49.669Z" }, - { url = "https://files.pythonhosted.org/packages/38/7d/b76438e70319796bfff717f325d97ce2e9310f752a267bfdf5192ac6082b/aiohttp-3.12.15-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e5a495cb1be69dae4b08f35a6c4579c539e9b5706f606632102c0f855bcba7c", size = 1716590, upload_time = "2025-07-29T05:50:51.368Z" }, - { url = "https://files.pythonhosted.org/packages/79/b1/60370d70cdf8b269ee1444b390cbd72ce514f0d1cd1a715821c784d272c9/aiohttp-3.12.15-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6404dfc8cdde35c69aaa489bb3542fb86ef215fc70277c892be8af540e5e21c0", size = 1699241, upload_time = "2025-07-29T05:50:53.628Z" }, - { url = "https://files.pythonhosted.org/packages/a3/2b/4968a7b8792437ebc12186db31523f541943e99bda8f30335c482bea6879/aiohttp-3.12.15-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3ead1c00f8521a5c9070fcb88f02967b1d8a0544e6d85c253f6968b785e1a2ab", size = 1754335, upload_time = "2025-07-29T05:50:55.394Z" }, - { url = "https://files.pythonhosted.org/packages/fb/c1/49524ed553f9a0bec1a11fac09e790f49ff669bcd14164f9fab608831c4d/aiohttp-3.12.15-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6990ef617f14450bc6b34941dba4f12d5613cbf4e33805932f853fbd1cf18bfb", size = 1800491, upload_time = "2025-07-29T05:50:57.202Z" }, - { url = "https://files.pythonhosted.org/packages/de/5e/3bf5acea47a96a28c121b167f5ef659cf71208b19e52a88cdfa5c37f1fcc/aiohttp-3.12.15-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd736ed420f4db2b8148b52b46b88ed038d0354255f9a73196b7bbce3ea97545", size = 1719929, upload_time = "2025-07-29T05:50:59.192Z" }, - { url = "https://files.pythonhosted.org/packages/39/94/8ae30b806835bcd1cba799ba35347dee6961a11bd507db634516210e91d8/aiohttp-3.12.15-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c5092ce14361a73086b90c6efb3948ffa5be2f5b6fbcf52e8d8c8b8848bb97c", size = 1635733, upload_time = "2025-07-29T05:51:01.394Z" }, - { url = "https://files.pythonhosted.org/packages/7a/46/06cdef71dd03acd9da7f51ab3a9107318aee12ad38d273f654e4f981583a/aiohttp-3.12.15-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:aaa2234bb60c4dbf82893e934d8ee8dea30446f0647e024074237a56a08c01bd", size = 1696790, upload_time = "2025-07-29T05:51:03.657Z" }, - { url = "https://files.pythonhosted.org/packages/02/90/6b4cfaaf92ed98d0ec4d173e78b99b4b1a7551250be8937d9d67ecb356b4/aiohttp-3.12.15-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:6d86a2fbdd14192e2f234a92d3b494dd4457e683ba07e5905a0b3ee25389ac9f", size = 1718245, upload_time = "2025-07-29T05:51:05.911Z" }, - { url = "https://files.pythonhosted.org/packages/2e/e6/2593751670fa06f080a846f37f112cbe6f873ba510d070136a6ed46117c6/aiohttp-3.12.15-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a041e7e2612041a6ddf1c6a33b883be6a421247c7afd47e885969ee4cc58bd8d", size = 1658899, upload_time = "2025-07-29T05:51:07.753Z" }, - { url = "https://files.pythonhosted.org/packages/8f/28/c15bacbdb8b8eb5bf39b10680d129ea7410b859e379b03190f02fa104ffd/aiohttp-3.12.15-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5015082477abeafad7203757ae44299a610e89ee82a1503e3d4184e6bafdd519", size = 1738459, upload_time = "2025-07-29T05:51:09.56Z" }, - { url = "https://files.pythonhosted.org/packages/00/de/c269cbc4faa01fb10f143b1670633a8ddd5b2e1ffd0548f7aa49cb5c70e2/aiohttp-3.12.15-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:56822ff5ddfd1b745534e658faba944012346184fbfe732e0d6134b744516eea", size = 1766434, upload_time = "2025-07-29T05:51:11.423Z" }, - { url = "https://files.pythonhosted.org/packages/52/b0/4ff3abd81aa7d929b27d2e1403722a65fc87b763e3a97b3a2a494bfc63bc/aiohttp-3.12.15-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b2acbbfff69019d9014508c4ba0401822e8bae5a5fdc3b6814285b71231b60f3", size = 1726045, upload_time = "2025-07-29T05:51:13.689Z" }, - { url = "https://files.pythonhosted.org/packages/71/16/949225a6a2dd6efcbd855fbd90cf476052e648fb011aa538e3b15b89a57a/aiohttp-3.12.15-cp312-cp312-win32.whl", hash = "sha256:d849b0901b50f2185874b9a232f38e26b9b3d4810095a7572eacea939132d4e1", size = 423591, upload_time = "2025-07-29T05:51:15.452Z" }, - { url = "https://files.pythonhosted.org/packages/2b/d8/fa65d2a349fe938b76d309db1a56a75c4fb8cc7b17a398b698488a939903/aiohttp-3.12.15-cp312-cp312-win_amd64.whl", hash = "sha256:b390ef5f62bb508a9d67cb3bba9b8356e23b3996da7062f1a57ce1a79d2b3d34", size = 450266, upload_time = "2025-07-29T05:51:17.239Z" }, - { url = "https://files.pythonhosted.org/packages/f2/33/918091abcf102e39d15aba2476ad9e7bd35ddb190dcdd43a854000d3da0d/aiohttp-3.12.15-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:9f922ffd05034d439dde1c77a20461cf4a1b0831e6caa26151fe7aa8aaebc315", size = 696741, upload_time = "2025-07-29T05:51:19.021Z" }, - { url = "https://files.pythonhosted.org/packages/b5/2a/7495a81e39a998e400f3ecdd44a62107254803d1681d9189be5c2e4530cd/aiohttp-3.12.15-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2ee8a8ac39ce45f3e55663891d4b1d15598c157b4d494a4613e704c8b43112cd", size = 474407, upload_time = "2025-07-29T05:51:21.165Z" }, - { url = "https://files.pythonhosted.org/packages/49/fc/a9576ab4be2dcbd0f73ee8675d16c707cfc12d5ee80ccf4015ba543480c9/aiohttp-3.12.15-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3eae49032c29d356b94eee45a3f39fdf4b0814b397638c2f718e96cfadf4c4e4", size = 466703, upload_time = "2025-07-29T05:51:22.948Z" }, - { url = "https://files.pythonhosted.org/packages/09/2f/d4bcc8448cf536b2b54eed48f19682031ad182faa3a3fee54ebe5b156387/aiohttp-3.12.15-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b97752ff12cc12f46a9b20327104448042fce5c33a624f88c18f66f9368091c7", size = 1705532, upload_time = "2025-07-29T05:51:25.211Z" }, - { url = "https://files.pythonhosted.org/packages/f1/f3/59406396083f8b489261e3c011aa8aee9df360a96ac8fa5c2e7e1b8f0466/aiohttp-3.12.15-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:894261472691d6fe76ebb7fcf2e5870a2ac284c7406ddc95823c8598a1390f0d", size = 1686794, upload_time = "2025-07-29T05:51:27.145Z" }, - { url = "https://files.pythonhosted.org/packages/dc/71/164d194993a8d114ee5656c3b7ae9c12ceee7040d076bf7b32fb98a8c5c6/aiohttp-3.12.15-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5fa5d9eb82ce98959fc1031c28198b431b4d9396894f385cb63f1e2f3f20ca6b", size = 1738865, upload_time = "2025-07-29T05:51:29.366Z" }, - { url = "https://files.pythonhosted.org/packages/1c/00/d198461b699188a93ead39cb458554d9f0f69879b95078dce416d3209b54/aiohttp-3.12.15-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f0fa751efb11a541f57db59c1dd821bec09031e01452b2b6217319b3a1f34f3d", size = 1788238, upload_time = "2025-07-29T05:51:31.285Z" }, - { url = "https://files.pythonhosted.org/packages/85/b8/9e7175e1fa0ac8e56baa83bf3c214823ce250d0028955dfb23f43d5e61fd/aiohttp-3.12.15-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5346b93e62ab51ee2a9d68e8f73c7cf96ffb73568a23e683f931e52450e4148d", size = 1710566, upload_time = "2025-07-29T05:51:33.219Z" }, - { url = "https://files.pythonhosted.org/packages/59/e4/16a8eac9df39b48ae102ec030fa9f726d3570732e46ba0c592aeeb507b93/aiohttp-3.12.15-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:049ec0360f939cd164ecbfd2873eaa432613d5e77d6b04535e3d1fbae5a9e645", size = 1624270, upload_time = "2025-07-29T05:51:35.195Z" }, - { url = "https://files.pythonhosted.org/packages/1f/f8/cd84dee7b6ace0740908fd0af170f9fab50c2a41ccbc3806aabcb1050141/aiohttp-3.12.15-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b52dcf013b57464b6d1e51b627adfd69a8053e84b7103a7cd49c030f9ca44461", size = 1677294, upload_time = "2025-07-29T05:51:37.215Z" }, - { url = "https://files.pythonhosted.org/packages/ce/42/d0f1f85e50d401eccd12bf85c46ba84f947a84839c8a1c2c5f6e8ab1eb50/aiohttp-3.12.15-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:9b2af240143dd2765e0fb661fd0361a1b469cab235039ea57663cda087250ea9", size = 1708958, upload_time = "2025-07-29T05:51:39.328Z" }, - { url = "https://files.pythonhosted.org/packages/d5/6b/f6fa6c5790fb602538483aa5a1b86fcbad66244997e5230d88f9412ef24c/aiohttp-3.12.15-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ac77f709a2cde2cc71257ab2d8c74dd157c67a0558a0d2799d5d571b4c63d44d", size = 1651553, upload_time = "2025-07-29T05:51:41.356Z" }, - { url = "https://files.pythonhosted.org/packages/04/36/a6d36ad545fa12e61d11d1932eef273928b0495e6a576eb2af04297fdd3c/aiohttp-3.12.15-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:47f6b962246f0a774fbd3b6b7be25d59b06fdb2f164cf2513097998fc6a29693", size = 1727688, upload_time = "2025-07-29T05:51:43.452Z" }, - { url = "https://files.pythonhosted.org/packages/aa/c8/f195e5e06608a97a4e52c5d41c7927301bf757a8e8bb5bbf8cef6c314961/aiohttp-3.12.15-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:760fb7db442f284996e39cf9915a94492e1896baac44f06ae551974907922b64", size = 1761157, upload_time = "2025-07-29T05:51:45.643Z" }, - { url = "https://files.pythonhosted.org/packages/05/6a/ea199e61b67f25ba688d3ce93f63b49b0a4e3b3d380f03971b4646412fc6/aiohttp-3.12.15-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ad702e57dc385cae679c39d318def49aef754455f237499d5b99bea4ef582e51", size = 1710050, upload_time = "2025-07-29T05:51:48.203Z" }, - { url = "https://files.pythonhosted.org/packages/b4/2e/ffeb7f6256b33635c29dbed29a22a723ff2dd7401fff42ea60cf2060abfb/aiohttp-3.12.15-cp313-cp313-win32.whl", hash = "sha256:f813c3e9032331024de2eb2e32a88d86afb69291fbc37a3a3ae81cc9917fb3d0", size = 422647, upload_time = "2025-07-29T05:51:50.718Z" }, - { url = "https://files.pythonhosted.org/packages/1b/8e/78ee35774201f38d5e1ba079c9958f7629b1fd079459aea9467441dbfbf5/aiohttp-3.12.15-cp313-cp313-win_amd64.whl", hash = "sha256:1a649001580bdb37c6fdb1bebbd7e3bc688e8ec2b5c6f52edbb664662b17dc84", size = 449067, upload_time = "2025-07-29T05:51:52.549Z" }, + { url = "https://files.pythonhosted.org/packages/63/97/77cb2450d9b35f517d6cf506256bf4f5bda3f93a66b4ad64ba7fc917899c/aiohttp-3.12.15-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:802d3868f5776e28f7bf69d349c26fc0efadb81676d0afa88ed00d98a26340b7", size = 702333, upload-time = "2025-07-29T05:50:46.507Z" }, + { url = "https://files.pythonhosted.org/packages/83/6d/0544e6b08b748682c30b9f65640d006e51f90763b41d7c546693bc22900d/aiohttp-3.12.15-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f2800614cd560287be05e33a679638e586a2d7401f4ddf99e304d98878c29444", size = 476948, upload-time = "2025-07-29T05:50:48.067Z" }, + { url = "https://files.pythonhosted.org/packages/3a/1d/c8c40e611e5094330284b1aea8a4b02ca0858f8458614fa35754cab42b9c/aiohttp-3.12.15-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8466151554b593909d30a0a125d638b4e5f3836e5aecde85b66b80ded1cb5b0d", size = 469787, upload-time = "2025-07-29T05:50:49.669Z" }, + { url = "https://files.pythonhosted.org/packages/38/7d/b76438e70319796bfff717f325d97ce2e9310f752a267bfdf5192ac6082b/aiohttp-3.12.15-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e5a495cb1be69dae4b08f35a6c4579c539e9b5706f606632102c0f855bcba7c", size = 1716590, upload-time = "2025-07-29T05:50:51.368Z" }, + { url = "https://files.pythonhosted.org/packages/79/b1/60370d70cdf8b269ee1444b390cbd72ce514f0d1cd1a715821c784d272c9/aiohttp-3.12.15-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6404dfc8cdde35c69aaa489bb3542fb86ef215fc70277c892be8af540e5e21c0", size = 1699241, upload-time = "2025-07-29T05:50:53.628Z" }, + { url = "https://files.pythonhosted.org/packages/a3/2b/4968a7b8792437ebc12186db31523f541943e99bda8f30335c482bea6879/aiohttp-3.12.15-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3ead1c00f8521a5c9070fcb88f02967b1d8a0544e6d85c253f6968b785e1a2ab", size = 1754335, upload-time = "2025-07-29T05:50:55.394Z" }, + { url = "https://files.pythonhosted.org/packages/fb/c1/49524ed553f9a0bec1a11fac09e790f49ff669bcd14164f9fab608831c4d/aiohttp-3.12.15-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6990ef617f14450bc6b34941dba4f12d5613cbf4e33805932f853fbd1cf18bfb", size = 1800491, upload-time = "2025-07-29T05:50:57.202Z" }, + { url = "https://files.pythonhosted.org/packages/de/5e/3bf5acea47a96a28c121b167f5ef659cf71208b19e52a88cdfa5c37f1fcc/aiohttp-3.12.15-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd736ed420f4db2b8148b52b46b88ed038d0354255f9a73196b7bbce3ea97545", size = 1719929, upload-time = "2025-07-29T05:50:59.192Z" }, + { url = "https://files.pythonhosted.org/packages/39/94/8ae30b806835bcd1cba799ba35347dee6961a11bd507db634516210e91d8/aiohttp-3.12.15-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c5092ce14361a73086b90c6efb3948ffa5be2f5b6fbcf52e8d8c8b8848bb97c", size = 1635733, upload-time = "2025-07-29T05:51:01.394Z" }, + { url = "https://files.pythonhosted.org/packages/7a/46/06cdef71dd03acd9da7f51ab3a9107318aee12ad38d273f654e4f981583a/aiohttp-3.12.15-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:aaa2234bb60c4dbf82893e934d8ee8dea30446f0647e024074237a56a08c01bd", size = 1696790, upload-time = "2025-07-29T05:51:03.657Z" }, + { url = "https://files.pythonhosted.org/packages/02/90/6b4cfaaf92ed98d0ec4d173e78b99b4b1a7551250be8937d9d67ecb356b4/aiohttp-3.12.15-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:6d86a2fbdd14192e2f234a92d3b494dd4457e683ba07e5905a0b3ee25389ac9f", size = 1718245, upload-time = "2025-07-29T05:51:05.911Z" }, + { url = "https://files.pythonhosted.org/packages/2e/e6/2593751670fa06f080a846f37f112cbe6f873ba510d070136a6ed46117c6/aiohttp-3.12.15-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a041e7e2612041a6ddf1c6a33b883be6a421247c7afd47e885969ee4cc58bd8d", size = 1658899, upload-time = "2025-07-29T05:51:07.753Z" }, + { url = "https://files.pythonhosted.org/packages/8f/28/c15bacbdb8b8eb5bf39b10680d129ea7410b859e379b03190f02fa104ffd/aiohttp-3.12.15-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5015082477abeafad7203757ae44299a610e89ee82a1503e3d4184e6bafdd519", size = 1738459, upload-time = "2025-07-29T05:51:09.56Z" }, + { url = "https://files.pythonhosted.org/packages/00/de/c269cbc4faa01fb10f143b1670633a8ddd5b2e1ffd0548f7aa49cb5c70e2/aiohttp-3.12.15-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:56822ff5ddfd1b745534e658faba944012346184fbfe732e0d6134b744516eea", size = 1766434, upload-time = "2025-07-29T05:51:11.423Z" }, + { url = "https://files.pythonhosted.org/packages/52/b0/4ff3abd81aa7d929b27d2e1403722a65fc87b763e3a97b3a2a494bfc63bc/aiohttp-3.12.15-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b2acbbfff69019d9014508c4ba0401822e8bae5a5fdc3b6814285b71231b60f3", size = 1726045, upload-time = "2025-07-29T05:51:13.689Z" }, + { url = "https://files.pythonhosted.org/packages/71/16/949225a6a2dd6efcbd855fbd90cf476052e648fb011aa538e3b15b89a57a/aiohttp-3.12.15-cp312-cp312-win32.whl", hash = "sha256:d849b0901b50f2185874b9a232f38e26b9b3d4810095a7572eacea939132d4e1", size = 423591, upload-time = "2025-07-29T05:51:15.452Z" }, + { url = "https://files.pythonhosted.org/packages/2b/d8/fa65d2a349fe938b76d309db1a56a75c4fb8cc7b17a398b698488a939903/aiohttp-3.12.15-cp312-cp312-win_amd64.whl", hash = "sha256:b390ef5f62bb508a9d67cb3bba9b8356e23b3996da7062f1a57ce1a79d2b3d34", size = 450266, upload-time = "2025-07-29T05:51:17.239Z" }, + { url = "https://files.pythonhosted.org/packages/f2/33/918091abcf102e39d15aba2476ad9e7bd35ddb190dcdd43a854000d3da0d/aiohttp-3.12.15-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:9f922ffd05034d439dde1c77a20461cf4a1b0831e6caa26151fe7aa8aaebc315", size = 696741, upload-time = "2025-07-29T05:51:19.021Z" }, + { url = "https://files.pythonhosted.org/packages/b5/2a/7495a81e39a998e400f3ecdd44a62107254803d1681d9189be5c2e4530cd/aiohttp-3.12.15-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2ee8a8ac39ce45f3e55663891d4b1d15598c157b4d494a4613e704c8b43112cd", size = 474407, upload-time = "2025-07-29T05:51:21.165Z" }, + { url = "https://files.pythonhosted.org/packages/49/fc/a9576ab4be2dcbd0f73ee8675d16c707cfc12d5ee80ccf4015ba543480c9/aiohttp-3.12.15-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3eae49032c29d356b94eee45a3f39fdf4b0814b397638c2f718e96cfadf4c4e4", size = 466703, upload-time = "2025-07-29T05:51:22.948Z" }, + { url = "https://files.pythonhosted.org/packages/09/2f/d4bcc8448cf536b2b54eed48f19682031ad182faa3a3fee54ebe5b156387/aiohttp-3.12.15-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b97752ff12cc12f46a9b20327104448042fce5c33a624f88c18f66f9368091c7", size = 1705532, upload-time = "2025-07-29T05:51:25.211Z" }, + { url = "https://files.pythonhosted.org/packages/f1/f3/59406396083f8b489261e3c011aa8aee9df360a96ac8fa5c2e7e1b8f0466/aiohttp-3.12.15-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:894261472691d6fe76ebb7fcf2e5870a2ac284c7406ddc95823c8598a1390f0d", size = 1686794, upload-time = "2025-07-29T05:51:27.145Z" }, + { url = "https://files.pythonhosted.org/packages/dc/71/164d194993a8d114ee5656c3b7ae9c12ceee7040d076bf7b32fb98a8c5c6/aiohttp-3.12.15-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5fa5d9eb82ce98959fc1031c28198b431b4d9396894f385cb63f1e2f3f20ca6b", size = 1738865, upload-time = "2025-07-29T05:51:29.366Z" }, + { url = "https://files.pythonhosted.org/packages/1c/00/d198461b699188a93ead39cb458554d9f0f69879b95078dce416d3209b54/aiohttp-3.12.15-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f0fa751efb11a541f57db59c1dd821bec09031e01452b2b6217319b3a1f34f3d", size = 1788238, upload-time = "2025-07-29T05:51:31.285Z" }, + { url = "https://files.pythonhosted.org/packages/85/b8/9e7175e1fa0ac8e56baa83bf3c214823ce250d0028955dfb23f43d5e61fd/aiohttp-3.12.15-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5346b93e62ab51ee2a9d68e8f73c7cf96ffb73568a23e683f931e52450e4148d", size = 1710566, upload-time = "2025-07-29T05:51:33.219Z" }, + { url = "https://files.pythonhosted.org/packages/59/e4/16a8eac9df39b48ae102ec030fa9f726d3570732e46ba0c592aeeb507b93/aiohttp-3.12.15-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:049ec0360f939cd164ecbfd2873eaa432613d5e77d6b04535e3d1fbae5a9e645", size = 1624270, upload-time = "2025-07-29T05:51:35.195Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f8/cd84dee7b6ace0740908fd0af170f9fab50c2a41ccbc3806aabcb1050141/aiohttp-3.12.15-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b52dcf013b57464b6d1e51b627adfd69a8053e84b7103a7cd49c030f9ca44461", size = 1677294, upload-time = "2025-07-29T05:51:37.215Z" }, + { url = "https://files.pythonhosted.org/packages/ce/42/d0f1f85e50d401eccd12bf85c46ba84f947a84839c8a1c2c5f6e8ab1eb50/aiohttp-3.12.15-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:9b2af240143dd2765e0fb661fd0361a1b469cab235039ea57663cda087250ea9", size = 1708958, upload-time = "2025-07-29T05:51:39.328Z" }, + { url = "https://files.pythonhosted.org/packages/d5/6b/f6fa6c5790fb602538483aa5a1b86fcbad66244997e5230d88f9412ef24c/aiohttp-3.12.15-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ac77f709a2cde2cc71257ab2d8c74dd157c67a0558a0d2799d5d571b4c63d44d", size = 1651553, upload-time = "2025-07-29T05:51:41.356Z" }, + { url = "https://files.pythonhosted.org/packages/04/36/a6d36ad545fa12e61d11d1932eef273928b0495e6a576eb2af04297fdd3c/aiohttp-3.12.15-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:47f6b962246f0a774fbd3b6b7be25d59b06fdb2f164cf2513097998fc6a29693", size = 1727688, upload-time = "2025-07-29T05:51:43.452Z" }, + { url = "https://files.pythonhosted.org/packages/aa/c8/f195e5e06608a97a4e52c5d41c7927301bf757a8e8bb5bbf8cef6c314961/aiohttp-3.12.15-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:760fb7db442f284996e39cf9915a94492e1896baac44f06ae551974907922b64", size = 1761157, upload-time = "2025-07-29T05:51:45.643Z" }, + { url = "https://files.pythonhosted.org/packages/05/6a/ea199e61b67f25ba688d3ce93f63b49b0a4e3b3d380f03971b4646412fc6/aiohttp-3.12.15-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ad702e57dc385cae679c39d318def49aef754455f237499d5b99bea4ef582e51", size = 1710050, upload-time = "2025-07-29T05:51:48.203Z" }, + { url = "https://files.pythonhosted.org/packages/b4/2e/ffeb7f6256b33635c29dbed29a22a723ff2dd7401fff42ea60cf2060abfb/aiohttp-3.12.15-cp313-cp313-win32.whl", hash = "sha256:f813c3e9032331024de2eb2e32a88d86afb69291fbc37a3a3ae81cc9917fb3d0", size = 422647, upload-time = "2025-07-29T05:51:50.718Z" }, + { url = "https://files.pythonhosted.org/packages/1b/8e/78ee35774201f38d5e1ba079c9958f7629b1fd079459aea9467441dbfbf5/aiohttp-3.12.15-cp313-cp313-win_amd64.whl", hash = "sha256:1a649001580bdb37c6fdb1bebbd7e3bc688e8ec2b5c6f52edbb664662b17dc84", size = 449067, upload-time = "2025-07-29T05:51:52.549Z" }, ] [[package]] @@ -82,36 +82,36 @@ dependencies = [ { name = "frozenlist" }, { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/61/62/06741b579156360248d1ec624842ad0edf697050bbaf7c3e46394e106ad1/aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7", size = 25007, upload_time = "2025-07-03T22:54:43.528Z" } +sdist = { url = "https://files.pythonhosted.org/packages/61/62/06741b579156360248d1ec624842ad0edf697050bbaf7c3e46394e106ad1/aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7", size = 25007, upload-time = "2025-07-03T22:54:43.528Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", size = 7490, upload_time = "2025-07-03T22:54:42.156Z" }, + { url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", size = 7490, upload-time = "2025-07-03T22:54:42.156Z" }, ] [[package]] name = "alt-utils" version = "0.0.8" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/31/15/67246107a8c808a9e99b34fd0024bebe954a67f3c315821eae985b87db7f/alt_utils-0.0.8.tar.gz", hash = "sha256:4b2901df0be4af736210277d58e231d4c4bce597a8fc665a8dd3e7b582705081", size = 6103, upload_time = "2025-05-10T19:36:49.187Z" } +sdist = { url = "https://files.pythonhosted.org/packages/31/15/67246107a8c808a9e99b34fd0024bebe954a67f3c315821eae985b87db7f/alt_utils-0.0.8.tar.gz", hash = "sha256:4b2901df0be4af736210277d58e231d4c4bce597a8fc665a8dd3e7b582705081", size = 6103, upload-time = "2025-05-10T19:36:49.187Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9a/5a/7fe15b55fa0ff5528643750c409cd14da005406aef312b32512d8a8487ab/alt_utils-0.0.8-py3-none-any.whl", hash = "sha256:af5549c49543ff4a02b735308bc2a5bfb7f20755620652fd969a648bbaecbc47", size = 6378, upload_time = "2025-05-10T19:36:47.954Z" }, + { url = "https://files.pythonhosted.org/packages/9a/5a/7fe15b55fa0ff5528643750c409cd14da005406aef312b32512d8a8487ab/alt_utils-0.0.8-py3-none-any.whl", hash = "sha256:af5549c49543ff4a02b735308bc2a5bfb7f20755620652fd969a648bbaecbc47", size = 6378, upload-time = "2025-05-10T19:36:47.954Z" }, ] [[package]] name = "attrs" version = "25.3.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/5a/b0/1367933a8532ee6ff8d63537de4f1177af4bff9f3e829baf7331f595bb24/attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b", size = 812032, upload_time = "2025-03-13T11:10:22.779Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/b0/1367933a8532ee6ff8d63537de4f1177af4bff9f3e829baf7331f595bb24/attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b", size = 812032, upload-time = "2025-03-13T11:10:22.779Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/77/06/bb80f5f86020c4551da315d78b3ab75e8228f89f0162f2c3a819e407941a/attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3", size = 63815, upload_time = "2025-03-13T11:10:21.14Z" }, + { url = "https://files.pythonhosted.org/packages/77/06/bb80f5f86020c4551da315d78b3ab75e8228f89f0162f2c3a819e407941a/attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3", size = 63815, upload-time = "2025-03-13T11:10:21.14Z" }, ] [[package]] name = "certifi" version = "2025.8.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/dc/67/960ebe6bf230a96cda2e0abcf73af550ec4f090005363542f0765df162e0/certifi-2025.8.3.tar.gz", hash = "sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407", size = 162386, upload_time = "2025-08-03T03:07:47.08Z" } +sdist = { url = "https://files.pythonhosted.org/packages/dc/67/960ebe6bf230a96cda2e0abcf73af550ec4f090005363542f0765df162e0/certifi-2025.8.3.tar.gz", hash = "sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407", size = 162386, upload-time = "2025-08-03T03:07:47.08Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e5/48/1549795ba7742c948d2ad169c1c8cdbae65bc450d6cd753d124b17c8cd32/certifi-2025.8.3-py3-none-any.whl", hash = "sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5", size = 161216, upload_time = "2025-08-03T03:07:45.777Z" }, + { url = "https://files.pythonhosted.org/packages/e5/48/1549795ba7742c948d2ad169c1c8cdbae65bc450d6cd753d124b17c8cd32/certifi-2025.8.3-py3-none-any.whl", hash = "sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5", size = 161216, upload-time = "2025-08-03T03:07:45.777Z" }, ] [[package]] @@ -121,155 +121,155 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pycparser" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621, upload_time = "2024-09-04T20:45:21.852Z" } +sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621, upload-time = "2024-09-04T20:45:21.852Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5a/84/e94227139ee5fb4d600a7a4927f322e1d4aea6fdc50bd3fca8493caba23f/cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4", size = 183178, upload_time = "2024-09-04T20:44:12.232Z" }, - { url = "https://files.pythonhosted.org/packages/da/ee/fb72c2b48656111c4ef27f0f91da355e130a923473bf5ee75c5643d00cca/cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c", size = 178840, upload_time = "2024-09-04T20:44:13.739Z" }, - { url = "https://files.pythonhosted.org/packages/cc/b6/db007700f67d151abadf508cbfd6a1884f57eab90b1bb985c4c8c02b0f28/cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36", size = 454803, upload_time = "2024-09-04T20:44:15.231Z" }, - { url = "https://files.pythonhosted.org/packages/1a/df/f8d151540d8c200eb1c6fba8cd0dfd40904f1b0682ea705c36e6c2e97ab3/cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5", size = 478850, upload_time = "2024-09-04T20:44:17.188Z" }, - { url = "https://files.pythonhosted.org/packages/28/c0/b31116332a547fd2677ae5b78a2ef662dfc8023d67f41b2a83f7c2aa78b1/cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff", size = 485729, upload_time = "2024-09-04T20:44:18.688Z" }, - { url = "https://files.pythonhosted.org/packages/91/2b/9a1ddfa5c7f13cab007a2c9cc295b70fbbda7cb10a286aa6810338e60ea1/cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99", size = 471256, upload_time = "2024-09-04T20:44:20.248Z" }, - { url = "https://files.pythonhosted.org/packages/b2/d5/da47df7004cb17e4955df6a43d14b3b4ae77737dff8bf7f8f333196717bf/cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93", size = 479424, upload_time = "2024-09-04T20:44:21.673Z" }, - { url = "https://files.pythonhosted.org/packages/0b/ac/2a28bcf513e93a219c8a4e8e125534f4f6db03e3179ba1c45e949b76212c/cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3", size = 484568, upload_time = "2024-09-04T20:44:23.245Z" }, - { url = "https://files.pythonhosted.org/packages/d4/38/ca8a4f639065f14ae0f1d9751e70447a261f1a30fa7547a828ae08142465/cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8", size = 488736, upload_time = "2024-09-04T20:44:24.757Z" }, - { url = "https://files.pythonhosted.org/packages/86/c5/28b2d6f799ec0bdecf44dced2ec5ed43e0eb63097b0f58c293583b406582/cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65", size = 172448, upload_time = "2024-09-04T20:44:26.208Z" }, - { url = "https://files.pythonhosted.org/packages/50/b9/db34c4755a7bd1cb2d1603ac3863f22bcecbd1ba29e5ee841a4bc510b294/cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903", size = 181976, upload_time = "2024-09-04T20:44:27.578Z" }, - { url = "https://files.pythonhosted.org/packages/8d/f8/dd6c246b148639254dad4d6803eb6a54e8c85c6e11ec9df2cffa87571dbe/cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e", size = 182989, upload_time = "2024-09-04T20:44:28.956Z" }, - { url = "https://files.pythonhosted.org/packages/8b/f1/672d303ddf17c24fc83afd712316fda78dc6fce1cd53011b839483e1ecc8/cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2", size = 178802, upload_time = "2024-09-04T20:44:30.289Z" }, - { url = "https://files.pythonhosted.org/packages/0e/2d/eab2e858a91fdff70533cab61dcff4a1f55ec60425832ddfdc9cd36bc8af/cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3", size = 454792, upload_time = "2024-09-04T20:44:32.01Z" }, - { url = "https://files.pythonhosted.org/packages/75/b2/fbaec7c4455c604e29388d55599b99ebcc250a60050610fadde58932b7ee/cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683", size = 478893, upload_time = "2024-09-04T20:44:33.606Z" }, - { url = "https://files.pythonhosted.org/packages/4f/b7/6e4a2162178bf1935c336d4da8a9352cccab4d3a5d7914065490f08c0690/cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5", size = 485810, upload_time = "2024-09-04T20:44:35.191Z" }, - { url = "https://files.pythonhosted.org/packages/c7/8a/1d0e4a9c26e54746dc08c2c6c037889124d4f59dffd853a659fa545f1b40/cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4", size = 471200, upload_time = "2024-09-04T20:44:36.743Z" }, - { url = "https://files.pythonhosted.org/packages/26/9f/1aab65a6c0db35f43c4d1b4f580e8df53914310afc10ae0397d29d697af4/cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd", size = 479447, upload_time = "2024-09-04T20:44:38.492Z" }, - { url = "https://files.pythonhosted.org/packages/5f/e4/fb8b3dd8dc0e98edf1135ff067ae070bb32ef9d509d6cb0f538cd6f7483f/cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed", size = 484358, upload_time = "2024-09-04T20:44:40.046Z" }, - { url = "https://files.pythonhosted.org/packages/f1/47/d7145bf2dc04684935d57d67dff9d6d795b2ba2796806bb109864be3a151/cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9", size = 488469, upload_time = "2024-09-04T20:44:41.616Z" }, - { url = "https://files.pythonhosted.org/packages/bf/ee/f94057fa6426481d663b88637a9a10e859e492c73d0384514a17d78ee205/cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d", size = 172475, upload_time = "2024-09-04T20:44:43.733Z" }, - { url = "https://files.pythonhosted.org/packages/7c/fc/6a8cb64e5f0324877d503c854da15d76c1e50eb722e320b15345c4d0c6de/cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", size = 182009, upload_time = "2024-09-04T20:44:45.309Z" }, + { url = "https://files.pythonhosted.org/packages/5a/84/e94227139ee5fb4d600a7a4927f322e1d4aea6fdc50bd3fca8493caba23f/cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4", size = 183178, upload-time = "2024-09-04T20:44:12.232Z" }, + { url = "https://files.pythonhosted.org/packages/da/ee/fb72c2b48656111c4ef27f0f91da355e130a923473bf5ee75c5643d00cca/cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c", size = 178840, upload-time = "2024-09-04T20:44:13.739Z" }, + { url = "https://files.pythonhosted.org/packages/cc/b6/db007700f67d151abadf508cbfd6a1884f57eab90b1bb985c4c8c02b0f28/cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36", size = 454803, upload-time = "2024-09-04T20:44:15.231Z" }, + { url = "https://files.pythonhosted.org/packages/1a/df/f8d151540d8c200eb1c6fba8cd0dfd40904f1b0682ea705c36e6c2e97ab3/cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5", size = 478850, upload-time = "2024-09-04T20:44:17.188Z" }, + { url = "https://files.pythonhosted.org/packages/28/c0/b31116332a547fd2677ae5b78a2ef662dfc8023d67f41b2a83f7c2aa78b1/cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff", size = 485729, upload-time = "2024-09-04T20:44:18.688Z" }, + { url = "https://files.pythonhosted.org/packages/91/2b/9a1ddfa5c7f13cab007a2c9cc295b70fbbda7cb10a286aa6810338e60ea1/cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99", size = 471256, upload-time = "2024-09-04T20:44:20.248Z" }, + { url = "https://files.pythonhosted.org/packages/b2/d5/da47df7004cb17e4955df6a43d14b3b4ae77737dff8bf7f8f333196717bf/cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93", size = 479424, upload-time = "2024-09-04T20:44:21.673Z" }, + { url = "https://files.pythonhosted.org/packages/0b/ac/2a28bcf513e93a219c8a4e8e125534f4f6db03e3179ba1c45e949b76212c/cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3", size = 484568, upload-time = "2024-09-04T20:44:23.245Z" }, + { url = "https://files.pythonhosted.org/packages/d4/38/ca8a4f639065f14ae0f1d9751e70447a261f1a30fa7547a828ae08142465/cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8", size = 488736, upload-time = "2024-09-04T20:44:24.757Z" }, + { url = "https://files.pythonhosted.org/packages/86/c5/28b2d6f799ec0bdecf44dced2ec5ed43e0eb63097b0f58c293583b406582/cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65", size = 172448, upload-time = "2024-09-04T20:44:26.208Z" }, + { url = "https://files.pythonhosted.org/packages/50/b9/db34c4755a7bd1cb2d1603ac3863f22bcecbd1ba29e5ee841a4bc510b294/cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903", size = 181976, upload-time = "2024-09-04T20:44:27.578Z" }, + { url = "https://files.pythonhosted.org/packages/8d/f8/dd6c246b148639254dad4d6803eb6a54e8c85c6e11ec9df2cffa87571dbe/cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e", size = 182989, upload-time = "2024-09-04T20:44:28.956Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f1/672d303ddf17c24fc83afd712316fda78dc6fce1cd53011b839483e1ecc8/cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2", size = 178802, upload-time = "2024-09-04T20:44:30.289Z" }, + { url = "https://files.pythonhosted.org/packages/0e/2d/eab2e858a91fdff70533cab61dcff4a1f55ec60425832ddfdc9cd36bc8af/cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3", size = 454792, upload-time = "2024-09-04T20:44:32.01Z" }, + { url = "https://files.pythonhosted.org/packages/75/b2/fbaec7c4455c604e29388d55599b99ebcc250a60050610fadde58932b7ee/cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683", size = 478893, upload-time = "2024-09-04T20:44:33.606Z" }, + { url = "https://files.pythonhosted.org/packages/4f/b7/6e4a2162178bf1935c336d4da8a9352cccab4d3a5d7914065490f08c0690/cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5", size = 485810, upload-time = "2024-09-04T20:44:35.191Z" }, + { url = "https://files.pythonhosted.org/packages/c7/8a/1d0e4a9c26e54746dc08c2c6c037889124d4f59dffd853a659fa545f1b40/cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4", size = 471200, upload-time = "2024-09-04T20:44:36.743Z" }, + { url = "https://files.pythonhosted.org/packages/26/9f/1aab65a6c0db35f43c4d1b4f580e8df53914310afc10ae0397d29d697af4/cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd", size = 479447, upload-time = "2024-09-04T20:44:38.492Z" }, + { url = "https://files.pythonhosted.org/packages/5f/e4/fb8b3dd8dc0e98edf1135ff067ae070bb32ef9d509d6cb0f538cd6f7483f/cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed", size = 484358, upload-time = "2024-09-04T20:44:40.046Z" }, + { url = "https://files.pythonhosted.org/packages/f1/47/d7145bf2dc04684935d57d67dff9d6d795b2ba2796806bb109864be3a151/cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9", size = 488469, upload-time = "2024-09-04T20:44:41.616Z" }, + { url = "https://files.pythonhosted.org/packages/bf/ee/f94057fa6426481d663b88637a9a10e859e492c73d0384514a17d78ee205/cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d", size = 172475, upload-time = "2024-09-04T20:44:43.733Z" }, + { url = "https://files.pythonhosted.org/packages/7c/fc/6a8cb64e5f0324877d503c854da15d76c1e50eb722e320b15345c4d0c6de/cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", size = 182009, upload-time = "2024-09-04T20:44:45.309Z" }, ] [[package]] name = "charset-normalizer" version = "3.4.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/83/2d/5fd176ceb9b2fc619e63405525573493ca23441330fcdaee6bef9460e924/charset_normalizer-3.4.3.tar.gz", hash = "sha256:6fce4b8500244f6fcb71465d4a4930d132ba9ab8e71a7859e6a5d59851068d14", size = 122371, upload_time = "2025-08-09T07:57:28.46Z" } +sdist = { url = "https://files.pythonhosted.org/packages/83/2d/5fd176ceb9b2fc619e63405525573493ca23441330fcdaee6bef9460e924/charset_normalizer-3.4.3.tar.gz", hash = "sha256:6fce4b8500244f6fcb71465d4a4930d132ba9ab8e71a7859e6a5d59851068d14", size = 122371, upload-time = "2025-08-09T07:57:28.46Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e9/5e/14c94999e418d9b87682734589404a25854d5f5d0408df68bc15b6ff54bb/charset_normalizer-3.4.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e28e334d3ff134e88989d90ba04b47d84382a828c061d0d1027b1b12a62b39b1", size = 205655, upload_time = "2025-08-09T07:56:08.475Z" }, - { url = "https://files.pythonhosted.org/packages/7d/a8/c6ec5d389672521f644505a257f50544c074cf5fc292d5390331cd6fc9c3/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0cacf8f7297b0c4fcb74227692ca46b4a5852f8f4f24b3c766dd94a1075c4884", size = 146223, upload_time = "2025-08-09T07:56:09.708Z" }, - { url = "https://files.pythonhosted.org/packages/fc/eb/a2ffb08547f4e1e5415fb69eb7db25932c52a52bed371429648db4d84fb1/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c6fd51128a41297f5409deab284fecbe5305ebd7e5a1f959bee1c054622b7018", size = 159366, upload_time = "2025-08-09T07:56:11.326Z" }, - { url = "https://files.pythonhosted.org/packages/82/10/0fd19f20c624b278dddaf83b8464dcddc2456cb4b02bb902a6da126b87a1/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3cfb2aad70f2c6debfbcb717f23b7eb55febc0bb23dcffc0f076009da10c6392", size = 157104, upload_time = "2025-08-09T07:56:13.014Z" }, - { url = "https://files.pythonhosted.org/packages/16/ab/0233c3231af734f5dfcf0844aa9582d5a1466c985bbed6cedab85af9bfe3/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1606f4a55c0fd363d754049cdf400175ee96c992b1f8018b993941f221221c5f", size = 151830, upload_time = "2025-08-09T07:56:14.428Z" }, - { url = "https://files.pythonhosted.org/packages/ae/02/e29e22b4e02839a0e4a06557b1999d0a47db3567e82989b5bb21f3fbbd9f/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:027b776c26d38b7f15b26a5da1044f376455fb3766df8fc38563b4efbc515154", size = 148854, upload_time = "2025-08-09T07:56:16.051Z" }, - { url = "https://files.pythonhosted.org/packages/05/6b/e2539a0a4be302b481e8cafb5af8792da8093b486885a1ae4d15d452bcec/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:42e5088973e56e31e4fa58eb6bd709e42fc03799c11c42929592889a2e54c491", size = 160670, upload_time = "2025-08-09T07:56:17.314Z" }, - { url = "https://files.pythonhosted.org/packages/31/e7/883ee5676a2ef217a40ce0bffcc3d0dfbf9e64cbcfbdf822c52981c3304b/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:cc34f233c9e71701040d772aa7490318673aa7164a0efe3172b2981218c26d93", size = 158501, upload_time = "2025-08-09T07:56:18.641Z" }, - { url = "https://files.pythonhosted.org/packages/c1/35/6525b21aa0db614cf8b5792d232021dca3df7f90a1944db934efa5d20bb1/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:320e8e66157cc4e247d9ddca8e21f427efc7a04bbd0ac8a9faf56583fa543f9f", size = 153173, upload_time = "2025-08-09T07:56:20.289Z" }, - { url = "https://files.pythonhosted.org/packages/50/ee/f4704bad8201de513fdc8aac1cabc87e38c5818c93857140e06e772b5892/charset_normalizer-3.4.3-cp312-cp312-win32.whl", hash = "sha256:fb6fecfd65564f208cbf0fba07f107fb661bcd1a7c389edbced3f7a493f70e37", size = 99822, upload_time = "2025-08-09T07:56:21.551Z" }, - { url = "https://files.pythonhosted.org/packages/39/f5/3b3836ca6064d0992c58c7561c6b6eee1b3892e9665d650c803bd5614522/charset_normalizer-3.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:86df271bf921c2ee3818f0522e9a5b8092ca2ad8b065ece5d7d9d0e9f4849bcc", size = 107543, upload_time = "2025-08-09T07:56:23.115Z" }, - { url = "https://files.pythonhosted.org/packages/65/ca/2135ac97709b400c7654b4b764daf5c5567c2da45a30cdd20f9eefe2d658/charset_normalizer-3.4.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:14c2a87c65b351109f6abfc424cab3927b3bdece6f706e4d12faaf3d52ee5efe", size = 205326, upload_time = "2025-08-09T07:56:24.721Z" }, - { url = "https://files.pythonhosted.org/packages/71/11/98a04c3c97dd34e49c7d247083af03645ca3730809a5509443f3c37f7c99/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41d1fc408ff5fdfb910200ec0e74abc40387bccb3252f3f27c0676731df2b2c8", size = 146008, upload_time = "2025-08-09T07:56:26.004Z" }, - { url = "https://files.pythonhosted.org/packages/60/f5/4659a4cb3c4ec146bec80c32d8bb16033752574c20b1252ee842a95d1a1e/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1bb60174149316da1c35fa5233681f7c0f9f514509b8e399ab70fea5f17e45c9", size = 159196, upload_time = "2025-08-09T07:56:27.25Z" }, - { url = "https://files.pythonhosted.org/packages/86/9e/f552f7a00611f168b9a5865a1414179b2c6de8235a4fa40189f6f79a1753/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:30d006f98569de3459c2fc1f2acde170b7b2bd265dc1943e87e1a4efe1b67c31", size = 156819, upload_time = "2025-08-09T07:56:28.515Z" }, - { url = "https://files.pythonhosted.org/packages/7e/95/42aa2156235cbc8fa61208aded06ef46111c4d3f0de233107b3f38631803/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:416175faf02e4b0810f1f38bcb54682878a4af94059a1cd63b8747244420801f", size = 151350, upload_time = "2025-08-09T07:56:29.716Z" }, - { url = "https://files.pythonhosted.org/packages/c2/a9/3865b02c56f300a6f94fc631ef54f0a8a29da74fb45a773dfd3dcd380af7/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6aab0f181c486f973bc7262a97f5aca3ee7e1437011ef0c2ec04b5a11d16c927", size = 148644, upload_time = "2025-08-09T07:56:30.984Z" }, - { url = "https://files.pythonhosted.org/packages/77/d9/cbcf1a2a5c7d7856f11e7ac2d782aec12bdfea60d104e60e0aa1c97849dc/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:fdabf8315679312cfa71302f9bd509ded4f2f263fb5b765cf1433b39106c3cc9", size = 160468, upload_time = "2025-08-09T07:56:32.252Z" }, - { url = "https://files.pythonhosted.org/packages/f6/42/6f45efee8697b89fda4d50580f292b8f7f9306cb2971d4b53f8914e4d890/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:bd28b817ea8c70215401f657edef3a8aa83c29d447fb0b622c35403780ba11d5", size = 158187, upload_time = "2025-08-09T07:56:33.481Z" }, - { url = "https://files.pythonhosted.org/packages/70/99/f1c3bdcfaa9c45b3ce96f70b14f070411366fa19549c1d4832c935d8e2c3/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:18343b2d246dc6761a249ba1fb13f9ee9a2bcd95decc767319506056ea4ad4dc", size = 152699, upload_time = "2025-08-09T07:56:34.739Z" }, - { url = "https://files.pythonhosted.org/packages/a3/ad/b0081f2f99a4b194bcbb1934ef3b12aa4d9702ced80a37026b7607c72e58/charset_normalizer-3.4.3-cp313-cp313-win32.whl", hash = "sha256:6fb70de56f1859a3f71261cbe41005f56a7842cc348d3aeb26237560bfa5e0ce", size = 99580, upload_time = "2025-08-09T07:56:35.981Z" }, - { url = "https://files.pythonhosted.org/packages/9a/8f/ae790790c7b64f925e5c953b924aaa42a243fb778fed9e41f147b2a5715a/charset_normalizer-3.4.3-cp313-cp313-win_amd64.whl", hash = "sha256:cf1ebb7d78e1ad8ec2a8c4732c7be2e736f6e5123a4146c5b89c9d1f585f8cef", size = 107366, upload_time = "2025-08-09T07:56:37.339Z" }, - { url = "https://files.pythonhosted.org/packages/8e/91/b5a06ad970ddc7a0e513112d40113e834638f4ca1120eb727a249fb2715e/charset_normalizer-3.4.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3cd35b7e8aedeb9e34c41385fda4f73ba609e561faedfae0a9e75e44ac558a15", size = 204342, upload_time = "2025-08-09T07:56:38.687Z" }, - { url = "https://files.pythonhosted.org/packages/ce/ec/1edc30a377f0a02689342f214455c3f6c2fbedd896a1d2f856c002fc3062/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b89bc04de1d83006373429975f8ef9e7932534b8cc9ca582e4db7d20d91816db", size = 145995, upload_time = "2025-08-09T07:56:40.048Z" }, - { url = "https://files.pythonhosted.org/packages/17/e5/5e67ab85e6d22b04641acb5399c8684f4d37caf7558a53859f0283a650e9/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2001a39612b241dae17b4687898843f254f8748b796a2e16f1051a17078d991d", size = 158640, upload_time = "2025-08-09T07:56:41.311Z" }, - { url = "https://files.pythonhosted.org/packages/f1/e5/38421987f6c697ee3722981289d554957c4be652f963d71c5e46a262e135/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8dcfc373f888e4fb39a7bc57e93e3b845e7f462dacc008d9749568b1c4ece096", size = 156636, upload_time = "2025-08-09T07:56:43.195Z" }, - { url = "https://files.pythonhosted.org/packages/a0/e4/5a075de8daa3ec0745a9a3b54467e0c2967daaaf2cec04c845f73493e9a1/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18b97b8404387b96cdbd30ad660f6407799126d26a39ca65729162fd810a99aa", size = 150939, upload_time = "2025-08-09T07:56:44.819Z" }, - { url = "https://files.pythonhosted.org/packages/02/f7/3611b32318b30974131db62b4043f335861d4d9b49adc6d57c1149cc49d4/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ccf600859c183d70eb47e05a44cd80a4ce77394d1ac0f79dbd2dd90a69a3a049", size = 148580, upload_time = "2025-08-09T07:56:46.684Z" }, - { url = "https://files.pythonhosted.org/packages/7e/61/19b36f4bd67f2793ab6a99b979b4e4f3d8fc754cbdffb805335df4337126/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:53cd68b185d98dde4ad8990e56a58dea83a4162161b1ea9272e5c9182ce415e0", size = 159870, upload_time = "2025-08-09T07:56:47.941Z" }, - { url = "https://files.pythonhosted.org/packages/06/57/84722eefdd338c04cf3030ada66889298eaedf3e7a30a624201e0cbe424a/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:30a96e1e1f865f78b030d65241c1ee850cdf422d869e9028e2fc1d5e4db73b92", size = 157797, upload_time = "2025-08-09T07:56:49.756Z" }, - { url = "https://files.pythonhosted.org/packages/72/2a/aff5dd112b2f14bcc3462c312dce5445806bfc8ab3a7328555da95330e4b/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d716a916938e03231e86e43782ca7878fb602a125a91e7acb8b5112e2e96ac16", size = 152224, upload_time = "2025-08-09T07:56:51.369Z" }, - { url = "https://files.pythonhosted.org/packages/b7/8c/9839225320046ed279c6e839d51f028342eb77c91c89b8ef2549f951f3ec/charset_normalizer-3.4.3-cp314-cp314-win32.whl", hash = "sha256:c6dbd0ccdda3a2ba7c2ecd9d77b37f3b5831687d8dc1b6ca5f56a4880cc7b7ce", size = 100086, upload_time = "2025-08-09T07:56:52.722Z" }, - { url = "https://files.pythonhosted.org/packages/ee/7a/36fbcf646e41f710ce0a563c1c9a343c6edf9be80786edeb15b6f62e17db/charset_normalizer-3.4.3-cp314-cp314-win_amd64.whl", hash = "sha256:73dc19b562516fc9bcf6e5d6e596df0b4eb98d87e4f79f3ae71840e6ed21361c", size = 107400, upload_time = "2025-08-09T07:56:55.172Z" }, - { url = "https://files.pythonhosted.org/packages/8a/1f/f041989e93b001bc4e44bb1669ccdcf54d3f00e628229a85b08d330615c5/charset_normalizer-3.4.3-py3-none-any.whl", hash = "sha256:ce571ab16d890d23b5c278547ba694193a45011ff86a9162a71307ed9f86759a", size = 53175, upload_time = "2025-08-09T07:57:26.864Z" }, + { url = "https://files.pythonhosted.org/packages/e9/5e/14c94999e418d9b87682734589404a25854d5f5d0408df68bc15b6ff54bb/charset_normalizer-3.4.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e28e334d3ff134e88989d90ba04b47d84382a828c061d0d1027b1b12a62b39b1", size = 205655, upload-time = "2025-08-09T07:56:08.475Z" }, + { url = "https://files.pythonhosted.org/packages/7d/a8/c6ec5d389672521f644505a257f50544c074cf5fc292d5390331cd6fc9c3/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0cacf8f7297b0c4fcb74227692ca46b4a5852f8f4f24b3c766dd94a1075c4884", size = 146223, upload-time = "2025-08-09T07:56:09.708Z" }, + { url = "https://files.pythonhosted.org/packages/fc/eb/a2ffb08547f4e1e5415fb69eb7db25932c52a52bed371429648db4d84fb1/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c6fd51128a41297f5409deab284fecbe5305ebd7e5a1f959bee1c054622b7018", size = 159366, upload-time = "2025-08-09T07:56:11.326Z" }, + { url = "https://files.pythonhosted.org/packages/82/10/0fd19f20c624b278dddaf83b8464dcddc2456cb4b02bb902a6da126b87a1/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3cfb2aad70f2c6debfbcb717f23b7eb55febc0bb23dcffc0f076009da10c6392", size = 157104, upload-time = "2025-08-09T07:56:13.014Z" }, + { url = "https://files.pythonhosted.org/packages/16/ab/0233c3231af734f5dfcf0844aa9582d5a1466c985bbed6cedab85af9bfe3/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1606f4a55c0fd363d754049cdf400175ee96c992b1f8018b993941f221221c5f", size = 151830, upload-time = "2025-08-09T07:56:14.428Z" }, + { url = "https://files.pythonhosted.org/packages/ae/02/e29e22b4e02839a0e4a06557b1999d0a47db3567e82989b5bb21f3fbbd9f/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:027b776c26d38b7f15b26a5da1044f376455fb3766df8fc38563b4efbc515154", size = 148854, upload-time = "2025-08-09T07:56:16.051Z" }, + { url = "https://files.pythonhosted.org/packages/05/6b/e2539a0a4be302b481e8cafb5af8792da8093b486885a1ae4d15d452bcec/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:42e5088973e56e31e4fa58eb6bd709e42fc03799c11c42929592889a2e54c491", size = 160670, upload-time = "2025-08-09T07:56:17.314Z" }, + { url = "https://files.pythonhosted.org/packages/31/e7/883ee5676a2ef217a40ce0bffcc3d0dfbf9e64cbcfbdf822c52981c3304b/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:cc34f233c9e71701040d772aa7490318673aa7164a0efe3172b2981218c26d93", size = 158501, upload-time = "2025-08-09T07:56:18.641Z" }, + { url = "https://files.pythonhosted.org/packages/c1/35/6525b21aa0db614cf8b5792d232021dca3df7f90a1944db934efa5d20bb1/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:320e8e66157cc4e247d9ddca8e21f427efc7a04bbd0ac8a9faf56583fa543f9f", size = 153173, upload-time = "2025-08-09T07:56:20.289Z" }, + { url = "https://files.pythonhosted.org/packages/50/ee/f4704bad8201de513fdc8aac1cabc87e38c5818c93857140e06e772b5892/charset_normalizer-3.4.3-cp312-cp312-win32.whl", hash = "sha256:fb6fecfd65564f208cbf0fba07f107fb661bcd1a7c389edbced3f7a493f70e37", size = 99822, upload-time = "2025-08-09T07:56:21.551Z" }, + { url = "https://files.pythonhosted.org/packages/39/f5/3b3836ca6064d0992c58c7561c6b6eee1b3892e9665d650c803bd5614522/charset_normalizer-3.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:86df271bf921c2ee3818f0522e9a5b8092ca2ad8b065ece5d7d9d0e9f4849bcc", size = 107543, upload-time = "2025-08-09T07:56:23.115Z" }, + { url = "https://files.pythonhosted.org/packages/65/ca/2135ac97709b400c7654b4b764daf5c5567c2da45a30cdd20f9eefe2d658/charset_normalizer-3.4.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:14c2a87c65b351109f6abfc424cab3927b3bdece6f706e4d12faaf3d52ee5efe", size = 205326, upload-time = "2025-08-09T07:56:24.721Z" }, + { url = "https://files.pythonhosted.org/packages/71/11/98a04c3c97dd34e49c7d247083af03645ca3730809a5509443f3c37f7c99/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41d1fc408ff5fdfb910200ec0e74abc40387bccb3252f3f27c0676731df2b2c8", size = 146008, upload-time = "2025-08-09T07:56:26.004Z" }, + { url = "https://files.pythonhosted.org/packages/60/f5/4659a4cb3c4ec146bec80c32d8bb16033752574c20b1252ee842a95d1a1e/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1bb60174149316da1c35fa5233681f7c0f9f514509b8e399ab70fea5f17e45c9", size = 159196, upload-time = "2025-08-09T07:56:27.25Z" }, + { url = "https://files.pythonhosted.org/packages/86/9e/f552f7a00611f168b9a5865a1414179b2c6de8235a4fa40189f6f79a1753/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:30d006f98569de3459c2fc1f2acde170b7b2bd265dc1943e87e1a4efe1b67c31", size = 156819, upload-time = "2025-08-09T07:56:28.515Z" }, + { url = "https://files.pythonhosted.org/packages/7e/95/42aa2156235cbc8fa61208aded06ef46111c4d3f0de233107b3f38631803/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:416175faf02e4b0810f1f38bcb54682878a4af94059a1cd63b8747244420801f", size = 151350, upload-time = "2025-08-09T07:56:29.716Z" }, + { url = "https://files.pythonhosted.org/packages/c2/a9/3865b02c56f300a6f94fc631ef54f0a8a29da74fb45a773dfd3dcd380af7/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6aab0f181c486f973bc7262a97f5aca3ee7e1437011ef0c2ec04b5a11d16c927", size = 148644, upload-time = "2025-08-09T07:56:30.984Z" }, + { url = "https://files.pythonhosted.org/packages/77/d9/cbcf1a2a5c7d7856f11e7ac2d782aec12bdfea60d104e60e0aa1c97849dc/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:fdabf8315679312cfa71302f9bd509ded4f2f263fb5b765cf1433b39106c3cc9", size = 160468, upload-time = "2025-08-09T07:56:32.252Z" }, + { url = "https://files.pythonhosted.org/packages/f6/42/6f45efee8697b89fda4d50580f292b8f7f9306cb2971d4b53f8914e4d890/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:bd28b817ea8c70215401f657edef3a8aa83c29d447fb0b622c35403780ba11d5", size = 158187, upload-time = "2025-08-09T07:56:33.481Z" }, + { url = "https://files.pythonhosted.org/packages/70/99/f1c3bdcfaa9c45b3ce96f70b14f070411366fa19549c1d4832c935d8e2c3/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:18343b2d246dc6761a249ba1fb13f9ee9a2bcd95decc767319506056ea4ad4dc", size = 152699, upload-time = "2025-08-09T07:56:34.739Z" }, + { url = "https://files.pythonhosted.org/packages/a3/ad/b0081f2f99a4b194bcbb1934ef3b12aa4d9702ced80a37026b7607c72e58/charset_normalizer-3.4.3-cp313-cp313-win32.whl", hash = "sha256:6fb70de56f1859a3f71261cbe41005f56a7842cc348d3aeb26237560bfa5e0ce", size = 99580, upload-time = "2025-08-09T07:56:35.981Z" }, + { url = "https://files.pythonhosted.org/packages/9a/8f/ae790790c7b64f925e5c953b924aaa42a243fb778fed9e41f147b2a5715a/charset_normalizer-3.4.3-cp313-cp313-win_amd64.whl", hash = "sha256:cf1ebb7d78e1ad8ec2a8c4732c7be2e736f6e5123a4146c5b89c9d1f585f8cef", size = 107366, upload-time = "2025-08-09T07:56:37.339Z" }, + { url = "https://files.pythonhosted.org/packages/8e/91/b5a06ad970ddc7a0e513112d40113e834638f4ca1120eb727a249fb2715e/charset_normalizer-3.4.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3cd35b7e8aedeb9e34c41385fda4f73ba609e561faedfae0a9e75e44ac558a15", size = 204342, upload-time = "2025-08-09T07:56:38.687Z" }, + { url = "https://files.pythonhosted.org/packages/ce/ec/1edc30a377f0a02689342f214455c3f6c2fbedd896a1d2f856c002fc3062/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b89bc04de1d83006373429975f8ef9e7932534b8cc9ca582e4db7d20d91816db", size = 145995, upload-time = "2025-08-09T07:56:40.048Z" }, + { url = "https://files.pythonhosted.org/packages/17/e5/5e67ab85e6d22b04641acb5399c8684f4d37caf7558a53859f0283a650e9/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2001a39612b241dae17b4687898843f254f8748b796a2e16f1051a17078d991d", size = 158640, upload-time = "2025-08-09T07:56:41.311Z" }, + { url = "https://files.pythonhosted.org/packages/f1/e5/38421987f6c697ee3722981289d554957c4be652f963d71c5e46a262e135/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8dcfc373f888e4fb39a7bc57e93e3b845e7f462dacc008d9749568b1c4ece096", size = 156636, upload-time = "2025-08-09T07:56:43.195Z" }, + { url = "https://files.pythonhosted.org/packages/a0/e4/5a075de8daa3ec0745a9a3b54467e0c2967daaaf2cec04c845f73493e9a1/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18b97b8404387b96cdbd30ad660f6407799126d26a39ca65729162fd810a99aa", size = 150939, upload-time = "2025-08-09T07:56:44.819Z" }, + { url = "https://files.pythonhosted.org/packages/02/f7/3611b32318b30974131db62b4043f335861d4d9b49adc6d57c1149cc49d4/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ccf600859c183d70eb47e05a44cd80a4ce77394d1ac0f79dbd2dd90a69a3a049", size = 148580, upload-time = "2025-08-09T07:56:46.684Z" }, + { url = "https://files.pythonhosted.org/packages/7e/61/19b36f4bd67f2793ab6a99b979b4e4f3d8fc754cbdffb805335df4337126/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:53cd68b185d98dde4ad8990e56a58dea83a4162161b1ea9272e5c9182ce415e0", size = 159870, upload-time = "2025-08-09T07:56:47.941Z" }, + { url = "https://files.pythonhosted.org/packages/06/57/84722eefdd338c04cf3030ada66889298eaedf3e7a30a624201e0cbe424a/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:30a96e1e1f865f78b030d65241c1ee850cdf422d869e9028e2fc1d5e4db73b92", size = 157797, upload-time = "2025-08-09T07:56:49.756Z" }, + { url = "https://files.pythonhosted.org/packages/72/2a/aff5dd112b2f14bcc3462c312dce5445806bfc8ab3a7328555da95330e4b/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d716a916938e03231e86e43782ca7878fb602a125a91e7acb8b5112e2e96ac16", size = 152224, upload-time = "2025-08-09T07:56:51.369Z" }, + { url = "https://files.pythonhosted.org/packages/b7/8c/9839225320046ed279c6e839d51f028342eb77c91c89b8ef2549f951f3ec/charset_normalizer-3.4.3-cp314-cp314-win32.whl", hash = "sha256:c6dbd0ccdda3a2ba7c2ecd9d77b37f3b5831687d8dc1b6ca5f56a4880cc7b7ce", size = 100086, upload-time = "2025-08-09T07:56:52.722Z" }, + { url = "https://files.pythonhosted.org/packages/ee/7a/36fbcf646e41f710ce0a563c1c9a343c6edf9be80786edeb15b6f62e17db/charset_normalizer-3.4.3-cp314-cp314-win_amd64.whl", hash = "sha256:73dc19b562516fc9bcf6e5d6e596df0b4eb98d87e4f79f3ae71840e6ed21361c", size = 107400, upload-time = "2025-08-09T07:56:55.172Z" }, + { url = "https://files.pythonhosted.org/packages/8a/1f/f041989e93b001bc4e44bb1669ccdcf54d3f00e628229a85b08d330615c5/charset_normalizer-3.4.3-py3-none-any.whl", hash = "sha256:ce571ab16d890d23b5c278547ba694193a45011ff86a9162a71307ed9f86759a", size = 53175, upload-time = "2025-08-09T07:57:26.864Z" }, ] [[package]] name = "frozenlist" version = "1.7.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/79/b1/b64018016eeb087db503b038296fd782586432b9c077fc5c7839e9cb6ef6/frozenlist-1.7.0.tar.gz", hash = "sha256:2e310d81923c2437ea8670467121cc3e9b0f76d3043cc1d2331d56c7fb7a3a8f", size = 45078, upload_time = "2025-06-09T23:02:35.538Z" } +sdist = { url = "https://files.pythonhosted.org/packages/79/b1/b64018016eeb087db503b038296fd782586432b9c077fc5c7839e9cb6ef6/frozenlist-1.7.0.tar.gz", hash = "sha256:2e310d81923c2437ea8670467121cc3e9b0f76d3043cc1d2331d56c7fb7a3a8f", size = 45078, upload-time = "2025-06-09T23:02:35.538Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ef/a2/c8131383f1e66adad5f6ecfcce383d584ca94055a34d683bbb24ac5f2f1c/frozenlist-1.7.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3dbf9952c4bb0e90e98aec1bd992b3318685005702656bc6f67c1a32b76787f2", size = 81424, upload_time = "2025-06-09T23:00:42.24Z" }, - { url = "https://files.pythonhosted.org/packages/4c/9d/02754159955088cb52567337d1113f945b9e444c4960771ea90eb73de8db/frozenlist-1.7.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:1f5906d3359300b8a9bb194239491122e6cf1444c2efb88865426f170c262cdb", size = 47952, upload_time = "2025-06-09T23:00:43.481Z" }, - { url = "https://files.pythonhosted.org/packages/01/7a/0046ef1bd6699b40acd2067ed6d6670b4db2f425c56980fa21c982c2a9db/frozenlist-1.7.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3dabd5a8f84573c8d10d8859a50ea2dec01eea372031929871368c09fa103478", size = 46688, upload_time = "2025-06-09T23:00:44.793Z" }, - { url = "https://files.pythonhosted.org/packages/d6/a2/a910bafe29c86997363fb4c02069df4ff0b5bc39d33c5198b4e9dd42d8f8/frozenlist-1.7.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa57daa5917f1738064f302bf2626281a1cb01920c32f711fbc7bc36111058a8", size = 243084, upload_time = "2025-06-09T23:00:46.125Z" }, - { url = "https://files.pythonhosted.org/packages/64/3e/5036af9d5031374c64c387469bfcc3af537fc0f5b1187d83a1cf6fab1639/frozenlist-1.7.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c193dda2b6d49f4c4398962810fa7d7c78f032bf45572b3e04dd5249dff27e08", size = 233524, upload_time = "2025-06-09T23:00:47.73Z" }, - { url = "https://files.pythonhosted.org/packages/06/39/6a17b7c107a2887e781a48ecf20ad20f1c39d94b2a548c83615b5b879f28/frozenlist-1.7.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bfe2b675cf0aaa6d61bf8fbffd3c274b3c9b7b1623beb3809df8a81399a4a9c4", size = 248493, upload_time = "2025-06-09T23:00:49.742Z" }, - { url = "https://files.pythonhosted.org/packages/be/00/711d1337c7327d88c44d91dd0f556a1c47fb99afc060ae0ef66b4d24793d/frozenlist-1.7.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8fc5d5cda37f62b262405cf9652cf0856839c4be8ee41be0afe8858f17f4c94b", size = 244116, upload_time = "2025-06-09T23:00:51.352Z" }, - { url = "https://files.pythonhosted.org/packages/24/fe/74e6ec0639c115df13d5850e75722750adabdc7de24e37e05a40527ca539/frozenlist-1.7.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b0d5ce521d1dd7d620198829b87ea002956e4319002ef0bc8d3e6d045cb4646e", size = 224557, upload_time = "2025-06-09T23:00:52.855Z" }, - { url = "https://files.pythonhosted.org/packages/8d/db/48421f62a6f77c553575201e89048e97198046b793f4a089c79a6e3268bd/frozenlist-1.7.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:488d0a7d6a0008ca0db273c542098a0fa9e7dfaa7e57f70acef43f32b3f69dca", size = 241820, upload_time = "2025-06-09T23:00:54.43Z" }, - { url = "https://files.pythonhosted.org/packages/1d/fa/cb4a76bea23047c8462976ea7b7a2bf53997a0ca171302deae9d6dd12096/frozenlist-1.7.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:15a7eaba63983d22c54d255b854e8108e7e5f3e89f647fc854bd77a237e767df", size = 236542, upload_time = "2025-06-09T23:00:56.409Z" }, - { url = "https://files.pythonhosted.org/packages/5d/32/476a4b5cfaa0ec94d3f808f193301debff2ea42288a099afe60757ef6282/frozenlist-1.7.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:1eaa7e9c6d15df825bf255649e05bd8a74b04a4d2baa1ae46d9c2d00b2ca2cb5", size = 249350, upload_time = "2025-06-09T23:00:58.468Z" }, - { url = "https://files.pythonhosted.org/packages/8d/ba/9a28042f84a6bf8ea5dbc81cfff8eaef18d78b2a1ad9d51c7bc5b029ad16/frozenlist-1.7.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e4389e06714cfa9d47ab87f784a7c5be91d3934cd6e9a7b85beef808297cc025", size = 225093, upload_time = "2025-06-09T23:01:00.015Z" }, - { url = "https://files.pythonhosted.org/packages/bc/29/3a32959e68f9cf000b04e79ba574527c17e8842e38c91d68214a37455786/frozenlist-1.7.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:73bd45e1488c40b63fe5a7df892baf9e2a4d4bb6409a2b3b78ac1c6236178e01", size = 245482, upload_time = "2025-06-09T23:01:01.474Z" }, - { url = "https://files.pythonhosted.org/packages/80/e8/edf2f9e00da553f07f5fa165325cfc302dead715cab6ac8336a5f3d0adc2/frozenlist-1.7.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:99886d98e1643269760e5fe0df31e5ae7050788dd288947f7f007209b8c33f08", size = 249590, upload_time = "2025-06-09T23:01:02.961Z" }, - { url = "https://files.pythonhosted.org/packages/1c/80/9a0eb48b944050f94cc51ee1c413eb14a39543cc4f760ed12657a5a3c45a/frozenlist-1.7.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:290a172aae5a4c278c6da8a96222e6337744cd9c77313efe33d5670b9f65fc43", size = 237785, upload_time = "2025-06-09T23:01:05.095Z" }, - { url = "https://files.pythonhosted.org/packages/f3/74/87601e0fb0369b7a2baf404ea921769c53b7ae00dee7dcfe5162c8c6dbf0/frozenlist-1.7.0-cp312-cp312-win32.whl", hash = "sha256:426c7bc70e07cfebc178bc4c2bf2d861d720c4fff172181eeb4a4c41d4ca2ad3", size = 39487, upload_time = "2025-06-09T23:01:06.54Z" }, - { url = "https://files.pythonhosted.org/packages/0b/15/c026e9a9fc17585a9d461f65d8593d281fedf55fbf7eb53f16c6df2392f9/frozenlist-1.7.0-cp312-cp312-win_amd64.whl", hash = "sha256:563b72efe5da92e02eb68c59cb37205457c977aa7a449ed1b37e6939e5c47c6a", size = 43874, upload_time = "2025-06-09T23:01:07.752Z" }, - { url = "https://files.pythonhosted.org/packages/24/90/6b2cebdabdbd50367273c20ff6b57a3dfa89bd0762de02c3a1eb42cb6462/frozenlist-1.7.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee80eeda5e2a4e660651370ebffd1286542b67e268aa1ac8d6dbe973120ef7ee", size = 79791, upload_time = "2025-06-09T23:01:09.368Z" }, - { url = "https://files.pythonhosted.org/packages/83/2e/5b70b6a3325363293fe5fc3ae74cdcbc3e996c2a11dde2fd9f1fb0776d19/frozenlist-1.7.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d1a81c85417b914139e3a9b995d4a1c84559afc839a93cf2cb7f15e6e5f6ed2d", size = 47165, upload_time = "2025-06-09T23:01:10.653Z" }, - { url = "https://files.pythonhosted.org/packages/f4/25/a0895c99270ca6966110f4ad98e87e5662eab416a17e7fd53c364bf8b954/frozenlist-1.7.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cbb65198a9132ebc334f237d7b0df163e4de83fb4f2bdfe46c1e654bdb0c5d43", size = 45881, upload_time = "2025-06-09T23:01:12.296Z" }, - { url = "https://files.pythonhosted.org/packages/19/7c/71bb0bbe0832793c601fff68cd0cf6143753d0c667f9aec93d3c323f4b55/frozenlist-1.7.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dab46c723eeb2c255a64f9dc05b8dd601fde66d6b19cdb82b2e09cc6ff8d8b5d", size = 232409, upload_time = "2025-06-09T23:01:13.641Z" }, - { url = "https://files.pythonhosted.org/packages/c0/45/ed2798718910fe6eb3ba574082aaceff4528e6323f9a8570be0f7028d8e9/frozenlist-1.7.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6aeac207a759d0dedd2e40745575ae32ab30926ff4fa49b1635def65806fddee", size = 225132, upload_time = "2025-06-09T23:01:15.264Z" }, - { url = "https://files.pythonhosted.org/packages/ba/e2/8417ae0f8eacb1d071d4950f32f229aa6bf68ab69aab797b72a07ea68d4f/frozenlist-1.7.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bd8c4e58ad14b4fa7802b8be49d47993182fdd4023393899632c88fd8cd994eb", size = 237638, upload_time = "2025-06-09T23:01:16.752Z" }, - { url = "https://files.pythonhosted.org/packages/f8/b7/2ace5450ce85f2af05a871b8c8719b341294775a0a6c5585d5e6170f2ce7/frozenlist-1.7.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:04fb24d104f425da3540ed83cbfc31388a586a7696142004c577fa61c6298c3f", size = 233539, upload_time = "2025-06-09T23:01:18.202Z" }, - { url = "https://files.pythonhosted.org/packages/46/b9/6989292c5539553dba63f3c83dc4598186ab2888f67c0dc1d917e6887db6/frozenlist-1.7.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6a5c505156368e4ea6b53b5ac23c92d7edc864537ff911d2fb24c140bb175e60", size = 215646, upload_time = "2025-06-09T23:01:19.649Z" }, - { url = "https://files.pythonhosted.org/packages/72/31/bc8c5c99c7818293458fe745dab4fd5730ff49697ccc82b554eb69f16a24/frozenlist-1.7.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8bd7eb96a675f18aa5c553eb7ddc24a43c8c18f22e1f9925528128c052cdbe00", size = 232233, upload_time = "2025-06-09T23:01:21.175Z" }, - { url = "https://files.pythonhosted.org/packages/59/52/460db4d7ba0811b9ccb85af996019f5d70831f2f5f255f7cc61f86199795/frozenlist-1.7.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:05579bf020096fe05a764f1f84cd104a12f78eaab68842d036772dc6d4870b4b", size = 227996, upload_time = "2025-06-09T23:01:23.098Z" }, - { url = "https://files.pythonhosted.org/packages/ba/c9/f4b39e904c03927b7ecf891804fd3b4df3db29b9e487c6418e37988d6e9d/frozenlist-1.7.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:376b6222d114e97eeec13d46c486facd41d4f43bab626b7c3f6a8b4e81a5192c", size = 242280, upload_time = "2025-06-09T23:01:24.808Z" }, - { url = "https://files.pythonhosted.org/packages/b8/33/3f8d6ced42f162d743e3517781566b8481322be321b486d9d262adf70bfb/frozenlist-1.7.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:0aa7e176ebe115379b5b1c95b4096fb1c17cce0847402e227e712c27bdb5a949", size = 217717, upload_time = "2025-06-09T23:01:26.28Z" }, - { url = "https://files.pythonhosted.org/packages/3e/e8/ad683e75da6ccef50d0ab0c2b2324b32f84fc88ceee778ed79b8e2d2fe2e/frozenlist-1.7.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3fbba20e662b9c2130dc771e332a99eff5da078b2b2648153a40669a6d0e36ca", size = 236644, upload_time = "2025-06-09T23:01:27.887Z" }, - { url = "https://files.pythonhosted.org/packages/b2/14/8d19ccdd3799310722195a72ac94ddc677541fb4bef4091d8e7775752360/frozenlist-1.7.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:f3f4410a0a601d349dd406b5713fec59b4cee7e71678d5b17edda7f4655a940b", size = 238879, upload_time = "2025-06-09T23:01:29.524Z" }, - { url = "https://files.pythonhosted.org/packages/ce/13/c12bf657494c2fd1079a48b2db49fa4196325909249a52d8f09bc9123fd7/frozenlist-1.7.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e2cdfaaec6a2f9327bf43c933c0319a7c429058e8537c508964a133dffee412e", size = 232502, upload_time = "2025-06-09T23:01:31.287Z" }, - { url = "https://files.pythonhosted.org/packages/d7/8b/e7f9dfde869825489382bc0d512c15e96d3964180c9499efcec72e85db7e/frozenlist-1.7.0-cp313-cp313-win32.whl", hash = "sha256:5fc4df05a6591c7768459caba1b342d9ec23fa16195e744939ba5914596ae3e1", size = 39169, upload_time = "2025-06-09T23:01:35.503Z" }, - { url = "https://files.pythonhosted.org/packages/35/89/a487a98d94205d85745080a37860ff5744b9820a2c9acbcdd9440bfddf98/frozenlist-1.7.0-cp313-cp313-win_amd64.whl", hash = "sha256:52109052b9791a3e6b5d1b65f4b909703984b770694d3eb64fad124c835d7cba", size = 43219, upload_time = "2025-06-09T23:01:36.784Z" }, - { url = "https://files.pythonhosted.org/packages/56/d5/5c4cf2319a49eddd9dd7145e66c4866bdc6f3dbc67ca3d59685149c11e0d/frozenlist-1.7.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:a6f86e4193bb0e235ef6ce3dde5cbabed887e0b11f516ce8a0f4d3b33078ec2d", size = 84345, upload_time = "2025-06-09T23:01:38.295Z" }, - { url = "https://files.pythonhosted.org/packages/a4/7d/ec2c1e1dc16b85bc9d526009961953df9cec8481b6886debb36ec9107799/frozenlist-1.7.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:82d664628865abeb32d90ae497fb93df398a69bb3434463d172b80fc25b0dd7d", size = 48880, upload_time = "2025-06-09T23:01:39.887Z" }, - { url = "https://files.pythonhosted.org/packages/69/86/f9596807b03de126e11e7d42ac91e3d0b19a6599c714a1989a4e85eeefc4/frozenlist-1.7.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:912a7e8375a1c9a68325a902f3953191b7b292aa3c3fb0d71a216221deca460b", size = 48498, upload_time = "2025-06-09T23:01:41.318Z" }, - { url = "https://files.pythonhosted.org/packages/5e/cb/df6de220f5036001005f2d726b789b2c0b65f2363b104bbc16f5be8084f8/frozenlist-1.7.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9537c2777167488d539bc5de2ad262efc44388230e5118868e172dd4a552b146", size = 292296, upload_time = "2025-06-09T23:01:42.685Z" }, - { url = "https://files.pythonhosted.org/packages/83/1f/de84c642f17c8f851a2905cee2dae401e5e0daca9b5ef121e120e19aa825/frozenlist-1.7.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:f34560fb1b4c3e30ba35fa9a13894ba39e5acfc5f60f57d8accde65f46cc5e74", size = 273103, upload_time = "2025-06-09T23:01:44.166Z" }, - { url = "https://files.pythonhosted.org/packages/88/3c/c840bfa474ba3fa13c772b93070893c6e9d5c0350885760376cbe3b6c1b3/frozenlist-1.7.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:acd03d224b0175f5a850edc104ac19040d35419eddad04e7cf2d5986d98427f1", size = 292869, upload_time = "2025-06-09T23:01:45.681Z" }, - { url = "https://files.pythonhosted.org/packages/a6/1c/3efa6e7d5a39a1d5ef0abeb51c48fb657765794a46cf124e5aca2c7a592c/frozenlist-1.7.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f2038310bc582f3d6a09b3816ab01737d60bf7b1ec70f5356b09e84fb7408ab1", size = 291467, upload_time = "2025-06-09T23:01:47.234Z" }, - { url = "https://files.pythonhosted.org/packages/4f/00/d5c5e09d4922c395e2f2f6b79b9a20dab4b67daaf78ab92e7729341f61f6/frozenlist-1.7.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b8c05e4c8e5f36e5e088caa1bf78a687528f83c043706640a92cb76cd6999384", size = 266028, upload_time = "2025-06-09T23:01:48.819Z" }, - { url = "https://files.pythonhosted.org/packages/4e/27/72765be905619dfde25a7f33813ac0341eb6b076abede17a2e3fbfade0cb/frozenlist-1.7.0-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:765bb588c86e47d0b68f23c1bee323d4b703218037765dcf3f25c838c6fecceb", size = 284294, upload_time = "2025-06-09T23:01:50.394Z" }, - { url = "https://files.pythonhosted.org/packages/88/67/c94103a23001b17808eb7dd1200c156bb69fb68e63fcf0693dde4cd6228c/frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:32dc2e08c67d86d0969714dd484fd60ff08ff81d1a1e40a77dd34a387e6ebc0c", size = 281898, upload_time = "2025-06-09T23:01:52.234Z" }, - { url = "https://files.pythonhosted.org/packages/42/34/a3e2c00c00f9e2a9db5653bca3fec306349e71aff14ae45ecc6d0951dd24/frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:c0303e597eb5a5321b4de9c68e9845ac8f290d2ab3f3e2c864437d3c5a30cd65", size = 290465, upload_time = "2025-06-09T23:01:53.788Z" }, - { url = "https://files.pythonhosted.org/packages/bb/73/f89b7fbce8b0b0c095d82b008afd0590f71ccb3dee6eee41791cf8cd25fd/frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:a47f2abb4e29b3a8d0b530f7c3598badc6b134562b1a5caee867f7c62fee51e3", size = 266385, upload_time = "2025-06-09T23:01:55.769Z" }, - { url = "https://files.pythonhosted.org/packages/cd/45/e365fdb554159462ca12df54bc59bfa7a9a273ecc21e99e72e597564d1ae/frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:3d688126c242a6fabbd92e02633414d40f50bb6002fa4cf995a1d18051525657", size = 288771, upload_time = "2025-06-09T23:01:57.4Z" }, - { url = "https://files.pythonhosted.org/packages/00/11/47b6117002a0e904f004d70ec5194fe9144f117c33c851e3d51c765962d0/frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:4e7e9652b3d367c7bd449a727dc79d5043f48b88d0cbfd4f9f1060cf2b414104", size = 288206, upload_time = "2025-06-09T23:01:58.936Z" }, - { url = "https://files.pythonhosted.org/packages/40/37/5f9f3c3fd7f7746082ec67bcdc204db72dad081f4f83a503d33220a92973/frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:1a85e345b4c43db8b842cab1feb41be5cc0b10a1830e6295b69d7310f99becaf", size = 282620, upload_time = "2025-06-09T23:02:00.493Z" }, - { url = "https://files.pythonhosted.org/packages/0b/31/8fbc5af2d183bff20f21aa743b4088eac4445d2bb1cdece449ae80e4e2d1/frozenlist-1.7.0-cp313-cp313t-win32.whl", hash = "sha256:3a14027124ddb70dfcee5148979998066897e79f89f64b13328595c4bdf77c81", size = 43059, upload_time = "2025-06-09T23:02:02.072Z" }, - { url = "https://files.pythonhosted.org/packages/bb/ed/41956f52105b8dbc26e457c5705340c67c8cc2b79f394b79bffc09d0e938/frozenlist-1.7.0-cp313-cp313t-win_amd64.whl", hash = "sha256:3bf8010d71d4507775f658e9823210b7427be36625b387221642725b515dcf3e", size = 47516, upload_time = "2025-06-09T23:02:03.779Z" }, - { url = "https://files.pythonhosted.org/packages/ee/45/b82e3c16be2182bff01179db177fe144d58b5dc787a7d4492c6ed8b9317f/frozenlist-1.7.0-py3-none-any.whl", hash = "sha256:9a5af342e34f7e97caf8c995864c7a396418ae2859cc6fdf1b1073020d516a7e", size = 13106, upload_time = "2025-06-09T23:02:34.204Z" }, + { url = "https://files.pythonhosted.org/packages/ef/a2/c8131383f1e66adad5f6ecfcce383d584ca94055a34d683bbb24ac5f2f1c/frozenlist-1.7.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3dbf9952c4bb0e90e98aec1bd992b3318685005702656bc6f67c1a32b76787f2", size = 81424, upload-time = "2025-06-09T23:00:42.24Z" }, + { url = "https://files.pythonhosted.org/packages/4c/9d/02754159955088cb52567337d1113f945b9e444c4960771ea90eb73de8db/frozenlist-1.7.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:1f5906d3359300b8a9bb194239491122e6cf1444c2efb88865426f170c262cdb", size = 47952, upload-time = "2025-06-09T23:00:43.481Z" }, + { url = "https://files.pythonhosted.org/packages/01/7a/0046ef1bd6699b40acd2067ed6d6670b4db2f425c56980fa21c982c2a9db/frozenlist-1.7.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3dabd5a8f84573c8d10d8859a50ea2dec01eea372031929871368c09fa103478", size = 46688, upload-time = "2025-06-09T23:00:44.793Z" }, + { url = "https://files.pythonhosted.org/packages/d6/a2/a910bafe29c86997363fb4c02069df4ff0b5bc39d33c5198b4e9dd42d8f8/frozenlist-1.7.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa57daa5917f1738064f302bf2626281a1cb01920c32f711fbc7bc36111058a8", size = 243084, upload-time = "2025-06-09T23:00:46.125Z" }, + { url = "https://files.pythonhosted.org/packages/64/3e/5036af9d5031374c64c387469bfcc3af537fc0f5b1187d83a1cf6fab1639/frozenlist-1.7.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c193dda2b6d49f4c4398962810fa7d7c78f032bf45572b3e04dd5249dff27e08", size = 233524, upload-time = "2025-06-09T23:00:47.73Z" }, + { url = "https://files.pythonhosted.org/packages/06/39/6a17b7c107a2887e781a48ecf20ad20f1c39d94b2a548c83615b5b879f28/frozenlist-1.7.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bfe2b675cf0aaa6d61bf8fbffd3c274b3c9b7b1623beb3809df8a81399a4a9c4", size = 248493, upload-time = "2025-06-09T23:00:49.742Z" }, + { url = "https://files.pythonhosted.org/packages/be/00/711d1337c7327d88c44d91dd0f556a1c47fb99afc060ae0ef66b4d24793d/frozenlist-1.7.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8fc5d5cda37f62b262405cf9652cf0856839c4be8ee41be0afe8858f17f4c94b", size = 244116, upload-time = "2025-06-09T23:00:51.352Z" }, + { url = "https://files.pythonhosted.org/packages/24/fe/74e6ec0639c115df13d5850e75722750adabdc7de24e37e05a40527ca539/frozenlist-1.7.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b0d5ce521d1dd7d620198829b87ea002956e4319002ef0bc8d3e6d045cb4646e", size = 224557, upload-time = "2025-06-09T23:00:52.855Z" }, + { url = "https://files.pythonhosted.org/packages/8d/db/48421f62a6f77c553575201e89048e97198046b793f4a089c79a6e3268bd/frozenlist-1.7.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:488d0a7d6a0008ca0db273c542098a0fa9e7dfaa7e57f70acef43f32b3f69dca", size = 241820, upload-time = "2025-06-09T23:00:54.43Z" }, + { url = "https://files.pythonhosted.org/packages/1d/fa/cb4a76bea23047c8462976ea7b7a2bf53997a0ca171302deae9d6dd12096/frozenlist-1.7.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:15a7eaba63983d22c54d255b854e8108e7e5f3e89f647fc854bd77a237e767df", size = 236542, upload-time = "2025-06-09T23:00:56.409Z" }, + { url = "https://files.pythonhosted.org/packages/5d/32/476a4b5cfaa0ec94d3f808f193301debff2ea42288a099afe60757ef6282/frozenlist-1.7.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:1eaa7e9c6d15df825bf255649e05bd8a74b04a4d2baa1ae46d9c2d00b2ca2cb5", size = 249350, upload-time = "2025-06-09T23:00:58.468Z" }, + { url = "https://files.pythonhosted.org/packages/8d/ba/9a28042f84a6bf8ea5dbc81cfff8eaef18d78b2a1ad9d51c7bc5b029ad16/frozenlist-1.7.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e4389e06714cfa9d47ab87f784a7c5be91d3934cd6e9a7b85beef808297cc025", size = 225093, upload-time = "2025-06-09T23:01:00.015Z" }, + { url = "https://files.pythonhosted.org/packages/bc/29/3a32959e68f9cf000b04e79ba574527c17e8842e38c91d68214a37455786/frozenlist-1.7.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:73bd45e1488c40b63fe5a7df892baf9e2a4d4bb6409a2b3b78ac1c6236178e01", size = 245482, upload-time = "2025-06-09T23:01:01.474Z" }, + { url = "https://files.pythonhosted.org/packages/80/e8/edf2f9e00da553f07f5fa165325cfc302dead715cab6ac8336a5f3d0adc2/frozenlist-1.7.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:99886d98e1643269760e5fe0df31e5ae7050788dd288947f7f007209b8c33f08", size = 249590, upload-time = "2025-06-09T23:01:02.961Z" }, + { url = "https://files.pythonhosted.org/packages/1c/80/9a0eb48b944050f94cc51ee1c413eb14a39543cc4f760ed12657a5a3c45a/frozenlist-1.7.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:290a172aae5a4c278c6da8a96222e6337744cd9c77313efe33d5670b9f65fc43", size = 237785, upload-time = "2025-06-09T23:01:05.095Z" }, + { url = "https://files.pythonhosted.org/packages/f3/74/87601e0fb0369b7a2baf404ea921769c53b7ae00dee7dcfe5162c8c6dbf0/frozenlist-1.7.0-cp312-cp312-win32.whl", hash = "sha256:426c7bc70e07cfebc178bc4c2bf2d861d720c4fff172181eeb4a4c41d4ca2ad3", size = 39487, upload-time = "2025-06-09T23:01:06.54Z" }, + { url = "https://files.pythonhosted.org/packages/0b/15/c026e9a9fc17585a9d461f65d8593d281fedf55fbf7eb53f16c6df2392f9/frozenlist-1.7.0-cp312-cp312-win_amd64.whl", hash = "sha256:563b72efe5da92e02eb68c59cb37205457c977aa7a449ed1b37e6939e5c47c6a", size = 43874, upload-time = "2025-06-09T23:01:07.752Z" }, + { url = "https://files.pythonhosted.org/packages/24/90/6b2cebdabdbd50367273c20ff6b57a3dfa89bd0762de02c3a1eb42cb6462/frozenlist-1.7.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee80eeda5e2a4e660651370ebffd1286542b67e268aa1ac8d6dbe973120ef7ee", size = 79791, upload-time = "2025-06-09T23:01:09.368Z" }, + { url = "https://files.pythonhosted.org/packages/83/2e/5b70b6a3325363293fe5fc3ae74cdcbc3e996c2a11dde2fd9f1fb0776d19/frozenlist-1.7.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d1a81c85417b914139e3a9b995d4a1c84559afc839a93cf2cb7f15e6e5f6ed2d", size = 47165, upload-time = "2025-06-09T23:01:10.653Z" }, + { url = "https://files.pythonhosted.org/packages/f4/25/a0895c99270ca6966110f4ad98e87e5662eab416a17e7fd53c364bf8b954/frozenlist-1.7.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cbb65198a9132ebc334f237d7b0df163e4de83fb4f2bdfe46c1e654bdb0c5d43", size = 45881, upload-time = "2025-06-09T23:01:12.296Z" }, + { url = "https://files.pythonhosted.org/packages/19/7c/71bb0bbe0832793c601fff68cd0cf6143753d0c667f9aec93d3c323f4b55/frozenlist-1.7.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dab46c723eeb2c255a64f9dc05b8dd601fde66d6b19cdb82b2e09cc6ff8d8b5d", size = 232409, upload-time = "2025-06-09T23:01:13.641Z" }, + { url = "https://files.pythonhosted.org/packages/c0/45/ed2798718910fe6eb3ba574082aaceff4528e6323f9a8570be0f7028d8e9/frozenlist-1.7.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6aeac207a759d0dedd2e40745575ae32ab30926ff4fa49b1635def65806fddee", size = 225132, upload-time = "2025-06-09T23:01:15.264Z" }, + { url = "https://files.pythonhosted.org/packages/ba/e2/8417ae0f8eacb1d071d4950f32f229aa6bf68ab69aab797b72a07ea68d4f/frozenlist-1.7.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bd8c4e58ad14b4fa7802b8be49d47993182fdd4023393899632c88fd8cd994eb", size = 237638, upload-time = "2025-06-09T23:01:16.752Z" }, + { url = "https://files.pythonhosted.org/packages/f8/b7/2ace5450ce85f2af05a871b8c8719b341294775a0a6c5585d5e6170f2ce7/frozenlist-1.7.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:04fb24d104f425da3540ed83cbfc31388a586a7696142004c577fa61c6298c3f", size = 233539, upload-time = "2025-06-09T23:01:18.202Z" }, + { url = "https://files.pythonhosted.org/packages/46/b9/6989292c5539553dba63f3c83dc4598186ab2888f67c0dc1d917e6887db6/frozenlist-1.7.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6a5c505156368e4ea6b53b5ac23c92d7edc864537ff911d2fb24c140bb175e60", size = 215646, upload-time = "2025-06-09T23:01:19.649Z" }, + { url = "https://files.pythonhosted.org/packages/72/31/bc8c5c99c7818293458fe745dab4fd5730ff49697ccc82b554eb69f16a24/frozenlist-1.7.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8bd7eb96a675f18aa5c553eb7ddc24a43c8c18f22e1f9925528128c052cdbe00", size = 232233, upload-time = "2025-06-09T23:01:21.175Z" }, + { url = "https://files.pythonhosted.org/packages/59/52/460db4d7ba0811b9ccb85af996019f5d70831f2f5f255f7cc61f86199795/frozenlist-1.7.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:05579bf020096fe05a764f1f84cd104a12f78eaab68842d036772dc6d4870b4b", size = 227996, upload-time = "2025-06-09T23:01:23.098Z" }, + { url = "https://files.pythonhosted.org/packages/ba/c9/f4b39e904c03927b7ecf891804fd3b4df3db29b9e487c6418e37988d6e9d/frozenlist-1.7.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:376b6222d114e97eeec13d46c486facd41d4f43bab626b7c3f6a8b4e81a5192c", size = 242280, upload-time = "2025-06-09T23:01:24.808Z" }, + { url = "https://files.pythonhosted.org/packages/b8/33/3f8d6ced42f162d743e3517781566b8481322be321b486d9d262adf70bfb/frozenlist-1.7.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:0aa7e176ebe115379b5b1c95b4096fb1c17cce0847402e227e712c27bdb5a949", size = 217717, upload-time = "2025-06-09T23:01:26.28Z" }, + { url = "https://files.pythonhosted.org/packages/3e/e8/ad683e75da6ccef50d0ab0c2b2324b32f84fc88ceee778ed79b8e2d2fe2e/frozenlist-1.7.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3fbba20e662b9c2130dc771e332a99eff5da078b2b2648153a40669a6d0e36ca", size = 236644, upload-time = "2025-06-09T23:01:27.887Z" }, + { url = "https://files.pythonhosted.org/packages/b2/14/8d19ccdd3799310722195a72ac94ddc677541fb4bef4091d8e7775752360/frozenlist-1.7.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:f3f4410a0a601d349dd406b5713fec59b4cee7e71678d5b17edda7f4655a940b", size = 238879, upload-time = "2025-06-09T23:01:29.524Z" }, + { url = "https://files.pythonhosted.org/packages/ce/13/c12bf657494c2fd1079a48b2db49fa4196325909249a52d8f09bc9123fd7/frozenlist-1.7.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e2cdfaaec6a2f9327bf43c933c0319a7c429058e8537c508964a133dffee412e", size = 232502, upload-time = "2025-06-09T23:01:31.287Z" }, + { url = "https://files.pythonhosted.org/packages/d7/8b/e7f9dfde869825489382bc0d512c15e96d3964180c9499efcec72e85db7e/frozenlist-1.7.0-cp313-cp313-win32.whl", hash = "sha256:5fc4df05a6591c7768459caba1b342d9ec23fa16195e744939ba5914596ae3e1", size = 39169, upload-time = "2025-06-09T23:01:35.503Z" }, + { url = "https://files.pythonhosted.org/packages/35/89/a487a98d94205d85745080a37860ff5744b9820a2c9acbcdd9440bfddf98/frozenlist-1.7.0-cp313-cp313-win_amd64.whl", hash = "sha256:52109052b9791a3e6b5d1b65f4b909703984b770694d3eb64fad124c835d7cba", size = 43219, upload-time = "2025-06-09T23:01:36.784Z" }, + { url = "https://files.pythonhosted.org/packages/56/d5/5c4cf2319a49eddd9dd7145e66c4866bdc6f3dbc67ca3d59685149c11e0d/frozenlist-1.7.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:a6f86e4193bb0e235ef6ce3dde5cbabed887e0b11f516ce8a0f4d3b33078ec2d", size = 84345, upload-time = "2025-06-09T23:01:38.295Z" }, + { url = "https://files.pythonhosted.org/packages/a4/7d/ec2c1e1dc16b85bc9d526009961953df9cec8481b6886debb36ec9107799/frozenlist-1.7.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:82d664628865abeb32d90ae497fb93df398a69bb3434463d172b80fc25b0dd7d", size = 48880, upload-time = "2025-06-09T23:01:39.887Z" }, + { url = "https://files.pythonhosted.org/packages/69/86/f9596807b03de126e11e7d42ac91e3d0b19a6599c714a1989a4e85eeefc4/frozenlist-1.7.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:912a7e8375a1c9a68325a902f3953191b7b292aa3c3fb0d71a216221deca460b", size = 48498, upload-time = "2025-06-09T23:01:41.318Z" }, + { url = "https://files.pythonhosted.org/packages/5e/cb/df6de220f5036001005f2d726b789b2c0b65f2363b104bbc16f5be8084f8/frozenlist-1.7.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9537c2777167488d539bc5de2ad262efc44388230e5118868e172dd4a552b146", size = 292296, upload-time = "2025-06-09T23:01:42.685Z" }, + { url = "https://files.pythonhosted.org/packages/83/1f/de84c642f17c8f851a2905cee2dae401e5e0daca9b5ef121e120e19aa825/frozenlist-1.7.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:f34560fb1b4c3e30ba35fa9a13894ba39e5acfc5f60f57d8accde65f46cc5e74", size = 273103, upload-time = "2025-06-09T23:01:44.166Z" }, + { url = "https://files.pythonhosted.org/packages/88/3c/c840bfa474ba3fa13c772b93070893c6e9d5c0350885760376cbe3b6c1b3/frozenlist-1.7.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:acd03d224b0175f5a850edc104ac19040d35419eddad04e7cf2d5986d98427f1", size = 292869, upload-time = "2025-06-09T23:01:45.681Z" }, + { url = "https://files.pythonhosted.org/packages/a6/1c/3efa6e7d5a39a1d5ef0abeb51c48fb657765794a46cf124e5aca2c7a592c/frozenlist-1.7.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f2038310bc582f3d6a09b3816ab01737d60bf7b1ec70f5356b09e84fb7408ab1", size = 291467, upload-time = "2025-06-09T23:01:47.234Z" }, + { url = "https://files.pythonhosted.org/packages/4f/00/d5c5e09d4922c395e2f2f6b79b9a20dab4b67daaf78ab92e7729341f61f6/frozenlist-1.7.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b8c05e4c8e5f36e5e088caa1bf78a687528f83c043706640a92cb76cd6999384", size = 266028, upload-time = "2025-06-09T23:01:48.819Z" }, + { url = "https://files.pythonhosted.org/packages/4e/27/72765be905619dfde25a7f33813ac0341eb6b076abede17a2e3fbfade0cb/frozenlist-1.7.0-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:765bb588c86e47d0b68f23c1bee323d4b703218037765dcf3f25c838c6fecceb", size = 284294, upload-time = "2025-06-09T23:01:50.394Z" }, + { url = "https://files.pythonhosted.org/packages/88/67/c94103a23001b17808eb7dd1200c156bb69fb68e63fcf0693dde4cd6228c/frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:32dc2e08c67d86d0969714dd484fd60ff08ff81d1a1e40a77dd34a387e6ebc0c", size = 281898, upload-time = "2025-06-09T23:01:52.234Z" }, + { url = "https://files.pythonhosted.org/packages/42/34/a3e2c00c00f9e2a9db5653bca3fec306349e71aff14ae45ecc6d0951dd24/frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:c0303e597eb5a5321b4de9c68e9845ac8f290d2ab3f3e2c864437d3c5a30cd65", size = 290465, upload-time = "2025-06-09T23:01:53.788Z" }, + { url = "https://files.pythonhosted.org/packages/bb/73/f89b7fbce8b0b0c095d82b008afd0590f71ccb3dee6eee41791cf8cd25fd/frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:a47f2abb4e29b3a8d0b530f7c3598badc6b134562b1a5caee867f7c62fee51e3", size = 266385, upload-time = "2025-06-09T23:01:55.769Z" }, + { url = "https://files.pythonhosted.org/packages/cd/45/e365fdb554159462ca12df54bc59bfa7a9a273ecc21e99e72e597564d1ae/frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:3d688126c242a6fabbd92e02633414d40f50bb6002fa4cf995a1d18051525657", size = 288771, upload-time = "2025-06-09T23:01:57.4Z" }, + { url = "https://files.pythonhosted.org/packages/00/11/47b6117002a0e904f004d70ec5194fe9144f117c33c851e3d51c765962d0/frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:4e7e9652b3d367c7bd449a727dc79d5043f48b88d0cbfd4f9f1060cf2b414104", size = 288206, upload-time = "2025-06-09T23:01:58.936Z" }, + { url = "https://files.pythonhosted.org/packages/40/37/5f9f3c3fd7f7746082ec67bcdc204db72dad081f4f83a503d33220a92973/frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:1a85e345b4c43db8b842cab1feb41be5cc0b10a1830e6295b69d7310f99becaf", size = 282620, upload-time = "2025-06-09T23:02:00.493Z" }, + { url = "https://files.pythonhosted.org/packages/0b/31/8fbc5af2d183bff20f21aa743b4088eac4445d2bb1cdece449ae80e4e2d1/frozenlist-1.7.0-cp313-cp313t-win32.whl", hash = "sha256:3a14027124ddb70dfcee5148979998066897e79f89f64b13328595c4bdf77c81", size = 43059, upload-time = "2025-06-09T23:02:02.072Z" }, + { url = "https://files.pythonhosted.org/packages/bb/ed/41956f52105b8dbc26e457c5705340c67c8cc2b79f394b79bffc09d0e938/frozenlist-1.7.0-cp313-cp313t-win_amd64.whl", hash = "sha256:3bf8010d71d4507775f658e9823210b7427be36625b387221642725b515dcf3e", size = 47516, upload-time = "2025-06-09T23:02:03.779Z" }, + { url = "https://files.pythonhosted.org/packages/ee/45/b82e3c16be2182bff01179db177fe144d58b5dc787a7d4492c6ed8b9317f/frozenlist-1.7.0-py3-none-any.whl", hash = "sha256:9a5af342e34f7e97caf8c995864c7a396418ae2859cc6fdf1b1073020d516a7e", size = 13106, upload-time = "2025-06-09T23:02:34.204Z" }, ] [[package]] name = "humanize" version = "4.12.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/22/d1/bbc4d251187a43f69844f7fd8941426549bbe4723e8ff0a7441796b0789f/humanize-4.12.3.tar.gz", hash = "sha256:8430be3a615106fdfceb0b2c1b41c4c98c6b0fc5cc59663a5539b111dd325fb0", size = 80514, upload_time = "2025-04-30T11:51:07.98Z" } +sdist = { url = "https://files.pythonhosted.org/packages/22/d1/bbc4d251187a43f69844f7fd8941426549bbe4723e8ff0a7441796b0789f/humanize-4.12.3.tar.gz", hash = "sha256:8430be3a615106fdfceb0b2c1b41c4c98c6b0fc5cc59663a5539b111dd325fb0", size = 80514, upload-time = "2025-04-30T11:51:07.98Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a0/1e/62a2ec3104394a2975a2629eec89276ede9dbe717092f6966fcf963e1bf0/humanize-4.12.3-py3-none-any.whl", hash = "sha256:2cbf6370af06568fa6d2da77c86edb7886f3160ecd19ee1ffef07979efc597f6", size = 128487, upload_time = "2025-04-30T11:51:06.468Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1e/62a2ec3104394a2975a2629eec89276ede9dbe717092f6966fcf963e1bf0/humanize-4.12.3-py3-none-any.whl", hash = "sha256:2cbf6370af06568fa6d2da77c86edb7886f3160ecd19ee1ffef07979efc597f6", size = 128487, upload-time = "2025-04-30T11:51:06.468Z" }, ] [[package]] name = "idna" version = "3.10" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload_time = "2024-09-15T18:07:39.745Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload_time = "2024-09-15T18:07:37.964Z" }, + { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" }, ] [[package]] name = "lego-monitoring" -version = "1.0.0" +version = "1.1.1" source = { editable = "." } dependencies = [ { name = "aiodns" }, @@ -296,150 +296,150 @@ requires-dist = [ name = "multidict" version = "6.6.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/69/7f/0652e6ed47ab288e3756ea9c0df8b14950781184d4bd7883f4d87dd41245/multidict-6.6.4.tar.gz", hash = "sha256:d2d4e4787672911b48350df02ed3fa3fffdc2f2e8ca06dd6afdf34189b76a9dd", size = 101843, upload_time = "2025-08-11T12:08:48.217Z" } +sdist = { url = "https://files.pythonhosted.org/packages/69/7f/0652e6ed47ab288e3756ea9c0df8b14950781184d4bd7883f4d87dd41245/multidict-6.6.4.tar.gz", hash = "sha256:d2d4e4787672911b48350df02ed3fa3fffdc2f2e8ca06dd6afdf34189b76a9dd", size = 101843, upload-time = "2025-08-11T12:08:48.217Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/05/f6/512ffd8fd8b37fb2680e5ac35d788f1d71bbaf37789d21a820bdc441e565/multidict-6.6.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0ffb87be160942d56d7b87b0fdf098e81ed565add09eaa1294268c7f3caac4c8", size = 76516, upload_time = "2025-08-11T12:06:53.393Z" }, - { url = "https://files.pythonhosted.org/packages/99/58/45c3e75deb8855c36bd66cc1658007589662ba584dbf423d01df478dd1c5/multidict-6.6.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d191de6cbab2aff5de6c5723101705fd044b3e4c7cfd587a1929b5028b9714b3", size = 45394, upload_time = "2025-08-11T12:06:54.555Z" }, - { url = "https://files.pythonhosted.org/packages/fd/ca/e8c4472a93a26e4507c0b8e1f0762c0d8a32de1328ef72fd704ef9cc5447/multidict-6.6.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:38a0956dd92d918ad5feff3db8fcb4a5eb7dba114da917e1a88475619781b57b", size = 43591, upload_time = "2025-08-11T12:06:55.672Z" }, - { url = "https://files.pythonhosted.org/packages/05/51/edf414f4df058574a7265034d04c935aa84a89e79ce90fcf4df211f47b16/multidict-6.6.4-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:6865f6d3b7900ae020b495d599fcf3765653bc927951c1abb959017f81ae8287", size = 237215, upload_time = "2025-08-11T12:06:57.213Z" }, - { url = "https://files.pythonhosted.org/packages/c8/45/8b3d6dbad8cf3252553cc41abea09ad527b33ce47a5e199072620b296902/multidict-6.6.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a2088c126b6f72db6c9212ad827d0ba088c01d951cee25e758c450da732c138", size = 258299, upload_time = "2025-08-11T12:06:58.946Z" }, - { url = "https://files.pythonhosted.org/packages/3c/e8/8ca2e9a9f5a435fc6db40438a55730a4bf4956b554e487fa1b9ae920f825/multidict-6.6.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0f37bed7319b848097085d7d48116f545985db988e2256b2e6f00563a3416ee6", size = 242357, upload_time = "2025-08-11T12:07:00.301Z" }, - { url = "https://files.pythonhosted.org/packages/0f/84/80c77c99df05a75c28490b2af8f7cba2a12621186e0a8b0865d8e745c104/multidict-6.6.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:01368e3c94032ba6ca0b78e7ccb099643466cf24f8dc8eefcfdc0571d56e58f9", size = 268369, upload_time = "2025-08-11T12:07:01.638Z" }, - { url = "https://files.pythonhosted.org/packages/0d/e9/920bfa46c27b05fb3e1ad85121fd49f441492dca2449c5bcfe42e4565d8a/multidict-6.6.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8fe323540c255db0bffee79ad7f048c909f2ab0edb87a597e1c17da6a54e493c", size = 269341, upload_time = "2025-08-11T12:07:02.943Z" }, - { url = "https://files.pythonhosted.org/packages/af/65/753a2d8b05daf496f4a9c367fe844e90a1b2cac78e2be2c844200d10cc4c/multidict-6.6.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8eb3025f17b0a4c3cd08cda49acf312a19ad6e8a4edd9dbd591e6506d999402", size = 256100, upload_time = "2025-08-11T12:07:04.564Z" }, - { url = "https://files.pythonhosted.org/packages/09/54/655be13ae324212bf0bc15d665a4e34844f34c206f78801be42f7a0a8aaa/multidict-6.6.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bbc14f0365534d35a06970d6a83478b249752e922d662dc24d489af1aa0d1be7", size = 253584, upload_time = "2025-08-11T12:07:05.914Z" }, - { url = "https://files.pythonhosted.org/packages/5c/74/ab2039ecc05264b5cec73eb018ce417af3ebb384ae9c0e9ed42cb33f8151/multidict-6.6.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:75aa52fba2d96bf972e85451b99d8e19cc37ce26fd016f6d4aa60da9ab2b005f", size = 251018, upload_time = "2025-08-11T12:07:08.301Z" }, - { url = "https://files.pythonhosted.org/packages/af/0a/ccbb244ac848e56c6427f2392741c06302bbfba49c0042f1eb3c5b606497/multidict-6.6.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4fefd4a815e362d4f011919d97d7b4a1e566f1dde83dc4ad8cfb5b41de1df68d", size = 251477, upload_time = "2025-08-11T12:07:10.248Z" }, - { url = "https://files.pythonhosted.org/packages/0e/b0/0ed49bba775b135937f52fe13922bc64a7eaf0a3ead84a36e8e4e446e096/multidict-6.6.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:db9801fe021f59a5b375ab778973127ca0ac52429a26e2fd86aa9508f4d26eb7", size = 263575, upload_time = "2025-08-11T12:07:11.928Z" }, - { url = "https://files.pythonhosted.org/packages/3e/d9/7fb85a85e14de2e44dfb6a24f03c41e2af8697a6df83daddb0e9b7569f73/multidict-6.6.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:a650629970fa21ac1fb06ba25dabfc5b8a2054fcbf6ae97c758aa956b8dba802", size = 259649, upload_time = "2025-08-11T12:07:13.244Z" }, - { url = "https://files.pythonhosted.org/packages/03/9e/b3a459bcf9b6e74fa461a5222a10ff9b544cb1cd52fd482fb1b75ecda2a2/multidict-6.6.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:452ff5da78d4720d7516a3a2abd804957532dd69296cb77319c193e3ffb87e24", size = 251505, upload_time = "2025-08-11T12:07:14.57Z" }, - { url = "https://files.pythonhosted.org/packages/86/a2/8022f78f041dfe6d71e364001a5cf987c30edfc83c8a5fb7a3f0974cff39/multidict-6.6.4-cp312-cp312-win32.whl", hash = "sha256:8c2fcb12136530ed19572bbba61b407f655e3953ba669b96a35036a11a485793", size = 41888, upload_time = "2025-08-11T12:07:15.904Z" }, - { url = "https://files.pythonhosted.org/packages/c7/eb/d88b1780d43a56db2cba24289fa744a9d216c1a8546a0dc3956563fd53ea/multidict-6.6.4-cp312-cp312-win_amd64.whl", hash = "sha256:047d9425860a8c9544fed1b9584f0c8bcd31bcde9568b047c5e567a1025ecd6e", size = 46072, upload_time = "2025-08-11T12:07:17.045Z" }, - { url = "https://files.pythonhosted.org/packages/9f/16/b929320bf5750e2d9d4931835a4c638a19d2494a5b519caaaa7492ebe105/multidict-6.6.4-cp312-cp312-win_arm64.whl", hash = "sha256:14754eb72feaa1e8ae528468f24250dd997b8e2188c3d2f593f9eba259e4b364", size = 43222, upload_time = "2025-08-11T12:07:18.328Z" }, - { url = "https://files.pythonhosted.org/packages/3a/5d/e1db626f64f60008320aab00fbe4f23fc3300d75892a3381275b3d284580/multidict-6.6.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f46a6e8597f9bd71b31cc708195d42b634c8527fecbcf93febf1052cacc1f16e", size = 75848, upload_time = "2025-08-11T12:07:19.912Z" }, - { url = "https://files.pythonhosted.org/packages/4c/aa/8b6f548d839b6c13887253af4e29c939af22a18591bfb5d0ee6f1931dae8/multidict-6.6.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:22e38b2bc176c5eb9c0a0e379f9d188ae4cd8b28c0f53b52bce7ab0a9e534657", size = 45060, upload_time = "2025-08-11T12:07:21.163Z" }, - { url = "https://files.pythonhosted.org/packages/eb/c6/f5e97e5d99a729bc2aa58eb3ebfa9f1e56a9b517cc38c60537c81834a73f/multidict-6.6.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5df8afd26f162da59e218ac0eefaa01b01b2e6cd606cffa46608f699539246da", size = 43269, upload_time = "2025-08-11T12:07:22.392Z" }, - { url = "https://files.pythonhosted.org/packages/dc/31/d54eb0c62516776f36fe67f84a732f97e0b0e12f98d5685bebcc6d396910/multidict-6.6.4-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:49517449b58d043023720aa58e62b2f74ce9b28f740a0b5d33971149553d72aa", size = 237158, upload_time = "2025-08-11T12:07:23.636Z" }, - { url = "https://files.pythonhosted.org/packages/c4/1c/8a10c1c25b23156e63b12165a929d8eb49a6ed769fdbefb06e6f07c1e50d/multidict-6.6.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ae9408439537c5afdca05edd128a63f56a62680f4b3c234301055d7a2000220f", size = 257076, upload_time = "2025-08-11T12:07:25.049Z" }, - { url = "https://files.pythonhosted.org/packages/ad/86/90e20b5771d6805a119e483fd3d1e8393e745a11511aebca41f0da38c3e2/multidict-6.6.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:87a32d20759dc52a9e850fe1061b6e41ab28e2998d44168a8a341b99ded1dba0", size = 240694, upload_time = "2025-08-11T12:07:26.458Z" }, - { url = "https://files.pythonhosted.org/packages/e7/49/484d3e6b535bc0555b52a0a26ba86e4d8d03fd5587d4936dc59ba7583221/multidict-6.6.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:52e3c8d43cdfff587ceedce9deb25e6ae77daba560b626e97a56ddcad3756879", size = 266350, upload_time = "2025-08-11T12:07:27.94Z" }, - { url = "https://files.pythonhosted.org/packages/bf/b4/aa4c5c379b11895083d50021e229e90c408d7d875471cb3abf721e4670d6/multidict-6.6.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ad8850921d3a8d8ff6fbef790e773cecfc260bbfa0566998980d3fa8f520bc4a", size = 267250, upload_time = "2025-08-11T12:07:29.303Z" }, - { url = "https://files.pythonhosted.org/packages/80/e5/5e22c5bf96a64bdd43518b1834c6d95a4922cc2066b7d8e467dae9b6cee6/multidict-6.6.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:497a2954adc25c08daff36f795077f63ad33e13f19bfff7736e72c785391534f", size = 254900, upload_time = "2025-08-11T12:07:30.764Z" }, - { url = "https://files.pythonhosted.org/packages/17/38/58b27fed927c07035abc02befacab42491e7388ca105e087e6e0215ead64/multidict-6.6.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:024ce601f92d780ca1617ad4be5ac15b501cc2414970ffa2bb2bbc2bd5a68fa5", size = 252355, upload_time = "2025-08-11T12:07:32.205Z" }, - { url = "https://files.pythonhosted.org/packages/d0/a1/dad75d23a90c29c02b5d6f3d7c10ab36c3197613be5d07ec49c7791e186c/multidict-6.6.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:a693fc5ed9bdd1c9e898013e0da4dcc640de7963a371c0bd458e50e046bf6438", size = 250061, upload_time = "2025-08-11T12:07:33.623Z" }, - { url = "https://files.pythonhosted.org/packages/b8/1a/ac2216b61c7f116edab6dc3378cca6c70dc019c9a457ff0d754067c58b20/multidict-6.6.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:190766dac95aab54cae5b152a56520fd99298f32a1266d66d27fdd1b5ac00f4e", size = 249675, upload_time = "2025-08-11T12:07:34.958Z" }, - { url = "https://files.pythonhosted.org/packages/d4/79/1916af833b800d13883e452e8e0977c065c4ee3ab7a26941fbfdebc11895/multidict-6.6.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:34d8f2a5ffdceab9dcd97c7a016deb2308531d5f0fced2bb0c9e1df45b3363d7", size = 261247, upload_time = "2025-08-11T12:07:36.588Z" }, - { url = "https://files.pythonhosted.org/packages/c5/65/d1f84fe08ac44a5fc7391cbc20a7cedc433ea616b266284413fd86062f8c/multidict-6.6.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:59e8d40ab1f5a8597abcef00d04845155a5693b5da00d2c93dbe88f2050f2812", size = 257960, upload_time = "2025-08-11T12:07:39.735Z" }, - { url = "https://files.pythonhosted.org/packages/13/b5/29ec78057d377b195ac2c5248c773703a6b602e132a763e20ec0457e7440/multidict-6.6.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:467fe64138cfac771f0e949b938c2e1ada2b5af22f39692aa9258715e9ea613a", size = 250078, upload_time = "2025-08-11T12:07:41.525Z" }, - { url = "https://files.pythonhosted.org/packages/c4/0e/7e79d38f70a872cae32e29b0d77024bef7834b0afb406ddae6558d9e2414/multidict-6.6.4-cp313-cp313-win32.whl", hash = "sha256:14616a30fe6d0a48d0a48d1a633ab3b8bec4cf293aac65f32ed116f620adfd69", size = 41708, upload_time = "2025-08-11T12:07:43.405Z" }, - { url = "https://files.pythonhosted.org/packages/9d/34/746696dffff742e97cd6a23da953e55d0ea51fa601fa2ff387b3edcfaa2c/multidict-6.6.4-cp313-cp313-win_amd64.whl", hash = "sha256:40cd05eaeb39e2bc8939451f033e57feaa2ac99e07dbca8afe2be450a4a3b6cf", size = 45912, upload_time = "2025-08-11T12:07:45.082Z" }, - { url = "https://files.pythonhosted.org/packages/c7/87/3bac136181e271e29170d8d71929cdeddeb77f3e8b6a0c08da3a8e9da114/multidict-6.6.4-cp313-cp313-win_arm64.whl", hash = "sha256:f6eb37d511bfae9e13e82cb4d1af36b91150466f24d9b2b8a9785816deb16605", size = 43076, upload_time = "2025-08-11T12:07:46.746Z" }, - { url = "https://files.pythonhosted.org/packages/64/94/0a8e63e36c049b571c9ae41ee301ada29c3fee9643d9c2548d7d558a1d99/multidict-6.6.4-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:6c84378acd4f37d1b507dfa0d459b449e2321b3ba5f2338f9b085cf7a7ba95eb", size = 82812, upload_time = "2025-08-11T12:07:48.402Z" }, - { url = "https://files.pythonhosted.org/packages/25/1a/be8e369dfcd260d2070a67e65dd3990dd635cbd735b98da31e00ea84cd4e/multidict-6.6.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0e0558693063c75f3d952abf645c78f3c5dfdd825a41d8c4d8156fc0b0da6e7e", size = 48313, upload_time = "2025-08-11T12:07:49.679Z" }, - { url = "https://files.pythonhosted.org/packages/26/5a/dd4ade298674b2f9a7b06a32c94ffbc0497354df8285f27317c66433ce3b/multidict-6.6.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3f8e2384cb83ebd23fd07e9eada8ba64afc4c759cd94817433ab8c81ee4b403f", size = 46777, upload_time = "2025-08-11T12:07:51.318Z" }, - { url = "https://files.pythonhosted.org/packages/89/db/98aa28bc7e071bfba611ac2ae803c24e96dd3a452b4118c587d3d872c64c/multidict-6.6.4-cp313-cp313t-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:f996b87b420995a9174b2a7c1a8daf7db4750be6848b03eb5e639674f7963773", size = 229321, upload_time = "2025-08-11T12:07:52.965Z" }, - { url = "https://files.pythonhosted.org/packages/c7/bc/01ddda2a73dd9d167bd85d0e8ef4293836a8f82b786c63fb1a429bc3e678/multidict-6.6.4-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cc356250cffd6e78416cf5b40dc6a74f1edf3be8e834cf8862d9ed5265cf9b0e", size = 249954, upload_time = "2025-08-11T12:07:54.423Z" }, - { url = "https://files.pythonhosted.org/packages/06/78/6b7c0f020f9aa0acf66d0ab4eb9f08375bac9a50ff5e3edb1c4ccd59eafc/multidict-6.6.4-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:dadf95aa862714ea468a49ad1e09fe00fcc9ec67d122f6596a8d40caf6cec7d0", size = 228612, upload_time = "2025-08-11T12:07:55.914Z" }, - { url = "https://files.pythonhosted.org/packages/00/44/3faa416f89b2d5d76e9d447296a81521e1c832ad6e40b92f990697b43192/multidict-6.6.4-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7dd57515bebffd8ebd714d101d4c434063322e4fe24042e90ced41f18b6d3395", size = 257528, upload_time = "2025-08-11T12:07:57.371Z" }, - { url = "https://files.pythonhosted.org/packages/05/5f/77c03b89af0fcb16f018f668207768191fb9dcfb5e3361a5e706a11db2c9/multidict-6.6.4-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:967af5f238ebc2eb1da4e77af5492219fbd9b4b812347da39a7b5f5c72c0fa45", size = 256329, upload_time = "2025-08-11T12:07:58.844Z" }, - { url = "https://files.pythonhosted.org/packages/cf/e9/ed750a2a9afb4f8dc6f13dc5b67b514832101b95714f1211cd42e0aafc26/multidict-6.6.4-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2a4c6875c37aae9794308ec43e3530e4aa0d36579ce38d89979bbf89582002bb", size = 247928, upload_time = "2025-08-11T12:08:01.037Z" }, - { url = "https://files.pythonhosted.org/packages/1f/b5/e0571bc13cda277db7e6e8a532791d4403dacc9850006cb66d2556e649c0/multidict-6.6.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:7f683a551e92bdb7fac545b9c6f9fa2aebdeefa61d607510b3533286fcab67f5", size = 245228, upload_time = "2025-08-11T12:08:02.96Z" }, - { url = "https://files.pythonhosted.org/packages/f3/a3/69a84b0eccb9824491f06368f5b86e72e4af54c3067c37c39099b6687109/multidict-6.6.4-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:3ba5aaf600edaf2a868a391779f7a85d93bed147854925f34edd24cc70a3e141", size = 235869, upload_time = "2025-08-11T12:08:04.746Z" }, - { url = "https://files.pythonhosted.org/packages/a9/9d/28802e8f9121a6a0804fa009debf4e753d0a59969ea9f70be5f5fdfcb18f/multidict-6.6.4-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:580b643b7fd2c295d83cad90d78419081f53fd532d1f1eb67ceb7060f61cff0d", size = 243446, upload_time = "2025-08-11T12:08:06.332Z" }, - { url = "https://files.pythonhosted.org/packages/38/ea/6c98add069b4878c1d66428a5f5149ddb6d32b1f9836a826ac764b9940be/multidict-6.6.4-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:37b7187197da6af3ee0b044dbc9625afd0c885f2800815b228a0e70f9a7f473d", size = 252299, upload_time = "2025-08-11T12:08:07.931Z" }, - { url = "https://files.pythonhosted.org/packages/3a/09/8fe02d204473e14c0af3affd50af9078839dfca1742f025cca765435d6b4/multidict-6.6.4-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e1b93790ed0bc26feb72e2f08299691ceb6da5e9e14a0d13cc74f1869af327a0", size = 246926, upload_time = "2025-08-11T12:08:09.467Z" }, - { url = "https://files.pythonhosted.org/packages/37/3d/7b1e10d774a6df5175ecd3c92bff069e77bed9ec2a927fdd4ff5fe182f67/multidict-6.6.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a506a77ddee1efcca81ecbeae27ade3e09cdf21a8ae854d766c2bb4f14053f92", size = 243383, upload_time = "2025-08-11T12:08:10.981Z" }, - { url = "https://files.pythonhosted.org/packages/50/b0/a6fae46071b645ae98786ab738447de1ef53742eaad949f27e960864bb49/multidict-6.6.4-cp313-cp313t-win32.whl", hash = "sha256:f93b2b2279883d1d0a9e1bd01f312d6fc315c5e4c1f09e112e4736e2f650bc4e", size = 47775, upload_time = "2025-08-11T12:08:12.439Z" }, - { url = "https://files.pythonhosted.org/packages/b2/0a/2436550b1520091af0600dff547913cb2d66fbac27a8c33bc1b1bccd8d98/multidict-6.6.4-cp313-cp313t-win_amd64.whl", hash = "sha256:6d46a180acdf6e87cc41dc15d8f5c2986e1e8739dc25dbb7dac826731ef381a4", size = 53100, upload_time = "2025-08-11T12:08:13.823Z" }, - { url = "https://files.pythonhosted.org/packages/97/ea/43ac51faff934086db9c072a94d327d71b7d8b40cd5dcb47311330929ef0/multidict-6.6.4-cp313-cp313t-win_arm64.whl", hash = "sha256:756989334015e3335d087a27331659820d53ba432befdef6a718398b0a8493ad", size = 45501, upload_time = "2025-08-11T12:08:15.173Z" }, - { url = "https://files.pythonhosted.org/packages/fd/69/b547032297c7e63ba2af494edba695d781af8a0c6e89e4d06cf848b21d80/multidict-6.6.4-py3-none-any.whl", hash = "sha256:27d8f8e125c07cb954e54d75d04905a9bba8a439c1d84aca94949d4d03d8601c", size = 12313, upload_time = "2025-08-11T12:08:46.891Z" }, + { url = "https://files.pythonhosted.org/packages/05/f6/512ffd8fd8b37fb2680e5ac35d788f1d71bbaf37789d21a820bdc441e565/multidict-6.6.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0ffb87be160942d56d7b87b0fdf098e81ed565add09eaa1294268c7f3caac4c8", size = 76516, upload-time = "2025-08-11T12:06:53.393Z" }, + { url = "https://files.pythonhosted.org/packages/99/58/45c3e75deb8855c36bd66cc1658007589662ba584dbf423d01df478dd1c5/multidict-6.6.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d191de6cbab2aff5de6c5723101705fd044b3e4c7cfd587a1929b5028b9714b3", size = 45394, upload-time = "2025-08-11T12:06:54.555Z" }, + { url = "https://files.pythonhosted.org/packages/fd/ca/e8c4472a93a26e4507c0b8e1f0762c0d8a32de1328ef72fd704ef9cc5447/multidict-6.6.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:38a0956dd92d918ad5feff3db8fcb4a5eb7dba114da917e1a88475619781b57b", size = 43591, upload-time = "2025-08-11T12:06:55.672Z" }, + { url = "https://files.pythonhosted.org/packages/05/51/edf414f4df058574a7265034d04c935aa84a89e79ce90fcf4df211f47b16/multidict-6.6.4-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:6865f6d3b7900ae020b495d599fcf3765653bc927951c1abb959017f81ae8287", size = 237215, upload-time = "2025-08-11T12:06:57.213Z" }, + { url = "https://files.pythonhosted.org/packages/c8/45/8b3d6dbad8cf3252553cc41abea09ad527b33ce47a5e199072620b296902/multidict-6.6.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a2088c126b6f72db6c9212ad827d0ba088c01d951cee25e758c450da732c138", size = 258299, upload-time = "2025-08-11T12:06:58.946Z" }, + { url = "https://files.pythonhosted.org/packages/3c/e8/8ca2e9a9f5a435fc6db40438a55730a4bf4956b554e487fa1b9ae920f825/multidict-6.6.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0f37bed7319b848097085d7d48116f545985db988e2256b2e6f00563a3416ee6", size = 242357, upload-time = "2025-08-11T12:07:00.301Z" }, + { url = "https://files.pythonhosted.org/packages/0f/84/80c77c99df05a75c28490b2af8f7cba2a12621186e0a8b0865d8e745c104/multidict-6.6.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:01368e3c94032ba6ca0b78e7ccb099643466cf24f8dc8eefcfdc0571d56e58f9", size = 268369, upload-time = "2025-08-11T12:07:01.638Z" }, + { url = "https://files.pythonhosted.org/packages/0d/e9/920bfa46c27b05fb3e1ad85121fd49f441492dca2449c5bcfe42e4565d8a/multidict-6.6.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8fe323540c255db0bffee79ad7f048c909f2ab0edb87a597e1c17da6a54e493c", size = 269341, upload-time = "2025-08-11T12:07:02.943Z" }, + { url = "https://files.pythonhosted.org/packages/af/65/753a2d8b05daf496f4a9c367fe844e90a1b2cac78e2be2c844200d10cc4c/multidict-6.6.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8eb3025f17b0a4c3cd08cda49acf312a19ad6e8a4edd9dbd591e6506d999402", size = 256100, upload-time = "2025-08-11T12:07:04.564Z" }, + { url = "https://files.pythonhosted.org/packages/09/54/655be13ae324212bf0bc15d665a4e34844f34c206f78801be42f7a0a8aaa/multidict-6.6.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bbc14f0365534d35a06970d6a83478b249752e922d662dc24d489af1aa0d1be7", size = 253584, upload-time = "2025-08-11T12:07:05.914Z" }, + { url = "https://files.pythonhosted.org/packages/5c/74/ab2039ecc05264b5cec73eb018ce417af3ebb384ae9c0e9ed42cb33f8151/multidict-6.6.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:75aa52fba2d96bf972e85451b99d8e19cc37ce26fd016f6d4aa60da9ab2b005f", size = 251018, upload-time = "2025-08-11T12:07:08.301Z" }, + { url = "https://files.pythonhosted.org/packages/af/0a/ccbb244ac848e56c6427f2392741c06302bbfba49c0042f1eb3c5b606497/multidict-6.6.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4fefd4a815e362d4f011919d97d7b4a1e566f1dde83dc4ad8cfb5b41de1df68d", size = 251477, upload-time = "2025-08-11T12:07:10.248Z" }, + { url = "https://files.pythonhosted.org/packages/0e/b0/0ed49bba775b135937f52fe13922bc64a7eaf0a3ead84a36e8e4e446e096/multidict-6.6.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:db9801fe021f59a5b375ab778973127ca0ac52429a26e2fd86aa9508f4d26eb7", size = 263575, upload-time = "2025-08-11T12:07:11.928Z" }, + { url = "https://files.pythonhosted.org/packages/3e/d9/7fb85a85e14de2e44dfb6a24f03c41e2af8697a6df83daddb0e9b7569f73/multidict-6.6.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:a650629970fa21ac1fb06ba25dabfc5b8a2054fcbf6ae97c758aa956b8dba802", size = 259649, upload-time = "2025-08-11T12:07:13.244Z" }, + { url = "https://files.pythonhosted.org/packages/03/9e/b3a459bcf9b6e74fa461a5222a10ff9b544cb1cd52fd482fb1b75ecda2a2/multidict-6.6.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:452ff5da78d4720d7516a3a2abd804957532dd69296cb77319c193e3ffb87e24", size = 251505, upload-time = "2025-08-11T12:07:14.57Z" }, + { url = "https://files.pythonhosted.org/packages/86/a2/8022f78f041dfe6d71e364001a5cf987c30edfc83c8a5fb7a3f0974cff39/multidict-6.6.4-cp312-cp312-win32.whl", hash = "sha256:8c2fcb12136530ed19572bbba61b407f655e3953ba669b96a35036a11a485793", size = 41888, upload-time = "2025-08-11T12:07:15.904Z" }, + { url = "https://files.pythonhosted.org/packages/c7/eb/d88b1780d43a56db2cba24289fa744a9d216c1a8546a0dc3956563fd53ea/multidict-6.6.4-cp312-cp312-win_amd64.whl", hash = "sha256:047d9425860a8c9544fed1b9584f0c8bcd31bcde9568b047c5e567a1025ecd6e", size = 46072, upload-time = "2025-08-11T12:07:17.045Z" }, + { url = "https://files.pythonhosted.org/packages/9f/16/b929320bf5750e2d9d4931835a4c638a19d2494a5b519caaaa7492ebe105/multidict-6.6.4-cp312-cp312-win_arm64.whl", hash = "sha256:14754eb72feaa1e8ae528468f24250dd997b8e2188c3d2f593f9eba259e4b364", size = 43222, upload-time = "2025-08-11T12:07:18.328Z" }, + { url = "https://files.pythonhosted.org/packages/3a/5d/e1db626f64f60008320aab00fbe4f23fc3300d75892a3381275b3d284580/multidict-6.6.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f46a6e8597f9bd71b31cc708195d42b634c8527fecbcf93febf1052cacc1f16e", size = 75848, upload-time = "2025-08-11T12:07:19.912Z" }, + { url = "https://files.pythonhosted.org/packages/4c/aa/8b6f548d839b6c13887253af4e29c939af22a18591bfb5d0ee6f1931dae8/multidict-6.6.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:22e38b2bc176c5eb9c0a0e379f9d188ae4cd8b28c0f53b52bce7ab0a9e534657", size = 45060, upload-time = "2025-08-11T12:07:21.163Z" }, + { url = "https://files.pythonhosted.org/packages/eb/c6/f5e97e5d99a729bc2aa58eb3ebfa9f1e56a9b517cc38c60537c81834a73f/multidict-6.6.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5df8afd26f162da59e218ac0eefaa01b01b2e6cd606cffa46608f699539246da", size = 43269, upload-time = "2025-08-11T12:07:22.392Z" }, + { url = "https://files.pythonhosted.org/packages/dc/31/d54eb0c62516776f36fe67f84a732f97e0b0e12f98d5685bebcc6d396910/multidict-6.6.4-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:49517449b58d043023720aa58e62b2f74ce9b28f740a0b5d33971149553d72aa", size = 237158, upload-time = "2025-08-11T12:07:23.636Z" }, + { url = "https://files.pythonhosted.org/packages/c4/1c/8a10c1c25b23156e63b12165a929d8eb49a6ed769fdbefb06e6f07c1e50d/multidict-6.6.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ae9408439537c5afdca05edd128a63f56a62680f4b3c234301055d7a2000220f", size = 257076, upload-time = "2025-08-11T12:07:25.049Z" }, + { url = "https://files.pythonhosted.org/packages/ad/86/90e20b5771d6805a119e483fd3d1e8393e745a11511aebca41f0da38c3e2/multidict-6.6.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:87a32d20759dc52a9e850fe1061b6e41ab28e2998d44168a8a341b99ded1dba0", size = 240694, upload-time = "2025-08-11T12:07:26.458Z" }, + { url = "https://files.pythonhosted.org/packages/e7/49/484d3e6b535bc0555b52a0a26ba86e4d8d03fd5587d4936dc59ba7583221/multidict-6.6.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:52e3c8d43cdfff587ceedce9deb25e6ae77daba560b626e97a56ddcad3756879", size = 266350, upload-time = "2025-08-11T12:07:27.94Z" }, + { url = "https://files.pythonhosted.org/packages/bf/b4/aa4c5c379b11895083d50021e229e90c408d7d875471cb3abf721e4670d6/multidict-6.6.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ad8850921d3a8d8ff6fbef790e773cecfc260bbfa0566998980d3fa8f520bc4a", size = 267250, upload-time = "2025-08-11T12:07:29.303Z" }, + { url = "https://files.pythonhosted.org/packages/80/e5/5e22c5bf96a64bdd43518b1834c6d95a4922cc2066b7d8e467dae9b6cee6/multidict-6.6.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:497a2954adc25c08daff36f795077f63ad33e13f19bfff7736e72c785391534f", size = 254900, upload-time = "2025-08-11T12:07:30.764Z" }, + { url = "https://files.pythonhosted.org/packages/17/38/58b27fed927c07035abc02befacab42491e7388ca105e087e6e0215ead64/multidict-6.6.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:024ce601f92d780ca1617ad4be5ac15b501cc2414970ffa2bb2bbc2bd5a68fa5", size = 252355, upload-time = "2025-08-11T12:07:32.205Z" }, + { url = "https://files.pythonhosted.org/packages/d0/a1/dad75d23a90c29c02b5d6f3d7c10ab36c3197613be5d07ec49c7791e186c/multidict-6.6.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:a693fc5ed9bdd1c9e898013e0da4dcc640de7963a371c0bd458e50e046bf6438", size = 250061, upload-time = "2025-08-11T12:07:33.623Z" }, + { url = "https://files.pythonhosted.org/packages/b8/1a/ac2216b61c7f116edab6dc3378cca6c70dc019c9a457ff0d754067c58b20/multidict-6.6.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:190766dac95aab54cae5b152a56520fd99298f32a1266d66d27fdd1b5ac00f4e", size = 249675, upload-time = "2025-08-11T12:07:34.958Z" }, + { url = "https://files.pythonhosted.org/packages/d4/79/1916af833b800d13883e452e8e0977c065c4ee3ab7a26941fbfdebc11895/multidict-6.6.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:34d8f2a5ffdceab9dcd97c7a016deb2308531d5f0fced2bb0c9e1df45b3363d7", size = 261247, upload-time = "2025-08-11T12:07:36.588Z" }, + { url = "https://files.pythonhosted.org/packages/c5/65/d1f84fe08ac44a5fc7391cbc20a7cedc433ea616b266284413fd86062f8c/multidict-6.6.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:59e8d40ab1f5a8597abcef00d04845155a5693b5da00d2c93dbe88f2050f2812", size = 257960, upload-time = "2025-08-11T12:07:39.735Z" }, + { url = "https://files.pythonhosted.org/packages/13/b5/29ec78057d377b195ac2c5248c773703a6b602e132a763e20ec0457e7440/multidict-6.6.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:467fe64138cfac771f0e949b938c2e1ada2b5af22f39692aa9258715e9ea613a", size = 250078, upload-time = "2025-08-11T12:07:41.525Z" }, + { url = "https://files.pythonhosted.org/packages/c4/0e/7e79d38f70a872cae32e29b0d77024bef7834b0afb406ddae6558d9e2414/multidict-6.6.4-cp313-cp313-win32.whl", hash = "sha256:14616a30fe6d0a48d0a48d1a633ab3b8bec4cf293aac65f32ed116f620adfd69", size = 41708, upload-time = "2025-08-11T12:07:43.405Z" }, + { url = "https://files.pythonhosted.org/packages/9d/34/746696dffff742e97cd6a23da953e55d0ea51fa601fa2ff387b3edcfaa2c/multidict-6.6.4-cp313-cp313-win_amd64.whl", hash = "sha256:40cd05eaeb39e2bc8939451f033e57feaa2ac99e07dbca8afe2be450a4a3b6cf", size = 45912, upload-time = "2025-08-11T12:07:45.082Z" }, + { url = "https://files.pythonhosted.org/packages/c7/87/3bac136181e271e29170d8d71929cdeddeb77f3e8b6a0c08da3a8e9da114/multidict-6.6.4-cp313-cp313-win_arm64.whl", hash = "sha256:f6eb37d511bfae9e13e82cb4d1af36b91150466f24d9b2b8a9785816deb16605", size = 43076, upload-time = "2025-08-11T12:07:46.746Z" }, + { url = "https://files.pythonhosted.org/packages/64/94/0a8e63e36c049b571c9ae41ee301ada29c3fee9643d9c2548d7d558a1d99/multidict-6.6.4-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:6c84378acd4f37d1b507dfa0d459b449e2321b3ba5f2338f9b085cf7a7ba95eb", size = 82812, upload-time = "2025-08-11T12:07:48.402Z" }, + { url = "https://files.pythonhosted.org/packages/25/1a/be8e369dfcd260d2070a67e65dd3990dd635cbd735b98da31e00ea84cd4e/multidict-6.6.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0e0558693063c75f3d952abf645c78f3c5dfdd825a41d8c4d8156fc0b0da6e7e", size = 48313, upload-time = "2025-08-11T12:07:49.679Z" }, + { url = "https://files.pythonhosted.org/packages/26/5a/dd4ade298674b2f9a7b06a32c94ffbc0497354df8285f27317c66433ce3b/multidict-6.6.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3f8e2384cb83ebd23fd07e9eada8ba64afc4c759cd94817433ab8c81ee4b403f", size = 46777, upload-time = "2025-08-11T12:07:51.318Z" }, + { url = "https://files.pythonhosted.org/packages/89/db/98aa28bc7e071bfba611ac2ae803c24e96dd3a452b4118c587d3d872c64c/multidict-6.6.4-cp313-cp313t-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:f996b87b420995a9174b2a7c1a8daf7db4750be6848b03eb5e639674f7963773", size = 229321, upload-time = "2025-08-11T12:07:52.965Z" }, + { url = "https://files.pythonhosted.org/packages/c7/bc/01ddda2a73dd9d167bd85d0e8ef4293836a8f82b786c63fb1a429bc3e678/multidict-6.6.4-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cc356250cffd6e78416cf5b40dc6a74f1edf3be8e834cf8862d9ed5265cf9b0e", size = 249954, upload-time = "2025-08-11T12:07:54.423Z" }, + { url = "https://files.pythonhosted.org/packages/06/78/6b7c0f020f9aa0acf66d0ab4eb9f08375bac9a50ff5e3edb1c4ccd59eafc/multidict-6.6.4-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:dadf95aa862714ea468a49ad1e09fe00fcc9ec67d122f6596a8d40caf6cec7d0", size = 228612, upload-time = "2025-08-11T12:07:55.914Z" }, + { url = "https://files.pythonhosted.org/packages/00/44/3faa416f89b2d5d76e9d447296a81521e1c832ad6e40b92f990697b43192/multidict-6.6.4-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7dd57515bebffd8ebd714d101d4c434063322e4fe24042e90ced41f18b6d3395", size = 257528, upload-time = "2025-08-11T12:07:57.371Z" }, + { url = "https://files.pythonhosted.org/packages/05/5f/77c03b89af0fcb16f018f668207768191fb9dcfb5e3361a5e706a11db2c9/multidict-6.6.4-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:967af5f238ebc2eb1da4e77af5492219fbd9b4b812347da39a7b5f5c72c0fa45", size = 256329, upload-time = "2025-08-11T12:07:58.844Z" }, + { url = "https://files.pythonhosted.org/packages/cf/e9/ed750a2a9afb4f8dc6f13dc5b67b514832101b95714f1211cd42e0aafc26/multidict-6.6.4-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2a4c6875c37aae9794308ec43e3530e4aa0d36579ce38d89979bbf89582002bb", size = 247928, upload-time = "2025-08-11T12:08:01.037Z" }, + { url = "https://files.pythonhosted.org/packages/1f/b5/e0571bc13cda277db7e6e8a532791d4403dacc9850006cb66d2556e649c0/multidict-6.6.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:7f683a551e92bdb7fac545b9c6f9fa2aebdeefa61d607510b3533286fcab67f5", size = 245228, upload-time = "2025-08-11T12:08:02.96Z" }, + { url = "https://files.pythonhosted.org/packages/f3/a3/69a84b0eccb9824491f06368f5b86e72e4af54c3067c37c39099b6687109/multidict-6.6.4-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:3ba5aaf600edaf2a868a391779f7a85d93bed147854925f34edd24cc70a3e141", size = 235869, upload-time = "2025-08-11T12:08:04.746Z" }, + { url = "https://files.pythonhosted.org/packages/a9/9d/28802e8f9121a6a0804fa009debf4e753d0a59969ea9f70be5f5fdfcb18f/multidict-6.6.4-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:580b643b7fd2c295d83cad90d78419081f53fd532d1f1eb67ceb7060f61cff0d", size = 243446, upload-time = "2025-08-11T12:08:06.332Z" }, + { url = "https://files.pythonhosted.org/packages/38/ea/6c98add069b4878c1d66428a5f5149ddb6d32b1f9836a826ac764b9940be/multidict-6.6.4-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:37b7187197da6af3ee0b044dbc9625afd0c885f2800815b228a0e70f9a7f473d", size = 252299, upload-time = "2025-08-11T12:08:07.931Z" }, + { url = "https://files.pythonhosted.org/packages/3a/09/8fe02d204473e14c0af3affd50af9078839dfca1742f025cca765435d6b4/multidict-6.6.4-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e1b93790ed0bc26feb72e2f08299691ceb6da5e9e14a0d13cc74f1869af327a0", size = 246926, upload-time = "2025-08-11T12:08:09.467Z" }, + { url = "https://files.pythonhosted.org/packages/37/3d/7b1e10d774a6df5175ecd3c92bff069e77bed9ec2a927fdd4ff5fe182f67/multidict-6.6.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a506a77ddee1efcca81ecbeae27ade3e09cdf21a8ae854d766c2bb4f14053f92", size = 243383, upload-time = "2025-08-11T12:08:10.981Z" }, + { url = "https://files.pythonhosted.org/packages/50/b0/a6fae46071b645ae98786ab738447de1ef53742eaad949f27e960864bb49/multidict-6.6.4-cp313-cp313t-win32.whl", hash = "sha256:f93b2b2279883d1d0a9e1bd01f312d6fc315c5e4c1f09e112e4736e2f650bc4e", size = 47775, upload-time = "2025-08-11T12:08:12.439Z" }, + { url = "https://files.pythonhosted.org/packages/b2/0a/2436550b1520091af0600dff547913cb2d66fbac27a8c33bc1b1bccd8d98/multidict-6.6.4-cp313-cp313t-win_amd64.whl", hash = "sha256:6d46a180acdf6e87cc41dc15d8f5c2986e1e8739dc25dbb7dac826731ef381a4", size = 53100, upload-time = "2025-08-11T12:08:13.823Z" }, + { url = "https://files.pythonhosted.org/packages/97/ea/43ac51faff934086db9c072a94d327d71b7d8b40cd5dcb47311330929ef0/multidict-6.6.4-cp313-cp313t-win_arm64.whl", hash = "sha256:756989334015e3335d087a27331659820d53ba432befdef6a718398b0a8493ad", size = 45501, upload-time = "2025-08-11T12:08:15.173Z" }, + { url = "https://files.pythonhosted.org/packages/fd/69/b547032297c7e63ba2af494edba695d781af8a0c6e89e4d06cf848b21d80/multidict-6.6.4-py3-none-any.whl", hash = "sha256:27d8f8e125c07cb954e54d75d04905a9bba8a439c1d84aca94949d4d03d8601c", size = 12313, upload-time = "2025-08-11T12:08:46.891Z" }, ] [[package]] name = "propcache" version = "0.3.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a6/16/43264e4a779dd8588c21a70f0709665ee8f611211bdd2c87d952cfa7c776/propcache-0.3.2.tar.gz", hash = "sha256:20d7d62e4e7ef05f221e0db2856b979540686342e7dd9973b815599c7057e168", size = 44139, upload_time = "2025-06-09T22:56:06.081Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a6/16/43264e4a779dd8588c21a70f0709665ee8f611211bdd2c87d952cfa7c776/propcache-0.3.2.tar.gz", hash = "sha256:20d7d62e4e7ef05f221e0db2856b979540686342e7dd9973b815599c7057e168", size = 44139, upload-time = "2025-06-09T22:56:06.081Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a8/42/9ca01b0a6f48e81615dca4765a8f1dd2c057e0540f6116a27dc5ee01dfb6/propcache-0.3.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:8de106b6c84506b31c27168582cd3cb3000a6412c16df14a8628e5871ff83c10", size = 73674, upload_time = "2025-06-09T22:54:30.551Z" }, - { url = "https://files.pythonhosted.org/packages/af/6e/21293133beb550f9c901bbece755d582bfaf2176bee4774000bd4dd41884/propcache-0.3.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:28710b0d3975117239c76600ea351934ac7b5ff56e60953474342608dbbb6154", size = 43570, upload_time = "2025-06-09T22:54:32.296Z" }, - { url = "https://files.pythonhosted.org/packages/0c/c8/0393a0a3a2b8760eb3bde3c147f62b20044f0ddac81e9d6ed7318ec0d852/propcache-0.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce26862344bdf836650ed2487c3d724b00fbfec4233a1013f597b78c1cb73615", size = 43094, upload_time = "2025-06-09T22:54:33.929Z" }, - { url = "https://files.pythonhosted.org/packages/37/2c/489afe311a690399d04a3e03b069225670c1d489eb7b044a566511c1c498/propcache-0.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bca54bd347a253af2cf4544bbec232ab982f4868de0dd684246b67a51bc6b1db", size = 226958, upload_time = "2025-06-09T22:54:35.186Z" }, - { url = "https://files.pythonhosted.org/packages/9d/ca/63b520d2f3d418c968bf596839ae26cf7f87bead026b6192d4da6a08c467/propcache-0.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:55780d5e9a2ddc59711d727226bb1ba83a22dd32f64ee15594b9392b1f544eb1", size = 234894, upload_time = "2025-06-09T22:54:36.708Z" }, - { url = "https://files.pythonhosted.org/packages/11/60/1d0ed6fff455a028d678df30cc28dcee7af77fa2b0e6962ce1df95c9a2a9/propcache-0.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:035e631be25d6975ed87ab23153db6a73426a48db688070d925aa27e996fe93c", size = 233672, upload_time = "2025-06-09T22:54:38.062Z" }, - { url = "https://files.pythonhosted.org/packages/37/7c/54fd5301ef38505ab235d98827207176a5c9b2aa61939b10a460ca53e123/propcache-0.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ee6f22b6eaa39297c751d0e80c0d3a454f112f5c6481214fcf4c092074cecd67", size = 224395, upload_time = "2025-06-09T22:54:39.634Z" }, - { url = "https://files.pythonhosted.org/packages/ee/1a/89a40e0846f5de05fdc6779883bf46ba980e6df4d2ff8fb02643de126592/propcache-0.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7ca3aee1aa955438c4dba34fc20a9f390e4c79967257d830f137bd5a8a32ed3b", size = 212510, upload_time = "2025-06-09T22:54:41.565Z" }, - { url = "https://files.pythonhosted.org/packages/5e/33/ca98368586c9566a6b8d5ef66e30484f8da84c0aac3f2d9aec6d31a11bd5/propcache-0.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7a4f30862869fa2b68380d677cc1c5fcf1e0f2b9ea0cf665812895c75d0ca3b8", size = 222949, upload_time = "2025-06-09T22:54:43.038Z" }, - { url = "https://files.pythonhosted.org/packages/ba/11/ace870d0aafe443b33b2f0b7efdb872b7c3abd505bfb4890716ad7865e9d/propcache-0.3.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:b77ec3c257d7816d9f3700013639db7491a434644c906a2578a11daf13176251", size = 217258, upload_time = "2025-06-09T22:54:44.376Z" }, - { url = "https://files.pythonhosted.org/packages/5b/d2/86fd6f7adffcfc74b42c10a6b7db721d1d9ca1055c45d39a1a8f2a740a21/propcache-0.3.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:cab90ac9d3f14b2d5050928483d3d3b8fb6b4018893fc75710e6aa361ecb2474", size = 213036, upload_time = "2025-06-09T22:54:46.243Z" }, - { url = "https://files.pythonhosted.org/packages/07/94/2d7d1e328f45ff34a0a284cf5a2847013701e24c2a53117e7c280a4316b3/propcache-0.3.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:0b504d29f3c47cf6b9e936c1852246c83d450e8e063d50562115a6be6d3a2535", size = 227684, upload_time = "2025-06-09T22:54:47.63Z" }, - { url = "https://files.pythonhosted.org/packages/b7/05/37ae63a0087677e90b1d14710e532ff104d44bc1efa3b3970fff99b891dc/propcache-0.3.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:ce2ac2675a6aa41ddb2a0c9cbff53780a617ac3d43e620f8fd77ba1c84dcfc06", size = 234562, upload_time = "2025-06-09T22:54:48.982Z" }, - { url = "https://files.pythonhosted.org/packages/a4/7c/3f539fcae630408d0bd8bf3208b9a647ccad10976eda62402a80adf8fc34/propcache-0.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:62b4239611205294cc433845b914131b2a1f03500ff3c1ed093ed216b82621e1", size = 222142, upload_time = "2025-06-09T22:54:50.424Z" }, - { url = "https://files.pythonhosted.org/packages/7c/d2/34b9eac8c35f79f8a962546b3e97e9d4b990c420ee66ac8255d5d9611648/propcache-0.3.2-cp312-cp312-win32.whl", hash = "sha256:df4a81b9b53449ebc90cc4deefb052c1dd934ba85012aa912c7ea7b7e38b60c1", size = 37711, upload_time = "2025-06-09T22:54:52.072Z" }, - { url = "https://files.pythonhosted.org/packages/19/61/d582be5d226cf79071681d1b46b848d6cb03d7b70af7063e33a2787eaa03/propcache-0.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:7046e79b989d7fe457bb755844019e10f693752d169076138abf17f31380800c", size = 41479, upload_time = "2025-06-09T22:54:53.234Z" }, - { url = "https://files.pythonhosted.org/packages/dc/d1/8c747fafa558c603c4ca19d8e20b288aa0c7cda74e9402f50f31eb65267e/propcache-0.3.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ca592ed634a73ca002967458187109265e980422116c0a107cf93d81f95af945", size = 71286, upload_time = "2025-06-09T22:54:54.369Z" }, - { url = "https://files.pythonhosted.org/packages/61/99/d606cb7986b60d89c36de8a85d58764323b3a5ff07770a99d8e993b3fa73/propcache-0.3.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9ecb0aad4020e275652ba3975740f241bd12a61f1a784df044cf7477a02bc252", size = 42425, upload_time = "2025-06-09T22:54:55.642Z" }, - { url = "https://files.pythonhosted.org/packages/8c/96/ef98f91bbb42b79e9bb82bdd348b255eb9d65f14dbbe3b1594644c4073f7/propcache-0.3.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7f08f1cc28bd2eade7a8a3d2954ccc673bb02062e3e7da09bc75d843386b342f", size = 41846, upload_time = "2025-06-09T22:54:57.246Z" }, - { url = "https://files.pythonhosted.org/packages/5b/ad/3f0f9a705fb630d175146cd7b1d2bf5555c9beaed54e94132b21aac098a6/propcache-0.3.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d1a342c834734edb4be5ecb1e9fb48cb64b1e2320fccbd8c54bf8da8f2a84c33", size = 208871, upload_time = "2025-06-09T22:54:58.975Z" }, - { url = "https://files.pythonhosted.org/packages/3a/38/2085cda93d2c8b6ec3e92af2c89489a36a5886b712a34ab25de9fbca7992/propcache-0.3.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8a544caaae1ac73f1fecfae70ded3e93728831affebd017d53449e3ac052ac1e", size = 215720, upload_time = "2025-06-09T22:55:00.471Z" }, - { url = "https://files.pythonhosted.org/packages/61/c1/d72ea2dc83ac7f2c8e182786ab0fc2c7bd123a1ff9b7975bee671866fe5f/propcache-0.3.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:310d11aa44635298397db47a3ebce7db99a4cc4b9bbdfcf6c98a60c8d5261cf1", size = 215203, upload_time = "2025-06-09T22:55:01.834Z" }, - { url = "https://files.pythonhosted.org/packages/af/81/b324c44ae60c56ef12007105f1460d5c304b0626ab0cc6b07c8f2a9aa0b8/propcache-0.3.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c1396592321ac83157ac03a2023aa6cc4a3cc3cfdecb71090054c09e5a7cce3", size = 206365, upload_time = "2025-06-09T22:55:03.199Z" }, - { url = "https://files.pythonhosted.org/packages/09/73/88549128bb89e66d2aff242488f62869014ae092db63ccea53c1cc75a81d/propcache-0.3.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8cabf5b5902272565e78197edb682017d21cf3b550ba0460ee473753f28d23c1", size = 196016, upload_time = "2025-06-09T22:55:04.518Z" }, - { url = "https://files.pythonhosted.org/packages/b9/3f/3bdd14e737d145114a5eb83cb172903afba7242f67c5877f9909a20d948d/propcache-0.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0a2f2235ac46a7aa25bdeb03a9e7060f6ecbd213b1f9101c43b3090ffb971ef6", size = 205596, upload_time = "2025-06-09T22:55:05.942Z" }, - { url = "https://files.pythonhosted.org/packages/0f/ca/2f4aa819c357d3107c3763d7ef42c03980f9ed5c48c82e01e25945d437c1/propcache-0.3.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:92b69e12e34869a6970fd2f3da91669899994b47c98f5d430b781c26f1d9f387", size = 200977, upload_time = "2025-06-09T22:55:07.792Z" }, - { url = "https://files.pythonhosted.org/packages/cd/4a/e65276c7477533c59085251ae88505caf6831c0e85ff8b2e31ebcbb949b1/propcache-0.3.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:54e02207c79968ebbdffc169591009f4474dde3b4679e16634d34c9363ff56b4", size = 197220, upload_time = "2025-06-09T22:55:09.173Z" }, - { url = "https://files.pythonhosted.org/packages/7c/54/fc7152e517cf5578278b242396ce4d4b36795423988ef39bb8cd5bf274c8/propcache-0.3.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4adfb44cb588001f68c5466579d3f1157ca07f7504fc91ec87862e2b8e556b88", size = 210642, upload_time = "2025-06-09T22:55:10.62Z" }, - { url = "https://files.pythonhosted.org/packages/b9/80/abeb4a896d2767bf5f1ea7b92eb7be6a5330645bd7fb844049c0e4045d9d/propcache-0.3.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:fd3e6019dc1261cd0291ee8919dd91fbab7b169bb76aeef6c716833a3f65d206", size = 212789, upload_time = "2025-06-09T22:55:12.029Z" }, - { url = "https://files.pythonhosted.org/packages/b3/db/ea12a49aa7b2b6d68a5da8293dcf50068d48d088100ac016ad92a6a780e6/propcache-0.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4c181cad81158d71c41a2bce88edce078458e2dd5ffee7eddd6b05da85079f43", size = 205880, upload_time = "2025-06-09T22:55:13.45Z" }, - { url = "https://files.pythonhosted.org/packages/d1/e5/9076a0bbbfb65d1198007059c65639dfd56266cf8e477a9707e4b1999ff4/propcache-0.3.2-cp313-cp313-win32.whl", hash = "sha256:8a08154613f2249519e549de2330cf8e2071c2887309a7b07fb56098f5170a02", size = 37220, upload_time = "2025-06-09T22:55:15.284Z" }, - { url = "https://files.pythonhosted.org/packages/d3/f5/b369e026b09a26cd77aa88d8fffd69141d2ae00a2abaaf5380d2603f4b7f/propcache-0.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:e41671f1594fc4ab0a6dec1351864713cb3a279910ae8b58f884a88a0a632c05", size = 40678, upload_time = "2025-06-09T22:55:16.445Z" }, - { url = "https://files.pythonhosted.org/packages/a4/3a/6ece377b55544941a08d03581c7bc400a3c8cd3c2865900a68d5de79e21f/propcache-0.3.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:9a3cf035bbaf035f109987d9d55dc90e4b0e36e04bbbb95af3055ef17194057b", size = 76560, upload_time = "2025-06-09T22:55:17.598Z" }, - { url = "https://files.pythonhosted.org/packages/0c/da/64a2bb16418740fa634b0e9c3d29edff1db07f56d3546ca2d86ddf0305e1/propcache-0.3.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:156c03d07dc1323d8dacaa221fbe028c5c70d16709cdd63502778e6c3ccca1b0", size = 44676, upload_time = "2025-06-09T22:55:18.922Z" }, - { url = "https://files.pythonhosted.org/packages/36/7b/f025e06ea51cb72c52fb87e9b395cced02786610b60a3ed51da8af017170/propcache-0.3.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:74413c0ba02ba86f55cf60d18daab219f7e531620c15f1e23d95563f505efe7e", size = 44701, upload_time = "2025-06-09T22:55:20.106Z" }, - { url = "https://files.pythonhosted.org/packages/a4/00/faa1b1b7c3b74fc277f8642f32a4c72ba1d7b2de36d7cdfb676db7f4303e/propcache-0.3.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f066b437bb3fa39c58ff97ab2ca351db465157d68ed0440abecb21715eb24b28", size = 276934, upload_time = "2025-06-09T22:55:21.5Z" }, - { url = "https://files.pythonhosted.org/packages/74/ab/935beb6f1756e0476a4d5938ff44bf0d13a055fed880caf93859b4f1baf4/propcache-0.3.2-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f1304b085c83067914721e7e9d9917d41ad87696bf70f0bc7dee450e9c71ad0a", size = 278316, upload_time = "2025-06-09T22:55:22.918Z" }, - { url = "https://files.pythonhosted.org/packages/f8/9d/994a5c1ce4389610838d1caec74bdf0e98b306c70314d46dbe4fcf21a3e2/propcache-0.3.2-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ab50cef01b372763a13333b4e54021bdcb291fc9a8e2ccb9c2df98be51bcde6c", size = 282619, upload_time = "2025-06-09T22:55:24.651Z" }, - { url = "https://files.pythonhosted.org/packages/2b/00/a10afce3d1ed0287cef2e09506d3be9822513f2c1e96457ee369adb9a6cd/propcache-0.3.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fad3b2a085ec259ad2c2842666b2a0a49dea8463579c606426128925af1ed725", size = 265896, upload_time = "2025-06-09T22:55:26.049Z" }, - { url = "https://files.pythonhosted.org/packages/2e/a8/2aa6716ffa566ca57c749edb909ad27884680887d68517e4be41b02299f3/propcache-0.3.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:261fa020c1c14deafd54c76b014956e2f86991af198c51139faf41c4d5e83892", size = 252111, upload_time = "2025-06-09T22:55:27.381Z" }, - { url = "https://files.pythonhosted.org/packages/36/4f/345ca9183b85ac29c8694b0941f7484bf419c7f0fea2d1e386b4f7893eed/propcache-0.3.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:46d7f8aa79c927e5f987ee3a80205c987717d3659f035c85cf0c3680526bdb44", size = 268334, upload_time = "2025-06-09T22:55:28.747Z" }, - { url = "https://files.pythonhosted.org/packages/3e/ca/fcd54f78b59e3f97b3b9715501e3147f5340167733d27db423aa321e7148/propcache-0.3.2-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:6d8f3f0eebf73e3c0ff0e7853f68be638b4043c65a70517bb575eff54edd8dbe", size = 255026, upload_time = "2025-06-09T22:55:30.184Z" }, - { url = "https://files.pythonhosted.org/packages/8b/95/8e6a6bbbd78ac89c30c225210a5c687790e532ba4088afb8c0445b77ef37/propcache-0.3.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:03c89c1b14a5452cf15403e291c0ccd7751d5b9736ecb2c5bab977ad6c5bcd81", size = 250724, upload_time = "2025-06-09T22:55:31.646Z" }, - { url = "https://files.pythonhosted.org/packages/ee/b0/0dd03616142baba28e8b2d14ce5df6631b4673850a3d4f9c0f9dd714a404/propcache-0.3.2-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:0cc17efde71e12bbaad086d679ce575268d70bc123a5a71ea7ad76f70ba30bba", size = 268868, upload_time = "2025-06-09T22:55:33.209Z" }, - { url = "https://files.pythonhosted.org/packages/c5/98/2c12407a7e4fbacd94ddd32f3b1e3d5231e77c30ef7162b12a60e2dd5ce3/propcache-0.3.2-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:acdf05d00696bc0447e278bb53cb04ca72354e562cf88ea6f9107df8e7fd9770", size = 271322, upload_time = "2025-06-09T22:55:35.065Z" }, - { url = "https://files.pythonhosted.org/packages/35/91/9cb56efbb428b006bb85db28591e40b7736847b8331d43fe335acf95f6c8/propcache-0.3.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4445542398bd0b5d32df908031cb1b30d43ac848e20470a878b770ec2dcc6330", size = 265778, upload_time = "2025-06-09T22:55:36.45Z" }, - { url = "https://files.pythonhosted.org/packages/9a/4c/b0fe775a2bdd01e176b14b574be679d84fc83958335790f7c9a686c1f468/propcache-0.3.2-cp313-cp313t-win32.whl", hash = "sha256:f86e5d7cd03afb3a1db8e9f9f6eff15794e79e791350ac48a8c924e6f439f394", size = 41175, upload_time = "2025-06-09T22:55:38.436Z" }, - { url = "https://files.pythonhosted.org/packages/a4/ff/47f08595e3d9b5e149c150f88d9714574f1a7cbd89fe2817158a952674bf/propcache-0.3.2-cp313-cp313t-win_amd64.whl", hash = "sha256:9704bedf6e7cbe3c65eca4379a9b53ee6a83749f047808cbb5044d40d7d72198", size = 44857, upload_time = "2025-06-09T22:55:39.687Z" }, - { url = "https://files.pythonhosted.org/packages/cc/35/cc0aaecf278bb4575b8555f2b137de5ab821595ddae9da9d3cd1da4072c7/propcache-0.3.2-py3-none-any.whl", hash = "sha256:98f1ec44fb675f5052cccc8e609c46ed23a35a1cfd18545ad4e29002d858a43f", size = 12663, upload_time = "2025-06-09T22:56:04.484Z" }, + { url = "https://files.pythonhosted.org/packages/a8/42/9ca01b0a6f48e81615dca4765a8f1dd2c057e0540f6116a27dc5ee01dfb6/propcache-0.3.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:8de106b6c84506b31c27168582cd3cb3000a6412c16df14a8628e5871ff83c10", size = 73674, upload-time = "2025-06-09T22:54:30.551Z" }, + { url = "https://files.pythonhosted.org/packages/af/6e/21293133beb550f9c901bbece755d582bfaf2176bee4774000bd4dd41884/propcache-0.3.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:28710b0d3975117239c76600ea351934ac7b5ff56e60953474342608dbbb6154", size = 43570, upload-time = "2025-06-09T22:54:32.296Z" }, + { url = "https://files.pythonhosted.org/packages/0c/c8/0393a0a3a2b8760eb3bde3c147f62b20044f0ddac81e9d6ed7318ec0d852/propcache-0.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce26862344bdf836650ed2487c3d724b00fbfec4233a1013f597b78c1cb73615", size = 43094, upload-time = "2025-06-09T22:54:33.929Z" }, + { url = "https://files.pythonhosted.org/packages/37/2c/489afe311a690399d04a3e03b069225670c1d489eb7b044a566511c1c498/propcache-0.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bca54bd347a253af2cf4544bbec232ab982f4868de0dd684246b67a51bc6b1db", size = 226958, upload-time = "2025-06-09T22:54:35.186Z" }, + { url = "https://files.pythonhosted.org/packages/9d/ca/63b520d2f3d418c968bf596839ae26cf7f87bead026b6192d4da6a08c467/propcache-0.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:55780d5e9a2ddc59711d727226bb1ba83a22dd32f64ee15594b9392b1f544eb1", size = 234894, upload-time = "2025-06-09T22:54:36.708Z" }, + { url = "https://files.pythonhosted.org/packages/11/60/1d0ed6fff455a028d678df30cc28dcee7af77fa2b0e6962ce1df95c9a2a9/propcache-0.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:035e631be25d6975ed87ab23153db6a73426a48db688070d925aa27e996fe93c", size = 233672, upload-time = "2025-06-09T22:54:38.062Z" }, + { url = "https://files.pythonhosted.org/packages/37/7c/54fd5301ef38505ab235d98827207176a5c9b2aa61939b10a460ca53e123/propcache-0.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ee6f22b6eaa39297c751d0e80c0d3a454f112f5c6481214fcf4c092074cecd67", size = 224395, upload-time = "2025-06-09T22:54:39.634Z" }, + { url = "https://files.pythonhosted.org/packages/ee/1a/89a40e0846f5de05fdc6779883bf46ba980e6df4d2ff8fb02643de126592/propcache-0.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7ca3aee1aa955438c4dba34fc20a9f390e4c79967257d830f137bd5a8a32ed3b", size = 212510, upload-time = "2025-06-09T22:54:41.565Z" }, + { url = "https://files.pythonhosted.org/packages/5e/33/ca98368586c9566a6b8d5ef66e30484f8da84c0aac3f2d9aec6d31a11bd5/propcache-0.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7a4f30862869fa2b68380d677cc1c5fcf1e0f2b9ea0cf665812895c75d0ca3b8", size = 222949, upload-time = "2025-06-09T22:54:43.038Z" }, + { url = "https://files.pythonhosted.org/packages/ba/11/ace870d0aafe443b33b2f0b7efdb872b7c3abd505bfb4890716ad7865e9d/propcache-0.3.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:b77ec3c257d7816d9f3700013639db7491a434644c906a2578a11daf13176251", size = 217258, upload-time = "2025-06-09T22:54:44.376Z" }, + { url = "https://files.pythonhosted.org/packages/5b/d2/86fd6f7adffcfc74b42c10a6b7db721d1d9ca1055c45d39a1a8f2a740a21/propcache-0.3.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:cab90ac9d3f14b2d5050928483d3d3b8fb6b4018893fc75710e6aa361ecb2474", size = 213036, upload-time = "2025-06-09T22:54:46.243Z" }, + { url = "https://files.pythonhosted.org/packages/07/94/2d7d1e328f45ff34a0a284cf5a2847013701e24c2a53117e7c280a4316b3/propcache-0.3.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:0b504d29f3c47cf6b9e936c1852246c83d450e8e063d50562115a6be6d3a2535", size = 227684, upload-time = "2025-06-09T22:54:47.63Z" }, + { url = "https://files.pythonhosted.org/packages/b7/05/37ae63a0087677e90b1d14710e532ff104d44bc1efa3b3970fff99b891dc/propcache-0.3.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:ce2ac2675a6aa41ddb2a0c9cbff53780a617ac3d43e620f8fd77ba1c84dcfc06", size = 234562, upload-time = "2025-06-09T22:54:48.982Z" }, + { url = "https://files.pythonhosted.org/packages/a4/7c/3f539fcae630408d0bd8bf3208b9a647ccad10976eda62402a80adf8fc34/propcache-0.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:62b4239611205294cc433845b914131b2a1f03500ff3c1ed093ed216b82621e1", size = 222142, upload-time = "2025-06-09T22:54:50.424Z" }, + { url = "https://files.pythonhosted.org/packages/7c/d2/34b9eac8c35f79f8a962546b3e97e9d4b990c420ee66ac8255d5d9611648/propcache-0.3.2-cp312-cp312-win32.whl", hash = "sha256:df4a81b9b53449ebc90cc4deefb052c1dd934ba85012aa912c7ea7b7e38b60c1", size = 37711, upload-time = "2025-06-09T22:54:52.072Z" }, + { url = "https://files.pythonhosted.org/packages/19/61/d582be5d226cf79071681d1b46b848d6cb03d7b70af7063e33a2787eaa03/propcache-0.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:7046e79b989d7fe457bb755844019e10f693752d169076138abf17f31380800c", size = 41479, upload-time = "2025-06-09T22:54:53.234Z" }, + { url = "https://files.pythonhosted.org/packages/dc/d1/8c747fafa558c603c4ca19d8e20b288aa0c7cda74e9402f50f31eb65267e/propcache-0.3.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ca592ed634a73ca002967458187109265e980422116c0a107cf93d81f95af945", size = 71286, upload-time = "2025-06-09T22:54:54.369Z" }, + { url = "https://files.pythonhosted.org/packages/61/99/d606cb7986b60d89c36de8a85d58764323b3a5ff07770a99d8e993b3fa73/propcache-0.3.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9ecb0aad4020e275652ba3975740f241bd12a61f1a784df044cf7477a02bc252", size = 42425, upload-time = "2025-06-09T22:54:55.642Z" }, + { url = "https://files.pythonhosted.org/packages/8c/96/ef98f91bbb42b79e9bb82bdd348b255eb9d65f14dbbe3b1594644c4073f7/propcache-0.3.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7f08f1cc28bd2eade7a8a3d2954ccc673bb02062e3e7da09bc75d843386b342f", size = 41846, upload-time = "2025-06-09T22:54:57.246Z" }, + { url = "https://files.pythonhosted.org/packages/5b/ad/3f0f9a705fb630d175146cd7b1d2bf5555c9beaed54e94132b21aac098a6/propcache-0.3.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d1a342c834734edb4be5ecb1e9fb48cb64b1e2320fccbd8c54bf8da8f2a84c33", size = 208871, upload-time = "2025-06-09T22:54:58.975Z" }, + { url = "https://files.pythonhosted.org/packages/3a/38/2085cda93d2c8b6ec3e92af2c89489a36a5886b712a34ab25de9fbca7992/propcache-0.3.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8a544caaae1ac73f1fecfae70ded3e93728831affebd017d53449e3ac052ac1e", size = 215720, upload-time = "2025-06-09T22:55:00.471Z" }, + { url = "https://files.pythonhosted.org/packages/61/c1/d72ea2dc83ac7f2c8e182786ab0fc2c7bd123a1ff9b7975bee671866fe5f/propcache-0.3.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:310d11aa44635298397db47a3ebce7db99a4cc4b9bbdfcf6c98a60c8d5261cf1", size = 215203, upload-time = "2025-06-09T22:55:01.834Z" }, + { url = "https://files.pythonhosted.org/packages/af/81/b324c44ae60c56ef12007105f1460d5c304b0626ab0cc6b07c8f2a9aa0b8/propcache-0.3.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c1396592321ac83157ac03a2023aa6cc4a3cc3cfdecb71090054c09e5a7cce3", size = 206365, upload-time = "2025-06-09T22:55:03.199Z" }, + { url = "https://files.pythonhosted.org/packages/09/73/88549128bb89e66d2aff242488f62869014ae092db63ccea53c1cc75a81d/propcache-0.3.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8cabf5b5902272565e78197edb682017d21cf3b550ba0460ee473753f28d23c1", size = 196016, upload-time = "2025-06-09T22:55:04.518Z" }, + { url = "https://files.pythonhosted.org/packages/b9/3f/3bdd14e737d145114a5eb83cb172903afba7242f67c5877f9909a20d948d/propcache-0.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0a2f2235ac46a7aa25bdeb03a9e7060f6ecbd213b1f9101c43b3090ffb971ef6", size = 205596, upload-time = "2025-06-09T22:55:05.942Z" }, + { url = "https://files.pythonhosted.org/packages/0f/ca/2f4aa819c357d3107c3763d7ef42c03980f9ed5c48c82e01e25945d437c1/propcache-0.3.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:92b69e12e34869a6970fd2f3da91669899994b47c98f5d430b781c26f1d9f387", size = 200977, upload-time = "2025-06-09T22:55:07.792Z" }, + { url = "https://files.pythonhosted.org/packages/cd/4a/e65276c7477533c59085251ae88505caf6831c0e85ff8b2e31ebcbb949b1/propcache-0.3.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:54e02207c79968ebbdffc169591009f4474dde3b4679e16634d34c9363ff56b4", size = 197220, upload-time = "2025-06-09T22:55:09.173Z" }, + { url = "https://files.pythonhosted.org/packages/7c/54/fc7152e517cf5578278b242396ce4d4b36795423988ef39bb8cd5bf274c8/propcache-0.3.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4adfb44cb588001f68c5466579d3f1157ca07f7504fc91ec87862e2b8e556b88", size = 210642, upload-time = "2025-06-09T22:55:10.62Z" }, + { url = "https://files.pythonhosted.org/packages/b9/80/abeb4a896d2767bf5f1ea7b92eb7be6a5330645bd7fb844049c0e4045d9d/propcache-0.3.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:fd3e6019dc1261cd0291ee8919dd91fbab7b169bb76aeef6c716833a3f65d206", size = 212789, upload-time = "2025-06-09T22:55:12.029Z" }, + { url = "https://files.pythonhosted.org/packages/b3/db/ea12a49aa7b2b6d68a5da8293dcf50068d48d088100ac016ad92a6a780e6/propcache-0.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4c181cad81158d71c41a2bce88edce078458e2dd5ffee7eddd6b05da85079f43", size = 205880, upload-time = "2025-06-09T22:55:13.45Z" }, + { url = "https://files.pythonhosted.org/packages/d1/e5/9076a0bbbfb65d1198007059c65639dfd56266cf8e477a9707e4b1999ff4/propcache-0.3.2-cp313-cp313-win32.whl", hash = "sha256:8a08154613f2249519e549de2330cf8e2071c2887309a7b07fb56098f5170a02", size = 37220, upload-time = "2025-06-09T22:55:15.284Z" }, + { url = "https://files.pythonhosted.org/packages/d3/f5/b369e026b09a26cd77aa88d8fffd69141d2ae00a2abaaf5380d2603f4b7f/propcache-0.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:e41671f1594fc4ab0a6dec1351864713cb3a279910ae8b58f884a88a0a632c05", size = 40678, upload-time = "2025-06-09T22:55:16.445Z" }, + { url = "https://files.pythonhosted.org/packages/a4/3a/6ece377b55544941a08d03581c7bc400a3c8cd3c2865900a68d5de79e21f/propcache-0.3.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:9a3cf035bbaf035f109987d9d55dc90e4b0e36e04bbbb95af3055ef17194057b", size = 76560, upload-time = "2025-06-09T22:55:17.598Z" }, + { url = "https://files.pythonhosted.org/packages/0c/da/64a2bb16418740fa634b0e9c3d29edff1db07f56d3546ca2d86ddf0305e1/propcache-0.3.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:156c03d07dc1323d8dacaa221fbe028c5c70d16709cdd63502778e6c3ccca1b0", size = 44676, upload-time = "2025-06-09T22:55:18.922Z" }, + { url = "https://files.pythonhosted.org/packages/36/7b/f025e06ea51cb72c52fb87e9b395cced02786610b60a3ed51da8af017170/propcache-0.3.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:74413c0ba02ba86f55cf60d18daab219f7e531620c15f1e23d95563f505efe7e", size = 44701, upload-time = "2025-06-09T22:55:20.106Z" }, + { url = "https://files.pythonhosted.org/packages/a4/00/faa1b1b7c3b74fc277f8642f32a4c72ba1d7b2de36d7cdfb676db7f4303e/propcache-0.3.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f066b437bb3fa39c58ff97ab2ca351db465157d68ed0440abecb21715eb24b28", size = 276934, upload-time = "2025-06-09T22:55:21.5Z" }, + { url = "https://files.pythonhosted.org/packages/74/ab/935beb6f1756e0476a4d5938ff44bf0d13a055fed880caf93859b4f1baf4/propcache-0.3.2-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f1304b085c83067914721e7e9d9917d41ad87696bf70f0bc7dee450e9c71ad0a", size = 278316, upload-time = "2025-06-09T22:55:22.918Z" }, + { url = "https://files.pythonhosted.org/packages/f8/9d/994a5c1ce4389610838d1caec74bdf0e98b306c70314d46dbe4fcf21a3e2/propcache-0.3.2-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ab50cef01b372763a13333b4e54021bdcb291fc9a8e2ccb9c2df98be51bcde6c", size = 282619, upload-time = "2025-06-09T22:55:24.651Z" }, + { url = "https://files.pythonhosted.org/packages/2b/00/a10afce3d1ed0287cef2e09506d3be9822513f2c1e96457ee369adb9a6cd/propcache-0.3.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fad3b2a085ec259ad2c2842666b2a0a49dea8463579c606426128925af1ed725", size = 265896, upload-time = "2025-06-09T22:55:26.049Z" }, + { url = "https://files.pythonhosted.org/packages/2e/a8/2aa6716ffa566ca57c749edb909ad27884680887d68517e4be41b02299f3/propcache-0.3.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:261fa020c1c14deafd54c76b014956e2f86991af198c51139faf41c4d5e83892", size = 252111, upload-time = "2025-06-09T22:55:27.381Z" }, + { url = "https://files.pythonhosted.org/packages/36/4f/345ca9183b85ac29c8694b0941f7484bf419c7f0fea2d1e386b4f7893eed/propcache-0.3.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:46d7f8aa79c927e5f987ee3a80205c987717d3659f035c85cf0c3680526bdb44", size = 268334, upload-time = "2025-06-09T22:55:28.747Z" }, + { url = "https://files.pythonhosted.org/packages/3e/ca/fcd54f78b59e3f97b3b9715501e3147f5340167733d27db423aa321e7148/propcache-0.3.2-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:6d8f3f0eebf73e3c0ff0e7853f68be638b4043c65a70517bb575eff54edd8dbe", size = 255026, upload-time = "2025-06-09T22:55:30.184Z" }, + { url = "https://files.pythonhosted.org/packages/8b/95/8e6a6bbbd78ac89c30c225210a5c687790e532ba4088afb8c0445b77ef37/propcache-0.3.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:03c89c1b14a5452cf15403e291c0ccd7751d5b9736ecb2c5bab977ad6c5bcd81", size = 250724, upload-time = "2025-06-09T22:55:31.646Z" }, + { url = "https://files.pythonhosted.org/packages/ee/b0/0dd03616142baba28e8b2d14ce5df6631b4673850a3d4f9c0f9dd714a404/propcache-0.3.2-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:0cc17efde71e12bbaad086d679ce575268d70bc123a5a71ea7ad76f70ba30bba", size = 268868, upload-time = "2025-06-09T22:55:33.209Z" }, + { url = "https://files.pythonhosted.org/packages/c5/98/2c12407a7e4fbacd94ddd32f3b1e3d5231e77c30ef7162b12a60e2dd5ce3/propcache-0.3.2-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:acdf05d00696bc0447e278bb53cb04ca72354e562cf88ea6f9107df8e7fd9770", size = 271322, upload-time = "2025-06-09T22:55:35.065Z" }, + { url = "https://files.pythonhosted.org/packages/35/91/9cb56efbb428b006bb85db28591e40b7736847b8331d43fe335acf95f6c8/propcache-0.3.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4445542398bd0b5d32df908031cb1b30d43ac848e20470a878b770ec2dcc6330", size = 265778, upload-time = "2025-06-09T22:55:36.45Z" }, + { url = "https://files.pythonhosted.org/packages/9a/4c/b0fe775a2bdd01e176b14b574be679d84fc83958335790f7c9a686c1f468/propcache-0.3.2-cp313-cp313t-win32.whl", hash = "sha256:f86e5d7cd03afb3a1db8e9f9f6eff15794e79e791350ac48a8c924e6f439f394", size = 41175, upload-time = "2025-06-09T22:55:38.436Z" }, + { url = "https://files.pythonhosted.org/packages/a4/ff/47f08595e3d9b5e149c150f88d9714574f1a7cbd89fe2817158a952674bf/propcache-0.3.2-cp313-cp313t-win_amd64.whl", hash = "sha256:9704bedf6e7cbe3c65eca4379a9b53ee6a83749f047808cbb5044d40d7d72198", size = 44857, upload-time = "2025-06-09T22:55:39.687Z" }, + { url = "https://files.pythonhosted.org/packages/cc/35/cc0aaecf278bb4575b8555f2b137de5ab821595ddae9da9d3cd1da4072c7/propcache-0.3.2-py3-none-any.whl", hash = "sha256:98f1ec44fb675f5052cccc8e609c46ed23a35a1cfd18545ad4e29002d858a43f", size = 12663, upload-time = "2025-06-09T22:56:04.484Z" }, ] [[package]] name = "psutil" version = "7.0.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/2a/80/336820c1ad9286a4ded7e845b2eccfcb27851ab8ac6abece774a6ff4d3de/psutil-7.0.0.tar.gz", hash = "sha256:7be9c3eba38beccb6495ea33afd982a44074b78f28c434a1f51cc07fd315c456", size = 497003, upload_time = "2025-02-13T21:54:07.946Z" } +sdist = { url = "https://files.pythonhosted.org/packages/2a/80/336820c1ad9286a4ded7e845b2eccfcb27851ab8ac6abece774a6ff4d3de/psutil-7.0.0.tar.gz", hash = "sha256:7be9c3eba38beccb6495ea33afd982a44074b78f28c434a1f51cc07fd315c456", size = 497003, upload-time = "2025-02-13T21:54:07.946Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ed/e6/2d26234410f8b8abdbf891c9da62bee396583f713fb9f3325a4760875d22/psutil-7.0.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:101d71dc322e3cffd7cea0650b09b3d08b8e7c4109dd6809fe452dfd00e58b25", size = 238051, upload_time = "2025-02-13T21:54:12.36Z" }, - { url = "https://files.pythonhosted.org/packages/04/8b/30f930733afe425e3cbfc0e1468a30a18942350c1a8816acfade80c005c4/psutil-7.0.0-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:39db632f6bb862eeccf56660871433e111b6ea58f2caea825571951d4b6aa3da", size = 239535, upload_time = "2025-02-13T21:54:16.07Z" }, - { url = "https://files.pythonhosted.org/packages/2a/ed/d362e84620dd22876b55389248e522338ed1bf134a5edd3b8231d7207f6d/psutil-7.0.0-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1fcee592b4c6f146991ca55919ea3d1f8926497a713ed7faaf8225e174581e91", size = 275004, upload_time = "2025-02-13T21:54:18.662Z" }, - { url = "https://files.pythonhosted.org/packages/bf/b9/b0eb3f3cbcb734d930fdf839431606844a825b23eaf9a6ab371edac8162c/psutil-7.0.0-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b1388a4f6875d7e2aff5c4ca1cc16c545ed41dd8bb596cefea80111db353a34", size = 277986, upload_time = "2025-02-13T21:54:21.811Z" }, - { url = "https://files.pythonhosted.org/packages/eb/a2/709e0fe2f093556c17fbafda93ac032257242cabcc7ff3369e2cb76a97aa/psutil-7.0.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5f098451abc2828f7dc6b58d44b532b22f2088f4999a937557b603ce72b1993", size = 279544, upload_time = "2025-02-13T21:54:24.68Z" }, - { url = "https://files.pythonhosted.org/packages/50/e6/eecf58810b9d12e6427369784efe814a1eec0f492084ce8eb8f4d89d6d61/psutil-7.0.0-cp37-abi3-win32.whl", hash = "sha256:ba3fcef7523064a6c9da440fc4d6bd07da93ac726b5733c29027d7dc95b39d99", size = 241053, upload_time = "2025-02-13T21:54:34.31Z" }, - { url = "https://files.pythonhosted.org/packages/50/1b/6921afe68c74868b4c9fa424dad3be35b095e16687989ebbb50ce4fceb7c/psutil-7.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:4cf3d4eb1aa9b348dec30105c55cd9b7d4629285735a102beb4441e38db90553", size = 244885, upload_time = "2025-02-13T21:54:37.486Z" }, + { url = "https://files.pythonhosted.org/packages/ed/e6/2d26234410f8b8abdbf891c9da62bee396583f713fb9f3325a4760875d22/psutil-7.0.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:101d71dc322e3cffd7cea0650b09b3d08b8e7c4109dd6809fe452dfd00e58b25", size = 238051, upload-time = "2025-02-13T21:54:12.36Z" }, + { url = "https://files.pythonhosted.org/packages/04/8b/30f930733afe425e3cbfc0e1468a30a18942350c1a8816acfade80c005c4/psutil-7.0.0-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:39db632f6bb862eeccf56660871433e111b6ea58f2caea825571951d4b6aa3da", size = 239535, upload-time = "2025-02-13T21:54:16.07Z" }, + { url = "https://files.pythonhosted.org/packages/2a/ed/d362e84620dd22876b55389248e522338ed1bf134a5edd3b8231d7207f6d/psutil-7.0.0-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1fcee592b4c6f146991ca55919ea3d1f8926497a713ed7faaf8225e174581e91", size = 275004, upload-time = "2025-02-13T21:54:18.662Z" }, + { url = "https://files.pythonhosted.org/packages/bf/b9/b0eb3f3cbcb734d930fdf839431606844a825b23eaf9a6ab371edac8162c/psutil-7.0.0-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b1388a4f6875d7e2aff5c4ca1cc16c545ed41dd8bb596cefea80111db353a34", size = 277986, upload-time = "2025-02-13T21:54:21.811Z" }, + { url = "https://files.pythonhosted.org/packages/eb/a2/709e0fe2f093556c17fbafda93ac032257242cabcc7ff3369e2cb76a97aa/psutil-7.0.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5f098451abc2828f7dc6b58d44b532b22f2088f4999a937557b603ce72b1993", size = 279544, upload-time = "2025-02-13T21:54:24.68Z" }, + { url = "https://files.pythonhosted.org/packages/50/e6/eecf58810b9d12e6427369784efe814a1eec0f492084ce8eb8f4d89d6d61/psutil-7.0.0-cp37-abi3-win32.whl", hash = "sha256:ba3fcef7523064a6c9da440fc4d6bd07da93ac726b5733c29027d7dc95b39d99", size = 241053, upload-time = "2025-02-13T21:54:34.31Z" }, + { url = "https://files.pythonhosted.org/packages/50/1b/6921afe68c74868b4c9fa424dad3be35b095e16687989ebbb50ce4fceb7c/psutil-7.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:4cf3d4eb1aa9b348dec30105c55cd9b7d4629285735a102beb4441e38db90553", size = 244885, upload-time = "2025-02-13T21:54:37.486Z" }, ] [[package]] name = "pyaes" version = "1.6.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/44/66/2c17bae31c906613795711fc78045c285048168919ace2220daa372c7d72/pyaes-1.6.1.tar.gz", hash = "sha256:02c1b1405c38d3c370b085fb952dd8bea3fadcee6411ad99f312cc129c536d8f", size = 28536, upload_time = "2017-09-20T21:17:54.23Z" } +sdist = { url = "https://files.pythonhosted.org/packages/44/66/2c17bae31c906613795711fc78045c285048168919ace2220daa372c7d72/pyaes-1.6.1.tar.gz", hash = "sha256:02c1b1405c38d3c370b085fb952dd8bea3fadcee6411ad99f312cc129c536d8f", size = 28536, upload-time = "2017-09-20T21:17:54.23Z" } [[package]] name = "pyasn1" version = "0.6.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ba/e9/01f1a64245b89f039897cb0130016d79f77d52669aae6ee7b159a6c4c018/pyasn1-0.6.1.tar.gz", hash = "sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034", size = 145322, upload_time = "2024-09-10T22:41:42.55Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/e9/01f1a64245b89f039897cb0130016d79f77d52669aae6ee7b159a6c4c018/pyasn1-0.6.1.tar.gz", hash = "sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034", size = 145322, upload-time = "2024-09-10T22:41:42.55Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c8/f1/d6a797abb14f6283c0ddff96bbdd46937f64122b8c925cab503dd37f8214/pyasn1-0.6.1-py3-none-any.whl", hash = "sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629", size = 83135, upload_time = "2024-09-11T16:00:36.122Z" }, + { url = "https://files.pythonhosted.org/packages/c8/f1/d6a797abb14f6283c0ddff96bbdd46937f64122b8c925cab503dd37f8214/pyasn1-0.6.1-py3-none-any.whl", hash = "sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629", size = 83135, upload-time = "2024-09-11T16:00:36.122Z" }, ] [[package]] @@ -449,43 +449,43 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e0/2f/5b46bb8e65070eb1f7f549d2f2e71db6b9899ef24ac9f82128014aeb1e25/pycares-4.10.0.tar.gz", hash = "sha256:9df70dce6e05afa5d477f48959170e569485e20dad1a089c4cf3b2d7ffbd8bf9", size = 654318, upload_time = "2025-08-05T22:35:34.863Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/2f/5b46bb8e65070eb1f7f549d2f2e71db6b9899ef24ac9f82128014aeb1e25/pycares-4.10.0.tar.gz", hash = "sha256:9df70dce6e05afa5d477f48959170e569485e20dad1a089c4cf3b2d7ffbd8bf9", size = 654318, upload-time = "2025-08-05T22:35:34.863Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6a/ac/ff843ee3e4e6c39e8582772bc75c0898e00c9859906b094569a09f64a1c8/pycares-4.10.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:870354741adb5d212a521c33005b368b5c8baa81e2f0d3143e868c025c5bb32f", size = 145860, upload_time = "2025-08-05T22:34:41.587Z" }, - { url = "https://files.pythonhosted.org/packages/ee/db/9cb8a2d3bdd138a62334320bf940ea321ae15bce8deac9078fcb2bb17dba/pycares-4.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8912544250edc3da6a1fc97ef9543f69ee4bc2812f90e17d294397382d1ecc80", size = 140889, upload_time = "2025-08-05T22:34:42.738Z" }, - { url = "https://files.pythonhosted.org/packages/d3/51/c51e4bb52020388222ddf04d73ae32aba4fa1e17fe4decd7a842d85e5dfb/pycares-4.10.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:49d896bb5ae3c571bc359d3076c1484fd4f99bb5138c1c597da1f57979238771", size = 637789, upload_time = "2025-08-05T22:34:43.876Z" }, - { url = "https://files.pythonhosted.org/packages/75/72/778c4210d7918f1955a0388cd2c2f9131620e8a8939094aad0e24ff95583/pycares-4.10.0-cp312-cp312-manylinux_2_28_ppc64le.whl", hash = "sha256:08e3d70c714e3955dc5ccfe6abc132d2f410ca1c610375faee42fda6cc90ca0f", size = 687708, upload_time = "2025-08-05T22:34:45.365Z" }, - { url = "https://files.pythonhosted.org/packages/2e/35/10913ee20bb03210c3ac841448d143b83185bbbc3b611b33ea80126c9d26/pycares-4.10.0-cp312-cp312-manylinux_2_28_s390x.whl", hash = "sha256:f4f76946b1d6eae7bdbfefef0f143efb8acf5b55e37d631f7ec947fc9a8d6b06", size = 678317, upload_time = "2025-08-05T22:34:46.895Z" }, - { url = "https://files.pythonhosted.org/packages/0c/b4/6523d53a0644bb270c58f7186da0d80ce3f6fab481b27a9868f572044c03/pycares-4.10.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:cf99fbdb5f566320d5c1330e55de4f3cbe49ca42690b782db6380523bcfbb34b", size = 641024, upload_time = "2025-08-05T22:34:48.727Z" }, - { url = "https://files.pythonhosted.org/packages/77/dc/00b6c06343e74eb637eef4d0f774998c1bafaf2df42cc448624f04c31530/pycares-4.10.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ba103bbc7f85d0b7c386021cafed122317d05bee56c75c06c22707d8a0393a3d", size = 622312, upload_time = "2025-08-05T22:34:50.396Z" }, - { url = "https://files.pythonhosted.org/packages/2c/0f/0134aaa668f50d8cc3f322c9b2774773360647ceb081d1c3597546f9e002/pycares-4.10.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:6f0e546194fa64e751e70e16239f54fbf34ba216f4d3c7b55ca8ac50a5d82eb5", size = 670245, upload_time = "2025-08-05T22:34:52.239Z" }, - { url = "https://files.pythonhosted.org/packages/15/01/892aa72b16baababb7a54255344793c7943d439566fd6f554dc00fb6ee3a/pycares-4.10.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:5c32115f7004c1b9071c0f250c9092bacd9090bd31a289bd155d58a60d4434fa", size = 652913, upload_time = "2025-08-05T22:34:53.433Z" }, - { url = "https://files.pythonhosted.org/packages/01/cc/e0319118001fab9c00830a62ce7b1d6595825b43c12ac282860b2b48b7ba/pycares-4.10.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:259c9b6b4547e1400515a373c6910506f3cebe6e65bb9814be10e59c49dcb634", size = 629195, upload_time = "2025-08-05T22:34:54.521Z" }, - { url = "https://files.pythonhosted.org/packages/60/2c/5638e18ca83d9e42f005cf9dcebad9b24756aa55a62cdd63e860a366cf4e/pycares-4.10.0-cp312-cp312-win32.whl", hash = "sha256:f972732b3ce1300e6eec8670967920cae56b44df014fd63a793b990d930da64f", size = 118867, upload_time = "2025-08-05T22:34:55.563Z" }, - { url = "https://files.pythonhosted.org/packages/ca/31/5284d053c3c0b956c7f3b9f846dca108eaca97b1e1f0f8b7601c7e4fd238/pycares-4.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:489584abc1523d7e444b2b27a563d1c3c0c0852b40f3b453fa3a74cf10b38ebb", size = 144512, upload_time = "2025-08-05T22:34:57.375Z" }, - { url = "https://files.pythonhosted.org/packages/35/5f/bb1594ae9a8640bec69e953944145f86a621084c39debbd1904f3369a85b/pycares-4.10.0-cp312-cp312-win_arm64.whl", hash = "sha256:468aa3bb19e7f6f193ae5375d1b21722a0cad5726e17c9817bfefbcf29cd662e", size = 115647, upload_time = "2025-08-05T22:34:58.647Z" }, - { url = "https://files.pythonhosted.org/packages/21/bd/7a1448f5f0852628520dc9cdff21b4d6f01f4ab5faaf208d030fba28e0e2/pycares-4.10.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d4904ebd5e4d0c78e9fd56e6c974da005eaa721365961764922929e8e8f7dd0a", size = 145861, upload_time = "2025-08-05T22:35:00.01Z" }, - { url = "https://files.pythonhosted.org/packages/4d/6d/0e436ddb540a06fa898b8b6cd135babe44893d31d439935eee42bcd4f07b/pycares-4.10.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7144676e54b0686605333ec62ffdb7bb2b6cb4a6c53eed3e35ae3249dc64676b", size = 140893, upload_time = "2025-08-05T22:35:01.128Z" }, - { url = "https://files.pythonhosted.org/packages/22/7a/ec4734c1274205d0ac1419310464bfa5e1a96924a77312e760790c02769c/pycares-4.10.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:f9a259bf46cc51c51c7402a2bf32d1416f029b9a4af3de8b8973345520278092", size = 637754, upload_time = "2025-08-05T22:35:02.258Z" }, - { url = "https://files.pythonhosted.org/packages/12/1d/306d071837073eccff6efb93560fdb4e53d53ca0c1002260bb34e074f706/pycares-4.10.0-cp313-cp313-manylinux_2_28_ppc64le.whl", hash = "sha256:1dcfdda868ad2cee8d171288a4cd725a9ad67498a2f679428874a917396d464e", size = 687690, upload_time = "2025-08-05T22:35:03.623Z" }, - { url = "https://files.pythonhosted.org/packages/5e/e9/2b517302d42a9ff101201b58e9e2cbd2458c0a1ed68cca7d4dc1397ed246/pycares-4.10.0-cp313-cp313-manylinux_2_28_s390x.whl", hash = "sha256:f2d57bb27c884d130ac62d8c0ac57a158d27f8d75011f8700c7d44601f093652", size = 678273, upload_time = "2025-08-05T22:35:04.794Z" }, - { url = "https://files.pythonhosted.org/packages/2b/bd/de9ed896e752fb22141d6310f6680bcb62ea1d6aa07dc129d914377bd4b4/pycares-4.10.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:95f4d976bf2feb3f406aef6b1314845dc1384d2e4ea0c439c7d50631f2b6d166", size = 640968, upload_time = "2025-08-05T22:35:05.928Z" }, - { url = "https://files.pythonhosted.org/packages/07/9f/be45f60277a0825d03feed2378a283ce514b4feea64785e917b926b8441e/pycares-4.10.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f9eecd9e28e43254c6fb1c69518bd6b753bf18230579c23e7f272ac52036d41f", size = 622316, upload_time = "2025-08-05T22:35:07.058Z" }, - { url = "https://files.pythonhosted.org/packages/91/21/ca7bd328d07c560a1fe0ba29008c24a48e88184d3ade658946aeaef25992/pycares-4.10.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f4f8ec43ce0db38152cded6939a3fa4d8aba888e323803cda99f67fa3053fa15", size = 670246, upload_time = "2025-08-05T22:35:08.213Z" }, - { url = "https://files.pythonhosted.org/packages/01/56/47fda9dbc23c3acfe42fa6d57bb850db6ede65a2a9476641a54621166464/pycares-4.10.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:ef107d30a9d667c295db58897390c2d32c206eb1802b14d98ac643990be4e04f", size = 652930, upload_time = "2025-08-05T22:35:09.701Z" }, - { url = "https://files.pythonhosted.org/packages/86/30/cc865c630d5c9f72f488a89463aabfd33895984955c489f66b5a524f9573/pycares-4.10.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:56c843e69aad724dc5a795f32ebd6fec1d1592f58cabf89d2d148697c22c41be", size = 629187, upload_time = "2025-08-05T22:35:10.954Z" }, - { url = "https://files.pythonhosted.org/packages/92/88/3ff7be2a4bf5a400309d3ffaf9aa58596f7dc6f6fcb99f844fc5e4994a49/pycares-4.10.0-cp313-cp313-win32.whl", hash = "sha256:4310259be37b586ba8cd0b4983689e4c18e15e03709bd88b1076494e91ff424b", size = 118869, upload_time = "2025-08-05T22:35:12.375Z" }, - { url = "https://files.pythonhosted.org/packages/58/5f/cac05cee0556388cabd0abc332021ed01391d6be0685be7b5daff45088f6/pycares-4.10.0-cp313-cp313-win_amd64.whl", hash = "sha256:893020d802afb54d929afda5289fe322b50110cd5386080178479a7381241f97", size = 144512, upload_time = "2025-08-05T22:35:13.549Z" }, - { url = "https://files.pythonhosted.org/packages/45/2e/89b6e83a716935752d62a3c0622a077a9d28f7c2645b7f9b90d6951b37ba/pycares-4.10.0-cp313-cp313-win_arm64.whl", hash = "sha256:ffa3e0f7a13f287b575e64413f2f9af6cf9096e383d1fd40f2870591628d843b", size = 115648, upload_time = "2025-08-05T22:35:15.891Z" }, + { url = "https://files.pythonhosted.org/packages/6a/ac/ff843ee3e4e6c39e8582772bc75c0898e00c9859906b094569a09f64a1c8/pycares-4.10.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:870354741adb5d212a521c33005b368b5c8baa81e2f0d3143e868c025c5bb32f", size = 145860, upload-time = "2025-08-05T22:34:41.587Z" }, + { url = "https://files.pythonhosted.org/packages/ee/db/9cb8a2d3bdd138a62334320bf940ea321ae15bce8deac9078fcb2bb17dba/pycares-4.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8912544250edc3da6a1fc97ef9543f69ee4bc2812f90e17d294397382d1ecc80", size = 140889, upload-time = "2025-08-05T22:34:42.738Z" }, + { url = "https://files.pythonhosted.org/packages/d3/51/c51e4bb52020388222ddf04d73ae32aba4fa1e17fe4decd7a842d85e5dfb/pycares-4.10.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:49d896bb5ae3c571bc359d3076c1484fd4f99bb5138c1c597da1f57979238771", size = 637789, upload-time = "2025-08-05T22:34:43.876Z" }, + { url = "https://files.pythonhosted.org/packages/75/72/778c4210d7918f1955a0388cd2c2f9131620e8a8939094aad0e24ff95583/pycares-4.10.0-cp312-cp312-manylinux_2_28_ppc64le.whl", hash = "sha256:08e3d70c714e3955dc5ccfe6abc132d2f410ca1c610375faee42fda6cc90ca0f", size = 687708, upload-time = "2025-08-05T22:34:45.365Z" }, + { url = "https://files.pythonhosted.org/packages/2e/35/10913ee20bb03210c3ac841448d143b83185bbbc3b611b33ea80126c9d26/pycares-4.10.0-cp312-cp312-manylinux_2_28_s390x.whl", hash = "sha256:f4f76946b1d6eae7bdbfefef0f143efb8acf5b55e37d631f7ec947fc9a8d6b06", size = 678317, upload-time = "2025-08-05T22:34:46.895Z" }, + { url = "https://files.pythonhosted.org/packages/0c/b4/6523d53a0644bb270c58f7186da0d80ce3f6fab481b27a9868f572044c03/pycares-4.10.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:cf99fbdb5f566320d5c1330e55de4f3cbe49ca42690b782db6380523bcfbb34b", size = 641024, upload-time = "2025-08-05T22:34:48.727Z" }, + { url = "https://files.pythonhosted.org/packages/77/dc/00b6c06343e74eb637eef4d0f774998c1bafaf2df42cc448624f04c31530/pycares-4.10.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ba103bbc7f85d0b7c386021cafed122317d05bee56c75c06c22707d8a0393a3d", size = 622312, upload-time = "2025-08-05T22:34:50.396Z" }, + { url = "https://files.pythonhosted.org/packages/2c/0f/0134aaa668f50d8cc3f322c9b2774773360647ceb081d1c3597546f9e002/pycares-4.10.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:6f0e546194fa64e751e70e16239f54fbf34ba216f4d3c7b55ca8ac50a5d82eb5", size = 670245, upload-time = "2025-08-05T22:34:52.239Z" }, + { url = "https://files.pythonhosted.org/packages/15/01/892aa72b16baababb7a54255344793c7943d439566fd6f554dc00fb6ee3a/pycares-4.10.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:5c32115f7004c1b9071c0f250c9092bacd9090bd31a289bd155d58a60d4434fa", size = 652913, upload-time = "2025-08-05T22:34:53.433Z" }, + { url = "https://files.pythonhosted.org/packages/01/cc/e0319118001fab9c00830a62ce7b1d6595825b43c12ac282860b2b48b7ba/pycares-4.10.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:259c9b6b4547e1400515a373c6910506f3cebe6e65bb9814be10e59c49dcb634", size = 629195, upload-time = "2025-08-05T22:34:54.521Z" }, + { url = "https://files.pythonhosted.org/packages/60/2c/5638e18ca83d9e42f005cf9dcebad9b24756aa55a62cdd63e860a366cf4e/pycares-4.10.0-cp312-cp312-win32.whl", hash = "sha256:f972732b3ce1300e6eec8670967920cae56b44df014fd63a793b990d930da64f", size = 118867, upload-time = "2025-08-05T22:34:55.563Z" }, + { url = "https://files.pythonhosted.org/packages/ca/31/5284d053c3c0b956c7f3b9f846dca108eaca97b1e1f0f8b7601c7e4fd238/pycares-4.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:489584abc1523d7e444b2b27a563d1c3c0c0852b40f3b453fa3a74cf10b38ebb", size = 144512, upload-time = "2025-08-05T22:34:57.375Z" }, + { url = "https://files.pythonhosted.org/packages/35/5f/bb1594ae9a8640bec69e953944145f86a621084c39debbd1904f3369a85b/pycares-4.10.0-cp312-cp312-win_arm64.whl", hash = "sha256:468aa3bb19e7f6f193ae5375d1b21722a0cad5726e17c9817bfefbcf29cd662e", size = 115647, upload-time = "2025-08-05T22:34:58.647Z" }, + { url = "https://files.pythonhosted.org/packages/21/bd/7a1448f5f0852628520dc9cdff21b4d6f01f4ab5faaf208d030fba28e0e2/pycares-4.10.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d4904ebd5e4d0c78e9fd56e6c974da005eaa721365961764922929e8e8f7dd0a", size = 145861, upload-time = "2025-08-05T22:35:00.01Z" }, + { url = "https://files.pythonhosted.org/packages/4d/6d/0e436ddb540a06fa898b8b6cd135babe44893d31d439935eee42bcd4f07b/pycares-4.10.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7144676e54b0686605333ec62ffdb7bb2b6cb4a6c53eed3e35ae3249dc64676b", size = 140893, upload-time = "2025-08-05T22:35:01.128Z" }, + { url = "https://files.pythonhosted.org/packages/22/7a/ec4734c1274205d0ac1419310464bfa5e1a96924a77312e760790c02769c/pycares-4.10.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:f9a259bf46cc51c51c7402a2bf32d1416f029b9a4af3de8b8973345520278092", size = 637754, upload-time = "2025-08-05T22:35:02.258Z" }, + { url = "https://files.pythonhosted.org/packages/12/1d/306d071837073eccff6efb93560fdb4e53d53ca0c1002260bb34e074f706/pycares-4.10.0-cp313-cp313-manylinux_2_28_ppc64le.whl", hash = "sha256:1dcfdda868ad2cee8d171288a4cd725a9ad67498a2f679428874a917396d464e", size = 687690, upload-time = "2025-08-05T22:35:03.623Z" }, + { url = "https://files.pythonhosted.org/packages/5e/e9/2b517302d42a9ff101201b58e9e2cbd2458c0a1ed68cca7d4dc1397ed246/pycares-4.10.0-cp313-cp313-manylinux_2_28_s390x.whl", hash = "sha256:f2d57bb27c884d130ac62d8c0ac57a158d27f8d75011f8700c7d44601f093652", size = 678273, upload-time = "2025-08-05T22:35:04.794Z" }, + { url = "https://files.pythonhosted.org/packages/2b/bd/de9ed896e752fb22141d6310f6680bcb62ea1d6aa07dc129d914377bd4b4/pycares-4.10.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:95f4d976bf2feb3f406aef6b1314845dc1384d2e4ea0c439c7d50631f2b6d166", size = 640968, upload-time = "2025-08-05T22:35:05.928Z" }, + { url = "https://files.pythonhosted.org/packages/07/9f/be45f60277a0825d03feed2378a283ce514b4feea64785e917b926b8441e/pycares-4.10.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f9eecd9e28e43254c6fb1c69518bd6b753bf18230579c23e7f272ac52036d41f", size = 622316, upload-time = "2025-08-05T22:35:07.058Z" }, + { url = "https://files.pythonhosted.org/packages/91/21/ca7bd328d07c560a1fe0ba29008c24a48e88184d3ade658946aeaef25992/pycares-4.10.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f4f8ec43ce0db38152cded6939a3fa4d8aba888e323803cda99f67fa3053fa15", size = 670246, upload-time = "2025-08-05T22:35:08.213Z" }, + { url = "https://files.pythonhosted.org/packages/01/56/47fda9dbc23c3acfe42fa6d57bb850db6ede65a2a9476641a54621166464/pycares-4.10.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:ef107d30a9d667c295db58897390c2d32c206eb1802b14d98ac643990be4e04f", size = 652930, upload-time = "2025-08-05T22:35:09.701Z" }, + { url = "https://files.pythonhosted.org/packages/86/30/cc865c630d5c9f72f488a89463aabfd33895984955c489f66b5a524f9573/pycares-4.10.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:56c843e69aad724dc5a795f32ebd6fec1d1592f58cabf89d2d148697c22c41be", size = 629187, upload-time = "2025-08-05T22:35:10.954Z" }, + { url = "https://files.pythonhosted.org/packages/92/88/3ff7be2a4bf5a400309d3ffaf9aa58596f7dc6f6fcb99f844fc5e4994a49/pycares-4.10.0-cp313-cp313-win32.whl", hash = "sha256:4310259be37b586ba8cd0b4983689e4c18e15e03709bd88b1076494e91ff424b", size = 118869, upload-time = "2025-08-05T22:35:12.375Z" }, + { url = "https://files.pythonhosted.org/packages/58/5f/cac05cee0556388cabd0abc332021ed01391d6be0685be7b5daff45088f6/pycares-4.10.0-cp313-cp313-win_amd64.whl", hash = "sha256:893020d802afb54d929afda5289fe322b50110cd5386080178479a7381241f97", size = 144512, upload-time = "2025-08-05T22:35:13.549Z" }, + { url = "https://files.pythonhosted.org/packages/45/2e/89b6e83a716935752d62a3c0622a077a9d28f7c2645b7f9b90d6951b37ba/pycares-4.10.0-cp313-cp313-win_arm64.whl", hash = "sha256:ffa3e0f7a13f287b575e64413f2f9af6cf9096e383d1fd40f2870591628d843b", size = 115648, upload-time = "2025-08-05T22:35:15.891Z" }, ] [[package]] name = "pycparser" version = "2.22" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", size = 172736, upload_time = "2024-03-30T13:22:22.564Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", size = 172736, upload-time = "2024-03-30T13:22:22.564Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552, upload_time = "2024-03-30T13:22:20.476Z" }, + { url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552, upload-time = "2024-03-30T13:22:20.476Z" }, ] [[package]] @@ -498,9 +498,9 @@ dependencies = [ { name = "idna" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e1/0a/929373653770d8a0d7ea76c37de6e41f11eb07559b103b1c02cafb3f7cf8/requests-2.32.4.tar.gz", hash = "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422", size = 135258, upload_time = "2025-06-09T16:43:07.34Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e1/0a/929373653770d8a0d7ea76c37de6e41f11eb07559b103b1c02cafb3f7cf8/requests-2.32.4.tar.gz", hash = "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422", size = 135258, upload-time = "2025-06-09T16:43:07.34Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7c/e4/56027c4a6b4ae70ca9de302488c5ca95ad4a39e190093d6c1a8ace08341b/requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c", size = 64847, upload_time = "2025-06-09T16:43:05.728Z" }, + { url = "https://files.pythonhosted.org/packages/7c/e4/56027c4a6b4ae70ca9de302488c5ca95ad4a39e190093d6c1a8ace08341b/requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c", size = 64847, upload-time = "2025-06-09T16:43:05.728Z" }, ] [[package]] @@ -510,18 +510,18 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyasn1" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/da/8a/22b7beea3ee0d44b1916c0c1cb0ee3af23b700b6da9f04991899d0c555d4/rsa-4.9.1.tar.gz", hash = "sha256:e7bdbfdb5497da4c07dfd35530e1a902659db6ff241e39d9953cad06ebd0ae75", size = 29034, upload_time = "2025-04-16T09:51:18.218Z" } +sdist = { url = "https://files.pythonhosted.org/packages/da/8a/22b7beea3ee0d44b1916c0c1cb0ee3af23b700b6da9f04991899d0c555d4/rsa-4.9.1.tar.gz", hash = "sha256:e7bdbfdb5497da4c07dfd35530e1a902659db6ff241e39d9953cad06ebd0ae75", size = 29034, upload-time = "2025-04-16T09:51:18.218Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/64/8d/0133e4eb4beed9e425d9a98ed6e081a55d195481b7632472be1af08d2f6b/rsa-4.9.1-py3-none-any.whl", hash = "sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762", size = 34696, upload_time = "2025-04-16T09:51:17.142Z" }, + { url = "https://files.pythonhosted.org/packages/64/8d/0133e4eb4beed9e425d9a98ed6e081a55d195481b7632472be1af08d2f6b/rsa-4.9.1-py3-none-any.whl", hash = "sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762", size = 34696, upload-time = "2025-04-16T09:51:17.142Z" }, ] [[package]] name = "six" version = "1.17.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload_time = "2024-12-04T17:35:28.174Z" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload_time = "2024-12-04T17:35:26.475Z" }, + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, ] [[package]] @@ -532,18 +532,19 @@ dependencies = [ { name = "pyaes" }, { name = "rsa" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/58/af/9b7111e3f63fffe8e55b7ceb8bda023173e2052f420b6debcb25fd2fbc15/telethon-1.40.0.tar.gz", hash = "sha256:40e83326877a2e68b754d4b6d0d1ca5ac924110045b039e02660f2d67add97db", size = 646723, upload_time = "2025-04-21T09:12:10.506Z" } +sdist = { url = "https://files.pythonhosted.org/packages/58/af/9b7111e3f63fffe8e55b7ceb8bda023173e2052f420b6debcb25fd2fbc15/telethon-1.40.0.tar.gz", hash = "sha256:40e83326877a2e68b754d4b6d0d1ca5ac924110045b039e02660f2d67add97db", size = 646723, upload-time = "2025-04-21T09:12:10.506Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2c/5a/c5370edb3215d19a6e858f4169b8eec725ba55f9d39df0f557508048c037/Telethon-1.40.0-py3-none-any.whl", hash = "sha256:146fd4cb2a7afa66bc67f9c2167756096a37b930f65711a3e7399ec9874dcfa7", size = 722013, upload_time = "2025-04-21T09:12:08.399Z" }, + { url = "https://files.pythonhosted.org/packages/2c/5a/c5370edb3215d19a6e858f4169b8eec725ba55f9d39df0f557508048c037/Telethon-1.40.0-py3-none-any.whl", hash = "sha256:146fd4cb2a7afa66bc67f9c2167756096a37b930f65711a3e7399ec9874dcfa7", size = 722013, upload-time = "2025-04-21T09:12:08.399Z" }, + { url = "https://files.pythonhosted.org/packages/ce/b0/78f74085b6c88c2bf2bec39c67267cd9ba6af24ceaea9654fb0c272a53da/telethon-1.40.0-py3-none-any.whl", hash = "sha256:1aebaca04fd8410968816645bdbcc0baeff55429b6d6bec37e647417bb8e8a2c", size = 744897, upload-time = "2025-09-01T15:32:34.212Z" }, ] [[package]] name = "typing-extensions" version = "4.14.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/98/5a/da40306b885cc8c09109dc2e1abd358d5684b1425678151cdaed4731c822/typing_extensions-4.14.1.tar.gz", hash = "sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36", size = 107673, upload_time = "2025-07-04T13:28:34.16Z" } +sdist = { url = "https://files.pythonhosted.org/packages/98/5a/da40306b885cc8c09109dc2e1abd358d5684b1425678151cdaed4731c822/typing_extensions-4.14.1.tar.gz", hash = "sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36", size = 107673, upload-time = "2025-07-04T13:28:34.16Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b5/00/d631e67a838026495268c2f6884f3711a15a9a2a96cd244fdaea53b823fb/typing_extensions-4.14.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76", size = 43906, upload_time = "2025-07-04T13:28:32.743Z" }, + { url = "https://files.pythonhosted.org/packages/b5/00/d631e67a838026495268c2f6884f3711a15a9a2a96cd244fdaea53b823fb/typing_extensions-4.14.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76", size = 43906, upload-time = "2025-07-04T13:28:32.743Z" }, ] [[package]] @@ -555,9 +556,9 @@ dependencies = [ { name = "six" }, { name = "uritemplate" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/72/e1/2e7d405f1bdef3dc52af7000d7299370fbd12eb2f9e2e30efde24bb9c945/uplink-0.10.0.tar.gz", hash = "sha256:a3b76b1cac5394126a72698d72b209bb80c8a94bad091870e463919979e4ab63", size = 210185, upload_time = "2025-06-14T20:24:08.708Z" } +sdist = { url = "https://files.pythonhosted.org/packages/72/e1/2e7d405f1bdef3dc52af7000d7299370fbd12eb2f9e2e30efde24bb9c945/uplink-0.10.0.tar.gz", hash = "sha256:a3b76b1cac5394126a72698d72b209bb80c8a94bad091870e463919979e4ab63", size = 210185, upload-time = "2025-06-14T20:24:08.708Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/69/5b/e22e345aff4ffcaa6d68d038f512ec1167ab7d2ec4d2b6ec8a0da7800fbd/uplink-0.10.0-py3-none-any.whl", hash = "sha256:03212163f8a83a608480ec15122884988eb82cc7a2368b9072d9af8ede2246d9", size = 71425, upload_time = "2025-06-14T20:24:07.779Z" }, + { url = "https://files.pythonhosted.org/packages/69/5b/e22e345aff4ffcaa6d68d038f512ec1167ab7d2ec4d2b6ec8a0da7800fbd/uplink-0.10.0-py3-none-any.whl", hash = "sha256:03212163f8a83a608480ec15122884988eb82cc7a2368b9072d9af8ede2246d9", size = 71425, upload-time = "2025-06-14T20:24:07.779Z" }, ] [package.optional-dependencies] @@ -569,18 +570,18 @@ aiohttp = [ name = "uritemplate" version = "4.2.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/98/60/f174043244c5306c9988380d2cb10009f91563fc4b31293d27e17201af56/uritemplate-4.2.0.tar.gz", hash = "sha256:480c2ed180878955863323eea31b0ede668795de182617fef9c6ca09e6ec9d0e", size = 33267, upload_time = "2025-06-02T15:12:06.318Z" } +sdist = { url = "https://files.pythonhosted.org/packages/98/60/f174043244c5306c9988380d2cb10009f91563fc4b31293d27e17201af56/uritemplate-4.2.0.tar.gz", hash = "sha256:480c2ed180878955863323eea31b0ede668795de182617fef9c6ca09e6ec9d0e", size = 33267, upload-time = "2025-06-02T15:12:06.318Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a9/99/3ae339466c9183ea5b8ae87b34c0b897eda475d2aec2307cae60e5cd4f29/uritemplate-4.2.0-py3-none-any.whl", hash = "sha256:962201ba1c4edcab02e60f9a0d3821e82dfc5d2d6662a21abd533879bdb8a686", size = 11488, upload_time = "2025-06-02T15:12:03.405Z" }, + { url = "https://files.pythonhosted.org/packages/a9/99/3ae339466c9183ea5b8ae87b34c0b897eda475d2aec2307cae60e5cd4f29/uritemplate-4.2.0-py3-none-any.whl", hash = "sha256:962201ba1c4edcab02e60f9a0d3821e82dfc5d2d6662a21abd533879bdb8a686", size = 11488, upload-time = "2025-06-02T15:12:03.405Z" }, ] [[package]] name = "urllib3" version = "2.5.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185, upload_time = "2025-06-18T14:07:41.644Z" } +sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185, upload-time = "2025-06-18T14:07:41.644Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload_time = "2025-06-18T14:07:40.39Z" }, + { url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" }, ] [[package]] @@ -592,58 +593,58 @@ dependencies = [ { name = "multidict" }, { name = "propcache" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/3c/fb/efaa23fa4e45537b827620f04cf8f3cd658b76642205162e072703a5b963/yarl-1.20.1.tar.gz", hash = "sha256:d017a4997ee50c91fd5466cef416231bb82177b93b029906cefc542ce14c35ac", size = 186428, upload_time = "2025-06-10T00:46:09.923Z" } +sdist = { url = "https://files.pythonhosted.org/packages/3c/fb/efaa23fa4e45537b827620f04cf8f3cd658b76642205162e072703a5b963/yarl-1.20.1.tar.gz", hash = "sha256:d017a4997ee50c91fd5466cef416231bb82177b93b029906cefc542ce14c35ac", size = 186428, upload-time = "2025-06-10T00:46:09.923Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5f/9a/cb7fad7d73c69f296eda6815e4a2c7ed53fc70c2f136479a91c8e5fbdb6d/yarl-1.20.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdcc4cd244e58593a4379fe60fdee5ac0331f8eb70320a24d591a3be197b94a9", size = 133667, upload_time = "2025-06-10T00:43:44.369Z" }, - { url = "https://files.pythonhosted.org/packages/67/38/688577a1cb1e656e3971fb66a3492501c5a5df56d99722e57c98249e5b8a/yarl-1.20.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b29a2c385a5f5b9c7d9347e5812b6f7ab267193c62d282a540b4fc528c8a9d2a", size = 91025, upload_time = "2025-06-10T00:43:46.295Z" }, - { url = "https://files.pythonhosted.org/packages/50/ec/72991ae51febeb11a42813fc259f0d4c8e0507f2b74b5514618d8b640365/yarl-1.20.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1112ae8154186dfe2de4732197f59c05a83dc814849a5ced892b708033f40dc2", size = 89709, upload_time = "2025-06-10T00:43:48.22Z" }, - { url = "https://files.pythonhosted.org/packages/99/da/4d798025490e89426e9f976702e5f9482005c548c579bdae792a4c37769e/yarl-1.20.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90bbd29c4fe234233f7fa2b9b121fb63c321830e5d05b45153a2ca68f7d310ee", size = 352287, upload_time = "2025-06-10T00:43:49.924Z" }, - { url = "https://files.pythonhosted.org/packages/1a/26/54a15c6a567aac1c61b18aa0f4b8aa2e285a52d547d1be8bf48abe2b3991/yarl-1.20.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:680e19c7ce3710ac4cd964e90dad99bf9b5029372ba0c7cbfcd55e54d90ea819", size = 345429, upload_time = "2025-06-10T00:43:51.7Z" }, - { url = "https://files.pythonhosted.org/packages/d6/95/9dcf2386cb875b234353b93ec43e40219e14900e046bf6ac118f94b1e353/yarl-1.20.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4a979218c1fdb4246a05efc2cc23859d47c89af463a90b99b7c56094daf25a16", size = 365429, upload_time = "2025-06-10T00:43:53.494Z" }, - { url = "https://files.pythonhosted.org/packages/91/b2/33a8750f6a4bc224242a635f5f2cff6d6ad5ba651f6edcccf721992c21a0/yarl-1.20.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:255b468adf57b4a7b65d8aad5b5138dce6a0752c139965711bdcb81bc370e1b6", size = 363862, upload_time = "2025-06-10T00:43:55.766Z" }, - { url = "https://files.pythonhosted.org/packages/98/28/3ab7acc5b51f4434b181b0cee8f1f4b77a65919700a355fb3617f9488874/yarl-1.20.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a97d67108e79cfe22e2b430d80d7571ae57d19f17cda8bb967057ca8a7bf5bfd", size = 355616, upload_time = "2025-06-10T00:43:58.056Z" }, - { url = "https://files.pythonhosted.org/packages/36/a3/f666894aa947a371724ec7cd2e5daa78ee8a777b21509b4252dd7bd15e29/yarl-1.20.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8570d998db4ddbfb9a590b185a0a33dbf8aafb831d07a5257b4ec9948df9cb0a", size = 339954, upload_time = "2025-06-10T00:43:59.773Z" }, - { url = "https://files.pythonhosted.org/packages/f1/81/5f466427e09773c04219d3450d7a1256138a010b6c9f0af2d48565e9ad13/yarl-1.20.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:97c75596019baae7c71ccf1d8cc4738bc08134060d0adfcbe5642f778d1dca38", size = 365575, upload_time = "2025-06-10T00:44:02.051Z" }, - { url = "https://files.pythonhosted.org/packages/2e/e3/e4b0ad8403e97e6c9972dd587388940a032f030ebec196ab81a3b8e94d31/yarl-1.20.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:1c48912653e63aef91ff988c5432832692ac5a1d8f0fb8a33091520b5bbe19ef", size = 365061, upload_time = "2025-06-10T00:44:04.196Z" }, - { url = "https://files.pythonhosted.org/packages/ac/99/b8a142e79eb86c926f9f06452eb13ecb1bb5713bd01dc0038faf5452e544/yarl-1.20.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4c3ae28f3ae1563c50f3d37f064ddb1511ecc1d5584e88c6b7c63cf7702a6d5f", size = 364142, upload_time = "2025-06-10T00:44:06.527Z" }, - { url = "https://files.pythonhosted.org/packages/34/f2/08ed34a4a506d82a1a3e5bab99ccd930a040f9b6449e9fd050320e45845c/yarl-1.20.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c5e9642f27036283550f5f57dc6156c51084b458570b9d0d96100c8bebb186a8", size = 381894, upload_time = "2025-06-10T00:44:08.379Z" }, - { url = "https://files.pythonhosted.org/packages/92/f8/9a3fbf0968eac704f681726eff595dce9b49c8a25cd92bf83df209668285/yarl-1.20.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:2c26b0c49220d5799f7b22c6838409ee9bc58ee5c95361a4d7831f03cc225b5a", size = 383378, upload_time = "2025-06-10T00:44:10.51Z" }, - { url = "https://files.pythonhosted.org/packages/af/85/9363f77bdfa1e4d690957cd39d192c4cacd1c58965df0470a4905253b54f/yarl-1.20.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:564ab3d517e3d01c408c67f2e5247aad4019dcf1969982aba3974b4093279004", size = 374069, upload_time = "2025-06-10T00:44:12.834Z" }, - { url = "https://files.pythonhosted.org/packages/35/99/9918c8739ba271dcd935400cff8b32e3cd319eaf02fcd023d5dcd487a7c8/yarl-1.20.1-cp312-cp312-win32.whl", hash = "sha256:daea0d313868da1cf2fac6b2d3a25c6e3a9e879483244be38c8e6a41f1d876a5", size = 81249, upload_time = "2025-06-10T00:44:14.731Z" }, - { url = "https://files.pythonhosted.org/packages/eb/83/5d9092950565481b413b31a23e75dd3418ff0a277d6e0abf3729d4d1ce25/yarl-1.20.1-cp312-cp312-win_amd64.whl", hash = "sha256:48ea7d7f9be0487339828a4de0360d7ce0efc06524a48e1810f945c45b813698", size = 86710, upload_time = "2025-06-10T00:44:16.716Z" }, - { url = "https://files.pythonhosted.org/packages/8a/e1/2411b6d7f769a07687acee88a062af5833cf1966b7266f3d8dfb3d3dc7d3/yarl-1.20.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:0b5ff0fbb7c9f1b1b5ab53330acbfc5247893069e7716840c8e7d5bb7355038a", size = 131811, upload_time = "2025-06-10T00:44:18.933Z" }, - { url = "https://files.pythonhosted.org/packages/b2/27/584394e1cb76fb771371770eccad35de400e7b434ce3142c2dd27392c968/yarl-1.20.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:14f326acd845c2b2e2eb38fb1346c94f7f3b01a4f5c788f8144f9b630bfff9a3", size = 90078, upload_time = "2025-06-10T00:44:20.635Z" }, - { url = "https://files.pythonhosted.org/packages/bf/9a/3246ae92d4049099f52d9b0fe3486e3b500e29b7ea872d0f152966fc209d/yarl-1.20.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f60e4ad5db23f0b96e49c018596707c3ae89f5d0bd97f0ad3684bcbad899f1e7", size = 88748, upload_time = "2025-06-10T00:44:22.34Z" }, - { url = "https://files.pythonhosted.org/packages/a3/25/35afe384e31115a1a801fbcf84012d7a066d89035befae7c5d4284df1e03/yarl-1.20.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:49bdd1b8e00ce57e68ba51916e4bb04461746e794e7c4d4bbc42ba2f18297691", size = 349595, upload_time = "2025-06-10T00:44:24.314Z" }, - { url = "https://files.pythonhosted.org/packages/28/2d/8aca6cb2cabc8f12efcb82749b9cefecbccfc7b0384e56cd71058ccee433/yarl-1.20.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:66252d780b45189975abfed839616e8fd2dbacbdc262105ad7742c6ae58f3e31", size = 342616, upload_time = "2025-06-10T00:44:26.167Z" }, - { url = "https://files.pythonhosted.org/packages/0b/e9/1312633d16b31acf0098d30440ca855e3492d66623dafb8e25b03d00c3da/yarl-1.20.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:59174e7332f5d153d8f7452a102b103e2e74035ad085f404df2e40e663a22b28", size = 361324, upload_time = "2025-06-10T00:44:27.915Z" }, - { url = "https://files.pythonhosted.org/packages/bc/a0/688cc99463f12f7669eec7c8acc71ef56a1521b99eab7cd3abb75af887b0/yarl-1.20.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e3968ec7d92a0c0f9ac34d5ecfd03869ec0cab0697c91a45db3fbbd95fe1b653", size = 359676, upload_time = "2025-06-10T00:44:30.041Z" }, - { url = "https://files.pythonhosted.org/packages/af/44/46407d7f7a56e9a85a4c207724c9f2c545c060380718eea9088f222ba697/yarl-1.20.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d1a4fbb50e14396ba3d375f68bfe02215d8e7bc3ec49da8341fe3157f59d2ff5", size = 352614, upload_time = "2025-06-10T00:44:32.171Z" }, - { url = "https://files.pythonhosted.org/packages/b1/91/31163295e82b8d5485d31d9cf7754d973d41915cadce070491778d9c9825/yarl-1.20.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:11a62c839c3a8eac2410e951301309426f368388ff2f33799052787035793b02", size = 336766, upload_time = "2025-06-10T00:44:34.494Z" }, - { url = "https://files.pythonhosted.org/packages/b4/8e/c41a5bc482121f51c083c4c2bcd16b9e01e1cf8729e380273a952513a21f/yarl-1.20.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:041eaa14f73ff5a8986b4388ac6bb43a77f2ea09bf1913df7a35d4646db69e53", size = 364615, upload_time = "2025-06-10T00:44:36.856Z" }, - { url = "https://files.pythonhosted.org/packages/e3/5b/61a3b054238d33d70ea06ebba7e58597891b71c699e247df35cc984ab393/yarl-1.20.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:377fae2fef158e8fd9d60b4c8751387b8d1fb121d3d0b8e9b0be07d1b41e83dc", size = 360982, upload_time = "2025-06-10T00:44:39.141Z" }, - { url = "https://files.pythonhosted.org/packages/df/a3/6a72fb83f8d478cb201d14927bc8040af901811a88e0ff2da7842dd0ed19/yarl-1.20.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:1c92f4390e407513f619d49319023664643d3339bd5e5a56a3bebe01bc67ec04", size = 369792, upload_time = "2025-06-10T00:44:40.934Z" }, - { url = "https://files.pythonhosted.org/packages/7c/af/4cc3c36dfc7c077f8dedb561eb21f69e1e9f2456b91b593882b0b18c19dc/yarl-1.20.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:d25ddcf954df1754ab0f86bb696af765c5bfaba39b74095f27eececa049ef9a4", size = 382049, upload_time = "2025-06-10T00:44:42.854Z" }, - { url = "https://files.pythonhosted.org/packages/19/3a/e54e2c4752160115183a66dc9ee75a153f81f3ab2ba4bf79c3c53b33de34/yarl-1.20.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:909313577e9619dcff8c31a0ea2aa0a2a828341d92673015456b3ae492e7317b", size = 384774, upload_time = "2025-06-10T00:44:45.275Z" }, - { url = "https://files.pythonhosted.org/packages/9c/20/200ae86dabfca89060ec6447649f219b4cbd94531e425e50d57e5f5ac330/yarl-1.20.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:793fd0580cb9664548c6b83c63b43c477212c0260891ddf86809e1c06c8b08f1", size = 374252, upload_time = "2025-06-10T00:44:47.31Z" }, - { url = "https://files.pythonhosted.org/packages/83/75/11ee332f2f516b3d094e89448da73d557687f7d137d5a0f48c40ff211487/yarl-1.20.1-cp313-cp313-win32.whl", hash = "sha256:468f6e40285de5a5b3c44981ca3a319a4b208ccc07d526b20b12aeedcfa654b7", size = 81198, upload_time = "2025-06-10T00:44:49.164Z" }, - { url = "https://files.pythonhosted.org/packages/ba/ba/39b1ecbf51620b40ab402b0fc817f0ff750f6d92712b44689c2c215be89d/yarl-1.20.1-cp313-cp313-win_amd64.whl", hash = "sha256:495b4ef2fea40596bfc0affe3837411d6aa3371abcf31aac0ccc4bdd64d4ef5c", size = 86346, upload_time = "2025-06-10T00:44:51.182Z" }, - { url = "https://files.pythonhosted.org/packages/43/c7/669c52519dca4c95153c8ad96dd123c79f354a376346b198f438e56ffeb4/yarl-1.20.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:f60233b98423aab21d249a30eb27c389c14929f47be8430efa7dbd91493a729d", size = 138826, upload_time = "2025-06-10T00:44:52.883Z" }, - { url = "https://files.pythonhosted.org/packages/6a/42/fc0053719b44f6ad04a75d7f05e0e9674d45ef62f2d9ad2c1163e5c05827/yarl-1.20.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:6f3eff4cc3f03d650d8755c6eefc844edde99d641d0dcf4da3ab27141a5f8ddf", size = 93217, upload_time = "2025-06-10T00:44:54.658Z" }, - { url = "https://files.pythonhosted.org/packages/4f/7f/fa59c4c27e2a076bba0d959386e26eba77eb52ea4a0aac48e3515c186b4c/yarl-1.20.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:69ff8439d8ba832d6bed88af2c2b3445977eba9a4588b787b32945871c2444e3", size = 92700, upload_time = "2025-06-10T00:44:56.784Z" }, - { url = "https://files.pythonhosted.org/packages/2f/d4/062b2f48e7c93481e88eff97a6312dca15ea200e959f23e96d8ab898c5b8/yarl-1.20.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3cf34efa60eb81dd2645a2e13e00bb98b76c35ab5061a3989c7a70f78c85006d", size = 347644, upload_time = "2025-06-10T00:44:59.071Z" }, - { url = "https://files.pythonhosted.org/packages/89/47/78b7f40d13c8f62b499cc702fdf69e090455518ae544c00a3bf4afc9fc77/yarl-1.20.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8e0fe9364ad0fddab2688ce72cb7a8e61ea42eff3c7caeeb83874a5d479c896c", size = 323452, upload_time = "2025-06-10T00:45:01.605Z" }, - { url = "https://files.pythonhosted.org/packages/eb/2b/490d3b2dc66f52987d4ee0d3090a147ea67732ce6b4d61e362c1846d0d32/yarl-1.20.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8f64fbf81878ba914562c672024089e3401974a39767747691c65080a67b18c1", size = 346378, upload_time = "2025-06-10T00:45:03.946Z" }, - { url = "https://files.pythonhosted.org/packages/66/ad/775da9c8a94ce925d1537f939a4f17d782efef1f973039d821cbe4bcc211/yarl-1.20.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f6342d643bf9a1de97e512e45e4b9560a043347e779a173250824f8b254bd5ce", size = 353261, upload_time = "2025-06-10T00:45:05.992Z" }, - { url = "https://files.pythonhosted.org/packages/4b/23/0ed0922b47a4f5c6eb9065d5ff1e459747226ddce5c6a4c111e728c9f701/yarl-1.20.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:56dac5f452ed25eef0f6e3c6a066c6ab68971d96a9fb441791cad0efba6140d3", size = 335987, upload_time = "2025-06-10T00:45:08.227Z" }, - { url = "https://files.pythonhosted.org/packages/3e/49/bc728a7fe7d0e9336e2b78f0958a2d6b288ba89f25a1762407a222bf53c3/yarl-1.20.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7d7f497126d65e2cad8dc5f97d34c27b19199b6414a40cb36b52f41b79014be", size = 329361, upload_time = "2025-06-10T00:45:10.11Z" }, - { url = "https://files.pythonhosted.org/packages/93/8f/b811b9d1f617c83c907e7082a76e2b92b655400e61730cd61a1f67178393/yarl-1.20.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:67e708dfb8e78d8a19169818eeb5c7a80717562de9051bf2413aca8e3696bf16", size = 346460, upload_time = "2025-06-10T00:45:12.055Z" }, - { url = "https://files.pythonhosted.org/packages/70/fd/af94f04f275f95da2c3b8b5e1d49e3e79f1ed8b6ceb0f1664cbd902773ff/yarl-1.20.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:595c07bc79af2494365cc96ddeb772f76272364ef7c80fb892ef9d0649586513", size = 334486, upload_time = "2025-06-10T00:45:13.995Z" }, - { url = "https://files.pythonhosted.org/packages/84/65/04c62e82704e7dd0a9b3f61dbaa8447f8507655fd16c51da0637b39b2910/yarl-1.20.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:7bdd2f80f4a7df852ab9ab49484a4dee8030023aa536df41f2d922fd57bf023f", size = 342219, upload_time = "2025-06-10T00:45:16.479Z" }, - { url = "https://files.pythonhosted.org/packages/91/95/459ca62eb958381b342d94ab9a4b6aec1ddec1f7057c487e926f03c06d30/yarl-1.20.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:c03bfebc4ae8d862f853a9757199677ab74ec25424d0ebd68a0027e9c639a390", size = 350693, upload_time = "2025-06-10T00:45:18.399Z" }, - { url = "https://files.pythonhosted.org/packages/a6/00/d393e82dd955ad20617abc546a8f1aee40534d599ff555ea053d0ec9bf03/yarl-1.20.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:344d1103e9c1523f32a5ed704d576172d2cabed3122ea90b1d4e11fe17c66458", size = 355803, upload_time = "2025-06-10T00:45:20.677Z" }, - { url = "https://files.pythonhosted.org/packages/9e/ed/c5fb04869b99b717985e244fd93029c7a8e8febdfcffa06093e32d7d44e7/yarl-1.20.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:88cab98aa4e13e1ade8c141daeedd300a4603b7132819c484841bb7af3edce9e", size = 341709, upload_time = "2025-06-10T00:45:23.221Z" }, - { url = "https://files.pythonhosted.org/packages/24/fd/725b8e73ac2a50e78a4534ac43c6addf5c1c2d65380dd48a9169cc6739a9/yarl-1.20.1-cp313-cp313t-win32.whl", hash = "sha256:b121ff6a7cbd4abc28985b6028235491941b9fe8fe226e6fdc539c977ea1739d", size = 86591, upload_time = "2025-06-10T00:45:25.793Z" }, - { url = "https://files.pythonhosted.org/packages/94/c3/b2e9f38bc3e11191981d57ea08cab2166e74ea770024a646617c9cddd9f6/yarl-1.20.1-cp313-cp313t-win_amd64.whl", hash = "sha256:541d050a355bbbc27e55d906bc91cb6fe42f96c01413dd0f4ed5a5240513874f", size = 93003, upload_time = "2025-06-10T00:45:27.752Z" }, - { url = "https://files.pythonhosted.org/packages/b4/2d/2345fce04cfd4bee161bf1e7d9cdc702e3e16109021035dbb24db654a622/yarl-1.20.1-py3-none-any.whl", hash = "sha256:83b8eb083fe4683c6115795d9fc1cfaf2cbbefb19b3a1cb68f6527460f483a77", size = 46542, upload_time = "2025-06-10T00:46:07.521Z" }, + { url = "https://files.pythonhosted.org/packages/5f/9a/cb7fad7d73c69f296eda6815e4a2c7ed53fc70c2f136479a91c8e5fbdb6d/yarl-1.20.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdcc4cd244e58593a4379fe60fdee5ac0331f8eb70320a24d591a3be197b94a9", size = 133667, upload-time = "2025-06-10T00:43:44.369Z" }, + { url = "https://files.pythonhosted.org/packages/67/38/688577a1cb1e656e3971fb66a3492501c5a5df56d99722e57c98249e5b8a/yarl-1.20.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b29a2c385a5f5b9c7d9347e5812b6f7ab267193c62d282a540b4fc528c8a9d2a", size = 91025, upload-time = "2025-06-10T00:43:46.295Z" }, + { url = "https://files.pythonhosted.org/packages/50/ec/72991ae51febeb11a42813fc259f0d4c8e0507f2b74b5514618d8b640365/yarl-1.20.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1112ae8154186dfe2de4732197f59c05a83dc814849a5ced892b708033f40dc2", size = 89709, upload-time = "2025-06-10T00:43:48.22Z" }, + { url = "https://files.pythonhosted.org/packages/99/da/4d798025490e89426e9f976702e5f9482005c548c579bdae792a4c37769e/yarl-1.20.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90bbd29c4fe234233f7fa2b9b121fb63c321830e5d05b45153a2ca68f7d310ee", size = 352287, upload-time = "2025-06-10T00:43:49.924Z" }, + { url = "https://files.pythonhosted.org/packages/1a/26/54a15c6a567aac1c61b18aa0f4b8aa2e285a52d547d1be8bf48abe2b3991/yarl-1.20.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:680e19c7ce3710ac4cd964e90dad99bf9b5029372ba0c7cbfcd55e54d90ea819", size = 345429, upload-time = "2025-06-10T00:43:51.7Z" }, + { url = "https://files.pythonhosted.org/packages/d6/95/9dcf2386cb875b234353b93ec43e40219e14900e046bf6ac118f94b1e353/yarl-1.20.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4a979218c1fdb4246a05efc2cc23859d47c89af463a90b99b7c56094daf25a16", size = 365429, upload-time = "2025-06-10T00:43:53.494Z" }, + { url = "https://files.pythonhosted.org/packages/91/b2/33a8750f6a4bc224242a635f5f2cff6d6ad5ba651f6edcccf721992c21a0/yarl-1.20.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:255b468adf57b4a7b65d8aad5b5138dce6a0752c139965711bdcb81bc370e1b6", size = 363862, upload-time = "2025-06-10T00:43:55.766Z" }, + { url = "https://files.pythonhosted.org/packages/98/28/3ab7acc5b51f4434b181b0cee8f1f4b77a65919700a355fb3617f9488874/yarl-1.20.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a97d67108e79cfe22e2b430d80d7571ae57d19f17cda8bb967057ca8a7bf5bfd", size = 355616, upload-time = "2025-06-10T00:43:58.056Z" }, + { url = "https://files.pythonhosted.org/packages/36/a3/f666894aa947a371724ec7cd2e5daa78ee8a777b21509b4252dd7bd15e29/yarl-1.20.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8570d998db4ddbfb9a590b185a0a33dbf8aafb831d07a5257b4ec9948df9cb0a", size = 339954, upload-time = "2025-06-10T00:43:59.773Z" }, + { url = "https://files.pythonhosted.org/packages/f1/81/5f466427e09773c04219d3450d7a1256138a010b6c9f0af2d48565e9ad13/yarl-1.20.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:97c75596019baae7c71ccf1d8cc4738bc08134060d0adfcbe5642f778d1dca38", size = 365575, upload-time = "2025-06-10T00:44:02.051Z" }, + { url = "https://files.pythonhosted.org/packages/2e/e3/e4b0ad8403e97e6c9972dd587388940a032f030ebec196ab81a3b8e94d31/yarl-1.20.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:1c48912653e63aef91ff988c5432832692ac5a1d8f0fb8a33091520b5bbe19ef", size = 365061, upload-time = "2025-06-10T00:44:04.196Z" }, + { url = "https://files.pythonhosted.org/packages/ac/99/b8a142e79eb86c926f9f06452eb13ecb1bb5713bd01dc0038faf5452e544/yarl-1.20.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4c3ae28f3ae1563c50f3d37f064ddb1511ecc1d5584e88c6b7c63cf7702a6d5f", size = 364142, upload-time = "2025-06-10T00:44:06.527Z" }, + { url = "https://files.pythonhosted.org/packages/34/f2/08ed34a4a506d82a1a3e5bab99ccd930a040f9b6449e9fd050320e45845c/yarl-1.20.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c5e9642f27036283550f5f57dc6156c51084b458570b9d0d96100c8bebb186a8", size = 381894, upload-time = "2025-06-10T00:44:08.379Z" }, + { url = "https://files.pythonhosted.org/packages/92/f8/9a3fbf0968eac704f681726eff595dce9b49c8a25cd92bf83df209668285/yarl-1.20.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:2c26b0c49220d5799f7b22c6838409ee9bc58ee5c95361a4d7831f03cc225b5a", size = 383378, upload-time = "2025-06-10T00:44:10.51Z" }, + { url = "https://files.pythonhosted.org/packages/af/85/9363f77bdfa1e4d690957cd39d192c4cacd1c58965df0470a4905253b54f/yarl-1.20.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:564ab3d517e3d01c408c67f2e5247aad4019dcf1969982aba3974b4093279004", size = 374069, upload-time = "2025-06-10T00:44:12.834Z" }, + { url = "https://files.pythonhosted.org/packages/35/99/9918c8739ba271dcd935400cff8b32e3cd319eaf02fcd023d5dcd487a7c8/yarl-1.20.1-cp312-cp312-win32.whl", hash = "sha256:daea0d313868da1cf2fac6b2d3a25c6e3a9e879483244be38c8e6a41f1d876a5", size = 81249, upload-time = "2025-06-10T00:44:14.731Z" }, + { url = "https://files.pythonhosted.org/packages/eb/83/5d9092950565481b413b31a23e75dd3418ff0a277d6e0abf3729d4d1ce25/yarl-1.20.1-cp312-cp312-win_amd64.whl", hash = "sha256:48ea7d7f9be0487339828a4de0360d7ce0efc06524a48e1810f945c45b813698", size = 86710, upload-time = "2025-06-10T00:44:16.716Z" }, + { url = "https://files.pythonhosted.org/packages/8a/e1/2411b6d7f769a07687acee88a062af5833cf1966b7266f3d8dfb3d3dc7d3/yarl-1.20.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:0b5ff0fbb7c9f1b1b5ab53330acbfc5247893069e7716840c8e7d5bb7355038a", size = 131811, upload-time = "2025-06-10T00:44:18.933Z" }, + { url = "https://files.pythonhosted.org/packages/b2/27/584394e1cb76fb771371770eccad35de400e7b434ce3142c2dd27392c968/yarl-1.20.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:14f326acd845c2b2e2eb38fb1346c94f7f3b01a4f5c788f8144f9b630bfff9a3", size = 90078, upload-time = "2025-06-10T00:44:20.635Z" }, + { url = "https://files.pythonhosted.org/packages/bf/9a/3246ae92d4049099f52d9b0fe3486e3b500e29b7ea872d0f152966fc209d/yarl-1.20.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f60e4ad5db23f0b96e49c018596707c3ae89f5d0bd97f0ad3684bcbad899f1e7", size = 88748, upload-time = "2025-06-10T00:44:22.34Z" }, + { url = "https://files.pythonhosted.org/packages/a3/25/35afe384e31115a1a801fbcf84012d7a066d89035befae7c5d4284df1e03/yarl-1.20.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:49bdd1b8e00ce57e68ba51916e4bb04461746e794e7c4d4bbc42ba2f18297691", size = 349595, upload-time = "2025-06-10T00:44:24.314Z" }, + { url = "https://files.pythonhosted.org/packages/28/2d/8aca6cb2cabc8f12efcb82749b9cefecbccfc7b0384e56cd71058ccee433/yarl-1.20.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:66252d780b45189975abfed839616e8fd2dbacbdc262105ad7742c6ae58f3e31", size = 342616, upload-time = "2025-06-10T00:44:26.167Z" }, + { url = "https://files.pythonhosted.org/packages/0b/e9/1312633d16b31acf0098d30440ca855e3492d66623dafb8e25b03d00c3da/yarl-1.20.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:59174e7332f5d153d8f7452a102b103e2e74035ad085f404df2e40e663a22b28", size = 361324, upload-time = "2025-06-10T00:44:27.915Z" }, + { url = "https://files.pythonhosted.org/packages/bc/a0/688cc99463f12f7669eec7c8acc71ef56a1521b99eab7cd3abb75af887b0/yarl-1.20.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e3968ec7d92a0c0f9ac34d5ecfd03869ec0cab0697c91a45db3fbbd95fe1b653", size = 359676, upload-time = "2025-06-10T00:44:30.041Z" }, + { url = "https://files.pythonhosted.org/packages/af/44/46407d7f7a56e9a85a4c207724c9f2c545c060380718eea9088f222ba697/yarl-1.20.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d1a4fbb50e14396ba3d375f68bfe02215d8e7bc3ec49da8341fe3157f59d2ff5", size = 352614, upload-time = "2025-06-10T00:44:32.171Z" }, + { url = "https://files.pythonhosted.org/packages/b1/91/31163295e82b8d5485d31d9cf7754d973d41915cadce070491778d9c9825/yarl-1.20.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:11a62c839c3a8eac2410e951301309426f368388ff2f33799052787035793b02", size = 336766, upload-time = "2025-06-10T00:44:34.494Z" }, + { url = "https://files.pythonhosted.org/packages/b4/8e/c41a5bc482121f51c083c4c2bcd16b9e01e1cf8729e380273a952513a21f/yarl-1.20.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:041eaa14f73ff5a8986b4388ac6bb43a77f2ea09bf1913df7a35d4646db69e53", size = 364615, upload-time = "2025-06-10T00:44:36.856Z" }, + { url = "https://files.pythonhosted.org/packages/e3/5b/61a3b054238d33d70ea06ebba7e58597891b71c699e247df35cc984ab393/yarl-1.20.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:377fae2fef158e8fd9d60b4c8751387b8d1fb121d3d0b8e9b0be07d1b41e83dc", size = 360982, upload-time = "2025-06-10T00:44:39.141Z" }, + { url = "https://files.pythonhosted.org/packages/df/a3/6a72fb83f8d478cb201d14927bc8040af901811a88e0ff2da7842dd0ed19/yarl-1.20.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:1c92f4390e407513f619d49319023664643d3339bd5e5a56a3bebe01bc67ec04", size = 369792, upload-time = "2025-06-10T00:44:40.934Z" }, + { url = "https://files.pythonhosted.org/packages/7c/af/4cc3c36dfc7c077f8dedb561eb21f69e1e9f2456b91b593882b0b18c19dc/yarl-1.20.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:d25ddcf954df1754ab0f86bb696af765c5bfaba39b74095f27eececa049ef9a4", size = 382049, upload-time = "2025-06-10T00:44:42.854Z" }, + { url = "https://files.pythonhosted.org/packages/19/3a/e54e2c4752160115183a66dc9ee75a153f81f3ab2ba4bf79c3c53b33de34/yarl-1.20.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:909313577e9619dcff8c31a0ea2aa0a2a828341d92673015456b3ae492e7317b", size = 384774, upload-time = "2025-06-10T00:44:45.275Z" }, + { url = "https://files.pythonhosted.org/packages/9c/20/200ae86dabfca89060ec6447649f219b4cbd94531e425e50d57e5f5ac330/yarl-1.20.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:793fd0580cb9664548c6b83c63b43c477212c0260891ddf86809e1c06c8b08f1", size = 374252, upload-time = "2025-06-10T00:44:47.31Z" }, + { url = "https://files.pythonhosted.org/packages/83/75/11ee332f2f516b3d094e89448da73d557687f7d137d5a0f48c40ff211487/yarl-1.20.1-cp313-cp313-win32.whl", hash = "sha256:468f6e40285de5a5b3c44981ca3a319a4b208ccc07d526b20b12aeedcfa654b7", size = 81198, upload-time = "2025-06-10T00:44:49.164Z" }, + { url = "https://files.pythonhosted.org/packages/ba/ba/39b1ecbf51620b40ab402b0fc817f0ff750f6d92712b44689c2c215be89d/yarl-1.20.1-cp313-cp313-win_amd64.whl", hash = "sha256:495b4ef2fea40596bfc0affe3837411d6aa3371abcf31aac0ccc4bdd64d4ef5c", size = 86346, upload-time = "2025-06-10T00:44:51.182Z" }, + { url = "https://files.pythonhosted.org/packages/43/c7/669c52519dca4c95153c8ad96dd123c79f354a376346b198f438e56ffeb4/yarl-1.20.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:f60233b98423aab21d249a30eb27c389c14929f47be8430efa7dbd91493a729d", size = 138826, upload-time = "2025-06-10T00:44:52.883Z" }, + { url = "https://files.pythonhosted.org/packages/6a/42/fc0053719b44f6ad04a75d7f05e0e9674d45ef62f2d9ad2c1163e5c05827/yarl-1.20.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:6f3eff4cc3f03d650d8755c6eefc844edde99d641d0dcf4da3ab27141a5f8ddf", size = 93217, upload-time = "2025-06-10T00:44:54.658Z" }, + { url = "https://files.pythonhosted.org/packages/4f/7f/fa59c4c27e2a076bba0d959386e26eba77eb52ea4a0aac48e3515c186b4c/yarl-1.20.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:69ff8439d8ba832d6bed88af2c2b3445977eba9a4588b787b32945871c2444e3", size = 92700, upload-time = "2025-06-10T00:44:56.784Z" }, + { url = "https://files.pythonhosted.org/packages/2f/d4/062b2f48e7c93481e88eff97a6312dca15ea200e959f23e96d8ab898c5b8/yarl-1.20.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3cf34efa60eb81dd2645a2e13e00bb98b76c35ab5061a3989c7a70f78c85006d", size = 347644, upload-time = "2025-06-10T00:44:59.071Z" }, + { url = "https://files.pythonhosted.org/packages/89/47/78b7f40d13c8f62b499cc702fdf69e090455518ae544c00a3bf4afc9fc77/yarl-1.20.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8e0fe9364ad0fddab2688ce72cb7a8e61ea42eff3c7caeeb83874a5d479c896c", size = 323452, upload-time = "2025-06-10T00:45:01.605Z" }, + { url = "https://files.pythonhosted.org/packages/eb/2b/490d3b2dc66f52987d4ee0d3090a147ea67732ce6b4d61e362c1846d0d32/yarl-1.20.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8f64fbf81878ba914562c672024089e3401974a39767747691c65080a67b18c1", size = 346378, upload-time = "2025-06-10T00:45:03.946Z" }, + { url = "https://files.pythonhosted.org/packages/66/ad/775da9c8a94ce925d1537f939a4f17d782efef1f973039d821cbe4bcc211/yarl-1.20.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f6342d643bf9a1de97e512e45e4b9560a043347e779a173250824f8b254bd5ce", size = 353261, upload-time = "2025-06-10T00:45:05.992Z" }, + { url = "https://files.pythonhosted.org/packages/4b/23/0ed0922b47a4f5c6eb9065d5ff1e459747226ddce5c6a4c111e728c9f701/yarl-1.20.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:56dac5f452ed25eef0f6e3c6a066c6ab68971d96a9fb441791cad0efba6140d3", size = 335987, upload-time = "2025-06-10T00:45:08.227Z" }, + { url = "https://files.pythonhosted.org/packages/3e/49/bc728a7fe7d0e9336e2b78f0958a2d6b288ba89f25a1762407a222bf53c3/yarl-1.20.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7d7f497126d65e2cad8dc5f97d34c27b19199b6414a40cb36b52f41b79014be", size = 329361, upload-time = "2025-06-10T00:45:10.11Z" }, + { url = "https://files.pythonhosted.org/packages/93/8f/b811b9d1f617c83c907e7082a76e2b92b655400e61730cd61a1f67178393/yarl-1.20.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:67e708dfb8e78d8a19169818eeb5c7a80717562de9051bf2413aca8e3696bf16", size = 346460, upload-time = "2025-06-10T00:45:12.055Z" }, + { url = "https://files.pythonhosted.org/packages/70/fd/af94f04f275f95da2c3b8b5e1d49e3e79f1ed8b6ceb0f1664cbd902773ff/yarl-1.20.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:595c07bc79af2494365cc96ddeb772f76272364ef7c80fb892ef9d0649586513", size = 334486, upload-time = "2025-06-10T00:45:13.995Z" }, + { url = "https://files.pythonhosted.org/packages/84/65/04c62e82704e7dd0a9b3f61dbaa8447f8507655fd16c51da0637b39b2910/yarl-1.20.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:7bdd2f80f4a7df852ab9ab49484a4dee8030023aa536df41f2d922fd57bf023f", size = 342219, upload-time = "2025-06-10T00:45:16.479Z" }, + { url = "https://files.pythonhosted.org/packages/91/95/459ca62eb958381b342d94ab9a4b6aec1ddec1f7057c487e926f03c06d30/yarl-1.20.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:c03bfebc4ae8d862f853a9757199677ab74ec25424d0ebd68a0027e9c639a390", size = 350693, upload-time = "2025-06-10T00:45:18.399Z" }, + { url = "https://files.pythonhosted.org/packages/a6/00/d393e82dd955ad20617abc546a8f1aee40534d599ff555ea053d0ec9bf03/yarl-1.20.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:344d1103e9c1523f32a5ed704d576172d2cabed3122ea90b1d4e11fe17c66458", size = 355803, upload-time = "2025-06-10T00:45:20.677Z" }, + { url = "https://files.pythonhosted.org/packages/9e/ed/c5fb04869b99b717985e244fd93029c7a8e8febdfcffa06093e32d7d44e7/yarl-1.20.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:88cab98aa4e13e1ade8c141daeedd300a4603b7132819c484841bb7af3edce9e", size = 341709, upload-time = "2025-06-10T00:45:23.221Z" }, + { url = "https://files.pythonhosted.org/packages/24/fd/725b8e73ac2a50e78a4534ac43c6addf5c1c2d65380dd48a9169cc6739a9/yarl-1.20.1-cp313-cp313t-win32.whl", hash = "sha256:b121ff6a7cbd4abc28985b6028235491941b9fe8fe226e6fdc539c977ea1739d", size = 86591, upload-time = "2025-06-10T00:45:25.793Z" }, + { url = "https://files.pythonhosted.org/packages/94/c3/b2e9f38bc3e11191981d57ea08cab2166e74ea770024a646617c9cddd9f6/yarl-1.20.1-cp313-cp313t-win_amd64.whl", hash = "sha256:541d050a355bbbc27e55d906bc91cb6fe42f96c01413dd0f4ed5a5240513874f", size = 93003, upload-time = "2025-06-10T00:45:27.752Z" }, + { url = "https://files.pythonhosted.org/packages/b4/2d/2345fce04cfd4bee161bf1e7d9cdc702e3e16109021035dbb24db654a622/yarl-1.20.1-py3-none-any.whl", hash = "sha256:83b8eb083fe4683c6115795d9fc1cfaf2cbbefb19b3a1cb68f6527460f483a77", size = 46542, upload-time = "2025-06-10T00:46:07.521Z" }, ] From 0a3923fbdd96979beb68b1ecc2022020ec962812 Mon Sep 17 00:00:00 2001 From: Alex Tau Date: Mon, 1 Dec 2025 17:54:49 +0300 Subject: [PATCH 47/56] upgrade to nixos 25.11 --- flake.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flake.nix b/flake.nix index 763f49d..2edc46b 100644 --- a/flake.nix +++ b/flake.nix @@ -1,6 +1,6 @@ { inputs = { - nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.05"; + nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.11"; pyproject-nix = { url = "github:pyproject-nix/pyproject.nix"; From 8c59bb2f31277cfb5373579ad47826e24da2df74 Mon Sep 17 00:00:00 2001 From: Alex Tau Date: Mon, 1 Dec 2025 19:10:06 +0300 Subject: [PATCH 48/56] update all flake references --- flake.lock | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/flake.lock b/flake.lock index c6a1e31..47f6a09 100644 --- a/flake.lock +++ b/flake.lock @@ -2,16 +2,16 @@ "nodes": { "nixpkgs": { "locked": { - "lastModified": 1749086602, - "narHash": "sha256-DJcgJMekoxVesl9kKjfLPix2Nbr42i7cpEHJiTnBUwU=", + "lastModified": 1764522689, + "narHash": "sha256-SqUuBFjhl/kpDiVaKLQBoD8TLD+/cTUzzgVFoaHrkqY=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "4792576cb003c994bd7cc1edada3129def20b27d", + "rev": "8bb5646e0bed5dbd3ab08c7a7cc15b75ab4e1d0f", "type": "github" }, "original": { "owner": "NixOS", - "ref": "nixos-25.05", + "ref": "nixos-25.11", "repo": "nixpkgs", "type": "github" } @@ -29,11 +29,11 @@ ] }, "locked": { - "lastModified": 1744599653, - "narHash": "sha256-nysSwVVjG4hKoOjhjvE6U5lIKA8sEr1d1QzEfZsannU=", + "lastModified": 1763662255, + "narHash": "sha256-4bocaOyLa3AfiS8KrWjZQYu+IAta05u3gYZzZ6zXbT0=", "owner": "pyproject-nix", "repo": "build-system-pkgs", - "rev": "7dba6dbc73120e15b558754c26024f6c93015dd7", + "rev": "042904167604c681a090c07eb6967b4dd4dae88c", "type": "github" }, "original": { @@ -49,11 +49,11 @@ ] }, "locked": { - "lastModified": 1743438845, - "narHash": "sha256-1GSaoubGtvsLRwoYwHjeKYq40tLwvuFFVhGrG8J9Oek=", + "lastModified": 1764134915, + "narHash": "sha256-xaKvtPx6YAnA3HQVp5LwyYG1MaN4LLehpQI8xEdBvBY=", "owner": "pyproject-nix", "repo": "pyproject.nix", - "rev": "8063ec98edc459571d042a640b1c5e334ecfca1e", + "rev": "2c8df1383b32e5443c921f61224b198a2282a657", "type": "github" }, "original": { @@ -80,11 +80,11 @@ ] }, "locked": { - "lastModified": 1749170547, - "narHash": "sha256-zOptuFhTr9P0A+unFaOBFx5E5T6yx0qE8VrUGVrM96U=", + "lastModified": 1764546642, + "narHash": "sha256-pCzgOjGEZyH7xKmpckdJzWyO0kvTIlaTK+ed/wguv5Y=", "owner": "pyproject-nix", "repo": "uv2nix", - "rev": "7ae60727d4fc2e41aefd30da665e4f92ba8298f1", + "rev": "0c56de7543459a23d0ebb7977fd555ced5d842ae", "type": "github" }, "original": { From 58e47ae58480a38401c1216b9a0a9ce3a4add966 Mon Sep 17 00:00:00 2001 From: Alex Tau Date: Sun, 7 Dec 2025 20:50:12 +0300 Subject: [PATCH 49/56] remove unused imports --- src/lego_monitoring/config/alert_channels.py | 2 +- src/lego_monitoring/config/checks/ups.py | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/lego_monitoring/config/alert_channels.py b/src/lego_monitoring/config/alert_channels.py index b2c4c97..fa33579 100644 --- a/src/lego_monitoring/config/alert_channels.py +++ b/src/lego_monitoring/config/alert_channels.py @@ -1,4 +1,4 @@ -from dataclasses import dataclass, field +from dataclasses import dataclass from typing import Optional from alt_utils import NestedDeserializableDataclass diff --git a/src/lego_monitoring/config/checks/ups.py b/src/lego_monitoring/config/checks/ups.py index f0ee64d..32b31ac 100644 --- a/src/lego_monitoring/config/checks/ups.py +++ b/src/lego_monitoring/config/checks/ups.py @@ -1,8 +1,6 @@ from dataclasses import dataclass from typing import Optional -from alt_utils import NestedDeserializableDataclass - @dataclass class UPSCheckConfig: From 40e30529eb0ff0d082f9b33a1649fb9bd20799fd Mon Sep 17 00:00:00 2001 From: Alex Tau Date: Fri, 19 Dec 2025 15:48:01 +0300 Subject: [PATCH 50/56] handle events sent to ups pipe --- pyproject.toml | 1 + src/lego_monitoring/__init__.py | 25 ++- src/lego_monitoring/alerting/sender.py | 6 +- src/lego_monitoring/checks/__init__.py | 2 +- src/lego_monitoring/checks/ups.py | 120 --------------- src/lego_monitoring/checks/ups/check.py | 184 +++++++++++++++++++++++ src/lego_monitoring/checks/ups/events.py | 44 ++++++ src/lego_monitoring/core/checkers.py | 83 +++++++++- src/lego_monitoring/core/const.py | 1 + uv.lock | 11 ++ 10 files changed, 343 insertions(+), 134 deletions(-) delete mode 100644 src/lego_monitoring/checks/ups.py create mode 100644 src/lego_monitoring/checks/ups/check.py create mode 100644 src/lego_monitoring/checks/ups/events.py diff --git a/pyproject.toml b/pyproject.toml index d9c6434..aefda49 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,6 +6,7 @@ readme = "README.md" requires-python = ">=3.12" dependencies = [ "aiodns>=3.5.0", + "aiofiles>=25.1.0", "aiohttp>=3.12.15", "alt-utils>=0.0.8", "humanize>=4.12.3", diff --git a/src/lego_monitoring/__init__.py b/src/lego_monitoring/__init__.py index 63536dd..29e5e56 100644 --- a/src/lego_monitoring/__init__.py +++ b/src/lego_monitoring/__init__.py @@ -12,7 +12,13 @@ from .checks.temp.sensors import print_readings from .config import enums as config_enums from .config import load_config from .core import cvars -from .core.checkers import BaseChecker, IntervalChecker, ScheduledChecker +from .core.checkers import ( + BaseChecker, + IntervalChecker, + PipeIntervalChecker, + ScheduledChecker, +) +from .core.const import UPS_PIPE_NAME stopping = False @@ -86,7 +92,14 @@ async def async_main(): check_sets.NET: [ IntervalChecker(checks.NetIOTracker().net_check, interval=datetime.timedelta(minutes=5), persistent=True) ], - check_sets.UPS: [IntervalChecker(checks.ups_check, interval=datetime.timedelta(minutes=5), persistent=True)], + check_sets.UPS: [ + PipeIntervalChecker( + checks.UPSTracker().ups_check, + interval=datetime.timedelta(minutes=5), + persistent=True, + pipe=UPS_PIPE_NAME, + ) + ], } checkers = [] @@ -131,7 +144,13 @@ async def async_main(): alert = checks.generate_stop_alert() await sender.send_alert(alert) await sender.send_healthchecks_status(alert) - await tg_client.disconnect() + for c in checkers: + try: + await c.graceful_stop() + except AttributeError: + continue + if tg_client: + await tg_client.disconnect() raise SystemExit else: await asyncio.sleep(3) diff --git a/src/lego_monitoring/alerting/sender.py b/src/lego_monitoring/alerting/sender.py index 8601172..e06907f 100644 --- a/src/lego_monitoring/alerting/sender.py +++ b/src/lego_monitoring/alerting/sender.py @@ -9,7 +9,7 @@ from ..checks.utils import format_for_healthchecks_slug from ..core import cvars from .alert import Alert from .clients.healthchecks import HealthchecksClient -from .enum import SEVERITY_TO_EMOJI, AlertType, Severity +from .enum import SEVERITY_TO_EMOJI, Severity async def get_tg_client() -> TelegramClient: @@ -23,9 +23,7 @@ async def get_tg_client() -> TelegramClient: def get_healthchecks_client() -> HealthchecksClient: config = cvars.config.get() base_url = config.alert_channels.healthchecks.pinging_api_endpoint - client = HealthchecksClient( - base_url=config.alert_channels.healthchecks.pinging_api_endpoint, client=AiohttpClient() - ) + client = HealthchecksClient(base_url=base_url, client=AiohttpClient()) return client diff --git a/src/lego_monitoring/checks/__init__.py b/src/lego_monitoring/checks/__init__.py index c5518ee..5be0d7f 100644 --- a/src/lego_monitoring/checks/__init__.py +++ b/src/lego_monitoring/checks/__init__.py @@ -4,5 +4,5 @@ from .ram import ram_check from .remind import remind_check from .self import generate_start_alert, generate_stop_alert, self_check from .temp import temp_check -from .ups import ups_check +from .ups.check import UPSTracker from .vulnix import vulnix_check diff --git a/src/lego_monitoring/checks/ups.py b/src/lego_monitoring/checks/ups.py deleted file mode 100644 index 447bbc8..0000000 --- a/src/lego_monitoring/checks/ups.py +++ /dev/null @@ -1,120 +0,0 @@ -import subprocess -from dataclasses import dataclass -from datetime import timedelta -from enum import StrEnum -from socket import gethostname - -from lego_monitoring.alerting.alert import Alert -from lego_monitoring.alerting.enum import AlertType, Severity -from lego_monitoring.core import cvars -from lego_monitoring.core.const import UPSC_PATH - -from .utils import format_for_healthchecks_slug - - -class UPSStatus(StrEnum): - """https://networkupstools.org/docs/developer-guide.chunked/new-drivers.html#_status_data""" - - ON_LINE = "OL" - ON_BATTERY = "OB" - BATTERY_LOW = "LB" - BATTERY_HIGH = "HB" - BATTERY_REPLACE = "RB" - BATTERY_CHARGING = "CHRG" - BATTERY_DISCHARGING = "DISCHRG" - UPS_BYPASS = "BYPASS" - UPS_OFFLINE = "OFF" - UPS_OVERLOAD = "OVER" - UPS_CALIBRATION = "CAL" - UPS_TRIM = "TRIM" - UPS_BOOST = "BOOST" - UPS_FSD = "FSD" - ALARM = "ALARM" - WAIT = "WAIT" - - -@dataclass -class UPSStats: - ups_status: list[UPSStatus] = None - battery_charge_percentage: int = None - battery_warning_percentage: int = 20 - battery_critical_percentage: int = 10 - battery_runtime: int = 1000 - - def __str__(self): - return f"""Status: {' '.join(self.ups_status)} -Battery: {self.battery_charge_percentage}% -Remaining runtime: {timedelta(seconds=self.battery_runtime)} -Will warn at {self.battery_warning_percentage}% -Will shut down at {self.battery_critical_percentage}% -""" - - -def get_ups_list() -> list[str]: - run_results = subprocess.run([UPSC_PATH, "-l"], stdout=subprocess.PIPE, encoding="utf-8") - return run_results.stdout.splitlines() - - -def get_ups_stats(ups: str) -> UPSStats: - stats = UPSStats() - - run_results = subprocess.run([UPSC_PATH, ups], stdout=subprocess.PIPE, encoding="utf-8") - for line in run_results.stdout.splitlines(): - variable, value = line.split(": ")[:2] - match variable: - case "battery.charge": - stats.battery_charge_percentage = int(value) - case "battery.charge.low": - stats.battery_critical_percentage = int(value) - case "battery.charge.warning": - stats.battery_warning_percentage = int(value) - case "battery.runtime": - stats.battery_runtime = int(value) - case "ups.status": - stats.ups_status = [UPSStatus(status) for status in value.split()] - case _: - ... - return stats - - -def ups_check() -> list[Alert]: - config = cvars.config.get().checks.ups - if config.ups_to_check is None: - ups_list = get_ups_list() - else: - ups_list = config.ups_to_check - alerts = [] - for ups in ups_list: - stats = get_ups_stats(ups) - slug = f"{format_for_healthchecks_slug(gethostname())}-ups-{format_for_healthchecks_slug(ups)}-periodic" - severity = Severity.OK - reasons_for_severity = [] - - if stats.battery_charge_percentage < stats.battery_critical_percentage: - severity = Severity.CRITICAL - reasons_for_severity.append("Critical percentage reached") - elif stats.battery_charge_percentage < stats.battery_critical_percentage: - severity = Severity.WARNING - reasons_for_severity.append("Warning percentage reached") - - for status in stats.ups_status: - if status == UPSStatus.UPS_OVERLOAD: - severity = Severity.CRITICAL - reasons_for_severity.append("UPS is overloaded") - elif status == UPSStatus.ON_BATTERY: - severity = max(Severity.WARNING, severity) - reasons_for_severity.append("UPS is on battery") - elif status == UPSStatus.ALARM: - severity = max(Severity.WARNING, severity) - reasons_for_severity.append("Alarm triggered") - elif status == UPSStatus.WAIT: - severity = max(Severity.INFO, severity) - reasons_for_severity.append("Waiting for info from UPS driver") - - if len(reasons_for_severity) > 0: - message = f"NOTE: {', '.join(reasons_for_severity)}\n{stats}" - else: - message = str(stats) - alerts.append(Alert(alert_type=AlertType.UPS, message=message, severity=severity, healthchecks_slug=slug)) - - return alerts diff --git a/src/lego_monitoring/checks/ups/check.py b/src/lego_monitoring/checks/ups/check.py new file mode 100644 index 0000000..deb78ee --- /dev/null +++ b/src/lego_monitoring/checks/ups/check.py @@ -0,0 +1,184 @@ +import logging +import subprocess +from dataclasses import dataclass, field +from datetime import datetime, timedelta +from enum import StrEnum +from socket import gethostname +from typing import Optional + +from lego_monitoring.alerting.alert import Alert +from lego_monitoring.alerting.enum import AlertType, Severity +from lego_monitoring.config.checks.ups import UPSCheckConfig +from lego_monitoring.core import cvars +from lego_monitoring.core.const import UPSC_PATH + +from ..utils import format_for_healthchecks_slug +from .events import UPSEvent, UPSEventType + + +class UPSStatus(StrEnum): + """https://networkupstools.org/docs/developer-guide.chunked/new-drivers.html#_status_data""" + + ON_LINE = "OL" + ON_BATTERY = "OB" + BATTERY_LOW = "LB" + BATTERY_HIGH = "HB" + BATTERY_REPLACE = "RB" + BATTERY_CHARGING = "CHRG" + BATTERY_DISCHARGING = "DISCHRG" + UPS_BYPASS = "BYPASS" + UPS_OFFLINE = "OFF" + UPS_OVERLOAD = "OVER" + UPS_CALIBRATION = "CAL" + UPS_TRIM = "TRIM" + UPS_BOOST = "BOOST" + UPS_FSD = "FSD" + ALARM = "ALARM" + WAIT = "WAIT" + + +@dataclass +class UPS: + name: str + ups_status: Optional[list[UPSStatus]] = None + latest_events: list[UPSEventType] = field(default_factory=list) + latest_event_time: Optional[datetime] = None + battery_charge_percentage: Optional[int] = None + battery_warning_percentage: Optional[int] = None + battery_critical_percentage: Optional[int] = None + battery_runtime: Optional[int] = None + + def __str__(self): + return f"""Name: {self.name} +Latest events: {f"{', '.join(self.latest_events)} @ {self.latest_event_time.isoformat()}" if len(self.latest_events) > 0 else 'no events recorded'} +Status: {' '.join(self.ups_status) if self.ups_status is not None else '?'} +Battery: {self.battery_charge_percentage if self.battery_charge_percentage is not None else '?'}% +Remaining runtime: {timedelta(seconds=self.battery_runtime) if self.battery_runtime is not None else '?'} +Will warn at {self.battery_warning_percentage if self.battery_warning_percentage is not None else '?'}% +Will shut down at {self.battery_critical_percentage if self.battery_critical_percentage is not None else '?'}% +""" + + +def get_ups_list() -> list[str]: + run_results = subprocess.run([UPSC_PATH, "-l"], stdout=subprocess.PIPE, encoding="utf-8") + return run_results.stdout.splitlines() + + +@dataclass +class UPSTracker: + upses: dict[str, UPS] = field(default_factory=dict) + config: UPSCheckConfig = None + + def __post_init__(self): + self.config = cvars.config.get().checks.ups + + def ups_check(self, ups_events_raw: list[dict]) -> list[Alert]: + ups_events: dict[str, list[UPSEvent]] = {} + for d in ups_events_raw: + event = UPSEvent(**d) + if event.ups_name not in ups_events: + ups_events[event.ups_name] = [event] + else: + ups_events[event.ups_name].append(event) + + if self.config.ups_to_check is None: + ups_list = get_ups_list() + else: + ups_list = self.config.ups_to_check + + alerts = [] + for ups_name in ups_list: + if ups_name not in self.upses: + ups = get_ups_stats(ups_name) + else: + ups = get_ups_stats(self.upses[ups_name]) + + self.upses[ups_name] = ups + + slug = f"{format_for_healthchecks_slug(gethostname())}-ups-{format_for_healthchecks_slug(ups_name)}" + severity = Severity.OK + reasons_for_severity = set() + + if ups_name in ups_events: + ups.latest_event_time = datetime.now() + ups.latest_events = [] + for event in ups_events[ups_name]: + ups.latest_events.append(event.type_) + + match event.type_: + case UPSEventType.FSD: + severity = Severity.CRITICAL + reasons_for_severity.add("Forced shutdown") + case UPSEventType.ALARM: + severity = max(severity, Severity.WARNING) + reasons_for_severity.add("Alarm triggered") + + for event in ups.latest_events: + match event: + case UPSEventType.COMMBAD: + severity = Severity.CRITICAL + reasons_for_severity.add("Communication lost") + case UPSEventType.SHUTDOWN: + severity = Severity.CRITICAL + reasons_for_severity.add("Shutting down now") + case UPSEventType.NOCOMM: + severity = Severity.CRITICAL + reasons_for_severity.add("Cannot establish communication") + + if ups.battery_charge_percentage < ups.battery_critical_percentage: + severity = Severity.CRITICAL + reasons_for_severity.add("Critical percentage reached") + elif ups.battery_charge_percentage < ups.battery_critical_percentage: + severity = max(severity, Severity.WARNING) + reasons_for_severity.add("Warning percentage reached") + + for status in ups.ups_status: + match status: + case UPSStatus.UPS_OVERLOAD: + severity = Severity.CRITICAL + reasons_for_severity.add("UPS is overloaded") + case UPSStatus.ON_BATTERY: + severity = max(Severity.WARNING, severity) + reasons_for_severity.add("UPS is on battery") + case UPSStatus.WAIT: + severity = max(Severity.INFO, severity) + reasons_for_severity.add("Waiting for info from UPS driver") + case UPSStatus.UPS_FSD: + severity = Severity.CRITICAL + reasons_for_severity.add("Forced shutdown") + case UPSStatus.ALARM: + severity = max(severity, Severity.WARNING) + reasons_for_severity.add("Alarm triggered") + + if len(reasons_for_severity) > 0: + message = f"NOTE: {', '.join(reasons_for_severity)}\n{ups}" + else: + message = str(ups) + alerts.append(Alert(alert_type=AlertType.UPS, message=message, severity=severity, healthchecks_slug=slug)) + + return alerts + + +def get_ups_stats(ups_or_name: str | UPS) -> UPS: + if isinstance(ups_or_name, UPS): + ups = ups_or_name + else: + ups = UPS(name=ups_or_name) + + run_results = subprocess.run([UPSC_PATH, ups.name], stdout=subprocess.PIPE, encoding="utf-8") + for line in run_results.stdout.splitlines(): + variable, value = line.split(": ")[:2] + match variable: + case "battery.charge": + ups.battery_charge_percentage = int(value) + case "battery.charge.low": + ups.battery_critical_percentage = int(value) + case "battery.charge.warning": + ups.battery_warning_percentage = int(value) + case "battery.runtime": + ups.battery_runtime = int(value) + case "ups.status": + ups.ups_status = [UPSStatus(status) for status in value.split()] + case _: + ... + return ups diff --git a/src/lego_monitoring/checks/ups/events.py b/src/lego_monitoring/checks/ups/events.py new file mode 100644 index 0000000..de3f132 --- /dev/null +++ b/src/lego_monitoring/checks/ups/events.py @@ -0,0 +1,44 @@ +from dataclasses import dataclass +from enum import StrEnum, auto + + +class UPSEventType(StrEnum): + """https://networkupstools.org/docs/man/upsmon.html#_notify_events""" + + ONLINE = "ONLINE" + ONBATT = "ONBATT" + LOWBATT = "LOWBATT" + FSD = "FSD" + COMMOK = "COMMOK" + COMMBAD = "COMMBAD" + SHUTDOWN = "SHUTDOWN" + REPLBATT = "REPLBATT" + NOCOMM = "NOCOMM" + NOPARENT = "NOPARENT" + CAL = "CAL" + NOTCAL = "NOTCAL" + OFF = "OFF" + NOTOFF = "NOTOFF" + BYPASS = "BYPASS" + NOTBYPASS = "NOTBYPASS" + ECO = "ECO" + NOTECO = "NOTECO" + ALARM = "ALARM" + NOTALARM = "NOTALARM" + OVER = "OVER" + NOTOVER = "NOTOVER" + TRIM = "TRIM" + NOTTRIM = "NOTTRIM" + BOOST = "BOOST" + NOTBOOST = "NOTBOOST" + OTHER = "OTHER" + NOTOTHER = "NOTOTHER" + SUSPEND_STARTING = "SUSPEND_STARTING" + SUSPEND_FINISHED = "SUSPEND_FINISHED" + + +@dataclass +class UPSEvent: + type_: UPSEventType + message: str + ups_name: str diff --git a/src/lego_monitoring/core/checkers.py b/src/lego_monitoring/core/checkers.py index d3e4a0f..29e041c 100644 --- a/src/lego_monitoring/core/checkers.py +++ b/src/lego_monitoring/core/checkers.py @@ -1,8 +1,12 @@ import asyncio import datetime +import json import logging +import os from dataclasses import KW_ONLY, dataclass, field -from typing import Any, Callable, Coroutine +from typing import Any, Callable, Coroutine, Optional + +import aiofiles from ..alerting.alert import Alert from ..alerting.current import CurrentAlerts @@ -10,7 +14,7 @@ from ..alerting.enum import Severity from ..alerting.sender import send_alert, send_healthchecks_status -@dataclass +@dataclass(repr=False) class BaseChecker: check: Callable | Coroutine @@ -51,9 +55,12 @@ class BaseChecker: check_kwargs: dict[str, Any] = field(default_factory=dict) current_alerts: CurrentAlerts = field(default_factory=CurrentAlerts, init=False) - async def _call_check(self) -> list[Alert]: + def __repr__(self): + return f"<{type(self).__name__}(check={self.check})>" + + async def _call_check(self, *extra_args, **extra_kwargs) -> list[Alert]: if isinstance(self.check, Callable): - result = self.check(*self.check_args, **self.check_kwargs) + result = self.check(*self.check_args, *extra_args, **self.check_kwargs, **extra_kwargs) if isinstance(result, Coroutine): result = await result elif isinstance(self.check, Coroutine): @@ -83,8 +90,10 @@ class BaseChecker: raise NotImplementedError -@dataclass +@dataclass(repr=False) class IntervalChecker(BaseChecker): + "Checker that calls the check each interval" + _: KW_ONLY interval: datetime.timedelta ignore_first_run: bool = False @@ -103,8 +112,10 @@ class IntervalChecker(BaseChecker): await asyncio.sleep(interval_secs) -@dataclass +@dataclass(repr=False) class ScheduledChecker(BaseChecker): + "Checker that calls the check each period (usually a day) at the specified time" + _: KW_ONLY period: datetime.timedelta when: datetime.time @@ -128,3 +139,63 @@ class ScheduledChecker(BaseChecker): await self._handle_alerts(result) case _: raise NotImplementedError + + +@dataclass(repr=False) +class PipeIntervalChecker(IntervalChecker): + """ + Checker that watches the specified pipe and calls the check if something arrives. + The check is guaranteed to be called at least once per interval, with empty argument list if nothing arrives + """ + + _: KW_ONLY + pipe: str + read_task: Optional[asyncio.Task] = None + + async def _read_status(self) -> list: + async with aiofiles.open(self.pipe, "r") as p: + return [json.loads(line.rstrip()) async for line in p] + # await asyncio.sleep(60) + # return [] + + async def run_checker(self) -> None: + interval_secs = self.interval.total_seconds() + ignore_first_run = self.ignore_first_run + try: + os.remove(self.pipe) + except FileNotFoundError: + pass + os.mkfifo(self.pipe) + + while True: + logging.info(f"Waiting on pipe {self.pipe}") + self.read_task = asyncio.create_task(self._read_status()) + try: + status = await asyncio.wait_for(self.read_task, interval_secs) + logging.info(f"Got {len(status)} arguments from pipe {self.pipe}") + except asyncio.TimeoutError: + status = [] + logging.info(f"No arguments from {self.pipe}, timeout exceeded") + self.read_task = None + + logging.info(f"Calling {self.check.__name__}") + result = await self._call_check(status) + logging.info(f"Got {len(result)} alerts") + if ignore_first_run: + ignore_first_run = False + else: + await self._handle_alerts(result) + + async def graceful_stop(self) -> None: + logging.info("Cancelling pipe read task") + if self.read_task: + self.read_task.cancel() + async with aiofiles.open(self.pipe, "w") as p: + await p.write("") + try: + await self.read_task + except asyncio.CancelledError: + pass + logging.info("Removing pipe") + os.remove(self.pipe) + logging.info("Done!") diff --git a/src/lego_monitoring/core/const.py b/src/lego_monitoring/core/const.py index 0a84522..99724e4 100644 --- a/src/lego_monitoring/core/const.py +++ b/src/lego_monitoring/core/const.py @@ -1,2 +1,3 @@ VULNIX_PATH: str = ... # path to vulnix executable UPSC_PATH = "/usr/bin/upsc" +UPS_PIPE_NAME = "/tmp/lego-monitoring-ups-status" diff --git a/uv.lock b/uv.lock index 66dc199..f9daed7 100644 --- a/uv.lock +++ b/uv.lock @@ -14,6 +14,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f6/2c/711076e5f5d0707b8ec55a233c8bfb193e0981a800cd1b3b123e8ff61ca1/aiodns-3.5.0-py3-none-any.whl", hash = "sha256:6d0404f7d5215849233f6ee44854f2bb2481adf71b336b2279016ea5990ca5c5", size = 8068, upload-time = "2025-06-13T16:21:52.45Z" }, ] +[[package]] +name = "aiofiles" +version = "25.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/41/c3/534eac40372d8ee36ef40df62ec129bee4fdb5ad9706e58a29be53b2c970/aiofiles-25.1.0.tar.gz", hash = "sha256:a8d728f0a29de45dc521f18f07297428d56992a742f0cd2701ba86e44d23d5b2", size = 46354, upload-time = "2025-10-09T20:51:04.358Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bc/8a/340a1555ae33d7354dbca4faa54948d76d89a27ceef032c8c3bc661d003e/aiofiles-25.1.0-py3-none-any.whl", hash = "sha256:abe311e527c862958650f9438e859c1fa7568a141b22abcd015e120e86a85695", size = 14668, upload-time = "2025-10-09T20:51:03.174Z" }, +] + [[package]] name = "aiohappyeyeballs" version = "2.6.1" @@ -273,6 +282,7 @@ version = "1.1.1" source = { editable = "." } dependencies = [ { name = "aiodns" }, + { name = "aiofiles" }, { name = "aiohttp" }, { name = "alt-utils" }, { name = "humanize" }, @@ -284,6 +294,7 @@ dependencies = [ [package.metadata] requires-dist = [ { name = "aiodns", specifier = ">=3.5.0" }, + { name = "aiofiles", specifier = ">=25.1.0" }, { name = "aiohttp", specifier = ">=3.12.15" }, { name = "alt-utils", specifier = ">=0.0.8" }, { name = "humanize", specifier = ">=4.12.3" }, From 10e79d6827d52486ffa2c124c38b917698e2ab45 Mon Sep 17 00:00:00 2001 From: Alex Tau Date: Fri, 19 Dec 2025 16:34:10 +0300 Subject: [PATCH 51/56] account for alert sending failing --- pyproject.toml | 2 ++ src/lego_monitoring/__init__.py | 5 ++-- src/lego_monitoring/alerting/sender.py | 32 +++++++++++----------- src/lego_monitoring/core/checkers.py | 29 ++++++++++---------- src/lego_monitoring/core/error_handling.py | 23 ++++++++++++++++ uv.lock | 25 +++++++++++++++++ 6 files changed, 84 insertions(+), 32 deletions(-) create mode 100644 src/lego_monitoring/core/error_handling.py diff --git a/pyproject.toml b/pyproject.toml index aefda49..9d8410d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,7 +11,9 @@ dependencies = [ "alt-utils>=0.0.8", "humanize>=4.12.3", "psutil>=7.0.0", + "returns>=0.26.0", "telethon>=1.40.0", + "tenacity>=9.1.2", "uplink[aiohttp]>=0.10.0", ] diff --git a/src/lego_monitoring/__init__.py b/src/lego_monitoring/__init__.py index 29e5e56..385ad42 100644 --- a/src/lego_monitoring/__init__.py +++ b/src/lego_monitoring/__init__.py @@ -142,8 +142,9 @@ async def async_main(): if stopping: if "self" in config.enabled_check_sets: alert = checks.generate_stop_alert() - await sender.send_alert(alert) - await sender.send_healthchecks_status(alert) + async with asyncio.TaskGroup() as tg: + tg.create_task(sender.send_alert(alert)) + tg.create_task(sender.send_healthchecks_status(alert)) for c in checkers: try: await c.graceful_stop() diff --git a/src/lego_monitoring/alerting/sender.py b/src/lego_monitoring/alerting/sender.py index e06907f..0f17b8d 100644 --- a/src/lego_monitoring/alerting/sender.py +++ b/src/lego_monitoring/alerting/sender.py @@ -1,12 +1,13 @@ import logging -from socket import gethostname +import tenacity +from returns.result import Failure, Success from telethon import TelegramClient from telethon.sessions import MemorySession from uplink import AiohttpClient -from ..checks.utils import format_for_healthchecks_slug from ..core import cvars +from ..core.error_handling import log_errors_async from .alert import Alert from .clients.healthchecks import HealthchecksClient from .enum import SEVERITY_TO_EMOJI, Severity @@ -38,27 +39,26 @@ def format_message(alert: Alert, note: str) -> str: return message -async def send_alert(alert: Alert, note: str = "") -> None: +async def send_alert(alert: Alert, note: str = "") -> Success[None] | Failure[tenacity.RetryError]: + await log_errors_async(_send_alert(alert, note)) + + +async def send_healthchecks_status(alert: Alert) -> Success[None] | Failure[tenacity.RetryError]: + await log_errors_async(_send_healthchecks_status(alert)) + + +@tenacity.retry(wait=tenacity.wait_random_exponential(multiplier=1, max=60)) +async def _send_alert(alert: Alert, note: str = "") -> None: logging.debug(f"Sending {alert.alert_type} alert to Telegram") - try: - tg_client = cvars.tg_client.get() - except LookupError: # being called standalone - # cvars.config.set(get_config()) - # temp_client = True - # client = await get_tg_client() - # cvars.matrix_client.set(client) - raise NotImplementedError # TODO - else: - ... # temp_client = False + tg_client = cvars.tg_client.get() if tg_client is not None: room_id = cvars.config.get().alert_channels.telegram.room_id message = format_message(alert, note) await tg_client.send_message(entity=room_id, message=message) - # if temp_client: - # await client.close() -async def send_healthchecks_status(alert: Alert) -> None: +@tenacity.retry(wait=tenacity.wait_random_exponential(multiplier=1, max=60)) +async def _send_healthchecks_status(alert: Alert) -> None: def get_pinging_key(keys: dict[str, str]): if alert.healthchecks_slug in keys: return keys[alert.healthchecks_slug] diff --git a/src/lego_monitoring/core/checkers.py b/src/lego_monitoring/core/checkers.py index 29e041c..3bc1d97 100644 --- a/src/lego_monitoring/core/checkers.py +++ b/src/lego_monitoring/core/checkers.py @@ -70,21 +70,22 @@ class BaseChecker: return result async def _handle_alerts(self, alerts: list[Alert]) -> None: - if not self.is_reminder: - for alert in alerts: - await send_healthchecks_status(alert) + async with asyncio.TaskGroup() as tg: + if not self.is_reminder: + for alert in alerts: + tg.create_task(send_healthchecks_status(alert)) - if not self.persistent: - for alert in alerts: - if alert.severity != Severity.OK: - await send_alert(alert, "ongoing" if self.is_reminder else "") - return - old_severity, new_severity = self.current_alerts.update(alerts) - if (old_severity != new_severity or self.send_any_state) and not ( - old_severity == None and new_severity == Severity.OK - ): - for alert in alerts: - await send_alert(alert, note="ongoing") + if not self.persistent: + for alert in alerts: + if alert.severity != Severity.OK: + tg.create_task(send_alert(alert, "ongoing" if self.is_reminder else "")) + return + old_severity, new_severity = self.current_alerts.update(alerts) + if (old_severity != new_severity or self.send_any_state) and not ( + old_severity == None and new_severity == Severity.OK + ): + for alert in alerts: + tg.create_task(send_alert(alert, note="ongoing")) async def run_checker(self) -> None: raise NotImplementedError diff --git a/src/lego_monitoring/core/error_handling.py b/src/lego_monitoring/core/error_handling.py new file mode 100644 index 0000000..1b340aa --- /dev/null +++ b/src/lego_monitoring/core/error_handling.py @@ -0,0 +1,23 @@ +import logging +import traceback +from typing import Awaitable, Callable, TypeVar + +from returns.result import Failure, Success + +T = TypeVar("T") + + +def log_errors(function: Callable[..., T], *args, **kwargs) -> Success[T] | Failure[Exception]: + try: + return Success(function(args, kwargs)) + except Exception as e: + logging.error(traceback.format_exc()) + return Failure(e) + + +async def log_errors_async(awaitable: Awaitable[T]) -> Success[T] | Failure[Exception]: + try: + return Success(await awaitable) + except Exception as e: + logging.error(traceback.format_exc()) + return Failure(e) diff --git a/uv.lock b/uv.lock index f9daed7..1148674 100644 --- a/uv.lock +++ b/uv.lock @@ -287,7 +287,9 @@ dependencies = [ { name = "alt-utils" }, { name = "humanize" }, { name = "psutil" }, + { name = "returns" }, { name = "telethon" }, + { name = "tenacity" }, { name = "uplink", extra = ["aiohttp"] }, ] @@ -299,7 +301,9 @@ requires-dist = [ { name = "alt-utils", specifier = ">=0.0.8" }, { name = "humanize", specifier = ">=4.12.3" }, { name = "psutil", specifier = ">=7.0.0" }, + { name = "returns", specifier = ">=0.26.0" }, { name = "telethon", specifier = ">=1.40.0" }, + { name = "tenacity", specifier = ">=9.1.2" }, { name = "uplink", extras = ["aiohttp"], specifier = ">=0.10.0" }, ] @@ -514,6 +518,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7c/e4/56027c4a6b4ae70ca9de302488c5ca95ad4a39e190093d6c1a8ace08341b/requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c", size = 64847, upload-time = "2025-06-09T16:43:05.728Z" }, ] +[[package]] +name = "returns" +version = "0.26.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/c2/6dda7ef39464568152e35c766a8b49ab1cdb1b03a5891441a7c2fa40dc61/returns-0.26.0.tar.gz", hash = "sha256:180320e0f6e9ea9845330ccfc020f542330f05b7250941d9b9b7c00203fcc3da", size = 105300, upload-time = "2025-07-24T13:11:21.772Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/57/4d/a7545bf6c62b0dbe5795f22ea9e88cc070fdced5c34663ebc5bed2f610c0/returns-0.26.0-py3-none-any.whl", hash = "sha256:7cae94c730d6c56ffd9d0f583f7a2c0b32cfe17d141837150c8e6cff3eb30d71", size = 160515, upload-time = "2025-07-24T13:11:20.041Z" }, +] + [[package]] name = "rsa" version = "4.9.1" @@ -549,6 +565,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ce/b0/78f74085b6c88c2bf2bec39c67267cd9ba6af24ceaea9654fb0c272a53da/telethon-1.40.0-py3-none-any.whl", hash = "sha256:1aebaca04fd8410968816645bdbcc0baeff55429b6d6bec37e647417bb8e8a2c", size = 744897, upload-time = "2025-09-01T15:32:34.212Z" }, ] +[[package]] +name = "tenacity" +version = "9.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0a/d4/2b0cd0fe285e14b36db076e78c93766ff1d529d70408bd1d2a5a84f1d929/tenacity-9.1.2.tar.gz", hash = "sha256:1169d376c297e7de388d18b4481760d478b0e99a777cad3a9c86e556f4b697cb", size = 48036, upload-time = "2025-04-02T08:25:09.966Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/30/643397144bfbfec6f6ef821f36f33e57d35946c44a2352d3c9f0ae847619/tenacity-9.1.2-py3-none-any.whl", hash = "sha256:f77bf36710d8b73a50b2dd155c97b870017ad21afe6ab300326b0371b3b05138", size = 28248, upload-time = "2025-04-02T08:25:07.678Z" }, +] + [[package]] name = "typing-extensions" version = "4.14.1" From 191839d30f0d7dc39bc2de5d7a954d41d3f29c6a Mon Sep 17 00:00:00 2001 From: Alex Tau Date: Fri, 19 Dec 2025 18:26:11 +0300 Subject: [PATCH 52/56] add script for writing to the pipe --- modules/default.nix | 4 + modules/options.nix | 6 + pyproject.toml | 1 + src/lego_monitoring/__init__.py | 159 +------------------- src/lego_monitoring/checks/__init__.py | 1 + src/lego_monitoring/checks/ups/check.py | 3 + src/lego_monitoring/checks/ups/events.py | 3 +- src/lego_monitoring/checks/ups/notifycmd.py | 27 ++++ src/lego_monitoring/config/checks/ups.py | 1 + src/lego_monitoring/core/checkers.py | 7 + src/lego_monitoring/core/fifo.py | 11 ++ src/lego_monitoring/main.py | 158 +++++++++++++++++++ 12 files changed, 223 insertions(+), 158 deletions(-) create mode 100644 src/lego_monitoring/checks/ups/notifycmd.py create mode 100644 src/lego_monitoring/core/fifo.py create mode 100644 src/lego_monitoring/main.py diff --git a/modules/default.nix b/modules/default.nix index 7035f05..b99764a 100644 --- a/modules/default.nix +++ b/modules/default.nix @@ -79,6 +79,7 @@ package: ups = with cfg.checks.ups; { ups_to_check = upsToCheck; + upsmon_group = upsmonGroup; }; }; }; @@ -97,5 +98,8 @@ package: StartLimitBurst = 3; }; }; + power.ups.upsmon.settings = lib.mkIf (builtins.elem "ups" cfg.enabledCheckSets) { + NOTIFYCMD = "${package}/bin/write-ups-status"; + }; }; } diff --git a/modules/options.nix b/modules/options.nix index a3af83e..82538c7 100644 --- a/modules/options.nix +++ b/modules/options.nix @@ -1,5 +1,6 @@ { lib, + config, ... }: @@ -179,6 +180,11 @@ in default = null; description = "List of UPS's to monitor, in `upsc`-compatible format. If null, all UPS's connected to localhost are checked."; }; + upsmonGroup = lib.mkOption { + type = lib.types.str; + default = config.power.ups.upsmon.user; + description = "Group to allow to send UPS status updates. This should usually include the user upsmon runs as."; + }; }; }; }; diff --git a/pyproject.toml b/pyproject.toml index 9d8410d..4acf0bd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,6 +19,7 @@ dependencies = [ [project.scripts] lego-monitoring = "lego_monitoring:main" +write-ups-status = "lego_monitoring:write_ups_status" [build-system] requires = ["hatchling"] diff --git a/src/lego_monitoring/__init__.py b/src/lego_monitoring/__init__.py index 385ad42..c8dc9e7 100644 --- a/src/lego_monitoring/__init__.py +++ b/src/lego_monitoring/__init__.py @@ -1,157 +1,2 @@ -import argparse -import asyncio -import datetime -import logging -import signal -from typing import Coroutine - -from . import checks -from .alerting import sender -from .alerting.commands import CommandHandlerManager -from .checks.temp.sensors import print_readings -from .config import enums as config_enums -from .config import load_config -from .core import cvars -from .core.checkers import ( - BaseChecker, - IntervalChecker, - PipeIntervalChecker, - ScheduledChecker, -) -from .core.const import UPS_PIPE_NAME - -stopping = False - - -def stop_gracefully(signum, frame): - global stopping - stopping = True - - -def main() -> None: - asyncio.run(async_main()) - - -async def async_main(): - parser = argparse.ArgumentParser( - prog="lego-monitoring", - description="Lego-monitoring service", - ) - parser.add_argument("-c", "--config", help="config file") - parser.add_argument("--print-temp", help="print temp sensor readings and exit", action="store_true") - args = parser.parse_args() - - if args.config: - config_path = parser.parse_args().config - config = load_config(config_path) - cvars.config.set(config) - - if args.print_temp: - print_readings() - raise SystemExit - - if not args.config: - raise RuntimeError("--config must be specified in standard operating mode") - - logging.basicConfig(level=config.log_level) - - check_sets = config_enums.CheckSet - - checker_sets: dict[config_enums.CheckSet, list[Coroutine | BaseChecker]] = { - check_sets.SELF: [ - sender.send_alert(checks.generate_start_alert()), - IntervalChecker(checks.self_check, interval=datetime.timedelta(minutes=5), persistent=False), - ], - check_sets.CPU: [ - IntervalChecker( - checks.cpu_check, interval=datetime.timedelta(minutes=3), persistent=True, ignore_first_run=True - ) - ], - check_sets.RAM: [IntervalChecker(checks.ram_check, interval=datetime.timedelta(minutes=1), persistent=True)], - check_sets.TEMP: [IntervalChecker(checks.temp_check, interval=datetime.timedelta(minutes=5), persistent=True)], - check_sets.VULNIX: [ - IntervalChecker( - checks.vulnix_check, - interval=datetime.timedelta(days=3), - persistent=True, - send_any_state=True, - # As those are checked less often than daily, reminds could lead to awkward situations - # when the vuln is fixed but you still get reminders about it for 2 more days. - remind=False, - ) - ], - check_sets.REMIND: [ - ScheduledChecker( - checks.remind_check, - period=datetime.timedelta(days=1), - when=datetime.time(hour=0, minute=0), - persistent=False, - is_reminder=True, - ) - ], - check_sets.NET: [ - IntervalChecker(checks.NetIOTracker().net_check, interval=datetime.timedelta(minutes=5), persistent=True) - ], - check_sets.UPS: [ - PipeIntervalChecker( - checks.UPSTracker().ups_check, - interval=datetime.timedelta(minutes=5), - persistent=True, - pipe=UPS_PIPE_NAME, - ) - ], - } - - checkers = [] - for enabled_set in config.enabled_check_sets: - for checker in checker_sets[enabled_set]: - checkers.append(checker) - - checker_sets[check_sets.REMIND][0].check_args = [checkers] - - if config.alert_channels.telegram is not None: - tg_client = await sender.get_tg_client() - my_username = (await tg_client.get_me()).username - logging.info(f"Logged in as @{my_username}") - - command_manager = CommandHandlerManager(checkers) - await command_manager.attach_handlers(tg_client) - else: - logging.info("Telegram integration is disabled") - tg_client = None - - cvars.tg_client.set(tg_client) - - if config.alert_channels.healthchecks is not None: - healthchecks_client = sender.get_healthchecks_client() - logging.info("Ready to send pings to healthchecks") - cvars.healthchecks_client.set(healthchecks_client) - else: - logging.info("Healthchecks integration is disabled") - - signal.signal(signal.SIGTERM, stop_gracefully) - - async with asyncio.TaskGroup() as tg: - checker_tasks: set[asyncio.Task] = set() - for c in checkers: - if isinstance(c, BaseChecker): - c = c.run_checker() - task = tg.create_task(c) - checker_tasks.add(task) - while True: - if stopping: - if "self" in config.enabled_check_sets: - alert = checks.generate_stop_alert() - async with asyncio.TaskGroup() as tg: - tg.create_task(sender.send_alert(alert)) - tg.create_task(sender.send_healthchecks_status(alert)) - for c in checkers: - try: - await c.graceful_stop() - except AttributeError: - continue - if tg_client: - await tg_client.disconnect() - raise SystemExit - else: - await asyncio.sleep(3) +from .checks import write_ups_status +from .main import main diff --git a/src/lego_monitoring/checks/__init__.py b/src/lego_monitoring/checks/__init__.py index 5be0d7f..86cecfc 100644 --- a/src/lego_monitoring/checks/__init__.py +++ b/src/lego_monitoring/checks/__init__.py @@ -5,4 +5,5 @@ from .remind import remind_check from .self import generate_start_alert, generate_stop_alert, self_check from .temp import temp_check from .ups.check import UPSTracker +from .ups.notifycmd import write_ups_status from .vulnix import vulnix_check diff --git a/src/lego_monitoring/checks/ups/check.py b/src/lego_monitoring/checks/ups/check.py index deb78ee..d9417cf 100644 --- a/src/lego_monitoring/checks/ups/check.py +++ b/src/lego_monitoring/checks/ups/check.py @@ -121,6 +121,9 @@ class UPSTracker: case UPSEventType.SHUTDOWN: severity = Severity.CRITICAL reasons_for_severity.add("Shutting down now") + case UPSEventType.SHUTDOWN_HOSTSYNC: + severity = Severity.CRITICAL + reasons_for_severity.add("Shutdown initiated (waiting for secondaries)") case UPSEventType.NOCOMM: severity = Severity.CRITICAL reasons_for_severity.add("Cannot establish communication") diff --git a/src/lego_monitoring/checks/ups/events.py b/src/lego_monitoring/checks/ups/events.py index de3f132..b55ea1f 100644 --- a/src/lego_monitoring/checks/ups/events.py +++ b/src/lego_monitoring/checks/ups/events.py @@ -1,5 +1,5 @@ from dataclasses import dataclass -from enum import StrEnum, auto +from enum import StrEnum class UPSEventType(StrEnum): @@ -12,6 +12,7 @@ class UPSEventType(StrEnum): COMMOK = "COMMOK" COMMBAD = "COMMBAD" SHUTDOWN = "SHUTDOWN" + SHUTDOWN_HOSTSYNC = "SHUTDOWN_HOSTSYNC" REPLBATT = "REPLBATT" NOCOMM = "NOCOMM" NOPARENT = "NOPARENT" diff --git a/src/lego_monitoring/checks/ups/notifycmd.py b/src/lego_monitoring/checks/ups/notifycmd.py new file mode 100644 index 0000000..983eada --- /dev/null +++ b/src/lego_monitoring/checks/ups/notifycmd.py @@ -0,0 +1,27 @@ +import json +import os +import sys +from dataclasses import asdict + +from lego_monitoring.core.const import UPS_PIPE_NAME +from lego_monitoring.core.fifo import pipe_exists + +from .events import UPSEvent, UPSEventType + + +def write_ups_status(): + if not pipe_exists(UPS_PIPE_NAME): + raise Exception("lego-monitoring not running!") + + notifytype = os.environ["NOTIFYTYPE"] + if notifytype not in UPSEventType: + notifytype = UPSEventType.OTHER + + upsname = os.environ["UPSNAME"] + message = sys.argv[1] + + event = UPSEvent(type_=notifytype, message=message, ups_name=upsname) + event_s = json.dumps(asdict(event)) + "\n" + + with open(UPS_PIPE_NAME, "a") as p: + p.write(event_s) diff --git a/src/lego_monitoring/config/checks/ups.py b/src/lego_monitoring/config/checks/ups.py index 32b31ac..3ecb596 100644 --- a/src/lego_monitoring/config/checks/ups.py +++ b/src/lego_monitoring/config/checks/ups.py @@ -4,4 +4,5 @@ from typing import Optional @dataclass class UPSCheckConfig: + upsmon_group: str = "nutmon" ups_to_check: Optional[list] = None diff --git a/src/lego_monitoring/core/checkers.py b/src/lego_monitoring/core/checkers.py index 3bc1d97..c5557c4 100644 --- a/src/lego_monitoring/core/checkers.py +++ b/src/lego_monitoring/core/checkers.py @@ -3,6 +3,7 @@ import datetime import json import logging import os +import shutil from dataclasses import KW_ONLY, dataclass, field from typing import Any, Callable, Coroutine, Optional @@ -151,6 +152,8 @@ class PipeIntervalChecker(IntervalChecker): _: KW_ONLY pipe: str + owner_user: Optional[str] = None + owner_group: Optional[str] = None read_task: Optional[asyncio.Task] = None async def _read_status(self) -> list: @@ -162,11 +165,15 @@ class PipeIntervalChecker(IntervalChecker): async def run_checker(self) -> None: interval_secs = self.interval.total_seconds() ignore_first_run = self.ignore_first_run + try: os.remove(self.pipe) except FileNotFoundError: pass os.mkfifo(self.pipe) + if self.owner_user is not None or self.owner_group is not None: + shutil.chown(self.pipe, self.owner_user, self.owner_group) + os.chmod(self.pipe, 0o660) while True: logging.info(f"Waiting on pipe {self.pipe}") diff --git a/src/lego_monitoring/core/fifo.py b/src/lego_monitoring/core/fifo.py new file mode 100644 index 0000000..373fd2c --- /dev/null +++ b/src/lego_monitoring/core/fifo.py @@ -0,0 +1,11 @@ +import os +import stat + + +def pipe_exists(path: str) -> bool: + try: + if stat.S_ISFIFO(os.stat(path).st_mode) == 0: + return False + return True + except FileNotFoundError: + return False diff --git a/src/lego_monitoring/main.py b/src/lego_monitoring/main.py new file mode 100644 index 0000000..3986280 --- /dev/null +++ b/src/lego_monitoring/main.py @@ -0,0 +1,158 @@ +import argparse +import asyncio +import datetime +import logging +import signal +from typing import Coroutine + +from . import checks +from .alerting import sender +from .alerting.commands import CommandHandlerManager +from .checks.temp.sensors import print_readings +from .config import enums as config_enums +from .config import load_config +from .core import cvars +from .core.checkers import ( + BaseChecker, + IntervalChecker, + PipeIntervalChecker, + ScheduledChecker, +) +from .core.const import UPS_PIPE_NAME + +stopping = False + + +def stop_gracefully(signum, frame): + global stopping + stopping = True + + +def main() -> None: + asyncio.run(async_main()) + + +async def async_main(): + parser = argparse.ArgumentParser( + prog="lego-monitoring", + description="Lego-monitoring service", + ) + parser.add_argument("-c", "--config", help="config file") + parser.add_argument("--print-temp", help="print temp sensor readings and exit", action="store_true") + args = parser.parse_args() + + if args.config: + config_path = parser.parse_args().config + config = load_config(config_path) + cvars.config.set(config) + + if args.print_temp: + print_readings() + raise SystemExit + + if not args.config: + raise RuntimeError("--config must be specified in standard operating mode") + + logging.basicConfig(level=config.log_level) + + check_sets = config_enums.CheckSet + + checker_sets: dict[config_enums.CheckSet, list[Coroutine | BaseChecker]] = { + check_sets.SELF: [ + sender.send_alert(checks.generate_start_alert()), + IntervalChecker(checks.self_check, interval=datetime.timedelta(minutes=5), persistent=False), + ], + check_sets.CPU: [ + IntervalChecker( + checks.cpu_check, interval=datetime.timedelta(minutes=3), persistent=True, ignore_first_run=True + ) + ], + check_sets.RAM: [IntervalChecker(checks.ram_check, interval=datetime.timedelta(minutes=1), persistent=True)], + check_sets.TEMP: [IntervalChecker(checks.temp_check, interval=datetime.timedelta(minutes=5), persistent=True)], + check_sets.VULNIX: [ + IntervalChecker( + checks.vulnix_check, + interval=datetime.timedelta(days=3), + persistent=True, + send_any_state=True, + # As those are checked less often than daily, reminds could lead to awkward situations + # when the vuln is fixed but you still get reminders about it for 2 more days. + remind=False, + ) + ], + check_sets.REMIND: [ + ScheduledChecker( + checks.remind_check, + period=datetime.timedelta(days=1), + when=datetime.time(hour=0, minute=0), + persistent=False, + is_reminder=True, + ) + ], + check_sets.NET: [ + IntervalChecker(checks.NetIOTracker().net_check, interval=datetime.timedelta(minutes=5), persistent=True) + ], + check_sets.UPS: [ + PipeIntervalChecker( + checks.UPSTracker().ups_check, + interval=datetime.timedelta(minutes=5), + persistent=True, + pipe=UPS_PIPE_NAME, + owner_group=config.checks.ups.upsmon_group, + ) + ], + } + + checkers = [] + for enabled_set in config.enabled_check_sets: + for checker in checker_sets[enabled_set]: + checkers.append(checker) + + checker_sets[check_sets.REMIND][0].check_args = [checkers] + + if config.alert_channels.telegram is not None: + tg_client = await sender.get_tg_client() + my_username = (await tg_client.get_me()).username + logging.info(f"Logged in as @{my_username}") + + command_manager = CommandHandlerManager(checkers) + await command_manager.attach_handlers(tg_client) + else: + logging.info("Telegram integration is disabled") + tg_client = None + + cvars.tg_client.set(tg_client) + + if config.alert_channels.healthchecks is not None: + healthchecks_client = sender.get_healthchecks_client() + logging.info("Ready to send pings to healthchecks") + cvars.healthchecks_client.set(healthchecks_client) + else: + logging.info("Healthchecks integration is disabled") + + signal.signal(signal.SIGTERM, stop_gracefully) + + async with asyncio.TaskGroup() as tg: + checker_tasks: set[asyncio.Task] = set() + for c in checkers: + if isinstance(c, BaseChecker): + c = c.run_checker() + task = tg.create_task(c) + checker_tasks.add(task) + while True: + if stopping: + if "self" in config.enabled_check_sets: + alert = checks.generate_stop_alert() + async with asyncio.TaskGroup() as tg: + tg.create_task(sender.send_alert(alert)) + tg.create_task(sender.send_healthchecks_status(alert)) + for c in checkers: + try: + await c.graceful_stop() + except AttributeError: + continue + if tg_client: + await tg_client.disconnect() + raise SystemExit + else: + await asyncio.sleep(3) From 6c8ae03b6a5cc580557a5a81da735ae0234b310b Mon Sep 17 00:00:00 2001 From: Alex Tau Date: Thu, 1 Jan 2026 16:49:13 +0300 Subject: [PATCH 53/56] stop if alert cannot be sent --- src/lego_monitoring/alerting/sender.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/lego_monitoring/alerting/sender.py b/src/lego_monitoring/alerting/sender.py index 0f17b8d..e7bd697 100644 --- a/src/lego_monitoring/alerting/sender.py +++ b/src/lego_monitoring/alerting/sender.py @@ -40,14 +40,14 @@ def format_message(alert: Alert, note: str) -> str: async def send_alert(alert: Alert, note: str = "") -> Success[None] | Failure[tenacity.RetryError]: - await log_errors_async(_send_alert(alert, note)) + return await log_errors_async(_send_alert(alert, note)) async def send_healthchecks_status(alert: Alert) -> Success[None] | Failure[tenacity.RetryError]: - await log_errors_async(_send_healthchecks_status(alert)) + return await log_errors_async(_send_healthchecks_status(alert)) -@tenacity.retry(wait=tenacity.wait_random_exponential(multiplier=1, max=60)) +@tenacity.retry(wait=tenacity.wait_random_exponential(multiplier=1, max=60), stop=tenacity.stop_after_attempt(3)) async def _send_alert(alert: Alert, note: str = "") -> None: logging.debug(f"Sending {alert.alert_type} alert to Telegram") tg_client = cvars.tg_client.get() @@ -57,7 +57,7 @@ async def _send_alert(alert: Alert, note: str = "") -> None: await tg_client.send_message(entity=room_id, message=message) -@tenacity.retry(wait=tenacity.wait_random_exponential(multiplier=1, max=60)) +@tenacity.retry(wait=tenacity.wait_random_exponential(multiplier=1, max=60), stop=tenacity.stop_after_attempt(3)) async def _send_healthchecks_status(alert: Alert) -> None: def get_pinging_key(keys: dict[str, str]): if alert.healthchecks_slug in keys: From 5cd3f47d658dba36e0eddefe32d8ea1a6169cc5a Mon Sep 17 00:00:00 2001 From: Alex Tau Date: Sun, 18 Jan 2026 15:33:56 +0300 Subject: [PATCH 54/56] add docs for ups --- README.md | 4 ++ docs/nixos-options.md | 45 ++++++++++++++++++++++- docs/ups.md | 85 +++++++++++++++++++++++++++++++++++++++++++ modules/options.nix | 1 + 4 files changed, 134 insertions(+), 1 deletion(-) create mode 100644 docs/ups.md diff --git a/README.md b/README.md index 4d642a8..fa48578 100644 --- a/README.md +++ b/README.md @@ -65,3 +65,7 @@ Then enable and start the service: ln -s /opt/lego-monitoring/lego-monitoring.service /etc/systemd/system/lego-monitoring.service systemctl enable --now lego-monitoring ``` + +### UPS monitoring + +See [docs/ups.md](docs/ups.md) for instructions. diff --git a/docs/nixos-options.md b/docs/nixos-options.md index bf01a66..a18d24a 100644 --- a/docs/nixos-options.md +++ b/docs/nixos-options.md @@ -36,12 +36,13 @@ List of enabled check sets\. Each check set is a module which checks something a - ram – alerts when RAM usage is above threshold - temp – alerts when temperature readings are above thresholds - net – alerts when network usage is above threshold + - ups – alerts on UPS events - vulnix – periodically scans system for known CVEs, alerts if any are found (NixOS only) *Type:* -list of (one of “self”, “remind”, “cpu”, “ram”, “temp”, “net”, “vulnix”) +list of (one of “self”, “remind”, “cpu”, “ram”, “temp”, “net”, “ups”, “vulnix”) @@ -635,6 +636,48 @@ null or (positive integer or floating point number, meaning >0) +## services\.lego-monitoring\.checks\.ups\.upsToCheck + + + +List of UPS’s to monitor, in ` upsc `-compatible format\. If null, all UPS’s connected to localhost are checked\. + + + +*Type:* +null or (list of string) + + + +*Default:* +` null ` + +*Declared by:* + - [modules/options\.nix](../modules/options.nix) + + + +## services\.lego-monitoring\.checks\.ups\.upsmonGroup + + + +Group to allow to send UPS status updates\. This should usually include the user upsmon runs as\. + + + +*Type:* +string + + + +*Default:* +` config.power.ups.upsmon.user ` + +*Declared by:* + - [modules/options\.nix](../modules/options.nix) + + + ## services\.lego-monitoring\.checks\.vulnix\.whitelist diff --git a/docs/ups.md b/docs/ups.md new file mode 100644 index 0000000..003401a --- /dev/null +++ b/docs/ups.md @@ -0,0 +1,85 @@ +# UPS monitoring + +Both steps require configuring upsmon at least to the point of outputting UPS updates to upsmon's logs. + +## NixOS + +NOTIFYCMD is set automatically. Make sure to set NOTIFYFLAGs to include EXEC for events that are to be reported. +The following snippet enables all events to be reported to wall, system's log and lego-monitoring: + +```nix +{ + power.ups.upsmon.settings.NOTIFYFLAG = (map (ntype: [ntype "SYSLOG+WALL+EXEC"]) [ + "ONLINE" + "ONBATT" + "LOWBATT" + "FSD" + "COMMOK" + "COMMBAD" + "SHUTDOWN" + "SHUTDOWN_HOSTSYNC" + "REPLBATT" + "NOCOMM" + "NOPARENT" + "CAL" + "NOTCAL" + "OFF" + "NOTOFF" + "BYPASS" + "NOTBYPASS" + "ECO" + "NOTECO" + "ALARM" + "NOTALARM" + "OVER" + "NOTOVER" + "TRIM" + "NOTTRIM" + "BOOST" + "NOTBOOST" + "OTHER" + "NOTOTHER" + "SUSPEND_STARTING" + "SUSPEND_FINISHED" + ]); +} +``` + +## Non-NixOS + +* NOTIFYCMD should be set to `/opt/lego-monitoring/.venv/bin/write-ups-status`. +* As above, NOTIFYFLAGs should include EXEC. Example for all events: + +``` +NOTIFYFLAG ONLINE SYSLOG+WALL+EXEC +NOTIFYFLAG ONBATT SYSLOG+WALL+EXEC +NOTIFYFLAG LOWBATT SYSLOG+WALL+EXEC +NOTIFYFLAG FSD SYSLOG+WALL+EXEC +NOTIFYFLAG COMMOK SYSLOG+WALL+EXEC +NOTIFYFLAG COMMBAD SYSLOG+WALL+EXEC +NOTIFYFLAG SHUTDOWN SYSLOG+WALL+EXEC +NOTIFYFLAG SHUTDOWN_HOSTSYNC SYSLOG+WALL+EXEC +NOTIFYFLAG REPLBATT SYSLOG+WALL+EXEC +NOTIFYFLAG NOCOMM SYSLOG+WALL+EXEC +NOTIFYFLAG NOPARENT SYSLOG+WALL+EXEC +NOTIFYFLAG CAL SYSLOG+WALL+EXEC +NOTIFYFLAG NOTCAL SYSLOG+WALL+EXEC +NOTIFYFLAG OFF SYSLOG+WALL+EXEC +NOTIFYFLAG NOTOFF SYSLOG+WALL+EXEC +NOTIFYFLAG BYPASS SYSLOG+WALL+EXEC +NOTIFYFLAG NOTBYPASS SYSLOG+WALL+EXEC +NOTIFYFLAG ECO SYSLOG+WALL+EXEC +NOTIFYFLAG NOTECO SYSLOG+WALL+EXEC +NOTIFYFLAG ALARM SYSLOG+WALL+EXEC +NOTIFYFLAG NOTALARM SYSLOG+WALL+EXEC +NOTIFYFLAG OVER SYSLOG+WALL+EXEC +NOTIFYFLAG NOTOVER SYSLOG+WALL+EXEC +NOTIFYFLAG TRIM SYSLOG+WALL+EXEC +NOTIFYFLAG NOTTRIM SYSLOG+WALL+EXEC +NOTIFYFLAG BOOST SYSLOG+WALL+EXEC +NOTIFYFLAG NOTBOOST SYSLOG+WALL+EXEC +NOTIFYFLAG OTHER SYSLOG+WALL+EXEC +NOTIFYFLAG NOTOTHER SYSLOG+WALL+EXEC +NOTIFYFLAG SUSPEND_STARTING SYSLOG+WALL+EXEC +NOTIFYFLAG SUSPEND_FINISHED SYSLOG+WALL+EXEC +``` diff --git a/modules/options.nix b/modules/options.nix index 82538c7..ad78efe 100644 --- a/modules/options.nix +++ b/modules/options.nix @@ -183,6 +183,7 @@ in upsmonGroup = lib.mkOption { type = lib.types.str; default = config.power.ups.upsmon.user; + defaultText = lib.literalExpression "config.power.ups.upsmon.user"; description = "Group to allow to send UPS status updates. This should usually include the user upsmon runs as."; }; }; From 8aa4c1d4dae6286f6a5b8122a6f3fe823a9940f8 Mon Sep 17 00:00:00 2001 From: Alex Tau Date: Sun, 18 Jan 2026 16:00:10 +0300 Subject: [PATCH 55/56] add ups event monitoring to description --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index fa48578..38fc625 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,7 @@ Simple system monitoring service. Sends alerts in Telegram and/or reports status to [Healthchecks](https://healthchecks.io/). Currently supports monitoring: * CPU/RAM/network usage * temperature readings +* UPS events * [vulnix](https://github.com/nix-community/vulnix) readings (NixOS only) ## Setup From ad1d956cc82c4d106f094f924f91a239836825a7 Mon Sep 17 00:00:00 2001 From: Alex Tau Date: Sun, 18 Jan 2026 16:02:59 +0300 Subject: [PATCH 56/56] update version in pyproject --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 4acf0bd..ad0fa88 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "lego-monitoring" -version = "1.1.1" +version = "1.2.0" description = "Monitoring software for the lego server" readme = "README.md" requires-python = ">=3.12"