Skip to content

Commit 41a72f9

Browse files
committed
feat(sdk): support inference_v3 config_file mount
Bump platform-api-python-client to 4.10.0 so ConfigFileMount is importable, and add a thin helper in centml/sdk/utils/config_file.py that reads a file off disk and returns a populated ConfigFileMount. Example create_inference.py updated to show the helper in context. Server (platform PRs #3656 and #3667) owns all field-level validation (64 KiB content cap, filename charset, mount_path rules); the helper deliberately stays validation-free so the SDK does not drift when server limits change. Signed-off-by: Honglin Cao <hocao@nvidia.com>
1 parent 13c7886 commit 41a72f9

5 files changed

Lines changed: 328 additions & 1 deletion

File tree

centml/sdk/utils/config_file.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import os
2+
from typing import Optional
3+
4+
from platform_api_python_client import ConfigFileMount
5+
6+
7+
# Load a file off disk into a ConfigFileMount. Field-level validation
8+
# (size cap, filename charset, mount_path rules) is intentionally left
9+
# to the API so SDK doesn't drift when server limits change.
10+
def load_config_file_mount(path: str, mount_path: str, filename: Optional[str] = None) -> ConfigFileMount:
11+
with open(path, "r", encoding="utf-8") as f:
12+
content = f.read()
13+
return ConfigFileMount(filename=filename or os.path.basename(path), mount_path=mount_path, content=content)
Lines changed: 265 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,265 @@
1+
# SDK 支持 inference_v3 `config_file`
2+
3+
日期:2026-05-21
4+
仓库:`centml-python-client`
5+
新建分支:`honglin/sdk-config-file-support`
6+
7+
## 背景
8+
9+
平台侧两个 PR 为 `inference_v3` deployment 增加了可选的单文件挂载支持:
10+
11+
- [`#3656`](https://github.com/CentML/platform/pull/3656) —— schema、catalog、
12+
generated clients、UI 全套:`CreateInferenceV3DeploymentRequest.config_file`
13+
`GetInferenceV3DeploymentResponse.config_file` 携带一个可选的
14+
`ConfigFileMount{filename, mount_path, content}`
15+
- [`#3667`](https://github.com/CentML/platform/pull/3667) —— 收紧 server 端校验:
16+
`content` 字段 64 KiB UTF-8 字节上限、`mount_path` 拒绝 `..` 段、
17+
PATH_MAX 按 UTF-8 字节计。
18+
19+
`centml-python-client` SDK 当前在 `requirements.txt` pin 了
20+
`platform-api-python-client==4.9.0`,早于这两个 PR。
21+
PyPI 现在 latest 是 `platform-api-python-client==4.10.0`
22+
(2026-05-21 用 `pip index versions` 验证过),其中含
23+
`platform_api_python_client/models/config_file_mount.py`
24+
(通过解包 wheel 验证)。
25+
26+
由于 pin 版本里没有这个 model,SDK 用户今天没法传 `config_file`
27+
bump pin 之后,`config_file` 作为 `CreateInferenceV3DeploymentRequest`
28+
的一个透传字段就能用,SDK 代码无需任何变动 —— 但每次使用都需要用户自己
29+
从磁盘读文件构造 `ConfigFileMount`,这个动作是高频重复的。
30+
31+
## 目标
32+
33+
用最小表面积让 `config_file` 在 SDK 中成为一等公民:
34+
35+
1. Bump generated client pin,让 `ConfigFileMount` 可被 import。
36+
2. 加一个轻量 helper,从磁盘读文件并返回填好的 `ConfigFileMount`
37+
匹配仓内 `centml/sdk/utils/client_certs.py` 现有 pattern。
38+
3. 更新 canonical example,让用户看到 helper 的实际用法。
39+
40+
非目标:
41+
42+
- 不加 CLI 命令(2026-05-21 用户确认 —— CLI 延后)。
43+
- 不在 client 侧重复 server 校验(size cap、path 规则)。
44+
API 是 source of truth,server 限制可能变。
45+
- 不对 generated `ConfigFileMount` 做 wrapper / subclass
46+
(仓内没有 subclass generated model 的先例)。
47+
- 不在 `centml.sdk` 顶层 re-export:与
48+
`centml/sdk/utils/client_certs.py` 一致,走 qualified path 导入。
49+
50+
## 沿用的仓内既有约定
51+
52+
- `centml/sdk/utils/client_certs.py` —— free function(动词命名,如
53+
`generate_ca_client_triplet``save_pem_file`),不对 generated model
54+
做 class wrapping,函数上方 `#` 块注释解释 *why*
55+
- `examples/sdk/create_inference.py` —— 单 `main()`,文件顶部
56+
`import centml` + `from centml.sdk.api import get_centml_client` +
57+
`from centml.sdk import ...`,文件末尾用三引号注释 pause/delete 示例。
58+
- `requirements.txt` —— 严格 `==` pinning。
59+
- `scripts/format.sh` —— Black,`--skip-string-normalization`
60+
`--skip-magic-trailing-comma``--line-length 120`
61+
- `tests/` —— 文件名 `test_<area>.py`,pytest,conftest 带 `--sanity` 选项;
62+
无 E2E 网络调用。
63+
64+
## 设计
65+
66+
### 1. 依赖 bump
67+
68+
`requirements.txt`
69+
70+
```diff
71+
-platform-api-python-client==4.9.0
72+
+platform-api-python-client==4.10.0
73+
```
74+
75+
理由:4.10.0 是 PyPI 当前 latest,同时包含 feature PR #3656 和校验收紧 PR
76+
#3667。已通过下载 wheel 验证。
77+
78+
无其他依赖变动。`setup.py``requirements.txt` 读,无需修改。
79+
80+
### 2. Helper
81+
82+
新文件 `centml/sdk/utils/config_file.py`
83+
84+
```python
85+
import os
86+
from typing import Optional
87+
88+
from platform_api_python_client import ConfigFileMount
89+
90+
91+
# Load a file off disk into a ConfigFileMount. Field-level validation
92+
# (size cap, filename charset, mount_path rules) is intentionally left
93+
# to the API so SDK doesn't drift when server limits change.
94+
def load_config_file_mount(path: str, mount_path: str, filename: Optional[str] = None) -> ConfigFileMount:
95+
with open(path, "r", encoding="utf-8") as f:
96+
content = f.read()
97+
return ConfigFileMount(
98+
filename=filename or os.path.basename(path),
99+
mount_path=mount_path,
100+
content=content,
101+
)
102+
```
103+
104+
设计决定,附理由:
105+
106+
- **Free function,不是 subclass。** 匹配 `client_certs.py`;仓内没有
107+
subclass generated client model 的先例。
108+
- **直接返回 generated `ConfigFileMount`** Type signature 直接 drop in
109+
`CreateInferenceV3DeploymentRequest(config_file=...)`,无需转换。
110+
- **`filename` 可选,默认 `os.path.basename(path)`** 常见情形就是
111+
`nginx.conf` 挂载为 `nginx.conf`;需要 rename 时显式传。
112+
- **`mount_path` 无默认值。** 容器内挂载位置正是这个字段的存在意义 ——
113+
没有合理默认。
114+
- **不做 client-side 校验。** 2026-05-21 用户确认:API 是 source of
115+
truth,server 侧限制(如 64 KiB content 上限)可能演化。Server 在
116+
超限时返回 422 并带具体字节数(见 #3667)。
117+
- **UTF-8 文本读取。** Server 契约是 `content: str`。二进制 config
118+
不在当前 platform schema 范围内。
119+
- **不改 `centml/sdk/utils/__init__.py`** 该文件当前为空;
120+
消费者用 qualified path 导入
121+
`from centml.sdk.utils.config_file import load_config_file_mount`
122+
`client_certs.py` 的消费方式一致。
123+
124+
函数命名 `load_config_file_mount`。"Load" 是"从磁盘读+构造"的精确动词;
125+
`build_*` 因为项目规则偏好精确动词、不推荐 `build` 而被淘汰。
126+
127+
### 3. Example 更新
128+
129+
`examples/sdk/create_inference.py` —— 增加 helper import,并在原有 request
130+
上加 `config_file=...` 字段。其他字段全部保留。最终形态:
131+
132+
```python
133+
import centml
134+
from centml.sdk.api import get_centml_client
135+
from centml.sdk import DeploymentType, CreateInferenceV3DeploymentRequest, UserVaultType
136+
from centml.sdk.utils.config_file import load_config_file_mount
137+
138+
139+
def main():
140+
with get_centml_client() as cclient:
141+
certs = cclient.get_user_vault(UserVaultType.CERTIFICATES)
142+
143+
request = CreateInferenceV3DeploymentRequest(
144+
name="nginx",
145+
cluster_id=1000,
146+
hardware_instance_id=1000,
147+
image_url="nginxinc/nginx-unprivileged",
148+
port=8080,
149+
min_replicas=1,
150+
max_replicas=3,
151+
initial_replicas=1,
152+
endpoint_certificate_authority=certs["my_cert"],
153+
max_surge=1,
154+
max_unavailable=0,
155+
healthcheck="/",
156+
concurrency=10,
157+
# Helper reads ./nginx.conf and wraps it; pass an inline
158+
# ConfigFileMount(filename=..., mount_path=..., content=...) if
159+
# the content is already in memory.
160+
config_file=load_config_file_mount(
161+
path="./nginx.conf",
162+
mount_path="/etc/nginx/conf.d/default.conf",
163+
),
164+
)
165+
response = cclient.create_inference(request)
166+
print("Create deployment response: ", response)
167+
168+
deployment = cclient.get_inference(response.id)
169+
print("Deployment details: ", deployment)
170+
171+
'''
172+
### Pause the deployment
173+
cclient.pause(deployment.id)
174+
175+
### Delete the deployment
176+
cclient.delete(deployment.id)
177+
'''
178+
179+
180+
if __name__ == "__main__":
181+
main()
182+
```
183+
184+
末尾三引号 pause/delete 块原样保留。
185+
186+
### 4. 测试
187+
188+
新文件 `tests/test_sdk_config_file_helper.py`。测试先于 helper 实现写
189+
(按项目 Prime Directive #2 的 TDD 要求)。无网络、无 platform mock ——
190+
只用 `tmp_path` fixture + pytest。
191+
192+
测试用例(每个一个 `def test_*`):
193+
194+
1. **`test_default_filename_from_basename`**:写 `tmp_path/nginx.conf`
195+
`load_config_file_mount(path, "/etc/nginx/conf.d/default.conf")`
196+
断言 `mount.filename == "nginx.conf"``mount.content == <文件内容>`
197+
`mount.mount_path == "/etc/nginx/conf.d/default.conf"`
198+
199+
2. **`test_explicit_filename_overrides_basename`**:写 `tmp_path/local.txt`
200+
`filename="remote.conf"`,断言 `mount.filename == "remote.conf"`
201+
content / mount_path 保持。
202+
203+
3. **`test_utf8_multibyte_content_roundtrips`**:写一个含 CJK 字符的文件,
204+
断言 `mount.content` 与原始字符串相等(catch 错误的 binary-mode 读)。
205+
206+
4. **`test_missing_file_raises_filenotfound`**:传一个不存在的 path,
207+
断言 `FileNotFoundError`。明确 helper ** 吞掉 I/O 错误。
208+
209+
测试不断言 server 侧限制(size cap、mount_path 规则)—— 那些是 server
210+
契约,不是 helper 的职责(见设计 / "不做 client-side 校验")。
211+
212+
### 5. 范围之外
213+
214+
- 不加 inference create/update 的 CLI 命令(按用户 2026-05-21 决定延后)。
215+
- 不动 `README.md`,除非已有 SDK 用法 doc 需要更新。
216+
- 不动 `centml/sdk/api.py` —— `create_inference` / `update_inference`
217+
已经吃 `CreateInferenceV3DeploymentRequest`,pass-through 就够用。
218+
- 不加 in-memory helper(如 `load_config_file_mount_from_bytes`);generated
219+
`ConfigFileMount(...)` 构造器对这种 case 已经够简单。
220+
221+
## 文件级变更汇总
222+
223+
| 文件 | 变更 |
224+
| --- | --- |
225+
| `requirements.txt` | bump `platform-api-python-client` `4.9.0``4.10.0` |
226+
| `centml/sdk/utils/config_file.py` | 新文件(约 15 行含注释 + import) |
227+
| `examples/sdk/create_inference.py` | 加 import + `config_file=load_config_file_mount(...)` |
228+
| `tests/test_sdk_config_file_helper.py` | 新文件,4 个 test(TDD:先写) |
229+
230+
不动任何其他文件。
231+
232+
## 验证计划
233+
234+
declare done 之前:
235+
236+
1. `pip install -r requirements.txt` 在新 pin 下成功。
237+
2. `python -c "from centml.sdk.utils.config_file import load_config_file_mount; print(load_config_file_mount.__doc__ or 'ok')"` 没有 ImportError。
238+
3. `cd tests && pytest --sanity` 通过;新加的 4 个 test 在 run output
239+
中可见。
240+
4. `./scripts/format.sh --check``./scripts/lint.sh` 干净。
241+
5. `./scripts/typecheck.sh` 干净(`Optional[str]` 已是仓内风格)。
242+
6. Manual smoke(可选,dev cluster 在手时):在 dev 跑修改后的
243+
`examples/sdk/create_inference.py`,带一个小的本地 `nginx.conf` 文件,
244+
确认 `cclient.get_inference(id).config_file` 与发出的内容一致。
245+
246+
## Commit 计划
247+
248+
单个 commit,conventional commits 风格,subject ≤72 字符:
249+
250+
```
251+
feat(sdk): support inference_v3 config_file mount
252+
```
253+
254+
Body:简要引用平台 PR #3656/#3667 和三处改动(bump、helper、example)。
255+
`Signed-off-by: Honglin Cao <hocao@nvidia.com>`
256+
`git add -A` / `git add .` —— 四个文件单独 `git add`
257+
258+
## 引用
259+
260+
- 平台 PR [#3656](https://github.com/CentML/platform/pull/3656) —— feature
261+
- 平台 PR [#3667](https://github.com/CentML/platform/pull/3667) —— 校验收紧
262+
- 仓内风格先例:`centml/sdk/utils/client_certs.py`
263+
- Example 先例:`examples/sdk/create_inference.py`
264+
- PyPI 验证:`platform-api-python-client==4.10.0` wheel 含
265+
`models/config_file_mount.py`(2026-05-21 验证)

examples/sdk/create_inference.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import centml
22
from centml.sdk.api import get_centml_client
33
from centml.sdk import DeploymentType, CreateInferenceV3DeploymentRequest, UserVaultType
4+
from centml.sdk.utils.config_file import load_config_file_mount
45

56

67
def main():
@@ -22,6 +23,13 @@ def main():
2223
max_unavailable=0, # Keep all pods available during updates
2324
healthcheck="/",
2425
concurrency=10,
26+
# Helper reads ./nginx.conf and wraps it; pass an inline
27+
# ConfigFileMount(filename=..., mount_path=..., content=...) if
28+
# the content is already in memory.
29+
config_file=load_config_file_mount(
30+
path="./nginx.conf",
31+
mount_path="/etc/nginx/conf.d/default.conf",
32+
),
2533
)
2634
response = cclient.create_inference(request)
2735
print("Create deployment response: ", response)

requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,4 @@ pyjwt>=2.8.0
55
cryptography==46.0.7
66
websockets>=16.0
77
pyte>=0.8.0
8-
platform-api-python-client==4.9.0
8+
platform-api-python-client==4.10.0
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
"""Tests for centml.sdk.utils.config_file.load_config_file_mount."""
2+
3+
import pytest
4+
5+
from centml.sdk.utils.config_file import load_config_file_mount
6+
7+
8+
def test_default_filename_from_basename(tmp_path):
9+
src = tmp_path / "nginx.conf"
10+
src.write_text("server { listen 80; }\n")
11+
12+
mount = load_config_file_mount(str(src), "/etc/nginx/conf.d/default.conf")
13+
14+
assert mount.filename == "nginx.conf"
15+
assert mount.mount_path == "/etc/nginx/conf.d/default.conf"
16+
assert mount.content == "server { listen 80; }\n"
17+
18+
19+
def test_explicit_filename_overrides_basename(tmp_path):
20+
src = tmp_path / "local.txt"
21+
src.write_text("payload")
22+
23+
mount = load_config_file_mount(str(src), "/app/etc/remote.conf", filename="remote.conf")
24+
25+
assert mount.filename == "remote.conf"
26+
assert mount.mount_path == "/app/etc/remote.conf"
27+
assert mount.content == "payload"
28+
29+
30+
def test_utf8_multibyte_content_roundtrips(tmp_path):
31+
src = tmp_path / "i18n.conf"
32+
src.write_text("配置内容 = 测试\n", encoding="utf-8")
33+
34+
mount = load_config_file_mount(str(src), "/etc/app/i18n.conf")
35+
36+
assert mount.content == "配置内容 = 测试\n"
37+
38+
39+
def test_missing_file_raises_filenotfound(tmp_path):
40+
with pytest.raises(FileNotFoundError):
41+
load_config_file_mount(str(tmp_path / "does-not-exist.conf"), "/etc/x")

0 commit comments

Comments
 (0)