diff --git a/pyproject.toml b/pyproject.toml index eff2bce..dc2c196 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,7 +21,7 @@ classifiers = [ ] dependencies = [ "pydantic>=2.10.6", - "requests>=2.32.3", + "httpx>=0.28.0", "langchain-core>=0.1.0", "bm25s>=0.2.2", "numpy>=1.24.0", @@ -61,7 +61,7 @@ dev = [ "pytest-asyncio>=0.25.3", "pytest-cov>=6.0.0", "pytest-snapshot>=0.9.0", - "responses>=0.25.8", + "respx>=0.22.0", "ruff>=0.9.6", "stackone-ai", "ty>=0.0.3", diff --git a/stackone_ai/models.py b/stackone_ai/models.py index 0047fde..1e8b813 100644 --- a/stackone_ai/models.py +++ b/stackone_ai/models.py @@ -10,10 +10,9 @@ from typing import Annotated, Any, ClassVar, cast from urllib.parse import quote -import requests +import httpx from langchain_core.tools import BaseTool from pydantic import BaseModel, BeforeValidator, Field, PrivateAttr -from requests.exceptions import RequestException # TODO: Remove when Python 3.9 support is dropped from typing_extensions import TypeAlias @@ -242,7 +241,7 @@ def execute( if query_params: request_kwargs["params"] = query_params - response = requests.request(**request_kwargs) + response = httpx.request(**request_kwargs) response_status = response.status_code response.raise_for_status() @@ -254,15 +253,21 @@ def execute( status = "error" error_message = f"Invalid JSON in arguments: {exc}" raise ValueError(error_message) from exc - except RequestException as exc: + except httpx.HTTPStatusError as exc: + status = "error" + response_body = None + if exc.response.text: + try: + response_body = exc.response.json() + except json.JSONDecodeError: + response_body = exc.response.text + raise StackOneAPIError( + str(exc), + exc.response.status_code, + response_body, + ) from exc + except httpx.RequestError as exc: status = "error" - error_message = str(exc) - if hasattr(exc, "response") and exc.response is not None: - raise StackOneAPIError( - str(exc), - exc.response.status_code, - exc.response.json() if exc.response.text else None, - ) from exc raise StackOneError(f"Request failed: {exc}") from exc finally: datetime.now(timezone.utc) diff --git a/tests/test_feedback.py b/tests/test_feedback.py index 398e7ea..53589ae 100644 --- a/tests/test_feedback.py +++ b/tests/test_feedback.py @@ -5,9 +5,10 @@ import json import os -from unittest.mock import Mock, patch +import httpx import pytest +import respx from stackone_ai.feedback import create_feedback_tool from stackone_ai.models import StackOneError @@ -55,165 +56,152 @@ def test_multiple_account_ids_validation(self) -> None: with pytest.raises(StackOneError, match="At least one valid account ID is required"): tool.execute({"feedback": "Great tools!", "account_id": ["", " "], "tool_names": ["test_tool"]}) + @respx.mock def test_json_string_input(self) -> None: """Test that JSON string input is properly parsed.""" tool = create_feedback_tool(api_key="test_key") - with patch("requests.request") as mock_request: - mock_response = Mock() - mock_response.status_code = 200 - mock_response.json.return_value = {"message": "Success"} - mock_response.raise_for_status = Mock() - mock_request.return_value = mock_response + route = respx.post("https://api.stackone.com/ai/tool-feedback").mock( + return_value=httpx.Response(200, json={"message": "Success"}) + ) - json_string = json.dumps( - {"feedback": "Great tools!", "account_id": "acc_123456", "tool_names": ["test_tool"]} - ) - result = tool.execute(json_string) - assert result["message"] == "Success" + json_string = json.dumps( + {"feedback": "Great tools!", "account_id": "acc_123456", "tool_names": ["test_tool"]} + ) + result = tool.execute(json_string) + assert result["message"] == "Success" + assert route.called class TestFeedbackToolExecution: """Test suite for feedback tool execution.""" + @respx.mock def test_single_account_execution(self) -> None: """Test execution with single account ID.""" tool = create_feedback_tool(api_key="test_key") api_response = {"message": "Feedback successfully stored", "trace_id": "test-trace-id"} - with patch("requests.request") as mock_request: - mock_response = Mock() - mock_response.status_code = 200 - mock_response.json.return_value = api_response - mock_response.raise_for_status = Mock() - mock_request.return_value = mock_response - - result = tool.execute( - { - "feedback": "Great tools!", - "account_id": "acc_123456", - "tool_names": ["data_export", "analytics"], - } - ) - - assert result == api_response - mock_request.assert_called_once() - call_kwargs = mock_request.call_args[1] - assert call_kwargs["method"] == "POST" - assert call_kwargs["url"] == "https://api.stackone.com/ai/tool-feedback" - assert call_kwargs["json"]["feedback"] == "Great tools!" - assert call_kwargs["json"]["account_id"] == "acc_123456" - assert call_kwargs["json"]["tool_names"] == ["data_export", "analytics"] - + route = respx.post("https://api.stackone.com/ai/tool-feedback").mock( + return_value=httpx.Response(200, json=api_response) + ) + + result = tool.execute( + { + "feedback": "Great tools!", + "account_id": "acc_123456", + "tool_names": ["data_export", "analytics"], + } + ) + + assert result == api_response + assert route.called + assert route.call_count == 1 + request = route.calls[0].request + body = json.loads(request.content) + assert body["feedback"] == "Great tools!" + assert body["account_id"] == "acc_123456" + assert body["tool_names"] == ["data_export", "analytics"] + + @respx.mock def test_call_method_interface(self) -> None: """Test that the .call() method works correctly.""" tool = create_feedback_tool(api_key="test_key") api_response = {"message": "Success", "trace_id": "test-trace-id"} - with patch("requests.request") as mock_request: - mock_response = Mock() - mock_response.status_code = 200 - mock_response.json.return_value = api_response - mock_response.raise_for_status = Mock() - mock_request.return_value = mock_response - - result = tool.call( - feedback="Testing the .call() method interface.", - account_id="acc_test004", - tool_names=["meta_collect_tool_feedback"], - ) + route = respx.post("https://api.stackone.com/ai/tool-feedback").mock( + return_value=httpx.Response(200, json=api_response) + ) + + result = tool.call( + feedback="Testing the .call() method interface.", + account_id="acc_test004", + tool_names=["meta_collect_tool_feedback"], + ) - assert result == api_response - mock_request.assert_called_once() + assert result == api_response + assert route.called + assert route.call_count == 1 + @respx.mock def test_api_error_handling(self) -> None: """Test that API errors are handled properly.""" tool = create_feedback_tool(api_key="test_key") - with patch("requests.request") as mock_request: - mock_response = Mock() - mock_response.status_code = 401 - mock_response.text = '{"error": "Unauthorized"}' - mock_response.json.return_value = {"error": "Unauthorized"} - mock_response.raise_for_status.side_effect = Exception("401 Client Error: Unauthorized") - mock_request.return_value = mock_response - - with pytest.raises(StackOneError): - tool.execute( - { - "feedback": "Great tools!", - "account_id": "acc_123456", - "tool_names": ["test_tool"], - } - ) + respx.post("https://api.stackone.com/ai/tool-feedback").mock( + return_value=httpx.Response(401, json={"error": "Unauthorized"}) + ) - def test_multiple_account_ids_execution(self) -> None: - """Test execution with multiple account IDs - both success and mixed scenarios.""" - tool = create_feedback_tool(api_key="test_key") - api_response = {"message": "Feedback successfully stored", "trace_id": "test-trace-id"} - - # Test all successful case - with patch("requests.request") as mock_request: - mock_response = Mock() - mock_response.status_code = 200 - mock_response.json.return_value = api_response - mock_response.raise_for_status = Mock() - mock_request.return_value = mock_response - - result = tool.execute( + with pytest.raises(StackOneError): + tool.execute( { "feedback": "Great tools!", - "account_id": ["acc_123456", "acc_789012", "acc_345678"], + "account_id": "acc_123456", "tool_names": ["test_tool"], } ) - assert result["message"] == "Feedback sent to 3 account(s)" - assert result["total_accounts"] == 3 - assert result["successful"] == 3 - assert result["failed"] == 0 - assert len(result["results"]) == 3 - assert mock_request.call_count == 3 + @respx.mock + def test_multiple_account_ids_execution(self) -> None: + """Test execution with multiple account IDs - both success and mixed scenarios.""" + tool = create_feedback_tool(api_key="test_key") + api_response = {"message": "Feedback successfully stored", "trace_id": "test-trace-id"} - # Test mixed success/error case - def mock_request_side_effect(*args, **kwargs): - account_id = kwargs.get("json", {}).get("account_id") + # Test all successful case + route = respx.post("https://api.stackone.com/ai/tool-feedback").mock( + return_value=httpx.Response(200, json=api_response) + ) + + result = tool.execute( + { + "feedback": "Great tools!", + "account_id": ["acc_123456", "acc_789012", "acc_345678"], + "tool_names": ["test_tool"], + } + ) + + assert result["message"] == "Feedback sent to 3 account(s)" + assert result["total_accounts"] == 3 + assert result["successful"] == 3 + assert result["failed"] == 0 + assert len(result["results"]) == 3 + assert route.call_count == 3 + + @respx.mock + def test_multiple_account_ids_mixed_success(self) -> None: + """Test execution with multiple account IDs - mixed success and error.""" + tool = create_feedback_tool(api_key="test_key") + + def custom_side_effect(request: httpx.Request) -> httpx.Response: + body = json.loads(request.content) + account_id = body.get("account_id") if account_id == "acc_123456": - mock_response = Mock() - mock_response.status_code = 200 - mock_response.json.return_value = {"message": "Success"} - mock_response.raise_for_status = Mock() - return mock_response + return httpx.Response(200, json={"message": "Success"}) else: - mock_response = Mock() - mock_response.status_code = 401 - mock_response.text = '{"error": "Unauthorized"}' - mock_response.json.return_value = {"error": "Unauthorized"} - mock_response.raise_for_status.side_effect = Exception("401 Client Error: Unauthorized") - return mock_response - - with patch("requests.request") as mock_request: - mock_request.side_effect = mock_request_side_effect - - result = tool.execute( - { - "feedback": "Great tools!", - "account_id": ["acc_123456", "acc_unauthorized"], - "tool_names": ["test_tool"], - } - ) - - assert result["total_accounts"] == 2 - assert result["successful"] == 1 - assert result["failed"] == 1 - assert len(result["results"]) == 2 - - success_result = next(r for r in result["results"] if r["account_id"] == "acc_123456") - assert success_result["status"] == "success" - - error_result = next(r for r in result["results"] if r["account_id"] == "acc_unauthorized") - assert error_result["status"] == "error" - assert "401 Client Error: Unauthorized" in error_result["error"] + return httpx.Response(401, json={"error": "Unauthorized"}) + + respx.post("https://api.stackone.com/ai/tool-feedback").mock(side_effect=custom_side_effect) + + result = tool.execute( + { + "feedback": "Great tools!", + "account_id": ["acc_123456", "acc_unauthorized"], + "tool_names": ["test_tool"], + } + ) + + assert result["total_accounts"] == 2 + assert result["successful"] == 1 + assert result["failed"] == 1 + assert len(result["results"]) == 2 + + success_result = next(r for r in result["results"] if r["account_id"] == "acc_123456") + assert success_result["status"] == "success" + + error_result = next(r for r in result["results"] if r["account_id"] == "acc_unauthorized") + assert error_result["status"] == "error" + assert "error" in error_result + assert "401" in error_result["error"] def test_tool_integration(self) -> None: """Test that feedback tool integrates properly with toolset.""" diff --git a/tests/test_meta_tools.py b/tests/test_meta_tools.py index fc793fe..06b7dc0 100644 --- a/tests/test_meta_tools.py +++ b/tests/test_meta_tools.py @@ -1,7 +1,8 @@ """Tests for meta tools functionality""" +import httpx import pytest -import responses +import respx from stackone_ai import StackOneTool, Tools from stackone_ai.meta_tools import ( @@ -209,24 +210,22 @@ def test_execute_tool_invalid_name(self, tools_collection): } ) + @respx.mock def test_execute_tool_call(self, tools_collection): """Test calling the execute tool with call method""" execute_tool = create_meta_execute_tool(tools_collection) - # Mock the actual tool execution by patching the requests - with responses.RequestsMock() as rsps: - rsps.add( - responses.GET, - "https://api.example.com/hibob/employee", - json={"success": True, "employees": []}, - status=200, - ) + # Mock the actual tool execution + route = respx.get("https://api.example.com/hibob/employee").mock( + return_value=httpx.Response(200, json={"success": True, "employees": []}) + ) - # Call the meta execute tool - result = execute_tool.call(toolName="hibob_list_employee", params={"limit": 10}) + # Call the meta execute tool + result = execute_tool.call(toolName="hibob_list_employee", params={"limit": 10}) - assert result is not None - assert "success" in result or "employees" in result + assert result is not None + assert "success" in result or "employees" in result + assert route.called class TestToolsMetaTools: diff --git a/tests/test_models.py b/tests/test_models.py index 834e831..c974979 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -56,9 +56,11 @@ def mock_specs() -> dict: def test_tool_execution(mock_tool): """Test tool execution with parameters""" - with patch("requests.request") as mock_request: + with patch("httpx.request") as mock_request: mock_response = MagicMock() mock_response.json.return_value = {"id": "123", "name": "Test User"} + mock_response.status_code = 200 + mock_response.raise_for_status = MagicMock() mock_request.return_value = mock_response result = mock_tool.execute({"id": "123"}) @@ -69,9 +71,11 @@ def test_tool_execution(mock_tool): def test_tool_execution_with_string_args(mock_tool): """Test tool execution with string arguments""" - with patch("requests.request") as mock_request: + with patch("httpx.request") as mock_request: mock_response = MagicMock() mock_response.json.return_value = {"id": "123", "name": "Test User"} + mock_response.status_code = 200 + mock_response.raise_for_status = MagicMock() mock_request.return_value = mock_response result = mock_tool.execute('{"id": "123"}') @@ -138,9 +142,11 @@ async def test_langchain_tool_execution(mock_tool): langchain_tool = langchain_tools[0] # Mock the HTTP request - with patch("requests.request") as mock_request: + with patch("httpx.request") as mock_request: mock_response = MagicMock() mock_response.json.return_value = {"id": "test_value", "name": "Test User"} + mock_response.status_code = 200 + mock_response.raise_for_status = MagicMock() mock_request.return_value = mock_response # Test sync execution with correct parameter name diff --git a/tests/test_tool_calling.py b/tests/test_tool_calling.py index 8e35aec..736df63 100644 --- a/tests/test_tool_calling.py +++ b/tests/test_tool_calling.py @@ -2,8 +2,9 @@ import json +import httpx import pytest -import responses +import respx from stackone_ai import StackOneTool from stackone_ai.models import ExecuteConfig, ToolParameters @@ -40,15 +41,12 @@ def mock_tool(): class TestToolCalling: """Test tool calling functionality""" - @responses.activate + @respx.mock def test_call_with_kwargs(self, mock_tool): """Test calling a tool with keyword arguments""" # Mock the API response - responses.add( - responses.POST, - "https://api.example.com/test", - json={"success": True, "result": "test_result"}, - status=200, + route = respx.post("https://api.example.com/test").mock( + return_value=httpx.Response(200, json={"success": True, "result": "test_result"}) ) # Call the tool with kwargs @@ -58,19 +56,17 @@ def test_call_with_kwargs(self, mock_tool): assert result == {"success": True, "result": "test_result"} # Verify the request was made correctly - assert len(responses.calls) == 1 - request = responses.calls[0].request - assert json.loads(request.body) == {"name": "test", "value": 42} + assert route.called + assert route.call_count == 1 + request = route.calls[0].request + assert json.loads(request.content) == {"name": "test", "value": 42} - @responses.activate + @respx.mock def test_call_with_dict_arg(self, mock_tool): """Test calling a tool with a dictionary argument""" # Mock the API response - responses.add( - responses.POST, - "https://api.example.com/test", - json={"success": True, "result": "test_result"}, - status=200, + route = respx.post("https://api.example.com/test").mock( + return_value=httpx.Response(200, json={"success": True, "result": "test_result"}) ) # Call the tool with a dict @@ -80,19 +76,17 @@ def test_call_with_dict_arg(self, mock_tool): assert result == {"success": True, "result": "test_result"} # Verify the request - assert len(responses.calls) == 1 - request = responses.calls[0].request - assert json.loads(request.body) == {"name": "test", "value": 42} + assert route.called + assert route.call_count == 1 + request = route.calls[0].request + assert json.loads(request.content) == {"name": "test", "value": 42} - @responses.activate + @respx.mock def test_call_with_json_string(self, mock_tool): """Test calling a tool with a JSON string argument""" # Mock the API response - responses.add( - responses.POST, - "https://api.example.com/test", - json={"success": True, "result": "test_result"}, - status=200, + route = respx.post("https://api.example.com/test").mock( + return_value=httpx.Response(200, json={"success": True, "result": "test_result"}) ) # Call the tool with a JSON string @@ -102,9 +96,10 @@ def test_call_with_json_string(self, mock_tool): assert result == {"success": True, "result": "test_result"} # Verify the request - assert len(responses.calls) == 1 - request = responses.calls[0].request - assert json.loads(request.body) == {"name": "test", "value": 42} + assert route.called + assert route.call_count == 1 + request = route.calls[0].request + assert json.loads(request.content) == {"name": "test", "value": 42} def test_call_with_both_args_and_kwargs_raises_error(self, mock_tool): """Test that providing both args and kwargs raises an error""" @@ -116,15 +111,12 @@ def test_call_with_multiple_args_raises_error(self, mock_tool): with pytest.raises(ValueError, match="Only one positional argument is allowed"): mock_tool.call({"name": "test"}, {"value": 42}) - @responses.activate + @respx.mock def test_call_without_arguments(self, mock_tool): """Test calling a tool without any arguments""" # Mock the API response - responses.add( - responses.POST, - "https://api.example.com/test", - json={"success": True, "result": "no_args"}, - status=200, + route = respx.post("https://api.example.com/test").mock( + return_value=httpx.Response(200, json={"success": True, "result": "no_args"}) ) # Call the tool without arguments @@ -134,10 +126,11 @@ def test_call_without_arguments(self, mock_tool): assert result == {"success": True, "result": "no_args"} # Verify the request body is empty or contains empty JSON - assert len(responses.calls) == 1 - request = responses.calls[0].request + assert route.called + assert route.call_count == 1 + request = route.calls[0].request # Handle case where body might be None for empty POST - if request.body: - assert json.loads(request.body) == {} + if request.content: + assert json.loads(request.content) == {} else: - assert request.body is None or request.body == b"" + assert request.content == b"" diff --git a/uv.lock b/uv.lock index 1cf5069..c31cbf9 100644 --- a/uv.lock +++ b/uv.lock @@ -4135,17 +4135,15 @@ wheels = [ ] [[package]] -name = "responses" -version = "0.25.8" +name = "respx" +version = "0.22.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyyaml" }, - { name = "requests" }, - { name = "urllib3" }, + { name = "httpx" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/0e/95/89c054ad70bfef6da605338b009b2e283485835351a9935c7bfbfaca7ffc/responses-0.25.8.tar.gz", hash = "sha256:9374d047a575c8f781b94454db5cab590b6029505f488d12899ddb10a4af1cf4", size = 79320, upload-time = "2025-08-08T19:01:46.709Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f4/7c/96bd0bc759cf009675ad1ee1f96535edcb11e9666b985717eb8c87192a95/respx-0.22.0.tar.gz", hash = "sha256:3c8924caa2a50bd71aefc07aa812f2466ff489f1848c96e954a5362d17095d91", size = 28439, upload-time = "2024-12-19T22:33:59.374Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1c/4c/cc276ce57e572c102d9542d383b2cfd551276581dc60004cb94fe8774c11/responses-0.25.8-py3-none-any.whl", hash = "sha256:0c710af92def29c8352ceadff0c3fe340ace27cf5af1bbe46fb71275bcd2831c", size = 34769, upload-time = "2025-08-08T19:01:45.018Z" }, + { url = "https://files.pythonhosted.org/packages/8e/67/afbb0978d5399bc9ea200f1d4489a23c9a1dad4eee6376242b8182389c79/respx-0.22.0-py2.py3-none-any.whl", hash = "sha256:631128d4c9aba15e56903fb5f66fb1eff412ce28dd387ca3a81339e52dbd3ad0", size = 25127, upload-time = "2024-12-19T22:33:57.837Z" }, ] [[package]] @@ -4593,12 +4591,12 @@ source = { editable = "." } dependencies = [ { name = "bm25s" }, { name = "eval-type-backport", marker = "python_full_version < '3.10'" }, + { name = "httpx" }, { name = "langchain-core" }, { name = "numpy", version = "2.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, { name = "numpy", version = "2.3.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "pydantic" }, - { name = "requests" }, { name = "typing-extensions" }, ] @@ -4620,7 +4618,7 @@ dev = [ { name = "pytest-asyncio" }, { name = "pytest-cov" }, { name = "pytest-snapshot" }, - { name = "responses" }, + { name = "respx" }, { name = "ruff" }, { name = "stackone-ai" }, { name = "ty" }, @@ -4631,6 +4629,7 @@ requires-dist = [ { name = "bm25s", specifier = ">=0.2.2" }, { name = "crewai", marker = "python_full_version >= '3.10' and extra == 'examples'", specifier = ">=0.102.0" }, { name = "eval-type-backport", marker = "python_full_version < '3.10'" }, + { name = "httpx", specifier = ">=0.28.0" }, { name = "langchain-core", specifier = ">=0.1.0" }, { name = "langchain-openai", marker = "extra == 'examples'", specifier = ">=0.3.6" }, { name = "langgraph", marker = "extra == 'examples'", specifier = ">=0.2.0" }, @@ -4639,7 +4638,6 @@ requires-dist = [ { name = "openai", marker = "extra == 'examples'", specifier = ">=1.63.2" }, { name = "pydantic", specifier = ">=2.10.6" }, { name = "python-dotenv", marker = "extra == 'examples'", specifier = ">=1.0.1" }, - { name = "requests", specifier = ">=2.32.3" }, { name = "typing-extensions", specifier = ">=4.0.0" }, ] provides-extras = ["mcp", "examples"] @@ -4650,7 +4648,7 @@ dev = [ { name = "pytest-asyncio", specifier = ">=0.25.3" }, { name = "pytest-cov", specifier = ">=6.0.0" }, { name = "pytest-snapshot", specifier = ">=0.9.0" }, - { name = "responses", specifier = ">=0.25.8" }, + { name = "respx", specifier = ">=0.22.0" }, { name = "ruff", specifier = ">=0.9.6" }, { name = "stackone-ai" }, { name = "ty", specifier = ">=0.0.3" }, @@ -4864,27 +4862,27 @@ wheels = [ [[package]] name = "ty" -version = "0.0.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/bb/cd/aee86c0da3240960d6b7e807f3a41c89bae741495d81ca303200b0103dc9/ty-0.0.3.tar.gz", hash = "sha256:831259e22d3855436701472d4c0da200cd45041bc677eae79415d684f541de8a", size = 4769098, upload-time = "2025-12-18T02:16:49.773Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0c/ef/2d0d18e8fe6b673d3e1ea642f18404d7edfa9d08310f7203e8f0e7dc862e/ty-0.0.3-py3-none-linux_armv6l.whl", hash = "sha256:cd035bb75acecb78ac1ba8c4cc696f57a586e29d36e84bd691bc3b5b8362794c", size = 9763890, upload-time = "2025-12-18T02:16:56.879Z" }, - { url = "https://files.pythonhosted.org/packages/bb/67/0ae31574619a7264df8cf8e641f246992db22ac1720c2a72953aa31cbe61/ty-0.0.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:7708eaf73485e263efc7ef339f8e4487d3f5885779edbeec504fd72e4521c376", size = 9558276, upload-time = "2025-12-18T02:16:45.453Z" }, - { url = "https://files.pythonhosted.org/packages/d7/f7/3b9c033e80910972fca3783e4a52ba9cb7cd5c8b6828a87986646d64082b/ty-0.0.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:3113a633f46ec789f6df675b7afc5d3ab20c247c92ae4dbb9aa5b704768c18b2", size = 9094451, upload-time = "2025-12-18T02:17:01.155Z" }, - { url = "https://files.pythonhosted.org/packages/9a/29/9a90ed6bef00142a088965100b5e0a5d11805b9729c151ca598331bbd92b/ty-0.0.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a451f3f73a04bf18e551b1ebebb79b20fac5f09740a353f7e07b5f607b217c4f", size = 9568049, upload-time = "2025-12-18T02:16:28.643Z" }, - { url = "https://files.pythonhosted.org/packages/2f/ab/8daeb12912c2de8a3154db652931f4ad0d27c555faebcaf34af08bcfd0d2/ty-0.0.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9f6e926b6de0becf0452e1afad75cb71f889a4777cd14269e5447d46c01b2770", size = 9547711, upload-time = "2025-12-18T02:16:54.464Z" }, - { url = "https://files.pythonhosted.org/packages/91/54/f5c1f293f647beda717fee2448cc927ac0d05f66bebe18647680a67e1d67/ty-0.0.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:160e7974150f9f359c31d5808214676d1baa05321ab5a7b29fb09f4906dbdb38", size = 9983225, upload-time = "2025-12-18T02:17:05.672Z" }, - { url = "https://files.pythonhosted.org/packages/95/34/065962cfa2e87c10db839512229940a366b8ca1caffa2254a277b1694e5a/ty-0.0.3-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:726576df31d4e76934ffc64f2939d4a9bc195c7427452c8c159261ad00bd1b5e", size = 10851148, upload-time = "2025-12-18T02:16:38.354Z" }, - { url = "https://files.pythonhosted.org/packages/54/27/e2a8cbfc33999eef882ccd1b816ed615293f96e96f6df60cd12f84b69ca2/ty-0.0.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5014cf4744c94d9ea7b43314199ddaf52564a80b3d006e4ba0fe982bc42f4e8b", size = 10564441, upload-time = "2025-12-18T02:17:03.584Z" }, - { url = "https://files.pythonhosted.org/packages/91/6d/dcce3e222e59477c1f2b3a012cc76428d7032248138cd5544ad7f1cda7bd/ty-0.0.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2a9a51dc040f2718725f34ae6ef51fe8f8bd689e21bd3e82f4e71767034928de", size = 10358651, upload-time = "2025-12-18T02:16:26.091Z" }, - { url = "https://files.pythonhosted.org/packages/53/36/b6d0154b83a5997d607bf1238200271c17223f68aab2c778ded5424f9c1e/ty-0.0.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e0e6188eddd3a228c449261bb398e8621d33b92c1fc03599afdfad4388327a48", size = 10120457, upload-time = "2025-12-18T02:16:51.864Z" }, - { url = "https://files.pythonhosted.org/packages/cc/46/05dc826674ee1a451406e4c253c71700a6f707bae88b706a4c9e9bba6919/ty-0.0.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:5cc55e08d5d18edf1c5051af02456bd359716f07aae0a305e4cefe7735188540", size = 9551642, upload-time = "2025-12-18T02:16:33.518Z" }, - { url = "https://files.pythonhosted.org/packages/64/8a/f90b60d103fd5ec04ecbac091a64e607e6cd37cec6e718bba17cb2022644/ty-0.0.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:34b2d589412a81d1fd6d7fe461353068496c2bf1f7113742bd6d88d1d57ec3ad", size = 9572234, upload-time = "2025-12-18T02:16:31.013Z" }, - { url = "https://files.pythonhosted.org/packages/e8/72/5d3c6d34562d019ba7f3102b2a6d0c8e9e24ef39e70f09645c36a66765b7/ty-0.0.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:8a065eb2959f141fe4adafc14d57463cfa34f6cc4844a4ed56b2dce1a53a419a", size = 9701682, upload-time = "2025-12-18T02:16:41.379Z" }, - { url = "https://files.pythonhosted.org/packages/ef/44/bda434f788b320c9550a48c549e4a8c507e3d8a6ccb04ba5bd098307ba1e/ty-0.0.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e7177421f830a493f98d22f86d940b5a38788866e6062f680881f19be35ba3bb", size = 10213714, upload-time = "2025-12-18T02:16:35.648Z" }, - { url = "https://files.pythonhosted.org/packages/53/a6/b76a787938026c3d209131e5773de32cf6fc41210e0dd97874aafa20f394/ty-0.0.3-py3-none-win32.whl", hash = "sha256:e3e590bf5f33cb118a53c6d5242eedf7924d45517a5ee676c7a16be3a1389d2f", size = 9160441, upload-time = "2025-12-18T02:16:43.404Z" }, - { url = "https://files.pythonhosted.org/packages/fe/db/da60eb8252768323aee0ce69a08b95011088c003f80204b12380fe562fd2/ty-0.0.3-py3-none-win_amd64.whl", hash = "sha256:5af25b1fed8a536ce8072a9ae6a70cd2b559aa5294d43f57071fbdcd31dd2b0e", size = 10034265, upload-time = "2025-12-18T02:16:47.602Z" }, - { url = "https://files.pythonhosted.org/packages/5f/9c/9045cebdfc394c6f8c1e73a99d3aeda1bc639aace392e8ff4d695f1fab73/ty-0.0.3-py3-none-win_arm64.whl", hash = "sha256:29078b3100351a8b37339771615f13b8e4a4ff52b344d33f774f8d1a665a0ca5", size = 9513095, upload-time = "2025-12-18T02:16:59.073Z" }, +version = "0.0.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9e/db/6299d478000f4f1c6f9bf2af749359381610ffc4cbe6713b66e436ecf6e7/ty-0.0.5.tar.gz", hash = "sha256:983da6330773ff71e2b249810a19c689f9a0372f6e21bbf7cde37839d05b4346", size = 4806218, upload-time = "2025-12-20T21:19:17.24Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/98/c1f61ba378b4191e641bb36c07b7fcc70ff844d61be7a4bf2fea7472b4a9/ty-0.0.5-py3-none-linux_armv6l.whl", hash = "sha256:1594cd9bb68015eb2f5a3c68a040860f3c9306dc6667d7a0e5f4df9967b460e2", size = 9785554, upload-time = "2025-12-20T21:19:05.024Z" }, + { url = "https://files.pythonhosted.org/packages/ab/f9/b37b77c03396bd779c1397dae4279b7ad79315e005b3412feed8812a4256/ty-0.0.5-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:7c0140ba980233d28699d9ddfe8f43d0b3535d6a3bbff9935df625a78332a3cf", size = 9603995, upload-time = "2025-12-20T21:19:15.256Z" }, + { url = "https://files.pythonhosted.org/packages/7d/70/4e75c11903b0e986c0203040472627cb61d6a709e1797fb08cdf9d565743/ty-0.0.5-py3-none-macosx_11_0_arm64.whl", hash = "sha256:15de414712cde92048ae4b1a77c4dc22920bd23653fe42acaf73028bad88f6b9", size = 9145815, upload-time = "2025-12-20T21:19:36.481Z" }, + { url = "https://files.pythonhosted.org/packages/89/05/93983dfcf871a41dfe58e5511d28e6aa332a1f826cc67333f77ae41a2f8a/ty-0.0.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:438aa51ad6c5fae64191f8d58876266e26f9250cf09f6624b6af47a22fa88618", size = 9619849, upload-time = "2025-12-20T21:19:19.084Z" }, + { url = "https://files.pythonhosted.org/packages/82/b6/896ab3aad59f846823f202e94be6016fb3f72434d999d2ae9bd0f28b3af9/ty-0.0.5-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1b3d373fd96af1564380caf153600481c676f5002ee76ba8a7c3508cdff82ee0", size = 9606611, upload-time = "2025-12-20T21:19:24.583Z" }, + { url = "https://files.pythonhosted.org/packages/ca/ae/098e33fc92330285ed843e2750127e896140c4ebd2d73df7732ea496f588/ty-0.0.5-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8453692503212ad316cf8b99efbe85a91e5f63769c43be5345e435a1b16cba5a", size = 10029523, upload-time = "2025-12-20T21:19:07.055Z" }, + { url = "https://files.pythonhosted.org/packages/04/5a/f4b4c33758b9295e9aca0de9645deca0f4addd21d38847228723a6e780fc/ty-0.0.5-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:2e4c454139473abbd529767b0df7a795ed828f780aef8d0d4b144558c0dc4446", size = 10870892, upload-time = "2025-12-20T21:19:34.495Z" }, + { url = "https://files.pythonhosted.org/packages/c3/c5/4e3e7e88389365aa1e631c99378711cf0c9d35a67478cb4720584314cf44/ty-0.0.5-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:426d4f3b82475b1ec75f3cc9ee5a667c8a4ae8441a09fcd8e823a53b706d00c7", size = 10599291, upload-time = "2025-12-20T21:19:26.557Z" }, + { url = "https://files.pythonhosted.org/packages/c1/5d/138f859ea87bd95e17b9818e386ae25a910e46521c41d516bf230ed83ffc/ty-0.0.5-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5710817b67c6b2e4c0224e4f319b7decdff550886e9020f6d46aa1ce8f89a609", size = 10413515, upload-time = "2025-12-20T21:19:11.094Z" }, + { url = "https://files.pythonhosted.org/packages/27/21/1cbcd0d3b1182172f099e88218137943e0970603492fb10c7c9342369d9a/ty-0.0.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e23c55ef08882c7c5ced1ccb90b4eeefa97f690aea254f58ac0987896c590f76", size = 10144992, upload-time = "2025-12-20T21:19:13.225Z" }, + { url = "https://files.pythonhosted.org/packages/ad/30/fdac06a5470c09ad2659a0806497b71f338b395d59e92611f71b623d05a0/ty-0.0.5-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:b9e4c1a28a23b14cf8f4f793f4da396939f16c30bfa7323477c8cc234e352ac4", size = 9606408, upload-time = "2025-12-20T21:19:09.212Z" }, + { url = "https://files.pythonhosted.org/packages/09/93/e99dcd7f53295192d03efd9cbcec089a916f49cad4935c0160ea9adbd53d/ty-0.0.5-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:4e9ebb61529b9745af662e37c37a01ad743cdd2c95f0d1421705672874d806cd", size = 9630040, upload-time = "2025-12-20T21:19:38.165Z" }, + { url = "https://files.pythonhosted.org/packages/d7/f8/6d1e87186e4c35eb64f28000c1df8fd5f73167ce126c5e3dd21fd1204a23/ty-0.0.5-py3-none-musllinux_1_2_i686.whl", hash = "sha256:5eb191a8e332f50f56dfe45391bdd7d43dd4ef6e60884710fd7ce84c5d8c1eb5", size = 9754016, upload-time = "2025-12-20T21:19:32.79Z" }, + { url = "https://files.pythonhosted.org/packages/28/e6/20f989342cb3115852dda404f1d89a10a3ce93f14f42b23f095a3d1a00c9/ty-0.0.5-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:92ed7451a1e82ee134a2c24ca43b74dd31e946dff2b08e5c34473e6b051de542", size = 10252877, upload-time = "2025-12-20T21:19:20.787Z" }, + { url = "https://files.pythonhosted.org/packages/57/9d/fc66fa557443233dfad9ae197ff3deb70ae0efcfb71d11b30ef62f5cdcc3/ty-0.0.5-py3-none-win32.whl", hash = "sha256:71f6707e4c1c010c158029a688a498220f28bb22fdb6707e5c20e09f11a5e4f2", size = 9212640, upload-time = "2025-12-20T21:19:30.817Z" }, + { url = "https://files.pythonhosted.org/packages/68/b6/05c35f6dea29122e54af0e9f8dfedd0a100c721affc8cc801ebe2bc2ed13/ty-0.0.5-py3-none-win_amd64.whl", hash = "sha256:2b8b754a0d7191e94acdf0c322747fec34371a4d0669f5b4e89549aef28814ae", size = 10034701, upload-time = "2025-12-20T21:19:28.311Z" }, + { url = "https://files.pythonhosted.org/packages/df/ca/4201ed5cb2af73912663d0c6ded927c28c28b3c921c9348aa8d2cfef4853/ty-0.0.5-py3-none-win_arm64.whl", hash = "sha256:83bea5a5296caac20d52b790ded2b830a7ff91c4ed9f36730fe1f393ceed6654", size = 9566474, upload-time = "2025-12-20T21:19:22.518Z" }, ] [[package]]