Skip to content
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,5 @@ FEATURE_SUMMARY.md
GEMINI.md
QWEN.md
.omx/
.opencode/
CODEBUDDY.md
48 changes: 48 additions & 0 deletions swanlab/sdk/internal/core_python/api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,54 @@
@file: __init__.py
@time: 2026/3/7 18:19
@description: SwanLab 运行时 API 封装

绝大多数API使用 Client 对象,少部分API使用requests库直接调用
我们以rpc风格封装API,方便调用
"""

from .experiment import (
create_or_resume_experiment,
delete_experiment,
get_experiment_metrics,
get_project_experiments,
get_single_experiment,
send_experiment_heartbeat,
update_experiment_state,
)
from .project import delete_project, get_or_create_project, get_project, get_workspace_projects
from .self_hosted import create_user, get_self_hosted_init, get_users
from .user import (
create_api_key,
delete_api_key,
get_api_keys,
get_latest_api_key,
get_user_groups,
get_workspace_info,
)

__all__ = [
# experiment
"create_or_resume_experiment",
"send_experiment_heartbeat",
"update_experiment_state",
"get_project_experiments",
"get_single_experiment",
"get_experiment_metrics",
"delete_experiment",
# project
"get_project",
"get_or_create_project",
"get_workspace_projects",
"delete_project",
# user
"create_api_key",
"delete_api_key",
"get_user_groups",
"get_workspace_info",
"get_api_keys",
"get_latest_api_key",
# self_hosted
"get_self_hosted_init",
"create_user",
"get_users",
]
99 changes: 95 additions & 4 deletions swanlab/sdk/internal/core_python/api/experiment.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,17 @@
@description: SwanLab 运行时实验API
"""

from typing import List, Literal, Optional
from typing import Dict, List, Literal, Optional, Union

from google.protobuf.timestamp_pb2 import Timestamp

from swanlab.exceptions import ApiError
from swanlab.proto.swanlab.run.v1.run_pb2 import RUN_STATE_ABORTED, RUN_STATE_CRASHED, RunState
from swanlab.sdk.internal.core_python import client
from swanlab.sdk.internal.pkg import helper
from swanlab.sdk.typings.core_python.api.experiment import InitExperimentType
from swanlab.sdk.typings.run import ResumeType
from swanlab.sdk.typings.core_python.api.experiment import InitExperimentType, RunType
from swanlab.sdk.typings.run import ResumeType, RunStateType
from swanlab.utils import parse_column_type, to_camel_case


