Mateo/lchain (#549)

The latest change to langchain-arcade was blocked becuase it still
relied on poetry, I moved that to uv
This commit is contained in:
Mateo Torres 2025-09-08 13:55:06 -03:00 committed by GitHub
parent c481f1d677
commit 639f726b30
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 202 additions and 142 deletions

View file

@ -13,6 +13,7 @@ jobs:
permissions: permissions:
contents: write contents: write
packages: write packages: write
id-token: write
steps: steps:
- name: Checkout code - name: Checkout code
@ -20,15 +21,11 @@ jobs:
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Install Poetry - name: Set up the environment
uses: snok/install-poetry@v1 uses: ./.github/actions/setup-uv-env
with:
version: 1.8.5
- uses: actions/setup-python@v5
with: with:
python-version: "3.11" python-version: "3.11"
cache: "pip" working-directory: contrib/langchain
- name: Test LangChain Arcade - name: Test LangChain Arcade
working-directory: contrib/langchain working-directory: contrib/langchain
@ -41,33 +38,38 @@ jobs:
if: inputs.version != '' if: inputs.version != ''
working-directory: contrib/langchain working-directory: contrib/langchain
run: | run: |
poetry version ${{ inputs.version }} uv version ${{ inputs.version }}
- name: Publish to PyPI - name: Build release distributions
working-directory: contrib/langchain working-directory: contrib/langchain
run: | run: |
poetry build VERSION=$(uv version --short)
# Extract version from pyproject.toml using poetry and save it
VERSION=$(poetry version -s)
echo "VERSION=$VERSION" >> $GITHUB_ENV echo "VERSION=$VERSION" >> $GITHUB_ENV
poetry config pypi-token.pypi ${{ secrets.PYPI_TOKEN }}
# Attempt to publish the toolkit to PyPI. Skip if the version already exists uv build --out-dir dist | tee build.log
if poetry publish --skip-existing 2>&1 | grep -q "File exists. Skipping"; then
echo "Version already exists on PyPI. Skipping publish." # Verify build artifacts
echo "skip_publish=true" >> $GITHUB_OUTPUT ls -la dist/
else echo "Built artifacts for langchain_arcade v${{ env.VERSION }}"
echo "skip_publish=false" >> $GITHUB_OUTPUT
fi - name: Upload release distributions
uses: actions/upload-artifact@v4
with:
name: release-dists-langchain_arcade-${{ env.VERSION }}
path: dist/
- name: Publish release distributions to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
- name: Send status to Slack - name: Send status to Slack
if: steps.Publish_LangChain.outputs.skip_publish != 'true' if: always()
uses: slackapi/slack-github-action@v2.0.0 uses: slackapi/slack-github-action@v2.0.0
with: with:
webhook: ${{ secrets.PACKAGE_RELEASE_SLACK_WEBHOOK_URL }} webhook: ${{ secrets.PACKAGE_RELEASE_SLACK_WEBHOOK_URL }}
webhook-type: webhook-trigger webhook-type: webhook-trigger
payload: | payload: |
{ {
"status": "${{ job.status }}", "status": "${{ job.status == 'failure' || job.status == 'cancelled' && 'Failed' || 'Success' }}",
"package": "langchain_arcade", "package": "langchain_arcade",
"version": "${{ env.VERSION }}", "version": "${{ env.VERSION }}",
"url": "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" "url": "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"

View file

@ -18,15 +18,17 @@ jobs:
- name: Check out - name: Check out
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Install Poetry
uses: snok/install-poetry@v1 - name: Set up the environment
uses: ./.github/actions/setup-uv-env
with: with:
version: 1.8.5 python-version: "3.11"
working-directory: contrib/langchain
- name: Install - name: Install
run: cd contrib/langchain && make install && make check run: cd contrib/langchain && make install && make check
tox: test:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
matrix: matrix:
@ -41,21 +43,18 @@ jobs:
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
- name: Install Poetry - name: Set up the environment
uses: snok/install-poetry@v1 uses: ./.github/actions/setup-uv-env
with: with:
version: 1.8.5 python-version: ${{ matrix.python-version }}
working-directory: contrib/langchain
- name: Install dependencies - name: Install dependencies
run: cd contrib/langchain && make install run: cd contrib/langchain && make install
- name: Install tox - name: Test & Coverage
run: | run: |
python -m pip install --upgrade pip cd contrib/langchain && make test && make coverage
python -m pip install tox tox-gh-actions
- name: Test with tox
run: cd contrib/langchain && tox
- name: Upload coverage reports to Codecov with GitHub Action on Python 3.11 - name: Upload coverage reports to Codecov with GitHub Action on Python 3.11
uses: codecov/codecov-action@v4.0.1 uses: codecov/codecov-action@v4.0.1

View file

@ -1,66 +1,47 @@
VERSION ?= "0.1.0" .PHONY: help
help:
@echo "🛠️ github Commands:\n"
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
.PHONY: install .PHONY: install
install: ## Install the poetry environment and install the pre-commit hooks install: ## Install the uv environment and install all packages with dependencies
@echo "📦 Checking if Poetry is installed" @echo "🚀 Creating virtual environment and installing all packages using uv"
@if ! command -v poetry >/dev/null 2>&1; then \ @uv sync --active --all-extras --no-sources
echo "📦 Installing Poetry with pip"; \ @uv run pre-commit install
pip install poetry==1.8.5; \ @echo "✅ All packages and dependencies installed via uv"
else \
echo "📦 Poetry is already installed"; \
fi
@echo "🚀 Installing package in development mode with all extras"
poetry install --all-extras
@echo "🚀 Installing pre-commit hooks"
poetry run pre-commit install
.PHONY: check .PHONY: build
check: ## Run code quality tools. build: clean-build ## Build wheel file using uv
@echo "🚀 Checking Poetry lock file consistency with 'pyproject.toml': Running poetry check --lock" @echo "🚀 Creating wheel file"
@poetry check --lock uv build
@echo "🚀 Linting code: Running pre-commit"
@poetry run pre-commit run -a .PHONY: clean-build
@echo "🚀 Static type checking: Running mypy" clean-build: ## clean build artifacts
@poetry run mypy $(git ls-files '*.py') @echo "🗑️ Cleaning dist directory"
rm -rf dist
.PHONY: test .PHONY: test
test: ## Test the code with pytest test: ## Test the code with pytest
@echo "🚀 Testing code: Running pytest" @echo "🚀 Testing code: Running pytest"
@poetry run pytest -W ignore -v --cov --cov-config=pyproject.toml --cov-report=xml @uv run pytest -W ignore -v --cov --cov-config=pyproject.toml --cov-report=xml
.PHONY: set-version .PHONY: coverage
set-version: ## Set the version in the pyproject.toml file coverage: ## Generate coverage report
@echo "🚀 Setting version in pyproject.toml" @echo "coverage report"
@poetry version $(VERSION) coverage report
@echo "Generating coverage report"
coverage html
.PHONY: unset-version .PHONY: bump-version
unset-version: ## Set the version in the pyproject.toml file bump-version: ## Bump the version in the pyproject.toml file by a patch version
@echo "🚀 Setting version in pyproject.toml" @echo "🚀 Bumping version in pyproject.toml"
@poetry version 0.1.0 uv version --bump patch
.PHONY: build .PHONY: check
build: clean-build ## Build wheel file using poetry check: ## Run code quality tools.
@echo "🚀 Creating wheel file" @echo "🚀 Linting code: Running pre-commit"
@poetry build @uv run pre-commit run -a
@echo "🚀 Static type checking: Running mypy"
.PHONY: clean-build @uv run mypy --config-file=pyproject.toml
clean-build: ## clean build artifacts
@rm -rf dist
.PHONY: publish
publish: ## publish a release to pypi.
@echo "🚀 Publishing: Dry run."
@poetry config pypi-token.pypi $(PYPI_TOKEN)
@poetry publish --dry-run
@echo "🚀 Publishing."
@poetry publish
.PHONY: build-and-publish
build-and-publish: build publish ## Build and publish.
.PHONY: help
help:
@echo "🛠️ Arcade Dev Commands:\n"
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
.DEFAULT_GOAL := help

View file

@ -53,7 +53,9 @@ def tool_definition_to_pydantic_model(tool_def: ToolDefinition) -> type[BaseMode
for param in tool_def.input.parameters or []: for param in tool_def.input.parameters or []:
param_type = get_python_type(param.value_schema.val_type) param_type = get_python_type(param.value_schema.val_type)
if param_type == list and param.value_schema.inner_val_type: # noqa: E721 if param_type == list and param.value_schema.inner_val_type: # noqa: E721
inner_type: type[Any] = get_python_type(param.value_schema.inner_val_type) inner_type: type[Any] = get_python_type(
param.value_schema.inner_val_type
)
param_type = list[inner_type] # type: ignore[valid-type] param_type = list[inner_type] # type: ignore[valid-type]
param_description = param.description or "No description provided." param_description = param.description or "No description provided."
default = ... if param.required else None default = ... if param.required else None
@ -90,9 +92,14 @@ def process_tool_execution_response(
"tool": tool_name, "tool": tool_name,
} }
if execute_response.output is not None and execute_response.output.error is not None: if (
execute_response.output is not None
and execute_response.output.error is not None
):
error = execute_response.output.error error = execute_response.output.error
error_message = str(error.message) if hasattr(error, "message") else "Unknown error" error_message = (
str(error.message) if hasattr(error, "message") else "Unknown error"
)
error_details["error"] = error_message error_details["error"] = error_message
# Add all non-None optional error fields to the details # Add all non-None optional error fields to the details
@ -133,10 +140,13 @@ def create_tool_function(
A callable function that executes the tool. A callable function that executes the tool.
""" """
if langgraph and not LANGGRAPH_ENABLED: if langgraph and not LANGGRAPH_ENABLED:
raise ImportError("LangGraph is not installed. Please install it to use this feature.") raise ImportError(
"LangGraph is not installed. Please install it to use this feature."
)
requires_authorization = ( requires_authorization = (
tool_def.requirements is not None and tool_def.requirements.authorization is not None tool_def.requirements is not None
and tool_def.requirements.authorization is not None
) )
def tool_function(config: RunnableConfig, **kwargs: Any) -> Any: def tool_function(config: RunnableConfig, **kwargs: Any) -> Any:
@ -161,7 +171,9 @@ def create_tool_function(
# Authorize the user for the tool # Authorize the user for the tool
auth_response = client.tools.authorize(tool_name=tool_name, user_id=user_id) auth_response = client.tools.authorize(tool_name=tool_name, user_id=user_id)
if auth_response.status != "completed": if auth_response.status != "completed":
auth_message = f"Please use the following link to authorize: {auth_response.url}" auth_message = (
f"Please use the following link to authorize: {auth_response.url}"
)
if langgraph: if langgraph:
raise NodeInterrupt(auth_message) raise NodeInterrupt(auth_message)
return {"error": auth_message} return {"error": auth_message}
@ -249,10 +261,13 @@ def create_async_tool_function(
An async callable function that executes the tool. An async callable function that executes the tool.
""" """
if langgraph and not LANGGRAPH_ENABLED: if langgraph and not LANGGRAPH_ENABLED:
raise ImportError("LangGraph is not installed. Please install it to use this feature.") raise ImportError(
"LangGraph is not installed. Please install it to use this feature."
)
requires_authorization = ( requires_authorization = (
tool_def.requirements is not None and tool_def.requirements.authorization is not None tool_def.requirements is not None
and tool_def.requirements.authorization is not None
) )
async def tool_function(config: RunnableConfig, **kwargs: Any) -> Any: async def tool_function(config: RunnableConfig, **kwargs: Any) -> Any:
@ -275,9 +290,13 @@ def create_async_tool_function(
return {"error": error_message} return {"error": error_message}
# Authorize the user for the tool # Authorize the user for the tool
auth_response = await client.tools.authorize(tool_name=tool_name, user_id=user_id) auth_response = await client.tools.authorize(
tool_name=tool_name, user_id=user_id
)
if auth_response.status != "completed": if auth_response.status != "completed":
auth_message = f"Please use the following link to authorize: {auth_response.url}" auth_message = (
f"Please use the following link to authorize: {auth_response.url}"
)
if langgraph: if langgraph:
raise NodeInterrupt(auth_message) raise NodeInterrupt(auth_message)
return {"error": auth_message} return {"error": auth_message}

View file

@ -188,7 +188,9 @@ class ToolManager(LangChainToolManager):
""" """
tool_map = _create_tool_map(self.definitions, use_underscores=use_underscores) tool_map = _create_tool_map(self.definitions, use_underscores=use_underscores)
return [ return [
wrap_arcade_tool(self._client, tool_name, definition, langgraph=use_interrupts) wrap_arcade_tool(
self._client, tool_name, definition, langgraph=use_interrupts
)
for tool_name, definition in tool_map.items() for tool_name, definition in tool_map.items()
] ]
@ -228,7 +230,9 @@ class ToolManager(LangChainToolManager):
Raises: Raises:
ValueError: If no tools or toolkits are provided and raise_on_empty is True. ValueError: If no tools or toolkits are provided and raise_on_empty is True.
""" """
tools_list = self._retrieve_tool_definitions(tools, toolkits, raise_on_empty, limit, offset) tools_list = self._retrieve_tool_definitions(
tools, toolkits, raise_on_empty, limit, offset
)
self._tools = _create_tool_map(tools_list) self._tools = _create_tool_map(tools_list)
return self.to_langchain() return self.to_langchain()
@ -332,7 +336,9 @@ class ToolManager(LangChainToolManager):
# If no specific tools or toolkits are requested, raise an error. # If no specific tools or toolkits are requested, raise an error.
if not tools and not toolkits: if not tools and not toolkits:
if raise_on_empty: if raise_on_empty:
raise ValueError("No tools or toolkits provided to retrieve tool definitions.") raise ValueError(
"No tools or toolkits provided to retrieve tool definitions."
)
return [] return []
# Retrieve individual tools if specified # Retrieve individual tools if specified
@ -378,7 +384,10 @@ class ToolManager(LangChainToolManager):
self._tools.update(_create_tool_map([tool])) self._tools.update(_create_tool_map([tool]))
def add_toolkit( def add_toolkit(
self, toolkit_name: str, limit: Optional[int] = None, offset: Optional[int] = None self,
toolkit_name: str,
limit: Optional[int] = None,
offset: Optional[int] = None,
) -> None: ) -> None:
""" """
Add all tools from a specific toolkit to the manager. Add all tools from a specific toolkit to the manager.
@ -584,7 +593,9 @@ class AsyncToolManager(LangChainToolManager):
""" """
tool_map = _create_tool_map(self.definitions, use_underscores=use_underscores) tool_map = _create_tool_map(self.definitions, use_underscores=use_underscores)
return [ return [
wrap_arcade_tool(self._client, tool_name, definition, langgraph=use_interrupts) wrap_arcade_tool(
self._client, tool_name, definition, langgraph=use_interrupts
)
for tool_name, definition in tool_map.items() for tool_name, definition in tool_map.items()
] ]
@ -686,7 +697,9 @@ class AsyncToolManager(LangChainToolManager):
# If no specific tools or toolkits are requested, raise an error. # If no specific tools or toolkits are requested, raise an error.
if not tools and not toolkits: if not tools and not toolkits:
if raise_on_empty: if raise_on_empty:
raise ValueError("No tools or toolkits provided to retrieve tool definitions.") raise ValueError(
"No tools or toolkits provided to retrieve tool definitions."
)
return [] return []
# First, gather single tools if the user specifically requested them. # First, gather single tools if the user specifically requested them.
@ -734,7 +747,10 @@ class AsyncToolManager(LangChainToolManager):
self._tools.update(_create_tool_map([tool])) self._tools.update(_create_tool_map([tool]))
async def add_toolkit( async def add_toolkit(
self, toolkit_name: str, limit: Optional[int] = None, offset: Optional[int] = None self,
toolkit_name: str,
limit: Optional[int] = None,
offset: Optional[int] = None,
) -> None: ) -> None:
""" """
Add all tools from a specific toolkit to the manager. Add all tools from a specific toolkit to the manager.

View file

@ -1,26 +1,32 @@
[tool.poetry] [build-system]
requires = [ "hatchling",]
build-backend = "hatchling.build"
[project]
name = "langchain-arcade" name = "langchain-arcade"
version = "1.4.0" version = "1.4.0"
description = "An integration package connecting Arcade and Langchain/LangGraph" description = "An integration package connecting Arcade and Langchain/LangGraph"
authors = ["Arcade <dev@arcade.dev>"]
readme = "README.md" readme = "README.md"
repository = "https://github.com/arcadeai/arcade-ai/tree/main/contrib/langchain" repository = "https://github.com/arcadeai/arcade-ai/tree/main/contrib/langchain"
license = "MIT" license = "MIT"
requires-python = ">=3.10"
[tool.poetry.dependencies] dependencies = [
python = ">=3.10,<4" "arcadepy>=1.7.0",
arcadepy = "1.7.*" "langchain-core>=0.3.49,<0.4",
langchain-core = ">=0.3.49,<0.4" ]
[tool.poetry.group.dev.dependencies] [project.optional-dependencies]
pytest = "^8.1.2" dev = [
pytest-cov = "^4.0.0" "pytest>=8.3.0,<8.4.0",
mypy = "^1.5.1" "pytest-cov>=4.0.0,<4.1.0",
pre-commit = "^3.4.0" "pytest-mock>=3.11.1,<3.12.0",
tox = "^4.11.1" "pytest-asyncio>=0.24.0,<0.25.0",
pytest-asyncio = "^0.23.7" "mypy>=1.5.1,<1.6.0",
langgraph = ">=0.3.23,<0.4" "pre-commit>=3.4.0,<3.5.0",
"ruff>=0.7.4,<0.8.0",
"langgraph>=0.3.23,<0.4"
]
[tool.mypy] [tool.mypy]
@ -45,3 +51,9 @@ source = ["langchain_arcade"]
[tool.coverage.report] [tool.coverage.report]
skip_empty = true skip_empty = true
[tool.ruff.lint]
ignore = ["C901"]
[tool.hatch.build.targets.wheel]
packages = [ "langchain_arcade",]

View file

@ -165,7 +165,9 @@ async def test_init_tools_parameterized(
client.tools.list.return_value = page_cls(items=[mock_tool]) client.tools.list.return_value = page_cls(items=[mock_tool])
# Act # Act
result = await maybe_await(manager.init_tools(tools=["GoogleSearch_Search"]), is_async) result = await maybe_await(
manager.init_tools(tools=["GoogleSearch_Search"]), is_async
)
# Assert # Assert
assert "GoogleSearch_Search" in manager.tools assert "GoogleSearch_Search" in manager.tools
@ -220,7 +222,9 @@ async def test_deprecated_get_tools_parameterized(
# Act - Check for deprecation warning # Act - Check for deprecation warning
with pytest.warns(DeprecationWarning): with pytest.warns(DeprecationWarning):
result = await maybe_await(manager.get_tools(tools=["GoogleSearch_Search"]), is_async) result = await maybe_await(
manager.get_tools(tools=["GoogleSearch_Search"]), is_async
)
# Assert - Method should still work # Assert - Method should still work
assert len(result) == 1 assert len(result) == 1
@ -280,7 +284,9 @@ async def test_add_toolkit_parameterized(
# Mock the response for toolkit listing # Mock the response for toolkit listing
page_cls = AsyncOffsetPage if is_async else SyncOffsetPage page_cls = AsyncOffsetPage if is_async else SyncOffsetPage
client.tools.list.return_value = page_cls(items=[mock_tool_list_emails, mock_tool_trash_email]) client.tools.list.return_value = page_cls(
items=[mock_tool_list_emails, mock_tool_trash_email]
)
# Act # Act
await maybe_await(manager.add_toolkit("Search"), is_async) await maybe_await(manager.add_toolkit("Search"), is_async)
@ -290,7 +296,9 @@ async def test_add_toolkit_parameterized(
assert "Gmail_SendEmail" in manager.tools assert "Gmail_SendEmail" in manager.tools
assert "Gmail_ListEmails" in manager.tools assert "Gmail_ListEmails" in manager.tools
assert "Gmail_TrashEmail" in manager.tools assert "Gmail_TrashEmail" in manager.tools
client.tools.list.assert_called_once_with(toolkit="Search", limit=NOT_GIVEN, offset=NOT_GIVEN) client.tools.list.assert_called_once_with(
toolkit="Search", limit=NOT_GIVEN, offset=NOT_GIVEN
)
@pytest.mark.asyncio @pytest.mark.asyncio
@ -339,7 +347,9 @@ async def test_wait_for_auth_with_response_object_parameterized(
client = async_mock_arcade_client if is_async else mock_arcade_client client = async_mock_arcade_client if is_async else mock_arcade_client
completed_response = AuthorizationResponse( completed_response = AuthorizationResponse(
id="auth_abc", status="completed", tool_fully_qualified_name="GoogleSearch_Search" id="auth_abc",
status="completed",
tool_fully_qualified_name="GoogleSearch_Search",
) )
client.auth.wait_for_completion.return_value = completed_response client.auth.wait_for_completion.return_value = completed_response
@ -409,7 +419,8 @@ async def test_get_tools_with_explicit_parameterized(
# Act - Check for deprecation warning # Act - Check for deprecation warning
with pytest.warns(DeprecationWarning): with pytest.warns(DeprecationWarning):
retrieved_tools = await maybe_await( retrieved_tools = await maybe_await(
manager.get_tools(tools=["GoogleSearch_Search", "BingSearch_Search"]), is_async manager.get_tools(tools=["GoogleSearch_Search", "BingSearch_Search"]),
is_async,
) )
# Assert # Assert
@ -427,7 +438,9 @@ def test_arcade_tool_manager_deprecation_warning():
with pytest.warns(DeprecationWarning) as warnings_record: with pytest.warns(DeprecationWarning) as warnings_record:
ArcadeToolManager(client=MagicMock()) ArcadeToolManager(client=MagicMock())
# Assert # Assert
assert any("ArcadeToolManager is deprecated" in str(w.message) for w in warnings_record) assert any(
"ArcadeToolManager is deprecated" in str(w.message) for w in warnings_record
)
@pytest.mark.asyncio @pytest.mark.asyncio
@ -468,7 +481,9 @@ def test_requires_auth_true(manager, make_tool):
# Arrange # Arrange
tool_name = "GoogleSearch_Search" tool_name = "GoogleSearch_Search"
# Pass a MagicMock with 'authorization' to ensure it gets converted # Pass a MagicMock with 'authorization' to ensure it gets converted
mock_tool_def = make_tool(tool_name, requirements=MagicMock(authorization="some_required_auth")) mock_tool_def = make_tool(
tool_name, requirements=MagicMock(authorization="some_required_auth")
)
manager._tools[tool_name] = mock_tool_def manager._tools[tool_name] = mock_tool_def
# Act # Act
@ -533,7 +548,9 @@ def test_retrieve_tool_definitions_tools_only(manager, mock_arcade_client, make_
mock_arcade_client.tools.get.return_value = mock_tool mock_arcade_client.tools.get.return_value = mock_tool
# Act # Act
results = manager._retrieve_tool_definitions(tools=["GoogleSearch_Search"], toolkits=None) results = manager._retrieve_tool_definitions(
tools=["GoogleSearch_Search"], toolkits=None
)
# Assert # Assert
assert len(results) == 1 assert len(results) == 1
@ -541,7 +558,9 @@ def test_retrieve_tool_definitions_tools_only(manager, mock_arcade_client, make_
mock_arcade_client.tools.get.assert_called_once_with(name="GoogleSearch_Search") mock_arcade_client.tools.get.assert_called_once_with(name="GoogleSearch_Search")
def test_retrieve_tool_definitions_toolkits_only(manager, mock_arcade_client, make_tool): def test_retrieve_tool_definitions_toolkits_only(
manager, mock_arcade_client, make_tool
):
""" """
Test the internal _retrieve_tool_definitions method by specifying toolkits. Test the internal _retrieve_tool_definitions method by specifying toolkits.
""" """
@ -567,7 +586,9 @@ def test_retrieve_tool_definitions_raise_on_empty(manager):
""" """
# Act & Assert # Act & Assert
with pytest.raises(ValueError) as excinfo: with pytest.raises(ValueError) as excinfo:
manager._retrieve_tool_definitions(tools=None, toolkits=None, raise_on_empty=True) manager._retrieve_tool_definitions(
tools=None, toolkits=None, raise_on_empty=True
)
assert "No tools or toolkits provided" in str(excinfo.value) assert "No tools or toolkits provided" in str(excinfo.value)
@ -578,7 +599,9 @@ def test_retrieve_tool_definitions_empty_no_raise(manager):
are provided and raise_on_empty is False. are provided and raise_on_empty is False.
""" """
# Act # Act
results = manager._retrieve_tool_definitions(tools=None, toolkits=None, raise_on_empty=False) results = manager._retrieve_tool_definitions(
tools=None, toolkits=None, raise_on_empty=False
)
# Assert # Assert
assert results == [] assert results == []
@ -601,9 +624,13 @@ async def test_retrieve_tool_definitions_with_limit_offset_parameterized(
# Act # Act
if is_async: if is_async:
results = await manager._retrieve_tool_definitions(toolkits=["Search"], limit=10, offset=5) results = await manager._retrieve_tool_definitions(
toolkits=["Search"], limit=10, offset=5
)
else: else:
results = manager._retrieve_tool_definitions(toolkits=["Search"], limit=10, offset=5) results = manager._retrieve_tool_definitions(
toolkits=["Search"], limit=10, offset=5
)
# Assert # Assert
assert len(results) > 0 assert len(results) > 0
@ -618,7 +645,9 @@ def test_get_client_config_with_kwargs():
manager = ToolManager(client=MagicMock()) # Client won't be used here manager = ToolManager(client=MagicMock()) # Client won't be used here
# Act # Act
with patch.dict("os.environ", {"ARCADE_API_KEY": "env_key", "ARCADE_BASE_URL": "env_url"}): with patch.dict(
"os.environ", {"ARCADE_API_KEY": "env_key", "ARCADE_BASE_URL": "env_url"}
):
result = manager._get_client_config(api_key="kwarg_key", base_url="kwarg_url") result = manager._get_client_config(api_key="kwarg_key", base_url="kwarg_url")
# Assert # Assert
@ -634,7 +663,9 @@ def test_get_client_config_with_env_vars():
manager = ToolManager(client=MagicMock()) # Client won't be used here manager = ToolManager(client=MagicMock()) # Client won't be used here
# Act # Act
with patch.dict("os.environ", {"ARCADE_API_KEY": "env_key", "ARCADE_BASE_URL": "env_url"}): with patch.dict(
"os.environ", {"ARCADE_API_KEY": "env_key", "ARCADE_BASE_URL": "env_url"}
):
result = manager._get_client_config() result = manager._get_client_config()
# Assert # Assert

View file

@ -10,7 +10,7 @@ python =
[testenv] [testenv]
passenv = PYTHON_VERSION passenv = PYTHON_VERSION
allowlist_externals = poetry allowlist_externals = uv
commands = commands =
poetry install -v --all-extras uv sync --active --all-extras
pytest --doctest-modules tests --cov --cov-config=pyproject.toml --cov-report=xml uv pytest --doctest-modules tests --cov --cov-config=pyproject.toml --cov-report=xml