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: owlbot:
# Address and port to bind the web server. # Address and port to bind the web server.
# Default: 127.0.0.1:8081
host: "127.0.0.1" host: "127.0.0.1"
port: 8081 port: 8081
# Secret token for the webhook URL path (/webhook/<secret>). # Secret token for the webhook URL path (/webhook/<secret>).
# A cryptographically secure secret is generated on first run and saved # Auto-generated on first run. Leave empty unless you have a specific
# back to this file automatically. Leave this empty unless you have a # reason to set it manually.
# specific reason to set it manually.
webhook_secret: "" webhook_secret: ""
# Public base URL for Owlbot's web server. Used to construct URLs # Public base URL for Owlbot's web server. Set this if Owlbot is
# generated by modules (e.g. via ctx.routes.url_for()). # hosted on a different domain than your Owncast instance.
#
# 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: "https://owlbot.example.com" #public_base_url: "https://owlbot.example.com"
# Prefix character for chat commands. # Prefix character for chat commands.
# Default: "!" command_prefix: "!"
#command_prefix: "!"
# Maximum seconds to wait for an event or command handler to complete. # Maximum seconds to wait for an event or command handler to complete.
# If a handler exceeds this, it is cancelled. # 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 # Directory for per-module SQLite databases. Each module gets its own
# <module_name>.db file in this directory. Relative paths are resolved # <module_name>.db file in this directory. Relative paths are resolved
# from the working directory. # from the working directory.
# Default: "data" storage_dir: "data"
#storage_dir: "data"
# Maximum number of SQLite connections per module in the connection pool. # Directory containing user modules. Built-in modules are loaded from
# Connections are created lazily as needed, up to this limit. # the package regardless of this setting.
# Default: 4 modules_dir: "modules"
#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 for the owlbot.log file. When set, logs are written to # Directory for the owlbot.log file. When set, logs are written to
# owlbot.log in this directory in addition to stdout. When unset, # owlbot.log in this directory in addition to stdout. When unset,
# logs go to stdout only. # logs go to stdout only.
#log_dir: "logs" #log_dir: "logs"
owncast: # Maximum number of SQLite connections per module in the connection pool.
# URL where viewers access your stream (e.g. "https://stream.logal.dev"). # Connections are created lazily as needed, up to this limit.
# Required. pool_size: 4
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"
# Per-module configuration. Each key is a module name (the filename without # Per-module configuration. Each key is a module name (the filename without
# .py for single-file modules, or the directory name for package modules). # .py for single-file modules, or the directory name for package modules).
# Modules are enabled by default; set enabled: false to disable. # 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: modules:
# clips: clips:
# # Seconds of recent stream footage to keep available for clipping. enabled: true
# # 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
# custom_commands: # How far back (in seconds) viewers can clip from the live stream.
# # Default cooldown in seconds for newly created custom commands. #cache_duration: 300
# # Default: 5 # Minimum clip length in seconds.
# default_cooldown: 5 #min_clip_length: 5
# # Maximum placeholder nesting depth in custom command responses. # Maximum clip length in seconds.
# # Default: 4 #max_clip_length: 120
# max_nesting_depth: 4 # 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 logging
import os import os
import secrets import secrets
from collections.abc import MutableMapping
from pathlib import Path from pathlib import Path
from typing import Any, TypeVar, overload 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") logger = logging.getLogger("owlbot.config")
_yaml = YAML(typ="rt")
_yaml.preserve_quotes = True
_UNSET = object() _UNSET = object()
_T = TypeVar("_T") _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: class Config:
"""Configuration manager for Owlbot.""" """Configuration manager for Owlbot."""
@@ -325,8 +353,8 @@ class Config:
if self.config_path.exists(): if self.config_path.exists():
try: try:
with self.config_path.open() as f: with self.config_path.open() as f:
self._data = yaml.safe_load(f) or {} self._data = _yaml.load(f) or {}
except yaml.YAMLError as e: except YAMLError as e:
logger.error(f"Failed to parse config file: {e}") logger.error(f"Failed to parse config file: {e}")
raise raise
except OSError as e: except OSError as e:
@@ -337,11 +365,11 @@ class Config:
raise FileNotFoundError(f"Config file not found: {self.config_path}") raise FileNotFoundError(f"Config file not found: {self.config_path}")
if not isinstance(self._data.get("owlbot"), dict): if not isinstance(self._data.get("owlbot"), dict):
self._data["owlbot"] = {} _cm_set(self._data, "owlbot", {})
if not isinstance(self._data.get("owncast"), dict): if not isinstance(self._data.get("owncast"), dict):
self._data["owncast"] = {} _cm_set(self._data, "owncast", {})
if not isinstance(self._data.get("modules"), dict): if not isinstance(self._data.get("modules"), dict):
self._data["modules"] = {} _cm_set(self._data, "modules", {})
if not self.owncast_url: if not self.owncast_url:
raise ValueError( raise ValueError(
@@ -355,7 +383,7 @@ class Config:
# (and not provided via env var or CLI override). # (and not provided via env var or CLI override).
if not self.webhook_secret: if not self.webhook_secret:
generated = secrets.token_urlsafe(32) generated = secrets.token_urlsafe(32)
self._data["owlbot"]["webhook_secret"] = generated _cm_set(self._data["owlbot"], "webhook_secret", generated)
self.save() self.save()
logger.info("Generated webhook secret and saved to config file.") 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()) logger.debug("Saving configuration to: %s", self.config_path.absolute())
try: try:
with self.config_path.open("w") as f: with self.config_path.open("w") as f:
yaml.safe_dump( _yaml.dump(self._data, f)
self._ordered_data(), f, default_flow_style=False, sort_keys=False except YAMLError as e:
)
except yaml.YAMLError as e:
logger.error(f"Failed to serialize config data: {e}") logger.error(f"Failed to serialize config data: {e}")
raise raise
except OSError as e: except OSError as e:
@@ -442,8 +468,8 @@ class Config:
:param config: Dict of configuration values to set. :param config: Dict of configuration values to set.
""" """
if not isinstance(self._data.get("modules"), dict): if not isinstance(self._data.get("modules"), dict):
self._data["modules"] = {} _cm_set(self._data, "modules", {})
self._data["modules"][module_name] = config _cm_set(self._data["modules"], module_name, config)
self.save() self.save()
module_logger = logging.getLogger(f"owlbot.modules.{module_name}.config") module_logger = logging.getLogger(f"owlbot.modules.{module_name}.config")
module_logger.debug("Updated config.") module_logger.debug("Updated config.")
@@ -463,7 +489,7 @@ class Config:
self._module_defaults[module_name] = defaults self._module_defaults[module_name] = defaults
if not isinstance(self._data.get("modules"), dict): if not isinstance(self._data.get("modules"), dict):
self._data["modules"] = {} _cm_set(self._data, "modules", {})
existing = self._data["modules"].get(module_name, {}) existing = self._data["modules"].get(module_name, {})
@@ -481,7 +507,7 @@ class Config:
# Existing section, but some default keys were missing. # Existing section, but some default keys were missing.
changed = True changed = True
self._data["modules"][module_name] = merged _cm_set(self._data["modules"], module_name, merged)
if changed: if changed:
self.save() self.save()
@@ -489,19 +515,6 @@ class Config:
module_logger = logging.getLogger(f"owlbot.modules.{module_name}.config") module_logger = logging.getLogger(f"owlbot.modules.{module_name}.config")
module_logger.debug("Registered defaults.") 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: class ModuleConfig:
"""Pre-scoped configuration for a specific module.""" """Pre-scoped configuration for a specific module."""
+2 -2
View File
@@ -19,7 +19,7 @@ from __future__ import annotations
import logging import logging
from typing import TYPE_CHECKING, Any from typing import TYPE_CHECKING, Any
import yaml from ruamel.yaml.error import YAMLError
from . import OWNCAST_TARGET_VERSION, __version__ from . import OWNCAST_TARGET_VERSION, __version__
from .api.config import Config from .api.config import Config
@@ -60,7 +60,7 @@ class Owlbot:
# Load config. Raises StartupError on missing/invalid config. # Load config. Raises StartupError on missing/invalid config.
try: try:
self.config = Config(config_path, overrides=overrides) 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 raise StartupError(str(e)) from e
self.modules_dir = self.config.modules_dir self.modules_dir = self.config.modules_dir
+1 -2
View File
@@ -15,7 +15,7 @@ dependencies = [
"jinja2>=3.1.6", "jinja2>=3.1.6",
"markupsafe>=3.0.3", "markupsafe>=3.0.3",
"m3u8>=6.0.0", "m3u8>=6.0.0",
"pyyaml>=6.0.3", "ruamel.yaml>=0.19.1",
] ]
[project.scripts] [project.scripts]
@@ -31,7 +31,6 @@ dev = [
"pytest-cov>=7.1.0", "pytest-cov>=7.1.0",
"freezegun>=1.5.5", "freezegun>=1.5.5",
"mypy>=1.18.1", "mypy>=1.18.1",
"types-PyYAML>=6.0.12.20250915",
"ruff>=0.15.7", "ruff>=0.15.7",
"pip-audit>=2.10.0", "pip-audit>=2.10.0",
"codespell>=2.4.2", "codespell>=2.4.2",
Generated
+11 -59
View File
@@ -881,7 +881,7 @@ dependencies = [
{ name = "jinja2" }, { name = "jinja2" },
{ name = "m3u8" }, { name = "m3u8" },
{ name = "markupsafe" }, { name = "markupsafe" },
{ name = "pyyaml" }, { name = "ruamel-yaml" },
] ]
[package.dev-dependencies] [package.dev-dependencies]
@@ -894,7 +894,6 @@ dev = [
{ name = "pytest-asyncio" }, { name = "pytest-asyncio" },
{ name = "pytest-cov" }, { name = "pytest-cov" },
{ name = "ruff" }, { name = "ruff" },
{ name = "types-pyyaml" },
] ]
[package.metadata] [package.metadata]
@@ -906,7 +905,7 @@ requires-dist = [
{ name = "jinja2", specifier = ">=3.1.6" }, { name = "jinja2", specifier = ">=3.1.6" },
{ name = "m3u8", specifier = ">=6.0.0" }, { name = "m3u8", specifier = ">=6.0.0" },
{ name = "markupsafe", specifier = ">=3.0.3" }, { name = "markupsafe", specifier = ">=3.0.3" },
{ name = "pyyaml", specifier = ">=6.0.3" }, { name = "ruamel-yaml", specifier = ">=0.19.1" },
] ]
[package.metadata.requires-dev] [package.metadata.requires-dev]
@@ -919,7 +918,6 @@ dev = [
{ name = "pytest-asyncio", specifier = ">=1.3.0" }, { name = "pytest-asyncio", specifier = ">=1.3.0" },
{ name = "pytest-cov", specifier = ">=7.1.0" }, { name = "pytest-cov", specifier = ">=7.1.0" },
{ name = "ruff", specifier = ">=0.15.7" }, { name = "ruff", specifier = ">=0.15.7" },
{ name = "types-pyyaml", specifier = ">=6.0.12.20250915" },
] ]
[[package]] [[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" }, { 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]] [[package]]
name = "requests" name = "requests"
version = "2.33.0" 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" }, { 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]] [[package]]
name = "ruff" name = "ruff"
version = "0.15.7" 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" }, { 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]] [[package]]
name = "typing-extensions" name = "typing-extensions"
version = "4.15.0" version = "4.15.0"