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
23 changes: 21 additions & 2 deletions sdk/python/openviking_sdk/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -691,6 +691,8 @@ async def add_resource(
add_type: Optional[str] = None,
tags: Optional[List[str]] = None,
tag_mode: str = "replace",
create_parent: bool = False,
source_name: Optional[str] = None,
) -> Dict[str, Any]:
if add_type is not None:
add_type = add_type.strip() or None
Expand Down Expand Up @@ -725,18 +727,26 @@ async def add_resource(
request_data["tag_mode"] = tag_mode
if preserve_structure is not None:
request_data["preserve_structure"] = preserve_structure
# Match CLI: only send create_parent when True so older servers that
# forbid unknown fields still accept the request.
if create_parent:
request_data["create_parent"] = True
if source_name is not None:
request_data["source_name"] = source_name

path_obj = Path(path)
if not add_type and path_obj.exists():
if path_obj.is_dir():
request_data["source_name"] = path_obj.name
if source_name is None:
request_data["source_name"] = path_obj.name
zip_path = self._zip_directory(path)
try:
request_data["temp_file_id"] = await self._upload_temp_file(zip_path)
finally:
Path(zip_path).unlink(missing_ok=True)
elif path_obj.is_file():
request_data["source_name"] = path_obj.name
if source_name is None:
request_data["source_name"] = path_obj.name
request_data["temp_file_id"] = await self._upload_temp_file(path)
else:
request_data["path"] = path
Expand Down Expand Up @@ -771,10 +781,13 @@ async def add_skill(
timeout: Optional[float] = None,
telemetry: Any = False,
target_uri: Optional[str] = None,
source_metadata: Optional[Dict[str, Any]] = None,
) -> Dict[str, Any]:
request_data = {"wait": wait, "timeout": timeout, "telemetry": telemetry}
if target_uri is not None:
request_data["target_uri"] = target_uri
if source_metadata is not None:
request_data["source_metadata"] = source_metadata
if isinstance(data, str):
path_obj = Path(data)
if path_obj.exists():
Expand Down Expand Up @@ -1946,6 +1959,8 @@ def add_resource(
add_type: Optional[str] = None,
tags: Optional[List[str]] = None,
tag_mode: str = "replace",
create_parent: bool = False,
source_name: Optional[str] = None,
) -> Dict[str, Any]:
return run_async(
self._async_client.add_resource(
Expand All @@ -1968,6 +1983,8 @@ def add_resource(
args=args,
tags=tags,
tag_mode=tag_mode,
create_parent=create_parent,
source_name=source_name,
telemetry=telemetry,
)
)
Expand All @@ -1989,6 +2006,7 @@ def add_skill(
timeout: Optional[float] = None,
telemetry: Any = False,
target_uri: Optional[str] = None,
source_metadata: Optional[Dict[str, Any]] = None,
) -> Dict[str, Any]:
return run_async(
self._async_client.add_skill(
Expand All @@ -1997,6 +2015,7 @@ def add_skill(
timeout=timeout,
telemetry=telemetry,
target_uri=target_uri,
source_metadata=source_metadata,
)
)

Expand Down
99 changes: 99 additions & 0 deletions sdk/python/tests/test_async_client_behaviors.py
Original file line number Diff line number Diff line change
Expand Up @@ -804,6 +804,105 @@ async def test_add_resource_sends_tags_and_tag_mode():
)


@pytest.mark.asyncio
async def test_add_resource_forwards_create_parent_and_source_name():
client = AsyncHTTPClient(url="http://localhost:1933")
fake_http = SimpleNamespace(post=AsyncMock(return_value=object()))
client._http = fake_http
client._handle_response_data = lambda _response: {
"result": {"root_uri": "viking://user/resources/docs/demo"}
}

await client.add_resource(
path="https://example.com/demo.md",
parent="viking://user/resources/docs",
create_parent=True,
source_name="custom-demo.md",
)

payload = fake_http.post.await_args.kwargs["json"]
assert payload["parent"] == "viking://user/resources/docs"
assert payload["create_parent"] is True
assert payload["source_name"] == "custom-demo.md"


@pytest.mark.asyncio
async def test_add_resource_omits_default_create_parent_for_legacy_servers():
client = AsyncHTTPClient(url="http://localhost:1933")
fake_http = SimpleNamespace(post=AsyncMock(return_value=object()))
client._http = fake_http
client._handle_response_data = lambda _response: {
"result": {"root_uri": "viking://resources/demo"}
}

await client.add_resource("https://example.com/demo.md")

payload = fake_http.post.await_args.kwargs["json"]
assert "create_parent" not in payload
assert "source_name" not in payload


@pytest.mark.asyncio
async def test_add_resource_explicit_source_name_overrides_local_filename(tmp_path):
resource_file = tmp_path / "demo.md"
resource_file.write_text("# Demo\n")

client = AsyncHTTPClient(url="http://127.0.0.1:1933")
fake_http = SimpleNamespace(post=AsyncMock(return_value=object()))
client._http = fake_http
client._upload_temp_file = AsyncMock(return_value="upload_resource.md")
client._handle_response_data = lambda _response: {
"result": {"root_uri": "viking://resources/demo"}
}

await client.add_resource(str(resource_file), source_name="renamed.md")

payload = fake_http.post.await_args.kwargs["json"]
assert payload["temp_file_id"] == "upload_resource.md"
assert payload["source_name"] == "renamed.md"


def test_sync_add_resource_accepts_and_forwards_create_parent():
client = SyncHTTPClient(url="http://localhost:1933")
with patch.object(
client._async_client,
"add_resource",
new=AsyncMock(return_value={"root_uri": "viking://resources/demo"}),
) as mock_add_resource:
result = client.add_resource(
"https://example.com/demo.md",
parent="viking://user/resources/docs",
create_parent=True,
source_name="custom-demo.md",
)

assert result == {"root_uri": "viking://resources/demo"}
assert mock_add_resource.await_args.kwargs["create_parent"] is True
assert mock_add_resource.await_args.kwargs["source_name"] == "custom-demo.md"
assert mock_add_resource.await_args.kwargs["parent"] == "viking://user/resources/docs"


@pytest.mark.asyncio
async def test_add_skill_forwards_source_metadata():
client = AsyncHTTPClient(url="http://localhost:1933")
fake_http = SimpleNamespace(post=AsyncMock(return_value=object()))
client._http = fake_http
client._handle_response_data = lambda _response: {"result": {"status": "ok"}}

await client.add_skill(
{"name": "demo", "description": "demo"},
source_metadata={"type": "api", "source": "inline_content", "operation": "add"},
)

payload = fake_http.post.await_args.kwargs["json"]
assert payload["source_metadata"] == {
"type": "api",
"source": "inline_content",
"operation": "add",
}
assert payload["data"] == {"name": "demo", "description": "demo"}


@pytest.mark.asyncio
async def test_find_uses_node_limit_as_http_limit_and_normalizes_target_uri_list():
client = AsyncHTTPClient(url="http://localhost:1933")
Expand Down