aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--prometheus_borgmatic_exporter/__init__.py134
-rw-r--r--prometheus_borgmatic_exporter/cli.py143
-rw-r--r--prometheus_borgmatic_exporter/types.py2
-rw-r--r--pyproject.toml8
-rw-r--r--uv.lock76
5 files changed, 193 insertions, 170 deletions
diff --git a/prometheus_borgmatic_exporter/__init__.py b/prometheus_borgmatic_exporter/__init__.py
index 2cfcce5..8bfd629 100644
--- a/prometheus_borgmatic_exporter/__init__.py
+++ b/prometheus_borgmatic_exporter/__init__.py
@@ -89,14 +89,11 @@ def run_command(
Logs the command at debug level before execution. Exits the application
with the command's return code if it fails.
- Args:
- ctx: The Click context, injected by @click.pass_context.
- command: The command and its arguments as a list of strings.
-
- Returns:
- The completed process result with stdout and stderr captured.
+ :param ctx: Click context injected by :func:`click.pass_context`.
+ :param command: Command and its arguments.
+ :return: Completed process with stdout and stderr captured.
"""
- debug("Running command", " ".join(command))
+ debug("Running command", " ".join(command), err=True)
try:
return subprocess.run(
command, capture_output=True, encoding="utf-8", check=True
@@ -112,15 +109,12 @@ def run_command(
def load_json(ctx: click.Context, raw: str) -> Any:
"""Parse a JSON string and return the result.
- Exits the application with code 1 if the input is not valid JSON, printing
- the parse error via the error() helper.
-
- Args:
- ctx: The Click context, injected by @click.pass_context.
- raw: The raw JSON string to parse.
+ Exits the application with status code 1 if the input is not valid JSON and
+ reports the parsing error using :func:`error`.
- Returns:
- The parsed JSON value.
+ :param ctx: Click context injected by :func:`click.pass_context`.
+ :param raw: Raw JSON string to parse.
+ :return: Parsed JSON value.
"""
try:
return json.loads(raw)
@@ -132,15 +126,11 @@ def load_json(ctx: click.Context, raw: str) -> Any:
def extract_archive_counts(
repositories: list[types.BorgInfoReturnType] | list[types.BorgListReturnType],
) -> list[types.ArchiveCount]:
- """Extract archive counts and repository metadata from borgmatic info output.
+ """Extract archive counts and repository metadata from borgmatic output.
- Args:
- repositories: List of repository objects as returned by borgmatic info
- or borgmatic list.
-
- Returns:
- A list of ArchiveCount dicts, one per repository, containing the
- repository id, label, location, and total number of archives.
+ :param repositories: Repository objects returned by ``borgmatic info`` or
+ ``borgmatic list``.
+ :return: Archive counts and identifying metadata for each repository.
"""
return [
{
@@ -158,29 +148,24 @@ def fetch_repositories(
) -> tuple[list[types.BorgInfoReturnType], list[types.ArchiveCount]]:
"""Fetch repository and archive data from borgmatic.
- When all_archives is False, runs `borgmatic info --archive latest` and
- `borgmatic list` in complementary intent: info provides per-repository
- stats and the latest archive metrics, while list provides the total archive
- count — since info with --archive latest only returns a single archive,
- making len() unreliable for counting.
-
- When all_archives is True, only `borgmatic info` is run. The full archive
- list is available directly from the info output, so no separate list call
- is needed and archive counts are derived from it.
+ When ``all_archives`` is false, runs ``borgmatic info --archive latest``
+ and ``borgmatic list``. The former provides per-repository statistics and
+ metrics for the latest archive, while the latter provides the complete
+ archive list needed to determine archive counts.
- Args:
- borgmatic_bin: Path to the borgmatic executable.
- borgmatic_config: Path to the borgmatic configuration file.
- all_archives: If True, fetch and return metrics for all archives.
- This is significantly slower for large repositories.
+ When ``all_archives`` is true, only ``borgmatic info`` is run. Its output
+ already contains the complete archive list, so archive counts can be
+ derived directly from it.
- Returns:
- A tuple of:
- - repositories: Raw borgmatic info output, one entry per repository.
- - archive_counts: Archive count and identifying metadata per repository,
- always populated regardless of the all_archives flag.
+ :param borgmatic_bin: Path to the borgmatic executable.
+ :param borgmatic_configs: Paths to borgmatic configuration files or
+ directories.
+ :param all_archives: Whether to fetch metrics for all archives instead of
+ only the latest archive.
+ :return: Raw ``borgmatic info`` output and archive counts with identifying
+ metadata for each repository.
"""
- verbose("Fetching repositories")
+ verbose("Fetching repositories", err=True)
borgmatic_cmd = [str(borgmatic_bin)]
for config in borgmatic_configs:
@@ -193,7 +178,7 @@ def fetch_repositories(
repositories = cast(
list[types.BorgInfoReturnType], load_json(borgmatic_info.stdout)
)
- debug(f"Fetched {len(repositories)} repositories")
+ debug(f"Fetched {len(repositories)} repositories", err=True)
return repositories, extract_archive_counts(repositories)
info_command += ["--archive", "latest"]
@@ -201,7 +186,7 @@ def fetch_repositories(
repositories = cast(
list[types.BorgInfoReturnType], load_json(borgmatic_info.stdout)
)
- debug(f"Fetched {len(repositories)} repositories")
+ debug(f"Fetched {len(repositories)} repositories", err=True)
borgmatic_list = run_command([*borgmatic_cmd, "list", "--json"])
archives = cast(list[types.BorgListReturnType], load_json(borgmatic_list.stdout))
@@ -211,16 +196,14 @@ def fetch_repositories(
def collect_repository_metrics(
repository: types.BorgInfoReturnType, all_archives: bool
) -> None:
- """Collect and set all Prometheus metrics for a single repository.
+ """Collect Prometheus metrics for a repository.
- Sets cache stats, encryption info, last modified timestamp, and latest
- archive metrics. When all_archives is True, also collects metrics for every
- individual archive by calling _collect_archive_metrics.
+ Sets cache statistics, encryption information, the last-modified timestamp,
+ and metrics for the latest archive. When ``all_archives`` is true, also
+ collects metrics for every individual archive.
- Args:
- repository: Repository info as returned by borgmatic.
- all_archives: If True, collect metrics for all archives, not just
- the latest.
+ :param repository: Repository information returned by borgmatic.
+ :param all_archives: Whether to collect metrics for every archive.
"""
repository_metadata = repository["repository"]
@@ -229,6 +212,7 @@ def collect_repository_metrics(
repository_metadata["label"],
f"({repository_metadata['id']})",
repository_metadata["location"],
+ err=True,
)
repository_labels: types.RepositoryLabels = {
@@ -260,11 +244,15 @@ def collect_repository_metrics(
metric.labels(**repository_labels).set(repository_stats[key])
if not repository["archives"]:
- warn("No archives found for repository:", repository_metadata["label"])
+ warn(
+ "No archives found for repository:", repository_metadata["label"], err=True
+ )
return
latest_archive = max(repository["archives"], key=lambda archive: archive["start"])
- debug(f"Latest archive {latest_archive['name']}:", latest_archive["start"])
+ debug(
+ f"Latest archive {latest_archive['name']}:", latest_archive["start"], err=True
+ )
metrics.LATEST_ARCHIVE_DURATION.labels(**repository_labels).set(
latest_archive["duration"]
@@ -287,14 +275,12 @@ def collect_repository_metrics(
def collect_archive_metrics(
archive: types.BorgArchive, repository_labels: types.RepositoryLabels
) -> None:
- """Collect and set Prometheus metrics for a single archive.
+ """Collect Prometheus metrics for an archive.
- Args:
- archive: Archive info as returned by borgmatic.
- repository_labels: The parent repository's label dict, used to
- populate the shared label dimensions on each metric.
+ :param archive: Archive information returned by borgmatic.
+ :param repository_labels: Labels inherited from the parent repository.
"""
- msg("Processing archive:", archive["name"], f"({archive['id']})")
+ msg("Processing archive:", archive["name"], f"({archive['id']})", err=True)
archive_labels = {
**repository_labels,
@@ -406,15 +392,12 @@ def prometheus_borgmatic_exporter(
"""Collect borgmatic repository metrics and export them for Prometheus.
Queries borgmatic for repository and archive statistics and writes them to
- a .prom file for consumption by the Prometheus node exporter textfile
+ a ``.prom`` file for consumption by the Prometheus node exporter textfile
collector.
- By default only the latest archive is queried per repository. Use
- --all-archives to collect metrics for every archive, at the cost of a
- significantly longer runtime.
-
- Using --all-archives will create one time series per archive and may
- significantly increase Prometheus storage usage.
+ By default, only the latest archive is queried for each repository. Use
+ ``--all-archives`` to collect metrics for every archive, at the cost of a
+ significantly longer runtime and increased Prometheus storage usage.
"""
ctx.ensure_object(dict)
@@ -422,17 +405,18 @@ def prometheus_borgmatic_exporter(
ctx.obj["quiet"] = quiet_flag
ctx.obj["verbose"] = verbose_flag
- debug("Effective configuration:")
- debug(" borgmatic_bin:", str(borgmatic_bin))
+ debug("Effective configuration:", err=True)
+ debug(" borgmatic_bin:", str(borgmatic_bin), err=True)
debug(
" borgmatic_configs:",
", ".join(str(config) for config in borgmatic_configs)
or "<borgmatic defaults>",
+ err=True,
)
- debug(" textfile_collector_dir:", str(textfile_collector_dir))
- debug(" all_archives:", str(all_archives_flag))
- debug(" verbose:", str(verbose_flag))
- debug(" quiet:", str(quiet_flag))
+ debug(" textfile_collector_dir:", str(textfile_collector_dir), err=True)
+ debug(" all_archives:", str(all_archives_flag), err=True)
+ debug(" verbose:", str(verbose_flag), err=True)
+ debug(" quiet:", str(quiet_flag), err=True)
repositories, archive_counts = fetch_repositories(
borgmatic_bin, borgmatic_configs, all_archives_flag
@@ -450,7 +434,7 @@ def prometheus_borgmatic_exporter(
metrics_file = textfile_collector_dir / "borgmatic.prom"
write_to_textfile(str(metrics_file.absolute()), metrics.REGISTRY)
- msg("Metrics written to:", str(metrics_file.absolute()))
+ msg("Metrics written to:", str(metrics_file.absolute()), err=True)
if __name__ == "__main__":
diff --git a/prometheus_borgmatic_exporter/cli.py b/prometheus_borgmatic_exporter/cli.py
index cc6f374..5d91b05 100644
--- a/prometheus_borgmatic_exporter/cli.py
+++ b/prometheus_borgmatic_exporter/cli.py
@@ -2,6 +2,12 @@
#
# SPDX-License-Identifier: BSD-3-Clause
+"""Define Click parameter types and styled terminal message helpers."""
+
+from collections.abc import Callable
+from functools import update_wrapper
+from typing import Any, Concatenate
+
import click_extra as click
@@ -11,22 +17,40 @@ class FlexibleColorOption(click.ColorOption):
_gnu_optional_value = False
-def emit(prefix: str, color: str, *message: str, enabled: bool = True) -> None:
- """Render a styled prefix and message to stdout.
+def pass_obj_silent[T, **P, R](
+ f: Callable[Concatenate[T | None, P], R],
+) -> Callable[P, R]:
+ """Pass the current context object to a callback, if available.
+
+ The current Click context is retrieved silently. If no context exists or
+ its object is not a dictionary, ``None`` is passed instead.
+
+ The context object is inserted as the first positional argument to *f*.
+ """
- Internal helper used by error(), msg(), warn(), verbose(), and debug()
- to avoid duplicating the Click styling and echo logic. The first message
- argument is rendered in bold, subsequent arguments are appended unstyled.
+ def new_func(*args: P.args, **kwargs: P.kwargs) -> R:
+ ctx = click.get_current_context(silent=True)
+ obj = ctx.obj if ctx is not None else None
+ return f(obj, *args, **kwargs)
- Args:
- prefix: The prefix string shown before the message (e.g. "==>", "==> ERROR:").
- color: A Click-compatible color name (e.g. "red", "green") applied to the prefix.
- *message: One or more message parts to display. At least one is required.
- enabled: If False, the function returns immediately without printing.
- Defaults to True.
+ return update_wrapper(new_func, f)
- Raises:
- TypeError: If no message arguments are provided.
+
+def emit(
+ prefix: str, color: str, *message: str, enabled: bool = True, err: bool = False
+) -> None:
+ """Render a styled prefix and message to stdout.
+
+ This helper is used by :func:`error`, :func:`msg`, :func:`warn`,
+ :func:`verbose`, and :func:`debug`. The first message argument is rendered in
+ bold and subsequent arguments are appended unstyled.
+
+ :param prefix: Prefix displayed before the message.
+ :param color: Click-compatible color name applied to the prefix.
+ :param message: One or more message parts to display.
+ :param enabled: Whether the message should be emitted.
+ :param err: Write to `stderr` instead of `stdout`.
+ :raises TypeError: If no message arguments are provided.
"""
if not message:
raise TypeError("emit() missing 1 required positional argument: 'message'")
@@ -41,76 +65,97 @@ def emit(prefix: str, color: str, *message: str, enabled: bool = True) -> None:
click.style(message[0], bold=True),
*message[1:],
]
- )
+ ),
+ err=err,
)
def error(*message: str) -> None:
"""Print a formatted error message to stdout in red.
- The first argument is rendered in bold, subsequent arguments are appended
- unstyled. Always prints regardless of quiet or verbose flags.
+ The first argument is rendered in bold and subsequent arguments are appended
+ unstyled. Error messages are always printed regardless of quiet or verbose
+ flags.
- Args:
- *message: One or more message parts to display.
+ :param message: One or more message parts to display.
"""
- emit("==> ERROR:", "red", *message)
+ emit("==> ERROR:", "red", *message, err=True)
-@click.pass_obj
-def msg(obj: dict[str, bool], *message: str) -> None:
+@pass_obj_silent
+def msg(obj: dict[str, Any] | None, *message: str, err: bool = False) -> None:
"""Print a formatted informational message to stdout in green.
- Suppressed when the quiet flag is set. The first argument is rendered
- in bold, subsequent arguments are appended unstyled.
+ The message is suppressed when the quiet flag is set. When called outside a
+ Click context, the message is emitted normally. The first argument is rendered
+ in bold and subsequent arguments are appended unstyled.
- Args:
- *message: One or more message parts to display.
+ :param message: One or more message parts to display.
"""
- emit("==>", "green", *message, enabled=not obj.get("quiet", False))
+ emit(
+ "==>",
+ "green",
+ *message,
+ enabled=obj is None or not obj.get("quiet", False),
+ err=err,
+ )
-@click.pass_obj
-def warn(obj: dict[str, bool], *message: str) -> None:
+@pass_obj_silent
+def warn(obj: dict[str, Any] | None, *message: str, err: bool = False) -> None:
"""Print a formatted warning message to stdout in yellow.
- Suppressed when the quiet flag is set. The first argument is rendered
- in bold, subsequent arguments are appended unstyled.
+ The message is suppressed when the quiet flag is set. When called outside a
+ Click context, the message is emitted normally. The first argument is rendered
+ in bold and subsequent arguments are appended unstyled.
- Args:
- *message: One or more message parts to display.
+ :param message: One or more message parts to display.
"""
- emit("==>", "yellow", *message, enabled=not obj.get("quiet", False))
+ emit(
+ "==>",
+ "yellow",
+ *message,
+ enabled=obj is None or not obj.get("quiet", False),
+ err=err,
+ )
-@click.pass_obj
-def verbose(obj: dict[str, bool], *message: str) -> None:
+@pass_obj_silent
+def verbose(obj: dict[str, Any] | None, *message: str, err: bool = False) -> None:
"""Print a formatted verbose message to stdout in blue.
- Only prints when the verbose flag is set and the quiet flag is not.
- The first argument is rendered in bold, subsequent arguments are
- appended unstyled.
+ The message is printed only when called from a Click context with the verbose
+ flag set and the quiet flag unset. Calls made outside a Click context are a
+ no-op.
- Args:
- *message: One or more message parts to display.
+ :param message: One or more message parts to display.
"""
emit(
"==>",
"blue",
*message,
- enabled=obj.get("verbose", False) and not obj.get("quiet", False),
+ enabled=(
+ obj is not None
+ and obj.get("verbose", False)
+ and not obj.get("quiet", False)
+ ),
+ err=err,
)
-@click.pass_obj
-def debug(obj: dict[str, bool], *message: str) -> None:
+@pass_obj_silent
+def debug(obj: dict[str, Any] | None, *message: str, err: bool = False) -> None:
"""Print a formatted debug message to stdout in magenta.
- Only prints when the DEBUG environment variable is set to a truthy
- value (1, true, yes). The first argument is rendered in bold,
- subsequent arguments are appended unstyled.
+ The message is printed only when called from a Click context with the debug
+ flag enabled. Calls made outside a Click context are a no-op.
- Args:
- *message: One or more message parts to display.
+ :param message: One or more message parts to display.
"""
- emit("==>", "magenta", *message, enabled=obj.get("debug", False))
+ emit(
+ "==>",
+ "magenta",
+ *message,
+ enabled=obj is not None and obj.get("debug", False),
+ err=err,
+ )
diff --git a/prometheus_borgmatic_exporter/types.py b/prometheus_borgmatic_exporter/types.py
index 043bf25..8b82fbe 100644
--- a/prometheus_borgmatic_exporter/types.py
+++ b/prometheus_borgmatic_exporter/types.py
@@ -1,7 +1,7 @@
-"""TypedDict definitions for borgmatic JSON output structures."""
# SPDX-FileCopyrightText: 2026 Dennis Fink <me+coding@dennisfink.me>
#
# SPDX-License-Identifier: BSD-3-Clause
+"""TypedDict definitions for borgmatic JSON output structures."""
from typing import Any, TypedDict
diff --git a/pyproject.toml b/pyproject.toml
index 8ed54aa..74d5892 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -8,7 +8,7 @@ authors = [
{name = "Dennis Fink", email = "me+coding@dennisfink.me"},
]
readme = "README.md"
-requires-python = ">=3.11"
+requires-python = ">=3.12"
dependencies = [
"click-extra>=9.1.0",
"prometheus-client>=0.26.0",
@@ -59,11 +59,5 @@ include = [
"pyproject.toml"
]
-[tool.ruff]
-target-version = "py311"
-
[tool.ruff.format]
skip-magic-trailing-comma = true
-
-[tool.pyright]
-pythonVersion = "3.11"
diff --git a/uv.lock b/uv.lock
index bf7da4d..f3c6629 100644
--- a/uv.lock
+++ b/uv.lock
@@ -1,14 +1,14 @@
version = 1
revision = 3
-requires-python = ">=3.11"
+requires-python = ">=3.12"
[[package]]
name = "boltons"
-version = "26.1.0"
+version = "26.2.0"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/47/99/12bace94ae2ba961bdc46d49277ff15d38dba074bc3987b0c0b4355a37a7/boltons-26.1.0.tar.gz", hash = "sha256:5764468aba493b15995ed17f46a16789023f123ca2a62d491a9ce825c1cbe26c", size = 227393, upload-time = "2026-07-17T18:38:42.454Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/71/56/14c4a4931910a81ddeccfbe227925ea738e3c445d3e2af960f0bcbba1616/boltons-26.2.0.tar.gz", hash = "sha256:d39cfd15c1a1c3bd4d705c82252fa9edb8e4f5e8cc039f8e39afac7b1b47e92c", size = 242047, upload-time = "2026-09-07T10:48:17.711Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/e9/ba/cea118a16647cc72633c5236f6f11a09ab76b136b1aaccfa0d7004958428/boltons-26.1.0-py3-none-any.whl", hash = "sha256:1d966b165805b83600b31af9f0db672e3b3313d9de438e22708a94ac5f4c93de", size = 196095, upload-time = "2026-07-17T18:38:41.164Z" },
+ { url = "https://files.pythonhosted.org/packages/d2/ac/1f8e4f4855e382a67392ca3bba329cb93aba1e3039289c52a86bf337f7a2/boltons-26.2.0-py3-none-any.whl", hash = "sha256:41942cbce440211bbcf2487ccdc356964ad674f95a01068b8a0dd7a1d1fed036", size = 199536, upload-time = "2026-09-07T10:48:16.319Z" },
]
[[package]]
@@ -31,7 +31,7 @@ wheels = [
[[package]]
name = "click-extra"
-version = "9.1.0"
+version = "9.3.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "boltons" },
@@ -43,21 +43,21 @@ dependencies = [
{ name = "wcmatch" },
{ name = "wcwidth" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/a3/b9/40f42f760ed0d4ce0e445cec909d252c88f8a165993551aa2eec7699b030/click_extra-9.1.0.tar.gz", hash = "sha256:68e7dbf80500deb369794ab1ef44ae1713d0bb64698072b0ba6f28fbdea98084", size = 1608251, upload-time = "2026-09-03T09:16:23.328Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/8c/6d/87ab37039564ac7490847e557056284e3bab7a09d89ff1b55089585b39e4/click_extra-9.3.2.tar.gz", hash = "sha256:2dd33e2c3700c3695b89d24640d7a584e009faf2e18dd8545861b5f3e88d0e18", size = 1679919, upload-time = "2026-09-19T14:10:39.267Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/73/8d/663be1cb83ef91f97b49529f6a91e2b2cedcfe2d2c874d61dee6f6406585/click_extra-9.1.0-py3-none-any.whl", hash = "sha256:b385b0d5d9eb05fd1a7795642bb577a1cd557b6a793351b7869ca1ed57ae30b5", size = 610197, upload-time = "2026-09-03T09:16:21.606Z" },
+ { url = "https://files.pythonhosted.org/packages/b9/84/66f8bc45f1d7bad1b9afcb4f4adffb8dd8f459c990509687956cfa47c425/click_extra-9.3.2-py3-none-any.whl", hash = "sha256:4e6b034a9b3099ab31716a99b05161f28ee735a53f6c6801c0dca936812c797a", size = 647496, upload-time = "2026-09-19T14:10:37.368Z" },
]
[[package]]
name = "cloup"
-version = "3.1.0"
+version = "4.0.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "click" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/42/ca/cf02e965cfeb70d65c61fd3abb8022aaf5111a0de71b3c73a6ec2113aa25/cloup-3.1.0.tar.gz", hash = "sha256:637c1e628fe98f3f20a5e44da591a72b42bf54d7d4527190bf39ed5f64af7585", size = 230167, upload-time = "2026-05-26T02:48:18.11Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/05/e2/d41446c6195eff0db3b671ddb202e39f42f9ea7c0dd15cd43fbf5cf0d7f7/cloup-4.0.0.tar.gz", hash = "sha256:83b0870ee863bcc85129e40e1b208bcfdebe4cd2142e9ce1d0daf7d276cab038", size = 252135, upload-time = "2026-09-12T03:23:39.698Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/21/07/644976263e2d346935b35305908bf89eb660ced7fbc7292b096f38c0a80d/cloup-3.1.0-py2.py3-none-any.whl", hash = "sha256:f4cfbdcdc96d30bcbd8c75eaffb0651e9c57180f2aee4b300cf0dd168b8415ed", size = 55028, upload-time = "2026-05-26T02:48:16.711Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/34/20224fd9d52e8ce7e3b319b5d6b7126da25e89d00cd1a5692b4b46ed2d26/cloup-4.0.0-py3-none-any.whl", hash = "sha256:0678744a3a2753e1d78dd3ea5c6e4cbbc3b41561fd7a7a7d075dbc8dad74f74f", size = 55776, upload-time = "2026-09-12T03:23:38.564Z" },
]
[[package]]
@@ -71,11 +71,11 @@ wheels = [
[[package]]
name = "extra-platforms"
-version = "13.7.1"
+version = "13.10.1"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/ae/7d/507e27b623cbc3a1d81b2a5ee9f1d94cd4151c605986396b41b97f20d804/extra_platforms-13.7.1.tar.gz", hash = "sha256:9c0056a60c89097e531745b3925a717aa4719cd3d8acd48f7edb8f9de7969079", size = 478006, upload-time = "2026-09-03T15:34:43.288Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/d4/10/5803927617f21359f940fe611db04ead99b0fee23b0be3300d0e4094ebb5/extra_platforms-13.10.1.tar.gz", hash = "sha256:adada8aadf654b92964b369c7fb5c548467fc1616ef15f6c5943ec0141efa96d", size = 484544, upload-time = "2026-09-19T11:32:28.146Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/78/95/4cb5e2e3c3c28ffeeeda086129047eabb7d5aac992db7cffb07e52c98a27/extra_platforms-13.7.1-py3-none-any.whl", hash = "sha256:e5255107fa18eb2fe63e88c267c0ef361cb4ff051182431cc35bc2f7b9d19962", size = 103240, upload-time = "2026-09-03T15:34:41.81Z" },
+ { url = "https://files.pythonhosted.org/packages/34/92/6164146cb8572c9bdc85763410624776109bff33a3201dd53f197963f48f/extra_platforms-13.10.1-py3-none-any.whl", hash = "sha256:22c67b0a4a84bb1d123b0884cb93c6928159239f4df313fec63bc6adcb05fa32", size = 104594, upload-time = "2026-09-19T11:32:26.508Z" },
]
[[package]]
@@ -124,40 +124,40 @@ wheels = [
[[package]]
name = "pyright"
-version = "1.1.411"
+version = "1.1.414"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "nodeenv" },
{ name = "typing-extensions" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/7e/ab/265f7dc69d28113ebba19092e57b075f41543b2ed048429c5f56e2b88eac/pyright-1.1.411.tar.gz", hash = "sha256:d885a0551f2e763b089a02702174e7f4ba77548cddabc972ab86d1f7f1b0f998", size = 4112861, upload-time = "2026-06-25T02:14:06.37Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/e1/1b/244c7b710031ada80f27e579ec20d28a2285dfc318fed0339866b1047f12/pyright-1.1.414.tar.gz", hash = "sha256:523c0a97c60da6333234955c277730c9cf4f5bd6d5399e7b7d2b0fc5d3599524", size = 4154638, upload-time = "2026-09-10T12:26:53.181Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/0a/49/385be530a6a5b78d1cbcd5c2e38debc8959a2fc6bdb716f4e581002979fc/pyright-1.1.411-py3-none-any.whl", hash = "sha256:dc7c72a8e2700c55baa127554040e067041ea53ccfd50bf96308cc4291c7d5d9", size = 6181526, upload-time = "2026-06-25T02:14:04.691Z" },
+ { url = "https://files.pythonhosted.org/packages/d7/ba/18b6e682ead424ad24bcc134339ae5d1b931cd9ae260540592a058a91279/pyright-1.1.414-py3-none-any.whl", hash = "sha256:2a6b4b3298c9eec174c5ed83bd338de6eee82df2992f3e1930e6199d381be36f", size = 6225049, upload-time = "2026-09-10T12:26:51.427Z" },
]
[[package]]
name = "ruff"
-version = "0.16.6"
+version = "0.16.8"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/a4/7c/6adb35d70e7c027e308274557901c7e00fb3407750faf3620c184ae058cb/ruff-0.16.6.tar.gz", hash = "sha256:dcf8a73d2ff77e99dde91244b4da16feba7f14e6beeb4015dee7c5a909e99050", size = 4921251, upload-time = "2026-09-03T16:57:29.037Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/ba/78/449cb84790bd5cc3823b2652ee405a4558856e5c4195aee3a16bf7b3eb5d/ruff-0.16.8.tar.gz", hash = "sha256:9247bf92b5f04d825c8639a4fe423ec2e4222acd9222e58412b0dab7e442798b", size = 4938814, upload-time = "2026-09-16T15:54:46.688Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/a4/28/9cc1b79639e284ec103f43c88c644db4eb58cbd0ea1ca11f1193435369ac/ruff-0.16.6-py3-none-linux_armv6l.whl", hash = "sha256:61c368c26bf8e973e5ab14a2772de587bc068ea3f9a277f673380749b4898fb8", size = 10015638, upload-time = "2026-09-03T16:56:40.986Z" },
- { url = "https://files.pythonhosted.org/packages/71/11/627d342ef727ea7794edf74fe23d60a074b02c3acc2e9436684e782286ca/ruff-0.16.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ecf4f068e2e123e43a26e9db4e19524cc56563912404e83bbfca375757e45a32", size = 10220762, upload-time = "2026-09-03T16:56:44.681Z" },
- { url = "https://files.pythonhosted.org/packages/43/d9/b75668ce41e4c8d073d18d6d08672ba6906ce45d5c06ea4fdb2e84ce3853/ruff-0.16.6-py3-none-macosx_11_0_arm64.whl", hash = "sha256:99b62ea33baf130f50368798d841f0d95527b6d817bf31817b65dd058f1d314c", size = 9835082, upload-time = "2026-09-03T16:56:47.142Z" },
- { url = "https://files.pythonhosted.org/packages/99/97/123ab10b05cde889c107c20f5a9774955104b5552796a2a8584b089ae8eb/ruff-0.16.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7fbf89013f2bb3f6835a6038ff658dc8a1b38c98dc8e724b964168ad4e881876", size = 9949304, upload-time = "2026-09-03T16:56:49.813Z" },
- { url = "https://files.pythonhosted.org/packages/3e/58/a4a2c59dd2e5b85929c912d9cac3056eb9ee8c7e75e9b9fe3e109174966b/ruff-0.16.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:56a67065e22efa6bc4d498299d3bb06c0c90aace8fac2068b5a12f9dc4d8d51d", size = 9840612, upload-time = "2026-09-03T16:56:52.368Z" },
- { url = "https://files.pythonhosted.org/packages/61/6a/ff8c8626a786c4f49d48ced4a752dadbca65f5263005f9c2416578194694/ruff-0.16.6-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e25cc89174874b176a157e4428d66761c2c0c006654419bf384f967f361ff1b1", size = 10543465, upload-time = "2026-09-03T16:56:55.089Z" },
- { url = "https://files.pythonhosted.org/packages/ad/bb/c47535923365f337b82e28192e4e9eef2176511007cfd99a62fc22df5dad/ruff-0.16.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0700580ed5303723cb3c11c2f1d2a8913ce77b7ea86646dddb887f5417a9ba70", size = 11267576, upload-time = "2026-09-03T16:56:57.791Z" },
- { url = "https://files.pythonhosted.org/packages/ba/50/e5119a5212b5cd63b51e1f4b25e7bd636a6668fc069a3160b108ad7e3c16/ruff-0.16.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:15f1d0b6e165a6e56567befb6629f8209271311d990bae0f37e6d065035ef5f3", size = 10781993, upload-time = "2026-09-03T16:57:00.666Z" },
- { url = "https://files.pythonhosted.org/packages/8b/98/083d8b4ef3c51a0d19db84367791cbe9f44e4b53343d19dfa83556e1cd9a/ruff-0.16.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d72c591a96986ee4268860e2b7235082129ca5e4cb9cbba653a4b57c11893757", size = 10317748, upload-time = "2026-09-03T16:57:03.428Z" },
- { url = "https://files.pythonhosted.org/packages/9a/29/68f7ff2c5ad95f19f00627ac2de95644e25fe47371ea60b2db1fd952315e/ruff-0.16.6-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:65a006baa18f33324325814c864daef03541d51564b98c517610ea756ab7003e", size = 10540096, upload-time = "2026-09-03T16:57:06.182Z" },
- { url = "https://files.pythonhosted.org/packages/c4/f9/79a8f6de85968641d68a7863aeec577551924ef066a990a48ff93167beab/ruff-0.16.6-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:cd02a7bf1a21a8735228a3e8c95a9dc5cf86bd2a52194f4aaae2a5755b4de0f4", size = 10100494, upload-time = "2026-09-03T16:57:09.194Z" },
- { url = "https://files.pythonhosted.org/packages/d9/e8/b81a22d9b90c00b892ccf2fa2ac36fa95de4c13ab85aea3e73795cfe4651/ruff-0.16.6-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:31b36f1e5ad85e0737f09d2be4e512e2e283583c14015da3b9dc07359ac0fc88", size = 9843663, upload-time = "2026-09-03T16:57:12.168Z" },
- { url = "https://files.pythonhosted.org/packages/39/aa/54f516ec5e5a11c4afdceb1c454ebb054ffb96e4f4a1705580b4346abd35/ruff-0.16.6-py3-none-musllinux_1_2_i686.whl", hash = "sha256:61029b4ab4aa723fd3064fab96b1d814492596bf0c792679fffcbde1e1679953", size = 10282461, upload-time = "2026-09-03T16:57:15.077Z" },
- { url = "https://files.pythonhosted.org/packages/52/0b/38d0aa8aa32372b96dc44f97b22e576c4147808271aab7b2cb1e353d4445/ruff-0.16.6-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:9ac8998457832c2061709d900856b7ad271dace0cb41f346588d540162bfa718", size = 10728808, upload-time = "2026-09-03T16:57:17.797Z" },
- { url = "https://files.pythonhosted.org/packages/5e/e5/9e274e24eeb027640ffc7442f21239f16d17f47acec15ae34f32e03a5c79/ruff-0.16.6-py3-none-win32.whl", hash = "sha256:0b87d9d16fcb63e8018423ca1d50b7260f15cb2da33e30db4baad4183a948c25", size = 10049212, upload-time = "2026-09-03T16:57:20.55Z" },
- { url = "https://files.pythonhosted.org/packages/22/31/72472449414223ed1a2da236b992adbb1a2ae59e34794574810f60ce068e/ruff-0.16.6-py3-none-win_amd64.whl", hash = "sha256:10d21c51c3495d8eaea7b703a16592117ea6eb1d649e36335aa965ff1173eb39", size = 10556402, upload-time = "2026-09-03T16:57:23.501Z" },
- { url = "https://files.pythonhosted.org/packages/fc/07/d781f8f8e1ac24bef9f3269cf62ffb1407ca24c3a8f12e5e22874f90528c/ruff-0.16.6-py3-none-win_arm64.whl", hash = "sha256:7a976c79b958f94e50a022a19f0f8c87387448020935ec14fc74331bd0a7f2c5", size = 10412850, upload-time = "2026-09-03T16:57:26.416Z" },
+ { url = "https://files.pythonhosted.org/packages/ac/25/6071aabc530e9be7e2c195e8fe3f7aea2735405b6cf447212832d7811831/ruff-0.16.8-py3-none-linux_armv6l.whl", hash = "sha256:6ffbd6d87383c1edf5f6fa890f10200950240d7c1a16052a19a09d3a2307dd38", size = 10048966, upload-time = "2026-09-16T15:53:57.605Z" },
+ { url = "https://files.pythonhosted.org/packages/54/98/07f90ecbc74dd5fb5764f11f2bc774d6a7cffef92d2ff5f5b4e9e23c754e/ruff-0.16.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:42ed6b878ed61e3acca92f2730a17acff39286944ea82398544696366a6f925e", size = 10165498, upload-time = "2026-09-16T15:54:01.14Z" },
+ { url = "https://files.pythonhosted.org/packages/fe/1f/e6a712e3b47cad4a40600134105ed193cb773f618a42eb7ba323cb812cc0/ruff-0.16.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:7ea781c7f2afba8c6a505ea0fb3f994020249e0c450635f5381286fea6b46170", size = 9830004, upload-time = "2026-09-16T15:54:03.998Z" },
+ { url = "https://files.pythonhosted.org/packages/23/f2/311a08776d75d81c7676e20b6b020ae63cbe881fcdc7a8dd64e6e18bdd93/ruff-0.16.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8efeae3bbe414a5efefda11a792dfb51ef90ac48d50c4830de2f644caf3e8659", size = 9986558, upload-time = "2026-09-16T15:54:06.804Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/ed/37b6cb3d3ba8c73e68ae3eb1d502383beb5aa05a582bb7bb3a922f929f54/ruff-0.16.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a79b795469fef7fc6e908b218eed2eb17332afd85031db6480dc864560e69b2", size = 9877332, upload-time = "2026-09-16T15:54:09.552Z" },
+ { url = "https://files.pythonhosted.org/packages/22/cc/40873a8f36ad084cc540d55fcca7077264d5b13b24659e9180c176fb2b08/ruff-0.16.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3fdc5563cdc50555e6fba39322850860e9267c1b3d12c26a74729d8604c3c812", size = 10507125, upload-time = "2026-09-16T15:54:12.152Z" },
+ { url = "https://files.pythonhosted.org/packages/c3/e4/fc91a642b78ccbab6b9477720f3644ae7a10a9bcce69a934679cd64f62bc/ruff-0.16.8-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:34508983c70665578dab88f5223d8e6228307e1135398ca8bfc8b7e9501e282b", size = 11336694, upload-time = "2026-09-16T15:54:15.489Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/3d/bbd2a9a600a4e73dc3e7548a249c8d1671273464b55822c6fae50f602dff/ruff-0.16.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:644bb578569e0ffc575741232bd385dacdd6fbe123f1a729e7a225f54aa3957f", size = 10774448, upload-time = "2026-09-16T15:54:18.16Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/41/d83af9879a7b6e8bf5fe16b1da0b134049d2f5d3afac12defb0897cb84bd/ruff-0.16.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15e7d226246961db9235098333caa13063906d3851136b84c2900b82f5daa1df", size = 10323796, upload-time = "2026-09-16T15:54:20.743Z" },
+ { url = "https://files.pythonhosted.org/packages/f5/2c/cefd07bfe914b84943ea769ade8d607bd22750b965d3228eefd7cebd15d0/ruff-0.16.8-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:a2bf6bc3e9ebdd4449abc6f06cf64b98051a2c61cf94d2fe9596518c881f1a1e", size = 10514115, upload-time = "2026-09-16T15:54:23.497Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/9d/76a2e26c79a23be6e6e3664c57bec9e9fc8de155cfb9e4b67ea91b64f9d7/ruff-0.16.8-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:6ca111ba0849539165e9e59d2b442542f3c1e8060ebbdea82494f1ffbccb1e1f", size = 10072582, upload-time = "2026-09-16T15:54:26.185Z" },
+ { url = "https://files.pythonhosted.org/packages/2e/d4/f42edddb39668af1a559ceafa3823aedd65633a48dc9768e775485faa2c1/ruff-0.16.8-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:359a1e5b495448ee1e91018064382ebc86f90e8aac2fed222c7d0e4e8df85fd2", size = 9879644, upload-time = "2026-09-16T15:54:29.278Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/d4/913e3195d95e0378786c6656945c865f534a3560e29139da4882aff630d1/ruff-0.16.8-py3-none-musllinux_1_2_i686.whl", hash = "sha256:59e8f5681349474110b24d62e93cfda6593f5fa3473446ca3705200cac1a08b9", size = 10231569, upload-time = "2026-09-16T15:54:32.036Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/c4/8aa6ea0bdcedbd1bf87397e2fc4ed8406448ea5842f8660bc6e5f163039d/ruff-0.16.8-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:efa3e7a16d1baaa79957888dfdf8be9ef2e44db81cb032af06d76632ab59e773", size = 10663666, upload-time = "2026-09-16T15:54:34.838Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/02/7f10ef4700bc223c30a3fdd10631a29830c45524b810a3c7ed947af64591/ruff-0.16.8-py3-none-win32.whl", hash = "sha256:55793ba85c69921e89be061426d91a78652d6e50317c962240922747a4eb713f", size = 10093472, upload-time = "2026-09-16T15:54:37.47Z" },
+ { url = "https://files.pythonhosted.org/packages/1e/5d/a509c07d714b6da88f2c518b4637cf6f1d46b074be8f0f1e5fb9ff5126fe/ruff-0.16.8-py3-none-win_amd64.whl", hash = "sha256:a6b85621fd3c81e31fc5f5add09c9c078b430db3595ca632efafdec9e64ebfaa", size = 10586899, upload-time = "2026-09-16T15:54:40.488Z" },
+ { url = "https://files.pythonhosted.org/packages/fe/a0/50787329e4f20bf9dc9f6230015d46ec69c51a97ace5bc202dae4755365d/ruff-0.16.8-py3-none-win_arm64.whl", hash = "sha256:d075e820af612102ce217f07cc93e69f9490b10ec13ea85fa87bd03d996cef8a", size = 10386316, upload-time = "2026-09-16T15:54:43.332Z" },
]
[[package]]
@@ -197,9 +197,9 @@ wheels = [
[[package]]
name = "wcwidth"
-version = "0.8.3"
+version = "0.8.4"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/36/57/ed58088fafdf4c55a0ad6bde846502567645424d7ebf325230b9237f4085/wcwidth-0.8.3.tar.gz", hash = "sha256:d128512515fbf4612e0ff21fd6380399210318b7b54a9af59dff8454cf9730eb", size = 1458450, upload-time = "2026-08-28T18:10:06.875Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/3d/7a/f98d4ada7c499565ab0c0fcef28a4e54fafa72b8228a6309803c80493c92/wcwidth-0.8.4.tar.gz", hash = "sha256:2dae09efa25253ae2874188e86d6861af3b1652aef4118cdf3f0bda288a957fb", size = 414609, upload-time = "2026-09-17T23:31:45.287Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/c4/0e/57f6bb3024a597b2e8ec4aee710ffe62ddc95af2e2bb1ee7a7abdc22c68c/wcwidth-0.8.3-py3-none-any.whl", hash = "sha256:d5b73dba6158a595ec9370350e7f2637bcac8d6c5e4fde34f30fcffb6103a5e4", size = 331669, upload-time = "2026-08-28T18:10:04.909Z" },
+ { url = "https://files.pythonhosted.org/packages/39/cc/9c0adc5861020de426ec71716b6c1bef8d90694b7d6d0a16202cdb1647f5/wcwidth-0.8.4-py3-none-any.whl", hash = "sha256:2097bb1d28a0ba8fe177c2eb607317f9b1627b03f33ebebfd3da670b337a65ba", size = 299474, upload-time = "2026-09-17T23:31:43.816Z" },
]