Skip to content
Merged
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
59 changes: 59 additions & 0 deletions src/arkruntime/resources/skills/skills.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from ..._resource import AsyncAPIResource, SyncAPIResource
from ..._types import NOT_GIVEN, NotGiven
from ...types.skill.skill import Skill
from ...types.skill.skill_version import SkillVersion

__all__ = ["Skills", "AsyncSkills"]

Expand Down Expand Up @@ -70,6 +71,35 @@ def retrieve(self, skill_id: str, *, extra_headers=None, extra_query=None, extra
cast_to=Skill,
)

def create_version(
self,
skill_id: str,
*,
files: FileArg,
display_title: Optional[str] | NotGiven = NOT_GIVEN,
extra_headers=None,
extra_query=None,
extra_body=None,
timeout=None,
) -> SkillVersion:
if not skill_id:
raise ValueError("skill_id is required")
body: dict = {}
if display_title is not NOT_GIVEN and display_title is not None:
body["display_title"] = display_title
return self._post(
f"{_PREFIX}/{skill_id}/versions",
body=body,
files=_files_kwarg(files),
options=make_request_options(
extra_headers=_multipart_headers(extra_headers),
extra_query=extra_query,
extra_body=extra_body,
timeout=timeout,
),
cast_to=SkillVersion,
)


class AsyncSkills(AsyncAPIResource):
async def create(
Expand Down Expand Up @@ -110,3 +140,32 @@ async def retrieve(
),
cast_to=Skill,
)

async def create_version(
self,
skill_id: str,
*,
files: FileArg,
display_title: Optional[str] | NotGiven = NOT_GIVEN,
extra_headers=None,
extra_query=None,
extra_body=None,
timeout=None,
) -> SkillVersion:
if not skill_id:
raise ValueError("skill_id is required")
body: dict = {}
if display_title is not NOT_GIVEN and display_title is not None:
body["display_title"] = display_title
return await self._post(
f"{_PREFIX}/{skill_id}/versions",
body=body,
files=_files_kwarg(files),
options=make_request_options(
extra_headers=_multipart_headers(extra_headers),
extra_query=extra_query,
extra_body=extra_body,
timeout=timeout,
),
cast_to=SkillVersion,
)
4 changes: 4 additions & 0 deletions src/arkruntime/types/skill/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,15 @@
from __future__ import annotations

from .create_skill_request import CreateSkillRequest
from .create_skill_version_request import CreateSkillVersionRequest
from .skill import Skill
from .skill_version import SkillVersion

__all__ = [
"CreateSkillRequest",
"CreateSkillVersionRequest",
"Skill",
"SkillVersion",
]

# Hand-written extras (preserved across regen via Makefile rsync --exclude=*_shim.py).
Expand Down
32 changes: 32 additions & 0 deletions src/arkruntime/types/skill/create_skill_version_request.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Generated by datamodel-code-generator from ark-apis typespec/openapi.
# DO NOT EDIT — regenerate with `make gen-py-<api>` in ark-apis.
#
# Copyright (c) 2026 ByteDance Ltd. and/or its affiliates.
# SPDX-License-Identifier: Apache-2.0

from __future__ import annotations

from typing import List, Optional

from typing_extensions import Annotated

from arkruntime._models import BaseModel
from pydantic import Field


class CreateSkillVersionRequest(BaseModel):
display_title: Optional[str] = None
"""
Skill 版本展示名。
"""
files: Optional[List[object]] = None
"""
新版本技能包内容。可以是单个 `.zip`,也可以是一组带相对路径的松散文件;
该字段名允许在同一次请求中重复出现以传多个文件。

与 `files[]` 语义等价,两者至少提供其一;同时提供时服务端会合并处理。
"""
files__: Annotated[Optional[List[object]], Field(alias="files[]")] = None
"""
`files` 的可重复字段名别名,语义完全等价。
"""
49 changes: 49 additions & 0 deletions src/arkruntime/types/skill/skill_version.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Generated by datamodel-code-generator from ark-apis typespec/openapi.
# DO NOT EDIT — regenerate with `make gen-py-<api>` in ark-apis.
#
# Copyright (c) 2026 ByteDance Ltd. and/or its affiliates.
# SPDX-License-Identifier: Apache-2.0

from __future__ import annotations

from typing import Literal, Optional

from arkruntime._models import BaseModel


class SkillVersion(BaseModel):
"""
Skill 版本摘要。

CreateSkillVersion / ListSkillVersions / GetSkillVersion 返回该对象;主
Skill 的 latest_version 会由服务端在新版本创建成功后推进。
"""

id: str
"""
SkillVersion ID,形如 `skillver-...`。
"""
type: Literal["skill_version"]
"""
固定 `"skill_version"`。
"""
skill_id: str
"""
归属 Skill ID。
"""
version: str
"""
版本号,服务端按字符串形式自增,例如 `"1"`、`"2"`。
"""
display_title: str
"""
该版本的展示名。
"""
description: Optional[str] = None
"""
该版本的描述信息。
"""
created_at: str
"""
创建时间,RFC 3339 UTC 秒级精度。
"""
79 changes: 79 additions & 0 deletions tests/test_skills.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# Copyright (c) 2026 ByteDance Ltd. and/or its affiliates.
# SPDX-License-Identifier: Apache-2.0

from __future__ import annotations

import pytest

from arkruntime.resources.skills.skills import AsyncSkills, Skills


class _SyncClient:
def __init__(self) -> None:
self.request: dict[str, object] | None = None

def post(self, *args: object, **kwargs: object) -> dict[str, object]:
self.request = {"args": args, **kwargs}
return {}

def post_without_retry(self, *args: object, **kwargs: object) -> dict[str, object]:
return self.post(*args, **kwargs)

def get(self, *args: object, **kwargs: object) -> dict[str, object]:
return {}

def delete(self, *args: object, **kwargs: object) -> dict[str, object]:
return {}

def get_api_list(self, *args: object, **kwargs: object) -> list[object]:
return []


class _AsyncClient:
def __init__(self) -> None:
self.request: dict[str, object] | None = None

async def post(self, *args: object, **kwargs: object) -> dict[str, object]:
self.request = {"args": args, **kwargs}
return {}

async def post_without_retry(self, *args: object, **kwargs: object) -> dict[str, object]:
return await self.post(*args, **kwargs)

async def get(self, *args: object, **kwargs: object) -> dict[str, object]:
return {}

async def delete(self, *args: object, **kwargs: object) -> dict[str, object]:
return {}

async def get_api_list(self, *args: object, **kwargs: object) -> list[object]:
return []


def test_create_version_builds_multipart_request() -> None:
client = _SyncClient()
Skills(client).create_version(
"skill-1",
files=("skill.zip", b"zip-bytes"),
display_title="Readiness Skill v2",
)

assert client.request is not None
assert client.request["args"] == ("/skills/skill-1/versions",)
assert client.request["body"] == {"display_title": "Readiness Skill v2"}
assert client.request["files"] == {"files": ("skill.zip", b"zip-bytes")}


@pytest.mark.asyncio
async def test_async_create_version_builds_multipart_request() -> None:
client = _AsyncClient()
await AsyncSkills(client).create_version(
"skill-1",
files=("skill.zip", b"zip-bytes"),
display_title="Readiness Skill v2",
)

assert client.request is not None
assert client.request["args"] == ("/skills/skill-1/versions",)
assert client.request["body"] == {"display_title": "Readiness Skill v2"}
assert client.request["files"] == {"files": ("skill.zip", b"zip-bytes")}
Loading