diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index f2d3c52..7573262 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -22,6 +22,4 @@ test: tags: - test script: - # - uv run --group integration-test pytest test/integration - - uv run --only-group test pytest test/unit - + - uv run --only-group test pytest test diff --git a/.logging_config.yaml b/.logging_config.yaml index 0403c77..a244917 100644 --- a/.logging_config.yaml +++ b/.logging_config.yaml @@ -8,7 +8,7 @@ formatters: # Console output colored: (): "colorlog.ColoredFormatter" - format: "{log_color}{asctime} | {levelname:11} | {name:70} | {message}" + format: "{log_color}{asctime}.{msecs:03.0f} | {levelname:11} | {name:70} | {message}" style: "{" datefmt: "%H:%M:%S" diff --git a/docs/conf.py b/docs/conf.py index 4f496e3..e448ae6 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -4,34 +4,35 @@ # https://www.sphinx-doc.org/en/master/usage/configuration.html import os import sys + sys.path.insert(0, os.path.abspath("../src")) # -- Project information ----------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information -project = 'control_backend' -copyright = '2025, Author' -author = 'Author' +project = "control_backend" +copyright = "2025, Author" +author = "Author" # -- General configuration --------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration extensions = [ - 'sphinx.ext.autodoc', - 'sphinx.ext.viewcode', - 'sphinx.ext.todo', + "sphinx.ext.autodoc", + "sphinx.ext.viewcode", + "sphinx.ext.todo", ] -templates_path = ['_templates'] -exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] +templates_path = ["_templates"] +exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] -language = 'en' +language = "en" # -- Options for HTML output ------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output -html_theme = 'sphinx_rtd_theme' -html_static_path = ['_static'] +html_theme = "sphinx_rtd_theme" +html_static_path = ["_static"] # -- Options for todo extension ---------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/extensions/todo.html#configuration diff --git a/pyproject.toml b/pyproject.toml index 40a8d88..e57a03c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,45 +5,51 @@ description = "Add your description here" readme = "README.md" requires-python = ">=3.13" dependencies = [ - "colorlog>=6.10.1", - "fastapi[all]>=0.115.6", - "mlx-whisper>=0.4.3 ; sys_platform == 'darwin'", - "numpy>=2.3.3", - "openai-whisper>=20250625", - "pyaudio>=0.2.14", - "pydantic>=2.12.0", - "pydantic-settings>=2.11.0", - "pytest>=8.4.2", - "pytest-asyncio>=1.2.0", - "pytest-cov>=7.0.0", - "pytest-mock>=3.15.1", - "python-json-logger>=4.0.0", - "pyyaml>=6.0.3", - "pyzmq>=27.1.0", - "silero-vad>=6.0.0", - "spade>=4.1.0", - "spade-bdi>=0.3.2", - "sphinx>=7.3.7", - "sphinx-rtd-theme>=3.0.2", - "torch>=2.8.0", - "uvicorn>=0.37.0", + "agentspeak>=0.2.2", + "colorlog>=6.10.1", + "fastapi[all]>=0.115.6", + "mlx-whisper>=0.4.3 ; sys_platform == 'darwin'", + "numpy>=2.3.3", + "openai-whisper>=20250625", + "pyaudio>=0.2.14", + "pydantic>=2.12.0", + "pydantic-settings>=2.11.0", + "python-json-logger>=4.0.0", + "pyyaml>=6.0.3", + "pyzmq>=27.1.0", + "silero-vad>=6.0.0", + "sphinx>=7.3.7", + "sphinx-rtd-theme>=3.0.2", + "torch>=2.8.0", + "uvicorn>=0.37.0", ] [dependency-groups] dev = [ - "pre-commit>=4.3.0", - "ruff>=0.14.2", - "ruff-format>=0.3.0", -] -integration-test = [ - "soundfile>=0.13.1", + "pre-commit>=4.3.0", + "pytest>=8.4.2", + "pytest-asyncio>=1.2.0", + "pytest-cov>=7.0.0", + "pytest-mock>=3.15.1", + "soundfile>=0.13.1", + "ruff>=0.14.2", + "ruff-format>=0.3.0", ] test = [ - "numpy>=2.3.3", - "pytest>=8.4.2", - "pytest-asyncio>=1.2.0", - "pytest-cov>=7.0.0", - "pytest-mock>=3.15.1", + "agentspeak>=0.2.2", + "fastapi>=0.115.6", + "httpx>=0.28.1", + "mlx-whisper>=0.4.3 ; sys_platform == 'darwin'", + "openai-whisper>=20250625", + "pydantic>=2.12.0", + "pydantic-settings>=2.11.0", + "pytest>=8.4.2", + "pytest-asyncio>=1.2.0", + "pytest-cov>=7.0.0", + "pytest-mock>=3.15.1", + "pyyaml>=6.0.3", + "pyzmq>=27.1.0", + "soundfile>=0.13.1", ] [tool.pytest.ini_options] @@ -54,15 +60,15 @@ line-length = 100 [tool.ruff.lint] extend-select = [ - "E", # pycodestyle - "F", # pyflakes - "I", # isort (import sorting) - "UP", # pyupgrade (modernize code) - "B", # flake8-bugbear (common bugs) - "C4", # flake8-comprehensions (unnecessary comprehensions) + "E", # pycodestyle + "F", # pyflakes + "I", # isort (import sorting) + "UP", # pyupgrade (modernize code) + "B", # flake8-bugbear (common bugs) + "C4", # flake8-comprehensions (unnecessary comprehensions) ] ignore = [ - "E226", # spaces around operators - "E701", # multiple statements on a single line + "E226", # spaces around operators + "E701", # multiple statements on a single line ] diff --git a/src/control_backend/agents/actuation/robot_speech_agent.py b/src/control_backend/agents/actuation/robot_speech_agent.py index 9b1ea61..e44f4bd 100644 --- a/src/control_backend/agents/actuation/robot_speech_agent.py +++ b/src/control_backend/agents/actuation/robot_speech_agent.py @@ -1,11 +1,10 @@ import json -import spade.agent import zmq -from spade.behaviour import CyclicBehaviour -from zmq.asyncio import Context +import zmq.asyncio as azmq from control_backend.agents import BaseAgent +from control_backend.core.agent_system import InternalMessage from control_backend.core.config import settings from control_backend.schemas.ri_message import SpeechCommand @@ -18,57 +17,21 @@ class RobotSpeechAgent(BaseAgent): def __init__( self, - jid: str, - password: str, - port: int = settings.agent_settings.default_spade_port, - verify_security: bool = False, + name: str, address=settings.zmq_settings.ri_command_address, bind=False, ): - super().__init__(jid, password, port, verify_security) + super().__init__(name) self.address = address self.bind = bind - class SendZMQCommandsBehaviour(CyclicBehaviour): - """Behaviour for sending commands received from the UI.""" - - async def run(self): - """ - Run the command publishing loop indefinetely. - """ - assert self.agent is not None - # Get a message internally (with topic command) - topic, body = await self.agent.subsocket.recv_multipart() - - # Try to get body - try: - body = json.loads(body) - message = SpeechCommand.model_validate(body) - - # Send to the robot. - await self.agent.pubsocket.send_json(message.model_dump()) - except Exception as e: - self.agent.logger.error("Error processing message: %s", e) - - class SendSpadeCommandsBehaviour(CyclicBehaviour): - """Behaviour for sending commands received from other Python agents.""" - - async def run(self): - message: spade.agent.Message = await self.receive(timeout=0.1) - if message and message.to == self.agent.jid: - try: - speech_command = SpeechCommand.model_validate_json(message.body) - await self.agent.pubsocket.send_json(speech_command.model_dump()) - except Exception as e: - self.agent.logger.error("Error processing message: %s", e) - async def setup(self): """ Setup the robot speech command agent """ - self.logger.info("Setting up %s", self.jid) + self.logger.info("Setting up %s", self.name) - context = Context.instance() + context = azmq.Context.instance() # To the robot self.pubsocket = context.socket(zmq.PUB) @@ -82,9 +45,38 @@ class RobotSpeechAgent(BaseAgent): self.subsocket.connect(settings.zmq_settings.internal_sub_address) self.subsocket.setsockopt(zmq.SUBSCRIBE, b"command") - # Add behaviour to our agent - commands_behaviour = self.SendZMQCommandsBehaviour() - self.add_behaviour(commands_behaviour) - self.add_behaviour(self.SendSpadeCommandsBehaviour()) + self.add_behavior(self._zmq_command_loop()) - self.logger.info("Finished setting up %s", self.jid) + self.logger.info("Finished setting up %s", self.name) + + async def stop(self): + if self.subsocket: + self.subsocket.close() + if self.pubsocket: + self.pubsocket.close() + await super().stop() + + async def handle_message(self, msg: InternalMessage): + """ + Handle commands received from other Python agents. + """ + try: + speech_command = SpeechCommand.model_validate_json(msg.body) + await self.pubsocket.send_json(speech_command.model_dump()) + except Exception: + self.logger.exception("Error processing internal message.") + + async def _zmq_command_loop(self): + """ + Handle commands from the UI. + """ + while self._running: + try: + _, body = await self.subsocket.recv_multipart() + + body = json.loads(body) + message = SpeechCommand.model_validate(body) + + await self.pubsocket.send_json(message.model_dump()) + except Exception: + self.logger.exception("Error processing ZMQ message.") diff --git a/src/control_backend/agents/base.py b/src/control_backend/agents/base.py index 51bf032..389c894 100644 --- a/src/control_backend/agents/base.py +++ b/src/control_backend/agents/base.py @@ -1,12 +1,12 @@ import logging -from spade.agent import Agent +from control_backend.core.agent_system import BaseAgent as CoreBaseAgent -class BaseAgent(Agent): +class BaseAgent(CoreBaseAgent): """ - Base agent class for our agents to inherit from. - This ensures that all agents have a logger. + Base agent class for our agents to inherit from. This just ensures + all agents have a logger. """ logger: logging.Logger diff --git a/src/control_backend/agents/bdi/__init__.py b/src/control_backend/agents/bdi/__init__.py index a7f6082..c8c8d47 100644 --- a/src/control_backend/agents/bdi/__init__.py +++ b/src/control_backend/agents/bdi/__init__.py @@ -1,7 +1,7 @@ from .bdi_core_agent.bdi_core_agent import BDICoreAgent as BDICoreAgent -from .belief_collector_agent.belief_collector_agent import ( +from .belief_collector_agent import ( BDIBeliefCollectorAgent as BDIBeliefCollectorAgent, ) -from .text_belief_extractor_agent.text_belief_extractor_agent import ( +from .text_belief_extractor_agent import ( TextBeliefExtractorAgent as TextBeliefExtractorAgent, ) diff --git a/src/control_backend/agents/bdi/bdi_core_agent/bdi_core_agent.py b/src/control_backend/agents/bdi/bdi_core_agent/bdi_core_agent.py index b20c872..b798982 100644 --- a/src/control_backend/agents/bdi/bdi_core_agent/bdi_core_agent.py +++ b/src/control_backend/agents/bdi/bdi_core_agent/bdi_core_agent.py @@ -1,67 +1,201 @@ -import logging +import asyncio +import copy +import time +from collections.abc import Iterable import agentspeak -from spade.behaviour import OneShotBehaviour -from spade.message import Message -from spade_bdi.bdi import BDIAgent +import agentspeak.runtime +import agentspeak.stdlib +from pydantic import ValidationError +from control_backend.agents.base import BaseAgent +from control_backend.core.agent_system import InternalMessage from control_backend.core.config import settings - -from .behaviours.belief_setter_behaviour import BeliefSetterBehaviour -from .behaviours.receive_llm_resp_behaviour import ReceiveLLMResponseBehaviour +from control_backend.schemas.belief_message import BeliefMessage +from control_backend.schemas.ri_message import SpeechCommand -class BDICoreAgent(BDIAgent): - """ - This is the Brain agent that does the belief inference with AgentSpeak. - This is a continous process that happens automatically in the background. - This class contains all the actions that can be called from AgentSpeak plans. - It has the BeliefSetter behaviour and can aks and recieve requests from the LLM agent. - """ +class BDICoreAgent(BaseAgent): + bdi_agent: agentspeak.runtime.Agent - logger = logging.getLogger(__package__).getChild(__name__) + def __init__(self, name: str, asl: str): + super().__init__(name) + self.asl_file = asl + self.env = agentspeak.runtime.Environment() + # Deep copy because we don't actually want to modify the standard actions globally + self.actions = copy.deepcopy(agentspeak.stdlib.actions) + self._wake_bdi_loop = asyncio.Event() async def setup(self) -> None: - """ - Initializes belief behaviors and message routing. - """ - self.logger.info("BDICoreAgent setup started.") + self.logger.debug("Setup started.") - self.add_behaviour(BeliefSetterBehaviour()) - self.add_behaviour(ReceiveLLMResponseBehaviour()) + self._add_custom_actions() - self.logger.info("BDICoreAgent setup complete.") + await self._load_asl() - def add_custom_actions(self, actions) -> None: + # Start the BDI cycle loop + self.add_behavior(self._bdi_loop()) + self._wake_bdi_loop.set() + self.logger.debug("Setup complete.") + + async def _load_asl(self): + try: + with open(self.asl_file) as source: + self.bdi_agent = self.env.build_agent(source, self.actions) + except FileNotFoundError: + self.logger.warning(f"Could not find the specified ASL file at {self.asl_file}.") + self.bdi_agent = agentspeak.runtime.Agent(self.env, self.name) + + async def _bdi_loop(self): """ - Registers custom AgentSpeak actions callable from plans. + Runs the AgentSpeak BDI loop. Efficiently checks for when the next expected work will be. + """ + while self._running: + await ( + self._wake_bdi_loop.wait() + ) # gets set whenever there's an update to the belief base + + # Agent knows when it's expected to have to do its next thing + maybe_more_work = True + while maybe_more_work: + maybe_more_work = False + self.logger.debug("Stepping BDI.") + if self.bdi_agent.step(): + maybe_more_work = True + + if not maybe_more_work: + deadline = self.bdi_agent.shortest_deadline() + if deadline: + self.logger.debug("Sleeping until %s", deadline) + await asyncio.sleep(deadline - time.time()) + maybe_more_work = True + else: + self._wake_bdi_loop.clear() + self.logger.debug("No more deadlines. Halting BDI loop.") + + async def handle_message(self, msg: InternalMessage): + """ + Route incoming messages (Beliefs or LLM responses). + """ + sender = msg.sender + + match sender: + case settings.agent_settings.bdi_belief_collector_name: + self.logger.debug("Processing message from belief collector.") + try: + if msg.thread == "beliefs": + beliefs = BeliefMessage.model_validate_json(msg.body).beliefs + self._add_beliefs(beliefs) + except ValidationError: + self.logger.exception("Error processing belief.") + case settings.agent_settings.llm_name: + content = msg.body + self.logger.info("Received LLM response: %s", content) + + # Forward to Robot Speech Agent + cmd = SpeechCommand(data=content) + out_msg = InternalMessage( + to=settings.agent_settings.robot_speech_name, + sender=self.name, + body=cmd.model_dump_json(), + ) + await self.send(out_msg) + + def _add_beliefs(self, beliefs: dict[str, list[str]]): + if not beliefs: + return + + for name, args in beliefs.items(): + self._add_belief(name, args) + + def _add_belief(self, name: str, args: Iterable[str] = []): + new_args = (agentspeak.Literal(arg) for arg in args) + term = agentspeak.Literal(name, new_args) + + self.bdi_agent.call( + agentspeak.Trigger.addition, + agentspeak.GoalType.belief, + term, + agentspeak.runtime.Intention(), + ) + + self._wake_bdi_loop.set() + + self.logger.debug(f"Added belief {self.format_belief_string(name, args)}") + + def _remove_belief(self, name: str, args: Iterable[str]): + """ + Removes a specific belief (with arguments), if it exists. + """ + new_args = (agentspeak.Literal(arg) for arg in args) + term = agentspeak.Literal(name, new_args) + + result = self.bdi_agent.call( + agentspeak.Trigger.removal, + agentspeak.GoalType.belief, + term, + agentspeak.runtime.Intention(), + ) + + if result: + self.logger.debug(f"Removed belief {self.format_belief_string(name, args)}") + self._wake_bdi_loop.set() + else: + self.logger.debug("Failed to remove belief (it was not in the belief base).") + + # TODO: decide if this is needed + def _remove_all_with_name(self, name: str): + """ + Removes all beliefs that match the given `name`. + """ + relevant_groups = [] + for key in self.bdi_agent.beliefs: + if key[0] == name: + relevant_groups.append(key) + + removed_count = 0 + for group in relevant_groups: + for belief in self.bdi_agent.beliefs[group]: + self.bdi_agent.call( + agentspeak.Trigger.removal, + agentspeak.GoalType.belief, + belief, + agentspeak.runtime.Intention(), + ) + removed_count += 1 + + self._wake_bdi_loop.set() + + self.logger.debug(f"Removed {removed_count} beliefs.") + + def _add_custom_actions(self) -> None: + """ + Add any custom actions here. Inside `@self.actions.add()`, the first argument is + the name of the function in the ASL file, and the second the amount of arguments + the function expects (which will be located in `term.args`). """ - @actions.add(".reply", 1) - def _reply(agent: "BDICoreAgent", term, intention): + @self.actions.add(".reply", 1) + def _reply(agent, term, intention): """ - Sends text to the LLM (AgentSpeak action). - Example: .reply("Hello LLM!") + Sends text to the LLM. """ message_text = agentspeak.grounded(term.args[0], intention.scope) - self.logger.debug("Reply action sending: %s", message_text) - self._send_to_llm(str(message_text)) + asyncio.create_task(self._send_to_llm(str(message_text))) yield - def _send_to_llm(self, text: str): + async def _send_to_llm(self, text: str): """ - Sends a text query to the LLM Agent asynchronously. + Sends a text query to the LLM agent asynchronously. """ + msg = InternalMessage(to=settings.agent_settings.llm_name, sender=self.name, body=text) + await self.send(msg) + self.logger.info("Message sent to LLM agent: %s", text) - class SendBehaviour(OneShotBehaviour): - async def run(self) -> None: - msg = Message( - to=settings.agent_settings.llm_name + "@" + settings.agent_settings.host, - body=text, - ) - - await self.send(msg) - self.agent.logger.info("Message sent to LLM agent: %s", text) - - self.add_behaviour(SendBehaviour()) + @staticmethod + def format_belief_string(name: str, args: Iterable[str] = []): + """ + Given a belief's name and its args, return a string of the form "name(*args)" + """ + return f"{name}{'(' if args else ''}{','.join(args)}{')' if args else ''}" diff --git a/src/control_backend/agents/bdi/bdi_core_agent/behaviours/belief_setter_behaviour.py b/src/control_backend/agents/bdi/bdi_core_agent/behaviours/belief_setter_behaviour.py deleted file mode 100644 index 105d6d2..0000000 --- a/src/control_backend/agents/bdi/bdi_core_agent/behaviours/belief_setter_behaviour.py +++ /dev/null @@ -1,85 +0,0 @@ -import json - -from spade.agent import Message -from spade.behaviour import CyclicBehaviour -from spade_bdi.bdi import BDIAgent - -from control_backend.core.config import settings - - -class BeliefSetterBehaviour(CyclicBehaviour): - """ - This is the behaviour that the BDI agent runs. This behaviour waits for incoming - message and updates the agent's beliefs accordingly. - """ - - agent: BDIAgent - - async def run(self): - """Polls for messages and processes them.""" - msg = await self.receive() - self.agent.logger.debug( - "Received message from %s with thread '%s' and body: %s", - msg.sender, - msg.thread, - msg.body, - ) - self._process_message(msg) - - def _process_message(self, message: Message): - """Routes the message to the correct processing function based on the sender.""" - sender = message.sender.node # removes host from jid and converts to str - self.agent.logger.debug("Processing message from sender: %s", sender) - - match sender: - case settings.agent_settings.bdi_belief_collector_name: - self.agent.logger.debug( - "Message is from the belief collector agent. Processing as belief message." - ) - self._process_belief_message(message) - case _: - self.agent.logger.debug("Not the belief agent, discarding message") - pass - - def _process_belief_message(self, message: Message): - if not message.body: - self.agent.logger.debug("Ignoring message with empty body from %s", message.sender.node) - return - - match message.thread: - case "beliefs": - try: - beliefs: dict[str, list[str]] = json.loads(message.body) - self._set_beliefs(beliefs) - except json.JSONDecodeError: - self.agent.logger.error( - "Could not decode beliefs from JSON. Message body: '%s'", - message.body, - exc_info=True, - ) - case _: - pass - - def _set_beliefs(self, beliefs: dict[str, list[str]]): - """Removes previous values for beliefs and updates them with the provided values.""" - if self.agent.bdi is None: - self.agent.logger.warning("Cannot set beliefs; agent's BDI is not yet initialized.") - return - - if not beliefs: - self.agent.logger.debug("Received an empty set of beliefs. No beliefs were updated.") - return - - # Set new beliefs (outdated beliefs are automatically removed) - for belief, arguments in beliefs.items(): - self.agent.logger.debug("Setting belief %s with arguments %s", belief, arguments) - self.agent.bdi.set_belief(belief, *arguments) - - # Special case: if there's a new user message, flag that we haven't responded yet - if belief == "user_said": - self.agent.bdi.set_belief("new_message") - self.agent.logger.debug( - "Detected 'user_said' belief, also setting 'new_message' belief." - ) - - self.agent.logger.info("Successfully updated %d beliefs.", len(beliefs)) diff --git a/src/control_backend/agents/bdi/bdi_core_agent/behaviours/receive_llm_resp_behaviour.py b/src/control_backend/agents/bdi/bdi_core_agent/behaviours/receive_llm_resp_behaviour.py deleted file mode 100644 index cf5cc03..0000000 --- a/src/control_backend/agents/bdi/bdi_core_agent/behaviours/receive_llm_resp_behaviour.py +++ /dev/null @@ -1,37 +0,0 @@ -from spade.behaviour import CyclicBehaviour -from spade.message import Message - -from control_backend.core.config import settings -from control_backend.schemas.ri_message import SpeechCommand - - -class ReceiveLLMResponseBehaviour(CyclicBehaviour): - """ - Adds behavior to receive responses from the LLM Agent. - """ - - async def run(self): - msg = await self.receive() - - sender = msg.sender.node - match sender: - case settings.agent_settings.llm_name: - content = msg.body - self.agent.logger.info("Received LLM response: %s", content) - - speech_command = SpeechCommand(data=content) - - message = Message( - to=settings.agent_settings.robot_speech_name - + "@" - + settings.agent_settings.host, - sender=self.agent.jid, - body=speech_command.model_dump_json(), - ) - - self.agent.logger.debug("Sending message: %s", message) - - await self.send(message) - case _: - self.agent.logger.debug("Discarding message from %s", sender) - pass diff --git a/src/control_backend/agents/bdi/bdi_core_agent/rules.asl b/src/control_backend/agents/bdi/bdi_core_agent/rules.asl index 0001d3c..a685f93 100644 --- a/src/control_backend/agents/bdi/bdi_core_agent/rules.asl +++ b/src/control_backend/agents/bdi/bdi_core_agent/rules.asl @@ -1,3 +1,3 @@ -+new_message : user_said(Message) <- - -new_message; ++user_said(Message) <- + -user_said(Message); .reply(Message). diff --git a/src/control_backend/agents/bdi/belief_collector_agent.py b/src/control_backend/agents/bdi/belief_collector_agent.py new file mode 100644 index 0000000..5d25204 --- /dev/null +++ b/src/control_backend/agents/bdi/belief_collector_agent.py @@ -0,0 +1,89 @@ +import json + +from control_backend.agents.base import BaseAgent +from control_backend.core.agent_system import InternalMessage +from control_backend.core.config import settings +from control_backend.schemas.belief_message import BeliefMessage + + +class BDIBeliefCollectorAgent(BaseAgent): + """ + Continuously collects beliefs/emotions from extractor agents and forwards a + unified belief packet to the BDI agent. + """ + + async def setup(self): + self.logger.info("Setting up %s", self.name) + + async def handle_message(self, msg: InternalMessage): + sender_node = msg.sender + + # Parse JSON payload + try: + payload = json.loads(msg.body) + except Exception as e: + self.logger.warning( + "BeliefCollector: failed to parse JSON from %s. Body=%r Error=%s", + sender_node, + msg.body, + e, + ) + return + + msg_type = payload.get("type") + + # Prefer explicit 'type' field + if msg_type == "belief_extraction_text": + self.logger.debug("Message routed to _handle_belief_text (sender=%s)", sender_node) + await self._handle_belief_text(payload, sender_node) + # This is not implemented yet, but we keep the structure for future use + elif msg_type == "emotion_extraction_text": + self.logger.debug("Message routed to _handle_emo_text (sender=%s)", sender_node) + await self._handle_emo_text(payload, sender_node) + else: + self.logger.warning( + "Unrecognized message (sender=%s, type=%r). Ignoring.", sender_node, msg_type + ) + + async def _handle_belief_text(self, payload: dict, origin: str): + """ + Expected payload: + { + "type": "belief_extraction_text", + "beliefs": {"user_said": ["Can you help me?"]} + + } + """ + beliefs = payload.get("beliefs", {}) + + if not beliefs: + self.logger.debug("Received empty beliefs set.") + return + + self.logger.debug("Forwarding %d beliefs.", len(beliefs)) + for belief_name, belief_list in beliefs.items(): + for belief in belief_list: + self.logger.debug(" - %s %s", belief_name, str(belief)) + + await self._send_beliefs_to_bdi(beliefs, origin=origin) + + async def _handle_emo_text(self, payload: dict, origin: str): + """TODO: implement (after we have emotional recognition)""" + pass + + async def _send_beliefs_to_bdi(self, beliefs: dict, origin: str | None = None): + """ + Sends a unified belief packet to the BDI agent. + """ + if not beliefs: + return + + msg = InternalMessage( + to=settings.agent_settings.bdi_core_name, + sender=self.name, + body=BeliefMessage(beliefs=beliefs).model_dump_json(), + thread="beliefs", + ) + + await self.send(msg) + self.logger.info("Sent %d belief(s) to BDI core.", len(beliefs)) diff --git a/src/control_backend/agents/bdi/belief_collector_agent/behaviours/belief_collector_behaviour.py b/src/control_backend/agents/bdi/belief_collector_agent/behaviours/belief_collector_behaviour.py deleted file mode 100644 index 7dfee28..0000000 --- a/src/control_backend/agents/bdi/belief_collector_agent/behaviours/belief_collector_behaviour.py +++ /dev/null @@ -1,92 +0,0 @@ -import json -from json import JSONDecodeError - -from spade.agent import Message -from spade.behaviour import CyclicBehaviour - -from control_backend.core.config import settings - - -class BeliefCollectorBehaviour(CyclicBehaviour): - """ - Continuously collects beliefs/emotions from extractor agents: - Then we send a unified belief packet to the BDI agent. - """ - - async def run(self): - msg = await self.receive() - await self._process_message(msg) - - async def _process_message(self, msg: Message): - sender_node = msg.sender.node - - # Parse JSON payload - try: - payload = json.loads(msg.body) - except JSONDecodeError as e: - self.agent.logger.warning( - "BeliefCollector: failed to parse JSON from %s. Body=%r Error=%s", - sender_node, - msg.body, - e, - ) - return - - msg_type = payload.get("type") - - # Prefer explicit 'type' field - if msg_type == "belief_extraction_text" or sender_node == "bel_text_agent_mock": - self.agent.logger.debug( - "Message routed to _handle_belief_text (sender=%s)", sender_node - ) - await self._handle_belief_text(payload, sender_node) - # This is not implemented yet, but we keep the structure for future use - elif msg_type == "emotion_extraction_text" or sender_node == "emo_text_agent_mock": - self.agent.logger.debug("Message routed to _handle_emo_text (sender=%s)", sender_node) - await self._handle_emo_text(payload, sender_node) - else: - self.agent.logger.warning( - "Unrecognized message (sender=%s, type=%r). Ignoring.", sender_node, msg_type - ) - - async def _handle_belief_text(self, payload: dict, origin: str): - """ - Expected payload: - { - "type": "belief_extraction_text", - "beliefs": {"user_said": ["Can you help me?"]} - - } - - """ - beliefs = payload.get("beliefs", {}) - - if not beliefs: - self.agent.logger.debug("Received empty beliefs set.") - return - - self.agent.logger.debug("Forwarding %d beliefs.", len(beliefs)) - for belief_name, belief_list in beliefs.items(): - for belief in belief_list: - self.agent.logger.debug(" - %s %s", belief_name, str(belief)) - - await self._send_beliefs_to_bdi(beliefs, origin=origin) - - async def _handle_emo_text(self, payload: dict, origin: str): - """TODO: implement (after we have emotional recogntion)""" - pass - - async def _send_beliefs_to_bdi(self, beliefs: list[str], origin: str | None = None): - """ - Sends a unified belief packet to the BDI agent. - """ - if not beliefs: - return - - to_jid = f"{settings.agent_settings.bdi_core_name}@{settings.agent_settings.host}" - - msg = Message(to=to_jid, sender=self.agent.jid, thread="beliefs") - msg.body = json.dumps(beliefs) - - await self.send(msg) - self.agent.logger.info("Sent %d belief(s) to BDI core.", len(beliefs)) diff --git a/src/control_backend/agents/bdi/belief_collector_agent/belief_collector_agent.py b/src/control_backend/agents/bdi/belief_collector_agent/belief_collector_agent.py deleted file mode 100644 index a82e230..0000000 --- a/src/control_backend/agents/bdi/belief_collector_agent/belief_collector_agent.py +++ /dev/null @@ -1,11 +0,0 @@ -from control_backend.agents.base import BaseAgent - -from .behaviours.belief_collector_behaviour import BeliefCollectorBehaviour - - -class BDIBeliefCollectorAgent(BaseAgent): - async def setup(self): - self.logger.info("BDIBeliefCollectorAgent starting (%s)", self.jid) - # Attach the continuous collector behaviour (listens and forwards to BDI) - self.add_behaviour(BeliefCollectorBehaviour()) - self.logger.info("BDIBeliefCollectorAgent ready.") diff --git a/src/control_backend/agents/bdi/text_belief_extractor_agent.py b/src/control_backend/agents/bdi/text_belief_extractor_agent.py new file mode 100644 index 0000000..5056c80 --- /dev/null +++ b/src/control_backend/agents/bdi/text_belief_extractor_agent.py @@ -0,0 +1,38 @@ +import json + +from control_backend.agents.base import BaseAgent +from control_backend.core.agent_system import InternalMessage +from control_backend.core.config import settings + + +class TextBeliefExtractorAgent(BaseAgent): + async def setup(self): + self.logger.info("Settting up %s.", self.name) + # Setup LLM belief context if needed (currently demo is just passthrough) + self.beliefs = {"mood": ["X"], "car": ["Y"]} + + async def handle_message(self, msg: InternalMessage): + sender = msg.sender + if sender == settings.agent_settings.transcription_name: + self.logger.debug("Received text from transcriber: %s", msg.body) + await self._process_transcription_demo(msg.body) + else: + self.logger.info("Discarding message from %s", sender) + + async def _process_transcription_demo(self, txt: str): + """ + Demo version to process the transcription input to beliefs. + """ + # For demo, just wrapping user text as user_said belief + belief = {"beliefs": {"user_said": [txt]}, "type": "belief_extraction_text"} + payload = json.dumps(belief) + + belief_msg = InternalMessage( + to=settings.agent_settings.bdi_belief_collector_name, + sender=self.name, + body=payload, + thread="beliefs", + ) + + await self.send(belief_msg) + self.logger.info("Sent %d beliefs to the belief collector.", len(belief["beliefs"])) diff --git a/src/control_backend/agents/bdi/text_belief_extractor_agent/behaviours/text_belief_extractor_behaviour.py b/src/control_backend/agents/bdi/text_belief_extractor_agent/behaviours/text_belief_extractor_behaviour.py deleted file mode 100644 index e09ed0c..0000000 --- a/src/control_backend/agents/bdi/text_belief_extractor_agent/behaviours/text_belief_extractor_behaviour.py +++ /dev/null @@ -1,104 +0,0 @@ -import json -import logging - -from spade.behaviour import CyclicBehaviour -from spade.message import Message - -from control_backend.core.config import settings - - -class TextBeliefExtractorBehaviour(CyclicBehaviour): - logger = logging.getLogger(__name__) - - # TODO: LLM prompt nog hardcoded - llm_instruction_prompt = """ - You are an information extraction assistent for a BDI agent. Your task is to extract values \ - from a user's text to bind a list of ungrounded beliefs. Rules: - You will receive a JSON object with "beliefs" (a list of ungrounded AgentSpeak beliefs) \ - and "text" (user's transcript). - Analyze the text to find values that sematically match the variables (X,Y,Z) in the beliefs. - A single piece of text might contain multiple instances that match a belief. - Respond ONLY with a single JSON object. - The JSON object's keys should be the belief functors (e.g., "weather"). - The value for each key must be a list of lists. - Each inner list must contain the extracted arguments (as strings) for one instance \ - of that belief. - CRITICAL: If no information in the text matches a belief, DO NOT include that key \ - in your response. - """ - - # on_start agent receives message containing the beliefs to look out for and - # sets up the LLM with instruction prompt - # async def on_start(self): - # msg = await self.receive(timeout=0.1) - # self.beliefs = dict uit message - # send instruction prompt to LLM - - beliefs: dict[str, list[str]] - beliefs = {"mood": ["X"], "car": ["Y"]} - - async def run(self): - msg = await self.receive() - if msg is None: - return - - sender = msg.sender.node - match sender: - case settings.agent_settings.transcription_name: - self.logger.debug("Received text from transcriber: %s", msg.body) - await self._process_transcription_demo(msg.body) - case _: - self.logger.info("Discarding message from %s", sender) - pass - - async def _process_transcription(self, text: str): - text_prompt = f"Text: {text}" - - beliefs_prompt = "These are the beliefs to be bound:\n" - for belief, values in self.beliefs.items(): - beliefs_prompt += f"{belief}({', '.join(values)})\n" - - prompt = text_prompt + beliefs_prompt - self.logger.info(prompt) - # prompt_msg = Message(to="LLMAgent@whatever") - # response = self.send(prompt_msg) - - # Mock response; response is beliefs in JSON format, it parses do dict[str,list[list[str]]] - response = '{"mood": [["happy"]]}' - # Verify by trying to parse - try: - json.loads(response) - belief_message = Message() - - belief_message.to = ( - settings.agent_settings.bdi_belief_collector_name - + "@" - + settings.agent_settings.host - ) - belief_message.body = response - belief_message.thread = "beliefs" - - await self.send(belief_message) - self.agent.logger.info("Sent beliefs to BDI.") - except json.JSONDecodeError: - # Parsing failed, so the response is in the wrong format, log warning - self.agent.logger.warning("Received LLM response in incorrect format.") - - async def _process_transcription_demo(self, txt: str): - """ - Demo version to process the transcription input to beliefs. For the demo only the belief - 'user_said' is relevant, so this function simply makes a dict with key: "user_said", - value: txt and passes this to the Belief Collector agent. - """ - belief = {"beliefs": {"user_said": [txt]}, "type": "belief_extraction_text"} - payload = json.dumps(belief) - belief_msg = Message() - - belief_msg.to = ( - settings.agent_settings.bdi_belief_collector_name + "@" + settings.agent_settings.host - ) - belief_msg.body = payload - belief_msg.thread = "beliefs" - - await self.send(belief_msg) - self.logger.info("Sent %d beliefs to the belief collector.", len(belief["beliefs"])) diff --git a/src/control_backend/agents/bdi/text_belief_extractor_agent/text_belief_extractor_agent.py b/src/control_backend/agents/bdi/text_belief_extractor_agent/text_belief_extractor_agent.py deleted file mode 100644 index 4baa420..0000000 --- a/src/control_backend/agents/bdi/text_belief_extractor_agent/text_belief_extractor_agent.py +++ /dev/null @@ -1,8 +0,0 @@ -from control_backend.agents.base import BaseAgent - -from .behaviours.text_belief_extractor_behaviour import TextBeliefExtractorBehaviour - - -class TextBeliefExtractorAgent(BaseAgent): - async def setup(self): - self.add_behaviour(TextBeliefExtractorBehaviour()) diff --git a/src/control_backend/agents/communication/ri_communication_agent.py b/src/control_backend/agents/communication/ri_communication_agent.py index 3b414a1..50ea284 100644 --- a/src/control_backend/agents/communication/ri_communication_agent.py +++ b/src/control_backend/agents/communication/ri_communication_agent.py @@ -1,8 +1,8 @@ import asyncio import json -import zmq.asyncio -from spade.behaviour import CyclicBehaviour +import zmq +import zmq.asyncio as azmq from zmq.asyncio import Context from control_backend.agents import BaseAgent @@ -12,109 +12,38 @@ from ..actuation.robot_speech_agent import RobotSpeechAgent class RICommunicationAgent(BaseAgent): - req_socket: zmq.Socket - _address = "" - _bind = True - connected = False - def __init__( self, - jid: str, - password: str, - port: int = settings.agent_settings.default_spade_port, - verify_security: bool = False, + name: str, address=settings.zmq_settings.ri_command_address, bind=False, ): - super().__init__(jid, password, port, verify_security) + super().__init__(name) self._address = address self._bind = bind - self._req_socket: zmq.asyncio.Socket | None = None - self.pub_socket: zmq.asyncio.Socket | None = None + self._req_socket: azmq.Socket | None = None + self.pub_socket: azmq.Socket | None = None + self.connected = False - class ListenBehaviour(CyclicBehaviour): - async def run(self): - """ - Run the listening (ping) loop indefinetely. - """ - assert self.agent is not None + async def setup(self): + """ + Try to set up the communication agent, we have `behaviour_settings.comm_setup_max_retries` + retries in case we don't have a response yet. + """ + self.logger.info("Setting up %s", self.name) - if not self.agent.connected: - await asyncio.sleep(settings.behaviour_settings.sleep_s) - return + # Bind request socket + await self._setup_sockets() - # We need to listen and sent pings. - message = {"endpoint": "ping", "data": {"id": "e.g. some reference id"}} - seconds_to_wait_total = settings.behaviour_settings.sleep_s - try: - await asyncio.wait_for( - self.agent._req_socket.send_json(message), timeout=seconds_to_wait_total / 2 - ) - except TimeoutError: - self.agent.logger.debug( - "Waited too long to send message - " - "we probably dont have any receivers... but let's check!" - ) + if await self._negotiate_connection(): + self.connected = True + self.add_behavior(self._listen_loop()) + else: + self.logger.warning("Failed to negotiate connection during setup.") - # Wait up to {seconds_to_wait_total/2} seconds for a reply - try: - message = await asyncio.wait_for( - self.agent._req_socket.recv_json(), timeout=seconds_to_wait_total / 2 - ) + self.logger.info("Finished setting up %s", self.name) - # We didnt get a reply - except TimeoutError: - self.agent.logger.info( - f"No ping retrieved in {seconds_to_wait_total} seconds, " - "sending UI disconnection event and attempting to restart." - ) - - # Make sure we dont retry receiving messages untill we're setup. - self.agent.connected = False - self.agent.remove_behaviour(self) - - # Tell UI we're disconnected. - topic = b"ping" - data = json.dumps(False).encode() - if self.agent.pub_socket is None: - self.agent.logger.warning( - "Communication agent pub socket not correctly initialized." - ) - else: - try: - await asyncio.wait_for( - self.agent.pub_socket.send_multipart([topic, data]), 5 - ) - except TimeoutError: - self.agent.logger.warning( - f"Initial connection ping for router timed out in {self.agent.name}." - ) - - # Try to reboot. - self.agent.logger.debug("Restarting communication agent.") - await self.agent.setup() - - self.agent.logger.debug(f'Received message "{message}" from RI.') - if "endpoint" not in message: - self.agent.logger.warning( - "No received endpoint in message, expected ping endpoint." - ) - return - - # See what endpoint we received - match message["endpoint"]: - case "ping": - topic = b"ping" - data = json.dumps(True).encode() - if self.agent.pub_socket is not None: - await self.agent.pub_socket.send_multipart([topic, data]) - await asyncio.sleep(settings.behaviour_settings.sleep_s) - case _: - self.agent.logger.debug( - "Received message with topic different than ping, while ping expected." - ) - - async def setup_sockets(self, force=False): + async def _setup_sockets(self, force=False): """ Sets up request socket for communication agent. """ @@ -130,21 +59,13 @@ class RICommunicationAgent(BaseAgent): self.pub_socket = Context.instance().socket(zmq.PUB) self.pub_socket.connect(settings.zmq_settings.internal_pub_address) - async def setup(self, max_retries: int = settings.behaviour_settings.comm_setup_max_retries): - """ - Try to set up the communication agent, we have `behaviour_settings.comm_setup_max_retries` - retries in case we don't have a response yet. - """ - self.logger.info("Setting up %s", self.jid) - - # Bind request socket - await self.setup_sockets() - + async def _negotiate_connection( + self, max_retries: int = settings.behaviour_settings.comm_setup_max_retries + ): retries = 0 - # Let's try a certain amount of times before failing connection while retries < max_retries: - # Make sure the socket is properly setup. if self._req_socket is None: + retries += 1 continue # Send our message and receive one back @@ -156,7 +77,6 @@ class RICommunicationAgent(BaseAgent): received_message = await asyncio.wait_for( self._req_socket.recv_json(), timeout=retry_frequency ) - except TimeoutError: self.logger.warning( "No connection established in %d seconds (attempt %d/%d)", @@ -166,7 +86,6 @@ class RICommunicationAgent(BaseAgent): ) retries += 1 continue - except Exception as e: self.logger.warning("Unexpected error during negotiation: %s", e) retries += 1 @@ -187,64 +106,129 @@ class RICommunicationAgent(BaseAgent): # At this point, we have a valid response try: - for port_data in received_message["data"]: - id = port_data["id"] - port = port_data["port"] - bind = port_data["bind"] - - if not bind: - addr = f"tcp://localhost:{port}" - else: - addr = f"tcp://*:{port}" - - match id: - case "main": - if addr != self._address: - if not bind: - self._req_socket.connect(addr) - else: - self._req_socket.bind(addr) - case "actuation": - ri_commands_agent = RobotSpeechAgent( - settings.agent_settings.robot_speech_name - + "@" - + settings.agent_settings.host, - settings.agent_settings.robot_speech_name, - address=addr, - bind=bind, - ) - await ri_commands_agent.start() - case _: - self.logger.warning("Unhandled negotiation id: %s", id) - + await self._handle_negotiation_response(received_message) + # Let UI know that we're connected + topic = b"ping" + data = json.dumps(True).encode() + if self.pub_socket: + await self.pub_socket.send_multipart([topic, data]) + return True except Exception as e: self.logger.warning("Error unpacking negotiation data: %s", e) retries += 1 - await asyncio.sleep(1) + await asyncio.sleep(settings.behaviour_settings.sleep_s) continue - # setup succeeded - break + return False - else: - self.logger.warning("Failed to set up %s after %d retries", self.name, max_retries) - return + async def _handle_negotiation_response(self, received_message): + for port_data in received_message["data"]: + id = port_data["id"] + port = port_data["port"] + bind = port_data["bind"] - # Set up ping behaviour - listen_behaviour = self.ListenBehaviour() - self.add_behaviour(listen_behaviour) + if not bind: + addr = f"tcp://localhost:{port}" + else: + addr = f"tcp://*:{port}" - # Let UI know that we're connected + match id: + case "main": + if addr != self._address: + assert self._req_socket is not None + if not bind: + self._req_socket.connect(addr) + else: + self._req_socket.bind(addr) + case "actuation": + ri_commands_agent = RobotSpeechAgent( + settings.agent_settings.robot_speech_name, + address=addr, + bind=bind, + ) + await ri_commands_agent.start() + case _: + self.logger.warning("Unhandled negotiation id: %s", id) + + async def stop(self): + if self._req_socket: + self._req_socket.close() + if self.pub_socket: + self.pub_socket.close() + await super().stop() + + async def _listen_loop(self): + """ + Run the listening (ping) loop indefinitely. + """ + while self._running: + if not self.connected: + await asyncio.sleep(settings.behaviour_settings.sleep_s) + continue + + # We need to listen and send pings. + message = {"endpoint": "ping", "data": {"id": "e.g. some reference id"}} + seconds_to_wait_total = settings.behaviour_settings.sleep_s + try: + assert self._req_socket is not None + await asyncio.wait_for( + self._req_socket.send_json(message), timeout=seconds_to_wait_total / 2 + ) + except TimeoutError: + self.logger.debug( + "Waited too long to send message - " + "we probably dont have any receivers... but let's check!" + ) + + # Wait up to {seconds_to_wait_total/2} seconds for a reply + try: + assert self._req_socket is not None + message = await asyncio.wait_for( + self._req_socket.recv_json(), timeout=seconds_to_wait_total / 2 + ) + + self.logger.debug(f'Received message "{message}" from RI.') + if "endpoint" not in message: + self.logger.warning("No received endpoint in message, expected ping endpoint.") + continue + + # See what endpoint we received + match message["endpoint"]: + case "ping": + topic = b"ping" + data = json.dumps(True).encode() + if self.pub_socket is not None: + await self.pub_socket.send_multipart([topic, data]) + await asyncio.sleep(settings.behaviour_settings.sleep_s) + case _: + self.logger.debug( + "Received message with topic different than ping, while ping expected." + ) + # We didnt get a reply + except TimeoutError: + self.logger.info( + f"No ping retrieved in {seconds_to_wait_total} seconds, " + "sending UI disconnection event and attempting to restart." + ) + await self._handle_disconnection() + continue + except Exception: + self.logger.error("Error while waiting for ping message.", exc_info=True) + raise + + async def _handle_disconnection(self): + self.connected = False + + # Tell UI we're disconnected. topic = b"ping" - data = json.dumps(True).encode() - if self.pub_socket is None: - self.logger.warning("Communication agent pub socket not correctly initialized.") - else: + data = json.dumps(False).encode() + if self.pub_socket: try: await asyncio.wait_for(self.pub_socket.send_multipart([topic, data]), 5) except TimeoutError: - self.logger.warning("Initial connection ping for router timed out in com_ri_agent.") + self.logger.warning("Connection ping for router timed out.") - # Make sure to start listening now that we're connected. - self.connected = True - self.logger.info("Finished setting up %s", self.jid) + # Try to reboot/renegotiate + self.logger.debug("Restarting communication negotiation.") + if await self._negotiate_connection(max_retries=1): + self.connected = True diff --git a/src/control_backend/agents/llm/llm_agent.py b/src/control_backend/agents/llm/llm_agent.py index eae41fd..a6950f2 100644 --- a/src/control_backend/agents/llm/llm_agent.py +++ b/src/control_backend/agents/llm/llm_agent.py @@ -3,10 +3,9 @@ import re from collections.abc import AsyncGenerator import httpx -from spade.behaviour import CyclicBehaviour -from spade.message import Message from control_backend.agents import BaseAgent +from control_backend.core.agent_system import InternalMessage from control_backend.core.config import settings from .llm_instructions import LLMInstructions @@ -19,143 +18,114 @@ class LLMAgent(BaseAgent): and responds with processed LLM output. """ - class ReceiveMessageBehaviour(CyclicBehaviour): - """ - Cyclic behaviour to continuously listen for incoming messages from - the BDI Core Agent and handle them. - """ - - async def run(self): - """ - Receives SPADE messages and processes only those originating from the - configured BDI agent. - """ - msg = await self.receive() - - sender = msg.sender.node - self.agent.logger.debug( - "Received message: %s from %s", - msg.body, - sender, - ) - - if sender == settings.agent_settings.bdi_core_name: - self.agent.logger.debug("Processing message from BDI Core Agent") - await self._process_bdi_message(msg) - else: - self.agent.logger.debug("Message ignored (not from BDI Core Agent)") - - async def _process_bdi_message(self, message: Message): - """ - Forwards user text from the BDI to the LLM and replies with the generated text in chunks - separated by punctuation. - """ - user_text = message.body - # Consume the streaming generator and send a reply for every chunk - async for chunk in self._query_llm(user_text): - await self._reply(chunk) - self.agent.logger.debug( - "Finished processing BDI message. Response sent in chunks to BDI Core Agent." - ) - - async def _reply(self, msg: str): - """ - Sends a response message back to the BDI Core Agent. - """ - reply = Message( - to=settings.agent_settings.bdi_core_name + "@" + settings.agent_settings.host, - body=msg, - ) - await self.send(reply) - - async def _query_llm(self, prompt: str) -> AsyncGenerator[str]: - """ - Sends a chat completion request to the local LLM service and streams the response by - yielding fragments separated by punctuation like. - - :param prompt: Input text prompt to pass to the LLM. - :yield: Fragments of the LLM-generated content. - """ - instructions = LLMInstructions( - "- Be friendly and respectful.\n" - "- Make the conversation feel natural and engaging.\n" - "- Speak like a pirate.\n" - "- When the user asks what you can do, tell them.", - "- Try to learn the user's name during conversation.\n" - "- Suggest playing a game of asking yes or no questions where you think of a word " - "and the user must guess it.", - ) - messages = [ - { - "role": "developer", - "content": instructions.build_developer_instruction(), - }, - { - "role": "user", - "content": prompt, - }, - ] - - try: - current_chunk = "" - async for token in self._stream_query_llm(messages): - current_chunk += token - - # Stream the message in chunks separated by punctuation. - # We include the delimiter in the emitted chunk for natural flow. - pattern = re.compile(r".*?(?:,|;|:|—|–|\.{3}|…|\.|\?|!)\s*", re.DOTALL) - for m in pattern.finditer(current_chunk): - chunk = m.group(0) - if chunk: - yield current_chunk - current_chunk = "" - - # Yield any remaining tail - if current_chunk: - yield current_chunk - except httpx.HTTPError as err: - self.agent.logger.error("HTTP error.", exc_info=err) - yield "LLM service unavailable." - except Exception as err: - self.agent.logger.error("Unexpected error.", exc_info=err) - yield "Error processing the request." - - async def _stream_query_llm(self, messages) -> AsyncGenerator[str]: - """Raises httpx.HTTPError when the API gives an error.""" - async with httpx.AsyncClient(timeout=None) as client: - async with client.stream( - "POST", - settings.llm_settings.local_llm_url, - json={ - "model": settings.llm_settings.local_llm_model, - "messages": messages, - "temperature": 0.3, - "stream": True, - }, - ) as response: - response.raise_for_status() - - async for line in response.aiter_lines(): - if not line or not line.startswith("data: "): - continue - - data = line[len("data: ") :] - if data.strip() == "[DONE]": - break - - try: - event = json.loads(data) - delta = event.get("choices", [{}])[0].get("delta", {}).get("content") - if delta: - yield delta - except json.JSONDecodeError: - self.agent.logger.error("Failed to parse LLM response: %s", data) - async def setup(self): + self.logger.info("Setting up %s.", self.name) + + async def handle_message(self, msg: InternalMessage): + if msg.sender == settings.agent_settings.bdi_core_name: + self.logger.debug("Processing message from BDI core.") + await self._process_bdi_message(msg) + else: + self.logger.debug("Message ignored (not from BDI core.") + + async def _process_bdi_message(self, message: InternalMessage): + user_text = message.body + async for chunk in self._query_llm(user_text): + await self._send_reply(chunk) + self.logger.debug( + "Finished processing BDI message. Response sent in chunks to BDI core." + ) + + async def _send_reply(self, msg: str): """ - Sets up the SPADE behaviour to filter and process messages from the - BDI Core Agent. + Sends a response message back to the BDI Core Agent. """ - behaviour = self.ReceiveMessageBehaviour() - self.add_behaviour(behaviour) - self.logger.info("LLMAgent setup complete") + reply = InternalMessage( + to=settings.agent_settings.bdi_core_name, + sender=self.name, + body=msg, + ) + await self.send(reply) + + async def _query_llm(self, prompt: str) -> AsyncGenerator[str]: + """ + Sends a chat completion request to the local LLM service and streams the response by + yielding fragments separated by punctuation like. + + :param prompt: Input text prompt to pass to the LLM. + :yield: Fragments of the LLM-generated content. + """ + instructions = LLMInstructions( + "- Be friendly and respectful.\n" + "- Make the conversation feel natural and engaging.\n" + "- Speak like a pirate.\n" + "- When the user asks what you can do, tell them.", + "- Try to learn the user's name during conversation.\n" + "- Suggest playing a game of asking yes or no questions where you think of a word " + "and the user must guess it.", + ) + messages = [ + { + "role": "developer", + "content": instructions.build_developer_instruction(), + }, + { + "role": "user", + "content": prompt, + }, + ] + + try: + current_chunk = "" + async for token in self._stream_query_llm(messages): + current_chunk += token + + # Stream the message in chunks separated by punctuation. + # We include the delimiter in the emitted chunk for natural flow. + pattern = re.compile(r".*?(?:,|;|:|—|–|\.{3}|…|\.|\?|!)\s*", re.DOTALL) + for m in pattern.finditer(current_chunk): + chunk = m.group(0) + if chunk: + yield current_chunk + current_chunk = "" + + # Yield any remaining tail + if current_chunk: + yield current_chunk + except httpx.HTTPError as err: + self.logger.error("HTTP error.", exc_info=err) + yield "LLM service unavailable." + except Exception as err: + self.logger.error("Unexpected error.", exc_info=err) + yield "Error processing the request." + + async def _stream_query_llm(self, messages) -> AsyncGenerator[str]: + """Raises httpx.HTTPError when the API gives an error.""" + async with httpx.AsyncClient(timeout=None) as client: + async with client.stream( + "POST", + settings.llm_settings.local_llm_url, + json={ + "model": settings.llm_settings.local_llm_model, + "messages": messages, + "temperature": 0.3, + "stream": True, + }, + ) as response: + response.raise_for_status() + + async for line in response.aiter_lines(): + if not line or not line.startswith("data: "): + continue + + data = line[len("data: ") :] + if data.strip() == "[DONE]": + break + + try: + event = json.loads(data) + delta = event.get("choices", [{}])[0].get("delta", {}).get("content") + if delta: + yield delta + except json.JSONDecodeError: + self.logger.error("Failed to parse LLM response: %s", data) diff --git a/src/control_backend/agents/perception/transcription_agent/transcription_agent.py b/src/control_backend/agents/perception/transcription_agent/transcription_agent.py index 44c1387..d0b0396 100644 --- a/src/control_backend/agents/perception/transcription_agent/transcription_agent.py +++ b/src/control_backend/agents/perception/transcription_agent/transcription_agent.py @@ -3,10 +3,9 @@ import asyncio import numpy as np import zmq import zmq.asyncio as azmq -from spade.behaviour import CyclicBehaviour -from spade.message import Message from control_backend.agents import BaseAgent +from control_backend.core.agent_system import InternalMessage from control_backend.core.config import settings from .speech_recognizer import SpeechRecognizer @@ -19,53 +18,31 @@ class TranscriptionAgent(BaseAgent): """ def __init__(self, audio_in_address: str): - jid = settings.agent_settings.transcription_name + "@" + settings.agent_settings.host - super().__init__(jid, settings.agent_settings.transcription_name) + super().__init__(settings.agent_settings.transcription_name) self.audio_in_address = audio_in_address self.audio_in_socket: azmq.Socket | None = None + self.speech_recognizer = None + self._concurrency = None - class TranscribingBehaviour(CyclicBehaviour): - def __init__(self, audio_in_socket: azmq.Socket): - super().__init__() - max_concurrent_tasks = settings.behaviour_settings.transcription_max_concurrent_tasks - self.audio_in_socket = audio_in_socket - self.speech_recognizer = SpeechRecognizer.best_type() - self._concurrency = asyncio.Semaphore(max_concurrent_tasks) + async def setup(self): + self.logger.info("Setting up %s", self.name) - def warmup(self): - """Load the transcription model into memory to speed up the first transcription.""" - self.speech_recognizer.load_model() + self._connect_audio_in_socket() - async def _transcribe(self, audio: np.ndarray) -> str: - async with self._concurrency: - return await asyncio.to_thread(self.speech_recognizer.recognize_speech, audio) + # Initialize recognizer and semaphore + max_concurrent_tasks = settings.behaviour_settings.transcription_max_concurrent_tasks + self._concurrency = asyncio.Semaphore(max_concurrent_tasks) + self.speech_recognizer = SpeechRecognizer.best_type() + self.speech_recognizer.load_model() # Warmup - async def _share_transcription(self, transcription: str): - """Share a transcription to the other agents that depend on it.""" - receiver_jids = [ - settings.agent_settings.text_belief_extractor_name - + "@" - + settings.agent_settings.host, - ] # Set message receivers here + # Start background loop + self.add_behavior(self._transcribing_loop()) - for receiver_jid in receiver_jids: - message = Message(to=receiver_jid, body=transcription) - await self.send(message) - - async def run(self) -> None: - audio = await self.audio_in_socket.recv() - audio = np.frombuffer(audio, dtype=np.float32) - speech = await self._transcribe(audio) - if not speech: - self.agent.logger.info("Nothing transcribed.") - return - - self.agent.logger.info("Transcribed speech: %s", speech) - - await self._share_transcription(speech) + self.logger.info("Finished setting up %s", self.name) async def stop(self): + assert self.audio_in_socket is not None self.audio_in_socket.close() self.audio_in_socket = None return await super().stop() @@ -75,13 +52,37 @@ class TranscriptionAgent(BaseAgent): self.audio_in_socket.setsockopt_string(zmq.SUBSCRIBE, "") self.audio_in_socket.connect(self.audio_in_address) - async def setup(self): - self.logger.info("Setting up %s", self.jid) + async def _transcribe(self, audio: np.ndarray) -> str: + assert self._concurrency is not None and self.speech_recognizer is not None + async with self._concurrency: + return await asyncio.to_thread(self.speech_recognizer.recognize_speech, audio) - self._connect_audio_in_socket() + async def _share_transcription(self, transcription: str): + """Share a transcription to the other agents that depend on it.""" + receiver_names = [ + settings.agent_settings.text_belief_extractor_name, + ] - transcribing = self.TranscribingBehaviour(self.audio_in_socket) - transcribing.warmup() - self.add_behaviour(transcribing) + for receiver_name in receiver_names: + message = InternalMessage( + to=receiver_name, + sender=self.name, + body=transcription, + ) + await self.send(message) - self.logger.info("Finished setting up %s", self.jid) + async def _transcribing_loop(self) -> None: + while self._running: + try: + assert self.audio_in_socket is not None + audio_data = await self.audio_in_socket.recv() + audio = np.frombuffer(audio_data, dtype=np.float32) + speech = await self._transcribe(audio) + if not speech: + self.logger.info("Nothing transcribed.") + continue + + self.logger.info("Transcribed speech: %s", speech) + await self._share_transcription(speech) + except Exception as e: + self.logger.error(f"Error in transcription loop: {e}") diff --git a/src/control_backend/agents/perception/vad_agent.py b/src/control_backend/agents/perception/vad_agent.py index 7c9d513..374ffa6 100644 --- a/src/control_backend/agents/perception/vad_agent.py +++ b/src/control_backend/agents/perception/vad_agent.py @@ -1,8 +1,9 @@ +import asyncio + import numpy as np import torch import zmq import zmq.asyncio as azmq -from spade.behaviour import CyclicBehaviour from control_backend.agents import BaseAgent from control_backend.core.config import settings @@ -26,7 +27,7 @@ class SocketPoller[T]: :param timeout_ms: A timeout in milliseconds to wait for data. """ self.socket = socket - self.poller = zmq.Poller() + self.poller = azmq.Poller() self.poller.register(self.socket, zmq.POLLIN) self.timeout_ms = timeout_ms @@ -38,81 +39,12 @@ class SocketPoller[T]: :return: Data from the socket or None. """ timeout_ms = timeout_ms or self.timeout_ms - socks = dict(self.poller.poll(timeout_ms)) + socks = dict(await self.poller.poll(timeout_ms)) if socks.get(self.socket) == zmq.POLLIN: return await self.socket.recv() return None -class StreamingBehaviour(CyclicBehaviour): - def __init__(self, audio_in_socket: azmq.Socket, audio_out_socket: azmq.Socket): - super().__init__() - self.audio_in_poller = SocketPoller[bytes](audio_in_socket) - self.model, _ = torch.hub.load( - repo_or_dir=settings.vad_settings.repo_or_dir, - model=settings.vad_settings.model_name, - force_reload=False, - ) - self.audio_out_socket = audio_out_socket - - self.audio_buffer = np.array([], dtype=np.float32) - self.i_since_speech = ( - settings.behaviour_settings.vad_initial_since_speech - ) # Used to allow small pauses in speech - self._ready = False - - async def reset(self): - """Clears the ZeroMQ queue and tells this behavior to start.""" - discarded = 0 - # Poll for the shortest amount of time possible to clear the queue - while await self.audio_in_poller.poll(1) is not None: - discarded += 1 - self.agent.logger.info(f"Discarded {discarded} audio packets before starting.") - self._ready = True - - async def run(self) -> None: - if not self._ready: - return - - data = await self.audio_in_poller.poll() - if data is None: - if len(self.audio_buffer) > 0: - self.agent.logger.debug( - "No audio data received. Discarding buffer until new data arrives." - ) - self.audio_buffer = np.array([], dtype=np.float32) - self.i_since_speech = settings.behaviour_settings.vad_initial_since_speech - return - - # copy otherwise Torch will be sad that it's immutable - chunk = np.frombuffer(data, dtype=np.float32).copy() - prob = self.model(torch.from_numpy(chunk), settings.vad_settings.sample_rate_hz).item() - non_speech_patience = settings.behaviour_settings.vad_non_speech_patience_chunks - prob_threshold = settings.behaviour_settings.vad_prob_threshold - - if prob > prob_threshold: - if self.i_since_speech > non_speech_patience: - self.agent.logger.debug("Speech started.") - self.audio_buffer = np.append(self.audio_buffer, chunk) - self.i_since_speech = 0 - return - self.i_since_speech += 1 - - # prob < 0.5, so speech maybe ended. Wait a bit more before to be more certain - if self.i_since_speech <= non_speech_patience: - self.audio_buffer = np.append(self.audio_buffer, chunk) - return - - # Speech probably ended. Make sure we have a usable amount of data. - if len(self.audio_buffer) >= 3 * len(chunk): - self.agent.logger.debug("Speech ended.") - await self.audio_out_socket.send(self.audio_buffer[: -2 * len(chunk)].tobytes()) - - # At this point, we know that the speech has ended. - # Prepend the last chunk that had no speech, for a more fluent boundary - self.audio_buffer = chunk - - class VADAgent(BaseAgent): """ An agent which listens to an audio stream, does Voice Activity Detection (VAD), and sends @@ -120,16 +52,54 @@ class VADAgent(BaseAgent): """ def __init__(self, audio_in_address: str, audio_in_bind: bool): - jid = settings.agent_settings.vad_name + "@" + settings.agent_settings.host - super().__init__(jid, settings.agent_settings.vad_name) + super().__init__(settings.agent_settings.vad_name) self.audio_in_address = audio_in_address self.audio_in_bind = audio_in_bind self.audio_in_socket: azmq.Socket | None = None self.audio_out_socket: azmq.Socket | None = None + self.audio_in_poller: SocketPoller | None = None - self.streaming_behaviour: StreamingBehaviour | None = None + self.audio_buffer = np.array([], dtype=np.float32) + self.i_since_speech = settings.behaviour_settings.vad_initial_since_speech + self._ready = asyncio.Event() + self.model = None + + async def setup(self): + self.logger.info("Setting up %s", self.name) + + self._connect_audio_in_socket() + + audio_out_port = self._connect_audio_out_socket() + if audio_out_port is None: + self.logger.error("Could not bind output socket, stopping.") + await self.stop() + return + audio_out_address = f"tcp://localhost:{audio_out_port}" + + # Initialize VAD model + try: + self.model, _ = torch.hub.load( + repo_or_dir=settings.vad_settings.repo_or_dir, + model=settings.vad_settings.model_name, + force_reload=False, + ) + except Exception: + self.logger.exception("Failed to load VAD model.") + await self.stop() + return + + # Warmup/reset + await self.reset_stream() + + self.add_behavior(self._streaming_loop()) + + # Start agents dependent on the output audio fragments here + transcriber = TranscriptionAgent(audio_out_address) + await transcriber.start() + + self.logger.info("Finished setting up %s", self.name) async def stop(self): """ @@ -141,7 +111,7 @@ class VADAgent(BaseAgent): if self.audio_out_socket is not None: self.audio_out_socket.close() self.audio_out_socket = None - return await super().stop() + await super().stop() def _connect_audio_in_socket(self): self.audio_in_socket = azmq.Context.instance().socket(zmq.SUB) @@ -156,28 +126,64 @@ class VADAgent(BaseAgent): """Returns the port bound, or None if binding failed.""" try: self.audio_out_socket = azmq.Context.instance().socket(zmq.PUB) - return self.audio_out_socket.bind_to_random_port("tcp://*", max_tries=100) + return self.audio_out_socket.bind_to_random_port("tcp://localhost", max_tries=100) except zmq.ZMQBindError: self.logger.error("Failed to bind an audio output socket after 100 tries.") self.audio_out_socket = None return None - async def setup(self): - self.logger.info("Setting up %s", self.jid) + async def reset_stream(self): + """ + Clears the ZeroMQ queue and sets ready state. + """ + discarded = 0 + assert self.audio_in_poller is not None + while await self.audio_in_poller.poll(1) is not None: + discarded += 1 + self.logger.info(f"Discarded {discarded} audio packets before starting.") + self._ready.set() - self._connect_audio_in_socket() + async def _streaming_loop(self): + await self._ready.wait() + while self._running: + assert self.audio_in_poller is not None + data = await self.audio_in_poller.poll() + if data is None: + if len(self.audio_buffer) > 0: + self.logger.debug( + "No audio data received. Discarding buffer until new data arrives." + ) + self.audio_buffer = np.array([], dtype=np.float32) + self.i_since_speech = settings.behaviour_settings.vad_initial_since_speech + continue - audio_out_port = self._connect_audio_out_socket() - if audio_out_port is None: - await self.stop() - return - audio_out_address = f"tcp://localhost:{audio_out_port}" + # copy otherwise Torch will be sad that it's immutable + chunk = np.frombuffer(data, dtype=np.float32).copy() + assert self.model is not None + prob = self.model(torch.from_numpy(chunk), settings.vad_settings.sample_rate_hz).item() + non_speech_patience = settings.behaviour_settings.vad_non_speech_patience_chunks + prob_threshold = settings.behaviour_settings.vad_prob_threshold - self.streaming_behaviour = StreamingBehaviour(self.audio_in_socket, self.audio_out_socket) - self.add_behaviour(self.streaming_behaviour) + if prob > prob_threshold: + if self.i_since_speech > non_speech_patience: + self.logger.debug("Speech started.") + self.audio_buffer = np.append(self.audio_buffer, chunk) + self.i_since_speech = 0 + continue - # Start agents dependent on the output audio fragments here - transcriber = TranscriptionAgent(audio_out_address) - await transcriber.start() + self.i_since_speech += 1 - self.logger.info("Finished setting up %s", self.jid) + # prob < threshold, so speech maybe ended. Wait a bit more before to be more certain + if self.i_since_speech <= non_speech_patience: + self.audio_buffer = np.append(self.audio_buffer, chunk) + continue + + # Speech probably ended. Make sure we have a usable amount of data. + if len(self.audio_buffer) >= 3 * len(chunk): + self.logger.debug("Speech ended.") + assert self.audio_out_socket is not None + await self.audio_out_socket.send(self.audio_buffer[: -2 * len(chunk)].tobytes()) + + # At this point, we know that the speech has ended. + # Prepend the last chunk that had no speech, for a more fluent boundary + self.audio_buffer = chunk diff --git a/src/control_backend/core/agent_system.py b/src/control_backend/core/agent_system.py new file mode 100644 index 0000000..b1d8456 --- /dev/null +++ b/src/control_backend/core/agent_system.py @@ -0,0 +1,142 @@ +import asyncio +import logging +from abc import ABC, abstractmethod +from asyncio import Task +from collections.abc import Coroutine + +import zmq +import zmq.asyncio as azmq + +from control_backend.core.config import settings +from control_backend.schemas.internal_message import InternalMessage + +# Central directory to resolve agent names to instances +_agent_directory: dict[str, "BaseAgent"] = {} + + +class AgentDirectory: + """ + Helper class to keep track of which agents are registered. + Used for handling message routing. + """ + + @staticmethod + def register(name: str, agent: "BaseAgent"): + _agent_directory[name] = agent + + @staticmethod + def get(name: str) -> "BaseAgent | None": + return _agent_directory.get(name) + + +class BaseAgent(ABC): + """ + Abstract base class for all agents. To make a new agent, inherit from + `control_backend.agents.BaseAgent`, not this class. That ensures that a + logger is present with the correct name pattern. + + When subclassing, the `setup()` method needs to be overwritten. To handle + messages from other agents, overwrite the `handle_message()` method. To + send messages to other agents, use the `send()` method. To add custom + behaviors/tasks to the agent, use the `add_background_task()` method. + """ + + logger: logging.Logger + + def __init__(self, name: str): + self.name = name + self.inbox: asyncio.Queue[InternalMessage] = asyncio.Queue() + self._tasks: set[asyncio.Task] = set() + self._running = False + + # Register immediately + AgentDirectory.register(name, self) + + @abstractmethod + async def setup(self): + """Overwrite this to initialize resources.""" + pass + + async def start(self): + """Starts the agent and its loops.""" + self.logger.info(f"Starting agent {self.name}") + self._running = True + + context = azmq.Context.instance() + + # Setup the internal publishing socket + self._internal_pub_socket = context.socket(zmq.PUB) + self._internal_pub_socket.connect(settings.zmq_settings.internal_pub_address) + + # Setup the internal receiving socket + self._internal_sub_socket = context.socket(zmq.SUB) + self._internal_sub_socket.connect(settings.zmq_settings.internal_sub_address) + self._internal_sub_socket.subscribe(f"internal/{self.name}") + + await self.setup() + + # Start processing inbox and ZMQ messages + self.add_behavior(self._process_inbox()) + self.add_behavior(self._receive_internal_zmq_loop()) + + async def stop(self): + """Stops the agent.""" + self._running = False + for task in self._tasks: + task.cancel() + self.logger.info(f"Agent {self.name} stopped") + + async def send(self, message: InternalMessage): + """ + Sends a message to another agent. + """ + target = AgentDirectory.get(message.to) + if target: + await target.inbox.put(message) + self.logger.debug(f"Sent message {message.body} to {message.to} via regular inbox.") + else: + # Apparently target agent is on a different process, send via ZMQ + topic = f"internal/{message.to}".encode() + body = message.model_dump_json().encode() + await self._internal_pub_socket.send_multipart([topic, body]) + self.logger.debug(f"Sent message {message.body} to {message.to} via ZMQ.") + + async def _process_inbox(self): + """Default loop: equivalent to a CyclicBehaviour receiving messages.""" + while self._running: + msg = await self.inbox.get() + self.logger.debug(f"Received message from {msg.sender}.") + await self.handle_message(msg) + + async def _receive_internal_zmq_loop(self): + """ + Listens for internal messages sent from agents on another process via ZMQ + and puts them into the normal inbox. + """ + while self._running: + try: + _, body = await self._internal_sub_socket.recv_multipart() + + msg = InternalMessage.model_validate_json(body) + + await self.inbox.put(msg) + except asyncio.CancelledError: + break + except Exception: + self.logger.exception("Could not process ZMQ message.") + + async def handle_message(self, msg: InternalMessage): + """Override this to handle incoming messages.""" + raise NotImplementedError + + def add_behavior(self, coro: Coroutine) -> Task: + """ + Helper to add a behavior to the agent. To add asynchronous behavior to an agent, define + an `async` function and add it to the task list by calling :func:`add_behavior` + with it. This should happen in the :func:`setup` method of the agent. For an example, see: + :func:`~control_backend.agents.bdi.BDICoreAgent`. + """ + task = asyncio.create_task(coro) + self._tasks.add(task) + task.add_done_callback(self._tasks.discard) + return task diff --git a/src/control_backend/core/config.py b/src/control_backend/core/config.py index 90ab512..bf131af 100644 --- a/src/control_backend/core/config.py +++ b/src/control_backend/core/config.py @@ -11,9 +11,6 @@ class ZMQSettings(BaseModel): class AgentSettings(BaseModel): - # connection settings - host: str = "localhost" - # agent names bdi_core_name: str = "bdi_core_agent" bdi_belief_collector_name: str = "belief_collector_agent" @@ -25,9 +22,6 @@ class AgentSettings(BaseModel): ri_communication_name: str = "ri_communication_agent" robot_speech_name: str = "robot_speech_agent" - # default SPADE port - default_spade_port: int = 5222 - class BehaviourSettings(BaseModel): sleep_s: float = 1.0 @@ -81,7 +75,7 @@ class Settings(BaseSettings): llm_settings: LLMSettings = LLMSettings() - model_config = SettingsConfigDict(env_file=".env") + model_config = SettingsConfigDict(env_file=".env", env_nested_delimiter="__") settings = Settings() diff --git a/src/control_backend/main.py b/src/control_backend/main.py index 04b34ff..b16e01d 100644 --- a/src/control_backend/main.py +++ b/src/control_backend/main.py @@ -7,7 +7,6 @@ from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware from zmq.asyncio import Context -# Act agents # BDI agents from control_backend.agents.bdi import ( BDIBeliefCollectorAgent, @@ -60,7 +59,6 @@ async def lifespan(app: FastAPI): # --- APPLICATION STARTUP --- setup_logging() logger.info("%s is starting up.", app.title) - logger.warning("testing extra", extra={"extra1": "one", "extra2": "two"}) # Initiate sockets proxy_thread = threading.Thread(target=setup_sockets) @@ -75,14 +73,12 @@ async def lifespan(app: FastAPI): # --- Initialize Agents --- logger.info("Initializing and starting agents.") + agents_to_start = { "RICommunicationAgent": ( RICommunicationAgent, { "name": settings.agent_settings.ri_communication_name, - "jid": f"{settings.agent_settings.ri_communication_name}" - f"@{settings.agent_settings.host}", - "password": settings.agent_settings.ri_communication_name, "address": settings.zmq_settings.ri_communication_address, "bind": True, }, @@ -91,16 +87,12 @@ async def lifespan(app: FastAPI): LLMAgent, { "name": settings.agent_settings.llm_name, - "jid": f"{settings.agent_settings.llm_name}@{settings.agent_settings.host}", - "password": settings.agent_settings.llm_name, }, ), "BDICoreAgent": ( BDICoreAgent, { "name": settings.agent_settings.bdi_core_name, - "jid": f"{settings.agent_settings.bdi_core_name}@{settings.agent_settings.host}", - "password": settings.agent_settings.bdi_core_name, "asl": "src/control_backend/agents/bdi/bdi_core_agent/rules.asl", }, ), @@ -108,18 +100,12 @@ async def lifespan(app: FastAPI): BDIBeliefCollectorAgent, { "name": settings.agent_settings.bdi_belief_collector_name, - "jid": f"{settings.agent_settings.bdi_belief_collector_name}@" - f"{settings.agent_settings.host}", - "password": settings.agent_settings.bdi_belief_collector_name, }, ), "TextBeliefExtractorAgent": ( TextBeliefExtractorAgent, { "name": settings.agent_settings.text_belief_extractor_name, - "jid": f"{settings.agent_settings.text_belief_extractor_name}@" - f"{settings.agent_settings.host}", - "password": settings.agent_settings.text_belief_extractor_name, }, ), "VADAgent": ( @@ -128,22 +114,25 @@ async def lifespan(app: FastAPI): ), } + agents = [] + vad_agent = None for name, (agent_class, kwargs) in agents_to_start.items(): try: logger.debug("Starting agent: %s", name) - agent_instance = agent_class(**{k: v for k, v in kwargs.items() if k != "name"}) + agent_instance = agent_class(**kwargs) await agent_instance.start() if isinstance(agent_instance, VADAgent): vad_agent = agent_instance + agents.append(agent_instance) logger.info("Agent '%s' started successfully.", name) except Exception as e: logger.error("Failed to start agent '%s': %s", name, e, exc_info=True) - # Consider if the application should continue if an agent fails to start. raise - await vad_agent.streaming_behaviour.reset() + assert vad_agent is not None + await vad_agent.reset_stream() logger.info("Application startup complete.") diff --git a/src/control_backend/schemas/belief_message.py b/src/control_backend/schemas/belief_message.py new file mode 100644 index 0000000..a5f7507 --- /dev/null +++ b/src/control_backend/schemas/belief_message.py @@ -0,0 +1,5 @@ +from pydantic import BaseModel + + +class BeliefMessage(BaseModel): + beliefs: dict[str, list[str]] diff --git a/src/control_backend/schemas/internal_message.py b/src/control_backend/schemas/internal_message.py new file mode 100644 index 0000000..0240d52 --- /dev/null +++ b/src/control_backend/schemas/internal_message.py @@ -0,0 +1,12 @@ +from pydantic import BaseModel + + +class InternalMessage(BaseModel): + """ + Represents a message to an agent. + """ + + to: str + sender: str + body: str + thread: str | None = None diff --git a/test/integration/agents/actuation/test_robot_speech_agent.py b/test/integration/agents/actuation/test_robot_speech_agent.py deleted file mode 100644 index 327415c..0000000 --- a/test/integration/agents/actuation/test_robot_speech_agent.py +++ /dev/null @@ -1,98 +0,0 @@ -import json -from unittest.mock import AsyncMock, MagicMock, patch - -import pytest -import zmq - -from control_backend.agents.actuation.robot_speech_agent import RobotSpeechAgent - - -@pytest.fixture -def zmq_context(mocker): - mock_context = mocker.patch( - "control_backend.agents.actuation.robot_speech_agent.zmq.Context.instance" - ) - mock_context.return_value = MagicMock() - return mock_context - - -@pytest.mark.asyncio -async def test_setup_bind(zmq_context, mocker): - """Test setup with bind=True""" - fake_socket = zmq_context.return_value.socket.return_value - - agent = RobotSpeechAgent("test@server", "password", address="tcp://localhost:5555", bind=True) - settings = mocker.patch("control_backend.agents.actuation.robot_speech_agent.settings") - settings.zmq_settings.internal_sub_address = "tcp://internal:1234" - - await agent.setup() - - fake_socket.bind.assert_any_call("tcp://localhost:5555") - fake_socket.connect.assert_any_call("tcp://internal:1234") - fake_socket.setsockopt.assert_any_call(zmq.SUBSCRIBE, b"command") - - # Ensure behaviour attached - assert any(isinstance(b, agent.SendZMQCommandsBehaviour) for b in agent.behaviours) - - -@pytest.mark.asyncio -async def test_setup_connect(zmq_context, mocker): - """Test setup with bind=False""" - fake_socket = zmq_context.return_value.socket.return_value - - agent = RobotSpeechAgent("test@server", "password", address="tcp://localhost:5555", bind=False) - settings = mocker.patch("control_backend.agents.actuation.robot_speech_agent.settings") - settings.zmq_settings.internal_sub_address = "tcp://internal:1234" - - await agent.setup() - - fake_socket.connect.assert_any_call("tcp://localhost:5555") - - -@pytest.mark.asyncio -async def test_send_commands_behaviour_valid_message(): - """Test behaviour with valid JSON message""" - fake_socket = AsyncMock() - message_dict = {"message": "hello"} - fake_socket.recv_multipart = AsyncMock( - return_value=(b"command", json.dumps(message_dict).encode("utf-8")) - ) - fake_socket.send_json = AsyncMock() - - agent = RobotSpeechAgent("test@server", "password") - agent.subsocket = fake_socket - agent.pubsocket = fake_socket - - behaviour = agent.SendZMQCommandsBehaviour() - behaviour.agent = agent - - with patch( - "control_backend.agents.actuation.robot_speech_agent.SpeechCommand" - ) as MockSpeechCommand: - mock_message = MagicMock() - MockSpeechCommand.model_validate.return_value = mock_message - - await behaviour.run() - - fake_socket.recv_multipart.assert_awaited() - fake_socket.send_json.assert_awaited_with(mock_message.model_dump()) - - -@pytest.mark.asyncio -async def test_send_commands_behaviour_invalid_message(): - """Test behaviour with invalid JSON message triggers error logging""" - fake_socket = AsyncMock() - fake_socket.recv_multipart = AsyncMock(return_value=(b"command", b"{invalid_json}")) - fake_socket.send_json = AsyncMock() - - agent = RobotSpeechAgent("test@server", "password") - agent.subsocket = fake_socket - agent.pubsocket = fake_socket - - behaviour = agent.SendZMQCommandsBehaviour() - behaviour.agent = agent - - await behaviour.run() - - fake_socket.recv_multipart.assert_awaited() - fake_socket.send_json.assert_not_awaited() diff --git a/test/integration/agents/communication/test_ri_communication_agent.py b/test/integration/agents/communication/test_ri_communication_agent.py deleted file mode 100644 index b82234b..0000000 --- a/test/integration/agents/communication/test_ri_communication_agent.py +++ /dev/null @@ -1,567 +0,0 @@ -import asyncio -from unittest.mock import ANY, AsyncMock, MagicMock, patch - -import pytest - -from control_backend.agents.communication.ri_communication_agent import RICommunicationAgent - - -def speech_agent_path(): - return "control_backend.agents.communication.ri_communication_agent.RobotSpeechAgent" - - -def fake_json_correct_negototiate_1(): - return AsyncMock( - return_value={ - "endpoint": "negotiate/ports", - "data": [ - {"id": "main", "port": 5555, "bind": False}, - {"id": "actuation", "port": 5556, "bind": True}, - ], - } - ) - - -def fake_json_correct_negototiate_2(): - return AsyncMock( - return_value={ - "endpoint": "negotiate/ports", - "data": [ - {"id": "main", "port": 5555, "bind": False}, - {"id": "actuation", "port": 5557, "bind": True}, - ], - } - ) - - -def fake_json_correct_negototiate_3(): - return AsyncMock( - return_value={ - "endpoint": "negotiate/ports", - "data": [ - {"id": "main", "port": 5555, "bind": True}, - {"id": "actuation", "port": 5557, "bind": True}, - ], - } - ) - - -def fake_json_correct_negototiate_4(): - # Different port, do bind - return AsyncMock( - return_value={ - "endpoint": "negotiate/ports", - "data": [ - {"id": "main", "port": 4555, "bind": True}, - {"id": "actuation", "port": 5557, "bind": True}, - ], - } - ) - - -def fake_json_correct_negototiate_5(): - # Different port, dont bind. - return AsyncMock( - return_value={ - "endpoint": "negotiate/ports", - "data": [ - {"id": "main", "port": 4555, "bind": False}, - {"id": "actuation", "port": 5557, "bind": True}, - ], - } - ) - - -def fake_json_wrong_negototiate_1(): - return AsyncMock(return_value={"endpoint": "ping", "data": ""}) - - -def fake_json_invalid_id_negototiate(): - return AsyncMock( - return_value={ - "endpoint": "negotiate/ports", - "data": [ - {"id": "banana", "port": 4555, "bind": False}, - {"id": "tomato", "port": 5557, "bind": True}, - ], - } - ) - - -@pytest.fixture -def zmq_context(mocker): - mock_context = mocker.patch( - "control_backend.agents.communication.ri_communication_agent.zmq.Context.instance" - ) - mock_context.return_value = MagicMock() - return mock_context - - -@pytest.mark.asyncio -async def test_setup_creates_socket_and_negotiate_1(zmq_context): - """ - Test the setup of the communication agent - """ - # --- Arrange --- - fake_socket = zmq_context.return_value.socket.return_value - fake_socket.send_json = AsyncMock() - fake_socket.recv_json = fake_json_correct_negototiate_1() - fake_socket.send_multipart = AsyncMock() - - # Mock ActSpeechAgent agent startup - with patch(speech_agent_path(), autospec=True) as MockCommandAgent: - fake_agent_instance = MockCommandAgent.return_value - fake_agent_instance.start = AsyncMock() - - # --- Act --- - agent = RICommunicationAgent( - "test@server", - "password", - address="tcp://localhost:5555", - bind=False, - ) - await agent.setup() - - # --- Assert --- - fake_socket.connect.assert_any_call("tcp://localhost:5555") - fake_socket.send_json.assert_any_call({"endpoint": "negotiate/ports", "data": {}}) - fake_socket.recv_json.assert_awaited() - fake_agent_instance.start.assert_awaited() - MockCommandAgent.assert_called_once_with( - ANY, # Server Name - ANY, # Server Password - address="tcp://*:5556", # derived from the 'port' value in negotiation - bind=True, - ) - # Ensure the agent attached a ListenBehaviour - assert any(isinstance(b, agent.ListenBehaviour) for b in agent.behaviours) - - -@pytest.mark.asyncio -async def test_setup_creates_socket_and_negotiate_2(zmq_context): - """ - Test the setup of the communication agent - """ - # --- Arrange --- - fake_socket = zmq_context.return_value.socket.return_value - fake_socket.send_json = AsyncMock() - fake_socket.recv_json = fake_json_correct_negototiate_2() - fake_socket.send_multipart = AsyncMock() - - # Mock ActSpeechAgent agent startup - with patch(speech_agent_path(), autospec=True) as MockCommandAgent: - fake_agent_instance = MockCommandAgent.return_value - fake_agent_instance.start = AsyncMock() - - # --- Act --- - agent = RICommunicationAgent( - "test@server", - "password", - address="tcp://localhost:5555", - bind=False, - ) - await agent.setup() - - # --- Assert --- - fake_socket.connect.assert_any_call("tcp://localhost:5555") - fake_socket.send_json.assert_any_call({"endpoint": "negotiate/ports", "data": {}}) - fake_socket.recv_json.assert_awaited() - fake_agent_instance.start.assert_awaited() - MockCommandAgent.assert_called_once_with( - ANY, # Server Name - ANY, # Server Password - address="tcp://*:5557", # derived from the 'port' value in negotiation - bind=True, - ) - # Ensure the agent attached a ListenBehaviour - assert any(isinstance(b, agent.ListenBehaviour) for b in agent.behaviours) - - -@pytest.mark.asyncio -async def test_setup_creates_socket_and_negotiate_3(zmq_context): - """ - Test the functionality of setup with incorrect negotiation message - """ - # --- Arrange --- - fake_socket = zmq_context.return_value.socket.return_value - fake_socket.send_json = AsyncMock() - fake_socket.recv_json = fake_json_wrong_negototiate_1() - fake_socket.send_multipart = AsyncMock() - - # Mock ActSpeechAgent agent startup - - # We are sending wrong negotiation info to the communication agent, - # so we should retry and expect a better response, within a limited time. - with patch(speech_agent_path(), autospec=True) as MockCommandAgent: - fake_agent_instance = MockCommandAgent.return_value - fake_agent_instance.start = AsyncMock() - # --- Act --- - - agent = RICommunicationAgent( - "test@server", - "password", - address="tcp://localhost:5555", - bind=False, - ) - await agent.setup(max_retries=1) - - # --- Assert --- - fake_socket.connect.assert_any_call("tcp://localhost:5555") - fake_socket.recv_json.assert_awaited() - - # Since it failed, there should not be any command agent. - fake_agent_instance.start.assert_not_awaited() - - # Ensure the agent did not attach a ListenBehaviour - assert not any(isinstance(b, agent.ListenBehaviour) for b in agent.behaviours) - - -@pytest.mark.asyncio -async def test_setup_creates_socket_and_negotiate_4(zmq_context): - """ - Test the setup of the communication agent with different bind value - """ - # --- Arrange --- - fake_socket = zmq_context.return_value.socket.return_value - fake_socket.send_json = AsyncMock() - fake_socket.recv_json = fake_json_correct_negototiate_3() - fake_socket.send_multipart = AsyncMock() - - # Mock ActSpeechAgent agent startup - with patch(speech_agent_path(), autospec=True) as MockCommandAgent: - fake_agent_instance = MockCommandAgent.return_value - fake_agent_instance.start = AsyncMock() - # --- Act --- - agent = RICommunicationAgent( - "test@server", - "password", - address="tcp://localhost:5555", - bind=True, - ) - await agent.setup() - - # --- Assert --- - fake_socket.bind.assert_any_call("tcp://localhost:5555") - fake_socket.send_json.assert_any_call({"endpoint": "negotiate/ports", "data": {}}) - fake_socket.recv_json.assert_awaited() - fake_agent_instance.start.assert_awaited() - MockCommandAgent.assert_called_once_with( - ANY, # Server Name - ANY, # Server Password - address="tcp://*:5557", # derived from the 'port' value in negotiation - bind=True, - ) - # Ensure the agent attached a ListenBehaviour - assert any(isinstance(b, agent.ListenBehaviour) for b in agent.behaviours) - - -@pytest.mark.asyncio -async def test_setup_creates_socket_and_negotiate_5(zmq_context): - """ - Test the setup of the communication agent - """ - # --- Arrange --- - fake_socket = zmq_context.return_value.socket.return_value - fake_socket.send_json = AsyncMock() - fake_socket.recv_json = fake_json_correct_negototiate_4() - fake_socket.send_multipart = AsyncMock() - - # Mock ActSpeechAgent agent startup - with patch(speech_agent_path(), autospec=True) as MockCommandAgent: - fake_agent_instance = MockCommandAgent.return_value - fake_agent_instance.start = AsyncMock() - # --- Act --- - agent = RICommunicationAgent( - "test@server", - "password", - address="tcp://localhost:5555", - bind=False, - ) - await agent.setup() - - # --- Assert --- - fake_socket.connect.assert_any_call("tcp://localhost:5555") - fake_socket.send_json.assert_any_call({"endpoint": "negotiate/ports", "data": {}}) - fake_socket.recv_json.assert_awaited() - fake_agent_instance.start.assert_awaited() - MockCommandAgent.assert_called_once_with( - ANY, # Server Name - ANY, # Server Password - address="tcp://*:5557", # derived from the 'port' value in negotiation - bind=True, - ) - # Ensure the agent attached a ListenBehaviour - assert any(isinstance(b, agent.ListenBehaviour) for b in agent.behaviours) - - -@pytest.mark.asyncio -async def test_setup_creates_socket_and_negotiate_6(zmq_context): - """ - Test the setup of the communication agent - """ - # --- Arrange --- - fake_socket = zmq_context.return_value.socket.return_value - fake_socket.send_json = AsyncMock() - fake_socket.recv_json = fake_json_correct_negototiate_5() - fake_socket.send_multipart = AsyncMock() - - # Mock ActSpeechAgent agent startup - with patch(speech_agent_path(), autospec=True) as MockCommandAgent: - fake_agent_instance = MockCommandAgent.return_value - fake_agent_instance.start = AsyncMock() - # --- Act --- - agent = RICommunicationAgent( - "test@server", - "password", - address="tcp://localhost:5555", - bind=False, - ) - await agent.setup() - - # --- Assert --- - fake_socket.connect.assert_any_call("tcp://localhost:5555") - fake_socket.send_json.assert_any_call({"endpoint": "negotiate/ports", "data": {}}) - fake_socket.recv_json.assert_awaited() - fake_agent_instance.start.assert_awaited() - MockCommandAgent.assert_called_once_with( - ANY, # Server Name - ANY, # Server Password - address="tcp://*:5557", # derived from the 'port' value in negotiation - bind=True, - ) - # Ensure the agent attached a ListenBehaviour - assert any(isinstance(b, agent.ListenBehaviour) for b in agent.behaviours) - - -@pytest.mark.asyncio -async def test_setup_creates_socket_and_negotiate_7(zmq_context): - """ - Test the functionality of setup with incorrect id - """ - # --- Arrange --- - fake_socket = zmq_context.return_value.socket.return_value - fake_socket.send_json = AsyncMock() - fake_socket.recv_json = fake_json_invalid_id_negototiate() - fake_socket.send_multipart = AsyncMock() - - # Mock ActSpeechAgent agent startup - - # We are sending wrong negotiation info to the communication agent, - # so we should retry and expect a better response, within a limited time. - with patch(speech_agent_path(), autospec=True) as MockCommandAgent: - fake_agent_instance = MockCommandAgent.return_value - fake_agent_instance.start = AsyncMock() - - # --- Act --- - - agent = RICommunicationAgent( - "test@server", - "password", - address="tcp://localhost:5555", - bind=False, - ) - await agent.setup(max_retries=1) - - # --- Assert --- - fake_socket.connect.assert_any_call("tcp://localhost:5555") - fake_socket.recv_json.assert_awaited() - - # Since it failed, there should not be any command agent. - fake_agent_instance.start.assert_not_awaited() - - -@pytest.mark.asyncio -async def test_setup_creates_socket_and_negotiate_timeout(zmq_context): - """ - Test the functionality of setup with incorrect negotiation message - """ - # --- Arrange --- - fake_socket = zmq_context.return_value.socket.return_value - fake_socket.send_json = AsyncMock() - fake_socket.recv_json = AsyncMock(side_effect=asyncio.TimeoutError) - fake_socket.send_multipart = AsyncMock() - - with patch(speech_agent_path(), autospec=True) as MockCommandAgent: - fake_agent_instance = MockCommandAgent.return_value - fake_agent_instance.start = AsyncMock() - - # --- Act --- - - agent = RICommunicationAgent( - "test@server", - "password", - address="tcp://localhost:5555", - bind=False, - ) - await agent.setup(max_retries=1) - - # --- Assert --- - fake_socket.connect.assert_any_call("tcp://localhost:5555") - - # Since it failed, there should not be any command agent. - fake_agent_instance.start.assert_not_awaited() - - # Ensure the agent did not attach a ListenBehaviour - assert not any(isinstance(b, agent.ListenBehaviour) for b in agent.behaviours) - - -@pytest.mark.asyncio -async def test_listen_behaviour_ping_correct(): - fake_socket = AsyncMock() - fake_socket.send_json = AsyncMock() - fake_socket.recv_json = AsyncMock(return_value={"endpoint": "ping", "data": {}}) - fake_socket.send_multipart = AsyncMock() - - agent = RICommunicationAgent("test@server", "password") - agent._req_socket = fake_socket - agent.connected = True - - behaviour = agent.ListenBehaviour() - agent.add_behaviour(behaviour) - - await behaviour.run() - - fake_socket.send_json.assert_awaited() - fake_socket.recv_json.assert_awaited() - - -@pytest.mark.asyncio -async def test_listen_behaviour_ping_wrong_endpoint(): - """ - Test if our listen behaviour can work with wrong messages (wrong endpoint) - """ - fake_socket = AsyncMock() - fake_socket.send_json = AsyncMock() - - # This is a message for the wrong endpoint >:( - fake_socket.recv_json = AsyncMock( - return_value={ - "endpoint": "negotiate/ports", - "data": [ - {"id": "main", "port": 5555, "bind": False}, - {"id": "actuation", "port": 5556, "bind": True}, - ], - } - ) - fake_pub_socket = AsyncMock() - - agent = RICommunicationAgent("test@server", "password", fake_pub_socket) - agent._req_socket = fake_socket - agent.connected = True - - behaviour = agent.ListenBehaviour() - agent.add_behaviour(behaviour) - - # Run once (CyclicBehaviour normally loops) - - await behaviour.run() - - fake_socket.send_json.assert_awaited() - fake_socket.recv_json.assert_awaited() - - -@pytest.mark.asyncio -async def test_listen_behaviour_timeout(zmq_context): - fake_socket = zmq_context.return_value.socket.return_value - fake_socket.send_json = AsyncMock() - # recv_json will never resolve, simulate timeout - fake_socket.recv_json = AsyncMock(side_effect=asyncio.TimeoutError) - fake_socket.send_multipart = AsyncMock() - - agent = RICommunicationAgent("test@server", "password") - agent._req_socket = fake_socket - agent.connected = True - - behaviour = agent.ListenBehaviour() - agent.add_behaviour(behaviour) - - await behaviour.run() - assert not any(isinstance(b, agent.ListenBehaviour) for b in agent.behaviours) - assert not agent.connected - - -@pytest.mark.asyncio -async def test_listen_behaviour_ping_no_endpoint(): - """ - Test if our listen behaviour can work with wrong messages (wrong endpoint) - """ - fake_socket = AsyncMock() - fake_socket.send_json = AsyncMock() - fake_socket.send_multipart = AsyncMock() - - # This is a message without endpoint >:( - fake_socket.recv_json = AsyncMock( - return_value={ - "data": "I dont have an endpoint >:)", - } - ) - - agent = RICommunicationAgent("test@server", "password") - agent._req_socket = fake_socket - agent.connected = True - - behaviour = agent.ListenBehaviour() - agent.add_behaviour(behaviour) - - await behaviour.run() - - fake_socket.send_json.assert_awaited() - fake_socket.recv_json.assert_awaited() - - -@pytest.mark.asyncio -async def test_setup_unexpected_exception(zmq_context): - fake_socket = zmq_context.return_value.socket.return_value - fake_socket.send_json = AsyncMock() - # Simulate unexpected exception during recv_json() - fake_socket.recv_json = AsyncMock(side_effect=Exception("boom!")) - fake_socket.send_multipart = AsyncMock() - - agent = RICommunicationAgent( - "test@server", - "password", - address="tcp://localhost:5555", - bind=False, - ) - - await agent.setup(max_retries=1) - - assert not any(isinstance(b, agent.ListenBehaviour) for b in agent.behaviours) - assert not agent.connected - - -@pytest.mark.asyncio -async def test_setup_unpacking_exception(zmq_context): - # --- Arrange --- - fake_socket = zmq_context.return_value.socket.return_value - fake_socket.send_json = AsyncMock() - fake_socket.send_multipart = AsyncMock() - - # Make recv_json return malformed negotiation data to trigger unpacking exception - malformed_data = { - "endpoint": "negotiate/ports", - "data": [{"id": "main"}], - } # missing 'port' and 'bind' - fake_socket.recv_json = AsyncMock(return_value=malformed_data) - - # Patch ActSpeechAgent so it won't actually start - with patch(speech_agent_path(), autospec=True) as MockCommandAgent: - fake_agent_instance = MockCommandAgent.return_value - fake_agent_instance.start = AsyncMock() - - agent = RICommunicationAgent( - "test@server", - "password", - address="tcp://localhost:5555", - bind=False, - ) - - # --- Act & Assert --- - - await agent.setup(max_retries=1) - - # Ensure no command agent was started - fake_agent_instance.start.assert_not_awaited() - - # Ensure no behaviour was attached - assert not any(isinstance(b, agent.ListenBehaviour) for b in agent.behaviours) diff --git a/test/integration/agents/perception/vad_agent/test_vad_agent.py b/test/integration/agents/perception/vad_agent/test_vad_agent.py index ecf9634..2b83eae 100644 --- a/test/integration/agents/perception/vad_agent/test_vad_agent.py +++ b/test/integration/agents/perception/vad_agent/test_vad_agent.py @@ -1,9 +1,8 @@ import random -from unittest.mock import AsyncMock, MagicMock, patch +from unittest.mock import AsyncMock, MagicMock import pytest import zmq -from spade.agent import Agent from control_backend.agents.perception.vad_agent import VADAgent @@ -15,11 +14,6 @@ def zmq_context(mocker): return mock_context -@pytest.fixture -def streaming(mocker): - return mocker.patch("control_backend.agents.perception.vad_agent.StreamingBehaviour") - - @pytest.fixture def per_transcription_agent(mocker): return mocker.patch( @@ -27,21 +21,36 @@ def per_transcription_agent(mocker): ) +@pytest.fixture(autouse=True) +def torch_load(mocker): + mock_torch = mocker.patch("control_backend.agents.perception.vad_agent.torch") + model = MagicMock() + mock_torch.hub.load.return_value = (model, None) + mock_torch.from_numpy.side_effect = lambda arr: arr + return mock_torch + + @pytest.mark.asyncio -async def test_normal_setup(streaming, per_transcription_agent): +async def test_normal_setup(per_transcription_agent): """ Test that during normal setup, the VAD agent creates a Streaming behavior and creates audio sockets, and starts the TranscriptionAgent without loading real models. """ per_vad_agent = VADAgent("tcp://localhost:12345", False) - per_vad_agent.add_behaviour = MagicMock() + per_vad_agent._streaming_loop = AsyncMock() + + async def swallow_background_task(coro): + coro.close() + + per_vad_agent.add_behavior = swallow_background_task + per_vad_agent.reset_stream = AsyncMock() await per_vad_agent.setup() - streaming.assert_called_once() - per_vad_agent.add_behaviour.assert_called_once_with(streaming.return_value) per_transcription_agent.assert_called_once() per_transcription_agent.return_value.start.assert_called_once() + per_vad_agent._streaming_loop.assert_called_once() + per_vad_agent.reset_stream.assert_called_once() assert per_vad_agent.audio_in_socket is not None assert per_vad_agent.audio_out_socket is not None @@ -91,16 +100,22 @@ async def test_out_socket_creation_failure(zmq_context): """ Test setup failure when the audio output socket cannot be created. """ - with patch.object(Agent, "stop", new_callable=AsyncMock) as mock_super_stop: - zmq_context.return_value.socket.return_value.bind_to_random_port.side_effect = ( - zmq.ZMQBindError - ) - per_vad_agent = VADAgent("tcp://localhost:12345", False) + zmq_context.return_value.socket.return_value.bind_to_random_port.side_effect = zmq.ZMQBindError + per_vad_agent = VADAgent("tcp://localhost:12345", False) + per_vad_agent.stop = AsyncMock() + per_vad_agent.reset_stream = AsyncMock() + per_vad_agent._streaming_loop = AsyncMock() + per_vad_agent._connect_audio_out_socket = MagicMock(return_value=None) - await per_vad_agent.setup() + async def swallow_background_task(coro): + coro.close() - assert per_vad_agent.audio_out_socket is None - mock_super_stop.assert_called_once() + per_vad_agent.add_behavior = swallow_background_task + + await per_vad_agent.setup() + + assert per_vad_agent.audio_out_socket is None + per_vad_agent.stop.assert_called_once() @pytest.mark.asyncio @@ -109,6 +124,13 @@ async def test_stop(zmq_context, per_transcription_agent): Test that when the VAD agent is stopped, the sockets are closed correctly. """ per_vad_agent = VADAgent("tcp://localhost:12345", False) + per_vad_agent.reset_stream = AsyncMock() + per_vad_agent._streaming_loop = AsyncMock() + + async def swallow_background_task(coro): + coro.close() + + per_vad_agent.add_behavior = swallow_background_task zmq_context.return_value.socket.return_value.bind_to_random_port.return_value = random.randint( 1000, 10000, diff --git a/test/integration/agents/perception/vad_agent/test_vad_with_audio.py b/test/integration/agents/perception/vad_agent/test_vad_with_audio.py index b197c31..32e2f3d 100644 --- a/test/integration/agents/perception/vad_agent/test_vad_with_audio.py +++ b/test/integration/agents/perception/vad_agent/test_vad_with_audio.py @@ -5,7 +5,24 @@ import pytest import soundfile as sf import zmq -from control_backend.agents.perception.vad_agent import StreamingBehaviour +from control_backend.agents.perception.vad_agent import VADAgent + + +@pytest.fixture(autouse=True) +def patch_settings(): + from control_backend.agents.perception import vad_agent + + vad_agent.settings.behaviour_settings.vad_prob_threshold = 0.5 + vad_agent.settings.behaviour_settings.vad_non_speech_patience_chunks = 3 + vad_agent.settings.behaviour_settings.vad_initial_since_speech = 0 + vad_agent.settings.vad_settings.sample_rate_hz = 16_000 + + +@pytest.fixture(autouse=True) +def mock_torch(mocker): + mock_torch = mocker.patch("control_backend.agents.perception.vad_agent.torch") + mock_torch.from_numpy.side_effect = lambda arr: arr + return mock_torch def get_audio_chunks() -> list[bytes]: @@ -42,16 +59,39 @@ async def test_real_audio(mocker): audio_in_socket = AsyncMock() audio_in_socket.recv.side_effect = audio_chunks - mock_poller: MagicMock = mocker.patch("control_backend.agents.perception.vad_agent.zmq.Poller") - mock_poller.return_value.poll.return_value = [(audio_in_socket, zmq.POLLIN)] + mock_poller: MagicMock = mocker.patch("control_backend.agents.perception.vad_agent.azmq.Poller") + mock_poller.return_value.poll = AsyncMock(return_value=[(audio_in_socket, zmq.POLLIN)]) audio_out_socket = AsyncMock() - vad_streamer = StreamingBehaviour(audio_in_socket, audio_out_socket) - vad_streamer._ready = True - vad_streamer.agent = MagicMock() - for _ in audio_chunks: - await vad_streamer.run() + vad_agent = VADAgent("tcp://localhost:12345", False) + vad_agent.audio_out_socket = audio_out_socket + + # Use a fake model that marks most chunks as speech and ends with a few silences + silence_padding = 5 + probabilities = [1.0] * len(audio_chunks) + [0.0] * silence_padding + chunk_bytes = audio_chunks + [b"\x00" * len(audio_chunks[0])] * silence_padding + model_item = MagicMock() + model_item.item.side_effect = probabilities + vad_agent.model = MagicMock(return_value=model_item) + + class DummyPoller: + def __init__(self, data, agent): + self.data = data + self.agent = agent + + async def poll(self, timeout_ms=None): + if self.data: + return self.data.pop(0) + self.agent._running = False + return None + + vad_agent.audio_in_poller = DummyPoller(chunk_bytes, vad_agent) + vad_agent._ready = AsyncMock() + vad_agent._running = True + vad_agent.i_since_speech = 0 + + await vad_agent._streaming_loop() audio_out_socket.send.assert_called() for args in audio_out_socket.send.call_args_list: diff --git a/test/unit/agents/actuation/test_robot_speech_agent.py b/test/unit/agents/actuation/test_robot_speech_agent.py new file mode 100644 index 0000000..15324f6 --- /dev/null +++ b/test/unit/agents/actuation/test_robot_speech_agent.py @@ -0,0 +1,139 @@ +import json +from unittest.mock import AsyncMock, MagicMock + +import pytest +import zmq + +from control_backend.agents.actuation.robot_speech_agent import RobotSpeechAgent +from control_backend.core.agent_system import InternalMessage + + +@pytest.fixture +def zmq_context(mocker): + mock_context = mocker.patch( + "control_backend.agents.actuation.robot_speech_agent.azmq.Context.instance" + ) + mock_context.return_value = MagicMock() + return mock_context + + +@pytest.mark.asyncio +async def test_setup_bind(zmq_context, mocker): + """Setup binds and subscribes to internal commands.""" + fake_socket = zmq_context.return_value.socket.return_value + agent = RobotSpeechAgent("robot_speech", address="tcp://localhost:5555", bind=True) + settings = mocker.patch("control_backend.agents.actuation.robot_speech_agent.settings") + settings.zmq_settings.internal_sub_address = "tcp://internal:1234" + + agent.add_behavior = MagicMock() + + await agent.setup() + + fake_socket.bind.assert_any_call("tcp://localhost:5555") + fake_socket.connect.assert_any_call("tcp://internal:1234") + fake_socket.setsockopt.assert_any_call(zmq.SUBSCRIBE, b"command") + agent.add_behavior.assert_called_once() + + +@pytest.mark.asyncio +async def test_setup_connect(zmq_context, mocker): + """Setup connects when bind=False.""" + fake_socket = zmq_context.return_value.socket.return_value + agent = RobotSpeechAgent("robot_speech", address="tcp://localhost:5555", bind=False) + settings = mocker.patch("control_backend.agents.actuation.robot_speech_agent.settings") + settings.zmq_settings.internal_sub_address = "tcp://internal:1234" + + agent.add_behavior = MagicMock() + + await agent.setup() + + fake_socket.connect.assert_any_call("tcp://localhost:5555") + fake_socket.connect.assert_any_call("tcp://internal:1234") + agent.add_behavior.assert_called_once() + + +@pytest.mark.asyncio +async def test_handle_message_sends_command(): + """Internal message is forwarded to robot pub socket as JSON.""" + pubsocket = AsyncMock() + agent = RobotSpeechAgent("robot_speech") + agent.pubsocket = pubsocket + + payload = {"endpoint": "actuate/speech", "data": "hello"} + msg = InternalMessage(to="robot", sender="tester", body=json.dumps(payload)) + + await agent.handle_message(msg) + + pubsocket.send_json.assert_awaited_once_with(payload) + + +@pytest.mark.asyncio +async def test_zmq_command_loop_valid_payload(zmq_context): + """UI command is read from SUB and published.""" + command = {"endpoint": "actuate/speech", "data": "hello"} + fake_socket = AsyncMock() + + async def recv_once(): + # stop after first iteration + agent._running = False + return (b"command", json.dumps(command).encode("utf-8")) + + fake_socket.recv_multipart = recv_once + fake_socket.send_json = AsyncMock() + agent = RobotSpeechAgent("robot_speech") + agent.subsocket = fake_socket + agent.pubsocket = fake_socket + agent._running = True + + await agent._zmq_command_loop() + + fake_socket.send_json.assert_awaited_once_with(command) + + +@pytest.mark.asyncio +async def test_zmq_command_loop_invalid_json(): + """Invalid JSON is ignored without sending.""" + fake_socket = AsyncMock() + + async def recv_once(): + agent._running = False + return (b"command", b"{not_json}") + + fake_socket.recv_multipart = recv_once + fake_socket.send_json = AsyncMock() + agent = RobotSpeechAgent("robot_speech") + agent.subsocket = fake_socket + agent.pubsocket = fake_socket + agent._running = True + + await agent._zmq_command_loop() + + fake_socket.send_json.assert_not_awaited() + + +@pytest.mark.asyncio +async def test_handle_message_invalid_payload(): + """Invalid payload is caught and does not send.""" + pubsocket = AsyncMock() + agent = RobotSpeechAgent("robot_speech") + agent.pubsocket = pubsocket + + msg = InternalMessage(to="robot", sender="tester", body=json.dumps({"bad": "data"})) + + await agent.handle_message(msg) + + pubsocket.send_json.assert_not_awaited() + + +@pytest.mark.asyncio +async def test_stop_closes_sockets(): + pubsocket = MagicMock() + subsocket = MagicMock() + agent = RobotSpeechAgent("robot_speech") + agent.pubsocket = pubsocket + agent.subsocket = subsocket + + await agent.stop() + + pubsocket.close.assert_called_once() + subsocket.close.assert_called_once() diff --git a/test/unit/agents/bdi/bdi_core_agent/behaviours/test_belief_setter.py b/test/unit/agents/bdi/bdi_core_agent/behaviours/test_belief_setter.py deleted file mode 100644 index 53b991e..0000000 --- a/test/unit/agents/bdi/bdi_core_agent/behaviours/test_belief_setter.py +++ /dev/null @@ -1,212 +0,0 @@ -import json -import logging -from unittest.mock import AsyncMock, MagicMock, call - -import pytest - -from control_backend.agents.bdi.bdi_core_agent.behaviours.belief_setter_behaviour import ( - BeliefSetterBehaviour, -) - -# Define a constant for the collector agent name to use in tests -COLLECTOR_AGENT_NAME = "belief_collector_agent" -COLLECTOR_AGENT_JID = f"{COLLECTOR_AGENT_NAME}@test" - - -@pytest.fixture -def mock_agent(mocker): - """Fixture to create a mock BDIAgent.""" - agent = MagicMock() - agent.bdi = MagicMock() - agent.jid = "bdi_agent@test" - return agent - - -@pytest.fixture -def belief_setter_behaviour(mock_agent, mocker): - """Fixture to create an instance of BeliefSetterBehaviour with a mocked agent.""" - # Patch the settings to use a predictable agent name - mocker.patch( - "control_backend.agents.bdi.bdi_core_agent." - "behaviours.belief_setter_behaviour.settings.agent_settings.bdi_belief_collector_name", - COLLECTOR_AGENT_NAME, - ) - - setter = BeliefSetterBehaviour() - setter.agent = mock_agent - # Mock the receive method, we will control its return value in each test - setter.receive = AsyncMock() - return setter - - -def create_mock_message(sender_node: str, body: str, thread: str) -> MagicMock: - """Helper function to create a configured mock message.""" - msg = MagicMock() - msg.sender.node = sender_node # MagicMock automatically creates nested mocks - msg.body = body - msg.thread = thread - return msg - - -@pytest.mark.asyncio -async def test_run_message_received(belief_setter_behaviour, mocker): - """ - Test that when a message is received, _process_message is called. - """ - # Arrange - msg = MagicMock() - belief_setter_behaviour.receive.return_value = msg - mocker.patch.object(belief_setter_behaviour, "_process_message") - - # Act - await belief_setter_behaviour.run() - - # Assert - belief_setter_behaviour._process_message.assert_called_once_with(msg) - - -def test_process_message_from_bdi_belief_collector_agent(belief_setter_behaviour, mocker): - """ - Test processing a message from the correct belief collector agent. - """ - # Arrange - msg = create_mock_message(sender_node=COLLECTOR_AGENT_NAME, body="", thread="") - mock_process_belief = mocker.patch.object(belief_setter_behaviour, "_process_belief_message") - - # Act - belief_setter_behaviour._process_message(msg) - - # Assert - mock_process_belief.assert_called_once_with(msg) - - -def test_process_message_from_other_agent(belief_setter_behaviour, mocker): - """ - Test that messages from other agents are ignored. - """ - # Arrange - msg = create_mock_message(sender_node="other_agent", body="", thread="") - mock_process_belief = mocker.patch.object(belief_setter_behaviour, "_process_belief_message") - - # Act - belief_setter_behaviour._process_message(msg) - - # Assert - mock_process_belief.assert_not_called() - - -def test_process_belief_message_valid_json(belief_setter_behaviour, mocker): - """ - Test processing a valid belief message with correct thread and JSON body. - """ - # Arrange - beliefs_payload = {"is_hot": ["kitchen"], "is_clean": ["kitchen", "bathroom"]} - msg = create_mock_message( - sender_node=COLLECTOR_AGENT_JID, body=json.dumps(beliefs_payload), thread="beliefs" - ) - mock_set_beliefs = mocker.patch.object(belief_setter_behaviour, "_set_beliefs") - - # Act - belief_setter_behaviour._process_belief_message(msg) - - # Assert - mock_set_beliefs.assert_called_once_with(beliefs_payload) - - -def test_process_belief_message_invalid_json(belief_setter_behaviour, mocker, caplog): - """ - Test that a message with invalid JSON is handled gracefully and an error is logged. - """ - # Arrange - msg = create_mock_message( - sender_node=COLLECTOR_AGENT_JID, body="this is not a json string", thread="beliefs" - ) - mock_set_beliefs = mocker.patch.object(belief_setter_behaviour, "_set_beliefs") - - # Act - belief_setter_behaviour._process_belief_message(msg) - - # Assert - mock_set_beliefs.assert_not_called() - - -def test_process_belief_message_wrong_thread(belief_setter_behaviour, mocker): - """ - Test that a message with an incorrect thread is ignored. - """ - # Arrange - msg = create_mock_message( - sender_node=COLLECTOR_AGENT_JID, body='{"some": "data"}', thread="not_beliefs" - ) - mock_set_beliefs = mocker.patch.object(belief_setter_behaviour, "_set_beliefs") - - # Act - belief_setter_behaviour._process_belief_message(msg) - - # Assert - mock_set_beliefs.assert_not_called() - - -def test_process_belief_message_empty_body(belief_setter_behaviour, mocker): - """ - Test that a message with an empty body is ignored. - """ - # Arrange - msg = create_mock_message(sender_node=COLLECTOR_AGENT_JID, body="", thread="beliefs") - mock_set_beliefs = mocker.patch.object(belief_setter_behaviour, "_set_beliefs") - - # Act - belief_setter_behaviour._process_belief_message(msg) - - # Assert - mock_set_beliefs.assert_not_called() - - -def test_set_beliefs_success(belief_setter_behaviour, mock_agent, caplog): - """ - Test that beliefs are correctly set on the agent's BDI. - """ - # Arrange - beliefs_to_set = { - "is_hot": ["kitchen"], - "door_opened": ["front_door", "back_door"], - } - - # Act - with caplog.at_level(logging.INFO): - belief_setter_behaviour._set_beliefs(beliefs_to_set) - - # Assert - expected_calls = [ - call("is_hot", "kitchen"), - call("door_opened", "front_door", "back_door"), - ] - mock_agent.bdi.set_belief.assert_has_calls(expected_calls, any_order=True) - assert mock_agent.bdi.set_belief.call_count == 2 - - -# def test_responded_unset(belief_setter_behaviour, mock_agent): -# # Arrange -# new_beliefs = {"user_said": ["message"]} -# -# # Act -# belief_setter_behaviour._set_beliefs(new_beliefs) -# -# # Assert -# mock_agent.bdi.set_belief.assert_has_calls([call("user_said", "message")]) -# mock_agent.bdi.remove_belief.assert_has_calls([call("responded")]) - -# def test_set_beliefs_bdi_not_initialized(belief_setter_behaviour, mock_agent, caplog): -# """ -# Test that a warning is logged if the agent's BDI is not initialized. -# """ -# # Arrange -# mock_agent.bdi = None # Simulate BDI not being ready -# beliefs_to_set = {"is_hot": ["kitchen"]} -# -# # Act -# with caplog.at_level(logging.WARNING): -# belief_setter_behaviour._set_beliefs(beliefs_to_set) -# -# # Assert -# assert "Cannot set beliefs, since agent's BDI is not yet initialized." in caplog.text diff --git a/test/unit/agents/bdi/belief_collector_agent/behaviours/test_continuous_collect.py b/test/unit/agents/bdi/belief_collector_agent/behaviours/test_continuous_collect.py deleted file mode 100644 index 4cb5ba1..0000000 --- a/test/unit/agents/bdi/belief_collector_agent/behaviours/test_continuous_collect.py +++ /dev/null @@ -1,101 +0,0 @@ -import json -from unittest.mock import AsyncMock, MagicMock - -import pytest - -from control_backend.agents.bdi.belief_collector_agent.behaviours.belief_collector_behaviour import ( # noqa: E501 - BeliefCollectorBehaviour, -) - - -def create_mock_message(sender_node: str, body: str) -> MagicMock: - """Helper function to create a configured mock message.""" - msg = MagicMock() - msg.sender.node = sender_node # MagicMock automatically creates nested mocks - msg.body = body - return msg - - -@pytest.fixture -def mock_agent(mocker): - """Fixture to create a mock Agent.""" - agent = MagicMock() - agent.jid = "belief_collector_agent@test" - return agent - - -@pytest.fixture -def bel_collector_behaviouror(mock_agent, mocker): - """Fixture to create an instance of BelCollectorBehaviour with a mocked agent.""" - # Patch asyncio.sleep to prevent tests from actually waiting - mocker.patch("asyncio.sleep", return_value=None) - - collector = BeliefCollectorBehaviour() - collector.agent = mock_agent - # Mock the receive method, we will control its return value in each test - collector.receive = AsyncMock() - return collector - - -@pytest.mark.asyncio -async def test_run_message_received(bel_collector_behaviouror, mocker): - """ - Test that when a message is received, _process_message is called with that message. - """ - # Arrange - mock_msg = MagicMock() - bel_collector_behaviouror.receive.return_value = mock_msg - mocker.patch.object(bel_collector_behaviouror, "_process_message") - - # Act - await bel_collector_behaviouror.run() - - # Assert - bel_collector_behaviouror._process_message.assert_awaited_once_with(mock_msg) - - -@pytest.mark.asyncio -async def test_routes_to_handle_belief_text_by_type(bel_collector_behaviouror, mocker): - msg = create_mock_message( - "anyone", - json.dumps({"type": "belief_extraction_text", "beliefs": {"user_said": [["hi"]]}}), - ) - spy = mocker.patch.object(bel_collector_behaviouror, "_handle_belief_text", new=AsyncMock()) - await bel_collector_behaviouror._process_message(msg) - spy.assert_awaited_once() - - -@pytest.mark.asyncio -async def test_routes_to_handle_belief_text_by_sender(bel_collector_behaviouror, mocker): - msg = create_mock_message( - "bel_text_agent_mock", json.dumps({"beliefs": {"user_said": [["hi"]]}}) - ) - spy = mocker.patch.object(bel_collector_behaviouror, "_handle_belief_text", new=AsyncMock()) - await bel_collector_behaviouror._process_message(msg) - spy.assert_awaited_once() - - -@pytest.mark.asyncio -async def test_routes_to_handle_emo_text(bel_collector_behaviouror, mocker): - msg = create_mock_message("anyone", json.dumps({"type": "emotion_extraction_text"})) - spy = mocker.patch.object(bel_collector_behaviouror, "_handle_emo_text", new=AsyncMock()) - await bel_collector_behaviouror._process_message(msg) - spy.assert_awaited_once() - - -@pytest.mark.asyncio -async def test_belief_text_happy_path_sends(bel_collector_behaviouror, mocker): - payload = {"type": "belief_extraction_text", "beliefs": {"user_said": ["hello test", "No"]}} - bel_collector_behaviouror.send = AsyncMock() - await bel_collector_behaviouror._handle_belief_text(payload, "bel_text_agent_mock") - - # make sure we attempted a send - bel_collector_behaviouror.send.assert_awaited_once() - - -@pytest.mark.asyncio -async def test_belief_text_coerces_non_strings(bel_collector_behaviouror, mocker): - payload = {"type": "belief_extraction_text", "beliefs": {"user_said": [["hi", 123]]}} - bel_collector_behaviouror.send = AsyncMock() - await bel_collector_behaviouror._handle_belief_text(payload, "origin") - bel_collector_behaviouror.send.assert_awaited_once() diff --git a/test/unit/agents/bdi/test_bdi_core_agent.py b/test/unit/agents/bdi/test_bdi_core_agent.py new file mode 100644 index 0000000..43ee033 --- /dev/null +++ b/test/unit/agents/bdi/test_bdi_core_agent.py @@ -0,0 +1,126 @@ +import json +from unittest.mock import AsyncMock, MagicMock, mock_open, patch + +import agentspeak +import pytest + +from control_backend.agents.bdi.bdi_core_agent.bdi_core_agent import BDICoreAgent +from control_backend.core.agent_system import InternalMessage +from control_backend.core.config import settings +from control_backend.schemas.belief_message import BeliefMessage + + +@pytest.fixture +def mock_agentspeak_env(): + with patch("agentspeak.runtime.Environment") as mock_env: + yield mock_env + + +@pytest.fixture +def agent(): + agent = BDICoreAgent("bdi_agent", "dummy.asl") + agent.send = AsyncMock() + agent.bdi_agent = MagicMock() + return agent + + +@pytest.mark.asyncio +async def test_setup_loads_asl(mock_agentspeak_env, agent): + # Mock file opening + with patch("builtins.open", mock_open(read_data="+initial_goal.")): + await agent.setup() + + # Check if environment tried to build agent + mock_agentspeak_env.return_value.build_agent.assert_called() + + +@pytest.mark.asyncio +async def test_setup_no_asl(mock_agentspeak_env, agent): + with patch("builtins.open", side_effect=FileNotFoundError): + await agent.setup() + + mock_agentspeak_env.return_value.build_agent.assert_not_called() + + +@pytest.mark.asyncio +async def test_handle_belief_collector_message(agent, mock_settings): + """Test that incoming beliefs are added to the BDI agent""" + beliefs = {"user_said": ["Hello"]} + msg = InternalMessage( + to="bdi_agent", + sender=mock_settings.agent_settings.bdi_belief_collector_name, + body=BeliefMessage(beliefs=beliefs).model_dump_json(), + thread="beliefs", + ) + + await agent.handle_message(msg) + + # Expect bdi_agent.call to be triggered to add belief + args = agent.bdi_agent.call.call_args.args + assert args[0] == agentspeak.Trigger.addition + assert args[1] == agentspeak.GoalType.belief + assert args[2] == agentspeak.Literal("user_said", (agentspeak.Literal("Hello"),)) + + +@pytest.mark.asyncio +async def test_incorrect_belief_collector_message(agent, mock_settings): + """Test that incorrect message format triggers an exception.""" + msg = InternalMessage( + to="bdi_agent", + sender=mock_settings.agent_settings.bdi_belief_collector_name, + body=json.dumps({"bad_format": "bad_format"}), + thread="beliefs", + ) + + await agent.handle_message(msg) + + agent.bdi_agent.call.assert_not_called() # did not set belief + + +@pytest.mark.asyncio +async def test(): + pass + + +@pytest.mark.asyncio +async def test_handle_llm_response(agent): + """Test that LLM responses are forwarded to the Robot Speech Agent""" + msg = InternalMessage( + to="bdi_agent", sender=settings.agent_settings.llm_name, body="This is the LLM reply" + ) + + await agent.handle_message(msg) + + # Verify forward + assert agent.send.called + sent_msg = agent.send.call_args[0][0] + assert sent_msg.to == settings.agent_settings.robot_speech_name + assert "This is the LLM reply" in sent_msg.body + + +@pytest.mark.asyncio +async def test_custom_actions(agent): + agent._send_to_llm = MagicMock(side_effect=agent.send) # Mock specific method + + # Initialize actions manually since we didn't call setup with real file + agent._add_custom_actions() + + # Find the action + action_fn = None + for (functor, _), fn in agent.actions.actions.items(): + if functor == ".reply": + action_fn = fn + break + + assert action_fn is not None + + # Invoke action + mock_term = MagicMock() + mock_term.args = ["Hello"] + mock_intention = MagicMock() + + # Run generator + gen = action_fn(agent, mock_term, mock_intention) + next(gen) # Execute + + agent._send_to_llm.assert_called_with("Hello") diff --git a/test/unit/agents/bdi/test_belief_collector.py b/test/unit/agents/bdi/test_belief_collector.py new file mode 100644 index 0000000..ca89a9d --- /dev/null +++ b/test/unit/agents/bdi/test_belief_collector.py @@ -0,0 +1,87 @@ +import json +from unittest.mock import AsyncMock + +import pytest + +from control_backend.agents.bdi import ( + BDIBeliefCollectorAgent, +) +from control_backend.core.agent_system import InternalMessage +from control_backend.core.config import settings + + +@pytest.fixture +def agent(): + agent = BDIBeliefCollectorAgent("belief_collector_agent") + return agent + + +def make_msg(body: dict, sender: str = "sender"): + return InternalMessage(to="collector", sender=sender, body=json.dumps(body)) + + +@pytest.mark.asyncio +async def test_handle_message_routes_belief_text(agent, mocker): + """ + Test that when a message is received, _handle_belief_text is called with that message. + """ + payload = {"type": "belief_extraction_text", "beliefs": {"user_said": [["hi"]]}} + spy = mocker.patch.object(agent, "_handle_belief_text", new_callable=AsyncMock) + + await agent.handle_message(make_msg(payload)) + + spy.assert_awaited_once_with(payload, "sender") + + +@pytest.mark.asyncio +async def test_handle_message_routes_emotion(agent, mocker): + payload = {"type": "emotion_extraction_text"} + spy = mocker.patch.object(agent, "_handle_emo_text", new_callable=AsyncMock) + + await agent.handle_message(make_msg(payload)) + + spy.assert_awaited_once_with(payload, "sender") + + +@pytest.mark.asyncio +async def test_handle_message_bad_json(agent, mocker): + agent._handle_belief_text = AsyncMock() + bad_msg = InternalMessage(to="collector", sender="sender", body="not json") + + await agent.handle_message(bad_msg) + + agent._handle_belief_text.assert_not_awaited() + + +@pytest.mark.asyncio +async def test_handle_belief_text_sends_when_beliefs_exist(agent, mocker): + payload = {"type": "belief_extraction_text", "beliefs": {"user_said": ["hello"]}} + spy = mocker.patch.object(agent, "_send_beliefs_to_bdi", new_callable=AsyncMock) + + await agent._handle_belief_text(payload, "origin") + + spy.assert_awaited_once_with(payload["beliefs"], origin="origin") + + +@pytest.mark.asyncio +async def test_handle_belief_text_no_send_when_empty(agent, mocker): + payload = {"type": "belief_extraction_text", "beliefs": {}} + spy = mocker.patch.object(agent, "_send_beliefs_to_bdi", new_callable=AsyncMock) + + await agent._handle_belief_text(payload, "origin") + + spy.assert_not_awaited() + + +@pytest.mark.asyncio +async def test_send_beliefs_to_bdi(agent): + agent.send = AsyncMock() + beliefs = {"user_said": ["hello", "world"]} + + await agent._send_beliefs_to_bdi(beliefs, origin="origin") + + agent.send.assert_awaited_once() + sent: InternalMessage = agent.send.call_args.args[0] + assert sent.to == settings.agent_settings.bdi_core_name + assert sent.thread == "beliefs" + assert json.loads(sent.body)["beliefs"] == beliefs diff --git a/test/unit/agents/bdi/test_text_extractor.py b/test/unit/agents/bdi/test_text_extractor.py new file mode 100644 index 0000000..8cc2d0f --- /dev/null +++ b/test/unit/agents/bdi/test_text_extractor.py @@ -0,0 +1,58 @@ +import json +from unittest.mock import AsyncMock + +import pytest + +from control_backend.agents.bdi import ( + TextBeliefExtractorAgent, +) +from control_backend.core.agent_system import InternalMessage + + +@pytest.fixture +def agent(): + agent = TextBeliefExtractorAgent("text_belief_agent") + agent.send = AsyncMock() + return agent + + +def make_msg(sender: str, body: str, thread: str | None = None) -> InternalMessage: + return InternalMessage(to="unused", sender=sender, body=body, thread=thread) + + +@pytest.mark.asyncio +async def test_handle_message_ignores_other_agents(agent): + msg = make_msg("unknown", "some data", None) + + await agent.handle_message(msg) + + agent.send.assert_not_called() # noqa # `agent.send` has no such property, but we mock it. + + +@pytest.mark.asyncio +async def test_handle_message_from_transcriber(agent, mock_settings): + transcription = "hello world" + msg = make_msg(mock_settings.agent_settings.transcription_name, transcription, None) + + await agent.handle_message(msg) + + agent.send.assert_awaited_once() # noqa # `agent.send` has no such property, but we mock it. + sent: InternalMessage = agent.send.call_args.args[0] # noqa + assert sent.to == mock_settings.agent_settings.bdi_belief_collector_name + assert sent.thread == "beliefs" + parsed = json.loads(sent.body) + assert parsed == {"beliefs": {"user_said": [transcription]}, "type": "belief_extraction_text"} + + +@pytest.mark.asyncio +async def test_process_transcription_demo(agent, mock_settings): + transcription = "this is a test" + + await agent._process_transcription_demo(transcription) + + agent.send.assert_awaited_once() # noqa # `agent.send` has no such property, but we mock it. + sent: InternalMessage = agent.send.call_args.args[0] # noqa + assert sent.to == mock_settings.agent_settings.bdi_belief_collector_name + assert sent.thread == "beliefs" + parsed = json.loads(sent.body) + assert parsed["beliefs"]["user_said"] == [transcription] diff --git a/test/unit/agents/bdi/text_belief_agent/behaviours/test_belief_from_text.py b/test/unit/agents/bdi/text_belief_agent/behaviours/test_belief_from_text.py deleted file mode 100644 index 294f00d..0000000 --- a/test/unit/agents/bdi/text_belief_agent/behaviours/test_belief_from_text.py +++ /dev/null @@ -1,191 +0,0 @@ -import json -from unittest.mock import AsyncMock, MagicMock, patch - -import pytest -from spade.message import Message - -from control_backend.agents.bdi.text_belief_extractor_agent.behaviours.text_belief_extractor_behaviour import ( # noqa: E501, We can't shorten this import. - TextBeliefExtractorBehaviour, -) - - -@pytest.fixture -def mock_settings(): - """ - Mocks the settings object that the behaviour imports. - We patch it at the source where it's imported by the module under test. - """ - # Create a mock object that mimics the nested structure - settings_mock = MagicMock() - settings_mock.agent_settings.transcription_name = "transcriber" - settings_mock.agent_settings.bdi_belief_collector_name = "collector" - settings_mock.agent_settings.host = "fake.host" - - # Use patch to replace the settings object during the test - # Adjust 'control_backend.behaviours.belief_from_text.settings' to where - # your behaviour file imports it from. - with patch( - "control_backend.agents.bdi.text_belief_extractor_agent.behaviours" - ".text_belief_extractor_behaviour.settings", - settings_mock, - ): - yield settings_mock - - -@pytest.fixture -def behavior(mock_settings): - """ - Creates an instance of the BDITextBeliefBehaviour behaviour and mocks its - agent, logger, send, and receive methods. - """ - b = TextBeliefExtractorBehaviour() - - b.agent = MagicMock() - b.send = AsyncMock() - b.receive = AsyncMock() - - return b - - -def create_mock_message(sender_node: str, body: str, thread: str) -> MagicMock: - """Helper function to create a configured mock message.""" - msg = MagicMock() - msg.sender.node = sender_node # MagicMock automatically creates nested mocks - msg.body = body - msg.thread = thread - return msg - - -@pytest.mark.asyncio -async def test_run_no_message(behavior): - """ - Tests the run() method when no message is received. - """ - # Arrange: Configure receive to return None - behavior.receive.return_value = None - - # Act: Run the behavior - await behavior.run() - - # Assert - # 1. Check that receive was called - behavior.receive.assert_called_once() - # 2. Check that no message was sent - behavior.send.assert_not_called() - - -@pytest.mark.asyncio -async def test_run_message_from_other_agent(behavior): - """ - Tests the run() method when a message is received from an - unknown agent (not the transcriber). - """ - # Arrange: Create a mock message from an unknown sender - mock_msg = create_mock_message("unknown", "some data", None) - behavior.receive.return_value = mock_msg - behavior._process_transcription_demo = MagicMock() - - # Act - await behavior.run() - - # Assert - # 1. Check that receive was called - behavior.receive.assert_called_once() - # 2. Check that _process_transcription_demo was not sent - behavior._process_transcription_demo.assert_not_called() - - -@pytest.mark.asyncio -async def test_run_message_from_transcriber_demo(behavior, mock_settings, monkeypatch): - """ - Tests the main success path: receiving a message from the - transcription agent, which triggers _process_transcription_demo. - """ - # Arrange: Create a mock message from the transcriber - transcription_text = "hello world" - mock_msg = create_mock_message( - mock_settings.agent_settings.transcription_name, transcription_text, None - ) - behavior.receive.return_value = mock_msg - - # Act - await behavior.run() - - # Assert - # 1. Check that receive was called - behavior.receive.assert_called_once() - - # 2. Check that send was called *once* - behavior.send.assert_called_once() - - # 3. Deeply inspect the message that was sent - sent_msg: Message = behavior.send.call_args[0][0] - - assert ( - sent_msg.to - == mock_settings.agent_settings.bdi_belief_collector_name - + "@" - + mock_settings.agent_settings.host - ) - - # Check thread - assert sent_msg.thread == "beliefs" - - # Parse the received JSON string back into a dict - expected_dict = { - "beliefs": {"user_said": [transcription_text]}, - "type": "belief_extraction_text", - } - sent_dict = json.loads(sent_msg.body) - - # Assert that the dictionaries are equal - assert sent_dict == expected_dict - - -@pytest.mark.asyncio -async def test_process_transcription_success(behavior, mock_settings): - """ - Tests the (currently unused) _process_transcription method's - success path, using its hardcoded mock response. - """ - # Arrange - test_text = "I am feeling happy" - # This is the hardcoded response inside the method - expected_response_body = '{"mood": [["happy"]]}' - - # Act - await behavior._process_transcription(test_text) - - # Assert - # 1. Check that a message was sent - behavior.send.assert_called_once() - - # 2. Inspect the sent message - sent_msg: Message = behavior.send.call_args[0][0] - expected_to = ( - mock_settings.agent_settings.bdi_belief_collector_name - + "@" - + mock_settings.agent_settings.host - ) - assert str(sent_msg.to) == expected_to - assert sent_msg.thread == "beliefs" - assert sent_msg.body == expected_response_body - - -@pytest.mark.asyncio -async def test_process_transcription_json_decode_error(behavior, mock_settings): - """ - Tests the _process_transcription method's error handling - when the (mocked) response is invalid JSON. - We do this by patching json.loads to raise an error. - """ - # Arrange - test_text = "I am feeling happy" - # Patch json.loads to raise an error when called - with patch("json.loads", side_effect=json.JSONDecodeError("Mock error", "", 0)): - # Act - await behavior._process_transcription(test_text) - - # Assert - # 1. Check that NO message was sent - behavior.send.assert_not_called() diff --git a/test/unit/agents/communication/test_ri_communication_agent.py b/test/unit/agents/communication/test_ri_communication_agent.py new file mode 100644 index 0000000..747c4d2 --- /dev/null +++ b/test/unit/agents/communication/test_ri_communication_agent.py @@ -0,0 +1,336 @@ +import asyncio +from unittest.mock import ANY, AsyncMock, MagicMock, patch + +import pytest + +from control_backend.agents.communication.ri_communication_agent import RICommunicationAgent + + +def speech_agent_path(): + return "control_backend.agents.communication.ri_communication_agent.RobotSpeechAgent" + + +@pytest.fixture +def zmq_context(mocker): + mock_context = mocker.patch( + "control_backend.agents.communication.ri_communication_agent.Context.instance" + ) + mock_context.return_value = MagicMock() + return mock_context + + +def negotiation_message( + actuation_port: int = 5556, + bind_main: bool = False, + bind_actuation: bool = True, + main_port: int = 5555, +): + return { + "endpoint": "negotiate/ports", + "data": [ + {"id": "main", "port": main_port, "bind": bind_main}, + {"id": "actuation", "port": actuation_port, "bind": bind_actuation}, + ], + } + + +@pytest.mark.asyncio +async def test_setup_success_connects_and_starts_robot(zmq_context): + fake_socket = zmq_context.return_value.socket.return_value + fake_socket.send_json = AsyncMock() + fake_socket.recv_json = AsyncMock(return_value=negotiation_message()) + fake_socket.send_multipart = AsyncMock() + + with patch(speech_agent_path(), autospec=True) as MockRobot: + robot_instance = MockRobot.return_value + robot_instance.start = AsyncMock() + agent = RICommunicationAgent("ri_comm", address="tcp://localhost:5555", bind=False) + + agent.add_behavior = MagicMock() + + await agent.setup() + + fake_socket.connect.assert_any_call("tcp://localhost:5555") + fake_socket.send_json.assert_any_call({"endpoint": "negotiate/ports", "data": {}}) + robot_instance.start.assert_awaited_once() + MockRobot.assert_called_once_with(ANY, address="tcp://*:5556", bind=True) + agent.add_behavior.assert_called_once() + assert agent.connected is True + + +@pytest.mark.asyncio +async def test_setup_binds_when_requested(zmq_context): + fake_socket = zmq_context.return_value.socket.return_value + fake_socket.send_json = AsyncMock() + fake_socket.recv_json = AsyncMock(return_value=negotiation_message(bind_main=True)) + fake_socket.send_multipart = AsyncMock() + + agent = RICommunicationAgent("ri_comm", address="tcp://localhost:5555", bind=True) + + agent.add_behavior = MagicMock() + + with patch(speech_agent_path(), autospec=True) as MockRobot: + MockRobot.return_value.start = AsyncMock() + await agent.setup() + + fake_socket.bind.assert_any_call("tcp://localhost:5555") + agent.add_behavior.assert_called_once() + + +@pytest.mark.asyncio +async def test_negotiate_invalid_endpoint_retries(zmq_context): + fake_socket = zmq_context.return_value.socket.return_value + fake_socket.send_json = AsyncMock() + fake_socket.recv_json = AsyncMock(return_value={"endpoint": "ping", "data": {}}) + fake_socket.send_multipart = AsyncMock() + + agent = RICommunicationAgent("ri_comm", address="tcp://localhost:5555", bind=False) + agent._req_socket = fake_socket + + success = await agent._negotiate_connection(max_retries=1) + + assert success is False + + +@pytest.mark.asyncio +async def test_negotiate_timeout(zmq_context): + fake_socket = zmq_context.return_value.socket.return_value + fake_socket.send_json = AsyncMock() + fake_socket.recv_json = AsyncMock(side_effect=asyncio.TimeoutError) + fake_socket.send_multipart = AsyncMock() + + agent = RICommunicationAgent("ri_comm", address="tcp://localhost:5555", bind=False) + agent._req_socket = fake_socket + + success = await agent._negotiate_connection(max_retries=1) + + assert success is False + + +@pytest.mark.asyncio +async def test_handle_negotiation_response_updates_req_socket(zmq_context): + fake_socket = zmq_context.return_value.socket.return_value + agent = RICommunicationAgent("ri_comm", address="tcp://localhost:5555", bind=False) + agent._req_socket = fake_socket + with patch(speech_agent_path(), autospec=True) as MockRobot: + MockRobot.return_value.start = AsyncMock() + await agent._handle_negotiation_response( + negotiation_message( + main_port=6000, + actuation_port=6001, + bind_main=False, + bind_actuation=False, + ) + ) + + fake_socket.connect.assert_any_call("tcp://localhost:6000") + + +@pytest.mark.asyncio +async def test_handle_disconnection_publishes_and_reconnects(): + pub_socket = AsyncMock() + agent = RICommunicationAgent("ri_comm") + agent.pub_socket = pub_socket + agent.connected = True + agent._negotiate_connection = AsyncMock(return_value=True) + + await agent._handle_disconnection() + + pub_socket.send_multipart.assert_awaited() + assert agent.connected is True + + +@pytest.mark.asyncio +async def test_listen_loop_handles_non_ping(zmq_context): + fake_socket = zmq_context.return_value.socket.return_value + fake_socket.send_json = AsyncMock() + + async def recv_once(): + agent._running = False + return {"endpoint": "negotiate/ports", "data": {}} + + fake_socket.recv_json = recv_once + agent = RICommunicationAgent("ri_comm") + agent._req_socket = fake_socket + agent.pub_socket = AsyncMock() + agent.connected = True + agent._running = True + + await agent._listen_loop() + + fake_socket.send_json.assert_called() + + +@pytest.mark.asyncio +async def test_negotiate_unexpected_error(zmq_context): + fake_socket = zmq_context.return_value.socket.return_value + fake_socket.send_json = AsyncMock() + fake_socket.recv_json = AsyncMock(side_effect=Exception("boom")) + agent = RICommunicationAgent("ri_comm") + agent._req_socket = fake_socket + + assert await agent._negotiate_connection(max_retries=1) is False + + +@pytest.mark.asyncio +async def test_negotiate_handle_response_error(zmq_context): + fake_socket = zmq_context.return_value.socket.return_value + fake_socket.send_json = AsyncMock() + fake_socket.recv_json = AsyncMock(return_value=negotiation_message()) + + agent = RICommunicationAgent("ri_comm") + agent._req_socket = fake_socket + agent._handle_negotiation_response = AsyncMock(side_effect=Exception("bad response")) + + assert await agent._negotiate_connection(max_retries=1) is False + + +@pytest.mark.asyncio +async def test_setup_warns_on_failed_negotiate(zmq_context, mocker): + fake_socket = zmq_context.return_value.socket.return_value + fake_socket.send_json = AsyncMock() + fake_socket.recv_json = AsyncMock() + agent = RICommunicationAgent("ri_comm") + + async def swallow(coro): + coro.close() + + agent.add_behavior = swallow + agent._negotiate_connection = AsyncMock(return_value=False) + + await agent.setup() + + assert agent.connected is False + + +@pytest.mark.asyncio +async def test_handle_negotiation_response_unhandled_id(): + agent = RICommunicationAgent("ri_comm") + + await agent._handle_negotiation_response( + {"data": [{"id": "other", "port": 5000, "bind": False}]} + ) + + +@pytest.mark.asyncio +async def test_stop_closes_sockets(): + req = MagicMock() + pub = MagicMock() + agent = RICommunicationAgent("ri_comm") + agent._req_socket = req + agent.pub_socket = pub + + await agent.stop() + + req.close.assert_called_once() + pub.close.assert_called_once() + + +@pytest.mark.asyncio +async def test_listen_loop_not_connected(monkeypatch): + agent = RICommunicationAgent("ri_comm") + agent._running = True + agent.connected = False + agent._req_socket = AsyncMock() + + async def fake_sleep(duration): + agent._running = False + + monkeypatch.setattr("asyncio.sleep", fake_sleep) + + await agent._listen_loop() + + +@pytest.mark.asyncio +async def test_listen_loop_send_and_recv_timeout(): + req = AsyncMock() + req.send_json = AsyncMock(side_effect=TimeoutError) + req.recv_json = AsyncMock(side_effect=TimeoutError) + + agent = RICommunicationAgent("ri_comm") + agent._req_socket = req + agent.pub_socket = AsyncMock() + agent.connected = True + agent._running = True + + async def stop_run(): + agent._running = False + + agent._handle_disconnection = AsyncMock(side_effect=stop_run) + + await agent._listen_loop() + + agent._handle_disconnection.assert_awaited() + + +@pytest.mark.asyncio +async def test_listen_loop_missing_endpoint(monkeypatch): + req = AsyncMock() + req.send_json = AsyncMock() + + async def recv_once(): + agent._running = False + return {"data": {}} + + req.recv_json = recv_once + + agent = RICommunicationAgent("ri_comm") + agent._req_socket = req + agent.pub_socket = AsyncMock() + agent.connected = True + agent._running = True + + await agent._listen_loop() + + +@pytest.mark.asyncio +async def test_listen_loop_generic_exception(): + req = AsyncMock() + req.send_json = AsyncMock() + req.recv_json = AsyncMock(side_effect=ValueError("boom")) + + agent = RICommunicationAgent("ri_comm") + agent._req_socket = req + agent.pub_socket = AsyncMock() + agent.connected = True + agent._running = True + + with pytest.raises(ValueError): + await agent._listen_loop() + + +@pytest.mark.asyncio +async def test_handle_disconnection_timeout(monkeypatch): + pub = AsyncMock() + pub.send_multipart = AsyncMock(side_effect=TimeoutError) + + agent = RICommunicationAgent("ri_comm") + agent.pub_socket = pub + agent._negotiate_connection = AsyncMock(return_value=False) + + await agent._handle_disconnection() + + pub.send_multipart.assert_awaited() + + +@pytest.mark.asyncio +async def test_listen_loop_ping_sends_internal(zmq_context): + fake_socket = zmq_context.return_value.socket.return_value + fake_socket.send_json = AsyncMock() + pub_socket = AsyncMock() + + agent = RICommunicationAgent("ri_comm") + agent._req_socket = fake_socket + agent.pub_socket = pub_socket + agent.connected = True + agent._running = True + + async def recv_once(): + agent._running = False + return {"endpoint": "ping", "data": {}} + + fake_socket.recv_json = recv_once + + await agent._listen_loop() + + pub_socket.send_multipart.assert_awaited() diff --git a/test/unit/agents/llm/test_llm_agent.py b/test/unit/agents/llm/test_llm_agent.py new file mode 100644 index 0000000..4a8b7df --- /dev/null +++ b/test/unit/agents/llm/test_llm_agent.py @@ -0,0 +1,124 @@ +"""Mocks `httpx` and tests chunking logic.""" + +from unittest.mock import AsyncMock, MagicMock, patch + +import httpx +import pytest + +from control_backend.agents.llm.llm_agent import LLMAgent, LLMInstructions +from control_backend.core.agent_system import InternalMessage + + +@pytest.fixture +def mock_httpx_client(): + with patch("httpx.AsyncClient") as mock_cls: + mock_client = AsyncMock() + mock_cls.return_value.__aenter__.return_value = mock_client + yield mock_client + + +@pytest.mark.asyncio +async def test_llm_processing_success(mock_httpx_client, mock_settings): + # Setup the mock response for the stream + mock_response = MagicMock() + mock_response.raise_for_status = MagicMock() + + # Simulate stream lines + lines = [ + b'data: {"choices": [{"delta": {"content": "Hello"}}]}', + b'data: {"choices": [{"delta": {"content": " world"}}]}', + b'data: {"choices": [{"delta": {"content": "."}}]}', + b"data: [DONE]", + ] + + async def aiter_lines_gen(): + for line in lines: + yield line.decode() + + mock_response.aiter_lines.side_effect = aiter_lines_gen + + mock_stream_context = MagicMock() + mock_stream_context.__aenter__ = AsyncMock(return_value=mock_response) + mock_stream_context.__aexit__ = AsyncMock(return_value=None) + + # Configure the client + mock_httpx_client.stream = MagicMock(return_value=mock_stream_context) + + # Setup Agent + agent = LLMAgent("llm_agent") + agent.send = AsyncMock() # Mock the send method to verify replies + + # Simulate receiving a message from BDI + msg = InternalMessage( + to="llm_agent", sender=mock_settings.agent_settings.bdi_core_name, body="Hi" + ) + + await agent.handle_message(msg) + + # Verification + # "Hello world." constitutes one sentence/chunk based on punctuation split + # The agent should call send once with the full sentence + assert agent.send.called + args = agent.send.call_args[0][0] + assert args.to == mock_settings.agent_settings.bdi_core_name + assert "Hello world." in args.body + + +@pytest.mark.asyncio +async def test_llm_processing_errors(mock_httpx_client, mock_settings): + agent = LLMAgent("llm_agent") + agent.send = AsyncMock() + msg = InternalMessage(to="llm", sender=mock_settings.agent_settings.bdi_core_name, body="Hi") + + # HTTP Error + mock_httpx_client.stream = MagicMock(side_effect=httpx.HTTPError("Fail")) + await agent.handle_message(msg) + assert "LLM service unavailable." in agent.send.call_args[0][0].body + + # General Exception + agent.send.reset_mock() + mock_httpx_client.stream = MagicMock(side_effect=Exception("Boom")) + await agent.handle_message(msg) + assert "Error processing the request." in agent.send.call_args[0][0].body + + +@pytest.mark.asyncio +async def test_llm_json_error(mock_httpx_client, mock_settings): + # Test malformed JSON in stream + mock_response = MagicMock() + mock_response.raise_for_status = MagicMock() + + async def aiter_lines_gen(): + yield "data: {bad_json" + yield "data: [DONE]" + + mock_response.aiter_lines.side_effect = aiter_lines_gen + + mock_stream_context = MagicMock() + mock_stream_context.__aenter__ = AsyncMock(return_value=mock_response) + mock_stream_context.__aexit__ = AsyncMock(return_value=None) + mock_httpx_client.stream = MagicMock(return_value=mock_stream_context) + + agent = LLMAgent("llm_agent") + agent.send = AsyncMock() + + with patch.object(agent.logger, "error") as log: + msg = InternalMessage( + to="llm", sender=mock_settings.agent_settings.bdi_core_name, body="Hi" + ) + await agent.handle_message(msg) + log.assert_called() # Should log JSONDecodeError + + +def test_llm_instructions(): + # Full custom + instr = LLMInstructions(norms="N", goals="G") + text = instr.build_developer_instruction() + assert "Norms to follow:\nN" in text + assert "Goals to reach:\nG" in text + + # Defaults + instr_def = LLMInstructions() + text_def = instr_def.build_developer_instruction() + assert "Norms to follow" in text_def + assert "Goals to reach" in text_def diff --git a/test/unit/agents/perception/transcription_agent/test_transcription_agent.py b/test/unit/agents/perception/transcription_agent/test_transcription_agent.py new file mode 100644 index 0000000..2458ad1 --- /dev/null +++ b/test/unit/agents/perception/transcription_agent/test_transcription_agent.py @@ -0,0 +1,122 @@ +import asyncio +from unittest.mock import AsyncMock, MagicMock, patch + +import numpy as np +import pytest + +from control_backend.agents.perception.transcription_agent.speech_recognizer import ( + MLXWhisperSpeechRecognizer, + OpenAIWhisperSpeechRecognizer, + SpeechRecognizer, +) +from control_backend.agents.perception.transcription_agent.transcription_agent import ( + TranscriptionAgent, +) + + +@pytest.mark.asyncio +async def test_transcription_agent_flow(mock_zmq_context): + mock_sub = MagicMock() + mock_sub.recv = AsyncMock() + + # Setup context to return this specific mock socket + mock_zmq_context.instance.return_value.socket.return_value = mock_sub + + # Data: [Audio Bytes, Cancel Loop] + fake_audio = np.zeros(16000, dtype=np.float32).tobytes() + mock_sub.recv.side_effect = [fake_audio, asyncio.CancelledError()] + + # Mock Recognizer + with patch.object(SpeechRecognizer, "best_type") as mock_best: + mock_recognizer = MagicMock() + mock_recognizer.recognize_speech.return_value = "Hello" + mock_best.return_value = mock_recognizer + + agent = TranscriptionAgent("tcp://in") + agent.send = AsyncMock() + + agent._running = True + agent.add_behavior = AsyncMock() + + await agent.setup() + + try: + await agent._transcribing_loop() + except asyncio.CancelledError: + pass + + # Check transcription happened + assert mock_recognizer.recognize_speech.called + # Check sending + assert agent.send.called + assert agent.send.call_args[0][0].body == "Hello" + + await agent.stop() + + +@pytest.mark.asyncio +async def test_transcription_empty(mock_zmq_context): + mock_sub = MagicMock() + mock_sub.recv = AsyncMock() + mock_zmq_context.instance.return_value.socket.return_value = mock_sub + + # Return valid audio, but recognizer returns empty string + fake_audio = np.zeros(10, dtype=np.float32).tobytes() + mock_sub.recv.side_effect = [fake_audio, asyncio.CancelledError()] + + with patch.object(SpeechRecognizer, "best_type") as mock_best: + mock_recognizer = MagicMock() + mock_recognizer.recognize_speech.return_value = "" + mock_best.return_value = mock_recognizer + + agent = TranscriptionAgent("tcp://in") + agent.send = AsyncMock() + await agent.setup() + + try: + await agent._transcribing_loop() + except asyncio.CancelledError: + pass + + # Should NOT send message + agent.send.assert_not_called() + + +def test_speech_recognizer_factory(): + # Test Factory Logic + with patch("torch.mps.is_available", return_value=True): + assert isinstance(SpeechRecognizer.best_type(), MLXWhisperSpeechRecognizer) + + with patch("torch.mps.is_available", return_value=False): + assert isinstance(SpeechRecognizer.best_type(), OpenAIWhisperSpeechRecognizer) + + +def test_openai_recognizer(): + with patch("whisper.load_model") as load_mock: + with patch("whisper.transcribe") as trans_mock: + rec = OpenAIWhisperSpeechRecognizer() + rec.load_model() + load_mock.assert_called() + + trans_mock.return_value = {"text": "Hi"} + res = rec.recognize_speech(np.zeros(10)) + assert res == "Hi" + + +def test_mlx_recognizer(): + # Fix: On Linux, 'mlx_whisper' isn't imported by the module, so it's missing from dir(). + # We must use create=True to inject it into the module namespace during the test. + module_path = "control_backend.agents.perception.transcription_agent.speech_recognizer" + + with patch("sys.platform", "darwin"): + with patch(f"{module_path}.mlx_whisper", create=True) as mlx_mock: + with patch(f"{module_path}.ModelHolder", create=True) as holder_mock: + # We also need to mock mlx.core if it's used for types/constants + with patch(f"{module_path}.mx", create=True): + rec = MLXWhisperSpeechRecognizer() + rec.load_model() + holder_mock.get_model.assert_called() + + mlx_mock.transcribe.return_value = {"text": "Hi"} + res = rec.recognize_speech(np.zeros(10)) + assert res == "Hi" diff --git a/test/unit/agents/perception/vad_agent/test_vad_socket_poller.py b/test/unit/agents/perception/vad_agent/test_vad_socket_poller.py index 6ac074f..2a4ae62 100644 --- a/test/unit/agents/perception/vad_agent/test_vad_socket_poller.py +++ b/test/unit/agents/perception/vad_agent/test_vad_socket_poller.py @@ -16,8 +16,8 @@ async def test_socket_poller_with_data(socket, mocker): socket_data = b"test" socket.recv.return_value = socket_data - mock_poller: MagicMock = mocker.patch("control_backend.agents.perception.vad_agent.zmq.Poller") - mock_poller.return_value.poll.return_value = [(socket, zmq.POLLIN)] + mock_poller: MagicMock = mocker.patch("control_backend.agents.perception.vad_agent.azmq.Poller") + mock_poller.return_value.poll = AsyncMock(return_value=[(socket, zmq.POLLIN)]) poller = SocketPoller(socket) # Calling `poll` twice to be able to check that the poller is reused @@ -35,8 +35,8 @@ async def test_socket_poller_with_data(socket, mocker): @pytest.mark.asyncio async def test_socket_poller_no_data(socket, mocker): - mock_poller: MagicMock = mocker.patch("control_backend.agents.perception.vad_agent.zmq.Poller") - mock_poller.return_value.poll.return_value = [] + mock_poller: MagicMock = mocker.patch("control_backend.agents.perception.vad_agent.azmq.Poller") + mock_poller.return_value.poll = AsyncMock(return_value=[]) poller = SocketPoller(socket) data = await poller.poll() diff --git a/test/unit/agents/perception/vad_agent/test_vad_streaming.py b/test/unit/agents/perception/vad_agent/test_vad_streaming.py index 13b3f23..da2f38c 100644 --- a/test/unit/agents/perception/vad_agent/test_vad_streaming.py +++ b/test/unit/agents/perception/vad_agent/test_vad_streaming.py @@ -3,12 +3,7 @@ from unittest.mock import AsyncMock, MagicMock import numpy as np import pytest -from control_backend.agents.perception.vad_agent import StreamingBehaviour - - -@pytest.fixture -def audio_in_socket(): - return AsyncMock() +from control_backend.agents.perception.vad_agent import VADAgent @pytest.fixture @@ -17,22 +12,8 @@ def audio_out_socket(): @pytest.fixture -def mock_agent(mocker): - """Fixture to create a mock BDIAgent.""" - agent = MagicMock() - agent.jid = "vad_agent@test" - return agent - - -@pytest.fixture -def streaming(audio_in_socket, audio_out_socket, mock_agent): - import torch - - torch.hub.load.return_value = (..., ...) # Mock - streaming = StreamingBehaviour(audio_in_socket, audio_out_socket) - streaming._ready = True - streaming.agent = mock_agent - return streaming +def vad_agent(audio_out_socket): + return VADAgent("tcp://localhost:5555", False) @pytest.fixture(autouse=True) @@ -61,25 +42,40 @@ async def simulate_streaming_with_probabilities(streaming, probabilities: list[f """ model_item = MagicMock() model_item.item.side_effect = probabilities - streaming.model = MagicMock() - streaming.model.return_value = model_item + streaming.model = MagicMock(return_value=model_item) - audio_in_poller = AsyncMock() - audio_in_poller.poll.return_value = np.empty(shape=512, dtype=np.float32) - streaming.audio_in_poller = audio_in_poller + # Prepare deterministic audio chunks and a poller that stops the loop when exhausted + chunk_bytes = np.empty(shape=512, dtype=np.float32).tobytes() + chunks = [chunk_bytes for _ in probabilities] - for _ in probabilities: - await streaming.run() + class DummyPoller: + def __init__(self, data, agent): + self.data = data + self.agent = agent + + async def poll(self, timeout_ms=None): + if self.data: + return self.data.pop(0) + # Stop the loop cleanly once we've consumed all chunks + self.agent._running = False + return None + + streaming.audio_in_poller = DummyPoller(chunks, streaming) + streaming._ready = AsyncMock() + streaming._running = True + + await streaming._streaming_loop() @pytest.mark.asyncio -async def test_voice_activity_detected(audio_in_socket, audio_out_socket, streaming): +async def test_voice_activity_detected(audio_out_socket, vad_agent): """ Test a scenario where there is voice activity detected between silences. """ speech_chunk_count = 5 probabilities = [0.0] * 5 + [1.0] * speech_chunk_count + [0.0] * 5 - await simulate_streaming_with_probabilities(streaming, probabilities) + vad_agent.audio_out_socket = audio_out_socket + await simulate_streaming_with_probabilities(vad_agent, probabilities) audio_out_socket.send.assert_called_once() data = audio_out_socket.send.call_args[0][0] @@ -88,7 +84,7 @@ async def test_voice_activity_detected(audio_in_socket, audio_out_socket, stream @pytest.mark.asyncio -async def test_voice_activity_short_pause(audio_in_socket, audio_out_socket, streaming): +async def test_voice_activity_short_pause(audio_out_socket, vad_agent): """ Test a scenario where there is a short pause between speech, checking whether it ignores the short pause. @@ -97,7 +93,8 @@ async def test_voice_activity_short_pause(audio_in_socket, audio_out_socket, str probabilities = ( [0.0] * 5 + [1.0] * speech_chunk_count + [0.0] + [1.0] * speech_chunk_count + [0.0] * 5 ) - await simulate_streaming_with_probabilities(streaming, probabilities) + vad_agent.audio_out_socket = audio_out_socket + await simulate_streaming_with_probabilities(vad_agent, probabilities) audio_out_socket.send.assert_called_once() data = audio_out_socket.send.call_args[0][0] @@ -107,15 +104,22 @@ async def test_voice_activity_short_pause(audio_in_socket, audio_out_socket, str @pytest.mark.asyncio -async def test_no_data(audio_in_socket, audio_out_socket, streaming): +async def test_no_data(audio_out_socket, vad_agent): """ Test a scenario where there is no data received. This should not cause errors. """ - audio_in_poller = AsyncMock() - audio_in_poller.poll.return_value = None - streaming.audio_in_poller = audio_in_poller - await streaming.run() + class DummyPoller: + async def poll(self, timeout_ms=None): + vad_agent._running = False + return None + + vad_agent.audio_out_socket = audio_out_socket + vad_agent.audio_in_poller = DummyPoller() + vad_agent._ready = AsyncMock() + vad_agent._running = True + + await vad_agent._streaming_loop() audio_out_socket.send.assert_not_called() - assert len(streaming.audio_buffer) == 0 + assert len(vad_agent.audio_buffer) == 0 diff --git a/test/integration/api/endpoints/test_program_endpoint.py b/test/unit/api/v1/endpoints/test_program_endpoint.py similarity index 100% rename from test/integration/api/endpoints/test_program_endpoint.py rename to test/unit/api/v1/endpoints/test_program_endpoint.py diff --git a/test/integration/api/endpoints/test_robot_endpoint.py b/test/unit/api/v1/endpoints/test_robot_endpoint.py similarity index 100% rename from test/integration/api/endpoints/test_robot_endpoint.py rename to test/unit/api/v1/endpoints/test_robot_endpoint.py diff --git a/test/unit/conftest.py b/test/unit/conftest.py index 97e7d15..6ab989e 100644 --- a/test/unit/conftest.py +++ b/test/unit/conftest.py @@ -1,66 +1,43 @@ -import sys -from unittest.mock import MagicMock +from unittest.mock import MagicMock, patch + +import pytest + +from control_backend.core.agent_system import _agent_directory -def pytest_configure(config): +@pytest.fixture(autouse=True) +def reset_agent_directory(): """ - This hook runs at the start of the pytest session, before any tests are - collected. It mocks heavy or unavailable modules to prevent ImportErrors. + Automatically clears the global agent directory before and after each test + to prevent state leakage between tests. """ - # --- Mock spade and spade-bdi --- - mock_agentspeak = MagicMock() - mock_httpx = MagicMock() - mock_pydantic = MagicMock() - mock_spade = MagicMock() - mock_spade.agent = MagicMock() - mock_spade.behaviour = MagicMock() - mock_spade.message = MagicMock() - mock_spade_bdi = MagicMock() - mock_spade_bdi.bdi = MagicMock() + _agent_directory.clear() + yield + _agent_directory.clear() - mock_spade.agent.Message = MagicMock() - mock_spade.behaviour.CyclicBehaviour = type("CyclicBehaviour", (object,), {}) - mock_spade_bdi.bdi.BDIAgent = type("BDIAgent", (object,), {}) - sys.modules["agentspeak"] = mock_agentspeak - sys.modules["httpx"] = mock_httpx - sys.modules["pydantic"] = mock_pydantic - sys.modules["spade"] = mock_spade - sys.modules["spade.agent"] = mock_spade.agent - sys.modules["spade.behaviour"] = mock_spade.behaviour - sys.modules["spade.message"] = mock_spade.message - sys.modules["spade_bdi"] = mock_spade_bdi - sys.modules["spade_bdi.bdi"] = mock_spade_bdi.bdi +@pytest.fixture +def mock_settings(): + with patch("control_backend.core.config.settings") as mock: + # Set default values that match the pydantic model defaults + # to avoid AttributeErrors during tests + mock.zmq_settings.internal_pub_address = "tcp://localhost:5560" + mock.zmq_settings.internal_sub_address = "tcp://localhost:5561" + mock.zmq_settings.ri_command_address = "tcp://localhost:0000" + mock.agent_settings.bdi_core_name = "bdi_core_agent" + mock.agent_settings.bdi_belief_collector_name = "belief_collector_agent" + mock.agent_settings.llm_name = "llm_agent" + mock.agent_settings.robot_speech_name = "robot_speech_agent" + mock.agent_settings.transcription_name = "transcription_agent" + mock.agent_settings.text_belief_extractor_name = "text_belief_extractor_agent" + mock.agent_settings.vad_name = "vad_agent" + mock.behaviour_settings.sleep_s = 0.01 # Speed up tests + mock.behaviour_settings.comm_setup_max_retries = 1 + yield mock - # --- Mock the config module to prevent Pydantic ImportError --- - mock_config_module = MagicMock() - # The code under test does `from ... import settings`, so our mock module - # must have a `settings` attribute. We'll make it a MagicMock so we can - # configure it later in our tests using mocker.patch. - mock_config_module.settings = MagicMock() - - sys.modules["control_backend.core.config"] = mock_config_module - - # --- Mock torch and zmq for VAD --- - mock_torch = MagicMock() - mock_zmq = MagicMock() - mock_zmq.asyncio = mock_zmq - - # In individual tests, these can be imported and the return values changed - sys.modules["torch"] = mock_torch - sys.modules["zmq"] = mock_zmq - sys.modules["zmq.asyncio"] = mock_zmq.asyncio - - # --- Mock whisper --- - mock_whisper = MagicMock() - mock_mlx = MagicMock() - mock_mlx.core = MagicMock() - mock_mlx_whisper = MagicMock() - mock_mlx_whisper.transcribe = MagicMock() - - sys.modules["whisper"] = mock_whisper - sys.modules["mlx"] = mock_mlx - sys.modules["mlx.core"] = mock_mlx - sys.modules["mlx_whisper"] = mock_mlx_whisper - sys.modules["mlx_whisper.transcribe"] = mock_mlx_whisper.transcribe +@pytest.fixture +def mock_zmq_context(): + with patch("zmq.asyncio.Context") as mock: + mock.instance.return_value = MagicMock() + yield mock diff --git a/test/unit/core/test_agent_system.py b/test/unit/core/test_agent_system.py new file mode 100644 index 0000000..f78b230 --- /dev/null +++ b/test/unit/core/test_agent_system.py @@ -0,0 +1,72 @@ +"""Test the base class logic, message passing and background task handling.""" + +import asyncio +import logging +from unittest.mock import AsyncMock + +import pytest + +from control_backend.core.agent_system import AgentDirectory, BaseAgent, InternalMessage + + +class ConcreteTestAgent(BaseAgent): + logger = logging.getLogger("test") + + def __init__(self, name: str): + super().__init__(name) + self.received = [] + + async def setup(self): + pass + + async def handle_message(self, msg: InternalMessage): + self.received.append(msg) + if msg.body == "stop": + await self.stop() + + +@pytest.mark.asyncio +async def test_agent_lifecycle(): + agent = ConcreteTestAgent("lifecycle_agent") + await agent.start() + assert agent._running is True + + # Test background task + async def dummy_task(): + pass + + task = agent.add_behavior(dummy_task()) + assert task in agent._tasks + + await task + + # Wait for task to finish + assert task not in agent._tasks + assert len(agent._tasks) == 2 # message handling tasks are still running + + await agent.stop() + assert agent._running is False + + await asyncio.sleep(0.01) + + # Tasks should be cancelled + assert len(agent._tasks) == 0 + + +@pytest.mark.asyncio +async def test_send_unknown_agent(): + agent = ConcreteTestAgent("sender") + msg = InternalMessage(to="unknown_receiver", sender="sender", body="boo") + + agent._internal_pub_socket = AsyncMock() + + await agent.send(msg) + + agent._internal_pub_socket.send_multipart.assert_called() + + +@pytest.mark.asyncio +async def test_get_agent(): + agent = ConcreteTestAgent("registrant") + assert AgentDirectory.get("registrant") == agent + assert AgentDirectory.get("non_existent") is None diff --git a/test/unit/core/test_config.py b/test/unit/core/test_config.py new file mode 100644 index 0000000..1e23b03 --- /dev/null +++ b/test/unit/core/test_config.py @@ -0,0 +1,14 @@ +"""Test if settings load correctly and environment variables override defaults.""" + +from control_backend.core.config import Settings + + +def test_default_settings(): + settings = Settings() + assert settings.app_title == "PepperPlus" + + +def test_env_override(monkeypatch): + monkeypatch.setenv("APP_TITLE", "TestPepper") + settings = Settings() + assert settings.app_title == "TestPepper" diff --git a/test/unit/core/test_logging.py b/test/unit/core/test_logging.py new file mode 100644 index 0000000..9f0cbed --- /dev/null +++ b/test/unit/core/test_logging.py @@ -0,0 +1,88 @@ +import logging +from unittest.mock import mock_open, patch + +import pytest + +from control_backend.logging.setup_logging import add_logging_level, setup_logging + + +def test_add_logging_level(): + # Add a unique level to avoid conflicts with other tests/libraries + level_name = "TESTLEVEL" + level_num = 35 + + add_logging_level(level_name, level_num) + + assert logging.getLevelName(level_num) == level_name + assert hasattr(logging, level_name) + assert hasattr(logging.getLoggerClass(), level_name.lower()) + + # Test functionality + logger = logging.getLogger("test_custom_level") + with patch.object(logger, "_log") as mock_log: + getattr(logger, level_name.lower())("message") + mock_log.assert_called_with(level_num, "message", ()) + + # Test duplicates + with pytest.raises(AttributeError): + add_logging_level(level_name, level_num) + + with pytest.raises(AttributeError): + add_logging_level("INFO", 20) # Existing level + + +def test_setup_logging_no_file(caplog): + with patch("os.path.exists", return_value=False): + setup_logging("dummy.yaml") + assert "Logging config file not found" in caplog.text + + +def test_setup_logging_yaml_error(caplog): + with patch("os.path.exists", return_value=True): + with patch("builtins.open", mock_open(read_data="invalid: [yaml")): + with patch("logging.config.dictConfig") as mock_dict_config: + setup_logging("config.yaml") + + # Verify we logged the warning + assert "Could not load logging configuration" in caplog.text + # Verify dictConfig was called with empty dict (which would crash real dictConfig) + mock_dict_config.assert_called_with({}) + assert "Could not load logging configuration" in caplog.text + + +def test_setup_logging_success(): + config_data = """ + version: 1 + handlers: + console: + class: logging.StreamHandler + root: + handlers: [console] + level: INFO + custom_levels: + MYLEVEL: 15 + """ + with patch("os.path.exists", return_value=True): + with patch("builtins.open", mock_open(read_data=config_data)): + with patch("logging.config.dictConfig") as mock_dict_config: + setup_logging("config.yaml") + mock_dict_config.assert_called() + assert hasattr(logging, "MYLEVEL") + + +def test_setup_logging_zmq_handler(mock_zmq_context): + config_data = """ + version: 1 + handlers: + ui: + class: logging.NullHandler + # In real config this would be a zmq handler, but for unit test logic + # we just want to see if the socket injection happens + """ + with patch("os.path.exists", return_value=True): + with patch("builtins.open", mock_open(read_data=config_data)): + with patch("logging.config.dictConfig") as mock_dict_config: + setup_logging("config.yaml") + + args = mock_dict_config.call_args[0][0] + assert "interface_or_socket" in args["handlers"]["ui"] diff --git a/test/integration/schemas/test_ri_message.py b/test/unit/schemas/test_ri_message.py similarity index 100% rename from test/integration/schemas/test_ri_message.py rename to test/unit/schemas/test_ri_message.py diff --git a/test/integration/schemas/test_ui_program_message.py b/test/unit/schemas/test_ui_program_message.py similarity index 100% rename from test/integration/schemas/test_ui_program_message.py rename to test/unit/schemas/test_ui_program_message.py diff --git a/uv.lock b/uv.lock index 61c1205..ff4b8a7 100644 --- a/uv.lock +++ b/uv.lock @@ -18,66 +18,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/03/b5/e95cbd9d9e999ac8dc4e0bb7a940112a2751cf98880b4ff0626e53d14249/agentspeak-0.2.2-py3-none-any.whl", hash = "sha256:9b454bc0adf63cb0d73fb4a3a9a489e7d892d5fbf17f750de532670736c0c4dd", size = 61628, upload-time = "2024-03-21T11:55:36.741Z" }, ] -[[package]] -name = "aiodns" -version = "3.5.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pycares" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/17/0a/163e5260cecc12de6abc259d158d9da3b8ec062ab863107dcdb1166cdcef/aiodns-3.5.0.tar.gz", hash = "sha256:11264edbab51896ecf546c18eb0dd56dff0428c6aa6d2cd87e643e07300eb310", size = 14380, upload-time = "2025-06-13T16:21:53.595Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f6/2c/711076e5f5d0707b8ec55a233c8bfb193e0981a800cd1b3b123e8ff61ca1/aiodns-3.5.0-py3-none-any.whl", hash = "sha256:6d0404f7d5215849233f6ee44854f2bb2481adf71b336b2279016ea5990ca5c5", size = 8068, upload-time = "2025-06-13T16:21:52.45Z" }, -] - -[[package]] -name = "aiohappyeyeballs" -version = "2.6.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/26/30/f84a107a9c4331c14b2b586036f40965c128aa4fee4dda5d3d51cb14ad54/aiohappyeyeballs-2.6.1.tar.gz", hash = "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558", size = 22760, upload-time = "2025-03-12T01:42:48.764Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8", size = 15265, upload-time = "2025-03-12T01:42:47.083Z" }, -] - -[[package]] -name = "aiohttp" -version = "3.10.4" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "aiohappyeyeballs" }, - { name = "aiosignal" }, - { name = "attrs" }, - { name = "frozenlist" }, - { name = "multidict" }, - { name = "yarl" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/80/5e/42678cd8af232a01441b375b963a6c79943718a0cb9da90ab7e5ff14f1d3/aiohttp-3.10.4.tar.gz", hash = "sha256:23a5f97e7dd22e181967fb6cb6c3b11653b0fdbbc4bb7739d9b6052890ccab96", size = 7524267, upload-time = "2024-08-17T20:11:37.59Z" } - -[[package]] -name = "aiohttp-jinja2" -version = "1.6" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "aiohttp" }, - { name = "jinja2" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/e6/39/da5a94dd89b1af7241fb7fc99ae4e73505b5f898b540b6aba6dc7afe600e/aiohttp-jinja2-1.6.tar.gz", hash = "sha256:a3a7ff5264e5bca52e8ae547bbfd0761b72495230d438d05b6c0915be619b0e2", size = 53057, upload-time = "2023-11-18T15:30:52.559Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/eb/90/65238d4246307195411b87a07d03539049819b022c01bcc773826f600138/aiohttp_jinja2-1.6-py3-none-any.whl", hash = "sha256:0df405ee6ad1b58e5a068a105407dc7dcc1704544c559f1938babde954f945c7", size = 11736, upload-time = "2023-11-18T15:30:50.743Z" }, -] - -[[package]] -name = "aiosignal" -version = "1.4.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "frozenlist" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/61/62/06741b579156360248d1ec624842ad0edf697050bbaf7c3e46394e106ad1/aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7", size = 25007, upload-time = "2025-07-03T22:54:43.528Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", size = 7490, upload-time = "2025-07-03T22:54:42.156Z" }, -] - [[package]] name = "alabaster" version = "0.7.16" @@ -87,20 +27,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/32/34/d4e1c02d3bee589efb5dfa17f88ea08bdb3e3eac12bc475462aec52ed223/alabaster-0.7.16-py3-none-any.whl", hash = "sha256:b46733c07dce03ae4e150330b975c75737fa60f0a7c591b6c8bf4928a28e2c92", size = 13511, upload-time = "2024-01-10T00:56:08.388Z" }, ] -[[package]] -name = "alembic" -version = "1.14.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "mako" }, - { name = "sqlalchemy" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/99/09/f844822e4e847a3f0bd41797f93c4674cd4d2462a3f6c459aa528cdf786e/alembic-1.14.1.tar.gz", hash = "sha256:496e888245a53adf1498fcab31713a469c65836f8de76e01399aa1c3e90dd213", size = 1918219, upload-time = "2025-01-19T23:15:30.12Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/54/7e/ac0991d1745f7d755fc1cd381b3990a45b404b4d008fc75e2a983516fbfe/alembic-1.14.1-py3-none-any.whl", hash = "sha256:1acdd7a3a478e208b0503cd73614d5e4c6efafa4e73518bb60e4f2846a37b1c5", size = 233565, upload-time = "2025-01-19T23:15:32.523Z" }, -] - [[package]] name = "annotated-types" version = "0.7.0" @@ -123,28 +49,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/15/b3/9b1a8074496371342ec1e796a96f99c82c945a339cd81a8e73de28b4cf9e/anyio-4.11.0-py3-none-any.whl", hash = "sha256:0287e96f4d26d4149305414d4e3bc32f0dcd0862365a4bddea19d7a1ec38c4fc", size = 109097, upload-time = "2025-09-23T09:19:10.601Z" }, ] -[[package]] -name = "arrow" -version = "1.3.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "python-dateutil" }, - { name = "types-python-dateutil" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/2e/00/0f6e8fcdb23ea632c866620cc872729ff43ed91d284c866b515c6342b173/arrow-1.3.0.tar.gz", hash = "sha256:d4540617648cb5f895730f1ad8c82a65f2dad0166f57b75f3ca54759c4d67a85", size = 131960, upload-time = "2023-09-30T22:11:18.25Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f8/ed/e97229a566617f2ae958a6b13e7cc0f585470eac730a73e9e82c32a3cdd2/arrow-1.3.0-py3-none-any.whl", hash = "sha256:c728b120ebc00eb84e01882a6f5e7927a53960aa990ce7dd2b10f39005a67f80", size = 66419, upload-time = "2023-09-30T22:11:16.072Z" }, -] - -[[package]] -name = "attrs" -version = "25.4.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6b/5c/685e6633917e101e5dcb62b9dd76946cbb57c26e133bae9e0cd36033c0a9/attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11", size = 934251, upload-time = "2025-10-06T13:54:44.725Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload-time = "2025-10-06T13:54:43.17Z" }, -] - [[package]] name = "babel" version = "2.17.0" @@ -154,56 +58,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b7/b8/3fe70c75fe32afc4bb507f75563d39bc5642255d1d94f1f23604725780bf/babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2", size = 10182537, upload-time = "2025-02-01T15:17:37.39Z" }, ] -[[package]] -name = "bcrypt" -version = "4.3.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/bb/5d/6d7433e0f3cd46ce0b43cd65e1db465ea024dbb8216fb2404e919c2ad77b/bcrypt-4.3.0.tar.gz", hash = "sha256:3a3fd2204178b6d2adcf09cb4f6426ffef54762577a7c9b54c159008cb288c18", size = 25697, upload-time = "2025-02-28T01:24:09.174Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/bf/2c/3d44e853d1fe969d229bd58d39ae6902b3d924af0e2b5a60d17d4b809ded/bcrypt-4.3.0-cp313-cp313t-macosx_10_12_universal2.whl", hash = "sha256:f01e060f14b6b57bbb72fc5b4a83ac21c443c9a2ee708e04a10e9192f90a6281", size = 483719, upload-time = "2025-02-28T01:22:34.539Z" }, - { url = "https://files.pythonhosted.org/packages/a1/e2/58ff6e2a22eca2e2cff5370ae56dba29d70b1ea6fc08ee9115c3ae367795/bcrypt-4.3.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5eeac541cefd0bb887a371ef73c62c3cd78535e4887b310626036a7c0a817bb", size = 272001, upload-time = "2025-02-28T01:22:38.078Z" }, - { url = "https://files.pythonhosted.org/packages/37/1f/c55ed8dbe994b1d088309e366749633c9eb90d139af3c0a50c102ba68a1a/bcrypt-4.3.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59e1aa0e2cd871b08ca146ed08445038f42ff75968c7ae50d2fdd7860ade2180", size = 277451, upload-time = "2025-02-28T01:22:40.787Z" }, - { url = "https://files.pythonhosted.org/packages/d7/1c/794feb2ecf22fe73dcfb697ea7057f632061faceb7dcf0f155f3443b4d79/bcrypt-4.3.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:0042b2e342e9ae3d2ed22727c1262f76cc4f345683b5c1715f0250cf4277294f", size = 272792, upload-time = "2025-02-28T01:22:43.144Z" }, - { url = "https://files.pythonhosted.org/packages/13/b7/0b289506a3f3598c2ae2bdfa0ea66969812ed200264e3f61df77753eee6d/bcrypt-4.3.0-cp313-cp313t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74a8d21a09f5e025a9a23e7c0fd2c7fe8e7503e4d356c0a2c1486ba010619f09", size = 289752, upload-time = "2025-02-28T01:22:45.56Z" }, - { url = "https://files.pythonhosted.org/packages/dc/24/d0fb023788afe9e83cc118895a9f6c57e1044e7e1672f045e46733421fe6/bcrypt-4.3.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:0142b2cb84a009f8452c8c5a33ace5e3dfec4159e7735f5afe9a4d50a8ea722d", size = 277762, upload-time = "2025-02-28T01:22:47.023Z" }, - { url = "https://files.pythonhosted.org/packages/e4/38/cde58089492e55ac4ef6c49fea7027600c84fd23f7520c62118c03b4625e/bcrypt-4.3.0-cp313-cp313t-manylinux_2_34_aarch64.whl", hash = "sha256:12fa6ce40cde3f0b899729dbd7d5e8811cb892d31b6f7d0334a1f37748b789fd", size = 272384, upload-time = "2025-02-28T01:22:49.221Z" }, - { url = "https://files.pythonhosted.org/packages/de/6a/d5026520843490cfc8135d03012a413e4532a400e471e6188b01b2de853f/bcrypt-4.3.0-cp313-cp313t-manylinux_2_34_x86_64.whl", hash = "sha256:5bd3cca1f2aa5dbcf39e2aa13dd094ea181f48959e1071265de49cc2b82525af", size = 277329, upload-time = "2025-02-28T01:22:51.603Z" }, - { url = "https://files.pythonhosted.org/packages/b3/a3/4fc5255e60486466c389e28c12579d2829b28a527360e9430b4041df4cf9/bcrypt-4.3.0-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:335a420cfd63fc5bc27308e929bee231c15c85cc4c496610ffb17923abf7f231", size = 305241, upload-time = "2025-02-28T01:22:53.283Z" }, - { url = "https://files.pythonhosted.org/packages/c7/15/2b37bc07d6ce27cc94e5b10fd5058900eb8fb11642300e932c8c82e25c4a/bcrypt-4.3.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:0e30e5e67aed0187a1764911af023043b4542e70a7461ad20e837e94d23e1d6c", size = 309617, upload-time = "2025-02-28T01:22:55.461Z" }, - { url = "https://files.pythonhosted.org/packages/5f/1f/99f65edb09e6c935232ba0430c8c13bb98cb3194b6d636e61d93fe60ac59/bcrypt-4.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:3b8d62290ebefd49ee0b3ce7500f5dbdcf13b81402c05f6dafab9a1e1b27212f", size = 335751, upload-time = "2025-02-28T01:22:57.81Z" }, - { url = "https://files.pythonhosted.org/packages/00/1b/b324030c706711c99769988fcb694b3cb23f247ad39a7823a78e361bdbb8/bcrypt-4.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2ef6630e0ec01376f59a006dc72918b1bf436c3b571b80fa1968d775fa02fe7d", size = 355965, upload-time = "2025-02-28T01:22:59.181Z" }, - { url = "https://files.pythonhosted.org/packages/aa/dd/20372a0579dd915dfc3b1cd4943b3bca431866fcb1dfdfd7518c3caddea6/bcrypt-4.3.0-cp313-cp313t-win32.whl", hash = "sha256:7a4be4cbf241afee43f1c3969b9103a41b40bcb3a3f467ab19f891d9bc4642e4", size = 155316, upload-time = "2025-02-28T01:23:00.763Z" }, - { url = "https://files.pythonhosted.org/packages/6d/52/45d969fcff6b5577c2bf17098dc36269b4c02197d551371c023130c0f890/bcrypt-4.3.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5c1949bf259a388863ced887c7861da1df681cb2388645766c89fdfd9004c669", size = 147752, upload-time = "2025-02-28T01:23:02.908Z" }, - { url = "https://files.pythonhosted.org/packages/11/22/5ada0b9af72b60cbc4c9a399fdde4af0feaa609d27eb0adc61607997a3fa/bcrypt-4.3.0-cp38-abi3-macosx_10_12_universal2.whl", hash = "sha256:f81b0ed2639568bf14749112298f9e4e2b28853dab50a8b357e31798686a036d", size = 498019, upload-time = "2025-02-28T01:23:05.838Z" }, - { url = "https://files.pythonhosted.org/packages/b8/8c/252a1edc598dc1ce57905be173328eda073083826955ee3c97c7ff5ba584/bcrypt-4.3.0-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:864f8f19adbe13b7de11ba15d85d4a428c7e2f344bac110f667676a0ff84924b", size = 279174, upload-time = "2025-02-28T01:23:07.274Z" }, - { url = "https://files.pythonhosted.org/packages/29/5b/4547d5c49b85f0337c13929f2ccbe08b7283069eea3550a457914fc078aa/bcrypt-4.3.0-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e36506d001e93bffe59754397572f21bb5dc7c83f54454c990c74a468cd589e", size = 283870, upload-time = "2025-02-28T01:23:09.151Z" }, - { url = "https://files.pythonhosted.org/packages/be/21/7dbaf3fa1745cb63f776bb046e481fbababd7d344c5324eab47f5ca92dd2/bcrypt-4.3.0-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:842d08d75d9fe9fb94b18b071090220697f9f184d4547179b60734846461ed59", size = 279601, upload-time = "2025-02-28T01:23:11.461Z" }, - { url = "https://files.pythonhosted.org/packages/6d/64/e042fc8262e971347d9230d9abbe70d68b0a549acd8611c83cebd3eaec67/bcrypt-4.3.0-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7c03296b85cb87db865d91da79bf63d5609284fc0cab9472fdd8367bbd830753", size = 297660, upload-time = "2025-02-28T01:23:12.989Z" }, - { url = "https://files.pythonhosted.org/packages/50/b8/6294eb84a3fef3b67c69b4470fcdd5326676806bf2519cda79331ab3c3a9/bcrypt-4.3.0-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:62f26585e8b219cdc909b6a0069efc5e4267e25d4a3770a364ac58024f62a761", size = 284083, upload-time = "2025-02-28T01:23:14.5Z" }, - { url = "https://files.pythonhosted.org/packages/62/e6/baff635a4f2c42e8788fe1b1633911c38551ecca9a749d1052d296329da6/bcrypt-4.3.0-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:beeefe437218a65322fbd0069eb437e7c98137e08f22c4660ac2dc795c31f8bb", size = 279237, upload-time = "2025-02-28T01:23:16.686Z" }, - { url = "https://files.pythonhosted.org/packages/39/48/46f623f1b0c7dc2e5de0b8af5e6f5ac4cc26408ac33f3d424e5ad8da4a90/bcrypt-4.3.0-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:97eea7408db3a5bcce4a55d13245ab3fa566e23b4c67cd227062bb49e26c585d", size = 283737, upload-time = "2025-02-28T01:23:18.897Z" }, - { url = "https://files.pythonhosted.org/packages/49/8b/70671c3ce9c0fca4a6cc3cc6ccbaa7e948875a2e62cbd146e04a4011899c/bcrypt-4.3.0-cp38-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:191354ebfe305e84f344c5964c7cd5f924a3bfc5d405c75ad07f232b6dffb49f", size = 312741, upload-time = "2025-02-28T01:23:21.041Z" }, - { url = "https://files.pythonhosted.org/packages/27/fb/910d3a1caa2d249b6040a5caf9f9866c52114d51523ac2fb47578a27faee/bcrypt-4.3.0-cp38-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:41261d64150858eeb5ff43c753c4b216991e0ae16614a308a15d909503617732", size = 316472, upload-time = "2025-02-28T01:23:23.183Z" }, - { url = "https://files.pythonhosted.org/packages/dc/cf/7cf3a05b66ce466cfb575dbbda39718d45a609daa78500f57fa9f36fa3c0/bcrypt-4.3.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:33752b1ba962ee793fa2b6321404bf20011fe45b9afd2a842139de3011898fef", size = 343606, upload-time = "2025-02-28T01:23:25.361Z" }, - { url = "https://files.pythonhosted.org/packages/e3/b8/e970ecc6d7e355c0d892b7f733480f4aa8509f99b33e71550242cf0b7e63/bcrypt-4.3.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:50e6e80a4bfd23a25f5c05b90167c19030cf9f87930f7cb2eacb99f45d1c3304", size = 362867, upload-time = "2025-02-28T01:23:26.875Z" }, - { url = "https://files.pythonhosted.org/packages/a9/97/8d3118efd8354c555a3422d544163f40d9f236be5b96c714086463f11699/bcrypt-4.3.0-cp38-abi3-win32.whl", hash = "sha256:67a561c4d9fb9465ec866177e7aebcad08fe23aaf6fbd692a6fab69088abfc51", size = 160589, upload-time = "2025-02-28T01:23:28.381Z" }, - { url = "https://files.pythonhosted.org/packages/29/07/416f0b99f7f3997c69815365babbc2e8754181a4b1899d921b3c7d5b6f12/bcrypt-4.3.0-cp38-abi3-win_amd64.whl", hash = "sha256:584027857bc2843772114717a7490a37f68da563b3620f78a849bcb54dc11e62", size = 152794, upload-time = "2025-02-28T01:23:30.187Z" }, - { url = "https://files.pythonhosted.org/packages/6e/c1/3fa0e9e4e0bfd3fd77eb8b52ec198fd6e1fd7e9402052e43f23483f956dd/bcrypt-4.3.0-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:0d3efb1157edebfd9128e4e46e2ac1a64e0c1fe46fb023158a407c7892b0f8c3", size = 498969, upload-time = "2025-02-28T01:23:31.945Z" }, - { url = "https://files.pythonhosted.org/packages/ce/d4/755ce19b6743394787fbd7dff6bf271b27ee9b5912a97242e3caf125885b/bcrypt-4.3.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:08bacc884fd302b611226c01014eca277d48f0a05187666bca23aac0dad6fe24", size = 279158, upload-time = "2025-02-28T01:23:34.161Z" }, - { url = "https://files.pythonhosted.org/packages/9b/5d/805ef1a749c965c46b28285dfb5cd272a7ed9fa971f970435a5133250182/bcrypt-4.3.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6746e6fec103fcd509b96bacdfdaa2fbde9a553245dbada284435173a6f1aef", size = 284285, upload-time = "2025-02-28T01:23:35.765Z" }, - { url = "https://files.pythonhosted.org/packages/ab/2b/698580547a4a4988e415721b71eb45e80c879f0fb04a62da131f45987b96/bcrypt-4.3.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:afe327968aaf13fc143a56a3360cb27d4ad0345e34da12c7290f1b00b8fe9a8b", size = 279583, upload-time = "2025-02-28T01:23:38.021Z" }, - { url = "https://files.pythonhosted.org/packages/f2/87/62e1e426418204db520f955ffd06f1efd389feca893dad7095bf35612eec/bcrypt-4.3.0-cp39-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d9af79d322e735b1fc33404b5765108ae0ff232d4b54666d46730f8ac1a43676", size = 297896, upload-time = "2025-02-28T01:23:39.575Z" }, - { url = "https://files.pythonhosted.org/packages/cb/c6/8fedca4c2ada1b6e889c52d2943b2f968d3427e5d65f595620ec4c06fa2f/bcrypt-4.3.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f1e3ffa1365e8702dc48c8b360fef8d7afeca482809c5e45e653af82ccd088c1", size = 284492, upload-time = "2025-02-28T01:23:40.901Z" }, - { url = "https://files.pythonhosted.org/packages/4d/4d/c43332dcaaddb7710a8ff5269fcccba97ed3c85987ddaa808db084267b9a/bcrypt-4.3.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:3004df1b323d10021fda07a813fd33e0fd57bef0e9a480bb143877f6cba996fe", size = 279213, upload-time = "2025-02-28T01:23:42.653Z" }, - { url = "https://files.pythonhosted.org/packages/dc/7f/1e36379e169a7df3a14a1c160a49b7b918600a6008de43ff20d479e6f4b5/bcrypt-4.3.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:531457e5c839d8caea9b589a1bcfe3756b0547d7814e9ce3d437f17da75c32b0", size = 284162, upload-time = "2025-02-28T01:23:43.964Z" }, - { url = "https://files.pythonhosted.org/packages/1c/0a/644b2731194b0d7646f3210dc4d80c7fee3ecb3a1f791a6e0ae6bb8684e3/bcrypt-4.3.0-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:17a854d9a7a476a89dcef6c8bd119ad23e0f82557afbd2c442777a16408e614f", size = 312856, upload-time = "2025-02-28T01:23:46.011Z" }, - { url = "https://files.pythonhosted.org/packages/dc/62/2a871837c0bb6ab0c9a88bf54de0fc021a6a08832d4ea313ed92a669d437/bcrypt-4.3.0-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:6fb1fd3ab08c0cbc6826a2e0447610c6f09e983a281b919ed721ad32236b8b23", size = 316726, upload-time = "2025-02-28T01:23:47.575Z" }, - { url = "https://files.pythonhosted.org/packages/0c/a1/9898ea3faac0b156d457fd73a3cb9c2855c6fd063e44b8522925cdd8ce46/bcrypt-4.3.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:e965a9c1e9a393b8005031ff52583cedc15b7884fce7deb8b0346388837d6cfe", size = 343664, upload-time = "2025-02-28T01:23:49.059Z" }, - { url = "https://files.pythonhosted.org/packages/40/f2/71b4ed65ce38982ecdda0ff20c3ad1b15e71949c78b2c053df53629ce940/bcrypt-4.3.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:79e70b8342a33b52b55d93b3a59223a844962bef479f6a0ea318ebbcadf71505", size = 363128, upload-time = "2025-02-28T01:23:50.399Z" }, - { url = "https://files.pythonhosted.org/packages/11/99/12f6a58eca6dea4be992d6c681b7ec9410a1d9f5cf368c61437e31daa879/bcrypt-4.3.0-cp39-abi3-win32.whl", hash = "sha256:b4d4e57f0a63fd0b358eb765063ff661328f69a04494427265950c71b992a39a", size = 160598, upload-time = "2025-02-28T01:23:51.775Z" }, - { url = "https://files.pythonhosted.org/packages/a9/cf/45fb5261ece3e6b9817d3d82b2f343a505fd58674a92577923bc500bd1aa/bcrypt-4.3.0-cp39-abi3-win_amd64.whl", hash = "sha256:e53e074b120f2877a35cc6c736b8eb161377caae8925c17688bd46ba56daaa5b", size = 152799, upload-time = "2025-02-28T01:23:53.139Z" }, -] - [[package]] name = "certifi" version = "2025.10.5" @@ -260,11 +114,11 @@ wheels = [ [[package]] name = "cfgv" -version = "3.4.0" +version = "3.5.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/11/74/539e56497d9bd1d484fd863dd69cbbfa653cd2aa27abfe35653494d85e94/cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560", size = 7114, upload-time = "2023-08-12T20:38:17.776Z" } +sdist = { url = "https://files.pythonhosted.org/packages/4e/b5/721b8799b04bf9afe054a3899c6cf4e880fcf8563cc71c15610242490a0c/cfgv-3.5.0.tar.gz", hash = "sha256:d5b1034354820651caa73ede66a6294d6e95c1b00acc5e9b098e917404669132", size = 7334, upload-time = "2025-11-19T20:55:51.612Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c5/55/51844dd50c4fc7a33b653bfaba4c2456f06955289ca770a5dbd5fd267374/cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9", size = 7249, upload-time = "2023-08-12T20:38:16.269Z" }, + { url = "https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl", hash = "sha256:a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0", size = 7445, upload-time = "2025-11-19T20:55:50.744Z" }, ] [[package]] @@ -404,35 +258,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5f/04/642c1d8a448ae5ea1369eac8495740a79eb4e581a9fb0cbdce56bbf56da1/coverage-7.11.0-py3-none-any.whl", hash = "sha256:4b7589765348d78fb4e5fb6ea35d07564e387da2fc5efff62e0222971f155f68", size = 207761, upload-time = "2025-10-15T15:15:06.439Z" }, ] -[[package]] -name = "cryptography" -version = "43.0.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/de/ba/0664727028b37e249e73879348cc46d45c5c1a2a2e81e8166462953c5755/cryptography-43.0.1.tar.gz", hash = "sha256:203e92a75716d8cfb491dc47c79e17d0d9207ccffcbcb35f598fbe463ae3444d", size = 686927, upload-time = "2024-09-03T20:04:20.788Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/58/28/b92c98a04ba762f8cdeb54eba5c4c84e63cac037a7c5e70117d337b15ad6/cryptography-43.0.1-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:8385d98f6a3bf8bb2d65a73e17ed87a3ba84f6991c155691c51112075f9ffc5d", size = 6223222, upload-time = "2024-09-03T20:04:14.466Z" }, - { url = "https://files.pythonhosted.org/packages/33/13/1193774705783ba364121aa2a60132fa31a668b8ababd5edfa1662354ccd/cryptography-43.0.1-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:27e613d7077ac613e399270253259d9d53872aaf657471473ebfc9a52935c062", size = 3794751, upload-time = "2024-09-03T20:04:16.725Z" }, - { url = "https://files.pythonhosted.org/packages/5e/4b/39bb3c4c8cfb3e94e736b8d8859ce5c81536e91a1033b1d26770c4249000/cryptography-43.0.1-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:68aaecc4178e90719e95298515979814bda0cbada1256a4485414860bd7ab962", size = 3981827, upload-time = "2024-09-03T20:03:55.035Z" }, - { url = "https://files.pythonhosted.org/packages/ce/dc/1471d4d56608e1013237af334b8a4c35d53895694fbb73882d1c4fd3f55e/cryptography-43.0.1-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:de41fd81a41e53267cb020bb3a7212861da53a7d39f863585d13ea11049cf277", size = 3780034, upload-time = "2024-09-03T20:03:58.972Z" }, - { url = "https://files.pythonhosted.org/packages/ad/43/7a9920135b0d5437cc2f8f529fa757431eb6a7736ddfadfdee1cc5890800/cryptography-43.0.1-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f98bf604c82c416bc829e490c700ca1553eafdf2912a91e23a79d97d9801372a", size = 3993407, upload-time = "2024-09-03T20:03:36.682Z" }, - { url = "https://files.pythonhosted.org/packages/cc/42/9ab8467af6c0b76f3d9b8f01d1cf25b9c9f3f2151f4acfab888d21c55a72/cryptography-43.0.1-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:61ec41068b7b74268fa86e3e9e12b9f0c21fcf65434571dbb13d954bceb08042", size = 3886457, upload-time = "2024-09-03T20:03:52.995Z" }, - { url = "https://files.pythonhosted.org/packages/a4/65/430509e31700286ec02868a2457d2111d03ccefc20349d24e58d171ae0a7/cryptography-43.0.1-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:014f58110f53237ace6a408b5beb6c427b64e084eb451ef25a28308270086494", size = 4081499, upload-time = "2024-09-03T20:03:32.522Z" }, - { url = "https://files.pythonhosted.org/packages/bb/18/a04b6467e6e09df8c73b91dcee8878f4a438a43a3603dc3cd6f8003b92d8/cryptography-43.0.1-cp37-abi3-win32.whl", hash = "sha256:2bd51274dcd59f09dd952afb696bf9c61a7a49dfc764c04dd33ef7a6b502a1e2", size = 2616504, upload-time = "2024-09-03T20:04:09.459Z" }, - { url = "https://files.pythonhosted.org/packages/cc/73/0eacbdc437202edcbdc07f3576ed8fb8b0ab79d27bf2c5d822d758a72faa/cryptography-43.0.1-cp37-abi3-win_amd64.whl", hash = "sha256:666ae11966643886c2987b3b721899d250855718d6d9ce41b521252a17985f4d", size = 3067456, upload-time = "2024-09-03T20:03:40.775Z" }, - { url = "https://files.pythonhosted.org/packages/8a/b6/bc54b371f02cffd35ff8dc6baba88304d7cf8e83632566b4b42e00383e03/cryptography-43.0.1-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:ac119bb76b9faa00f48128b7f5679e1d8d437365c5d26f1c2c3f0da4ce1b553d", size = 6225263, upload-time = "2024-09-03T20:03:43.181Z" }, - { url = "https://files.pythonhosted.org/packages/00/0e/8217e348a1fa417ec4c78cd3cdf24154f5e76fd7597343a35bd403650dfd/cryptography-43.0.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1bbcce1a551e262dfbafb6e6252f1ae36a248e615ca44ba302df077a846a8806", size = 3794368, upload-time = "2024-09-03T20:03:18.051Z" }, - { url = "https://files.pythonhosted.org/packages/3d/ed/38b6be7254d8f7251fde8054af597ee8afa14f911da67a9410a45f602fc3/cryptography-43.0.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58d4e9129985185a06d849aa6df265bdd5a74ca6e1b736a77959b498e0505b85", size = 3981750, upload-time = "2024-09-03T20:04:18.775Z" }, - { url = "https://files.pythonhosted.org/packages/64/f3/b7946c3887cf7436f002f4cbb1e6aec77b8d299b86be48eeadfefb937c4b/cryptography-43.0.1-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:d03a475165f3134f773d1388aeb19c2d25ba88b6a9733c5c590b9ff7bbfa2e0c", size = 3778925, upload-time = "2024-09-03T20:03:45.022Z" }, - { url = "https://files.pythonhosted.org/packages/ac/7e/ebda4dd4ae098a0990753efbb4b50954f1d03003846b943ea85070782da7/cryptography-43.0.1-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:511f4273808ab590912a93ddb4e3914dfd8a388fed883361b02dea3791f292e1", size = 3993152, upload-time = "2024-09-03T20:03:30.108Z" }, - { url = "https://files.pythonhosted.org/packages/43/f6/feebbd78a3e341e3913846a3bb2c29d0b09b1b3af1573c6baabc2533e147/cryptography-43.0.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:80eda8b3e173f0f247f711eef62be51b599b5d425c429b5d4ca6a05e9e856baa", size = 3886392, upload-time = "2024-09-03T20:03:34.543Z" }, - { url = "https://files.pythonhosted.org/packages/bd/4c/ab0b9407d5247576290b4fd8abd06b7f51bd414f04eef0f2800675512d61/cryptography-43.0.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:38926c50cff6f533f8a2dae3d7f19541432610d114a70808f0926d5aaa7121e4", size = 4082606, upload-time = "2024-09-03T20:03:27.836Z" }, - { url = "https://files.pythonhosted.org/packages/05/36/e532a671998d6fcfdb9122da16434347a58a6bae9465e527e450e0bc60a5/cryptography-43.0.1-cp39-abi3-win32.whl", hash = "sha256:a575913fb06e05e6b4b814d7f7468c2c660e8bb16d8d5a1faf9b33ccc569dd47", size = 2617948, upload-time = "2024-09-03T20:03:25.446Z" }, - { url = "https://files.pythonhosted.org/packages/b3/c6/c09cee6968add5ff868525c3815e5dccc0e3c6e89eec58dc9135d3c40e88/cryptography-43.0.1-cp39-abi3-win_amd64.whl", hash = "sha256:d75601ad10b059ec832e78823b348bfa1a59f6b8d545db3a24fd44362a1564cb", size = 3070445, upload-time = "2024-09-03T20:03:21.179Z" }, -] - [[package]] name = "distlib" version = "0.4.0" @@ -559,79 +384,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ee/1b/00a78aa2e8fbd63f9af08c9c19e6deb3d5d66b4dda677a0f61654680ee89/flatbuffers-25.9.23-py2.py3-none-any.whl", hash = "sha256:255538574d6cb6d0a79a17ec8bc0d30985913b87513a01cce8bcdb6b4c44d0e2", size = 30869, upload-time = "2025-09-24T05:25:28.912Z" }, ] -[[package]] -name = "frozenlist" -version = "1.8.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/2d/f5/c831fac6cc817d26fd54c7eaccd04ef7e0288806943f7cc5bbf69f3ac1f0/frozenlist-1.8.0.tar.gz", hash = "sha256:3ede829ed8d842f6cd48fc7081d7a41001a56f1f38603f9d49bf3020d59a31ad", size = 45875, upload-time = "2025-10-06T05:38:17.865Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2d/40/0832c31a37d60f60ed79e9dfb5a92e1e2af4f40a16a29abcc7992af9edff/frozenlist-1.8.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8d92f1a84bb12d9e56f818b3a746f3efba93c1b63c8387a73dde655e1e42282a", size = 85717, upload-time = "2025-10-06T05:36:27.341Z" }, - { url = "https://files.pythonhosted.org/packages/30/ba/b0b3de23f40bc55a7057bd38434e25c34fa48e17f20ee273bbde5e0650f3/frozenlist-1.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:96153e77a591c8adc2ee805756c61f59fef4cf4073a9275ee86fe8cba41241f7", size = 49651, upload-time = "2025-10-06T05:36:28.855Z" }, - { url = "https://files.pythonhosted.org/packages/0c/ab/6e5080ee374f875296c4243c381bbdef97a9ac39c6e3ce1d5f7d42cb78d6/frozenlist-1.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f21f00a91358803399890ab167098c131ec2ddd5f8f5fd5fe9c9f2c6fcd91e40", size = 49417, upload-time = "2025-10-06T05:36:29.877Z" }, - { url = "https://files.pythonhosted.org/packages/d5/4e/e4691508f9477ce67da2015d8c00acd751e6287739123113a9fca6f1604e/frozenlist-1.8.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fb30f9626572a76dfe4293c7194a09fb1fe93ba94c7d4f720dfae3b646b45027", size = 234391, upload-time = "2025-10-06T05:36:31.301Z" }, - { url = "https://files.pythonhosted.org/packages/40/76/c202df58e3acdf12969a7895fd6f3bc016c642e6726aa63bd3025e0fc71c/frozenlist-1.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eaa352d7047a31d87dafcacbabe89df0aa506abb5b1b85a2fb91bc3faa02d822", size = 233048, upload-time = "2025-10-06T05:36:32.531Z" }, - { url = "https://files.pythonhosted.org/packages/f9/c0/8746afb90f17b73ca5979c7a3958116e105ff796e718575175319b5bb4ce/frozenlist-1.8.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:03ae967b4e297f58f8c774c7eabcce57fe3c2434817d4385c50661845a058121", size = 226549, upload-time = "2025-10-06T05:36:33.706Z" }, - { url = "https://files.pythonhosted.org/packages/7e/eb/4c7eefc718ff72f9b6c4893291abaae5fbc0c82226a32dcd8ef4f7a5dbef/frozenlist-1.8.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f6292f1de555ffcc675941d65fffffb0a5bcd992905015f85d0592201793e0e5", size = 239833, upload-time = "2025-10-06T05:36:34.947Z" }, - { url = "https://files.pythonhosted.org/packages/c2/4e/e5c02187cf704224f8b21bee886f3d713ca379535f16893233b9d672ea71/frozenlist-1.8.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29548f9b5b5e3460ce7378144c3010363d8035cea44bc0bf02d57f5a685e084e", size = 245363, upload-time = "2025-10-06T05:36:36.534Z" }, - { url = "https://files.pythonhosted.org/packages/1f/96/cb85ec608464472e82ad37a17f844889c36100eed57bea094518bf270692/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ec3cc8c5d4084591b4237c0a272cc4f50a5b03396a47d9caaf76f5d7b38a4f11", size = 229314, upload-time = "2025-10-06T05:36:38.582Z" }, - { url = "https://files.pythonhosted.org/packages/5d/6f/4ae69c550e4cee66b57887daeebe006fe985917c01d0fff9caab9883f6d0/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:517279f58009d0b1f2e7c1b130b377a349405da3f7621ed6bfae50b10adf20c1", size = 243365, upload-time = "2025-10-06T05:36:40.152Z" }, - { url = "https://files.pythonhosted.org/packages/7a/58/afd56de246cf11780a40a2c28dc7cbabbf06337cc8ddb1c780a2d97e88d8/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:db1e72ede2d0d7ccb213f218df6a078a9c09a7de257c2fe8fcef16d5925230b1", size = 237763, upload-time = "2025-10-06T05:36:41.355Z" }, - { url = "https://files.pythonhosted.org/packages/cb/36/cdfaf6ed42e2644740d4a10452d8e97fa1c062e2a8006e4b09f1b5fd7d63/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b4dec9482a65c54a5044486847b8a66bf10c9cb4926d42927ec4e8fd5db7fed8", size = 240110, upload-time = "2025-10-06T05:36:42.716Z" }, - { url = "https://files.pythonhosted.org/packages/03/a8/9ea226fbefad669f11b52e864c55f0bd57d3c8d7eb07e9f2e9a0b39502e1/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:21900c48ae04d13d416f0e1e0c4d81f7931f73a9dfa0b7a8746fb2fe7dd970ed", size = 233717, upload-time = "2025-10-06T05:36:44.251Z" }, - { url = "https://files.pythonhosted.org/packages/1e/0b/1b5531611e83ba7d13ccc9988967ea1b51186af64c42b7a7af465dcc9568/frozenlist-1.8.0-cp313-cp313-win32.whl", hash = "sha256:8b7b94a067d1c504ee0b16def57ad5738701e4ba10cec90529f13fa03c833496", size = 39628, upload-time = "2025-10-06T05:36:45.423Z" }, - { url = "https://files.pythonhosted.org/packages/d8/cf/174c91dbc9cc49bc7b7aab74d8b734e974d1faa8f191c74af9b7e80848e6/frozenlist-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:878be833caa6a3821caf85eb39c5ba92d28e85df26d57afb06b35b2efd937231", size = 43882, upload-time = "2025-10-06T05:36:46.796Z" }, - { url = "https://files.pythonhosted.org/packages/c1/17/502cd212cbfa96eb1388614fe39a3fc9ab87dbbe042b66f97acb57474834/frozenlist-1.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:44389d135b3ff43ba8cc89ff7f51f5a0bb6b63d829c8300f79a2fe4fe61bcc62", size = 39676, upload-time = "2025-10-06T05:36:47.8Z" }, - { url = "https://files.pythonhosted.org/packages/d2/5c/3bbfaa920dfab09e76946a5d2833a7cbdf7b9b4a91c714666ac4855b88b4/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:e25ac20a2ef37e91c1b39938b591457666a0fa835c7783c3a8f33ea42870db94", size = 89235, upload-time = "2025-10-06T05:36:48.78Z" }, - { url = "https://files.pythonhosted.org/packages/d2/d6/f03961ef72166cec1687e84e8925838442b615bd0b8854b54923ce5b7b8a/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:07cdca25a91a4386d2e76ad992916a85038a9b97561bf7a3fd12d5d9ce31870c", size = 50742, upload-time = "2025-10-06T05:36:49.837Z" }, - { url = "https://files.pythonhosted.org/packages/1e/bb/a6d12b7ba4c3337667d0e421f7181c82dda448ce4e7ad7ecd249a16fa806/frozenlist-1.8.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4e0c11f2cc6717e0a741f84a527c52616140741cd812a50422f83dc31749fb52", size = 51725, upload-time = "2025-10-06T05:36:50.851Z" }, - { url = "https://files.pythonhosted.org/packages/bc/71/d1fed0ffe2c2ccd70b43714c6cab0f4188f09f8a67a7914a6b46ee30f274/frozenlist-1.8.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b3210649ee28062ea6099cfda39e147fa1bc039583c8ee4481cb7811e2448c51", size = 284533, upload-time = "2025-10-06T05:36:51.898Z" }, - { url = "https://files.pythonhosted.org/packages/c9/1f/fb1685a7b009d89f9bf78a42d94461bc06581f6e718c39344754a5d9bada/frozenlist-1.8.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:581ef5194c48035a7de2aefc72ac6539823bb71508189e5de01d60c9dcd5fa65", size = 292506, upload-time = "2025-10-06T05:36:53.101Z" }, - { url = "https://files.pythonhosted.org/packages/e6/3b/b991fe1612703f7e0d05c0cf734c1b77aaf7c7d321df4572e8d36e7048c8/frozenlist-1.8.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3ef2d026f16a2b1866e1d86fc4e1291e1ed8a387b2c333809419a2f8b3a77b82", size = 274161, upload-time = "2025-10-06T05:36:54.309Z" }, - { url = "https://files.pythonhosted.org/packages/ca/ec/c5c618767bcdf66e88945ec0157d7f6c4a1322f1473392319b7a2501ded7/frozenlist-1.8.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5500ef82073f599ac84d888e3a8c1f77ac831183244bfd7f11eaa0289fb30714", size = 294676, upload-time = "2025-10-06T05:36:55.566Z" }, - { url = "https://files.pythonhosted.org/packages/7c/ce/3934758637d8f8a88d11f0585d6495ef54b2044ed6ec84492a91fa3b27aa/frozenlist-1.8.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:50066c3997d0091c411a66e710f4e11752251e6d2d73d70d8d5d4c76442a199d", size = 300638, upload-time = "2025-10-06T05:36:56.758Z" }, - { url = "https://files.pythonhosted.org/packages/fc/4f/a7e4d0d467298f42de4b41cbc7ddaf19d3cfeabaf9ff97c20c6c7ee409f9/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5c1c8e78426e59b3f8005e9b19f6ff46e5845895adbde20ece9218319eca6506", size = 283067, upload-time = "2025-10-06T05:36:57.965Z" }, - { url = "https://files.pythonhosted.org/packages/dc/48/c7b163063d55a83772b268e6d1affb960771b0e203b632cfe09522d67ea5/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:eefdba20de0d938cec6a89bd4d70f346a03108a19b9df4248d3cf0d88f1b0f51", size = 292101, upload-time = "2025-10-06T05:36:59.237Z" }, - { url = "https://files.pythonhosted.org/packages/9f/d0/2366d3c4ecdc2fd391e0afa6e11500bfba0ea772764d631bbf82f0136c9d/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:cf253e0e1c3ceb4aaff6df637ce033ff6535fb8c70a764a8f46aafd3d6ab798e", size = 289901, upload-time = "2025-10-06T05:37:00.811Z" }, - { url = "https://files.pythonhosted.org/packages/b8/94/daff920e82c1b70e3618a2ac39fbc01ae3e2ff6124e80739ce5d71c9b920/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:032efa2674356903cd0261c4317a561a6850f3ac864a63fc1583147fb05a79b0", size = 289395, upload-time = "2025-10-06T05:37:02.115Z" }, - { url = "https://files.pythonhosted.org/packages/e3/20/bba307ab4235a09fdcd3cc5508dbabd17c4634a1af4b96e0f69bfe551ebd/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6da155091429aeba16851ecb10a9104a108bcd32f6c1642867eadaee401c1c41", size = 283659, upload-time = "2025-10-06T05:37:03.711Z" }, - { url = "https://files.pythonhosted.org/packages/fd/00/04ca1c3a7a124b6de4f8a9a17cc2fcad138b4608e7a3fc5877804b8715d7/frozenlist-1.8.0-cp313-cp313t-win32.whl", hash = "sha256:0f96534f8bfebc1a394209427d0f8a63d343c9779cda6fc25e8e121b5fd8555b", size = 43492, upload-time = "2025-10-06T05:37:04.915Z" }, - { url = "https://files.pythonhosted.org/packages/59/5e/c69f733a86a94ab10f68e496dc6b7e8bc078ebb415281d5698313e3af3a1/frozenlist-1.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5d63a068f978fc69421fb0e6eb91a9603187527c86b7cd3f534a5b77a592b888", size = 48034, upload-time = "2025-10-06T05:37:06.343Z" }, - { url = "https://files.pythonhosted.org/packages/16/6c/be9d79775d8abe79b05fa6d23da99ad6e7763a1d080fbae7290b286093fd/frozenlist-1.8.0-cp313-cp313t-win_arm64.whl", hash = "sha256:bf0a7e10b077bf5fb9380ad3ae8ce20ef919a6ad93b4552896419ac7e1d8e042", size = 41749, upload-time = "2025-10-06T05:37:07.431Z" }, - { url = "https://files.pythonhosted.org/packages/f1/c8/85da824b7e7b9b6e7f7705b2ecaf9591ba6f79c1177f324c2735e41d36a2/frozenlist-1.8.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cee686f1f4cadeb2136007ddedd0aaf928ab95216e7691c63e50a8ec066336d0", size = 86127, upload-time = "2025-10-06T05:37:08.438Z" }, - { url = "https://files.pythonhosted.org/packages/8e/e8/a1185e236ec66c20afd72399522f142c3724c785789255202d27ae992818/frozenlist-1.8.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:119fb2a1bd47307e899c2fac7f28e85b9a543864df47aa7ec9d3c1b4545f096f", size = 49698, upload-time = "2025-10-06T05:37:09.48Z" }, - { url = "https://files.pythonhosted.org/packages/a1/93/72b1736d68f03fda5fdf0f2180fb6caaae3894f1b854d006ac61ecc727ee/frozenlist-1.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4970ece02dbc8c3a92fcc5228e36a3e933a01a999f7094ff7c23fbd2beeaa67c", size = 49749, upload-time = "2025-10-06T05:37:10.569Z" }, - { url = "https://files.pythonhosted.org/packages/a7/b2/fabede9fafd976b991e9f1b9c8c873ed86f202889b864756f240ce6dd855/frozenlist-1.8.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:cba69cb73723c3f329622e34bdbf5ce1f80c21c290ff04256cff1cd3c2036ed2", size = 231298, upload-time = "2025-10-06T05:37:11.993Z" }, - { url = "https://files.pythonhosted.org/packages/3a/3b/d9b1e0b0eed36e70477ffb8360c49c85c8ca8ef9700a4e6711f39a6e8b45/frozenlist-1.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:778a11b15673f6f1df23d9586f83c4846c471a8af693a22e066508b77d201ec8", size = 232015, upload-time = "2025-10-06T05:37:13.194Z" }, - { url = "https://files.pythonhosted.org/packages/dc/94/be719d2766c1138148564a3960fc2c06eb688da592bdc25adcf856101be7/frozenlist-1.8.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0325024fe97f94c41c08872db482cf8ac4800d80e79222c6b0b7b162d5b13686", size = 225038, upload-time = "2025-10-06T05:37:14.577Z" }, - { url = "https://files.pythonhosted.org/packages/e4/09/6712b6c5465f083f52f50cf74167b92d4ea2f50e46a9eea0523d658454ae/frozenlist-1.8.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:97260ff46b207a82a7567b581ab4190bd4dfa09f4db8a8b49d1a958f6aa4940e", size = 240130, upload-time = "2025-10-06T05:37:15.781Z" }, - { url = "https://files.pythonhosted.org/packages/f8/d4/cd065cdcf21550b54f3ce6a22e143ac9e4836ca42a0de1022da8498eac89/frozenlist-1.8.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:54b2077180eb7f83dd52c40b2750d0a9f175e06a42e3213ce047219de902717a", size = 242845, upload-time = "2025-10-06T05:37:17.037Z" }, - { url = "https://files.pythonhosted.org/packages/62/c3/f57a5c8c70cd1ead3d5d5f776f89d33110b1addae0ab010ad774d9a44fb9/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2f05983daecab868a31e1da44462873306d3cbfd76d1f0b5b69c473d21dbb128", size = 229131, upload-time = "2025-10-06T05:37:18.221Z" }, - { url = "https://files.pythonhosted.org/packages/6c/52/232476fe9cb64f0742f3fde2b7d26c1dac18b6d62071c74d4ded55e0ef94/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:33f48f51a446114bc5d251fb2954ab0164d5be02ad3382abcbfe07e2531d650f", size = 240542, upload-time = "2025-10-06T05:37:19.771Z" }, - { url = "https://files.pythonhosted.org/packages/5f/85/07bf3f5d0fb5414aee5f47d33c6f5c77bfe49aac680bfece33d4fdf6a246/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:154e55ec0655291b5dd1b8731c637ecdb50975a2ae70c606d100750a540082f7", size = 237308, upload-time = "2025-10-06T05:37:20.969Z" }, - { url = "https://files.pythonhosted.org/packages/11/99/ae3a33d5befd41ac0ca2cc7fd3aa707c9c324de2e89db0e0f45db9a64c26/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:4314debad13beb564b708b4a496020e5306c7333fa9a3ab90374169a20ffab30", size = 238210, upload-time = "2025-10-06T05:37:22.252Z" }, - { url = "https://files.pythonhosted.org/packages/b2/60/b1d2da22f4970e7a155f0adde9b1435712ece01b3cd45ba63702aea33938/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:073f8bf8becba60aa931eb3bc420b217bb7d5b8f4750e6f8b3be7f3da85d38b7", size = 231972, upload-time = "2025-10-06T05:37:23.5Z" }, - { url = "https://files.pythonhosted.org/packages/3f/ab/945b2f32de889993b9c9133216c068b7fcf257d8595a0ac420ac8677cab0/frozenlist-1.8.0-cp314-cp314-win32.whl", hash = "sha256:bac9c42ba2ac65ddc115d930c78d24ab8d4f465fd3fc473cdedfccadb9429806", size = 40536, upload-time = "2025-10-06T05:37:25.581Z" }, - { url = "https://files.pythonhosted.org/packages/59/ad/9caa9b9c836d9ad6f067157a531ac48b7d36499f5036d4141ce78c230b1b/frozenlist-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:3e0761f4d1a44f1d1a47996511752cf3dcec5bbdd9cc2b4fe595caf97754b7a0", size = 44330, upload-time = "2025-10-06T05:37:26.928Z" }, - { url = "https://files.pythonhosted.org/packages/82/13/e6950121764f2676f43534c555249f57030150260aee9dcf7d64efda11dd/frozenlist-1.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:d1eaff1d00c7751b7c6662e9c5ba6eb2c17a2306ba5e2a37f24ddf3cc953402b", size = 40627, upload-time = "2025-10-06T05:37:28.075Z" }, - { url = "https://files.pythonhosted.org/packages/c0/c7/43200656ecc4e02d3f8bc248df68256cd9572b3f0017f0a0c4e93440ae23/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:d3bb933317c52d7ea5004a1c442eef86f426886fba134ef8cf4226ea6ee1821d", size = 89238, upload-time = "2025-10-06T05:37:29.373Z" }, - { url = "https://files.pythonhosted.org/packages/d1/29/55c5f0689b9c0fb765055629f472c0de484dcaf0acee2f7707266ae3583c/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8009897cdef112072f93a0efdce29cd819e717fd2f649ee3016efd3cd885a7ed", size = 50738, upload-time = "2025-10-06T05:37:30.792Z" }, - { url = "https://files.pythonhosted.org/packages/ba/7d/b7282a445956506fa11da8c2db7d276adcbf2b17d8bb8407a47685263f90/frozenlist-1.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2c5dcbbc55383e5883246d11fd179782a9d07a986c40f49abe89ddf865913930", size = 51739, upload-time = "2025-10-06T05:37:32.127Z" }, - { url = "https://files.pythonhosted.org/packages/62/1c/3d8622e60d0b767a5510d1d3cf21065b9db874696a51ea6d7a43180a259c/frozenlist-1.8.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:39ecbc32f1390387d2aa4f5a995e465e9e2f79ba3adcac92d68e3e0afae6657c", size = 284186, upload-time = "2025-10-06T05:37:33.21Z" }, - { url = "https://files.pythonhosted.org/packages/2d/14/aa36d5f85a89679a85a1d44cd7a6657e0b1c75f61e7cad987b203d2daca8/frozenlist-1.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92db2bf818d5cc8d9c1f1fc56b897662e24ea5adb36ad1f1d82875bd64e03c24", size = 292196, upload-time = "2025-10-06T05:37:36.107Z" }, - { url = "https://files.pythonhosted.org/packages/05/23/6bde59eb55abd407d34f77d39a5126fb7b4f109a3f611d3929f14b700c66/frozenlist-1.8.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2dc43a022e555de94c3b68a4ef0b11c4f747d12c024a520c7101709a2144fb37", size = 273830, upload-time = "2025-10-06T05:37:37.663Z" }, - { url = "https://files.pythonhosted.org/packages/d2/3f/22cff331bfad7a8afa616289000ba793347fcd7bc275f3b28ecea2a27909/frozenlist-1.8.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb89a7f2de3602cfed448095bab3f178399646ab7c61454315089787df07733a", size = 294289, upload-time = "2025-10-06T05:37:39.261Z" }, - { url = "https://files.pythonhosted.org/packages/a4/89/5b057c799de4838b6c69aa82b79705f2027615e01be996d2486a69ca99c4/frozenlist-1.8.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:33139dc858c580ea50e7e60a1b0ea003efa1fd42e6ec7fdbad78fff65fad2fd2", size = 300318, upload-time = "2025-10-06T05:37:43.213Z" }, - { url = "https://files.pythonhosted.org/packages/30/de/2c22ab3eb2a8af6d69dc799e48455813bab3690c760de58e1bf43b36da3e/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:168c0969a329b416119507ba30b9ea13688fafffac1b7822802537569a1cb0ef", size = 282814, upload-time = "2025-10-06T05:37:45.337Z" }, - { url = "https://files.pythonhosted.org/packages/59/f7/970141a6a8dbd7f556d94977858cfb36fa9b66e0892c6dd780d2219d8cd8/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:28bd570e8e189d7f7b001966435f9dac6718324b5be2990ac496cf1ea9ddb7fe", size = 291762, upload-time = "2025-10-06T05:37:46.657Z" }, - { url = "https://files.pythonhosted.org/packages/c1/15/ca1adae83a719f82df9116d66f5bb28bb95557b3951903d39135620ef157/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b2a095d45c5d46e5e79ba1e5b9cb787f541a8dee0433836cea4b96a2c439dcd8", size = 289470, upload-time = "2025-10-06T05:37:47.946Z" }, - { url = "https://files.pythonhosted.org/packages/ac/83/dca6dc53bf657d371fbc88ddeb21b79891e747189c5de990b9dfff2ccba1/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:eab8145831a0d56ec9c4139b6c3e594c7a83c2c8be25d5bcf2d86136a532287a", size = 289042, upload-time = "2025-10-06T05:37:49.499Z" }, - { url = "https://files.pythonhosted.org/packages/96/52/abddd34ca99be142f354398700536c5bd315880ed0a213812bc491cff5e4/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:974b28cf63cc99dfb2188d8d222bc6843656188164848c4f679e63dae4b0708e", size = 283148, upload-time = "2025-10-06T05:37:50.745Z" }, - { url = "https://files.pythonhosted.org/packages/af/d3/76bd4ed4317e7119c2b7f57c3f6934aba26d277acc6309f873341640e21f/frozenlist-1.8.0-cp314-cp314t-win32.whl", hash = "sha256:342c97bf697ac5480c0a7ec73cd700ecfa5a8a40ac923bd035484616efecc2df", size = 44676, upload-time = "2025-10-06T05:37:52.222Z" }, - { url = "https://files.pythonhosted.org/packages/89/76/c615883b7b521ead2944bb3480398cbb07e12b7b4e4d073d3752eb721558/frozenlist-1.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:06be8f67f39c8b1dc671f5d83aaefd3358ae5cdcf8314552c57e7ed3e6475bdd", size = 49451, upload-time = "2025-10-06T05:37:53.425Z" }, - { url = "https://files.pythonhosted.org/packages/e0/a3/5982da14e113d07b325230f95060e2169f5311b1017ea8af2a29b374c289/frozenlist-1.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:102e6314ca4da683dca92e3b1355490fed5f313b768500084fbe6371fddfdb79", size = 42507, upload-time = "2025-10-06T05:37:54.513Z" }, - { url = "https://files.pythonhosted.org/packages/9a/9a/e35b4a917281c0b8419d4207f4334c8e8c5dbf4f3f5f9ada73958d937dcc/frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d", size = 13409, upload-time = "2025-10-06T05:38:16.721Z" }, -] - [[package]] name = "fsspec" version = "2025.9.0" @@ -641,34 +393,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/47/71/70db47e4f6ce3e5c37a607355f80da8860a33226be640226ac52cb05ef2e/fsspec-2025.9.0-py3-none-any.whl", hash = "sha256:530dc2a2af60a414a832059574df4a6e10cce927f6f4a78209390fe38955cfb7", size = 199289, upload-time = "2025-09-02T19:10:47.708Z" }, ] -[[package]] -name = "greenlet" -version = "3.2.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/03/b8/704d753a5a45507a7aab61f18db9509302ed3d0a27ac7e0359ec2905b1a6/greenlet-3.2.4.tar.gz", hash = "sha256:0dca0d95ff849f9a364385f36ab49f50065d76964944638be9691e1832e9f86d", size = 188260, upload-time = "2025-08-07T13:24:33.51Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/49/e8/58c7f85958bda41dafea50497cbd59738c5c43dbbea5ee83d651234398f4/greenlet-3.2.4-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:1a921e542453fe531144e91e1feedf12e07351b1cf6c9e8a3325ea600a715a31", size = 272814, upload-time = "2025-08-07T13:15:50.011Z" }, - { url = "https://files.pythonhosted.org/packages/62/dd/b9f59862e9e257a16e4e610480cfffd29e3fae018a68c2332090b53aac3d/greenlet-3.2.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cd3c8e693bff0fff6ba55f140bf390fa92c994083f838fece0f63be121334945", size = 641073, upload-time = "2025-08-07T13:42:57.23Z" }, - { url = "https://files.pythonhosted.org/packages/f7/0b/bc13f787394920b23073ca3b6c4a7a21396301ed75a655bcb47196b50e6e/greenlet-3.2.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:710638eb93b1fa52823aa91bf75326f9ecdfd5e0466f00789246a5280f4ba0fc", size = 655191, upload-time = "2025-08-07T13:45:29.752Z" }, - { url = "https://files.pythonhosted.org/packages/f2/d6/6adde57d1345a8d0f14d31e4ab9c23cfe8e2cd39c3baf7674b4b0338d266/greenlet-3.2.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:c5111ccdc9c88f423426df3fd1811bfc40ed66264d35aa373420a34377efc98a", size = 649516, upload-time = "2025-08-07T13:53:16.314Z" }, - { url = "https://files.pythonhosted.org/packages/7f/3b/3a3328a788d4a473889a2d403199932be55b1b0060f4ddd96ee7cdfcad10/greenlet-3.2.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d76383238584e9711e20ebe14db6c88ddcedc1829a9ad31a584389463b5aa504", size = 652169, upload-time = "2025-08-07T13:18:32.861Z" }, - { url = "https://files.pythonhosted.org/packages/ee/43/3cecdc0349359e1a527cbf2e3e28e5f8f06d3343aaf82ca13437a9aa290f/greenlet-3.2.4-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:23768528f2911bcd7e475210822ffb5254ed10d71f4028387e5a99b4c6699671", size = 610497, upload-time = "2025-08-07T13:18:31.636Z" }, - { url = "https://files.pythonhosted.org/packages/b8/19/06b6cf5d604e2c382a6f31cafafd6f33d5dea706f4db7bdab184bad2b21d/greenlet-3.2.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:00fadb3fedccc447f517ee0d3fd8fe49eae949e1cd0f6a611818f4f6fb7dc83b", size = 1121662, upload-time = "2025-08-07T13:42:41.117Z" }, - { url = "https://files.pythonhosted.org/packages/a2/15/0d5e4e1a66fab130d98168fe984c509249c833c1a3c16806b90f253ce7b9/greenlet-3.2.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:d25c5091190f2dc0eaa3f950252122edbbadbb682aa7b1ef2f8af0f8c0afefae", size = 1149210, upload-time = "2025-08-07T13:18:24.072Z" }, - { url = "https://files.pythonhosted.org/packages/1c/53/f9c440463b3057485b8594d7a638bed53ba531165ef0ca0e6c364b5cc807/greenlet-3.2.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6e343822feb58ac4d0a1211bd9399de2b3a04963ddeec21530fc426cc121f19b", size = 1564759, upload-time = "2025-11-04T12:42:19.395Z" }, - { url = "https://files.pythonhosted.org/packages/47/e4/3bb4240abdd0a8d23f4f88adec746a3099f0d86bfedb623f063b2e3b4df0/greenlet-3.2.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ca7f6f1f2649b89ce02f6f229d7c19f680a6238af656f61e0115b24857917929", size = 1634288, upload-time = "2025-11-04T12:42:21.174Z" }, - { url = "https://files.pythonhosted.org/packages/0b/55/2321e43595e6801e105fcfdee02b34c0f996eb71e6ddffca6b10b7e1d771/greenlet-3.2.4-cp313-cp313-win_amd64.whl", hash = "sha256:554b03b6e73aaabec3745364d6239e9e012d64c68ccd0b8430c64ccc14939a8b", size = 299685, upload-time = "2025-08-07T13:24:38.824Z" }, - { url = "https://files.pythonhosted.org/packages/22/5c/85273fd7cc388285632b0498dbbab97596e04b154933dfe0f3e68156c68c/greenlet-3.2.4-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:49a30d5fda2507ae77be16479bdb62a660fa51b1eb4928b524975b3bde77b3c0", size = 273586, upload-time = "2025-08-07T13:16:08.004Z" }, - { url = "https://files.pythonhosted.org/packages/d1/75/10aeeaa3da9332c2e761e4c50d4c3556c21113ee3f0afa2cf5769946f7a3/greenlet-3.2.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:299fd615cd8fc86267b47597123e3f43ad79c9d8a22bebdce535e53550763e2f", size = 686346, upload-time = "2025-08-07T13:42:59.944Z" }, - { url = "https://files.pythonhosted.org/packages/c0/aa/687d6b12ffb505a4447567d1f3abea23bd20e73a5bed63871178e0831b7a/greenlet-3.2.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:c17b6b34111ea72fc5a4e4beec9711d2226285f0386ea83477cbb97c30a3f3a5", size = 699218, upload-time = "2025-08-07T13:45:30.969Z" }, - { url = "https://files.pythonhosted.org/packages/dc/8b/29aae55436521f1d6f8ff4e12fb676f3400de7fcf27fccd1d4d17fd8fecd/greenlet-3.2.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b4a1870c51720687af7fa3e7cda6d08d801dae660f75a76f3845b642b4da6ee1", size = 694659, upload-time = "2025-08-07T13:53:17.759Z" }, - { url = "https://files.pythonhosted.org/packages/92/2e/ea25914b1ebfde93b6fc4ff46d6864564fba59024e928bdc7de475affc25/greenlet-3.2.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:061dc4cf2c34852b052a8620d40f36324554bc192be474b9e9770e8c042fd735", size = 695355, upload-time = "2025-08-07T13:18:34.517Z" }, - { url = "https://files.pythonhosted.org/packages/72/60/fc56c62046ec17f6b0d3060564562c64c862948c9d4bc8aa807cf5bd74f4/greenlet-3.2.4-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:44358b9bf66c8576a9f57a590d5f5d6e72fa4228b763d0e43fee6d3b06d3a337", size = 657512, upload-time = "2025-08-07T13:18:33.969Z" }, - { url = "https://files.pythonhosted.org/packages/23/6e/74407aed965a4ab6ddd93a7ded3180b730d281c77b765788419484cdfeef/greenlet-3.2.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2917bdf657f5859fbf3386b12d68ede4cf1f04c90c3a6bc1f013dd68a22e2269", size = 1612508, upload-time = "2025-11-04T12:42:23.427Z" }, - { url = "https://files.pythonhosted.org/packages/0d/da/343cd760ab2f92bac1845ca07ee3faea9fe52bee65f7bcb19f16ad7de08b/greenlet-3.2.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:015d48959d4add5d6c9f6c5210ee3803a830dce46356e3bc326d6776bde54681", size = 1680760, upload-time = "2025-11-04T12:42:25.341Z" }, - { url = "https://files.pythonhosted.org/packages/e3/a5/6ddab2b4c112be95601c13428db1d8b6608a8b6039816f2ba09c346c08fc/greenlet-3.2.4-cp314-cp314-win_amd64.whl", hash = "sha256:e37ab26028f12dbb0ff65f29a8d3d44a765c61e729647bf2ddfbbed621726f01", size = 303425, upload-time = "2025-08-07T13:32:27.59Z" }, -] - [[package]] name = "h11" version = "0.16.0" @@ -819,19 +543,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/20/9a/e5d9ec41927401e41aea8af6d16e78b5e612bca4699d417f646a9610a076/Jinja2-3.0.3-py3-none-any.whl", hash = "sha256:077ce6014f7b40d03b47d1f1ca4b0fc8328a692bd284016f806ed0eaca390ad8", size = 133630, upload-time = "2021-11-09T20:27:27.116Z" }, ] -[[package]] -name = "jinja2-time" -version = "0.2.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "arrow" }, - { name = "jinja2" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/de/7c/ee2f2014a2a0616ad3328e58e7dac879251babdb4cb796d770b5d32c469f/jinja2-time-0.2.0.tar.gz", hash = "sha256:d14eaa4d315e7688daa4969f616f226614350c48730bfa1692d2caebd8c90d40", size = 5701, upload-time = "2016-06-08T23:36:52.454Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6a/a1/d44fa38306ffa34a7e1af09632b158e13ec89670ce491f8a15af3ebcb4e4/jinja2_time-0.2.0-py2.py3-none-any.whl", hash = "sha256:d3eab6605e3ec8b7a0863df09cc1d23714908fa61aa6986a845c20ba488b4efa", size = 6360, upload-time = "2016-06-08T23:36:48.197Z" }, -] - [[package]] name = "llvmlite" version = "0.45.1" @@ -845,31 +556,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/09/56/ed35668130e32dbfad2eb37356793b0a95f23494ab5be7d9bf5cb75850ee/llvmlite-0.45.1-cp313-cp313-win_amd64.whl", hash = "sha256:080e6f8d0778a8239cd47686d402cb66eb165e421efa9391366a9b7e5810a38b", size = 38132232, upload-time = "2025-10-01T18:05:14.477Z" }, ] -[[package]] -name = "loguru" -version = "0.7.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, - { name = "win32-setctime", marker = "sys_platform == 'win32'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/9e/30/d87a423766b24db416a46e9335b9602b054a72b96a88a241f2b09b560fa8/loguru-0.7.2.tar.gz", hash = "sha256:e671a53522515f34fd406340ee968cb9ecafbc4b36c679da03c18fd8d0bd51ac", size = 145103, upload-time = "2023-09-11T15:24:37.926Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/03/0a/4f6fed21aa246c6b49b561ca55facacc2a44b87d65b8b92362a8e99ba202/loguru-0.7.2-py3-none-any.whl", hash = "sha256:003d71e3d3ed35f0f8984898359d65b79e5b21943f78af86aa5491210429b8eb", size = 62549, upload-time = "2023-09-11T15:24:35.016Z" }, -] - -[[package]] -name = "mako" -version = "1.3.10" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "markupsafe" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/9e/38/bd5b78a920a64d708fe6bc8e0a2c075e1389d53bef8413725c63ba041535/mako-1.3.10.tar.gz", hash = "sha256:99579a6f39583fa7e5630a28c3c1f440e4e97a414b80372649c0ce338da2ea28", size = 392474, upload-time = "2025-04-10T12:44:31.16Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/87/fb/99f81ac72ae23375f22b7afdb7642aba97c00a713c217124420147681a2f/mako-1.3.10-py3-none-any.whl", hash = "sha256:baef24a52fc4fc514a0887ac600f9f1cff3d82c61d4d700a1fa84d597b88db59", size = 78509, upload-time = "2025-04-10T12:50:53.297Z" }, -] - [[package]] name = "markdown-it-py" version = "4.0.0" @@ -1003,87 +689,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c", size = 536198, upload-time = "2023-03-07T16:47:09.197Z" }, ] -[[package]] -name = "multidict" -version = "6.7.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/80/1e/5492c365f222f907de1039b91f922b93fa4f764c713ee858d235495d8f50/multidict-6.7.0.tar.gz", hash = "sha256:c6e99d9a65ca282e578dfea819cfa9c0a62b2499d8677392e09feaf305e9e6f5", size = 101834, upload-time = "2025-10-06T14:52:30.657Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d2/86/33272a544eeb36d66e4d9a920602d1a2f57d4ebea4ef3cdfe5a912574c95/multidict-6.7.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:bee7c0588aa0076ce77c0ea5d19a68d76ad81fcd9fe8501003b9a24f9d4000f6", size = 76135, upload-time = "2025-10-06T14:49:54.26Z" }, - { url = "https://files.pythonhosted.org/packages/91/1c/eb97db117a1ebe46d457a3d235a7b9d2e6dcab174f42d1b67663dd9e5371/multidict-6.7.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7ef6b61cad77091056ce0e7ce69814ef72afacb150b7ac6a3e9470def2198159", size = 45117, upload-time = "2025-10-06T14:49:55.82Z" }, - { url = "https://files.pythonhosted.org/packages/f1/d8/6c3442322e41fb1dd4de8bd67bfd11cd72352ac131f6368315617de752f1/multidict-6.7.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9c0359b1ec12b1d6849c59f9d319610b7f20ef990a6d454ab151aa0e3b9f78ca", size = 43472, upload-time = "2025-10-06T14:49:57.048Z" }, - { url = "https://files.pythonhosted.org/packages/75/3f/e2639e80325af0b6c6febdf8e57cc07043ff15f57fa1ef808f4ccb5ac4cd/multidict-6.7.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cd240939f71c64bd658f186330603aac1a9a81bf6273f523fca63673cb7378a8", size = 249342, upload-time = "2025-10-06T14:49:58.368Z" }, - { url = "https://files.pythonhosted.org/packages/5d/cc/84e0585f805cbeaa9cbdaa95f9a3d6aed745b9d25700623ac89a6ecff400/multidict-6.7.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a60a4d75718a5efa473ebd5ab685786ba0c67b8381f781d1be14da49f1a2dc60", size = 257082, upload-time = "2025-10-06T14:49:59.89Z" }, - { url = "https://files.pythonhosted.org/packages/b0/9c/ac851c107c92289acbbf5cfb485694084690c1b17e555f44952c26ddc5bd/multidict-6.7.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:53a42d364f323275126aff81fb67c5ca1b7a04fda0546245730a55c8c5f24bc4", size = 240704, upload-time = "2025-10-06T14:50:01.485Z" }, - { url = "https://files.pythonhosted.org/packages/50/cc/5f93e99427248c09da95b62d64b25748a5f5c98c7c2ab09825a1d6af0e15/multidict-6.7.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3b29b980d0ddbecb736735ee5bef69bb2ddca56eff603c86f3f29a1128299b4f", size = 266355, upload-time = "2025-10-06T14:50:02.955Z" }, - { url = "https://files.pythonhosted.org/packages/ec/0c/2ec1d883ceb79c6f7f6d7ad90c919c898f5d1c6ea96d322751420211e072/multidict-6.7.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f8a93b1c0ed2d04b97a5e9336fd2d33371b9a6e29ab7dd6503d63407c20ffbaf", size = 267259, upload-time = "2025-10-06T14:50:04.446Z" }, - { url = "https://files.pythonhosted.org/packages/c6/2d/f0b184fa88d6630aa267680bdb8623fb69cb0d024b8c6f0d23f9a0f406d3/multidict-6.7.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9ff96e8815eecacc6645da76c413eb3b3d34cfca256c70b16b286a687d013c32", size = 254903, upload-time = "2025-10-06T14:50:05.98Z" }, - { url = "https://files.pythonhosted.org/packages/06/c9/11ea263ad0df7dfabcad404feb3c0dd40b131bc7f232d5537f2fb1356951/multidict-6.7.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7516c579652f6a6be0e266aec0acd0db80829ca305c3d771ed898538804c2036", size = 252365, upload-time = "2025-10-06T14:50:07.511Z" }, - { url = "https://files.pythonhosted.org/packages/41/88/d714b86ee2c17d6e09850c70c9d310abac3d808ab49dfa16b43aba9d53fd/multidict-6.7.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:040f393368e63fb0f3330e70c26bfd336656bed925e5cbe17c9da839a6ab13ec", size = 250062, upload-time = "2025-10-06T14:50:09.074Z" }, - { url = "https://files.pythonhosted.org/packages/15/fe/ad407bb9e818c2b31383f6131ca19ea7e35ce93cf1310fce69f12e89de75/multidict-6.7.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b3bc26a951007b1057a1c543af845f1c7e3e71cc240ed1ace7bf4484aa99196e", size = 249683, upload-time = "2025-10-06T14:50:10.714Z" }, - { url = "https://files.pythonhosted.org/packages/8c/a4/a89abdb0229e533fb925e7c6e5c40201c2873efebc9abaf14046a4536ee6/multidict-6.7.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7b022717c748dd1992a83e219587aabe45980d88969f01b316e78683e6285f64", size = 261254, upload-time = "2025-10-06T14:50:12.28Z" }, - { url = "https://files.pythonhosted.org/packages/8d/aa/0e2b27bd88b40a4fb8dc53dd74eecac70edaa4c1dd0707eb2164da3675b3/multidict-6.7.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:9600082733859f00d79dee64effc7aef1beb26adb297416a4ad2116fd61374bd", size = 257967, upload-time = "2025-10-06T14:50:14.16Z" }, - { url = "https://files.pythonhosted.org/packages/d0/8e/0c67b7120d5d5f6d874ed85a085f9dc770a7f9d8813e80f44a9fec820bb7/multidict-6.7.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:94218fcec4d72bc61df51c198d098ce2b378e0ccbac41ddbed5ef44092913288", size = 250085, upload-time = "2025-10-06T14:50:15.639Z" }, - { url = "https://files.pythonhosted.org/packages/ba/55/b73e1d624ea4b8fd4dd07a3bb70f6e4c7c6c5d9d640a41c6ffe5cdbd2a55/multidict-6.7.0-cp313-cp313-win32.whl", hash = "sha256:a37bd74c3fa9d00be2d7b8eca074dc56bd8077ddd2917a839bd989612671ed17", size = 41713, upload-time = "2025-10-06T14:50:17.066Z" }, - { url = "https://files.pythonhosted.org/packages/32/31/75c59e7d3b4205075b4c183fa4ca398a2daf2303ddf616b04ae6ef55cffe/multidict-6.7.0-cp313-cp313-win_amd64.whl", hash = "sha256:30d193c6cc6d559db42b6bcec8a5d395d34d60c9877a0b71ecd7c204fcf15390", size = 45915, upload-time = "2025-10-06T14:50:18.264Z" }, - { url = "https://files.pythonhosted.org/packages/31/2a/8987831e811f1184c22bc2e45844934385363ee61c0a2dcfa8f71b87e608/multidict-6.7.0-cp313-cp313-win_arm64.whl", hash = "sha256:ea3334cabe4d41b7ccd01e4d349828678794edbc2d3ae97fc162a3312095092e", size = 43077, upload-time = "2025-10-06T14:50:19.853Z" }, - { url = "https://files.pythonhosted.org/packages/e8/68/7b3a5170a382a340147337b300b9eb25a9ddb573bcdfff19c0fa3f31ffba/multidict-6.7.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:ad9ce259f50abd98a1ca0aa6e490b58c316a0fce0617f609723e40804add2c00", size = 83114, upload-time = "2025-10-06T14:50:21.223Z" }, - { url = "https://files.pythonhosted.org/packages/55/5c/3fa2d07c84df4e302060f555bbf539310980362236ad49f50eeb0a1c1eb9/multidict-6.7.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:07f5594ac6d084cbb5de2df218d78baf55ef150b91f0ff8a21cc7a2e3a5a58eb", size = 48442, upload-time = "2025-10-06T14:50:22.871Z" }, - { url = "https://files.pythonhosted.org/packages/fc/56/67212d33239797f9bd91962bb899d72bb0f4c35a8652dcdb8ed049bef878/multidict-6.7.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:0591b48acf279821a579282444814a2d8d0af624ae0bc600aa4d1b920b6e924b", size = 46885, upload-time = "2025-10-06T14:50:24.258Z" }, - { url = "https://files.pythonhosted.org/packages/46/d1/908f896224290350721597a61a69cd19b89ad8ee0ae1f38b3f5cd12ea2ac/multidict-6.7.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:749a72584761531d2b9467cfbdfd29487ee21124c304c4b6cb760d8777b27f9c", size = 242588, upload-time = "2025-10-06T14:50:25.716Z" }, - { url = "https://files.pythonhosted.org/packages/ab/67/8604288bbd68680eee0ab568fdcb56171d8b23a01bcd5cb0c8fedf6e5d99/multidict-6.7.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b4c3d199f953acd5b446bf7c0de1fe25d94e09e79086f8dc2f48a11a129cdf1", size = 249966, upload-time = "2025-10-06T14:50:28.192Z" }, - { url = "https://files.pythonhosted.org/packages/20/33/9228d76339f1ba51e3efef7da3ebd91964d3006217aae13211653193c3ff/multidict-6.7.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:9fb0211dfc3b51efea2f349ec92c114d7754dd62c01f81c3e32b765b70c45c9b", size = 228618, upload-time = "2025-10-06T14:50:29.82Z" }, - { url = "https://files.pythonhosted.org/packages/f8/2d/25d9b566d10cab1c42b3b9e5b11ef79c9111eaf4463b8c257a3bd89e0ead/multidict-6.7.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a027ec240fe73a8d6281872690b988eed307cd7d91b23998ff35ff577ca688b5", size = 257539, upload-time = "2025-10-06T14:50:31.731Z" }, - { url = "https://files.pythonhosted.org/packages/b6/b1/8d1a965e6637fc33de3c0d8f414485c2b7e4af00f42cab3d84e7b955c222/multidict-6.7.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1d964afecdf3a8288789df2f5751dc0a8261138c3768d9af117ed384e538fad", size = 256345, upload-time = "2025-10-06T14:50:33.26Z" }, - { url = "https://files.pythonhosted.org/packages/ba/0c/06b5a8adbdeedada6f4fb8d8f193d44a347223b11939b42953eeb6530b6b/multidict-6.7.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:caf53b15b1b7df9fbd0709aa01409000a2b4dd03a5f6f5cc548183c7c8f8b63c", size = 247934, upload-time = "2025-10-06T14:50:34.808Z" }, - { url = "https://files.pythonhosted.org/packages/8f/31/b2491b5fe167ca044c6eb4b8f2c9f3b8a00b24c432c365358eadac5d7625/multidict-6.7.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:654030da3197d927f05a536a66186070e98765aa5142794c9904555d3a9d8fb5", size = 245243, upload-time = "2025-10-06T14:50:36.436Z" }, - { url = "https://files.pythonhosted.org/packages/61/1a/982913957cb90406c8c94f53001abd9eafc271cb3e70ff6371590bec478e/multidict-6.7.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:2090d3718829d1e484706a2f525e50c892237b2bf9b17a79b059cb98cddc2f10", size = 235878, upload-time = "2025-10-06T14:50:37.953Z" }, - { url = "https://files.pythonhosted.org/packages/be/c0/21435d804c1a1cf7a2608593f4d19bca5bcbd7a81a70b253fdd1c12af9c0/multidict-6.7.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:2d2cfeec3f6f45651b3d408c4acec0ebf3daa9bc8a112a084206f5db5d05b754", size = 243452, upload-time = "2025-10-06T14:50:39.574Z" }, - { url = "https://files.pythonhosted.org/packages/54/0a/4349d540d4a883863191be6eb9a928846d4ec0ea007d3dcd36323bb058ac/multidict-6.7.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:4ef089f985b8c194d341eb2c24ae6e7408c9a0e2e5658699c92f497437d88c3c", size = 252312, upload-time = "2025-10-06T14:50:41.612Z" }, - { url = "https://files.pythonhosted.org/packages/26/64/d5416038dbda1488daf16b676e4dbfd9674dde10a0cc8f4fc2b502d8125d/multidict-6.7.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e93a0617cd16998784bf4414c7e40f17a35d2350e5c6f0bd900d3a8e02bd3762", size = 246935, upload-time = "2025-10-06T14:50:43.972Z" }, - { url = "https://files.pythonhosted.org/packages/9f/8c/8290c50d14e49f35e0bd4abc25e1bc7711149ca9588ab7d04f886cdf03d9/multidict-6.7.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f0feece2ef8ebc42ed9e2e8c78fc4aa3cf455733b507c09ef7406364c94376c6", size = 243385, upload-time = "2025-10-06T14:50:45.648Z" }, - { url = "https://files.pythonhosted.org/packages/ef/a0/f83ae75e42d694b3fbad3e047670e511c138be747bc713cf1b10d5096416/multidict-6.7.0-cp313-cp313t-win32.whl", hash = "sha256:19a1d55338ec1be74ef62440ca9e04a2f001a04d0cc49a4983dc320ff0f3212d", size = 47777, upload-time = "2025-10-06T14:50:47.154Z" }, - { url = "https://files.pythonhosted.org/packages/dc/80/9b174a92814a3830b7357307a792300f42c9e94664b01dee8e457551fa66/multidict-6.7.0-cp313-cp313t-win_amd64.whl", hash = "sha256:3da4fb467498df97e986af166b12d01f05d2e04f978a9c1c680ea1988e0bc4b6", size = 53104, upload-time = "2025-10-06T14:50:48.851Z" }, - { url = "https://files.pythonhosted.org/packages/cc/28/04baeaf0428d95bb7a7bea0e691ba2f31394338ba424fb0679a9ed0f4c09/multidict-6.7.0-cp313-cp313t-win_arm64.whl", hash = "sha256:b4121773c49a0776461f4a904cdf6264c88e42218aaa8407e803ca8025872792", size = 45503, upload-time = "2025-10-06T14:50:50.16Z" }, - { url = "https://files.pythonhosted.org/packages/e2/b1/3da6934455dd4b261d4c72f897e3a5728eba81db59959f3a639245891baa/multidict-6.7.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3bab1e4aff7adaa34410f93b1f8e57c4b36b9af0426a76003f441ee1d3c7e842", size = 75128, upload-time = "2025-10-06T14:50:51.92Z" }, - { url = "https://files.pythonhosted.org/packages/14/2c/f069cab5b51d175a1a2cb4ccdf7a2c2dabd58aa5bd933fa036a8d15e2404/multidict-6.7.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b8512bac933afc3e45fb2b18da8e59b78d4f408399a960339598374d4ae3b56b", size = 44410, upload-time = "2025-10-06T14:50:53.275Z" }, - { url = "https://files.pythonhosted.org/packages/42/e2/64bb41266427af6642b6b128e8774ed84c11b80a90702c13ac0a86bb10cc/multidict-6.7.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:79dcf9e477bc65414ebfea98ffd013cb39552b5ecd62908752e0e413d6d06e38", size = 43205, upload-time = "2025-10-06T14:50:54.911Z" }, - { url = "https://files.pythonhosted.org/packages/02/68/6b086fef8a3f1a8541b9236c594f0c9245617c29841f2e0395d979485cde/multidict-6.7.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:31bae522710064b5cbeddaf2e9f32b1abab70ac6ac91d42572502299e9953128", size = 245084, upload-time = "2025-10-06T14:50:56.369Z" }, - { url = "https://files.pythonhosted.org/packages/15/ee/f524093232007cd7a75c1d132df70f235cfd590a7c9eaccd7ff422ef4ae8/multidict-6.7.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a0df7ff02397bb63e2fd22af2c87dfa39e8c7f12947bc524dbdc528282c7e34", size = 252667, upload-time = "2025-10-06T14:50:57.991Z" }, - { url = "https://files.pythonhosted.org/packages/02/a5/eeb3f43ab45878f1895118c3ef157a480db58ede3f248e29b5354139c2c9/multidict-6.7.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7a0222514e8e4c514660e182d5156a415c13ef0aabbd71682fc714e327b95e99", size = 233590, upload-time = "2025-10-06T14:50:59.589Z" }, - { url = "https://files.pythonhosted.org/packages/6a/1e/76d02f8270b97269d7e3dbd45644b1785bda457b474315f8cf999525a193/multidict-6.7.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2397ab4daaf2698eb51a76721e98db21ce4f52339e535725de03ea962b5a3202", size = 264112, upload-time = "2025-10-06T14:51:01.183Z" }, - { url = "https://files.pythonhosted.org/packages/76/0b/c28a70ecb58963847c2a8efe334904cd254812b10e535aefb3bcce513918/multidict-6.7.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8891681594162635948a636c9fe0ff21746aeb3dd5463f6e25d9bea3a8a39ca1", size = 261194, upload-time = "2025-10-06T14:51:02.794Z" }, - { url = "https://files.pythonhosted.org/packages/b4/63/2ab26e4209773223159b83aa32721b4021ffb08102f8ac7d689c943fded1/multidict-6.7.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18706cc31dbf402a7945916dd5cddf160251b6dab8a2c5f3d6d5a55949f676b3", size = 248510, upload-time = "2025-10-06T14:51:04.724Z" }, - { url = "https://files.pythonhosted.org/packages/93/cd/06c1fa8282af1d1c46fd55c10a7930af652afdce43999501d4d68664170c/multidict-6.7.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f844a1bbf1d207dd311a56f383f7eda2d0e134921d45751842d8235e7778965d", size = 248395, upload-time = "2025-10-06T14:51:06.306Z" }, - { url = "https://files.pythonhosted.org/packages/99/ac/82cb419dd6b04ccf9e7e61befc00c77614fc8134362488b553402ecd55ce/multidict-6.7.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:d4393e3581e84e5645506923816b9cc81f5609a778c7e7534054091acc64d1c6", size = 239520, upload-time = "2025-10-06T14:51:08.091Z" }, - { url = "https://files.pythonhosted.org/packages/fa/f3/a0f9bf09493421bd8716a362e0cd1d244f5a6550f5beffdd6b47e885b331/multidict-6.7.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:fbd18dc82d7bf274b37aa48d664534330af744e03bccf696d6f4c6042e7d19e7", size = 245479, upload-time = "2025-10-06T14:51:10.365Z" }, - { url = "https://files.pythonhosted.org/packages/8d/01/476d38fc73a212843f43c852b0eee266b6971f0e28329c2184a8df90c376/multidict-6.7.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:b6234e14f9314731ec45c42fc4554b88133ad53a09092cc48a88e771c125dadb", size = 258903, upload-time = "2025-10-06T14:51:12.466Z" }, - { url = "https://files.pythonhosted.org/packages/49/6d/23faeb0868adba613b817d0e69c5f15531b24d462af8012c4f6de4fa8dc3/multidict-6.7.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:08d4379f9744d8f78d98c8673c06e202ffa88296f009c71bbafe8a6bf847d01f", size = 252333, upload-time = "2025-10-06T14:51:14.48Z" }, - { url = "https://files.pythonhosted.org/packages/1e/cc/48d02ac22b30fa247f7dad82866e4b1015431092f4ba6ebc7e77596e0b18/multidict-6.7.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9fe04da3f79387f450fd0061d4dd2e45a72749d31bf634aecc9e27f24fdc4b3f", size = 243411, upload-time = "2025-10-06T14:51:16.072Z" }, - { url = "https://files.pythonhosted.org/packages/4a/03/29a8bf5a18abf1fe34535c88adbdfa88c9fb869b5a3b120692c64abe8284/multidict-6.7.0-cp314-cp314-win32.whl", hash = "sha256:fbafe31d191dfa7c4c51f7a6149c9fb7e914dcf9ffead27dcfd9f1ae382b3885", size = 40940, upload-time = "2025-10-06T14:51:17.544Z" }, - { url = "https://files.pythonhosted.org/packages/82/16/7ed27b680791b939de138f906d5cf2b4657b0d45ca6f5dd6236fdddafb1a/multidict-6.7.0-cp314-cp314-win_amd64.whl", hash = "sha256:2f67396ec0310764b9222a1728ced1ab638f61aadc6226f17a71dd9324f9a99c", size = 45087, upload-time = "2025-10-06T14:51:18.875Z" }, - { url = "https://files.pythonhosted.org/packages/cd/3c/e3e62eb35a1950292fe39315d3c89941e30a9d07d5d2df42965ab041da43/multidict-6.7.0-cp314-cp314-win_arm64.whl", hash = "sha256:ba672b26069957ee369cfa7fc180dde1fc6f176eaf1e6beaf61fbebbd3d9c000", size = 42368, upload-time = "2025-10-06T14:51:20.225Z" }, - { url = "https://files.pythonhosted.org/packages/8b/40/cd499bd0dbc5f1136726db3153042a735fffd0d77268e2ee20d5f33c010f/multidict-6.7.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:c1dcc7524066fa918c6a27d61444d4ee7900ec635779058571f70d042d86ed63", size = 82326, upload-time = "2025-10-06T14:51:21.588Z" }, - { url = "https://files.pythonhosted.org/packages/13/8a/18e031eca251c8df76daf0288e6790561806e439f5ce99a170b4af30676b/multidict-6.7.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:27e0b36c2d388dc7b6ced3406671b401e84ad7eb0656b8f3a2f46ed0ce483718", size = 48065, upload-time = "2025-10-06T14:51:22.93Z" }, - { url = "https://files.pythonhosted.org/packages/40/71/5e6701277470a87d234e433fb0a3a7deaf3bcd92566e421e7ae9776319de/multidict-6.7.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2a7baa46a22e77f0988e3b23d4ede5513ebec1929e34ee9495be535662c0dfe2", size = 46475, upload-time = "2025-10-06T14:51:24.352Z" }, - { url = "https://files.pythonhosted.org/packages/fe/6a/bab00cbab6d9cfb57afe1663318f72ec28289ea03fd4e8236bb78429893a/multidict-6.7.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7bf77f54997a9166a2f5675d1201520586439424c2511723a7312bdb4bcc034e", size = 239324, upload-time = "2025-10-06T14:51:25.822Z" }, - { url = "https://files.pythonhosted.org/packages/2a/5f/8de95f629fc22a7769ade8b41028e3e5a822c1f8904f618d175945a81ad3/multidict-6.7.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e011555abada53f1578d63389610ac8a5400fc70ce71156b0aa30d326f1a5064", size = 246877, upload-time = "2025-10-06T14:51:27.604Z" }, - { url = "https://files.pythonhosted.org/packages/23/b4/38881a960458f25b89e9f4a4fdcb02ac101cfa710190db6e5528841e67de/multidict-6.7.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:28b37063541b897fd6a318007373930a75ca6d6ac7c940dbe14731ffdd8d498e", size = 225824, upload-time = "2025-10-06T14:51:29.664Z" }, - { url = "https://files.pythonhosted.org/packages/1e/39/6566210c83f8a261575f18e7144736059f0c460b362e96e9cf797a24b8e7/multidict-6.7.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:05047ada7a2fde2631a0ed706f1fd68b169a681dfe5e4cf0f8e4cb6618bbc2cd", size = 253558, upload-time = "2025-10-06T14:51:31.684Z" }, - { url = "https://files.pythonhosted.org/packages/00/a3/67f18315100f64c269f46e6c0319fa87ba68f0f64f2b8e7fd7c72b913a0b/multidict-6.7.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:716133f7d1d946a4e1b91b1756b23c088881e70ff180c24e864c26192ad7534a", size = 252339, upload-time = "2025-10-06T14:51:33.699Z" }, - { url = "https://files.pythonhosted.org/packages/c8/2a/1cb77266afee2458d82f50da41beba02159b1d6b1f7973afc9a1cad1499b/multidict-6.7.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d1bed1b467ef657f2a0ae62844a607909ef1c6889562de5e1d505f74457d0b96", size = 244895, upload-time = "2025-10-06T14:51:36.189Z" }, - { url = "https://files.pythonhosted.org/packages/dd/72/09fa7dd487f119b2eb9524946ddd36e2067c08510576d43ff68469563b3b/multidict-6.7.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ca43bdfa5d37bd6aee89d85e1d0831fb86e25541be7e9d376ead1b28974f8e5e", size = 241862, upload-time = "2025-10-06T14:51:41.291Z" }, - { url = "https://files.pythonhosted.org/packages/65/92/bc1f8bd0853d8669300f732c801974dfc3702c3eeadae2f60cef54dc69d7/multidict-6.7.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:44b546bd3eb645fd26fb949e43c02a25a2e632e2ca21a35e2e132c8105dc8599", size = 232376, upload-time = "2025-10-06T14:51:43.55Z" }, - { url = "https://files.pythonhosted.org/packages/09/86/ac39399e5cb9d0c2ac8ef6e10a768e4d3bc933ac808d49c41f9dc23337eb/multidict-6.7.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:a6ef16328011d3f468e7ebc326f24c1445f001ca1dec335b2f8e66bed3006394", size = 240272, upload-time = "2025-10-06T14:51:45.265Z" }, - { url = "https://files.pythonhosted.org/packages/3d/b6/fed5ac6b8563ec72df6cb1ea8dac6d17f0a4a1f65045f66b6d3bf1497c02/multidict-6.7.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:5aa873cbc8e593d361ae65c68f85faadd755c3295ea2c12040ee146802f23b38", size = 248774, upload-time = "2025-10-06T14:51:46.836Z" }, - { url = "https://files.pythonhosted.org/packages/6b/8d/b954d8c0dc132b68f760aefd45870978deec6818897389dace00fcde32ff/multidict-6.7.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:3d7b6ccce016e29df4b7ca819659f516f0bc7a4b3efa3bb2012ba06431b044f9", size = 242731, upload-time = "2025-10-06T14:51:48.541Z" }, - { url = "https://files.pythonhosted.org/packages/16/9d/a2dac7009125d3540c2f54e194829ea18ac53716c61b655d8ed300120b0f/multidict-6.7.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:171b73bd4ee683d307599b66793ac80981b06f069b62eea1c9e29c9241aa66b0", size = 240193, upload-time = "2025-10-06T14:51:50.355Z" }, - { url = "https://files.pythonhosted.org/packages/39/ca/c05f144128ea232ae2178b008d5011d4e2cea86e4ee8c85c2631b1b94802/multidict-6.7.0-cp314-cp314t-win32.whl", hash = "sha256:b2d7f80c4e1fd010b07cb26820aae86b7e73b681ee4889684fb8d2d4537aab13", size = 48023, upload-time = "2025-10-06T14:51:51.883Z" }, - { url = "https://files.pythonhosted.org/packages/ba/8f/0a60e501584145588be1af5cc829265701ba3c35a64aec8e07cbb71d39bb/multidict-6.7.0-cp314-cp314t-win_amd64.whl", hash = "sha256:09929cab6fcb68122776d575e03c6cc64ee0b8fca48d17e135474b042ce515cd", size = 53507, upload-time = "2025-10-06T14:51:53.672Z" }, - { url = "https://files.pythonhosted.org/packages/7f/ae/3148b988a9c6239903e786eac19c889fab607c31d6efa7fb2147e5680f23/multidict-6.7.0-cp314-cp314t-win_arm64.whl", hash = "sha256:cc41db090ed742f32bd2d2c721861725e6109681eddf835d0a82bd3a5c382827", size = 44804, upload-time = "2025-10-06T14:51:55.415Z" }, - { url = "https://files.pythonhosted.org/packages/b7/da/7d22601b625e241d4f23ef1ebff8acfc60da633c9e7e7922e24d10f592b3/multidict-6.7.0-py3-none-any.whl", hash = "sha256:394fc5c42a333c9ffc3e421a4c85e08580d990e08b99f6bf35b4132114c5dcb3", size = 12317, upload-time = "2025-10-06T14:52:29.272Z" }, -] - [[package]] name = "networkx" version = "3.5" @@ -1382,6 +987,7 @@ name = "pepperplus-cb" version = "0.1.0" source = { virtual = "." } dependencies = [ + { name = "agentspeak" }, { name = "colorlog" }, { name = "fastapi", extra = ["all"] }, { name = "mlx-whisper", marker = "sys_platform == 'darwin'" }, @@ -1390,16 +996,10 @@ dependencies = [ { name = "pyaudio" }, { name = "pydantic" }, { name = "pydantic-settings" }, - { name = "pytest" }, - { name = "pytest-asyncio" }, - { name = "pytest-cov" }, - { name = "pytest-mock" }, { name = "python-json-logger" }, { name = "pyyaml" }, { name = "pyzmq" }, { name = "silero-vad" }, - { name = "spade" }, - { name = "spade-bdi" }, { name = "sphinx" }, { name = "sphinx-rtd-theme" }, { name = "torch" }, @@ -1409,22 +1009,34 @@ dependencies = [ [package.dev-dependencies] dev = [ { name = "pre-commit" }, - { name = "ruff" }, - { name = "ruff-format" }, -] -integration-test = [ - { name = "soundfile" }, -] -test = [ - { name = "numpy" }, { name = "pytest" }, { name = "pytest-asyncio" }, { name = "pytest-cov" }, { name = "pytest-mock" }, + { name = "ruff" }, + { name = "ruff-format" }, + { name = "soundfile" }, +] +test = [ + { name = "agentspeak" }, + { name = "fastapi" }, + { name = "httpx" }, + { name = "mlx-whisper", marker = "sys_platform == 'darwin'" }, + { name = "openai-whisper" }, + { name = "pydantic" }, + { name = "pydantic-settings" }, + { name = "pytest" }, + { name = "pytest-asyncio" }, + { name = "pytest-cov" }, + { name = "pytest-mock" }, + { name = "pyyaml" }, + { name = "pyzmq" }, + { name = "soundfile" }, ] [package.metadata] requires-dist = [ + { name = "agentspeak", specifier = ">=0.2.2" }, { name = "colorlog", specifier = ">=6.10.1" }, { name = "fastapi", extras = ["all"], specifier = ">=0.115.6" }, { name = "mlx-whisper", marker = "sys_platform == 'darwin'", specifier = ">=0.4.3" }, @@ -1433,16 +1045,10 @@ requires-dist = [ { name = "pyaudio", specifier = ">=0.2.14" }, { name = "pydantic", specifier = ">=2.12.0" }, { name = "pydantic-settings", specifier = ">=2.11.0" }, - { name = "pytest", specifier = ">=8.4.2" }, - { name = "pytest-asyncio", specifier = ">=1.2.0" }, - { name = "pytest-cov", specifier = ">=7.0.0" }, - { name = "pytest-mock", specifier = ">=3.15.1" }, { name = "python-json-logger", specifier = ">=4.0.0" }, { name = "pyyaml", specifier = ">=6.0.3" }, { name = "pyzmq", specifier = ">=27.1.0" }, { name = "silero-vad", specifier = ">=6.0.0" }, - { name = "spade", specifier = ">=4.1.0" }, - { name = "spade-bdi", specifier = ">=0.3.2" }, { name = "sphinx", specifier = ">=7.3.7" }, { name = "sphinx-rtd-theme", specifier = ">=3.0.2" }, { name = "torch", specifier = ">=2.8.0" }, @@ -1452,16 +1058,29 @@ requires-dist = [ [package.metadata.requires-dev] dev = [ { name = "pre-commit", specifier = ">=4.3.0" }, - { name = "ruff", specifier = ">=0.14.2" }, - { name = "ruff-format", specifier = ">=0.3.0" }, -] -integration-test = [{ name = "soundfile", specifier = ">=0.13.1" }] -test = [ - { name = "numpy", specifier = ">=2.3.3" }, { name = "pytest", specifier = ">=8.4.2" }, { name = "pytest-asyncio", specifier = ">=1.2.0" }, { name = "pytest-cov", specifier = ">=7.0.0" }, { name = "pytest-mock", specifier = ">=3.15.1" }, + { name = "ruff", specifier = ">=0.14.2" }, + { name = "ruff-format", specifier = ">=0.3.0" }, + { name = "soundfile", specifier = ">=0.13.1" }, +] +test = [ + { name = "agentspeak", specifier = ">=0.2.2" }, + { name = "fastapi", specifier = ">=0.115.6" }, + { name = "httpx", specifier = ">=0.28.1" }, + { name = "mlx-whisper", marker = "sys_platform == 'darwin'", specifier = ">=0.4.3" }, + { name = "openai-whisper", specifier = ">=20250625" }, + { name = "pydantic", specifier = ">=2.12.0" }, + { name = "pydantic-settings", specifier = ">=2.11.0" }, + { name = "pytest", specifier = ">=8.4.2" }, + { name = "pytest-asyncio", specifier = ">=1.2.0" }, + { name = "pytest-cov", specifier = ">=7.0.0" }, + { name = "pytest-mock", specifier = ">=3.15.1" }, + { name = "pyyaml", specifier = ">=6.0.3" }, + { name = "pyzmq", specifier = ">=27.1.0" }, + { name = "soundfile", specifier = ">=0.13.1" }, ] [[package]] @@ -1484,7 +1103,7 @@ wheels = [ [[package]] name = "pre-commit" -version = "4.3.0" +version = "4.4.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cfgv" }, @@ -1493,78 +1112,9 @@ dependencies = [ { name = "pyyaml" }, { name = "virtualenv" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ff/29/7cf5bbc236333876e4b41f56e06857a87937ce4bf91e117a6991a2dbb02a/pre_commit-4.3.0.tar.gz", hash = "sha256:499fe450cc9d42e9d58e606262795ecb64dd05438943c62b66f6a8673da30b16", size = 193792, upload-time = "2025-08-09T18:56:14.651Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a6/49/7845c2d7bf6474efd8e27905b51b11e6ce411708c91e829b93f324de9929/pre_commit-4.4.0.tar.gz", hash = "sha256:f0233ebab440e9f17cabbb558706eb173d19ace965c68cdce2c081042b4fab15", size = 197501, upload-time = "2025-11-08T21:12:11.607Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5b/a5/987a405322d78a73b66e39e4a90e4ef156fd7141bf71df987e50717c321b/pre_commit-4.3.0-py2.py3-none-any.whl", hash = "sha256:2b0747ad7e6e967169136edffee14c16e148a778a54e4f967921aa1ebf2308d8", size = 220965, upload-time = "2025-08-09T18:56:13.192Z" }, -] - -[[package]] -name = "propcache" -version = "0.4.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ea/c8/d70cd26d845c6d85479d8f5a11a0fd7151e9bc4794cc5e6eb5a790f12df8/propcache-0.4.0.tar.gz", hash = "sha256:c1ad731253eb738f9cadd9fa1844e019576c70bca6a534252e97cf33a57da529", size = 45187, upload-time = "2025-10-04T21:57:39.546Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/cd/dd/f405b0fe84d29d356895bc048404d3321a2df849281cf3f932158c9346ac/propcache-0.4.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e2d01fd53e89cb3d71d20b8c225a8c70d84660f2d223afc7ed7851a4086afe6d", size = 77565, upload-time = "2025-10-04T21:55:52.907Z" }, - { url = "https://files.pythonhosted.org/packages/c0/48/dfb2c45e1b0d92228c9c66fa929af7316c15cbe69a7e438786aaa60c1b3c/propcache-0.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7dfa60953169d2531dd8ae306e9c27c5d4e5efe7a2ba77049e8afdaece062937", size = 44602, upload-time = "2025-10-04T21:55:54.406Z" }, - { url = "https://files.pythonhosted.org/packages/d0/d9/b15e88b4463df45a7793fb04e2b5497334f8fcc24e281c221150a0af9aff/propcache-0.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:227892597953611fce2601d49f1d1f39786a6aebc2f253c2de775407f725a3f6", size = 46168, upload-time = "2025-10-04T21:55:55.537Z" }, - { url = "https://files.pythonhosted.org/packages/40/ac/983e69cce8800251aab85858069cf9359b22222a9cda47591e03e2f24eec/propcache-0.4.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5e0a5bc019014531308fb67d86066d235daa7551baf2e00e1ea7b00531f6ea85", size = 207997, upload-time = "2025-10-04T21:55:57.022Z" }, - { url = "https://files.pythonhosted.org/packages/ae/9c/5586a7a54e7e0b9a87fdd8ba935961f398c0e6eaecd57baaa8eca468a236/propcache-0.4.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6ebc6e2e65c31356310ddb6519420eaa6bb8c30fbd809d0919129c89dcd70f4c", size = 210948, upload-time = "2025-10-04T21:55:58.397Z" }, - { url = "https://files.pythonhosted.org/packages/5f/ba/644e367f8a86461d45bd023ace521180938e76515040550af9b44085e99a/propcache-0.4.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1927b78dd75fc31a7fdc76cc7039e39f3170cb1d0d9a271e60f0566ecb25211a", size = 217988, upload-time = "2025-10-04T21:56:00.251Z" }, - { url = "https://files.pythonhosted.org/packages/24/0e/1e21af74b4732d002b0452605bdf31d6bf990fd8b720cb44e27a97d80db5/propcache-0.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5b113feeda47f908562d9a6d0e05798ad2f83d4473c0777dafa2bc7756473218", size = 204442, upload-time = "2025-10-04T21:56:01.93Z" }, - { url = "https://files.pythonhosted.org/packages/fd/30/ae2eec96995a8a760acb9a0b6c92b9815f1fc885c7d8481237ccb554eab0/propcache-0.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4596c12aa7e3bb2abf158ea8f79eb0fb4851606695d04ab846b2bb386f5690a1", size = 199371, upload-time = "2025-10-04T21:56:03.25Z" }, - { url = "https://files.pythonhosted.org/packages/45/1d/a18fac8cb04f8379ccb79cf15aac31f4167a270d1cd1111f33c0d38ce4fb/propcache-0.4.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:6d1f67dad8cc36e8abc2207a77f3f952ac80be7404177830a7af4635a34cbc16", size = 196638, upload-time = "2025-10-04T21:56:04.619Z" }, - { url = "https://files.pythonhosted.org/packages/48/45/3549a2b6f74dce6f21b2664d078bd26ceb876aae9c58f3c017cf590f0ee3/propcache-0.4.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:e6229ad15366cd8b6d6b4185c55dd48debf9ca546f91416ba2e5921ad6e210a6", size = 203651, upload-time = "2025-10-04T21:56:06.153Z" }, - { url = "https://files.pythonhosted.org/packages/7d/f0/90ea14d518c919fc154332742a9302db3004af4f1d3df688676959733283/propcache-0.4.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:2a4bf309d057327f1f227a22ac6baf34a66f9af75e08c613e47c4d775b06d6c7", size = 205726, upload-time = "2025-10-04T21:56:07.955Z" }, - { url = "https://files.pythonhosted.org/packages/f6/de/8efc1dbafeb42108e7af744822cdca944b990869e9da70e79efb21569d6b/propcache-0.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c2e274f3d1cbb2ddcc7a55ce3739af0f8510edc68a7f37981b2258fa1eedc833", size = 199576, upload-time = "2025-10-04T21:56:09.43Z" }, - { url = "https://files.pythonhosted.org/packages/d7/38/4d79fe3477b050398fb8d8f59301ed116d8c6ea3c4dbf09498c679103f90/propcache-0.4.0-cp313-cp313-win32.whl", hash = "sha256:f114a3e1f8034e2957d34043b7a317a8a05d97dfe8fddb36d9a2252c0117dbbc", size = 37474, upload-time = "2025-10-04T21:56:10.74Z" }, - { url = "https://files.pythonhosted.org/packages/36/9b/a283daf665a1945cff1b03d1104e7c9ee92bb7b6bbcc6518b24fcdac8bd0/propcache-0.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:9ba68c57cde9c667f6b65b98bc342dfa7240b1272ffb2c24b32172ee61b6d281", size = 40685, upload-time = "2025-10-04T21:56:11.896Z" }, - { url = "https://files.pythonhosted.org/packages/e9/f7/def8fc0b4d7a89f1628f337cb122bb9a946c5ed97760f2442b27b7fa5a69/propcache-0.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:eb77a85253174bf73e52c968b689d64be62d71e8ac33cabef4ca77b03fb4ef92", size = 37046, upload-time = "2025-10-04T21:56:13.021Z" }, - { url = "https://files.pythonhosted.org/packages/ca/6b/f6e8b36b58d17dfb6c505b9ae1163fcf7a4cf98825032fdc77bba4ab5c4a/propcache-0.4.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:c0e1c218fff95a66ad9f2f83ad41a67cf4d0a3f527efe820f57bde5fda616de4", size = 81274, upload-time = "2025-10-04T21:56:14.206Z" }, - { url = "https://files.pythonhosted.org/packages/8e/c5/1fd0baa222b8faf53ba04dd4f34de33ea820b80e34f87c7960666bae5f4f/propcache-0.4.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:5710b1c01472542bb024366803812ca13e8774d21381bcfc1f7ae738eeb38acc", size = 46232, upload-time = "2025-10-04T21:56:15.337Z" }, - { url = "https://files.pythonhosted.org/packages/cb/6b/7aa5324983cab7666ed58fc32c68a0430468a18e02e3f04e7a879c002414/propcache-0.4.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d7f008799682e8826ce98f25e8bc43532d2cd26c187a1462499fa8d123ae054f", size = 48239, upload-time = "2025-10-04T21:56:16.768Z" }, - { url = "https://files.pythonhosted.org/packages/24/0f/58c192301c0436762ed5fed5a3edadb0ae399cb73528fb9c1b5cb8e53523/propcache-0.4.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0596d2ae99d74ca436553eb9ce11fe4163dc742fcf8724ebe07d7cb0db679bb1", size = 275804, upload-time = "2025-10-04T21:56:18.066Z" }, - { url = "https://files.pythonhosted.org/packages/f7/b9/092ee32064ebfabedae4251952787e63e551075af1a1205e8061b3ed5838/propcache-0.4.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ab9c1bd95ebd1689f0e24f2946c495808777e9e8df7bb3c1dfe3e9eb7f47fe0d", size = 273996, upload-time = "2025-10-04T21:56:19.801Z" }, - { url = "https://files.pythonhosted.org/packages/43/82/becf618ed28e732f3bba3df172cd290a1afbd99f291074f747fd5bd031bb/propcache-0.4.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a8ef2ea819549ae2e8698d2ec229ae948d7272feea1cb2878289f767b6c585a4", size = 280266, upload-time = "2025-10-04T21:56:21.136Z" }, - { url = "https://files.pythonhosted.org/packages/51/be/b370930249a9332a81b5c4c550dac614b7e11b6c160080777e903d57e197/propcache-0.4.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:71a400b2f0b079438cc24f9a27f02eff24d8ef78f2943f949abc518b844ade3d", size = 263186, upload-time = "2025-10-04T21:56:22.787Z" }, - { url = "https://files.pythonhosted.org/packages/33/b6/546fd3e31770aed3aed1c01b120944c689edb510aeb7a25472edc472ce23/propcache-0.4.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4c2735d3305e6cecab6e53546909edf407ad3da5b9eeaf483f4cf80142bb21be", size = 260721, upload-time = "2025-10-04T21:56:24.22Z" }, - { url = "https://files.pythonhosted.org/packages/80/70/3751930d16e5984490c73ca65b80777e4b26e7a0015f2d41f31d75959a71/propcache-0.4.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:72b51340047ac43b3cf388eebd362d052632260c9f73a50882edbb66e589fd44", size = 247516, upload-time = "2025-10-04T21:56:25.577Z" }, - { url = "https://files.pythonhosted.org/packages/59/90/4bc96ce6476f67e2e6b72469f328c92b53259a0e4d1d5386d71a36e9258c/propcache-0.4.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:184c779363740d6664982ad05699f378f7694220e2041996f12b7c2a4acdcad0", size = 262675, upload-time = "2025-10-04T21:56:27.065Z" }, - { url = "https://files.pythonhosted.org/packages/6f/d1/f16d096869c5f1c93d67fc37488c0c814add0560574f6877653a10239cde/propcache-0.4.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:a60634a9de41f363923c6adfb83105d39e49f7a3058511563ed3de6748661af6", size = 263379, upload-time = "2025-10-04T21:56:28.517Z" }, - { url = "https://files.pythonhosted.org/packages/ab/2a/da5cd1bc1c6412939c457ea65bbe7e034045c395d98ff8ff880d06ec4553/propcache-0.4.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c9b8119244d122241a9c4566bce49bb20408a6827044155856735cf14189a7da", size = 257694, upload-time = "2025-10-04T21:56:30.051Z" }, - { url = "https://files.pythonhosted.org/packages/a5/11/938e67c07189b662a6c72551d48285a02496de885408392447c25657dd47/propcache-0.4.0-cp313-cp313t-win32.whl", hash = "sha256:515b610a364c8cdd2b72c734cc97dece85c416892ea8d5c305624ac8734e81db", size = 41321, upload-time = "2025-10-04T21:56:31.406Z" }, - { url = "https://files.pythonhosted.org/packages/f4/6e/72b11a4dcae68c728b15126cc5bc830bf275c84836da2633412b768d07e0/propcache-0.4.0-cp313-cp313t-win_amd64.whl", hash = "sha256:7ea86eb32e74f9902df57e8608e8ac66f1e1e1d24d1ed2ddeb849888413b924d", size = 44846, upload-time = "2025-10-04T21:56:32.5Z" }, - { url = "https://files.pythonhosted.org/packages/94/09/0ef3c025e0621e703ef71b69e0085181a3124bcc1beef29e0ffef59ed7f4/propcache-0.4.0-cp313-cp313t-win_arm64.whl", hash = "sha256:c1443fa4bb306461a3a8a52b7de0932a2515b100ecb0ebc630cc3f87d451e0a9", size = 39689, upload-time = "2025-10-04T21:56:33.686Z" }, - { url = "https://files.pythonhosted.org/packages/60/89/7699d8e9f8c222bbef1fae26afd72d448353f164a52125d5f87dd9fec2c7/propcache-0.4.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:de8e310d24b5a61de08812dd70d5234da1458d41b059038ee7895a9e4c8cae79", size = 77977, upload-time = "2025-10-04T21:56:34.836Z" }, - { url = "https://files.pythonhosted.org/packages/77/c5/2758a498199ce46d6d500ba4391a8594df35400cc85738aa9f0c9b8366db/propcache-0.4.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:55a54de5266bc44aa274915cdf388584fa052db8748a869e5500ab5993bac3f4", size = 44715, upload-time = "2025-10-04T21:56:36.075Z" }, - { url = "https://files.pythonhosted.org/packages/0d/da/5a44e10282a28c2dd576e5e1a2c7bb8145587070ddab7375fb643f7129d7/propcache-0.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:88d50d662c917ec2c9d3858920aa7b9d5bfb74ab9c51424b775ccbe683cb1b4e", size = 46463, upload-time = "2025-10-04T21:56:37.227Z" }, - { url = "https://files.pythonhosted.org/packages/d5/5a/b2c314f655f46c10c204dc0d69e19fadfb1cc4d40ab33f403698a35c3281/propcache-0.4.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ae3adf88a66f5863cf79394bc359da523bb27a2ed6ba9898525a6a02b723bfc5", size = 206980, upload-time = "2025-10-04T21:56:38.828Z" }, - { url = "https://files.pythonhosted.org/packages/7c/4e/f6643ec2cd5527b92c93488f9b67a170494736bb1c5460136399d709ce5a/propcache-0.4.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7f088e21d15b3abdb9047e4b7b7a0acd79bf166893ac2b34a72ab1062feb219e", size = 211385, upload-time = "2025-10-04T21:56:40.2Z" }, - { url = "https://files.pythonhosted.org/packages/71/41/362766a346c3f8d3bbeb7899e1ff40f18844e0fe37e9f6f536553cf6b6be/propcache-0.4.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a4efbaf10793fd574c76a5732c75452f19d93df6e0f758c67dd60552ebd8614b", size = 215315, upload-time = "2025-10-04T21:56:41.574Z" }, - { url = "https://files.pythonhosted.org/packages/ff/98/17385d51816d56fa6acc035d8625fbf833b6a795d7ef7fb37ea3f62db6c9/propcache-0.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:681a168d06284602d56e97f09978057aa88bcc4177352b875b3d781df4efd4cb", size = 201416, upload-time = "2025-10-04T21:56:42.947Z" }, - { url = "https://files.pythonhosted.org/packages/7a/83/801178ca1c29e217564ee507ff2a49d3f24a4dd85c9b9d681fd1d62b15f2/propcache-0.4.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a7f06f077fc4ef37e8a37ca6bbb491b29e29db9fb28e29cf3896aad10dbd4137", size = 197726, upload-time = "2025-10-04T21:56:44.313Z" }, - { url = "https://files.pythonhosted.org/packages/d2/38/c8743917bca92b7e5474366b6b04c7b3982deac32a0fe4b705f2e92c09bb/propcache-0.4.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:082a643479f49a6778dcd68a80262fc324b14fd8e9b1a5380331fe41adde1738", size = 192819, upload-time = "2025-10-04T21:56:45.702Z" }, - { url = "https://files.pythonhosted.org/packages/0b/74/3de3ef483e8615aaaf62026fcdcb20cbfc4535ea14871b12f72d52c1d6dc/propcache-0.4.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:26692850120241a99bb4a4eec675cd7b4fdc431144f0d15ef69f7f8599f6165f", size = 202492, upload-time = "2025-10-04T21:56:47.388Z" }, - { url = "https://files.pythonhosted.org/packages/46/86/a130dd85199d651a6986ba6bf1ce297b7bbcafc01c8e139e6ba2b8218a20/propcache-0.4.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:33ad7d37b9a386f97582f5d042cc7b8d4b3591bb384cf50866b749a17e4dba90", size = 204106, upload-time = "2025-10-04T21:56:49.139Z" }, - { url = "https://files.pythonhosted.org/packages/b2/f7/44eab58659d71d21995146c94139e63882bac280065b3a9ed10376897bcc/propcache-0.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1e7fd82d4a5b7583588f103b0771e43948532f1292105f13ee6f3b300933c4ca", size = 198043, upload-time = "2025-10-04T21:56:50.561Z" }, - { url = "https://files.pythonhosted.org/packages/96/14/df37be1bf1423d2dda201a4cdb1c5cb44048d34e31a97df227cc25b0a55c/propcache-0.4.0-cp314-cp314-win32.whl", hash = "sha256:213eb0d3bc695a70cffffe11a1c2e1c2698d89ffd8dba35a49bc44a035d45c93", size = 38036, upload-time = "2025-10-04T21:56:51.868Z" }, - { url = "https://files.pythonhosted.org/packages/99/96/9cea65d6c50224737e80c57a3f3db4ca81bc7b1b52bc73346df8c50db400/propcache-0.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:087e2d3d7613e1b59b2ffca0daabd500c1a032d189c65625ee05ea114afcad0b", size = 41156, upload-time = "2025-10-04T21:56:53.242Z" }, - { url = "https://files.pythonhosted.org/packages/52/4d/91523dcbe23cc127b097623a6ba177da51fca6b7c979082aa49745b527b7/propcache-0.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:94b0f7407d18001dbdcbb239512e753b1b36725a6e08a4983be1c948f5435f79", size = 37976, upload-time = "2025-10-04T21:56:54.351Z" }, - { url = "https://files.pythonhosted.org/packages/ec/f7/7118a944cb6cdb548c9333cf311bda120f9793ecca54b2ca4a3f7e58723e/propcache-0.4.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:b730048ae8b875e2c0af1a09ca31b303fc7b5ed27652beec03fa22b29545aec9", size = 81270, upload-time = "2025-10-04T21:56:55.516Z" }, - { url = "https://files.pythonhosted.org/packages/ab/f9/04a8bc9977ea201783f3ccb04106f44697f635f70439a208852d4d08554d/propcache-0.4.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:f495007ada16a4e16312b502636fafff42a9003adf1d4fb7541e0a0870bc056f", size = 46224, upload-time = "2025-10-04T21:56:56.695Z" }, - { url = "https://files.pythonhosted.org/packages/0f/3d/808b074034156f130a0047304d811a5a5df3bb0976c9adfb9383718fd888/propcache-0.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:659a0ea6d9017558ed7af00fb4028186f64d0ba9adfc70a4d2c85fcd3d026321", size = 48246, upload-time = "2025-10-04T21:56:57.926Z" }, - { url = "https://files.pythonhosted.org/packages/66/eb/e311f3a59ddc93078cb079b12699af9fd844142c4b4d382b386ee071d921/propcache-0.4.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d74aa60b1ec076d4d5dcde27c9a535fc0ebb12613f599681c438ca3daa68acac", size = 275562, upload-time = "2025-10-04T21:56:59.221Z" }, - { url = "https://files.pythonhosted.org/packages/f4/05/a146094d6a00bb2f2036dd2a2f4c2b2733ff9574b59ce53bd8513edfca5d/propcache-0.4.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:34000e31795bdcda9826e0e70e783847a42e3dcd0d6416c5d3cb717905ebaec0", size = 273627, upload-time = "2025-10-04T21:57:00.582Z" }, - { url = "https://files.pythonhosted.org/packages/91/95/a6d138f6e3d5f6c9b34dbd336b964a1293f2f1a79cafbe70ae3403d7cc46/propcache-0.4.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bcb5bfac5b9635e6fc520c8af6efc7a0a56f12a1fe9e9d3eb4328537e316dd6a", size = 279778, upload-time = "2025-10-04T21:57:01.944Z" }, - { url = "https://files.pythonhosted.org/packages/ac/09/19594a20da0519bfa00deef8cf35dda6c9a5b51bba947f366e85ea59b3de/propcache-0.4.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0ea11fceb31fa95b0fa2007037f19e922e2caceb7dc6c6cac4cb56e2d291f1a2", size = 262833, upload-time = "2025-10-04T21:57:03.326Z" }, - { url = "https://files.pythonhosted.org/packages/b5/92/60d2ddc7662f7b2720d3b628ad8ce888015f4ab5c335b7b1b50183194e68/propcache-0.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:cd8684f628fe285ea5c86f88e1c30716239dc9d6ac55e7851a4b7f555b628da3", size = 260456, upload-time = "2025-10-04T21:57:05.159Z" }, - { url = "https://files.pythonhosted.org/packages/6f/e2/4c2e25c77cf43add2e05a86c4fcf51107edc4d92318e5c593bbdc2515d57/propcache-0.4.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:790286d3d542c0ef9f6d0280d1049378e5e776dcba780d169298f664c39394db", size = 247284, upload-time = "2025-10-04T21:57:06.566Z" }, - { url = "https://files.pythonhosted.org/packages/dc/3e/c273ab8edc80683ec8b15b486e95c03096ef875d99e4b0ab0a36c1e42c94/propcache-0.4.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:009093c9b5dbae114a5958e6a649f8a5d94dd6866b0f82b60395eb92c58002d4", size = 262368, upload-time = "2025-10-04T21:57:08.231Z" }, - { url = "https://files.pythonhosted.org/packages/ac/a9/3fa231f65a9f78614c5aafa9cee788d7f55c22187cc2f33e86c7c16d0262/propcache-0.4.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:728d98179e92d77096937fdfecd2c555a3d613abe56c9909165c24196a3b5012", size = 263010, upload-time = "2025-10-04T21:57:09.641Z" }, - { url = "https://files.pythonhosted.org/packages/38/a0/f4f5d368e60c9dc04d3158eaf1ca0ad899b40ac3d29c015bf62735225a6f/propcache-0.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a9725d96a81e17e48a0fe82d0c3de2f5e623d7163fec70a6c7df90753edd1bec", size = 257298, upload-time = "2025-10-04T21:57:11.125Z" }, - { url = "https://files.pythonhosted.org/packages/c7/30/f78d6758dc36a98f1cddc39b3185cefde616cc58248715b7c65495491cb1/propcache-0.4.0-cp314-cp314t-win32.whl", hash = "sha256:0964c55c95625193defeb4fd85f8f28a9a754ed012cab71127d10e3dc66b1373", size = 42484, upload-time = "2025-10-04T21:57:12.652Z" }, - { url = "https://files.pythonhosted.org/packages/4e/ad/de0640e9b56d2caa796c4266d7d1e6cc4544cc327c25b7ced5c59893b625/propcache-0.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:24403152e41abf09488d3ae9c0c3bf7ff93e2fb12b435390718f21810353db28", size = 46229, upload-time = "2025-10-04T21:57:14.034Z" }, - { url = "https://files.pythonhosted.org/packages/da/bf/5aed62dddbf2bbe62a3564677436261909c9dd63a0fa1fb6cf0629daa13c/propcache-0.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0363a696a9f24b37a04ed5e34c2e07ccbe92798c998d37729551120a1bb744c4", size = 40329, upload-time = "2025-10-04T21:57:15.198Z" }, - { url = "https://files.pythonhosted.org/packages/c7/16/794c114f6041bbe2de23eb418ef58a0f45de27224d5540f5dbb266a73d72/propcache-0.4.0-py3-none-any.whl", hash = "sha256:015b2ca2f98ea9e08ac06eecc409d5d988f78c5fd5821b2ad42bc9afcd6b1557", size = 13183, upload-time = "2025-10-04T21:57:38.054Z" }, + { url = "https://files.pythonhosted.org/packages/27/11/574fe7d13acf30bfd0a8dd7fa1647040f2b8064f13f43e8c963b1e65093b/pre_commit-4.4.0-py2.py3-none-any.whl", hash = "sha256:b35ea52957cbf83dcc5d8ee636cbead8624e3a15fbfa61a370e42158ac8a5813", size = 226049, upload-time = "2025-11-08T21:12:10.228Z" }, ] [[package]] @@ -1581,27 +1131,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/97/b7/15cc7d93443d6c6a84626ae3258a91f4c6ac8c0edd5df35ea7658f71b79c/protobuf-6.32.1-py3-none-any.whl", hash = "sha256:2601b779fc7d32a866c6b4404f9d42a3f67c5b9f3f15b4db3cccabe06b95c346", size = 169289, upload-time = "2025-09-11T21:38:41.234Z" }, ] -[[package]] -name = "pyasn1" -version = "0.6.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ba/e9/01f1a64245b89f039897cb0130016d79f77d52669aae6ee7b159a6c4c018/pyasn1-0.6.1.tar.gz", hash = "sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034", size = 145322, upload-time = "2024-09-10T22:41:42.55Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c8/f1/d6a797abb14f6283c0ddff96bbdd46937f64122b8c925cab503dd37f8214/pyasn1-0.6.1-py3-none-any.whl", hash = "sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629", size = 83135, upload-time = "2024-09-11T16:00:36.122Z" }, -] - -[[package]] -name = "pyasn1-modules" -version = "0.4.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyasn1" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/e9/e6/78ebbb10a8c8e4b61a59249394a4a594c1a7af95593dc933a349c8d00964/pyasn1_modules-0.4.2.tar.gz", hash = "sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6", size = 307892, upload-time = "2025-03-28T02:41:22.17Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl", hash = "sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a", size = 181259, upload-time = "2025-03-28T02:41:19.028Z" }, -] - [[package]] name = "pyaudio" version = "0.2.14" @@ -1612,56 +1141,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a5/8b/7f9a061c1cc2b230f9ac02a6003fcd14c85ce1828013aecbaf45aa988d20/PyAudio-0.2.14-cp313-cp313-win_amd64.whl", hash = "sha256:692d8c1446f52ed2662120bcd9ddcb5aa2b71f38bda31e58b19fb4672fffba69", size = 173655, upload-time = "2024-11-20T19:12:13.616Z" }, ] -[[package]] -name = "pycares" -version = "4.11.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cffi" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/8d/ad/9d1e96486d2eb5a2672c4d9a2dd372d015b8d7a332c6ac2722c4c8e6bbbf/pycares-4.11.0.tar.gz", hash = "sha256:c863d9003ca0ce7df26429007859afd2a621d3276ed9fef154a9123db9252557", size = 654473, upload-time = "2025-09-09T15:18:21.849Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/dc/a9/62fea7ad72ac1fed2ac9dd8e9a7379b7eb0288bf2b3ea5731642c3a6f7de/pycares-4.11.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2c296ab94d1974f8d2f76c499755a9ce31ffd4986e8898ef19b90e32525f7d84", size = 145909, upload-time = "2025-09-09T15:17:10.491Z" }, - { url = "https://files.pythonhosted.org/packages/f4/ac/0317d6d0d3bd7599c53b8f1db09ad04260647d2f6842018e322584791fd5/pycares-4.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e0fcd3a8bac57a0987d9b09953ba0f8703eb9dca7c77f7051d8c2ed001185be8", size = 141974, upload-time = "2025-09-09T15:17:11.634Z" }, - { url = "https://files.pythonhosted.org/packages/63/11/731b565ae1e81c43dac247a248ee204628186f6df97c9927bd06c62237f8/pycares-4.11.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:bac55842047567ddae177fb8189b89a60633ac956d5d37260f7f71b517fd8b87", size = 637796, upload-time = "2025-09-09T15:17:12.815Z" }, - { url = "https://files.pythonhosted.org/packages/f5/30/a2631fe2ffaa85475cdbff7df1d9376bc0b2a6ae77ca55d53233c937a5da/pycares-4.11.0-cp313-cp313-manylinux_2_28_ppc64le.whl", hash = "sha256:4da2e805ed8c789b9444ef4053f6ef8040cd13b0c1ca6d3c4fe6f9369c458cb4", size = 687734, upload-time = "2025-09-09T15:17:14.015Z" }, - { url = "https://files.pythonhosted.org/packages/a9/b7/b3a5f99d4ab776662e71d5a56e8f6ea10741230ff988d1f502a8d429236b/pycares-4.11.0-cp313-cp313-manylinux_2_28_s390x.whl", hash = "sha256:ea785d1f232b42b325578f0c8a2fa348192e182cc84a1e862896076a4a2ba2a7", size = 678320, upload-time = "2025-09-09T15:17:15.442Z" }, - { url = "https://files.pythonhosted.org/packages/ea/77/a00d962b90432993afbf3bd05da8fe42117e0d9037cd7fd428dc41094d7b/pycares-4.11.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:aa160dc9e785212c49c12bb891e242c949758b99542946cc8e2098ef391f93b0", size = 641012, upload-time = "2025-09-09T15:17:16.728Z" }, - { url = "https://files.pythonhosted.org/packages/c6/fb/9266979ba59d37deee1fd74452b2ae32a7395acafe1bee510ac023c6c9a5/pycares-4.11.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7830709c23bbc43fbaefbb3dde57bdd295dc86732504b9d2e65044df8fd5e9fb", size = 622363, upload-time = "2025-09-09T15:17:17.835Z" }, - { url = "https://files.pythonhosted.org/packages/91/c2/16dbc3dc33781a3c79cbdd76dd1cda808d98ba078d9a63a725d6a1fad181/pycares-4.11.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3ef1ab7abbd238bb2dbbe871c3ea39f5a7fc63547c015820c1e24d0d494a1689", size = 670294, upload-time = "2025-09-09T15:17:19.214Z" }, - { url = "https://files.pythonhosted.org/packages/ff/75/f003905e55298a6dd5e0673a2dc11e31518a5141393b925dc05fcaba9fb4/pycares-4.11.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:a4060d8556c908660512d42df1f4a874e4e91b81f79e3a9090afedc7690ea5ba", size = 652973, upload-time = "2025-09-09T15:17:20.388Z" }, - { url = "https://files.pythonhosted.org/packages/55/2a/eafb235c371979e11f8998d686cbaa91df6a84a34ffe4d997dfe57c45445/pycares-4.11.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a98fac4a3d4f780817016b6f00a8a2c2f41df5d25dfa8e5b1aa0d783645a6566", size = 629235, upload-time = "2025-09-09T15:17:21.92Z" }, - { url = "https://files.pythonhosted.org/packages/05/99/60f19eb1c8eb898882dd8875ea51ad0aac3aff5780b27247969e637cc26a/pycares-4.11.0-cp313-cp313-win32.whl", hash = "sha256:faa8321bc2a366189dcf87b3823e030edf5ac97a6b9a7fc99f1926c4bf8ef28e", size = 118918, upload-time = "2025-09-09T15:17:23.327Z" }, - { url = "https://files.pythonhosted.org/packages/2a/14/bc89ad7225cba73068688397de09d7cad657d67b93641c14e5e18b88e685/pycares-4.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:6f74b1d944a50fa12c5006fd10b45e1a45da0c5d15570919ce48be88e428264c", size = 144556, upload-time = "2025-09-09T15:17:24.341Z" }, - { url = "https://files.pythonhosted.org/packages/af/88/4309576bd74b5e6fc1f39b9bc5e4b578df2cadb16bdc026ac0cc15663763/pycares-4.11.0-cp313-cp313-win_arm64.whl", hash = "sha256:4b6f7581793d8bb3014028b8397f6f80b99db8842da58f4409839c29b16397ad", size = 115692, upload-time = "2025-09-09T15:17:25.637Z" }, - { url = "https://files.pythonhosted.org/packages/2a/70/a723bc79bdcac60361b40184b649282ac0ab433b90e9cc0975370c2ff9c9/pycares-4.11.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:df0a17f4e677d57bca3624752bbb515316522ad1ce0de07ed9d920e6c4ee5d35", size = 145910, upload-time = "2025-09-09T15:17:26.774Z" }, - { url = "https://files.pythonhosted.org/packages/d5/4e/46311ef5a384b5f0bb206851135dde8f86b3def38fdbee9e3c03475d35ae/pycares-4.11.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:3b44e54cad31d3c3be5e8149ac36bc1c163ec86e0664293402f6f846fb22ad00", size = 142053, upload-time = "2025-09-09T15:17:27.956Z" }, - { url = "https://files.pythonhosted.org/packages/74/23/d236fc4f134d6311e4ad6445571e8285e84a3e155be36422ff20c0fbe471/pycares-4.11.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:80752133442dc7e6dd9410cec227c49f69283c038c316a8585cca05ec32c2766", size = 637878, upload-time = "2025-09-09T15:17:29.173Z" }, - { url = "https://files.pythonhosted.org/packages/f7/92/6edd41282b3f0e3d9defaba7b05c39730d51c37c165d9d3b319349c975aa/pycares-4.11.0-cp314-cp314-manylinux_2_28_ppc64le.whl", hash = "sha256:84b0b402dd333403fdce0e204aef1ef834d839c439c0c1aa143dc7d1237bb197", size = 687865, upload-time = "2025-09-09T15:17:30.549Z" }, - { url = "https://files.pythonhosted.org/packages/a7/a9/4d7cf4d72600fd47d9518f9ce99703a3e8711fb08d2ef63d198056cdc9a9/pycares-4.11.0-cp314-cp314-manylinux_2_28_s390x.whl", hash = "sha256:c0eec184df42fc82e43197e073f9cc8f93b25ad2f11f230c64c2dc1c80dbc078", size = 678396, upload-time = "2025-09-09T15:17:32.304Z" }, - { url = "https://files.pythonhosted.org/packages/0b/4b/e546eeb1d8ff6559e2e3bef31a6ea0c6e57ec826191941f83a3ce900ca89/pycares-4.11.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:ee751409322ff10709ee867d5aea1dc8431eec7f34835f0f67afd016178da134", size = 640786, upload-time = "2025-09-09T15:17:33.602Z" }, - { url = "https://files.pythonhosted.org/packages/0e/f5/b4572d9ee9c26de1f8d1dc80730df756276b9243a6794fa3101bbe56613d/pycares-4.11.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1732db81e348bfce19c9bf9448ba660aea03042eeeea282824da1604a5bd4dcf", size = 621857, upload-time = "2025-09-09T15:17:34.74Z" }, - { url = "https://files.pythonhosted.org/packages/17/f2/639090376198bcaeff86562b25e1bce05a481cfb1e605f82ce62285230cd/pycares-4.11.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:702d21823996f139874aba5aa9bb786d69e93bde6e3915b99832eb4e335d31ae", size = 670130, upload-time = "2025-09-09T15:17:35.982Z" }, - { url = "https://files.pythonhosted.org/packages/3a/c4/cf40773cd9c36a12cebbe1e9b6fb120f9160dc9bfe0398d81a20b6c69972/pycares-4.11.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:218619b912cef7c64a339ab0e231daea10c994a05699740714dff8c428b9694a", size = 653133, upload-time = "2025-09-09T15:17:37.179Z" }, - { url = "https://files.pythonhosted.org/packages/32/6b/06054d977b0a9643821043b59f523f3db5e7684c4b1b4f5821994d5fa780/pycares-4.11.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:719f7ddff024fdacde97b926b4b26d0cc25901d5ef68bb994a581c420069936d", size = 629344, upload-time = "2025-09-09T15:17:38.308Z" }, - { url = "https://files.pythonhosted.org/packages/d6/6f/14bb0c2171a286d512e3f02d6168e608ffe5f6eceab78bf63e3073091ae3/pycares-4.11.0-cp314-cp314-win32.whl", hash = "sha256:d552fb2cb513ce910d1dc22dbba6420758a991a356f3cd1b7ec73a9e31f94d01", size = 121804, upload-time = "2025-09-09T15:17:39.388Z" }, - { url = "https://files.pythonhosted.org/packages/24/dc/6822f9ad6941027f70e1cf161d8631456531a87061588ed3b1dcad07d49d/pycares-4.11.0-cp314-cp314-win_amd64.whl", hash = "sha256:23d50a0842e8dbdddf870a7218a7ab5053b68892706b3a391ecb3d657424d266", size = 148005, upload-time = "2025-09-09T15:17:40.44Z" }, - { url = "https://files.pythonhosted.org/packages/ea/24/24ff3a80aa8471fbb62785c821a8e90f397ca842e0489f83ebf7ee274397/pycares-4.11.0-cp314-cp314-win_arm64.whl", hash = "sha256:836725754c32363d2c5d15b931b3ebd46b20185c02e850672cb6c5f0452c1e80", size = 119239, upload-time = "2025-09-09T15:17:42.094Z" }, - { url = "https://files.pythonhosted.org/packages/54/fe/2f3558d298ff8db31d5c83369001ab72af3b86a0374d9b0d40dc63314187/pycares-4.11.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c9d839b5700542b27c1a0d359cbfad6496341e7c819c7fea63db9588857065ed", size = 146408, upload-time = "2025-09-09T15:17:43.74Z" }, - { url = "https://files.pythonhosted.org/packages/3c/c8/516901e46a1a73b3a75e87a35f3a3a4fe085f1214f37d954c9d7e782bd6d/pycares-4.11.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:31b85ad00422b38f426e5733a71dfb7ee7eb65a99ea328c508d4f552b1760dc8", size = 142371, upload-time = "2025-09-09T15:17:45.186Z" }, - { url = "https://files.pythonhosted.org/packages/ac/99/c3fba0aa575f331ebed91f87ba960ffbe0849211cdf103ab275bc0107ac6/pycares-4.11.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:cdac992206756b024b371760c55719eb5cd9d6b2cb25a8d5a04ae1b0ff426232", size = 647504, upload-time = "2025-09-09T15:17:46.503Z" }, - { url = "https://files.pythonhosted.org/packages/5c/e4/1cdc3ec9c92f8069ec18c58b016b2df7c44a088e2849f37ed457554961aa/pycares-4.11.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:ffb22cee640bc12ee0e654eba74ecfb59e2e0aebc5bccc3cc7ef92f487008af7", size = 697122, upload-time = "2025-09-09T15:17:47.772Z" }, - { url = "https://files.pythonhosted.org/packages/9c/d5/bd8f370b97bb73e5bdd55dc2a78e18d6f49181cf77e88af0599d16f5c073/pycares-4.11.0-cp314-cp314t-manylinux_2_28_s390x.whl", hash = "sha256:00538826d2eaf4a0e4becb0753b0ac8d652334603c445c9566c9eb273657eb4c", size = 687543, upload-time = "2025-09-09T15:17:49.183Z" }, - { url = "https://files.pythonhosted.org/packages/33/38/49b77b9cf5dffc0b1fdd86656975c3bc1a58b79bdc883a9ef749b17a013c/pycares-4.11.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:29daa36548c04cdcd1a78ae187a4b7b003f0b357a2f4f1f98f9863373eedc759", size = 649565, upload-time = "2025-09-09T15:17:51.03Z" }, - { url = "https://files.pythonhosted.org/packages/3c/23/f6d57bfb99d00a6a7363f95c8d3a930fe82a868d9de24c64c8048d66f16a/pycares-4.11.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:cf306f3951740d7bed36149a6d8d656a7d5432dd4bbc6af3bb6554361fc87401", size = 631242, upload-time = "2025-09-09T15:17:52.298Z" }, - { url = "https://files.pythonhosted.org/packages/33/a2/7b9121c71cfe06a8474e221593f83a78176fae3b79e5853d2dfd13ab01cc/pycares-4.11.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:386da2581db4ea2832629e275c061103b0be32f9391c5dfaea7f6040951950ad", size = 680304, upload-time = "2025-09-09T15:17:53.638Z" }, - { url = "https://files.pythonhosted.org/packages/5b/07/dfe76807f637d8b80e1a59dfc4a1bceabdd0205a45b2ebf78b415ae72af3/pycares-4.11.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:45d3254a694459fdb0640ef08724ca9d4b4f6ff6d7161c9b526d7d2e2111379e", size = 661039, upload-time = "2025-09-09T15:17:55.024Z" }, - { url = "https://files.pythonhosted.org/packages/b2/9b/55d50c5acd46cbe95d0da27740a83e721d89c0ce7e42bff9891a9f29a855/pycares-4.11.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:eddf5e520bb88b23b04ac1f28f5e9a7c77c718b8b4af3a4a7a2cc4a600f34502", size = 637560, upload-time = "2025-09-09T15:17:56.492Z" }, - { url = "https://files.pythonhosted.org/packages/1f/79/2b2e723d1b929dbe7f99e80a56abb29a4f86988c1f73195d960d706b1629/pycares-4.11.0-cp314-cp314t-win32.whl", hash = "sha256:8a75a406432ce39ce0ca41edff7486df6c970eb0fe5cfbe292f195a6b8654461", size = 122235, upload-time = "2025-09-09T15:17:57.576Z" }, - { url = "https://files.pythonhosted.org/packages/93/fe/bf3b3ed9345a38092e72cd9890a5df5c2349fc27846a714d823a41f0ee27/pycares-4.11.0-cp314-cp314t-win_amd64.whl", hash = "sha256:3784b80d797bcc2ff2bf3d4b27f46d8516fe1707ff3b82c2580dc977537387f9", size = 148575, upload-time = "2025-09-09T15:17:58.699Z" }, - { url = "https://files.pythonhosted.org/packages/ce/20/c0c5cfcf89725fe533b27bc5f714dc4efa8e782bf697c36f9ddf04ba975d/pycares-4.11.0-cp314-cp314t-win_arm64.whl", hash = "sha256:afc6503adf8b35c21183b9387be64ca6810644ef54c9ef6c99d1d5635c01601b", size = 119690, upload-time = "2025-09-09T15:17:59.809Z" }, -] - [[package]] name = "pycparser" version = "2.23" @@ -1772,27 +1251,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, ] -[[package]] -name = "pyjabber" -version = "0.3.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "aiohttp" }, - { name = "alembic" }, - { name = "bcrypt" }, - { name = "click" }, - { name = "cryptography" }, - { name = "loguru" }, - { name = "pyyaml" }, - { name = "sqlalchemy" }, - { name = "uvloop", marker = "sys_platform != 'win32'" }, - { name = "winloop", marker = "sys_platform == 'win32'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/8f/f3/14a4f7b1d4a59b5c5651b7a7efb56d46a785f76cb1dd0f9e16b35579273f/pyjabber-0.3.0.tar.gz", hash = "sha256:618969ccd83abf5e2118f7ddba5fb8b236d6edf1d0202af46d7fff454e221706", size = 1024144, upload-time = "2025-05-26T08:51:14.581Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/07/da/2e43c965b5b9d93677d684ba9348b3f47d0cfb4953796066189a5d2b6e52/pyjabber-0.3.0-py3-none-any.whl", hash = "sha256:46983d1957bcdb3f5b6f96bd2ffb3ad05df2fcf6cef5ba5737d28c276f665d24", size = 985144, upload-time = "2025-05-26T08:51:12.833Z" }, -] - [[package]] name = "pyreadline3" version = "3.5.4" @@ -1856,18 +1314,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5a/cc/06253936f4a7fa2e0f48dfe6d851d9c56df896a9ab09ac019d70b760619c/pytest_mock-3.15.1-py3-none-any.whl", hash = "sha256:0a25e2eb88fe5168d535041d09a4529a188176ae608a6d249ee65abc0949630d", size = 10095, upload-time = "2025-09-16T16:37:25.734Z" }, ] -[[package]] -name = "python-dateutil" -version = "2.9.0.post0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "six" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } -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 = "python-dotenv" version = "1.1.1" @@ -1895,15 +1341,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/45/58/38b5afbc1a800eeea951b9285d3912613f2603bdf897a4ab0f4bd7f405fc/python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104", size = 24546, upload-time = "2024-12-16T19:45:44.423Z" }, ] -[[package]] -name = "pytz" -version = "2022.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/2f/5f/a0f653311adff905bbcaa6d3dfaf97edcf4d26138393c6ccd37a484851fb/pytz-2022.1.tar.gz", hash = "sha256:1e760e2fe6a8163bc0b3d9a19c4f84342afa0a2affebfaa84b01b978a02ecaa7", size = 320473, upload-time = "2022-03-20T00:37:10.116Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/60/2e/dec1cc18c51b8df33c7c4d0a321b084cf38e1733b98f9d15018880fb4970/pytz-2022.1-py2.py3-none-any.whl", hash = "sha256:e68985985296d9a66a881eb3193b0906246245294a881e7c8afe623866ac6a5c", size = 503520, upload-time = "2022-03-20T00:37:06.783Z" }, -] - [[package]] name = "pyyaml" version = "6.0.3" @@ -2127,58 +1564,64 @@ wheels = [ [[package]] name = "ruff" -version = "0.14.2" +version = "0.14.6" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ee/34/8218a19b2055b80601e8fd201ec723c74c7fe1ca06d525a43ed07b6d8e85/ruff-0.14.2.tar.gz", hash = "sha256:98da787668f239313d9c902ca7c523fe11b8ec3f39345553a51b25abc4629c96", size = 5539663, upload-time = "2025-10-23T19:37:00.956Z" } +sdist = { url = "https://files.pythonhosted.org/packages/52/f0/62b5a1a723fe183650109407fa56abb433b00aa1c0b9ba555f9c4efec2c6/ruff-0.14.6.tar.gz", hash = "sha256:6f0c742ca6a7783a736b867a263b9a7a80a45ce9bee391eeda296895f1b4e1cc", size = 5669501, upload-time = "2025-11-21T14:26:17.903Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/16/dd/23eb2db5ad9acae7c845700493b72d3ae214dce0b226f27df89216110f2b/ruff-0.14.2-py3-none-linux_armv6l.whl", hash = "sha256:7cbe4e593505bdec5884c2d0a4d791a90301bc23e49a6b1eb642dd85ef9c64f1", size = 12533390, upload-time = "2025-10-23T19:36:18.044Z" }, - { url = "https://files.pythonhosted.org/packages/5a/8c/5f9acff43ddcf3f85130d0146d0477e28ccecc495f9f684f8f7119b74c0d/ruff-0.14.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:8d54b561729cee92f8d89c316ad7a3f9705533f5903b042399b6ae0ddfc62e11", size = 12887187, upload-time = "2025-10-23T19:36:22.664Z" }, - { url = "https://files.pythonhosted.org/packages/99/fa/047646491479074029665022e9f3dc6f0515797f40a4b6014ea8474c539d/ruff-0.14.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:5c8753dfa44ebb2cde10ce5b4d2ef55a41fb9d9b16732a2c5df64620dbda44a3", size = 11925177, upload-time = "2025-10-23T19:36:24.778Z" }, - { url = "https://files.pythonhosted.org/packages/15/8b/c44cf7fe6e59ab24a9d939493a11030b503bdc2a16622cede8b7b1df0114/ruff-0.14.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3d0bbeffb8d9f4fccf7b5198d566d0bad99a9cb622f1fc3467af96cb8773c9e3", size = 12358285, upload-time = "2025-10-23T19:36:26.979Z" }, - { url = "https://files.pythonhosted.org/packages/45/01/47701b26254267ef40369aea3acb62a7b23e921c27372d127e0f3af48092/ruff-0.14.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7047f0c5a713a401e43a88d36843d9c83a19c584e63d664474675620aaa634a8", size = 12303832, upload-time = "2025-10-23T19:36:29.192Z" }, - { url = "https://files.pythonhosted.org/packages/2d/5c/ae7244ca4fbdf2bee9d6405dcd5bc6ae51ee1df66eb7a9884b77b8af856d/ruff-0.14.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3bf8d2f9aa1602599217d82e8e0af7fd33e5878c4d98f37906b7c93f46f9a839", size = 13036995, upload-time = "2025-10-23T19:36:31.861Z" }, - { url = "https://files.pythonhosted.org/packages/27/4c/0860a79ce6fd4c709ac01173f76f929d53f59748d0dcdd662519835dae43/ruff-0.14.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:1c505b389e19c57a317cf4b42db824e2fca96ffb3d86766c1c9f8b96d32048a7", size = 14512649, upload-time = "2025-10-23T19:36:33.915Z" }, - { url = "https://files.pythonhosted.org/packages/7f/7f/d365de998069720a3abfc250ddd876fc4b81a403a766c74ff9bde15b5378/ruff-0.14.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a307fc45ebd887b3f26b36d9326bb70bf69b01561950cdcc6c0bdf7bb8e0f7cc", size = 14088182, upload-time = "2025-10-23T19:36:36.983Z" }, - { url = "https://files.pythonhosted.org/packages/6c/ea/d8e3e6b209162000a7be1faa41b0a0c16a133010311edc3329753cc6596a/ruff-0.14.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:61ae91a32c853172f832c2f40bd05fd69f491db7289fb85a9b941ebdd549781a", size = 13599516, upload-time = "2025-10-23T19:36:39.208Z" }, - { url = "https://files.pythonhosted.org/packages/fa/ea/c7810322086db68989fb20a8d5221dd3b79e49e396b01badca07b433ab45/ruff-0.14.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1967e40286f63ee23c615e8e7e98098dedc7301568bd88991f6e544d8ae096", size = 13272690, upload-time = "2025-10-23T19:36:41.453Z" }, - { url = "https://files.pythonhosted.org/packages/a9/39/10b05acf8c45786ef501d454e00937e1b97964f846bf28883d1f9619928a/ruff-0.14.2-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:2877f02119cdebf52a632d743a2e302dea422bfae152ebe2f193d3285a3a65df", size = 13496497, upload-time = "2025-10-23T19:36:43.61Z" }, - { url = "https://files.pythonhosted.org/packages/59/a1/1f25f8301e13751c30895092485fada29076e5e14264bdacc37202e85d24/ruff-0.14.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e681c5bc777de5af898decdcb6ba3321d0d466f4cb43c3e7cc2c3b4e7b843a05", size = 12266116, upload-time = "2025-10-23T19:36:45.625Z" }, - { url = "https://files.pythonhosted.org/packages/5c/fa/0029bfc9ce16ae78164e6923ef392e5f173b793b26cc39aa1d8b366cf9dc/ruff-0.14.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:e21be42d72e224736f0c992cdb9959a2fa53c7e943b97ef5d081e13170e3ffc5", size = 12281345, upload-time = "2025-10-23T19:36:47.618Z" }, - { url = "https://files.pythonhosted.org/packages/a5/ab/ece7baa3c0f29b7683be868c024f0838770c16607bea6852e46b202f1ff6/ruff-0.14.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:b8264016f6f209fac16262882dbebf3f8be1629777cf0f37e7aff071b3e9b92e", size = 12629296, upload-time = "2025-10-23T19:36:49.789Z" }, - { url = "https://files.pythonhosted.org/packages/a4/7f/638f54b43f3d4e48c6a68062794e5b367ddac778051806b9e235dfb7aa81/ruff-0.14.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5ca36b4cb4db3067a3b24444463ceea5565ea78b95fe9a07ca7cb7fd16948770", size = 13371610, upload-time = "2025-10-23T19:36:51.882Z" }, - { url = "https://files.pythonhosted.org/packages/8d/35/3654a973ebe5b32e1fd4a08ed2d46755af7267da7ac710d97420d7b8657d/ruff-0.14.2-py3-none-win32.whl", hash = "sha256:41775927d287685e08f48d8eb3f765625ab0b7042cc9377e20e64f4eb0056ee9", size = 12415318, upload-time = "2025-10-23T19:36:53.961Z" }, - { url = "https://files.pythonhosted.org/packages/71/30/3758bcf9e0b6a4193a6f51abf84254aba00887dfa8c20aba18aa366c5f57/ruff-0.14.2-py3-none-win_amd64.whl", hash = "sha256:0df3424aa5c3c08b34ed8ce099df1021e3adaca6e90229273496b839e5a7e1af", size = 13565279, upload-time = "2025-10-23T19:36:56.578Z" }, - { url = "https://files.pythonhosted.org/packages/2e/5d/aa883766f8ef9ffbe6aa24f7192fb71632f31a30e77eb39aa2b0dc4290ac/ruff-0.14.2-py3-none-win_arm64.whl", hash = "sha256:ea9d635e83ba21569fbacda7e78afbfeb94911c9434aff06192d9bc23fd5495a", size = 12554956, upload-time = "2025-10-23T19:36:58.714Z" }, + { url = "https://files.pythonhosted.org/packages/67/d2/7dd544116d107fffb24a0064d41a5d2ed1c9d6372d142f9ba108c8e39207/ruff-0.14.6-py3-none-linux_armv6l.whl", hash = "sha256:d724ac2f1c240dbd01a2ae98db5d1d9a5e1d9e96eba999d1c48e30062df578a3", size = 13326119, upload-time = "2025-11-21T14:25:24.2Z" }, + { url = "https://files.pythonhosted.org/packages/36/6a/ad66d0a3315d6327ed6b01f759d83df3c4d5f86c30462121024361137b6a/ruff-0.14.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:9f7539ea257aa4d07b7ce87aed580e485c40143f2473ff2f2b75aee003186004", size = 13526007, upload-time = "2025-11-21T14:25:26.906Z" }, + { url = "https://files.pythonhosted.org/packages/a3/9d/dae6db96df28e0a15dea8e986ee393af70fc97fd57669808728080529c37/ruff-0.14.6-py3-none-macosx_11_0_arm64.whl", hash = "sha256:7f6007e55b90a2a7e93083ba48a9f23c3158c433591c33ee2e99a49b889c6332", size = 12676572, upload-time = "2025-11-21T14:25:29.826Z" }, + { url = "https://files.pythonhosted.org/packages/76/a4/f319e87759949062cfee1b26245048e92e2acce900ad3a909285f9db1859/ruff-0.14.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a8e7b9d73d8728b68f632aa8e824ef041d068d231d8dbc7808532d3629a6bef", size = 13140745, upload-time = "2025-11-21T14:25:32.788Z" }, + { url = "https://files.pythonhosted.org/packages/95/d3/248c1efc71a0a8ed4e8e10b4b2266845d7dfc7a0ab64354afe049eaa1310/ruff-0.14.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d50d45d4553a3ebcbd33e7c5e0fe6ca4aafd9a9122492de357205c2c48f00775", size = 13076486, upload-time = "2025-11-21T14:25:35.601Z" }, + { url = "https://files.pythonhosted.org/packages/a5/19/b68d4563fe50eba4b8c92aa842149bb56dd24d198389c0ed12e7faff4f7d/ruff-0.14.6-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:118548dd121f8a21bfa8ab2c5b80e5b4aed67ead4b7567790962554f38e598ce", size = 13727563, upload-time = "2025-11-21T14:25:38.514Z" }, + { url = "https://files.pythonhosted.org/packages/47/ac/943169436832d4b0e867235abbdb57ce3a82367b47e0280fa7b4eabb7593/ruff-0.14.6-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:57256efafbfefcb8748df9d1d766062f62b20150691021f8ab79e2d919f7c11f", size = 15199755, upload-time = "2025-11-21T14:25:41.516Z" }, + { url = "https://files.pythonhosted.org/packages/c9/b9/288bb2399860a36d4bb0541cb66cce3c0f4156aaff009dc8499be0c24bf2/ruff-0.14.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ff18134841e5c68f8e5df1999a64429a02d5549036b394fafbe410f886e1989d", size = 14850608, upload-time = "2025-11-21T14:25:44.428Z" }, + { url = "https://files.pythonhosted.org/packages/ee/b1/a0d549dd4364e240f37e7d2907e97ee80587480d98c7799d2d8dc7a2f605/ruff-0.14.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:29c4b7ec1e66a105d5c27bd57fa93203637d66a26d10ca9809dc7fc18ec58440", size = 14118754, upload-time = "2025-11-21T14:25:47.214Z" }, + { url = "https://files.pythonhosted.org/packages/13/ac/9b9fe63716af8bdfddfacd0882bc1586f29985d3b988b3c62ddce2e202c3/ruff-0.14.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:167843a6f78680746d7e226f255d920aeed5e4ad9c03258094a2d49d3028b105", size = 13949214, upload-time = "2025-11-21T14:25:50.002Z" }, + { url = "https://files.pythonhosted.org/packages/12/27/4dad6c6a77fede9560b7df6802b1b697e97e49ceabe1f12baf3ea20862e9/ruff-0.14.6-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:16a33af621c9c523b1ae006b1b99b159bf5ac7e4b1f20b85b2572455018e0821", size = 14106112, upload-time = "2025-11-21T14:25:52.841Z" }, + { url = "https://files.pythonhosted.org/packages/6a/db/23e322d7177873eaedea59a7932ca5084ec5b7e20cb30f341ab594130a71/ruff-0.14.6-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:1432ab6e1ae2dc565a7eea707d3b03a0c234ef401482a6f1621bc1f427c2ff55", size = 13035010, upload-time = "2025-11-21T14:25:55.536Z" }, + { url = "https://files.pythonhosted.org/packages/a8/9c/20e21d4d69dbb35e6a1df7691e02f363423658a20a2afacf2a2c011800dc/ruff-0.14.6-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:4c55cfbbe7abb61eb914bfd20683d14cdfb38a6d56c6c66efa55ec6570ee4e71", size = 13054082, upload-time = "2025-11-21T14:25:58.625Z" }, + { url = "https://files.pythonhosted.org/packages/66/25/906ee6a0464c3125c8d673c589771a974965c2be1a1e28b5c3b96cb6ef88/ruff-0.14.6-py3-none-musllinux_1_2_i686.whl", hash = "sha256:efea3c0f21901a685fff4befda6d61a1bf4cb43de16da87e8226a281d614350b", size = 13303354, upload-time = "2025-11-21T14:26:01.816Z" }, + { url = "https://files.pythonhosted.org/packages/4c/58/60577569e198d56922b7ead07b465f559002b7b11d53f40937e95067ca1c/ruff-0.14.6-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:344d97172576d75dc6afc0e9243376dbe1668559c72de1864439c4fc95f78185", size = 14054487, upload-time = "2025-11-21T14:26:05.058Z" }, + { url = "https://files.pythonhosted.org/packages/67/0b/8e4e0639e4cc12547f41cb771b0b44ec8225b6b6a93393176d75fe6f7d40/ruff-0.14.6-py3-none-win32.whl", hash = "sha256:00169c0c8b85396516fdd9ce3446c7ca20c2a8f90a77aa945ba6b8f2bfe99e85", size = 13013361, upload-time = "2025-11-21T14:26:08.152Z" }, + { url = "https://files.pythonhosted.org/packages/fb/02/82240553b77fd1341f80ebb3eaae43ba011c7a91b4224a9f317d8e6591af/ruff-0.14.6-py3-none-win_amd64.whl", hash = "sha256:390e6480c5e3659f8a4c8d6a0373027820419ac14fa0d2713bd8e6c3e125b8b9", size = 14432087, upload-time = "2025-11-21T14:26:10.891Z" }, + { url = "https://files.pythonhosted.org/packages/a5/1f/93f9b0fad9470e4c829a5bb678da4012f0c710d09331b860ee555216f4ea/ruff-0.14.6-py3-none-win_arm64.whl", hash = "sha256:d43c81fbeae52cfa8728d8766bbf46ee4298c888072105815b392da70ca836b2", size = 13520930, upload-time = "2025-11-21T14:26:13.951Z" }, ] [[package]] name = "ruff-format" -version = "0.3.0" +version = "0.4.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/3b/3c/71dfce0e8269271969381b1a629772aeeb62c693f8aca8560bf145e413ca/ruff_format-0.3.0.tar.gz", hash = "sha256:f579b32b9dd041b0fe7b04da9ba932ff5d108f7ce4c763bd58e659a03f1d408a", size = 15541, upload-time = "2025-10-10T03:13:11.805Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e6/ae/5767b436b41af8add7432286fce65a489a4db88ed090fb66275fa0076cd3/ruff_format-0.4.1.tar.gz", hash = "sha256:2aff271154b088ee131cef63a92afbc4cdc3905acf03d279c4a8aa3f6b3fb564", size = 15622, upload-time = "2025-10-28T18:31:39.817Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/75/b9/5866b53f870231f61716753b471cca1c79042678b96d25bff75ca1ee361a/ruff_format-0.3.0-cp311-abi3-macosx_10_12_x86_64.whl", hash = "sha256:46e543b0c6c858d963ca337ded9e37887ba6fc903caf13bd7200274faef9178c", size = 2127810, upload-time = "2025-10-10T03:12:42.416Z" }, - { url = "https://files.pythonhosted.org/packages/42/0a/311803a69bb9302749eb22b4a193cc87dfe172a5ee6940d3e4c9362418f5/ruff_format-0.3.0-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:d549c4cd5e6ae1fac9c4c083b5c3d51bca5b1fdb622384bd5dd2c1d01f99dc66", size = 2059792, upload-time = "2025-10-10T03:12:40.849Z" }, - { url = "https://files.pythonhosted.org/packages/17/bb/7e09e91464291dc1f4b947d858d1206b3df618fdb96cda17fad3bc245977/ruff_format-0.3.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7cb10f784ff0dc8f57183d7edbf33ce32d8efd8582794e9415c8a53a0e6d0e0b", size = 2247834, upload-time = "2025-10-10T03:12:06.404Z" }, - { url = "https://files.pythonhosted.org/packages/6d/20/8d1d5c63acacee481e7a92e8d5a9cfa1fa6266082bf844f66c981033b43b/ruff_format-0.3.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:adf38aae1b1468c55f4f8732d077bb30dd705599875cf6783bbb1808373d9fa4", size = 2187813, upload-time = "2025-10-10T03:12:13.535Z" }, - { url = "https://files.pythonhosted.org/packages/bd/87/c23b0ef5efa4624882601fbcacc8e64f4f1687387acb1873babb82413e27/ruff_format-0.3.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:642e8edadbc348718ef4aaf750ffa993376338669d5bf7c085c66d1a181ea26f", size = 3076735, upload-time = "2025-10-10T03:12:20.593Z" }, - { url = "https://files.pythonhosted.org/packages/df/60/2dd758eac6f835505de4bdcf7be5c993a930e6f6c475bec21e92df1359e5/ruff_format-0.3.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1e894da47f72e538731793953b213c80e17aeea5635067e2054c9a8ffe71331b", size = 2393207, upload-time = "2025-10-10T03:12:28.3Z" }, - { url = "https://files.pythonhosted.org/packages/29/8c/f55bcc419596929da754ffa59f415e498a17be1a32b2a59c472440526625/ruff_format-0.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2c73eabe1f9a08ca7430f317c358bb31c3e0017b262488bac636a50cc7d7948d", size = 2429534, upload-time = "2025-10-10T03:12:43.675Z" }, - { url = "https://files.pythonhosted.org/packages/6d/ae/24e1bf20a13d67fd4b4629efa8c015a20de9fa09ec3767b27a5e0beec4c7/ruff_format-0.3.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:470ca14276c98eb06404c0966d3b306c63c1560fd926416fd5c6c00f24f3410c", size = 2445547, upload-time = "2025-10-10T03:12:50.626Z" }, - { url = "https://files.pythonhosted.org/packages/c8/aa/5c343854a1d6c74a1db7ecd345f7fa6712f7b73adabd9c6ceb5db4356a69/ruff_format-0.3.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:ebdf4a35223860e7a697ef3a2d5dc0cf1c94656b09ba9139b400c1602c18db3a", size = 2452623, upload-time = "2025-10-10T03:12:57.66Z" }, - { url = "https://files.pythonhosted.org/packages/fd/0f/8ffaa38f228176478ca6f1e9faf23749220f3fd97ad804559ac85e3cfc98/ruff_format-0.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f3bf308531ad99a745438701df88d306a416d002a36143b23c5b5dad85965a42", size = 2473830, upload-time = "2025-10-10T03:13:05.376Z" }, - { url = "https://files.pythonhosted.org/packages/13/2f/3f53cfb6f14d2f2bfcf29fef41712ee04caa84155334e4602db1e08523d8/ruff_format-0.3.0-cp314-abi3-win32.whl", hash = "sha256:cc9e2bf654290999a2d0bdac8dd289302dcbc8cced2db5e1600f1d1850b4066e", size = 1785021, upload-time = "2025-10-10T03:13:13.785Z" }, - { url = "https://files.pythonhosted.org/packages/64/49/81c0ebc86540f856e0f1ffa6d47a95111328306650f63d6a453d34f05295/ruff_format-0.3.0-cp314-abi3-win_amd64.whl", hash = "sha256:52d47afcf18cd070e9ea8eb7701b6942a28323089fdd4a7a8934c68e57228475", size = 1892439, upload-time = "2025-10-10T03:13:12.546Z" }, - { url = "https://files.pythonhosted.org/packages/e0/5e/bfaf109bb50cc1c108d494288072419ba3acf0e9bfcf3be587b707454c50/ruff_format-0.3.0-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:623156d3a1e2ef8ece2b7195aa64f122c036605ce495e06e99c53a52927b7871", size = 2249416, upload-time = "2025-10-10T03:12:08.096Z" }, - { url = "https://files.pythonhosted.org/packages/8c/01/113a0e8f15dc1309b6331695a084bc36207b26fad065c26abfadbf24f5a7/ruff_format-0.3.0-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:073d4be5fb2fbb6668e14fb9a3aae1b03bbb2ef6d63622979e5657d22a69fb36", size = 2190621, upload-time = "2025-10-10T03:12:14.806Z" }, - { url = "https://files.pythonhosted.org/packages/66/8d/979b6ccde9fe4018b01a9a4215cc4c3455519465943c9862876311e239da/ruff_format-0.3.0-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:45e34fe85e7bc833f85e873f6cb9e3606510e678760c7128c737b009e3b9fdfd", size = 3077988, upload-time = "2025-10-10T03:12:22.204Z" }, - { url = "https://files.pythonhosted.org/packages/53/4b/791ce063a6bf17c783fe036f302bfcec8a9e1f99bf591e8b0cc73a25b719/ruff_format-0.3.0-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:135f1306e51198790fcf402c6574539e51dc1bcfa6d8c67e8b51c701d9ebab11", size = 2395129, upload-time = "2025-10-10T03:12:29.808Z" }, - { url = "https://files.pythonhosted.org/packages/ed/7b/08df01b8925ea4fdf7959199ccffc599314a179695fa8bc886146971b30b/ruff_format-0.3.0-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:451d3502ccd85ec055fdc1ce52f60f6c8d469bda3b8c7a3e9ac5fa99a64fde9c", size = 2302808, upload-time = "2025-10-10T03:12:38.299Z" }, - { url = "https://files.pythonhosted.org/packages/b4/0d/24d3616081e283b38cf228a6765b913fd1320e780febd4ea3ec98a0db5ff/ruff_format-0.3.0-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:76e0c088e18bd23b124d225926b8d64db6419a7f86b3a123346e2bacae679940", size = 2364885, upload-time = "2025-10-10T03:12:35.341Z" }, - { url = "https://files.pythonhosted.org/packages/05/2f/3efec36107cd974ed48ab63b61b15e49139575ff305daf0c52c24ea14cdb/ruff_format-0.3.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:81651ba409a6de07f5c6b25ac609401649a3cccdd19c7cb76e735481e6ed859a", size = 2431420, upload-time = "2025-10-10T03:12:45.127Z" }, - { url = "https://files.pythonhosted.org/packages/f7/bb/9ec44a9203f668974a896efc9cf26c9e332226b578f7ae6ca3449642e7cb/ruff_format-0.3.0-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:da2d9cc4d0c4cfd5b8180a19f0b8eda86cc2cffc0e5d01dd2b6133eb85e7e76f", size = 2447058, upload-time = "2025-10-10T03:12:51.926Z" }, - { url = "https://files.pythonhosted.org/packages/a0/57/be709bc005ec1008773a9361b0d1dac23fc0425ea2510b3b575cb3d44865/ruff_format-0.3.0-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:c0f1c971a9eb50b7145158fd96ac29d5d5aaf4373c9d4c438113a1a09a97be03", size = 2453965, upload-time = "2025-10-10T03:12:59.07Z" }, - { url = "https://files.pythonhosted.org/packages/d3/a4/3a09b363d5bf7c4e2b97f770b308973759dce2acdf296b4023c3239ae7a7/ruff_format-0.3.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:c905725e0dad3016a0c7cd16eea64edec7bc42cd60036378a4e206a56ee565fd", size = 2475816, upload-time = "2025-10-10T03:13:06.68Z" }, + { url = "https://files.pythonhosted.org/packages/2e/00/2d7778a97bcae6a3e1ddbc740936b0fcc7e7abe2e0ee054b18b4100bed5c/ruff_format-0.4.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:ced72ef509b427483b27c7d85411fe9e11cba38cfd20f899579a22f7582fa598", size = 2148646, upload-time = "2025-10-28T18:31:25.359Z" }, + { url = "https://files.pythonhosted.org/packages/be/da/1c136748eeb09609c06859fdfec93e3f35f928ca2be7ca34973df172bb39/ruff_format-0.4.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:af3df907875a5ac8e156a9b63163fa4385ba629d743970b20a86a1f6eaaf8f20", size = 2087325, upload-time = "2025-10-28T18:31:22.905Z" }, + { url = "https://files.pythonhosted.org/packages/54/b2/bba49938eeeb6b57a26cd86923c82d7fa52f0ee80cb79aad0e3cc75ca815/ruff_format-0.4.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:226155424d5454998697593d70518a77a6fb85ffb78f334e9ec3e651977289da", size = 2275529, upload-time = "2025-10-28T18:31:05.102Z" }, + { url = "https://files.pythonhosted.org/packages/18/43/ea43a81b45a62c0b4475d85062c829de24b8465255f1af01ba8819db5dac/ruff_format-0.4.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b363a61c116e5d74f4909b95708049a5f1a577a8fb0e75ac4d1b2bd02eac7440", size = 2221105, upload-time = "2025-10-28T18:31:08.147Z" }, + { url = "https://files.pythonhosted.org/packages/c4/5f/7127239e4aeec9afe61bb37c41296c8cd858b5ad4b7f46eeb8ba418b9f1f/ruff_format-0.4.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a990c9439bd658d481641ba87051471f9f205a33d32dbfa2c4aa1b3448eea3bb", size = 3136332, upload-time = "2025-10-28T18:31:11.572Z" }, + { url = "https://files.pythonhosted.org/packages/cc/b9/b59a438d9197e73347f4b6fad498e36f41fd3c56984614ed016f7b53fb2e/ruff_format-0.4.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:014714c227f2d12eb988bc1913aa58f8113aa25b0056220eff77ed9e2a5c31bd", size = 2431819, upload-time = "2025-10-28T18:31:14.559Z" }, + { url = "https://files.pythonhosted.org/packages/6c/e9/1a365b24d5c5bf1838a267eda49a399dd491104740210d11db817ca8dbf7/ruff_format-0.4.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a6adf7223143f42c89cb05cb0dd3ff2fc578c57ea84de6e0809b9e3324ff926c", size = 2331924, upload-time = "2025-10-28T18:31:19.791Z" }, + { url = "https://files.pythonhosted.org/packages/2e/79/79b8f418bce106a45a9c56c10722266782f3d987351aa6e88f385c27f99f/ruff_format-0.4.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:56e17ee28d47e4572f495221e96cc6fb87102aaed8f48110816e0c02768f2a6e", size = 2402844, upload-time = "2025-10-28T18:31:17.066Z" }, + { url = "https://files.pythonhosted.org/packages/67/e7/fa9e7d5b48a5bf0c285428f9861e389dbd0b6dae0040a4a16db02416090b/ruff_format-0.4.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:67f4402da91604d73dac8ce35a56419cf3ecd874ff8d26cd82122220a5fdca25", size = 2455140, upload-time = "2025-10-28T18:31:28.368Z" }, + { url = "https://files.pythonhosted.org/packages/49/0c/384d7983e6b33076e8d96aa96f669120932816d8e837f70495299d71459a/ruff_format-0.4.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:c3300140c01622a3f43e968dd226a42d19b1e6d5d9ce45c8c595e8f5918aa8cf", size = 2488239, upload-time = "2025-10-28T18:31:31.463Z" }, + { url = "https://files.pythonhosted.org/packages/ac/6d/df79fca14652a70b287458626bbadb26978086395726c0010f0438e114e6/ruff_format-0.4.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:8bc80217523edec20d76f03fe65872278a347f09ccfe4993641d0687782202cc", size = 2493273, upload-time = "2025-10-28T18:31:34.374Z" }, + { url = "https://files.pythonhosted.org/packages/33/50/f46613603c24a33377e129b75499585f88e89cdeae0bb3e92be6c717f02e/ruff_format-0.4.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d127ceb09f8938ceb91a9750dce59ac03e81e6c148a18d4baa2ecccb3df68bef", size = 2505607, upload-time = "2025-10-28T18:31:37.089Z" }, + { url = "https://files.pythonhosted.org/packages/3b/c0/6ebccef59083405b77a5c95779d1acece038f0b5ed223b19cc1704d2755d/ruff_format-0.4.1-cp313-cp313t-win32.whl", hash = "sha256:75c7f4cb08466dbd1d042c0c6013d25d4b61780f08aa05a8384bb553a73d88d7", size = 1818617, upload-time = "2025-10-28T18:31:42.864Z" }, + { url = "https://files.pythonhosted.org/packages/7b/ee/17aeeffa8dd4241360a8c6f9937e99cbbfb9d52dc033d382c9d4a87fec0e/ruff_format-0.4.1-cp313-cp313t-win_amd64.whl", hash = "sha256:0f5dccef951c5e161087c930ccbd8969712acc6081dce520fe31555025d5602f", size = 1923603, upload-time = "2025-10-28T18:31:40.526Z" }, + { url = "https://files.pythonhosted.org/packages/a9/0f/47755cda55e7d55211e43d5de61d80bd75d1c0f63d3df996d53d5cfbe1c5/ruff_format-0.4.1-cp38-abi3-macosx_10_12_x86_64.whl", hash = "sha256:60c5aca1dc8b0fa5133bd7983035999cce03daf97da24668945d69114c630dc8", size = 2153211, upload-time = "2025-10-28T18:31:26.698Z" }, + { url = "https://files.pythonhosted.org/packages/10/03/daeff0742bc47c2fc56d250049bdf6b81074d23dcab3b9486c9c33857cb1/ruff_format-0.4.1-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:f1eb8a30f2c03d27764a2985b56b2053d2f2e74a65cc98550edbc71aa4bfbb3c", size = 2090816, upload-time = "2025-10-28T18:31:24.175Z" }, + { url = "https://files.pythonhosted.org/packages/81/f0/64525240a46f1bdc9e7cf7c53342ab271c2ab22cd10f4c016153b6582ecb/ruff_format-0.4.1-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ae67a0d1d9b6826deefbbf3c326bcf9a4d4e6cd46536f8f7d33d2cd06bc79b97", size = 2280121, upload-time = "2025-10-28T18:31:06.578Z" }, + { url = "https://files.pythonhosted.org/packages/82/56/192aaa9e3ea2003f1da44cf3d766b93325dfae41ffbdc0133506456af06c/ruff_format-0.4.1-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7edcbaed5c66e46bf1c57efa93151624cfe6eb7f2af0864fe6194da4dad01524", size = 2226372, upload-time = "2025-10-28T18:31:09.988Z" }, + { url = "https://files.pythonhosted.org/packages/3c/a8/8759d7525b394b78dfa8484eba64db8373293f655e42a2ea48bf18a4028c/ruff_format-0.4.1-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0151c72b3f4a8eb2dbbcee360e68581676e7e490cd3cc4ba098fb96a831659db", size = 3139010, upload-time = "2025-10-28T18:31:12.958Z" }, + { url = "https://files.pythonhosted.org/packages/5a/70/5c52d0dd2d8d5287b4e27724f022f7229d4c45fe0f75f53d659f67d408f6/ruff_format-0.4.1-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:00e0a83bd9a7acc8d50a63d8057441085d7498e94b130377082bea4cc57fdc62", size = 2437646, upload-time = "2025-10-28T18:31:15.862Z" }, + { url = "https://files.pythonhosted.org/packages/66/99/b89a34911e7287505e13efa487f2488e9a537d2429b37f465c94ea6612bf/ruff_format-0.4.1-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9086e8efea0925699b1c2a649922b936356d88f64c36b859aac2ed6033991042", size = 2338267, upload-time = "2025-10-28T18:31:21.419Z" }, + { url = "https://files.pythonhosted.org/packages/ff/02/92178e6b14b93f7dfed57c6c5f249ad7b858218aaf55f303f932d04d6459/ruff_format-0.4.1-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:be26579b974401f8e6cadd4837bdb4033696740758973c43dc470bd8404abf0a", size = 2408411, upload-time = "2025-10-28T18:31:18.593Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d8/61f41a959595adfb00f60c9a8fcbb0b5d32c69d32cafbdf807d38cd543a9/ruff_format-0.4.1-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:95f3c7dee3067616aaeb1d3e08968ea1d71e030c1c9704524ad91149df1cbda2", size = 2460360, upload-time = "2025-10-28T18:31:30.19Z" }, + { url = "https://files.pythonhosted.org/packages/f3/1e/d32bea94d19e1a5df9cbb581ffe46b25d72f2f13dc380ebc301366dd0791/ruff_format-0.4.1-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:c00576aac8e66548d778116be4e8d83abf994303c9426d2d9fa3ee47e952ebeb", size = 2492780, upload-time = "2025-10-28T18:31:32.804Z" }, + { url = "https://files.pythonhosted.org/packages/a8/71/b6d9b84cbd716a2c968c9cfe365070d2f5411e448287ed925d9c8786bade/ruff_format-0.4.1-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:15ad2649a213a78f907893a42712381e7a0f66107e5d5f48e9891657a5147852", size = 2498606, upload-time = "2025-10-28T18:31:35.818Z" }, + { url = "https://files.pythonhosted.org/packages/ec/2e/3f40d244fdd5bdbe993f55d46e2285900d4b03f0a50fdcd8280ce8635209/ruff_format-0.4.1-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a322cd9062664d0461efc804418b349c30a68e1e58c8794b1e9b28aa57ee02a2", size = 2509832, upload-time = "2025-10-28T18:31:38.757Z" }, + { url = "https://files.pythonhosted.org/packages/59/86/3253d7f4354a104e1e79110d740f4293042fd399981c59653a8353dc450a/ruff_format-0.4.1-cp38-abi3-win32.whl", hash = "sha256:66524a2088eb0f2bb95f297ad8bda2bec0143e9d690bbce76a2b326b4e668968", size = 1823361, upload-time = "2025-10-28T18:31:44.07Z" }, + { url = "https://files.pythonhosted.org/packages/49/2d/7f20db74ff5156900852077026771bd20f3ca1ba2a4987d98e3385c13ad8/ruff_format-0.4.1-cp38-abi3-win_amd64.whl", hash = "sha256:3f5360ebfe9465c4bb803dd69c6cdc38610860ee9087b40ae038a595c4779653", size = 1927799, upload-time = "2025-10-28T18:31:41.716Z" }, ] [[package]] @@ -2253,39 +1696,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fb/6a/a0a024878a1933a2326c42a3ce24fff6c0bf4882655f156c960ba50c2ed4/silero_vad-6.0.0-py3-none-any.whl", hash = "sha256:37d29be8944d2a2e6f1cc38a066076f13e78e6fc1b567a1beddcca72096f077f", size = 6119146, upload-time = "2025-08-26T07:10:00.637Z" }, ] -[[package]] -name = "singletonify" -version = "0.2.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/97/61/b297dab1cca05651aac73e93fd6e8083ae08bab7b549cb2d3f0ce7e92111/singletonify-0.2.4.tar.gz", hash = "sha256:05be9f3eefc9dcd93fc18eabc72468f586a317af6b216a821e7a1f2ea351f26f", size = 2173, upload-time = "2018-10-17T03:26:59.566Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/4b/31/7aa2ad2a40cada659a46ad2441de4bda08cc2385c40ce7886b976f59b2ca/singletonify-0.2.4-py3-none-any.whl", hash = "sha256:2508c0630611f72061bb396427c9a2932d9909cc07ebaa479edef01f64dab336", size = 3211, upload-time = "2018-10-17T03:26:58.687Z" }, -] - -[[package]] -name = "six" -version = "1.17.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, -] - -[[package]] -name = "slixmpp" -version = "1.9.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "aiodns" }, - { name = "pyasn1" }, - { name = "pyasn1-modules" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ca/c3/bfeeab121935bcf5e982ab67f347cfd2d752cbeaa1c794583849c5a65c7a/slixmpp-1.9.1.tar.gz", hash = "sha256:26d05a1700f7ea492a279c9f53707679d322bbe84c87ab97a87810302237916c", size = 708818, upload-time = "2025-03-11T22:38:48.527Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/aa/d5/643c683995b80911d1ef3b669dfa39a03ed3af21e302a591191889548f75/slixmpp-1.9.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e31d47aa7c189bfc6f0724621817c186434fe1e85b3d82bca8f514be38a2eab6", size = 988179, upload-time = "2025-03-12T23:40:58.873Z" }, - { url = "https://files.pythonhosted.org/packages/9e/85/8af9a942a5333e02cfc57cdc5c0426a5b0f76a74498c9449dd620def266b/slixmpp-1.9.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef204ca375020c6b205d0db216e9448ce10307a828b62703415713d3bba25fde", size = 991524, upload-time = "2025-03-11T22:52:16.462Z" }, -] - [[package]] name = "sniffio" version = "1.3.1" @@ -2323,43 +1733,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/14/e9/6b761de83277f2f02ded7e7ea6f07828ec78e4b229b80e4ca55dd205b9dc/soundfile-0.13.1-py2.py3-none-win_amd64.whl", hash = "sha256:1e70a05a0626524a69e9f0f4dd2ec174b4e9567f4d8b6c11d38b5c289be36ee9", size = 1019162, upload-time = "2025-01-25T09:16:59.573Z" }, ] -[[package]] -name = "spade" -version = "4.1.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "aiohttp" }, - { name = "aiohttp-jinja2" }, - { name = "jinja2" }, - { name = "jinja2-time" }, - { name = "pyjabber" }, - { name = "pytz" }, - { name = "rich" }, - { name = "singletonify" }, - { name = "slixmpp" }, - { name = "timeago" }, - { name = "uvloop", marker = "sys_platform != 'win32'" }, - { name = "winloop", marker = "sys_platform == 'win32'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/4f/59/014e183abbb814f16002fcdfade5eff9ed0ac9f4d4db2c363e89f5487f3d/spade-4.1.0.tar.gz", hash = "sha256:df67921bdfb05b7c1650dd24bdd48cf077d8fd9506d5bcf50f7d5d576a2a7704", size = 479166, upload-time = "2025-05-22T17:19:08.466Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e9/06/21d0e937f4daa905a9a007700f59b06de644a44e5f594c3428c3ff93ca39/spade-4.1.0-py2.py3-none-any.whl", hash = "sha256:8b20e7fcb12f836cb0504e9da31f7bd867c7276440e19ebca864aecabc71b114", size = 37033, upload-time = "2025-05-22T17:19:06.524Z" }, -] - -[[package]] -name = "spade-bdi" -version = "0.3.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "agentspeak" }, - { name = "loguru" }, - { name = "spade" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/16/b4/d52d9d06ad17d4b3a90ca11b64a14194f3f944f561f4da1395ce3fe3994d/spade_bdi-0.3.2.tar.gz", hash = "sha256:5d03661425f78771e39f3592f8a602ff8240465682b79d333926d3e562657d81", size = 21208, upload-time = "2025-01-03T14:16:43.755Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/90/c2/986de9abaad805d92a33912ab06b08bb81bd404bcef9ad0f2fd7a09f274b/spade_bdi-0.3.2-py2.py3-none-any.whl", hash = "sha256:2039271f586b108660a0a6a951d9ec815197caf14915317c6eec19ff496c2cff", size = 7416, upload-time = "2025-01-03T14:16:42.226Z" }, -] - [[package]] name = "sphinx" version = "7.3.7" @@ -2467,27 +1840,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/52/a7/d2782e4e3f77c8450f727ba74a8f12756d5ba823d81b941f1b04da9d033a/sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl", hash = "sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331", size = 92072, upload-time = "2024-07-29T01:10:08.203Z" }, ] -[[package]] -name = "sqlalchemy" -version = "2.0.40" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "greenlet", marker = "(python_full_version < '3.14' and platform_machine == 'AMD64') or (python_full_version < '3.14' and platform_machine == 'WIN32') or (python_full_version < '3.14' and platform_machine == 'aarch64') or (python_full_version < '3.14' and platform_machine == 'amd64') or (python_full_version < '3.14' and platform_machine == 'ppc64le') or (python_full_version < '3.14' and platform_machine == 'win32') or (python_full_version < '3.14' and platform_machine == 'x86_64')" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/68/c3/3f2bfa5e4dcd9938405fe2fab5b6ab94a9248a4f9536ea2fd497da20525f/sqlalchemy-2.0.40.tar.gz", hash = "sha256:d827099289c64589418ebbcaead0145cd19f4e3e8a93919a0100247af245fa00", size = 9664299, upload-time = "2025-03-27T17:52:31.876Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/8c/18/4e3a86cc0232377bc48c373a9ba6a1b3fb79ba32dbb4eda0b357f5a2c59d/sqlalchemy-2.0.40-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:915866fd50dd868fdcc18d61d8258db1bf9ed7fbd6dfec960ba43365952f3b01", size = 2107887, upload-time = "2025-03-27T18:40:05.461Z" }, - { url = "https://files.pythonhosted.org/packages/cb/60/9fa692b1d2ffc4cbd5f47753731fd332afed30137115d862d6e9a1e962c7/sqlalchemy-2.0.40-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4a4c5a2905a9ccdc67a8963e24abd2f7afcd4348829412483695c59e0af9a705", size = 2098367, upload-time = "2025-03-27T18:40:07.182Z" }, - { url = "https://files.pythonhosted.org/packages/4c/9f/84b78357ca641714a439eb3fbbddb17297dacfa05d951dbf24f28d7b5c08/sqlalchemy-2.0.40-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55028d7a3ebdf7ace492fab9895cbc5270153f75442a0472d8516e03159ab364", size = 3184806, upload-time = "2025-03-27T18:51:29.356Z" }, - { url = "https://files.pythonhosted.org/packages/4b/7d/e06164161b6bfce04c01bfa01518a20cccbd4100d5c951e5a7422189191a/sqlalchemy-2.0.40-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6cfedff6878b0e0d1d0a50666a817ecd85051d12d56b43d9d425455e608b5ba0", size = 3198131, upload-time = "2025-03-27T18:50:31.616Z" }, - { url = "https://files.pythonhosted.org/packages/6d/51/354af20da42d7ec7b5c9de99edafbb7663a1d75686d1999ceb2c15811302/sqlalchemy-2.0.40-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bb19e30fdae77d357ce92192a3504579abe48a66877f476880238a962e5b96db", size = 3131364, upload-time = "2025-03-27T18:51:31.336Z" }, - { url = "https://files.pythonhosted.org/packages/7a/2f/48a41ff4e6e10549d83fcc551ab85c268bde7c03cf77afb36303c6594d11/sqlalchemy-2.0.40-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:16d325ea898f74b26ffcd1cf8c593b0beed8714f0317df2bed0d8d1de05a8f26", size = 3159482, upload-time = "2025-03-27T18:50:33.201Z" }, - { url = "https://files.pythonhosted.org/packages/33/ac/e5e0a807163652a35be878c0ad5cfd8b1d29605edcadfb5df3c512cdf9f3/sqlalchemy-2.0.40-cp313-cp313-win32.whl", hash = "sha256:a669cbe5be3c63f75bcbee0b266779706f1a54bcb1000f302685b87d1b8c1500", size = 2080704, upload-time = "2025-03-27T18:46:00.193Z" }, - { url = "https://files.pythonhosted.org/packages/1c/cb/f38c61f7f2fd4d10494c1c135ff6a6ddb63508d0b47bccccd93670637309/sqlalchemy-2.0.40-cp313-cp313-win_amd64.whl", hash = "sha256:641ee2e0834812d657862f3a7de95e0048bdcb6c55496f39c6fa3d435f6ac6ad", size = 2104564, upload-time = "2025-03-27T18:46:01.442Z" }, - { url = "https://files.pythonhosted.org/packages/d1/7c/5fc8e802e7506fe8b55a03a2e1dab156eae205c91bee46305755e086d2e2/sqlalchemy-2.0.40-py3-none-any.whl", hash = "sha256:32587e2e1e359276957e6fe5dad089758bc042a971a8a09ae8ecf7a8fe23d07a", size = 1903894, upload-time = "2025-03-27T18:40:43.796Z" }, -] - [[package]] name = "starlette" version = "0.41.3" @@ -2552,14 +1904,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/af/df/c7891ef9d2712ad774777271d39fdef63941ffba0a9d59b7ad1fd2765e57/tiktoken-0.12.0-cp314-cp314t-win_amd64.whl", hash = "sha256:f61c0aea5565ac82e2ec50a05e02a6c44734e91b51c10510b084ea1b8e633a71", size = 920667, upload-time = "2025-10-06T20:22:34.444Z" }, ] -[[package]] -name = "timeago" -version = "1.0.16" -source = { registry = "https://pypi.org/simple" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5f/88/8dac5496354650972434966ba570a4a824fafed43471cf190faea4b085fc/timeago-1.0.16-py3-none-any.whl", hash = "sha256:9b8cb2e3102b329f35a04aa4531982d867b093b19481cfbb1dac7845fa2f79b0", size = 29693, upload-time = "2022-08-18T21:54:38.399Z" }, -] - [[package]] name = "torch" version = "2.8.0" @@ -2656,15 +2000,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/00/22/35617eee79080a5d071d0f14ad698d325ee6b3bf824fc0467c03b30e7fa8/typer-0.19.2-py3-none-any.whl", hash = "sha256:755e7e19670ffad8283db353267cb81ef252f595aa6834a0d1ca9312d9326cb9", size = 46748, upload-time = "2025-09-23T09:47:46.777Z" }, ] -[[package]] -name = "types-python-dateutil" -version = "2.9.0.20251008" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fc/83/24ed25dd0c6277a1a170c180ad9eef5879ecc9a4745b58d7905a4588c80d/types_python_dateutil-2.9.0.20251008.tar.gz", hash = "sha256:c3826289c170c93ebd8360c3485311187df740166dbab9dd3b792e69f2bc1f9c", size = 16128, upload-time = "2025-10-08T02:51:34.93Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/da/af/5d24b8d49ef358468ecfdff5c556adf37f4fd28e336b96f923661a808329/types_python_dateutil-2.9.0.20251008-py3-none-any.whl", hash = "sha256:b9a5232c8921cf7661b29c163ccc56055c418ab2c6eabe8f917cbcc73a4c4157", size = 17934, upload-time = "2025-10-08T02:51:33.55Z" }, -] - [[package]] name = "typing-extensions" version = "4.15.0" @@ -2776,16 +2111,16 @@ wheels = [ [[package]] name = "virtualenv" -version = "20.35.3" +version = "20.35.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "distlib" }, { name = "filelock" }, { name = "platformdirs" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a4/d5/b0ccd381d55c8f45d46f77df6ae59fbc23d19e901e2d523395598e5f4c93/virtualenv-20.35.3.tar.gz", hash = "sha256:4f1a845d131133bdff10590489610c98c168ff99dc75d6c96853801f7f67af44", size = 6002907, upload-time = "2025-10-10T21:23:33.178Z" } +sdist = { url = "https://files.pythonhosted.org/packages/20/28/e6f1a6f655d620846bd9df527390ecc26b3805a0c5989048c210e22c5ca9/virtualenv-20.35.4.tar.gz", hash = "sha256:643d3914d73d3eeb0c552cbb12d7e82adf0e504dbf86a3182f8771a153a1971c", size = 6028799, upload-time = "2025-10-29T06:57:40.511Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/27/73/d9a94da0e9d470a543c1b9d3ccbceb0f59455983088e727b8a1824ed90fb/virtualenv-20.35.3-py3-none-any.whl", hash = "sha256:63d106565078d8c8d0b206d48080f938a8b25361e19432d2c9db40d2899c810a", size = 5981061, upload-time = "2025-10-10T21:23:30.433Z" }, + { url = "https://files.pythonhosted.org/packages/79/0c/c05523fa3181fdf0c9c52a6ba91a23fbf3246cc095f26f6516f9c60e6771/virtualenv-20.35.4-py3-none-any.whl", hash = "sha256:c21c9cede36c9753eeade68ba7d523529f228a403463376cf821eaae2b650f1b", size = 6005095, upload-time = "2025-10-29T06:57:37.598Z" }, ] [[package]] @@ -2861,99 +2196,3 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1b/6c/c65773d6cab416a64d191d6ee8a8b1c68a09970ea6909d16965d26bfed1e/websockets-15.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561", size = 176837, upload-time = "2025-03-05T20:02:55.237Z" }, { url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743, upload-time = "2025-03-05T20:03:39.41Z" }, ] - -[[package]] -name = "win32-setctime" -version = "1.2.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b3/8f/705086c9d734d3b663af0e9bb3d4de6578d08f46b1b101c2442fd9aecaa2/win32_setctime-1.2.0.tar.gz", hash = "sha256:ae1fdf948f5640aae05c511ade119313fb6a30d7eabe25fef9764dca5873c4c0", size = 4867, upload-time = "2024-12-07T15:28:28.314Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e1/07/c6fe3ad3e685340704d314d765b7912993bcb8dc198f0e7a89382d37974b/win32_setctime-1.2.0-py3-none-any.whl", hash = "sha256:95d644c4e708aba81dc3704a116d8cbc974d70b3bdb8be1d150e36be6e9d1390", size = 4083, upload-time = "2024-12-07T15:28:26.465Z" }, -] - -[[package]] -name = "winloop" -version = "0.1.8" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d3/a2/2baddef6f51d00958b938aed7860296fa9bedbe03c199bc72520c914b273/winloop-0.1.8.tar.gz", hash = "sha256:bbb1b8e12bd9d231153e4a143440d862886a67675aa1a0701f98dff42c19d857", size = 1827060, upload-time = "2025-01-13T23:01:58.546Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fd/6a/5133b4da347bb2fd334320cacb36a8bd232af3ded2235cd085c0a2247274/winloop-0.1.8-cp313-cp313-win_amd64.whl", hash = "sha256:0c1c2d2087cb2c1b7defefed44bd875c9b040d46a32caa27d1847e06cb4e5f50", size = 712469, upload-time = "2025-01-13T23:01:53.483Z" }, -] - -[[package]] -name = "yarl" -version = "1.22.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "idna" }, - { name = "multidict" }, - { name = "propcache" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/57/63/0c6ebca57330cd313f6102b16dd57ffaf3ec4c83403dcb45dbd15c6f3ea1/yarl-1.22.0.tar.gz", hash = "sha256:bebf8557577d4401ba8bd9ff33906f1376c877aa78d1fe216ad01b4d6745af71", size = 187169, upload-time = "2025-10-06T14:12:55.963Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ea/f3/d67de7260456ee105dc1d162d43a019ecad6b91e2f51809d6cddaa56690e/yarl-1.22.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8dee9c25c74997f6a750cd317b8ca63545169c098faee42c84aa5e506c819b53", size = 139980, upload-time = "2025-10-06T14:10:14.601Z" }, - { url = "https://files.pythonhosted.org/packages/01/88/04d98af0b47e0ef42597b9b28863b9060bb515524da0a65d5f4db160b2d5/yarl-1.22.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:01e73b85a5434f89fc4fe27dcda2aff08ddf35e4d47bbbea3bdcd25321af538a", size = 93424, upload-time = "2025-10-06T14:10:16.115Z" }, - { url = "https://files.pythonhosted.org/packages/18/91/3274b215fd8442a03975ce6bee5fe6aa57a8326b29b9d3d56234a1dca244/yarl-1.22.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:22965c2af250d20c873cdbee8ff958fb809940aeb2e74ba5f20aaf6b7ac8c70c", size = 93821, upload-time = "2025-10-06T14:10:17.993Z" }, - { url = "https://files.pythonhosted.org/packages/61/3a/caf4e25036db0f2da4ca22a353dfeb3c9d3c95d2761ebe9b14df8fc16eb0/yarl-1.22.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b4f15793aa49793ec8d1c708ab7f9eded1aa72edc5174cae703651555ed1b601", size = 373243, upload-time = "2025-10-06T14:10:19.44Z" }, - { url = "https://files.pythonhosted.org/packages/6e/9e/51a77ac7516e8e7803b06e01f74e78649c24ee1021eca3d6a739cb6ea49c/yarl-1.22.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5542339dcf2747135c5c85f68680353d5cb9ffd741c0f2e8d832d054d41f35a", size = 342361, upload-time = "2025-10-06T14:10:21.124Z" }, - { url = "https://files.pythonhosted.org/packages/d4/f8/33b92454789dde8407f156c00303e9a891f1f51a0330b0fad7c909f87692/yarl-1.22.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5c401e05ad47a75869c3ab3e35137f8468b846770587e70d71e11de797d113df", size = 387036, upload-time = "2025-10-06T14:10:22.902Z" }, - { url = "https://files.pythonhosted.org/packages/d9/9a/c5db84ea024f76838220280f732970aa4ee154015d7f5c1bfb60a267af6f/yarl-1.22.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:243dda95d901c733f5b59214d28b0120893d91777cb8aa043e6ef059d3cddfe2", size = 397671, upload-time = "2025-10-06T14:10:24.523Z" }, - { url = "https://files.pythonhosted.org/packages/11/c9/cd8538dc2e7727095e0c1d867bad1e40c98f37763e6d995c1939f5fdc7b1/yarl-1.22.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bec03d0d388060058f5d291a813f21c011041938a441c593374da6077fe21b1b", size = 377059, upload-time = "2025-10-06T14:10:26.406Z" }, - { url = "https://files.pythonhosted.org/packages/a1/b9/ab437b261702ced75122ed78a876a6dec0a1b0f5e17a4ac7a9a2482d8abe/yarl-1.22.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b0748275abb8c1e1e09301ee3cf90c8a99678a4e92e4373705f2a2570d581273", size = 365356, upload-time = "2025-10-06T14:10:28.461Z" }, - { url = "https://files.pythonhosted.org/packages/b2/9d/8e1ae6d1d008a9567877b08f0ce4077a29974c04c062dabdb923ed98e6fe/yarl-1.22.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:47fdb18187e2a4e18fda2c25c05d8251a9e4a521edaed757fef033e7d8498d9a", size = 361331, upload-time = "2025-10-06T14:10:30.541Z" }, - { url = "https://files.pythonhosted.org/packages/ca/5a/09b7be3905962f145b73beb468cdd53db8aa171cf18c80400a54c5b82846/yarl-1.22.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c7044802eec4524fde550afc28edda0dd5784c4c45f0be151a2d3ba017daca7d", size = 382590, upload-time = "2025-10-06T14:10:33.352Z" }, - { url = "https://files.pythonhosted.org/packages/aa/7f/59ec509abf90eda5048b0bc3e2d7b5099dffdb3e6b127019895ab9d5ef44/yarl-1.22.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:139718f35149ff544caba20fce6e8a2f71f1e39b92c700d8438a0b1d2a631a02", size = 385316, upload-time = "2025-10-06T14:10:35.034Z" }, - { url = "https://files.pythonhosted.org/packages/e5/84/891158426bc8036bfdfd862fabd0e0fa25df4176ec793e447f4b85cf1be4/yarl-1.22.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e1b51bebd221006d3d2f95fbe124b22b247136647ae5dcc8c7acafba66e5ee67", size = 374431, upload-time = "2025-10-06T14:10:37.76Z" }, - { url = "https://files.pythonhosted.org/packages/bb/49/03da1580665baa8bef5e8ed34c6df2c2aca0a2f28bf397ed238cc1bbc6f2/yarl-1.22.0-cp313-cp313-win32.whl", hash = "sha256:d3e32536234a95f513bd374e93d717cf6b2231a791758de6c509e3653f234c95", size = 81555, upload-time = "2025-10-06T14:10:39.649Z" }, - { url = "https://files.pythonhosted.org/packages/9a/ee/450914ae11b419eadd067c6183ae08381cfdfcb9798b90b2b713bbebddda/yarl-1.22.0-cp313-cp313-win_amd64.whl", hash = "sha256:47743b82b76d89a1d20b83e60d5c20314cbd5ba2befc9cda8f28300c4a08ed4d", size = 86965, upload-time = "2025-10-06T14:10:41.313Z" }, - { url = "https://files.pythonhosted.org/packages/98/4d/264a01eae03b6cf629ad69bae94e3b0e5344741e929073678e84bf7a3e3b/yarl-1.22.0-cp313-cp313-win_arm64.whl", hash = "sha256:5d0fcda9608875f7d052eff120c7a5da474a6796fe4d83e152e0e4d42f6d1a9b", size = 81205, upload-time = "2025-10-06T14:10:43.167Z" }, - { url = "https://files.pythonhosted.org/packages/88/fc/6908f062a2f77b5f9f6d69cecb1747260831ff206adcbc5b510aff88df91/yarl-1.22.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:719ae08b6972befcba4310e49edb1161a88cdd331e3a694b84466bd938a6ab10", size = 146209, upload-time = "2025-10-06T14:10:44.643Z" }, - { url = "https://files.pythonhosted.org/packages/65/47/76594ae8eab26210b4867be6f49129861ad33da1f1ebdf7051e98492bf62/yarl-1.22.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:47d8a5c446df1c4db9d21b49619ffdba90e77c89ec6e283f453856c74b50b9e3", size = 95966, upload-time = "2025-10-06T14:10:46.554Z" }, - { url = "https://files.pythonhosted.org/packages/ab/ce/05e9828a49271ba6b5b038b15b3934e996980dd78abdfeb52a04cfb9467e/yarl-1.22.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:cfebc0ac8333520d2d0423cbbe43ae43c8838862ddb898f5ca68565e395516e9", size = 97312, upload-time = "2025-10-06T14:10:48.007Z" }, - { url = "https://files.pythonhosted.org/packages/d1/c5/7dffad5e4f2265b29c9d7ec869c369e4223166e4f9206fc2243ee9eea727/yarl-1.22.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4398557cbf484207df000309235979c79c4356518fd5c99158c7d38203c4da4f", size = 361967, upload-time = "2025-10-06T14:10:49.997Z" }, - { url = "https://files.pythonhosted.org/packages/50/b2/375b933c93a54bff7fc041e1a6ad2c0f6f733ffb0c6e642ce56ee3b39970/yarl-1.22.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2ca6fd72a8cd803be290d42f2dec5cdcd5299eeb93c2d929bf060ad9efaf5de0", size = 323949, upload-time = "2025-10-06T14:10:52.004Z" }, - { url = "https://files.pythonhosted.org/packages/66/50/bfc2a29a1d78644c5a7220ce2f304f38248dc94124a326794e677634b6cf/yarl-1.22.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ca1f59c4e1ab6e72f0a23c13fca5430f889634166be85dbf1013683e49e3278e", size = 361818, upload-time = "2025-10-06T14:10:54.078Z" }, - { url = "https://files.pythonhosted.org/packages/46/96/f3941a46af7d5d0f0498f86d71275696800ddcdd20426298e572b19b91ff/yarl-1.22.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6c5010a52015e7c70f86eb967db0f37f3c8bd503a695a49f8d45700144667708", size = 372626, upload-time = "2025-10-06T14:10:55.767Z" }, - { url = "https://files.pythonhosted.org/packages/c1/42/8b27c83bb875cd89448e42cd627e0fb971fa1675c9ec546393d18826cb50/yarl-1.22.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d7672ecf7557476642c88497c2f8d8542f8e36596e928e9bcba0e42e1e7d71f", size = 341129, upload-time = "2025-10-06T14:10:57.985Z" }, - { url = "https://files.pythonhosted.org/packages/49/36/99ca3122201b382a3cf7cc937b95235b0ac944f7e9f2d5331d50821ed352/yarl-1.22.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:3b7c88eeef021579d600e50363e0b6ee4f7f6f728cd3486b9d0f3ee7b946398d", size = 346776, upload-time = "2025-10-06T14:10:59.633Z" }, - { url = "https://files.pythonhosted.org/packages/85/b4/47328bf996acd01a4c16ef9dcd2f59c969f495073616586f78cd5f2efb99/yarl-1.22.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:f4afb5c34f2c6fecdcc182dfcfc6af6cccf1aa923eed4d6a12e9d96904e1a0d8", size = 334879, upload-time = "2025-10-06T14:11:01.454Z" }, - { url = "https://files.pythonhosted.org/packages/c2/ad/b77d7b3f14a4283bffb8e92c6026496f6de49751c2f97d4352242bba3990/yarl-1.22.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:59c189e3e99a59cf8d83cbb31d4db02d66cda5a1a4374e8a012b51255341abf5", size = 350996, upload-time = "2025-10-06T14:11:03.452Z" }, - { url = "https://files.pythonhosted.org/packages/81/c8/06e1d69295792ba54d556f06686cbd6a7ce39c22307100e3fb4a2c0b0a1d/yarl-1.22.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:5a3bf7f62a289fa90f1990422dc8dff5a458469ea71d1624585ec3a4c8d6960f", size = 356047, upload-time = "2025-10-06T14:11:05.115Z" }, - { url = "https://files.pythonhosted.org/packages/4b/b8/4c0e9e9f597074b208d18cef227d83aac36184bfbc6eab204ea55783dbc5/yarl-1.22.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:de6b9a04c606978fdfe72666fa216ffcf2d1a9f6a381058d4378f8d7b1e5de62", size = 342947, upload-time = "2025-10-06T14:11:08.137Z" }, - { url = "https://files.pythonhosted.org/packages/e0/e5/11f140a58bf4c6ad7aca69a892bff0ee638c31bea4206748fc0df4ebcb3a/yarl-1.22.0-cp313-cp313t-win32.whl", hash = "sha256:1834bb90991cc2999f10f97f5f01317f99b143284766d197e43cd5b45eb18d03", size = 86943, upload-time = "2025-10-06T14:11:10.284Z" }, - { url = "https://files.pythonhosted.org/packages/31/74/8b74bae38ed7fe6793d0c15a0c8207bbb819cf287788459e5ed230996cdd/yarl-1.22.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ff86011bd159a9d2dfc89c34cfd8aff12875980e3bd6a39ff097887520e60249", size = 93715, upload-time = "2025-10-06T14:11:11.739Z" }, - { url = "https://files.pythonhosted.org/packages/69/66/991858aa4b5892d57aef7ee1ba6b4d01ec3b7eb3060795d34090a3ca3278/yarl-1.22.0-cp313-cp313t-win_arm64.whl", hash = "sha256:7861058d0582b847bc4e3a4a4c46828a410bca738673f35a29ba3ca5db0b473b", size = 83857, upload-time = "2025-10-06T14:11:13.586Z" }, - { url = "https://files.pythonhosted.org/packages/46/b3/e20ef504049f1a1c54a814b4b9bed96d1ac0e0610c3b4da178f87209db05/yarl-1.22.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:34b36c2c57124530884d89d50ed2c1478697ad7473efd59cfd479945c95650e4", size = 140520, upload-time = "2025-10-06T14:11:15.465Z" }, - { url = "https://files.pythonhosted.org/packages/e4/04/3532d990fdbab02e5ede063676b5c4260e7f3abea2151099c2aa745acc4c/yarl-1.22.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:0dd9a702591ca2e543631c2a017e4a547e38a5c0f29eece37d9097e04a7ac683", size = 93504, upload-time = "2025-10-06T14:11:17.106Z" }, - { url = "https://files.pythonhosted.org/packages/11/63/ff458113c5c2dac9a9719ac68ee7c947cb621432bcf28c9972b1c0e83938/yarl-1.22.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:594fcab1032e2d2cc3321bb2e51271e7cd2b516c7d9aee780ece81b07ff8244b", size = 94282, upload-time = "2025-10-06T14:11:19.064Z" }, - { url = "https://files.pythonhosted.org/packages/a7/bc/315a56aca762d44a6aaaf7ad253f04d996cb6b27bad34410f82d76ea8038/yarl-1.22.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f3d7a87a78d46a2e3d5b72587ac14b4c16952dd0887dbb051451eceac774411e", size = 372080, upload-time = "2025-10-06T14:11:20.996Z" }, - { url = "https://files.pythonhosted.org/packages/3f/3f/08e9b826ec2e099ea6e7c69a61272f4f6da62cb5b1b63590bb80ca2e4a40/yarl-1.22.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:852863707010316c973162e703bddabec35e8757e67fcb8ad58829de1ebc8590", size = 338696, upload-time = "2025-10-06T14:11:22.847Z" }, - { url = "https://files.pythonhosted.org/packages/e3/9f/90360108e3b32bd76789088e99538febfea24a102380ae73827f62073543/yarl-1.22.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:131a085a53bfe839a477c0845acf21efc77457ba2bcf5899618136d64f3303a2", size = 387121, upload-time = "2025-10-06T14:11:24.889Z" }, - { url = "https://files.pythonhosted.org/packages/98/92/ab8d4657bd5b46a38094cfaea498f18bb70ce6b63508fd7e909bd1f93066/yarl-1.22.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:078a8aefd263f4d4f923a9677b942b445a2be970ca24548a8102689a3a8ab8da", size = 394080, upload-time = "2025-10-06T14:11:27.307Z" }, - { url = "https://files.pythonhosted.org/packages/f5/e7/d8c5a7752fef68205296201f8ec2bf718f5c805a7a7e9880576c67600658/yarl-1.22.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bca03b91c323036913993ff5c738d0842fc9c60c4648e5c8d98331526df89784", size = 372661, upload-time = "2025-10-06T14:11:29.387Z" }, - { url = "https://files.pythonhosted.org/packages/b6/2e/f4d26183c8db0bb82d491b072f3127fb8c381a6206a3a56332714b79b751/yarl-1.22.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:68986a61557d37bb90d3051a45b91fa3d5c516d177dfc6dd6f2f436a07ff2b6b", size = 364645, upload-time = "2025-10-06T14:11:31.423Z" }, - { url = "https://files.pythonhosted.org/packages/80/7c/428e5812e6b87cd00ee8e898328a62c95825bf37c7fa87f0b6bb2ad31304/yarl-1.22.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:4792b262d585ff0dff6bcb787f8492e40698443ec982a3568c2096433660c694", size = 355361, upload-time = "2025-10-06T14:11:33.055Z" }, - { url = "https://files.pythonhosted.org/packages/ec/2a/249405fd26776f8b13c067378ef4d7dd49c9098d1b6457cdd152a99e96a9/yarl-1.22.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:ebd4549b108d732dba1d4ace67614b9545b21ece30937a63a65dd34efa19732d", size = 381451, upload-time = "2025-10-06T14:11:35.136Z" }, - { url = "https://files.pythonhosted.org/packages/67/a8/fb6b1adbe98cf1e2dd9fad71003d3a63a1bc22459c6e15f5714eb9323b93/yarl-1.22.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f87ac53513d22240c7d59203f25cc3beac1e574c6cd681bbfd321987b69f95fd", size = 383814, upload-time = "2025-10-06T14:11:37.094Z" }, - { url = "https://files.pythonhosted.org/packages/d9/f9/3aa2c0e480fb73e872ae2814c43bc1e734740bb0d54e8cb2a95925f98131/yarl-1.22.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:22b029f2881599e2f1b06f8f1db2ee63bd309e2293ba2d566e008ba12778b8da", size = 370799, upload-time = "2025-10-06T14:11:38.83Z" }, - { url = "https://files.pythonhosted.org/packages/50/3c/af9dba3b8b5eeb302f36f16f92791f3ea62e3f47763406abf6d5a4a3333b/yarl-1.22.0-cp314-cp314-win32.whl", hash = "sha256:6a635ea45ba4ea8238463b4f7d0e721bad669f80878b7bfd1f89266e2ae63da2", size = 82990, upload-time = "2025-10-06T14:11:40.624Z" }, - { url = "https://files.pythonhosted.org/packages/ac/30/ac3a0c5bdc1d6efd1b41fa24d4897a4329b3b1e98de9449679dd327af4f0/yarl-1.22.0-cp314-cp314-win_amd64.whl", hash = "sha256:0d6e6885777af0f110b0e5d7e5dda8b704efed3894da26220b7f3d887b839a79", size = 88292, upload-time = "2025-10-06T14:11:42.578Z" }, - { url = "https://files.pythonhosted.org/packages/df/0a/227ab4ff5b998a1b7410abc7b46c9b7a26b0ca9e86c34ba4b8d8bc7c63d5/yarl-1.22.0-cp314-cp314-win_arm64.whl", hash = "sha256:8218f4e98d3c10d683584cb40f0424f4b9fd6e95610232dd75e13743b070ee33", size = 82888, upload-time = "2025-10-06T14:11:44.863Z" }, - { url = "https://files.pythonhosted.org/packages/06/5e/a15eb13db90abd87dfbefb9760c0f3f257ac42a5cac7e75dbc23bed97a9f/yarl-1.22.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:45c2842ff0e0d1b35a6bf1cd6c690939dacb617a70827f715232b2e0494d55d1", size = 146223, upload-time = "2025-10-06T14:11:46.796Z" }, - { url = "https://files.pythonhosted.org/packages/18/82/9665c61910d4d84f41a5bf6837597c89e665fa88aa4941080704645932a9/yarl-1.22.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:d947071e6ebcf2e2bee8fce76e10faca8f7a14808ca36a910263acaacef08eca", size = 95981, upload-time = "2025-10-06T14:11:48.845Z" }, - { url = "https://files.pythonhosted.org/packages/5d/9a/2f65743589809af4d0a6d3aa749343c4b5f4c380cc24a8e94a3c6625a808/yarl-1.22.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:334b8721303e61b00019474cc103bdac3d7b1f65e91f0bfedeec2d56dfe74b53", size = 97303, upload-time = "2025-10-06T14:11:50.897Z" }, - { url = "https://files.pythonhosted.org/packages/b0/ab/5b13d3e157505c43c3b43b5a776cbf7b24a02bc4cccc40314771197e3508/yarl-1.22.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1e7ce67c34138a058fd092f67d07a72b8e31ff0c9236e751957465a24b28910c", size = 361820, upload-time = "2025-10-06T14:11:52.549Z" }, - { url = "https://files.pythonhosted.org/packages/fb/76/242a5ef4677615cf95330cfc1b4610e78184400699bdda0acb897ef5e49a/yarl-1.22.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d77e1b2c6d04711478cb1c4ab90db07f1609ccf06a287d5607fcd90dc9863acf", size = 323203, upload-time = "2025-10-06T14:11:54.225Z" }, - { url = "https://files.pythonhosted.org/packages/8c/96/475509110d3f0153b43d06164cf4195c64d16999e0c7e2d8a099adcd6907/yarl-1.22.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4647674b6150d2cae088fc07de2738a84b8bcedebef29802cf0b0a82ab6face", size = 363173, upload-time = "2025-10-06T14:11:56.069Z" }, - { url = "https://files.pythonhosted.org/packages/c9/66/59db471aecfbd559a1fd48aedd954435558cd98c7d0da8b03cc6c140a32c/yarl-1.22.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:efb07073be061c8f79d03d04139a80ba33cbd390ca8f0297aae9cce6411e4c6b", size = 373562, upload-time = "2025-10-06T14:11:58.783Z" }, - { url = "https://files.pythonhosted.org/packages/03/1f/c5d94abc91557384719da10ff166b916107c1b45e4d0423a88457071dd88/yarl-1.22.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e51ac5435758ba97ad69617e13233da53908beccc6cfcd6c34bbed8dcbede486", size = 339828, upload-time = "2025-10-06T14:12:00.686Z" }, - { url = "https://files.pythonhosted.org/packages/5f/97/aa6a143d3afba17b6465733681c70cf175af89f76ec8d9286e08437a7454/yarl-1.22.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:33e32a0dd0c8205efa8e83d04fc9f19313772b78522d1bdc7d9aed706bfd6138", size = 347551, upload-time = "2025-10-06T14:12:02.628Z" }, - { url = "https://files.pythonhosted.org/packages/43/3c/45a2b6d80195959239a7b2a8810506d4eea5487dce61c2a3393e7fc3c52e/yarl-1.22.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:bf4a21e58b9cde0e401e683ebd00f6ed30a06d14e93f7c8fd059f8b6e8f87b6a", size = 334512, upload-time = "2025-10-06T14:12:04.871Z" }, - { url = "https://files.pythonhosted.org/packages/86/a0/c2ab48d74599c7c84cb104ebd799c5813de252bea0f360ffc29d270c2caa/yarl-1.22.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:e4b582bab49ac33c8deb97e058cd67c2c50dac0dd134874106d9c774fd272529", size = 352400, upload-time = "2025-10-06T14:12:06.624Z" }, - { url = "https://files.pythonhosted.org/packages/32/75/f8919b2eafc929567d3d8411f72bdb1a2109c01caaab4ebfa5f8ffadc15b/yarl-1.22.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:0b5bcc1a9c4839e7e30b7b30dd47fe5e7e44fb7054ec29b5bb8d526aa1041093", size = 357140, upload-time = "2025-10-06T14:12:08.362Z" }, - { url = "https://files.pythonhosted.org/packages/cf/72/6a85bba382f22cf78add705d8c3731748397d986e197e53ecc7835e76de7/yarl-1.22.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c0232bce2170103ec23c454e54a57008a9a72b5d1c3105dc2496750da8cfa47c", size = 341473, upload-time = "2025-10-06T14:12:10.994Z" }, - { url = "https://files.pythonhosted.org/packages/35/18/55e6011f7c044dc80b98893060773cefcfdbf60dfefb8cb2f58b9bacbd83/yarl-1.22.0-cp314-cp314t-win32.whl", hash = "sha256:8009b3173bcd637be650922ac455946197d858b3630b6d8787aa9e5c4564533e", size = 89056, upload-time = "2025-10-06T14:12:13.317Z" }, - { url = "https://files.pythonhosted.org/packages/f9/86/0f0dccb6e59a9e7f122c5afd43568b1d31b8ab7dda5f1b01fb5c7025c9a9/yarl-1.22.0-cp314-cp314t-win_amd64.whl", hash = "sha256:9fb17ea16e972c63d25d4a97f016d235c78dd2344820eb35bc034bc32012ee27", size = 96292, upload-time = "2025-10-06T14:12:15.398Z" }, - { url = "https://files.pythonhosted.org/packages/48/b7/503c98092fb3b344a179579f55814b613c1fbb1c23b3ec14a7b008a66a6e/yarl-1.22.0-cp314-cp314t-win_arm64.whl", hash = "sha256:9f6d73c1436b934e3f01df1e1b21ff765cd1d28c77dfb9ace207f746d4610ee1", size = 85171, upload-time = "2025-10-06T14:12:16.935Z" }, - { url = "https://files.pythonhosted.org/packages/73/ae/b48f95715333080afb75a4504487cbe142cae1268afc482d06692d605ae6/yarl-1.22.0-py3-none-any.whl", hash = "sha256:1380560bdba02b6b6c90de54133c81c9f2a453dee9912fe58c1dcced1edb7cff", size = 46814, upload-time = "2025-10-06T14:12:53.872Z" }, -]