feat: add tests and ci

This commit is contained in:
Clelia (Astra) Bertelli
2025-12-12 14:08:47 +01:00
parent 91d1db564a
commit 9410793849
23 changed files with 773 additions and 74 deletions
+19
View File
@@ -0,0 +1,19 @@
name: Build
on:
pull_request:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install uv
uses: astral-sh/setup-uv@v6
- name: Set up Python
run: uv python install 3.13
- name: Build package
run: make build
+24
View File
@@ -0,0 +1,24 @@
name: Linting
on:
pull_request:
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install uv
uses: astral-sh/setup-uv@v6
- name: Set up Python
run: uv python install 3.12
- name: Run formatter
shell: bash
run: make format-check
- name: Run linter
shell: bash
run: make lint
+25
View File
@@ -0,0 +1,25 @@
name: CI Tests - Pull Request
on:
pull_request:
jobs:
testing_pr:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Install uv
uses: astral-sh/setup-uv@v6
with:
python-version: ${{ matrix.python-version }}
enable-cache: true
- name: Run Tests on Main Package
run: make test
+21
View File
@@ -0,0 +1,21 @@
name: Typecheck
on:
pull_request:
jobs:
core-typecheck:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Install uv
uses: astral-sh/setup-uv@v6
- name: Set up Python
run: uv python install
- name: Run Mypy
run: make typecheck
+3
View File
@@ -8,3 +8,6 @@ wheels/
# Virtual environments
.venv
# caches
*_cache/
+12
View File
@@ -0,0 +1,12 @@
---
default_language_version:
python: python3
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.5.0
hooks:
- id: check-merge-conflict
- id: check-symlinks
- id: check-yaml
- id: detect-private-key
+27
View File
@@ -0,0 +1,27 @@
.PHONY: test lint format format-check typecheck
all: test lint format typecheck
test:
$(info ****************** running tests ******************)
uv run pytest tests
lint:
$(info ****************** linting ******************)
uv run pre-commit run -a
format:
$(info ****************** formatting ******************)
uv run ruff format
format-check:
$(info ****************** checking formatting ******************)
uv run ruff format --check
typecheck:
$(info ****************** type checking ******************)
uv run ty check src/fs_explorer/
build:
$(info ****************** building ******************)
uv build
+12 -3
View File
@@ -5,9 +5,9 @@ build-backend = "uv_build"
[project]
name = "fs-explorer"
version = "0.1.0"
description = "Add your description here"
description = "Explore and understand your filesystem better with AI."
readme = "README.md"
requires-python = ">=3.13"
requires-python = ">=3.10"
dependencies = [
"google-genai>=1.55.0",
"llama-index-workflows>=2.11.5",
@@ -17,5 +17,14 @@ dependencies = [
[tool.uv.build-backend]
module-name = "fs_explorer"
[dependency-groups]
dev = [
"pre-commit>=4.5.0",
"pytest>=9.0.2",
"pytest-asyncio>=1.3.0",
"ruff>=0.14.9",
"ty>=0.0.1a33",
]
[project.scripts]
explore = "fs_explorer.main:app"
explore = "fs_explorer.main:app"
+31 -13
View File
@@ -6,9 +6,9 @@ from .models import Action, ActionType, ToolCallAction, Tools
from .fs import read_file, grep_file_content, glob_paths
TOOLS: dict[Tools, Callable] = {
"read": read_file,
"read": read_file,
"grep": grep_file_content,
"glob": glob_paths
"glob": glob_paths,
}
SYSTEM_PROMPT = """
@@ -26,17 +26,26 @@ Every time, you will be asked to take one of the following actions:
Choose the action based on the current situation, inferred from the previous chat history.
"""
class FsExplorerAgent:
def __init__(self, api_key: str | None = None):
if api_key is None:
api_key = os.getenv("GOOGLE_API_KEY")
if api_key is None:
raise ValueError("GOOGLE_API_KEY not found within the current environment: please export it or provide it to the class constructor.")
self._client = GenAIClient(api_key=api_key, http_options=HttpOptions(api_version="v1beta"))
self._chat_history: list[Content] = [Content(role="system", parts=[Part.from_text(text=SYSTEM_PROMPT)])]
raise ValueError(
"GOOGLE_API_KEY not found within the current environment: please export it or provide it to the class constructor."
)
self._client = GenAIClient(
api_key=api_key, http_options=HttpOptions(api_version="v1beta")
)
self._chat_history: list[Content] = [
Content(role="system", parts=[Part.from_text(text=SYSTEM_PROMPT)])
]
def configure_task(self, task: str) -> None:
self._chat_history.append(Content(role="user", parts=[Part.from_text(text=task)]))
self._chat_history.append(
Content(role="user", parts=[Part.from_text(text=task)])
)
async def take_action(self) -> tuple[Action, ActionType] | None:
response = await self._client.aio.models.generate_content(
@@ -45,7 +54,7 @@ class FsExplorerAgent:
config={
"response_mime_type": "application/json",
"response_json_schema": Action.model_json_schema(),
}
},
)
if response.candidates is not None:
if response.candidates[0].content is not None:
@@ -54,14 +63,23 @@ class FsExplorerAgent:
action = Action.model_validate_json(response.text)
if action.to_action_type() == "toolcall":
toolcall = cast(ToolCallAction, action.action)
self.call_tool(tool_name=toolcall.tool_name, tool_input=toolcall.to_fn_args())
self.call_tool(
tool_name=toolcall.tool_name, tool_input=toolcall.to_fn_args()
)
return action, action.to_action_type()
return None
return None
def call_tool(self, tool_name: Tools, tool_input: dict[str, Any]) -> None:
try:
result = TOOLS[tool_name](**tool_input)
except Exception as e:
result = f"An error occurred while calling tool {tool_name} with {tool_input}: {e}"
self._chat_history.append(Content(role="user", parts=[Part.from_text(text=f"Tool result for {tool_name}:\n\n{result}")]))
return None
self._chat_history.append(
Content(
role="user",
parts=[
Part.from_text(text=f"Tool result for {tool_name}:\n\n{result}")
],
)
)
return None
+12 -2
View File
@@ -2,6 +2,7 @@ import os
import re
import glob
def describe_dir_content(directory: str) -> str:
if not os.path.exists(directory) or not os.path.isdir(directory):
return f"No such directory: {directory}"
@@ -12,7 +13,7 @@ def describe_dir_content(directory: str) -> str:
files = []
directories = []
for child in children:
fullpath = os.path.join(child)
fullpath = os.path.join(directory, child)
if os.path.isfile(fullpath):
files.append(fullpath)
else:
@@ -24,11 +25,17 @@ def describe_dir_content(directory: str) -> str:
description += "\nSUBFOLDERS:\n- " + "\n- ".join(directories)
return description
def read_file(file_path: str) -> str:
if not os.path.exists(file_path) or not os.path.isfile(file_path):
return f"No such file: {file_path}"
with open(file_path, "r") as f:
return f.read()
def grep_file_content(file_path: str, pattern: str) -> str:
if not os.path.exists(file_path) or not os.path.isfile(file_path):
return f"No such file: {file_path}"
with open(file_path, "r") as f:
content = f.read()
r = re.compile(pattern=pattern, flags=re.MULTILINE)
@@ -37,8 +44,11 @@ def grep_file_content(file_path: str, pattern: str) -> str:
return f"MATCHES for {pattern} in {file_path}:\n\n- " + "\n- ".join(matches)
return "No matches found"
def glob_paths(directory: str, pattern: str) -> str:
if not os.path.exists(directory) or not os.path.isdir(directory):
return f"No such directory: {directory}"
matches = glob.glob(f"./{directory}/{pattern}")
if matches:
return f"MATCHES for {pattern} in {directory}:\n\n- " + "\n- ".join(matches)
return "No matches found"
return "No matches found"
+21 -4
View File
@@ -11,6 +11,7 @@ from .workflow import workflow, InputEvent, ToolCallEvent, GoDeeperAction
app = Typer()
async def run_workflow(task: str):
console = Console()
handler = workflow.run(start_event=InputEvent(task=task))
@@ -19,13 +20,23 @@ async def run_workflow(task: str):
if isinstance(event, ToolCallEvent):
status.update("Tool calling...")
content = f"Calling tool `{event.tool_name}` with input:\n\n```\n{json.dumps(event.tool_input, indent=2)}\n```\n\nThe tool call is motivated by: {event.reason}"
panel = Panel(Markdown(content), title_align="left", title="Tool Call", border_style="bold yellow")
panel = Panel(
Markdown(content),
title_align="left",
title="Tool Call",
border_style="bold yellow",
)
console.print(panel)
status.update("Working on the next move...")
elif isinstance(event, GoDeeperAction):
status.update("Tool calling...")
content = f"Going to directory: `{event.directory}` because of: {event.reason}"
panel = Panel(Markdown(content), title_align="left", title="Moving within the file system", border_style="bold magenta")
panel = Panel(
Markdown(content),
title_align="left",
title="Moving within the file system",
border_style="bold magenta",
)
console.print(panel)
status.update("Working on the next move...")
result = await handler
@@ -33,11 +44,17 @@ async def run_workflow(task: str):
await asyncio.sleep(0.1)
status.update("Tool calling...")
content = result.final_result
panel = Panel(Markdown(content), title_align="left", title="Final result", border_style="bold green")
panel = Panel(
Markdown(content),
title_align="left",
title="Final result",
border_style="bold green",
)
console.print(panel)
status.stop()
return None
@app.command()
def main(
task: Annotated[
@@ -49,4 +66,4 @@ def main(
),
],
) -> None:
asyncio.run(run_workflow(task))
asyncio.run(run_workflow(task))
+13 -2
View File
@@ -4,21 +4,29 @@ from typing import TypeAlias, Literal, Any
Tools: TypeAlias = Literal["read", "grep", "glob"]
ActionType: TypeAlias = Literal["stop", "godeeper", "toolcall"]
class StopAction(BaseModel):
"""Action that is used when the end goal has been reached"""
final_result: str = Field(description="Final result of the operation")
class GoDeeperAction(BaseModel):
"""Action that is used when it is necessary to go one level deeper in the filesystem"""
directory: str = Field(description="Directory where to go")
class ToolCallArg(BaseModel):
"""Input to the tool call, based on the tool schema"""
parameter_name: str = Field(description="Name of the parameter")
parameter_value: Any = Field(description="Value associated to the parameter")
class ToolCallAction(BaseModel):
"""Action thast is used when it is necessary to call one of the available tools"""
tool_name: Tools = Field(description="Chosen tool")
tool_input: list[ToolCallArg] = Field(description="Input to call the tool with")
@@ -31,7 +39,10 @@ class ToolCallAction(BaseModel):
class Action(BaseModel):
"""Action to take based on the current chat history"""
action: ToolCallAction | GoDeeperAction | StopAction = Field(description="Action specification for the next step")
action: ToolCallAction | GoDeeperAction | StopAction = Field(
description="Action specification for the next step"
)
reason: str = Field(description="Reason for taking this specific action")
def to_action_type(self) -> ActionType:
@@ -40,4 +51,4 @@ class Action(BaseModel):
elif isinstance(self.action, GoDeeperAction):
return "godeeper"
else:
return "stop"
return "stop"
+91 -50
View File
@@ -10,36 +10,50 @@ from .fs import describe_dir_content
AGENT = FsExplorerAgent()
class WorkflowState(BaseModel):
intial_task: str = ""
current_directory: str = "."
class InputEvent(StartEvent):
task: str
class GoDeeperEvent(Event):
directory: str
reason: str
class ToolCallEvent(Event):
tool_name: str
tool_input: dict[str, Any]
reason: str
class ExplorationEndEvent(StopEvent):
final_result: str | None = None
error: str | None = None
def get_agent(*args, **kwargs) -> FsExplorerAgent:
return AGENT
class FsExplorerWorkflow(Workflow):
@step
async def start_exploration(self, ev: InputEvent, ctx: Context[WorkflowState], agent: Annotated[FsExplorerAgent, Resource(get_agent)]) -> ExplorationEndEvent | GoDeeperEvent | ToolCallEvent:
async def start_exploration(
self,
ev: InputEvent,
ctx: Context[WorkflowState],
agent: Annotated[FsExplorerAgent, Resource(get_agent)],
) -> ExplorationEndEvent | GoDeeperEvent | ToolCallEvent:
async with ctx.store.edit_state() as state:
state.intial_task = ev.task
dirdescription = describe_dir_content(".")
agent.configure_task(f"Given that the current directory ('.') looks like this:\n\n```text\n{dirdescription}\n```\n\nAnd that the user is giving you this task: '{ev.task}', what action should you take first?")
agent.configure_task(
f"Given that the current directory ('.') looks like this:\n\n```text\n{dirdescription}\n```\n\nAnd that the user is giving you this task: '{ev.task}', what action should you take first?"
)
result = await agent.take_action()
if result is None:
return ExplorationEndEvent(error="Could not produce action to take")
@@ -52,57 +66,84 @@ class FsExplorerWorkflow(Workflow):
ctx.write_event_to_stream(res)
elif action_type == "toolcall":
toolcall = cast(ToolCallAction, action.action)
res = ToolCallEvent(tool_name=toolcall.tool_name, tool_input=toolcall.to_fn_args(), reason=action.reason)
ctx.write_event_to_stream(res)
else:
stopaction = cast(StopAction, action.action)
res = ExplorationEndEvent(final_result=stopaction.final_result)
return res
@step
async def go_deeper_action(self, ev: GoDeeperEvent, ctx: Context[WorkflowState], agent: Annotated[FsExplorerAgent, Resource(get_agent)]) -> ExplorationEndEvent | ToolCallEvent | GoDeeperEvent:
state = await ctx.store.get_state()
dirdescription = describe_dir_content(state.current_directory)
agent.configure_task(f"Given that the current directory ('{state.current_directory}') looks like this:\n\n```text\n{dirdescription}\n```\n\nAnd that the user is giving you this task: '{state.intial_task}', what action should you take next?")
result = await agent.take_action()
if result is None:
return ExplorationEndEvent(error="Could not produce action to take")
action, action_type = result
if action_type == "godeeper":
godeeper = cast(GoDeeperAction, action.action)
res = GoDeeperEvent(directory=godeeper.directory, reason=action.reason)
async with ctx.store.edit_state() as state:
state.current_directory = godeeper.directory
ctx.write_event_to_stream(res)
elif action_type == "toolcall":
toolcall = cast(ToolCallAction, action.action)
res = ToolCallEvent(tool_name=toolcall.tool_name, tool_input=toolcall.to_fn_args(), reason=action.reason)
ctx.write_event_to_stream(res)
else:
stopaction = cast(StopAction, action.action)
res = ExplorationEndEvent(final_result=stopaction.final_result)
return res
@step
async def tool_call_action(self, ev: ToolCallEvent, ctx: Context[WorkflowState], agent: Annotated[FsExplorerAgent, Resource(get_agent)]) -> ExplorationEndEvent | ToolCallEvent | GoDeeperEvent:
agent.configure_task("Given the result from the tool call you just performed, what action should you take next?")
result = await agent.take_action()
if result is None:
return ExplorationEndEvent(error="Could not produce action to take")
action, action_type = result
if action_type == "godeeper":
godeeper = cast(GoDeeperAction, action.action)
res = GoDeeperEvent(directory=godeeper.directory, reason=action.reason)
async with ctx.store.edit_state() as state:
state.current_directory = godeeper.directory
ctx.write_event_to_stream(res)
elif action_type == "toolcall":
toolcall = cast(ToolCallAction, action.action)
res = ToolCallEvent(tool_name=toolcall.tool_name, tool_input=toolcall.to_fn_args(), reason=action.reason)
res = ToolCallEvent(
tool_name=toolcall.tool_name,
tool_input=toolcall.to_fn_args(),
reason=action.reason,
)
ctx.write_event_to_stream(res)
else:
stopaction = cast(StopAction, action.action)
res = ExplorationEndEvent(final_result=stopaction.final_result)
return res
workflow = FsExplorerWorkflow(timeout=120)
@step
async def go_deeper_action(
self,
ev: GoDeeperEvent,
ctx: Context[WorkflowState],
agent: Annotated[FsExplorerAgent, Resource(get_agent)],
) -> ExplorationEndEvent | ToolCallEvent | GoDeeperEvent:
state = await ctx.store.get_state()
dirdescription = describe_dir_content(state.current_directory)
agent.configure_task(
f"Given that the current directory ('{state.current_directory}') looks like this:\n\n```text\n{dirdescription}\n```\n\nAnd that the user is giving you this task: '{state.intial_task}', what action should you take next?"
)
result = await agent.take_action()
if result is None:
return ExplorationEndEvent(error="Could not produce action to take")
action, action_type = result
if action_type == "godeeper":
godeeper = cast(GoDeeperAction, action.action)
res = GoDeeperEvent(directory=godeeper.directory, reason=action.reason)
async with ctx.store.edit_state() as state:
state.current_directory = godeeper.directory
ctx.write_event_to_stream(res)
elif action_type == "toolcall":
toolcall = cast(ToolCallAction, action.action)
res = ToolCallEvent(
tool_name=toolcall.tool_name,
tool_input=toolcall.to_fn_args(),
reason=action.reason,
)
ctx.write_event_to_stream(res)
else:
stopaction = cast(StopAction, action.action)
res = ExplorationEndEvent(final_result=stopaction.final_result)
return res
@step
async def tool_call_action(
self,
ev: ToolCallEvent,
ctx: Context[WorkflowState],
agent: Annotated[FsExplorerAgent, Resource(get_agent)],
) -> ExplorationEndEvent | ToolCallEvent | GoDeeperEvent:
agent.configure_task(
"Given the result from the tool call you just performed, what action should you take next?"
)
result = await agent.take_action()
if result is None:
return ExplorationEndEvent(error="Could not produce action to take")
action, action_type = result
if action_type == "godeeper":
godeeper = cast(GoDeeperAction, action.action)
res = GoDeeperEvent(directory=godeeper.directory, reason=action.reason)
async with ctx.store.edit_state() as state:
state.current_directory = godeeper.directory
ctx.write_event_to_stream(res)
elif action_type == "toolcall":
toolcall = cast(ToolCallAction, action.action)
res = ToolCallEvent(
tool_name=toolcall.tool_name,
tool_input=toolcall.to_fn_args(),
reason=action.reason,
)
ctx.write_event_to_stream(res)
else:
stopaction = cast(StopAction, action.action)
res = ExplorationEndEvent(final_result=stopaction.final_result)
return res
workflow = FsExplorerWorkflow(timeout=120)
View File
+46
View File
@@ -0,0 +1,46 @@
from google.genai.types import (
HttpOptions,
Content,
GenerateContentResponse,
Candidate,
Part,
)
from fs_explorer.models import StopAction, Action
class MockModels:
async def generate_content(self, *args, **kwargs) -> GenerateContentResponse:
return GenerateContentResponse(
candidates=[
Candidate(
content=Content(
role="assistant",
parts=[
Part.from_text(
text=Action(
action=StopAction(
final_result="this is a final result"
),
reason="I am done",
).model_dump_json()
)
],
)
)
]
)
class MockAio:
@property
def models(self):
return MockModels()
class MockGenAIClient:
def __init__(self, api_key: str, http_options: HttpOptions) -> None:
return None
@property
def aio(self) -> MockAio:
return MockAio()
+49
View File
@@ -0,0 +1,49 @@
import pytest
import os
from unittest.mock import patch
from google.genai import Client as GenAIClient
from google.genai.types import HttpOptions
from fs_explorer.agent import FsExplorerAgent, SYSTEM_PROMPT
from fs_explorer.models import Action, StopAction
from .conftest import MockGenAIClient
@patch.dict(os.environ, {"GOOGLE_API_KEY": "test-api-key"})
def test_agent_init():
agent = FsExplorerAgent()
assert isinstance(agent._client, GenAIClient)
assert len(agent._chat_history) == 1
assert agent._chat_history[0].role == "system"
assert isinstance(agent._chat_history[0].parts, list)
assert agent._chat_history[0].parts[0].text == SYSTEM_PROMPT
del os.environ["GOOGLE_API_KEY"]
with pytest.raises(ValueError):
FsExplorerAgent()
@patch.dict(os.environ, {"GOOGLE_API_KEY": "test-api-key"})
def test_agent_configure_task():
agent = FsExplorerAgent()
agent.configure_task("this is a task")
assert len(agent._chat_history) == 2
assert isinstance(agent._chat_history[1].parts, list)
assert agent._chat_history[1].parts[0].text == "this is a task"
@pytest.mark.asyncio
@patch.dict(os.environ, {"GOOGLE_API_KEY": "test-api-key"})
async def test_agent_take_action():
agent = FsExplorerAgent()
agent.configure_task("this is a task")
agent._client = MockGenAIClient( # type: ignore
os.getenv("GOOGLE_API_KEY", ""), http_options=HttpOptions(api_version="v1beta")
)
result = await agent.take_action()
assert result is not None
action, action_type = result
assert isinstance(action, Action)
assert isinstance(action.action, StopAction)
assert action.action.final_result == "this is a final result"
assert action.reason == "I am done"
assert action_type == "stop"
+32
View File
@@ -0,0 +1,32 @@
import pytest
import os
from workflows.testing import WorkflowTestRunner
SKIP_IF, SKIP_REASON = (
os.getenv("GOOGLE_API_KEY") is None,
"GOOGLE_API_KEY not available",
)
@pytest.mark.asyncio
@pytest.mark.skipif(condition=SKIP_IF, reason=SKIP_REASON)
async def test_e2e() -> None:
from fs_explorer.workflow import (
workflow,
InputEvent,
ExplorationEndEvent,
ToolCallEvent,
GoDeeperEvent,
)
start_event = InputEvent(
task="Starting from the current directory, individuate the python file responsible for file system operations and explain what it does"
)
runner = WorkflowTestRunner(workflow=workflow)
result = await runner.run(start_event=start_event)
assert isinstance(result.result, ExplorationEndEvent)
assert result.result.error is None
assert result.result.final_result is not None
assert len(result.collected) > 1
assert ToolCallEvent in result.event_types or GoDeeperEvent in result.event_types
+47
View File
@@ -0,0 +1,47 @@
from fs_explorer.fs import (
describe_dir_content,
read_file,
grep_file_content,
glob_paths,
)
def test_describe_dir_content() -> None:
description = describe_dir_content("tests/testfiles")
assert (
description
== "Content of tests/testfiles\nFILES:\n- tests/testfiles/file1.txt\n- tests/testfiles/file2.md\nSUBFOLDERS:\n- tests/testfiles/last"
)
description = describe_dir_content("tests/testfile")
assert description == f"No such directory: tests/testfile"
description = describe_dir_content("tests/testfiles/last")
assert (
description
== "Content of tests/testfiles/last\nFILES:\n- tests/testfiles/last/lastfile.txt\nThis folder does not have any sub-folders"
)
def test_read_file() -> None:
content = read_file("tests/testfiles/file1.txt")
assert content.strip() == "this is a test"
content = read_file("tests/testfiles/file2.txt")
assert content.strip() == "No such file: tests/testfiles/file2.txt"
def test_grep_file_content() -> None:
result = grep_file_content("tests/testfiles/file2.md", r"(are|is) a test")
assert result == "MATCHES for (are|is) a test in tests/testfiles/file2.md:\n\n- is"
result = grep_file_content("tests/testfiles/last/lastfile.txt", r"test")
assert result == "No matches found"
result = grep_file_content("tests/testfiles/file2.txt", r"test")
assert result == "No such file: tests/testfiles/file2.txt"
def test_glob_paths() -> None:
result = glob_paths("tests/testfiles", "file?.*")
assert (
result
== "MATCHES for file?.* in tests/testfiles:\n\n- ./tests/testfiles/file1.txt\n- ./tests/testfiles/file2.md"
)
result = glob_paths("tests/testfiles", "test*")
assert result == "No matches found"
+41
View File
@@ -0,0 +1,41 @@
from fs_explorer.models import (
ToolCallAction,
Action,
ToolCallArg,
GoDeeperAction,
StopAction,
)
def test_tool_call_action_to_tool_args() -> None:
tool_call_action = ToolCallAction(
tool_name="glob",
tool_input=[
ToolCallArg(parameter_name="directory", parameter_value="tests/testfiles"),
ToolCallArg(parameter_name="pattern", parameter_value="file?.*"),
],
)
assert tool_call_action.to_fn_args() == {
"directory": "tests/testfiles",
"pattern": "file?.*",
}
def test_action_to_action_type() -> None:
action = Action(
action=ToolCallAction(
tool_name="glob",
tool_input=[
ToolCallArg(
parameter_name="directory", parameter_value="tests/testfiles"
),
ToolCallArg(parameter_name="pattern", parameter_value="file?.*"),
],
),
reason="",
)
assert action.to_action_type() == "toolcall"
action = Action(action=GoDeeperAction(directory="tests/testfiles/last"), reason="")
assert action.to_action_type() == "godeeper"
action = Action(action=StopAction(final_result="hello"), reason="")
assert action.to_action_type() == "stop"
+1
View File
@@ -0,0 +1 @@
this is a test
+1
View File
@@ -0,0 +1 @@
# this is a test!
+1
View File
@@ -0,0 +1 @@
hello
Generated
+244
View File
@@ -41,6 +41,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/70/7d/9bc192684cea499815ff478dfcdc13835ddf401365057044fb721ec6bddb/certifi-2025.11.12-py3-none-any.whl", hash = "sha256:97de8790030bbd5c2d96b7ec782fc2f7820ef8dba6db909ccf95449f2d062d4b", size = 159438, upload-time = "2025-11-12T02:54:49.735Z" },
]
[[package]]
name = "cfgv"
version = "3.5.0"
source = { registry = "https://pypi.org/simple" }
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/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]]
name = "charset-normalizer"
version = "3.4.4"
@@ -115,6 +124,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/84/d0/205d54408c08b13550c733c4b85429e7ead111c7f0014309637425520a9a/deprecated-1.3.1-py2.py3-none-any.whl", hash = "sha256:597bfef186b6f60181535a29fbe44865ce137a5079f295b479886c82729d5f3f", size = 11298, upload-time = "2025-10-30T08:19:00.758Z" },
]
[[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 = "distro"
version = "1.9.0"
@@ -124,6 +142,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" },
]
[[package]]
name = "filelock"
version = "3.20.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/58/46/0028a82567109b5ef6e4d2a1f04a583fb513e6cf9527fcdd09afd817deeb/filelock-3.20.0.tar.gz", hash = "sha256:711e943b4ec6be42e1d4e6690b48dc175c822967466bb31c0c293f34334c13f4", size = 18922, upload-time = "2025-10-08T18:03:50.056Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/76/91/7216b27286936c16f5b4d0c530087e4a54eead683e6b0b73dd0c64844af6/filelock-3.20.0-py3-none-any.whl", hash = "sha256:339b4732ffda5cd79b13f4e2711a31b0365ce445d95d243bb996273d072546a2", size = 16054, upload-time = "2025-10-08T18:03:48.35Z" },
]
[[package]]
name = "fs-explorer"
version = "0.1.0"
@@ -134,6 +161,15 @@ dependencies = [
{ name = "typer" },
]
[package.dev-dependencies]
dev = [
{ name = "pre-commit" },
{ name = "pytest" },
{ name = "pytest-asyncio" },
{ name = "ruff" },
{ name = "ty" },
]
[package.metadata]
requires-dist = [
{ name = "google-genai", specifier = ">=1.55.0" },
@@ -141,6 +177,15 @@ requires-dist = [
{ name = "typer", specifier = ">=0.20.0" },
]
[package.metadata.requires-dev]
dev = [
{ name = "pre-commit", specifier = ">=4.5.0" },
{ name = "pytest", specifier = ">=9.0.2" },
{ name = "pytest-asyncio", specifier = ">=1.3.0" },
{ name = "ruff", specifier = ">=0.14.9" },
{ name = "ty", specifier = ">=0.0.1a33" },
]
[[package]]
name = "google-auth"
version = "2.43.0"
@@ -218,6 +263,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" },
]
[[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.11"
@@ -227,6 +281,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" },
]
[[package]]
name = "iniconfig"
version = "2.3.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
]
[[package]]
name = "llama-index-instrumentation"
version = "0.4.2"
@@ -275,6 +338,58 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" },
]
[[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 = "packaging"
version = "25.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" },
]
[[package]]
name = "platformdirs"
version = "4.5.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/cf/86/0248f086a84f01b37aaec0fa567b397df1a119f73c16f6c7a9aac73ea309/platformdirs-4.5.1.tar.gz", hash = "sha256:61d5cdcc6065745cdd94f0f878977f8de9437be93de97c1c12f853c9c0cdcbda", size = 21715, upload-time = "2025-12-05T13:52:58.638Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/cb/28/3bfe2fa5a7b9c46fe7e13c97bda14c895fb10fa2ebf1d0abb90e0cea7ee1/platformdirs-4.5.1-py3-none-any.whl", hash = "sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31", size = 18731, upload-time = "2025-12-05T13:52:56.823Z" },
]
[[package]]
name = "pluggy"
version = "1.6.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
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.5.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/f4/9b/6a4ffb4ed980519da959e1cf3122fc6cb41211daa58dbae1c73c0e519a37/pre_commit-4.5.0.tar.gz", hash = "sha256:dc5a065e932b19fc1d4c653c6939068fe54325af8e741e74e88db4d28a4dd66b", size = 198428, upload-time = "2025-11-22T21:02:42.304Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/5d/c4/b2d28e9d2edf4f1713eb3c29307f1a63f3d67cf09bdda29715a36a68921a/pre_commit-4.5.0-py2.py3-none-any.whl", hash = "sha256:25e2ce09595174d9c97860a95609f9f852c0614ba602de3561e267547f2335e1", size = 226429, upload-time = "2025-11-22T21:02:40.836Z" },
]
[[package]]
name = "pyasn1"
version = "0.6.1"
@@ -373,6 +488,70 @@ 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 = "pytest"
version = "9.0.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
{ name = "iniconfig" },
{ name = "packaging" },
{ name = "pluggy" },
{ name = "pygments" },
]
sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" },
]
[[package]]
name = "pytest-asyncio"
version = "1.3.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pytest" },
]
sdist = { url = "https://files.pythonhosted.org/packages/90/2c/8af215c0f776415f3590cac4f9086ccefd6fd463befeae41cd4d3f193e5a/pytest_asyncio-1.3.0.tar.gz", hash = "sha256:d7f52f36d231b80ee124cd216ffb19369aa168fc10095013c6b014a34d3ee9e5", size = 50087, upload-time = "2025-11-10T16:07:47.256Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e5/35/f8b19922b6a25bc0880171a2f1a003eaeb93657475193ab516fd87cac9da/pytest_asyncio-1.3.0-py3-none-any.whl", hash = "sha256:611e26147c7f77640e6d0a92a38ed17c3e9848063698d5c93d5aa7aa11cebff5", size = 15075, upload-time = "2025-11-10T16:07:45.537Z" },
]
[[package]]
name = "pyyaml"
version = "6.0.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" },
{ url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" },
{ url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" },
{ url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" },
{ url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" },
{ url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" },
{ url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" },
{ url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" },
{ url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" },
{ url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" },
{ url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" },
{ url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" },
{ url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" },
{ url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" },
{ url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" },
{ url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" },
{ url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" },
{ url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" },
{ url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" },
{ url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" },
{ url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" },
{ url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" },
{ url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" },
{ url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" },
{ url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" },
{ url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" },
{ url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" },
{ url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" },
]
[[package]]
name = "requests"
version = "2.32.5"
@@ -413,6 +592,32 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/64/8d/0133e4eb4beed9e425d9a98ed6e081a55d195481b7632472be1af08d2f6b/rsa-4.9.1-py3-none-any.whl", hash = "sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762", size = 34696, upload-time = "2025-04-16T09:51:17.142Z" },
]
[[package]]
name = "ruff"
version = "0.14.9"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f6/1b/ab712a9d5044435be8e9a2beb17cbfa4c241aa9b5e4413febac2a8b79ef2/ruff-0.14.9.tar.gz", hash = "sha256:35f85b25dd586381c0cc053f48826109384c81c00ad7ef1bd977bfcc28119d5b", size = 5809165, upload-time = "2025-12-11T21:39:47.381Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b8/1c/d1b1bba22cffec02351c78ab9ed4f7d7391876e12720298448b29b7229c1/ruff-0.14.9-py3-none-linux_armv6l.whl", hash = "sha256:f1ec5de1ce150ca6e43691f4a9ef5c04574ad9ca35c8b3b0e18877314aba7e75", size = 13576541, upload-time = "2025-12-11T21:39:14.806Z" },
{ url = "https://files.pythonhosted.org/packages/94/ab/ffe580e6ea1fca67f6337b0af59fc7e683344a43642d2d55d251ff83ceae/ruff-0.14.9-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ed9d7417a299fc6030b4f26333bf1117ed82a61ea91238558c0268c14e00d0c2", size = 13779363, upload-time = "2025-12-11T21:39:20.29Z" },
{ url = "https://files.pythonhosted.org/packages/7d/f8/2be49047f929d6965401855461e697ab185e1a6a683d914c5c19c7962d9e/ruff-0.14.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d5dc3473c3f0e4a1008d0ef1d75cee24a48e254c8bed3a7afdd2b4392657ed2c", size = 12925292, upload-time = "2025-12-11T21:39:38.757Z" },
{ url = "https://files.pythonhosted.org/packages/9e/e9/08840ff5127916bb989c86f18924fd568938b06f58b60e206176f327c0fe/ruff-0.14.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:84bf7c698fc8f3cb8278830fb6b5a47f9bcc1ed8cb4f689b9dd02698fa840697", size = 13362894, upload-time = "2025-12-11T21:39:02.524Z" },
{ url = "https://files.pythonhosted.org/packages/31/1c/5b4e8e7750613ef43390bb58658eaf1d862c0cc3352d139cd718a2cea164/ruff-0.14.9-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:aa733093d1f9d88a5d98988d8834ef5d6f9828d03743bf5e338bf980a19fce27", size = 13311482, upload-time = "2025-12-11T21:39:17.51Z" },
{ url = "https://files.pythonhosted.org/packages/5b/3a/459dce7a8cb35ba1ea3e9c88f19077667a7977234f3b5ab197fad240b404/ruff-0.14.9-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6a1cfb04eda979b20c8c19550c8b5f498df64ff8da151283311ce3199e8b3648", size = 14016100, upload-time = "2025-12-11T21:39:41.948Z" },
{ url = "https://files.pythonhosted.org/packages/a6/31/f064f4ec32524f9956a0890fc6a944e5cf06c63c554e39957d208c0ffc45/ruff-0.14.9-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:1e5cb521e5ccf0008bd74d5595a4580313844a42b9103b7388eca5a12c970743", size = 15477729, upload-time = "2025-12-11T21:39:23.279Z" },
{ url = "https://files.pythonhosted.org/packages/7a/6d/f364252aad36ccd443494bc5f02e41bf677f964b58902a17c0b16c53d890/ruff-0.14.9-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cd429a8926be6bba4befa8cdcf3f4dd2591c413ea5066b1e99155ed245ae42bb", size = 15122386, upload-time = "2025-12-11T21:39:33.125Z" },
{ url = "https://files.pythonhosted.org/packages/20/02/e848787912d16209aba2799a4d5a1775660b6a3d0ab3944a4ccc13e64a02/ruff-0.14.9-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ab208c1b7a492e37caeaf290b1378148f75e13c2225af5d44628b95fd7834273", size = 14497124, upload-time = "2025-12-11T21:38:59.33Z" },
{ url = "https://files.pythonhosted.org/packages/f3/51/0489a6a5595b7760b5dbac0dd82852b510326e7d88d51dbffcd2e07e3ff3/ruff-0.14.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:72034534e5b11e8a593f517b2f2f2b273eb68a30978c6a2d40473ad0aaa4cb4a", size = 14195343, upload-time = "2025-12-11T21:39:44.866Z" },
{ url = "https://files.pythonhosted.org/packages/f6/53/3bb8d2fa73e4c2f80acc65213ee0830fa0c49c6479313f7a68a00f39e208/ruff-0.14.9-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:712ff04f44663f1b90a1195f51525836e3413c8a773574a7b7775554269c30ed", size = 14346425, upload-time = "2025-12-11T21:39:05.927Z" },
{ url = "https://files.pythonhosted.org/packages/ad/04/bdb1d0ab876372da3e983896481760867fc84f969c5c09d428e8f01b557f/ruff-0.14.9-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:a111fee1db6f1d5d5810245295527cda1d367c5aa8f42e0fca9a78ede9b4498b", size = 13258768, upload-time = "2025-12-11T21:39:08.691Z" },
{ url = "https://files.pythonhosted.org/packages/40/d9/8bf8e1e41a311afd2abc8ad12be1b6c6c8b925506d9069b67bb5e9a04af3/ruff-0.14.9-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8769efc71558fecc25eb295ddec7d1030d41a51e9dcf127cbd63ec517f22d567", size = 13326939, upload-time = "2025-12-11T21:39:53.842Z" },
{ url = "https://files.pythonhosted.org/packages/f4/56/a213fa9edb6dd849f1cfbc236206ead10913693c72a67fb7ddc1833bf95d/ruff-0.14.9-py3-none-musllinux_1_2_i686.whl", hash = "sha256:347e3bf16197e8a2de17940cd75fd6491e25c0aa7edf7d61aa03f146a1aa885a", size = 13578888, upload-time = "2025-12-11T21:39:35.988Z" },
{ url = "https://files.pythonhosted.org/packages/33/09/6a4a67ffa4abae6bf44c972a4521337ffce9cbc7808faadede754ef7a79c/ruff-0.14.9-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:7715d14e5bccf5b660f54516558aa94781d3eb0838f8e706fb60e3ff6eff03a8", size = 14314473, upload-time = "2025-12-11T21:39:50.78Z" },
{ url = "https://files.pythonhosted.org/packages/12/0d/15cc82da5d83f27a3c6b04f3a232d61bc8c50d38a6cd8da79228e5f8b8d6/ruff-0.14.9-py3-none-win32.whl", hash = "sha256:df0937f30aaabe83da172adaf8937003ff28172f59ca9f17883b4213783df197", size = 13202651, upload-time = "2025-12-11T21:39:26.628Z" },
{ url = "https://files.pythonhosted.org/packages/32/f7/c78b060388eefe0304d9d42e68fab8cffd049128ec466456cef9b8d4f06f/ruff-0.14.9-py3-none-win_amd64.whl", hash = "sha256:c0b53a10e61df15a42ed711ec0bda0c582039cf6c754c49c020084c55b5b0bc2", size = 14702079, upload-time = "2025-12-11T21:39:11.954Z" },
{ url = "https://files.pythonhosted.org/packages/26/09/7a9520315decd2334afa65ed258fed438f070e31f05a2e43dd480a5e5911/ruff-0.14.9-py3-none-win_arm64.whl", hash = "sha256:8e821c366517a074046d92f0e9213ed1c13dbc5b37a7fc20b07f79b64d62cc84", size = 13744730, upload-time = "2025-12-11T21:39:29.659Z" },
]
[[package]]
name = "shellingham"
version = "1.5.4"
@@ -440,6 +645,31 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/e5/30/643397144bfbfec6f6ef821f36f33e57d35946c44a2352d3c9f0ae847619/tenacity-9.1.2-py3-none-any.whl", hash = "sha256:f77bf36710d8b73a50b2dd155c97b870017ad21afe6ab300326b0371b3b05138", size = 28248, upload-time = "2025-04-02T08:25:07.678Z" },
]
[[package]]
name = "ty"
version = "0.0.1a33"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/60/34/82f76e63277f0a6585ea48a8d373cfee417a73755daa078250af65421c77/ty-0.0.1a33.tar.gz", hash = "sha256:1db139aa7cbc9879e93146c99bf5f1f5273ca608683f71b3a9a75f9f812b729f", size = 4704365, upload-time = "2025-12-09T22:35:19.424Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/99/4c/4aec80e452268432f60f17da3840ffd6fef46394300808d0af32766dc989/ty-0.0.1a33-py3-none-linux_armv6l.whl", hash = "sha256:2126e6b62a50dc807d45f56629668861bac95944c77b4b6b6dc13f629d5a5a7e", size = 9674171, upload-time = "2025-12-09T22:34:59.757Z" },
{ url = "https://files.pythonhosted.org/packages/fe/71/ad51a14e00aa0d7e57533f2a68f0865b240bd197c36b87ddab1dd12a1cdd/ty-0.0.1a33-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:f171a278a242b06c2f99327dacfa9c7f2d0328140f2976a46ca46e18cde2d6e3", size = 9466420, upload-time = "2025-12-09T22:34:54.884Z" },
{ url = "https://files.pythonhosted.org/packages/79/fa/72bf596a977e5d5343893bb1eb4092fdd0f22ed8c0f11427cc2201225bdb/ty-0.0.1a33-py3-none-macosx_11_0_arm64.whl", hash = "sha256:7b4249f030d24deeae7b25949d33832b4a25b5c893d679b32df1042584b9091f", size = 9009208, upload-time = "2025-12-09T22:35:27.871Z" },
{ url = "https://files.pythonhosted.org/packages/c4/0d/0e20c21e4473a6ea7109c252f6c6bbc41f895b18307e507d6c12a636e6b6/ty-0.0.1a33-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:176f56fc7a6ea176b36d397c42b35efebb441f1fa42524a010579d7019ca8b67", size = 9280560, upload-time = "2025-12-09T22:35:24.258Z" },
{ url = "https://files.pythonhosted.org/packages/6e/dd/627a0a3e2a270b7200c5f92cb01382a3f9ac4f072abf5e7eb3be8f2f4267/ty-0.0.1a33-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8ace2379e9c915c4c6d4dfd3737b290ebe2b008c20031233f4a6e9df0758f427", size = 9457161, upload-time = "2025-12-09T22:35:02.394Z" },
{ url = "https://files.pythonhosted.org/packages/ad/5a/974a48b39c885a17471c3f0847165567a77f05beef3b2573984b9b722378/ty-0.0.1a33-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4341a1daa7857b4de3a68658bad7aaa85577a82448182af2c6b412da02b19c14", size = 9873399, upload-time = "2025-12-09T22:34:49.919Z" },
{ url = "https://files.pythonhosted.org/packages/04/0e/8c09a95b91e3ba0d75a6cea69b06b0a070f085de7dd2aabf86d999175f29/ty-0.0.1a33-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:42c45b50b242af5868198131569d9f4ea37f83212a72494b2553e60f385874cc", size = 10487274, upload-time = "2025-12-09T22:34:52.579Z" },
{ url = "https://files.pythonhosted.org/packages/56/37/8d6e898ecf85f67a9bfaaff9c5194d9eaf4d826363a7dab27460eb2d630c/ty-0.0.1a33-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:229b8d927d7815ba4af0b45f1a97766813b62ee97599199900b8ccc1be911284", size = 10244389, upload-time = "2025-12-09T22:35:09.667Z" },
{ url = "https://files.pythonhosted.org/packages/e2/c4/f98a35b12b552d28feb4157334484aa5f472c30944418e23b4a49fad2e40/ty-0.0.1a33-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5018ac5b64865d416b098246da38d2809fdc69e9d86b4e1cf94266e102e7c77f", size = 10224857, upload-time = "2025-12-09T22:35:04.661Z" },
{ url = "https://files.pythonhosted.org/packages/3b/5d/fffb85c5fd7bdffcef212f514b439f229ecaee14e9bf7c199a625819c502/ty-0.0.1a33-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2cd6d6304302ad28e0412d80118a5f63d01af37d3cb39abf33856d348c0819e1", size = 9792377, upload-time = "2025-12-09T22:34:44.634Z" },
{ url = "https://files.pythonhosted.org/packages/6e/ea/0664b0e4a2c286bd880be47121c781befad8077e15cd8a50b9b1f51b8676/ty-0.0.1a33-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:38b75adc050d26a88bbf85d55a4f7633216e455b76e9ee21d6f38640aa040d73", size = 9262018, upload-time = "2025-12-09T22:34:47.274Z" },
{ url = "https://files.pythonhosted.org/packages/89/dd/3d99564d7e649326c98c72b863f0aad771abfc75140413e7b70559ae4850/ty-0.0.1a33-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:efabd881d5b00058c3945b08abbe853b19c93cd0c7148bbcfd27c5d9e6c738f3", size = 9494056, upload-time = "2025-12-09T22:35:21.967Z" },
{ url = "https://files.pythonhosted.org/packages/49/9b/3471118edc5f945e2589c66c27e71b5d9a9efe21c82ced03ea698dbe9a19/ty-0.0.1a33-py3-none-musllinux_1_2_i686.whl", hash = "sha256:ca3b8f84fe661bfb60d1e7665e54dd9c6c84769bff117b00e76ef537473cc59c", size = 9623498, upload-time = "2025-12-09T22:35:07.072Z" },
{ url = "https://files.pythonhosted.org/packages/b8/6d/12dcb22b015a4d3e677f394ab7dc80307f2b59f898ea785ea6bfcef8cffa/ty-0.0.1a33-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:f46ae07e353a54512b64b590eae4d82eb22c3a5f5947cea04f950dc1993f64f1", size = 9904193, upload-time = "2025-12-09T22:34:57.106Z" },
{ url = "https://files.pythonhosted.org/packages/c2/e8/628063386fda2f9182089bfe0c8a27ede0c1a120bef74294008468cd2d7d/ty-0.0.1a33-py3-none-win32.whl", hash = "sha256:9020b8be11a184bbe26d07b1a8f0b2e3b75302b08b98b4b1fb6d5d2d03e64aca", size = 9095241, upload-time = "2025-12-09T22:35:14.475Z" },
{ url = "https://files.pythonhosted.org/packages/e0/fe/8ad29c47c9499132849cd5401f67c6bdd2912be8dcb298e774b4f39e1cce/ty-0.0.1a33-py3-none-win_amd64.whl", hash = "sha256:553b5281d424c69389508a60dfd8af8e3014529ca6856dfed1f231020bc58d09", size = 9948007, upload-time = "2025-12-09T22:35:17.043Z" },
{ url = "https://files.pythonhosted.org/packages/2f/fc/1825f1f8c77d4d8fe75543882d9ad5934e568aa807e1a4cb7e999f701750/ty-0.0.1a33-py3-none-win_arm64.whl", hash = "sha256:d9937e9ddc7b383c6b1ab3065982fb2b8d0a2884ae5bd7b542e4208a807e326e", size = 9471473, upload-time = "2025-12-09T22:35:12.105Z" },
]
[[package]]
name = "typer"
version = "0.20.0"
@@ -485,6 +715,20 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/bc/56/190ceb8cb10511b730b564fb1e0293fa468363dbad26145c34928a60cb0c/urllib3-2.6.1-py3-none-any.whl", hash = "sha256:e67d06fe947c36a7ca39f4994b08d73922d40e6cca949907be05efa6fd75110b", size = 131138, upload-time = "2025-12-08T15:25:25.51Z" },
]
[[package]]
name = "virtualenv"
version = "20.35.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "distlib" },
{ name = "filelock" },
{ name = "platformdirs" },
]
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/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]]
name = "websockets"
version = "15.0.1"