Switched from PyYAML to ruamel.yaml for comment-preserving config and improved example config layout.
CI / Formatting (push) Successful in 4s
CI / Linting (push) Successful in 8s
CI / Tests (Python 3.12) (push) Failing after 45s
CI / Tests (Python 3.13) (push) Successful in 38s
CI / Tests (Python 3.14) (push) Successful in 17s
CI / Type Checking (push) Successful in 9s
CI / Spelling (push) Successful in 5s

This commit is contained in:
2026-03-30 10:22:13 -04:00
parent 2face83f65
commit bb69285511
6 changed files with 113 additions and 161 deletions
+57 -69
View File
@@ -1,100 +1,88 @@
owncast:
# REQUIRED: URL where viewers access your stream.
# Example: "https://stream.logal.dev"
url: ""
# REQUIRED: API access token for the Owncast integrations API.
# Generate one in Owncast under Admin -> Integrations -> Access Tokens.
access_token: ""
# OPTIONAL: admin API access for privileged operations like user
# management, server configuration, and moderation.
admin:
enabled: false
username: "admin"
password: "abc123"
owlbot:
# Address and port to bind the web server.
# Default: 127.0.0.1:8081
host: "127.0.0.1"
port: 8081
# Secret token for the webhook URL path (/webhook/<secret>).
# A cryptographically secure secret is generated on first run and saved
# back to this file automatically. Leave this empty unless you have a
# specific reason to set it manually.
# Auto-generated on first run. Leave empty unless you have a specific
# reason to set it manually.
webhook_secret: ""
# Public base URL for Owlbot's web server. Used to construct URLs
# generated by modules (e.g. via ctx.routes.url_for()).
#
# Set this if Owlbot is hosted on a different domain than your Owncast
# instance. If not set, falls back to owncast.url.
# Public base URL for Owlbot's web server. Set this if Owlbot is
# hosted on a different domain than your Owncast instance.
#public_base_url: "https://owlbot.example.com"
# Prefix character for chat commands.
# Default: "!"
#command_prefix: "!"
command_prefix: "!"
# Maximum seconds to wait for an event or command handler to complete.
# If a handler exceeds this, it is cancelled.
# Default: 30.0
#handler_timeout: 30.0
handler_timeout: 30.0
# Directory for per-module SQLite databases. Each module gets its own
# <module_name>.db file in this directory. Relative paths are resolved
# from the working directory.
# Default: "data"
#storage_dir: "data"
storage_dir: "data"
# Maximum number of SQLite connections per module in the connection pool.
# Connections are created lazily as needed, up to this limit.
# Default: 4
#pool_size: 4
# Directory containing user modules. Built-in modules are loaded from the
# package regardless of this setting.
# Default: "modules" (relative to the working directory)
#modules_dir: "modules"
# Directory containing user modules. Built-in modules are loaded from
# the package regardless of this setting.
modules_dir: "modules"
# Directory for the owlbot.log file. When set, logs are written to
# owlbot.log in this directory in addition to stdout. When unset,
# logs go to stdout only.
#log_dir: "logs"
owncast:
# URL where viewers access your stream (e.g. "https://stream.logal.dev").
# Required.
url: ""
# API access token for the Owncast integrations API. Required for
# sending messages, hiding messages, listing connected clients, etc.
# Generate one in Owncast under Admin -> Integrations -> Access Tokens.
access_token: ""
# Optional admin API access for privileged operations like user
# management, server configuration, and moderation. If not enabled,
# ctx.admin_client will be None in handlers.
admin:
enabled: false
username: "admin"
password: "abc123"
# Maximum number of SQLite connections per module in the connection pool.
# Connections are created lazily as needed, up to this limit.
pool_size: 4
# Per-module configuration. Each key is a module name (the filename without
# .py for single-file modules, or the directory name for package modules).
# Modules are enabled by default; set enabled: false to disable.
# Modules can define their own config keys via ctx.config.register_defaults()
# in their setup() function.
modules:
# clips:
# # Seconds of recent stream footage to keep available for clipping.
# # Default: 300
# cache_duration: 300
# # Minimum clip length in seconds.
# # Default: 5
# min_clip_length: 5
# # Maximum clip length in seconds.
# # Default: 120
# max_clip_length: 120
# # Directory where saved clip files are stored.
# # Default: "data/clips"
# clips_dir: "data/clips"
# # Seconds before an unused clip editor session expires.
# # Default: 900
# session_expiry: 900
# # Seconds to keep stream footage available after the stream stops.
# # Default: 120
# grace_period: 120
clips:
enabled: true
# custom_commands:
# # Default cooldown in seconds for newly created custom commands.
# # Default: 5
# default_cooldown: 5
# # Maximum placeholder nesting depth in custom command responses.
# # Default: 4
# max_nesting_depth: 4
# How far back (in seconds) viewers can clip from the live stream.
#cache_duration: 300
# Minimum clip length in seconds.
#min_clip_length: 5
# Maximum clip length in seconds.
#max_clip_length: 120
# Directory where saved clip files are stored.
#clips_dir: "data/clips"
# Seconds before a clip editing session expires if unused.
#session_expiry: 900
# Seconds to keep footage available after stream stops.
#grace_period: 120
custom_commands:
enabled: true
# Default cooldown in seconds for new custom commands.
#default_cooldown: 5
# Maximum nesting depth for placeholders in command responses.
#max_nesting_depth: 4
quotes:
enabled: true
timers:
enabled: true
+1 -1
Submodule docs updated: 8f83416203...3c26a82898
+41 -28
View File
@@ -17,17 +17,45 @@
import logging
import os
import secrets
from collections.abc import MutableMapping
from pathlib import Path
from typing import Any, TypeVar, overload
import yaml
from ruamel.yaml import YAML
from ruamel.yaml.comments import CommentedMap
from ruamel.yaml.error import YAMLError
logger = logging.getLogger("owlbot.config")
_yaml = YAML(typ="rt")
_yaml.preserve_quotes = True
_UNSET = object()
_T = TypeVar("_T")
def _cm_set(cm: MutableMapping[str, Any], key: str, value: object) -> None:
"""Set a key on a mapping, preserving comment placement for CommentedMaps.
When appending a new key to a ruamel.yaml CommentedMap, the trailing
comment on the previous last key (e.g. a blank line + section header)
would visually attach to the wrong location. This helper migrates that
trailing comment token to the newly inserted key so the output stays clean.
Falls back to plain dict assignment for non-CommentedMap mappings.
"""
if isinstance(cm, CommentedMap) and key not in cm and cm:
last_key = next(reversed(cm))
items = cm.ca.items
if last_key in items and items[last_key][2] is not None:
cm[key] = value
items.setdefault(key, [None, None, None, None])
items[key][2] = items[last_key][2]
items[last_key][2] = None
return
cm[key] = value
class Config:
"""Configuration manager for Owlbot."""
@@ -325,8 +353,8 @@ class Config:
if self.config_path.exists():
try:
with self.config_path.open() as f:
self._data = yaml.safe_load(f) or {}
except yaml.YAMLError as e:
self._data = _yaml.load(f) or {}
except YAMLError as e:
logger.error(f"Failed to parse config file: {e}")
raise
except OSError as e:
@@ -337,11 +365,11 @@ class Config:
raise FileNotFoundError(f"Config file not found: {self.config_path}")
if not isinstance(self._data.get("owlbot"), dict):
self._data["owlbot"] = {}
_cm_set(self._data, "owlbot", {})
if not isinstance(self._data.get("owncast"), dict):
self._data["owncast"] = {}
_cm_set(self._data, "owncast", {})
if not isinstance(self._data.get("modules"), dict):
self._data["modules"] = {}
_cm_set(self._data, "modules", {})
if not self.owncast_url:
raise ValueError(
@@ -355,7 +383,7 @@ class Config:
# (and not provided via env var or CLI override).
if not self.webhook_secret:
generated = secrets.token_urlsafe(32)
self._data["owlbot"]["webhook_secret"] = generated
_cm_set(self._data["owlbot"], "webhook_secret", generated)
self.save()
logger.info("Generated webhook secret and saved to config file.")
@@ -388,10 +416,8 @@ class Config:
logger.debug("Saving configuration to: %s", self.config_path.absolute())
try:
with self.config_path.open("w") as f:
yaml.safe_dump(
self._ordered_data(), f, default_flow_style=False, sort_keys=False
)
except yaml.YAMLError as e:
_yaml.dump(self._data, f)
except YAMLError as e:
logger.error(f"Failed to serialize config data: {e}")
raise
except OSError as e:
@@ -442,8 +468,8 @@ class Config:
:param config: Dict of configuration values to set.
"""
if not isinstance(self._data.get("modules"), dict):
self._data["modules"] = {}
self._data["modules"][module_name] = config
_cm_set(self._data, "modules", {})
_cm_set(self._data["modules"], module_name, config)
self.save()
module_logger = logging.getLogger(f"owlbot.modules.{module_name}.config")
module_logger.debug("Updated config.")
@@ -463,7 +489,7 @@ class Config:
self._module_defaults[module_name] = defaults
if not isinstance(self._data.get("modules"), dict):
self._data["modules"] = {}
_cm_set(self._data, "modules", {})
existing = self._data["modules"].get(module_name, {})
@@ -481,7 +507,7 @@ class Config:
# Existing section, but some default keys were missing.
changed = True
self._data["modules"][module_name] = merged
_cm_set(self._data["modules"], module_name, merged)
if changed:
self.save()
@@ -489,19 +515,6 @@ class Config:
module_logger = logging.getLogger(f"owlbot.modules.{module_name}.config")
module_logger.debug("Registered defaults.")
def _ordered_data(self) -> dict[str, Any]:
"""Return config data with sections in a stable order."""
ordered: dict[str, Any] = {}
for key in ("owlbot", "owncast", "modules"):
if key in self._data:
value = self._data[key]
if key == "modules" and isinstance(value, dict):
value = dict(sorted(value.items()))
ordered[key] = value
for key in sorted(self._data.keys() - ordered.keys()):
ordered[key] = self._data[key]
return ordered
class ModuleConfig:
"""Pre-scoped configuration for a specific module."""
+2 -2
View File
@@ -19,7 +19,7 @@ from __future__ import annotations
import logging
from typing import TYPE_CHECKING, Any
import yaml
from ruamel.yaml.error import YAMLError
from . import OWNCAST_TARGET_VERSION, __version__
from .api.config import Config
@@ -60,7 +60,7 @@ class Owlbot:
# Load config. Raises StartupError on missing/invalid config.
try:
self.config = Config(config_path, overrides=overrides)
except (FileNotFoundError, ValueError, yaml.YAMLError, OSError) as e:
except (FileNotFoundError, ValueError, YAMLError, OSError) as e:
raise StartupError(str(e)) from e
self.modules_dir = self.config.modules_dir
+1 -2
View File
@@ -15,7 +15,7 @@ dependencies = [
"jinja2>=3.1.6",
"markupsafe>=3.0.3",
"m3u8>=6.0.0",
"pyyaml>=6.0.3",
"ruamel.yaml>=0.19.1",
]
[project.scripts]
@@ -31,7 +31,6 @@ dev = [
"pytest-cov>=7.1.0",
"freezegun>=1.5.5",
"mypy>=1.18.1",
"types-PyYAML>=6.0.12.20250915",
"ruff>=0.15.7",
"pip-audit>=2.10.0",
"codespell>=2.4.2",
Generated
+11 -59
View File
@@ -881,7 +881,7 @@ dependencies = [
{ name = "jinja2" },
{ name = "m3u8" },
{ name = "markupsafe" },
{ name = "pyyaml" },
{ name = "ruamel-yaml" },
]
[package.dev-dependencies]
@@ -894,7 +894,6 @@ dev = [
{ name = "pytest-asyncio" },
{ name = "pytest-cov" },
{ name = "ruff" },
{ name = "types-pyyaml" },
]
[package.metadata]
@@ -906,7 +905,7 @@ requires-dist = [
{ name = "jinja2", specifier = ">=3.1.6" },
{ name = "m3u8", specifier = ">=6.0.0" },
{ name = "markupsafe", specifier = ">=3.0.3" },
{ name = "pyyaml", specifier = ">=6.0.3" },
{ name = "ruamel-yaml", specifier = ">=0.19.1" },
]
[package.metadata.requires-dev]
@@ -919,7 +918,6 @@ dev = [
{ name = "pytest-asyncio", specifier = ">=1.3.0" },
{ name = "pytest-cov", specifier = ">=7.1.0" },
{ name = "ruff", specifier = ">=0.15.7" },
{ name = "types-pyyaml", specifier = ">=6.0.12.20250915" },
]
[[package]]
@@ -1191,52 +1189,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" },
]
[[package]]
name = "pyyaml"
version = "6.0.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" },
{ url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" },
{ url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" },
{ url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" },
{ url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" },
{ url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" },
{ url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" },
{ url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" },
{ url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" },
{ url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" },
{ url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" },
{ url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" },
{ url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" },
{ url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" },
{ url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" },
{ url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" },
{ url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" },
{ url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" },
{ url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" },
{ url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" },
{ url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" },
{ url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" },
{ url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" },
{ url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" },
{ url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" },
{ url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" },
{ url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" },
{ url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" },
{ url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" },
{ url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" },
{ url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" },
{ url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" },
{ url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" },
{ url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" },
{ url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" },
{ url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" },
{ url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" },
{ url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" },
]
[[package]]
name = "requests"
version = "2.33.0"
@@ -1265,6 +1217,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/14/25/b208c5683343959b670dc001595f2f3737e051da617f66c31f7c4fa93abc/rich-14.3.3-py3-none-any.whl", hash = "sha256:793431c1f8619afa7d3b52b2cdec859562b950ea0d4b6b505397612db8d5362d", size = 310458, upload-time = "2026-02-19T17:23:13.732Z" },
]
[[package]]
name = "ruamel-yaml"
version = "0.19.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/c7/3b/ebda527b56beb90cb7652cb1c7e4f91f48649fbcd8d2eb2fb6e77cd3329b/ruamel_yaml-0.19.1.tar.gz", hash = "sha256:53eb66cd27849eff968ebf8f0bf61f46cdac2da1d1f3576dd4ccee9b25c31993", size = 142709, upload-time = "2026-01-02T16:50:31.84Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b8/0c/51f6841f1d84f404f92463fc2b1ba0da357ca1e3db6b7fbda26956c3b82a/ruamel_yaml-0.19.1-py3-none-any.whl", hash = "sha256:27592957fedf6e0b62f281e96effd28043345e0e66001f97683aa9a40c667c93", size = 118102, upload-time = "2026-01-02T16:50:29.201Z" },
]
[[package]]
name = "ruff"
version = "0.15.7"
@@ -1362,15 +1323,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/c7/18/c86eb8e0202e32dd3df50d43d7ff9854f8e0603945ff398974c1d91ac1ef/tomli_w-1.2.0-py3-none-any.whl", hash = "sha256:188306098d013b691fcadc011abd66727d3c414c571bb01b1a174ba8c983cf90", size = 6675, upload-time = "2025-01-15T12:07:22.074Z" },
]
[[package]]
name = "types-pyyaml"
version = "6.0.12.20250915"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/7e/69/3c51b36d04da19b92f9e815be12753125bd8bc247ba0470a982e6979e71c/types_pyyaml-6.0.12.20250915.tar.gz", hash = "sha256:0f8b54a528c303f0e6f7165687dd33fafa81c807fcac23f632b63aa624ced1d3", size = 17522, upload-time = "2025-09-15T03:01:00.728Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/bd/e0/1eed384f02555dde685fff1a1ac805c1c7dcb6dd019c916fe659b1c1f9ec/types_pyyaml-6.0.12.20250915-py3-none-any.whl", hash = "sha256:e7d4d9e064e89a3b3cae120b4990cd370874d2bf12fa5f46c97018dd5d3c9ab6", size = 20338, upload-time = "2025-09-15T03:00:59.218Z" },
]
[[package]]
name = "typing-extensions"
version = "4.15.0"