def create_or_resume_experiment(
Expand Down Expand Up @@ -83,11 +84,101 @@ def stop_experiment(username: str, project: str, cuid: str, *, state: RunState,
this_state = "CRASHED"
elif state == RUN_STATE_ABORTED:
this_state = "ABORTED"
client.put(
resp = client.put(
f"/project/{username}/{project}/runs/{cuid}/state",
{
"state": this_state,
"finishedAt": finished_at.ToDatetime().isoformat() + "Z",
"from": "sdk",
},
)
return resp.raw.status_code == 201
Comment thread
Nexisato marked this conversation as resolved.
Outdated


def send_experiment_heartbeat(*, cuid: str, flag_id: str) -> None:
"""
发送实验心跳,保持实验处于活跃状态
:param cuid: 实验唯一标识符
:param flag_id: 实验标记ID
"""
client.post(f"/house/experiments/{cuid}/heartbeat", {"flagId": flag_id})


def update_experiment_state(
*,
username: str,
projname: str,
cuid: str,
state: RunStateType,
finished_at: Optional[str] = None,
) -> None:
"""
更新实验状态
:param username: 实验所属用户名
:param projname: 实验所属项目名称
:param cuid: 实验唯一标识符
:param state: 实验状态
:param finished_at: 实验结束时间,格式为 ISO 8601,如果不提供则使用当前时间
"""
put_data = {
"state": state,
"finishedAt": finished_at,
"from": "sdk",
}
put_data = {k: v for k, v in put_data.items() if v is not None}
client.put(f"/project/{username}/{projname}/runs/{cuid}/state", put_data)


def get_project_experiments(
*,
path: str,
filters: Optional[Dict[str, object]] = None,
) -> Union[List[RunType], Dict[str, List[RunType]]]:
"""
获取指定项目下的所有实验信息
若有实验分组,则返回一个字典,使用时需递归展平实验数据
:param path: 项目路径 username/project
:param filters: 筛选实验的条件,可选
"""
parsed_filters = (
[
{
"key": to_camel_case(key) if parse_column_type(key) == "STABLE" else key.split(".", 1)[-1],
"active": True,
"value": [value],
"op": "EQ",
"type": parse_column_type(key),
Comment thread
Nexisato marked this conversation as resolved.
Outdated
}
for key, value in filters.items()
]
if filters
else []
)
return client.post(f"/project/{path}/runs/shows", data={"filters": parsed_filters}).data


def get_single_experiment(*, path: str) -> RunType:
"""
获取指定实验信息
:param path: 实验路径 username/project/expid
"""
proj_path, expid = path.rsplit("/", 1)
return client.get(f"/project/{proj_path}/runs/{expid}").data


def get_experiment_metrics(*, expid: str, key: str) -> Dict[str, str]:
"""
获取指定字段的指标数据,返回csv网址
:param expid: 实验cuid
:param key: 指定字段列表
"""
return client.get(f"/experiment/{expid}/column/csv", params={"key": key}).data


def delete_experiment(*, path: str) -> None:
"""
删除指定实验
:param path: 实验路径 'username/project/expid'
"""
proj_path, expid = path.rsplit("/", 1)
client.delete(f"/project/{proj_path}/runs/{expid}")
34 changes: 32 additions & 2 deletions swanlab/sdk/internal/core_python/api/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,12 @@
from swanlab.sdk.internal.core_python import client
from swanlab.sdk.internal.pkg import helper
from swanlab.sdk.internal.pkg.client.utils import decode_response
from swanlab.sdk.typings.core_python.api.project import InitProjectType, ProjectType
from swanlab.sdk.typings.core_python.api.project import InitProjectType, ProjectType, ProjResponseType


def get_project(*, username: str, name: str) -> ProjectType:
"""
获取项目信息
获取项目详情信息
:param username: 项目所属的用户名
:param name: 项目名称
:return: 项目信息
Expand All @@ -42,3 +42,33 @@ def get_or_create_project(*, username: Optional[str], name: str, public: bool) -
else:
# 此接口为后端处理,sdk 在理论上不会出现其他错误,因此不需要处理其他错误
raise e


def get_workspace_projects(
*,
path: str,
page: int = 1,
size: int = 20,
sort: Optional[str] = None,
search: Optional[str] = None,
detail: Optional[bool] = True,
) -> ProjResponseType:
"""
获取指定页数和条件下的项目信息
:param path: 工作空间名称
:param page: 页码
:param size: 每页项目数量
:param sort: 排序规则, 可选
:param search: 搜索的项目名称关键字, 可选
:param detail: 是否包含项目下实验的相关信息, 可选, 默认为true
"""
params = {"page": page, "size": size, "sort": sort, "search": search, "detail": detail}
return client.get(f"/project/{path}", params=helper.strip_none(params, strip_empty_str=True)).data


def delete_project(*, path: str) -> None:
"""
删除指定项目
:param path: 项目路径 'username/project'
"""
client.delete(f"/project/{path}")
36 changes: 36 additions & 0 deletions swanlab/sdk/internal/core_python/api/self_hosted.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
"""
@author: cunyue
@file: self_hosted.py
@time: 2026/4/14 19:00
@description: SwanLab 私有化部署API
"""

from swanlab.sdk.internal.core_python import client
from swanlab.sdk.typings.core_python.api.user import SelfHostedInfoType


def get_self_hosted_init() -> SelfHostedInfoType:
"""
获取私有化部署信息
"""
return client.get("/self_hosted/info").data


def create_user(*, username: str, password: str) -> None:
"""
添加用户(私有化管理员限定)
:param username: 用户名
:param password: 用户密码
"""
data = {"users": [{"username": username, "password": password}]}
client.post("/self_hosted/users", data=data)


def get_users(*, page: int = 1, size: int = 20):
"""
分页获取用户(管理员限定)
:param page: 页码
:param size: 每页大小
"""
params = {"page": page, "size": size}
return client.get("/self_hosted/users", params=params).data
58 changes: 58 additions & 0 deletions swanlab/sdk/internal/core_python/api/user.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
"""
@author: cunyue
@file: user.py
@time: 2026/4/14 19:00
@description: SwanLab 运行时用户API
"""

from typing import List, Optional

from swanlab.sdk.internal.core_python import client
from swanlab.sdk.typings.core_python.api.user import ApiKeyType, GroupType
from swanlab.sdk.typings.core_python.api.workspace import WorkspaceInfoType


def create_api_key(*, name: Optional[str] = None) -> None:
"""
创建一个api_key
:param name: api_key 的名称
"""
client.post("/user/key", data={"name": name} if name else None)


def delete_api_key(*, key_id: int) -> None:
"""
删除指定id的api_key
:param key_id: api_key的id
"""
client.delete(f"/user/key/{key_id}")


def get_user_groups(*, username: str) -> List[GroupType]:
"""
获取用户加入的组织
:param username: 用户名称
"""
return client.get(f"/user/{username}/groups").data


def get_workspace_info(*, path: str) -> WorkspaceInfoType:
"""
获取指定工作空间的信息
:param path: 工作空间名称
"""
return client.get(f"/group/{path}").data


def get_api_keys() -> List[ApiKeyType]:
"""
获取当前全部的api_key
"""
return client.get("/user/key").data


def get_latest_api_key() -> ApiKeyType:
"""
获取最新的api_key
"""
return client.get("/user/key/latest").data
18 changes: 18 additions & 0 deletions swanlab/sdk/typings/core_python/api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,21 @@
@description: SwanLab API类型提示
所有后端响应类型命名以 Response 结尾
"""

from .common import LabelType
from .experiment import RunType
from .project import InitProjectType, ProjectType, ProjResponseType
from .user import ApiKeyType, GroupType, SelfHostedInfoType
from .workspace import WorkspaceInfoType

__all__ = [
"RunType",
"ProjectType",
"InitProjectType",
"ProjResponseType",
"LabelType",
"GroupType",
"ApiKeyType",
"SelfHostedInfoType",
"WorkspaceInfoType",
]
7 changes: 7 additions & 0 deletions swanlab/sdk/typings/core_python/api/common.py
Comment thread
Nexisato marked this conversation as resolved.
Outdated
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
"""Common Type Definition"""

from typing import TypedDict


class LabelType(TypedDict):
name: str
Loading
Loading