diff --git a/README.md b/README.md index 60f9a88..c2ec7bb 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,15 @@ # hermes-plugin-kit -> Lifecycle helpers for [hermes-agent](https://github.com/NousResearch/hermes-agent) plugins — convention-correct tools, hooks, skills, validation, and safe logging, baked in. +> Lifecycle helpers for [hermes-agent](https://github.com/NousResearch/hermes-agent) plugins — convention-correct commands, tools, hooks, skills, validation, and safe logging, baked in. [![test](https://github.com/offendingcommit/hermes-plugin-kit/actions/workflows/test.yml/badge.svg)](https://github.com/offendingcommit/hermes-plugin-kit/actions/workflows/test.yml) ![python](https://img.shields.io/badge/python-3.11%2B-blue) `hermes-plugin-kit` is a tiny, dependency-free helper for authoring plugins for -[hermes-agent](https://github.com/NousResearch/hermes-agent). Decorate a tool -with `@tool` or a lifecycle callback with `@hook`, then use `register_plugin` to -register tools, hooks, and plugin-owned skills together. Existing tool-only +[hermes-agent](https://github.com/NousResearch/hermes-agent). Decorate a slash +command with `@command`, a tool with `@tool`, or a lifecycle callback with +`@hook`, then use `register_plugin` to register commands, tools, hooks, and +plugin-owned skills together. Existing tool-only plugins can keep using `register_all`; the LLM-facing schema, argument validation, structured logging, and the JSON result envelope are all generated for you — correctly, every time. @@ -131,13 +132,18 @@ That's it. `discord_read_thread` is registered with a `parameters`-wrapped schem self-documenting description, required-argument validation, logging, and the JSON envelope — none of which you had to write. -## Hooks and plugin skills +## Commands, hooks, and plugin skills Use the lifecycle entrypoint when a plugin provides more than tools: ```python from pathlib import Path -from hermes_plugin_kit import hook, plugin_skill, register_plugin +from hermes_plugin_kit import command, hook, plugin_skill, register_plugin + +@command("valdris-status", args_hint="") +def valdris_status(raw_args): + """Show the current Valdris plugin status.""" + return build_status(raw_args) @hook("pre_llm_call") def inject_context(**kwargs): @@ -156,6 +162,13 @@ def register(ctx): return register_plugin(ctx, __name__, skills=SKILLS) ``` +`@command` requires a bare lowercase kebab-case name without the leading slash. +Its handler receives the trailing command text unchanged and may return +`str | None` synchronously or asynchronously. The optional `args_hint` is +forwarded to Hermes for native command pickers. Command logs include only the +command name, elapsed time, result type, and argument character count, never +the raw arguments. + `@hook` forwards Hermes keyword arguments and return values unchanged. It logs only the hook name, elapsed time, result type, and supplied `session_id` or `task_id`; callback payloads and exception messages are never logged. Exceptions diff --git a/hermes_plugin_kit/__init__.py b/hermes_plugin_kit/__init__.py index d6863de..b8a7613 100644 --- a/hermes_plugin_kit/__init__.py +++ b/hermes_plugin_kit/__init__.py @@ -1,4 +1,4 @@ -"""hermes-plugin-kit — convention-correct tool registration for hermes-agent plugins. +"""hermes-plugin-kit — convention-correct surface registration for Hermes plugins. Reach for ``@tool`` + ``register_all`` and every hermes tool convention is applied for you, so the classes of bug that bite hand-written plugins cannot recur: @@ -61,6 +61,7 @@ def register(ctx): __all__ = [ "tool", + "command", "hook", "plugin_skill", "register_plugin", @@ -86,10 +87,12 @@ def register(ctx): ] _SPEC_ATTR = "_hpk_tool_spec" +_COMMAND_SPEC_ATTR = "_hpk_command_spec" _HOOK_SPEC_ATTR = "_hpk_hook_spec" _REDACT_HINTS = ("token", "secret", "password", "passwd", "api_key", "apikey", "auth") _MAX_LOG_CHARS = 200 _TOOL_NAME_RE = re.compile(r"^[a-z][a-z0-9_]*$") +_COMMAND_NAME_RE = re.compile(r"^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$") _AGENT_LOOP_TOOL_NAMES = frozenset({"todo", "memory", "session_search", "delegate_task"}) _RESERVED_NAMESPACE_PREFIXES = ("memory_",) _SKILL_NAME_RE = re.compile(r"^[a-zA-Z0-9_-]+$") @@ -127,6 +130,7 @@ class RegistrationSummary: hooks: tuple[str, ...] = () skills: tuple[str, ...] = () skipped_optional_skills: tuple[str, ...] = () + commands: tuple[str, ...] = () class MediaType(str, Enum): @@ -396,6 +400,106 @@ def _safe_context(kwargs: dict[str, Any]) -> dict[str, Any]: # The decorator # --------------------------------------------------------------------------- +def command( + name: str, + description: str | None = None, + args_hint: str = "", +) -> Callable: + """Mark and instrument a Hermes in-session slash command. + + ``name`` is the bare command name without a leading slash. The wrapped + handler receives the original ``raw_args`` string and returns ``str | None``. + Synchronous and asynchronous handlers preserve their native callable shape. + """ + if not isinstance(name, str) or not _COMMAND_NAME_RE.fullmatch(name): + raise ValueError( + "command name must be a bare lowercase kebab-case name matching " + f"{_COMMAND_NAME_RE.pattern!r}" + ) + if description is not None and not isinstance(description, str): + raise TypeError("command description must be a string or None") + if not isinstance(args_hint, str): + raise TypeError("command args_hint must be a string") + clean_args_hint = args_hint.strip() + + def decorate(fn: Callable) -> Callable: + explicit_description = (description or "").strip() + doc = explicit_description or (inspect.getdoc(fn) or "").strip() + if not doc: + raise ValueError( + f"@command {name!r}: a description is required " + "(docstring or description=)." + ) + log = logging.getLogger(fn.__module__ or "hermes_plugin_kit") + + def log_invocation(raw_args: str) -> float: + started = time.perf_counter() + try: + args_chars = len(raw_args) + except TypeError: + args_chars = len(str(raw_args)) + log.debug("%s: invoked; args_chars=%d", name, args_chars) + return started + + def log_failure(started: float, exc: Exception) -> None: + log.warning( + "%s: handler raised; elapsed_ms=%.2f; error_type=%s", + name, + (time.perf_counter() - started) * 1000, + type(exc).__name__, + ) + + def log_success(started: float, result: Any) -> None: + log.info( + "%s: ok; elapsed_ms=%.2f; result=%s", + name, + (time.perf_counter() - started) * 1000, + type(result).__name__, + ) + + if inspect.iscoroutinefunction(fn): + + @functools.wraps(fn) + async def async_wrapper(raw_args: str) -> str | None: + started = log_invocation(raw_args) + try: + result = await fn(raw_args) + except Exception as exc: + log_failure(started, exc) + raise + log_success(started, result) + return result + + wrapper = async_wrapper + else: + + @functools.wraps(fn) + def sync_wrapper(raw_args: str) -> str | None: + started = log_invocation(raw_args) + try: + result = fn(raw_args) + except Exception as exc: + log_failure(started, exc) + raise + log_success(started, result) + return result + + wrapper = sync_wrapper + + setattr( + wrapper, + _COMMAND_SPEC_ATTR, + { + "name": name, + "description": doc, + "args_hint": clean_args_hint, + }, + ) + return wrapper + + return decorate + + def hook(name: str) -> Callable: """Mark and instrument a Hermes lifecycle hook callback. @@ -1136,7 +1240,7 @@ def register_plugin( module: Any, skills: tuple[PluginSkill, ...] | list[PluginSkill] = (), ) -> RegistrationSummary: - """Register decorated tools, hooks, and declared skills from *module*. + """Register decorated commands, tools, hooks, and skills from *module*. Unlike the backward-compatible :func:`register_all`, this lifecycle-level entrypoint rejects distinct declarations that share a public name. Missing @@ -1146,9 +1250,17 @@ def register_plugin( module = sys.modules[module] log = logging.getLogger(getattr(module, "__name__", "hermes_plugin_kit")) + commands: dict[str, Callable] = {} tools: dict[str, Callable] = {} hooks: dict[str, Callable] = {} for _, obj in inspect.getmembers(module): + command_spec = getattr(obj, _COMMAND_SPEC_ATTR, None) + if command_spec: + existing = commands.get(command_spec["name"]) + if existing is not None and existing is not obj: + raise ValueError(f"duplicate command name: {command_spec['name']}") + commands[command_spec["name"]] = obj + tool_spec = getattr(obj, _SPEC_ATTR, None) if tool_spec: existing = tools.get(tool_spec["name"]) @@ -1187,6 +1299,18 @@ def register_plugin( ) skipped_skills.append(name) + registered_commands: list[str] = [] + for name in sorted(commands): + obj = commands[name] + spec = getattr(obj, _COMMAND_SPEC_ATTR) + ctx.register_command( + name=spec["name"], + handler=obj, + description=spec["description"], + args_hint=spec["args_hint"], + ) + registered_commands.append(name) + registered_tools: list[str] = [] for name in sorted(tools): obj = tools[name] @@ -1209,14 +1333,16 @@ def register_plugin( registered_skills.append(skill.name) summary = RegistrationSummary( + commands=tuple(registered_commands), tools=tuple(registered_tools), hooks=tuple(registered_hooks), skills=tuple(registered_skills), skipped_optional_skills=tuple(skipped_skills), ) log.info( - "hermes_plugin_kit: registered plugin lifecycle; tools=%s; hooks=%s; " - "skills=%s; skipped_optional_skills=%s", + "hermes_plugin_kit: registered plugin lifecycle; commands=%s; tools=%s; " + "hooks=%s; skills=%s; skipped_optional_skills=%s", + ",".join(summary.commands) or "", ",".join(summary.tools) or "", ",".join(summary.hooks) or "", ",".join(summary.skills) or "", diff --git a/pyproject.toml b/pyproject.toml index c557c33..97db6a7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,8 +8,8 @@ build-backend = "setuptools.build_meta" [project] name = "hermes-plugin-kit" -version = "0.3.0" -description = "Convention-correct lifecycle registration for hermes-agent plugins." +version = "0.4.0" +description = "Convention-correct command and lifecycle registration for hermes-agent plugins." readme = "README.md" requires-python = ">=3.11" license = { text = "MIT" } @@ -20,10 +20,10 @@ dependencies = [] [project.urls] Repository = "https://github.com/offendingcommit/hermes-plugin-kit" -# Runtime stays dependency-free. PyYAML is dev-only: the hermes contract tests -# import hermes_cli.plugins, which transitively needs yaml. Skipped without it. +# Runtime stays dependency-free. The hermes contract tests import current +# upstream source, whose plugin and gateway seams transitively need these. [dependency-groups] -dev = ["pyyaml"] +dev = ["pyyaml", "requests==2.33.0"] [tool.setuptools] packages = ["hermes_plugin_kit"] diff --git a/tests/test_hermes_contract.py b/tests/test_hermes_contract.py index ac34377..b4dd53b 100644 --- a/tests/test_hermes_contract.py +++ b/tests/test_hermes_contract.py @@ -49,6 +49,7 @@ def _try(): PluginManager, PluginManifest, VALID_HOOKS, + resolve_plugin_command_result, ) from tools.registry import registry # type: ignore @@ -64,6 +65,7 @@ def _try(): PluginManager=PluginManager, PluginManifest=PluginManifest, VALID_HOOKS=set(VALID_HOOKS), + resolve_plugin_command_result=resolve_plugin_command_result, registry=registry, ) @@ -115,6 +117,15 @@ def hpk_contract_probe(args, **kwargs): _SPEC = getattr(hpk_contract_probe, "_hpk_tool_spec") +@hpk.command( + "hpk-contract-probe", + description="Probe command registration.", + args_hint="", +) +async def hpk_command_contract_probe(raw_args): + return f"command:{raw_args}" + + @unittest.skipUnless(_REAL is not None, "hermes-agent source not importable") class HermesContractTests(unittest.TestCase): """Validate the kit's output against genuine hermes-agent runtime APIs.""" @@ -199,7 +210,35 @@ def contract_hook(**kwargs): ) self.assertEqual(manager.find_plugin_skill("contract-plugin:probe"), path) + def test_command_registers_and_dispatches_through_real_plugin_context(self) -> None: + manager = _REAL.PluginManager() + manifest = _REAL.PluginManifest(name="contract-plugin") + ctx = _REAL.PluginContext(manifest, manager) + module = types.ModuleType("contract_command_plugin") + module.hpk_command_contract_probe = hpk_command_contract_probe + + summary = hpk.register_plugin(ctx, module) + + self.assertEqual(summary.commands, ("hpk-contract-probe",)) + entry = manager._plugin_commands["hpk-contract-probe"] + self.assertEqual(entry["description"], "Probe command registration.") + self.assertEqual(entry["args_hint"], "") + result = entry["handler"]("exact raw args") + self.assertEqual( + _REAL.resolve_plugin_command_result(result), + "command:exact raw args", + ) + def test_lifecycle_calls_bind_to_real_plugincontext_signatures(self) -> None: + command_sig = inspect.signature(_REAL.PluginContext.register_command) + command_sig.bind( + None, + name="probe-command", + handler=lambda raw_args: raw_args, + description="Probe command.", + args_hint="", + ) + hook_sig = inspect.signature(_REAL.PluginContext.register_hook) hook_sig.bind(None, "pre_llm_call", lambda **kwargs: None) diff --git a/tests/test_kit.py b/tests/test_kit.py index e1e925b..775c2f3 100644 --- a/tests/test_kit.py +++ b/tests/test_kit.py @@ -1,6 +1,7 @@ from __future__ import annotations import asyncio +import inspect import json import sys import tempfile @@ -23,9 +24,13 @@ def register_tool(self, **kwargs) -> None: class FakePluginCtx(FakeCtx): def __init__(self) -> None: super().__init__() + self.commands: list[dict] = [] self.hooks: list[tuple[str, object]] = [] self.skills: list[dict] = [] + def register_command(self, **kwargs) -> None: + self.commands.append(kwargs) + def register_hook(self, hook_name, callback) -> None: self.hooks.append((hook_name, callback)) @@ -242,6 +247,99 @@ def test_hook_name_is_required(self) -> None: hpk.hook("") +class CommandBehaviorTests(unittest.IsolatedAsyncioTestCase): + def test_forwards_raw_args_and_uses_docstring_description(self) -> None: + @hpk.command("valdris-status", args_hint=" ") + def status(raw_args): + """ Show Valdris status. """ + return f"status:{raw_args}" + + spec = getattr(status, "_hpk_command_spec") + self.assertEqual(spec["name"], "valdris-status") + self.assertEqual(spec["description"], "Show Valdris status.") + self.assertEqual(spec["args_hint"], "") + + raw_args = " exact input --keep-spacing " + with self.assertLogs(level="DEBUG") as cap: + self.assertEqual(status(raw_args), f"status:{raw_args}") + joined = "\n".join(cap.output) + self.assertIn("valdris-status: invoked", joined) + self.assertIn(f"args_chars={len(raw_args)}", joined) + self.assertNotIn(raw_args, joined) + self.assertRegex(joined, r"elapsed_ms=\d+\.\d{2}") + self.assertIn("result=str", joined) + + async def test_async_handler_remains_async_and_returns_none(self) -> None: + @hpk.command("valdris-sync", description="Synchronize Valdris.") + async def sync(raw_args): + self.assertEqual(raw_args, "apply") + return None + + self.assertTrue(inspect.iscoroutinefunction(sync)) + with self.assertLogs(level="INFO") as cap: + self.assertIsNone(await sync("apply")) + self.assertIn("result=NoneType", "\n".join(cap.output)) + + def test_reraises_without_logging_args_or_exception_message(self) -> None: + @hpk.command("valdris-fail", description="Fail safely.") + def fail(raw_args): + raise RuntimeError("private command failure") + + with self.assertLogs(level="WARNING") as cap: + with self.assertRaisesRegex(RuntimeError, "private command failure"): + fail("private command arguments") + joined = "\n".join(cap.output) + self.assertIn("error_type=RuntimeError", joined) + self.assertNotIn("private command failure", joined) + self.assertNotIn("private command arguments", joined) + + def test_rejects_invalid_names(self) -> None: + for name in ( + "", + "/valdris-status", + "Valdris", + "valdris status", + "valdris_status", + "9valdris", + "valdris-", + ): + with self.subTest(name=name), self.assertRaisesRegex( + ValueError, "command name" + ): + hpk.command(name) + + def test_requires_description_or_docstring(self) -> None: + with self.assertRaisesRegex(ValueError, "description is required"): + + @hpk.command("valdris-empty") + def empty(raw_args): + return raw_args + + def test_description_argument_overrides_docstring(self) -> None: + @hpk.command("valdris-help", description=" Explicit description. ") + def help_command(raw_args): + """Ignored description.""" + return raw_args + + spec = getattr(help_command, "_hpk_command_spec") + self.assertEqual(spec["description"], "Explicit description.") + + def test_blank_description_falls_back_to_docstring(self) -> None: + @hpk.command("valdris-help", description=" ") + def help_command(raw_args): + """Show Valdris help.""" + return raw_args + + spec = getattr(help_command, "_hpk_command_spec") + self.assertEqual(spec["description"], "Show Valdris help.") + + def test_description_and_args_hint_must_be_strings(self) -> None: + with self.assertRaisesRegex(TypeError, "description"): + hpk.command("valdris-help", description=object()) + with self.assertRaisesRegex(TypeError, "args_hint"): + hpk.command("valdris-help", description="Help.", args_hint=object()) + + class HostToolInvocationTests(unittest.TestCase): def _runtime_modules(self, *, block_message=None, result='{"success": true}'): plugins = types.ModuleType("hermes_cli.plugins") @@ -756,7 +854,15 @@ def _module(self, **attrs): setattr(module, name, value) return module - def test_registers_tools_hooks_and_skills_with_summary(self) -> None: + def test_registers_commands_tools_hooks_and_skills_with_summary(self) -> None: + @hpk.command( + "valdris-status", + description="Show Valdris status.", + args_hint="", + ) + def command_handler(raw_args): + return raw_args + @hpk.hook("pre_llm_call") def callback(**kwargs): return kwargs @@ -768,16 +874,33 @@ def callback(**kwargs): "temporal-awareness", skill_path, "Use local timing context." ) ctx = FakePluginCtx() - module = self._module(callback=callback, sample_read=sample_read) + module = self._module( + callback=callback, + command_handler=command_handler, + sample_read=sample_read, + ) with self.assertLogs(level="INFO") as cap: summary = hpk.register_plugin(ctx, module, skills=(skill,)) + self.assertEqual(summary.commands, ("valdris-status",)) self.assertEqual(summary.tools, ("sample_read_thread",)) self.assertEqual(summary.hooks, ("pre_llm_call",)) self.assertEqual(summary.skills, ("temporal-awareness",)) self.assertEqual(summary.skipped_optional_skills, ()) + self.assertEqual( + ctx.commands, + [ + { + "name": "valdris-status", + "handler": command_handler, + "description": "Show Valdris status.", + "args_hint": "", + } + ], + ) self.assertEqual(ctx.hooks, [("pre_llm_call", callback)]) self.assertEqual(ctx.skills[0]["name"], "temporal-awareness") + self.assertIn("commands=valdris-status", "\n".join(cap.output)) self.assertIn("tools=sample_read_thread", "\n".join(cap.output)) self.assertIn("hooks=pre_llm_call", "\n".join(cap.output)) self.assertIn("skills=temporal-awareness", "\n".join(cap.output)) @@ -846,6 +969,21 @@ def second(args, **kwargs): hpk.register_plugin(ctx, self._module(first=first, second=second)) self.assertEqual(ctx.tools, []) + def test_rejects_duplicate_command_names_before_registration(self) -> None: + @hpk.command("valdris-status", description="First status.") + def first(raw_args): + return raw_args + + @hpk.command("valdris-status", description="Second status.") + def second(raw_args): + return raw_args + + ctx = FakePluginCtx() + with self.assertRaisesRegex(ValueError, "duplicate command"): + hpk.register_plugin(ctx, self._module(first=first, second=second)) + self.assertEqual(ctx.commands, []) + self.assertEqual(ctx.tools, []) + def test_rejects_duplicate_skill_names(self) -> None: skills = ( hpk.plugin_skill("same", "one/SKILL.md", "First", optional=True), diff --git a/uv.lock b/uv.lock index 1e83f76..ffcbe9b 100644 --- a/uv.lock +++ b/uv.lock @@ -2,20 +2,116 @@ version = 1 revision = 3 requires-python = ">=3.11" +[[package]] +name = "certifi" +version = "2026.7.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a3/c2/24167ea9858356b47a87a50d39908bfdb72ceeefe0041586e704e5376b3a/certifi-2026.7.22.tar.gz", hash = "sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55", size = 138112, upload-time = "2026-07-22T03:35:12.644Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/a7/71ac2cff56fec219ed242bb11b8efb69fcc4bec75db06fb7bfe35de520e6/certifi-2026.7.22-py3-none-any.whl", hash = "sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775", size = 136983, upload-time = "2026-07-22T03:35:11.276Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bd/2a/23f34ec9d04624958e137efdc394888716353190e75f25dd22c7a2c7a8aa/charset_normalizer-3.4.9.tar.gz", hash = "sha256:673611bbd43f0810bec0b0f028ddeaaa501190339cac411f347ac76917c3ae7b", size = 152439, upload-time = "2026-07-07T14:34:58.454Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/e3/85ec501f206fb049259288c1f3506e53876937fb00edb47009348e66756b/charset_normalizer-3.4.9-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0e94703ec9684807f20cfb5eed95c70f67f2a8f21ad620146d7b5a13677b93e5", size = 317075, upload-time = "2026-07-07T14:32:56.021Z" }, + { url = "https://files.pythonhosted.org/packages/c3/69/2a5385192e67175f7d8bd5ce4f57c24bc956439adeae5c13a99aa28a53d1/charset_normalizer-3.4.9-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2a441ea71902098ffe78c5abe6c494f44160b4af614ed16c3d9a3b1d17fd8ee2", size = 213837, upload-time = "2026-07-07T14:32:57.78Z" }, + { url = "https://files.pythonhosted.org/packages/b3/46/03ddc7da576d814fe0a36dd1f0fd3258e95404b4b2e3c026b7923d7e133f/charset_normalizer-3.4.9-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:304b13570067b2547562e308af560b3963857b1fa90bd6afd978130130fe2d6a", size = 235503, upload-time = "2026-07-07T14:32:59.205Z" }, + { url = "https://files.pythonhosted.org/packages/4e/6e/de0229a7ef40f6f9d28a837eebf4ec47bdca5dab4e900c84f22919af636a/charset_normalizer-3.4.9-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4773092f8019072343a7447203308b176e10199920eb02d6195e81bbb3274c29", size = 229944, upload-time = "2026-07-07T14:33:00.803Z" }, + { url = "https://files.pythonhosted.org/packages/a5/34/49b9060e8418b14fb5cba9cf6bfb383111e2538a03a1fb18e66a95aeb3d5/charset_normalizer-3.4.9-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:04ce310cb89c15df659582aee80a0603788732a5e017d5bd5c81158106ce249c", size = 221276, upload-time = "2026-07-07T14:33:02.199Z" }, + { url = "https://files.pythonhosted.org/packages/44/95/80282cce0fae9c3061203d723ee87da996aed79679e65d8935050ee7ca1f/charset_normalizer-3.4.9-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:c0323c9daef75ef2e5083624b4585018a0c9d5e3b40f607eed81a311270b934b", size = 205260, upload-time = "2026-07-07T14:33:03.698Z" }, + { url = "https://files.pythonhosted.org/packages/0c/74/2f62c8821b969ea3bd67cc2e6976834f48ca5d12664d2559ebcd9bcfbed7/charset_normalizer-3.4.9-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:871ff67ea1aad4dfd91736464934d56b32dac49f9fbe16cddba36198a7b3a0db", size = 217786, upload-time = "2026-07-07T14:33:05.12Z" }, + { url = "https://files.pythonhosted.org/packages/d9/8d/feabb82cb49fcad14515b1d7d1ca4787b0da7fc723a212bf89bc9e0fac52/charset_normalizer-3.4.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:67830fc78e67501f47bb950471b2dcb9b35b140084429318e862895a8e89c993", size = 216798, upload-time = "2026-07-07T14:33:06.629Z" }, + { url = "https://files.pythonhosted.org/packages/a5/ff/c946d63bc3786d5b84d960b0f7ab7e25b828486a946b5aa997625bcaf6a6/charset_normalizer-3.4.9-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:3d92613ec25e43b05f042302531ec0f00b8445190e43325880cbd6ab7c2581da", size = 206429, upload-time = "2026-07-07T14:33:08.006Z" }, + { url = "https://files.pythonhosted.org/packages/af/ba/5e5007c370702f85d2ef75791fac7943ed41e080364a673b20142e430e3e/charset_normalizer-3.4.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:280081916dc341820640489a66e4696049401ef1cf6dd672f672e70ad915aca3", size = 223066, upload-time = "2026-07-07T14:33:09.783Z" }, + { url = "https://files.pythonhosted.org/packages/83/d5/9096aa3cf532dfad237861544eb47a0f20d5adbf1039760fed8eaae935d9/charset_normalizer-3.4.9-cp311-cp311-win32.whl", hash = "sha256:ac351b3b8014eead140e77e9717e2992c6bbe30b63bc3422422eb84865412e3d", size = 150456, upload-time = "2026-07-07T14:33:11.217Z" }, + { url = "https://files.pythonhosted.org/packages/ed/a1/e29995109e455dc8eff8d0fac6ae509be39561318a7cfeac5d33ad029213/charset_normalizer-3.4.9-cp311-cp311-win_amd64.whl", hash = "sha256:6366a16e1a25018694d6a5d784d09b046edc9eac40ea2b54065c3052672516a1", size = 161410, upload-time = "2026-07-07T14:33:12.743Z" }, + { url = "https://files.pythonhosted.org/packages/4f/8d/1569f4d0032d6ba2a4fe4591c35bf87868c600c41a71eb5c2e1ffa8464c2/charset_normalizer-3.4.9-cp311-cp311-win_arm64.whl", hash = "sha256:1d22856ffbe153a602df38e4a5464f0b748a54002e0d69ac6d2ad0a197cc99ec", size = 152649, upload-time = "2026-07-07T14:33:14.173Z" }, + { url = "https://files.pythonhosted.org/packages/70/4a/ecbd131485c07fcdfad54e28946d513e3da22ef3b4bd854dcafae54ec739/charset_normalizer-3.4.9-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:45b0cc4e3556cd875e09102988d1ab8356c998b596c9fced84547c8138b487a0", size = 319300, upload-time = "2026-07-07T14:33:15.666Z" }, + { url = "https://files.pythonhosted.org/packages/ec/96/5d9364e3342d69f3a045e1777bc47c85c383e6e9466d561b33fdb419d1f9/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9b2aff1c7b3884512b9512c3eaadd9bab39fb45042ffaaa1dd08ff2b9f8109d9", size = 215802, upload-time = "2026-07-07T14:33:17.031Z" }, + { url = "https://files.pythonhosted.org/packages/4b/4c/5361f9aa7f2cb58d94f2ab831b3d493f69efb1d239654b4744e3c09527cb/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9104ed0bd76a429d46f9ec0dbc9b08ad1d2dcdf2b00a5a0daa1c145329b35b44", size = 237171, upload-time = "2026-07-07T14:33:18.576Z" }, + { url = "https://files.pythonhosted.org/packages/50/78/ce342ca4ff30b2eb49fe6d9578df85974f90c67d294113e94efdd9664cbd/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7b86a2b16095d250c6f58b3d9b2eee6f4147754344f3dab0922f7c9bf7d226c9", size = 233075, upload-time = "2026-07-07T14:33:20.084Z" }, + { url = "https://files.pythonhosted.org/packages/01/c4/4fa4c8b3097a11f3c5f09a35b72ed6855fb1d332469504962ab7bafcc702/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e226f6218febc71f6c1fc2fafb91c226f75bdc1d8fb12d66823716e891608fd", size = 224256, upload-time = "2026-07-07T14:33:21.747Z" }, + { url = "https://files.pythonhosted.org/packages/87/3a/ad914516df7e358a81aae018caa5e0470ba827fa6d763b1d2e87d920a5f6/charset_normalizer-3.4.9-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:90c44bc373b7687f6948b693cceaea1348ae0975d7474746559494468e3c1d84", size = 208784, upload-time = "2026-07-07T14:33:23.313Z" }, + { url = "https://files.pythonhosted.org/packages/d7/74/3c12f9755717dfe5c5c87da63f35d765fa0c00382ec26bf23f7fae34f2ba/charset_normalizer-3.4.9-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cdef90ae47919cae358d8ab15797a800ed41da7aba5d72419fb510729e2ed4b", size = 219928, upload-time = "2026-07-07T14:33:24.814Z" }, + { url = "https://files.pythonhosted.org/packages/33/9a/895095b83e7907abd6d3d99aad3a38ad0d9686cc186cb0c94c24320fe63e/charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:60f44ade2cf573dad7a277e6f8ca9a51a21dda572b13bd7d8539bb3cd5dbedde", size = 218489, upload-time = "2026-07-07T14:33:26.42Z" }, + { url = "https://files.pythonhosted.org/packages/a1/34/ef5c05f412f42520d7709b7d3784d19640839eb7366ded1755511585429f/charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:a1786910334ed46ab1dd73222f2cd1e05c2c3bb39f6dddb4f8b36fc382058a39", size = 210267, upload-time = "2026-07-07T14:33:27.952Z" }, + { url = "https://files.pythonhosted.org/packages/83/dc/9b29fa4412b318bf3bfea985c35d67eb55e04b59a7c3f2237168b0e0be6f/charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:03d07803992c6c7bbc976327f34b18b6160327fc81cb82c9d504720ac0be3b62", size = 226030, upload-time = "2026-07-07T14:33:29.397Z" }, + { url = "https://files.pythonhosted.org/packages/0e/42/6dbc00b8cd16011691203e33570fa42ed5746599a2e878112d16eab403a3/charset_normalizer-3.4.9-cp312-cp312-win32.whl", hash = "sha256:78841cccf1af7b40f6f716338d50c0902dbe88d9f800b3c973b7a9a0a693a642", size = 151185, upload-time = "2026-07-07T14:33:30.781Z" }, + { url = "https://files.pythonhosted.org/packages/80/cc/f920afd1a23c58ccd53c1d36085a71893a4737ff5e66e0371efab6809850/charset_normalizer-3.4.9-cp312-cp312-win_amd64.whl", hash = "sha256:4b3dac63058cc36820b0dd072f89898604e2d39686fe05321729d00d8ac185a0", size = 162557, upload-time = "2026-07-07T14:33:32.176Z" }, + { url = "https://files.pythonhosted.org/packages/f0/e6/0386d43a261ff4e4b30c5857af7df877254b46bec7b9d1b74b6bf969a90b/charset_normalizer-3.4.9-cp312-cp312-win_arm64.whl", hash = "sha256:78fa18e436a1a0e58dbd7e02fc4473f3f32cceb12df9dfca542d075961c307d2", size = 152665, upload-time = "2026-07-07T14:33:33.711Z" }, + { url = "https://files.pythonhosted.org/packages/b2/06/97ec2aeae780b31d742b6352218b43841a6871e2564578ca522dce4a45c3/charset_normalizer-3.4.9-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:440eede837960000d74978f0eba527be106b5b9aee0daf779d395276ed0b0614", size = 317688, upload-time = "2026-07-07T14:33:35.408Z" }, + { url = "https://files.pythonhosted.org/packages/d0/39/8ff066c672434225f8d25f8b739f992af250944392173dcc88362681c9bf/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21e764fd1e70b6a3e205a0e46f3051701f98a8cb3fad66eeb80e48bb502f8698", size = 214982, upload-time = "2026-07-07T14:33:36.996Z" }, + { url = "https://files.pythonhosted.org/packages/92/8f/3a47a3667c83c2df9483d91644c6c107de3bf8874aa1793da9d3012eb986/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e4fd89cc178bced6ad29cb3e6dd4aa63fa5017c3524dbd0b25998fb64a87cc8b", size = 236460, upload-time = "2026-07-07T14:33:38.536Z" }, + { url = "https://files.pythonhosted.org/packages/f1/60/b22cdbee7e4013dab8b0d7647fc6181120fbbbc8f7025c226d15bd5a47fc/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bd47ba7fc3ca94896759ea0109775132d3e7ab921fbf54038e1bab2e46c313c9", size = 232003, upload-time = "2026-07-07T14:33:40.059Z" }, + { url = "https://files.pythonhosted.org/packages/ea/f8/72eb13dcabe7257035cea8aefd922caad2f110d252bf9f67c4c2ca763aee/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:84fd18bcc17526fc2b3c1af7d2b9217d32c9c04448c16ec693b9b4f1985c3d33", size = 223149, upload-time = "2026-07-07T14:33:41.631Z" }, + { url = "https://files.pythonhosted.org/packages/b0/3e/faee8f9de92b14ee1198e9163252bb15efee7301b31256a3b6d9ebfdd0dd/charset_normalizer-3.4.9-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:5b10cd92fc5c498b35a8635df6d5a100207f88b63a4dc1de7ef9a548e1e2cd63", size = 207901, upload-time = "2026-07-07T14:33:43.209Z" }, + { url = "https://files.pythonhosted.org/packages/3a/25/45f30093ae27dd7b92a793b61882a38685f993700113ca36e0c9c14965e1/charset_normalizer-3.4.9-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a4fbdde9dd4a9ce5fd52c2b3a347bb50cc89483ef783f1cb00d408c13f7a96c0", size = 219176, upload-time = "2026-07-07T14:33:44.725Z" }, + { url = "https://files.pythonhosted.org/packages/48/18/c8f397329c35e32f6a837e488986f4ae03bd2abebc453b48714991630c2f/charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:416c229f77e5ea25b3dfd4b582f8d73d7e43c22320302b9ab128a2d3a0b38efe", size = 217356, upload-time = "2026-07-07T14:33:46.192Z" }, + { url = "https://files.pythonhosted.org/packages/86/7e/5ce0bba863470fd1902d5e5843968951bddf38abe4742fc97116ef4598b3/charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:75286256590a6320cf106a0d28970d3560aad9ee09aa7b34fb40524792436d35", size = 209614, upload-time = "2026-07-07T14:33:47.705Z" }, + { url = "https://files.pythonhosted.org/packages/6c/ef/2473d3c4d869155be4af1191111d59c4d5c4e0173026f7e85b176e23bf65/charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:69b157c5d3292bcd443faca052f3096f637f1e074b98212a933c074ae23dc3b8", size = 224991, upload-time = "2026-07-07T14:33:49.238Z" }, + { url = "https://files.pythonhosted.org/packages/d0/a3/53ddae3db108a088156aa8ddfafd411ebbc1340f48c5573f697b27f69a39/charset_normalizer-3.4.9-cp313-cp313-win32.whl", hash = "sha256:51307f5c71007673a2bf8232ad973483d281e74cb99c8c5a990af1eefa6277d9", size = 150622, upload-time = "2026-07-07T14:33:50.711Z" }, + { url = "https://files.pythonhosted.org/packages/e8/ef/6953a77c7cf2c2ff9998e6f575ab3e380119f100223381565a4f94c1f836/charset_normalizer-3.4.9-cp313-cp313-win_amd64.whl", hash = "sha256:fe2c7201c642b7c308f1675355ad7ff7b66acfe3541625efe5a3ad38f29d6115", size = 161947, upload-time = "2026-07-07T14:33:52.197Z" }, + { url = "https://files.pythonhosted.org/packages/6e/fb/d560d1d1555debbfe7849d9cac6145c1b537709d79576bf22557ed803b82/charset_normalizer-3.4.9-cp313-cp313-win_arm64.whl", hash = "sha256:611057cc5d5c0afc743ba8be6bd828c17e0aaa8643f9d0a9b9bb7dea80eb8012", size = 152594, upload-time = "2026-07-07T14:33:53.486Z" }, + { url = "https://files.pythonhosted.org/packages/7e/8d/496817fa0944239ecae662dd57ea765cfeaec6a735f9f025d4b7b72e7143/charset_normalizer-3.4.9-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:0327fcd59a935777d83410750c50600ee9571af2846f71ce40f25b13da1ef380", size = 317253, upload-time = "2026-07-07T14:33:54.994Z" }, + { url = "https://files.pythonhosted.org/packages/2b/f9/ef4a69ea338ad3c0deceea0f5f7d2380ae8b52132b06d652cb0d2cd86706/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a79d9f4d8001473a30c163556b3c3bfebec837495a412dde78b51672f6134f9", size = 215898, upload-time = "2026-07-07T14:33:56.334Z" }, + { url = "https://files.pythonhosted.org/packages/8c/e7/5ddfd76fc061eb52de219658a4aa431cbacadf0a0219c8854f00da50d289/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:33bdcc2a32c0a0e861f60841a512c8acc658c87c2ac59d89e3a46dacf7d866e4", size = 236718, upload-time = "2026-07-07T14:33:57.9Z" }, + { url = "https://files.pythonhosted.org/packages/49/ba/768fa3f36048d81c477a0ce61f813bc1454d80917ccfe550abd9f44f5e24/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f840ed6d8ecba8255df8c42b87fadeda98ddfc6eeec05e2dc66e26d46dd6f58a", size = 232519, upload-time = "2026-07-07T14:33:59.811Z" }, + { url = "https://files.pythonhosted.org/packages/f4/c4/b3e049d2aa3766180c78507110543d9d50894cc97f57de543f1be521dcdc/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c25fe15c70c59eb7c5ce8c06a1f3fa1da0ecc5ea1e7a5922c40fd2fa9b0d5046", size = 223143, upload-time = "2026-07-07T14:34:01.517Z" }, + { url = "https://files.pythonhosted.org/packages/19/79/55c32d06d76ae4feafe053f061f3e3ab70bcf19f4007797ce8c3efda7830/charset_normalizer-3.4.9-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:f7fb7d750cfa0a070d2c24e831fd3481019a60dd317ea2b39acbcebc08b6ed81", size = 206742, upload-time = "2026-07-07T14:34:03.04Z" }, + { url = "https://files.pythonhosted.org/packages/10/e0/47c079dd82d217c807479cd59ffd30af56307ea31c108b75758970459ad3/charset_normalizer-3.4.9-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4d1c96a7a18b9690a4d46df09e3e3382406ae3213727cd1019ebade1c4a81917", size = 219191, upload-time = "2026-07-07T14:34:04.657Z" }, + { url = "https://files.pythonhosted.org/packages/42/ab/b9bc2e77d6b44a7e46ef62ec5cac1c9a6ba7b9135a5d560f002696ec9995/charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a4cfde78a9f2880208d16a93b795726a3017d5977e08d1e162a7a31322479c41", size = 218328, upload-time = "2026-07-07T14:34:06.115Z" }, + { url = "https://files.pythonhosted.org/packages/f1/78/c9c71d599f5aa2d42bcdd35cbbd46d7f535351a57e40ff7d8e5a7e219401/charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:d4d6fcde76f94f5cb9e43e9e9a61f16dacefd228cbbf6f1a09bd9b219a92f1a1", size = 207406, upload-time = "2026-07-07T14:34:07.554Z" }, + { url = "https://files.pythonhosted.org/packages/f6/39/c914445c321a845097ce4f6ac7de9a18228a77b766272125a1ce00d851eb/charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:898f0e9068ca27d37f8e83a5b962821df851532e6c4a7d615c1c033f9da6eedf", size = 225157, upload-time = "2026-07-07T14:34:09.061Z" }, + { url = "https://files.pythonhosted.org/packages/9b/f2/c0d4b8508565a36bc5c624e88ed297f5b0b1095011034d7f5b83a69908b5/charset_normalizer-3.4.9-cp314-cp314-win32.whl", hash = "sha256:c1c948747b03be832dceed96ca815cef7360de9aa19d37c730f8e3f6101aca48", size = 151095, upload-time = "2026-07-07T14:34:10.901Z" }, + { url = "https://files.pythonhosted.org/packages/49/fd/a1d26144398c67486422a72bf5812cda22cb4ccfcd95a290fb41ceb4b8e2/charset_normalizer-3.4.9-cp314-cp314-win_amd64.whl", hash = "sha256:16b65ea0f2465b6fb52aa22de5eca612aa964ddfec00a912e26f4656cbef890b", size = 162796, upload-time = "2026-07-07T14:34:12.47Z" }, + { url = "https://files.pythonhosted.org/packages/20/95/d75e82f8ce9fd323ebf059c16c9aadefb22a1ecde13b7840b35835e4886c/charset_normalizer-3.4.9-cp314-cp314-win_arm64.whl", hash = "sha256:40a126142a56b2dfc0aacbad1de8310cbf60da7656db0e6b16eebd48e3e93519", size = 153334, upload-time = "2026-07-07T14:34:14.044Z" }, + { url = "https://files.pythonhosted.org/packages/00/5e/17398df3a139985ba9d11ed072531986f408c8fca952835ef1ab1820c02b/charset_normalizer-3.4.9-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:609b3ba8fcc0fb5ab7af00719d0fb6ad0cb518e48e7712d12fd68f1327951198", size = 338848, upload-time = "2026-07-07T14:34:15.688Z" }, + { url = "https://files.pythonhosted.org/packages/cd/91/7253a32e86b7e1d1239b1b36ba6dd0f021a21107ab33054b53119cc083b9/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:51447e9aa2684679af07ca5021c3db526e0284347ebf4ffcec1154c3350cfe32", size = 223022, upload-time = "2026-07-07T14:34:17.248Z" }, + { url = "https://files.pythonhosted.org/packages/cb/32/2e64bd2be10e89c61e57ebe6a93fd98ae88eb7ebe414b5121f22c96c69eb/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cc1b0fff8ead343dae06305f954eb8468ba0ec1a97881f42489d198e4ce3c632", size = 241590, upload-time = "2026-07-07T14:34:18.813Z" }, + { url = "https://files.pythonhosted.org/packages/3d/ef/d96ec496cfea0c21db43b0ad03891308b02388d054cc902cf0e5a1ad6a88/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fa36ec09ef71d158186bc79e359ff5fdd6e7996fe8ab638f00d6b93139ba4fcf", size = 239584, upload-time = "2026-07-07T14:34:20.52Z" }, + { url = "https://files.pythonhosted.org/packages/d4/ce/9af95f7876194bd7a14e3dfe4a4de2e0bff02666a3910d72beafd06cc297/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:df115d4d83168fdf2cae48ef1ff6d1cb4c466364e30861b37121de0f3bf1b990", size = 230224, upload-time = "2026-07-07T14:34:22.189Z" }, + { url = "https://files.pythonhosted.org/packages/52/94/af74dde74a3996bd959c350709bfe50e297823d70a8c1cbd54b838880863/charset_normalizer-3.4.9-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:f86c6358749bd4fda175388691e3ba8c46e24c5347d0afd20f9b7edfc9faf07d", size = 212667, upload-time = "2026-07-07T14:34:23.857Z" }, + { url = "https://files.pythonhosted.org/packages/ee/f0/f1c4fe746c395922961b5916ed1d7d6e7d4c84851d19ed43cc89980ec953/charset_normalizer-3.4.9-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:32286a2c8d167e897177b673176c1e3e00d4057caf5d2b64eef9a3666b03018e", size = 227179, upload-time = "2026-07-07T14:34:25.586Z" }, + { url = "https://files.pythonhosted.org/packages/e4/56/6c745619ac397e8871e2bcd3cea1eec86b877488f33888b3aef5c3ed506e/charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:83aed2c10721ddd90f68140685391b50811a880af20654c59af6b6c66c40513c", size = 225372, upload-time = "2026-07-07T14:34:27.212Z" }, + { url = "https://files.pythonhosted.org/packages/78/ad/98aae8630ac71f16711968e38a5acfecce41b778bf2f0312851020f565a8/charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cd6c3d4b783c556fa00bf540854e42f135e2f256abd29669fcd0da0f2dec79c2", size = 215222, upload-time = "2026-07-07T14:34:28.774Z" }, + { url = "https://files.pythonhosted.org/packages/f7/40/9593d54209765207a7f11073c06494c1721e4ca4a0a426c597679bf7f91e/charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ee2f2a527e3c1a6e6411eb4209642e138b544a2d72fe5d0d76daf77b24063534", size = 231958, upload-time = "2026-07-07T14:34:30.345Z" }, + { url = "https://files.pythonhosted.org/packages/b1/27/693ee5e8a18191eb38647360c51cd505013e2bd3b366aa43fd5344c21e3c/charset_normalizer-3.4.9-cp314-cp314t-win32.whl", hash = "sha256:0d861473f743244d349b50f850d10eb87aeb22bbdcc8e64f79273c94af5a8226", size = 155580, upload-time = "2026-07-07T14:34:31.884Z" }, + { url = "https://files.pythonhosted.org/packages/80/3f/bd97d3d9c613013d07cb7733d299385b41df37f0471310f5a73dc359f0b8/charset_normalizer-3.4.9-cp314-cp314t-win_amd64.whl", hash = "sha256:9b8e0f3107e2200b76f6054de99016eac3ee6762713587b36baaa7e4bd2ae177", size = 167620, upload-time = "2026-07-07T14:34:33.438Z" }, + { url = "https://files.pythonhosted.org/packages/3d/c6/eee9dca4439b1061f76373f06ea855678cc4a64c1c3c90b50e479edbb8eb/charset_normalizer-3.4.9-cp314-cp314t-win_arm64.whl", hash = "sha256:19ac87f93086ce37b86e098888555c4b4bc48102279bae3350098c0ed664b501", size = 158037, upload-time = "2026-07-07T14:34:35.018Z" }, + { url = "https://files.pythonhosted.org/packages/98/2b/f97f1c193fb855c345d678f5077d6926034db0722df74c8f057020e05a25/charset_normalizer-3.4.9-py3-none-any.whl", hash = "sha256:68e5f26a1ad57ded6d1cfb85331d1c1a195314756471d97758c48498bb4dcdf5", size = 64538, upload-time = "2026-07-07T14:34:56.993Z" }, +] + [[package]] name = "hermes-plugin-kit" -version = "0.3.0" +version = "0.4.0" source = { editable = "." } [package.dev-dependencies] dev = [ { name = "pyyaml" }, + { name = "requests" }, ] [package.metadata] [package.metadata.requires-dev] -dev = [{ name = "pyyaml" }] +dev = [ + { name = "pyyaml" }, + { name = "requests", specifier = "==2.33.0" }, +] + +[[package]] +name = "idna" +version = "3.18" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/63/9496c57188a2ee585e0f1db071d75089a11e98aa86eb99d9d7618fc1edce/idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848", size = 196711, upload-time = "2026-06-02T14:34:07.794Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455, upload-time = "2026-06-02T14:34:06.319Z" }, +] [[package]] name = "pyyaml" @@ -71,3 +167,27 @@ wheels = [ { 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" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/34/64/8860370b167a9721e8956ae116825caff829224fbca0ca6e7bf8ddef8430/requests-2.33.0.tar.gz", hash = "sha256:c7ebc5e8b0f21837386ad0e1c8fe8b829fa5f544d8df3b2253bff14ef29d7652", size = 134232, upload-time = "2026-03-25T15:10:41.586Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/56/5d/c814546c2333ceea4ba42262d8c4d55763003e767fa169adc693bd524478/requests-2.33.0-py3-none-any.whl", hash = "sha256:3324635456fa185245e24865e810cecec7b4caf933d7eb133dcde67d48cee69b", size = 65017, upload-time = "2026-03-25T15:10:40.382Z" }, +] + +[[package]] +name = "urllib3" +version = "2.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" }, +]