Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 21 additions & 7 deletions ymir/tools/privileged/copr.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@
import gzip
import logging
import random
import tempfile
import time
from pathlib import Path
from shutil import rmtree
from urllib.parse import urljoin, urlparse

import aiohttp
Expand All @@ -12,7 +14,6 @@
from beeai_framework.emitter import Emitter
from beeai_framework.tools import (
JSONToolOutput,
StringToolOutput,
ToolError,
ToolRunOptions,
)
Expand Down Expand Up @@ -324,13 +325,22 @@ async def branch_to_chroot(dist_git_branch: str, upcoming_z_streams: dict[str, s

class DownloadArtifactsToolInput(BaseModel):
artifacts_urls: list[str] = Field(description="URLs to build artifacts (logs and RPM files)")
target_path: AbsolutePath = Field(description="Absolute path where to download the artifacts")


class DownloadArtifactsTool(Tool[DownloadArtifactsToolInput, ToolRunOptions, StringToolOutput]):
class DownloadArtifactsResult(BaseModel):
target_path: str = Field(description="Location of downloaded files")


class DownloadArtifactsToolOutput(JSONToolOutput[DownloadArtifactsResult]):
def get_text_content(self) -> str:
"""Return content with minimum tokens possible"""
return f"target_path: {self.result.target_path}"


class DownloadArtifactsTool(Tool[DownloadArtifactsToolInput, ToolRunOptions, DownloadArtifactsToolOutput]):
name = "download_artifacts"
description = """
Downloads build artifacts to the specified location.
Downloads build artifacts to a temporary location.
Any gzipped log files will be automatically decompressed,
for example `http://example.com/builder-live.log.gz`
will be downloaded as `builder-live.log`.
Expand All @@ -348,9 +358,9 @@ async def _run(
tool_input: DownloadArtifactsToolInput,
options: ToolRunOptions | None,
context: RunContext,
) -> StringToolOutput:
) -> DownloadArtifactsToolOutput:
artifacts_urls = tool_input.artifacts_urls
target_path = tool_input.target_path
target_path = tempfile.mkdtemp()
async with aiohttp.ClientSession(
timeout=AIOHTTP_TIMEOUT, headers={"User-Agent": YMIR_USER_AGENT}
) as session:
Expand All @@ -369,7 +379,11 @@ async def _run(
target = target.with_suffix("")
(target_path / target).write_bytes(content)
else:
# Cleanup temporary dir
rmtree(target_path)
raise ToolError(f"Failed to download {url}: {response.status} {response.reason}")
except (aiohttp.ClientError, TimeoutError) as e:
# Cleanup temporary dir
rmtree(target_path)
raise ToolError(f"Failed to download {url}: {e}") from e
return StringToolOutput(result="Successfully downloaded the specified build artifacts")
return DownloadArtifactsToolOutput(result=DownloadArtifactsResult(target_path=target_path))
18 changes: 5 additions & 13 deletions ymir/tools/privileged/tests/unit/test_copr.py
Original file line number Diff line number Diff line change
Expand Up @@ -170,9 +170,8 @@ async def sleep(*_):
],
)
@pytest.mark.asyncio
async def test_download_artifacts(url, tmp_path):
async def test_download_artifacts(url):
artifacts_urls = [url]
target_path = tmp_path
content = b"12345"
content_gz = b"\x1f\x8b\x00\x00"

Expand All @@ -189,19 +188,12 @@ async def read():
)
if "broken" in url:
with pytest.raises(ToolError):
await DownloadArtifactsTool().run(
input={"artifacts_urls": artifacts_urls, "target_path": target_path}
)
await DownloadArtifactsTool().run(input={"artifacts_urls": artifacts_urls})
else:
out = await DownloadArtifactsTool().run(
input={"artifacts_urls": artifacts_urls, "target_path": target_path}
)
out = await DownloadArtifactsTool().run(input={"artifacts_urls": artifacts_urls})
result = out.result
assert result.startswith("Successfully")
path = target_path / url.rsplit("/", 1)[-1].removesuffix(".gz")
if "broken" in url:
assert not path.is_file()
else:
target_path = Path(result.target_path)
path = target_path / url.rsplit("/", 1)[-1].removesuffix(".gz")
assert path.read_bytes() == content
Comment on lines +195 to 197

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The unit test currently leaks the dynamically created temporary directory on successful runs. Since DownloadArtifactsTool now creates a temporary directory using tempfile.mkdtemp() and returns it, the test should ensure that this directory is cleaned up after assertions are complete.

Suggested change
target_path = Path(result.target_path)
path = target_path / url.rsplit("/", 1)[-1].removesuffix(".gz")
assert path.read_bytes() == content
import shutil
target_path = Path(result.target_path)
try:
path = target_path / url.rsplit("/", 1)[-1].removesuffix(".gz")
assert path.read_bytes() == content
finally:
shutil.rmtree(target_path, ignore_errors=True)
References
  1. Ensure temporary resources created during test execution are cleaned up to prevent resource leaks on the host system.



Expand Down
2 changes: 1 addition & 1 deletion ymir/tools/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "hatchling.build"

[project]
name = "ymir-tools"
version = "0.6.1"
version = "0.7.0"
description = "Ymir MCP tools for AI workflows"
requires-python = ">=3.13"
dynamic = ["dependencies"]
Expand Down
Loading