diff --git a/.gitignore b/.gitignore index 9b6769052..bce32a1b6 100644 --- a/.gitignore +++ b/.gitignore @@ -56,3 +56,4 @@ FEATURE_SUMMARY.md GEMINI.md QWEN.md .omx/ +CODEBUDDY.md diff --git a/pyproject.toml b/pyproject.toml index 09e0d88d2..d614c6b90 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -49,6 +49,8 @@ dependencies = [ "protobuf>=3.19.0,!=4.21.0,!=5.28.0,<7; sys_platform != 'linux'", "rich>=13.6.0,<14.0.0", "pydantic-settings>=2.8.1", + "orjson<=3.11.5; python_version == '3.9'", + "orjson; python_version > '3.9'", ] # 对应 requirements-media.txt 和 requirements-dashboard.txt [cite: 3, 5] diff --git a/swanlab/__init__.py b/swanlab/__init__.py index e8039572d..55de5e63d 100644 --- a/swanlab/__init__.py +++ b/swanlab/__init__.py @@ -1,3 +1,4 @@ +from swanlab.api import Api from swanlab.sdk import ( Audio, Callback, @@ -28,6 +29,8 @@ __version__ = helper.get_swanlab_version() __all__ = [ + # api + "Api", # cmd "merge_settings", "init", diff --git a/swanlab/api/__init__.py b/swanlab/api/__init__.py new file mode 100644 index 000000000..a4aed3843 --- /dev/null +++ b/swanlab/api/__init__.py @@ -0,0 +1,229 @@ +""" +@author: caddiesnew +@file: __init__.py +@time: 2026/4/20 +@description: SwanLab 公共查询 API 入口,面向用户的 OOP 查询接口 +""" + +from typing import Optional + +from swanlab.exceptions import AuthenticationError +from swanlab.sdk.internal.pkg import nrc, scope +from swanlab.sdk.internal.pkg.client import Client +from swanlab.sdk.internal.settings import settings as global_settings + +from .base import BaseEntity +from .experiment import Experiment, Experiments +from .project import Project, Projects +from .selfhosted import User, Users +from .typings.common import ApiResponseType +from .workspace import Workspace, Workspaces + + +class Api(BaseEntity): + """ + SwanLab 公共查询 API 入口。 + + 通过独立的 Client 实例与 SwanLab 云端交互,不与 SDK 运行时单例共享。 + 继承 BaseEntity 以复用 _get/_post/_put/_delete/_paginate 等安全 HTTP 方法。 + + 用法:: + + from swanlab import Api + + api = Api() # 自动从 .netrc 读取凭证 + api = Api(api_key="...", host="...") # 显式传入凭证 + + resp = api.project("username/project") + if resp.ok: + project = resp.data + print(project.name) + """ + + def __init__( + self, + api_key: Optional[str] = None, + host: Optional[str] = None, + web_host: Optional[str] = None, + ) -> None: + """ + 初始化 Api 实例。 + + 认证优先级:显式参数 > Settings(含 .netrc / 环境变量) + + :param api_key: API 密钥,为 None 时从 Settings / .netrc / 环境变量读取 + :param host: API 主机地址,为 None 时从 Settings 读取 + :param web_host: Web 面板地址,为 None 时从 Settings 读取 + """ + api_key, api_host, resolved_web_host = self._resolve_credentials(api_key, host, web_host) + client = Client(api_key=str(api_key), base_url=api_host) + super().__init__(client, resolved_web_host, api_host) + self._login_resp = scope.get_context("login_resp") + self._username: str = self._login_resp["userInfo"]["username"] if self._login_resp else "" + + def to_dict(self) -> dict: + """Api 非数据实体,返回空字典。""" + return {} + + @staticmethod + def _resolve_credentials( + api_key: Optional[str], + host: Optional[str], + web_host: Optional[str], + ) -> tuple[str, str, str]: + """ + 按优先级解析凭证:显式参数 > Settings(含 .netrc / 环境变量)。 + 返回 (api_key, api_host, web_host)。 + """ + if api_key is None: + api_key = global_settings.api_key + if api_key is None: + raise AuthenticationError("No API key found. Please login with `swanlab login` or pass api_key parameter.") + + api_host: str = nrc.fmt(host) if host is not None else global_settings.api_host + resolved_web_host: str = nrc.fmt(web_host) if web_host is not None else global_settings.web_host + + return api_key, api_host, resolved_web_host + + # ------------------------------------------------------------------ + # 实体查询方法 — 统一返回 ApiResponse + # ------------------------------------------------------------------ + + def workspace(self, username: Optional[str] = None) -> ApiResponseType: + """ + 获取工作空间信息,默认为当前登录用户的工作空间。 + + :param username: 指定工作空间用户名,为 None 时使用当前登录用户 + """ + if username is None: + username = self._username + resp = self._get(f"/group/{username}") + if resp.ok: + return ApiResponseType( + ok=True, + data=Workspace( + self._client, + self._web_host, + self._api_host, + username=username, + data=resp.data, + ), + ) + return resp + + def workspaces(self, username: Optional[str] = None) -> ApiResponseType: + """ + 获取工作空间列表迭代器。 + + :param username: 指定用户名,为 None 时使用当前登录用户 + """ + if username is None: + username = self._username + return ApiResponseType( + ok=True, + data=Workspaces(self._client, self._web_host, self._api_host, username=username), + ) + + def project(self, path: str) -> ApiResponseType: + """ + 获取项目信息。 + + :param path: 项目路径,格式为 'username/project-name' + """ + resp = self._get(f"/project/{path}") + if resp.ok: + return ApiResponseType( + ok=True, + data=Project(self._client, self._web_host, self._api_host, path=path, data=resp.data), + ) + return resp + + def projects( + self, + path: str, + sort: Optional[str] = None, + search: Optional[str] = None, + detail: Optional[bool] = True, + ) -> ApiResponseType: + """ + 获取工作空间下的项目列表迭代器。 + + :param path: 工作空间名称 'username' + :param sort: 排序方式 + :param search: 搜索关键词 + :param detail: 是否返回详细信息 + """ + return ApiResponseType( + ok=True, + data=Projects( + self._client, self._web_host, self._api_host, path=path, sort=sort, search=search, detail=detail + ), + ) + + def run(self, path: str) -> ApiResponseType: + """ + 获取单个实验。 + + :param path: 实验路径,格式为 'username/project/run_id' + """ + parts = path.split("/") + if len(parts) != 3: + return ApiResponseType( + ok=False, errmsg=f"Invalid path '{path}'. Expected format: 'username/project/run_id'" + ) + proj_path = path.rsplit("/", 1)[0] + expid = parts[2] + resp = self._get(f"/project/{proj_path}/runs/{expid}") + if resp.ok: + return ApiResponseType( + ok=True, + data=Experiment( + self._client, + self._web_host, + self._api_host, + path=proj_path, + data=resp.data, + ), + ) + return resp + + def runs(self, path: str, filters: Optional[dict] = None) -> ApiResponseType: + """ + 获取项目下的实验列表迭代器。 + + :param path: 项目路径,格式为 'username/project' + :param filters: 筛选条件 + """ + return ApiResponseType( + ok=True, + data=Experiments(self._client, self._web_host, self._api_host, path=path, filters=filters), + ) + + def user(self, username: Optional[str] = None) -> ApiResponseType: + """ + 获取用户信息,默认为当前登录用户。 + + :param username: 指定用户名 + """ + return ApiResponseType( + ok=True, + data=User( + self._client, + self._web_host, + self._api_host, + username=username or self._username, + login_user=self._username, + ), + ) + + def users(self) -> ApiResponseType: + """ + 获取用户列表迭代器(私有化部署管理员限定)。 + """ + return ApiResponseType( + ok=True, + data=Users(self._client, self._web_host, self._api_host, login_user=self._username), + ) + + +__all__ = ["Api"] diff --git a/swanlab/api/base.py b/swanlab/api/base.py new file mode 100644 index 000000000..8ea6fa13d --- /dev/null +++ b/swanlab/api/base.py @@ -0,0 +1,90 @@ +""" +@author: caddiesnew +@file: base.py +@time: 2026/4/20 +@description: 所有实体类的公共基类 +""" + +from abc import ABC, abstractmethod +from typing import TYPE_CHECKING, Any, Callable, Dict, Iterator, Optional + +from swanlab.sdk.internal.pkg import safe + +from .typings.common import ApiResponseType + +if TYPE_CHECKING: + from swanlab.sdk.internal.pkg.client import Client + + +class BaseEntity(ABC): + """ + swanlab/api 实体类公共基类。 + + 统一持有 _client、_web_host 和 _api_host,提供 _get/_post/_put/_delete HTTP 快捷方法和 _paginate 分页迭代。 + 所有 HTTP 请求通过 _safe_request 包裹,保证任何异常都不会导致程序 crash,统一返回 ApiResponse。 + 子类只需实现 to_dict() 和业务逻辑。 + """ + + def __init__(self, client: "Client", web_host: str, api_host: str) -> None: + self._client: "Client" = client + self._web_host: str = web_host + self._api_host: str = api_host + self._errors: list[str] = [] + + @abstractmethod + def to_dict(self) -> Dict[str, Any]: + """将实体序列化为 JSON 可序列化的字典。""" + + def _safe_request(self, method: Callable, path: str, **kwargs) -> ApiResponseType: + """安全请求包装:捕获所有异常,始终返回 ApiResponse 而不抛出。""" + + def _on_error(e: BaseException) -> None: + _err_msg[0] = str(e) + + _err_msg: list[Optional[str]] = [None] + with safe.block(message=f"API request failed: {path}", on_error=_on_error): + data = method(path, **kwargs).data + return ApiResponseType(ok=True, data=data) + result = ApiResponseType(ok=False, errmsg=_err_msg[0] or "request failed") + self._errors.append(result.errmsg) + return result + + def _get(self, path: str, **kwargs) -> ApiResponseType: + return self._safe_request(self._client.get, path, **kwargs) + + def _post(self, path: str, **kwargs) -> ApiResponseType: + return self._safe_request(self._client.post, path, **kwargs) + + def _put(self, path: str, **kwargs) -> ApiResponseType: + return self._safe_request(self._client.put, path, **kwargs) + + def _delete(self, path: str, **kwargs) -> ApiResponseType: + return self._safe_request(self._client.delete, path, **kwargs) + + def _build_url(self, path: str) -> str: + return f"{self._api_host}/{path}" + + def _paginate(self, path: str, *, page_size: int = 20, params: Optional[dict] = None) -> Iterator[dict]: + """通用分页迭代器,自动处理 page/size 参数。""" + page = 1 + while True: + p = {"page": page, "size": page_size} + if params: + p.update({k: v for k, v in params.items() if v is not None}) + resp = self._get(path, params=p) + if not resp.ok: + return + body = resp.data + items = body.get("list", []) if isinstance(body, dict) else body + if not items: + break + yield from items + total_pages = body.get("pages", 1) if isinstance(body, dict) else 1 + if page >= total_pages: + break + page += 1 + + def __repr__(self) -> str: + cls = self.__class__.__name__ + ident = getattr(self, "_path", None) or getattr(self, "_username", None) or "?" + return f"{cls}('{ident}')" diff --git a/swanlab/api/experiment.py b/swanlab/api/experiment.py new file mode 100644 index 000000000..4a41d3441 --- /dev/null +++ b/swanlab/api/experiment.py @@ -0,0 +1,280 @@ +""" +@author: caddiesnew +@file: experiment.py +@time: 2026/4/20 +@description: Experiment 实体类 — 单个实验的查询与操作 +""" + +from typing import TYPE_CHECKING, Any, Dict, Iterator, List, Optional, Union, cast + +from swanlab.utils import parse_column_type, to_camel_case + +from .base import BaseEntity +from .typings.experiment import ApiExperimentType, ApiExperimentUserType +from .utils import Label, get_properties + +if TYPE_CHECKING: + from swanlab.sdk.internal.pkg.client import Client + + +class Profile: + """Experiment profile containing config, metadata, requirements, and conda info.""" + + def __init__(self, data: Dict) -> None: + self._data = data + + @staticmethod + def _clean_field(value: Any) -> Any: + """Recursively clean config field, removing desc/sort and keeping value.""" + if isinstance(value, dict): + if "value" in value: + return Profile._clean_field(value["value"]) + else: + return {k: Profile._clean_field(v) for k, v in value.items()} + elif isinstance(value, list): + return [Profile._clean_field(item) for item in value] + return value + + @property + def config(self) -> Dict: + """Experiment configuration (cleaned, without desc/sort fields).""" + raw_config = self._data.get("config", {}) + return {k: Profile._clean_field(v) for k, v in raw_config.items()} if isinstance(raw_config, dict) else {} + + @property + def metadata(self) -> Dict: + return self._data.get("metadata", {}) + + @property + def requirements(self) -> str: + return self._data.get("requirements", "") + + @property + def conda(self) -> str: + return self._data.get("conda", "") + + +class Experiment(BaseEntity): + """ + 表示一个 SwanLab 实验。 + + 支持双模式:构造时传入 data,或 data=None(按需懒加载)。 + """ + + def __init__( + self, + client: "Client", + web_host: str, + api_host: str, + *, + path: str, + data: Optional[ApiExperimentType] = None, + ) -> None: + super().__init__(client, web_host, api_host) + self._path = path # 'username/project-name' + self._data = data + + def _ensure_data(self) -> ApiExperimentType: + if self._data is None: + resp = self._get(f"/project/{self._path}/runs/{self.id}") + self._data = resp.data if resp.ok and resp.data else cast(ApiExperimentType, {}) + return self._data + + @property + def id(self) -> str: + return self._data.get("cuid", "") if self._data is not None else self._ensure_data().get("cuid", "") + + @property + def name(self) -> str: + return self._ensure_data().get("name", "") + + @property + def description(self) -> str: + return self._ensure_data().get("description", "") + + @property + def state(self) -> str: + return self._ensure_data().get("state", "") + + @property + def url(self) -> str: + return self._build_url(f"@{self._path}/runs/{self.id}/chart") + + @property + def show(self) -> bool: + return self._ensure_data().get("show", True) + + @property + def labels(self) -> List[Label]: + return [Label(label["name"]) for label in self._ensure_data().get("labels", [])] + + @property + def group(self) -> str: + return self._ensure_data().get("cluster", "") + + @property + def job_type(self) -> str: + return self._ensure_data().get("job", "") + + @property + def user(self) -> ApiExperimentUserType: + user_data = self._ensure_data().get("user", {}) + return user_data if isinstance(user_data, dict) else cast(ApiExperimentUserType, {}) + + @property + def created_at(self) -> str: + return self._ensure_data().get("createdAt", "") + + @property + def finished_at(self) -> str: + return self._ensure_data().get("finishedAt", "") + + @property + def profile(self) -> Profile: + """Experiment profile containing config, metadata, requirements, and conda.""" + data = self._ensure_data() + if "profile" not in data and self.id: + resp = self._get(f"/project/{self._path}/runs/{self.id}") + if resp.ok and resp.data: + self._data = resp.data + data = self._data + return Profile(data.get("profile", {})) + + def metrics( + self, keys: Optional[List[str]] = None, x_axis: Optional[str] = None, sample: Optional[int] = None + ) -> Any: + """ + 获取实验指标数据,返回 pandas DataFrame。 + + :param keys: 指标 key 列表 + :param x_axis: x 轴指标,默认 step + :param sample: 均匀采样 N 条数据(等间距采样,保留整体趋势) + """ + from swanlab.vendor import pd + + if not keys: + return pd.DataFrame() + + fetch_keys = list(keys) + use_x_axis = x_axis is not None and x_axis != "step" + if use_x_axis and x_axis is not None: + fetch_keys.append(x_axis) + + dfs = [] + prefix = "" + for idx, key in enumerate(fetch_keys): + resp = self._get(f"/experiment/{self.id}/column/csv", params={"key": key}) + if not resp.ok: + continue + data = resp.data + csv_url = data[0].get("url", "") if isinstance(data, list) and data else "" + if not csv_url: + continue + df = pd.read_csv(csv_url, index_col=0) + + if idx == 0: + first_col = str(df.columns[0]) + suffix = f"{key}_" + prefix = first_col.split(suffix)[0] if suffix in first_col else "" + + def strip_suffix(col, suffix="_step"): + return col[: -len(suffix)] if col.endswith(suffix) else col + + df.columns = [ + strip_suffix(col[len(prefix) :]) if prefix and col.startswith(prefix) else strip_suffix(col) + for col in df.columns + ] + dfs.append(df) + + if not dfs: + return pd.DataFrame() + + result_df = dfs[0].join(dfs[1:], how="outer") if len(dfs) > 1 else dfs[0] + result_df = result_df.sort_index() + + if use_x_axis: + result_df = result_df.drop( + columns=[c for c in result_df.columns if c.endswith("_timestamp")], errors="ignore" + ) + if x_axis not in result_df.columns: + return pd.DataFrame() + cols = [x_axis] + [c for c in result_df.columns if c != x_axis] + result_df = result_df[cols].dropna(subset=[x_axis]) + + if sample is not None and len(result_df) > sample: + indices = [int(i * (len(result_df) - 1) / (sample - 1)) for i in range(sample)] + result_df = result_df.iloc[indices] + + return result_df + + def delete(self) -> bool: + """删除此实验。""" + resp = self._delete(f"/project/{self._path}/runs/{self.id}") + return resp.ok + + def to_dict(self) -> Dict[str, Any]: + return get_properties(self) + + +def _flatten_runs(runs: Union[list, Dict]) -> list: + """展开分组后的实验数据,返回一个包含所有实验的列表。""" + if isinstance(runs, dict): + return [item for v in runs.values() for item in _flatten_runs(v)] + if isinstance(runs, list): + return list(runs) + return [runs] + + +class Experiments(BaseEntity): + """ + 项目下实验集合的迭代器。 + + 用法:: + + for run in api.runs("username/project"): + print(run.name) + """ + + def __init__( + self, + client: "Client", + web_host: str, + api_host: str, + *, + path: str, + filters: Optional[Dict[str, object]] = None, + ) -> None: + super().__init__(client, web_host, api_host) + self._path = path + self._filters = filters + + def __iter__(self) -> Iterator[Experiment]: + 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), + } + for key, value in self._filters.items() + ] + if self._filters + else [] + ) + resp = self._post(f"/project/{self._path}/runs/shows", data={"filters": parsed_filters}) + if not resp.ok: + return + body = resp.data + runs: list = [] + if isinstance(body, list): + runs = body + elif isinstance(body, dict): + runs = _flatten_runs(body) + + for run_data in runs: + yield Experiment(self._client, self._web_host, self._api_host, path=self._path, data=run_data) + + def to_dict(self) -> Dict[str, Any]: + return {"path": self._path} diff --git a/swanlab/api/project.py b/swanlab/api/project.py new file mode 100644 index 000000000..63d521e51 --- /dev/null +++ b/swanlab/api/project.py @@ -0,0 +1,134 @@ +""" +@author: caddiesnew +@file: project.py +@time: 2026/4/20 +@description: Project 实体类 — 单个项目的查询与操作 +""" + +from typing import TYPE_CHECKING, Any, Dict, Iterator, List, Optional, cast + +from .base import BaseEntity +from .typings.project import ApiProjectCountType, ApiProjectType +from .utils import Label, get_properties + +if TYPE_CHECKING: + from swanlab.sdk.internal.pkg.client import Client + + +class Project(BaseEntity): + """ + 表示一个 SwanLab 项目。 + + 支持双模式:构造时传入 data(列表迭代注入),或 data=None(按需懒加载)。 + """ + + def __init__( + self, + client: "Client", + web_host: str, + api_host: str, + *, + path: str, + data: Optional[ApiProjectType] = None, + ) -> None: + super().__init__(client, web_host, api_host) + self._path = path + self._data = data + + def _ensure_data(self) -> ApiProjectType: + if self._data is None: + resp = self._get(f"/project/{self._path}") + self._data = resp.data if resp.ok and resp.data else cast(ApiProjectType, {}) + return self._data + + @property + def name(self) -> str: + return self._ensure_data().get("name", "") + + @property + def path(self) -> str: + return self._ensure_data().get("path", "") + + @property + def url(self) -> str: + return self._build_url(f"@{self.path}") + + @property + def description(self) -> str: + return self._ensure_data().get("description", "") + + @property + def visibility(self) -> str: + return self._ensure_data().get("visibility", "PUBLIC") + + @property + def created_at(self) -> str: + return self._ensure_data().get("createdAt", "") + + @property + def updated_at(self) -> str: + return self._ensure_data().get("updatedAt", "") + + @property + def labels(self) -> List[Label]: + return [Label(label["name"]) for label in self._ensure_data().get("projectLabels", [])] + + @property + def count(self) -> ApiProjectCountType: + return self._ensure_data().get("_count", {}) + + def runs(self, filters: Optional[Dict[str, object]] = None): + """获取项目下的实验列表。""" + from .experiment import Experiments + + return Experiments(self._client, self._web_host, self._api_host, path=self.path, filters=filters) + + def delete(self) -> bool: + """删除此项目。""" + resp = self._delete(f"/project/{self.path}") + return resp.ok + + def to_dict(self) -> Dict[str, Any]: + return get_properties(self) + + +class Projects(BaseEntity): + """ + 工作空间下项目集合的分页迭代器。 + + 用法:: + + for project in api.projects("username"): + print(project.name) + """ + + def __init__( + self, + client: "Client", + web_host: str, + api_host: str, + *, + path: str, + sort: Optional[str] = None, + search: Optional[str] = None, + detail: Optional[bool] = True, + ) -> None: + super().__init__(client, web_host, api_host) + self._path = path + self._sort = sort + self._search = search + self._detail = detail + + def __iter__(self) -> Iterator[Project]: + params = {"sort": self._sort, "search": self._search, "detail": self._detail} + for item in self._paginate(f"/project/{self._path}", params=params): + yield Project( + self._client, + self._web_host, + self._api_host, + path=str(item.get("path", "")), + data=cast(ApiProjectType, item), + ) + + def to_dict(self) -> Dict[str, Any]: + return {"path": self._path} diff --git a/swanlab/api/selfhosted.py b/swanlab/api/selfhosted.py new file mode 100644 index 000000000..6169501be --- /dev/null +++ b/swanlab/api/selfhosted.py @@ -0,0 +1,147 @@ +""" +@author: caddiesnew +@file: user.py +@time: 2026/4/20 +@description: User 实体类 — 用户信息与 API Key 管理 +""" + +import re +from typing import TYPE_CHECKING, Any, Dict, Iterator, List, Optional + +from .base import BaseEntity +from .typings.selfhosted import ApiApiKeyType, ApiSelfHostedInfoType +from .utils import get_properties + +if TYPE_CHECKING: + from swanlab.sdk.internal.pkg.client import Client + +_USERNAME_PATTERN = re.compile(r"^[a-zA-Z0-9_-]+$") +_PASSWORD_PATTERN = re.compile(r"^(?=.*[0-9])(?=.*[a-zA-Z]).{8,}$") + + +class User(BaseEntity): + """ + 表示一个 SwanLab 用户。 + """ + + def __init__( + self, + client: "Client", + web_host: str, + api_host: str, + *, + username: str, + login_user: str = "", + ) -> None: + super().__init__(client, web_host, api_host) + self._username = username + self._login_user = login_user or username + self._teams: Optional[List[str]] = None + self._api_keys_cache: Optional[List[ApiApiKeyType]] = None + + @property + def username(self) -> str: + return self._username + + @property + def is_self(self) -> bool: + return self._username == self._login_user + + @property + def teams(self) -> List[str]: + """用户所属的团队列表。""" + if self._teams is None: + resp = self._get(f"/user/{self._username}/groups") + self._teams = resp.data if isinstance(resp.data, list) else [] + return self._teams + + @property + def api_keys(self) -> List[str]: + """当前用户的 API Key 列表(仅限本人)。""" + if not self.is_self: + return [] + if self._api_keys_cache is None: + resp = self._get("/user/key") + self._api_keys_cache = resp.data if resp.ok else [] + return [r["key"] for r in self._api_keys_cache or []] + + def _invalidate_api_keys_cache(self) -> None: + self._api_keys_cache = None + + def generate_api_key(self, description: Optional[str] = None) -> Optional[str]: + """生成新的 API Key(仅限本人)。""" + if not self.is_self: + return None + self._post("/user/key", data={"name": description} if description else None) + self._invalidate_api_keys_cache() + resp = self._get("/user/key/latest") + return resp.data.get("key") if resp.ok and resp.data else None + + def delete_api_key(self, api_key: str) -> bool: + """删除指定 API Key(仅限本人)。""" + if not self.is_self: + return False + self._invalidate_api_keys_cache() + resp = self._get("/user/key") + if not resp.ok: + return False + keys: List[ApiApiKeyType] = resp.data + for key_info in keys: + if key_info["key"] == api_key: + self._delete(f"/user/key/{key_info['id']}") + return True + return False + + @staticmethod + def _validate_create_info(username: str, password: str) -> bool: + return bool(_USERNAME_PATTERN.match(username) and _PASSWORD_PATTERN.match(password)) + + def _check_self_hosted_permission(self) -> bool: + """检查私有化部署权限:已启用、未过期、当前用户为 root。""" + resp = self._get("/self_hosted/info") + if not resp.ok: + return False + info: ApiSelfHostedInfoType = resp.data + return info.get("enabled", False) and not info.get("expired", True) and info.get("root", False) + + def create(self, username: str, password: str) -> bool: + """创建新用户(仅限私有化部署 root 用户)。""" + if ( + not self.is_self + or not self._check_self_hosted_permission() + or not self._validate_create_info(username, password) + ): + return False + create_resp = self._post("/self_hosted/users", data={"users": [{"username": username, "password": password}]}) + return create_resp.ok + + def to_dict(self) -> Dict[str, Any]: + return get_properties(self) + + +class Users(BaseEntity): + """ + 用户集合的分页迭代器(私有化部署管理员限定)。 + + 用法:: + + for user in api.users(): + print(user.username) + """ + + def __init__(self, client: "Client", web_host: str, api_host: str, *, login_user: str = "") -> None: + super().__init__(client, web_host, api_host) + self._username = login_user + + def __iter__(self) -> Iterator[User]: + for item in self._paginate("/self_hosted/users"): + yield User( + self._client, + self._web_host, + self._api_host, + username=item.get("username", ""), + login_user=self._username, + ) + + def to_dict(self) -> Dict[str, Any]: + return {} diff --git a/swanlab/api/typings/__init__.py b/swanlab/api/typings/__init__.py new file mode 100644 index 000000000..1fdcb0ac8 --- /dev/null +++ b/swanlab/api/typings/__init__.py @@ -0,0 +1,26 @@ +""" +@author: caddiesnew +@file: __init__.py +@time: 2026/4/20 +@description: 公共查询 API 类型定义 — 统一导出 +""" + +from .common import ApiLabelType, ApiPaginationType, ApiResponseType +from .experiment import ApiExperimentType, ApiExperimentUserType +from .project import ApiProjectCountType, ApiProjectType +from .selfhosted import ApiApiKeyType, ApiGroupType, ApiSelfHostedInfoType +from .workspace import ApiWorkspaceInfoType + +__all__ = [ + "ApiLabelType", + "ApiPaginationType", + "ApiResponseType", + "ApiExperimentType", + "ApiExperimentUserType", + "ApiProjectCountType", + "ApiProjectType", + "ApiApiKeyType", + "ApiGroupType", + "ApiSelfHostedInfoType", + "ApiWorkspaceInfoType", +] diff --git a/swanlab/api/typings/common.py b/swanlab/api/typings/common.py new file mode 100644 index 000000000..cc19c2644 --- /dev/null +++ b/swanlab/api/typings/common.py @@ -0,0 +1,63 @@ +""" +@author: caddiesnew +@file: common.py +@time: 2026/4/20 +@description: 公共查询 API 通用类型定义 +""" + +from typing import Any, Dict, List, TypedDict + + +class ApiLabelType(TypedDict): + name: str + + +class ApiPaginationType(TypedDict): + list: List + size: int + pages: int + total: int + + +class ApiResponseType: + """ + API 响应的统一封装,保证任何异常都不会导致程序 crash。 + + - ok=True 时 data 持有正常返回值 + - ok=False 时 data 为 None,errmsg 描述失败原因 + """ + + __slots__ = ("ok", "errmsg", "data") + + def __init__(self, *, ok: bool, errmsg: str = "", data: Any = None) -> None: + self.ok = ok + self.errmsg = errmsg + self.data = data + + def to_dict(self) -> Dict[str, Any]: + return {"ok": self.ok, "errmsg": self.errmsg, "data": self.data} + + def to_json_dict(self) -> Dict[str, Any]: + """返回 JSON 可序列化的字典,自动将实体 data 转为 dict。""" + data = self.data + errors: list[str] = [] + if not self.ok and self.errmsg: + errors.append(self.errmsg) + if data is not None and hasattr(data, "to_dict"): + data = data.to_dict() + # 收集实体内部子请求的错误 + if hasattr(data, "__getitem__"): + # to_dict 返回的 dict 不带 _errors,需要从实体取 + pass + entity_errors = getattr(self.data, "_errors", []) + errors.extend(entity_errors) + ok = self.ok and not errors + return {"ok": ok, "errmsg": "; ".join(errors) if errors else "", "data": data} + + def __repr__(self) -> str: + if self.ok: + return f"ApiResponse(ok=True, data={self.data!r})" + return f"ApiResponse(ok=False, errmsg={self.errmsg!r})" + + +__all__ = ["ApiLabelType", "ApiPaginationType", "ApiResponseType"] diff --git a/swanlab/api/typings/experiment.py b/swanlab/api/typings/experiment.py new file mode 100644 index 000000000..453274bbe --- /dev/null +++ b/swanlab/api/typings/experiment.py @@ -0,0 +1,32 @@ +""" +@author: caddiesnew +@file: experiment.py +@time: 2026/4/20 +@description: 公共查询 API 实验类型定义 +""" + +from typing import Dict, List, Optional, TypedDict + +from swanlab.sdk.typings.run import RunStateType + +from .common import ApiLabelType + + +class ApiExperimentUserType(TypedDict): + username: str + name: str + + +class ApiExperimentType(TypedDict): + cuid: str + name: str + description: str + labels: List[ApiLabelType] + profile: Dict[str, object] + show: bool + state: RunStateType + cluster: str + job: str + user: ApiExperimentUserType + rootExpId: Optional[str] + rootProId: Optional[str] diff --git a/swanlab/api/typings/project.py b/swanlab/api/typings/project.py new file mode 100644 index 000000000..d189742ec --- /dev/null +++ b/swanlab/api/typings/project.py @@ -0,0 +1,30 @@ +""" +@author: caddiesnew +@file: project.py +@time: 2026/4/20 +@description: 公共查询 API 项目类型定义 +""" + +from typing import Dict, List, TypedDict + +from swanlab.sdk.typings.run import VisibilityType + +from .common import ApiLabelType + + +class ApiProjectCountType(TypedDict): + experiments: int + contributors: int + collaborators: int + clones: int + + +class ApiProjectType(TypedDict): + name: str + username: str + path: str + visibility: VisibilityType + description: str + group: Dict[str, str] + projectLabels: List[ApiLabelType] + _count: ApiProjectCountType diff --git a/swanlab/api/typings/selfhosted.py b/swanlab/api/typings/selfhosted.py new file mode 100644 index 000000000..2e4a854ec --- /dev/null +++ b/swanlab/api/typings/selfhosted.py @@ -0,0 +1,29 @@ +""" +@author: caddiesnew +@file: user.py +@time: 2026/4/20 +@description: 公共查询 API self-hosted 类型定义 +""" + +from typing import TypedDict + +from swanlab.sdk.typings.run import LicensePlanType + + +class ApiGroupType(TypedDict): + name: str + username: str + + +class ApiApiKeyType(TypedDict): + id: int + name: str + key: str + + +class ApiSelfHostedInfoType(TypedDict): + enabled: bool + expired: bool + root: bool + plan: LicensePlanType + seats: int diff --git a/swanlab/api/typings/user.py b/swanlab/api/typings/user.py new file mode 100644 index 000000000..fb3e2eeea --- /dev/null +++ b/swanlab/api/typings/user.py @@ -0,0 +1,24 @@ +""" +@author: caddiesnew +@file: user.py +@time: 2026/4/20 +@description: 公共查询 API 用户类型定义 +""" + +from typing import TypedDict + + +class ApiUserType(TypedDict): + bio: str + institution: str + localtion: str + school: str + email: str + idc: str + url: str + telephone: str + + +class ApiGroupType(TypedDict): + name: str + username: str diff --git a/swanlab/api/typings/workspace.py b/swanlab/api/typings/workspace.py new file mode 100644 index 000000000..fdc700f06 --- /dev/null +++ b/swanlab/api/typings/workspace.py @@ -0,0 +1,19 @@ +""" +@author: caddiesnew +@file: workspace.py +@time: 2026/4/20 +@description: 公共查询 API 工作空间类型定义 +""" + +from typing import Dict, TypedDict + +from swanlab.sdk.typings.run import RoleType, WorkspaceType + + +class ApiWorkspaceInfoType(TypedDict): + name: str + username: str + profile: Dict[str, str] + type: WorkspaceType + comment: str + role: RoleType diff --git a/swanlab/api/utils.py b/swanlab/api/utils.py new file mode 100644 index 000000000..9f95692c7 --- /dev/null +++ b/swanlab/api/utils.py @@ -0,0 +1,34 @@ +""" +@author: caddiesnew +@file: utils.py +@time: 2026/4/20 +@description: 公共查询 API 工具函数 +""" + +from typing import Dict, NamedTuple, Optional, Set + + +class Label(NamedTuple): + name: str + + def __str__(self) -> str: + return self.name + + +def get_properties(obj: object, _visited: Optional[Set[int]] = None) -> Dict[str, object]: + """递归获取实例中所有 property 的值,用于 to_dict() 默认实现。""" + if _visited is None: + _visited = set() + obj_id = id(obj) + if obj_id in _visited: + return {} + _visited = _visited | {obj_id} + + result = {} + for name in dir(obj): + if name.startswith("_"): + continue + if isinstance(getattr(type(obj), name, None), property): + value = getattr(obj, name, None) + result[name] = value if type(value).__module__ == "builtins" else get_properties(value, _visited) + return result diff --git a/swanlab/api/workspace.py b/swanlab/api/workspace.py new file mode 100644 index 000000000..ca60b86c7 --- /dev/null +++ b/swanlab/api/workspace.py @@ -0,0 +1,120 @@ +""" +@author: caddiesnew +@file: workspace.py +@time: 2026/4/20 +@description: Workspace 实体类 — 工作空间的查询 +""" + +from typing import TYPE_CHECKING, Any, Dict, Iterator, Optional, cast + +from swanlab.sdk.typings.run import WorkspaceType + +from .base import BaseEntity +from .typings.workspace import ApiWorkspaceInfoType +from .utils import get_properties + +if TYPE_CHECKING: + from swanlab.sdk.internal.pkg.client import Client + + +class Workspace(BaseEntity): + """ + 表示一个 SwanLab 工作空间(个人或团队)。 + """ + + def __init__( + self, + client: "Client", + web_host: str, + api_host: str, + *, + username: str, + data: Optional[ApiWorkspaceInfoType] = None, + ) -> None: + super().__init__(client, web_host, api_host) + self._username = username + self._data = data + + def _ensure_data(self) -> ApiWorkspaceInfoType: + if self._data is None: + resp = self._get(f"/group/{self._username}") + self._data = resp.data if resp.ok and resp.data else cast(ApiWorkspaceInfoType, {}) + return self._data + + @property + def name(self) -> str: + return self._ensure_data().get("name", "") + + @property + def username(self) -> str: + return self._ensure_data().get("username", "") + + @property + def workspace_type(self) -> WorkspaceType: + return self._ensure_data().get("type", "") + + @property + def profile(self) -> Dict[str, str]: + return self._ensure_data().get("profile", {}) + + @property + def comment(self) -> str: + return self._ensure_data().get("comment", "") + + @property + def role(self) -> str: + return self._ensure_data().get("role", "") + + def projects( + self, + sort: Optional[str] = None, + search: Optional[str] = None, + detail: Optional[bool] = True, + ): + """获取工作空间下的项目列表。""" + from .project import Projects + + return Projects( + self._client, + self._web_host, + self._api_host, + path=self.username, + sort=sort, + search=search, + detail=detail, + ) + + def to_dict(self) -> Dict[str, Any]: + return get_properties(self) + + +class Workspaces(BaseEntity): + """ + 用户工作空间集合的迭代器。 + + 用法:: + + for ws in api.workspaces("username"): + print(ws.name) + """ + + def __init__(self, client: "Client", web_host: str, api_host: str, *, username: str) -> None: + super().__init__(client, web_host, api_host) + self._username = username + + def _get_all_workspace_names(self) -> list[str]: + """获取用户个人空间 + 所属团队空间名称列表。""" + resp = self._get(f"/user/{self._username}/groups") + if not resp.ok: + return [self._username] + group_names = [r["username"] for r in resp.data] + return [self._username] + group_names + + def __iter__(self) -> Iterator[Workspace]: + for name in self._get_all_workspace_names(): + resp = self._get(f"/group/{name}") + data = resp.data if resp.ok else None + yield Workspace(self._client, self._web_host, self._api_host, username=name, data=data) + + def to_dict(self) -> Dict[str, Any]: + return {"username": self._username} diff --git a/swanlab/cli/__init__.py b/swanlab/cli/__init__.py index 97fe8aa8d..42f77c0ce 100644 --- a/swanlab/cli/__init__.py +++ b/swanlab/cli/__init__.py @@ -9,6 +9,7 @@ from swanlab.sdk.internal.pkg.helper import get_swanlab_version +from .api import api_cli from .auth import login, logout, verify from .converter import convert from .dashboard import watch @@ -55,4 +56,8 @@ def cli(): cli.add_command(disabled) +# OpenApi +# noinspection PyTypeChecker +cli.add_command(api_cli) + __all__ = ["cli"] diff --git a/swanlab/cli/api/__init__.py b/swanlab/cli/api/__init__.py new file mode 100644 index 000000000..08e92a779 --- /dev/null +++ b/swanlab/cli/api/__init__.py @@ -0,0 +1,28 @@ +""" +@author: caddiesnew +@file: __init__.py +@time: 2026/4/20 +@description: CLI API 子命令 — 通过命令行调用 SwanLab 公共查询 API +""" + +import click + +from .experiment import experiment_cli +from .project import project_cli +from .selfhosted import selfhosted_cli +from .workspace import workspace_cli + + +@click.group("api") +def api_cli(): + """Generic SwanLab API requests.""" + pass + + +api_cli.add_command(project_cli) +api_cli.add_command(experiment_cli) +api_cli.add_command(workspace_cli) +api_cli.add_command(selfhosted_cli) + + +__all__ = ["api_cli"] diff --git a/swanlab/cli/api/experiment.py b/swanlab/cli/api/experiment.py new file mode 100644 index 000000000..a6a2d6e73 --- /dev/null +++ b/swanlab/cli/api/experiment.py @@ -0,0 +1,26 @@ +import click +import orjson + +from swanlab.api.typings.common import ApiResponseType + +from .helper import format_output + + +@click.group("run") +def experiment_cli(): + """Experiment(Run) management commands.""" + pass + + +@experiment_cli.command("info") +@click.argument("path", required=True) +def get_info(path: str): + """Get run(experiment) info by path (username/project/run_id).""" + from swanlab.api import Api + + try: + api = Api() + resp = api.run(path) + except Exception as e: + resp = ApiResponseType(ok=False, errmsg=str(e)) + click.echo(orjson.dumps(resp.to_json_dict(), option=orjson.OPT_INDENT_2).decode()) diff --git a/swanlab/cli/api/helper.py b/swanlab/cli/api/helper.py new file mode 100644 index 000000000..84b9a8ff3 --- /dev/null +++ b/swanlab/cli/api/helper.py @@ -0,0 +1,9 @@ +import click +import orjson + +from swanlab.api.typings.common import ApiResponseType + + +def format_output(resp: ApiResponseType) -> None: + """统一输出 ApiResponse JSON。""" + click.echo(orjson.dumps(resp.to_json_dict(), option=orjson.OPT_INDENT_2).decode()) diff --git a/swanlab/cli/api/project.py b/swanlab/cli/api/project.py new file mode 100644 index 000000000..571c26696 --- /dev/null +++ b/swanlab/cli/api/project.py @@ -0,0 +1,24 @@ +import click +import orjson + +from swanlab.api.typings.common import ApiResponseType + + +@click.group("project") +def project_cli(): + """Project management commands.""" + pass + + +@project_cli.command("info") +@click.argument("path", required=True) +def get_info(path: str): + """Get project info by path (username/project).""" + from swanlab.api import Api + + try: + api = Api() + resp = api.project(path) + except Exception as e: + resp = ApiResponseType(ok=False, errmsg=str(e)) + click.echo(orjson.dumps(resp.to_json_dict(), option=orjson.OPT_INDENT_2).decode()) diff --git a/swanlab/cli/api/selfhosted.py b/swanlab/cli/api/selfhosted.py new file mode 100644 index 000000000..b04df9f2f --- /dev/null +++ b/swanlab/cli/api/selfhosted.py @@ -0,0 +1,65 @@ +import click +import orjson + +from swanlab.api.typings.common import ApiResponseType + +from .helper import format_output + + +@click.group("selfhosted") +def selfhosted_cli(): + """Self-hosted deployment management commands.""" + pass + + +@selfhosted_cli.command("info") +@click.option("-u", "--username", default=None, help="Target username (defaults to current user).") +def get_info(username): + """Get self-hosted info for a user.""" + from swanlab.api import Api + + try: + api = Api() + resp = api.user(username=username) + except Exception as e: + resp = ApiResponseType(ok=False, errmsg=str(e)) + format_output(resp) + + +@selfhosted_cli.command("keys") +@click.option("-u", "--username", default=None, help="Target username (defaults to current user).") +def get_keys(username): + """List API keys for a user (only available for the logged-in user).""" + from swanlab.api import Api + + try: + api = Api() + user_resp = api.user(username=username) + if not user_resp.ok: + format_output(user_resp) + return + user = user_resp.data + keys = user.api_keys + format_output(ApiResponseType(ok=True, data=keys)) + except Exception as e: + format_output(ApiResponseType(ok=False, errmsg=str(e))) + + +@selfhosted_cli.command("create-user") +@click.argument("new_username", required=True) +@click.argument("password", required=True) +def create_user(new_username: str, password: str): + """Create a new user in self-hosted deployment (root only).""" + from swanlab.api import Api + + try: + api = Api() + user_resp = api.user() + if not user_resp.ok: + format_output(user_resp) + return + user = user_resp.data + success = user.create(new_username, password) + format_output(ApiResponseType(ok=success, data=None, errmsg="" if success else "Failed to create user")) + except Exception as e: + format_output(ApiResponseType(ok=False, errmsg=str(e))) diff --git a/swanlab/cli/api/workspace.py b/swanlab/cli/api/workspace.py new file mode 100644 index 000000000..539e9265c --- /dev/null +++ b/swanlab/cli/api/workspace.py @@ -0,0 +1,26 @@ +from typing import Optional + +import click +import orjson + +from swanlab.api.typings.common import ApiResponseType + + +@click.group("workspace") +def workspace_cli(): + """Workspace management commands.""" + pass + + +@workspace_cli.command("info") +@click.argument("username", required=True) +def get_info(username: Optional[str] = None): + """Get workspace info.""" + from swanlab.api import Api + + try: + api = Api() + resp = api.workspace(username) + except Exception as e: + resp = ApiResponseType(ok=False, errmsg=str(e)) + click.echo(orjson.dumps(resp.to_json_dict(), option=orjson.OPT_INDENT_2).decode()) diff --git a/swanlab/sdk/internal/core_python/api/__init__.py b/swanlab/sdk/internal/core_python/api/__init__.py index c3c3f6b31..ccabd22cf 100644 --- a/swanlab/sdk/internal/core_python/api/__init__.py +++ b/swanlab/sdk/internal/core_python/api/__init__.py @@ -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", +] diff --git a/swanlab/sdk/internal/core_python/api/experiment.py b/swanlab/sdk/internal/core_python/api/experiment.py index 1f276485f..d47012bf8 100644 --- a/swanlab/sdk/internal/core_python/api/experiment.py +++ b/swanlab/sdk/internal/core_python/api/experiment.py @@ -5,7 +5,7 @@ @description: SwanLab 运行时实验API """ -from typing import List, Literal, Optional +from typing import Dict, List, Literal, Optional, Union from google.protobuf.timestamp_pb2 import Timestamp @@ -13,8 +13,9 @@ 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( @@ -83,7 +84,7 @@ 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, @@ -91,3 +92,93 @@ def stop_experiment(username: str, project: str, cuid: str, *, state: RunState, "from": "sdk", }, ) + return resp.raw.status_code == 201 + + +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), + } + 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}") diff --git a/swanlab/sdk/internal/core_python/api/project.py b/swanlab/sdk/internal/core_python/api/project.py index 81e7e03a9..e266eaac7 100644 --- a/swanlab/sdk/internal/core_python/api/project.py +++ b/swanlab/sdk/internal/core_python/api/project.py @@ -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: 项目信息 @@ -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}") diff --git a/swanlab/sdk/internal/core_python/api/self_hosted.py b/swanlab/sdk/internal/core_python/api/self_hosted.py new file mode 100644 index 000000000..c42ae5d50 --- /dev/null +++ b/swanlab/sdk/internal/core_python/api/self_hosted.py @@ -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 diff --git a/swanlab/sdk/internal/core_python/api/user.py b/swanlab/sdk/internal/core_python/api/user.py new file mode 100644 index 000000000..183799f0f --- /dev/null +++ b/swanlab/sdk/internal/core_python/api/user.py @@ -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 diff --git a/swanlab/sdk/typings/core_python/api/__init__.py b/swanlab/sdk/typings/core_python/api/__init__.py index 25e6e5592..4a4d6f301 100644 --- a/swanlab/sdk/typings/core_python/api/__init__.py +++ b/swanlab/sdk/typings/core_python/api/__init__.py @@ -3,5 +3,23 @@ @file: __init__.py @time: 2026/3/7 18:40 @description: SwanLab API类型提示 -所有后端响应类型命名以 Response 结尾 + +所有后端响应类型命名以 Type 结尾 """ + +from .experiment import RunType +from .project import InitProjectType, ProjectLabelType, ProjectType, ProjResponseType +from .user import ApiKeyType, GroupType, SelfHostedInfoType +from .workspace import WorkspaceInfoType + +__all__ = [ + "RunType", + "ProjectType", + "InitProjectType", + "ProjResponseType", + "ProjectLabelType", + "GroupType", + "ApiKeyType", + "SelfHostedInfoType", + "WorkspaceInfoType", +] diff --git a/swanlab/sdk/typings/core_python/api/experiment.py b/swanlab/sdk/typings/core_python/api/experiment.py index b39212bd5..05ad36c5b 100644 --- a/swanlab/sdk/typings/core_python/api/experiment.py +++ b/swanlab/sdk/typings/core_python/api/experiment.py @@ -1,13 +1,54 @@ """ @author: cunyue @file: experiment.py -@time: 2026/4/18 17:43 +@time: 2026/3/10 19:02 @description: SwanLab 运行时实验API类型 """ -from typing import TypedDict +from typing import Dict, List, Optional, TypedDict + +from swanlab.sdk.typings.run import RunStateType class InitExperimentType(TypedDict): # 实验cuid cuid: str + + +class _ExperimentLabelType(TypedDict): + # 标签名称 + name: str + + +class _UserType(TypedDict): + # 用户名 + username: str + # 用户显示名称 + name: str + + +class RunType(TypedDict): + # 实验CUID, 唯一标识符 + cuid: str + # 实验名称 + name: str + # 实验描述 + description: str + # 实验标签列表 + labels: List[_ExperimentLabelType] + # 实验配置和摘要信息,包含 'config' 和 'scalar' + profile: Dict[str, Dict[str, object]] + # 是否显示 + show: bool + # 实验状态 + state: RunStateType + # 实验组 + cluster: str + # 任务类型 + job: str + # 实验所属用户 + user: _UserType + # 祖宗实验对应的实验cuid,如果为克隆实验则必传 + rootExpId: Optional[str] + # 祖宗实验对应的项目cuid,如果为克隆实验则必传 + rootProId: Optional[str] diff --git a/swanlab/sdk/typings/core_python/api/project.py b/swanlab/sdk/typings/core_python/api/project.py index d66c6f92b..c6be78008 100644 --- a/swanlab/sdk/typings/core_python/api/project.py +++ b/swanlab/sdk/typings/core_python/api/project.py @@ -5,7 +5,7 @@ @description: SwanLab 运行时项目API类型 """ -from typing import Literal, TypedDict +from typing import Dict, List, Literal, TypedDict class _ProjectCount(TypedDict): @@ -19,6 +19,11 @@ class _ProjectCount(TypedDict): clones: int +class ProjectLabelType(TypedDict): + # 项目标签名称 + name: str + + class ProjectType(TypedDict): # 项目名称 name: str @@ -28,6 +33,12 @@ class ProjectType(TypedDict): path: str # 项目可见性 visibility: Literal["PUBLIC", "PRIVATE"] + # 项目描述 + description: str + # 项目所属工作空间 + group: Dict[str, str] + # 项目标签 + projectLabels: List[ProjectLabelType] # 项目统计信息 _count: _ProjectCount @@ -39,3 +50,14 @@ class InitProjectType(TypedDict): username: str # 项目路径 '/:username/:name' path: str + + +class ProjResponseType(TypedDict): + # 项目列表 + list: List[ProjectType] + # 每页项目数量 + size: int + # 总页数 + pages: int + # 总项目数量 + total: int diff --git a/swanlab/sdk/typings/core_python/api/user.py b/swanlab/sdk/typings/core_python/api/user.py new file mode 100644 index 000000000..6c75b564b --- /dev/null +++ b/swanlab/sdk/typings/core_python/api/user.py @@ -0,0 +1,39 @@ +""" +@author: cunyue +@file: user.py +@time: 2026/3/10 19:02 +@description: SwanLab 运行时用户API类型 +""" + +from typing import TypedDict + +from swanlab.sdk.typings.run import LicensePlanType + + +class GroupType(TypedDict): + # 组织名称 + name: str + # 组织用户名 + username: str + + +class ApiKeyType(TypedDict): + # API Key ID + id: int + # API Key 名称 + name: str + # API Key 值 + key: str + + +class SelfHostedInfoType(TypedDict): + # 是否成功部署 + enabled: bool + # licence是否过期 + expired: bool + # 是否为根用户 + root: bool + # 私有化版本(免费、商业) + plan: LicensePlanType + # 余剩席位 + seats: int diff --git a/swanlab/sdk/typings/core_python/api/workspace.py b/swanlab/sdk/typings/core_python/api/workspace.py new file mode 100644 index 000000000..0c313282d --- /dev/null +++ b/swanlab/sdk/typings/core_python/api/workspace.py @@ -0,0 +1,25 @@ +""" +@author: cunyue +@file: workspace.py +@time: 2026/3/10 19:02 +@description: SwanLab 运行时工作空间API类型 +""" + +from typing import Dict, TypedDict + +from swanlab.sdk.typings.run import RoleType, WorkspaceType + + +class WorkspaceInfoType(TypedDict): + # 工作空间名称 + name: str + # 工作空间用户名 + username: str + # 工作空间配置信息 + profile: Dict[str, str] + # 工作空间类型 + type: WorkspaceType + # 组织或者个人的描述 + comment: str + # 组织成员的角色(组织信息特有) + role: RoleType diff --git a/swanlab/sdk/typings/run/__init__.py b/swanlab/sdk/typings/run/__init__.py index 1f4881d87..447cb684c 100644 --- a/swanlab/sdk/typings/run/__init__.py +++ b/swanlab/sdk/typings/run/__init__.py @@ -27,3 +27,40 @@ """ 异步日志上报类型 """ + + +WorkspaceType = Literal["TEAM", "PERSON"] +""" +工作空间类型 +""" + +VisibilityType = Literal["PUBLIC", "PRIVATE"] +""" +项目可见类型 +""" + +RoleType = Literal["VISITOR", "VIEWER", "MEMBER", "OWNER"] +""" +组织成员类型 +""" + +IdentityType = Literal["root", "user"] +""" +Self-Hosted 用户身份类型 +""" + +LicensePlanType = Literal["free", "commercial"] +""" +Self-Hosted 许可证类型 +""" + + +RunStateType = Literal["RUNNING", "FINISHED", "CRASHED", "ABORTED", "OFFLINE"] +""" +实验状态类型 +""" + +ColumnType = Literal["SCALAR", "CONFIG", "STABLE"] +""" +列类型 +""" diff --git a/swanlab/utils/__init__.py b/swanlab/utils/__init__.py index 0a3e026c8..60a9aefa1 100644 --- a/swanlab/utils/__init__.py +++ b/swanlab/utils/__init__.py @@ -6,6 +6,7 @@ 理论上本模块的内容都可以被用户调用,并被写入API文档中 """ +from .column import parse_column_type, to_camel_case from .experiment import generate_color, generate_id, generate_name -__all__ = ["generate_color", "generate_id", "generate_name"] +__all__ = ["generate_color", "generate_id", "generate_name", "to_camel_case", "parse_column_type"] diff --git a/swanlab/utils/column/__init__.py b/swanlab/utils/column/__init__.py new file mode 100644 index 000000000..924ba3cc0 --- /dev/null +++ b/swanlab/utils/column/__init__.py @@ -0,0 +1,17 @@ +from swanlab.sdk.typings.run import ColumnType + + +def parse_column_type(column: str) -> ColumnType: + """从前缀中获取指标类型""" + column_type = column.split(".", 1)[0] + if column_type == "summary": + return "SCALAR" + elif column_type == "config": + return "CONFIG" + else: + return "STABLE" + + +def to_camel_case(name: str) -> str: + """将下划线命名转化为驼峰命名""" + return "".join([w.capitalize() if i > 0 else w for i, w in enumerate(name.split("_"))]) diff --git a/uv.lock b/uv.lock index 745120723..ed4c9363d 100644 --- a/uv.lock +++ b/uv.lock @@ -2790,6 +2790,201 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e9/a5/1be1516390333ff9be3a9cb648c9f33df79d5096e5884b5df71a588af463/opencv_python-4.13.0.92-cp37-abi3-win_amd64.whl", hash = "sha256:423d934c9fafb91aad38edf26efb46da91ffbc05f3f59c4b0c72e699720706f5", size = 40212062, upload-time = "2026-02-05T07:02:12.724Z" }, ] +[[package]] +name = "orjson" +version = "3.11.5" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10' and sys_platform == 'linux'", + "python_full_version < '3.10' and sys_platform != 'linux'", +] +sdist = { url = "https://files.pythonhosted.org/packages/04/b8/333fdb27840f3bf04022d21b654a35f58e15407183aeb16f3b41aa053446/orjson-3.11.5.tar.gz", hash = "sha256:82393ab47b4fe44ffd0a7659fa9cfaacc717eb617c93cde83795f14af5c2e9d5", size = 5972347, upload-time = "2025-12-06T15:55:39.458Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/19/b22cf9dad4db20c8737041046054cbd4f38bb5a2d0e4bb60487832ce3d76/orjson-3.11.5-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:df9eadb2a6386d5ea2bfd81309c505e125cfc9ba2b1b99a97e60985b0b3665d1", size = 245719, upload-time = "2025-12-06T15:53:43.877Z" }, + { url = "https://files.pythonhosted.org/packages/03/2e/b136dd6bf30ef5143fbe76a4c142828b55ccc618be490201e9073ad954a1/orjson-3.11.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ccc70da619744467d8f1f49a8cadae5ec7bbe054e5232d95f92ed8737f8c5870", size = 132467, upload-time = "2025-12-06T15:53:45.379Z" }, + { url = "https://files.pythonhosted.org/packages/ae/fc/ae99bfc1e1887d20a0268f0e2686eb5b13d0ea7bbe01de2b566febcd2130/orjson-3.11.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:073aab025294c2f6fc0807201c76fdaed86f8fc4be52c440fb78fbb759a1ac09", size = 130702, upload-time = "2025-12-06T15:53:46.659Z" }, + { url = "https://files.pythonhosted.org/packages/6e/43/ef7912144097765997170aca59249725c3ab8ef6079f93f9d708dd058df5/orjson-3.11.5-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:835f26fa24ba0bb8c53ae2a9328d1706135b74ec653ed933869b74b6909e63fd", size = 135907, upload-time = "2025-12-06T15:53:48.487Z" }, + { url = "https://files.pythonhosted.org/packages/3f/da/24d50e2d7f4092ddd4d784e37a3fa41f22ce8ed97abc9edd222901a96e74/orjson-3.11.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:667c132f1f3651c14522a119e4dd631fad98761fa960c55e8e7430bb2a1ba4ac", size = 139935, upload-time = "2025-12-06T15:53:49.88Z" }, + { url = "https://files.pythonhosted.org/packages/02/4a/b4cb6fcbfff5b95a3a019a8648255a0fac9b221fbf6b6e72be8df2361feb/orjson-3.11.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:42e8961196af655bb5e63ce6c60d25e8798cd4dfbc04f4203457fa3869322c2e", size = 137541, upload-time = "2025-12-06T15:53:51.226Z" }, + { url = "https://files.pythonhosted.org/packages/a5/99/a11bd129f18c2377c27b2846a9d9be04acec981f770d711ba0aaea563984/orjson-3.11.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75412ca06e20904c19170f8a24486c4e6c7887dea591ba18a1ab572f1300ee9f", size = 139031, upload-time = "2025-12-06T15:53:52.309Z" }, + { url = "https://files.pythonhosted.org/packages/64/29/d7b77d7911574733a036bb3e8ad7053ceb2b7d6ea42208b9dbc55b23b9ed/orjson-3.11.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:6af8680328c69e15324b5af3ae38abbfcf9cbec37b5346ebfd52339c3d7e8a18", size = 141622, upload-time = "2025-12-06T15:53:53.606Z" }, + { url = "https://files.pythonhosted.org/packages/93/41/332db96c1de76b2feda4f453e91c27202cd092835936ce2b70828212f726/orjson-3.11.5-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:a86fe4ff4ea523eac8f4b57fdac319faf037d3c1be12405e6a7e86b3fbc4756a", size = 413800, upload-time = "2025-12-06T15:53:54.866Z" }, + { url = "https://files.pythonhosted.org/packages/76/e1/5a0d148dd1f89ad2f9651df67835b209ab7fcb1118658cf353425d7563e9/orjson-3.11.5-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e607b49b1a106ee2086633167033afbd63f76f2999e9236f638b06b112b24ea7", size = 151198, upload-time = "2025-12-06T15:53:56.383Z" }, + { url = "https://files.pythonhosted.org/packages/0d/96/8db67430d317a01ae5cf7971914f6775affdcfe99f5bff9ef3da32492ecc/orjson-3.11.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7339f41c244d0eea251637727f016b3d20050636695bc78345cce9029b189401", size = 141984, upload-time = "2025-12-06T15:53:57.746Z" }, + { url = "https://files.pythonhosted.org/packages/71/49/40d21e1aa1ac569e521069228bb29c9b5a350344ccf922a0227d93c2ed44/orjson-3.11.5-cp310-cp310-win32.whl", hash = "sha256:8be318da8413cdbbce77b8c5fac8d13f6eb0f0db41b30bb598631412619572e8", size = 135272, upload-time = "2025-12-06T15:53:59.769Z" }, + { url = "https://files.pythonhosted.org/packages/c4/7e/d0e31e78be0c100e08be64f48d2850b23bcb4d4c70d114f4e43b39f6895a/orjson-3.11.5-cp310-cp310-win_amd64.whl", hash = "sha256:b9f86d69ae822cabc2a0f6c099b43e8733dda788405cba2665595b7e8dd8d167", size = 133360, upload-time = "2025-12-06T15:54:01.25Z" }, + { url = "https://files.pythonhosted.org/packages/fd/68/6b3659daec3a81aed5ab47700adb1a577c76a5452d35b91c88efee89987f/orjson-3.11.5-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:9c8494625ad60a923af6b2b0bd74107146efe9b55099e20d7740d995f338fcd8", size = 245318, upload-time = "2025-12-06T15:54:02.355Z" }, + { url = "https://files.pythonhosted.org/packages/e9/00/92db122261425f61803ccf0830699ea5567439d966cbc35856fe711bfe6b/orjson-3.11.5-cp311-cp311-macosx_15_0_arm64.whl", hash = "sha256:7bb2ce0b82bc9fd1168a513ddae7a857994b780b2945a8c51db4ab1c4b751ebc", size = 129491, upload-time = "2025-12-06T15:54:03.877Z" }, + { url = "https://files.pythonhosted.org/packages/94/4f/ffdcb18356518809d944e1e1f77589845c278a1ebbb5a8297dfefcc4b4cb/orjson-3.11.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67394d3becd50b954c4ecd24ac90b5051ee7c903d167459f93e77fc6f5b4c968", size = 132167, upload-time = "2025-12-06T15:54:04.944Z" }, + { url = "https://files.pythonhosted.org/packages/97/c6/0a8caff96f4503f4f7dd44e40e90f4d14acf80d3b7a97cb88747bb712d3e/orjson-3.11.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:298d2451f375e5f17b897794bcc3e7b821c0f32b4788b9bcae47ada24d7f3cf7", size = 130516, upload-time = "2025-12-06T15:54:06.274Z" }, + { url = "https://files.pythonhosted.org/packages/4d/63/43d4dc9bd9954bff7052f700fdb501067f6fb134a003ddcea2a0bb3854ed/orjson-3.11.5-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aa5e4244063db8e1d87e0f54c3f7522f14b2dc937e65d5241ef0076a096409fd", size = 135695, upload-time = "2025-12-06T15:54:07.702Z" }, + { url = "https://files.pythonhosted.org/packages/87/6f/27e2e76d110919cb7fcb72b26166ee676480a701bcf8fc53ac5d0edce32f/orjson-3.11.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1db2088b490761976c1b2e956d5d4e6409f3732e9d79cfa69f876c5248d1baf9", size = 139664, upload-time = "2025-12-06T15:54:08.828Z" }, + { url = "https://files.pythonhosted.org/packages/d4/f8/5966153a5f1be49b5fbb8ca619a529fde7bc71aa0a376f2bb83fed248bcd/orjson-3.11.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c2ed66358f32c24e10ceea518e16eb3549e34f33a9d51f99ce23b0251776a1ef", size = 137289, upload-time = "2025-12-06T15:54:09.898Z" }, + { url = "https://files.pythonhosted.org/packages/a7/34/8acb12ff0299385c8bbcbb19fbe40030f23f15a6de57a9c587ebf71483fb/orjson-3.11.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c2021afda46c1ed64d74b555065dbd4c2558d510d8cec5ea6a53001b3e5e82a9", size = 138784, upload-time = "2025-12-06T15:54:11.022Z" }, + { url = "https://files.pythonhosted.org/packages/ee/27/910421ea6e34a527f73d8f4ee7bdffa48357ff79c7b8d6eb6f7b82dd1176/orjson-3.11.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b42ffbed9128e547a1647a3e50bc88ab28ae9daa61713962e0d3dd35e820c125", size = 141322, upload-time = "2025-12-06T15:54:12.427Z" }, + { url = "https://files.pythonhosted.org/packages/87/a3/4b703edd1a05555d4bb1753d6ce44e1a05b7a6d7c164d5b332c795c63d70/orjson-3.11.5-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:8d5f16195bb671a5dd3d1dbea758918bada8f6cc27de72bd64adfbd748770814", size = 413612, upload-time = "2025-12-06T15:54:13.858Z" }, + { url = "https://files.pythonhosted.org/packages/1b/36/034177f11d7eeea16d3d2c42a1883b0373978e08bc9dad387f5074c786d8/orjson-3.11.5-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:c0e5d9f7a0227df2927d343a6e3859bebf9208b427c79bd31949abcc2fa32fa5", size = 150993, upload-time = "2025-12-06T15:54:15.189Z" }, + { url = "https://files.pythonhosted.org/packages/44/2f/ea8b24ee046a50a7d141c0227c4496b1180b215e728e3b640684f0ea448d/orjson-3.11.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:23d04c4543e78f724c4dfe656b3791b5f98e4c9253e13b2636f1af5d90e4a880", size = 141774, upload-time = "2025-12-06T15:54:16.451Z" }, + { url = "https://files.pythonhosted.org/packages/8a/12/cc440554bf8200eb23348a5744a575a342497b65261cd65ef3b28332510a/orjson-3.11.5-cp311-cp311-win32.whl", hash = "sha256:c404603df4865f8e0afe981aa3c4b62b406e6d06049564d58934860b62b7f91d", size = 135109, upload-time = "2025-12-06T15:54:17.73Z" }, + { url = "https://files.pythonhosted.org/packages/a3/83/e0c5aa06ba73a6760134b169f11fb970caa1525fa4461f94d76e692299d9/orjson-3.11.5-cp311-cp311-win_amd64.whl", hash = "sha256:9645ef655735a74da4990c24ffbd6894828fbfa117bc97c1edd98c282ecb52e1", size = 133193, upload-time = "2025-12-06T15:54:19.426Z" }, + { url = "https://files.pythonhosted.org/packages/cb/35/5b77eaebc60d735e832c5b1a20b155667645d123f09d471db0a78280fb49/orjson-3.11.5-cp311-cp311-win_arm64.whl", hash = "sha256:1cbf2735722623fcdee8e712cbaaab9e372bbcb0c7924ad711b261c2eccf4a5c", size = 126830, upload-time = "2025-12-06T15:54:20.836Z" }, + { url = "https://files.pythonhosted.org/packages/ef/a4/8052a029029b096a78955eadd68ab594ce2197e24ec50e6b6d2ab3f4e33b/orjson-3.11.5-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:334e5b4bff9ad101237c2d799d9fd45737752929753bf4faf4b207335a416b7d", size = 245347, upload-time = "2025-12-06T15:54:22.061Z" }, + { url = "https://files.pythonhosted.org/packages/64/67/574a7732bd9d9d79ac620c8790b4cfe0717a3d5a6eb2b539e6e8995e24a0/orjson-3.11.5-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:ff770589960a86eae279f5d8aa536196ebda8273a2a07db2a54e82b93bc86626", size = 129435, upload-time = "2025-12-06T15:54:23.615Z" }, + { url = "https://files.pythonhosted.org/packages/52/8d/544e77d7a29d90cf4d9eecd0ae801c688e7f3d1adfa2ebae5e1e94d38ab9/orjson-3.11.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed24250e55efbcb0b35bed7caaec8cedf858ab2f9f2201f17b8938c618c8ca6f", size = 132074, upload-time = "2025-12-06T15:54:24.694Z" }, + { url = "https://files.pythonhosted.org/packages/6e/57/b9f5b5b6fbff9c26f77e785baf56ae8460ef74acdb3eae4931c25b8f5ba9/orjson-3.11.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a66d7769e98a08a12a139049aac2f0ca3adae989817f8c43337455fbc7669b85", size = 130520, upload-time = "2025-12-06T15:54:26.185Z" }, + { url = "https://files.pythonhosted.org/packages/f6/6d/d34970bf9eb33f9ec7c979a262cad86076814859e54eb9a059a52f6dc13d/orjson-3.11.5-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:86cfc555bfd5794d24c6a1903e558b50644e5e68e6471d66502ce5cb5fdef3f9", size = 136209, upload-time = "2025-12-06T15:54:27.264Z" }, + { url = "https://files.pythonhosted.org/packages/e7/39/bc373b63cc0e117a105ea12e57280f83ae52fdee426890d57412432d63b3/orjson-3.11.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a230065027bc2a025e944f9d4714976a81e7ecfa940923283bca7bbc1f10f626", size = 139837, upload-time = "2025-12-06T15:54:28.75Z" }, + { url = "https://files.pythonhosted.org/packages/cb/aa/7c4818c8d7d324da220f4f1af55c343956003aa4d1ce1857bdc1d396ba69/orjson-3.11.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b29d36b60e606df01959c4b982729c8845c69d1963f88686608be9ced96dbfaa", size = 137307, upload-time = "2025-12-06T15:54:29.856Z" }, + { url = "https://files.pythonhosted.org/packages/46/bf/0993b5a056759ba65145effe3a79dd5a939d4a070eaa5da2ee3180fbb13f/orjson-3.11.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c74099c6b230d4261fdc3169d50efc09abf38ace1a42ea2f9994b1d79153d477", size = 139020, upload-time = "2025-12-06T15:54:31.024Z" }, + { url = "https://files.pythonhosted.org/packages/65/e8/83a6c95db3039e504eda60fc388f9faedbb4f6472f5aba7084e06552d9aa/orjson-3.11.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e697d06ad57dd0c7a737771d470eedc18e68dfdefcdd3b7de7f33dfda5b6212e", size = 141099, upload-time = "2025-12-06T15:54:32.196Z" }, + { url = "https://files.pythonhosted.org/packages/b9/b4/24fdc024abfce31c2f6812973b0a693688037ece5dc64b7a60c1ce69e2f2/orjson-3.11.5-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:e08ca8a6c851e95aaecc32bc44a5aa75d0ad26af8cdac7c77e4ed93acf3d5b69", size = 413540, upload-time = "2025-12-06T15:54:33.361Z" }, + { url = "https://files.pythonhosted.org/packages/d9/37/01c0ec95d55ed0c11e4cae3e10427e479bba40c77312b63e1f9665e0737d/orjson-3.11.5-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e8b5f96c05fce7d0218df3fdfeb962d6b8cfff7e3e20264306b46dd8b217c0f3", size = 151530, upload-time = "2025-12-06T15:54:34.6Z" }, + { url = "https://files.pythonhosted.org/packages/f9/d4/f9ebc57182705bb4bbe63f5bbe14af43722a2533135e1d2fb7affa0c355d/orjson-3.11.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ddbfdb5099b3e6ba6d6ea818f61997bb66de14b411357d24c4612cf1ebad08ca", size = 141863, upload-time = "2025-12-06T15:54:35.801Z" }, + { url = "https://files.pythonhosted.org/packages/0d/04/02102b8d19fdcb009d72d622bb5781e8f3fae1646bf3e18c53d1bc8115b5/orjson-3.11.5-cp312-cp312-win32.whl", hash = "sha256:9172578c4eb09dbfcf1657d43198de59b6cef4054de385365060ed50c458ac98", size = 135255, upload-time = "2025-12-06T15:54:37.209Z" }, + { url = "https://files.pythonhosted.org/packages/d4/fb/f05646c43d5450492cb387de5549f6de90a71001682c17882d9f66476af5/orjson-3.11.5-cp312-cp312-win_amd64.whl", hash = "sha256:2b91126e7b470ff2e75746f6f6ee32b9ab67b7a93c8ba1d15d3a0caaf16ec875", size = 133252, upload-time = "2025-12-06T15:54:38.401Z" }, + { url = "https://files.pythonhosted.org/packages/dc/a6/7b8c0b26ba18c793533ac1cd145e131e46fcf43952aa94c109b5b913c1f0/orjson-3.11.5-cp312-cp312-win_arm64.whl", hash = "sha256:acbc5fac7e06777555b0722b8ad5f574739e99ffe99467ed63da98f97f9ca0fe", size = 126777, upload-time = "2025-12-06T15:54:39.515Z" }, + { url = "https://files.pythonhosted.org/packages/10/43/61a77040ce59f1569edf38f0b9faadc90c8cf7e9bec2e0df51d0132c6bb7/orjson-3.11.5-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:3b01799262081a4c47c035dd77c1301d40f568f77cc7ec1bb7db5d63b0a01629", size = 245271, upload-time = "2025-12-06T15:54:40.878Z" }, + { url = "https://files.pythonhosted.org/packages/55/f9/0f79be617388227866d50edd2fd320cb8fb94dc1501184bb1620981a0aba/orjson-3.11.5-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:61de247948108484779f57a9f406e4c84d636fa5a59e411e6352484985e8a7c3", size = 129422, upload-time = "2025-12-06T15:54:42.403Z" }, + { url = "https://files.pythonhosted.org/packages/77/42/f1bf1549b432d4a78bfa95735b79b5dac75b65b5bb815bba86ad406ead0a/orjson-3.11.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:894aea2e63d4f24a7f04a1908307c738d0dce992e9249e744b8f4e8dd9197f39", size = 132060, upload-time = "2025-12-06T15:54:43.531Z" }, + { url = "https://files.pythonhosted.org/packages/25/49/825aa6b929f1a6ed244c78acd7b22c1481fd7e5fda047dc8bf4c1a807eb6/orjson-3.11.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ddc21521598dbe369d83d4d40338e23d4101dad21dae0e79fa20465dbace019f", size = 130391, upload-time = "2025-12-06T15:54:45.059Z" }, + { url = "https://files.pythonhosted.org/packages/42/ec/de55391858b49e16e1aa8f0bbbb7e5997b7345d8e984a2dec3746d13065b/orjson-3.11.5-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7cce16ae2f5fb2c53c3eafdd1706cb7b6530a67cc1c17abe8ec747f5cd7c0c51", size = 135964, upload-time = "2025-12-06T15:54:46.576Z" }, + { url = "https://files.pythonhosted.org/packages/1c/40/820bc63121d2d28818556a2d0a09384a9f0262407cf9fa305e091a8048df/orjson-3.11.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e46c762d9f0e1cfb4ccc8515de7f349abbc95b59cb5a2bd68df5973fdef913f8", size = 139817, upload-time = "2025-12-06T15:54:48.084Z" }, + { url = "https://files.pythonhosted.org/packages/09/c7/3a445ca9a84a0d59d26365fd8898ff52bdfcdcb825bcc6519830371d2364/orjson-3.11.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d7345c759276b798ccd6d77a87136029e71e66a8bbf2d2755cbdde1d82e78706", size = 137336, upload-time = "2025-12-06T15:54:49.426Z" }, + { url = "https://files.pythonhosted.org/packages/9a/b3/dc0d3771f2e5d1f13368f56b339c6782f955c6a20b50465a91acb79fe961/orjson-3.11.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75bc2e59e6a2ac1dd28901d07115abdebc4563b5b07dd612bf64260a201b1c7f", size = 138993, upload-time = "2025-12-06T15:54:50.939Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a2/65267e959de6abe23444659b6e19c888f242bf7725ff927e2292776f6b89/orjson-3.11.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:54aae9b654554c3b4edd61896b978568c6daa16af96fa4681c9b5babd469f863", size = 141070, upload-time = "2025-12-06T15:54:52.414Z" }, + { url = "https://files.pythonhosted.org/packages/63/c9/da44a321b288727a322c6ab17e1754195708786a04f4f9d2220a5076a649/orjson-3.11.5-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:4bdd8d164a871c4ec773f9de0f6fe8769c2d6727879c37a9666ba4183b7f8228", size = 413505, upload-time = "2025-12-06T15:54:53.67Z" }, + { url = "https://files.pythonhosted.org/packages/7f/17/68dc14fa7000eefb3d4d6d7326a190c99bb65e319f02747ef3ebf2452f12/orjson-3.11.5-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:a261fef929bcf98a60713bf5e95ad067cea16ae345d9a35034e73c3990e927d2", size = 151342, upload-time = "2025-12-06T15:54:55.113Z" }, + { url = "https://files.pythonhosted.org/packages/c4/c5/ccee774b67225bed630a57478529fc026eda33d94fe4c0eac8fe58d4aa52/orjson-3.11.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c028a394c766693c5c9909dec76b24f37e6a1b91999e8d0c0d5feecbe93c3e05", size = 141823, upload-time = "2025-12-06T15:54:56.331Z" }, + { url = "https://files.pythonhosted.org/packages/67/80/5d00e4155d0cd7390ae2087130637671da713959bb558db9bac5e6f6b042/orjson-3.11.5-cp313-cp313-win32.whl", hash = "sha256:2cc79aaad1dfabe1bd2d50ee09814a1253164b3da4c00a78c458d82d04b3bdef", size = 135236, upload-time = "2025-12-06T15:54:57.507Z" }, + { url = "https://files.pythonhosted.org/packages/95/fe/792cc06a84808dbdc20ac6eab6811c53091b42f8e51ecebf14b540e9cfe4/orjson-3.11.5-cp313-cp313-win_amd64.whl", hash = "sha256:ff7877d376add4e16b274e35a3f58b7f37b362abf4aa31863dadacdd20e3a583", size = 133167, upload-time = "2025-12-06T15:54:58.71Z" }, + { url = "https://files.pythonhosted.org/packages/46/2c/d158bd8b50e3b1cfdcf406a7e463f6ffe3f0d167b99634717acdaf5e299f/orjson-3.11.5-cp313-cp313-win_arm64.whl", hash = "sha256:59ac72ea775c88b163ba8d21b0177628bd015c5dd060647bbab6e22da3aad287", size = 126712, upload-time = "2025-12-06T15:54:59.892Z" }, + { url = "https://files.pythonhosted.org/packages/c2/60/77d7b839e317ead7bb225d55bb50f7ea75f47afc489c81199befc5435b50/orjson-3.11.5-cp314-cp314-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:e446a8ea0a4c366ceafc7d97067bfd55292969143b57e3c846d87fc701e797a0", size = 245252, upload-time = "2025-12-06T15:55:01.127Z" }, + { url = "https://files.pythonhosted.org/packages/f1/aa/d4639163b400f8044cef0fb9aa51b0337be0da3a27187a20d1166e742370/orjson-3.11.5-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:53deb5addae9c22bbe3739298f5f2196afa881ea75944e7720681c7080909a81", size = 129419, upload-time = "2025-12-06T15:55:02.723Z" }, + { url = "https://files.pythonhosted.org/packages/30/94/9eabf94f2e11c671111139edf5ec410d2f21e6feee717804f7e8872d883f/orjson-3.11.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:82cd00d49d6063d2b8791da5d4f9d20539c5951f965e45ccf4e96d33505ce68f", size = 132050, upload-time = "2025-12-06T15:55:03.918Z" }, + { url = "https://files.pythonhosted.org/packages/3d/c8/ca10f5c5322f341ea9a9f1097e140be17a88f88d1cfdd29df522970d9744/orjson-3.11.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3fd15f9fc8c203aeceff4fda211157fad114dde66e92e24097b3647a08f4ee9e", size = 130370, upload-time = "2025-12-06T15:55:05.173Z" }, + { url = "https://files.pythonhosted.org/packages/25/d4/e96824476d361ee2edd5c6290ceb8d7edf88d81148a6ce172fc00278ca7f/orjson-3.11.5-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9df95000fbe6777bf9820ae82ab7578e8662051bb5f83d71a28992f539d2cda7", size = 136012, upload-time = "2025-12-06T15:55:06.402Z" }, + { url = "https://files.pythonhosted.org/packages/85/8e/9bc3423308c425c588903f2d103cfcfe2539e07a25d6522900645a6f257f/orjson-3.11.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:92a8d676748fca47ade5bc3da7430ed7767afe51b2f8100e3cd65e151c0eaceb", size = 139809, upload-time = "2025-12-06T15:55:07.656Z" }, + { url = "https://files.pythonhosted.org/packages/e9/3c/b404e94e0b02a232b957c54643ce68d0268dacb67ac33ffdee24008c8b27/orjson-3.11.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:aa0f513be38b40234c77975e68805506cad5d57b3dfd8fe3baa7f4f4051e15b4", size = 137332, upload-time = "2025-12-06T15:55:08.961Z" }, + { url = "https://files.pythonhosted.org/packages/51/30/cc2d69d5ce0ad9b84811cdf4a0cd5362ac27205a921da524ff42f26d65e0/orjson-3.11.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa1863e75b92891f553b7922ce4ee10ed06db061e104f2b7815de80cdcb135ad", size = 138983, upload-time = "2025-12-06T15:55:10.595Z" }, + { url = "https://files.pythonhosted.org/packages/0e/87/de3223944a3e297d4707d2fe3b1ffb71437550e165eaf0ca8bbe43ccbcb1/orjson-3.11.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d4be86b58e9ea262617b8ca6251a2f0d63cc132a6da4b5fcc8e0a4128782c829", size = 141069, upload-time = "2025-12-06T15:55:11.832Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/81d5087ae74be33bcae3ff2d80f5ccaa4a8fedc6d39bf65a427a95b8977f/orjson-3.11.5-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:b923c1c13fa02084eb38c9c065afd860a5cff58026813319a06949c3af5732ac", size = 413491, upload-time = "2025-12-06T15:55:13.314Z" }, + { url = "https://files.pythonhosted.org/packages/d0/6f/f6058c21e2fc1efaf918986dbc2da5cd38044f1a2d4b7b91ad17c4acf786/orjson-3.11.5-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:1b6bd351202b2cd987f35a13b5e16471cf4d952b42a73c391cc537974c43ef6d", size = 151375, upload-time = "2025-12-06T15:55:14.715Z" }, + { url = "https://files.pythonhosted.org/packages/54/92/c6921f17d45e110892899a7a563a925b2273d929959ce2ad89e2525b885b/orjson-3.11.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:bb150d529637d541e6af06bbe3d02f5498d628b7f98267ff87647584293ab439", size = 141850, upload-time = "2025-12-06T15:55:15.94Z" }, + { url = "https://files.pythonhosted.org/packages/88/86/cdecb0140a05e1a477b81f24739da93b25070ee01ce7f7242f44a6437594/orjson-3.11.5-cp314-cp314-win32.whl", hash = "sha256:9cc1e55c884921434a84a0c3dd2699eb9f92e7b441d7f53f3941079ec6ce7499", size = 135278, upload-time = "2025-12-06T15:55:17.202Z" }, + { url = "https://files.pythonhosted.org/packages/e4/97/b638d69b1e947d24f6109216997e38922d54dcdcdb1b11c18d7efd2d3c59/orjson-3.11.5-cp314-cp314-win_amd64.whl", hash = "sha256:a4f3cb2d874e03bc7767c8f88adaa1a9a05cecea3712649c3b58589ec7317310", size = 133170, upload-time = "2025-12-06T15:55:18.468Z" }, + { url = "https://files.pythonhosted.org/packages/8f/dd/f4fff4a6fe601b4f8f3ba3aa6da8ac33d17d124491a3b804c662a70e1636/orjson-3.11.5-cp314-cp314-win_arm64.whl", hash = "sha256:38b22f476c351f9a1c43e5b07d8b5a02eb24a6ab8e75f700f7d479d4568346a5", size = 126713, upload-time = "2025-12-06T15:55:19.738Z" }, + { url = "https://files.pythonhosted.org/packages/50/c7/7b682849dd4c9fb701a981669b964ea700516ecbd8e88f62aae07c6852bd/orjson-3.11.5-cp39-cp39-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:1b280e2d2d284a6713b0cfec7b08918ebe57df23e3f76b27586197afca3cb1e9", size = 245298, upload-time = "2025-12-06T15:55:20.984Z" }, + { url = "https://files.pythonhosted.org/packages/1b/3f/194355a9335707a15fdc79ddc670148987b43d04712dd26898a694539ce6/orjson-3.11.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c8d8a112b274fae8c5f0f01954cb0480137072c271f3f4958127b010dfefaec", size = 132150, upload-time = "2025-12-06T15:55:22.364Z" }, + { url = "https://files.pythonhosted.org/packages/e9/08/d74b3a986d37e6c2e04b8821c62927620c9a1924bb49ea51519a87751b86/orjson-3.11.5-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f0a2ae6f09ac7bd47d2d5a5305c1d9ed08ac057cda55bb0a49fa506f0d2da00", size = 130490, upload-time = "2025-12-06T15:55:23.619Z" }, + { url = "https://files.pythonhosted.org/packages/b2/16/ebd04c38c1db01e493a68eee442efdffc505a43112eccd481e0146c6acc2/orjson-3.11.5-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c0d87bd1896faac0d10b4f849016db81a63e4ec5df38757ffae84d45ab38aa71", size = 135726, upload-time = "2025-12-06T15:55:24.912Z" }, + { url = "https://files.pythonhosted.org/packages/06/64/2ce4b2c09a099403081c37639c224bdcdfe401138bd66fed5c96d4f8dbd3/orjson-3.11.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:801a821e8e6099b8c459ac7540b3c32dba6013437c57fdcaec205b169754f38c", size = 139640, upload-time = "2025-12-06T15:55:26.535Z" }, + { url = "https://files.pythonhosted.org/packages/cd/e2/425796df8ee1d7cea3a7edf868920121dd09162859dbb76fffc9a5c37fd3/orjson-3.11.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:69a0f6ac618c98c74b7fbc8c0172ba86f9e01dbf9f62aa0b1776c2231a7bffe5", size = 137289, upload-time = "2025-12-06T15:55:27.78Z" }, + { url = "https://files.pythonhosted.org/packages/32/a2/88e482eb8e899a037dcc9eff85ef117a568e6ca1ffa1a2b2be3fcb51b7bb/orjson-3.11.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fea7339bdd22e6f1060c55ac31b6a755d86a5b2ad3657f2669ec243f8e3b2bdb", size = 138761, upload-time = "2025-12-06T15:55:29.388Z" }, + { url = "https://files.pythonhosted.org/packages/f1/fd/131dd6d32eeb74c513bfa487f434a2150811d0fbd9cb06689284f2f21b34/orjson-3.11.5-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:4dad582bc93cef8f26513e12771e76385a7e6187fd713157e971c784112aad56", size = 141357, upload-time = "2025-12-06T15:55:31.064Z" }, + { url = "https://files.pythonhosted.org/packages/7a/90/e4a0abbcca7b53e9098ac854f27f5ed9949c796f3c760bc04af997da0eb2/orjson-3.11.5-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:0522003e9f7fba91982e83a97fec0708f5a714c96c4209db7104e6b9d132f111", size = 413638, upload-time = "2025-12-06T15:55:32.344Z" }, + { url = "https://files.pythonhosted.org/packages/d1/c2/df91e385514924120001ade9cd52d6295251023d3bfa2c0a01f38cfc485a/orjson-3.11.5-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:7403851e430a478440ecc1258bcbacbfbd8175f9ac1e39031a7121dd0de05ff8", size = 150972, upload-time = "2025-12-06T15:55:33.725Z" }, + { url = "https://files.pythonhosted.org/packages/a6/ff/c76cc5a30a4451191ff1b868a331ad1354433335277fc40931f5fc3cab9d/orjson-3.11.5-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:5f691263425d3177977c8d1dd896cde7b98d93cbf390b2544a090675e83a6a0a", size = 141729, upload-time = "2025-12-06T15:55:35.317Z" }, + { url = "https://files.pythonhosted.org/packages/27/c3/7830bf74389ea1eaab2b017d8b15d1cab2bb0737d9412dfa7fb8644f7d78/orjson-3.11.5-cp39-cp39-win32.whl", hash = "sha256:61026196a1c4b968e1b1e540563e277843082e9e97d78afa03eb89315af531f1", size = 135100, upload-time = "2025-12-06T15:55:36.57Z" }, + { url = "https://files.pythonhosted.org/packages/69/e6/babf31154e047e465bc194eb72d1326d7c52ad4d7f50bf92b02b3cacda5c/orjson-3.11.5-cp39-cp39-win_amd64.whl", hash = "sha256:09b94b947ac08586af635ef922d69dc9bc63321527a3a04647f4986a73f4bd30", size = 133189, upload-time = "2025-12-06T15:55:38.143Z" }, +] + +[[package]] +name = "orjson" +version = "3.11.8" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'linux'", + "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and sys_platform == 'linux'", + "python_full_version == '3.10.*' and sys_platform == 'linux'", + "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.14' and sys_platform == 'emscripten'", + "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32'", + "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", + "python_full_version == '3.11.*' and sys_platform == 'emscripten'", + "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32'", + "python_full_version == '3.10.*' and sys_platform != 'linux'", +] +sdist = { url = "https://files.pythonhosted.org/packages/9d/1b/2024d06792d0779f9dbc51531b61c24f76c75b9f4ce05e6f3377a1814cea/orjson-3.11.8.tar.gz", hash = "sha256:96163d9cdc5a202703e9ad1b9ae757d5f0ca62f4fa0cc93d1f27b0e180cc404e", size = 5603832, upload-time = "2026-03-31T16:16:27.878Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2f/90/5d81f61fe3e4270da80c71442864c091cee3003cc8984c75f413fe742a07/orjson-3.11.8-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:e6693ff90018600c72fd18d3d22fa438be26076cd3c823da5f63f7bab28c11cb", size = 229663, upload-time = "2026-03-31T16:14:30.708Z" }, + { url = "https://files.pythonhosted.org/packages/6c/ef/85e06b0eb11de6fb424120fd5788a07035bd4c5e6bb7841ae9972a0526d1/orjson-3.11.8-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93de06bc920854552493c81f1f729fab7213b7db4b8195355db5fda02c7d1363", size = 132321, upload-time = "2026-03-31T16:14:32.317Z" }, + { url = "https://files.pythonhosted.org/packages/86/71/089338ee51b3132f050db0864a7df9bdd5e94c2a03820ab8a91e8f655618/orjson-3.11.8-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fe0b8c83e0f36247fc9431ce5425a5d95f9b3a689133d494831bdbd6f0bceb13", size = 130658, upload-time = "2026-03-31T16:14:33.935Z" }, + { url = "https://files.pythonhosted.org/packages/10/0d/f39d8802345d0ad65f7fd4374b29b9b59f98656dc30f21ca5c773265b2f0/orjson-3.11.8-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:97d823831105c01f6c8029faf297633dbeb30271892bd430e9c24ceae3734744", size = 135708, upload-time = "2026-03-31T16:14:35.224Z" }, + { url = "https://files.pythonhosted.org/packages/ff/b5/40aae576b3473511696dcffea84fde638b2b64774eb4dcb8b2c262729f8a/orjson-3.11.8-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c60c0423f15abb6cf78f56dff00168a1b582f7a1c23f114036e2bfc697814d5f", size = 147047, upload-time = "2026-03-31T16:14:36.489Z" }, + { url = "https://files.pythonhosted.org/packages/7b/f0/778a84458d1fdaa634b2e572e51ce0b354232f580b2327e1f00a8d88c38c/orjson-3.11.8-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:01928d0476b216ad2201823b0a74000440360cef4fed1912d297b8d84718f277", size = 133072, upload-time = "2026-03-31T16:14:37.715Z" }, + { url = "https://files.pythonhosted.org/packages/bf/d3/1bbf2fc3ffcc4b829ade554b574af68cec898c9b5ad6420a923c75a073d3/orjson-3.11.8-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6a4a639049c44d36a6d1ae0f4a94b271605c745aee5647fa8ffaabcdc01b69a6", size = 133867, upload-time = "2026-03-31T16:14:39.356Z" }, + { url = "https://files.pythonhosted.org/packages/08/94/6413da22edc99a69a8d0c2e83bf42973b8aa94d83ef52a6d39ac85da00bc/orjson-3.11.8-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3222adff1e1ff0dce93c16146b93063a7793de6c43d52309ae321234cdaf0f4d", size = 142268, upload-time = "2026-03-31T16:14:40.972Z" }, + { url = "https://files.pythonhosted.org/packages/4a/5f/aa5dbaa6136d7ba55f5461ac2e885efc6e6349424a428927fd46d68f4396/orjson-3.11.8-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:3223665349bbfb68da234acd9846955b1a0808cbe5520ff634bf253a4407009b", size = 424008, upload-time = "2026-03-31T16:14:42.637Z" }, + { url = "https://files.pythonhosted.org/packages/fa/aa/2c1962d108c7fe5e27aa03a354b378caf56d8eafdef15fd83dec081ce45a/orjson-3.11.8-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:61c9d357a59465736022d5d9ba06687afb7611dfb581a9d2129b77a6fcf78e59", size = 147942, upload-time = "2026-03-31T16:14:44.256Z" }, + { url = "https://files.pythonhosted.org/packages/47/d1/65f404f4c47eb1b0b4476f03ec838cac0c4aa933920ff81e5dda4dee14e7/orjson-3.11.8-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:58fb9b17b4472c7b1dcf1a54583629e62e23779b2331052f09a9249edf81675b", size = 136640, upload-time = "2026-03-31T16:14:45.884Z" }, + { url = "https://files.pythonhosted.org/packages/90/5f/7b784aea98bdb125a2f2da7c27d6c2d2f6d943d96ef0278bae596d563f85/orjson-3.11.8-cp310-cp310-win32.whl", hash = "sha256:b43dc2a391981d36c42fa57747a49dae793ef1d2e43898b197925b5534abd10a", size = 132066, upload-time = "2026-03-31T16:14:47.397Z" }, + { url = "https://files.pythonhosted.org/packages/92/ec/2e284af8d6c9478df5ef938917743f61d68f4c70d17f1b6e82f7e3b8dba1/orjson-3.11.8-cp310-cp310-win_amd64.whl", hash = "sha256:c98121237fea2f679480765abd566f7713185897f35c9e6c2add7e3a9900eb61", size = 127609, upload-time = "2026-03-31T16:14:48.78Z" }, + { url = "https://files.pythonhosted.org/packages/67/41/5aa7fa3b0f4dc6b47dcafc3cea909299c37e40e9972feabc8b6a74e2730d/orjson-3.11.8-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:003646067cc48b7fcab2ae0c562491c9b5d2cbd43f1e5f16d98fd118c5522d34", size = 229229, upload-time = "2026-03-31T16:14:50.424Z" }, + { url = "https://files.pythonhosted.org/packages/0a/d7/57e7f2458e0a2c41694f39fc830030a13053a84f837a5b73423dca1f0938/orjson-3.11.8-cp311-cp311-macosx_15_0_arm64.whl", hash = "sha256:ed193ce51d77a3830cad399a529cd4ef029968761f43ddc549e1bc62b40d88f8", size = 128871, upload-time = "2026-03-31T16:14:51.888Z" }, + { url = "https://files.pythonhosted.org/packages/53/4a/e0fdb9430983e6c46e0299559275025075568aad5d21dd606faee3703924/orjson-3.11.8-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f30491bc4f862aa15744b9738517454f1e46e56c972a2be87d70d727d5b2a8f8", size = 132104, upload-time = "2026-03-31T16:14:53.142Z" }, + { url = "https://files.pythonhosted.org/packages/08/4a/2025a60ff3f5c8522060cda46612d9b1efa653de66ed2908591d8d82f22d/orjson-3.11.8-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6eda5b8b6be91d3f26efb7dc6e5e68ee805bc5617f65a328587b35255f138bf4", size = 130483, upload-time = "2026-03-31T16:14:54.605Z" }, + { url = "https://files.pythonhosted.org/packages/2d/3c/b9cde05bdc7b2385c66014e0620627da638d3d04e4954416ab48c31196c5/orjson-3.11.8-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ee8db7bfb6fe03581bbab54d7c4124a6dd6a7f4273a38f7267197890f094675f", size = 135481, upload-time = "2026-03-31T16:14:55.901Z" }, + { url = "https://files.pythonhosted.org/packages/ff/f2/a8238e7734de7cb589fed319857a8025d509c89dc52fdcc88f39c6d03d5a/orjson-3.11.8-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5d8b5231de76c528a46b57010bbd83fb51e056aa0220a372fd5065e978406f1c", size = 146819, upload-time = "2026-03-31T16:14:57.548Z" }, + { url = "https://files.pythonhosted.org/packages/db/10/dbf1e2a3cafea673b1b4350e371877b759060d6018a998643b7040e5de48/orjson-3.11.8-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:58a4a208a6fbfdb7a7327b8f201c6014f189f721fd55d047cafc4157af1bc62a", size = 132846, upload-time = "2026-03-31T16:14:58.91Z" }, + { url = "https://files.pythonhosted.org/packages/f8/fc/55e667ec9c85694038fcff00573d221b085d50777368ee3d77f38668bf3c/orjson-3.11.8-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5f8952d6d2505c003e8f0224ff7858d341fa4e33fef82b91c4ff0ef070f2393c", size = 133580, upload-time = "2026-03-31T16:15:00.519Z" }, + { url = "https://files.pythonhosted.org/packages/7e/a6/c08c589a9aad0cb46c4831d17de212a2b6901f9d976814321ff8e69e8785/orjson-3.11.8-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0022bb50f90da04b009ce32c512dc1885910daa7cb10b7b0cba4505b16db82a8", size = 142042, upload-time = "2026-03-31T16:15:01.906Z" }, + { url = "https://files.pythonhosted.org/packages/5c/cc/2f78ea241d52b717d2efc38878615fe80425bf2beb6e68c984dde257a766/orjson-3.11.8-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:ff51f9d657d1afb6f410cb435792ce4e1fe427aab23d2fcd727a2876e21d4cb6", size = 423845, upload-time = "2026-03-31T16:15:03.703Z" }, + { url = "https://files.pythonhosted.org/packages/70/07/c17dcf05dd8045457538428a983bf1f1127928df5bf328cb24d2b7cddacb/orjson-3.11.8-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:6dbe9a97bdb4d8d9d5367b52a7c32549bba70b2739c58ef74a6964a6d05ae054", size = 147729, upload-time = "2026-03-31T16:15:05.203Z" }, + { url = "https://files.pythonhosted.org/packages/90/6c/0fb6e8a24e682e0958d71711ae6f39110e4b9cd8cab1357e2a89cb8e1951/orjson-3.11.8-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a5c370674ebabe16c6ccac33ff80c62bf8a6e59439f5e9d40c1f5ab8fd2215b7", size = 136425, upload-time = "2026-03-31T16:15:07.052Z" }, + { url = "https://files.pythonhosted.org/packages/b2/35/4d3cc3a3d616035beb51b24a09bb872942dc452cf2df0c1d11ab35046d9f/orjson-3.11.8-cp311-cp311-win32.whl", hash = "sha256:0e32f7154299f42ae66f13488963269e5eccb8d588a65bc839ed986919fc9fac", size = 131870, upload-time = "2026-03-31T16:15:08.678Z" }, + { url = "https://files.pythonhosted.org/packages/13/26/9fe70f81d16b702f8c3a775e8731b50ad91d22dacd14c7599b60a0941cd1/orjson-3.11.8-cp311-cp311-win_amd64.whl", hash = "sha256:25e0c672a2e32348d2eb33057b41e754091f2835f87222e4675b796b92264f06", size = 127440, upload-time = "2026-03-31T16:15:09.994Z" }, + { url = "https://files.pythonhosted.org/packages/e8/c6/b038339f4145efd2859c1ca53097a52c0bb9cbdd24f947ebe146da1ad067/orjson-3.11.8-cp311-cp311-win_arm64.whl", hash = "sha256:9185589c1f2a944c17e26c9925dcdbc2df061cc4a145395c57f0c51f9b5dbfcd", size = 127399, upload-time = "2026-03-31T16:15:11.412Z" }, + { url = "https://files.pythonhosted.org/packages/01/f6/8d58b32ab32d9215973a1688aebd098252ee8af1766c0e4e36e7831f0295/orjson-3.11.8-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:1cd0b77e77c95758f8e1100139844e99f3ccc87e71e6fc8e1c027e55807c549f", size = 229233, upload-time = "2026-03-31T16:15:12.762Z" }, + { url = "https://files.pythonhosted.org/packages/a9/8b/2ffe35e71f6b92622e8ea4607bf33ecf7dfb51b3619dcfabfd36cbe2d0a5/orjson-3.11.8-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:6a3d159d5ffa0e3961f353c4b036540996bf8b9697ccc38261c0eac1fd3347a6", size = 128772, upload-time = "2026-03-31T16:15:14.237Z" }, + { url = "https://files.pythonhosted.org/packages/27/d2/1f8682ae50d5c6897a563cb96bc106da8c9cb5b7b6e81a52e4cc086679b9/orjson-3.11.8-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76070a76e9c5ae661e2d9848f216980d8d533e0f8143e6ed462807b242e3c5e8", size = 131946, upload-time = "2026-03-31T16:15:15.607Z" }, + { url = "https://files.pythonhosted.org/packages/52/4b/5500f76f0eece84226e0689cb48dcde081104c2fa6e2483d17ca13685ffb/orjson-3.11.8-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:54153d21520a71a4c82a0dbb4523e468941d549d221dc173de0f019678cf3813", size = 130368, upload-time = "2026-03-31T16:15:17.066Z" }, + { url = "https://files.pythonhosted.org/packages/da/4e/58b927e08fbe9840e6c920d9e299b051ea667463b1f39a56e668669f8508/orjson-3.11.8-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:469ac2125611b7c5741a0b3798cd9e5786cbad6345f9f400c77212be89563bec", size = 135540, upload-time = "2026-03-31T16:15:18.404Z" }, + { url = "https://files.pythonhosted.org/packages/56/7c/ba7cb871cba1bcd5cd02ee34f98d894c6cea96353ad87466e5aef2429c60/orjson-3.11.8-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:14778ffd0f6896aa613951a7fbf4690229aa7a543cb2bfbe9f358e08aafa9546", size = 146877, upload-time = "2026-03-31T16:15:19.833Z" }, + { url = "https://files.pythonhosted.org/packages/0b/5d/eb9c25fc1386696c6a342cd361c306452c75e0b55e86ad602dd4827a7fd7/orjson-3.11.8-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea56a955056a6d6c550cf18b3348656a9d9a4f02e2d0c02cabf3c73f1055d506", size = 132837, upload-time = "2026-03-31T16:15:21.282Z" }, + { url = "https://files.pythonhosted.org/packages/37/87/5ddeb7fc1fbd9004aeccab08426f34c81a5b4c25c7061281862b015fce2b/orjson-3.11.8-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:53a0f57e59a530d18a142f4d4ba6dfc708dc5fdedce45e98ff06b44930a2a48f", size = 133624, upload-time = "2026-03-31T16:15:22.641Z" }, + { url = "https://files.pythonhosted.org/packages/22/09/90048793db94ee4b2fcec4ac8e5ddb077367637d6650be896b3494b79bb7/orjson-3.11.8-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9b48e274f8824567d74e2158199e269597edf00823a1b12b63d48462bbf5123e", size = 141904, upload-time = "2026-03-31T16:15:24.435Z" }, + { url = "https://files.pythonhosted.org/packages/c0/cf/eb284847487821a5d415e54149a6449ba9bfc5872ce63ab7be41b8ec401c/orjson-3.11.8-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:3f262401086a3960586af06c054609365e98407151f5ea24a62893a40d80dbbb", size = 423742, upload-time = "2026-03-31T16:15:26.155Z" }, + { url = "https://files.pythonhosted.org/packages/44/09/e12423d327071c851c13e76936f144a96adacfc037394dec35ac3fc8d1e8/orjson-3.11.8-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:8e8c6218b614badf8e229b697865df4301afa74b791b6c9ade01d19a9953a942", size = 147806, upload-time = "2026-03-31T16:15:27.909Z" }, + { url = "https://files.pythonhosted.org/packages/b3/6d/37c2589ba864e582ffe7611643314785c6afb1f83c701654ef05daa8fcc7/orjson-3.11.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:093d489fa039ddade2db541097dbb484999fcc65fc2b0ff9819141e2ab364f25", size = 136485, upload-time = "2026-03-31T16:15:29.749Z" }, + { url = "https://files.pythonhosted.org/packages/be/c9/135194a02ab76b04ed9a10f68624b7ebd238bbe55548878b11ff15a0f352/orjson-3.11.8-cp312-cp312-win32.whl", hash = "sha256:e0950ed1bcb9893f4293fd5c5a7ee10934fbf82c4101c70be360db23ce24b7d2", size = 131966, upload-time = "2026-03-31T16:15:31.687Z" }, + { url = "https://files.pythonhosted.org/packages/ed/9a/9796f8fbe3cf30ce9cb696748dbb535e5c87be4bf4fe2e9ca498ef1fa8cf/orjson-3.11.8-cp312-cp312-win_amd64.whl", hash = "sha256:3cf17c141617b88ced4536b2135c552490f07799f6ad565948ea07bef0dcb9a6", size = 127441, upload-time = "2026-03-31T16:15:33.333Z" }, + { url = "https://files.pythonhosted.org/packages/cc/47/5aaf54524a7a4a0dd09dd778f3fa65dd2108290615b652e23d944152bc8e/orjson-3.11.8-cp312-cp312-win_arm64.whl", hash = "sha256:48854463b0572cc87dac7d981aa72ed8bf6deedc0511853dc76b8bbd5482d36d", size = 127364, upload-time = "2026-03-31T16:15:34.748Z" }, + { url = "https://files.pythonhosted.org/packages/66/7f/95fba509bb2305fab0073558f1e8c3a2ec4b2afe58ed9fcb7d3b8beafe94/orjson-3.11.8-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:3f23426851d98478c8970da5991f84784a76682213cd50eb73a1da56b95239dc", size = 229180, upload-time = "2026-03-31T16:15:36.426Z" }, + { url = "https://files.pythonhosted.org/packages/f6/9d/b237215c743ca073697d759b5503abd2cb8a0d7b9c9e21f524bcf176ab66/orjson-3.11.8-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:ebaed4cef74a045b83e23537b52ef19a367c7e3f536751e355a2a394f8648559", size = 128754, upload-time = "2026-03-31T16:15:38.049Z" }, + { url = "https://files.pythonhosted.org/packages/42/3d/27d65b6d11e63f133781425f132807aef793ed25075fec686fc8e46dd528/orjson-3.11.8-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:97c8f5d3b62380b70c36ffacb2a356b7c6becec86099b177f73851ba095ef623", size = 131877, upload-time = "2026-03-31T16:15:39.484Z" }, + { url = "https://files.pythonhosted.org/packages/dd/cc/faee30cd8f00421999e40ef0eba7332e3a625ce91a58200a2f52c7fef235/orjson-3.11.8-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:436c4922968a619fb7fef1ccd4b8b3a76c13b67d607073914d675026e911a65c", size = 130361, upload-time = "2026-03-31T16:15:41.274Z" }, + { url = "https://files.pythonhosted.org/packages/5c/bb/a6c55896197f97b6d4b4e7c7fd77e7235517c34f5d6ad5aadd43c54c6d7c/orjson-3.11.8-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1ab359aff0436d80bfe8a23b46b5fea69f1e18aaf1760a709b4787f1318b317f", size = 135521, upload-time = "2026-03-31T16:15:42.758Z" }, + { url = "https://files.pythonhosted.org/packages/9c/7c/ca3a3525aa32ff636ebb1778e77e3587b016ab2edb1b618b36ba96f8f2c0/orjson-3.11.8-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f89b6d0b3a8d81e1929d3ab3d92bbc225688bd80a770c49432543928fe09ac55", size = 146862, upload-time = "2026-03-31T16:15:44.341Z" }, + { url = "https://files.pythonhosted.org/packages/3c/0c/18a9d7f18b5edd37344d1fd5be17e94dc652c67826ab749c6e5948a78112/orjson-3.11.8-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:29c009e7a2ca9ad0ed1376ce20dd692146a5d9fe4310848904b6b4fee5c5c137", size = 132847, upload-time = "2026-03-31T16:15:46.368Z" }, + { url = "https://files.pythonhosted.org/packages/23/91/7e722f352ad67ca573cee44de2a58fb810d0f4eb4e33276c6a557979fd8a/orjson-3.11.8-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:705b895b781b3e395c067129d8551655642dfe9437273211d5404e87ac752b53", size = 133637, upload-time = "2026-03-31T16:15:48.123Z" }, + { url = "https://files.pythonhosted.org/packages/af/04/32845ce13ac5bd1046ddb02ac9432ba856cc35f6d74dde95864fe0ad5523/orjson-3.11.8-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:88006eda83858a9fdf73985ce3804e885c2befb2f506c9a3723cdeb5a2880e3e", size = 141906, upload-time = "2026-03-31T16:15:49.626Z" }, + { url = "https://files.pythonhosted.org/packages/02/5e/c551387ddf2d7106d9039369862245c85738b828844d13b99ccb8d61fd06/orjson-3.11.8-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:55120759e61309af7fcf9e961c6f6af3dde5921cdb3ee863ef63fd9db126cae6", size = 423722, upload-time = "2026-03-31T16:15:51.176Z" }, + { url = "https://files.pythonhosted.org/packages/00/a3/ecfe62434096f8a794d4976728cb59bcfc4a643977f21c2040545d37eb4c/orjson-3.11.8-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:98bdc6cb889d19bed01de46e67574a2eab61f5cc6b768ed50e8ac68e9d6ffab6", size = 147801, upload-time = "2026-03-31T16:15:52.939Z" }, + { url = "https://files.pythonhosted.org/packages/18/6d/0dce10b9f6643fdc59d99333871a38fa5a769d8e2fc34a18e5d2bfdee900/orjson-3.11.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:708c95f925a43ab9f34625e45dcdadf09ec8a6e7b664a938f2f8d5650f6c090b", size = 136460, upload-time = "2026-03-31T16:15:54.431Z" }, + { url = "https://files.pythonhosted.org/packages/01/d6/6dde4f31842d87099238f1f07b459d24edc1a774d20687187443ab044191/orjson-3.11.8-cp313-cp313-win32.whl", hash = "sha256:01c4e5a6695dc09098f2e6468a251bc4671c50922d4d745aff1a0a33a0cf5b8d", size = 131956, upload-time = "2026-03-31T16:15:56.081Z" }, + { url = "https://files.pythonhosted.org/packages/c1/f9/4e494a56e013db957fb77186b818b916d4695b8fa2aa612364974160e91b/orjson-3.11.8-cp313-cp313-win_amd64.whl", hash = "sha256:c154a35dd1330707450bb4d4e7dd1f17fa6f42267a40c1e8a1daa5e13719b4b8", size = 127410, upload-time = "2026-03-31T16:15:57.54Z" }, + { url = "https://files.pythonhosted.org/packages/57/7f/803203d00d6edb6e9e7eef421d4e1adbb5ea973e40b3533f3cfd9aeb374e/orjson-3.11.8-cp313-cp313-win_arm64.whl", hash = "sha256:4861bde57f4d253ab041e374f44023460e60e71efaa121f3c5f0ed457c3a701e", size = 127338, upload-time = "2026-03-31T16:15:59.106Z" }, + { url = "https://files.pythonhosted.org/packages/6d/35/b01910c3d6b85dc882442afe5060cbf719c7d1fc85749294beda23d17873/orjson-3.11.8-cp314-cp314-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:ec795530a73c269a55130498842aaa762e4a939f6ce481a7e986eeaa790e9da4", size = 229171, upload-time = "2026-03-31T16:16:00.651Z" }, + { url = "https://files.pythonhosted.org/packages/c2/56/c9ec97bd11240abef39b9e5d99a15462809c45f677420fd148a6c5e6295e/orjson-3.11.8-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:c492a0e011c0f9066e9ceaa896fbc5b068c54d365fea5f3444b697ee01bc8625", size = 128746, upload-time = "2026-03-31T16:16:02.673Z" }, + { url = "https://files.pythonhosted.org/packages/3b/e4/66d4f30a90de45e2f0cbd9623588e8ae71eef7679dbe2ae954ed6d66a41f/orjson-3.11.8-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:883206d55b1bd5f5679ad5e6ddd3d1a5e3cac5190482927fdb8c78fb699193b5", size = 131867, upload-time = "2026-03-31T16:16:04.342Z" }, + { url = "https://files.pythonhosted.org/packages/19/30/2a645fc9286b928675e43fa2a3a16fb7b6764aa78cc719dc82141e00f30b/orjson-3.11.8-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5774c1fdcc98b2259800b683b19599c133baeb11d60033e2095fd9d4667b82db", size = 124664, upload-time = "2026-03-31T16:16:05.837Z" }, + { url = "https://files.pythonhosted.org/packages/db/44/77b9a86d84a28d52ba3316d77737f6514e17118119ade3f91b639e859029/orjson-3.11.8-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8ac7381c83dd3d4a6347e6635950aa448f54e7b8406a27c7ecb4a37e9f1ae08b", size = 129701, upload-time = "2026-03-31T16:16:07.407Z" }, + { url = "https://files.pythonhosted.org/packages/b3/ea/eff3d9bfe47e9bc6969c9181c58d9f71237f923f9c86a2d2f490cd898c82/orjson-3.11.8-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:14439063aebcb92401c11afc68ee4e407258d2752e62d748b6942dad20d2a70d", size = 141202, upload-time = "2026-03-31T16:16:09.48Z" }, + { url = "https://files.pythonhosted.org/packages/52/c8/90d4b4c60c84d62068d0cf9e4d8f0a4e05e76971d133ac0c60d818d4db20/orjson-3.11.8-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fa72e71977bff96567b0f500fc5bfd2fdf915f34052c782a4c6ebbdaa97aa858", size = 127194, upload-time = "2026-03-31T16:16:11.02Z" }, + { url = "https://files.pythonhosted.org/packages/8d/c7/ea9e08d1f0ba981adffb629811148b44774d935171e7b3d780ae43c4c254/orjson-3.11.8-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7679bc2f01bb0d219758f1a5f87bb7c8a81c0a186824a393b366876b4948e14f", size = 133639, upload-time = "2026-03-31T16:16:13.434Z" }, + { url = "https://files.pythonhosted.org/packages/6c/8c/ddbbfd6ba59453c8fc7fe1d0e5983895864e264c37481b2a791db635f046/orjson-3.11.8-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:14f7b8fcb35ef403b42fa5ecfa4ed032332a91f3dc7368fbce4184d59e1eae0d", size = 141914, upload-time = "2026-03-31T16:16:14.955Z" }, + { url = "https://files.pythonhosted.org/packages/4e/31/dbfbefec9df060d34ef4962cd0afcb6fa7a9ec65884cb78f04a7859526c3/orjson-3.11.8-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:c2bdf7b2facc80b5e34f48a2d557727d5c5c57a8a450de122ae81fa26a81c1bc", size = 423800, upload-time = "2026-03-31T16:16:16.594Z" }, + { url = "https://files.pythonhosted.org/packages/87/cf/f74e9ae9803d4ab46b163494adba636c6d7ea955af5cc23b8aaa94cfd528/orjson-3.11.8-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ccd7ba1b0605813a0715171d39ec4c314cb97a9c85893c2c5c0c3a3729df38bf", size = 147837, upload-time = "2026-03-31T16:16:18.585Z" }, + { url = "https://files.pythonhosted.org/packages/64/e6/9214f017b5db85e84e68602792f742e5dc5249e963503d1b356bee611e01/orjson-3.11.8-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:cdbc8c9c02463fef4d3c53a9ba3336d05496ec8e1f1c53326a1e4acc11f5c600", size = 136441, upload-time = "2026-03-31T16:16:20.151Z" }, + { url = "https://files.pythonhosted.org/packages/24/dd/3590348818f58f837a75fb969b04cdf187ae197e14d60b5e5a794a38b79d/orjson-3.11.8-cp314-cp314-win32.whl", hash = "sha256:0b57f67710a8cd459e4e54eb96d5f77f3624eba0c661ba19a525807e42eccade", size = 131983, upload-time = "2026-03-31T16:16:21.823Z" }, + { url = "https://files.pythonhosted.org/packages/3f/0f/b6cb692116e05d058f31ceee819c70f097fa9167c82f67fabe7516289abc/orjson-3.11.8-cp314-cp314-win_amd64.whl", hash = "sha256:735e2262363dcbe05c35e3a8869898022af78f89dde9e256924dc02e99fe69ca", size = 127396, upload-time = "2026-03-31T16:16:23.685Z" }, + { url = "https://files.pythonhosted.org/packages/c0/d1/facb5b5051fabb0ef9d26c6544d87ef19a939a9a001198655d0d891062dd/orjson-3.11.8-cp314-cp314-win_arm64.whl", hash = "sha256:6ccdea2c213cf9f3d9490cbd5d427693c870753df41e6cb375bd79bcbafc8817", size = 127330, upload-time = "2026-03-31T16:16:25.496Z" }, +] + [[package]] name = "packaging" version = "26.0" @@ -4419,6 +4614,8 @@ dependencies = [ { name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "click", version = "8.3.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "nvidia-ml-py" }, + { name = "orjson", version = "3.11.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "orjson", version = "3.11.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "platformdirs", version = "4.4.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "platformdirs", version = "4.9.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "protobuf" }, @@ -4499,6 +4696,8 @@ requires-dist = [ { name = "moviepy", marker = "extra == 'media'" }, { name = "numpy", marker = "extra == 'media'" }, { name = "nvidia-ml-py" }, + { name = "orjson", marker = "python_full_version == '3.9.*'", specifier = "<=3.11.5" }, + { name = "orjson", marker = "python_full_version >= '3.10'" }, { name = "pillow", marker = "extra == 'media'" }, { name = "platformdirs", specifier = ">=4.2.0" }, { name = "protobuf", marker = "sys_platform != 'linux'", specifier = ">=3.19.0,!=4.21.0,!=5.28.0,<7" },