Merge remote-tracking branch 'origin/dev' into feat/cb2ui-robot-connections

This commit is contained in:
Björn Otgaar
2025-10-28 11:09:23 +01:00
14 changed files with 241 additions and 71 deletions

10
.pre-commit-config.yaml Normal file
View File

@@ -0,0 +1,10 @@
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.14.2
hooks:
# Run the linter.
- id: ruff-check
args: [ --fix ]
# Run the formatter.
- id: ruff-format

View File

@@ -42,3 +42,9 @@ If your commit fails its either:
branch name != <type>/description-of-branch , branch name != <type>/description-of-branch ,
commit name != <type>: description of the commit. commit name != <type>: description of the commit.
<ref>: N25B-Num's <ref>: N25B-Num's
To add automatic linting and formatting, run:
```shell
uv run pre-commit install
```

View File

@@ -24,6 +24,11 @@ dependencies = [
] ]
[dependency-groups] [dependency-groups]
dev = [
"pre-commit>=4.3.0",
"ruff>=0.14.2",
"ruff-format>=0.3.0",
]
test = [ test = [
"pytest>=8.4.2", "pytest>=8.4.2",
"pytest-asyncio>=1.2.0", "pytest-asyncio>=1.2.0",
@@ -33,3 +38,21 @@ test = [
[tool.pytest.ini_options] [tool.pytest.ini_options]
pythonpath = ["src"] pythonpath = ["src"]
[tool.ruff]
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)
]
ignore = [
"E226", # spaces around operators
"E701", # multiple statements on a single line
]

View File

@@ -5,6 +5,7 @@ from spade_bdi.bdi import BDIAgent
from control_backend.agents.bdi.behaviours.belief_setter import BeliefSetter from control_backend.agents.bdi.behaviours.belief_setter import BeliefSetter
class BDICoreAgent(BDIAgent): class BDICoreAgent(BDIAgent):
""" """
This is the Brain agent that does the belief inference with AgentSpeak. This is the Brain agent that does the belief inference with AgentSpeak.
@@ -12,6 +13,7 @@ class BDICoreAgent(BDIAgent):
This class contains all the actions that can be called from AgentSpeak plans. This class contains all the actions that can be called from AgentSpeak plans.
It has the BeliefSetter behaviour. It has the BeliefSetter behaviour.
""" """
logger = logging.getLogger("BDI Core") logger = logging.getLogger("BDI Core")
async def setup(self): async def setup(self):
@@ -31,5 +33,3 @@ class BDICoreAgent(BDIAgent):
def _send_to_llm(self, message) -> str: def _send_to_llm(self, message) -> str:
"""TODO: implement""" """TODO: implement"""
return f"This is a reply to {message}" return f"This is a reply to {message}"

View File

@@ -8,12 +8,14 @@ from spade_bdi.bdi import BDIAgent
from control_backend.core.config import settings from control_backend.core.config import settings
class BeliefSetter(CyclicBehaviour): class BeliefSetter(CyclicBehaviour):
""" """
This is the behaviour that the BDI agent runs. This is the behaviour that the BDI agent runs. This behaviour waits for incoming
This behaviour waits for incoming message and processes it based on sender. message and processes it based on sender. Currently, it only waits for messages
Currently, t only waits for messages containing beliefs from Belief Collector and adds these to its KB. containing beliefs from BeliefCollector and adds these to its KB.
""" """
agent: BDIAgent agent: BDIAgent
logger = logging.getLogger("BDI/Belief Setter") logger = logging.getLogger("BDI/Belief Setter")
@@ -36,7 +38,8 @@ class BeliefSetter(CyclicBehaviour):
pass pass
def _process_belief_message(self, message: Message): def _process_belief_message(self, message: Message):
if not message.body: return if not message.body:
return
match message.thread: match message.thread:
case "beliefs": case "beliefs":
@@ -48,7 +51,6 @@ class BeliefSetter(CyclicBehaviour):
case _: case _:
pass pass
def _set_beliefs(self, beliefs: dict[str, list[list[str]]]): def _set_beliefs(self, beliefs: dict[str, list[list[str]]]):
if self.agent.bdi is None: if self.agent.bdi is None:
self.logger.warning("Cannot set beliefs, since agent's BDI is not yet initialized.") self.logger.warning("Cannot set beliefs, since agent's BDI is not yet initialized.")

View File

@@ -1,6 +1,6 @@
from fastapi import APIRouter, Request
import logging import logging
from fastapi import APIRouter, Request
from zmq import Socket from zmq import Socket
from control_backend.schemas.message import Message from control_backend.schemas.message import Message
@@ -9,6 +9,7 @@ logger = logging.getLogger(__name__)
router = APIRouter() router = APIRouter()
@router.post("/message", status_code=202) @router.post("/message", status_code=202)
async def receive_message(message: Message, request: Request): async def receive_message(message: Message, request: Request):
logger.info("Received message: %s", message.message) logger.info("Received message: %s", message.message)

View File

@@ -2,6 +2,7 @@ from fastapi import APIRouter, Request
router = APIRouter() router = APIRouter()
# TODO: implement # TODO: implement
@router.get("/sse") @router.get("/sse")
async def sse(request: Request): async def sse(request: Request):

View File

@@ -4,10 +4,7 @@ from control_backend.api.v1.endpoints import message, sse, command
api_router = APIRouter() api_router = APIRouter()
api_router.include_router( api_router.include_router(message.router, tags=["Messages"])
message.router,
tags=["Messages"]
)
api_router.include_router( api_router.include_router(
sse.router, sse.router,

View File

@@ -1,10 +1,11 @@
from re import L
from pydantic import BaseModel from pydantic import BaseModel
from pydantic_settings import BaseSettings, SettingsConfigDict from pydantic_settings import BaseSettings, SettingsConfigDict
class ZMQSettings(BaseModel): class ZMQSettings(BaseModel):
internal_comm_address: str = "tcp://localhost:5560" internal_comm_address: str = "tcp://localhost:5560"
class AgentSettings(BaseModel): class AgentSettings(BaseModel):
host: str = "localhost" host: str = "localhost"
bdi_core_agent_name: str = "bdi_core" bdi_core_agent_name: str = "bdi_core"
@@ -25,4 +26,5 @@ class Settings(BaseSettings):
model_config = SettingsConfigDict(env_file=".env") model_config = SettingsConfigDict(env_file=".env")
settings = Settings() settings = Settings()

View File

@@ -1,26 +1,24 @@
# Standard library imports # Standard library imports
import asyncio
import json
# External imports # External imports
import contextlib import contextlib
import logging
import zmq
from fastapi import FastAPI from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
import logging
from spade.agent import Agent, Message
from spade.behaviour import OneShotBehaviour
import zmq
# Internal imports # Internal imports
from control_backend.agents.ri_communication_agent import RICommunicationAgent from control_backend.agents.ri_communication_agent import RICommunicationAgent
from control_backend.agents.bdi.bdi_core import BDICoreAgent from control_backend.agents.bdi.bdi_core import BDICoreAgent
from control_backend.api.v1.router import api_router from control_backend.api.v1.router import api_router
from control_backend.core.config import AgentSettings, settings from control_backend.core.config import settings
from control_backend.core.zmq_context import context from control_backend.core.zmq_context import context
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
logging.basicConfig(level=logging.DEBUG) logging.basicConfig(level=logging.DEBUG)
@contextlib.asynccontextmanager @contextlib.asynccontextmanager
async def lifespan(app: FastAPI): async def lifespan(app: FastAPI):
logger.info("%s starting up.", app.title) logger.info("%s starting up.", app.title)
@@ -49,17 +47,19 @@ async def lifespan(app: FastAPI):
logger.info("%s shutting down.", app.title) logger.info("%s shutting down.", app.title)
# if __name__ == "__main__": # if __name__ == "__main__":
app = FastAPI(title=settings.app_title, lifespan=lifespan) app = FastAPI(title=settings.app_title, lifespan=lifespan)
# This middleware allows other origins to communicate with us # This middleware allows other origins to communicate with us
app.add_middleware( app.add_middleware(
CORSMiddleware, # https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CORS CORSMiddleware, # https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CORS
allow_origins=[settings.ui_url], # address of our UI application allow_origins=[settings.ui_url], # address of our UI application
allow_methods=["*"], # GET, POST, etc. allow_methods=["*"], # GET, POST, etc.
) )
app.include_router(api_router, prefix="") # TODO: make prefix /api/v1 app.include_router(api_router, prefix="") # TODO: make prefix /api/v1
@app.get("/") @app.get("/")
async def root(): async def root():

View File

@@ -1,4 +1,5 @@
from pydantic import BaseModel from pydantic import BaseModel
class Message(BaseModel): class Message(BaseModel):
message: str message: str

View File

@@ -1,6 +1,6 @@
import json import json
import logging import logging
from unittest.mock import MagicMock, AsyncMock, call from unittest.mock import AsyncMock, MagicMock, call
import pytest import pytest
@@ -26,7 +26,7 @@ def belief_setter(mock_agent, mocker):
# Patch the settings to use a predictable agent name # Patch the settings to use a predictable agent name
mocker.patch( mocker.patch(
"control_backend.agents.bdi.behaviours.belief_setter.settings.agent_settings.belief_collector_agent_name", "control_backend.agents.bdi.behaviours.belief_setter.settings.agent_settings.belief_collector_agent_name",
COLLECTOR_AGENT_NAME COLLECTOR_AGENT_NAME,
) )
# Patch asyncio.sleep to prevent tests from actually waiting # Patch asyncio.sleep to prevent tests from actually waiting
mocker.patch("asyncio.sleep", return_value=None) mocker.patch("asyncio.sleep", return_value=None)
@@ -69,7 +69,7 @@ async def test_run_message_received(belief_setter, mocker):
Test that when a message is received, _process_message is called. Test that when a message is received, _process_message is called.
""" """
# Arrange # Arrange
msg = MagicMock(); msg = MagicMock()
belief_setter.receive.return_value = msg belief_setter.receive.return_value = msg
mocker.patch.object(belief_setter, "_process_message") mocker.patch.object(belief_setter, "_process_message")
@@ -115,14 +115,9 @@ def test_process_belief_message_valid_json(belief_setter, mocker):
Test processing a valid belief message with correct thread and JSON body. Test processing a valid belief message with correct thread and JSON body.
""" """
# Arrange # Arrange
beliefs_payload = { beliefs_payload = {"is_hot": [["kitchen"]], "is_clean": [["kitchen"], ["bathroom"]]}
"is_hot": [["kitchen"]],
"is_clean": [["kitchen"], ["bathroom"]]
}
msg = create_mock_message( msg = create_mock_message(
sender_node=COLLECTOR_AGENT_JID, sender_node=COLLECTOR_AGENT_JID, body=json.dumps(beliefs_payload), thread="beliefs"
body=json.dumps(beliefs_payload),
thread="beliefs"
) )
mock_set_beliefs = mocker.patch.object(belief_setter, "_set_beliefs") mock_set_beliefs = mocker.patch.object(belief_setter, "_set_beliefs")
@@ -139,9 +134,7 @@ def test_process_belief_message_invalid_json(belief_setter, mocker, caplog):
""" """
# Arrange # Arrange
msg = create_mock_message( msg = create_mock_message(
sender_node=COLLECTOR_AGENT_JID, sender_node=COLLECTOR_AGENT_JID, body="this is not a json string", thread="beliefs"
body="this is not a json string",
thread="beliefs"
) )
mock_set_beliefs = mocker.patch.object(belief_setter, "_set_beliefs") mock_set_beliefs = mocker.patch.object(belief_setter, "_set_beliefs")
@@ -160,9 +153,7 @@ def test_process_belief_message_wrong_thread(belief_setter, mocker):
""" """
# Arrange # Arrange
msg = create_mock_message( msg = create_mock_message(
sender_node=COLLECTOR_AGENT_JID, sender_node=COLLECTOR_AGENT_JID, body='{"some": "data"}', thread="not_beliefs"
body='{"some": "data"}',
thread="not_beliefs"
) )
mock_set_beliefs = mocker.patch.object(belief_setter, "_set_beliefs") mock_set_beliefs = mocker.patch.object(belief_setter, "_set_beliefs")
@@ -172,16 +163,13 @@ def test_process_belief_message_wrong_thread(belief_setter, mocker):
# Assert # Assert
mock_set_beliefs.assert_not_called() mock_set_beliefs.assert_not_called()
def test_process_belief_message_empty_body(belief_setter, mocker): def test_process_belief_message_empty_body(belief_setter, mocker):
""" """
Test that a message with an empty body is ignored. Test that a message with an empty body is ignored.
""" """
# Arrange # Arrange
msg = create_mock_message( msg = create_mock_message(sender_node=COLLECTOR_AGENT_JID, body="", thread="beliefs")
sender_node=COLLECTOR_AGENT_JID,
body="",
thread="beliefs"
)
mock_set_beliefs = mocker.patch.object(belief_setter, "_set_beliefs") mock_set_beliefs = mocker.patch.object(belief_setter, "_set_beliefs")
# Act # Act
@@ -198,7 +186,7 @@ def test_set_beliefs_success(belief_setter, mock_agent, caplog):
# Arrange # Arrange
beliefs_to_set = { beliefs_to_set = {
"is_hot": [["kitchen"], ["living_room"]], "is_hot": [["kitchen"], ["living_room"]],
"door_is": [["front_door", "closed"]] "door_is": [["front_door", "closed"]],
} }
# Act # Act
@@ -209,7 +197,7 @@ def test_set_beliefs_success(belief_setter, mock_agent, caplog):
expected_calls = [ expected_calls = [
call("is_hot", "kitchen"), call("is_hot", "kitchen"),
call("is_hot", "living_room"), call("is_hot", "living_room"),
call("door_is", "front_door", "closed") call("door_is", "front_door", "closed"),
] ]
mock_agent.bdi.set_belief.assert_has_calls(expected_calls, any_order=True) mock_agent.bdi.set_belief.assert_has_calls(expected_calls, any_order=True)
assert mock_agent.bdi.set_belief.call_count == 3 assert mock_agent.bdi.set_belief.call_count == 3

View File

@@ -1,8 +1,6 @@
import sys import sys
from unittest.mock import MagicMock from unittest.mock import MagicMock
import sys
from unittest.mock import MagicMock
def pytest_configure(config): def pytest_configure(config):
""" """
@@ -17,14 +15,14 @@ def pytest_configure(config):
mock_spade_bdi.bdi = MagicMock() mock_spade_bdi.bdi = MagicMock()
mock_spade.agent.Message = MagicMock() mock_spade.agent.Message = MagicMock()
mock_spade.behaviour.CyclicBehaviour = type('CyclicBehaviour', (object,), {}) mock_spade.behaviour.CyclicBehaviour = type("CyclicBehaviour", (object,), {})
mock_spade_bdi.bdi.BDIAgent = type('BDIAgent', (object,), {}) mock_spade_bdi.bdi.BDIAgent = type("BDIAgent", (object,), {})
sys.modules['spade'] = mock_spade sys.modules["spade"] = mock_spade
sys.modules['spade.agent'] = mock_spade.agent sys.modules["spade.agent"] = mock_spade.agent
sys.modules['spade.behaviour'] = mock_spade.behaviour sys.modules["spade.behaviour"] = mock_spade.behaviour
sys.modules['spade_bdi'] = mock_spade_bdi sys.modules["spade_bdi"] = mock_spade_bdi
sys.modules['spade_bdi.bdi'] = mock_spade_bdi.bdi sys.modules["spade_bdi.bdi"] = mock_spade_bdi.bdi
# --- Mock the config module to prevent Pydantic ImportError --- # --- Mock the config module to prevent Pydantic ImportError ---
mock_config_module = MagicMock() mock_config_module = MagicMock()
@@ -34,4 +32,4 @@ def pytest_configure(config):
# configure it later in our tests using mocker.patch. # configure it later in our tests using mocker.patch.
mock_config_module.settings = MagicMock() mock_config_module.settings = MagicMock()
sys.modules['control_backend.core.config'] = mock_config_module sys.modules["control_backend.core.config"] = mock_config_module

141
uv.lock generated
View File

@@ -240,6 +240,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" },
] ]
[[package]]
name = "cfgv"
version = "3.4.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" }
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" },
]
[[package]] [[package]]
name = "charset-normalizer" name = "charset-normalizer"
version = "3.4.3" version = "3.4.3"
@@ -394,6 +403,15 @@ wheels = [
{ 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" }, { 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"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/96/8e/709914eb2b5749865801041647dc7f4e6d00b549cfe88b65ca192995f07c/distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d", size = 614605, upload-time = "2025-07-17T16:52:00.465Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047, upload-time = "2025-07-17T16:51:58.613Z" },
]
[[package]] [[package]]
name = "dnspython" name = "dnspython"
version = "2.8.0" version = "2.8.0"
@@ -701,6 +719,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/f0/0f/310fb31e39e2d734ccaa2c0fb981ee41f7bd5056ce9bc29b2248bd569169/humanfriendly-10.0-py2.py3-none-any.whl", hash = "sha256:1697e1a8a8f550fd43c2865cd84542fc175a61dcb779b6fee18cf6b6ccba1477", size = 86794, upload-time = "2021-09-17T21:40:39.897Z" }, { url = "https://files.pythonhosted.org/packages/f0/0f/310fb31e39e2d734ccaa2c0fb981ee41f7bd5056ce9bc29b2248bd569169/humanfriendly-10.0-py2.py3-none-any.whl", hash = "sha256:1697e1a8a8f550fd43c2865cd84542fc175a61dcb779b6fee18cf6b6ccba1477", size = 86794, upload-time = "2021-09-17T21:40:39.897Z" },
] ]
[[package]]
name = "identify"
version = "2.6.15"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/ff/e7/685de97986c916a6d93b3876139e00eef26ad5bbbd61925d670ae8013449/identify-2.6.15.tar.gz", hash = "sha256:e4f4864b96c6557ef2a1e1c951771838f4edc9df3a72ec7118b338801b11c7bf", size = 99311, upload-time = "2025-10-02T17:43:40.631Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/0f/1c/e5fd8f973d4f375adb21565739498e2e9a1e54c858a97b9a8ccfdc81da9b/identify-2.6.15-py2.py3-none-any.whl", hash = "sha256:1181ef7608e00704db228516541eb83a88a9f94433a8c80bb9b5bd54b1d81757", size = 99183, upload-time = "2025-10-02T17:43:39.137Z" },
]
[[package]] [[package]]
name = "idna" name = "idna"
version = "3.10" version = "3.10"
@@ -1014,6 +1041,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/eb/8d/776adee7bbf76365fdd7f2552710282c79a4ead5d2a46408c9043a2b70ba/networkx-3.5-py3-none-any.whl", hash = "sha256:0030d386a9a06dee3565298b4a734b68589749a544acbb6c412dc9e2489ec6ec", size = 2034406, upload-time = "2025-05-29T11:35:04.961Z" }, { url = "https://files.pythonhosted.org/packages/eb/8d/776adee7bbf76365fdd7f2552710282c79a4ead5d2a46408c9043a2b70ba/networkx-3.5-py3-none-any.whl", hash = "sha256:0030d386a9a06dee3565298b4a734b68589749a544acbb6c412dc9e2489ec6ec", size = 2034406, upload-time = "2025-05-29T11:35:04.961Z" },
] ]
[[package]]
name = "nodeenv"
version = "1.9.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437, upload-time = "2024-06-04T18:44:11.171Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314, upload-time = "2024-06-04T18:44:08.352Z" },
]
[[package]] [[package]]
name = "numba" name = "numba"
version = "0.62.1" version = "0.62.1"
@@ -1313,6 +1349,11 @@ dependencies = [
] ]
[package.dev-dependencies] [package.dev-dependencies]
dev = [
{ name = "pre-commit" },
{ name = "ruff" },
{ name = "ruff-format" },
]
test = [ test = [
{ name = "pytest" }, { name = "pytest" },
{ name = "pytest-asyncio" }, { name = "pytest-asyncio" },
@@ -1341,6 +1382,11 @@ requires-dist = [
] ]
[package.metadata.requires-dev] [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" },
]
test = [ test = [
{ name = "pytest", specifier = ">=8.4.2" }, { name = "pytest", specifier = ">=8.4.2" },
{ name = "pytest-asyncio", specifier = ">=1.2.0" }, { name = "pytest-asyncio", specifier = ">=1.2.0" },
@@ -1348,6 +1394,15 @@ test = [
{ name = "pytest-mock", specifier = ">=3.15.1" }, { name = "pytest-mock", specifier = ">=3.15.1" },
] ]
[[package]]
name = "platformdirs"
version = "4.5.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/61/33/9611380c2bdb1225fdef633e2a9610622310fed35ab11dac9620972ee088/platformdirs-4.5.0.tar.gz", hash = "sha256:70ddccdd7c99fc5942e9fc25636a8b34d04c24b335100223152c2803e4063312", size = 21632, upload-time = "2025-10-08T17:44:48.791Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/73/cb/ac7874b3e5d58441674fb70742e6c374b28b0c7cb988d37d991cde47166c/platformdirs-4.5.0-py3-none-any.whl", hash = "sha256:e578a81bb873cbb89a41fcc904c7ef523cc18284b7e3b3ccf06aca1403b7ebd3", size = 18651, upload-time = "2025-10-08T17:44:47.223Z" },
]
[[package]] [[package]]
name = "pluggy" name = "pluggy"
version = "1.6.0" version = "1.6.0"
@@ -1358,6 +1413,22 @@ wheels = [
] ]
[[package]]
name = "pre-commit"
version = "4.3.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cfgv" },
{ name = "identify" },
{ name = "nodeenv" },
{ 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" }
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]] [[package]]
name = "propcache" name = "propcache"
version = "0.4.0" version = "0.4.0"
@@ -1976,6 +2047,62 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/1c/63/0d7df1237c6353d1a85d8a0bc1797ac766c68e8bc6fbca241db74124eb61/rignore-0.7.0-cp314-cp314-win_amd64.whl", hash = "sha256:2401637dc8ab074f5e642295f8225d2572db395ae504ffc272a8d21e9fe77b2c", size = 717404, upload-time = "2025-10-02T13:26:29.936Z" }, { url = "https://files.pythonhosted.org/packages/1c/63/0d7df1237c6353d1a85d8a0bc1797ac766c68e8bc6fbca241db74124eb61/rignore-0.7.0-cp314-cp314-win_amd64.whl", hash = "sha256:2401637dc8ab074f5e642295f8225d2572db395ae504ffc272a8d21e9fe77b2c", size = 717404, upload-time = "2025-10-02T13:26:29.936Z" },
] ]
[[package]]
name = "ruff"
version = "0.14.2"
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" }
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" },
]
[[package]]
name = "ruff-format"
version = "0.3.0"
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" }
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" },
]
[[package]] [[package]]
name = "scipy" name = "scipy"
version = "1.16.2" version = "1.16.2"
@@ -2434,6 +2561,20 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/63/9a/0962b05b308494e3202d3f794a6e85abe471fe3cafdbcf95c2e8c713aabd/uvloop-0.21.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a5c39f217ab3c663dc699c04cbd50c13813e31d917642d459fdcec07555cc553", size = 4660018, upload-time = "2024-10-14T23:38:10.888Z" }, { url = "https://files.pythonhosted.org/packages/63/9a/0962b05b308494e3202d3f794a6e85abe471fe3cafdbcf95c2e8c713aabd/uvloop-0.21.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a5c39f217ab3c663dc699c04cbd50c13813e31d917642d459fdcec07555cc553", size = 4660018, upload-time = "2024-10-14T23:38:10.888Z" },
] ]
[[package]]
name = "virtualenv"
version = "20.35.3"
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" }
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" },
]
[[package]] [[package]]
name = "watchfiles" name = "watchfiles"
version = "1.1.0" version = "1.1.0"