Fix style and add linting/formatting pre-commits #9

Merged
k.marinus merged 2 commits from style/fix-style into dev 2025-10-28 09:32:11 +00:00
14 changed files with 247 additions and 75 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 ,
commit name != <type>: description of the commit.
<ref>: N25B-Num's
To add automatic linting and formatting, run:
```shell
uv run pre-commit install
```

View File

@@ -20,6 +20,11 @@ dependencies = [
]
[dependency-groups]
dev = [
"pre-commit>=4.3.0",
"ruff>=0.14.2",
"ruff-format>=0.3.0",
]
test = [
"pytest>=8.4.2",
"pytest-asyncio>=1.2.0",
@@ -29,3 +34,21 @@ test = [
[tool.pytest.ini_options]
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
class BDICoreAgent(BDIAgent):
"""
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.
It has the BeliefSetter behaviour.
"""
logger = logging.getLogger("BDI Core")
async def setup(self):
@@ -31,5 +33,3 @@ class BDICoreAgent(BDIAgent):
def _send_to_llm(self, message) -> str:
"""TODO: implement"""
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
class BeliefSetter(CyclicBehaviour):
"""
This is the behaviour that the BDI agent runs.
This behaviour waits for incoming message and processes it based on sender.
Currently, t only waits for messages containing beliefs from Belief Collector and adds these to its KB.
This is the behaviour that the BDI agent runs. This behaviour waits for incoming
message and processes it based on sender. Currently, it only waits for messages
containing beliefs from BeliefCollector and adds these to its KB.
"""
agent: BDIAgent
logger = logging.getLogger("BDI/Belief Setter")
@@ -36,7 +38,8 @@ class BeliefSetter(CyclicBehaviour):
pass
def _process_belief_message(self, message: Message):
if not message.body: return
if not message.body:
return
match message.thread:
case "beliefs":
@@ -48,7 +51,6 @@ class BeliefSetter(CyclicBehaviour):
case _:
pass
def _set_beliefs(self, beliefs: dict[str, list[list[str]]]):
if self.agent.bdi is None:
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
from fastapi import APIRouter, Request
from zmq import Socket
from control_backend.schemas.message import Message
@@ -9,6 +9,7 @@ logger = logging.getLogger(__name__)
router = APIRouter()
@router.post("/message", status_code=202)
async def receive_message(message: Message, request: Request):
logger.info("Received message: %s", message.message)

View File

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

View File

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

View File

@@ -1,16 +1,18 @@
from re import L
from pydantic import BaseModel
from pydantic_settings import BaseSettings, SettingsConfigDict
class ZMQSettings(BaseModel):
internal_comm_address: str = "tcp://localhost:5560"
class AgentSettings(BaseModel):
host: str = "localhost"
bdi_core_agent_name: str = "bdi_core"
belief_collector_agent_name: str = "belief_collector"
test_agent_name: str = "test_agent"
class Settings(BaseSettings):
app_title: str = "PepperPlus"
@@ -22,4 +24,5 @@ class Settings(BaseSettings):
model_config = SettingsConfigDict(env_file=".env")
settings = Settings()

View File

@@ -1,25 +1,23 @@
# Standard library imports
import asyncio
import json
# External imports
import contextlib
import logging
import zmq
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
import logging
from spade.agent import Agent, Message
from spade.behaviour import OneShotBehaviour
import zmq
# Internal imports
from control_backend.agents.bdi.bdi_core import BDICoreAgent
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
logger = logging.getLogger(__name__)
logging.basicConfig(level=logging.DEBUG)
@contextlib.asynccontextmanager
async def lifespan(app: FastAPI):
logger.info("%s starting up.", app.title)
@@ -32,13 +30,18 @@ async def lifespan(app: FastAPI):
logger.info("Internal publishing socket bound to %s", internal_comm_socket)
# Initiate agents
bdi_core = BDICoreAgent(settings.agent_settings.bdi_core_agent_name + '@' + settings.agent_settings.host, settings.agent_settings.bdi_core_agent_name, "src/control_backend/agents/bdi/rules.asl")
bdi_core = BDICoreAgent(
settings.agent_settings.bdi_core_agent_name + "@" + settings.agent_settings.host,
settings.agent_settings.bdi_core_agent_name,
"src/control_backend/agents/bdi/rules.asl",
)
await bdi_core.start()
yield
logger.info("%s shutting down.", app.title)
# if __name__ == "__main__":
app = FastAPI(title=settings.app_title, lifespan=lifespan)
@@ -51,6 +54,7 @@ app.add_middleware(
app.include_router(api_router, prefix="") # TODO: make prefix /api/v1
@app.get("/")
async def root():
return {"status": "ok"}

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
import json
import logging
from unittest.mock import MagicMock, AsyncMock, call
from unittest.mock import AsyncMock, MagicMock, call
import pytest
@@ -26,7 +26,7 @@ def belief_setter(mock_agent, mocker):
# Patch the settings to use a predictable agent name
mocker.patch(
"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
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.
"""
# Arrange
msg = MagicMock();
msg = MagicMock()
belief_setter.receive.return_value = msg
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.
"""
# Arrange
beliefs_payload = {
"is_hot": [["kitchen"]],
"is_clean": [["kitchen"], ["bathroom"]]
}
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"
sender_node=COLLECTOR_AGENT_JID, body=json.dumps(beliefs_payload), thread="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
msg = create_mock_message(
sender_node=COLLECTOR_AGENT_JID,
body="this is not a json string",
thread="beliefs"
sender_node=COLLECTOR_AGENT_JID, body="this is not a json string", thread="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
msg = create_mock_message(
sender_node=COLLECTOR_AGENT_JID,
body='{"some": "data"}',
thread="not_beliefs"
sender_node=COLLECTOR_AGENT_JID, body='{"some": "data"}', thread="not_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
mock_set_beliefs.assert_not_called()
def test_process_belief_message_empty_body(belief_setter, mocker):
"""
Test that a message with an empty body is ignored.
"""
# Arrange
msg = create_mock_message(
sender_node=COLLECTOR_AGENT_JID,
body="",
thread="beliefs"
)
msg = create_mock_message(sender_node=COLLECTOR_AGENT_JID, body="", thread="beliefs")
mock_set_beliefs = mocker.patch.object(belief_setter, "_set_beliefs")
# Act
@@ -198,7 +186,7 @@ def test_set_beliefs_success(belief_setter, mock_agent, caplog):
# Arrange
beliefs_to_set = {
"is_hot": [["kitchen"], ["living_room"]],
"door_is": [["front_door", "closed"]]
"door_is": [["front_door", "closed"]],
}
# Act
@@ -209,7 +197,7 @@ def test_set_beliefs_success(belief_setter, mock_agent, caplog):
expected_calls = [
call("is_hot", "kitchen"),
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)
assert mock_agent.bdi.set_belief.call_count == 3

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" },
]
[[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]]
name = "charset-normalizer"
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" },
]
[[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]]
name = "dnspython"
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" },
]
[[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]]
name = "idna"
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" },
]
[[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]]
name = "numba"
version = "0.62.1"
@@ -1309,6 +1345,11 @@ dependencies = [
]
[package.dev-dependencies]
dev = [
{ name = "pre-commit" },
{ name = "ruff" },
{ name = "ruff-format" },
]
test = [
{ name = "pytest" },
{ name = "pytest-asyncio" },
@@ -1333,6 +1374,11 @@ 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" },
]
test = [
{ name = "pytest", specifier = ">=8.4.2" },
{ name = "pytest-asyncio", specifier = ">=1.2.0" },
@@ -1340,6 +1386,15 @@ test = [
{ 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]]
name = "pluggy"
version = "1.6.0"
@@ -1349,6 +1404,22 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
]
[[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]]
name = "propcache"
version = "0.4.0"
@@ -1967,6 +2038,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" },
]
[[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]]
name = "scipy"
version = "1.16.2"
@@ -2425,6 +2552,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" },
]
[[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]]
name = "watchfiles"
version = "1.1.0"